Prhub

#42454 [Bugfix] Handle real-world gpt-oss tool call output in Harmony parsing

原始 PR 作者 bbrowning 合并时间 2026-05-14 01:54 文件变更 12 提交数 2 评论 10 代码增减 +801 / -50

执行摘要

修复 gpt-oss 模型 bare 工具调用解析丢失 bug

gpt-oss models sometimes diverge from ideal Harmony output in two ways:

  1. Bare function names: Tool call recipients are emitted without the functions. prefix (e.g. get_weather instead of functions.get_weather), particularly in long-context scenarios. Without this fix, bare names are silently dropped in the Chat Completions path and misclassified as MCP tool calls in the Responses API path. 2. Tool calls on any channel: Function tool calls can appear on the analysis channel, not just commentary, and sometimes also appear on other places, like a comment channel. Some code paths accepted any channel, others checked one or more specific channels, causing inconsistent behavior. All paths now allow tool calls on any channel. The fix introduces is_function_recipient() in harmony_utils.py which classifies a recipient as a function call when appropriate.

值得精读。PR 展示了如何在不改动模型输出的前提下,通过工具名称列表和优先级规则健壮解析非标准格式。is_function_recipient 的设计可复用,review 中对边界情况的讨论有参考价值。

讨论亮点
  • 空 frozenset 检查:gemini-code-assist 指出原实现 if function_tool_names: 在空 frozenset 时错误 fallback 到启发式规则,应改为 if function_tool_names is not None:。已在最终代码中修复。
  • 通道限制解除:bbrowning 在评论中解释,测试发现工具调用会出现在 comment 等非标准通道,因此决定所有路径不再限制通道,依赖 is_function_recipient 检测。
  • 代码路径统一:sfeng33 指出所有工具调用分发站点还有进一步统一的空间,当前修复已覆盖,未来可考虑重构。

实现拆解

  1. 新增核心检测函数:在 vllm/entrypoints/openai/parser/harmony_utils.py 中添加 is_function_recipient()extract_function_from_recipient()is_function_recipient 按优先级规则分类 receiver:拒绝空字符串和 Harmony 特殊 token → 接受 functions. 前缀 → 拒绝 assistant、内置工具(python, browser, container)→ 如果 allowed_function_tool_names 不为 None,则仅当 receiver 在集合中才接受 → 最后启发式回退为 True。extract_function_from_recipient 剥离 functions. 前缀。

  2. 重构 Responses Streaming Events:在 vllm/entrypoints/openai/responses/streaming_events.py 中重写 is_mcp_tool_by_namespace()is_function_recipient() 的逆,并修改 emit_content_delta_events()emit_previous_item_done_events(),移除之前硬编码的通道限制,统一通过新函数判断工具调用类型。

  3. 更新流式 Chat 和 Tool Parser:在 stream_harmony.pyopenai_tool_parser.py 中分别调整 extract_harmony_streaming_delta()extract_tool_calls(),使用新函数取代原有的 startswith('functions.') 和通道检查。

  4. 调整 Responses API 输出:在 vllm/entrypoints/openai/responses/harmony.py 中,harmony_to_response_output() 新增可选的 allowed_function_tool_names 参数;在 utils.py 新增 extract_function_tool_names() 辅助函数,用于从请求中提取工具名称集合。

  5. 新增全面测试覆盖:包括单元测试文件 tests/entrypoints/openai/parser/test_harmony_utils.py 中的 TestIsFunctionRecipientTestIsFunctionRecipientWithAllowedNamestests/entrypoints/openai/responses/test_harmony_utils.py 中的 TestHarmonyToResponseOutputWithFunctionToolNames;以及 tests/entrypoints/openai/chat_completion/test_serving_chat_stream_harmony.pytests/tool_parsers/test_openai_tool_parser.py 中的集成测试。252 个单元测试全部通过,76 个集成测试通过(一个 xpassed 与本次变更无关)。

