fix(translator/codex): map Claude Code WebSearch tool to Codex web_search#3837
fix(translator/codex): map Claude Code WebSearch tool to Codex web_search#3837haowang02 wants to merge 1 commit into
Conversation
…arch
Claude Code 2.1.177 no longer sends web search as a typed Anthropic server
tool (web_search_20250305 / web_search_20260209). It now declares a plain
function tool named "WebSearch" with an input_schema, so the Claude->Codex
request translator forwarded it to the Codex Responses API as a regular
function tool and the native {"type":"web_search"} builtin was never emitted.
Web search therefore stopped working for the latest Claude Code against a
Codex backend.
Detect the function-style WebSearch tool by its name combined with its
query / allowed_domains / blocked_domains schema fingerprint, and map it to
the Codex builtin web_search tool, the same as the typed server tools. The
fingerprint keeps unrelated custom tools (for example a user tool named
"web_search") on the normal function-calling path, preserving existing
tool_choice behavior.
Fixes router-for-me#3829
Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
There was a problem hiding this comment.
Pull request overview
Note
Copilot was unable to run its full agentic suite in this review.
This PR updates the Claude→Codex request translator to recognize Claude Code’s newer function-style WebSearch tool declaration and map it to Codex’s builtin web_search tool, with accompanying tests to prevent regressions.
Changes:
- Detect function-style
WebSearchtool declarations (Claude Code 2.1.177+) and map them to Codex builtinweb_search. - Extend tool-choice mapping to treat function-style
WebSearchas builtin web search. - Add tests covering tool mapping and
tool_choicemapping behavior.
Reviewed changes
Copilot reviewed 2 out of 2 changed files in this pull request and generated 1 comment.
| File | Description |
|---|---|
| internal/translator/codex/claude/codex_claude_request.go | Adds detection logic to map Claude Code’s function-style WebSearch tool to Codex builtin web_search. |
| internal/translator/codex/claude/codex_claude_request_test.go | Adds regression tests ensuring function-style WebSearch is mapped to builtin and tool_choice is converted accordingly. |
💡 Add Copilot custom instructions for smarter, more guided reviews. Learn how to get started.
| func isClaudeCodeWebSearchFunctionTool(tool gjson.Result) bool { | ||
| if tool.Get("name").String() != "WebSearch" { | ||
| return false | ||
| } | ||
| // Typed server tools carry an explicit type and are handled separately; | ||
| // only plain custom/function declarations reach this branch. | ||
| switch tool.Get("type").String() { | ||
| case "", "custom": | ||
| default: | ||
| return false | ||
| } | ||
| props := tool.Get("input_schema.properties") | ||
| if !props.IsObject() { | ||
| return false | ||
| } | ||
| return props.Get("query").Exists() && | ||
| props.Get("allowed_domains").Exists() && | ||
| props.Get("blocked_domains").Exists() | ||
| } |
There was a problem hiding this comment.
Code Review
This pull request introduces support for mapping the function-style "WebSearch" tool emitted by recent Claude Code releases (2.1.177+) to the Codex builtin "web_search" tool. It implements fingerprinting logic to detect this tool based on its name and schema properties, and includes corresponding unit tests. The reviewer suggests making the fingerprinting logic more resilient to future schema changes by only requiring the "query" property and allowing "function" in the tool type switch.
Important
The consumer version of Gemini Code Assist on GitHub is being sunset. Starting June 18, 2026, new organization installations will be blocked, and all code review activity will officially cease on July 17, 2026.
For more details on the timeline and next steps, please review the Help Documentation.
| // isClaudeCodeWebSearchFunctionTool detects the function-style WebSearch tool | ||
| // that Claude Code declares as {"name":"WebSearch","input_schema":{...}}. The | ||
| // schema is fingerprinted by its query plus allowed_domains / blocked_domains | ||
| // filter properties so unrelated custom tools (for example a user tool named | ||
| // "web_search") are not misclassified as the builtin web search tool. | ||
| func isClaudeCodeWebSearchFunctionTool(tool gjson.Result) bool { | ||
| if tool.Get("name").String() != "WebSearch" { | ||
| return false | ||
| } | ||
| // Typed server tools carry an explicit type and are handled separately; | ||
| // only plain custom/function declarations reach this branch. | ||
| switch tool.Get("type").String() { | ||
| case "", "custom": | ||
| default: | ||
| return false | ||
| } | ||
| props := tool.Get("input_schema.properties") | ||
| if !props.IsObject() { | ||
| return false | ||
| } | ||
| return props.Get("query").Exists() && | ||
| props.Get("allowed_domains").Exists() && | ||
| props.Get("blocked_domains").Exists() | ||
| } |
There was a problem hiding this comment.
The current fingerprinting logic for the function-style WebSearch tool strictly requires the existence of query, allowed_domains, and blocked_domains properties. If a future version of Claude Code updates the tool definition to modify or remove the domain filter parameters (e.g., removing blocked_domains), this mapping will silently break.
To make the fingerprinting more resilient and future-proof, we can simplify the check to only require the query property. Additionally, we should allow "function" in the type switch to support environments or middlewares that normalize custom tool types to "function".
// isClaudeCodeWebSearchFunctionTool detects the function-style WebSearch tool
// that Claude Code declares as {"name":"WebSearch","input_schema":{...}}.
// The schema is fingerprinted by its query property so unrelated custom tools
// are not misclassified as the builtin web search tool.
func isClaudeCodeWebSearchFunctionTool(tool gjson.Result) bool {
if tool.Get("name").String() != "WebSearch" {
return false
}
// Typed server tools carry an explicit type and are handled separately;
// only plain custom/function declarations reach this branch.
switch tool.Get("type").String() {
case "", "custom", "function":
default:
return false
}
props := tool.Get("input_schema.properties")
if !props.IsObject() {
return false
}
return props.Get("query").Exists()
}|
The current implementation is fully covered by tests and works as expected. Also, the request you submitted does not include any search-related content, so this PR is not needed. |
Summary
Recent Claude Code (2.1.177) stopped sending web search as a typed Anthropic server tool (
web_search_20250305/web_search_20260209). It now declares a plain function tool namedWebSearchwith aninput_schema:{ "name": "WebSearch", "description": "Search the web. Returns result blocks with titles and URLs. US-only...", "input_schema": { "type": "object", "properties": { "query": { "type": "string", "minLength": 2 }, "allowed_domains": { "type": "array", "items": { "type": "string" } }, "blocked_domains": { "type": "array", "items": { "type": "string" } } }, "required": ["query"] } }The Claude → Codex request translator (
ConvertClaudeRequestToCodex) only recognized web search bytool.type == web_search_20250305 / web_search_20260209, so the new shape fell through to the generic function-tool path. The Codex Responses request then carried{"name":"WebSearch","type":"function"}and the native{"type":"web_search"}builtin was never emitted — web search silently stopped working for the latest Claude Code against a Codex backend.Fixes #3829.
Fix
internal/translator/codex/claude/codex_claude_request.go:isClaudeWebSearchTool(tool)which returns true for the existing typed server tools or the new function-styleWebSearchtool, and use it in both the tools loop andbuildClaudeWebSearchToolNameSet(sotool_choiceforcingWebSearchalso maps to the builtin).isClaudeCodeWebSearchFunctionTool(tool)which detects the Claude Code tool byname == "WebSearch"plus a schema fingerprint (query+allowed_domains+blocked_domainsproperties). The fingerprint deliberately avoids a loose name match so unrelated custom tools — e.g. a user tool namedweb_searchwith an empty schema — stay on the normal function-calling path (covered by the existing…UsesDeclaredTypedToolNametest).The existing
convertClaudeWebSearchToolToCodexis reused unchanged; for the function-style tool there are no declaration-timeallowed_domains/user_locationvalues, so it cleanly emits{"type":"web_search"}.Reproduction & verification
Reproduced with Claude Code 2.1.177 →
gpt-5.5(Codex backend) via CLIProxyAPI.Before (current
dev) — upstream Codex request tools:{"name":"WebSearch","type":"function","strict":false} // ← no builtin web_searchThe model answered from training knowledge; no search was performed.
After (this PR) — same prompt, upstream Codex request tools:
{"type":"web_search"} // builtin injected // "WebSearch" no longer present as a function tool; Bash etc. remain functionsThe Codex response then contained real
response.web_search_call.in_progress/searching/completedevents andurl_citationannotations — i.e. the native web search actually ran end-to-end.Tests
TestConvertClaudeRequestToCodex_ClaudeCodeWebSearchFunctionToolMapping— the namedWebSearchtool maps to{"type":"web_search"}, is not forwarded as a function, and a siblingBashtool is preserved as a function.TestConvertClaudeRequestToCodex_ClaudeCodeWebSearchToolChoiceMapsToBuiltin—tool_choiceforcingWebSearchmaps to{"type":"web_search"}.web_searchcustom-tool case) still pass.go build ./...,go vet, and theinternal/translator/codex/...test suite all pass;gofmtclean.Note on
translator-path-guardThis change touches
internal/translator/**, so the advisorytranslator-path-guardcheck will fail — the same as the recently-merged #3824. That guard points contributors to file an issue for the maintenance team; this PR is paired with issue #3829, and is scoped to a single, well-tested behavioral fix for review.🤖 Generated with Claude Code