Prhub

#35540 [Bugfix] Fix empty channel/recipient in harmony for /v1/responses

原始 PR 作者 kg6-sleipnir 合并时间 2026-05-12 16:45 文件变更 3 提交数 5 评论 14 代码增减 +768 / -0

执行摘要

修复 /v1/responses 中 function_call_output 缺失 channel/recipient

PR body 指出:function_call_output 消息未正确设置 channel 和 recipient,导致 Harmony 格式转换后缺失 commentary channel 和 assistant recipient,影响了工具调用响应在聊天记录中的正确展示。该问题在 BFCL 测试中显著降低了 gpt-oss 的准确率。

建议尽快合并并发布,因为该修复直接提升 gpt-oss 等依赖 responses API 的工具调用准确率。开发者可关注后续 reasoning 分支健壮性改进以及测试文件合并建议。

讨论亮点
  • gemini-code-assist[bot] 建议将 reasoning 分支的 assert len(content)==1 改为更健壮的拼接处理,但作者 kg6-sleipnir 认为超出当前 scope,未采纳。
  • bbrowning 批准 PR,指出 chat completions 路径此前已修复,此 PR 将 responses 路径对齐;同时提议后续合并测试文件以消除冗余。
  • chaunceyjiang 询问最新代码是否仍存在该问题,作者确认问题仍在,最后批准合并。

实现拆解

  1. vllm/entrypoints/openai/responses/harmony.pyresponse_input_to_harmony 函数中,function_call_output 分支在构造消息后添加 msg.with_channel('commentary')msg.with_recipient('assistant'),使输出与 chat completions 路径保持一致。
  2. 新增 tests/entrypoints/openai/responses/test_response_input_to_harmony.py,覆盖 response_input_to_harmony 所有类型分支(message、reasoning、function_call_output、function_call 等),验证 role、channel、recipient、content 等字段正确。
  3. 新增 tests/entrypoints/openai/parser/test_harmony_render_parity.py,针对每种消息场景(user、assistant、reasoning、function_call、function_call_output、组合),从 chat completions 和 responses 两条路径分别生成 Harmony 消息,断言它们内容一致,且 render_for_completion 输出相同 token 序列。
文件 模块 状态 重要度
vllm/entrypoints/openai/responses/harmony.py 入口 modified 4.82
tests/entrypoints/openai/parser/test_harmony_render_parity.py 渲染对比 added 8.14
tests/entrypoints/openai/responses/test_response_input_to_harmony.py 单元测试 added 8.14

关键符号

response_input_to_harmony TestResponseInputToHarmonyRenderParity TestResponseInputToHarmonyMessage

关键源码片段

vllm/entrypoints/openai/responses/harmony.py core-logic

核心修复文件:在 `response_input_to_harmony` 函数的 `function_call_output` 分支中增加了 `with_channel` 和 `with_recipient` 调用,补全 Harmony 消息格式。

    elif response_msg["type"] == "function_call_output":
        call_id = response_msg["call_id"]
        call_response: ResponseFunctionToolCall | None = None
        # 从历史响应中反向查找匹配的 function call,获取函数名
        for prev_response in reversed(prev_responses):
            if (
                isinstance(prev_response, ResponseFunctionToolCall)
                and prev_response.call_id == call_id
            ):
                call_response = prev_response
                break
        if call_response is None:
            raise ValueError(f"No call message found for {call_id}")
        # 构造 Tool 角色消息,作者为 functions.<name>
        msg = Message.from_author_and_content(
            Author.new(Role.TOOL, f"functions.{call_response.name}"),
            response_msg["output"],
        )
        # 修复:添加 channel 和 recipient,与 chat completions 路径对齐
        msg = msg.with_channel("commentary")
        msg = msg.with_recipient("assistant")
tests/entrypoints/openai/parser/test_harmony_render_parity.py test-coverage

新增的跨 API 渲染对比测试,验证 chat completions 路径和 responses 路径对等效输入产生相同的 Harmony 消息和渲染 token 序列,确保 prompt 一致性。

def test_reasoning_item(self):
    # chat completions 路径:assistant 消息仅含 reasoning 字段,无 content
    chat_msgs = parse_chat_input_to_harmony_message(
        {
            "role": "assistant",
            "reasoning": "I should call get_weather.",
            "content": "",
        }
    )
    # responses 路径:type=reasoning 的输入项
    resp_msgs = [
        response_input_to_harmony(
            {
                "type": "reasoning",
                "content": [
                    {"type": "reasoning_text", "text": "I should call get_weather."}
                ],
            },
            prev_responses=[],
        )
    ]
    expected = [
        {
            "role": "assistant",
            "channel": "analysis",
            "content": "I should call get_weather.",
        }
    ]
    verify_harmony_messages(chat_msgs, expected)
    verify_harmony_messages(resp_msgs, expected)
    # 最终渲染出的 token 序列必须完全一致
    assert render_for_completion([_system()] + chat_msgs) == render_for_completion(
        [_system()] + resp_msgs
    )
tests/entrypoints/openai/responses/test_response_input_to_harmony.py test-coverage

新增的 `response_input_to_harmony` 单元测试,覆盖每个 type 分支以及边界情况(缺失 type、数组 content 等),确保修复正确且不退化。

def test_assistant_message_gets_final_channel(self):
    # type="message", role="assistant" 应自动获得 final channel
    msg = response_input_to_harmony(
        {"type": "message", "role": "assistant", "content": "The answer is 42."},
        prev_responses=[],
    )
    assert msg.author.role == Role.ASSISTANT
    assert msg.channel == "final"
    assert msg.content[0].text == "The answer is 42."def test_reasoning_gets_analysis_channel(self):
    # type="reasoning" 应获得 analysis channel
    msg = response_input_to_harmony(
        {
            "type": "reasoning",
            "content": [{"type": "reasoning_text", "text": "Thinking hard."}],
        },
        prev_responses=[_REASONING_ITEM],
    )
    assert msg.channel == "analysis"

评论区精华

reasoning 分支 assert 健壮性 正确性

gemini-code-assist[bot] 指出 `assert len(content)==1` 可能因多 content 元素导致 AssertionError,建议改为拼接所有 text。

结论:未采纳,作者认为超出当前 scope,保留原写法。 · unresolved

测试文件合并建议 测试

bbrowning 注意到已有相关测试在 tests/entrypoints/openai/parser/test_harmony_utils.py 中,建议后续合并以减少重复。

结论:暂不合并,优先修复问题,后续再优化测试组织。 · approved

风险与影响

风险极低:核心改动仅两行,仅影响 function_call_output 消息的处理路径;两条新增测试套件覆盖了所有 type 分支和跨 API 渲染一致性,回包问题可以尽早发现。但 reasoning 分支中的 assert len(content)==1 在面对多 content 元素时仍可能崩溃,不过此问题已超出本次修复范围。

影响范围集中在 /v1/responses 端点使用 Harmony 格式的场景(gpt-oss 等工具调用服务)。修复后 function_call_output 消息将正确携带 commentary channel 和 assistant recipient,使得客户端解析聊天历史时不再丢失消息类型,显著提升函数调用链的准确性。对不使用 responses API 或 Harmony 格式的用户无影响。

低回归风险 测试覆盖增强

关联 Issue

未识别关联 Issue

当前没有检测到明确关联的 Issue 链接,后续同步到相关引用后会出现在这里。

完整报告

参与讨论