Unverified Commit 87a721a8 authored by Ayush Agarwal's avatar Ayush Agarwal Committed by GitHub
Browse files

feat: added parser name bindings (#2808)


Signed-off-by: default avatarAyush Agarwal <ayushag@nvidia.com>
parent 561ecb98
...@@ -14,6 +14,7 @@ from typing import Any, Dict, Optional ...@@ -14,6 +14,7 @@ from typing import Any, Dict, Optional
from sglang.srt.server_args import ServerArgs from sglang.srt.server_args import ServerArgs
from dynamo._core import get_reasoning_parser_names, get_tool_parser_names
from dynamo.sglang import __version__ from dynamo.sglang import __version__
DEFAULT_ENDPOINT = "dyn://dynamo.backend.generate" DEFAULT_ENDPOINT = "dyn://dynamo.backend.generate"
...@@ -33,13 +34,15 @@ DYNAMO_ARGS: Dict[str, Dict[str, Any]] = { ...@@ -33,13 +34,15 @@ DYNAMO_ARGS: Dict[str, Dict[str, Any]] = {
"flags": ["--dyn-tool-call-parser"], "flags": ["--dyn-tool-call-parser"],
"type": str, "type": str,
"default": None, "default": None,
"help": "Tool call parser name for the model. Available options: 'hermes', 'nemotron_deci', 'llama3_json', 'mistral', 'phi4', 'pythonic', 'harmony'.", "choices": get_tool_parser_names(),
"help": "Tool call parser name for the model.",
}, },
"reasoning-parser": { "reasoning-parser": {
"flags": ["--dyn-reasoning-parser"], "flags": ["--dyn-reasoning-parser"],
"type": str, "type": str,
"default": None, "default": None,
"help": "Reasoning parser name for the model. Available options: 'basic', 'deepseek_r1', 'gpt_oss', 'kimi', 'step3', 'qwen3', 'nemotron_deci', 'mistral'.", "choices": get_reasoning_parser_names(),
"help": "Reasoning parser name for the model.",
}, },
} }
...@@ -94,6 +97,7 @@ def parse_args(args: list[str]) -> Config: ...@@ -94,6 +97,7 @@ def parse_args(args: list[str]) -> Config:
type=info["type"], type=info["type"],
default=info["default"] if "default" in info else None, default=info["default"] if "default" in info else None,
help=info["help"], help=info["help"],
choices=info.get("choices", None),
) )
# SGLang args # SGLang args
......
...@@ -6,6 +6,7 @@ from typing import Optional ...@@ -6,6 +6,7 @@ from typing import Optional
from tensorrt_llm.llmapi import BuildConfig from tensorrt_llm.llmapi import BuildConfig
from dynamo._core import get_reasoning_parser_names, get_tool_parser_names
from dynamo.trtllm import __version__ from dynamo.trtllm import __version__
from dynamo.trtllm.request_handlers.handler_base import ( from dynamo.trtllm.request_handlers.handler_base import (
DisaggregationMode, DisaggregationMode,
...@@ -274,13 +275,15 @@ def cmd_line_args(): ...@@ -274,13 +275,15 @@ def cmd_line_args():
"--dyn-tool-call-parser", "--dyn-tool-call-parser",
type=str, type=str,
default=None, default=None,
help="Tool call parser name for the model. Available options: 'hermes', 'nemotron_deci', 'llama3_json', 'mistral', 'phi4', 'pythonic', 'harmony'.", choices=get_tool_parser_names(),
help="Tool call parser name for the model.",
) )
parser.add_argument( parser.add_argument(
"--dyn-reasoning-parser", "--dyn-reasoning-parser",
type=str, type=str,
default=None, default=None,
help="Reasoning parser name for the model. Available options: 'basic', 'deepseek_r1', 'gpt_oss', 'kimi', 'step3', 'qwen3', 'nemotron_deci', 'mistral'.", choices=get_reasoning_parser_names(),
help="Reasoning parser name for the model.",
) )
args = parser.parse_args() args = parser.parse_args()
......
...@@ -12,6 +12,8 @@ from vllm.distributed.kv_events import KVEventsConfig ...@@ -12,6 +12,8 @@ from vllm.distributed.kv_events import KVEventsConfig
from vllm.engine.arg_utils import AsyncEngineArgs from vllm.engine.arg_utils import AsyncEngineArgs
from vllm.utils import FlexibleArgumentParser from vllm.utils import FlexibleArgumentParser
from dynamo._core import get_reasoning_parser_names, get_tool_parser_names
from . import __version__ from . import __version__
from .ports import ( from .ports import (
DEFAULT_DYNAMO_PORT_MAX, DEFAULT_DYNAMO_PORT_MAX,
...@@ -111,13 +113,15 @@ def parse_args() -> Config: ...@@ -111,13 +113,15 @@ def parse_args() -> Config:
"--dyn-tool-call-parser", "--dyn-tool-call-parser",
type=str, type=str,
default=None, default=None,
help="Tool call parser name for the model. Available options: 'hermes', 'nemotron_deci', 'llama3_json', 'mistral', 'phi4', 'pythonic', 'harmony'.", choices=get_tool_parser_names(),
help="Tool call parser name for the model.",
) )
parser.add_argument( parser.add_argument(
"--dyn-reasoning-parser", "--dyn-reasoning-parser",
type=str, type=str,
default=None, default=None,
help="Reasoning parser name for the model. Available options: 'basic', 'deepseek_r1', 'gpt_oss', 'kimi', 'step3', 'qwen3', 'nemotron_deci', 'mistral'.", choices=get_reasoning_parser_names(),
help="Reasoning parser name for the model.",
) )
parser = AsyncEngineArgs.add_cli_args(parser) parser = AsyncEngineArgs.add_cli_args(parser)
......
...@@ -40,6 +40,7 @@ block-manager = ["dynamo-llm/block-manager", "dep:dlpark", "dep:cudarc"] ...@@ -40,6 +40,7 @@ block-manager = ["dynamo-llm/block-manager", "dep:dlpark", "dep:cudarc"]
[dependencies] [dependencies]
dynamo-llm = { path = "../../llm" } dynamo-llm = { path = "../../llm" }
dynamo-runtime = { path = "../../runtime" } dynamo-runtime = { path = "../../runtime" }
dynamo-parsers = { path = "../../parsers" }
anyhow = { version = "1" } anyhow = { version = "1" }
dynamo-async-openai = { path = "../../async-openai" } dynamo-async-openai = { path = "../../async-openai" }
......
...@@ -50,6 +50,7 @@ mod context; ...@@ -50,6 +50,7 @@ mod context;
mod engine; mod engine;
mod http; mod http;
mod llm; mod llm;
mod parsers;
type JsonServerStreamingIngress = type JsonServerStreamingIngress =
Ingress<SingleIn<serde_json::Value>, ManyOut<RsAnnotated<serde_json::Value>>>; Ingress<SingleIn<serde_json::Value>, ManyOut<RsAnnotated<serde_json::Value>>>;
...@@ -117,6 +118,7 @@ fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> { ...@@ -117,6 +118,7 @@ fn _core(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_class::<RouterMode>()?; m.add_class::<RouterMode>()?;
engine::add_to_module(m)?; engine::add_to_module(m)?;
parsers::add_to_module(m)?;
#[cfg(feature = "block-manager")] #[cfg(feature = "block-manager")]
llm::block_manager::add_to_module(m)?; llm::block_manager::add_to_module(m)?;
......
// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0
use dynamo_parsers::reasoning::get_available_reasoning_parsers;
use dynamo_parsers::tool_calling::parsers::get_available_tool_parsers;
use pyo3::prelude::*;
/// Get list of available parser names
#[pyfunction]
pub fn get_tool_parser_names() -> Vec<&'static str> {
get_available_tool_parsers()
}
/// Get list of available reasoning parser names
#[pyfunction]
pub fn get_reasoning_parser_names() -> Vec<&'static str> {
get_available_reasoning_parsers()
}
/// Add parsers module functions to the Python module
pub fn add_to_module(m: &Bound<'_, PyModule>) -> PyResult<()> {
m.add_function(wrap_pyfunction!(get_tool_parser_names, m)?)?;
m.add_function(wrap_pyfunction!(get_reasoning_parser_names, m)?)?;
Ok(())
}
# SPDX-FileCopyrightText: Copyright (c) 2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
# SPDX-License-Identifier: Apache-2.0
#
# Licensed under the Apache License, Version 2.0 (the "License");
# you may not use this file except in compliance with the License.
# You may obtain a copy of the License at
#
# http://www.apache.org/licenses/LICENSE-2.0
#
# Unless required by applicable law or agreed to in writing, software
# distributed under the License is distributed on an "AS IS" BASIS,
# WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
# See the License for the specific language governing permissions and
# limitations under the License.
from dynamo._core import get_reasoning_parser_names, get_tool_parser_names
def test_get_tool_parser_names():
parsers = get_tool_parser_names()
# Just make sure it's not None and has some parsers
# No Need to update this test when adding a new parser everytime
assert parsers is not None
assert len(parsers) > 0
def test_get_reasoning_parser_names():
parsers = get_reasoning_parser_names()
# Just make sure it's not None and has some parsers
# No Need to update this test when adding a new parser everytime
assert parsers is not None
assert len(parsers) > 0
// SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved. // SPDX-FileCopyrightText: Copyright (c) 2024-2025 NVIDIA CORPORATION & AFFILIATES. All rights reserved.
// SPDX-License-Identifier: Apache-2.0 // SPDX-License-Identifier: Apache-2.0
use std::collections::HashMap;
use std::sync::OnceLock;
mod base_parser; mod base_parser;
mod gpt_oss_parser; mod gpt_oss_parser;
...@@ -8,6 +10,29 @@ mod gpt_oss_parser; ...@@ -8,6 +10,29 @@ mod gpt_oss_parser;
pub use base_parser::BasicReasoningParser; pub use base_parser::BasicReasoningParser;
pub use gpt_oss_parser::GptOssReasoningParser; pub use gpt_oss_parser::GptOssReasoningParser;
static REASONING_PARSER_MAP: OnceLock<HashMap<&'static str, ReasoningParserType>> = OnceLock::new();
/// Initialize the global reasoning parser map
fn get_reasoning_parser_map() -> &'static HashMap<&'static str, ReasoningParserType> {
REASONING_PARSER_MAP.get_or_init(|| {
let mut map = HashMap::new();
map.insert("deepseek_r1", ReasoningParserType::DeepseekR1);
map.insert("basic", ReasoningParserType::Basic);
map.insert("gpt_oss", ReasoningParserType::GptOss);
map.insert("qwen3", ReasoningParserType::Qwen);
map.insert("nemotron_deci", ReasoningParserType::NemotronDeci);
map.insert("kimi", ReasoningParserType::Kimi);
map.insert("step3", ReasoningParserType::Step3);
map.insert("mistral", ReasoningParserType::Mistral);
map
})
}
/// Get all available reasoning parser names
pub fn get_available_reasoning_parsers() -> Vec<&'static str> {
get_reasoning_parser_map().keys().copied().collect()
}
#[derive(Debug, Clone, Default)] #[derive(Debug, Clone, Default)]
pub struct ParserResult { pub struct ParserResult {
/// The normal text outside of reasoning blocks. /// The normal text outside of reasoning blocks.
...@@ -144,17 +169,14 @@ impl ReasoningParserType { ...@@ -144,17 +169,14 @@ impl ReasoningParserType {
} }
pub fn get_reasoning_parser_from_name(name: &str) -> ReasoningParserWrapper { pub fn get_reasoning_parser_from_name(name: &str) -> ReasoningParserWrapper {
tracing::debug!(parser_name = name, "Selected reasoning parser"); tracing::debug!("Selected reasoning parser: {}", name);
match name.to_lowercase().as_str() {
"deepseek_r1" => Self::DeepseekR1.get_reasoning_parser(), let parser_map = get_reasoning_parser_map();
"basic" => Self::Basic.get_reasoning_parser(), let normalized_name = name.to_lowercase();
"gpt_oss" => Self::GptOss.get_reasoning_parser(),
"qwen3" => Self::Qwen.get_reasoning_parser(), match parser_map.get(normalized_name.as_str()) {
"nemotron_deci" => Self::NemotronDeci.get_reasoning_parser(), Some(parser_type) => parser_type.get_reasoning_parser(),
"kimi" => Self::Kimi.get_reasoning_parser(), None => {
"step3" => Self::Step3.get_reasoning_parser(),
"mistral" => Self::Mistral.get_reasoning_parser(),
_ => {
tracing::warn!( tracing::warn!(
parser_name = name, parser_name = name,
"Unknown reasoning parser type, falling back to Basic Reasoning Parser", "Unknown reasoning parser type, falling back to Basic Reasoning Parser",
...@@ -164,3 +186,28 @@ impl ReasoningParserType { ...@@ -164,3 +186,28 @@ impl ReasoningParserType {
} }
} }
} }
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn test_get_available_reasoning_parsers() {
let parsers = get_available_reasoning_parsers();
assert!(!parsers.is_empty());
// Update this list when adding a new parser
let available_parsers = [
"deepseek_r1",
"basic",
"gpt_oss",
"qwen3",
"nemotron_deci",
"kimi",
"step3",
"mistral",
];
for parser in available_parsers {
assert!(parsers.contains(&parser));
}
}
}
...@@ -6,6 +6,30 @@ use super::harmony_parser::parse_tool_calls_harmony; ...@@ -6,6 +6,30 @@ use super::harmony_parser::parse_tool_calls_harmony;
use super::json_parser::try_tool_call_parse_json; use super::json_parser::try_tool_call_parse_json;
use super::pythonic_parser::try_tool_call_parse_pythonic; use super::pythonic_parser::try_tool_call_parse_pythonic;
use super::response::ToolCallResponse; use super::response::ToolCallResponse;
use std::collections::HashMap;
use std::sync::OnceLock;
static PARSER_MAP: OnceLock<HashMap<&'static str, ToolCallConfig>> = OnceLock::new();
// Always update this parsermap when adding a new parser
pub fn get_tool_parser_map() -> &'static HashMap<&'static str, ToolCallConfig> {
PARSER_MAP.get_or_init(|| {
let mut map = HashMap::new();
map.insert("hermes", ToolCallConfig::hermes());
map.insert("nemotron_deci", ToolCallConfig::nemotron_deci());
map.insert("llama3_json", ToolCallConfig::llama3_json());
map.insert("mistral", ToolCallConfig::mistral());
map.insert("phi4", ToolCallConfig::phi4());
map.insert("pythonic", ToolCallConfig::pythonic());
map.insert("harmony", ToolCallConfig::harmony());
map.insert("default", ToolCallConfig::default());
map
})
}
pub fn get_available_tool_parsers() -> Vec<&'static str> {
get_tool_parser_map().keys().copied().collect()
}
pub fn try_tool_call_parse( pub fn try_tool_call_parse(
message: &str, message: &str,
...@@ -39,16 +63,8 @@ pub fn detect_and_parse_tool_call( ...@@ -39,16 +63,8 @@ pub fn detect_and_parse_tool_call(
message: &str, message: &str,
parser_str: Option<&str>, parser_str: Option<&str>,
) -> anyhow::Result<(Vec<ToolCallResponse>, Option<String>)> { ) -> anyhow::Result<(Vec<ToolCallResponse>, Option<String>)> {
let mut parser_map: std::collections::HashMap<&str, ToolCallConfig> = // Get the tool parser map
std::collections::HashMap::new(); let parser_map = get_tool_parser_map();
parser_map.insert("hermes", ToolCallConfig::hermes());
parser_map.insert("nemotron_deci", ToolCallConfig::nemotron_deci());
parser_map.insert("llama3_json", ToolCallConfig::llama3_json());
parser_map.insert("mistral", ToolCallConfig::mistral());
parser_map.insert("phi4", ToolCallConfig::phi4());
parser_map.insert("pythonic", ToolCallConfig::pythonic());
parser_map.insert("harmony", ToolCallConfig::harmony());
parser_map.insert("default", ToolCallConfig::default()); // Add default key
// Handle None or empty string by defaulting to "default" // Handle None or empty string by defaulting to "default"
let parser_key = match parser_str { let parser_key = match parser_str {
...@@ -61,7 +77,11 @@ pub fn detect_and_parse_tool_call( ...@@ -61,7 +77,11 @@ pub fn detect_and_parse_tool_call(
let (results, normal_content) = try_tool_call_parse(message, config)?; let (results, normal_content) = try_tool_call_parse(message, config)?;
Ok((results, normal_content)) Ok((results, normal_content))
} }
None => anyhow::bail!("Parser for the given config is not implemented"), // Original message None => anyhow::bail!(
"Parser '{}' is not implemented. Available parsers: {:?}",
parser_key,
get_available_tool_parsers()
),
} }
} }
...@@ -77,6 +97,26 @@ mod tests { ...@@ -77,6 +97,26 @@ mod tests {
(call.function.name, args) (call.function.name, args)
} }
#[test]
fn test_get_available_tool_parsers() {
let parsers = get_available_tool_parsers();
assert!(!parsers.is_empty());
// Update this list when adding a new parser
let available_parsers = [
"hermes",
"llama3_json",
"harmony",
"nemotron_deci",
"mistral",
"phi4",
"default",
"pythonic",
];
for parser in available_parsers {
assert!(parsers.contains(&parser));
}
}
#[test] #[test]
fn parses_single_parameters_object() { fn parses_single_parameters_object() {
let input = r#"{ "name": "hello", "parameters": { "x": 1, "y": 2 } }"#; let input = r#"{ "name": "hello", "parameters": { "x": 1, "y": 2 } }"#;
......
Markdown is supported
0% or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment