diff --git a/apps/kimi-code/src/utils/usage/debug-timing.ts b/apps/kimi-code/src/utils/usage/debug-timing.ts index 457b686a3..ab87ebdd8 100644 --- a/apps/kimi-code/src/utils/usage/debug-timing.ts +++ b/apps/kimi-code/src/utils/usage/debug-timing.ts @@ -1,7 +1,16 @@ +import { formatTokenCount } from './usage-format'; + +interface DebugTokenUsage { + readonly inputOther?: number; + readonly inputCacheRead?: number; + readonly inputCacheCreation?: number; + readonly output?: number; +} + export interface StepTimingInput { - readonly llmFirstTokenLatencyMs?: number | undefined; - readonly llmStreamDurationMs?: number | undefined; - readonly usage?: { readonly output: number } | undefined; + readonly llmFirstTokenLatencyMs?: number; + readonly llmStreamDurationMs?: number; + readonly usage?: DebugTokenUsage; } // Decode TPS is only meaningful when the output actually streamed over a @@ -29,9 +38,33 @@ export function formatStepDebugTiming(input: StepTimingInput): string | undefine ); } } + + const inputTokens = usageInputTotal(input.usage); + const hasInputUsage = + input.usage !== undefined && + (input.usage.inputOther !== undefined || + input.usage.inputCacheRead !== undefined || + input.usage.inputCacheCreation !== undefined); + if (hasInputUsage && (inputTokens > 0 || (outputTokens ?? 0) > 0)) { + const cacheReadTokens = input.usage.inputCacheRead ?? 0; + const cacheCreationTokens = input.usage.inputCacheCreation ?? 0; + const cacheHitRate = inputTokens > 0 ? Math.round((cacheReadTokens / inputTokens) * 100) : 0; + const cacheParts = [`cache read ${formatTokenCount(cacheReadTokens)} (${cacheHitRate}%)`]; + if (cacheCreationTokens > 0) { + cacheParts.push(`write ${formatTokenCount(cacheCreationTokens)}`); + } + parts.push(`tokens in ${formatTokenCount(inputTokens)}`); + parts.push(cacheParts.join(' / ')); + } + return `[Debug] ${parts.join(' | ')}`; } +function usageInputTotal(usage: DebugTokenUsage | undefined): number { + if (usage === undefined) return 0; + return (usage.inputOther ?? 0) + (usage.inputCacheRead ?? 0) + (usage.inputCacheCreation ?? 0); +} + function formatDuration(ms: number): string { if (ms < 1000) return `${Math.round(ms)}ms`; return `${(ms / 1000).toFixed(1)}s`; diff --git a/apps/kimi-code/test/utils/usage/debug-timing.test.ts b/apps/kimi-code/test/utils/usage/debug-timing.test.ts index be871f3c1..feb857204 100644 --- a/apps/kimi-code/test/utils/usage/debug-timing.test.ts +++ b/apps/kimi-code/test/utils/usage/debug-timing.test.ts @@ -27,6 +27,38 @@ describe('formatStepDebugTiming', () => { expect(result).toBe('[Debug] TTFT: 800ms | TPS: 40.0 tok/s (200 tokens in 5.0s)'); }); + it('formats input tokens and cache read/write counts', () => { + const result = formatStepDebugTiming({ + llmFirstTokenLatencyMs: 800, + llmStreamDurationMs: 5000, + usage: { + inputOther: 700, + inputCacheRead: 1200, + inputCacheCreation: 100, + output: 200, + }, + }); + expect(result).toBe( + '[Debug] TTFT: 800ms | TPS: 40.0 tok/s (200 tokens in 5.0s) | tokens in 2.0k | cache read 1.2k (60%) / write 100', + ); + }); + + it('omits cache write count when it is zero', () => { + const result = formatStepDebugTiming({ + llmFirstTokenLatencyMs: 800, + llmStreamDurationMs: 5000, + usage: { + inputOther: 1000, + inputCacheRead: 0, + inputCacheCreation: 0, + output: 200, + }, + }); + expect(result).toContain('tokens in 1.0k'); + expect(result).toContain('cache read 0 (0%)'); + expect(result).not.toContain('/ write 0'); + }); + it('omits TPS when the streamed window is too short to measure', () => { const result = formatStepDebugTiming({ llmFirstTokenLatencyMs: 1200,