Skip to content

fix Grok web search: 400s on Claude Code's tool declarations, unrenderable hosted-search blocks, and seized tools - #112

Merged
raine merged 2 commits into
raine:mainfrom
AlexanderKugel:fix/grok-hosted-search-rendering
Aug 14, 2026
Merged

fix Grok web search: 400s on Claude Code's tool declarations, unrenderable hosted-search blocks, and seized tools#112
raine merged 2 commits into
raine:mainfrom
AlexanderKugel:fix/grok-hosted-search-rendering

Conversation

@AlexanderKugel

@AlexanderKugel AlexanderKugel commented Aug 14, 2026

Copy link
Copy Markdown
Contributor

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_use and
Unsupported content type: web_search_tool_result, two lines per search.

Cause. The proxy reports a hosted search as server_tool_use plus
web_search_tool_result, or an x_search_tool_result that is not an Anthropic
type 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=native keeps the previous block types for
a client that does render them.

2. max_uses failed the request (#108)

Symptom. 400 unsupported tool field: max_uses, no search runs.

Cause. Claude Code caps its own WebSearch with max_uses.
parse_tools whitelists 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 cap
is not enforced because the provider exposes no equivalent.

3. Anthropic's server web_search declaration 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 declaration
carries no input_schema, so it could not take the function path either.

Fix. Accept it and map it to Grok hosted web_search -- the mapping
providers/grok.md already advertised. The match is exact on both type and
name, 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, sometimes
answers with an x_search for the word "hi". Three times in 25 identical
requests against v0.1.34; zero results, one billed search request, and two
content blocks from #107.

Cause. x_search was pushed onto every request, including one that
declared 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 the
caller's tools and system prompt are left alone. An X-intent turn is still
offered x_search as one additional tool, never in place of anything else, so
xAI'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 Bash and WebSearch declared, hi, do pwd in the bash
runs pwd. Add also search the web for cat pictures and Bash is gone from
the request, so pwd never runs. Reproduced five times out of five.

Cause. A turn matching one of fourteen substrings had its whole tools
array replaced with a single hosted tool and tool_choice: required. Nine
substrings route to x_search, five to web_search. Matching is
text.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_choice stays 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 --all all
pass. 962 tests before, 981 after: 19 new, none removed, none ignored.

Verified end to end against live Grok, on this commit:

  • Anthropic's server web_search declaration carrying max_uses returns 200.
    The search runs, and the response contains no server_tool_use and no
    web_search_tool_result -- the queries arrive as text.
  • hi, do pwd in the bash. also search the web for cat pictures, with Bash
    and WebSearch declared, calls both: Bash for pwd and the caller's own
    WebSearch for the search. Both halves of the message are answered.
  • A bare hi with 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.md and reference/configuration.md are updated -- that
provider page described the old search behaviour as current. CHANGELOG.md is
untouched.

Happy to split this into separate PRs if you'd prefer.

AlexanderKugel and others added 2 commits August 14, 2026 02:29
…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.
@raine
raine merged commit e60cf00 into raine:main Aug 14, 2026
@raine

raine commented Aug 14, 2026

Copy link
Copy Markdown
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>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment