Skip to content
Latentism

Fix Claude Code 400 Errors Behind LiteLLM and LLM Gateways

Why Claude Code returns 400 errors through a custom ANTHROPIC_BASE_URL or LiteLLM, what it actually sends, and three fixes that hold up across releases.

13 min read

Top-to-bottom flow: Claude Code, configured with ANTHROPIC_BASE_URL, sends 11 beta values and body fields to an LLM gateway such as LiteLLM, which forwards, filters or translates them for an upstream such as Anthropic, Bedrock or Vertex AI; an unknown value comes back as a 400

Claude Code returns a 400 through a custom ANTHROPIC_BASE_URL when something between it and the model rejects a request field Claude Code sends to the Anthropic API. Usually that’s an anthropic-beta value the upstream doesn’t recognise, or a body field such as context_management that pairs with one. The quickest fix is to set CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 in the env block of your Claude Code settings. The durable fixes are a gateway that filters betas per upstream provider (current LiteLLM does) or connecting Claude Code in the provider’s own format.

This guide shows what Claude Code 2.1.291 actually sends, what LiteLLM 1.104.0 forwards on each route, and a LiteLLM setup that works. Everything below was captured by running both against mock upstreams, so you can rerun it yourself.

Match your error message to the fix

Find your error text in the left column. Versions are Claude Code versions.

Error text Cause Fix
Unexpected value(s) `<beta>` for the `anthropic-beta` header The upstream doesn’t know a beta value Claude Code sent CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1, or a gateway that filters betas for that upstream
Extra inputs are not permitted, naming context_management, output_config or a tool field A body field reached an upstream that doesn’t accept it, usually Bedrock or Vertex AI behind an Anthropic-format gateway Same variable, or the provider-format route
Input tag 'adaptive' found or another error naming thinking The upstream model build doesn’t accept adaptive thinking Upgrade the upstream. On Opus 4.6 and Sonnet 4.6 only, CLAUDE_CODE_DISABLE_ADAPTIVE_THINKING=1
Input tag 'advisor_20260301' on every request, on 2.1.275 An advisor tool entry sent during a gradual rollout Update to 2.1.276 or later, or set CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1
A tool schema pattern rejected on every request, on 2.1.265 to 2.1.267 A regular expression in the Artifact tool schema that some gateways can’t parse Update to 2.1.268 or later, or turn artifacts off
ContextWindowExceededError or a token limit in the gateway’s own words The gateway enforces a smaller context than the model’s window Run /compact, then set CLAUDE_CODE_AUTO_COMPACT_WINDOW to the gateway’s limit
401 The credential is in a header the gateway doesn’t read Swap ANTHROPIC_AUTH_TOKEN (sends Authorization: Bearer) for ANTHROPIC_API_KEY (sends x-api-key), or the other way round

Sources: Anthropic’s gateway troubleshooting table and gateway compatibility guide, October 2026.

Two real reports show the first row in the wild. In anthropics/claude-code#70563, Claude Code 2.1.187 behind a proxy failed every request with:

API Error: 400 Unexpected value(s) `advisor-tool-2026-03-01` for the `anthropic-beta` header. Please consult our documentation at docs.claude.com or try again without the header.

In BerriAI/litellm#19984, LiteLLM v1.81.0 forwarded Claude Code 2.1.23’s prompt-caching-scope-2026-01-05 beta to Vertex AI, which rejected it in the same way. That issue was closed with a LiteLLM fix that filters beta values per provider.

Why Claude Code sends values your gateway rejects

When you set ANTHROPIC_BASE_URL, Claude Code treats the address as the Anthropic API. It can’t tell what sits behind it, so it sends everything it would send to api.anthropic.com: every current anthropic-beta value, and the request body fields that go with them.

Three things make that fragile:

  • The set changes with every release. New capabilities arrive as new beta values and body fields. A gateway that allowlists the values it saw last month strips next month’s, and Anthropic’s own compatibility guide says to forward them as open lists.
  • Headers and body fields travel in pairs. Context management is a beta value plus a context_management body field. Strip the header but forward the body, and the upstream rejects the body field. That’s why “just remove the anthropic-beta header in the proxy” turns one 400 into another.
  • Cloud providers accept a smaller set. Amazon Bedrock and Google’s Vertex AI support fewer betas than the Anthropic API. An Anthropic-format gateway that forwards to them has to bridge the difference, and older gateway versions didn’t.

