Skip to content

Commit 28a5a67

Browse files
committed
feat: add usage tracking and token extraction for Anthropic messages
1 parent 2cb836a commit 28a5a67

1 file changed

Lines changed: 42 additions & 0 deletions

File tree

index.ts

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -43,6 +43,7 @@ type AnthropicMessageLike = {
4343
content?: Array<any>;
4444
stop_reason?: string | null;
4545
parsed_output?: unknown;
46+
usage?: AnthropicUsage;
4647
};
4748

4849
type AnthropicToolUseBlock = {
@@ -67,6 +68,19 @@ type LangChainModelCallRequest = {
6768
messages: LangChainMessageLike[];
6869
};
6970

71+
type AnthropicUsage = {
72+
input_tokens?: number;
73+
cache_creation_input_tokens?: number | null;
74+
cache_read_input_tokens?: number | null;
75+
output_tokens?: number;
76+
};
77+
78+
type UsedTokens = {
79+
input_uncached: number;
80+
input_cached: number;
81+
output: number;
82+
};
83+
7084
function getApiKey(options: AdapterOptions): string | undefined {
7185
return options.anthropicApiKey || options.antropicApiKey;
7286
}
@@ -112,6 +126,21 @@ function extractReasoning(data: AnthropicMessageLike): string | undefined {
112126
return reasoning || undefined;
113127
}
114128

129+
function extractUsedTokens(data: AnthropicMessageLike): UsedTokens | undefined {
130+
const usage = data.usage;
131+
if (!usage) return undefined;
132+
133+
const inputTokens = usage.input_tokens ?? 0;
134+
const cacheWriteTokens = usage.cache_creation_input_tokens ?? 0;
135+
const cacheReadTokens = usage.cache_read_input_tokens ?? 0;
136+
137+
return {
138+
input_uncached: inputTokens + cacheWriteTokens,
139+
input_cached: cacheReadTokens,
140+
output: usage.output_tokens ?? 0,
141+
};
142+
}
143+
115144
function extractToolUse(data: AnthropicMessageLike): AnthropicToolUseBlock | undefined {
116145
for (const block of data.content ?? []) {
117146
if (block?.type === "tool_use") {
@@ -323,6 +352,7 @@ export default class CompletionAdapterAnthropicMessages
323352
content?: string;
324353
finishReason?: string;
325354
error?: string;
355+
used_tokens?: UsedTokens;
326356
}> => {
327357
const request =
328358
typeof requestOrContent === "string"
@@ -386,6 +416,7 @@ export default class CompletionAdapterAnthropicMessages
386416
? extractOutputText(parsedMessage)
387417
: JSON.stringify(parsedMessage.parsed_output);
388418
const parsedReasoning = extractReasoning(parsedMessage);
419+
const usedTokens = extractUsedTokens(parsedMessage);
389420

390421
if (parsedReasoning && isStreaming) {
391422
await streamChunkCallback?.(parsedReasoning, {
@@ -417,25 +448,29 @@ export default class CompletionAdapterAnthropicMessages
417448
return {
418449
content: toolResult,
419450
finishReason: "tool_call",
451+
used_tokens: usedTokens,
420452
};
421453
} catch (error) {
422454
return {
423455
error: getErrorMessage(error),
424456
finishReason: "tool_call",
457+
used_tokens: usedTokens,
425458
};
426459
}
427460
}
428461

429462
return {
430463
content: parsedOutput || undefined,
431464
finishReason: parsedMessage.stop_reason || undefined,
465+
used_tokens: usedTokens,
432466
};
433467
}
434468

435469
if (!isStreaming) {
436470
const message = (await this.getClient().messages.create(
437471
body as any,
438472
)) as AnthropicMessageLike;
473+
const usedTokens = extractUsedTokens(message);
439474

440475
const toolUse = extractToolUse(message);
441476
if (toolUse) {
@@ -444,18 +479,21 @@ export default class CompletionAdapterAnthropicMessages
444479
return {
445480
content: toolResult,
446481
finishReason: "tool_call",
482+
used_tokens: usedTokens,
447483
};
448484
} catch (error) {
449485
return {
450486
error: getErrorMessage(error),
451487
finishReason: "tool_call",
488+
used_tokens: usedTokens,
452489
};
453490
}
454491
}
455492

456493
return {
457494
content: extractOutputText(message) || undefined,
458495
finishReason: message.stop_reason || undefined,
496+
used_tokens: usedTokens,
459497
};
460498
}
461499

@@ -492,6 +530,7 @@ export default class CompletionAdapterAnthropicMessages
492530
const message = (await stream.finalMessage()) as AnthropicMessageLike;
493531
const finalContent = extractOutputText(message);
494532
const finalReasoning = extractReasoning(message) || "";
533+
const usedTokens = extractUsedTokens(message);
495534

496535
if (finalReasoning && finalReasoning !== fullReasoning) {
497536
const delta = finalReasoning.startsWith(fullReasoning)
@@ -541,19 +580,22 @@ export default class CompletionAdapterAnthropicMessages
541580
return {
542581
content: toolResult,
543582
finishReason: "tool_call",
583+
used_tokens: usedTokens,
544584
};
545585
} catch (error) {
546586
return {
547587
error: getErrorMessage(error),
548588
content: fullContent || undefined,
549589
finishReason: "tool_call",
590+
used_tokens: usedTokens,
550591
};
551592
}
552593
}
553594

554595
return {
555596
content: fullContent || undefined,
556597
finishReason: message.stop_reason || undefined,
598+
used_tokens: usedTokens,
557599
};
558600
} catch (error) {
559601
return {

0 commit comments

Comments
 (0)