diff --git a/backend/internal/service/openai_codex_transform.go b/backend/internal/service/openai_codex_transform.go index c75cfa13ee..65f7d4a3a6 100644 --- a/backend/internal/service/openai_codex_transform.go +++ b/backend/internal/service/openai_codex_transform.go @@ -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 } @@ -908,10 +910,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 { @@ -919,25 +921,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") @@ -946,7 +947,6 @@ func extractSystemMessagesFromInput(reqBody map[string]any) bool { } else { reqBody["instructions"] = extracted } - reqBody["input"] = remaining return true } diff --git a/backend/internal/service/openai_codex_transform_test.go b/backend/internal/service/openai_codex_transform_test.go index 4c182b8eed..9043fe0e1d 100644 --- a/backend/internal/service/openai_codex_transform_test.go +++ b/backend/internal/service/openai_codex_transform_test.go @@ -1035,10 +1035,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"]) }) @@ -1058,7 +1062,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) { @@ -1074,7 +1085,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) { @@ -1089,13 +1110,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) { @@ -1109,6 +1134,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"]) }) } @@ -1168,14 +1198,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) {