![【Bug已解决】[Bug]:[Anthropic API] ValidationError in message_stream_converter masks underlying 500 error](http://pic.xiahunao.cn/yaotu/【Bug已解决】[Bug]:[Anthropic API] ValidationError in message_stream_converter masks underlying 500 error)
【Bug已解决】[Bug]:[Anthropic API] ValidationError in message_stream_converter masks underlying 500 error when serving GLM-5.2 解决方案一、现象长什么样用 vLLM 的 Anthropic API 兼容层把 GLM-5.2 当 Claude 来服务时如果后端模型推理失败返回了HTTP 500客户端看到的却是422 ValidationError: field delta is required in message_stream或500 ... but body is ValidationError: value is not a valid... (本该是模型的 500 错误体)几个典型表征真正错误是上游 500模型推理崩但客户端收到 ValidationErrorAnthropic 兼容层在把流式消息转成 Anthropic 格式时先按成功流去解析/校验响应结构但 500 的响应体根本不是合法的流式消息格式于是校验层抛出ValidationError把原始 500 错误盖掉了。只在后端真的出错500时出现正常推理不出现说明问题在错误路径——正常响应能正确转换但错误响应被转换层当成了格式不对而校验失败。用户排障被误导看到 ValidationError 会去查我的请求字段格式而真实原因是模型推理 500方向完全错。这不是 GLM-5.2 推理问题而是Anthropic 兼容层的message_stream_converter没有先判断上游 HTTP 状态码就直接按成功流去校验转换导致上游 500 被校验错误掩盖。下面给出定位与修复先判状态、再转换、错误透传。二、背景Anthropic API 兼容层的工作流客户端 → vLLM Anthropic 端点 → 转发到 OpenAI/内部推理 → 拿到响应流 → message_stream_converter 把内部流转换成 Anthropic 的 SSE 格式 → 返回客户端问题在于转换层的假设响应流一定是成功的流式消息。但后端可能在已经开始流式返回后才出错或一开始就 500响应体是错误 JSON如{error: {message: ..., code: 500}}根本不是 Anthropic 的message_delta事件格式。转换层在parse(event)时按成功格式pydantic校验遇到错误体就ValidationError然后这个 ValidationError 被当成响应内容发出去甚至把状态码也从 500 改成 422原始 500 信息丢失。根因是转换层先转换后判错且没有把上游错误状态码透传。修复就是先判 HTTP 状态/错误体再转换错误就原样透传含状态码。下面用可运行代码复现并修复。三、根因拆成两条根因转换层先校验成功格式错误体触发 ValidationErrormessage_stream_converter拿到事件就Model.validate(event)错误体500 的 JSON不符合成功 schema →ValidationError且不识别这是上游错误。根因是没有先区分成功流与错误体。上游错误状态码被覆盖/丢失ValidationError 被当成响应内容状态码可能从 500 变成 422原始错误信息丢失。根因是错误没有按原状态码透传。修复方向转换前先探测错误体有error字段 / 非预期结构→ 直接透传原始错误与状态码只有确认是成功流才做格式转换。四、最小可运行复现下面复现错误体被当成功流校验 → ValidationError 掩盖 500from typing import Optional from dataclasses import dataclass dataclass class StreamEvent: type: str delta: Optional[str] None def naive_convert(raw_event: dict): 现状直接按成功流校验转换不识别错误体。 # 假设成功事件必有 type/delta if delta not in raw_event: raise ValueError(ValidationError: field delta is required) return StreamEvent(**raw_event) # 上游 500 的错误体不是流式消息格式 upstream_500 {error: {message: model crashed: OOM, code: 500}} try: naive_convert(upstream_500) except ValueError as e: print(复现错误被掩盖:, e) # ValidationError 而非 500 信息复现错误被掩盖: ... ValidationError即复现真实的 500 信息model crashed: OOM被ValidationError盖掉。下面改成先判错误体。五、解决方案第一层最小直接修复最小修复转换前先探测错误体是错误就原样透传含状态码只有成功流才校验转换。from dataclasses import dataclass from typing import Tuple, Optional dataclass class ConvertResult: is_error: bool status: int payload: dict event: Optional[StreamEvent] None def detect_error(raw: dict, upstream_status: int) - Optional[dict]: 若上游是非 2xx 或体含 error 字段识别为错误体。 if upstream_status 400: return {message: raw.get(error, {}).get(message, unknown), code: upstream_status} if error in raw: code raw[error].get(code, upstream_status) return {message: raw[error].get(message, unknown), code: code} return None def safe_convert(raw_event: dict, upstream_status: int) - ConvertResult: err detect_error(raw_event, upstream_status) if err is not None: # 错误体原样透传保留真实状态码与信息不转成功流 return ConvertResult(is_errorTrue, statuserr[code], payloaderr) # 成功流才做格式校验与转换 if delta not in raw_event: raise ValueError(成功流缺少 delta 字段) return ConvertResult(is_errorFalse, status200, payloadraw_event, eventStreamEvent(**raw_event)) # 复现修复500 错误体被识别并透传不再变成 ValidationError r safe_convert(upstream_500, upstream_status500) print(透传错误:, r.status, r.payload) # 500 {message: model crashed: OOM, ...}这一层改动让上游 500 在转换层被识别为错误并原样透传保留 500 状态码与真实信息客户端看到的不再是掩盖性的 ValidationError。六、解决方案第二层结构化改进把Anthropic 流转换做成结构化组件集中管理错误体识别 → 透传与成功流转换并支持 SSE 包装错误也按 Anthropic 的错误事件格式返回但状态码正确。from enum import Enum from typing import Dict, Callable class StreamPhase(Enum): SUCCESS success ERROR error class MessageStreamConverter: def __init__(self): self._handlers: Dict[StreamPhase, Callable] {} def register(self, phase: StreamPhase, fn: Callable): self._handlers[phase] fn def convert(self, raw: dict, upstream_status: int): err detect_error(raw, upstream_status) if err is not None: # 错误用 Anthropic 错误事件格式包装但保留真实 status err_event {type: error, error: err} return ConvertResult(is_errorTrue, statuserr[code], payloaderr_event) # 成功转成 Anthropic message_delta 事件 return ConvertResult(is_errorFalse, status200, payload{type: content_block_delta, delta: {type: text_delta, text: raw.get(delta, )}}) # 用法 conv MessageStreamConverter() r conv.convert(upstream_500, 500) print(客户端应收到状态码, r.status, 体:, r.payload)MessageStreamConverter把错误识别/透传与成功转换分离错误仍按 Anthropic 风格包装客户端好解析但状态码与真实信息不被掩盖。七、解决方案第三层断言 / CI 守护错误掩盖最怕线上 500 被当成 422。用断言守两条不变量def check_converter_invariants(raw, upstream_status): r MessageStreamConverter().convert(raw, upstream_status) err detect_error(raw, upstream_status) if err is not None: # 不变量 1错误必须透传真实状态码不被改成 422 assert r.status err[code], f状态码被改: {r.status} ! {err[code]} # 不变量 2透传体必须含真实错误信息不得是 ValidationError 文案 assert model crashed in str(r.payload) or r.is_error return True def test_anthropic_error_passthrough(): # 上游 500必须透传 500不得变 ValidationError check_converter_invariants(upstream_500, 500) # 上游 200 成功流正常转换 check_converter_invariants({delta: hi}, 200) print(OK: Anthropic 流转换错误透传不变量通过) if __name__ __main__: test_anthropic_error_passthrough()把test_anthropic_error_passthrough接进 CI任何又把 500 改成 422/ValidationError的改动都会立即红。八、排查清单Anthropic 兼容层把 500 掩盖成 ValidationError按序查先看原始上游状态码客户端收到 ValidationError 时去查 vLLM 后端的真实响应——若后端是 500根因就是转换层掩盖。转换前先判错误体detect_error检查上游 status ≥ 400 或体含error字段是错误就原样透传不进成功流校验。保留真实状态码错误透传时状态码必须是上游真实码500绝不能变成 422/ValidationError 的码。错误也按 Anthropic 风格包装返回{type:error,error:{...}}让客户端能解析但 HTTP 状态码与 message 是真实的。成功流才校验格式只有确认是成功响应才按 Anthropic schema 校验delta等字段避免错误体触发 ValidationError。日志记原始错误透传前把上游原始 500 错误体打到服务端日志方便排障客户端只看到透传后的服务端看完整。CI 接test_anthropic_error_passthrough锁死500 不被改成 422 / 真实信息不丢防止错误掩盖回归。九、小结Anthropic API 兼容层把 GLM-5.2 上游 500 掩盖成 ValidationError 的根因是**message_stream_converter先按成功流校验转换、遇到错误体就 ValidationError且没把上游错误状态码透传**。三层修复第一层detect_error先识别错误体status≥400 或含error字段safe_convert错误原样透传保留真实状态码与信息只有成功流才校验第二层MessageStreamConverter分离错误透传与成功转换错误仍按 Anthropic 风格包装但状态码/信息不掩盖第三层CI 断言守住500 不被改成 422 / 真实信息不丢任何掩盖立即红。落实后vLLM 的 Anthropic 兼容层在后端模型推理 500 时客户端收到的是带真实 500 状态码和真实错误信息的错误事件而不是误导性的 ValidationError。