mirror of
https://gitee.com/wanwujie/sub2api
synced 2026-04-17 13:24:45 +08:00
fix(oauth): extract system-role input items into instructions field
OAuth upstreams (ChatGPT) reject requests containing role:"system" in the input array with HTTP 400 "System messages are not allowed". Extract such items before forwarding and merge their content into the top-level instructions field, prepending to any existing value. Co-Authored-By: Claude Sonnet 4.6 <noreply@anthropic.com>
This commit is contained in:
@@ -172,6 +172,11 @@ func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool, isCompact
|
|||||||
result.PromptCacheKey = strings.TrimSpace(v)
|
result.PromptCacheKey = strings.TrimSpace(v)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// 提取 input 中 role:"system" 消息至 instructions(OAuth 上游不支持 system role)。
|
||||||
|
if extractSystemMessagesFromInput(reqBody) {
|
||||||
|
result.Modified = true
|
||||||
|
}
|
||||||
|
|
||||||
// instructions 处理逻辑:根据是否是 Codex CLI 分别调用不同方法
|
// instructions 处理逻辑:根据是否是 Codex CLI 分别调用不同方法
|
||||||
if applyInstructions(reqBody, isCodexCLI) {
|
if applyInstructions(reqBody, isCodexCLI) {
|
||||||
result.Modified = true
|
result.Modified = true
|
||||||
@@ -301,6 +306,73 @@ func getNormalizedCodexModel(modelID string) string {
|
|||||||
return ""
|
return ""
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// extractTextFromContent extracts plain text from a content value that is either
|
||||||
|
// a Go string or a []any of content-part maps with type:"text".
|
||||||
|
func extractTextFromContent(content any) string {
|
||||||
|
switch v := content.(type) {
|
||||||
|
case string:
|
||||||
|
return v
|
||||||
|
case []any:
|
||||||
|
var parts []string
|
||||||
|
for _, part := range v {
|
||||||
|
m, ok := part.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if t, _ := m["type"].(string); t == "text" {
|
||||||
|
if text, ok := m["text"].(string); ok {
|
||||||
|
parts = append(parts, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return strings.Join(parts, "")
|
||||||
|
default:
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// extractSystemMessagesFromInput scans the input array for items with role=="system",
|
||||||
|
// removes them, and merges their content into reqBody["instructions"].
|
||||||
|
// If instructions is already non-empty, extracted content is prepended with "\n\n".
|
||||||
|
// Returns true if any system messages were extracted.
|
||||||
|
func extractSystemMessagesFromInput(reqBody map[string]any) bool {
|
||||||
|
input, ok := reqBody["input"].([]any)
|
||||||
|
if !ok || len(input) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
var systemTexts []string
|
||||||
|
remaining := make([]any, 0, len(input))
|
||||||
|
|
||||||
|
for _, item := range input {
|
||||||
|
m, ok := item.(map[string]any)
|
||||||
|
if !ok {
|
||||||
|
remaining = append(remaining, item)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if role, _ := m["role"].(string); role != "system" {
|
||||||
|
remaining = append(remaining, item)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if text := extractTextFromContent(m["content"]); text != "" {
|
||||||
|
systemTexts = append(systemTexts, text)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(systemTexts) == 0 {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
extracted := strings.Join(systemTexts, "\n\n")
|
||||||
|
if existing, ok := reqBody["instructions"].(string); ok && strings.TrimSpace(existing) != "" {
|
||||||
|
reqBody["instructions"] = extracted + "\n\n" + existing
|
||||||
|
} else {
|
||||||
|
reqBody["instructions"] = extracted
|
||||||
|
}
|
||||||
|
reqBody["input"] = remaining
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
// applyInstructions 处理 instructions 字段:仅在 instructions 为空时填充默认值。
|
// applyInstructions 处理 instructions 字段:仅在 instructions 为空时填充默认值。
|
||||||
func applyInstructions(reqBody map[string]any, isCodexCLI bool) bool {
|
func applyInstructions(reqBody map[string]any, isCodexCLI bool) bool {
|
||||||
if !isInstructionsEmpty(reqBody) {
|
if !isInstructionsEmpty(reqBody) {
|
||||||
|
|||||||
@@ -344,6 +344,135 @@ func TestApplyCodexOAuthTransform_StringInputWithToolsField(t *testing.T) {
|
|||||||
require.Len(t, input, 1)
|
require.Len(t, input, 1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestExtractSystemMessagesFromInput(t *testing.T) {
|
||||||
|
t.Run("no system messages", func(t *testing.T) {
|
||||||
|
reqBody := map[string]any{
|
||||||
|
"input": []any{
|
||||||
|
map[string]any{"role": "user", "content": "hello"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result := extractSystemMessagesFromInput(reqBody)
|
||||||
|
require.False(t, result)
|
||||||
|
input, ok := reqBody["input"].([]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Len(t, input, 1)
|
||||||
|
_, hasInstructions := reqBody["instructions"]
|
||||||
|
require.False(t, hasInstructions)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("string content system message", func(t *testing.T) {
|
||||||
|
reqBody := map[string]any{
|
||||||
|
"input": []any{
|
||||||
|
map[string]any{"role": "system", "content": "You are an assistant."},
|
||||||
|
map[string]any{"role": "user", "content": "hello"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result := extractSystemMessagesFromInput(reqBody)
|
||||||
|
require.True(t, result)
|
||||||
|
input, ok := reqBody["input"].([]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Len(t, input, 1)
|
||||||
|
msg, ok := input[0].(map[string]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, "user", msg["role"])
|
||||||
|
require.Equal(t, "You are an assistant.", reqBody["instructions"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("array content system message", func(t *testing.T) {
|
||||||
|
reqBody := map[string]any{
|
||||||
|
"input": []any{
|
||||||
|
map[string]any{
|
||||||
|
"role": "system",
|
||||||
|
"content": []any{
|
||||||
|
map[string]any{"type": "text", "text": "Be helpful."},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result := extractSystemMessagesFromInput(reqBody)
|
||||||
|
require.True(t, result)
|
||||||
|
require.Equal(t, "Be helpful.", reqBody["instructions"])
|
||||||
|
input, ok := reqBody["input"].([]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Len(t, input, 0)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("multiple system messages concatenated", func(t *testing.T) {
|
||||||
|
reqBody := map[string]any{
|
||||||
|
"input": []any{
|
||||||
|
map[string]any{"role": "system", "content": "First."},
|
||||||
|
map[string]any{"role": "system", "content": "Second."},
|
||||||
|
map[string]any{"role": "user", "content": "hi"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result := extractSystemMessagesFromInput(reqBody)
|
||||||
|
require.True(t, result)
|
||||||
|
require.Equal(t, "First.\n\nSecond.", reqBody["instructions"])
|
||||||
|
input, ok := reqBody["input"].([]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Len(t, input, 1)
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("mixed system and non-system preserves non-system", func(t *testing.T) {
|
||||||
|
reqBody := map[string]any{
|
||||||
|
"input": []any{
|
||||||
|
map[string]any{"role": "user", "content": "hello"},
|
||||||
|
map[string]any{"role": "system", "content": "Sys prompt."},
|
||||||
|
map[string]any{"role": "assistant", "content": "Hi there"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
result := extractSystemMessagesFromInput(reqBody)
|
||||||
|
require.True(t, result)
|
||||||
|
input, ok := reqBody["input"].([]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Len(t, input, 2)
|
||||||
|
first, ok := input[0].(map[string]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, "user", first["role"])
|
||||||
|
second, ok := input[1].(map[string]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, "assistant", second["role"])
|
||||||
|
})
|
||||||
|
|
||||||
|
t.Run("existing instructions prepended", func(t *testing.T) {
|
||||||
|
reqBody := map[string]any{
|
||||||
|
"input": []any{
|
||||||
|
map[string]any{"role": "system", "content": "Extracted."},
|
||||||
|
map[string]any{"role": "user", "content": "hi"},
|
||||||
|
},
|
||||||
|
"instructions": "Existing instructions.",
|
||||||
|
}
|
||||||
|
result := extractSystemMessagesFromInput(reqBody)
|
||||||
|
require.True(t, result)
|
||||||
|
require.Equal(t, "Extracted.\n\nExisting instructions.", reqBody["instructions"])
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func TestApplyCodexOAuthTransform_ExtractsSystemMessages(t *testing.T) {
|
||||||
|
reqBody := map[string]any{
|
||||||
|
"model": "gpt-5.1",
|
||||||
|
"input": []any{
|
||||||
|
map[string]any{"role": "system", "content": "You are a coding assistant."},
|
||||||
|
map[string]any{"role": "user", "content": "Write a function."},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
result := applyCodexOAuthTransform(reqBody, false, false)
|
||||||
|
|
||||||
|
require.True(t, result.Modified)
|
||||||
|
|
||||||
|
input, ok := reqBody["input"].([]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Len(t, input, 1)
|
||||||
|
msg, ok := input[0].(map[string]any)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, "user", msg["role"])
|
||||||
|
|
||||||
|
instructions, ok := reqBody["instructions"].(string)
|
||||||
|
require.True(t, ok)
|
||||||
|
require.Equal(t, "You are a coding assistant.", instructions)
|
||||||
|
}
|
||||||
|
|
||||||
func TestIsInstructionsEmpty(t *testing.T) {
|
func TestIsInstructionsEmpty(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
|
|||||||
Reference in New Issue
Block a user