diff --git a/src/CodexCommands.ts b/src/CodexCommands.ts index 321ac5da..0af237f6 100644 --- a/src/CodexCommands.ts +++ b/src/CodexCommands.ts @@ -476,9 +476,10 @@ export class CodexCommands { } const total = this.formatTokenCount(usage.totalTokens); const input = this.formatTokenCount(usage.inputTokens); - const cachedInput = this.formatTokenCount(usage.cachedInputTokens); + const cacheRead = this.formatTokenCount(usage.cachedInputTokens); + const cacheWrite = this.formatTokenCount(usage.cacheWriteInputTokens); const output = this.formatTokenCount(usage.outputTokens); - return `${total} total (${input} input + ${cachedInput} cached input, ${output} output)`; + return `${total} total (${input} input + ${cacheRead} cache read + ${cacheWrite} cache write, ${output} output)`; } private formatContextWindow(usage: TokenCount | null, contextWindow: number | null): string { diff --git a/src/TokenCount.ts b/src/TokenCount.ts index 1f7cf0b5..83e4d671 100644 --- a/src/TokenCount.ts +++ b/src/TokenCount.ts @@ -5,16 +5,18 @@ import type {TokenUsageBreakdown} from "./app-server/v2"; * Token usage information for a turn. * This interface decouples our API from Codex's internal types. * - * [totalTokens]: total number of tokens used (the sum of all other fields) - * [inputTokens]: number of non-cached input tokens - * [cachedInputTokens]: number of cached input tokens + * [totalTokens]: ordinary input + cache reads + cache writes + output + * [inputTokens]: number of ordinary input tokens + * [cachedInputTokens]: number of cache-read input tokens + * [cacheWriteInputTokens]: number of cache-write input tokens * [outputTokens]: number of output tokens (including reasoning output tokens) - * [reasoningOutputTokens]: number of reasoning output tokens + * [reasoningOutputTokens]: reasoning subset of output tokens */ export interface TokenCount { totalTokens: number; inputTokens: number; cachedInputTokens: number; + cacheWriteInputTokens: number; outputTokens: number; reasoningOutputTokens: number; } @@ -22,14 +24,16 @@ export interface TokenCount { /** * Maps Codex's TokenUsageBreakdown to our TokenCount interface. * This explicit mapping ensures compile-time errors if Codex changes their types. - * Note: Codex includes cached input tokens in the input token count, so they are subtracted here. + * Note: Codex includes cache reads and writes in the input token count, so they are subtracted here. */ export function toTokenCount(usage: TokenUsageBreakdown): TokenCount { + const cacheWriteInputTokens = usage.cacheWriteInputTokens ?? 0; return { totalTokens: usage.totalTokens, - inputTokens: usage.inputTokens - usage.cachedInputTokens, + inputTokens: usage.inputTokens - usage.cachedInputTokens - cacheWriteInputTokens, cachedInputTokens: usage.cachedInputTokens, + cacheWriteInputTokens: cacheWriteInputTokens, outputTokens: usage.outputTokens, reasoningOutputTokens: usage.reasoningOutputTokens, }; @@ -37,14 +41,15 @@ export function toTokenCount(usage: TokenUsageBreakdown): TokenCount { /** * Maps our per-turn token breakdown to ACP PromptResponse usage fields. - * Cached input tokens are reported as ACP cache reads, and reasoning output - * tokens are exposed through ACP's thoughtTokens field. + * Cache reads and writes are reported through their corresponding ACP fields, + * and reasoning output tokens are exposed through ACP's thoughtTokens field. */ export function toPromptUsage(tokenCount: TokenCount): Usage { return { totalTokens: tokenCount.totalTokens, inputTokens: tokenCount.inputTokens, cachedReadTokens: tokenCount.cachedInputTokens, + cachedWriteTokens: tokenCount.cacheWriteInputTokens, outputTokens: tokenCount.outputTokens, thoughtTokens: tokenCount.reasoningOutputTokens, }; diff --git a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts index fe51f160..3739ceca 100644 --- a/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts +++ b/src/__tests__/CodexACPAgent/CodexAcpClient.test.ts @@ -1624,8 +1624,17 @@ describe('ACP server test', { timeout: 40_000 }, () => { const {mockFixture, sessionState} = setupPromptFixture({ lastTokenUsage: { totalTokens: 41_500, - inputTokens: 40_000, + inputTokens: 39_000, + cachedInputTokens: 1_000, + cacheWriteInputTokens: 1_000, + outputTokens: 500, + reasoningOutputTokens: 100, + }, + totalTokenUsage: { + totalTokens: 41_500, + inputTokens: 39_000, cachedInputTokens: 1_000, + cacheWriteInputTokens: 1_000, outputTokens: 500, reasoningOutputTokens: 100, }, @@ -1641,6 +1650,9 @@ describe('ACP server test', { timeout: 40_000 }, () => { expect(mockFixture.getAcpConnectionDump([])).toContain( "**Context window:** 16% used (41.5K used / 258.4K)", ); + expect(mockFixture.getAcpConnectionDump([])).toContain( + "**Token usage:** 41.5K total (39.0K input + 1.0K cache read + 1.0K cache write, 500 output)", + ); }); it('resets the previous context usage before a model turn', async () => { @@ -1649,6 +1661,7 @@ describe('ACP server test', { timeout: 40_000 }, () => { totalTokens: 41_500, inputTokens: 40_000, cachedInputTokens: 1_000, + cacheWriteInputTokens: 0, outputTokens: 500, reasoningOutputTokens: 100, }, diff --git a/src/__tests__/CodexACPAgent/data/token-usage-cancelled.json b/src/__tests__/CodexACPAgent/data/token-usage-cancelled.json index 426011ab..f620b831 100644 --- a/src/__tests__/CodexACPAgent/data/token-usage-cancelled.json +++ b/src/__tests__/CodexACPAgent/data/token-usage-cancelled.json @@ -4,6 +4,7 @@ "totalTokens": 1500, "inputTokens": 1200, "cachedReadTokens": 0, + "cachedWriteTokens": 0, "outputTokens": 300, "thoughtTokens": 0 }, @@ -13,6 +14,7 @@ "totalTokens": 1500, "inputTokens": 1200, "cachedInputTokens": 0, + "cacheWriteInputTokens": 0, "outputTokens": 300, "reasoningOutputTokens": 0 }, @@ -23,6 +25,7 @@ "totalTokens": 1500, "inputTokens": 1200, "cachedInputTokens": 0, + "cacheWriteInputTokens": 0, "outputTokens": 300, "reasoningOutputTokens": 0 } diff --git a/src/__tests__/CodexACPAgent/data/token-usage-end-turn.json b/src/__tests__/CodexACPAgent/data/token-usage-end-turn.json index 60ba2e2e..80b203b2 100644 --- a/src/__tests__/CodexACPAgent/data/token-usage-end-turn.json +++ b/src/__tests__/CodexACPAgent/data/token-usage-end-turn.json @@ -2,18 +2,20 @@ "stopReason": "end_turn", "usage": { "totalTokens": 2500, - "inputTokens": 1500, + "inputTokens": 1250, "cachedReadTokens": 500, - "outputTokens": 450, + "cachedWriteTokens": 250, + "outputTokens": 500, "thoughtTokens": 50 }, "_meta": { "quota": { "token_count": { "totalTokens": 2500, - "inputTokens": 1500, + "inputTokens": 1250, "cachedInputTokens": 500, - "outputTokens": 450, + "cacheWriteInputTokens": 250, + "outputTokens": 500, "reasoningOutputTokens": 50 }, "model_usage": [ @@ -21,9 +23,10 @@ "model": "model-id", "token_count": { "totalTokens": 2500, - "inputTokens": 1500, + "inputTokens": 1250, "cachedInputTokens": 500, - "outputTokens": 450, + "cacheWriteInputTokens": 250, + "outputTokens": 500, "reasoningOutputTokens": 50 } } diff --git a/src/__tests__/CodexACPAgent/data/token-usage-multiple-updates.json b/src/__tests__/CodexACPAgent/data/token-usage-multiple-updates.json index aebc1783..d8c346db 100644 --- a/src/__tests__/CodexACPAgent/data/token-usage-multiple-updates.json +++ b/src/__tests__/CodexACPAgent/data/token-usage-multiple-updates.json @@ -4,7 +4,8 @@ "totalTokens": 1500, "inputTokens": 700, "cachedReadTokens": 500, - "outputTokens": 200, + "cachedWriteTokens": 0, + "outputTokens": 300, "thoughtTokens": 100 }, "_meta": { @@ -13,7 +14,8 @@ "totalTokens": 1500, "inputTokens": 700, "cachedInputTokens": 500, - "outputTokens": 200, + "cacheWriteInputTokens": 0, + "outputTokens": 300, "reasoningOutputTokens": 100 }, "model_usage": [ @@ -23,7 +25,8 @@ "totalTokens": 1500, "inputTokens": 700, "cachedInputTokens": 500, - "outputTokens": 200, + "cacheWriteInputTokens": 0, + "outputTokens": 300, "reasoningOutputTokens": 100 } } diff --git a/src/__tests__/CodexACPAgent/token-usage-events.test.ts b/src/__tests__/CodexACPAgent/token-usage-events.test.ts index dc49c1fc..b47860a0 100644 --- a/src/__tests__/CodexACPAgent/token-usage-events.test.ts +++ b/src/__tests__/CodexACPAgent/token-usage-events.test.ts @@ -60,16 +60,16 @@ describe('Token Usage Events', () => { totalTokens: 5000, inputTokens: 4000, cachedInputTokens: 1000, - cacheWriteInputTokens: 0, - outputTokens: 900, + cacheWriteInputTokens: 500, + outputTokens: 1000, reasoningOutputTokens: 100, }, last: { totalTokens: 2500, inputTokens: 2000, cachedInputTokens: 500, - cacheWriteInputTokens: 0, - outputTokens: 450, + cacheWriteInputTokens: 250, + outputTokens: 500, reasoningOutputTokens: 50, }, modelContextWindow: 128000, @@ -87,24 +87,23 @@ describe('Token Usage Events', () => { ); }); - it('should include token_count in PromptResponse on cancelled', async () => { + it('should include token_count on cancelled with legacy token usage', async () => { const tokenUsageNotification = createTokenUsageNotification(sessionId, { + // Older Codex binaries can omit cacheWriteInputTokens at runtime. total: { totalTokens: 3000, inputTokens: 2500, cachedInputTokens: 0, - cacheWriteInputTokens: 0, outputTokens: 500, reasoningOutputTokens: 0, - }, + } as TokenUsageBreakdown, last: { totalTokens: 1500, inputTokens: 1200, cachedInputTokens: 0, - cacheWriteInputTokens: 0, outputTokens: 300, reasoningOutputTokens: 0, - }, + } as TokenUsageBreakdown, modelContextWindow: 128000, }); @@ -146,8 +145,8 @@ describe('Token Usage Events', () => { modelContextWindow: 128000, }), createTokenUsageNotification(sessionId, { - total: { totalTokens: 3500, inputTokens: 2800, cachedInputTokens: 500, cacheWriteInputTokens: 0, outputTokens: 600, reasoningOutputTokens: 100 }, - last: { totalTokens: 1500, inputTokens: 1200, cachedInputTokens: 500, cacheWriteInputTokens: 0, outputTokens: 200, reasoningOutputTokens: 100 }, + total: { totalTokens: 3500, inputTokens: 2800, cachedInputTokens: 500, cacheWriteInputTokens: 0, outputTokens: 700, reasoningOutputTokens: 100 }, + last: { totalTokens: 1500, inputTokens: 1200, cachedInputTokens: 500, cacheWriteInputTokens: 0, outputTokens: 300, reasoningOutputTokens: 100 }, modelContextWindow: 128000, }), ]; @@ -201,16 +200,16 @@ describe('Token Usage Events', () => { totalTokens: 5000, inputTokens: 4000, cachedInputTokens: 1000, - cacheWriteInputTokens: 0, - outputTokens: 900, + cacheWriteInputTokens: 500, + outputTokens: 1000, reasoningOutputTokens: 100, }, last: { totalTokens: 2500, inputTokens: 2000, cachedInputTokens: 500, - cacheWriteInputTokens: 0, - outputTokens: 450, + cacheWriteInputTokens: 250, + outputTokens: 500, reasoningOutputTokens: 50, }, modelContextWindow: 128000, @@ -233,8 +232,8 @@ describe('Token Usage Events', () => { modelContextWindow: 128000, }), createTokenUsageNotification(sessionId, { - total: { totalTokens: 3500, inputTokens: 2800, cachedInputTokens: 500, cacheWriteInputTokens: 0, outputTokens: 600, reasoningOutputTokens: 100 }, - last: { totalTokens: 1500, inputTokens: 1200, cachedInputTokens: 500, cacheWriteInputTokens: 0, outputTokens: 200, reasoningOutputTokens: 100 }, + total: { totalTokens: 3500, inputTokens: 2800, cachedInputTokens: 500, cacheWriteInputTokens: 0, outputTokens: 700, reasoningOutputTokens: 100 }, + last: { totalTokens: 1500, inputTokens: 1200, cachedInputTokens: 500, cacheWriteInputTokens: 0, outputTokens: 300, reasoningOutputTokens: 100 }, modelContextWindow: 128000, }), ])();