diff --git a/tests/entrypoints/anthropic/test_anthropic_messages_conversion.py b/tests/entrypoints/anthropic/test_anthropic_messages_conversion.py index 5790b9b082..a6cfb15d60 100644 --- a/tests/entrypoints/anthropic/test_anthropic_messages_conversion.py +++ b/tests/entrypoints/anthropic/test_anthropic_messages_conversion.py @@ -1804,6 +1804,28 @@ class TestThinkingConfig: thinking=thinking, ) + def test_pd_prefill_leg_skips_budget_check(self): + """P/D sidecars resend the request to the prefill node with + max_tokens=1; the budget is enforced on the decode leg instead.""" + thinking = {"type": "enabled", "budget_tokens": 1024} + request = AnthropicMessagesRequest( + model="test-model", + max_tokens=1, + messages=[{"role": "user", "content": "Hello"}], + thinking=thinking, + kv_transfer_params={"do_remote_decode": True}, + ) + + assert _convert(request).thinking_token_budget == 1024 + with pytest.raises(ValidationError): + AnthropicMessagesRequest( + model="test-model", + max_tokens=1, + messages=[{"role": "user", "content": "Hello"}], + thinking=thinking, + kv_transfer_params={"do_remote_decode": False}, + ) + def test_adaptive_pins_nothing_and_keeps_effort_ceiling(self): """`adaptive` lets the model choose depth, so only the ceiling from output_config.effort should survive.""" diff --git a/vllm/entrypoints/anthropic/protocol.py b/vllm/entrypoints/anthropic/protocol.py index 48202831fe..9bd40a9161 100644 --- a/vllm/entrypoints/anthropic/protocol.py +++ b/vllm/entrypoints/anthropic/protocol.py @@ -233,6 +233,10 @@ class AnthropicMessagesRequest(BaseModel): @model_validator(mode="after") def validate_thinking_budget(self) -> "AnthropicMessagesRequest": + # P/D prefill legs are sent with max_tokens=1 and never decode; the + # decode leg carries the client's max_tokens and is still checked. + if self.kv_transfer_params and self.kv_transfer_params.get("do_remote_decode"): + return self if ( isinstance(self.thinking, AnthropicThinkingConfigEnabled) and self.thinking.budget_tokens >= self.max_tokens