mirror of
https://github.com/Wei-Shaw/sub2api.git
synced 2026-10-07 16:48:45 +08:00
Merge pull request #6049 from MokoYee/fix/openai-sticky-prefix-system
修复 OpenAI Chat 动态系统消息导致账号亲和漂移
This commit is contained in:
@@ -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
|
||||
})
|
||||
|
||||
Reference in New Issue
Block a user