From 9f1f203b8404d247a3a51dc714bf7bed621c553f Mon Sep 17 00:00:00 2001 From: rickylin047 Date: Tue, 10 Mar 2026 23:31:54 +0800 Subject: [PATCH] fix(openai): convert string input to array for Codex OAuth responses endpoint The ChatGPT backend-api codex/responses endpoint requires `input` to be an array, but the OpenAI Responses API spec allows it to be a plain string. When a client sends a string input, sub2api now converts it to the expected message array format. Empty/whitespace-only strings become an empty array to avoid triggering a 400 "Input must be a list" error. --- .../service/openai_codex_transform.go | 16 +++++++ .../service/openai_codex_transform_test.go | 44 +++++++++++++++++++ 2 files changed, 60 insertions(+) diff --git a/backend/internal/service/openai_codex_transform.go b/backend/internal/service/openai_codex_transform.go index e4f548dd..d1920140 100644 --- a/backend/internal/service/openai_codex_transform.go +++ b/backend/internal/service/openai_codex_transform.go @@ -146,6 +146,22 @@ func applyCodexOAuthTransform(reqBody map[string]any, isCodexCLI bool, isCompact input = filterCodexInput(input, needsToolContinuation) reqBody["input"] = input result.Modified = true + } else if inputStr, ok := reqBody["input"].(string); ok { + // ChatGPT codex endpoint requires input to be a list, not a string. + // Convert string input to the expected message array format. + trimmed := strings.TrimSpace(inputStr) + if trimmed != "" { + reqBody["input"] = []any{ + map[string]any{ + "type": "message", + "role": "user", + "content": inputStr, + }, + } + } else { + reqBody["input"] = []any{} + } + result.Modified = true } return result diff --git a/backend/internal/service/openai_codex_transform_test.go b/backend/internal/service/openai_codex_transform_test.go index 23abcb00..c8097aed 100644 --- a/backend/internal/service/openai_codex_transform_test.go +++ b/backend/internal/service/openai_codex_transform_test.go @@ -249,6 +249,50 @@ func TestApplyCodexOAuthTransform_NonCodexCLI_PreservesExistingInstructions(t *t require.Equal(t, "old instructions", instructions) } +func TestApplyCodexOAuthTransform_StringInputConvertedToArray(t *testing.T) { + reqBody := map[string]any{"model": "gpt-5.4", "input": "Hello, world!"} + 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, "message", msg["type"]) + require.Equal(t, "user", msg["role"]) + require.Equal(t, "Hello, world!", msg["content"]) +} + +func TestApplyCodexOAuthTransform_EmptyStringInputBecomesEmptyArray(t *testing.T) { + reqBody := map[string]any{"model": "gpt-5.4", "input": ""} + result := applyCodexOAuthTransform(reqBody, false, false) + require.True(t, result.Modified) + input, ok := reqBody["input"].([]any) + require.True(t, ok) + require.Len(t, input, 0) +} + +func TestApplyCodexOAuthTransform_WhitespaceStringInputBecomesEmptyArray(t *testing.T) { + reqBody := map[string]any{"model": "gpt-5.4", "input": " "} + result := applyCodexOAuthTransform(reqBody, false, false) + require.True(t, result.Modified) + input, ok := reqBody["input"].([]any) + require.True(t, ok) + require.Len(t, input, 0) +} + +func TestApplyCodexOAuthTransform_StringInputWithToolsField(t *testing.T) { + reqBody := map[string]any{ + "model": "gpt-5.4", + "input": "Run the tests", + "tools": []any{map[string]any{"type": "function", "name": "bash"}}, + } + applyCodexOAuthTransform(reqBody, false, false) + input, ok := reqBody["input"].([]any) + require.True(t, ok) + require.Len(t, input, 1) +} + func TestIsInstructionsEmpty(t *testing.T) { tests := []struct { name string