Skip to main content

tool_parser/parsers/
deepseek31.rs

1use async_trait::async_trait;
2use openai_protocol::common::Tool;
3use regex::Regex;
4use serde_json::Value;
5
6use crate::{
7    errors::{ParserError, ParserResult},
8    parsers::helpers,
9    traits::ToolParser,
10    types::{FunctionCall, StreamingParseResult, ToolCall, ToolCallItem},
11};
12
13/// DeepSeek V3.1 format parser for tool calls
14///
15/// Handles the DeepSeek V3.1 format:
16/// `<|tool▁calls▁begin|><|tool▁call▁begin|>{name}<|tool▁sep|>{json_args}<|tool▁call▁end|><|tool▁calls▁end|>`
17///
18/// Differences from V3:
19/// - No `function` type prefix before `<|tool▁sep|>`
20/// - No markdown code block wrapping around JSON arguments
21/// - Function name directly after `<|tool▁call▁begin|>`
22/// - Raw JSON arguments directly after `<|tool▁sep|>`
23///
24/// Reference: https://huggingface.co/deepseek-ai/DeepSeek-V3.1
25pub struct DeepSeek31Parser {
26    /// Regex for extracting complete tool call blocks
27    tool_call_extractor: Regex,
28    /// Regex for extracting function name and arguments from a complete block
29    func_detail_extractor: Regex,
30    /// Regex for matching partial tool calls during streaming
31    partial_tool_call_regex: Regex,
32    /// Regex for removing completed tool calls from buffer
33    tool_call_end_pattern: Regex,
34
35    /// Buffer for accumulating incomplete patterns across chunks
36    buffer: String,
37
38    /// Stores complete tool call info (name and arguments) for each tool being parsed
39    prev_tool_call_arr: Vec<Value>,
40
41    /// Index of currently streaming tool call (-1 means no active tool)
42    current_tool_id: i32,
43
44    /// Flag for whether current tool's name has been sent to client
45    current_tool_name_sent: bool,
46
47    /// Tracks raw JSON string content streamed to client for each tool's arguments
48    streamed_args_for_tool: Vec<String>,
49}
50
51impl DeepSeek31Parser {
52    /// Create a new DeepSeek V3.1 parser
53    #[expect(
54        clippy::expect_used,
55        reason = "regex patterns are compile-time string literals"
56    )]
57    pub fn new() -> Self {
58        let tool_call_extractor = Regex::new(r"(?s)<|tool▁call▁begin|>.*?<|tool▁call▁end|>")
59            .expect("Valid regex pattern");
60
61        let func_detail_extractor =
62            Regex::new(r"(?s)<|tool▁call▁begin|>(.*?)<|tool▁sep|>(.*?)<|tool▁call▁end|>")
63                .expect("Valid regex pattern");
64
65        let partial_tool_call_regex =
66            Regex::new(r"(?s)<|tool▁call▁begin|>(.*)<|tool▁sep|>(.*)")
67                .expect("Valid regex pattern");
68
69        let tool_call_end_pattern = Regex::new(r"(?s)<|tool▁call▁begin|>.*?<|tool▁call▁end|>")
70            .expect("Valid regex pattern");
71
72        Self {
73            tool_call_extractor,
74            func_detail_extractor,
75            partial_tool_call_regex,
76            tool_call_end_pattern,
77            buffer: String::new(),
78            prev_tool_call_arr: Vec::new(),
79            current_tool_id: -1,
80            current_tool_name_sent: false,
81            streamed_args_for_tool: Vec::new(),
82        }
83    }
84
85    /// Parse a single complete tool call block
86    fn parse_tool_call(&self, block: &str) -> ParserResult<ToolCall> {
87        let captures = self.func_detail_extractor.captures(block).ok_or_else(|| {
88            ParserError::ParsingFailed("Failed to match tool call pattern".to_string())
89        })?;
90
91        let func_name = captures.get(1).map_or("", |m| m.as_str()).trim();
92        if func_name.is_empty() {
93            return Err(ParserError::ParsingFailed(
94                "Empty function name".to_string(),
95            ));
96        }
97
98        let json_args = captures.get(2).map_or("{}", |m| m.as_str()).trim();
99
100        let value = serde_json::from_str::<Value>(json_args)
101            .map_err(|e| ParserError::ParsingFailed(format!("Invalid JSON: {e}")))?;
102
103        let args = if value.is_object() {
104            value
105        } else {
106            serde_json::json!({ "value": value })
107        };
108
109        let arguments =
110            serde_json::to_string(&args).map_err(|e| ParserError::ParsingFailed(e.to_string()))?;
111
112        Ok(ToolCall {
113            function: FunctionCall {
114                name: func_name.to_string(),
115                arguments,
116            },
117        })
118    }
119}
120
121impl Default for DeepSeek31Parser {
122    fn default() -> Self {
123        Self::new()
124    }
125}
126
127#[async_trait]
128impl ToolParser for DeepSeek31Parser {
129    async fn parse_complete(&self, text: &str) -> ParserResult<(String, Vec<ToolCall>)> {
130        if !self.has_tool_markers(text) {
131            return Ok((text.to_string(), vec![]));
132        }
133
134        let idx = text
135            .find("<|tool▁calls▁begin|>")
136            .ok_or_else(|| ParserError::ParsingFailed("tool call marker not found".to_string()))?;
137        let normal_text = text[..idx].to_string();
138
139        let mut tools = Vec::new();
140        for mat in self.tool_call_extractor.find_iter(text) {
141            match self.parse_tool_call(mat.as_str()) {
142                Ok(tool) => tools.push(tool),
143                Err(e) => {
144                    tracing::debug!("Failed to parse tool call: {}", e);
145                    continue;
146                }
147            }
148        }
149
150        if tools.is_empty() {
151            return Ok((text.to_string(), vec![]));
152        }
153
154        Ok((normal_text, tools))
155    }
156
157    async fn parse_incremental(
158        &mut self,
159        chunk: &str,
160        tools: &[Tool],
161    ) -> ParserResult<StreamingParseResult> {
162        self.buffer.push_str(chunk);
163        let current_text = self.buffer.clone();
164
165        let has_tool_call =
166            self.has_tool_markers(&current_text) || current_text.contains("<|tool▁call▁begin|>");
167
168        if !has_tool_call {
169            let mut normal_text = std::mem::take(&mut self.buffer);
170            for end_token in [
171                "<|tool▁calls▁end|>",
172                "<|tool▁call▁end|>",
173                "<|end▁of▁sentence|>",
174            ] {
175                normal_text = normal_text.replace(end_token, "");
176            }
177            return Ok(StreamingParseResult {
178                normal_text,
179                calls: vec![],
180            });
181        }
182
183        let tool_indices = helpers::get_tool_indices(tools);
184        let mut calls: Vec<ToolCallItem> = Vec::new();
185
186        if let Some(captures) = self.partial_tool_call_regex.captures(&current_text) {
187            let func_name = captures.get(1).map_or("", |m| m.as_str()).trim();
188            let func_args_raw = captures.get(2).map_or("", |m| m.as_str()).trim();
189
190            if !tool_indices.contains_key(func_name) {
191                tracing::debug!("Invalid tool name '{}' - skipping", func_name);
192                helpers::reset_current_tool_state(
193                    &mut self.buffer,
194                    &mut self.current_tool_name_sent,
195                    &mut self.streamed_args_for_tool,
196                    &self.prev_tool_call_arr,
197                );
198                return Ok(StreamingParseResult::default());
199            }
200
201            if self.current_tool_id == -1 {
202                self.current_tool_id = 0;
203                self.prev_tool_call_arr = Vec::new();
204                self.streamed_args_for_tool = vec![String::new()];
205            }
206
207            helpers::ensure_capacity(
208                self.current_tool_id,
209                &mut self.prev_tool_call_arr,
210                &mut self.streamed_args_for_tool,
211            );
212
213            if self.current_tool_name_sent {
214                let tool_id = self.current_tool_id as usize;
215                let last_sent = self
216                    .streamed_args_for_tool
217                    .get(tool_id)
218                    .map(|s| s.as_str())
219                    .unwrap_or("");
220
221                // Strip end markers that may arrive in the same chunk as the final
222                // JSON bytes. The partial_tool_call_regex group 2 greedily captures
223                // everything after <|tool▁sep|>, including any trailing end tokens.
224                // Use an iterative loop so stacked markers in any order are all removed.
225                const END_MARKERS: [&str; 3] = [
226                    "<|end▁of▁sentence|>",
227                    "<|tool▁calls▁end|>",
228                    "<|tool▁call▁end|>",
229                ];
230                let mut func_args_clean = func_args_raw.trim_end();
231                loop {
232                    let before = func_args_clean;
233                    for marker in END_MARKERS {
234                        func_args_clean = func_args_clean.trim_end_matches(marker).trim_end();
235                    }
236                    if func_args_clean == before {
237                        break;
238                    }
239                }
240
241                let argument_diff = func_args_clean
242                    .strip_prefix(last_sent)
243                    .unwrap_or(func_args_clean);
244
245                if !argument_diff.is_empty() {
246                    calls.push(ToolCallItem {
247                        tool_index: tool_id,
248                        name: None,
249                        parameters: argument_diff.to_string(),
250                    });
251                    if tool_id < self.streamed_args_for_tool.len() {
252                        self.streamed_args_for_tool[tool_id].push_str(argument_diff);
253                    }
254                }
255
256                if helpers::is_complete_json(func_args_clean) {
257                    if let Ok(parsed_args) = serde_json::from_str::<Value>(func_args_clean) {
258                        let tool_id = self.current_tool_id as usize;
259                        if tool_id < self.prev_tool_call_arr.len() {
260                            if let Some(obj) = self.prev_tool_call_arr[tool_id].as_object_mut() {
261                                obj.insert("arguments".to_string(), parsed_args);
262                            }
263                        }
264                    }
265
266                    if let Some(mat) = self.tool_call_end_pattern.find(&current_text) {
267                        self.buffer = current_text[mat.end()..].to_string();
268                    } else {
269                        self.buffer.clear();
270                    }
271
272                    let result = StreamingParseResult {
273                        normal_text: String::new(),
274                        calls,
275                    };
276
277                    self.current_tool_id += 1;
278                    self.current_tool_name_sent = false;
279                    return Ok(result);
280                }
281            } else {
282                calls.push(ToolCallItem {
283                    tool_index: self.current_tool_id as usize,
284                    name: Some(func_name.to_string()),
285                    parameters: String::new(),
286                });
287                self.current_tool_name_sent = true;
288
289                let tool_id = self.current_tool_id as usize;
290                if self.prev_tool_call_arr.len() <= tool_id {
291                    self.prev_tool_call_arr
292                        .resize_with(tool_id + 1, || Value::Null);
293                }
294                self.prev_tool_call_arr[tool_id] = serde_json::json!({
295                    "name": func_name,
296                    "arguments": {},
297                });
298            }
299        }
300
301        Ok(StreamingParseResult {
302            normal_text: String::new(),
303            calls,
304        })
305    }
306
307    fn has_tool_markers(&self, text: &str) -> bool {
308        text.contains("<|tool▁calls▁begin|>")
309    }
310
311    fn get_unstreamed_tool_args(&self) -> Option<Vec<ToolCallItem>> {
312        helpers::get_unstreamed_args(&self.prev_tool_call_arr, &self.streamed_args_for_tool)
313    }
314
315    fn reset(&mut self) {
316        self.buffer.clear();
317        self.prev_tool_call_arr.clear();
318        self.current_tool_id = -1;
319        self.current_tool_name_sent = false;
320        self.streamed_args_for_tool.clear();
321    }
322}