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:
2026-08-12 23:00:04 +02:00
parent e47c57b50d
commit 6f0300f038
2 changed files with 33 additions and 3 deletions

View File

@@ -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();