Skip to content
Draft
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
5 changes: 5 additions & 0 deletions .changeset/langchain-js-token-total-metrics.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,5 @@
---
"braintrust": patch
---

fix: Emit canonical LangChain JS total token metrics
Original file line number Diff line number Diff line change
Expand Up @@ -283,6 +283,7 @@
"prompt_cached_tokens": "<number>",
"prompt_tokens": "<number>",
"start": 0,
"tokens": "<number>",
"total_tokens": "<number>"
},
"output": {
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@
"completion_tokens",
"prompt_cached_tokens",
"prompt_tokens",
"tokens",
"total_tokens"
]
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -116,6 +116,7 @@ span_tree:
│ "completion_tokens",
│ "prompt_cached_tokens",
│ "prompt_tokens",
│ "tokens",
│ "total_tokens"
│ ]
└── sayBye [task]
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -163,6 +163,7 @@
"completion_tokens": 2,
"prompt_cached_tokens": 0,
"prompt_tokens": 12,
"tokens": 14,
"total_tokens": 14
}
}
Expand Down Expand Up @@ -433,6 +434,7 @@
"completion_tokens": 2,
"prompt_cached_tokens": 0,
"prompt_tokens": 18,
"tokens": 20,
"total_tokens": 20
}
}
Expand Down Expand Up @@ -761,6 +763,7 @@
"prompt_cached_tokens": 0,
"prompt_tokens": 22,
"time_to_first_token": 0,
"tokens": 28,
"total_tokens": 28
}
}
Expand Down Expand Up @@ -996,6 +999,7 @@
"completion_tokens": 16,
"prompt_cached_tokens": 0,
"prompt_tokens": 72,
"tokens": 88,
"total_tokens": 88
}
}
Expand Down Expand Up @@ -1259,6 +1263,7 @@
"completion_tokens": 21,
"prompt_cached_tokens": 0,
"prompt_tokens": 76,
"tokens": 97,
"total_tokens": 97
Comment on lines +1266 to 1267

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

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

What's the actual diff between tokens and total_tokens?

}
},
Expand Down Expand Up @@ -1533,6 +1538,7 @@
"completion_tokens": 11,
"prompt_cached_tokens": 0,
"prompt_tokens": 106,
"tokens": 117,
"total_tokens": 117
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -162,6 +162,7 @@ span_tree:
│ "completion_tokens": 2,
│ "prompt_cached_tokens": 0,
│ "prompt_tokens": 12,
│ "tokens": 14,
│ "total_tokens": 14
│ }
├── langchain-chain-operation
Expand Down Expand Up @@ -564,6 +565,7 @@ span_tree:
│ "completion_tokens": 2,
│ "prompt_cached_tokens": 0,
│ "prompt_tokens": 18,
│ "tokens": 20,
│ "total_tokens": 20
│ }
├── langchain-stream-operation
Expand Down Expand Up @@ -735,6 +737,7 @@ span_tree:
│ "prompt_cached_tokens": 0,
│ "prompt_tokens": 22,
│ "time_to_first_token": 0,
│ "tokens": 28,
│ "total_tokens": 28
│ }
├── langchain-tool-operation
Expand Down Expand Up @@ -962,6 +965,7 @@ span_tree:
│ "completion_tokens": 16,
│ "prompt_cached_tokens": 0,
│ "prompt_tokens": 72,
│ "tokens": 88,
│ "total_tokens": 88
│ }
└── langchain-tool-result-operation
Expand Down Expand Up @@ -1217,6 +1221,7 @@ span_tree:
│ "completion_tokens": 21,
│ "prompt_cached_tokens": 0,
│ "prompt_tokens": 76,
│ "tokens": 97,
│ "total_tokens": 97
│ }
└── ChatOpenAI [llm]
Expand Down Expand Up @@ -1487,5 +1492,6 @@ span_tree:
"completion_tokens": 11,
"prompt_cached_tokens": 0,
"prompt_tokens": 106,
"tokens": 117,
"total_tokens": 117
}
132 changes: 132 additions & 0 deletions js/src/wrappers/langchain/callback-handler.test.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,132 @@
import { describe, expect, it, vi } from "vitest";
import { BraintrustLangChainCallbackHandler } from "./callback-handler";

