Skip to content

[Rust Frontend] Support --exclude-tools-when-tool-choice-none - #62741

Merged
BugenZhao merged 4 commits into
mainfrom
woosuk/rust-exclude-tools-tool-choice-none
Oct 21, 2026
Merged

BugenZhao merged 4 commits into
mainfrom
woosuk/rust-exclude-tools-tool-choice-none

Conversation

@WoosukKwon

@WoosukKwon WoosukKwon commented Oct 20, 2026 •

Copy link
Copy Markdown
Collaborator

Overview

The Rust frontend now supports --exclude-tools-when-tool-choice-none. Before this change, it rejected the flag at startup. Its default handling of tools for tool_choice="none" now also matches the Python frontend.

Claims

  • --exclude-tools-when-tool-choice-none works with vllm serve (including the Python→Rust --args-json handoff) and with vllm-rs render. When the flag is set and a chat completion request has tool_choice="none", its request-level tools are left out of the prompt.
  • Default behavior change (now matches Python): by default, request tools are rendered into the prompt even when tool_choice="none". Before this change, the HF, DeepSeek (V3/V3.2/V4/V4.1), Harmony and Inkling renderers always dropped them, as if the flag were always on. Kimi K3 already matched Python. tool_choice="none" still disables tool-call parsing, and tool calls that a unified parser (Gemma 4, HunYuan, Inkling, Kimi K3, …) still recognizes are now dropped from the response instead of being returned as tool_calls, matching Python's parser engine.

Validation

Qwen3-0.6B on GB200, vllm serve --enforce-eager --max-model-len 2048. The request has one tool and max_tokens=1; the table shows usage.prompt_tokens:

frontend flag tool_choice="none" + tool no tools
Python (VLLM_USE_RUST_FRONTEND=0) off 127 9
Python on 9 9
Rust, main off 9 9
Rust, main on fails at startup ("not implemented in Rust frontend") –
Rust, this PR off 127 9
Rust, this PR on 9 9
cd rust
cargo fmt --all --check
cargo clippy --workspace --all-targets -- -D warnings  # clean
cargo test --workspace --no-fail-fast

All tests pass except three that also fail on main or for environment reasons in my setup. None of them involve this change:

  • grammar_replay: HF cache permission error while downloading DeepSeek-V4.1-Flash.
  • roundtrip_gemma4_hf: the locally cached tokenizer has no response_template. It fails on main too.
  • timing::tests::does_not_enable_debug_at_info: order-dependent in a full workspace run and passes on its own. It is in the untouched vllm-tracing crate.

New and updated tests:

  • exclude_tools_only_drops_tools_for_tool_choice_none: only an explicit "none" drops tools. "auto" and an unset tool_choice keep them.
  • chat_template_exposes_tools_to_templates_for_any_tool_choice: the HF renderer passes tools to the template for both auto and none.
  • frontend_config_excludes_tools_when_tool_choice_none_from_args_json: the flag survives the --args-json handoff from Python.
  • processor_excludes_request_tools_for_tool_choice_none_when_configured: with the flag on, none drops only request-level tools and keeps developer-message tools; auto keeps both; without the flag, none renders both.
  • tool_choice_none_drops_tool_calls_parsed_by_unified_parsers: a Gemma 4 tool call is returned for auto and dropped for none.

Details

  • Where the flag is applied. Python configures the flag on OnlineRenderer at construction and sets tool_dicts = None for tool_choice == "none". Here it is likewise configured on ChatRequestProcessor at construction (like tool_strict_level) and applied right before rendering, so the chat-completions, /render and tokenize paths all get the same behavior. Request tools are still converted and validated; only request-level tools are dropped, and message-scoped tools are kept, as in Python. The server stores the setting in Config, and vllm-rs render in RenderConfig.
  • Dropping tool calls under none. Unified parsers are built from the request tools regardless of tool_choice. Now that tools are rendered for none, a model may emit a call there, so structured assembly limits surfaced tool calls to zero for none, reusing the existing parallel_tool_calls=false limit (now max_tool_calls: Option<usize>).
  • Renderer change. The renderers decided whether to render tools based on tool_parsing_enabled(), which is false for tool_choice="none". Python's native renderers never look at tool_choice when deciding whether to render tools, so those gates are removed. Without that removal, the flag would have no effect for every renderer except Kimi K3.
  • Not duplicating existing work. No open PR covers this flag. The other unsupported flags mentioned alongside it in the tracking note are out of scope here.
  • Follow-up. The GPT-OSS cases in test_serving_chat.py start the server with this flag. Running them under the Rust frontend in CI would need a larger GPU than the current Rust e2e step uses, so this PR doesn't add them.

