fix Grok web search: 400s on Claude Code's tool declarations, unrenderable hosted-search blocks, and seized tools - #112
Merged
raine merged 2 commits intoAug 14, 2026
Conversation
…rendered All five were found by running Claude Code against the proxy -- VS Code and CLI -- not by reading the code. Each is reproduced against the released v0.1.34 binary, so all five are fixes rather than regressions. raine#107 is probably not Grok-specific: the VS Code webview's render chain has no branch for these block types, which is provider-blind. A captured Codex session emits the same block types, so the same error lines look likely there too. Grok reaches those block types on every hosted search, which is why Grok sessions hit it constantly. This commit closes the Grok half only. Listed in the order they were diagnosed and fixed. raine#107 -- hosted-search blocks were unrenderable in the VS Code extension Description: A hosted search was reported as server_tool_use followed by web_search_tool_result or x_search_tool_result. The Claude Code VS Code webview renders text, thinking, redacted_thinking, tool_use, tool_result, image, document and fallback, and falls through to a catch-all for anything else. Neither hosted block type is in that set. Observation: Two error lines printed per search, in the middle of the answer: Unsupported content type: server_tool_use Unsupported content type: web_search_tool_result The search itself succeeded; only the rendering failed. Fix: Report a hosted search as a text block naming the query. Text renders in every Anthropic client. CCP_GROK_SEARCH_BLOCKS=native restores the previous block types for a client that draws them. The streaming path emits an empty second text block so the block index sequence stays contiguous; the accumulating path builds an array through a position map and needs no such filler. Tests (6 new): accumulated_hosted_search_renders_as_text_by_default accumulated_hosted_search_keeps_the_native_shape_on_request text_search_blocks_keep_the_block_index_sequence_contiguous names_map_to_readable_labels an_unknown_hosted_tool_keeps_its_own_name an_empty_query_drops_the_colon Tests (2 pre-existing, extended to assert both block shapes): stream_translates_hosted_web_search_and_citations stream_translates_hosted_x_search_usage_and_citations raine#108 -- the max_uses field on a caller's search tool failed the request Description: parse_tools whitelists five tool fields and bails on any other. Claude Code caps its own WebSearch with a max_uses field, which is not in that list, so the whole request was rejected before it reached xAI. Observation: API Error: 400 unsupported tool field: max_uses The request is rejected, so the turn produces no reply. Fix: Accept max_uses and drop it. The cap is enforced by the caller that declared the tool, so there is nothing to forward. Every other unknown field is still rejected. Tests (2 new): grok_translation_accepts_web_search_max_uses_without_forwarding_it grok_translation_still_rejects_an_unknown_tool_field raine#109 -- Anthropic's server web_search declaration failed the request Description: Claude Code declares WebSearch as an ordinary function until the model actually searches. It then re-issues the turn with Anthropic's server-tool form, captured from Claude Code 2.1.220 as: {"type":"web_search_20250305","name":"web_search","max_uses":8} parse_tools rejected the type field, and the declaration carries no input_schema so it could not take the function path either. Observation: API Error: 400 unsupported tool field: type The same error repeats on every search attempt. The session then falls back to WebFetch. A traffic capture shows four such requests rejected by the proxy, with no upstream call made. Fix: Accept the declaration and map it to xAI's hosted web_search -- the mapping docs/providers/grok.md already advertised. Any other server-tool type is still rejected, now by type rather than by field. Cleaning up raine#109: the match is exact on both type and name, so a future version, or another tool that merely carries such a type, does not become a hosted search by accident. Tests (3 new): grok_translation_maps_the_anthropic_server_web_search_declaration grok_translation_rejects_an_unknown_server_tool_type grok_translation_rejects_a_server_tool_type_it_does_not_know_exactly raine#110 -- searches ran on turns that asked for nothing Description: translate_request pushed xAI's hosted x_search onto every request, including one that declared no tools at all, and appended two directives to the caller's system prompt telling the model to prefer the hosted tools. Every turn therefore arrived at xAI carrying a standing search tool and an instruction to use it. Observation: A bare "hi", sent with no system prompt and no tools, ran an x_search for the word "hi" in 3 of 25 identical requests against v0.1.34. Zero results, one billed search request, and two content blocks from raine#107. The model's own reasoning block in those runs identified the message as a greeting. input_tokens was 3708 for a two-character message with no system prompt. Fix: See raine#111 -- one gate covers both. raine#111 -- asking for a search deleted the caller's other tools Description: A turn whose text matched one of fourteen hardcoded substrings had its whole tools array replaced with a single hosted tool and tool_choice pinned to required. Nine substrings route to x_search, five to web_search. Matching is text.contains on the lowercased message, so "unix search for files" matches "x search" and triggers it too. Observation: With Bash and WebSearch declared, "hi, do pwd in the bash" runs tool_use Bash {"command": "pwd"}. Adding "also search the web for cat pictures" removes Bash from the request, so pwd never runs and only the search half of the message is answered. Reproduced 5 times out of 5. Fix (raine#110 and raine#111): Gate the whole behaviour behind CCP_GROK_HOSTED_SEARCH, off by default. On the default path the caller keeps every tool it declared, its system prompt is untouched, and tool_choice stays whatever it sent. xAI's x_search has no client-side equivalent, so an X-intent turn is offered it as one additional tool -- never forced, never in place of anything else. The opt-in path is preserved unchanged for callers who want xAI's own search and citations. Tests (8 new, covering both): grok_translation_offers_x_search_without_forcing_it grok_translation_offers_no_x_search_without_x_intent grok_translation_keeps_client_web_search_tool_by_default grok_translation_keeps_dedicated_xsearch_as_a_function_by_default grok_translation_attaches_no_search_tool_to_a_greeting_by_default grok_translation_omits_x_search_guidance_without_x_intent grok_translation_appends_x_search_guidance_on_x_intent grok_translation_appends_x_search_guidance_for_dedicated_x_tool Tests (3 pre-existing, re-pointed at the opt-in path so it stays covered): grok_translation_maps_claude_web_search_to_hosted_web_search grok_translation_maps_x_intent_to_required_hosted_x_search grok_translation_maps_dedicated_xsearch_with_domain_schema SETTINGS ADDED CCP_GROK_HOSTED_SEARCH=1 restore hosted tools replacing the caller's search tools, with forcing. Default off. CCP_GROK_SEARCH_BLOCKS=native restore server_tool_use and *_tool_result blocks. Default text. The two are independent, so hosted search can now be forced and still render, which was not previously possible. Both are documented in docs/providers/grok.md and docs/reference/configuration.md. That provider page described the old search behaviour as current, so it is corrected in the same commit. CHANGELOG.md is untouched; upstream edits it only in release commits. VERIFICATION cargo fmt --check, clippy -D warnings, build --all, test --all: all pass. Test count 962 before, 981 after: 19 new, none removed, none ignored. Per issue: 6 + 2 + 3 + 8 = 19, the last group covering raine#110 and raine#111. Five pre-existing tests were also modified, three re-pointed at the opt-in path and two extended to cover both shapes. Live end to end against Grok through a real Claude Code session: the turn searches, both server-tool requests reach xAI carrying the hosted tool, the answer comes back sourced, no 400s, and no block type the webview cannot draw appears in the downstream response.
Prevent broad substring matching from offering x_search for Unix and Linux phrases. Named Anthropic hosted-search choices select only the corresponding upstream hosted tool and use the required policy, preserving the caller's forced-choice semantics on an endpoint that rejects hosted choice objects. Validate versioned tool types and max_uses values before translation. Accept null optional search constraints, reject non-null domain and location constraints explicitly, and keep max_uses limited to supported search forms. The Grok CLI endpoint cannot enforce these Anthropic options, so the provider and compatibility docs describe the boundary rather than silently weakening caller constraints. Rewrite hosted-search comments and documentation around durable policy and protocol properties. Add regression coverage for word boundaries, named hosted choices, optional constraints, and malformed search fields.
Owner
|
Thanks |
bp0lr
pushed a commit
to bp0lr/claude-code-proxy
that referenced
this pull request
Aug 29, 2026
…rable hosted-search blocks, and seized tools (raine#112) * fix Grok web search: five defects that broke it, seized tools, or misrendered All five were found by running Claude Code against the proxy -- VS Code and CLI -- not by reading the code. Each is reproduced against the released v0.1.34 binary, so all five are fixes rather than regressions. has no branch for these block types, which is provider-blind. A captured Codex session emits the same block types, so the same error lines look likely there too. Grok reaches those block types on every hosted search, which is why Grok sessions hit it constantly. This commit closes the Grok half only. Listed in the order they were diagnosed and fixed. Description: A hosted search was reported as server_tool_use followed by web_search_tool_result or x_search_tool_result. The Claude Code VS Code webview renders text, thinking, redacted_thinking, tool_use, tool_result, image, document and fallback, and falls through to a catch-all for anything else. Neither hosted block type is in that set. Observation: Two error lines printed per search, in the middle of the answer: Unsupported content type: server_tool_use Unsupported content type: web_search_tool_result The search itself succeeded; only the rendering failed. Fix: Report a hosted search as a text block naming the query. Text renders in every Anthropic client. CCP_GROK_SEARCH_BLOCKS=native restores the previous block types for a client that draws them. The streaming path emits an empty second text block so the block index sequence stays contiguous; the accumulating path builds an array through a position map and needs no such filler. Tests (6 new): accumulated_hosted_search_renders_as_text_by_default accumulated_hosted_search_keeps_the_native_shape_on_request text_search_blocks_keep_the_block_index_sequence_contiguous names_map_to_readable_labels an_unknown_hosted_tool_keeps_its_own_name an_empty_query_drops_the_colon Tests (2 pre-existing, extended to assert both block shapes): stream_translates_hosted_web_search_and_citations stream_translates_hosted_x_search_usage_and_citations Description: parse_tools whitelists five tool fields and bails on any other. Claude Code caps its own WebSearch with a max_uses field, which is not in that list, so the whole request was rejected before it reached xAI. Observation: API Error: 400 unsupported tool field: max_uses The request is rejected, so the turn produces no reply. Fix: Accept max_uses and drop it. The cap is enforced by the caller that declared the tool, so there is nothing to forward. Every other unknown field is still rejected. Tests (2 new): grok_translation_accepts_web_search_max_uses_without_forwarding_it grok_translation_still_rejects_an_unknown_tool_field Description: Claude Code declares WebSearch as an ordinary function until the model actually searches. It then re-issues the turn with Anthropic's server-tool form, captured from Claude Code 2.1.220 as: {"type":"web_search_20250305","name":"web_search","max_uses":8} parse_tools rejected the type field, and the declaration carries no input_schema so it could not take the function path either. Observation: API Error: 400 unsupported tool field: type The same error repeats on every search attempt. The session then falls back to WebFetch. A traffic capture shows four such requests rejected by the proxy, with no upstream call made. Fix: Accept the declaration and map it to xAI's hosted web_search -- the mapping docs/providers/grok.md already advertised. Any other server-tool type is still rejected, now by type rather than by field. Cleaning up raine#109: the match is exact on both type and name, so a future version, or another tool that merely carries such a type, does not become a hosted search by accident. Tests (3 new): grok_translation_maps_the_anthropic_server_web_search_declaration grok_translation_rejects_an_unknown_server_tool_type grok_translation_rejects_a_server_tool_type_it_does_not_know_exactly Description: translate_request pushed xAI's hosted x_search onto every request, including one that declared no tools at all, and appended two directives to the caller's system prompt telling the model to prefer the hosted tools. Every turn therefore arrived at xAI carrying a standing search tool and an instruction to use it. Observation: A bare "hi", sent with no system prompt and no tools, ran an x_search for the word "hi" in 3 of 25 identical requests against v0.1.34. Zero results, one billed search request, and two content blocks from raine#107. The model's own reasoning block in those runs identified the message as a greeting. input_tokens was 3708 for a two-character message with no system prompt. Fix: See raine#111 -- one gate covers both. Description: A turn whose text matched one of fourteen hardcoded substrings had its whole tools array replaced with a single hosted tool and tool_choice pinned to required. Nine substrings route to x_search, five to web_search. Matching is text.contains on the lowercased message, so "unix search for files" matches "x search" and triggers it too. Observation: With Bash and WebSearch declared, "hi, do pwd in the bash" runs tool_use Bash {"command": "pwd"}. Adding "also search the web for cat pictures" removes Bash from the request, so pwd never runs and only the search half of the message is answered. Reproduced 5 times out of 5. Fix (raine#110 and raine#111): Gate the whole behaviour behind CCP_GROK_HOSTED_SEARCH, off by default. On the default path the caller keeps every tool it declared, its system prompt is untouched, and tool_choice stays whatever it sent. xAI's x_search has no client-side equivalent, so an X-intent turn is offered it as one additional tool -- never forced, never in place of anything else. The opt-in path is preserved unchanged for callers who want xAI's own search and citations. Tests (8 new, covering both): grok_translation_offers_x_search_without_forcing_it grok_translation_offers_no_x_search_without_x_intent grok_translation_keeps_client_web_search_tool_by_default grok_translation_keeps_dedicated_xsearch_as_a_function_by_default grok_translation_attaches_no_search_tool_to_a_greeting_by_default grok_translation_omits_x_search_guidance_without_x_intent grok_translation_appends_x_search_guidance_on_x_intent grok_translation_appends_x_search_guidance_for_dedicated_x_tool Tests (3 pre-existing, re-pointed at the opt-in path so it stays covered): grok_translation_maps_claude_web_search_to_hosted_web_search grok_translation_maps_x_intent_to_required_hosted_x_search grok_translation_maps_dedicated_xsearch_with_domain_schema SETTINGS ADDED CCP_GROK_HOSTED_SEARCH=1 restore hosted tools replacing the caller's search tools, with forcing. Default off. CCP_GROK_SEARCH_BLOCKS=native restore server_tool_use and *_tool_result blocks. Default text. The two are independent, so hosted search can now be forced and still render, which was not previously possible. Both are documented in docs/providers/grok.md and docs/reference/configuration.md. That provider page described the old search behaviour as current, so it is corrected in the same commit. CHANGELOG.md is untouched; upstream edits it only in release commits. VERIFICATION cargo fmt --check, clippy -D warnings, build --all, test --all: all pass. Test count 962 before, 981 after: 19 new, none removed, none ignored. Per issue: 6 + 2 + 3 + 8 = 19, the last group covering raine#110 and raine#111. Five pre-existing tests were also modified, three re-pointed at the opt-in path and two extended to cover both shapes. Live end to end against Grok through a real Claude Code session: the turn searches, both server-tool requests reach xAI carrying the hosted tool, the answer comes back sourced, no 400s, and no block type the webview cannot draw appears in the downstream response. * harden Grok hosted search translation Prevent broad substring matching from offering x_search for Unix and Linux phrases. Named Anthropic hosted-search choices select only the corresponding upstream hosted tool and use the required policy, preserving the caller's forced-choice semantics on an endpoint that rejects hosted choice objects. Validate versioned tool types and max_uses values before translation. Accept null optional search constraints, reject non-null domain and location constraints explicitly, and keep max_uses limited to supported search forms. The Grok CLI endpoint cannot enforce these Anthropic options, so the provider and compatibility docs describe the boundary rather than silently weakening caller constraints. Rewrite hosted-search comments and documentation around durable policy and protocol properties. Add regression coverage for word boundaries, named hosted choices, optional constraints, and malformed search fields. --------- Co-authored-by: Raine Virta <raine.virta@gmail.com>
This file contains hidden or bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Sign up for free
to join this conversation on GitHub.
Already have an account?
Sign in to comment
Add this suggestion to a batch that can be applied as a single commit.This suggestion is invalid because no changes were made to the code.Suggestions cannot be applied while the pull request is closed.Suggestions cannot be applied while viewing a subset of changes.Only one suggestion per line can be applied in a batch.Add this suggestion to a batch that can be applied as a single commit.Applying suggestions on deleted lines is not supported.You must change the existing code in this line in order to create a valid suggestion.Outdated suggestions cannot be applied.This suggestion has been applied or marked resolved.Suggestions cannot be applied from pending reviews.Suggestions cannot be applied on multi-line comments.Suggestions cannot be applied while the pull request is queued to merge.Suggestion cannot be applied right now. Please check back later.
Fixes #107
Fixes #108
Fixes #109
Fixes #110
Fixes #111
Five defects on the Grok provider, all found by running Claude Code against
the proxy and capturing the traffic. Two of them stop web search working at
all. One commit, 981 tests, docs updated.
1. Hosted-search blocks no client draws (#107)
Symptom.
Unsupported content type: server_tool_useandUnsupported content type: web_search_tool_result, two lines per search.Cause. The proxy reports a hosted search as
server_tool_useplusweb_search_tool_result, or anx_search_tool_resultthat is not an Anthropictype at all. No Anthropic client I can find draws these today.
Fix. Report a hosted search as a text block naming the query. Every client
draws text.
CCP_GROK_SEARCH_BLOCKS=nativekeeps the previous block types fora client that does render them.
2.
max_usesfailed the request (#108)Symptom.
400 unsupported tool field: max_uses, no search runs.Cause. Claude Code caps its own
WebSearchwithmax_uses.parse_toolswhitelists five fields and bails on anything else.Fix. Accept it and drop it. That matches the position already documented
for Codex in
reference/compatibility-and-limitations.md-- the Anthropic capis not enforced because the provider exposes no equivalent.
3. Anthropic's server
web_searchdeclaration failed the request (#109)Symptom.
400 unsupported tool field: type, on every search attempt.Cause. Claude Code escalates to
{"type":"web_search_20250305", "name":"web_search","max_uses":8}when the model searches. The declarationcarries no
input_schema, so it could not take the function path either.Fix. Accept it and map it to Grok hosted
web_search-- the mappingproviders/grok.mdalready advertised. The match is exact on both type andname, so a future version, or another tool carrying such a type, is still
rejected.
4. Searches on turns that asked for nothing (#110)
Symptom. A bare
hi, sent with no tools and no system prompt, sometimesanswers with an
x_searchfor the word "hi". Three times in 25 identicalrequests against v0.1.34; zero results, one billed search request, and two
content blocks from #107.
Cause.
x_searchwas pushed onto every request, including one thatdeclared no tools, and two directives were appended to the caller's system
prompt telling the model to use the hosted tools.
Fix. Gated behind
CCP_GROK_HOSTED_SEARCH, off by default. By default thecaller's tools and system prompt are left alone. An X-intent turn is still
offered
x_searchas one additional tool, never in place of anything else, soxAI's index stays reachable -- Claude Code has no X tool of its own.
5. Asking for a search deleted the caller's other tools (#111)
Symptom. With
BashandWebSearchdeclared,hi, do pwd in the bashruns
pwd. Addalso search the web for cat picturesandBashis gone fromthe request, so
pwdnever runs. Reproduced five times out of five.Cause. A turn matching one of fourteen substrings had its whole
toolsarray replaced with a single hosted tool and
tool_choice: required. Ninesubstrings route to
x_search, five toweb_search. Matching istext.contains, so "unix search for files" triggers it too.Fix. Same gate as #110. On the default path nothing is removed from
the tool list and
tool_choicestays whatever the caller sent.#110 and #111 are the changes here that alter behaviour you chose
deliberately, so they are the ones to push back on if any. Commits 259f2cc and
7ad36f7 say the forcing exists so shell tools cannot become a search fallback,
which is a real problem -- I have kept the whole mechanism intact behind
CCP_GROK_HOSTED_SEARCH=1, which restores the old behaviour exactly.Verification
cargo fmt --check,clippy -D warnings,build --all,test --allallpass. 962 tests before, 981 after: 19 new, none removed, none ignored.
Verified end to end against live Grok, on this commit:
web_searchdeclaration carryingmax_usesreturns 200.The search runs, and the response contains no
server_tool_useand noweb_search_tool_result-- the queries arrive as text.hi, do pwd in the bash. also search the web for cat pictures, withBashand
WebSearchdeclared, calls both:Bashforpwdand the caller's ownWebSearchfor the search. Both halves of the message are answered.hiwith no tools answers the greeting and runs no search.Every defect above was reproduced against the released v0.1.34 binary first,
so these are fixes rather than regressions.
providers/grok.mdandreference/configuration.mdare updated -- thatprovider page described the old search behaviour as current.
CHANGELOG.mdisuntouched.
Happy to split this into separate PRs if you'd prefer.