From be2dd60ee74f7914229eb946864fc38a903259a1 Mon Sep 17 00:00:00 2001 From: Junyi Du Date: Thu, 19 Mar 2026 03:23:14 +0800 Subject: [PATCH 1/4] fix: normalize web_search_preview for codex responses --- .../codex_openai-responses_request.go | 24 +++++++++++++++++++ .../codex_openai-responses_request_test.go | 20 ++++++++++++++++ 2 files changed, 44 insertions(+) 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 360c037f..fd9bf92d 100644 --- a/internal/translator/codex/openai/responses/codex_openai-responses_request.go +++ b/internal/translator/codex/openai/responses/codex_openai-responses_request.go @@ -39,6 +39,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 +83,26 @@ 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) + if gjson.GetBytes(result, typePath).String() == "web_search_preview" { + result, _ = sjson.SetBytes(result, typePath, "web_search") + } + } + } + + if gjson.GetBytes(result, "tool_choice.type").String() == "web_search_preview" { + result, _ = sjson.SetBytes(result, "tool_choice.type", "web_search") + } + + return result +} 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 a2ede1b8..49587c9b 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,26 @@ 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"} + ], + "tool_choice": {"type": "web_search_preview"} + }`) + + 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 != "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", From 8f421de532ea379c50a7f6671d37ae6adb10acc7 Mon Sep 17 00:00:00 2001 From: Junyi Du Date: Thu, 19 Mar 2026 03:36:06 +0800 Subject: [PATCH 2/4] fix: handle sjson errors in codex tool normalization --- .../openai/responses/codex_openai-responses_request.go | 8 ++++++-- 1 file changed, 6 insertions(+), 2 deletions(-) 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 fd9bf92d..f6ea4771 100644 --- a/internal/translator/codex/openai/responses/codex_openai-responses_request.go +++ b/internal/translator/codex/openai/responses/codex_openai-responses_request.go @@ -95,13 +95,17 @@ func normalizeCodexBuiltinTools(rawJSON []byte) []byte { for i := 0; i < len(toolArray); i++ { typePath := fmt.Sprintf("tools.%d.type", i) if gjson.GetBytes(result, typePath).String() == "web_search_preview" { - result, _ = sjson.SetBytes(result, typePath, "web_search") + if updated, err := sjson.SetBytes(result, typePath, "web_search"); err == nil { + result = updated + } } } } if gjson.GetBytes(result, "tool_choice.type").String() == "web_search_preview" { - result, _ = sjson.SetBytes(result, "tool_choice.type", "web_search") + if updated, err := sjson.SetBytes(result, "tool_choice.type", "web_search"); err == nil { + result = updated + } } return result From 793840cdb4e9083948be4b8e581b2e6ae88b49a1 Mon Sep 17 00:00:00 2001 From: Junyi Du Date: Thu, 19 Mar 2026 03:41:12 +0800 Subject: [PATCH 3/4] fix: cover dated and nested codex web search aliases --- .../codex_openai-responses_request.go | 30 ++++++++++++++++--- .../codex_openai-responses_request_test.go | 30 +++++++++++++++++-- 2 files changed, 54 insertions(+), 6 deletions(-) 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 f6ea4771..64d5f824 100644 --- a/internal/translator/codex/openai/responses/codex_openai-responses_request.go +++ b/internal/translator/codex/openai/responses/codex_openai-responses_request.go @@ -94,19 +94,41 @@ func normalizeCodexBuiltinTools(rawJSON []byte) []byte { toolArray := tools.Array() for i := 0; i < len(toolArray); i++ { typePath := fmt.Sprintf("tools.%d.type", i) - if gjson.GetBytes(result, typePath).String() == "web_search_preview" { - if updated, err := sjson.SetBytes(result, typePath, "web_search"); err == nil { + if normalized := normalizeCodexBuiltinToolType(gjson.GetBytes(result, typePath).String()); normalized != "" { + if updated, err := sjson.SetBytes(result, typePath, normalized); err == nil { result = updated } } } } - if gjson.GetBytes(result, "tool_choice.type").String() == "web_search_preview" { - if updated, err := sjson.SetBytes(result, "tool_choice.type", "web_search"); err == nil { + if normalized := normalizeCodexBuiltinToolType(gjson.GetBytes(result, "tool_choice.type").String()); normalized != "" { + if updated, err := sjson.SetBytes(result, "tool_choice.type", normalized); err == nil { result = updated } } + 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) + if normalized := normalizeCodexBuiltinToolType(gjson.GetBytes(result, typePath).String()); normalized != "" { + if updated, err := sjson.SetBytes(result, typePath, normalized); err == nil { + result = updated + } + } + } + } + return result } + +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 49587c9b..3b48a76e 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 @@ -269,9 +269,15 @@ func TestConvertOpenAIResponsesRequestToCodex_NormalizesWebSearchPreview(t *test "model": "gpt-5.4-mini", "input": "find latest OpenAI model news", "tools": [ - {"type": "web_search_preview"} + {"type": "web_search_preview_2025_03_11"} ], - "tool_choice": {"type": "web_search_preview"} + "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) @@ -279,6 +285,26 @@ func TestConvertOpenAIResponsesRequestToCodex_NormalizesWebSearchPreview(t *test 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)) } From d1df70d02f2cee6fb97cfa6d2115961d9ef8c4f0 Mon Sep 17 00:00:00 2001 From: Junyi Du Date: Fri, 20 Mar 2026 14:08:37 +0800 Subject: [PATCH 4/4] chore: add codex builtin tool normalization logging --- .../codex_openai-responses_request.go | 38 +++++++++++-------- 1 file changed, 23 insertions(+), 15 deletions(-) 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 64d5f824..13c336b6 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" ) @@ -94,36 +95,43 @@ func normalizeCodexBuiltinTools(rawJSON []byte) []byte { toolArray := tools.Array() for i := 0; i < len(toolArray); i++ { typePath := fmt.Sprintf("tools.%d.type", i) - if normalized := normalizeCodexBuiltinToolType(gjson.GetBytes(result, typePath).String()); normalized != "" { - if updated, err := sjson.SetBytes(result, typePath, normalized); err == nil { - result = updated - } - } + result = normalizeCodexBuiltinToolAtPath(result, typePath) } } - if normalized := normalizeCodexBuiltinToolType(gjson.GetBytes(result, "tool_choice.type").String()); normalized != "" { - if updated, err := sjson.SetBytes(result, "tool_choice.type", normalized); err == nil { - result = updated - } - } + 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) - if normalized := normalizeCodexBuiltinToolType(gjson.GetBytes(result, typePath).String()); normalized != "" { - if updated, err := sjson.SetBytes(result, typePath, normalized); err == nil { - result = updated - } - } + 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":