What Claude Code actually sends

To see exactly what your Claude Code version sends, point it at a server that prints each request. This one uses only the Python standard library and answers every request with a 400, so Claude Code shows an error after it prints:

"""Print what Claude Code sends to a gateway: beta values and body fields.

Run:   python3 beta_dump.py
Then:  ANTHROPIC_BASE_URL=http://127.0.0.1:8787 ANTHROPIC_AUTH_TOKEN=dummy \
       claude -p "hi" < /dev/null
Claude Code reports an error, because this server answers every request with 400.
"""
import json
from http.server import BaseHTTPRequestHandler, HTTPServer


class Dump(BaseHTTPRequestHandler):
    def do_HEAD(self):  # connection-warming probe
        self.send_response(200)
        self.end_headers()

    def do_POST(self):
        body = json.loads(self.rfile.read(int(self.headers["content-length"])) or b"{}")
        betas = (self.headers.get("anthropic-beta") or "").split(",")
        print(f"\n{self.command} {self.path}  model={body.get('model')}")
        print(f"anthropic-beta ({len(betas)}):")
        for beta in betas:
            print(f"  {beta}")
        print("body fields:", ", ".join(sorted(body)))
        self.send_response(400)
        self.send_header("content-type", "application/json")
        self.end_headers()
        self.wfile.write(b'{"type":"error","error":{"type":"invalid_request_error",'
                         b'"message":"beta_dump: request printed"}}')

    def log_message(self, *args):
        pass


HTTPServer(("127.0.0.1", 8787), Dump).serve_forever()

Claude Code 2.1.291 on Linux, with default settings, printed:

POST /v1/messages?beta=true  model=claude-opus-5-5
anthropic-beta (11):
  claude-code-20250219
  interleaved-thinking-2025-05-14
  thinking-token-count-2026-05-13
  context-management-2025-06-27
  prompt-caching-scope-2026-01-05
  mid-conversation-system-2026-04-07
  per-turn-control-2026-07-01
  mid-conversation-tool-changes-2026-07-01
  effort-2025-11-24
  dangerous-tool-use-2026-09-03
  afk-mode-2026-01-31
body fields: context_management, max_tokens, messages, metadata, model, output_config, safeguards, stream, system, thinking, tools

After the 400, Claude Code retried once without dangerous-tool-use-2026-09-03 and the safeguards field, then gave up. With CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1, the same command printed:

POST /v1/messages?beta=true  model=claude-opus-5-5
anthropic-beta (4):
  claude-code-20250219
  interleaved-thinking-2025-05-14
  mid-conversation-system-2026-04-07
  effort-2025-11-24
body fields: max_tokens, messages, metadata, model, output_config, stream, system, thinking, tools

Your version will print a different list, which is the point: run it after each Claude Code upgrade if your gateway is strict.

Bar chart of how many anthropic-beta values reach the upstream, captured from Claude Code 2.1.291 and LiteLLM 1.104.0: Claude Code direct with defaults 11, direct with experimental betas off 4, LiteLLM to Anthropic 11, LiteLLM to Bedrock 3, LiteLLM to Bedrock with betas off 1

The full capture, setup by setup. “Betas off” means CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1, and the Bedrock rows show the first attempt, before Claude Code retried:

  • Direct, default (11): all 11 values listed above, with the context_management, output_config and safeguards body fields.
  • Direct, betas off (4): claude-code-20250219, interleaved-thinking-2025-05-14, mid-conversation-system-2026-04-07 and effort-2025-11-24, with output_config.
  • LiteLLM to Anthropic (11): identical to direct, unchanged.
  • LiteLLM to Bedrock (3): context-management-2025-06-27, dangerous-tool-use-2026-09-03 and effort-2025-11-24, with safeguards. LiteLLM removed context_management and output_config.
  • LiteLLM to Bedrock, betas off (1): effort-2025-11-24 only, with none of the three body fields.

Fix 1: turn off experimental betas

CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 makes Claude Code stop sending pre-release capabilities and the body fields that pair with them. It’s the fastest fix, and it works with any gateway.

Set it in the env block of ~/.claude/settings.json, next to your gateway variables:

{
  "env": {
    "ANTHROPIC_BASE_URL": "https://llm-gateway.example.com",
    "ANTHROPIC_AUTH_TOKEN": "sk-your-gateway-key",
    "CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS": "1"
  }
}

A few details trip people up:

  • It’s an environment variable, not a setting. The reporter in #70563 tried "disableExperimentalBetas": true at the top level of settings.json. That string doesn’t appear anywhere in the Claude Code 2.1.291 binary, so the key does nothing. Put the variable in the env block or export it in your shell.
  • The settings file wins over your shell. When both set the same variable, the settings-file value applies. Run /status to check which base URL and credential Claude Code is using.
  • The VS Code extension reads its own setting. Put the variables in claudeCode.environmentVariables in VS Code’s user settings, as the connect guide shows.
  • Never put the credential in a project’s .claude/settings.json. That file is committed with the repository. Use ~/.claude/settings.json or .claude/settings.local.json.

What you give up, according to Anthropic’s list of what the variable removes: context management, beta tool fields such as defer_loading, structured-output and task-budget fields, and MCP tool search, so every MCP tool loads up front. It keeps extended context, interleaved thinking, effort and adaptive thinking.

It also doesn’t gate every beta. The advisor tool problem on 2.1.275 needed its own variable, CLAUDE_CODE_DISABLE_ADVISOR_TOOL=1. If the variable doesn’t clear your error, rerun beta_dump.py with it set and look for the value your upstream rejects.

Fix 2: a LiteLLM setup that works with Claude Code

If you run LiteLLM as the gateway, upgrade it first, then set it up so each route gets what it can handle. These steps were tested with LiteLLM 1.104.0 and Claude Code 2.1.291.

1. Install and configure LiteLLM

python3 -m venv litellm-venv
./litellm-venv/bin/pip install 'litellm[proxy]'

Each model_name is the name Claude Code asks for; litellm_params.model is where LiteLLM sends it. Save as litellm.yaml:

model_list:
  # Claude on the Anthropic API: beta values pass through unchanged
  - model_name: claude-sonnet-5-5
    litellm_params:
      model: anthropic/claude-sonnet-5-5
      api_key: os.environ/ANTHROPIC_API_KEY

  # Claude on Amazon Bedrock: beta values are filtered to what Bedrock accepts
  - model_name: claude-bedrock
    litellm_params:
      model: bedrock/invoke/us.anthropic.claude-sonnet-4-5-20250929-v1:0
      aws_region_name: us-east-1

general_settings:
  master_key: os.environ/LITELLM_MASTER_KEY

2. Start it

export ANTHROPIC_API_KEY=sk-ant-...        # your Anthropic key
export LITELLM_MASTER_KEY=sk-litellm-...   # the key Claude Code will present
./litellm-venv/bin/litellm --config litellm.yaml --port 4000

Bedrock routes also need AWS credentials in the environment, as for any boto-based client.

3. Point Claude Code at it

{
  "env": {
    "ANTHROPIC_BASE_URL": "http://localhost:4000",
    "ANTHROPIC_AUTH_TOKEN": "sk-litellm-...",
    "ANTHROPIC_MODEL": "claude-sonnet-5-5"
  }
}

ANTHROPIC_AUTH_TOKEN sends the key as Authorization: Bearer, which LiteLLM accepts for its master key and virtual keys. To change which LiteLLM model the opus, sonnet and haiku aliases resolve to, set ANTHROPIC_DEFAULT_OPUS_MODEL, ANTHROPIC_DEFAULT_SONNET_MODEL and ANTHROPIC_DEFAULT_HAIKU_MODEL to model_name values from your config.

4. Verify before you open Claude Code

