Commit c5aac125 authored by YanzheL's avatar YanzheL
Browse files

fix(gateway): add content-based session hash fallback for non-Codex clients

When no explicit session signals (session_id, conversation_id, prompt_cache_key)
are provided, derive a stable session seed from the request body content
(model + tools + system prompt + first user message) to enable sticky routing
and prompt caching for non-Codex clients using the Chat Completions API.

This mirrors the content-based fallback already present in GatewayService.
GenerateSessionHash, adapted for the OpenAI gateway's request formats (both
Chat Completions messages and Responses API input).

JSON fragments are canonicalized via normalizeCompatSeedJSON to ensure
semantically identical requests produce the same seed regardless of
whitespace or key ordering.

Closes #1421
parent 83a16dec
package service
import (
"encoding/json"
"strings"
"github.com/tidwall/gjson"
)
// contentSessionSeedPrefix prevents collisions between content-derived seeds
// and explicit session IDs (e.g. "sess-xxx" or "compat_cc_xxx").
const contentSessionSeedPrefix = "compat_cs_"
// deriveOpenAIContentSessionSeed builds a stable session seed from an
// OpenAI-format request body. Only fields constant across conversation turns
// are included: model, tools/functions definitions, system/developer prompts,
// instructions (Responses API), and the first user message.
// Supports both Chat Completions (messages) and Responses API (input).
func deriveOpenAIContentSessionSeed(body []byte) string {
if len(body) == 0 {
return ""
}
var b strings.Builder
if model := gjson.GetBytes(body, "model").String(); model != "" {
b.WriteString("model=")
b.WriteString(model)
}
if tools := gjson.GetBytes(body, "tools"); tools.Exists() && tools.IsArray() && tools.Raw != "[]" {
b.WriteString("|tools=")
b.WriteString(normalizeCompatSeedJSON(json.RawMessage(tools.Raw)))
}
if funcs := gjson.GetBytes(body, "functions"); funcs.Exists() && funcs.IsArray() && funcs.Raw != "[]" {
b.WriteString("|functions=")
b.WriteString(normalizeCompatSeedJSON(json.RawMessage(funcs.Raw)))
}
if instr := gjson.GetBytes(body, "instructions").String(); instr != "" {
b.WriteString("|instructions=")
b.WriteString(instr)
}
firstUserCaptured := false
msgs := gjson.GetBytes(body, "messages")
if msgs.Exists() && msgs.IsArray() {
msgs.ForEach(func(_, msg gjson.Result) bool {
role := msg.Get("role").String()
switch role {
case "system", "developer":
b.WriteString("|system=")
if c := msg.Get("content"); c.Exists() {
b.WriteString(normalizeCompatSeedJSON(json.RawMessage(c.Raw)))
}
case "user":
if !firstUserCaptured {
b.WriteString("|first_user=")
if c := msg.Get("content"); c.Exists() {
b.WriteString(normalizeCompatSeedJSON(json.RawMessage(c.Raw)))
}
firstUserCaptured = true
}
}
return true
})
} else if inp := gjson.GetBytes(body, "input"); inp.Exists() {
if inp.Type == gjson.String {
b.WriteString("|input=")
b.WriteString(inp.String())
} else if inp.IsArray() {
inp.ForEach(func(_, item gjson.Result) bool {
role := item.Get("role").String()
switch role {
case "system", "developer":
b.WriteString("|system=")
if c := item.Get("content"); c.Exists() {
b.WriteString(normalizeCompatSeedJSON(json.RawMessage(c.Raw)))
}
case "user":
if !firstUserCaptured {
b.WriteString("|first_user=")
if c := item.Get("content"); c.Exists() {
b.WriteString(normalizeCompatSeedJSON(json.RawMessage(c.Raw)))
}
firstUserCaptured = true
}
}
if !firstUserCaptured && item.Get("type").String() == "input_text" {
b.WriteString("|first_user=")
if text := item.Get("text").String(); text != "" {
b.WriteString(text)
}
firstUserCaptured = true
}
return true
})
}
}
if b.Len() == 0 {
return ""
}
return contentSessionSeedPrefix + b.String()
}
......@@ -1044,6 +1044,7 @@ func (s *OpenAIGatewayService) ExtractSessionID(c *gin.Context, body []byte) str
// 1. Header: session_id
// 2. Header: conversation_id
// 3. Body: prompt_cache_key (opencode)
// 4. Body: content-based fallback (model + system + tools + first user message)
func (s *OpenAIGatewayService) GenerateSessionHash(c *gin.Context, body []byte) string {
if c == nil {
return ""
......@@ -1056,6 +1057,9 @@ func (s *OpenAIGatewayService) GenerateSessionHash(c *gin.Context, body []byte)
if sessionID == "" && len(body) > 0 {
sessionID = strings.TrimSpace(gjson.GetBytes(body, "prompt_cache_key").String())
}
if sessionID == "" && len(body) > 0 {
sessionID = deriveOpenAIContentSessionSeed(body)
}
if sessionID == "" {
return ""
}
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment