执行摘要
- 一句话:修复 PoolsideV1 工具解析器的空白处理和工具选择冲突
- 推荐动作:值得精读——该 PR 展示了在工具解析器中处理两种不同 API 协议下的工具形状差异(嵌套 vs 扁平)的兼容模式,以及如何通过 early return 避免 JSON 引导与原生模板的冲突。测试设计也值得参考(分别覆盖 ChatCompletion 和 Responses 的 required/named 路径)。建议关注 #39870 类似的跨 API 兼容性修复模式,并留意未来迁移到统一 parser engine 的演进方向。
功能与动机
PR body 说明“handle the XML tags properly even if JSON is requested, handle whitespace more intelligently”。测试注释具体指出两个 bug:required/named 工具选择时 JSON 引导与 XML 模板冲突,以及字符串参数空白被破坏。这是内部修复的上游贡献。
实现拆解
- 添加导入及类属性:在
vllm/tool_parsers/poolside_v1_tool_parser.py 中导入 ChatCompletionNamedToolChoiceParam 和 ToolChoiceFunction;在类上设置 supports_required_and_named = False。
- 修改
adjust_request:当 request.tools 存在且 tool_choice 为 required 或 ChatCompletionNamedToolChoiceParam/ToolChoiceFunction 时,直接设置 skip_special_tokens=False 并返回,跳过基类的 JSON 引导解码。
- 修改
_is_string_type:使用 getattr(tool, 'function', tool) 统一处理 ChatCompletion 嵌套工具和 Responses 扁平工具,避免 AttributeError。
- 修改
extract_tool_calls:对于字符串类型参数保持原值(不 strip),仅对非字符串类型执行 strip 和反序列化。
- 新增测试文件
tests/tool_parsers/test_poolside_v1_tool_parser.py,覆盖 required/named 跳过结构化输出(ChatCompletion 和 Responses)、字符串空白保留、以及扁平工具形状的提取场景。
关键文件:
vllm/tool_parsers/poolside_v1_tool_parser.py(模块 工具解析;类别 source;类型 core-logic;符号 PoolsideV1ToolParser, supports_required_and_named, adjust_request, extract_tool_calls): 核心源文件,修复了 adjust_request、extract_tool_calls 和 _is_string_type 三个关键方法,兼容 ChatCompletion 和 Responses 两种工具形状。
tests/tool_parsers/test_poolside_v1_tool_parser.py(模块 工具解析;类别 test;类型 test-coverage;符号 _write_file_tool, _responses_write_file_tool, _build_chat_request, _build_responses_request): 新增测试文件,覆盖两个 bug(结构化输出跳过、字符串空白保留)以及 Responses 扁平工具形状,确保修复正确性。
关键符号:adjust_request, extract_tool_calls, _is_string_type
关键源码片段
vllm/tool_parsers/poolside_v1_tool_parser.py
核心源文件,修复了 adjust_request、extract_tool_calls 和 _is_string_type 三个关键方法,兼容 ChatCompletion 和 Responses 两种工具形状。
# _is_string_type 方法兼容 ChatCompletion 与 Responses 工具形状
@staticmethod
def _is_string_type(
tool_name: str,
arg_name: str,
tools: list[Tool] | None,
) -> bool:
if tools is None:
return False
for tool in tools:
# ChatCompletion 工具嵌套在 .function 下,
# Responses FunctionTool 扁平(.name 在顶层)
fn = getattr(tool, "function", tool)
if getattr(fn, "name", None) != tool_name:
continue
params = getattr(fn, "parameters", None)
if params is None:
return False
arg_type = params.get("properties", {}).get(arg_name, {}).get("type", None)
return arg_type == "string"
return False
# adjust_request 方法:required/named 时跳过 JSON 引导
def adjust_request(
self,
request: ChatCompletionRequest | ResponsesRequest
) -> ChatCompletionRequest | ResponsesRequest:
"""
当 tool_choice 为 required 或命名时,直接返回,
避免 super().adjust_request 安装 JSON 引导解码。
这些模型使用 XML 模板,JSON 引导会破坏输出。
"""
if request.tools:
tc = request.tool_choice
if tc == "required" or isinstance(
tc, (ChatCompletionNamedToolChoiceParam, ToolChoiceFunction)
):
request.skip_special_tokens = False
return request
request = super().adjust_request(request)
if request.tools and request.tool_choice != "none":
# 原有逻辑:确保工具调用 token 不被跳过
... # 未变更部分省略
return request
# extract_tool_calls 中字符串处理的关键循环
for key, value in pairs:
arg_key = key.strip()
# 字符串参数保持原文,空白有意义(如代码 / 文件内容)
if self._is_string_type(tc_name, arg_key, request.tools):
arg_val = value
else:
arg_val = self._deserialize(value.strip())
arg_dct[arg_key] = arg_val
tests/tool_parsers/test_poolside_v1_tool_parser.py
新增测试文件,覆盖两个 bug(结构化输出跳过、字符串空白保留)以及 Responses 扁平工具形状,确保修复正确性。
# 构建 ChatCompletion 测试请求的辅助函数
def _write_file_tool() -> dict[str, Any]:
return {
"type": "function",
"function": {
"name": "write_file",
"description": "Write content to a file",
"parameters": {
"type": "object",
"properties": {
"content": {"type": "string"},
"mode": {"type": "integer"},
},
"required": ["content"],
},
},
}
def _make_parser(request: ChatCompletionRequest) -> PoolsideV1ToolParser:
return PoolsideV1ToolParser(_StubTokenizer(), tools=request.tools)
# 测试 required 时跳过结构化输出
def test_required_skips_structured_outputs_chatcompletion() -> None:
request = _build_chat_request(tool_choice="required")
_make_parser(request).adjust_request(request)
assert request.structured_outputs is None
assert request.skip_special_tokens is False
评论区精华
sfeng33 在 review 中指出:_is_string_type 直接访问 tool.function.name 只适用于 ChatCompletion 工具,Responses API 的 FunctionTool 是扁平结构(.name 在顶层),会导致 AttributeError。joerowell 回复已修复,使用 getattr(tool, 'function', tool) 兼容两种形状,并添加了对应的测试。此外,sfeng33 非阻塞地建议将 Poolside 迁移到新的 parser engine(类似 GLM),以便受益于增量流式解析和 schema 感知类型转换。
- Responses 工具形状兼容性 (correctness): 已修复:
_is_string_type 使用 getattr(tool, 'function', tool) 统一处理嵌套和扁平工具;新增 test_responses_extract_tool_calls_with_flat_tools 覆盖。
风险与影响
- 风险:
- 工具形状兼容性:
_is_string_type 的 getattr 修复假设所有工具对象都有 .function 或扁平 .name,若未来出现其他形状可能仍需调整。
- 结构化输出跳过逻辑:
adjust_request 的 early return 只在 request.tools 非空时触发,若 tools 为空但设置了 tool_choice 可能仍走旧路径(但这种情况被调用方排除)。
- 测试覆盖:新增测试覆盖了 ChatCompletion 和 Responses 的 required/named 场景,但未覆盖
auto 或 none 等边角情况(但这些路径未改动)。
- 与旧行为的兼容性:原来
supports_required_and_named 默认 True,现设为 False,可能影响依赖此属性的其他调用方(但该属性仅用于工具解析器选择器)。
- 影响:直接影响使用 PoolsideV1 模型进行工具调用的用户,修复了字符串参数空白丢失和 required/named 工具选择时 JSON 引导破坏输出的问题。ChatCompletion 和 Responses API 均受益。影响范围限于单一模型解析器,代码变更量小(+247/-11),回归风险可控。
- 风险标记:Responses 兼容性风险, 结构化输出跳过逻辑风险
关联脉络
参与讨论