From 4463e90dd2333ecd4e3176f913797c0bf7297e9a Mon Sep 17 00:00:00 2001 From: Chang Su Date: Fri, 24 Oct 2025 15:14:41 -0700 Subject: [PATCH] [router][grpc] Remove gpt_oss parsers and remove _parser suffix in tool parser files (#12091) --- sgl-router/src/tool_parser/factory.rs | 32 +-- sgl-router/src/tool_parser/mod.rs | 4 +- .../{deepseek_parser.rs => deepseek.rs} | 0 .../{glm4_moe_parser.rs => glm4_moe.rs} | 0 .../parsers/gpt_oss_harmony_parser.rs | 72 ------ .../src/tool_parser/parsers/gpt_oss_parser.rs | 244 ------------------ .../parsers/{json_parser.rs => json.rs} | 0 .../parsers/{kimik2_parser.rs => kimik2.rs} | 0 .../parsers/{llama_parser.rs => llama.rs} | 0 .../parsers/{mistral_parser.rs => mistral.rs} | 0 sgl-router/src/tool_parser/parsers/mod.rs | 44 ++-- .../{passthrough_parser.rs => passthrough.rs} | 0 .../{pythonic_parser.rs => pythonic.rs} | 0 .../parsers/{qwen_parser.rs => qwen.rs} | 0 .../parsers/{step3_parser.rs => step3.rs} | 0 sgl-router/tests/tool_parser_gpt_oss.rs | 192 -------------- 16 files changed, 24 insertions(+), 564 deletions(-) rename sgl-router/src/tool_parser/parsers/{deepseek_parser.rs => deepseek.rs} (100%) rename sgl-router/src/tool_parser/parsers/{glm4_moe_parser.rs => glm4_moe.rs} (100%) delete mode 100644 sgl-router/src/tool_parser/parsers/gpt_oss_harmony_parser.rs delete mode 100644 sgl-router/src/tool_parser/parsers/gpt_oss_parser.rs rename sgl-router/src/tool_parser/parsers/{json_parser.rs => json.rs} (100%) rename sgl-router/src/tool_parser/parsers/{kimik2_parser.rs => kimik2.rs} (100%) rename sgl-router/src/tool_parser/parsers/{llama_parser.rs => llama.rs} (100%) rename sgl-router/src/tool_parser/parsers/{mistral_parser.rs => mistral.rs} (100%) rename sgl-router/src/tool_parser/parsers/{passthrough_parser.rs => passthrough.rs} (100%) rename sgl-router/src/tool_parser/parsers/{pythonic_parser.rs => pythonic.rs} (100%) rename sgl-router/src/tool_parser/parsers/{qwen_parser.rs => qwen.rs} (100%) rename sgl-router/src/tool_parser/parsers/{step3_parser.rs => step3.rs} (100%) delete mode 100644 sgl-router/tests/tool_parser_gpt_oss.rs diff --git a/sgl-router/src/tool_parser/factory.rs b/sgl-router/src/tool_parser/factory.rs index a9fc9fd5f..3f3a6fba4 100644 --- a/sgl-router/src/tool_parser/factory.rs +++ b/sgl-router/src/tool_parser/factory.rs @@ -9,8 +9,8 @@ use tokio::sync::Mutex; use crate::tool_parser::{ parsers::{ - DeepSeekParser, Glm4MoeParser, GptOssHarmonyParser, GptOssParser, JsonParser, KimiK2Parser, - LlamaParser, MistralParser, PassthroughParser, PythonicParser, QwenParser, Step3Parser, + DeepSeekParser, Glm4MoeParser, JsonParser, KimiK2Parser, LlamaParser, MistralParser, + PassthroughParser, PythonicParser, QwenParser, Step3Parser, }, traits::ToolParser, }; @@ -243,17 +243,6 @@ impl ParserFactory { registry.register_parser("step3", || Box::new(Step3Parser::new())); registry.register_parser("kimik2", || Box::new(KimiK2Parser::new())); - // Register GPT-OSS parsers - registry.register_parser("gpt_oss_legacy", || Box::new(GptOssParser::new())); - registry.register_parser("gpt_oss_harmony", || Box::new(GptOssHarmonyParser::new())); - - // Choose which GPT-OSS variant to use as default - if use_harmony_gpt_oss() { - registry.register_parser("gpt_oss", || Box::new(GptOssHarmonyParser::new())); - } else { - registry.register_parser("gpt_oss", || Box::new(GptOssParser::new())); - } - // Register default model mappings Self::register_default_mappings(®istry); @@ -304,10 +293,6 @@ impl ParserFactory { registry.map_model("Kimi-K2*", "kimik2"); registry.map_model("moonshot*/Kimi-K2*", "kimik2"); - // GPT-OSS models - registry.map_model("gpt-oss*", "gpt_oss"); - registry.map_model("t4-*", "gpt_oss"); - // Other models registry.map_model("gemini-*", "json"); registry.map_model("palm-*", "json"); @@ -392,16 +377,3 @@ impl Default for ParserFactory { Self::new() } } - -fn use_harmony_gpt_oss() -> bool { - std::env::var("ROUTER_USE_HARMONY_GPT_OSS") - .ok() - .map(|value| { - let normalized = value.trim(); - matches!( - normalized, - "1" | "true" | "TRUE" | "True" | "yes" | "YES" | "Yes" | "on" | "ON" | "On" - ) - }) - .unwrap_or(false) -} diff --git a/sgl-router/src/tool_parser/mod.rs b/sgl-router/src/tool_parser/mod.rs index 11950e19d..da7a703a4 100644 --- a/sgl-router/src/tool_parser/mod.rs +++ b/sgl-router/src/tool_parser/mod.rs @@ -20,8 +20,8 @@ pub use errors::{ParserError, ParserResult}; pub use factory::{ParserFactory, ParserRegistry, PooledParser}; // Re-export parsers for convenience pub use parsers::{ - DeepSeekParser, Glm4MoeParser, GptOssParser, JsonParser, KimiK2Parser, LlamaParser, - MistralParser, PythonicParser, QwenParser, Step3Parser, + DeepSeekParser, Glm4MoeParser, JsonParser, KimiK2Parser, LlamaParser, MistralParser, + PythonicParser, QwenParser, Step3Parser, }; pub use traits::{PartialJsonParser, ToolParser}; pub use types::{FunctionCall, PartialToolCall, StreamingParseResult, ToolCall}; diff --git a/sgl-router/src/tool_parser/parsers/deepseek_parser.rs b/sgl-router/src/tool_parser/parsers/deepseek.rs similarity index 100% rename from sgl-router/src/tool_parser/parsers/deepseek_parser.rs rename to sgl-router/src/tool_parser/parsers/deepseek.rs diff --git a/sgl-router/src/tool_parser/parsers/glm4_moe_parser.rs b/sgl-router/src/tool_parser/parsers/glm4_moe.rs similarity index 100% rename from sgl-router/src/tool_parser/parsers/glm4_moe_parser.rs rename to sgl-router/src/tool_parser/parsers/glm4_moe.rs diff --git a/sgl-router/src/tool_parser/parsers/gpt_oss_harmony_parser.rs b/sgl-router/src/tool_parser/parsers/gpt_oss_harmony_parser.rs deleted file mode 100644 index 79e998750..000000000 --- a/sgl-router/src/tool_parser/parsers/gpt_oss_harmony_parser.rs +++ /dev/null @@ -1,72 +0,0 @@ -use async_trait::async_trait; - -use crate::{ - protocols::common::Tool, - tool_parser::{ - errors::ParserResult, - traits::{TokenToolParser, ToolParser}, - types::{StreamingParseResult, ToolCall}, - }, -}; - -/// Placeholder for the Harmony-backed GPT-OSS parser. -/// -/// regex implementation. This struct will be fleshed out in subsequent phases to -/// reuse Harmony's tokenizer and message reconstruction logic. -#[derive(Default)] -pub struct GptOssHarmonyParser; - -impl GptOssHarmonyParser { - pub fn new() -> Self { - Self - } -} - -#[async_trait] -impl ToolParser for GptOssHarmonyParser { - async fn parse_complete(&self, output: &str) -> ParserResult<(String, Vec)> { - // Temporary stub: fall back to returning the raw text with no tool calls. - // Later phases will decode Harmony tokens into structured tool calls. - Ok((output.to_string(), Vec::new())) - } - - async fn parse_incremental( - &mut self, - _chunk: &str, - _tools: &[Tool], - ) -> ParserResult { - // Temporary stub until the Harmony streaming pipeline is implemented. - Ok(StreamingParseResult::default()) - } - - fn has_tool_markers(&self, text: &str) -> bool { - // Reuse the legacy heuristics for now; this will be replaced with Harmony-specific - // start-token detection when the parser is fully implemented. - text.contains("<|channel|>commentary") - } - - fn as_token_parser(&self) -> Option<&dyn TokenToolParser> { - Some(self) - } -} - -#[async_trait] -impl TokenToolParser for GptOssHarmonyParser { - async fn parse_complete_tokens( - &self, - _tokens: &[u32], - ) -> ParserResult<(String, Vec)> { - // Placeholder until Harmony integration lands. Returning an empty tool list ensures - // that enabling the parser without full implementation results in a no-op rather - // than a runtime panic. - Ok((String::new(), Vec::new())) - } - - async fn parse_incremental_tokens( - &mut self, - _tokens: &[u32], - _tools: &[Tool], - ) -> ParserResult { - Ok(StreamingParseResult::default()) - } -} diff --git a/sgl-router/src/tool_parser/parsers/gpt_oss_parser.rs b/sgl-router/src/tool_parser/parsers/gpt_oss_parser.rs deleted file mode 100644 index b38fa52f2..000000000 --- a/sgl-router/src/tool_parser/parsers/gpt_oss_parser.rs +++ /dev/null @@ -1,244 +0,0 @@ -use async_trait::async_trait; -use regex::Regex; -use serde_json::Value; - -use crate::{ - protocols::common::Tool, - tool_parser::{ - errors::{ParserError, ParserResult}, - parsers::helpers, - partial_json::PartialJson, - traits::ToolParser, - types::{FunctionCall, StreamingParseResult, ToolCall, ToolCallItem}, - }, -}; - -/// GPT-OSS format parser for tool calls -/// -/// Handles the GPT-OSS specific channel format: -/// `<|channel|>commentary to={namespace.function}<|constrain|>json<|message|>{json_args}<|call|>` -/// -/// Features: -/// - Channel-based format with commentary -/// - Namespaced function calls -/// - JSON arguments -pub struct GptOssParser { - /// Parser for handling incomplete JSON during streaming - partial_json: PartialJson, - /// Regex for extracting complete function calls - function_call_extractor: Regex, - /// Regex for extracting streaming function calls - streaming_extractor: Regex, - - /// Buffer for accumulating chunks - buffer: String, - /// Whether the tool name has been sent (for streaming) - name_sent: bool, -} - -impl GptOssParser { - /// Create a new GPT-OSS parser - pub fn new() -> Self { - // Pattern for complete function calls with to= parameter - // Handles optional <|start|>assistant prefix and whitespace after function name - let function_call_pattern = r"(?s)(?:<\|start\|>assistant)?<\|channel\|>commentary to=([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_-]*)*)\s*<\|constrain\|>json<\|message\|>(.*?)<\|call\|>(?:commentary)?"; - let function_call_extractor = - Regex::new(function_call_pattern).expect("Valid regex pattern"); - - // Pattern for streaming function calls (incomplete) - let streaming_pattern = r"(?s)(?:<\|start\|>assistant)?<\|channel\|>commentary to=([a-zA-Z_][a-zA-Z0-9_]*(?:\.[a-zA-Z_][a-zA-Z0-9_-]*)*)\s*<\|constrain\|>json<\|message\|>(.*)"; - let streaming_extractor = Regex::new(streaming_pattern).expect("Valid regex pattern"); - - Self { - partial_json: PartialJson::default(), - function_call_extractor, - streaming_extractor, - - buffer: String::new(), - name_sent: false, - } - } - - /// Extract function name from full namespace (e.g., "functions.get_weather" -> "get_weather") - fn extract_function_name(&self, full_name: &str) -> String { - if let Some(dot_pos) = full_name.rfind('.') { - full_name[dot_pos + 1..].to_string() - } else { - full_name.to_string() - } - } -} - -impl Default for GptOssParser { - fn default() -> Self { - Self::new() - } -} - -#[async_trait] -impl ToolParser for GptOssParser { - async fn parse_complete(&self, text: &str) -> ParserResult<(String, Vec)> { - // Check if text contains GPT-OSS format - if !self.has_tool_markers(text) { - return Ok((text.to_string(), vec![])); - } - - let mut tools = Vec::new(); - let mut _tool_index = 0; - - // Extract all function calls - for captures in self.function_call_extractor.captures_iter(text) { - if let (Some(name_match), Some(args_match)) = (captures.get(1), captures.get(2)) { - let full_function_name = name_match.as_str(); - let args_content = args_match.as_str().trim(); - - // Extract actual function name - let function_name = self.extract_function_name(full_function_name); - - // Parse JSON arguments - let arguments = if args_content.is_empty() { - "{}".to_string() - } else { - match serde_json::from_str::(args_content) { - Ok(value) => serde_json::to_string(&value) - .map_err(|e| ParserError::ParsingFailed(e.to_string()))?, - Err(_) => { - // Skip malformed JSON - continue; - } - } - }; - - tools.push(ToolCall { - function: FunctionCall { - name: function_name, - arguments, - }, - }); - - _tool_index += 1; - } - } - - Ok((String::new(), tools)) // GPT-OSS parser returns empty normal text - } - - async fn parse_incremental( - &mut self, - chunk: &str, - tools: &[Tool], - ) -> ParserResult { - self.buffer.push_str(chunk); - - // Check for tool markers - if !self.has_tool_markers(&self.buffer) { - // No markers found, clear buffer and return - self.buffer.clear(); - return Ok(StreamingParseResult::default()); - } - - // Try to match streaming pattern - if let Some(captures) = self.streaming_extractor.captures(&self.buffer) { - if let (Some(name_match), Some(args_match)) = (captures.get(1), captures.get(2)) { - let full_function_name = name_match.as_str(); - let partial_args = args_match.as_str(); - - // Extract actual function name - let function_name = self.extract_function_name(full_function_name); - - // Send function name if not sent yet - if !self.name_sent { - // Validate tool name - let tool_indices = helpers::get_tool_indices(tools); - if !tool_indices.contains_key(&function_name) { - // Invalid tool name - skip - tracing::warn!("Invalid tool name '{}' - skipping", function_name); - self.buffer.clear(); - self.name_sent = false; - return Ok(StreamingParseResult::default()); - } - - self.name_sent = true; // Mark name as sent - return Ok(StreamingParseResult { - normal_text: String::new(), - calls: vec![ToolCallItem { - tool_index: 0, - name: Some(function_name.clone()), - parameters: String::new(), - }], - }); - } - - // Check if we have a complete function call - if let Some(complete_match) = self.function_call_extractor.captures(&self.buffer) { - if let Some(args_match) = complete_match.get(2) { - let args_content = args_match.as_str().trim(); - - // Parse JSON arguments - let arguments = if args_content.is_empty() { - "{}".to_string() - } else { - match serde_json::from_str::(args_content) { - Ok(value) => serde_json::to_string(&value) - .unwrap_or_else(|_| "{}".to_string()), - Err(_) => "{}".to_string(), - } - }; - - // Remove the processed part from buffer - let complete_end = complete_match.get(0).unwrap().end(); - self.buffer.drain(..complete_end); - - // Reset state for next tool - self.name_sent = false; - - // Return final arguments - return Ok(StreamingParseResult { - normal_text: String::new(), - calls: vec![ToolCallItem { - tool_index: 0, - name: None, - parameters: arguments, - }], - }); - } - } else { - // Try to parse partial JSON for streaming arguments - if !partial_args.is_empty() { - // Look for the end of JSON (before <|call|>) - let json_part = if let Some(call_pos) = partial_args.find("<|call|>") { - &partial_args[..call_pos] - } else { - partial_args - }; - - match self.partial_json.parse_value(json_part, true) { - Ok((value, _consumed)) => { - let args_str = serde_json::to_string(&value) - .unwrap_or_else(|_| "{}".to_string()); - - return Ok(StreamingParseResult { - normal_text: String::new(), - calls: vec![ToolCallItem { - tool_index: 0, - name: None, - parameters: args_str, - }], - }); - } - Err(_) => { - // Can't parse yet, keep buffering - } - } - } - } - } - } - - Ok(StreamingParseResult::default()) - } - - fn has_tool_markers(&self, text: &str) -> bool { - text.contains("<|channel|>commentary") - } -} diff --git a/sgl-router/src/tool_parser/parsers/json_parser.rs b/sgl-router/src/tool_parser/parsers/json.rs similarity index 100% rename from sgl-router/src/tool_parser/parsers/json_parser.rs rename to sgl-router/src/tool_parser/parsers/json.rs diff --git a/sgl-router/src/tool_parser/parsers/kimik2_parser.rs b/sgl-router/src/tool_parser/parsers/kimik2.rs similarity index 100% rename from sgl-router/src/tool_parser/parsers/kimik2_parser.rs rename to sgl-router/src/tool_parser/parsers/kimik2.rs diff --git a/sgl-router/src/tool_parser/parsers/llama_parser.rs b/sgl-router/src/tool_parser/parsers/llama.rs similarity index 100% rename from sgl-router/src/tool_parser/parsers/llama_parser.rs rename to sgl-router/src/tool_parser/parsers/llama.rs diff --git a/sgl-router/src/tool_parser/parsers/mistral_parser.rs b/sgl-router/src/tool_parser/parsers/mistral.rs similarity index 100% rename from sgl-router/src/tool_parser/parsers/mistral_parser.rs rename to sgl-router/src/tool_parser/parsers/mistral.rs diff --git a/sgl-router/src/tool_parser/parsers/mod.rs b/sgl-router/src/tool_parser/parsers/mod.rs index 541c15baa..befde0172 100644 --- a/sgl-router/src/tool_parser/parsers/mod.rs +++ b/sgl-router/src/tool_parser/parsers/mod.rs @@ -3,32 +3,28 @@ /// This module contains concrete parser implementations for various model-specific /// tool/function call formats. // Individual parser modules -pub mod deepseek_parser; -pub mod glm4_moe_parser; -pub mod gpt_oss_harmony_parser; -pub mod gpt_oss_parser; -pub mod json_parser; -pub mod kimik2_parser; -pub mod llama_parser; -pub mod mistral_parser; -pub mod passthrough_parser; -pub mod pythonic_parser; -pub mod qwen_parser; -pub mod step3_parser; +pub mod deepseek; +pub mod glm4_moe; +pub mod json; +pub mod kimik2; +pub mod llama; +pub mod mistral; +pub mod passthrough; +pub mod pythonic; +pub mod qwen; +pub mod step3; // Shared helpers and utilities pub mod helpers; // Re-export parser types for convenience -pub use deepseek_parser::DeepSeekParser; -pub use glm4_moe_parser::Glm4MoeParser; -pub use gpt_oss_harmony_parser::GptOssHarmonyParser; -pub use gpt_oss_parser::GptOssParser; -pub use json_parser::JsonParser; -pub use kimik2_parser::KimiK2Parser; -pub use llama_parser::LlamaParser; -pub use mistral_parser::MistralParser; -pub use passthrough_parser::PassthroughParser; -pub use pythonic_parser::PythonicParser; -pub use qwen_parser::QwenParser; -pub use step3_parser::Step3Parser; +pub use deepseek::DeepSeekParser; +pub use glm4_moe::Glm4MoeParser; +pub use json::JsonParser; +pub use kimik2::KimiK2Parser; +pub use llama::LlamaParser; +pub use mistral::MistralParser; +pub use passthrough::PassthroughParser; +pub use pythonic::PythonicParser; +pub use qwen::QwenParser; +pub use step3::Step3Parser; diff --git a/sgl-router/src/tool_parser/parsers/passthrough_parser.rs b/sgl-router/src/tool_parser/parsers/passthrough.rs similarity index 100% rename from sgl-router/src/tool_parser/parsers/passthrough_parser.rs rename to sgl-router/src/tool_parser/parsers/passthrough.rs diff --git a/sgl-router/src/tool_parser/parsers/pythonic_parser.rs b/sgl-router/src/tool_parser/parsers/pythonic.rs similarity index 100% rename from sgl-router/src/tool_parser/parsers/pythonic_parser.rs rename to sgl-router/src/tool_parser/parsers/pythonic.rs diff --git a/sgl-router/src/tool_parser/parsers/qwen_parser.rs b/sgl-router/src/tool_parser/parsers/qwen.rs similarity index 100% rename from sgl-router/src/tool_parser/parsers/qwen_parser.rs rename to sgl-router/src/tool_parser/parsers/qwen.rs diff --git a/sgl-router/src/tool_parser/parsers/step3_parser.rs b/sgl-router/src/tool_parser/parsers/step3.rs similarity index 100% rename from sgl-router/src/tool_parser/parsers/step3_parser.rs rename to sgl-router/src/tool_parser/parsers/step3.rs diff --git a/sgl-router/tests/tool_parser_gpt_oss.rs b/sgl-router/tests/tool_parser_gpt_oss.rs deleted file mode 100644 index 98b197252..000000000 --- a/sgl-router/tests/tool_parser_gpt_oss.rs +++ /dev/null @@ -1,192 +0,0 @@ -//! GPT-OSS Parser Integration Tests - -use sglang_router_rs::tool_parser::{GptOssParser, ToolParser}; - -mod common; -use common::create_test_tools; - -#[tokio::test] -async fn test_gpt_oss_complete_parsing() { - let parser = GptOssParser::new(); - - let input = r#"Let me search for that information. -<|channel|>commentary to=functions.search<|constrain|>json<|message|>{"query": "rust programming", "limit": 10}<|call|> -Here are the results..."#; - - let (_normal_text, tools) = parser.parse_complete(input).await.unwrap(); - assert_eq!(tools.len(), 1); - assert_eq!(tools[0].function.name, "search"); - - let args: serde_json::Value = serde_json::from_str(&tools[0].function.arguments).unwrap(); - assert_eq!(args["query"], "rust programming"); - assert_eq!(args["limit"], 10); -} - -#[tokio::test] -async fn test_gpt_oss_multiple_tools() { - let parser = GptOssParser::new(); - - 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 (_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"); -} - -#[tokio::test] -async fn test_gpt_oss_with_namespace() { - let parser = GptOssParser::new(); - - let input = r#"<|channel|>commentary to=api.users.create<|constrain|>json<|message|>{"name": "John", "email": "john@example.com"}<|call|> -<|channel|>commentary to=tools.calculator.add<|constrain|>json<|message|>{"x": 10, "y": 20}<|call|>"#; - - let (_normal_text, tools) = parser.parse_complete(input).await.unwrap(); - assert_eq!(tools.len(), 2); - assert_eq!(tools[0].function.name, "create"); // Should extract last part - assert_eq!(tools[1].function.name, "add"); -} - -#[tokio::test] -async fn test_gpt_oss_with_assistant_prefix() { - let parser = GptOssParser::new(); - - let input = r#"<|start|>assistant<|channel|>commentary to=functions.test<|constrain|>json<|message|>{"key": "value"}<|call|>"#; - - let (_normal_text, tools) = parser.parse_complete(input).await.unwrap(); - assert_eq!(tools.len(), 1); - assert_eq!(tools[0].function.name, "test"); -} - -#[tokio::test] -async fn test_gpt_oss_empty_args() { - let parser = GptOssParser::new(); - - let input = - r#"<|channel|>commentary to=functions.get_time<|constrain|>json<|message|>{}<|call|>"#; - - 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, "{}"); -} - -#[tokio::test] -async fn test_gpt_oss_streaming() { - let tools = create_test_tools(); - - let mut parser = GptOssParser::new(); - - // Simulate streaming chunks - let chunks = vec![ - "<|channel|>commentary to=", - "functions.calculate", - "<|constrain|>json<|message|>", - r#"{"x": 10"#, - r#", "y": 20}"#, - "<|call|>", - ]; - - let mut found_complete = false; - - for chunk in chunks { - let result = parser.parse_incremental(chunk, &tools).await.unwrap(); - - if !result.calls.is_empty() { - if let Some(name) = &result.calls[0].name { - assert_eq!(name, "calculate"); - found_complete = true; - } - } - } - - assert!(found_complete); -} - -#[test] -fn test_gpt_oss_format_detection() { - let parser = GptOssParser::new(); - - // Should detect GPT-OSS format - assert!(parser.has_tool_markers("<|channel|>commentary to=")); - assert!(parser.has_tool_markers("<|channel|>commentary")); - assert!(parser.has_tool_markers("text with <|channel|>commentary to= marker")); - - // Should not detect other formats - assert!(!parser.has_tool_markers("[TOOL_CALLS]")); - assert!(!parser.has_tool_markers("")); - assert!(!parser.has_tool_markers("plain text")); -} - -#[tokio::test] -async fn test_gpt_oss_with_whitespace() { - let parser = GptOssParser::new(); - - let input = r#"<|channel|>commentary to=functions.test <|constrain|>json<|message|>{"key": "value"}<|call|>"#; - - let (_normal_text, tools) = parser.parse_complete(input).await.unwrap(); - assert_eq!(tools.len(), 1); - assert_eq!(tools[0].function.name, "test"); -} - -#[tokio::test] -async fn test_gpt_oss_complex_json() { - let parser = GptOssParser::new(); - - let input = r#"<|channel|>commentary to=functions.process<|constrain|>json<|message|>{ - "nested": { - "data": [1, 2, 3], - "config": { - "enabled": true - } - } -}<|call|>"#; - - let (_normal_text, tools) = parser.parse_complete(input).await.unwrap(); - assert_eq!(tools.len(), 1); - assert_eq!(tools[0].function.name, "process"); - - let args: serde_json::Value = serde_json::from_str(&tools[0].function.arguments).unwrap(); - assert!(args["nested"]["data"].is_array()); - assert_eq!(args["nested"]["config"]["enabled"], true); -} - -#[tokio::test] -async fn test_commentary_without_function() { - let parser = GptOssParser::new(); - - // Python should extract commentary as normal text - let input = r#"<|channel|>commentary<|message|>**Action plan**: 1. Do X 2. Do Y<|end|>"#; - - let (_normal_text, tools) = parser.parse_complete(input).await.unwrap(); - assert_eq!(tools.len(), 0); // No tool calls - // TODO: Verify normal text = "**Action plan**: 1. Do X 2. Do Y" -} - -#[tokio::test] -async fn test_final_channel() { - let parser = GptOssParser::new(); - - let input = r#"<|channel|>commentary to=functions.test<|constrain|>json<|message|>{"x": 1}<|call|> -<|channel|>final<|message|>The result is calculated.<|return|>"#; - - let (_normal_text, tools) = parser.parse_complete(input).await.unwrap(); - assert_eq!(tools.len(), 1); - assert_eq!(tools[0].function.name, "test"); - // TODO: Verify normal text = "The result is calculated." -} - -#[tokio::test] -async fn test_mixed_commentary_and_calls() { - let parser = GptOssParser::new(); - - let input = r#"<|channel|>commentary<|message|>Let me think<|end|> -<|channel|>commentary to=functions.calc<|constrain|>json<|message|>{"x": 5}<|call|> -<|channel|>commentary<|message|>Processing...<|end|>"#; - - let (_normal_text, tools) = parser.parse_complete(input).await.unwrap(); - assert_eq!(tools.len(), 1); - assert_eq!(tools[0].function.name, "calc"); - // TODO: Verify normal text = "Let me think Processing..." -}