diff --git a/internal/lua/adapters/opencodego.lua b/internal/lua/adapters/opencodego.lua index 98022aa..b3528ee 100644 --- a/internal/lua/adapters/opencodego.lua +++ b/internal/lua/adapters/opencodego.lua @@ -31,12 +31,20 @@ end function adapter.build_headers(meta) local ts = tostring(meta.timestamp or "") + local src = (meta.source and meta.source.name) or "" return { ["User-Agent"] = adapter.headers["User-Agent"], ["x-opencode-client"] = "cli", -- project 固定:同网关实例共享一个工作区身份 - ["x-opencode-project"] = string.sub(sha256_hex("llmsproxy|" .. (meta.source and meta.source.name or "")), 1, 32), - ["x-opencode-session"] = rand_id("ses_", "session|" .. ts), + ["x-opencode-project"] = string.sub(sha256_hex("llmsproxy|" .. src), 1, 32), + -- session 必须**按源稳定**,不能每请求换:上游的前缀缓存在同一 session + -- 内才复用。实测(同一段 6032 token 提示词): + -- 固定 session -> 第 2 次命中 5888/6032,cached_tokens=5888 + -- 每请求换 session -> 永远 0 命中 + -- 原先用 meta.timestamp 派生,等于每请求都是新会话,缓存永远无效, + -- 上游也无法做会话亲和路由。 + ["x-opencode-session"] = rand_id("ses_", "session|llmsproxy|" .. src), + -- request id 仍每请求唯一(它只是请求标识,不参与缓存键) ["x-opencode-request"] = rand_id("msg_", "request|" .. ts .. "|" .. tostring(meta.body or "")), } end diff --git a/internal/lua/adapters/opencodezen.lua b/internal/lua/adapters/opencodezen.lua index 7940a84..c836fb0 100644 --- a/internal/lua/adapters/opencodezen.lua +++ b/internal/lua/adapters/opencodezen.lua @@ -32,12 +32,20 @@ end function adapter.build_headers(meta) local ts = tostring(meta.timestamp or "") + local src = (meta.source and meta.source.name) or "" return { ["User-Agent"] = adapter.headers["User-Agent"], ["x-opencode-client"] = "cli", -- project 固定:同网关实例共享一个工作区身份 - ["x-opencode-project"] = string.sub(sha256_hex("llmsproxy|" .. (meta.source and meta.source.name or "")), 1, 32), - ["x-opencode-session"] = rand_id("ses_", "session|" .. ts), + ["x-opencode-project"] = string.sub(sha256_hex("llmsproxy|" .. src), 1, 32), + -- session 必须**按源稳定**,不能每请求换:上游的前缀缓存在同一 session + -- 内才复用。实测(同一段 6032 token 提示词): + -- 固定 session -> 第 2 次命中 5888/6032,cached_tokens=5888 + -- 每请求换 session -> 永远 0 命中 + -- 原先用 meta.timestamp 派生,等于每请求都是新会话,缓存永远无效, + -- 上游也无法做会话亲和路由。 + ["x-opencode-session"] = rand_id("ses_", "session|llmsproxy|" .. src), + -- request id 仍每请求唯一(它只是请求标识,不参与缓存键) ["x-opencode-request"] = rand_id("msg_", "request|" .. ts .. "|" .. tostring(meta.body or "")), } end diff --git a/internal/lua/toolcall_preservation_test.go b/internal/lua/toolcall_preservation_test.go index 31e18ad..33ae354 100644 --- a/internal/lua/toolcall_preservation_test.go +++ b/internal/lua/toolcall_preservation_test.go @@ -231,3 +231,53 @@ func TestOpenCodeGoVsZenReasoning(t *testing.T) { } } } + +// TestOpenCodeSessionIsStableForCache pins that the opencode client header set +// keeps x-opencode-session STABLE per source while x-opencode-request stays +// unique per call. +// +// Measured against the live OpenCode Go endpoint: the upstream prefix cache is +// session-scoped. Replaying the same 6032-token prompt hits 5888 cached tokens +// when the session id is fixed, and 0 when it changes per request. Deriving the +// session from meta.timestamp (the previous behaviour) made every request a new +// session, so the cache could never hit. +func TestOpenCodeSessionIsStableForCache(t *testing.T) { + for _, name := range []string{"opencodego", "opencodezen"} { + vm := NewVM(freshAdapterDir(t)) + if err := vm.Start(); err != nil { + t.Fatal(err) + } + + metaA := map[string]interface{}{ + "timestamp": 1000, + "body": `{"messages":[{"role":"user","content":"one"}]}`, + "source": map[string]interface{}{"name": "somesource", "meta": nil}, + } + metaB := map[string]interface{}{ + "timestamp": 2000, // different second, different body + "body": `{"messages":[{"role":"user","content":"two"}]}`, + "source": map[string]interface{}{"name": "somesource", "meta": nil}, + } + h1, err := vm.BuildHeaders(name, metaA) + if err != nil { + t.Fatalf("%s headers A: %v", name, err) + } + h2, err := vm.BuildHeaders(name, metaB) + if err != nil { + t.Fatalf("%s headers B: %v", name, err) + } + + if h1["x-opencode-session"] == "" { + t.Fatalf("%s: session header must be set", name) + } + if h1["x-opencode-session"] != h2["x-opencode-session"] { + t.Errorf("%s: x-opencode-session must be stable across requests (%q vs %q) — "+ + "the upstream prefix cache is session-scoped, a rotating session kills every cache hit", + name, h1["x-opencode-session"], h2["x-opencode-session"]) + } + if h1["x-opencode-request"] == h2["x-opencode-request"] { + t.Errorf("%s: x-opencode-request must differ per request", name) + } + vm.Stop() + } +}