From 70aaec832bde70ada17cbad2061105bbd07541dc Mon Sep 17 00:00:00 2001 From: Do_it_now_! Date: Sun, 16 Aug 2026 16:05:17 +0800 Subject: [PATCH] [Bugfix][Anthropic] Return 4xx for client-caused errors in /v1/messages (#52246) Signed-off-by: Do_it_now_! --- .../test_anthropic_messages_conversion.py | 95 +++++++++++++++++++ .../test_error_sanitization.py | 1 - vllm/entrypoints/anthropic/api_router.py | 24 +---- 3 files changed, 100 insertions(+), 20 deletions(-) diff --git a/tests/entrypoints/anthropic/test_anthropic_messages_conversion.py b/tests/entrypoints/anthropic/test_anthropic_messages_conversion.py index a54e13666f..1cd7227bdc 100644 --- a/tests/entrypoints/anthropic/test_anthropic_messages_conversion.py +++ b/tests/entrypoints/anthropic/test_anthropic_messages_conversion.py @@ -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" diff --git a/tests/entrypoints/serve/exception_handling/test_error_sanitization.py b/tests/entrypoints/serve/exception_handling/test_error_sanitization.py index f98d870318..426d1ede74 100644 --- a/tests/entrypoints/serve/exception_handling/test_error_sanitization.py +++ b/tests/entrypoints/serve/exception_handling/test_error_sanitization.py @@ -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", ], diff --git a/vllm/entrypoints/anthropic/api_router.py b/vllm/entrypoints/anthropic/api_router.py index 7c6d23183d..584a295801 100644 --- a/vllm/entrypoints/anthropic/api_router.py +++ b/vllm/entrypoints/anthropic/api_router.py @@ -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)