diff --git a/CHANGELOG.md b/CHANGELOG.md index e970d16..f2e3f0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,6 +11,12 @@ version with its date and start a fresh empty `[Unreleased]` above it. ## [Unreleased] +### Fixed + +- The context usage meter no longer counts cached prompt tokens twice, so its + percentage now reflects the actual occupied portion of the selected context + window. + ## [1.0.17] - 2026-10-10 ### Changed diff --git a/package-lock.json b/package-lock.json index 0bdc774..e8aee52 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,7 +14,7 @@ "@codemirror/state": "^6.5.0", "@codemirror/view": "^6.38.6", "@modelcontextprotocol/sdk": "~1.32.1", - "@qoder-ai/qoder-agent-sdk": "^1.0.45", + "@qoder-ai/qoder-agent-sdk": "^1.0.51", "tslib": "^2.8.1" }, "devDependencies": { @@ -2954,9 +2954,9 @@ } }, "node_modules/@qoder-ai/qoder-agent-sdk": { - "version": "1.0.45", - "resolved": "https://registry.npmjs.org/@qoder-ai/qoder-agent-sdk/-/qoder-agent-sdk-1.0.45.tgz", - "integrity": "sha512-SQFx702DxLiXpbWJw/CtzqG+QYWztRkxcdlS/Ai7Y1SJSesFEWfwMEE7xazoXU89pzMRzE0uvDvcBcB7w4fRCQ==", + "version": "1.0.51", + "resolved": "https://registry.npmjs.org/@qoder-ai/qoder-agent-sdk/-/qoder-agent-sdk-1.0.51.tgz", + "integrity": "sha512-cMctrqlvGQ5s9qlICeRXZOybGjFZY8mULOKnjQArRDSIhU7n35U4AVYQL2GKi1b29ITwKOsFtokShIP2njoxhw==", "hasInstallScript": true, "license": "SEE LICENSE IN LICENSE", "dependencies": { @@ -6832,37 +6832,6 @@ "node": ">=10.13.0" } }, - "node_modules/glob/node_modules/balanced-match": { - "version": "1.0.2", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", - "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", - "dev": true, - "license": "MIT" - }, - "node_modules/glob/node_modules/brace-expansion": { - "version": "1.1.21", - "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.21.tgz", - "integrity": "sha512-9zeA+KLZNNzglF2TPKRQEDyx6Yby7daAkuy8MiPzpXPsYDWi/DRM8jmwUDxokQjYqBpv5DgPiwD4h4ZZSy1Ujw==", - "dev": true, - "license": "MIT", - "dependencies": { - "balanced-match": "^1.0.0", - "concat-map": "0.0.1" - } - }, - "node_modules/glob/node_modules/minimatch": { - "version": "3.1.5", - "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.1.5.tgz", - "integrity": "sha512-VgjWUsnnT6n+NUk6eZq77zeFdpW2LWDzP6zFGrCbHXiYNul5Dzqk2HHQ5uFH2DNW5Xbp8+jVzaeNt94ssEEl4w==", - "dev": true, - "license": "ISC", - "dependencies": { - "brace-expansion": "^1.1.7" - }, - "engines": { - "node": "*" - } - }, "node_modules/globals": { "version": "14.0.0", "resolved": "https://registry.npmjs.org/globals/-/globals-14.0.0.tgz", diff --git a/package.json b/package.json index f93655f..427ea51 100644 --- a/package.json +++ b/package.json @@ -56,7 +56,7 @@ "@codemirror/state": "^6.5.0", "@codemirror/view": "^6.38.6", "@modelcontextprotocol/sdk": "~1.32.1", - "@qoder-ai/qoder-agent-sdk": "^1.0.45", + "@qoder-ai/qoder-agent-sdk": "^1.0.51", "tslib": "^2.8.1" }, "overrides": { diff --git a/src/core/types/chat.ts b/src/core/types/chat.ts index 7d89b55..8843f2c 100644 --- a/src/core/types/chat.ts +++ b/src/core/types/chat.ts @@ -165,8 +165,8 @@ export type StreamChunk = /** * Context window usage information. * - * `contextTokens` is the Qoder-computed total token count in the context window: - * `inputTokens + cacheCreationInputTokens + cacheReadInputTokens`. + * `contextTokens` is the complete prompt size reported by Qoder. Cache creation + * and cache read counts are subsets of `inputTokens`, not additional context. * * Cache token fields are optional for older Qoder CLI responses. Feature code * should use `contextTokens` for display, not recompute from the breakdown. diff --git a/src/qoder/stream/transform-qoder-message.ts b/src/qoder/stream/transform-qoder-message.ts index 78c1fd5..a7331f8 100644 --- a/src/qoder/stream/transform-qoder-message.ts +++ b/src/qoder/stream/transform-qoder-message.ts @@ -311,7 +311,9 @@ function toPromptUsageSnapshot(usage: MessageUsage): PromptUsageSnapshot { inputTokens, cacheCreationInputTokens, cacheReadInputTokens, - contextTokens: inputTokens + cacheCreationInputTokens + cacheReadInputTokens, + // qodercli normalizes input_tokens to the complete prompt size. Cache read + // and creation counts are subsets used for billing detail, not extra context. + contextTokens: inputTokens, }; } @@ -327,7 +329,7 @@ function mergePromptUsage( inputTokens, cacheCreationInputTokens, cacheReadInputTokens, - contextTokens: inputTokens + cacheCreationInputTokens + cacheReadInputTokens, + contextTokens: inputTokens, }; } diff --git a/tests/unit/qoder/stream/transform-qoder-message.test.ts b/tests/unit/qoder/stream/transform-qoder-message.test.ts index ee7da9d..08016dd 100644 --- a/tests/unit/qoder/stream/transform-qoder-message.test.ts +++ b/tests/unit/qoder/stream/transform-qoder-message.test.ts @@ -1522,17 +1522,17 @@ describe('transformSDKMessage', () => { }); describe('assistant message usage extraction', () => { - it('yields usage info from main agent assistant message', () => { + it('does not double-count cache details toward context occupancy', () => { const message = msg({ type: 'assistant', parent_tool_use_id: null, // Main agent message: { content: [{ type: 'text', text: 'Hello' }], usage: { - input_tokens: 1000, + input_tokens: 100_000, output_tokens: 500, - cache_creation_input_tokens: 300, - cache_read_input_tokens: 200, + cache_creation_input_tokens: 10_000, + cache_read_input_tokens: 90_000, }, }, }); @@ -1543,12 +1543,12 @@ describe('transformSDKMessage', () => { expect(usageResults).toHaveLength(1); const usage = (usageResults[0] as any).usage; - expect(usage.inputTokens).toBe(1000); - expect(usage.cacheCreationInputTokens).toBe(300); - expect(usage.cacheReadInputTokens).toBe(200); - expect(usage.contextTokens).toBe(1500); // 1000 + 300 + 200 - expect(usage.contextWindow).toBe(200000); // Standard context window - expect(usage.percentage).toBe(1); // 1500 / 200000 * 100 rounded + expect(usage.inputTokens).toBe(100_000); + expect(usage.cacheCreationInputTokens).toBe(10_000); + expect(usage.cacheReadInputTokens).toBe(90_000); + expect(usage.contextTokens).toBe(100_000); + expect(usage.contextWindow).toBe(200_000); + expect(usage.percentage).toBe(50); }); it('yields usage from assistant message when usage state has no stream prompt usage', () => { @@ -1580,7 +1580,7 @@ describe('transformSDKMessage', () => { cacheCreationInputTokens: 300, cacheReadInputTokens: 200, contextWindow: 200000, - contextTokens: 1500, + contextTokens: 1000, percentage: 1, }); });