# PR #43016 完整报告

- 仓库：`vllm-project/vllm`
- 标题：[ROCm][CI] Stabilize 400 error return code for invalid schema inputs
- 合并时间：2026-05-24 18:06
- 原文链接：http://prhub.com.cn/vllm-project/vllm/pull/43016

---

# 执行摘要

- 一句话：修复无效输入逃逸为 500，统一返回 400 错误
- 推荐动作：值得精读。该 PR 展示了如何在 post-Pydantic 阶段捕获验证错误并映射到合适 HTTP 状态码，涉及 catch-all vs local exception handling 的设计权衡，对入口层开发者有参考价值。

# 功能与动机

PR body 指出 Schemathesis 生成的畸形但可解析请求会穿透 Pydantic 模型，在后续 conversation 渲染、采样处理、token 验证路径中引发未捕获异常，返回 500 或超时。这是客户端输入错误，应返回 HTTP 400。该问题在 ROCm CI 中因服务器启动更慢而更频繁暴露。

# 实现拆解

1. 在 `chat_utils.py` 的 `_postprocess_messages` 中增加 tool_calls 结构和类型校验，不符合则抛 VLLMValidationError。
2. 在 `batch_serving.py` 的 `chat_completion_full_generator_batch` 中将 role 计算移入每个 prompt 循环，正确处理 batch 内不同角色。
3. 在 `disagg/serving.py` 的 `serve_tokens` 中添加 max_num_seqs 上限检查和 msgspec 序列化检查，失败返回 400。
4. 在 `v1/utils.py` 中修复 shutdown 超时逻辑，timeout=None 时不再设为 0 再取 max，直接使用 5 秒保底。
5. 在协议模型（`protocol.py`）中给 batch messages 增加 min_length=1 约束。
测试配套：更新 weight_transfer 测试 mock 适配新签名，添加 MockSchedulerConfig，补充 shutdown 超时测试。

关键文件：
- `vllm/entrypoints/chat_utils.py`（模块 消息处理；类别 source；类型 core-logic；符号 _postprocess_messages）: 核心验证增强：新增 tool_calls 结构和类型校验，涵盖绝大部分助手消息的非法输入场景。
- `vllm/entrypoints/serve/disagg/serving.py`（模块 分片服务；类别 source；类型 dependency-wiring；符号 serve_tokens）: 分片服务入口增加采样参数前置检查，防止无效参数到达引擎。
- `vllm/entrypoints/openai/chat_completion/batch_serving.py`（模块 批处理；类别 source；类型 core-logic；符号 chat_completion_full_generator_batch）: 修复 batch 对话中 role 计算时机，确保每个 choice 的角色正确。
- `vllm/v1/utils.py`（模块 工具函数；类别 source；类型 core-logic；符号 shutdown）: 关闭超时逻辑修正，避免因 timeout=0 导致进程未完全终止。
- `vllm/entrypoints/openai/chat_completion/protocol.py`（模块 协议模型；类别 source；类型 core-logic；符号 BatchChatCompletionRequest）: 增加 batch messages 非空约束，拒绝空对话。
- `tests/entrypoints/weight_transfer/test_weight_transfer_llm.py`（模块 权重传输测试；类别 test；类型 test-coverage；符号 MockWeightTransferEngine.__init__, mock_create_engine）: 适配上游接口变更：WeightTransferEngine 签名增加 model 参数，调整 mock 保持测试通过。
- `tests/entrypoints/openai/completion/test_shutdown.py`（模块 关闭测试；类别 test；类型 test-coverage）: 补充超时行为测试，确保 shutdown 函数在 timeout=None 时至少等待 5 秒。

关键符号：_postprocess_messages, serve_tokens, chat_completion_full_generator_batch, shutdown, MockWeightTransferEngine.__init__, mock_create_engine

## 关键源码片段

### `vllm/entrypoints/chat_utils.py`

核心验证增强：新增 tool_calls 结构和类型校验，涵盖绝大部分助手消息的非法输入场景。

