diff --git a/.changeset/usage-cache-stats.md b/.changeset/usage-cache-stats.md new file mode 100644 index 000000000..7d771267d --- /dev/null +++ b/.changeset/usage-cache-stats.md @@ -0,0 +1,5 @@ +--- +"@moonshot-ai/kimi-code": minor +--- + +Show prompt cache read and write token counts in the usage report. diff --git a/apps/kimi-code/src/tui/components/messages/usage-panel.ts b/apps/kimi-code/src/tui/components/messages/usage-panel.ts index 1eeba55e2..2eac2cefa 100644 --- a/apps/kimi-code/src/tui/components/messages/usage-panel.ts +++ b/apps/kimi-code/src/tui/components/messages/usage-panel.ts @@ -62,6 +62,17 @@ function usageInputTotal(usage: TokenUsage): number { ); } +function formatCacheLine(usage: TokenUsage, value: Colorize, muted: Colorize): string | undefined { + const cacheRead = usageNumber(usage.inputCacheRead); + const cacheWrite = usageNumber(usage.inputCacheCreation); + if (cacheRead === 0 && cacheWrite === 0) return undefined; + return ( + ` ${muted('cache')} read ${value(formatTokenCount(cacheRead))} write ${value( + formatTokenCount(cacheWrite), + )}` + ); +} + function buildSessionUsageSection( usage: SessionUsage | undefined, error: string | undefined, @@ -78,23 +89,39 @@ function buildSessionUsageSection( const lines: string[] = []; let totalInput = 0; let totalOutput = 0; + let totalCacheRead = 0; + let totalCacheWrite = 0; for (const [model, row] of entries) { const input = usageInputTotal(row); const output = usageNumber(row.output); + const cacheRead = usageNumber(row.inputCacheRead); + const cacheWrite = usageNumber(row.inputCacheCreation); totalInput += input; totalOutput += output; + totalCacheRead += cacheRead; + totalCacheWrite += cacheWrite; lines.push( ` ${muted(model)} input ${value(formatTokenCount(input))} output ${value( formatTokenCount(output), )} total ${value(formatTokenCount(input + output))}`, ); + const cacheLine = formatCacheLine(row, value, muted); + if (cacheLine !== undefined) lines.push(cacheLine); } if (entries.length > 1) { + const total: TokenUsage = { + inputOther: totalInput - totalCacheRead - totalCacheWrite, + output: totalOutput, + inputCacheRead: totalCacheRead, + inputCacheCreation: totalCacheWrite, + }; lines.push( ` ${muted('total')} input ${value(formatTokenCount(totalInput))} output ${value( formatTokenCount(totalOutput), )} total ${value(formatTokenCount(totalInput + totalOutput))}`, ); + const cacheLine = formatCacheLine(total, value, muted); + if (cacheLine !== undefined) lines.push(cacheLine); } return lines; } diff --git a/apps/kimi-code/test/tui/components/messages/usage-panel.test.ts b/apps/kimi-code/test/tui/components/messages/usage-panel.test.ts index 5540b9b75..b7ba60f50 100644 --- a/apps/kimi-code/test/tui/components/messages/usage-panel.test.ts +++ b/apps/kimi-code/test/tui/components/messages/usage-panel.test.ts @@ -23,6 +23,12 @@ describe('UsagePanelComponent', () => { inputCacheCreation: 500, output: 250, }, + 'kimi-lite': { + inputOther: 200, + inputCacheRead: 0, + inputCacheCreation: 100, + output: 50, + }, }, } as never, contextUsage: 0.25, @@ -41,6 +47,11 @@ describe('UsagePanelComponent', () => { expect(lines).toContain('Session usage'); expect(lines).toContain(' kimi input 2.0k output 250 total 2.3k'); + expect(lines).toContain(' cache read 500 write 500'); + expect(lines).toContain(' kimi-lite input 300 output 50 total 350'); + expect(lines).toContain(' cache read 0 write 100'); + expect(lines).toContain(' total input 2.3k output 300 total 2.6k'); + expect(lines).toContain(' cache read 500 write 600'); expect(lines).toContain('Context window'); expect(lines.join('\n')).toContain('25.0%'); expect(lines).toContain('Plan usage'); @@ -48,6 +59,27 @@ describe('UsagePanelComponent', () => { expect(lines.join('\n')).toContain('resets tomorrow'); }); + it('omits cache rows when no cache tokens were recorded', () => { + const lines = buildUsageReportLines({ + sessionUsage: { + byModel: { + kimi: { + inputOther: 1000, + inputCacheRead: 0, + inputCacheCreation: 0, + output: 250, + }, + }, + } as never, + contextUsage: 0, + contextTokens: 0, + maxContextTokens: 0, + }).map(strip); + + expect(lines).toContain(' kimi input 1.0k output 250 total 1.3k'); + expect(lines.join('\n')).not.toContain('cache'); + }); + it('wraps preformatted usage lines in a bordered panel', () => { const component = new UsagePanelComponent(() => ['Session usage'], 'primary'); const output = component.render(80).map(strip);