diff --git a/packages/openai/src/index.ts b/packages/openai/src/index.ts index 5b69e093c..42304f5c8 100644 --- a/packages/openai/src/index.ts +++ b/packages/openai/src/index.ts @@ -1,2 +1,31 @@ export { observeOpenAI } from "./observeOpenAI.js"; export * from "./types.js"; +export type { WithLangfuseIds } from "./langfuseIds.js"; + +/** + * Module augmentation for OpenAI SDK response types. + * + * When `observeOpenAI` wraps a client, all response objects are augmented + * with Langfuse observation and trace IDs at runtime. These declarations + * make TypeScript aware of those properties so consumers don't need casts. + * + * The properties are marked optional because the base OpenAI types are also + * used without `observeOpenAI`. + */ +declare module "openai/resources/chat/completions/completions" { + interface ChatCompletion { + /** Langfuse observation ID — present when the client is wrapped with `observeOpenAI` */ + langfuseObservationId?: string; + /** Langfuse trace ID — present when the client is wrapped with `observeOpenAI` */ + langfuseTraceId?: string; + } +} + +declare module "openai/resources/responses/responses" { + interface Response { + /** Langfuse observation ID — present when the client is wrapped with `observeOpenAI` */ + langfuseObservationId?: string; + /** Langfuse trace ID — present when the client is wrapped with `observeOpenAI` */ + langfuseTraceId?: string; + } +} diff --git a/packages/openai/src/langfuseIds.ts b/packages/openai/src/langfuseIds.ts new file mode 100644 index 000000000..60ca022fd --- /dev/null +++ b/packages/openai/src/langfuseIds.ts @@ -0,0 +1,47 @@ +import type { LangfuseGeneration } from "@langfuse/tracing"; + +/** + * Extends a response object with Langfuse observation and trace IDs. + * + * @public + */ +export type WithLangfuseIds = T & { + /** The Langfuse observation ID for this generation */ + langfuseObservationId: string; + /** The Langfuse trace ID for this generation */ + langfuseTraceId: string; +}; + +/** + * Attaches Langfuse observation and trace IDs to a response object as non-enumerable properties. + * + * The properties are non-enumerable so they don't appear in JSON.stringify output + * or for-in loops, but are directly accessible on the response object. + * + * @internal + */ +export function attachLangfuseIds( + result: T, + generation: LangfuseGeneration, +): asserts result is T & WithLangfuseIds { + if (result && typeof result === "object") { + try { + Object.defineProperty(result, "langfuseObservationId", { + value: generation.id, + enumerable: false, + writable: false, + configurable: false, + }); + Object.defineProperty(result, "langfuseTraceId", { + value: generation.traceId, + enumerable: false, + writable: false, + configurable: false, + }); + } catch { + // In practice this never happens: OpenAI SDK responses are plain objects + // from JSON parsing (never frozen/sealed), and this function is only called + // once per response. Guard kept for defensive robustness. + } + } +} diff --git a/packages/openai/src/traceMethod.ts b/packages/openai/src/traceMethod.ts index bafe35ca1..49e4ea7fb 100644 --- a/packages/openai/src/traceMethod.ts +++ b/packages/openai/src/traceMethod.ts @@ -5,6 +5,7 @@ import { } from "@langfuse/tracing"; import type OpenAI from "openai"; +import { attachLangfuseIds } from "./langfuseIds.js"; import { getToolCallOutput, parseChunk, @@ -129,6 +130,8 @@ const wrapMethod = ( }) .end(); + attachLangfuseIds(result, generation); + return result; }) .catch((err) => { @@ -258,5 +261,8 @@ function wrapAsyncIterable( .end(); } - return tracedOutputGenerator() as R; + const generator = tracedOutputGenerator(); + attachLangfuseIds(generator, generation); + + return generator as R; }