执行摘要
- 一句话:Anthropic 入口将客户端错误统一映射为 4xx
- 推荐动作:值得精读。这是一个小而完整的『入口错误处理对齐共享框架』示例:评审推动作者放弃手写类型分支、复用
create_error_response,最终以极小 diff 完成语义修复,并配套了高质量回归测试(用真实 pydantic.ValidationError 复现 Issue 场景)。对维护其他 entrypoint 的团队,本 PR 展示了如何在共享异常框架演进时保持各入口一致,值得作为参考模式。
功能与动机
Issue #52088 报告:CI 的 Schemathesis 测试生成的 Anthropic 请求可包含多于 4 个 stop_sequences(Anthropic 侧 schema 允许),但 Anthropic→OpenAI 转换构造 ChatCompletionRequest 时抛出 pydantic ValidationError(OpenAI stop 字段最多 4 项)。api_router 的 except Exception 把该异常吞成 internal_error 导致 HTTP 500,与 Issue 中『Invalid user input should not result in HTTP 500』的预期不符。PR body 明确指出 schema 半部分已由 #51997 解决,本 PR 修复剩余的转换错误映射缺口。
实现拆解
- 变更入口:
vllm/entrypoints/anthropic/api_router.py。create_messages 与 count_tokens 两个路由的异常分支从「手写 JSON 500 + internal_error 响应体」改为 translate_error_response(create_error_response(e))。create_error_response 来自 vllm/entrypoints/serve/exception_handling/error_response.py(#52261 整合后的共享异常映射入口),按异常类型分类:pydantic.ValidationError(ValueError 子类)与 VLLMClientError 类客户端错误映射为 400 BadRequestError,其余异常保持 500。translate_error_response 负责把 OpenAI 风格 ErrorResponse 翻译成 Anthropic 错误格式(type + message),状态码沿用 ErrorResponse.error.code。同时删除对 sanitize_message 的直接 import——脱敏改由 create_error_response 内部完成。
- 实现演进(4 个 commit):首版(c90de82)手写
_translate_exception 类型分支区分客户端错误;评审(DarkLight1337)要求跟随 server_utils.py 的异常处理器逻辑;第二版(c21c8ed)删除手写类型检查,直接复用 create_error_response,净 diff 收敛为 +6/−26,并去掉不再使用的 VLLMClientError/ValidationError import;第三版(4bb117d)按 AGENTS.md 精简冗余注释;第四版(90b3250)rebase 到 #52261 后修正两处测试。
- 测试配套:
tests/entrypoints/anthropic/test_anthropic_messages_conversion.py。新增 TestClientErrorResponses 类,覆盖 4 个场景:转换期 ValidationError → 400 且错误 type 为 BadRequestError、错误消息保留『at most 4 items』;VLLMValidationError → 400;RuntimeError → 500(回归保护);count_tokens 路由 → 400。测试用 _StubRequest(stop: Annotated[list[str], Field(max_length=4)] + 6 个元素)构造真实 pydantic.ValidationError,与线上 ChatCompletionRequest 校验行为等价。
- 配套调整:
tests/entrypoints/serve/exception_handling/test_error_sanitization.py。rebase 后 api_router 改为间接脱敏,从『必须直接调用 sanitize_message』的源级检查参数化清单中移除 vllm.entrypoints.anthropic.api_router。
- 无配置/schema/部署改动:路由的 OpenAPI responses 本就声明 400/500 两类响应,无需变更文档。
关键文件:
vllm/entrypoints/anthropic/api_router.py(模块 请求路由;类别 source;类型 entrypoint;符号 create_messages, count_tokens, translate_error_response, attach_router): PR 的核心改动文件:create_messages 与 count_tokens 两个路由的异常处理从手写 internal_error 500 改为复用共享 create_error_response 统一分类(客户端 4xx、服务端 500),并移除对 sanitize_message 的直接 import。
tests/entrypoints/anthropic/test_anthropic_messages_conversion.py(模块 消息转换;类别 test;类型 test-coverage;符号 TestClientErrorResponses, _make_api_app, _request_body, _conversion_error): 新增 TestClientErrorResponses 回归测试类,覆盖转换期 ValidationError、VLLMValidationError → 400,RuntimeError → 500,以及 count_tokens 路由 → 400,是本 PR 语义修复的验证主体。
tests/entrypoints/serve/exception_handling/test_error_sanitization.py(模块 错误脱敏;类别 test;类型 test-coverage;符号 TestAffectedModulesUseSanitize): rebase 到 #52261 后 api_router 改由 create_error_response 间接脱敏,不再直接调用 sanitize_message,因此从源级检查的参数化清单中移除该模块。
关键符号:create_messages, count_tokens, translate_error_response, TestClientErrorResponses._conversion_error, TestClientErrorResponses.test_validation_error_returns_bad_request, TestClientErrorResponses.test_generic_error_still_returns_internal_server_error
关键源码片段
tests/entrypoints/anthropic/test_anthropic_messages_conversion.py
新增 TestClientErrorResponses 回归测试类,覆盖转换期 ValidationError、VLLMValidationError → 400,RuntimeError → 500,以及 count_tokens 路由 → 400,是本 PR 语义修复的验证主体。
# tests/entrypoints/anthropic/test_anthropic_messages_conversion.py
# 客户端导致的错误必须是 4xx,而不是 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)
# 注册 FastAPI 请求校验异常处理器,保持 schema 校验错误 → 4xx 语义
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:
"""构造真实 pydantic ValidationError,等价于 ChatCompletionRequest
构造时对 stop 字段超长的校验失败(复现 Issue #52088 场景)。"""
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):
# 转换阶段 pydantic ValidationError 应映射为 400,而非 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):
# Serving 层抛出的 VLLMValidationError 也映射为 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):
# 非客户端错误必须保持 500,防止服务端故障被误报成客户端问题
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):
# count_tokens 路由同样需要 4xx 语义
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"
评论区精华
核心讨论围绕『是否手写异常类型判断』展开:DarkLight1337 在本 PR 进行中提醒存在 #52261 异常处理重构,要求跟随 vllm/entrypoints/serve/utils/server_utils.py 的既有异常处理器逻辑,而不是在 api_router 里再维护一套类型检查。作者据此把首版手写的 _translate_exception 分支删除,改为直接调用 create_error_response(e),让分类语义由共享模块统一保证;api_router 只负责把结果翻译成 Anthropic 格式。此外,rebase 到 #52261 后出现了两个行为变化需要同步:通用错误映射出的 type 从 internal_error 变为 OpenAI 风格 InternalServerError(测试断言随之更新);sanitize_message 不再被 api_router 直接调用(源级检查清单随之移除)。流程上还有多次 merge 冲突提示与 /ci run、/ci retry 触发,最终由 DarkLight1337 Approve(『Thanks for fixing!』)。
- 复用共享异常处理 create_error_response 而非手写类型分支 (design): api_router 统一走 create_error_response 做异常分类,再经 translate_error_response 转成 Anthropic 错误格式;净 diff 收敛为 +6/−26。
- rebase 到 #52261 后的错误类型与 sanitize 检查变化 (design): 测试断言更新为 InternalServerError;test_error_sanitization.py 的参数化清单移除 api_router。
- merge 冲突与 CI 触发流程 (other): rebase 到最新 main(含 #52261)后无冲突合并,CI 通过。
风险与影响
- 风险:
- 错误类型字符串变化:rebase 到 #52261 后,通用异常映射出的错误 type 从原来的
internal_error 变为 OpenAI 风格的 InternalServerError,依赖该字符串做监控或客户端分支的用户会观察到差异;测试已同步更新,但线上兼容性仍值得留意。
- 依赖共享异常映射的语义:
create_error_response 对异常的分类逻辑集中在 vllm/entrypoints/serve/exception_handling/error_response.py,其中 pydantic.ValidationError 因是 ValueError 子类被归为客户端错误。若 serving 层未来用 ValueError 表达服务端故障,会被误映射为 4xx;若共享模块调整分类规则,Anthropic 入口会自动继承新行为,属于「单一职责收益」与「隐式耦合风险」并存。
- 流式响应中途错误不在本映射范围:
StreamingResponse 开始输出后发生的迭代异常发生在 try/except 之外,本次改动未覆盖,属既有行为,不是本 PR 引入的回归。
- 兼容性:路由 OpenAPI responses 原本就声明 400/500,无需文档或客户端适配;错误消息脱敏仍生效(间接调用
sanitize_message)。
- 影响:用户侧:Anthropic Messages API 与 count_tokens 的客户端错误从误导性的 500 变为准确的 4xx,便于客户端区分输入问题与服务端故障、制定重试策略;服务端 500 告警的信号价值提升。系统侧:消除了 api_router 中重复的手写错误构造代码,与 #52261 的整合架构对齐,后续异常分类改动只需在共享模块进行。团队侧:新增 TestClientErrorResponses 建立了 400/500 边界的回归防线,并同步维护了 test_error_sanitization.py 的源级检查清单,防止入口脱离统一脱敏路径。影响范围集中在 Anthropic 前端入口层,不涉及核心调度、注意力或模型代码。
- 风险标记:错误类型字符串变更, 依赖共享异常映射框架语义, 任意 ValueError 子类按客户端错误处理, 流式中途错误不受本映射保护(既有行为)
关联脉络
- PR #51997 [Bugfix][Anthropic] Add max_length on stop_sequences(标题依 PR body 描述推断): PR body 明确提到 #51997 解决了 Issue #52088 的 schema 半部分(给 stop_sequences 增加 max_length 约束),与本 PR 的转换错误映射修复共同闭环同一 issue。
- PR #52261 [Refactor] Consolidate entrypoint exception handling(标题依评审与 commit 描述推断): 评审 DarkLight1337 要求本 PR 对齐该异常处理整合重构;rebase 后复用其 create_error_response,并相应调整测试断言与 sanitize 源级检查清单。
参与讨论