Track cached prompt tokens through to Langfuse

OpenAI's chat completions response includes prompt_tokens_details.
cached_tokens when prompt caching kicks in - previously dropped
entirely. Now extracted in LlmClient and forwarded to Langfuse as the
flat cache_read_input_tokens usage key.

Used the flat key rather than OpenAI's nested prompt_tokens_details
shape: the installed @langfuse/tracing SDK's usageDetails type is
Record<string, number> | OpenAiUsage (a 3-field camelCase shape, no
_details nesting) even though the wire schema in @langfuse/core
supports nesting - a gap in the SDK's public types, not something
worth fighting with a cast.

Verified: full suite (82/82) passing, tsc/eslint clean.
This commit is contained in:
2026-08-12 23:15:42 +02:00
parent 6f0300f038
commit df4ca0bf76
5 changed files with 65 additions and 2 deletions

View File

@@ -78,6 +78,27 @@ describe('LangfuseTracer', () => {
expect(generationEnd).toHaveBeenCalledTimes(1);
});
it('maps cachedTokens to the flat cache_read_input_tokens 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'] });
gen.end({
output: 'done',
usage: { promptTokens: 100, completionTokens: 20, totalTokens: 120, cachedTokens: 80 },
});
expect(generationUpdate).toHaveBeenCalledWith(
expect.objectContaining({
usageDetails: {
prompt_tokens: 100,
completion_tokens: 20,
total_tokens: 120,
cache_read_input_tokens: 80,
},
})
);
});
it('omits usageDetails entirely when no usage is reported', () => {
const tracer = new LangfuseTracer({ publicKey: 'pk', secretKey: 'sk', baseUrl: 'http://lf' });
const handle = tracer.startTrace('my-pipeline');