```python
def _postprocess_messages(messages: list[ConversationMessage]) -> None:
    '''后处理消息：将 tool_call 参数从 JSON 字符串解析为 dict，并校验结构'''
    for message in messages:
        if message['role'] == 'assistant' and 'tool_calls' in message:
            tool_calls = message.get('tool_calls')
            if not isinstance(tool_calls, list):
                continue

            if len(tool_calls) == 0:
                # 空 tool_calls 应移除，使模板走普通助手路径
                message.pop('tool_calls', None)
                continue

            for item in tool_calls:
                # 校验每个 tool_call 条目必须是 dict 类型
                if not isinstance(item, dict):
                    raise VLLMValidationError(
                        'assistant tool_calls 条目必须是对象。',
                        parameter='tool_calls',
                    )

                function = item.get('function')
                # 仅支持 function 类型的 tool_calls
                if item.get('type', 'function') != 'function' or not isinstance(
                    function, dict
                ):
                    raise VLLMValidationError(
                        'chat completions 仅支持 function 类型的 assistant tool_calls。',
                        parameter='tool_calls',
                    )

                # 将 arguments 从 JSON 字符串解析为字典（如果还不是）
                if content := function.get('arguments'):
                    if not isinstance(content, (dict, list)):
                        function['arguments'] = json.loads(content)
                else:
                    function['arguments'] = {}

```

### `vllm/entrypoints/serve/disagg/serving.py`

分片服务入口增加采样参数前置检查，防止无效参数到达引擎。

```python
async def serve_tokens(self, request, raw_request=None):
    # ... 前置检查 ...
    sampling_params = request.sampling_params
    max_num_seqs = self.engine_client.vllm_config.scheduler_config.max_num_seqs
    if sampling_params.n > max_num_seqs:
        return self.create_error_response(
            f'sampling_params.n must be at most the server max_num_seqs '
            f'({max_num_seqs}), got {sampling_params.n}.'
        )
    try:
        msgspec.msgpack.encode(sampling_params)
    except (OverflowError, TypeError, ValueError) as e:
        return self.create_error_response(e)
    # ... 继续构建 engine_input ...

```

### `vllm/entrypoints/openai/chat_completion/batch_serving.py`

修复 batch 对话中 role 计算时机，确保每个 choice 的角色正确。

```python
# 在 chat_completion_full_generator_batch 方法中，原代码在开头一次计算 role
# 修改后，在每个 prompt 输出循环内逐步计算
for prompt_idx in range(len(generators)):
    final_res = final_results.get(prompt_idx)
    # ... 前置处理 ...

    for output in final_res.outputs:
        # ... logprobs, reasoning 等 ...
        # 动态获取当前 prompt 的 role
        role = (
            self.response_role
            if request.add_generation_prompt
            else request.messages[prompt_idx][-1]['role']
        )
        message = ChatMessage(role=role, reasoning=reasoning, content=content)
        # ... 构建 choice ...

```

# 评论区精华

· 设计权衡：DarkLight1337 对在 serving 方法中本地 try-except 捕获 ValueError 提出质疑，认为应依赖全局 exception_handler 避免重复代码。AndreasKaratzas 解释初衷是显式声明预期错误，最终接受建议移除本地 catch，改用全局处理器。
· 性能考量：DarkLight1337 指出在 kv_transfer 清理包装器中每请求尝试 msgpack 编码会引入开销，AndreasKaratzas 确认并移除该 eager check。

- Post-Pydantic 错误处理方式 (design): 采用全局 catch-all 异常处理器
- KV transfer 参数验证开销 (performance): 移除，仅在需要时验证

# 风险与影响

- 风险：· API 响应码变更：部分之前返回 500 的请求现在返回 400，可能被客户端或监控视为行为变化，但符合 HTTP 规范。
· shutdown 超时行为变化：timeout=None 时从 0→5 秒，所有调用方至少等待 5 秒，减少意外强制杀进程。
· 测试 mock 签名变更：MockWeightTransferEngine 和 mock_create_engine 增加 model 参数，外部直接调用需同步更新，已在 PR 中修复。
· 额外序列化开销：serve_tokens 中新增 msgpack.encode 对成功请求也有一次调用，但采样参数小，影响可忽略。

- 影响：用户：无效请求将获得更准确的 400 错误，而不是 500 或超时，有利于调试和熔断。系统：API 边界更加健壮，减少因畸形请求导致的服务器不稳定。团队：ROCm CI Entrypoints 测试组 flakiness 预期降低，但部分 mock 签名变更需其他开发者留意。
- 风险标记：API 响应码变更 , 关闭超时行为变化 , 测试 mock 依赖接口变更

# 关联脉络

- 暂无明显关联 PR