This is Anthropic’s one-token check, pointed at the LiteLLM model name:

curl -sS -w '\n%{http_code}\n' -X POST "$ANTHROPIC_BASE_URL/v1/messages" \
  -H "Authorization: Bearer $ANTHROPIC_AUTH_TOKEN" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{"model": "claude-sonnet-5-5", "max_tokens": 1, "messages": [{"role": "user", "content": "."}]}'

A JSON body starting {"id":"msg_ means the route works. Then start claude, send a message and run /status: the Anthropic base URL line should show http://localhost:4000.

What LiteLLM does to Claude Code’s requests

The list above shows the captures. Per route, LiteLLM 1.104.0 did this:

  • To the Anthropic API: forwarded all 11 beta values and every body field unchanged. That’s what you want: the Anthropic API accepts everything Claude Code sends.
  • To Bedrock: cut the beta values from 11 to 3, removed the output_config and context_management body fields, converted thinking: {"type": "adaptive"} to {"type": "enabled", "budget_tokens": 4096}, and sent the betas both as a header and as the anthropic_beta body field Bedrock uses.

The filtering comes from a per-provider allowlist, anthropic_beta_headers_config.json. Values the list doesn’t know are dropped, unsupported ones are dropped, and a few are renamed: advanced-tool-use-2025-11-20 becomes tool-search-tool-2025-10-19 for Bedrock and Vertex AI.

Fix 3: connect in the provider’s own format

If your gateway forwards to Bedrock or Vertex AI, the cleanest fix is to stop sending it Anthropic-format requests at all. When Claude Code is configured for Bedrock or Vertex AI, it sends only the beta values and fields that provider accepts, and uses the provider’s model IDs. The gateway then has nothing to bridge.

This needs a gateway that exposes a Bedrock- or Vertex-format endpoint; check your gateway’s documentation for one. For a Bedrock-format gateway, Anthropic’s connect guide uses:

export CLAUDE_CODE_USE_BEDROCK=1
export ANTHROPIC_BEDROCK_BASE_URL=https://llm-gateway.example.com/bedrock
export CLAUDE_CODE_SKIP_BEDROCK_AUTH=1        # the gateway holds the AWS credentials
export ANTHROPIC_AUTH_TOKEN=sk-your-gateway-key

The Vertex AI version uses CLAUDE_CODE_USE_VERTEX, ANTHROPIC_VERTEX_BASE_URL, ANTHROPIC_VERTEX_PROJECT_ID, CLOUD_ML_REGION and CLAUDE_CODE_SKIP_VERTEX_AUTH. Keep the skip-auth variable set. Without it, Claude Code removes the Authorization header that carries your gateway token.

Which fix to use

Your setup Use
You need it working in the next five minutes Fix 1: CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1
Gateway forwards to the Anthropic API Forward anthropic-* headers and the body unchanged. If it already does, the 400 is coming from somewhere else: check the version-specific rows in the first table
LiteLLM forwards to Bedrock, Vertex AI or Azure Fix 2: upgrade LiteLLM so its per-provider filtering covers your Claude Code version
A gateway with a Bedrock- or Vertex-format endpoint Fix 3: the provider-format route
A strict proxy you don’t control Fix 1, plus beta_dump.py after each Claude Code upgrade

Mapping non-Claude models through LiteLLM

LiteLLM can translate Claude Code’s requests for other models, and people do use this for local or cheaper models. Know before you start that Anthropic doesn’t support routing Claude Code to non-Claude models through any gateway. Tool use, thinking and caching are built around Claude’s behaviour, so expect rough edges and treat it as an experiment.

If you try it, two things are easy to miss:

  • LiteLLM may call the Responses API. For openai/ models, LiteLLM 1.104.0 translated Claude Code’s /v1/messages request into a call to /v1/responses. Many local servers only implement /v1/chat/completions. With LITELLM_USE_CHAT_COMPLETIONS_URL_FOR_ANTHROPIC_MESSAGES=True in LiteLLM’s environment, it called /v1/chat/completions instead, with all 20 of Claude Code’s tools translated.
  • Claude Code doesn’t know the model’s context window. For a model name it doesn’t recognise, Claude Code 2.1.291 printed:
"local-coder" isn't described by this version's model catalog; update Claude Code, or map it with behavesAs on a modelPicker row (or modelOverrides, if it is a provider id of a model this version knows). Until then auto-compact keeps this session within 200k tokens (the context window it assumes); if the model accepts more, append [1m] to the model name for 1M, or set CLAUDE_CODE_MAX_CONTEXT_TOKENS to its real window; CLAUDE_CODE_DISABLE_UNKNOWN_MODEL_WINDOW_ENFORCEMENT=1 restores the previous wait-for-the-API behavior.

Set CLAUDE_CODE_MAX_CONTEXT_TOKENS to the model’s real window. A local model with a 32K window and Claude Code assuming 200K means requests fail long before auto-compaction starts.

The route in litellm.yaml looks like this, here for a vLLM server on port 8000:

  - model_name: local-coder
    litellm_params:
      model: openai/qwen-coder
      api_key: os.environ/LOCAL_API_KEY
      api_base: http://localhost:8000/v1

Gotchas once it works

  • A web application firewall blocks real sessions but not the test. Claude Code prompts contain XML-style tags and source code, which can match cross-site-scripting body rules. A 403 with an HTML body that never shows up in the gateway’s logs points here. Exempt the /v1/messages path from body inspection.
  • Buffered streaming stalls Claude Code. Claude Code reads the response as a stream. A gateway that buffers the whole response, or strips the ping events, can trip its idle timeout during long thinking pauses.
  • Rewritten error bodies break automatic recovery. Claude Code retries some rejections without the rejected capability, matching on the upstream’s error wording. Forward error bodies unchanged.
  • Prompt caching silently stops. If the gateway converts system blocks to a plain string or drops cache_control, nothing errors, but every turn bills as uncached input. Watch cache_read_input_tokens in the gateway’s logs.
  • A gateway is also where budgets live. LiteLLM’s per-key budgets are a common reason to put it in front of Claude Code; for how hard caps should work, see LLM API budget caps.

FAQ

Is it safe to leave experimental betas turned off?

Yes. CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS only removes pre-release capabilities. Claude Code keeps working, without context management, MCP tool search and a few newer request fields. Turn it off again once your gateway forwards everything, so you get those features back.

Why did this start after a Claude Code update when nothing else changed?

New releases add new beta values. Both reports quoted above began right after an upgrade: #70563 on 2.1.187 (2.1.181 worked) and #19984 on 2.1.23 (2.1.22 worked). Pinning an older Claude Code version works as a stopgap, but the variable or a gateway upgrade is the real fix.

Can I just strip the anthropic-beta header in my proxy?

Not on its own. Several capabilities pair a beta value with a body field, and an upstream that receives the field without its beta rejects the request. Either remove both halves, as CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS does on the client, or let a gateway that knows each provider translate the request, as LiteLLM does for Bedrock.

Does LiteLLM forward Claude Code’s beta values to the Anthropic API?

Yes. In LiteLLM 1.104.0, anthropic/ routes forward the anthropic-beta header unchanged; the per-provider allowlist only applies when LiteLLM translates for Bedrock, Vertex AI, Azure and similar providers.

How do I see what my gateway forwards upstream?

Point the gateway’s upstream api_base at beta_dump.py instead of the real provider. That’s how the captures in this post were made: Claude Code to LiteLLM, and LiteLLM to a local server that printed each request.

Bottom line

A 400 through a custom base URL almost always means Claude Code sent something the upstream doesn’t accept, because it treats any ANTHROPIC_BASE_URL as the Anthropic API.

  1. Unblock yourself with CLAUDE_CODE_DISABLE_EXPERIMENTAL_BETAS=1 in the settings env block.
  2. Fix it properly by upgrading the gateway so it forwards unchanged to Anthropic and filters per provider for Bedrock and Vertex AI, or by using the provider-format route.
  3. After each Claude Code upgrade, run beta_dump.py if your gateway is strict, before your whole team hits the same 400.