Pull Request Checklist
  • I used vLLM's /pr-checklist skill. (Mandatory for agents, optional for humans).

  • AI assistance was used during the creation of this PR.

  • Design Fit: Minimizes impact on core components, reuses existing functionality, and justifies added complexity.

  • Testing and Validation: Validates the change and ensures any added tests are meaningful and reliable, with CI coverage or documented CI resource constraints and validation performed outside CI.

  • Code Quality and Style: Keeps code and comments clear and concise, and updates relevant documentation and examples.

  • Pull Request Contents: Includes a brief summary and relevant links, supports claims with evidence, explains root causes and implementation trade-offs, and follows the contributing guide.

🤖 Generated with Claude Code

Render request tools for tool_choice="none" by default to match the Python
frontend, and drop them only when --exclude-tools-when-tool-choice-none is
set. Previously most Rust renderers always dropped them, and the flag was
rejected as unsupported.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>

@claude claude Bot left a comment

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Claude Code Review

This repository is configured for manual code reviews. Comment @claude review for a one-time review, or @claude review always to subscribe this PR to a review on every future push.

Tip: disable this comment in your organization's Code Review settings.

BugenZhao and others added 2 commits October 21, 2026 06:50
Unified parsers (gemma4, hy, inkling, kimi_k3, muse, hf, beam) are built
from the request tools regardless of `tool_choice`, so a call the model
emits under `tool_choice=none` reached the response as `tool_calls`. Now
that tools are rendered for `none`, this is reachable on the default path.

Express the rule as a limit on surfaced tool calls in structured assembly,
next to the existing `parallel_tool_calls=false` limit: `none` allows zero,
so the call and its markup are parsed and dropped, matching Python's parser
engine.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A4uoBqR3wrXPrDzZLok7Xq
Signed-off-by: Bugen Zhao <i@bugenzhao.com>
…equestProcessor`

The flag was applied to the HTTP DTO in two places (the chat-completions
handler and `lower_chat_request`), threaded as a bool argument and a
parallel `RenderState` field, and it skipped tool conversion and validation
for `none` requests.

Configure it on `ChatRequestProcessor` at construction, like
`tool_strict_level` (Python configures `OnlineRenderer` the same way), and
apply it right before rendering. Every entry point that renders through the
processor gets the same behavior, and request tools are still validated.
Only request-level tools are dropped; message-scoped tools are kept.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01A4uoBqR3wrXPrDzZLok7Xq
Signed-off-by: Bugen Zhao <i@bugenzhao.com>

@BugenZhao BugenZhao left a comment

Copy link
Copy Markdown
Member

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Thanks @WoosukKwon. I added a commit to unify handling for this flag with parallel_tool_calls

@github-project-automation github-project-automation Bot moved this from To Triage to Ready in gpt-oss Issues & Enhancements Oct 21, 2026
@BugenZhao

Copy link
Copy Markdown
Member

/ci run

@github-actions

Copy link
Copy Markdown

✅ Triggered Buildkite CI #96370 for commit 00a0054edbb2.

ℹ️ This PR is 47 commits behind upstream main. Merged upstream main into this branch as 00a0054edbb2 before starting CI. Pull the branch before pushing more changes.

@BugenZhao
BugenZhao merged commit 6ae1051 into main Oct 21, 2026
35 checks passed
@BugenZhao
BugenZhao deleted the woosuk/rust-exclude-tools-tool-choice-none branch October 21, 2026 09:37
bantianyue pushed a commit to bantianyue/vllm that referenced this pull request Oct 28, 2026
…roject#62741)

Signed-off-by: Woosuk Kwon <woosuk@inferact.ai>
Signed-off-by: Bugen Zhao <i@bugenzhao.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
Co-authored-by: Bugen Zhao <i@bugenzhao.com>
Co-authored-by: vllm-agent <claw@inferact.ai>
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

gpt-oss Related to GPT-OSS models rust

Projects

Status: Done

Development

Successfully merging this pull request may close these issues.

3 participants