diff --git a/src/core/langfuse/LangfuseTracer.ts b/src/core/langfuse/LangfuseTracer.ts index 5f8510f..2bb2d47 100644 --- a/src/core/langfuse/LangfuseTracer.ts +++ b/src/core/langfuse/LangfuseTracer.ts @@ -11,27 +11,35 @@ import type { TraceHandle, } from '../llm/Tracer'; +interface OpenAiUsageDetails { + prompt_tokens?: number; + completion_tokens?: number; + total_tokens?: number; + prompt_tokens_details?: { cached_tokens: number }; +} + /** - * Langfuse only recognizes usage in a flat schema (input/output/...) or - * OpenAI's native snake_case schema (prompt_tokens/completion_tokens/...); - * our own ChatResult.usage uses camelCase for our internal contract, so it - * needs translating at this boundary or Langfuse silently shows zero usage. + * Langfuse recognizes usage in either a flat generic schema (input/output/ + * cache_read_input_tokens/...) or OpenAI's native nested schema + * (prompt_tokens/completion_tokens/prompt_tokens_details.cached_tokens/...) + * - but NOT a mix of the two ("do not mix formats" per Langfuse's docs). + * Mixing them previously caused the whole generation.update() payload + * (including output) to land wrong, so this stays entirely within the + * OpenAI-native schema. * - * Cached tokens use the flat schema's cache_read_input_tokens key rather - * than OpenAI's nested prompt_tokens_details.cached_tokens: the installed - * @langfuse/tracing SDK's usageDetails type is `Record | - * OpenAiUsage` (a 3-field camelCase shape with no _details nesting) even - * though the wire schema (@langfuse/core's OpenAiCompletionUsageSchema) - * supports the nested form — keeping everything as flat numbers avoids - * fighting that gap in the SDK's public types. + * The installed @langfuse/tracing SDK's usageDetails type is + * `Record | OpenAiUsage` (a 3-field camelCase shape with + * no _details nesting) even though the wire schema (@langfuse/core's + * OpenAiCompletionUsageSchema) supports this nested form - a gap in the + * SDK's public types, not a real runtime restriction, hence the cast. */ -function toUsageDetails(usage: GenerationOutput['usage']): Record | undefined { +function toUsageDetails(usage: GenerationOutput['usage']): OpenAiUsageDetails | undefined { if (!usage) return undefined; - const details: Record = {}; + const details: OpenAiUsageDetails = {}; if (usage.promptTokens !== undefined) details.prompt_tokens = usage.promptTokens; if (usage.completionTokens !== undefined) details.completion_tokens = usage.completionTokens; if (usage.totalTokens !== undefined) details.total_tokens = usage.totalTokens; - if (usage.cachedTokens !== undefined) details.cache_read_input_tokens = usage.cachedTokens; + if (usage.cachedTokens !== undefined) details.prompt_tokens_details = { cached_tokens: usage.cachedTokens }; return Object.keys(details).length > 0 ? details : undefined; } @@ -64,7 +72,7 @@ export class LangfuseTracer extends Tracer { end(output: GenerationOutput) { generation.update({ output: output.output, - usageDetails: toUsageDetails(output.usage), + usageDetails: toUsageDetails(output.usage) as Record | undefined, metadata: output.metadata, }); generation.end(); diff --git a/test/core/langfuse/LangfuseTracer.test.ts b/test/core/langfuse/LangfuseTracer.test.ts index 78024f6..c1f8784 100644 --- a/test/core/langfuse/LangfuseTracer.test.ts +++ b/test/core/langfuse/LangfuseTracer.test.ts @@ -78,7 +78,7 @@ describe('LangfuseTracer', () => { expect(generationEnd).toHaveBeenCalledTimes(1); }); - it('maps cachedTokens to the flat cache_read_input_tokens key', () => { + it('maps cachedTokens to the OpenAI-native nested prompt_tokens_details.cached_tokens, not a mixed flat key', () => { const tracer = new LangfuseTracer({ publicKey: 'pk', secretKey: 'sk', baseUrl: 'http://lf' }); const handle = tracer.startTrace('my-pipeline'); const gen = handle.startGeneration({ name: 'step-1', model: 'gpt-4o-mini', input: ['hi'] }); @@ -93,7 +93,7 @@ describe('LangfuseTracer', () => { prompt_tokens: 100, completion_tokens: 20, total_tokens: 120, - cache_read_input_tokens: 80, + prompt_tokens_details: { cached_tokens: 80 }, }, }) );