[Frontend] Let an unset --tool-strict-level fall back to the tool parser's default (#61328)

Signed-off-by: Bugen Zhao <i@bugenzhao.com>
Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
Bugen Zhao
2026-10-15 15:48:38 +08:00
committed by GitHub
co-authored by Claude Opus 5.5
parent 91ed2859c7
commit 8fa5ae844a
26 changed files with 181 additions and 90 deletions
+3 -1
View File
@@ -146,7 +146,7 @@ Most OpenAI-compatible clients and agent frameworks never set `strict` on their
| Value | Behavior |
| --- | --- |
| `auto` (default) | Follow the request's tool choice and per-tool strictness. Required/named choices activate structural tags; `tool_choice="auto"` activates them when at least one tool sets `strict: true`. |
| `auto` | Follow the request's tool choice and per-tool strictness. Required/named choices activate structural tags; `tool_choice="auto"` activates them when at least one tool sets `strict: true`. |
| `function` | Constrain the tool-call envelope (markup and the function name) for every request with tools, leaving argument contents free unless the client marked the tool `strict: true`. |
| `parameter` | Additionally pin argument schemas for every tool, as if every tool had `strict: true`. |
@@ -154,6 +154,8 @@ Most OpenAI-compatible clients and agent frameworks never set `strict` on their
vllm serve ... --tool-strict-level function
```
When the option is unset, the tool parser's default level applies: `function` for `glm47`, `auto` for the other parsers. Passing `--tool-strict-level auto` overrides a parser default.
The floor never relaxes a constraint the request would already receive: tools the client marked `strict: true` keep their schemas at every level. With `tool_choice="auto"`, the grammar does not force a tool call; a plain text response stays valid. `VLLM_ENFORCE_STRICT_TOOL_CALLING=false` disables structural tags entirely and takes precedence over this option.
## Automatic Function Calling
+1 -1
View File
@@ -380,7 +380,7 @@ mod tests {
let error = match backend.new_chat_output_processor(
&mut request,
NewChatOutputProcessorOptions {
tool_strict_level: crate::ToolStrictLevel::Auto,
tool_strict_level: None,
tool_call_parser: &ParserSelection::Explicit("json".to_string()),
reasoning_parser: &ParserSelection::Auto,
},
+3 -1
View File
@@ -21,7 +21,9 @@ pub mod hf;
pub struct NewChatOutputProcessorOptions<'a> {
pub tool_call_parser: &'a ParserSelection,
pub reasoning_parser: &'a ParserSelection,
pub tool_strict_level: ToolStrictLevel,
/// Server-side floor for tool-call structural tags, or `None` for the
/// tool parser's default level.
pub tool_strict_level: Option<ToolStrictLevel>,
}
/// Minimal prompt-processing backend needed by `vllm-chat`.
+11 -8
View File
@@ -79,8 +79,9 @@ pub struct ChatRequestProcessor {
tool_call_parser: ParserSelection,
/// Reasoning parser selection used when preparing generation requests.
reasoning_parser: ParserSelection,
/// Server-side floor for tool-call structural tags.
tool_strict_level: ToolStrictLevel,
/// Server-side floor for tool-call structural tags, or `None` for the
/// tool parser's default level.
tool_strict_level: Option<ToolStrictLevel>,
}
impl ChatRequestProcessor {
@@ -93,7 +94,7 @@ impl ChatRequestProcessor {
max_model_len: Some(max_model_len as usize),
tool_call_parser: ParserSelection::Auto,
reasoning_parser: ParserSelection::Auto,
tool_strict_level: ToolStrictLevel::Auto,
tool_strict_level: None,
}
}
@@ -105,7 +106,7 @@ impl ChatRequestProcessor {
max_model_len: None,
tool_call_parser: ParserSelection::Auto,
reasoning_parser: ParserSelection::Auto,
tool_strict_level: ToolStrictLevel::Auto,
tool_strict_level: None,
}
}
@@ -120,8 +121,9 @@ impl ChatRequestProcessor {
self
}
/// Configure the server-side floor for tool-call structural tags.
pub fn with_tool_strict_level(mut self, tool_strict_level: ToolStrictLevel) -> Self {
/// Configure the server-side floor for tool-call structural tags, or
/// `None` for the tool parser's default level.
pub fn with_tool_strict_level(mut self, tool_strict_level: Option<ToolStrictLevel>) -> Self {
self.tool_strict_level = tool_strict_level;
self
}
@@ -295,8 +297,9 @@ impl ChatLlm {
self
}
/// Set the server-side floor for tool-call structural tags.
pub fn with_tool_strict_level(mut self, tool_strict_level: ToolStrictLevel) -> Self {
/// Set the server-side floor for tool-call structural tags, or `None` for
/// the tool parser's default level.
pub fn with_tool_strict_level(mut self, tool_strict_level: Option<ToolStrictLevel>) -> Self {
self.processor.tool_strict_level = tool_strict_level;
self
}
+62 -37
View File
@@ -70,7 +70,7 @@ impl DefaultChatOutputProcessor {
tokenizer: DynTokenizer,
tool_call_parser: &ParserSelection,
reasoning_parser: &ParserSelection,
tool_strict_level: ToolStrictLevel,
tool_strict_level: Option<ToolStrictLevel>,
) -> ChatResult<Self> {
Self::with_response_template(
request,
@@ -92,7 +92,7 @@ impl DefaultChatOutputProcessor {
response_template: &std::result::Result<Arc<ResponseTemplate>, HfTemplateError>,
tool_call_parser: &ParserSelection,
reasoning_parser: &ParserSelection,
tool_strict_level: ToolStrictLevel,
tool_strict_level: Option<ToolStrictLevel>,
) -> ChatResult<Self> {
let tool_name = tool_call_parser.resolve_tool_name(model_id);
let reasoning_name = reasoning_parser.resolve_reasoning_name(model_id);
@@ -135,6 +135,8 @@ impl DefaultChatOutputProcessor {
if parser.preserve_special_tokens() {
request.decode_options.skip_special_tokens = false;
}
let tool_strict_level =
tool_strict_level.unwrap_or_else(|| parser.default_tool_strict_level());
Ok(Self {
parser,
@@ -292,6 +294,7 @@ mod tests {
use thiserror_ext::AsReport as _;
use vllm_engine_core_client::protocol::structured_outputs::StructuredOutputsParams;
use vllm_parser::output_grammar::BuiltOutputGrammar;
use vllm_tokenizer::test_utils::TestTokenizer;
use xgrammar_structural_tag::format::Format;
@@ -309,36 +312,45 @@ mod tests {
)
}
/// The output grammar for one `search` tool with `strict` under `parser`.
fn search_grammar(
parser: &str,
level: Option<ToolStrictLevel>,
strict: Option<bool>,
choice: ChatToolChoice,
) -> Option<BuiltOutputGrammar> {
let tools = vec![ChatTool {
name: "search".to_string(),
description: None,
parameters: serde_json::json!({
"type": "object",
"properties": { "query": { "type": "string" } },
"required": ["query"]
}),
strict,
defer_loading: None,
}];
let mut request = ChatRequest {
tool_context: ResolvedToolContext::new(&[], tools, Some(choice), true).unwrap(),
..ChatRequest::for_test()
};
let mut processor = DefaultChatOutputProcessor::new(
&mut request,
"other-model",
tokenizer(),
&ParserSelection::Explicit(parser.to_string()),
&ParserSelection::None,
level,
)
.unwrap();
processor.initialize(&[]).unwrap();
processor.build_output_grammar().unwrap()
}
#[test]
fn output_grammar_preserves_tool_strict_level() {
let build = |level, strict, choice| {
let tools = vec![ChatTool {
name: "search".to_string(),
description: None,
parameters: serde_json::json!({
"type": "object",
"properties": { "query": { "type": "string" } },
"required": ["query"]
}),
strict,
defer_loading: None,
}];
let mut request = ChatRequest {
tool_context: ResolvedToolContext::new(&[], tools, Some(choice), true).unwrap(),
..ChatRequest::for_test()
};
let mut processor = DefaultChatOutputProcessor::new(
&mut request,
"other-model",
tokenizer(),
&ParserSelection::Explicit("qwen3_coder".to_string()),
&ParserSelection::None,
level,
)
.unwrap();
processor.initialize(&[]).unwrap();
processor.build_output_grammar().unwrap()
};
let build =
|level, strict, choice| search_grammar("qwen3_coder", Some(level), strict, choice);
assert!(build(ToolStrictLevel::Auto, None, ChatToolChoice::Auto).is_none());
let envelope = build(ToolStrictLevel::Function, None, ChatToolChoice::Auto).unwrap();
@@ -349,6 +361,19 @@ mod tests {
assert!(build(ToolStrictLevel::Parameter, None, ChatToolChoice::None).is_none());
}
#[test]
fn unset_tool_strict_level_falls_back_to_the_parser_default() {
let build = |parser, level| search_grammar(parser, level, None, ChatToolChoice::Auto);
// GLM-4.7 constrains auto tool calls unless the level is set explicitly.
let function = build("glm47", Some(ToolStrictLevel::Function));
assert_eq!(build("glm47", None), function);
assert!(function.is_some());
assert!(build("glm47", Some(ToolStrictLevel::Auto)).is_none());
// Other parsers default to `auto`.
assert!(build("qwen3_coder", None).is_none());
}
#[test]
fn answer_constraint_keeps_optional_tool_calls() {
let tools = vec![ChatTool {
@@ -377,7 +402,7 @@ mod tests {
tokenizer(),
&ParserSelection::Explicit("qwen3_coder".to_string()),
&ParserSelection::None,
ToolStrictLevel::Auto,
None,
)
.unwrap();
processor.initialize(&[]).unwrap();
@@ -420,7 +445,7 @@ mod tests {
tokenizer(),
&ParserSelection::Explicit("qwen3_coder".to_string()),
&ParserSelection::None,
ToolStrictLevel::Auto,
None,
)
.unwrap();
processor.initialize(&[]).unwrap();
@@ -444,7 +469,7 @@ mod tests {
tokenizer(),
&selection,
&selection,
ToolStrictLevel::Auto,
None,
)
.unwrap();
}
@@ -459,7 +484,7 @@ mod tests {
tokenizer(),
&ParserSelection::Auto,
&ParserSelection::Auto,
ToolStrictLevel::Auto,
None,
)
.unwrap();
}
@@ -477,7 +502,7 @@ mod tests {
tokenizer(),
tool,
reasoning,
ToolStrictLevel::Auto,
None,
)
.unwrap();
}
@@ -492,7 +517,7 @@ mod tests {
tokenizer(),
&ParserSelection::Auto,
&ParserSelection::Explicit("gemma4".to_string()),
ToolStrictLevel::Auto,
None,
) {
Ok(_) => panic!("expected mixed Gemma4 parser selection to fail"),
Err(error) => error,
@@ -539,7 +564,7 @@ mod tests {
template,
tool,
reasoning,
ToolStrictLevel::Auto,
None,
)?;
Ok(!request.decode_options.skip_special_tokens)
}
+1 -1
View File
@@ -958,7 +958,7 @@ async fn parse_completion(
NewChatOutputProcessorOptions {
// Pin every tool schema so that builder-backed parsers build an
// output grammar for replay. The strict level does not affect parsing.
tool_strict_level: ToolStrictLevel::Parameter,
tool_strict_level: Some(ToolStrictLevel::Parameter),
tool_call_parser: &case.tool_call_parser,
reasoning_parser: &case.reasoning_parser,
},
+8 -6
View File
@@ -137,9 +137,10 @@ pub struct RenderArgs {
#[arg(long, default_value_t)]
reasoning_parser: ParserSelection,
/// Server-side floor for structural-tag based tool calling: `auto`,
/// `function`, or `parameter`.
#[arg(long, default_value_t)]
tool_strict_level: ToolStrictLevel,
/// `function`, or `parameter`. When unset, the tool parser's default
/// level applies, else `auto`.
#[arg(long)]
tool_strict_level: Option<ToolStrictLevel>,
/// Select the native chat renderer implementation.
#[arg(long = "tokenizer-mode", default_value_t)]
renderer: RendererSelection,
@@ -252,10 +253,11 @@ pub struct SharedRuntimeArgs {
/// top of the per-tool `strict` field: `auto` follows the request's
/// tool choice and per-tool strictness,
/// `function` constrains the tool-call envelope for every request with
/// tools, `parameter` additionally pins argument schemas.
#[arg(long, default_value_t)]
/// tools, `parameter` additionally pins argument schemas. When unset, the
/// tool parser's default level applies, else `auto`.
#[arg(long)]
#[serde(default)]
pub tool_strict_level: ToolStrictLevel,
pub tool_strict_level: Option<ToolStrictLevel>,
/// Select the chat renderer implementation.
#[arg(long = "tokenizer-mode", default_value_t)]
#[serde(default, rename = "tokenizer_mode")]
+6 -6
View File
@@ -185,7 +185,7 @@ fn serve_args_forward_python_flags_with_separator() {
engine_ready_timeout_secs: 600,
tool_call_parser: Auto,
reasoning_parser: Auto,
tool_strict_level: Auto,
tool_strict_level: None,
renderer: Auto,
language_model_only: false,
max_logprobs: None,
@@ -1088,7 +1088,7 @@ fn frontend_args_accept_json() {
engine_ready_timeout_secs: 600,
tool_call_parser: None,
reasoning_parser: None,
tool_strict_level: Auto,
tool_strict_level: None,
renderer: Auto,
language_model_only: false,
max_logprobs: None,
@@ -1760,7 +1760,7 @@ fn serve_args_accept_handshake_aliases() {
engine_ready_timeout_secs: 600,
tool_call_parser: Auto,
reasoning_parser: Auto,
tool_strict_level: Auto,
tool_strict_level: None,
renderer: Auto,
language_model_only: false,
max_logprobs: None,
@@ -1919,7 +1919,7 @@ fn serve_frontend_config_uses_dp_address_as_advertised_host() {
grpc_listener_mode: None,
tool_call_parser: Auto,
reasoning_parser: Auto,
tool_strict_level: Auto,
tool_strict_level: None,
renderer: Auto,
language_model_only: false,
chat_template: None,
@@ -2014,7 +2014,7 @@ fn serve_frontend_config_keeps_tcp_transport_for_non_local_only_topology() {
grpc_listener_mode: None,
tool_call_parser: Auto,
reasoning_parser: Auto,
tool_strict_level: Auto,
tool_strict_level: None,
renderer: Auto,
language_model_only: false,
chat_template: None,
@@ -2153,7 +2153,7 @@ fn frontend_config_uses_external_coordinator_when_coordinator_address_is_present
grpc_listener_mode: None,
tool_call_parser: None,
reasoning_parser: None,
tool_strict_level: Auto,
tool_strict_level: None,
renderer: Auto,
language_model_only: false,
chat_template: None,
@@ -2,6 +2,7 @@
// SPDX-FileCopyrightText: Copyright contributors to the vLLM project
use super::{GlmXmlToolParser, Separator};
use crate::output_grammar::ToolStrictLevel;
use crate::tool::{Result, StructuralTagBuilder, Tool, ToolParser, ToolParserOutput};
/// Tool parser for GLM-4.7 MoE XML-style tool calls.
@@ -25,6 +26,11 @@ impl ToolParser for Glm47MoeToolParser {
Ok(Box::new(Self::new(tools)))
}
// Constrain auto tool calls by default, like the Python `glm47` parser.
fn default_tool_strict_level(&self) -> ToolStrictLevel {
ToolStrictLevel::Function
}
fn structural_tag_builder(&self) -> Option<&dyn StructuralTagBuilder> {
Some(xgrammar_structural_tag::Model::Glm47.builder())
}
+7 -1
View File
@@ -46,7 +46,7 @@ pub use xgrammar_structural_tag::builders::StructuralTagBuilder;
use xgrammar_structural_tag::format::Format;
use xgrammar_structural_tag::tool::{AllowedToolsMode, ToolChoiceValue};
use crate::output_grammar::{self, OutputGrammarContext};
use crate::output_grammar::{self, OutputGrammarContext, ToolStrictLevel};
use crate::utils;
/// One function-style tool made available to the model.
@@ -199,6 +199,12 @@ pub trait ToolParser: Send {
false
}
/// Return the tool strict level applied when the operator leaves
/// `--tool-strict-level` unset.
fn default_tool_strict_level(&self) -> ToolStrictLevel {
ToolStrictLevel::Auto
}
/// Build the language of everything the model may emit after reasoning
/// ends for the context's tool choice: visible text plus tool calls. `None`
/// means no tool grammar applies.
+7 -1
View File
@@ -6,7 +6,7 @@
use vllm_tokenizer::{DecodedText, DynTokenizer};
use super::{Result, UnifiedParser, UnifiedParserError, UnifiedParserOutput};
use crate::output_grammar::{self, BuiltOutputGrammar, OutputGrammarContext};
use crate::output_grammar::{self, BuiltOutputGrammar, OutputGrammarContext, ToolStrictLevel};
use crate::reasoning::ReasoningParser;
use crate::tool::{Tool, ToolParser, ToolParserOutput};
@@ -83,6 +83,12 @@ impl UnifiedParser for CombinedParser {
|| self.tool.as_ref().is_some_and(|parser| parser.preserve_special_tokens())
}
fn default_tool_strict_level(&self) -> ToolStrictLevel {
self.tool.as_ref().map_or(ToolStrictLevel::Auto, |parser| {
parser.default_tool_strict_level()
})
}
fn build_output_grammar(
&self,
ctx: &OutputGrammarContext<'_>,
+7 -1
View File
@@ -20,7 +20,7 @@ use thiserror::Error;
use thiserror_ext::Macro;
use vllm_tokenizer::{DecodedText, DynTokenizer};
use crate::output_grammar::{self, BuiltOutputGrammar, OutputGrammarContext};
use crate::output_grammar::{self, BuiltOutputGrammar, OutputGrammarContext, ToolStrictLevel};
use crate::reasoning::ReasoningError;
use crate::tool::{Tool, ToolCallDelta, ToolParserError, ToolParserEvent, ToolParserOutput};
use crate::utils::SpecialToken;
@@ -242,6 +242,12 @@ pub trait UnifiedParser: Send {
false
}
/// Return the tool strict level applied when the operator leaves
/// `--tool-strict-level` unset.
fn default_tool_strict_level(&self) -> ToolStrictLevel {
ToolStrictLevel::Auto
}
/// Build the request output grammar after prompt-based initialization.
fn build_output_grammar(
&self,
@@ -17,7 +17,6 @@ use clap::Parser;
use futures::StreamExt as _;
use tokio_util::sync::CancellationToken;
use tracing_subscriber::EnvFilter;
use vllm_chat::ToolStrictLevel;
use vllm_engine_core_client::TransportMode;
use vllm_server::{
ApiServerOptions, ChatTemplateContentFormatOption, Config, CoordinatorMode, CorsConfig,
@@ -73,7 +72,7 @@ async fn main() -> Result<()> {
grpc_listener_mode: None,
tool_call_parser: ParserSelection::Auto,
reasoning_parser: ParserSelection::Auto,
tool_strict_level: ToolStrictLevel::Auto,
tool_strict_level: None,
renderer: RendererSelection::Auto,
language_model_only: false,
chat_template: None,
+3 -2
View File
@@ -238,8 +238,9 @@ pub struct Config {
pub tool_call_parser: ParserSelection,
/// Reasoning parser selection.
pub reasoning_parser: ParserSelection,
/// Server-side floor for tool-call structural tags.
pub tool_strict_level: ToolStrictLevel,
/// Server-side floor for tool-call structural tags, or `None` for the
/// tool parser's default level.
pub tool_strict_level: Option<ToolStrictLevel>,
/// Chat renderer selection.
pub renderer: RendererSelection,
/// Disable frontend-side multimodal preprocessing and render the model as
+2 -2
View File
@@ -32,7 +32,7 @@ pub struct RenderConfig {
pub port: u16,
pub tool_call_parser: ParserSelection,
pub reasoning_parser: ParserSelection,
pub tool_strict_level: ToolStrictLevel,
pub tool_strict_level: Option<ToolStrictLevel>,
pub renderer: RendererSelection,
pub chat_template: Option<String>,
pub default_chat_template_kwargs: HashMap<String, Value>,
@@ -157,7 +157,7 @@ mod tests {
shutdown.cancel();
let error = serve_render(
RenderConfig {
tool_strict_level: ToolStrictLevel::Auto,
tool_strict_level: None,
model: "test-model".to_string(),
revision: None,
hf_overrides: Default::default(),
@@ -24,6 +24,7 @@ from vllm.entrypoints.openai.responses.protocol import ResponsesRequest
from vllm.entrypoints.openai.responses.utils import construct_tool_dicts
from vllm.exceptions import VLLMValidationError
from vllm.parser.abstract_parser import DelegatingParser
from vllm.parser.parser_manager import ParserManager
from vllm.parser.plamo3 import (
BEGIN_TOOL_ARGUMENTS,
BEGIN_TOOL_NAME,
@@ -1991,6 +1992,34 @@ def test_glm47_operator_level_overrides_parser_default(
assert content["json_schema"] == sample_tools[0].function.parameters
@pytest.mark.parametrize(
("tool_strict_level", "constrained"), [(None, True), ("auto", False)]
)
def test_glm47_default_level_applies_only_when_the_level_is_unset(
monkeypatch: pytest.MonkeyPatch,
sample_tools: list[ChatCompletionToolsParam],
tool_strict_level: str | None,
constrained: bool,
):
monkeypatch.setattr(envs, "VLLM_ENFORCE_STRICT_TOOL_CALLING", True)
parser_cls = ParserManager.get_parser(
tool_parser_name="glm47",
enable_auto_tools=True,
tool_strict_level=tool_strict_level,
)
assert parser_cls is not None
request = ChatCompletionRequest(
messages=[],
model="m",
tools=[tool.model_dump(exclude_none=True) for tool in sample_tools],
tool_choice="auto",
)
out = parser_cls(MagicMock(), tools=sample_tools).adjust_request(request)
assert (out.structured_outputs is not None) is constrained
def test_glm47_required_with_non_strict_tools_attaches_shallow_tag(
monkeypatch: pytest.MonkeyPatch,
sample_tools: list[ChatCompletionToolsParam],
+1 -1
View File
@@ -160,7 +160,7 @@ class AnthropicServingMessages(OpenAIServingChat):
reasoning_parser: str = "",
enable_auto_tools: bool = False,
tool_parser: str | None = None,
tool_strict_level: str = "auto",
tool_strict_level: str | None = None,
enable_prompt_tokens_details: bool = False,
enable_force_include_usage: bool = False,
enable_log_outputs: bool = False,
+1 -1
View File
@@ -251,7 +251,7 @@ class CohereServingChatV2(OpenAIServingChat):
reasoning_parser: str = "",
enable_auto_tools: bool = False,
tool_parser: str | None = None,
tool_strict_level: str = "auto",
tool_strict_level: str | None = None,
enable_prompt_tokens_details: bool = False,
enable_force_include_usage: bool = False,
enable_log_outputs: bool = False,
+3 -2
View File
@@ -128,13 +128,14 @@ class BaseFrontendArgs:
"""Special the tool parser plugin write to parse the model-generated tool
into OpenAI API format, the name register in this plugin can be used in
`--tool-call-parser`."""
tool_strict_level: ToolStrictLevelName = "auto"
tool_strict_level: ToolStrictLevelName | None = None
"""Server-side floor for structural-tag based tool calling, applied on top
of the per-tool `strict` field. `auto` follows the request's tool choice
and per-tool strictness; `function` constrains the
tool-call envelope (markup and function name) for every request with
tools; `parameter` additionally pins argument schemas, as if every tool
were `strict: true`."""
were `strict: true`. When unset, the tool parser's default level applies,
which is `auto` for most parsers."""
tool_server: str | None = None
"""Comma-separated list of host:port pairs (IPv4, IPv6, or hostname).
Examples: 127.0.0.1:8000, [::1]:8000, localhost:1234. Or `demo` for
@@ -132,7 +132,7 @@ class OpenAIServingChat(GenerateBaseServing):
enable_auto_tools: bool = False,
exclude_tools_when_tool_choice_none: bool = False,
tool_parser: str | None = None,
tool_strict_level: str = "auto",
tool_strict_level: str | None = None,
enable_prompt_tokens_details: bool = False,
enable_force_include_usage: bool = False,
enable_log_outputs: bool = False,
+1 -1
View File
@@ -113,7 +113,7 @@ class OpenAIServingResponses(GenerateBaseServing):
reasoning_parser: str = "",
enable_auto_tools: bool = False,
tool_parser: str | None = None,
tool_strict_level: str = "auto",
tool_strict_level: str | None = None,
tool_server: ToolServer | None = None,
enable_prompt_tokens_details: bool = False,
enable_force_include_usage: bool = False,
+4 -6
View File
@@ -118,8 +118,9 @@ class Parser:
# Subclasses should override these if they use specific parser classes
reasoning_parser_cls: type[ReasoningParser] | None = None
tool_parser_cls: type[ToolParser] | None = None
# Server-side floor for tool-call structural tags (--tool-strict-level).
tool_strict_level: ToolStrictLevel = ToolStrictLevel.AUTO
# Server-side floor for tool-call structural tags (--tool-strict-level), or
# None for the tool parser's default level.
tool_strict_level: ToolStrictLevel | None = None
always_adjust_request: bool = False
def __init__(
@@ -519,10 +520,7 @@ class DelegatingParser(Parser):
is_auto = request.tool_choice == "auto"
single_call = request.parallel_tool_calls is False
strict_level = self.tool_strict_level
if (
strict_level == ToolStrictLevel.AUTO
and tool_parser.default_tool_strict_level is not None
):
if strict_level is None:
strict_level = tool_parser.default_tool_strict_level
if single_call and not (is_auto and structured_outputs is not None):
# Limiting the call count needs the call envelope in the grammar.
+9 -4
View File
@@ -104,7 +104,7 @@ class ParserManager:
enable_auto_tools: bool = False,
model_name: str | None = None,
is_harmony: bool = False,
tool_strict_level: str = "auto",
tool_strict_level: str | None = None,
tokenizer: TokenizerLike | None = None,
) -> type[Parser] | None:
"""Get a Parser that handles both reasoning and tool parsing.
@@ -119,7 +119,8 @@ class ParserManager:
is_harmony: Whether the selected model uses the Harmony format.
If True, HarmonyParser is always returned.
tool_strict_level: Server-side floor for tool-call structural
tags (``--tool-strict-level``).
tags (``--tool-strict-level``), or ``None`` for the tool
parser's default level.
tokenizer: Tokenizer the composed parser is validated against
at startup (see :meth:`validate_parser_tokenizer`). ``None``
skips the check (e.g. ``skip_tokenizer_init``).
@@ -155,7 +156,7 @@ class ParserManager:
enable_auto_tools: bool,
model_name: str | None,
is_harmony: bool,
tool_strict_level: str,
tool_strict_level: str | None,
tokenizer: TokenizerLike | None,
) -> type[Parser] | None:
if not tool_parser_name and not reasoning_parser_name:
@@ -169,7 +170,11 @@ class ParserManager:
if reasoning_parser_cls is None and tool_parser_cls is None:
return None
strict_level = ToolStrictLevel.from_name(tool_strict_level)
strict_level = (
None
if tool_strict_level is None
else ToolStrictLevel.from_name(tool_strict_level)
)
if is_harmony:
from vllm.parser.harmony import HarmonyParser
+1 -1
View File
@@ -69,7 +69,7 @@ class OnlineDerenderer:
exclude_tools_when_tool_choice_none: bool = False,
tool_parser: str | None = None,
reasoning_parser: str | None = None,
tool_strict_level: str = "auto",
tool_strict_level: str | None = None,
default_chat_template_kwargs: dict[str, Any] | None = None,
log_error_stack: bool = False,
) -> None:
+1 -1
View File
@@ -165,7 +165,7 @@ class OnlineRenderer:
exclude_tools_when_tool_choice_none: bool = False,
tool_parser: str | None = None,
reasoning_parser: str | None = None,
tool_strict_level: str = "auto",
tool_strict_level: str | None = None,
default_chat_template_kwargs: dict[str, Any] | None = None,
log_error_stack: bool = False,
) -> None:
+2 -2
View File
@@ -60,9 +60,9 @@ class ToolParser:
# their parsed tool-call syntax matches a builtin xgrammar format.
structural_tag_model: str | None = None
# Strict level applied when the operator leaves ``--tool-strict-level``
# at auto. Operator flags override it, and
# unset. Operator flags override it, and
# ``VLLM_ENFORCE_STRICT_TOOL_CALLING=0`` still disables structural tags.
default_tool_strict_level: ToolStrictLevel | None = None
default_tool_strict_level: ToolStrictLevel = ToolStrictLevel.AUTO
engine_based_streaming: bool = False
def __init_subclass__(cls, **kwargs: Any) -> None: