Merge pull request #6049 from MokoYee/fix/openai-sticky-prefix-system

修复 OpenAI Chat 动态系统消息导致账号亲和漂移
This commit is contained in:
Wesley Liddick
2026-08-21 21:34:57 +08:00
committed by GitHub
2 changed files with 83 additions and 11 deletions
@@ -18,8 +18,9 @@ const contentStablePrefixSessionSeedPrefix = "compat_csp_"
// 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.
// are included: model, tools/functions definitions, the leading system/developer
// prompt prefix in Chat messages, 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 {
@@ -117,15 +118,19 @@ scanRoot:
msgs := fields[messagesField]
if msgs.Exists() && msgs.IsArray() {
systemPrefixOpen := true
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)))
if systemPrefixOpen {
_, _ = b.WriteString("|system=")
if c := msg.Get("content"); c.Exists() {
_, _ = b.WriteString(normalizeCompatSeedJSON(json.RawMessage(c.Raw)))
}
}
case "user":
systemPrefixOpen = false
if !firstUserCaptured {
_, _ = b.WriteString("|first_user=")
if c := msg.Get("content"); c.Exists() {
@@ -133,6 +138,8 @@ scanRoot:
}
firstUserCaptured = true
}
default:
systemPrefixOpen = false
}
return true
})
@@ -44,6 +44,65 @@ func TestDeriveOpenAIContentSessionSeed_ChatCompletions_StableAcrossTurns(t *tes
require.NotEmpty(t, s1)
}
func TestDeriveOpenAIContentSessionSeed_ChatCompletions_IgnoresLaterSystemMessages(t *testing.T) {
turn1 := []byte(`{
"model": "gpt-5.4",
"messages": [
{"role": "system", "content": "You are helpful."},
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi there!"},
{"role": "user", "content": "How are you?"}
]
}`)
turn2 := []byte(`{
"model": "gpt-5.4",
"messages": [
{"role": "system", "content": "You are helpful."},
{"role": "user", "content": "Hello"},
{"role": "assistant", "content": "Hi there!"},
{"role": "system", "content": "Return JSON for this turn."},
{"role": "user", "content": "How are you?"}
]
}`)
require.Equal(t, deriveOpenAIContentSessionSeed(turn1), deriveOpenAIContentSessionSeed(turn2))
}
func TestDeriveOpenAIContentSessionSeed_ChatCompletions_UsesLeadingSystemDeveloperPrefix(t *testing.T) {
firstSystem := []byte(`{
"model": "gpt-5.4",
"messages": [
{"role": "system", "content": "System A"},
{"role": "developer", "content": "Developer B"},
{"role": "user", "content": "Hello"}
]
}`)
changedLaterSystem := []byte(`{
"model": "gpt-5.4",
"messages": [
{"role": "system", "content": "System A"},
{"role": "developer", "content": "Developer C"},
{"role": "user", "content": "Hello"}
]
}`)
seed := deriveOpenAIContentSessionSeed(firstSystem)
require.Contains(t, seed, "System A")
require.Contains(t, seed, "Developer B")
require.NotEqual(t, seed, deriveOpenAIContentSessionSeed(changedLaterSystem))
withLaterSystem := []byte(`{
"model": "gpt-5.4",
"messages": [
{"role": "system", "content": "System A"},
{"role": "developer", "content": "Developer B"},
{"role": "user", "content": "Hello"},
{"role": "system", "content": "Dynamic system"}
]
}`)
require.Equal(t, seed, deriveOpenAIContentSessionSeed(withLaterSystem))
}
func TestDeriveOpenAIContentSessionSeed_ChatCompletions_DifferentFirstUserDiffers(t *testing.T) {
req1 := []byte(`{"model":"gpt-5.4","messages":[{"role":"user","content":"Question A"}]}`)
req2 := []byte(`{"model":"gpt-5.4","messages":[{"role":"user","content":"Question B"}]}`)
@@ -270,7 +329,7 @@ func TestDeriveOpenAIContentSessionSeed_SingleScanMatchesLegacyBytes(t *testing.
for _, test := range tests {
t.Run(test.name, func(t *testing.T) {
require.Equal(t, legacyDeriveOpenAIContentSessionSeed(test.body), deriveOpenAIContentSessionSeed(test.body))
require.Equal(t, referenceDeriveOpenAIContentSessionSeed(test.body), deriveOpenAIContentSessionSeed(test.body))
})
}
}
@@ -283,12 +342,12 @@ func TestDeriveOpenAIContentSessionSeed_AllTruncationOffsetsMatchLegacyBytes(t *
for bodyIndex, body := range bodies {
for end := 1; end < len(body); end++ {
truncated := []byte(body[:end])
require.Equalf(t, legacyDeriveOpenAIContentSessionSeed(truncated), deriveOpenAIContentSessionSeed(truncated), "body %d truncated at byte %d", bodyIndex, end)
require.Equalf(t, referenceDeriveOpenAIContentSessionSeed(truncated), deriveOpenAIContentSessionSeed(truncated), "body %d truncated at byte %d", bodyIndex, end)
}
}
}
func legacyDeriveOpenAIContentSessionSeed(body []byte) string {
func referenceDeriveOpenAIContentSessionSeed(body []byte) string {
if len(body) == 0 {
return ""
}
@@ -319,15 +378,19 @@ func legacyDeriveOpenAIContentSessionSeed(body []byte) string {
msgs := gjson.GetBytes(body, "messages")
if msgs.Exists() && msgs.IsArray() {
systemPrefixOpen := true
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)))
if systemPrefixOpen {
_, _ = b.WriteString("|system=")
if c := msg.Get("content"); c.Exists() {
_, _ = b.WriteString(normalizeCompatSeedJSON(json.RawMessage(c.Raw)))
}
}
case "user":
systemPrefixOpen = false
if !firstUserCaptured {
_, _ = b.WriteString("|first_user=")
if c := msg.Get("content"); c.Exists() {
@@ -335,6 +398,8 @@ func legacyDeriveOpenAIContentSessionSeed(body []byte) string {
}
firstUserCaptured = true
}
default:
systemPrefixOpen = false
}
return true
})