mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-09-23 02:18:01 +00:00
feat(opencode): per-conversation session via first-user-message fingerprint
Follow-on to the session-stability fix. "Per source" already made the prefix cache hit, but it puts every conversation into one upstream session. Using the client's own session id is not possible: capturing real agent traffic (tcpdump on 127.0.0.1:8081) shows generic OpenAI clients send NO session identifier at all — no user / session_id / conversation_id / metadata in the body, and no session header (only X-Stainless-* plus User-Agent: pi). The x-opencode-session the Go endpoint asks for is an OpenCode native-client concept that a generic client cannot forward. Since history is replayed every turn, the FIRST user message is invariant for the life of a conversation, so it is used as the conversation fingerprint. The session becomes stable within a conversation and distinct across conversations; requests with no user message fall back to per-source stability. Measured through the gateway (same 5.7k-token prompt): 2nd call cached_tokens=5504, and an unrelated conversation gets its own session. Test: TestOpenCodeSessionIsStableForCache covers same-conversation stability, cross-conversation separation, per-request request ids and the sessionless fallback.
This commit is contained in:
@ -232,52 +232,72 @@ 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.
|
||||
// TestOpenCodeSessionIsStableForCache pins the session-derivation contract that
|
||||
// makes the upstream prefix cache usable.
|
||||
//
|
||||
// Measured against the live OpenCode Go endpoint: the upstream prefix cache is
|
||||
// session-scoped. Replaying the same 6032-token prompt hits 5888 cached tokens
|
||||
// Measured against the live OpenCode Go endpoint: the 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.
|
||||
// session from meta.timestamp (the previous behaviour) meant every request was a
|
||||
// new session, so the cache could never hit.
|
||||
//
|
||||
// The session must be stable WITHIN one conversation and differ BETWEEN
|
||||
// conversations. Since generic OpenAI clients send no session id at all
|
||||
// (verified by capturing real agent traffic: no user / session_id /
|
||||
// conversation_id / metadata in the body, no session header), the conversation
|
||||
// is fingerprinted by its FIRST user message — history is replayed every turn,
|
||||
// so that message is invariant for the life of the conversation.
|
||||
func TestOpenCodeSessionIsStableForCache(t *testing.T) {
|
||||
for _, name := range []string{"opencodego", "opencodezen"} {
|
||||
vm := NewVM(freshAdapterDir(t))
|
||||
if err := vm.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
meta := func(ts int, body string) map[string]interface{} {
|
||||
return map[string]interface{}{
|
||||
"timestamp": ts,
|
||||
"body": body,
|
||||
"source": map[string]interface{}{"name": "somesource", "meta": nil},
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
// Same conversation, later turn: history grew, first user msg unchanged.
|
||||
h1, err := vm.BuildHeaders(name, meta(1000,
|
||||
`{"messages":[{"role":"user","content":"fix the bug"},{"role":"assistant","content":"looking"}]}`))
|
||||
if err != nil {
|
||||
t.Fatalf("%s headers A: %v", name, err)
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
h2, err := vm.BuildHeaders(name, metaB)
|
||||
h2, err := vm.BuildHeaders(name, meta(2000,
|
||||
`{"messages":[{"role":"user","content":"fix the bug"},{"role":"assistant","content":"looking"},{"role":"user","content":"and tests"}]}`))
|
||||
if err != nil {
|
||||
t.Fatalf("%s headers B: %v", name, err)
|
||||
t.Fatalf("%s: %v", name, err)
|
||||
}
|
||||
// Different conversation.
|
||||
h3, err := vm.BuildHeaders(name, meta(3000,
|
||||
`{"messages":[{"role":"user","content":"an unrelated task"}]}`))
|
||||
if err != nil {
|
||||
t.Fatalf("%s: %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"])
|
||||
t.Errorf("%s: x-opencode-session must be STABLE across turns of one conversation "+
|
||||
"(%q vs %q) — the prefix cache is session-scoped", name, h1["x-opencode-session"], h2["x-opencode-session"])
|
||||
}
|
||||
if h1["x-opencode-session"] == h3["x-opencode-session"] {
|
||||
t.Errorf("%s: a different conversation must get a different session", name)
|
||||
}
|
||||
if h1["x-opencode-request"] == h2["x-opencode-request"] {
|
||||
t.Errorf("%s: x-opencode-request must differ per request", name)
|
||||
}
|
||||
// No user message at all falls back to per-source stability.
|
||||
h4, _ := vm.BuildHeaders(name, meta(4000, `{"messages":[{"role":"system","content":"s"}]}`))
|
||||
h5, _ := vm.BuildHeaders(name, meta(5000, `{"messages":[{"role":"system","content":"s"}]}`))
|
||||
if h4["x-opencode-session"] != h5["x-opencode-session"] {
|
||||
t.Errorf("%s: sessionless requests must still be stable per source", name)
|
||||
}
|
||||
vm.Stop()
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user