Skip to content
Open
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
Expand Down Expand Up @@ -82,3 +83,52 @@ 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 normalized := normalizeCodexBuiltinToolType(gjson.GetBytes(result, typePath).String()); normalized != "" {
if updated, err := sjson.SetBytes(result, typePath, normalized); err == nil {
result = updated
}
}
}
}

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"
Comment on lines +137 to +138

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

P2 Badge Preserve external_web_access semantics for preview web search

For requests that still use {"type":"web_search_preview"} (or the dated alias) together with external_web_access: false, this rewrite changes behavior instead of just making the request acceptable. OpenAI’s current web-search guide says preview variants ignore external_web_access and behave as if it were true, but after translating only the type field we forward {"type":"web_search","external_web_access":false} to Codex, which disables live web access. Existing preview clients relying on the documented preview behavior will silently start getting cache-only search results.

Useful? React with 👍 / 👎.

default:
return ""
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading