执行摘要
- 一句话:修复 /v1/responses 中 function_call_output 缺失 channel/recipient
- 推荐动作:建议尽快合并并发布,因为该修复直接提升 gpt-oss 等依赖 responses API 的工具调用准确率。开发者可关注后续
reasoning 分支健壮性改进以及测试文件合并建议。
功能与动机
PR body 指出:function_call_output 消息未正确设置 channel 和 recipient,导致 Harmony 格式转换后缺失 commentary channel 和 assistant recipient,影响了工具调用响应在聊天记录中的正确展示。该问题在 BFCL 测试中显著降低了 gpt-oss 的准确率。
实现拆解
- 在
vllm/entrypoints/openai/responses/harmony.py 的 response_input_to_harmony 函数中,function_call_output 分支在构造消息后添加 msg.with_channel('commentary') 和 msg.with_recipient('assistant'),使输出与 chat completions 路径保持一致。
- 新增
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 等字段正确。
- 新增
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(模块 入口;类别 source;类型 core-logic;符号 response_input_to_harmony): 核心修复文件:在 response_input_to_harmony 函数的 function_call_output 分支中增加了 with_channel 和 with_recipient 调用,补全 Harmony 消息格式。
tests/entrypoints/openai/parser/test_harmony_render_parity.py(模块 渲染对比;类别 test;类型 test-coverage;符号 _system, TestResponseInputToHarmonyRenderParity, test_user_message, test_assistant_final_message): 新增的跨 API 渲染对比测试,验证 chat completions 路径和 responses 路径对等效输入产生相同的 Harmony 消息和渲染 token 序列,确保 prompt 一致性。
tests/entrypoints/openai/responses/test_response_input_to_harmony.py(模块 单元测试;类别 test;类型 test-coverage;符号 TestResponseInputToHarmonyMessage, test_user_message_string_content, test_no_type_key_defaults_to_message_branch, test_system_message): 新增的 response_input_to_harmony 单元测试,覆盖每个 type 分支以及边界情况(缺失 type、数组 content 等),确保修复正确且不退化。
关键符号:response_input_to_harmony, TestResponseInputToHarmonyRenderParity, TestResponseInputToHarmonyMessage
关键源码片段
vllm/entrypoints/openai/responses/harmony.py
核心修复文件:在 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
新增的跨 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
新增的 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"
评论区精华
风险与影响
- 风险:风险极低:核心改动仅两行,仅影响
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 格式的用户无影响。
- 风险标记:低回归风险, 测试覆盖增强
关联脉络
参与讨论