[Bugfix][Anthropic] Return 4xx for client-caused errors in /v1/messages (#52246)

Signed-off-by: Do_it_now_! <lizhengcheng233@gmail.com>
This commit is contained in:
Do_it_now_!
2026-08-16 01:05:17 -07:00
committed by GitHub
parent 41f12a0daf
commit 70aaec832b
3 changed files with 100 additions and 20 deletions
@@ -15,12 +15,14 @@ Also covers cache usage computation in ``_build_anthropic_usage``.
import json
from argparse import Namespace
from http import HTTPStatus
from typing import Annotated
from unittest.mock import MagicMock
import pytest
from fastapi import FastAPI
from fastapi.exceptions import RequestValidationError
from fastapi.testclient import TestClient
from pydantic import BaseModel, Field, ValidationError
from vllm.entrypoints.anthropic.api_router import attach_router
from vllm.entrypoints.anthropic.protocol import (
@@ -47,6 +49,7 @@ from vllm.entrypoints.openai.engine.protocol import (
from vllm.entrypoints.serve.exception_handling.handlers.validation import (
validation_exception_handler,
)
from vllm.exceptions import VLLMValidationError
_convert = AnthropicServingMessages._convert_anthropic_to_openai_request
_img_url = AnthropicServingMessages._convert_image_source_to_url
@@ -1458,3 +1461,95 @@ class TestCacheSalt:
assert response.status_code == HTTPStatus.BAD_REQUEST
handler.create_messages.assert_not_awaited()
# ======================================================================
# Client-caused errors are 4xx, not 500 (Issue #52088)
# ======================================================================
class TestClientErrorResponses:
@staticmethod
def _make_api_app(handler: MagicMock):
app = FastAPI()
attach_router(app)
app.state.args = Namespace(log_error_stack=False)
app.exception_handler(RequestValidationError)(validation_exception_handler)
app.state.anthropic_serving_messages = handler
return app
@staticmethod
def _request_body() -> dict:
return {
"model": "test-model",
"max_tokens": 1,
"messages": [{"role": "user", "content": "Hello"}],
}
@staticmethod
def _conversion_error() -> ValidationError:
"""A real pydantic ValidationError like the one ChatCompletionRequest
construction raises when Anthropic input violates the OpenAI schema."""
class _StubRequest(BaseModel):
stop: Annotated[list[str], Field(max_length=4)] | None = None
with pytest.raises(ValidationError) as exc_info:
_StubRequest(stop=["a"] * 6)
return exc_info.value
def test_validation_error_returns_bad_request(self):
"""A pydantic ValidationError during Anthropic->OpenAI conversion is
surfaced as a 400 BadRequestError, not a 500."""
handler = MagicMock(spec=AnthropicServingMessages)
handler.create_messages.side_effect = self._conversion_error()
app = self._make_api_app(handler)
with TestClient(app, raise_server_exceptions=False) as client:
response = client.post("/v1/messages", json=self._request_body())
assert response.status_code == HTTPStatus.BAD_REQUEST
body = response.json()
assert body["type"] == "error"
assert body["error"]["type"] == "BadRequestError"
assert "at most 4 items" in body["error"]["message"]
def test_vllm_client_error_returns_bad_request(self):
"""VLLMClientError raised by the serving layer maps to 400."""
handler = MagicMock(spec=AnthropicServingMessages)
handler.create_messages.side_effect = VLLMValidationError(
"Invalid value for stop", parameter="stop"
)
app = self._make_api_app(handler)
with TestClient(app, raise_server_exceptions=False) as client:
response = client.post("/v1/messages", json=self._request_body())
assert response.status_code == HTTPStatus.BAD_REQUEST
assert response.json()["error"]["type"] == "BadRequestError"
def test_generic_error_still_returns_internal_server_error(self):
"""Non-client errors keep the existing 500 behaviour."""
handler = MagicMock(spec=AnthropicServingMessages)
handler.create_messages.side_effect = RuntimeError("boom")
app = self._make_api_app(handler)
with TestClient(app, raise_server_exceptions=False) as client:
response = client.post("/v1/messages", json=self._request_body())
assert response.status_code == HTTPStatus.INTERNAL_SERVER_ERROR
assert response.json()["error"]["type"] == "InternalServerError"
def test_count_tokens_validation_error_returns_bad_request(self):
"""The count_tokens route maps conversion errors to 400 as well."""
handler = MagicMock(spec=AnthropicServingMessages)
handler.count_tokens.side_effect = self._conversion_error()
app = self._make_api_app(handler)
with TestClient(app, raise_server_exceptions=False) as client:
response = client.post(
"/v1/messages/count_tokens", json=self._request_body()
)
assert response.status_code == HTTPStatus.BAD_REQUEST
assert response.json()["error"]["type"] == "BadRequestError"
@@ -116,7 +116,6 @@ class TestAffectedModulesUseSanitize:
@pytest.mark.parametrize(
"module",
[
"vllm.entrypoints.anthropic.api_router",
"vllm.entrypoints.anthropic.serving",
"vllm.entrypoints.speech_to_text.realtime.connection",
],
+5 -19
View File
@@ -17,7 +17,9 @@ from vllm.entrypoints.anthropic.protocol import (
)
from vllm.entrypoints.anthropic.serving import AnthropicServingMessages
from vllm.entrypoints.openai.engine.protocol import ErrorResponse
from vllm.entrypoints.serve.exception_handling.utils import sanitize_message
from vllm.entrypoints.serve.exception_handling.error_response import (
create_error_response,
)
from vllm.entrypoints.serve.utils.api_utils import (
load_aware_call,
validate_json_request,
@@ -71,15 +73,7 @@ async def create_messages(request: AnthropicMessagesRequest, raw_request: Reques
generator = await handler.create_messages(request, raw_request)
except Exception as e:
logger.exception("Error in create_messages: %s", e)
return JSONResponse(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value,
content=AnthropicErrorResponse(
error=AnthropicError(
type="internal_error",
message=sanitize_message(str(e)),
)
).model_dump(),
)
return translate_error_response(create_error_response(e))
if isinstance(generator, ErrorResponse):
return translate_error_response(generator)
@@ -117,15 +111,7 @@ async def count_tokens(request: AnthropicCountTokensRequest, raw_request: Reques
response = await handler.count_tokens(request, raw_request)
except Exception as e:
logger.exception("Error in count_tokens: %s", e)
return JSONResponse(
status_code=HTTPStatus.INTERNAL_SERVER_ERROR.value,
content=AnthropicErrorResponse(
error=AnthropicError(
type="internal_error",
message=sanitize_message(str(e)),
)
).model_dump(),
)
return translate_error_response(create_error_response(e))
if isinstance(response, ErrorResponse):
return translate_error_response(response)