#57324·vllm

[Bug]: Claude Code tool search: first request of every session rejected with 400 (tool_addition content blocks not accepted by /v1/messages)

Author: Jiangw2718iCreated Sep 17, 2026Updated Sep 17, 2026
Labelstool-calling

Correction (2026-09-17): the diagnosis below is wrong. The 400 is not caused by AnthropicTool.input_schema. Every tool in the real request carries an input_schema; the request is rejected because messages[] contains tool_addition content blocks, which AnthropicContentBlock.type does not accept. Captured request, error body and checks are in this comment. The symptom described below is accurate; the cause, the ~163-tool extrapolation and the curl reproduction are not. Original text kept unchanged for the record.

Your current environment

vLLM:     0.1.dev20073+g8e685d198
Client:   Claude Code 2.1.273 (macOS), ANTHROPIC_BASE_URL pointed at this endpoint
Model:    Qwen3.8-Flash-Next (NVFP4)
Serving:  --max-model-len 262144 --enable-prefix-caching --enable-chunked-prefill
Hardware: NVIDIA DGX Spark (GB10), 128 GB unified memory

Describe the bug

Same validation point as #46790, different trigger, and a different practical impact.

#46790 reaches AnthropicTool.input_schema through Anthropic server tools (web_search_20250305, computer use, bash, text_editor, code_execution), which carry no schema by design. The same required field also rejects deferred tools. With ENABLE_TOOL_SEARCH=true, Claude Code omits most tool schemas from the first request of a session and sends names only, so every one of those entries fails the same check and a single request produces a multi-error response.

Unlike the WebSearch case, this one is silent. The proxy in front of vLLM logs the rejection:

[WARN] non-200 response, recording partial metrics: status=400, path=/v1/messages
[INFO] Request ... "POST /v1/messages HTTP/1.1" 400 20214 "claude-cli/2.1.273 (external, sdk-cli)" 19.705389ms

and vLLM logs the retry landing on the same connection:

INFO:  ... - "POST /v1/messages?beta=true HTTP/1.1" 400 Bad Request
INFO:  ... - "POST /v1/messages?beta=true HTTP/1.1" 200 OK

The client swallows the 400 and resends with full schemas. The session completes normally and nothing appears on the command line, so the cost is easy to miss: one rejected round-trip plus a ~20 KB error body on every session start.

Over one afternoon on this setup: 29 × 200 and 3 × 400. Each 400 landed on the first request of one of the three sessions that announced deferred tools, matching to the second; the four sessions without deferred tools produced none. All three rejected bodies were 20214 bytes.

I did not capture the request body, so the entry count is inferred. Sending N name-only tools to the same endpoint grows the error body linearly at roughly 123 bytes each:

name-only tools   response body
      1                234
     10               1343
     30               3823
     60               7543
    100              12504

Extrapolating 20214 bytes gives ~163 entries. The client's session transcript recorded total_deferred_tools: 148, the same order of magnitude.

Minimal reproduction, no client needed:

bash
curl -s -X POST "$BASE_URL/v1/messages?beta=true" \
  -H 'content-type: application/json' \
  -H 'anthropic-version: 2023-06-01' \
  -d '{"model":"<model>","max_tokens":8,
       "messages":[{"role":"user","content":"hi"}],
       "tools":[{"name":"Tool000"},{"name":"Tool001"}]}'
{"error":{"message":"2 validation errors:\n  {'type': 'missing', 'loc': ('body', 'tools', 0, 'input_schema'), 'msg': 'Field required', 'input': {'name': 'Tool000'}}\n ...","type":"Bad Request","param":"body.tools.0.input_schema","code":400}}

The deferral flag is already plumbed through. _convert_tools in vllm/entrypoints/anthropic/serving.py forwards tool.defer_loading into the converted tool definition (line 581), directly after "parameters": tool.input_schema (line 579). Deferral is not an unknown concept on this path; there is a slot prepared for it. A deferred tool has no schema by definition, and AnthropicTool.input_schema in vllm/entrypoints/anthropic/protocol.py:79 is required, so the request is rejected at validation and never reaches that slot.

Verified unchanged on main (528fa835fd37, 2026-09-17) and in the current release v0.29.0 (2026-09-09); input_schema sits at line 79 and defer_loading at line 81 in both.

One note on #46790's conclusion that "such a 400 doesn't seem to have any practical impact on usage": that holds for server tools, where the effect is confined to WebSearch being unavailable. On this path it is a per-session tax that nobody sees, on a setup where the endpoint is otherwise working correctly.

#46797 makes input_schema optional and skips schema-less entries during conversion, which would unblock this path as well as the server-tool one it was written for. It was rebased on 2026-09-01 and has had no review since it was opened.

Before submitting a new issue...

  • Make sure you already searched for relevant issues, and asked the chatbot living at the bottom right corner of the documentation page, which can answer lots of frequently asked questions.