Skip to content

Grok hosted web search constraints cause a proxy 400, after which Claude Code retries without them #123

Description

@AlexanderKugel

Environment

claude-code-proxy live grok-claude instance, 127.0.0.1:18765
Claude Code 2.1.220, VS Code extension
provider: grok
model: grok-4.6
Grok client version (proxy header): 0.2.93

Problem

Claude Code can declare Anthropic hosted web search with allowed_domains, blocked_domains, or user_location. Grok's hosted web_search does not have equivalent fields.

When any of those fields is non-null, claude-code-proxy returns HTTP 400 without calling the Grok agent:

API Error: 400 Grok hosted web search does not support allowed_domains

The design is meant to stop incompatible requests from reaching Grok. Claude Code, however, receives the proxy's HTTP 400, presents that error to the model as an error tool_result, and retries the search without allowed_domains. The retry is accepted and runs as an unrestricted web search.

The hard HTTP 400 therefore adds another turn, tokens, and latency, while the search still runs without the requested restriction. The intended protection from the 400 is not achieved.


How to reproduce

Reliable replay against 127.0.0.1:18765:

curl -s -X POST 'http://127.0.0.1:18765/v1/messages?beta=true' \
  -H 'content-type: application/json' \
  -H 'anthropic-version: 2023-06-01' \
  -H 'authorization: Bearer unused' \
  -d '{
    "model": "grok-4.6",
    "max_tokens": 64,
    "stream": false,
    "messages": [{"role": "user", "content": "What are LangChain Deep Agents LangGraph 2026 tools"}],
    "tools": [{
      "type": "web_search_20250305",
      "name": "web_search",
      "max_uses": 8,
      "allowed_domains": ["langchain.com", "docs.langchain.com", "blog.langchain.com"]
    }]
  }'

Observed results

  1. The proxy rejects the hosted search containing allowed_domains. Status: 400. No upstream Grok call is made.
  2. Claude Code presents the proxy error as a tool_result for the original WebSearch tool use, with is_error: true.
  3. On the next assistant turn, the model issues two new WebSearch calls for the same topic without allowed_domains.
  4. Those calls return search results.

Expected

A site-limited or location-limited hosted search should not consume a failed round only to be retried without its constraint on the next turn.

If the provider cannot enforce the constraint, the proxy should still be able to complete the search in one round. The existing hard 400 behavior should remain available as an opt-in legacy mode rather than being the default.

When the proxy does reject a request, the error should name every unsupported setting in that request -- allowed_domains, blocked_domains, user_location, or any combination of them -- instead of reporting only the first field it finds.


Proposal

CCP_SEARCH_CONSTRAINTS=hard|soft|warning

Default: soft.

Value Behaviour
hard Preserve the current proxy 400. In the observed Claude Code flow, the model then retries without the constraint. End effect: unrestricted search in two rounds. Legacy opt-in.
soft Drop the unsupported fields, turn the constraints into instructions to the model, and send the hosted search. Best effort only. There is no guarantee that the model stays within the named sites. In testing, every listed source stayed in-domain. One round instead of two, saving tokens and latency.
warning Drop the unsupported fields, log a warning, and send the hosted search without a prompt hint. This uses the same unrestricted search configuration as the retry after hard, but avoids the failed round and saves tokens and latency.

In the observed retry path, hard and warning both lead to an unrestricted hosted-search request. warning avoids the failed round. soft is the only mode that still attempts to preserve the caller's requested sites.

The setting applies to providers that cannot enforce these Anthropic hosted-search options. Grok is the first such provider. The documentation should list the providers to which the setting applies.

max_uses remains dropped, as it is today.


Why soft is the default

Grok cannot enforce allowed_domains. With hard, Claude Code retries without domain restrictions, which does not preserve the original intent. With soft, the proxy turns each unsupported field into a direct instruction and appends it to the instructions field of the upstream Grok request. That field carries the system prompt.

This was tested 10 times with soft and domain restrictions, and 10 times without a domain list (warning / unrestricted). Both groups used the same query.

Group Runs Total [inside, outside] In-domain share
soft (directive) 10/10 [161, 0] 100.0%
open (no domains) 10/10 [147, 210] 41.2%

Every soft run listed in-domain sources only. Grok applied the instruction by adding site: filters to its own search queries. The unrestricted group returned more off-domain sources than in-domain ones.

The open group reaches 41.2% because the query names LangChain, so an unrestricted search finds the allowed domains anyway. This is why both groups must share one query.

soft remains a bias, not an enforcement mechanism. Grok can ignore the instruction. The measurement shows that it followed the instruction in all 10 runs.

Status: A fix is implemented and running in the fork AlexanderKugel/claude-code-proxy. The default is soft. Set CCP_SEARCH_CONSTRAINTS=hard to keep the current 400 behavior. A pull request to this repository will follow.

Metadata

Metadata

Assignees

No one assigned

    Labels

    No labels
    No labels

    Projects

    No projects

      Milestone

      No milestone

      Relationships

      None yet

      Development

      No branches or pull requests

      Issue actions