1import type { SDKResultMessage } from "@anthropic-ai/claude-agent-sdk";
2import type { ProviderRuntimeTurnStatus, TurnTokenUsage } from "@t3tools/contracts";
3
4function finiteNonNegativeInteger(value: unknown): number | undefined {
5 return typeof value === "number" && Number.isFinite(value) && value >= 0
6 ? Math.round(value)
7 : undefined;
8}
9
10const BILLED_USAGE_FIELDS = [
11 "input_tokens",
12 "cache_read_input_tokens",
13 "cache_creation_input_tokens",
14 "output_tokens",
15] as const;
16
17// Adds earlier segments' counts to a count the final result reports. A count
18// the final result lacks stays missing, so status rules see the same fields.
19function addKnownCount(finalCount: unknown, earlierCounts: ReadonlyArray<unknown>): unknown {
20 const known = finiteNonNegativeInteger(finalCount);
21 if (known === undefined) return finalCount;
22 return earlierCounts.reduce<number>(
23 (sum, count) => sum + (finiteNonNegativeInteger(count) ?? 0),
24 known,
25 );
26}
27
28// A steer ends the running segment with an aborted result, and the SDK scopes
29// each result's usage to its own segment. Adds the billed counts of those
30// earlier segments onto the final result's usage. Other usage fields describe
31// the final result only and are left as they are.
32function withEarlierSegmentUsage(
33 usage: Record<string, unknown>,
34 earlierSegmentUsage: ReadonlyArray<unknown>,
35): Record<string, unknown> {
36 if (earlierSegmentUsage.length === 0) return usage;
37 const segments = earlierSegmentUsage.map((segment) => (segment ?? {}) as Record<string, unknown>);
38 const total = { ...usage };
39 for (const field of BILLED_USAGE_FIELDS) {
40 total[field] = addKnownCount(
41 usage[field],
42 segments.map((segment) => segment[field]),
43 );
44 }
45 const outputDetails = usage.output_tokens_details as Record<string, unknown> | undefined;
46 if (outputDetails != null) {
47 total.output_tokens_details = {
48 ...outputDetails,
49 thinking_tokens: addKnownCount(
50 outputDetails.thinking_tokens,
51 segments.map(
52 (segment) =>
53 (segment.output_tokens_details as Record<string, unknown> | undefined)?.thinking_tokens,
54 ),
55 ),
56 };
57 }
58 return total;
59}
60
61/**
62 * Normalizes a Claude turn's billed usage from its final SDK result.
63 * `earlierSegmentUsage` holds the usage of results a steer ended before the
64 * final one, oldest first.
65 */
66export function normalizeClaudeTurnTokenUsage(
67 result: { readonly subtype: SDKResultMessage["subtype"]; readonly usage?: unknown } | undefined,
68 hasSubagents: boolean,
69 terminalStatus: ProviderRuntimeTurnStatus,
70 earlierSegmentUsage: ReadonlyArray<unknown> = [],
71): TurnTokenUsage {
72 const finalUsage = result?.usage as Record<string, unknown> | undefined;
73 const usage = finalUsage && withEarlierSegmentUsage(finalUsage, earlierSegmentUsage);
74 if (!usage) {
75 return {
76 usageStatus: "unavailable",
77 usageScope: "main_agent",
78 hasSubagents,
79 };
80 }
81
82 const uncachedInputTokens = finiteNonNegativeInteger(usage.input_tokens);
83 const cachedInputTokens = finiteNonNegativeInteger(usage.cache_read_input_tokens);
84 const cacheCreationTokens = finiteNonNegativeInteger(usage.cache_creation_input_tokens);
85 const rawOutputTokens = finiteNonNegativeInteger(usage.output_tokens);
86 const outputDetails = usage.output_tokens_details as Record<string, unknown> | undefined;
87 const thinkingTokens = finiteNonNegativeInteger(outputDetails?.thinking_tokens);
88 const cachedInputContribution = usage.cache_read_input_tokens == null ? 0 : cachedInputTokens;
89 const cacheCreationContribution =
90 usage.cache_creation_input_tokens == null ? 0 : cacheCreationTokens;
91 const inputTokens =
92 uncachedInputTokens !== undefined &&
93 cachedInputContribution !== undefined &&
94 cacheCreationContribution !== undefined
95 ? uncachedInputTokens + cachedInputContribution + cacheCreationContribution
96 : undefined;
97 const hasKnownUsage =
98 uncachedInputTokens !== undefined ||
99 cachedInputTokens !== undefined ||
100 cacheCreationTokens !== undefined ||
101 rawOutputTokens !== undefined;
102 const hasPositiveUsage =
103 (uncachedInputTokens ?? 0) +
104 (cachedInputTokens ?? 0) +
105 (cacheCreationTokens ?? 0) +
106 (rawOutputTokens ?? 0) >
107 0;
108
109 if (!hasKnownUsage || (result?.subtype !== "success" && !hasPositiveUsage)) {
110 return {
111 usageStatus: "unavailable",
112 usageScope: "main_agent",
113 hasSubagents,
114 };
115 }
116
117 const commonUsage = {
118 usageScope: "main_agent",
119 ...(cachedInputTokens !== undefined ? { cachedInputTokens } : {}),
120 ...(cacheCreationTokens !== undefined ? { cacheCreationTokens } : {}),
121 ...(thinkingTokens !== undefined && rawOutputTokens !== undefined
122 ? { reasoningTokens: Math.min(rawOutputTokens, thinkingTokens) }
123 : {}),
124 hasSubagents,
125 } as const;
126 if (
127 terminalStatus === "completed" &&
128 result?.subtype === "success" &&
129 inputTokens !== undefined &&
130 rawOutputTokens !== undefined
131 ) {
132 return {
133 ...commonUsage,
134 usageStatus: "complete",
135 inputTokens,
136 outputTokens: rawOutputTokens,
137 };
138 }
139 return {
140 ...commonUsage,
141 usageStatus: "partial",
142 ...(inputTokens !== undefined ? { inputTokens } : {}),
143 ...(rawOutputTokens !== undefined ? { outputTokens: rawOutputTokens } : {}),
144 };
145}