Prhub

#47024 [Frontend] Support OpenAI Responses API namespace tools

原始 PR 作者 zhongjing123 合并时间 2026-07-06 14:21 文件变更 8 提交数 4 评论 11 代码增减 +383 / -22

执行摘要

支持 Responses API namespace 工具名恢复

关联 Issue #46737 报告:Codex Desktop 使用 /v1/responses API 发送 namespace 类型工具时,返回的 function_call.name 是展平后的 mcp__computer_use__get_app_state,Codex 无法执行该调用。需要在 Responses API 输出中保持外部原始工具名,同时内部仍使用展平名进行 prompt 和解析。

建议精读,特别是 vllm/tool_parsers/utils.py 中的映射函数设计。该 PR 解决了真实的客户端兼容性问题,设计上清晰地分离了内部展平名与外部原始名,是一个值得学习的前端协议适配案例。审核者 chaunceyjiang 的 review 提出了关键的代码组织建议,也值得关注。

讨论亮点

审核者 chaunceyjiang 提出关键设计建议:

  • 将部分工具映射逻辑从 vllm/entrypoints/openai/responses/utils.py 移至 vllm/tool_parsers/utils.py 以集中管理(已采纳)。
  • 要求 find_tool_name() 和 find_tool_properties() 支持递归检查 namespace 工具的内部函数(已在最终实现中覆盖)。
  • 对 iter_response_function_tool_dicts() 提出简化建议:去掉单独的 FunctionTool 分支(已在后续提交中调整)。
  • 审核者本地测试了 Qwen3.5 和 GLM-5.2,初期 GLM-5.2 异常,最终两个模型均验证通过。
  • 建议在 test_function_call.py 中增加 E2E 测试,作者同意在后续 PR 添加。

实现拆解

  1. 在 vllm/tool_parsers/utils.py 中新增核心工具函数:ResponsesToolCallName dataclass 用于存储映射关系;flat_namespace_tool_name() 构建内部展平名;iter_response_function_tool_info() 和 iter_response_function_tool_dicts() 展开 NamespaceTool 为平面列表;build_responses_tool_call_name_map() 建立从展平名到原始名的映射字典;resolve_responses_tool_call_name() 反向解析工具名。同时修改 find_tool_properties 和 find_tool_name 以支持 namespace 工具。

  2. 在 vllm/entrypoints/openai/responses/utils.py 中修改 build_response_output_items() 使其接受 tools 参数并利用映射恢复工具名;修改 _construct_message_from_response_item() 在构建历史 function_call 时使用展平名(内部表示);修改 extract_function_tool_names() 和 construct_tool_dicts() 支持 namespace 类型。

  3. 在 vllm/entrypoints/openai/responses/streaming_events.py 中修改 SimpleStreamingState 新增 tool_call_namespace 字段;emit_simple_tool_call_open() 和 emit_simple_tool_call_done() 传递 namespace;SimpleStreamingEventProcessor.init() 接受 tools 并构建映射,在 open handler 中通过 resolve_responses_tool_call_name() 还原工具名。

  4. 在 vllm/entrypoints/openai/responses/protocol.py 中修改 check_tool_usage 类方法,使 named_tool_choice 验证支持 namespace 工具的内部展平名。

  5. vllm/entrypoints/openai/responses/serving.py 和 context.py 做最小适配传递 tools 参数。

  6. 新增回归测试 tests/entrypoints/openai/responses/test_namespace_tool_separator.py 覆盖非流式和流式场景;tests/tool_parsers/test_glm47_moe_tool_parser.py 增加 namespace 工具的 round-trip 测试验证从解析到输出的全流程。

文件 模块 状态 重要度
vllm/tool_parsers/utils.py 工具解析器 modified 8.84
tests/entrypoints/openai/responses/test_namespace_tool_separator.py 测试 added 7.55
vllm/entrypoints/openai/responses/streaming_events.py 流式处理 modified 6.92
vllm/entrypoints/openai/responses/utils.py 工具函数 modified 6.78
vllm/entrypoints/openai/responses/protocol.py 协议层 modified 6.08
tests/tool_parsers/test_glm47_moe_tool_parser.py 测试 modified 6.61
vllm/entrypoints/openai/responses/serving.py 服务层 modified 4.99

关键符号

flat_namespace_tool_name iter_response_function_tool_info iter_response_function_tool_dicts build_responses_tool_call_name_map resolve_responses_tool_call_name build_response_output_items extract_function_tool_names construct_tool_dicts emit_simple_tool_call_open emit_simple_tool_call_done check_tool_usage

关键源码片段

vllm/tool_parsers/utils.py core-logic

核心映射逻辑所在,新增了 ResponsesToolCallName、flat_namespace_tool_name、iter_response_function_tool_info、iter_response_function_tool_dicts、build_responses_tool_call_name_map、resolve_responses_tool_call_name 等关键函数,是整个 PR 的枢纽。

# vllm/tool_parsers/utils.py 新增核心映射from dataclasses import dataclass
from openai.types.responses import FunctionTool, NamespaceTool# 常量:内部展平分隔符
_NAMESPACE_TOOL_SEPARATOR = "__"
​
​
@dataclass(frozen=True)
class ResponsesToolCallName:
    """存储一个工具调用的外部名称和可选的 namespace。"""
    name: str
    namespace: str | None = None
​
​
def flat_namespace_tool_name(namespace: str, name: str) -> str:
    """将 namespace 和内部工具名拼接为内部展平名,例如 `mcp__computer_use__get_app_state`。"""
    return f"{namespace}{_NAMESPACE_TOOL_SEPARATOR}{name}"
​
​
def iter_response_function_tool_info(
    tool: ResponsesTool,
) -> list[tuple[str, dict[str, Any] | None]]:
    """从单个 tool 中提取所有函数工具的信息(name, parameters)。    遇到 FunctionTool 直接返回其自身;遇到 NamespaceTool 则遍历其内部的 tools 列表,
    将每个嵌套函数的 name 展平后返回。非函数工具返回空列表。
    """
    if isinstance(tool, FunctionTool):
        return [(tool.name, tool.parameters)]
    if not isinstance(tool, NamespaceTool):
        return []
    namespace = tool.name
    return [
        (flat_namespace_tool_name(namespace, nested.name), nested.parameters)
        for nested in tool.tools
        if nested.type == "function"
    ]
​
​
def build_responses_tool_call_name_map(
    tools: list[ResponsesTool] | None,
) -> dict[str, ResponsesToolCallName]:
    """构建从展平名到外部原始名的映射字典。    仅处理 NamespaceTool,非 namespace 工具不会出现在映射中(保持原样)。
    """
    if not tools:
        return {}
    name_map: dict[str, ResponsesToolCallName] = {}
    for tool in tools:
        if not isinstance(tool, NamespaceTool):
            continue
        namespace = tool.name
        for nested in tool.tools:
            if nested.type != "function":
                continue
            flat_name = flat_namespace_tool_name(namespace, nested.name)
            name_map[flat_name] = ResponsesToolCallName(
                name=nested.name, namespace=namespace
            )
    return name_map
​
​
def resolve_responses_tool_call_name(
    name: str,
    tools: list[ResponsesTool] | None = None,
    tool_call_name_map: dict[str, ResponsesToolCallName] | None = None,
) -> ResponsesToolCallName:
    """反向解析:给定一个展平的工具名(或原始名),返回外部 ResponsesToolCallName。    如果在映射中找到,则拆分为 name 和 namespace;否则视为普通工具,namespace 为 None。
    允许传入预制映射或自动从 tools 构建。
    """
    name_map = tool_call_name_map
    if name_map is None:
        name_map = build_responses_tool_call_name_map(tools)
    return name_map.get(name, ResponsesToolCallName(name=name))
tests/entrypoints/openai/responses/test_namespace_tool_separator.py test-coverage

新增的回归测试文件,覆盖非流式和流式场景,验证工具名正确恢复以及 namespace 属性正确设置。

# tests/entrypoints/openai/responses/test_namespace_tool_separator.pyimport json
import openai
import pytestMODEL_NAME = "Qwen/Qwen3-1.7B"
NAMESPACE = "mcp__computer_use"
TOOL_NAME = "get_app_state"
FLAT_TOOL_NAME = f"{NAMESPACE}__{TOOL_NAME}"# 模拟 Codex 发送的 namespace tools 结构
tools = [
    {
        "type": "namespace",
        "name": NAMESPACE,
        "description": "Computer control tools.",
        "tools": [
            {
                "type": "function",
                "name": TOOL_NAME,
                "description": "Get the current state of a desktop application.",
                "parameters": {
                    "type": "object",
                    "properties": {"app": {"type": "string"}},
                    "required": ["app"],
                    "additionalProperties": False,
                },
            }
        ],
    }
]prompt = [{"role": "user", "content": "Use the computer app state tool to inspect Google Chrome."}]
​
​
def _assert_namespace_tool_call(tool_call) -> None:
    """统一断言:工具名应为原始内部名(如 'get_app_state'),而非展平名;namespace 属性正确。"""
    assert tool_call.type == "function_call"
    assert tool_call.name == TOOL_NAME
    assert tool_call.namespace == NAMESPACE
    assert tool_call.name != FLAT_TOOL_NAME
    args = json.loads(tool_call.arguments)
    assert args["app"]
​
​
@pytest.mark.asyncio
async def test_namespace_tool_separator(client, model_name):
    """非流式场景:返回的 tool_call 应具有原始 name 和 namespace。"""
    response = await client.responses.create(
        model=model_name,
        input=prompt,
        tools=tools,
        temperature=0.0,
    )
    tool_call = next(out for out in response.output if out.type == "function_call")
    _assert_namespace_tool_call(tool_call)
​
​
@pytest.mark.asyncio
async def test_namespace_tool_separator_streaming(client, model_name):
    """流式场景:verify both output_item.added and done events have correct names."""
    stream = await client.responses.create(
        model=model_name, input=prompt, tools=tools, temperature=0.0, stream=True
    )
    events = [event async for event in stream]
    added_call = next(
        event.item for event in events
        if event.type == "response.output_item.added"
        and getattr(event.item, "type", None) == "function_call"
    )
    done_call = next(
        event.item for event in events
        if event.type == "response.output_item.done"
        and getattr(event.item, "type", None) == "function_call"
    )
    assert added_call.name == TOOL_NAME
    assert added_call.namespace == NAMESPACE
    _assert_namespace_tool_call(done_call)
vllm/entrypoints/openai/responses/streaming_events.py core-logic

流式事件处理模块,修改了 SimpleStreamingState、emit_simple_tool_call_open/done 和 SimpleStreamingEventProcessor,确保 namespace 在流式事件中正确传递。

# vllm/entrypoints/openai/responses/streaming_events.py 部分关键改动from vllm.entrypoints.openai.responses.utils import (
    build_responses_tool_call_name_map,
    resolve_responses_tool_call_name,
)@dataclass
class SimpleStreamingState:
    # ... 原有字段 ...
    tool_call_namespace: str | None = None # 新增:用于存储当前流式工具调用的 namespace
​
​
def emit_simple_tool_call_open(
    state: SimpleStreamingState,
    name: str,
    index: int | None,
    namespace: str | None = None, # 新增参数
) -> list[StreamingResponsesResponse]:
    # ... 设置 state ...
    state.tool_call_name = name
    state.tool_call_namespace = namespace # 保存 namespace
    # ... 构造 added 事件时携带 namespace
    return [ResponseOutputItemAddedEvent(
        ...,
        item=ResponseFunctionToolCall(
            id=..., call_id=..., name=name, namespace=namespace,
            arguments="", status="in_progress",
        ),
    )]
​
​
def emit_simple_tool_call_done(state, ...) -> list[StreamingResponsesResponse]:
    # ... 构造 done 事件时使用 state.tool_call_namespace
    item=ResponseFunctionToolCall(
        type="function_call",
        name=state.tool_call_name,
        namespace=state.tool_call_namespace,
        arguments=state.accumulated_text,
        status="completed",
        id=state.current_item_id,
        call_id=...,
    )
    state.tool_call_namespace = None # 重置class SimpleStreamingEventProcessor:
    def __init__(self, state=None, tools=None):
        self.state = state or SimpleStreamingState()
        self.tool_call_name_map = build_responses_tool_call_name_map(tools)
​
    def open(self, delta_message):
        if target_state == _StateType.TOOL_CALL:
            call_name = resolve_responses_tool_call_name(
                tool_call.function.name,
                tool_call_name_map=self.tool_call_name_map,
            )
            return handlers.open_fn(
                self.state,
                call_name.name,
                tool_call.index,
                call_name.namespace,
            )

评论区精华

代码组织:将工具映射逻辑移至 vllm/tool_parsers/utils.py 设计

审核者 chaunceyjiang 建议将部分工具映射逻辑从 responses/utils.py 移至 tool_parsers/utils.py 以集中管理,便于 find_tool_properties 和 find_tool_name 复用。

结论:已采纳,新函数全部位于 vllm/tool_parsers/utils.py。 · 已解决

find_tool_properties 和 find_tool_name 需要支持 namespace 正确性

审核者指出 find_tool_properties 和 find_tool_name 只支持 FunctionTool,需要添加 NamespaceTool 支持,使用内部展平名查找。

结论:已通过 iter_response_function_tool_info 扩展支持,在 find_tool_properties 循环中增加对 NamespaceTool 的处理。 · 已解决

iter_response_function_tool_dicts 处理 FunctionTool 分支合理性 设计

审核者建议简化 iter_response_function_tool_dicts:移除单独的 FunctionTool 分支,因为 FunctionTool 是 NamespaceTool 的子类?实际不是,但逻辑上可以精简。最终版本采用了审核者的建议 diff。

结论:已按建议简化,移除了 FunctionTool 分支。 · 已解决

E2E 测试的进一步补充 测试

审核者测试后确认 Qwen3.5 和 GLM-5.2 均可正常工作,但建议在 tests/entrypoints/openai/responses/test_function_call.py 中增加对应的 E2E 测试。

结论:作者同意在后续 PR 中添加。 · partially_resolved

风险与影响

  1. 兼容性风险:修改了 Responses API 多个核心函数的行为(build_response_output_items、emit_simple_tool_call_*),若映射逻辑未正确覆盖所有调用路径,可能导致非 namespace 工具遭受回归(如 name 字段意外被修改)。但设计通过分离内部展平名与外部原始名、仅在有映射时修改名称,降低了影响。
  2. 流式事件一致性风险:output_item.added 和 output_item.done 事件中的 name 必须一致,测试验证了这点。
  3. 历史回放风险:当 replay 带有 namespace 的 function_call 历史时,需要反向展平以匹配内部格式,_construct_message_from_response_item 中进行了处理。
  4. GLM-5.2 工具解析器适配风险:早期测试显示 GLM-5.2 异常,但后续修复后通过。对非 Qwen 模型可能仍有边界情况。
  5. 配置键调整风险:check_tool_usage 中的工具名集合现在同时包含原始名和展平名,可能影响输入验证逻辑。

用户影响:使用 Responses API namespace 工具(如 Codex 的 Computer Use)的客户端将正常工作,获取到可执行的原始工具名和 namespace 属性。对其他类型工具(function、web_search 等)无影响。系统影响:核心工具映射逻辑集中在 vllm/tool_parsers/utils.py,新增了约 130 行纯函数,依赖清晰;流式处理模块新增了 namespace 字段,状态机复杂度略有增加。团队影响:明确了 namespace 工具在 Responses 管道中的处理策略,后续添加新的工具类型可参考此模式。

核心路径变更 多模型适配差异 流式 / 非流式一致性 GLM-5.2 初始异常

关联 Issue

#46737 [Bug]: Responses API with Codex namespace tools returns flattened function names, causing unsupported call

完整报告

参与讨论