Prhub

#51144 [Rust Frontend] Support dynamic tools from developer messages

原始 PR 作者 BugenZhao 合并时间 2026-08-11 09:30 文件变更 28 提交数 6 评论 9 代码增减 +686 / -258

执行摘要

Rust 前端新增 ResolvedToolContext,统一动态与请求级工具解析

PR body 明确指出:"The Rust frontend already carries message-scoped function tools on developer messages, but tool choice resolution, parser activation, and structural-tag generation only used request-level tools. Dynamic-only declarations therefore reached some renderers without becoming available to downstream tool handling." 即动态工具只到达了部分 renderer,却未参与 tool_choice 解析、parser 激活与结构约束生成,导致仅通过 developer 消息声明工具(例如把 vendor 的 system.tools 形状映射为带空内容的 developer 消息)时工具不可用。

值得精读。核心看 rust/src/chat/src/request.rsResolvedToolContext::new 的「一次解析、处处消费」模式与错误类型化设计,以及 rust/src/server/src/routes/openai/chat_completions/convert.rs 的入口装配;types.rs 把校验从 validator 下沉到 resolver 的做法可作为多来源配置统一校验的范式参考。

讨论亮点

该 PR 没有代码行级 review 评论(review_comments_count = 0),主要审查来自自动化工具与 maintainer 批准。核心结论:

  • chatgpt-codex-connector[bot] 在作者触发 @codex review 后给出结论:"Didn't find any major issues."
  • mergify[bot] 曾提示 pre-commit 失败,作者按要求修复后重跑 CI;随后多次 /ci run 触发 Buildkite(#82690、#83220)均通过。
  • claude[bot] 因 PR 来自 fork 禁用自动 review,maintainer njhill 直接 APPROVED 并合入。
  • 设计取舍记录(来自 PR body 与实现):系统消息 DTO 刻意保持 content-only,vendor system.tools 形状可映射为带空内容的 developer 消息;AllowedTools 在入口显式拒绝并列出工具名,而不是依赖后续校验兜底。

实现拆解

  1. 数据模型收口:在 rust/src/chat/src/request.rs 中新增 ResolvedToolContext 结构体,保存 initial_tools(请求级、渲染在对话前)、effective_tools(请求级与消息级的有序并集)、tool_choiceparallel_tool_callsChatRequest 原有的 tools/tool_choice/parallel_tool_calls 三个公开字段合并为单个 tool_context,并新增 tools()initial_tools()tool_choice()parallel_tool_calls() 访问器供下游消费。同时给 ChatMessage 增加 declared_tools(),抽取 developer 消息上的非空工具声明。
  2. 解析与校验逻辑ResolvedToolContext::new 按「请求级工具 → 消息声明顺序」拼接 effective_tools,用 HashSet 检测重名并返回新增的 Error::DuplicateToolName;未显式指定 tool_choice 时按 effective_tools 是否为空推导 None/Auto;统一校验 Auto/Required 必须有可用工具(Error::ToolChoiceRequiresTools)、具名 Function 必须命中并集(Error::ToolChoiceFunctionNotFound)。tool_parsing_enabled 改为委托 parsing_enabled()
  3. 消费方适配rust/src/chat/src/output/default/structural_tag.rsapply_structural_tag_constraintstructural_tag_tool_choice 改用 request.tools(),使仅动态声明的工具也能生成 xgrammar 结构标签;rust/src/chat/src/renderer/hf/mod.rsapply_chat_template_inner 中用 to_template_tools(request.tools()) 把有效工具暴露给模板 tools 变量,同时保留 developer 消息上的 tools 字段;rust/src/chat/src/renderer/inkling/mod.rsrendered_tools 删除手工合并逻辑,直接取 request.tools();DeepSeek V3.2/V4、Kimi K3 的编码路径改用新访问器。
  4. 服务入口装配rust/src/server/src/routes/openai/chat_completions/convert.rsprepare_chat_request 中构造 ResolvedToolContext::new 并映射为 chat_submit_errorrust/src/server/src/routes/tokenize/types.rs 同样接入。rust/src/server/src/routes/openai/chat_completions/types.rs 删除 normalize() 中的 tool_choice 默认逻辑与 validate_chat_cross_parameters 中 5 项 tool_choice 校验,全部下沉到 resolver;convert_tool_choice 签名改为非 Option,并对 AllowedTools 显式报错、列出引用工具名。
  5. 测试配套:新增 resolver 单元测试(动态工具默认 Auto、顺序保持、重名拒绝、具名校验)、structural-tag 动态工具测试、Kimi K3 golden fixture(dynamic_only_tool_declare)、DeepSeek V3.2/V4 developer tools fixture(含 renders_developer_tools_like_hf_python 对齐测试)及 HF 渲染器 dynamic_tools_are_exposed_as_effective_template_tools 测试。测试结果 625 passed / 1 skipped,clippy/fmt 通过。
文件 模块 状态 重要度
rust/src/chat/src/request.rs 请求模型 modified 8.83
rust/src/chat/src/output/default/structural_tag.rs 结构约束 modified 7.52
rust/src/server/src/routes/openai/chat_completions/convert.rs 入口转换 modified 7.21
rust/src/server/src/routes/openai/chat_completions/types.rs API 类型 modified 6.89
rust/src/chat/src/renderer/hf/mod.rs 渲染器 modified 7.42
rust/src/chat/src/renderer/inkling/mod.rs 渲染器 modified 5.59
rust/src/server/src/routes/tokenize/types.rs 分词入口 modified 5.59
rust/src/chat/src/renderer/deepseek_v32/fixtures/test_input_developer_tools.json 测试夹具 added 5.29

关键符号

ResolvedToolContext::new ChatMessage::declared_tools ChatRequest::tools ChatRequest::initial_tools ChatRequest::tool_choice ChatRequest::parallel_tool_calls apply_structural_tag_constraint convert_tool_choice rendered_tools

关键源码片段

rust/src/chat/src/request.rs core-logic

核心数据模型变更:新增 ResolvedToolContext 统一解析请求级与消息级工具,ChatRequest 三字段合并为 tool_context,并新增访问器与错误类型。

/// 解析请求级工具与消息级工具,生成统一的工具上下文。
///
/// 顺序约定:请求级工具在前,developer 消息中声明的动态工具按消息
/// 顺序拼接在后;同名工具直接报错而不是静默覆盖,避免下游 parser /
/// structural-tag 出现二义性。
pub fn new(
    messages: &[ChatMessage],
    initial_tools: Vec<ChatTool>,
    requested_tool_choice: Option<ChatToolChoice>,
    parallel_tool_calls: bool,
) -> Result<Self> {
    // 先统计动态工具数量,一次性分配容量,避免逐个 push 时反复扩容
    let dynamic_tool_count = messages
        .iter()
        .filter_map(ChatMessage::declared_tools)
        .map(<[ChatTool]>::len)
        .sum::<usize>();
    let mut effective_tools = Vec::with_capacity(initial_tools.len() + dynamic_tool_count);
    let mut names = HashSet::with_capacity(effective_tools.capacity());    // 用 HashSet 记录已出现名字:请求级与消息级工具统一去重,重复即报错
    for tool in initial_tools
        .iter()
        .chain(messages.iter().filter_map(ChatMessage::declared_tools).flatten())
    {
        if !names.insert(tool.name.clone()) {
            return Err(Error::DuplicateToolName {
                name: tool.name.clone(),
            });
        }
        effective_tools.push(tool.clone());
    }    // 调用方未显式指定 tool_choice 时按工具并集推导:
    // 没有任何可用工具则视为 None,否则默认 Auto(动态工具因此自动激活解析)
    let tool_choice = requested_tool_choice.unwrap_or({
        if effective_tools.is_empty() {
            ChatToolChoice::None
        } else {
            ChatToolChoice::Auto
        }
    });    // 统一校验:Auto / Required 必须有可用的 effective_tools;
    // 具名 Function 必须命中并集中的工具名
    match &tool_choice {
        ChatToolChoice::Auto | ChatToolChoice::Required if effective_tools.is_empty() => {
            return Err(Error::ToolChoiceRequiresTools);
        }
        ChatToolChoice::Function { name } if !names.contains(name) => {
            return Err(Error::ToolChoiceFunctionNotFound { name: name.clone() });
        }
        ChatToolChoice::None
        | ChatToolChoice::Auto
        | ChatToolChoice::Required
        | ChatToolChoice::Function { .. } => {}
    }    Ok(Self {
        initial_tools,
        effective_tools,
        tool_choice,
        parallel_tool_calls,
    })
}
rust/src/server/src/routes/openai/chat_completions/convert.rs entrypoint

Chat Completions 入口在 prepare_chat_request 中装配 ResolvedToolContext,convert_tool_choice 签名收紧并对 AllowedTools 显式报错,是动态工具进入服务端的入口。

/// 把 OpenAI 的 tool_choice 形状转换为 vllm-chat 内部表示。
///
/// 与旧版本不同,这里不再接收 `Option<&ToolChoice>`:缺省推导
/// (无工具为 None、有工具为 Auto)已下沉到 `ResolvedToolContext::new`,
/// 本函数只负责形状转换与显式报错。
fn convert_tool_choice(tool_choice: &ToolChoice) -> Result<ChatToolChoice, ApiError> {
    match tool_choice {
        ToolChoice::Value(ToolChoiceValue::Auto) => Ok(ChatToolChoice::Auto),
        ToolChoice::Value(ToolChoiceValue::None) => Ok(ChatToolChoice::None),
        ToolChoice::Value(ToolChoiceValue::Required) => Ok(ChatToolChoice::Required),
        // 仅支持 function 类型;其他 tool_type 落入兜底分支报错
        ToolChoice::Function { tool_type, function } if tool_type == "function" => {
            Ok(ChatToolChoice::Function {
                name: function.name.clone(),
            })
        }
        // 显式拒绝 allowed_tools,并在报错信息中列出引用到的工具名,
        // 方便调用方定位;此前该分支会掉进兜底报错,语义不变但信息更清晰
        ToolChoice::AllowedTools { tools, .. } => bail_invalid_request!(
            "allowed_tools tool_choice is not supported yet: {}.",
            tools.iter().map(ToolReference::identifier).join(", ")
        ),
        _ => bail_invalid_request!("tool_choice={:?} is not supported yet.", tool_choice),
    }
}

评论区精华

Pre-commit 失败与修复 other

mergify[bot] 提示 pre-commit 检查失败,要求作者执行 `pre-commit run --all-files` 并提交修复;作者随后两次触发 /ci run。

结论:pre-commit 修复后 CI 通过,后续提交经 mergify 合入 main。 · 已解决

Codex 自动审查结论 other

作者评论 `@codex review` 触发 chatgpt-codex-connector[bot] 审查,结论为 "Didn't find any major issues.",并给出 👍 反应。

结论:自动审查无重大问题。 · 已解决

维护者审批与合入 other

claude[bot] 因 PR 来自 fork 自动禁用 review 并提示 maintainer 可手动触发;maintainer njhill 直接 APPROVED(无评论),随后由 mergify 合入。

结论:njhill 批准并合入 main。 · 已解决

系统消息 DTO 保持 content-only 的设计取舍 设计

PR body 说明公共系统消息 DTO 在本范围内保持 content-only,vendor 的 `system.tools` 形状可映射为带空内容的 developer 消息;这避免了 DTO 外扩,由 developer 消息承载动态工具声明。

结论:接受该设计,动态工具统一走 developer 消息路径。 · 已解决

风险与影响

  1. 内部 API 破坏性变更ChatRequest 删除了 tools/tool_choice/parallel_tool_calls 三个公开字段,workspace 内全部调用点已在本 PR 更新,但 vllm-chat/vllm-server crate 的外部使用者需迁移到 tool_context
  2. 默认语义变化rust/src/server/src/routes/openai/chat_completions/convert.rs 测试显示,无 request-level 工具且未指定 tool_choice 时,resolved 值由原 Auto 变为 None(更符合 Python 语义),依赖旧默认值的调用方需注意。
  3. 错误路径收紧:重名工具从静默接受变为 DuplicateToolName 请求错误,经 chat_submit_error 以 400 返回,是新增的失败路径;types.rs 中原 AllowedTools 校验实为死代码(convert 层早已拒绝),删除无回归风险。
  4. 影响范围:改动集中于 Rust chat/server,Python 侧与其它后端不受影响;测试覆盖充分但网络依赖的 Nemotron tokenizer 测试被排除在外。

对调用方,动态工具(只存在于 developer 消息)现在能正常参与 tool_choice 解析、parser 激活与 xgrammar 结构约束,修复了「渲染中可见但下游不识别」的静默割裂,Kimi K3 场景下 tool_choice=none 与 dynamic-only 声明均有 golden 覆盖。对系统,工具状态在入口一次性解析,渲染/解析/约束/回复组装共享同一快照,消除多处独立判定的不一致风险。对团队,为后续 Rust 前端新增 renderer 或后端预留了统一工具上下文,DeepSeek V3.2/V4、Harmony、Inkling、Kimi K3 渲染行为统一以 tool_context 为准。

核心数据结构变更 内部 API 破坏性变更 默认语义变化 错误路径收紧

关联 Issue

未识别关联 Issue

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

完整报告

参与讨论