Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
29 changes: 29 additions & 0 deletions packages/openai/src/index.ts
Original file line number Diff line number Diff line change
@@ -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;
}
}
47 changes: 47 additions & 0 deletions packages/openai/src/langfuseIds.ts
Original file line number Diff line number Diff line change
@@ -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> = 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<T>(
result: T,
generation: LangfuseGeneration,
): asserts result is T & WithLangfuseIds<T> {
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.
}
}
}
Comment on lines +23 to +47

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Object.defineProperty may throw on non-extensible or sealed objects

Object.defineProperty throws a TypeError if the target object is non-extensible (e.g. Object.preventExtensions() / Object.seal() / Object.freeze()) or if either property already exists as non-configurable. Because configurable: false is set, any accidental double-call on the same object will also throw. Neither case is caught here, so the error would propagate up through the .then() chain and reject the user's promise.

Consider wrapping the property definitions in a try/catch to make the attachment best-effort:

export function attachLangfuseIds<T>(
  result: T,
  generation: LangfuseGeneration,
): asserts result is T & WithLangfuseIds<T> {
  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 {
      // Silently ignore if properties cannot be defined (e.g. frozen object)
    }
  }
}

Copy link
Copy Markdown
Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good catch — added a try/catch in 8d02412. In practice this can't happen since OpenAI SDK responses are plain objects from JSON parsing (never frozen/sealed) and the function is only called once per response, but it's worth guarding defensively.

8 changes: 7 additions & 1 deletion packages/openai/src/traceMethod.ts
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,7 @@ import {
} from "@langfuse/tracing";
import type OpenAI from "openai";

import { attachLangfuseIds } from "./langfuseIds.js";
import {
getToolCallOutput,
parseChunk,
Expand Down Expand Up @@ -129,6 +130,8 @@ const wrapMethod = <T extends GenericMethod>(
})
.end();

attachLangfuseIds(result, generation);

return result;
})
.catch((err) => {
Expand Down Expand Up @@ -258,5 +261,8 @@ function wrapAsyncIterable<R>(
.end();
}

return tracedOutputGenerator() as R;
const generator = tracedOutputGenerator();
attachLangfuseIds(generator, generation);

return generator as R;
}