mirror of
https://github.com/FJurmanovic/sugarr.git
synced 2026-11-10 23:31:08 +00:00
Fix Langfuse usage tracking: map to snake_case keys it recognizes
LangfuseTracer was passing our internal camelCase usage fields (promptTokens/completionTokens/totalTokens) straight through as usageDetails. Langfuse only recognizes a flat schema (input/output/...) or OpenAI's native snake_case schema (prompt_tokens/completion_tokens/ total_tokens) for usage/cost calculation - anything else is silently ignored, so every generation was showing zero usage regardless of what LlmClient actually reported. Note: cost still requires a price-list entry for the model name. Langfuse's built-in list only covers hosted providers (OpenAI, Anthropic, Google); self-hosted models (e.g. LLM_MODEL=llama3 via Ollama) need a custom model price added in Project Settings > Models for a $ figure to show up, even with token counts now correct. Verified: full suite (80/80) passing, tsc/eslint clean.
This commit is contained in:
@@ -11,6 +11,21 @@ import type {
|
||||
TraceHandle,
|
||||
} from '../llm/Tracer';
|
||||
|
||||
/**
|
||||
* 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.
|
||||
*/
|
||||
function toUsageDetails(usage: GenerationOutput['usage']): Record<string, number> | undefined {
|
||||
if (!usage) return undefined;
|
||||
const details: Record<string, number> = {};
|
||||
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;
|
||||
return Object.keys(details).length > 0 ? details : undefined;
|
||||
}
|
||||
|
||||
export class LangfuseTracer extends Tracer {
|
||||
private readonly processor: LangfuseSpanProcessor;
|
||||
private readonly sdk: NodeSDK;
|
||||
@@ -40,7 +55,7 @@ export class LangfuseTracer extends Tracer {
|
||||
end(output: GenerationOutput) {
|
||||
generation.update({
|
||||
output: output.output,
|
||||
usageDetails: output.usage as Record<string, number> | undefined,
|
||||
usageDetails: toUsageDetails(output.usage),
|
||||
metadata: output.metadata,
|
||||
});
|
||||
generation.end();
|
||||
|
||||
@@ -59,7 +59,10 @@ describe('LangfuseTracer', () => {
|
||||
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: 'llama3', input: ['hi'] });
|
||||
gen.end({ output: 'done', usage: { totalTokens: 5 } });
|
||||
gen.end({
|
||||
output: 'done',
|
||||
usage: { promptTokens: 10, completionTokens: 5, totalTokens: 15 },
|
||||
});
|
||||
|
||||
expect(childStartObservation).toHaveBeenCalledWith(
|
||||
'step-1',
|
||||
@@ -67,11 +70,23 @@ describe('LangfuseTracer', () => {
|
||||
{ asType: 'generation' }
|
||||
);
|
||||
expect(generationUpdate).toHaveBeenCalledWith(
|
||||
expect.objectContaining({ output: 'done', usageDetails: { totalTokens: 5 } })
|
||||
expect.objectContaining({
|
||||
output: 'done',
|
||||
usageDetails: { prompt_tokens: 10, completion_tokens: 5, total_tokens: 15 },
|
||||
})
|
||||
);
|
||||
expect(generationEnd).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
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');
|
||||
const gen = handle.startGeneration({ name: 'step-1', model: 'llama3', input: ['hi'] });
|
||||
gen.end({ output: 'done' });
|
||||
|
||||
expect(generationUpdate).toHaveBeenCalledWith(expect.objectContaining({ usageDetails: undefined }));
|
||||
});
|
||||
|
||||
it('flush() force-flushes the span processor without shutting down the SDK', async () => {
|
||||
const tracer = new LangfuseTracer({ publicKey: 'pk', secretKey: 'sk', baseUrl: 'http://lf' });
|
||||
await tracer.flush();
|
||||
|
||||
Reference in New Issue
Block a user