diff --git a/tools/ui/src/lib/services/protocols/anthropic.ts b/tools/ui/src/lib/services/protocols/anthropic.ts index 34281b3110..17ee30fae1 100644 --- a/tools/ui/src/lib/services/protocols/anthropic.ts +++ b/tools/ui/src/lib/services/protocols/anthropic.ts @@ -224,10 +224,18 @@ function usageOf(raw: Record | undefined): ApiChatCompletionUsa if (!raw) return undefined; const usage: ApiChatCompletionUsage = {}; + const fields = [ + 'cache_creation_input_tokens', + 'cache_read_input_tokens', + 'input_tokens', + 'output_tokens' + ] as const; - if (typeof raw.input_tokens === 'number') usage.input_tokens = raw.input_tokens; + for (const field of fields) { + const value = raw[field]; - if (typeof raw.output_tokens === 'number') usage.output_tokens = raw.output_tokens; + if (typeof value === 'number') usage[field] = value; + } return Object.keys(usage).length > 0 ? usage : undefined; } diff --git a/tools/ui/src/lib/types/api.d.ts b/tools/ui/src/lib/types/api.d.ts index ebb41d79a6..d3fe2161bc 100644 --- a/tools/ui/src/lib/types/api.d.ts +++ b/tools/ui/src/lib/types/api.d.ts @@ -379,10 +379,15 @@ export interface ApiChatCompletionStreamChunk { } export interface ApiChatCompletionUsage { + cache_creation_input_tokens?: number; + cache_read_input_tokens?: number; + cached_tokens?: number; completion_tokens?: number; input_tokens?: number; output_tokens?: number; + prompt_cache_hit_tokens?: number; prompt_tokens?: number; + prompt_tokens_details?: { cached_tokens?: number; cache_write_tokens?: number }; total_tokens?: number; } diff --git a/tools/ui/src/lib/utils/timings.ts b/tools/ui/src/lib/utils/timings.ts index bd0179f8b7..401ecf579b 100644 --- a/tools/ui/src/lib/utils/timings.ts +++ b/tools/ui/src/lib/utils/timings.ts @@ -17,14 +17,33 @@ export interface StreamClock { lastTokenAt: number | null; } -/** Prompt/output token counts, accepting OpenAI and Anthropic usage fields. */ +/** + * Prompt/output/cache token counts, accepting OpenAI and Anthropic usage + * fields. `promptTokens` excludes the cache read tokens, which are returned + * separately as `cacheTokens`, so the two always add up to the prompt size. + */ export function usageTokenCounts(usage: ApiChatCompletionUsage | undefined): { - promptTokens: number; + cacheTokens: number; completionTokens: number; + promptTokens: number; } { + // Anthropic reports the input excluding cache tokens and splits reads from + // writes; OpenAI-compatible servers report a total that includes the reads + const isAnthropicStyle = usage?.input_tokens !== undefined; + const cacheTokens = isAnthropicStyle + ? (usage?.cache_read_input_tokens ?? 0) + : (usage?.prompt_tokens_details?.cached_tokens ?? + usage?.prompt_cache_hit_tokens ?? + usage?.cached_tokens ?? + 0); + const promptTotal = isAnthropicStyle + ? (usage?.input_tokens ?? 0) + (usage?.cache_creation_input_tokens ?? 0) + : (usage?.prompt_tokens ?? 0); + return { + cacheTokens, completionTokens: usage?.completion_tokens ?? usage?.output_tokens ?? 0, - promptTokens: usage?.prompt_tokens ?? usage?.input_tokens ?? 0 + promptTokens: isAnthropicStyle ? promptTotal : Math.max(0, promptTotal - cacheTokens) }; } @@ -33,7 +52,7 @@ export function buildTimingsFromUsage( clock: StreamClock, fallbackTokens = 0 ): ChatMessageTimings | null { - const { completionTokens, promptTokens } = usageTokenCounts(usage); + const { cacheTokens, completionTokens, promptTokens } = usageTokenCounts(usage); const predictedN = completionTokens || fallbackTokens; if (promptTokens === 0 && predictedN === 0) return null; @@ -42,6 +61,7 @@ export function buildTimingsFromUsage( const lastTokenAt = clock.lastTokenAt ?? firstTokenAt; return { + cache_n: cacheTokens, // clamp so a one-token reply still reports a positive duration predicted_ms: firstTokenAt && lastTokenAt ? Math.max(1, lastTokenAt - firstTokenAt) : undefined, predicted_n: predictedN,