Skip to content
Merged
2 changes: 1 addition & 1 deletion sgl-router/benches/tool_parser_benchmark.rs
Original file line number Diff line number Diff line change
Expand Up @@ -409,7 +409,7 @@ fn bench_concurrent_parsing(c: &mut Criterion) {
let result =
rt.block_on(async { parser.parse_complete(input).await });

if let Ok(tools) = result {
if let Ok((_normal_text, tools)) = result {
total_p.fetch_add(tools.len() as u64, Ordering::Relaxed);
}
}
Expand Down
2 changes: 1 addition & 1 deletion sgl-router/src/reasoning_parser/traits.rs
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ use std::fmt;
/// Result of parsing text for reasoning content.
#[derive(Debug, Clone, Default, PartialEq)]
pub struct ParserResult {
/// The normal text outside of reasoning blocks.
/// The normal text outside reasoning blocks.
pub normal_text: String,

/// The extracted reasoning text from within reasoning blocks.
Expand Down
4 changes: 2 additions & 2 deletions sgl-router/src/routers/grpc/router.rs
Original file line number Diff line number Diff line change
Expand Up @@ -804,7 +804,7 @@ impl GrpcRouter {
.get_parser(&original_request.model)
{
match parser.parse_complete(&processed_text).await {
Ok(parsed_tool_calls) => {
Ok((normal_text, parsed_tool_calls)) => {
if !parsed_tool_calls.is_empty() {
let spec_tool_calls = parsed_tool_calls
.into_iter()
Expand All @@ -821,7 +821,7 @@ impl GrpcRouter {
})
.collect();
tool_calls = Some(spec_tool_calls);
processed_text = String::new();
processed_text = normal_text;
}
}
Err(e) => {
Expand Down
61 changes: 36 additions & 25 deletions sgl-router/src/tool_parser/parsers/deepseek_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -50,14 +50,6 @@ impl DeepSeekParser {
text.contains("<|tool▁calls▁begin|>")
}

/// Extract all tool call blocks from text
fn extract_tool_calls<'a>(&self, text: &'a str) -> Vec<&'a str> {
self.tool_call_extractor
.find_iter(text)
.map(|m| m.as_str())
.collect()
}

/// Parse a single tool call block
fn parse_tool_call(&self, block: &str) -> ToolParserResult<Option<ToolCall>> {
if let Some(captures) = self.func_detail_extractor.captures(block) {
Expand Down Expand Up @@ -115,23 +107,42 @@ impl Default for DeepSeekParser {

#[async_trait]
impl ToolParser for DeepSeekParser {
async fn parse_complete(&self, text: &str) -> ToolParserResult<Vec<ToolCall>> {
async fn parse_complete(&self, text: &str) -> ToolParserResult<(String, Vec<ToolCall>)> {
// Check if text contains DeepSeek format
if !self.has_tool_markers(text) {
return Ok(vec![]);
return Ok((text.to_string(), vec![]));
}

// Extract all tool call blocks
let tool_blocks = self.extract_tool_calls(text);
// Collect matches with positions and parse tools in one pass
let matches: Vec<_> = self.tool_call_extractor.find_iter(text).collect();
let mut tools = Vec::new();

for block in tool_blocks {
if let Some(tool) = self.parse_tool_call(block)? {
for mat in matches.iter() {
if let Some(tool) = self.parse_tool_call(mat.as_str())? {
tools.push(tool);
}
}

Ok(tools)
// Extract normal text using first and last match positions
let normal_text = if tools.is_empty() || matches.is_empty() {
text.to_string()
} else {
let first_start = matches[0].start();
let last_end = matches.last().unwrap().end();
let before = if first_start > 0 {
&text[..first_start]
} else {
""
};
let after = if last_end < text.len() {
&text[last_end..]
} else {
""
};
format!("{}{}", before, after)
};

Ok((normal_text, tools))
}

async fn parse_incremental(
Expand Down Expand Up @@ -241,10 +252,10 @@ mod tests {
{"location": "Tokyo", "units": "celsius"}
```<|tool▁call▁end|><|tool▁calls▁end|>More text"#;

let result = parser.parse_complete(input).await.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].function.name, "get_weather");
assert!(result[0].function.arguments.contains("Tokyo"));
let (_normal_text, tools) = parser.parse_complete(input).await.unwrap();
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].function.name, "get_weather");
assert!(tools[0].function.arguments.contains("Tokyo"));
}

#[tokio::test]
Expand All @@ -259,12 +270,12 @@ mod tests {
{"location": "Paris"}
```<|tool▁call▁end|><|tool▁calls▁end|>"#;

let result = parser.parse_complete(input).await.unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0].function.name, "get_weather");
assert_eq!(result[1].function.name, "get_weather");
assert!(result[0].function.arguments.contains("Tokyo"));
assert!(result[1].function.arguments.contains("Paris"));
let (_normal_text, tools) = parser.parse_complete(input).await.unwrap();
assert_eq!(tools.len(), 2);
assert_eq!(tools[0].function.name, "get_weather");
assert_eq!(tools[1].function.name, "get_weather");
assert!(tools[0].function.arguments.contains("Tokyo"));
assert!(tools[1].function.arguments.contains("Paris"));
}

#[test]
Expand Down
64 changes: 44 additions & 20 deletions sgl-router/src/tool_parser/parsers/glm4_moe_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -130,21 +130,42 @@ impl Default for Glm4MoeParser {

#[async_trait]
impl ToolParser for Glm4MoeParser {
async fn parse_complete(&self, text: &str) -> ToolParserResult<Vec<ToolCall>> {
async fn parse_complete(&self, text: &str) -> ToolParserResult<(String, Vec<ToolCall>)> {
// Check if text contains GLM-4 MoE format
if !self.has_tool_markers(text) {
return Ok(vec![]);
return Ok((text.to_string(), vec![]));
}

// Extract all tool call blocks
// Collect matches with positions and parse tools in one pass
let matches: Vec<_> = self.tool_call_extractor.find_iter(text).collect();
let mut tools = Vec::new();
for mat in self.tool_call_extractor.find_iter(text) {

for mat in matches.iter() {
if let Some(tool) = self.parse_tool_call(mat.as_str())? {
tools.push(tool);
}
}

Ok(tools)
// Extract normal text using first and last match positions
let normal_text = if tools.is_empty() {
text.to_string()
} else {
let first_start = matches[0].start();
let last_end = matches.last().unwrap().end();
let before = if first_start > 0 {
&text[..first_start]
} else {
""
};
let after = if last_end < text.len() {
&text[last_end..]
} else {
""
};
format!("{}{}", before, after)
};

Ok((normal_text, tools))
}

async fn parse_incremental(
Expand Down Expand Up @@ -232,11 +253,12 @@ mod tests {
<arg_value>2024-06-27</arg_value>
</tool_call>More text"#;

let result = parser.parse_complete(input).await.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].function.name, "get_weather");
assert!(result[0].function.arguments.contains("Beijing"));
assert!(result[0].function.arguments.contains("2024-06-27"));
let (normal_text, tools) = parser.parse_complete(input).await.unwrap();
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].function.name, "get_weather");
assert!(tools[0].function.arguments.contains("Beijing"));
assert!(tools[0].function.arguments.contains("2024-06-27"));
assert_eq!(normal_text, "Some text\nMore text"); // Text before and after tool call
}

#[tokio::test]
Expand All @@ -251,12 +273,13 @@ mod tests {
<arg_value>Shanghai</arg_value>
</tool_call>"#;

let result = parser.parse_complete(input).await.unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0].function.name, "get_weather");
assert_eq!(result[1].function.name, "get_weather");
assert!(result[0].function.arguments.contains("Beijing"));
assert!(result[1].function.arguments.contains("Shanghai"));
let (normal_text, tools) = parser.parse_complete(input).await.unwrap();
assert_eq!(tools.len(), 2);
assert_eq!(tools[0].function.name, "get_weather");
assert_eq!(tools[1].function.name, "get_weather");
assert!(tools[0].function.arguments.contains("Beijing"));
assert!(tools[1].function.arguments.contains("Shanghai"));
assert_eq!(normal_text, ""); // Pure tool calls, no normal text
}

#[tokio::test]
Expand All @@ -271,12 +294,13 @@ mod tests {
<arg_value>test</arg_value>
</tool_call>"#;

let result = parser.parse_complete(input).await.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].function.name, "process_data");
let (normal_text, tools) = parser.parse_complete(input).await.unwrap();
assert_eq!(tools.len(), 1);
assert_eq!(normal_text, ""); // Pure tool call, no normal text
assert_eq!(tools[0].function.name, "process_data");

// Parse arguments to check types
let args: serde_json::Value = serde_json::from_str(&result[0].function.arguments).unwrap();
let args: serde_json::Value = serde_json::from_str(&tools[0].function.arguments).unwrap();
assert_eq!(args["count"], 42);
assert_eq!(args["active"], true);
assert_eq!(args["name"], "test");
Expand Down
40 changes: 20 additions & 20 deletions sgl-router/src/tool_parser/parsers/gpt_oss_parser.rs
Original file line number Diff line number Diff line change
Expand Up @@ -71,10 +71,10 @@ impl Default for GptOssParser {

#[async_trait]
impl ToolParser for GptOssParser {
async fn parse_complete(&self, text: &str) -> ToolParserResult<Vec<ToolCall>> {
async fn parse_complete(&self, text: &str) -> ToolParserResult<(String, Vec<ToolCall>)> {
// Check if text contains GPT-OSS format
if !self.has_tool_markers(text) {
return Ok(vec![]);
return Ok((text.to_string(), vec![]));
}

let mut tools = Vec::new();
Expand Down Expand Up @@ -119,7 +119,7 @@ impl ToolParser for GptOssParser {
}
}

Ok(tools)
Ok((String::new(), tools)) // GPT-OSS parser returns empty normal text
}

async fn parse_incremental(
Expand Down Expand Up @@ -239,10 +239,10 @@ mod tests {
<|channel|>commentary to=functions.get_weather<|constrain|>json<|message|>{"location": "San Francisco"}<|call|>
More text"#;

let result = parser.parse_complete(input).await.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].function.name, "get_weather");
assert!(result[0].function.arguments.contains("San Francisco"));
let (_normal_text, tools) = parser.parse_complete(input).await.unwrap();
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].function.name, "get_weather");
assert!(tools[0].function.arguments.contains("San Francisco"));
}

#[tokio::test]
Expand All @@ -251,22 +251,22 @@ More text"#;
let input = r#"<|channel|>commentary to=functions.get_weather<|constrain|>json<|message|>{"location": "Paris"}<|call|>commentary
<|channel|>commentary to=functions.search<|constrain|>json<|message|>{"query": "Paris tourism"}<|call|>"#;

let result = parser.parse_complete(input).await.unwrap();
assert_eq!(result.len(), 2);
assert_eq!(result[0].function.name, "get_weather");
assert_eq!(result[1].function.name, "search");
assert!(result[0].function.arguments.contains("Paris"));
assert!(result[1].function.arguments.contains("Paris tourism"));
let (_normal_text, tools) = parser.parse_complete(input).await.unwrap();
assert_eq!(tools.len(), 2);
assert_eq!(tools[0].function.name, "get_weather");
assert_eq!(tools[1].function.name, "search");
assert!(tools[0].function.arguments.contains("Paris"));
assert!(tools[1].function.arguments.contains("Paris tourism"));
}

#[tokio::test]
async fn test_parse_gpt_oss_with_prefix() {
let parser = GptOssParser::new();
let input = r#"<|start|>assistant<|channel|>commentary to=functions.test<|constrain|>json<|message|>{"key": "value"}<|call|>"#;

let result = parser.parse_complete(input).await.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].function.name, "test");
let (_normal_text, tools) = parser.parse_complete(input).await.unwrap();
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].function.name, "test");
}

#[tokio::test]
Expand All @@ -275,10 +275,10 @@ More text"#;
let input =
r#"<|channel|>commentary to=functions.get_time<|constrain|>json<|message|>{}<|call|>"#;

let result = parser.parse_complete(input).await.unwrap();
assert_eq!(result.len(), 1);
assert_eq!(result[0].function.name, "get_time");
assert_eq!(result[0].function.arguments, "{}");
let (_normal_text, tools) = parser.parse_complete(input).await.unwrap();
assert_eq!(tools.len(), 1);
assert_eq!(tools[0].function.name, "get_time");
assert_eq!(tools[0].function.arguments, "{}");
}

#[test]
Expand Down
Loading
Loading