文件 模块 状态 重要度
vllm/entrypoints/openai/parser/harmony_utils.py 工具解析 modified 7.53
vllm/entrypoints/openai/responses/streaming_events.py 流事件 modified 7.09
tests/entrypoints/openai/parser/test_harmony_utils.py 解析测试 modified 7.12
tests/entrypoints/openai/responses/test_harmony_utils.py 响应测试 modified 7.13

关键符号

is_function_recipient extract_function_from_recipient is_mcp_tool_by_namespace harmony_to_response_output extract_tool_calls

关键源码片段

vllm/entrypoints/openai/parser/harmony_utils.py core-logic

新增 is_function_recipient 和 extract_function_from_recipient,是此 bugfix 的核心逻辑

def is_function_recipient(
    recipient: str,
    allowed_function_tool_names: frozenset[str] | None = None,
) -> bool:
    # 拒绝空字符串和 Harmony 特殊 token(例如 <|start|>)
    if not recipient or recipient.startswith('<|'):
        return False
    # functions. 前缀明确表示函数调用,但排除仅有前缀的情况
    if recipient.startswith('functions.'):
        return len(recipient) > len('functions.')
    # assistant 接收器不是工具调用
    if recipient == 'assistant':
        return False
    # 内置工具(python, browser, container)不是函数调用
    if recipient in BUILTIN_TOOL_TO_MCP_SERVER_LABEL:
        return False
    # 检查接收器的第一个段(例如 browser.search -> browser)
    first_segment = recipient.split('.', 1)[0]
    if first_segment in BUILTIN_TOOL_TO_MCP_SERVER_LABEL:
        return False
    # 如果提供了明确的工具名称集合(Responses API),则必须在集合内
    if allowed_function_tool_names is not None:
        return recipient in allowed_function_tool_names
    # 否则启发式回退,假设是函数调用(Chat Completions 路径)
    return True
​
​
def extract_function_from_recipient(recipient: str) -> str:
    # 去除 functions. 前缀以获取纯函数名
    return recipient.removeprefix('functions.')

评论区精华

空 frozenset 误判 heuristic fallback 正确性

gemini-code-assist 指出原实现 `if function_tool_names:` 在空 frozenset 时错误 fallback 到 True,应改为 `if function_tool_names is not None:` 以区分未提供和空列表。

结论:已修复,最终代码使用 `is not None`。 · 已解决

工具调用出现在非标准通道上 设计

bbrowning 注意到实际模型输出中,工具调用会出现在 `comment` 通道而非 `commentary`,因此决定解除所有路径的通道限制。

结论:一致同意解除通道限制,使用 is_function_recipient 作为唯一门控。 · 已解决

代码路径统一的可能性 设计

sfeng33 指出所有工具调用分发站点还有进一步统一的空间,当前修复可接受,未来可重构。

结论:作者认为当前修复已经覆盖,未来可以进一步重构。 · open(非本PR阻塞)

风险与影响

  • 启发式回退风险:当未提供 allowed_function_tool_names 时,任何 bare 名称都被当作函数调用。在 Chat Completions 中原本期望函数调用,影响有限;Responses API 强制要求工具名称列表,避免误分类。
  • 通道完全开放:工具调用可在任何通道触发,模型生成非预期内容可能产生意外工具调用。测试覆盖了异常通道场景。
  • 兼容性:依赖旧通道限制的定制解析行为可能需要更新。未发现突破性变化。
  • 对用户:gpt-oss 用户直接受益,工具调用丢失率显著降低(手动测试中约 50% 的 bare 名称被正确恢复)。
  • 对系统:修改了四条分发路径(Chat 流/非流、Responses 流/非流),新增 801 行代码,大量测试确保回归风险可控。
  • 对团队:该模式(名称列表 + 启发式检测)可推广到其他需要健壮解析的模型。
核心路径变更 启发式回退

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论