From 69ba0da3051865bd6f60346afff26f6870e8e87b Mon Sep 17 00:00:00 2001 From: IfkumRfnl Date: Mon, 3 Aug 2026 05:49:31 +0000 Subject: [PATCH 1/2] fix(stream): correct cached input accounting --- CHANGELOG.md | 4 ++++ src/core.ts | 10 +++++++--- tests/test-stream.ts | 11 +++++++---- 3 files changed, 18 insertions(+), 7 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index df21563..e6d6c4a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,5 +1,9 @@ # Changelog +## 0.4.4 - 2026-08-03 + +- Fix cached input tokens being counted twice. + ## 0.4.3 - 2026-08-02 - Allow pi to start when model discovery is unavailable. The provider now caches the last successfully fetched model catalog so previously discovered Command Code models remain selectable offline; a first offline start without a cache keeps Command Code unavailable until `/reload` succeeds. diff --git a/src/core.ts b/src/core.ts index 301ff64..e696c86 100644 --- a/src/core.ts +++ b/src/core.ts @@ -388,10 +388,14 @@ export function createStreamCommandCode(deps: CoreDependencies) { const usage = commandCodeUsage(event) if (usage) { const details = commandCodeInputTokenDetails(usage) - output.usage.input = numberValue(usage.inputTokens) ?? 0 + const totalInput = numberValue(usage.inputTokens) ?? 0 + const input = numberValue(details?.noCacheTokens) + const cacheRead = numberValue(details?.cacheReadTokens) ?? 0 + const cacheWrite = numberValue(details?.cacheWriteTokens) ?? 0 + output.usage.input = input ?? Math.max(0, totalInput - cacheRead - cacheWrite) output.usage.output = numberValue(usage.outputTokens) ?? 0 - output.usage.cacheRead = numberValue(details?.cacheReadTokens) ?? 0 - output.usage.cacheWrite = numberValue(details?.cacheWriteTokens) ?? 0 + output.usage.cacheRead = cacheRead + output.usage.cacheWrite = cacheWrite output.usage.totalTokens = output.usage.input + output.usage.output + diff --git a/tests/test-stream.ts b/tests/test-stream.ts index 225b52d..31e4d03 100644 --- a/tests/test-stream.ts +++ b/tests/test-stream.ts @@ -102,9 +102,9 @@ describe("streamCommandCode — successful streams", () => { type: "finish", finishReason: "stop", totalUsage: { - inputTokens: 5, - outputTokens: 2, - inputTokenDetails: { cacheReadTokens: 3, cacheWriteTokens: 1 }, + inputTokens: 3124, + outputTokens: 15, + inputTokenDetails: { noCacheTokens: 52, cacheReadTokens: 3072 }, }, }), ], @@ -134,7 +134,10 @@ describe("streamCommandCode — successful streams", () => { done.message.content[0]?.type === "text" ? done.message.content[0].text : "", "Hello", ) - assert.equal(done.message.usage.totalTokens, 11) + assert.equal(done.message.usage.input, 52) + assert.equal(done.message.usage.cacheRead, 3072) + assert.equal(done.message.usage.cacheWrite, 0) + assert.equal(done.message.usage.totalTokens, 3139) assert.equal(calculatedUsages.length, 1) }) From 9cff90340c418861602fb571437b38edec8df16b Mon Sep 17 00:00:00 2001 From: Patrick Wozniak Date: Tue, 4 Aug 2026 01:41:22 +0200 Subject: [PATCH 2/2] test(stream): cover cached input fallbacks --- tests/test-stream.ts | 64 ++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 64 insertions(+) diff --git a/tests/test-stream.ts b/tests/test-stream.ts index 31e4d03..745bf06 100644 --- a/tests/test-stream.ts +++ b/tests/test-stream.ts @@ -141,6 +141,70 @@ describe("streamCommandCode — successful streams", () => { assert.equal(calculatedUsages.length, 1) }) + it("derives uncached input when noCacheTokens is missing", async () => { + server.mockResponse({ + type: "success", + events: [ + JSON.stringify({ + type: "finish", + finishReason: "stop", + totalUsage: { + inputTokens: 100, + outputTokens: 10, + inputTokenDetails: { cacheReadTokens: 75 }, + }, + }), + ], + }) + const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() }) + + const events = await collectEvents( + streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }), + ) + + const done = events.at(-1) + assert.equal(done?.type, "done") + if (done?.type !== "done") throw new Error("expected done") + assert.equal(done.message.usage.input, 25) + assert.equal(done.message.usage.cacheRead, 75) + assert.equal(done.message.usage.cacheWrite, 0) + assert.equal(done.message.usage.totalTokens, 110) + }) + + it("accounts for cache writes separately from uncached input", async () => { + server.mockResponse({ + type: "success", + events: [ + JSON.stringify({ + type: "finish", + finishReason: "stop", + totalUsage: { + inputTokens: 100, + outputTokens: 10, + inputTokenDetails: { + noCacheTokens: 20, + cacheReadTokens: 70, + cacheWriteTokens: 10, + }, + }, + }), + ], + }) + const { streamCommandCode } = createTestDeps({ apiBase: server.baseUrl() }) + + const events = await collectEvents( + streamCommandCode(makeModel(), makeContext(), { apiKey: "mock-key" }), + ) + + const done = events.at(-1) + assert.equal(done?.type, "done") + if (done?.type !== "done") throw new Error("expected done") + assert.equal(done.message.usage.input, 20) + assert.equal(done.message.usage.cacheRead, 70) + assert.equal(done.message.usage.cacheWrite, 10) + assert.equal(done.message.usage.totalTokens, 110) + }) + it("ends on finish without waiting for an open upstream connection", async () => { server.mockResponse({ type: "success",