Skip to content

Commit f5e94f4

Browse files
committed
handle token consumption
1 parent a606d26 commit f5e94f4

File tree

3 files changed

+47
-14
lines changed
  • dev-packages/node-integration-tests/suites/tracing/langgraph
  • packages/core/src/tracing/langgraph

3 files changed

+47
-14
lines changed

dev-packages/node-integration-tests/suites/tracing/langgraph/test.ts

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -168,9 +168,9 @@ describe('LangGraph integration', () => {
168168
'gen_ai.response.text': expect.stringMatching(/"role":"tool"/),
169169
// Verify tool_calls are captured
170170
'gen_ai.response.tool_calls': expect.stringContaining('get_weather'),
171-
'gen_ai.usage.input_tokens': 50,
172-
'gen_ai.usage.output_tokens': 20,
173-
'gen_ai.usage.total_tokens': 70,
171+
'gen_ai.usage.input_tokens': 80,
172+
'gen_ai.usage.output_tokens': 40,
173+
'gen_ai.usage.total_tokens': 120,
174174
}),
175175
description: 'invoke_agent tool_calling_agent',
176176
op: 'gen_ai.invoke_agent',

packages/core/src/tracing/langgraph/index.ts

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -122,7 +122,8 @@ function instrumentCompiledGraphInvoke(
122122
// Parse input messages
123123
const recordInputs = options.recordInputs;
124124
const recordOutputs = options.recordOutputs;
125-
const inputMessages = args.length > 0 ? (args[0] as { messages?: LangChainMessage[] }).messages : [];
125+
const inputMessages =
126+
args.length > 0 ? ((args[0] as { messages?: LangChainMessage[] }).messages ?? []) : [];
126127

127128
if (inputMessages && recordInputs) {
128129
const normalizedMessages = normalizeLangChainMessages(inputMessages);

packages/core/src/tracing/langgraph/utils.ts

Lines changed: 42 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -35,24 +35,32 @@ export function extractToolCalls(messages: Array<Record<string, unknown>> | null
3535
}
3636

3737
/**
38-
* Extract token usage from a message's usage_metadata
38+
* Extract token usage from a message's usage_metadata or response_metadata
39+
* Returns token counts without setting span attributes
3940
*/
40-
export function extractTokenUsageFromMetadata(span: Span, message: LangChainMessage): void {
41+
export function extractTokenUsageFromMessage(message: LangChainMessage): {
42+
inputTokens: number;
43+
outputTokens: number;
44+
totalTokens: number;
45+
} {
4146
const msg = message as Record<string, unknown>;
47+
let inputTokens = 0;
48+
let outputTokens = 0;
49+
let totalTokens = 0;
4250

4351
// Extract from usage_metadata (newer format)
4452
if (msg.usage_metadata && typeof msg.usage_metadata === 'object') {
4553
const usage = msg.usage_metadata as Record<string, unknown>;
4654
if (typeof usage.input_tokens === 'number') {
47-
span.setAttribute(GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, usage.input_tokens);
55+
inputTokens = usage.input_tokens;
4856
}
4957
if (typeof usage.output_tokens === 'number') {
50-
span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, usage.output_tokens);
58+
outputTokens = usage.output_tokens;
5159
}
5260
if (typeof usage.total_tokens === 'number') {
53-
span.setAttribute(GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, usage.total_tokens);
61+
totalTokens = usage.total_tokens;
5462
}
55-
return; // Found usage_metadata, no need to check fallback
63+
return { inputTokens, outputTokens, totalTokens };
5664
}
5765

5866
// Fallback: Extract from response_metadata.tokenUsage
@@ -61,16 +69,18 @@ export function extractTokenUsageFromMetadata(span: Span, message: LangChainMess
6169
if (metadata.tokenUsage && typeof metadata.tokenUsage === 'object') {
6270
const tokenUsage = metadata.tokenUsage as Record<string, unknown>;
6371
if (typeof tokenUsage.promptTokens === 'number') {
64-
span.setAttribute(GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, tokenUsage.promptTokens);
72+
inputTokens = tokenUsage.promptTokens;
6573
}
6674
if (typeof tokenUsage.completionTokens === 'number') {
67-
span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, tokenUsage.completionTokens);
75+
outputTokens = tokenUsage.completionTokens;
6876
}
6977
if (typeof tokenUsage.totalTokens === 'number') {
70-
span.setAttribute(GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, tokenUsage.totalTokens);
78+
totalTokens = tokenUsage.totalTokens;
7179
}
7280
}
7381
}
82+
83+
return { inputTokens, outputTokens, totalTokens };
7484
}
7585

7686
/**
@@ -147,9 +157,31 @@ export function setResponseAttributes(span: Span, inputMessages: LangChainMessag
147157
const normalizedNewMessages = normalizeLangChainMessages(newMessages);
148158
span.setAttribute(GEN_AI_RESPONSE_TEXT_ATTRIBUTE, JSON.stringify(normalizedNewMessages));
149159

160+
// Accumulate token usage across all messages
161+
let totalInputTokens = 0;
162+
let totalOutputTokens = 0;
163+
let totalTokens = 0;
164+
150165
// Extract metadata from messages
151166
for (const message of newMessages) {
152-
extractTokenUsageFromMetadata(span, message);
167+
// Accumulate token usage
168+
const tokens = extractTokenUsageFromMessage(message);
169+
totalInputTokens += tokens.inputTokens;
170+
totalOutputTokens += tokens.outputTokens;
171+
totalTokens += tokens.totalTokens;
172+
173+
// Extract model metadata (last message's metadata wins for model/finish_reason)
153174
extractModelMetadata(span, message);
154175
}
176+
177+
// Set accumulated token usage on span
178+
if (totalInputTokens > 0) {
179+
span.setAttribute(GEN_AI_USAGE_INPUT_TOKENS_ATTRIBUTE, totalInputTokens);
180+
}
181+
if (totalOutputTokens > 0) {
182+
span.setAttribute(GEN_AI_USAGE_OUTPUT_TOKENS_ATTRIBUTE, totalOutputTokens);
183+
}
184+
if (totalTokens > 0) {
185+
span.setAttribute(GEN_AI_USAGE_TOTAL_TOKENS_ATTRIBUTE, totalTokens);
186+
}
155187
}

0 commit comments

Comments
 (0)