diff --git a/internal/translator/codex/openai/responses/codex_openai-responses_request.go b/internal/translator/codex/openai/responses/codex_openai-responses_request.go index 360c037f8b..13c336b658 100644 --- a/internal/translator/codex/openai/responses/codex_openai-responses_request.go +++ b/internal/translator/codex/openai/responses/codex_openai-responses_request.go @@ -3,6 +3,7 @@ package responses import ( "fmt" + log "github.com/sirupsen/logrus" "github.com/tidwall/gjson" "github.com/tidwall/sjson" ) @@ -39,6 +40,7 @@ func ConvertOpenAIResponsesRequestToCodex(modelName string, inputRawJSON []byte, // Convert role "system" to "developer" in input array to comply with Codex API requirements. rawJSON = convertSystemRoleToDeveloper(rawJSON) + rawJSON = normalizeCodexBuiltinTools(rawJSON) return rawJSON } @@ -82,3 +84,59 @@ func convertSystemRoleToDeveloper(rawJSON []byte) []byte { return result } + +// normalizeCodexBuiltinTools rewrites legacy/preview built-in tool variants to the +// stable names expected by the current Codex upstream. +func normalizeCodexBuiltinTools(rawJSON []byte) []byte { + result := rawJSON + + tools := gjson.GetBytes(result, "tools") + if tools.IsArray() { + toolArray := tools.Array() + for i := 0; i < len(toolArray); i++ { + typePath := fmt.Sprintf("tools.%d.type", i) + result = normalizeCodexBuiltinToolAtPath(result, typePath) + } + } + + result = normalizeCodexBuiltinToolAtPath(result, "tool_choice.type") + + toolChoiceTools := gjson.GetBytes(result, "tool_choice.tools") + if toolChoiceTools.IsArray() { + toolArray := toolChoiceTools.Array() + for i := 0; i < len(toolArray); i++ { + typePath := fmt.Sprintf("tool_choice.tools.%d.type", i) + result = normalizeCodexBuiltinToolAtPath(result, typePath) + } + } + + return result +} + +func normalizeCodexBuiltinToolAtPath(rawJSON []byte, path string) []byte { + currentType := gjson.GetBytes(rawJSON, path).String() + normalizedType := normalizeCodexBuiltinToolType(currentType) + if normalizedType == "" { + return rawJSON + } + + updated, err := sjson.SetBytes(rawJSON, path, normalizedType) + if err != nil { + return rawJSON + } + + log.Debugf("codex responses: normalized builtin tool type at %s from %q to %q", path, currentType, normalizedType) + return updated +} + +// normalizeCodexBuiltinToolType centralizes the current known Codex Responses +// built-in tool alias compatibility. If Codex introduces more legacy aliases, +// extend this helper instead of adding path-specific rewrite logic elsewhere. +func normalizeCodexBuiltinToolType(toolType string) string { + switch toolType { + case "web_search_preview", "web_search_preview_2025_03_11": + return "web_search" + default: + return "" + } +} diff --git a/internal/translator/codex/openai/responses/codex_openai-responses_request_test.go b/internal/translator/codex/openai/responses/codex_openai-responses_request_test.go index a2ede1b874..3b48a76e04 100644 --- a/internal/translator/codex/openai/responses/codex_openai-responses_request_test.go +++ b/internal/translator/codex/openai/responses/codex_openai-responses_request_test.go @@ -264,6 +264,52 @@ func TestConvertSystemRoleToDeveloper_AssistantRole(t *testing.T) { } } +func TestConvertOpenAIResponsesRequestToCodex_NormalizesWebSearchPreview(t *testing.T) { + inputJSON := []byte(`{ + "model": "gpt-5.4-mini", + "input": "find latest OpenAI model news", + "tools": [ + {"type": "web_search_preview_2025_03_11"} + ], + "tool_choice": { + "type": "allowed_tools", + "tools": [ + {"type": "web_search_preview"}, + {"type": "web_search_preview_2025_03_11"} + ] + } + }`) + + output := ConvertOpenAIResponsesRequestToCodex("gpt-5.4-mini", inputJSON, false) + + if got := gjson.GetBytes(output, "tools.0.type").String(); got != "web_search" { + t.Fatalf("tools.0.type = %q, want %q: %s", got, "web_search", string(output)) + } + if got := gjson.GetBytes(output, "tool_choice.type").String(); got != "allowed_tools" { + t.Fatalf("tool_choice.type = %q, want %q: %s", got, "allowed_tools", string(output)) + } + if got := gjson.GetBytes(output, "tool_choice.tools.0.type").String(); got != "web_search" { + t.Fatalf("tool_choice.tools.0.type = %q, want %q: %s", got, "web_search", string(output)) + } + if got := gjson.GetBytes(output, "tool_choice.tools.1.type").String(); got != "web_search" { + t.Fatalf("tool_choice.tools.1.type = %q, want %q: %s", got, "web_search", string(output)) + } +} + +func TestConvertOpenAIResponsesRequestToCodex_NormalizesTopLevelToolChoicePreviewAlias(t *testing.T) { + inputJSON := []byte(`{ + "model": "gpt-5.4-mini", + "input": "find latest OpenAI model news", + "tool_choice": {"type": "web_search_preview_2025_03_11"} + }`) + + output := ConvertOpenAIResponsesRequestToCodex("gpt-5.4-mini", inputJSON, false) + + if got := gjson.GetBytes(output, "tool_choice.type").String(); got != "web_search" { + t.Fatalf("tool_choice.type = %q, want %q: %s", got, "web_search", string(output)) + } +} + func TestUserFieldDeletion(t *testing.T) { inputJSON := []byte(`{ "model": "gpt-5.2",