Merge pull request #31 from IfkumRfnl/fix/cache-accounting

fix(stream): correct cached input accounting
This commit is contained in:
Patrick Wozniak
2026-08-04 01:43:35 +02:00
committed by GitHub
3 changed files with 82 additions and 7 deletions
+4
View File
@@ -1,5 +1,9 @@
# Changelog # Changelog
## 0.4.4 - 2026-08-03
- Fix cached input tokens being counted twice.
## 0.4.3 - 2026-08-02 ## 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. - 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.
+7 -3
View File
@@ -388,10 +388,14 @@ export function createStreamCommandCode(deps: CoreDependencies) {
const usage = commandCodeUsage(event) const usage = commandCodeUsage(event)
if (usage) { if (usage) {
const details = commandCodeInputTokenDetails(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.output = numberValue(usage.outputTokens) ?? 0
output.usage.cacheRead = numberValue(details?.cacheReadTokens) ?? 0 output.usage.cacheRead = cacheRead
output.usage.cacheWrite = numberValue(details?.cacheWriteTokens) ?? 0 output.usage.cacheWrite = cacheWrite
output.usage.totalTokens = output.usage.totalTokens =
output.usage.input + output.usage.input +
output.usage.output + output.usage.output +
+71 -4
View File
@@ -102,9 +102,9 @@ describe("streamCommandCode — successful streams", () => {
type: "finish", type: "finish",
finishReason: "stop", finishReason: "stop",
totalUsage: { totalUsage: {
inputTokens: 5, inputTokens: 3124,
outputTokens: 2, outputTokens: 15,
inputTokenDetails: { cacheReadTokens: 3, cacheWriteTokens: 1 }, inputTokenDetails: { noCacheTokens: 52, cacheReadTokens: 3072 },
}, },
}), }),
], ],
@@ -134,10 +134,77 @@ describe("streamCommandCode — successful streams", () => {
done.message.content[0]?.type === "text" ? done.message.content[0].text : "", done.message.content[0]?.type === "text" ? done.message.content[0].text : "",
"Hello", "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) 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 () => { it("ends on finish without waiting for an open upstream connection", async () => {
server.mockResponse({ server.mockResponse({
type: "success", type: "success",