[Bugfix][Anthropic] Return 4xx for client-caused errors in /v1/messages (#52246)
Signed-off-by: Do_it_now_! <lizhengcheng233@gmail.com>
Do_it_now_! committed
Aug 16, 2026 at 16:05 UTC
70aaec832bde70ada17cbad2061105bbd07541dc
3 files changed
+100
-20
tests/entrypoints/anthropic/test_anthropic_messages_conversion.py
+95
@@ -15,12 +15,14 @@ Also covers cache usage computation in ``_build_anthropic_usage``.
15
import json
16
from argparse import Namespace
17
from http import HTTPStatus
18
+from typing import Annotated
19
from unittest.mock import MagicMock
20
21
import pytest
22
from fastapi import FastAPI
23
from fastapi.exceptions import RequestValidationError
24
from fastapi.testclient import TestClient
25
+from pydantic import BaseModel, Field, ValidationError
26
27
from vllm.entrypoints.anthropic.api_router import attach_router
28
from vllm.entrypoints.anthropic.protocol import (
@@ -47,6 +49,7 @@ from vllm.entrypoints.openai.engine.protocol import (
49
from vllm.entrypoints.serve.exception_handling.handlers.validation import (
50
validation_exception_handler,
51
)
52
+from vllm.exceptions import VLLMValidationError
53
54
_convert = AnthropicServingMessages._convert_anthropic_to_openai_request
55
_img_url = AnthropicServingMessages._convert_image_source_to_url
@@ -1458,3 +1461,95 @@ class TestCacheSalt:
1461
1462
assert response.status_code == HTTPStatus.BAD_REQUEST
1463
handler.create_messages.assert_not_awaited()
1464
+
1465
+
1466
+# ======================================================================
1467
+# Client-caused errors are 4xx, not 500 (Issue #52088)
1468
+# ======================================================================
1469
+
1470
+
1471
+class TestClientErrorResponses:
1472
+ @staticmethod
1473
+ def _make_api_app(handler: MagicMock):
1474
+ app = FastAPI()
1475
+ attach_router(app)
1476
+ app.state.args = Namespace(log_error_stack=False)
1477
+ app.exception_handler(RequestValidationError)(validation_exception_handler)
1478
+ app.state.anthropic_serving_messages = handler
1479
+ return app
1480
+
1481
+ @staticmethod
1482
+ def _request_body() -> dict:
1483
+ return {
1484
+ "model": "test-model",
1485
+ "max_tokens": 1,
1486
+ "messages": [{"role": "user", "content": "Hello"}],
1487
+ }
1488
+
1489
+ @staticmethod
1490
+ def _conversion_error() -> ValidationError:
1491
+ """A real pydantic ValidationError like the one ChatCompletionRequest
1492
+ construction raises when Anthropic input violates the OpenAI schema."""
1493
+
1494
+ class _StubRequest(BaseModel):
1495
+ stop: Annotated[list[str], Field(max_length=4)] | None = None
1496
+
1497
+ with pytest.raises(ValidationError) as exc_info:
1498
+ _StubRequest(stop=["a"] * 6)
1499
+ return exc_info.value
1500
+
1501
+ def test_validation_error_returns_bad_request(self):
1502
+ """A pydantic ValidationError during Anthropic->OpenAI conversion is
1503
+ surfaced as a 400 BadRequestError, not a 500."""
1504
+ handler = MagicMock(spec=AnthropicServingMessages)
1505
+ handler.create_messages.side_effect = self._conversion_error()
1506
+
1507
+ app = self._make_api_app(handler)
1508
+ with TestClient(app, raise_server_exceptions=False) as client:
1509
+ response = client.post("/v1/messages", json=self._request_body())
1510
+
1511
+ assert response.status_code == HTTPStatus.BAD_REQUEST
1512
+ body = response.json()
1513
+ assert body["type"] == "error"
1514
+ assert body["error"]["type"] == "BadRequestError"
1515
+ assert "at most 4 items" in body["error"]["message"]
1516
+
1517
+ def test_vllm_client_error_returns_bad_request(self):
1518
+ """VLLMClientError raised by the serving layer maps to 400."""
1519
+ handler = MagicMock(spec=AnthropicServingMessages)
1520
+ handler.create_messages.side_effect = VLLMValidationError(
1521
+ "Invalid value for stop", parameter="stop"
1522
+ )
1523
+
1524
+ app = self._make_api_app(handler)
1525
+ with TestClient(app, raise_server_exceptions=False) as client:
1526
+ response = client.post("/v1/messages", json=self._request_body())
1527
+
1528
+ assert response.status_code == HTTPStatus.BAD_REQUEST
1529
+ assert response.json()["error"]["type"] == "BadRequestError"
1530
+
1531
+ def test_generic_error_still_returns_internal_server_error(self):
1532
+ """Non-client errors keep the existing 500 behaviour."""
1533
+ handler = MagicMock(spec=AnthropicServingMessages)
1534
+ handler.create_messages.side_effect = RuntimeError("boom")
1535
+
1536
+ app = self._make_api_app(handler)
1537
+ with TestClient(app, raise_server_exceptions=False) as client:
1538
+ response = client.post("/v1/messages", json=self._request_body())
1539
+
1540
+ assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
1541
+ assert response.json()["error"]["type"] == "InternalServerError"
1542
+
1543
+ def test_count_tokens_validation_error_returns_bad_request(self):
1544
+ """The count_tokens route maps conversion errors to 400 as well."""
1545
+ handler = MagicMock(spec=AnthropicServingMessages)
1546
+ handler.count_tokens.side_effect = self._conversion_error()
1547
+
1548
+ app = self._make_api_app(handler)
1549
+ with TestClient(app, raise_server_exceptions=False) as client:
1550
+ response = client.post(
1551
+ "/v1/messages/count_tokens", json=self._request_body()
1552
+ )
1553
+
1554
+ assert response.status_code == HTTPStatus.BAD_REQUEST
1555
+ assert response.json()["error"]["type"] == "BadRequestError"
tests/entrypoints/serve/exception_handling/test_error_sanitization.py
-1
@@ -116,7 +116,6 @@ class TestAffectedModulesUseSanitize:
116
@pytest.mark.parametrize(
117
"module",
118
[
119
- "vllm.entrypoints.anthropic.api_router",
119
"vllm.entrypoints.anthropic.serving",
120
"vllm.entrypoints.speech_to_text.realtime.connection",
121
],
vllm/entrypoints/anthropic/api_router.py
+5
-19
@@ -17,7 +17,9 @@ from vllm.entrypoints.anthropic.protocol import (
17
)
18
from vllm.entrypoints.anthropic.serving import AnthropicServingMessages
19
from vllm.entrypoints.openai.engine.protocol import ErrorResponse
20
-from vllm.entrypoints.serve.exception_handling.utils import sanitize_message
20
+from vllm.entrypoints.serve.exception_handling.error_response import (
21
+ create_error_response,
22
+)
23
from vllm.entrypoints.serve.utils.api_utils import (
24
load_aware_call,
25
validate_json_request,
@@ -71,15 +73,7 @@ async def create_messages(request: AnthropicMessagesRequest, raw_request: Reques
73
generator = await handler.create_messages(request, raw_request)
74
except Exception as e:
75
logger.exception("Error in create_messages: %s", e)
74
- return JSONResponse(
75
- status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value,
76
- content=AnthropicErrorResponse(
77
- error=AnthropicError(
78
- type="internal_error",
79
- message=sanitize_message(str(e)),
80
- )
81
- ).model_dump(),
82
- )
76
+ return translate_error_response(create_error_response(e))
77
78
if isinstance(generator, ErrorResponse):
79
return translate_error_response(generator)
@@ -117,15 +111,7 @@ async def count_tokens(request: AnthropicCountTokensRequest, raw_request: Reques
111
response = await handler.count_tokens(request, raw_request)
112
except Exception as e:
113
logger.exception("Error in count_tokens: %s", e)
120
- return JSONResponse(
121
- status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value,
122
- content=AnthropicErrorResponse(
123
- error=AnthropicError(
124
- type="internal_error",
125
- message=sanitize_message(str(e)),
126
- )
127
- ).model_dump(),
128
- )
114
+ return translate_error_response(create_error_response(e))
115
116
if isinstance(response, ErrorResponse):
117
return translate_error_response(response)