Files
magpie/internal/gateway/responses_cache_write_test.go
yetone f1ca6139b5 usage: a Codex call through the gateway that wrote to its upstream's cache is no longer listed a second time as a local session call (laowangba233 on #589: 用量中的统计是不是重复了 … 为啥会一个是提供商的一个是本地会话的) — the Usage page drops a Codex rollout's call when it matches a gateway record by session, end time within 2s and equal tokens. Codex reads input_tokens_details.cache_write_tokens from a Responses reply and keeps it as cache_write_input_tokens; the gateway's rUsage read only cached_tokens, so for a call whose upstream told a write the record had CacheWrite 0 and the rollout didn't, the tokens never matched, and the call stayed listed under Local session beside its provider's row. Calls without a write matched, so only some calls were counted twice (Maru Code 92 calls plus 22 "本地会话" in the report). Also, sessions' spent took only the cache read out of Codex's input_tokens, which holds the write too, so such a call's write was counted twice in its own input. Now: the gateway reads cache_write_tokens (input = input_tokens - cached - written) and tells it to Codex when it translates a reply; spent takes the write out of the input as well; and the matcher compares a call's input plus cache writes, its output and its cache reads, so a record and a rollout that split the write differently — an older Codex without the field, or records already in usage.jsonl from before this change — still match, and the duplicates already on a user's page go too. The session caches' versions go up (sessions.json 11, calls-v4) so kept parses are read again, and older calls-v* folders are removed. Under a provider filter the provider ranking still lists the other providers, as it is a picker; the duplicates were what made it read as double counting. TestCodexCacheWriteMatchesGatewayCall (a Codex rollout and gateway records for two calls, one with a 9000-token cache write; with the rollout naming the write and the record not, both naming it, and an older Codex without it: two rows, no Local session row, the provider ranking maru-code alone, unfiltered and filtered to the provider, by QueryPage and LedgerOf; fails without the change with a third row from the log), TestCodexCacheWriteNotInInput and TestResponsesUsageCacheWrite (parse and round trip through what Codex is told) are new and fail without the change; TestCallsCodex's call with a write now expects input 200 (2500 - 2000 read - 300 written). go vet ./internal/..., go build -tags nogui (darwin, GOOS=linux), GOOS=windows go build, go test -tags nogui -count=1 ./internal/... under a temp HOME pass (TestFXInState's known TempDir-cleanup flake aside, which passes alone).
2026-10-03 00:47:33 +08:00

37 lines
1.3 KiB
Go

package gateway
import (
"encoding/json"
"testing"
)
// #589: a Responses upstream's cache_write_tokens is counted as a cache
// write, not as input, as Codex counts it from the same reply; and a reply
// translated for Codex tells it what was written.
func TestResponsesUsageCacheWrite(t *testing.T) {
for _, c := range []struct {
name, usage string
want Usage
}{
{"read", `{"input_tokens":120000,"output_tokens":300,"input_tokens_details":{"cached_tokens":113000}}`, Usage{Input: 7000, Output: 300, CacheRead: 113000}},
{"read and write", `{"input_tokens":130000,"output_tokens":400,"input_tokens_details":{"cached_tokens":113000,"cache_write_tokens":9000},"output_tokens_details":{"reasoning_tokens":50}}`,
Usage{Input: 8000, Output: 400, CacheRead: 113000, CacheWrite: 9000, Reasoning: 50}},
{"none", `{"input_tokens":1000,"output_tokens":5}`, Usage{Input: 1000, Output: 5}},
} {
var u rUsage
if err := json.Unmarshal([]byte(c.usage), &u); err != nil {
t.Fatal(err)
}
got := u.usage()
if got != c.want {
t.Errorf("%s: %+v, want %+v", c.name, got, c.want)
}
// told to Codex again, it reads back the same
b, _ := json.Marshal(got.responses())
var back rUsage
if err := json.Unmarshal(b, &back); err != nil || back.usage() != c.want {
t.Errorf("%s: told as %s, read back %+v", c.name, b, back.usage())
}
}
}