diff --git a/sgl-router/src/tool_parser/mod.rs b/sgl-router/src/tool_parser/mod.rs
index dad9c23b5..7a6bdfc24 100644
--- a/sgl-router/src/tool_parser/mod.rs
+++ b/sgl-router/src/tool_parser/mod.rs
@@ -25,5 +25,5 @@ pub use types::{FunctionCall, PartialToolCall, StreamResult, TokenConfig, ToolCa
// Re-export parsers for convenience
pub use parsers::{
- DeepSeekParser, JsonParser, LlamaParser, MistralParser, PythonicParser, QwenParser,
+ DeepSeekParser, JsonParser, LlamaParser, MistralParser, PythonicParser, QwenParser, Step3Parser,
};
diff --git a/sgl-router/src/tool_parser/parsers/mod.rs b/sgl-router/src/tool_parser/parsers/mod.rs
index 1166b70d1..399e2dc98 100644
--- a/sgl-router/src/tool_parser/parsers/mod.rs
+++ b/sgl-router/src/tool_parser/parsers/mod.rs
@@ -9,12 +9,15 @@ pub mod llama_parser;
pub mod mistral_parser;
pub mod pythonic_parser;
pub mod qwen_parser;
+pub mod step3_parser;
// Re-export parser types for convenience
pub use deepseek_parser::DeepSeekParser;
pub use json_parser::JsonParser;
+
pub use llama_parser::LlamaParser;
pub use mistral_parser::MistralParser;
pub use pythonic_parser::PythonicParser;
pub use qwen_parser::QwenParser;
+pub use step3_parser::Step3Parser;
diff --git a/sgl-router/src/tool_parser/parsers/step3_parser.rs b/sgl-router/src/tool_parser/parsers/step3_parser.rs
new file mode 100644
index 000000000..721d5c037
--- /dev/null
+++ b/sgl-router/src/tool_parser/parsers/step3_parser.rs
@@ -0,0 +1,348 @@
+use async_trait::async_trait;
+use regex::Regex;
+use serde_json::Value;
+
+use crate::tool_parser::{
+ errors::{ToolParserError, ToolParserResult},
+ state::ParseState,
+ traits::ToolParser,
+ types::{FunctionCall, StreamResult, ToolCall},
+};
+
+/// Step3 format parser for tool calls
+///
+/// Handles the Step3 specific format with steptml XML:
+/// `<|tool_calls_begin|><|tool_call_begin|>function<|tool_sep|>{v}<|tool_call_end|><|tool_calls_end|>`
+///
+/// Features:
+/// - Unicode token delimiters
+/// - StepTML XML format for invocations
+/// - Support for multiple sequential tool calls
+pub struct Step3Parser {
+ /// Regex for extracting tool call blocks
+ tool_call_extractor: Regex,
+ /// Regex for extracting steptml invocations
+ invoke_extractor: Regex,
+ /// Regex for extracting parameters
+ param_extractor: Regex,
+}
+
+impl Step3Parser {
+ /// Create a new Step3 parser
+ pub fn new() -> Self {
+ // Pattern for individual tool calls
+ let tool_call_pattern = r"(?s)<|tool_call_begin|>.*?<|tool_call_end|>";
+ let tool_call_extractor = Regex::new(tool_call_pattern).expect("Valid regex pattern");
+
+ // Pattern for steptml invocations
+ let invoke_pattern = r#"(?s)(.+?)"#;
+ let invoke_extractor = Regex::new(invoke_pattern).expect("Valid regex pattern");
+
+ // Pattern for steptml parameters - using non-greedy match for values to handle < characters
+ let param_pattern = r#"(?s)(.+?)"#;
+ let param_extractor = Regex::new(param_pattern).expect("Valid regex pattern");
+
+ Self {
+ tool_call_extractor,
+ invoke_extractor,
+ param_extractor,
+ }
+ }
+
+ /// Check if text contains Step3 tool markers
+ fn has_tool_markers(&self, text: &str) -> bool {
+ text.contains("<|tool_calls_begin|>")
+ }
+
+ /// Parse parameters from steptml format
+ fn parse_steptml_parameters(
+ &self,
+ params_text: &str,
+ ) -> ToolParserResult> {
+ let mut parameters = serde_json::Map::new();
+
+ for capture in self.param_extractor.captures_iter(params_text) {
+ let param_name = capture.get(1).map_or("", |m| m.as_str()).trim();
+ let param_value_str = capture.get(2).map_or("", |m| m.as_str()).trim();
+
+ // Try to parse the value as JSON first, fallback to string
+ let param_value = if let Ok(json_val) = serde_json::from_str::(param_value_str) {
+ json_val
+ } else {
+ // Try parsing as Python literal
+ if param_value_str == "true" || param_value_str == "True" {
+ Value::Bool(true)
+ } else if param_value_str == "false" || param_value_str == "False" {
+ Value::Bool(false)
+ } else if param_value_str == "null" || param_value_str == "None" {
+ Value::Null
+ } else if let Ok(num) = param_value_str.parse::() {
+ Value::Number(num.into())
+ } else if let Ok(num) = param_value_str.parse::() {
+ if let Some(n) = serde_json::Number::from_f64(num) {
+ Value::Number(n)
+ } else {
+ Value::String(param_value_str.to_string())
+ }
+ } else {
+ Value::String(param_value_str.to_string())
+ }
+ };
+
+ parameters.insert(param_name.to_string(), param_value);
+ }
+
+ Ok(parameters)
+ }
+
+ /// Parse a single tool call block
+ fn parse_tool_call(&self, block: &str) -> ToolParserResult