[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:
@@ -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",
|
||||
],
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user