Merge pull request #3337 from ddnio/codex/openai-json-mode-developer-input

fix(openai): preserve JSON instructions in Codex OAuth input
This commit is contained in:
Wesley Liddick
2026-06-29 09:21:05 +08:00
committed by GitHub
2 changed files with 90 additions and 21 deletions
@@ -212,7 +212,9 @@ func applyCodexOAuthTransformWithOptions(reqBody map[string]any, opts codexOAuth
}
}
// 提取 input 中 role:"system" 消息至 instructions(OAuth 上游不支持 system role)。
// ChatGPT internal Codex endpoint does not accept role:"system".
// Keep the guidance in input as developer for Responses JSON mode, and
// also mirror it into instructions because Codex OAuth requires it.
if extractSystemMessagesFromInput(reqBody) {
result.Modified = true
}
@@ -948,10 +950,10 @@ func extractTextFromContent(content any) string {
}
}
// 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.
// extractSystemMessagesFromInput scans input for role=="system", maps those
// items to developer, and mirrors their text into reqBody["instructions"].
// It preserves the input items so Responses JSON mode can still see JSON
// instructions in input messages.
func extractSystemMessagesFromInput(reqBody map[string]any) bool {
input, ok := reqBody["input"].([]any)
if !ok || len(input) == 0 {
@@ -959,25 +961,24 @@ func extractSystemMessagesFromInput(reqBody map[string]any) bool {
}
var systemTexts []string
remaining := make([]any, 0, len(input))
modified := false
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
}
m["role"] = "developer"
modified = true
if text := extractTextFromContent(m["content"]); text != "" {
systemTexts = append(systemTexts, text)
}
}
if len(systemTexts) == 0 {
return false
return modified
}
extracted := strings.Join(systemTexts, "\n\n")
@@ -986,7 +987,6 @@ func extractSystemMessagesFromInput(reqBody map[string]any) bool {
} else {
reqBody["instructions"] = extracted
}
reqBody["input"] = remaining
return true
}
@@ -1094,10 +1094,14 @@ func TestExtractSystemMessagesFromInput(t *testing.T) {
require.True(t, result)
input, ok := reqBody["input"].([]any)
require.True(t, ok)
require.Len(t, input, 1)
require.Len(t, input, 2)
msg, ok := input[0].(map[string]any)
require.True(t, ok)
require.Equal(t, "user", msg["role"])
require.Equal(t, "developer", msg["role"])
require.Equal(t, "You are an assistant.", msg["content"])
user, ok := input[1].(map[string]any)
require.True(t, ok)
require.Equal(t, "user", user["role"])
require.Equal(t, "You are an assistant.", reqBody["instructions"])
})
@@ -1117,7 +1121,14 @@ func TestExtractSystemMessagesFromInput(t *testing.T) {
require.Equal(t, "Be helpful.", reqBody["instructions"])
input, ok := reqBody["input"].([]any)
require.True(t, ok)
require.Len(t, input, 0)
require.Len(t, input, 1)
msg, ok := input[0].(map[string]any)
require.True(t, ok)
require.Equal(t, "developer", msg["role"])
require.Equal(t, []any{
map[string]any{"type": "text", "text": "Be helpful."},
}, msg["content"])
require.Equal(t, "Be helpful.", reqBody["instructions"])
})
t.Run("multiple system messages concatenated", func(t *testing.T) {
@@ -1133,7 +1144,17 @@ func TestExtractSystemMessagesFromInput(t *testing.T) {
require.Equal(t, "First.\n\nSecond.", reqBody["instructions"])
input, ok := reqBody["input"].([]any)
require.True(t, ok)
require.Len(t, input, 1)
require.Len(t, input, 3)
first, ok := input[0].(map[string]any)
require.True(t, ok)
require.Equal(t, "developer", first["role"])
second, ok := input[1].(map[string]any)
require.True(t, ok)
require.Equal(t, "developer", second["role"])
user, ok := input[2].(map[string]any)
require.True(t, ok)
require.Equal(t, "user", user["role"])
require.Equal(t, "First.\n\nSecond.", reqBody["instructions"])
})
t.Run("mixed system and non-system preserves non-system", func(t *testing.T) {
@@ -1148,13 +1169,17 @@ func TestExtractSystemMessagesFromInput(t *testing.T) {
require.True(t, result)
input, ok := reqBody["input"].([]any)
require.True(t, ok)
require.Len(t, input, 2)
require.Len(t, input, 3)
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"])
require.Equal(t, "developer", second["role"])
third, ok := input[2].(map[string]any)
require.True(t, ok)
require.Equal(t, "assistant", third["role"])
require.Equal(t, "Sys prompt.", reqBody["instructions"])
})
t.Run("existing instructions prepended", func(t *testing.T) {
@@ -1168,6 +1193,11 @@ func TestExtractSystemMessagesFromInput(t *testing.T) {
result := extractSystemMessagesFromInput(reqBody)
require.True(t, result)
require.Equal(t, "Extracted.\n\nExisting instructions.", reqBody["instructions"])
input, ok := reqBody["input"].([]any)
require.True(t, ok)
msg, ok := input[0].(map[string]any)
require.True(t, ok)
require.Equal(t, "developer", msg["role"])
})
}
@@ -1227,14 +1257,53 @@ func TestApplyCodexOAuthTransform_ExtractsSystemMessages(t *testing.T) {
input, ok := reqBody["input"].([]any)
require.True(t, ok)
require.Len(t, input, 1)
msg, ok := input[0].(map[string]any)
require.Len(t, input, 2)
system, ok := input[0].(map[string]any)
require.True(t, ok)
require.Equal(t, "user", msg["role"])
require.Equal(t, "developer", system["role"])
require.Equal(t, "You are a coding assistant.", system["content"])
user, ok := input[1].(map[string]any)
require.True(t, ok)
require.Equal(t, "user", user["role"])
require.Equal(t, "You are a coding assistant.", reqBody["instructions"])
}
func TestApplyCodexOAuthTransform_JsonObjectKeepsJsonInstructionInInput(t *testing.T) {
reqBody := map[string]any{
"model": "gpt-5.4",
"input": []any{
map[string]any{
"role": "system",
"content": "You are an assistant. Output JSON only.",
},
map[string]any{
"role": "user",
"content": "symbol data without the keyword",
},
},
"text": map[string]any{
"format": map[string]any{
"type": "json_object",
},
},
}
result := applyCodexOAuthTransform(reqBody, false, false)
require.True(t, result.Modified)
instructions, ok := reqBody["instructions"].(string)
require.True(t, ok)
require.Equal(t, "You are a coding assistant.", instructions)
require.Contains(t, instructions, "JSON")
input, ok := reqBody["input"].([]any)
require.True(t, ok)
require.Len(t, input, 2)
developer, ok := input[0].(map[string]any)
require.True(t, ok)
require.Equal(t, "developer", developer["role"])
require.Contains(t, developer["content"], "JSON")
user, ok := input[1].(map[string]any)
require.True(t, ok)
require.Equal(t, "user", user["role"])
}
func TestIsInstructionsEmpty(t *testing.T) {