1use async_trait::async_trait;
27use openai_protocol::common::Tool;
28use serde_json::Value;
29
30use crate::{
31 errors::ParserResult,
32 parsers::{
33 helpers,
34 pythonic::{expression_to_json, parse_python_expression},
35 },
36 traits::ToolParser,
37 types::{FunctionCall, StreamingParseResult, ToolCall, ToolCallItem},
38};
39
40const TOOL_CALLS_MARKER: &str = "<|tool_calls|>";
41
42#[derive(Debug, Clone, Copy, PartialEq)]
44enum ParseState {
45 Text,
47 InCalls,
49}
50
51pub struct SarashinaParser {
56 state: ParseState,
58 buffer: String,
60 stream_tool_index: usize,
62}
63
64impl SarashinaParser {
65 pub fn new() -> Self {
67 Self {
68 state: ParseState::Text,
69 buffer: String::new(),
70 stream_tool_index: 0,
71 }
72 }
73
74 fn starts_bare_list(trimmed: &str) -> bool {
77 let mut chars = trimmed.chars();
78 if chars.next() != Some('[') {
79 return false;
80 }
81 matches!(chars.find(|c| !c.is_whitespace()), Some('{'))
82 }
83
84 fn maybe_bare_list_prefix(trimmed: &str) -> bool {
87 if trimmed.is_empty() || Self::starts_bare_list(trimmed) {
88 return true;
89 }
90 let mut chars = trimmed.chars();
93 chars.next() == Some('[') && chars.all(|c| c.is_whitespace())
94 }
95
96 fn parse_payload(payload: &str) -> Vec<ToolCall> {
100 let Ok(expr) = parse_python_expression(payload) else {
101 return vec![];
102 };
103 let Ok(value) = expression_to_json(&expr) else {
104 return vec![];
105 };
106
107 let items = match value {
108 Value::Array(arr) => arr,
109 single @ Value::Object(_) => vec![single],
110 _ => return vec![],
111 };
112
113 let mut calls = Vec::new();
114 for item in items {
115 let Value::Object(obj) = item else { continue };
116 let Some(name) = obj.get("name").and_then(|v| v.as_str()) else {
117 continue;
118 };
119 let arguments = obj
120 .get("arguments")
121 .map(|v| v.to_string())
122 .unwrap_or_else(|| "{}".to_string());
123 calls.push(ToolCall {
124 function: FunctionCall {
125 name: name.to_string(),
126 arguments,
127 },
128 });
129 }
130 calls
131 }
132
133 fn find_balanced_end(s: &str, open_idx: usize) -> Option<usize> {
137 let bytes = s.as_bytes();
138 let open = bytes[open_idx];
139 let close = match open {
140 b'[' => b']',
141 b'{' => b'}',
142 _ => return None,
143 };
144
145 let mut depth = 0usize;
146 let mut in_str: Option<u8> = None;
147 let mut escaped = false;
148
149 for (i, &c) in bytes.iter().enumerate().skip(open_idx) {
150 if let Some(quote) = in_str {
151 if escaped {
152 escaped = false;
153 } else if c == b'\\' {
154 escaped = true;
155 } else if c == quote {
156 in_str = None;
157 }
158 } else if c == b'\'' || c == b'"' {
159 in_str = Some(c);
160 } else if c == open {
161 depth += 1;
162 } else if c == close {
163 depth -= 1;
164 if depth == 0 {
165 return Some(i + 1);
166 }
167 }
168 }
169 None
170 }
171
172 fn try_extract_calls(&mut self) -> StreamingParseResult {
177 let Some(open) = self.buffer.find(['[', '{']) else {
178 return StreamingParseResult::default();
179 };
180 let Some(end) = Self::find_balanced_end(&self.buffer, open) else {
181 return StreamingParseResult::default();
182 };
183
184 let calls = Self::parse_payload(&self.buffer[open..end]);
185 let consumed = self.buffer[..end].to_string();
186 self.buffer.drain(..end);
187 self.state = ParseState::Text;
188
189 if calls.is_empty() {
190 return StreamingParseResult {
192 normal_text: consumed,
193 calls: vec![],
194 };
195 }
196
197 let mut items = Vec::with_capacity(calls.len());
200 for call in calls {
201 items.push(ToolCallItem {
202 tool_index: self.stream_tool_index,
203 name: Some(call.function.name),
204 parameters: call.function.arguments,
205 });
206 self.stream_tool_index += 1;
207 }
208 StreamingParseResult {
209 normal_text: String::new(),
210 calls: items,
211 }
212 }
213}
214
215impl Default for SarashinaParser {
216 fn default() -> Self {
217 Self::new()
218 }
219}
220
221#[async_trait]
222impl ToolParser for SarashinaParser {
223 async fn parse_complete(&self, text: &str) -> ParserResult<(String, Vec<ToolCall>)> {
224 let (prefix, rest, had_marker) = match text.find(TOOL_CALLS_MARKER) {
226 Some(i) => (&text[..i], &text[i + TOOL_CALLS_MARKER.len()..], true),
227 None => ("", text, false),
228 };
229 let no_call = || {
232 if had_marker {
233 format!("{prefix}{rest}").trim().to_string()
234 } else {
235 text.trim().to_string()
236 }
237 };
238
239 let Some(open) = rest.find(['[', '{']) else {
240 return Ok((no_call(), vec![]));
241 };
242
243 if !had_marker && !rest[..open].trim().is_empty() {
247 return Ok((no_call(), vec![]));
248 }
249
250 let Some(end) = Self::find_balanced_end(rest, open) else {
251 return Ok((no_call(), vec![]));
252 };
253
254 let tool_calls = Self::parse_payload(&rest[open..end]);
255 if tool_calls.is_empty() {
256 return Ok((no_call(), vec![]));
258 }
259
260 let mut normal_text = prefix.to_string();
261 normal_text.push_str(&rest[end..]);
262 Ok((normal_text.trim().to_string(), tool_calls))
263 }
264
265 async fn parse_incremental(
266 &mut self,
267 chunk: &str,
268 _tools: &[Tool],
269 ) -> ParserResult<StreamingParseResult> {
270 self.buffer.push_str(chunk);
271
272 match self.state {
273 ParseState::Text => {
274 if let Some(pos) = self.buffer.find(TOOL_CALLS_MARKER) {
275 let text_before = self.buffer[..pos].to_string();
276 self.buffer.drain(..pos + TOOL_CALLS_MARKER.len());
277 self.state = ParseState::InCalls;
278
279 let mut result = self.try_extract_calls();
280 result.normal_text = format!("{}{}", text_before, result.normal_text);
281 return Ok(result);
282 }
283 if helpers::ends_with_partial_token(&self.buffer, TOOL_CALLS_MARKER).is_some() {
284 return Ok(StreamingParseResult::default());
286 }
287
288 let trimmed = self.buffer.trim_start();
290 if Self::starts_bare_list(trimmed) {
291 self.state = ParseState::InCalls;
292 return Ok(self.try_extract_calls());
293 }
294 if Self::maybe_bare_list_prefix(trimmed) {
295 return Ok(StreamingParseResult::default());
297 }
298
299 let normal_text = std::mem::take(&mut self.buffer);
300 Ok(StreamingParseResult {
301 normal_text,
302 calls: vec![],
303 })
304 }
305 ParseState::InCalls => Ok(self.try_extract_calls()),
306 }
307 }
308
309 fn has_tool_markers(&self, text: &str) -> bool {
310 text.contains(TOOL_CALLS_MARKER) || Self::starts_bare_list(text.trim_start())
311 }
312
313 fn reset(&mut self) {
314 self.state = ParseState::Text;
315 self.buffer.clear();
316 self.stream_tool_index = 0;
317 }
318}
319
320#[cfg(test)]
321mod tests {
322 use super::*;
323
324 #[tokio::test]
325 async fn test_with_marker() {
326 let parser = SarashinaParser::new();
327 let input = "<|tool_calls|>[{'name': 'get_weather', 'arguments': {'city': 'Tokyo'}}]";
328 let (normal_text, tools) = parser.parse_complete(input).await.unwrap();
329 assert_eq!(normal_text, "");
330 assert_eq!(tools.len(), 1);
331 assert_eq!(tools[0].function.name, "get_weather");
332 let args: Value = serde_json::from_str(&tools[0].function.arguments).unwrap();
333 assert_eq!(args["city"], "Tokyo");
334 }
335
336 #[tokio::test]
337 async fn test_markerless_bare_list() {
338 let parser = SarashinaParser::new();
340 let input = "[{'name': 'get_weather', 'arguments': {'city': 'Tokyo'}}]";
341 let (normal_text, tools) = parser.parse_complete(input).await.unwrap();
342 assert_eq!(normal_text, "");
343 assert_eq!(tools.len(), 1);
344 assert_eq!(tools[0].function.name, "get_weather");
345 let args: Value = serde_json::from_str(&tools[0].function.arguments).unwrap();
346 assert_eq!(args["city"], "Tokyo");
347 }
348
349 #[tokio::test]
350 async fn test_leading_text_with_marker() {
351 let parser = SarashinaParser::new();
352 let input = "Let me check.<|tool_calls|>[{'name': 'ping', 'arguments': {}}]";
353 let (normal_text, tools) = parser.parse_complete(input).await.unwrap();
354 assert_eq!(normal_text, "Let me check.");
355 assert_eq!(tools.len(), 1);
356 assert_eq!(tools[0].function.arguments, "{}");
357 }
358
359 #[tokio::test]
360 async fn test_multiple_tool_calls() {
361 let parser = SarashinaParser::new();
362 let input = "[{'name': 'search', 'arguments': {'q': 'rust'}}, \
363 {'name': 'get_weather', 'arguments': {'city': 'Paris'}}]";
364 let (_, tools) = parser.parse_complete(input).await.unwrap();
365 assert_eq!(tools.len(), 2);
366 assert_eq!(tools[0].function.name, "search");
367 assert_eq!(tools[1].function.name, "get_weather");
368 }
369
370 #[tokio::test]
371 async fn test_nested_arguments() {
372 let parser = SarashinaParser::new();
373 let input =
374 "[{'name': 'process', 'arguments': {'config': {'nested': {'values': [1, 2, 3]}}}}]";
375 let (_, tools) = parser.parse_complete(input).await.unwrap();
376 assert_eq!(tools.len(), 1);
377 let args: Value = serde_json::from_str(&tools[0].function.arguments).unwrap();
378 assert_eq!(
379 args["config"]["nested"]["values"],
380 serde_json::json!([1, 2, 3])
381 );
382 }
383
384 #[tokio::test]
385 async fn test_python_literals_coerced() {
386 let parser = SarashinaParser::new();
387 let input = "[{'name': 'flags', 'arguments': {'a': True, 'b': False, 'c': None}}]";
388 let (_, tools) = parser.parse_complete(input).await.unwrap();
389 let args: Value = serde_json::from_str(&tools[0].function.arguments).unwrap();
390 assert_eq!(args["a"], serde_json::json!(true));
391 assert_eq!(args["b"], serde_json::json!(false));
392 assert_eq!(args["c"], Value::Null);
393 }
394
395 #[tokio::test]
396 async fn test_apostrophe_in_value() {
397 let parser = SarashinaParser::new();
398 let input = "[{'name': 'say', 'arguments': {'text': 'it\\'s [done]'}}]";
399 let (_, tools) = parser.parse_complete(input).await.unwrap();
400 assert_eq!(tools.len(), 1);
401 let args: Value = serde_json::from_str(&tools[0].function.arguments).unwrap();
402 assert_eq!(args["text"], "it's [done]");
403 }
404
405 #[tokio::test]
406 async fn test_plain_text_untouched() {
407 let parser = SarashinaParser::new();
408 let input = "The weather in Tokyo is sunny.";
409 let (normal_text, tools) = parser.parse_complete(input).await.unwrap();
410 assert_eq!(tools.len(), 0);
411 assert_eq!(normal_text, "The weather in Tokyo is sunny.");
412 }
413
414 #[tokio::test]
415 async fn test_non_toolcall_list_untouched() {
416 let parser = SarashinaParser::new();
418 let input = "[1, 2, 3]";
419 let (normal_text, tools) = parser.parse_complete(input).await.unwrap();
420 assert_eq!(tools.len(), 0);
421 assert_eq!(normal_text, "[1, 2, 3]");
422 }
423
424 #[tokio::test]
425 async fn test_has_tool_markers() {
426 let parser = SarashinaParser::new();
427 assert!(parser.has_tool_markers("<|tool_calls|>[]"));
428 assert!(parser.has_tool_markers("[{'name': 'x', 'arguments': {}}]"));
429 assert!(!parser.has_tool_markers("just text"));
430 assert!(!parser.has_tool_markers("[TOOL_CALLS]")); }
432
433 #[tokio::test]
434 async fn test_streaming_markerless_chunked() {
435 let mut parser = SarashinaParser::new();
436 let chunks = [
437 "[{'name': 'get_",
438 "weather', 'arguments': {'city': 'To",
439 "kyo'}}]",
440 ];
441 let mut calls = Vec::new();
442 for c in chunks {
443 let r = parser.parse_incremental(c, &[]).await.unwrap();
444 calls.extend(r.calls);
445 }
446 assert_eq!(calls.len(), 1);
447 assert_eq!(calls[0].name.as_deref(), Some("get_weather"));
448 let args: Value = serde_json::from_str(&calls[0].parameters).unwrap();
449 assert_eq!(args["city"], "Tokyo");
450 }
451
452 #[tokio::test]
453 async fn test_streaming_with_marker_chunked() {
454 let mut parser = SarashinaParser::new();
455 let chunks = [
456 "Sure.",
457 "<|tool_ca",
458 "lls|>[{'name': 'ping', 'arg",
459 "uments': {}}]",
460 ];
461 let mut normal = String::new();
462 let mut calls = Vec::new();
463 for c in chunks {
464 let r = parser.parse_incremental(c, &[]).await.unwrap();
465 normal.push_str(&r.normal_text);
466 calls.extend(r.calls);
467 }
468 assert_eq!(normal, "Sure.");
469 assert_eq!(calls.len(), 1);
470 assert_eq!(calls[0].name.as_deref(), Some("ping"));
471 }
472
473 #[tokio::test]
474 async fn test_streaming_markerless_whitespace_split() {
475 let mut parser = SarashinaParser::new();
478 let chunks = ["[ ", " {'name': 'ping', 'arguments': {}}]"];
479 let mut normal = String::new();
480 let mut calls = Vec::new();
481 for c in chunks {
482 let r = parser.parse_incremental(c, &[]).await.unwrap();
483 normal.push_str(&r.normal_text);
484 calls.extend(r.calls);
485 }
486 assert_eq!(normal, "");
487 assert_eq!(calls.len(), 1);
488 assert_eq!(calls[0].name.as_deref(), Some("ping"));
489 }
490
491 #[tokio::test]
492 async fn test_marker_not_leaked_on_malformed() {
493 let parser = SarashinaParser::new();
496 let input = "before <|tool_calls|>[{'name': 'x'";
497 let (normal_text, tools) = parser.parse_complete(input).await.unwrap();
498 assert_eq!(tools.len(), 0);
499 assert!(!normal_text.contains("<|tool_calls|>"));
500 assert!(normal_text.starts_with("before"));
501 }
502}