function createHarness() {
const logs: unknown[] = [];
const span = {
log: vi.fn((event: unknown) => {
logs.push(event);
}),
end: vi.fn(),
};
const parent = {
startSpan: vi.fn(() => span),
};
const handler = new BraintrustLangChainCallbackHandler({
parent: parent as never,
});

return { handler, logs, parent, span };
}

async function finishChatModelRun(output: unknown) {
const harness = createHarness();

await harness.handler.handleChatModelStart(
{ name: "ChatOpenAI" },
[[{ role: "user", content: "hello" }]],
"run-1",
);
await harness.handler.handleLLMEnd(output as never, "run-1");

const endLog = harness.logs.at(-1) as {
metrics?: Record<string, number>;
};
return { ...harness, endLog };
}

describe("BraintrustLangChainCallbackHandler metrics", () => {
it("synthesizes tokens from message usage metadata prompt and completion counts", async () => {
const { endLog } = await finishChatModelRun({
generations: [
[
{
message: {
usage_metadata: {
input_tokens: 10,
output_tokens: 2,
},
},
},
],
],
});

expect(endLog.metrics).toEqual({
prompt_tokens: 10,
completion_tokens: 2,
tokens: 12,
});
});

it("prefers explicit total tokens from message usage metadata", async () => {
const { endLog } = await finishChatModelRun({
generations: [
[
{
message: {
usage_metadata: {
input_tokens: 10,
output_tokens: 2,
total_tokens: 99,
},
},
},
],
],
});

expect(endLog.metrics).toEqual({
total_tokens: 99,
prompt_tokens: 10,
completion_tokens: 2,
tokens: 99,
});
});

it("synthesizes tokens from llmOutput token usage prompt and completion counts", async () => {
const { endLog } = await finishChatModelRun({
llmOutput: {
tokenUsage: {
promptTokens: 10,
completionTokens: 2,
},
},
});

expect(endLog.metrics).toEqual({
prompt_tokens: 10,
completion_tokens: 2,
tokens: 12,
});
});

it("preserves cache metrics from message usage metadata", async () => {
const { endLog } = await finishChatModelRun({
generations: [
[
{
message: {
usage_metadata: {
input_tokens: 10,
output_tokens: 2,
input_token_details: {
cache_creation: 4,
cache_read: 3,
},
},
},
},
],
],
});

expect(endLog.metrics).toEqual({
prompt_tokens: 10,
completion_tokens: 2,
prompt_cache_creation_tokens: 4,
prompt_cached_tokens: 3,
tokens: 12,
});
});
});
21 changes: 19 additions & 2 deletions js/src/wrappers/langchain/callback-handler.ts
Original file line number Diff line number Diff line change
Expand Up @@ -438,6 +438,23 @@ function cleanObject(obj: Record<string, unknown>): Record<string, number> {
) as Record<string, number>;
}

function normalizeTokenMetrics(
obj: Record<string, unknown>,
): Record<string, number> {
const metrics = cleanObject(obj);

if (metrics.total_tokens !== undefined) {
metrics.tokens = metrics.total_tokens;
} else if (
metrics.prompt_tokens !== undefined &&
metrics.completion_tokens !== undefined
) {
metrics.tokens = metrics.prompt_tokens + metrics.completion_tokens;
}

return metrics;
}

function walkGenerations(
response: LangChainLLMResult,
): Record<string, unknown>[] {
Expand Down Expand Up @@ -497,7 +514,7 @@ function getMetricsFromResponse(
}

const inputTokenDetails = usageMetadata.input_token_details;
return cleanObject({
return normalizeTokenMetrics({
total_tokens: usageMetadata.total_tokens,
prompt_tokens: usageMetadata.input_tokens,
completion_tokens: usageMetadata.output_tokens,
Expand All @@ -517,7 +534,7 @@ function getMetricsFromResponse(
? llmOutput.estimatedTokens
: {};

return cleanObject({
return normalizeTokenMetrics({
total_tokens: tokenUsage.totalTokens,
prompt_tokens: tokenUsage.promptTokens,
completion_tokens: tokenUsage.completionTokens,
Expand Down
Loading