Skip to content

fix(translator/codex): map Claude Code WebSearch tool to Codex web_search#3837

Closed
haowang02 wants to merge 1 commit into
router-for-me:devfrom
haowang02:fix/codex-claude-code-websearch-tool-mapping
Closed

fix(translator/codex): map Claude Code WebSearch tool to Codex web_search#3837
haowang02 wants to merge 1 commit into
router-for-me:devfrom
haowang02:fix/codex-claude-code-websearch-tool-mapping

Conversation

@haowang02

Copy link
Copy Markdown
Contributor

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 named WebSearch with an input_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 by tool.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:

  • Add isClaudeWebSearchTool(tool) which returns true for the existing typed server tools or the new function-style WebSearch tool, and use it in both the tools loop and buildClaudeWebSearchToolNameSet (so tool_choice forcing WebSearch also maps to the builtin).
  • Add isClaudeCodeWebSearchFunctionTool(tool) which detects the Claude Code tool by name == "WebSearch" plus a schema fingerprint (query + allowed_domains + blocked_domains properties). The fingerprint deliberately avoids a loose name match so unrelated custom tools — e.g. a user tool named web_search with an empty schema — stay on the normal function-calling path (covered by the existing …UsesDeclaredTypedToolName test).

The existing convertClaudeWebSearchToolToCodex is reused unchanged; for the function-style tool there are no declaration-time allowed_domains / user_location values, 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_search

The 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 functions

The Codex response then contained real response.web_search_call.in_progress/searching/completed events and url_citation annotations — i.e. the native web search actually ran end-to-end.

Tests

  • TestConvertClaudeRequestToCodex_ClaudeCodeWebSearchFunctionToolMapping — the named WebSearch tool maps to {"type":"web_search"}, is not forwarded as a function, and a sibling Bash tool is preserved as a function.
  • TestConvertClaudeRequestToCodex_ClaudeCodeWebSearchToolChoiceMapsToBuiltintool_choice forcing WebSearch maps to {"type":"web_search"}.
  • Existing web-search tests (typed mapping + the lowercase web_search custom-tool case) still pass.

go build ./..., go vet, and the internal/translator/codex/... test suite all pass; gofmt clean.

Note on translator-path-guard

This change touches internal/translator/**, so the advisory translator-path-guard check 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

…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>
Copilot AI review requested due to automatic review settings June 14, 2026 06:44

Copilot AI left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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 WebSearch tool declarations (Claude Code 2.1.177+) and map them to Codex builtin web_search.
  • Extend tool-choice mapping to treat function-style WebSearch as builtin web search.
  • Add tests covering tool mapping and tool_choice mapping 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.

Comment on lines +376 to +394
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()
}

@gemini-code-assist gemini-code-assist Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

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.

Comment on lines +371 to +394
// 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()
}

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

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

medium

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()
}

@luispater

Copy link
Copy Markdown
Collaborator

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.

@luispater luispater closed this Jun 20, 2026
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

3 participants