1use std::collections::HashSet;
28
29use adk_core::Part;
30
31pub fn contains_tool_call_tag(text: &str) -> bool {
33 text.contains("<tool_call>")
34 || text.contains("<|tool_call>")
35 || text.contains("<|python_tag|>")
36 || text.contains("[TOOL_CALLS]")
37 || text.contains("<|tool▁call")
38 || text.contains("<|action_start|>")
39 || (text.contains("```json") && text.contains("\"name\""))
40}
41
42pub fn parse_text_tool_calls(text: &str) -> Option<Vec<Part>> {
50 if !contains_tool_call_tag(text) {
51 return None;
52 }
53
54 let mut parts = Vec::new();
55
56 if let Some(parsed) = parse_qwen_format(text, &mut parts) {
58 return Some(parsed);
59 }
60
61 if let Some(parsed) = parse_llama_format(text, &mut parts) {
63 return Some(parsed);
64 }
65
66 if let Some(parsed) = parse_mistral_nemo_format(text, &mut parts) {
68 return Some(parsed);
69 }
70
71 if let Some(parsed) = parse_deepseek_format(text) {
73 return Some(parsed);
74 }
75
76 if let Some(parsed) = parse_gemma4_format(text) {
78 return Some(parsed);
79 }
80
81 if let Some(parsed) = parse_action_tag_format(text) {
83 return Some(parsed);
84 }
85
86 None
87}
88
89pub fn parse_declared_tool_calls(
113 text: &str,
114 declared_tools: &HashSet<String>,
115) -> Option<Vec<Part>> {
116 if declared_tools.is_empty() {
117 return None;
118 }
119 let parts = parse_text_tool_calls(text)?;
120 let all_declared = parts.iter().all(|part| match part {
121 Part::FunctionCall { name, .. } => declared_tools.contains(name),
122 _ => true,
123 });
124 let has_call = parts.iter().any(|part| matches!(part, Part::FunctionCall { .. }));
125 (all_declared && has_call).then_some(parts)
126}
127
128fn parse_qwen_format(text: &str, _parts: &mut Vec<Part>) -> Option<Vec<Part>> {
134 let mut result = Vec::new();
135 let mut remaining = text;
136
137 loop {
138 let start = remaining.find("<tool_call>")?;
139
140 let before = &remaining[..start];
142 if !before.is_empty() {
143 result.push(Part::Text { text: before.to_string() });
144 }
145
146 let after_open = &remaining[start + "<tool_call>".len()..];
147 let end = after_open.find("</tool_call>")?;
148 let inner = after_open[..end].trim();
149
150 if let Some(part) = parse_json_tool_call(inner) {
152 result.push(part);
153 }
154 else if let Some(part) = parse_function_tag(inner) {
156 result.push(part);
157 } else {
158 result.push(Part::Text {
160 text: remaining[start..start + "<tool_call>".len() + end + "</tool_call>".len()]
161 .to_string(),
162 });
163 }
164
165 remaining = &after_open[end + "</tool_call>".len()..];
166 if remaining.trim().is_empty() || !remaining.contains("<tool_call>") {
167 let trailing = remaining;
168 if !trailing.is_empty() {
169 result.push(Part::Text { text: trailing.to_string() });
170 }
171 break;
172 }
173 }
174
175 if result.is_empty() { None } else { Some(result) }
176}
177
178fn parse_function_tag(inner: &str) -> Option<Part> {
180 let func_start = inner.find("<function=")?;
181 let after_eq = &inner[func_start + "<function=".len()..];
182 let name_end = after_eq.find('>')?;
183 let name = after_eq[..name_end].trim().to_string();
184
185 let body_start = name_end + 1;
186 let func_end = after_eq.find("</function>")?;
187 let body = after_eq[body_start..func_end].trim();
188
189 let args = if body.is_empty() {
190 serde_json::json!({})
191 } else {
192 serde_json::from_str(body).unwrap_or_else(|_| serde_json::json!({}))
193 };
194
195 Some(Part::FunctionCall { name, args, id: None, thought_signature: None })
196}
197
198fn parse_json_tool_call(json_str: &str) -> Option<Part> {
201 let value: serde_json::Value = serde_json::from_str(json_str).ok()?;
202 let obj = value.as_object()?;
203
204 let name =
205 obj.get("name").or_else(|| obj.get("function")).and_then(|v| v.as_str())?.to_string();
206
207 let args = obj
208 .get("arguments")
209 .or_else(|| obj.get("parameters"))
210 .cloned()
211 .unwrap_or(serde_json::json!({}));
212
213 Some(Part::FunctionCall { name, args, id: None, thought_signature: None })
214}
215
216fn parse_llama_format(text: &str, _parts: &mut Vec<Part>) -> Option<Vec<Part>> {
218 let tag = "<|python_tag|>";
219 let start = text.find(tag)?;
220
221 let mut result = Vec::new();
222 let before = &text[..start];
223 if !before.is_empty() {
224 result.push(Part::Text { text: before.to_string() });
225 }
226
227 let json_str = text[start + tag.len()..].trim();
228 if let Some(part) = parse_json_tool_call(json_str) {
229 result.push(part);
230 } else {
231 return None;
232 }
233
234 Some(result)
235}
236
237fn parse_mistral_nemo_format(text: &str, _parts: &mut Vec<Part>) -> Option<Vec<Part>> {
239 let tag = "[TOOL_CALLS]";
240 let start = text.find(tag)?;
241
242 let mut result = Vec::new();
243 let before = &text[..start];
244 if !before.is_empty() {
245 result.push(Part::Text { text: before.to_string() });
246 }
247
248 let json_str = text[start + tag.len()..].trim();
249 let arr: Vec<serde_json::Value> = serde_json::from_str(json_str).ok()?;
251 for item in &arr {
252 let obj = item.as_object()?;
253 let name = obj.get("name").and_then(|v| v.as_str())?.to_string();
254 let args = obj
255 .get("arguments")
256 .or_else(|| obj.get("parameters"))
257 .cloned()
258 .unwrap_or(serde_json::json!({}));
259 result.push(Part::FunctionCall { name, args, id: None, thought_signature: None });
260 }
261
262 if result.is_empty() { None } else { Some(result) }
263}
264
265const DEEPSEEK_END_MARKERS: &[&str] = &[
267 "<\u{ff5c}tool\u{2581}call\u{2581}end\u{ff5c}>",
268 "<\u{ff5c}tool\u{2581}calls\u{2581}end\u{ff5c}>",
269];
270
271fn parse_deepseek_format(text: &str) -> Option<Vec<Part>> {
278 let fence_start = text.find("```json")?;
279 let json_start = fence_start + "```json".len();
280 let after_fence = &text[json_start..];
281 let fence_end = after_fence.find("```")?;
282 let json_str = after_fence[..fence_end].trim();
283
284 let value: serde_json::Value = serde_json::from_str(json_str).ok()?;
285 let calls = match &value {
286 serde_json::Value::Array(items) => {
287 items.iter().map(parse_fenced_tool_call).collect::<Option<Vec<_>>>()?
288 }
289 object => vec![parse_fenced_tool_call(object)?],
290 };
291 if calls.is_empty() {
292 return None;
293 }
294
295 let mut result = Vec::new();
296 let before = &text[..fence_start];
297 if !before.is_empty() {
298 result.push(Part::Text { text: before.to_string() });
299 }
300 result.extend(calls);
301
302 let mut trailing = &after_fence[fence_end + "```".len()..];
303 while let Some(rest) =
304 DEEPSEEK_END_MARKERS.iter().find_map(|m| trailing.trim_start().strip_prefix(m))
305 {
306 trailing = rest;
307 }
308 if !trailing.is_empty() {
309 result.push(Part::Text { text: trailing.to_string() });
310 }
311
312 Some(result)
313}
314
315fn parse_fenced_tool_call(value: &serde_json::Value) -> Option<Part> {
318 let obj = value.as_object()?;
319 let name = obj.get("name").or_else(|| obj.get("function")).and_then(|v| v.as_str())?;
320 let args = obj.get("arguments").or_else(|| obj.get("parameters")).filter(|v| v.is_object())?;
321 Some(Part::FunctionCall {
322 name: name.to_string(),
323 args: args.clone(),
324 id: None,
325 thought_signature: None,
326 })
327}
328
329fn parse_gemma4_format(text: &str) -> Option<Vec<Part>> {
334 let mut result = Vec::new();
335 let mut remaining = text;
336
337 loop {
338 let start = remaining.find("<|tool_call>")?;
339 let before = &remaining[..start];
340 if !before.is_empty() {
341 result.push(Part::Text { text: before.to_string() });
342 }
343
344 let after_open = &remaining[start + "<|tool_call>".len()..];
345 let end = after_open.find("<tool_call|>")?;
346 let inner = after_open[..end].trim();
347
348 if let Some(call_body) = inner.strip_prefix("call:") {
350 let brace_start = call_body.find('{');
351 let name = if let Some(bs) = brace_start {
352 call_body[..bs].trim().to_string()
353 } else {
354 call_body.trim().to_string()
355 };
356
357 let args = if let Some(bs) = brace_start {
358 let args_raw = &call_body[bs..];
359 let json_str = args_raw.replace("<|\"|>", "\"");
361 serde_json::from_str(&json_str).unwrap_or(serde_json::json!({}))
362 } else {
363 serde_json::json!({})
364 };
365
366 result.push(Part::FunctionCall { name, args, id: None, thought_signature: None });
367 }
368
369 remaining = &after_open[end + "<tool_call|>".len()..];
370 if remaining.trim().is_empty() || !remaining.contains("<|tool_call>") {
371 let trailing = remaining;
372 if !trailing.is_empty() {
373 result.push(Part::Text { text: trailing.to_string() });
374 }
375 break;
376 }
377 }
378
379 if result.is_empty() { None } else { Some(result) }
380}
381
382fn parse_action_tag_format(text: &str) -> Option<Vec<Part>> {
387 let start_tag = "<|action_start|>";
388 let end_tag = "<|action_end|>";
389
390 let start = text.find(start_tag)?;
391 let mut result = Vec::new();
392
393 let before = &text[..start];
394 if !before.is_empty() {
395 result.push(Part::Text { text: before.to_string() });
396 }
397
398 let after_open = &text[start + start_tag.len()..];
399 let end = after_open.find(end_tag)?;
400 let inner = after_open[..end].trim();
401
402 if let Some(part) = parse_json_tool_call(inner) {
403 result.push(part);
404 } else {
405 return None;
406 }
407
408 let trailing = &after_open[end + end_tag.len()..];
409 if !trailing.is_empty() {
410 result.push(Part::Text { text: trailing.to_string() });
411 }
412
413 Some(result)
414}
415
416const TOOL_CALL_PREFIXES: &[&str] = &[
420 "<tool_call",
421 "<|tool_call>",
422 "<|python_tag|",
423 "[TOOL_CALLS]",
424 "<|action_start|>",
425 "<\u{ff5c}\u{2581}tool", ];
427
428const MAX_BUFFER_SIZE: usize = 4096;
430
431pub struct ToolCallBuffer {
454 buffer: String,
455 buffering: bool,
456 declared_tools: Option<HashSet<String>>,
458}
459
460pub enum BufferAction {
462 Emit(Vec<Part>),
464 Buffering,
466}
467
468impl ToolCallBuffer {
469 pub fn new() -> Self {
471 Self { buffer: String::new(), buffering: false, declared_tools: None }
472 }
473
474 pub fn for_declared_tools(declared_tools: HashSet<String>) -> Self {
495 Self { buffer: String::new(), buffering: false, declared_tools: Some(declared_tools) }
496 }
497
498 pub fn push(&mut self, text: &str) -> BufferAction {
503 self.buffer.push_str(text);
504
505 if self.declared_tools.as_ref().is_some_and(HashSet::is_empty) {
506 return self.flush_as_emit();
507 }
508
509 if self.buffering {
510 if self.has_complete_tool_call() {
512 return self.try_parse_and_emit();
513 }
514 if !self.starts_tool_call_prefix() && !self.has_partial_prefix() {
516 return self.flush_as_emit();
517 }
518 if self.buffer.len() > MAX_BUFFER_SIZE {
520 return self.flush_as_emit();
521 }
522 BufferAction::Buffering
523 } else {
524 if self.starts_tool_call_prefix() {
526 self.buffering = true;
527 if self.has_complete_tool_call() {
529 return self.try_parse_and_emit();
530 }
531 BufferAction::Buffering
532 } else if self.has_partial_prefix() {
533 self.buffering = true;
535 BufferAction::Buffering
536 } else {
537 self.flush_as_emit()
539 }
540 }
541 }
542
543 pub fn flush(&mut self) -> Vec<Part> {
546 if self.buffer.is_empty() {
547 return Vec::new();
548 }
549
550 if let Some(parts) = self.parse_buffer() {
552 self.buffer.clear();
553 self.buffering = false;
554 return parts;
555 }
556
557 let text = std::mem::take(&mut self.buffer);
559 self.buffering = false;
560 if text.is_empty() { Vec::new() } else { vec![Part::Text { text }] }
561 }
562
563 fn parse_buffer(&self) -> Option<Vec<Part>> {
564 match &self.declared_tools {
565 Some(declared_tools) => parse_declared_tool_calls(&self.buffer, declared_tools),
566 None => parse_text_tool_calls(&self.buffer),
567 }
568 }
569
570 fn starts_tool_call_prefix(&self) -> bool {
571 TOOL_CALL_PREFIXES.iter().any(|prefix| self.buffer.contains(prefix))
572 }
573
574 fn has_partial_prefix(&self) -> bool {
575 let buf = &self.buffer;
577 for prefix in TOOL_CALL_PREFIXES {
578 let prefix_chars: Vec<char> = prefix.chars().collect();
580 for i in 1..prefix_chars.len() {
581 let partial: String = prefix_chars[..i].iter().collect();
582 if buf.ends_with(&partial) {
583 return true;
584 }
585 }
586 }
587 false
588 }
589
590 fn has_complete_tool_call(&self) -> bool {
591 (self.buffer.contains("<tool_call>") && self.buffer.contains("</tool_call>"))
592 || (self.buffer.contains("<|tool_call>") && self.buffer.contains("<tool_call|>"))
593 || (self.buffer.contains("<|python_tag|>")
594 && self.buffer.contains('\n')
595 && self.buffer.len() > "<|python_tag|>".len() + 5)
596 || (self.buffer.contains("[TOOL_CALLS]")
597 && self.buffer.contains(']')
598 && self.buffer.rfind(']') > self.buffer.find("[TOOL_CALLS]").map(|i| i + 12))
599 || (self.buffer.contains("```json") && self.buffer.matches("```").count() >= 2)
600 || (self.buffer.contains("<|action_start|>") && self.buffer.contains("<|action_end|>"))
601 }
602
603 fn try_parse_and_emit(&mut self) -> BufferAction {
604 if let Some(parts) = self.parse_buffer() {
605 self.buffer.clear();
606 self.buffering = false;
607 BufferAction::Emit(parts)
608 } else {
609 self.flush_as_emit()
611 }
612 }
613
614 fn flush_as_emit(&mut self) -> BufferAction {
615 let text = std::mem::take(&mut self.buffer);
616 self.buffering = false;
617 if text.is_empty() {
618 BufferAction::Emit(Vec::new())
619 } else {
620 BufferAction::Emit(vec![Part::Text { text }])
621 }
622 }
623}
624
625impl Default for ToolCallBuffer {
626 fn default() -> Self {
627 Self::new()
628 }
629}
630
631#[cfg(test)]
632mod tests {
633 use super::*;
634
635 #[test]
636 fn test_qwen_json_format() {
637 let text =
638 r#"<tool_call>{"name": "get_weather", "arguments": {"city": "Tokyo"}}</tool_call>"#;
639 let parts = parse_text_tool_calls(text).unwrap();
640 assert_eq!(parts.len(), 1);
641 match &parts[0] {
642 Part::FunctionCall { name, args, .. } => {
643 assert_eq!(name, "get_weather");
644 assert_eq!(args["city"], "Tokyo");
645 }
646 _ => panic!("expected FunctionCall"),
647 }
648 }
649
650 #[test]
651 fn test_qwen_function_tag_format() {
652 let text = r#"<tool_call><function=screenshot></function></tool_call>"#;
653 let parts = parse_text_tool_calls(text).unwrap();
654 assert_eq!(parts.len(), 1);
655 match &parts[0] {
656 Part::FunctionCall { name, args, .. } => {
657 assert_eq!(name, "screenshot");
658 assert_eq!(*args, serde_json::json!({}));
659 }
660 _ => panic!("expected FunctionCall"),
661 }
662 }
663
664 #[test]
665 fn test_qwen_function_tag_with_args() {
666 let text = r#"<tool_call><function=get_weather>{"city": "Tokyo"}</function></tool_call>"#;
667 let parts = parse_text_tool_calls(text).unwrap();
668 assert_eq!(parts.len(), 1);
669 match &parts[0] {
670 Part::FunctionCall { name, args, .. } => {
671 assert_eq!(name, "get_weather");
672 assert_eq!(args["city"], "Tokyo");
673 }
674 _ => panic!("expected FunctionCall"),
675 }
676 }
677
678 #[test]
679 fn test_text_before_tool_call() {
680 let text = r#"Let me check that for you.
681<tool_call>{"name": "search", "arguments": {"query": "rust"}}</tool_call>"#;
682 let parts = parse_text_tool_calls(text).unwrap();
683 assert_eq!(parts.len(), 2);
684 assert!(matches!(&parts[0], Part::Text { text } if text.contains("check that")));
685 assert!(matches!(&parts[1], Part::FunctionCall { name, .. } if name == "search"));
686 }
687
688 #[test]
689 fn test_multiple_tool_calls() {
690 let text = r#"<tool_call>{"name": "a", "arguments": {}}</tool_call>
695<tool_call>{"name": "b", "arguments": {"x": 1}}</tool_call>"#;
696 let parts = parse_text_tool_calls(text).unwrap();
697 assert_eq!(parts.len(), 3);
698 assert!(matches!(&parts[0], Part::FunctionCall { name, .. } if name == "a"));
699 assert!(matches!(&parts[1], Part::Text { text } if text == "\n"));
700 assert!(matches!(&parts[2], Part::FunctionCall { name, .. } if name == "b"));
701 }
702
703 #[test]
704 fn test_llama_format() {
705 let text = r#"<|python_tag|>{"name": "get_weather", "parameters": {"city": "NYC"}}"#;
706 let parts = parse_text_tool_calls(text).unwrap();
707 assert_eq!(parts.len(), 1);
708 match &parts[0] {
709 Part::FunctionCall { name, args, .. } => {
710 assert_eq!(name, "get_weather");
711 assert_eq!(args["city"], "NYC");
712 }
713 _ => panic!("expected FunctionCall"),
714 }
715 }
716
717 #[test]
718 fn test_mistral_nemo_format() {
719 let text = r#"[TOOL_CALLS][{"name": "search", "arguments": {"q": "rust"}}]"#;
720 let parts = parse_text_tool_calls(text).unwrap();
721 assert_eq!(parts.len(), 1);
722 match &parts[0] {
723 Part::FunctionCall { name, args, .. } => {
724 assert_eq!(name, "search");
725 assert_eq!(args["q"], "rust");
726 }
727 _ => panic!("expected FunctionCall"),
728 }
729 }
730
731 #[test]
732 fn test_no_tool_call_returns_none() {
733 assert!(parse_text_tool_calls("Hello, how can I help?").is_none());
734 assert!(parse_text_tool_calls("").is_none());
735 }
736
737 #[test]
738 fn test_contains_tool_call_tag() {
739 assert!(contains_tool_call_tag("<tool_call>"));
740 assert!(contains_tool_call_tag("text <tool_call> more"));
741 assert!(contains_tool_call_tag("<|python_tag|>"));
742 assert!(contains_tool_call_tag("[TOOL_CALLS]"));
743 assert!(!contains_tool_call_tag("normal text"));
744 }
745
746 #[test]
749 fn test_buffer_normal_text_emits_immediately() {
750 let mut buf = ToolCallBuffer::new();
751 match buf.push("Hello world") {
752 BufferAction::Emit(parts) => {
753 assert_eq!(parts.len(), 1);
754 assert!(matches!(&parts[0], Part::Text { text } if text == "Hello world"));
755 }
756 BufferAction::Buffering => panic!("should emit immediately"),
757 }
758 }
759
760 #[test]
761 fn test_buffer_complete_tool_call_in_one_chunk() {
762 let mut buf = ToolCallBuffer::new();
763 let text = r#"<tool_call>{"name": "search", "arguments": {"q": "rust"}}</tool_call>"#;
764 match buf.push(text) {
765 BufferAction::Emit(parts) => {
766 assert_eq!(parts.len(), 1);
767 assert!(matches!(&parts[0], Part::FunctionCall { name, .. } if name == "search"));
768 }
769 BufferAction::Buffering => panic!("should emit parsed tool call"),
770 }
771 }
772
773 #[test]
774 fn test_buffer_tool_call_split_across_chunks() {
775 let mut buf = ToolCallBuffer::new();
776
777 assert!(matches!(buf.push("<tool_call>"), BufferAction::Buffering));
779
780 assert!(matches!(
782 buf.push(r#"{"name": "get_weather", "arguments": {"city": "Tokyo"}}"#),
783 BufferAction::Buffering
784 ));
785
786 match buf.push("</tool_call>") {
788 BufferAction::Emit(parts) => {
789 assert_eq!(parts.len(), 1);
790 assert!(
791 matches!(&parts[0], Part::FunctionCall { name, .. } if name == "get_weather")
792 );
793 }
794 BufferAction::Buffering => panic!("should emit after closing tag"),
795 }
796 }
797
798 #[test]
799 fn test_buffer_text_then_tool_call() {
800 let mut buf = ToolCallBuffer::new();
801
802 match buf.push("Let me check. ") {
804 BufferAction::Emit(parts) => {
805 assert_eq!(parts.len(), 1);
806 assert!(matches!(&parts[0], Part::Text { .. }));
807 }
808 BufferAction::Buffering => panic!("should emit text"),
809 }
810
811 let tc = r#"<tool_call>{"name": "search", "arguments": {}}</tool_call>"#;
813 match buf.push(tc) {
814 BufferAction::Emit(parts) => {
815 assert_eq!(parts.len(), 1);
816 assert!(matches!(&parts[0], Part::FunctionCall { name, .. } if name == "search"));
817 }
818 BufferAction::Buffering => panic!("should emit tool call"),
819 }
820 }
821
822 #[test]
823 fn test_buffer_flush_incomplete_as_text() {
824 let mut buf = ToolCallBuffer::new();
825 assert!(matches!(buf.push("<tool_call>partial"), BufferAction::Buffering));
826
827 let parts = buf.flush();
829 assert_eq!(parts.len(), 1);
830 assert!(matches!(&parts[0], Part::Text { text } if text.contains("<tool_call>")));
831 }
832
833 #[test]
834 fn test_buffer_flush_empty() {
835 let mut buf = ToolCallBuffer::new();
836 let parts = buf.flush();
837 assert!(parts.is_empty());
838 }
839
840 #[test]
841 fn test_buffer_partial_prefix_detection() {
842 let mut buf = ToolCallBuffer::new();
843 assert!(matches!(buf.push("<tool"), BufferAction::Buffering));
845 assert!(matches!(buf.push("_call>"), BufferAction::Buffering));
847 match buf.push(r#"{"name":"x","arguments":{}}</tool_call>"#) {
849 BufferAction::Emit(parts) => {
850 assert_eq!(parts.len(), 1);
851 assert!(matches!(&parts[0], Part::FunctionCall { name, .. } if name == "x"));
852 }
853 BufferAction::Buffering => panic!("should emit"),
854 }
855 }
856
857 #[test]
858 fn test_buffer_markdown_link_is_not_mistaken_for_tool_call() {
859 let mut buf = ToolCallBuffer::new();
860
861 assert!(matches!(buf.push("["), BufferAction::Buffering));
862
863 match buf.push("documentation](https://example.com) trailing text") {
864 BufferAction::Emit(parts) => {
865 assert_eq!(
866 parts,
867 vec trailing text".to_string()
869 }]
870 );
871 }
872 BufferAction::Buffering => panic!("markdown link must not stay buffered"),
873 }
874
875 assert!(buf.flush().is_empty());
876 }
877
878 #[test]
879 fn test_buffer_mistral_prefix_can_still_span_chunks() {
880 let mut buf = ToolCallBuffer::new();
881
882 assert!(matches!(buf.push("["), BufferAction::Buffering));
883 assert!(matches!(buf.push("TOOL"), BufferAction::Buffering));
884
885 match buf.push(r#"_CALLS][{"name":"search","arguments":{"q":"rust"}}]"#) {
886 BufferAction::Emit(parts) => {
887 assert_eq!(parts.len(), 1);
888 assert!(matches!(
889 &parts[0],
890 Part::FunctionCall { name, .. } if name == "search"
891 ));
892 }
893 BufferAction::Buffering => panic!("should emit parsed tool call"),
894 }
895 }
896
897 #[test]
900 fn test_deepseek_json_fence() {
901 let text = "```json\n{\"name\": \"search\", \"arguments\": {\"q\": \"rust\"}}\n```";
902 let parts = parse_text_tool_calls(text).unwrap();
903 assert_eq!(parts.len(), 1);
904 match &parts[0] {
905 Part::FunctionCall { name, args, .. } => {
906 assert_eq!(name, "search");
907 assert_eq!(args["q"], "rust");
908 }
909 _ => panic!("expected FunctionCall"),
910 }
911 }
912
913 #[test]
914 fn test_deepseek_with_text_before() {
915 let text = "I'll search for that.\n```json\n{\"name\": \"search\", \"arguments\": {\"q\": \"rust\"}}\n```\n<|tool▁call▁end|>";
916 let parts = parse_text_tool_calls(text).unwrap();
917 assert!(!parts.is_empty());
918 let has_fn_call =
919 parts.iter().any(|p| matches!(p, Part::FunctionCall { name, .. } if name == "search"));
920 assert!(has_fn_call);
921 }
922
923 fn declared(names: &[&str]) -> HashSet<String> {
924 names.iter().map(|name| (*name).to_string()).collect()
925 }
926
927 #[test]
928 fn json_example_without_declared_tools_stays_text() {
929 let text = "Here is the record:\n```json\n{\"name\":\"Alice\",\"age\":30}\n```\nDone.";
930
931 assert_eq!(parse_declared_tool_calls(text, &declared(&[])), None);
932 }
933
934 #[test]
935 fn json_example_with_other_declared_tool_stays_text() {
936 let text = "Here is the record:\n```json\n{\"name\":\"Alice\",\"age\":30}\n```\nDone.";
937
938 assert_eq!(parse_declared_tool_calls(text, &declared(&["lookup"])), None);
939 assert_eq!(parse_text_tool_calls(text), None);
941 }
942
943 #[test]
944 fn fenced_call_to_undeclared_tool_stays_text() {
945 let text = "```json\n{\"name\":\"delete_all\",\"arguments\":{}}\n```";
946
947 assert_eq!(parse_declared_tool_calls(text, &declared(&["lookup"])), None);
948 }
949
950 #[test]
951 fn fenced_call_to_declared_tool_parses_and_keeps_trailing_text() {
952 let text = "Checking.\n```json\n{\"name\":\"lookup\",\"arguments\":{\"id\":7}}\n```\nI will report back.";
953
954 assert_eq!(
955 parse_declared_tool_calls(text, &declared(&["lookup"])),
956 Some(vec![
957 Part::Text { text: "Checking.\n".to_string() },
958 Part::FunctionCall {
959 name: "lookup".to_string(),
960 args: serde_json::json!({"id": 7}),
961 id: None,
962 thought_signature: None,
963 },
964 Part::Text { text: "\nI will report back.".to_string() },
965 ])
966 );
967 }
968
969 #[test]
970 fn deepseek_end_marker_is_not_kept_as_text() {
971 let text = "```json\n{\"name\":\"lookup\",\"arguments\":{}}\n```\n<|tool▁call▁end|>";
972
973 assert_eq!(
974 parse_text_tool_calls(text),
975 Some(vec![Part::FunctionCall {
976 name: "lookup".to_string(),
977 args: serde_json::json!({}),
978 id: None,
979 thought_signature: None,
980 }])
981 );
982 }
983
984 #[test]
985 fn declared_buffer_keeps_undeclared_calls_as_text() {
986 let text = "<tool_call>{\"name\":\"rm\",\"arguments\":{}}</tool_call>";
987 let mut buffer = ToolCallBuffer::for_declared_tools(declared(&["lookup"]));
988
989 let BufferAction::Emit(parts) = buffer.push(text) else {
990 panic!("a complete tag is resolved immediately");
991 };
992 assert_eq!(parts, vec![Part::Text { text: text.to_string() }]);
993 }
994
995 #[test]
998 fn test_gemma4_simple() {
999 let text = "<|tool_call>call:get_weather{}<tool_call|>";
1000 let parts = parse_text_tool_calls(text).unwrap();
1001 assert_eq!(parts.len(), 1);
1002 match &parts[0] {
1003 Part::FunctionCall { name, .. } => assert_eq!(name, "get_weather"),
1004 _ => panic!("expected FunctionCall"),
1005 }
1006 }
1007
1008 #[test]
1009 fn test_gemma4_with_args() {
1010 let text = "<|tool_call>call:get_weather{<|\"|>city<|\"|>:<|\"|>Tokyo<|\"|>}<tool_call|>";
1011 let parts = parse_text_tool_calls(text).unwrap();
1012 assert_eq!(parts.len(), 1);
1013 match &parts[0] {
1014 Part::FunctionCall { name, args, .. } => {
1015 assert_eq!(name, "get_weather");
1016 assert_eq!(args["city"], "Tokyo");
1017 }
1018 _ => panic!("expected FunctionCall"),
1019 }
1020 }
1021
1022 #[test]
1025 fn test_action_tags() {
1026 let text = "<|action_start|>{\"name\": \"search\", \"arguments\": {\"q\": \"rust\"}}<|action_end|>";
1027 let parts = parse_text_tool_calls(text).unwrap();
1028 assert_eq!(parts.len(), 1);
1029 match &parts[0] {
1030 Part::FunctionCall { name, args, .. } => {
1031 assert_eq!(name, "search");
1032 assert_eq!(args["q"], "rust");
1033 }
1034 _ => panic!("expected FunctionCall"),
1035 }
1036 }
1037
1038 #[test]
1039 fn test_action_tags_with_surrounding_text() {
1040 let text = "Let me look that up. <|action_start|>{\"name\": \"search\", \"arguments\": {}}<|action_end|> Done.";
1041 let parts = parse_text_tool_calls(text).unwrap();
1042 assert!(parts.len() >= 2); let has_fn_call =
1044 parts.iter().any(|p| matches!(p, Part::FunctionCall { name, .. } if name == "search"));
1045 assert!(has_fn_call);
1046 }
1047
1048 fn stream_and_reconstruct(deltas: &[&str]) -> String {
1053 let mut buf = ToolCallBuffer::new();
1054 let mut out = String::new();
1055 let mut collect = |parts: Vec<Part>| {
1056 for part in parts {
1057 if let Part::Text { text } = part {
1058 out.push_str(&text);
1059 }
1060 }
1061 };
1062 for delta in deltas {
1063 if let BufferAction::Emit(parts) = buf.push(delta) {
1064 collect(parts);
1065 }
1066 }
1067 collect(buf.flush());
1068 out
1069 }
1070
1071 #[test]
1072 fn test_regression_markdown_structure_reconstructs() {
1073 let reconstructed =
1076 stream_and_reconstruct(&["Heading", "\n\n", "- first", "\n", "- second"]);
1077 assert_eq!(reconstructed, "Heading\n\n- first\n- second");
1078 }
1079
1080 #[test]
1081 fn test_regression_single_space_reconstructs() {
1082 let reconstructed = stream_and_reconstruct(&["Hello", " ", "world"]);
1085 assert_eq!(reconstructed, "Hello world");
1086 }
1087
1088 #[test]
1089 fn test_flush_as_emit_whitespace_only_emits_text() {
1090 let mut buf = ToolCallBuffer::new();
1093 match buf.push("\n\n") {
1094 BufferAction::Emit(parts) => {
1095 assert_eq!(parts.len(), 1);
1096 assert!(matches!(&parts[0], Part::Text { text } if text == "\n\n"));
1097 }
1098 BufferAction::Buffering => panic!("whitespace-only delta should emit"),
1099 }
1100 }
1101
1102 #[test]
1103 fn test_flush_whitespace_only_emits_text() {
1104 let mut buf = ToolCallBuffer::new();
1107 buf.buffer.push_str(" \n\t");
1110 buf.buffering = true;
1111 let parts = buf.flush();
1112 assert_eq!(parts.len(), 1);
1113 assert!(matches!(&parts[0], Part::Text { text } if text == " \n\t"));
1114 }
1115
1116 #[test]
1119 fn test_boundary_whitespace_qwen_preserved() {
1120 let text = "lead \n<tool_call> {\"name\": \"search\", \"arguments\": {\"q\": \"rust\"}} </tool_call>\n trail";
1123 let parts = parse_text_tool_calls(text).unwrap();
1124 assert_eq!(parts.len(), 3);
1125 assert!(matches!(&parts[0], Part::Text { text } if text == "lead \n"));
1126 match &parts[1] {
1127 Part::FunctionCall { name, args, .. } => {
1128 assert_eq!(name, "search");
1129 assert_eq!(args["q"], "rust");
1130 }
1131 _ => panic!("expected FunctionCall"),
1132 }
1133 assert!(matches!(&parts[2], Part::Text { text } if text == "\n trail"));
1134 }
1135
1136 #[test]
1137 fn test_boundary_whitespace_llama_preserved() {
1138 let text = "pre \n<|python_tag|> {\"name\": \"get_weather\", \"parameters\": {\"city\": \"NYC\"}}";
1139 let parts = parse_text_tool_calls(text).unwrap();
1140 assert_eq!(parts.len(), 2);
1141 assert!(matches!(&parts[0], Part::Text { text } if text == "pre \n"));
1142 match &parts[1] {
1143 Part::FunctionCall { name, args, .. } => {
1144 assert_eq!(name, "get_weather");
1145 assert_eq!(args["city"], "NYC");
1146 }
1147 _ => panic!("expected FunctionCall"),
1148 }
1149 }
1150
1151 #[test]
1152 fn test_boundary_whitespace_mistral_nemo_preserved() {
1153 let text = "pre \n[TOOL_CALLS] [{\"name\": \"search\", \"arguments\": {\"q\": \"rust\"}}]";
1154 let parts = parse_text_tool_calls(text).unwrap();
1155 assert_eq!(parts.len(), 2);
1156 assert!(matches!(&parts[0], Part::Text { text } if text == "pre \n"));
1157 match &parts[1] {
1158 Part::FunctionCall { name, args, .. } => {
1159 assert_eq!(name, "search");
1160 assert_eq!(args["q"], "rust");
1161 }
1162 _ => panic!("expected FunctionCall"),
1163 }
1164 }
1165
1166 #[test]
1167 fn test_boundary_whitespace_deepseek_preserved() {
1168 let text = "pre \n```json\n{\"name\": \"search\", \"arguments\": {\"q\": \"rust\"}}\n```";
1169 let parts = parse_text_tool_calls(text).unwrap();
1170 assert_eq!(parts.len(), 2);
1171 assert!(matches!(&parts[0], Part::Text { text } if text == "pre \n"));
1172 match &parts[1] {
1173 Part::FunctionCall { name, args, .. } => {
1174 assert_eq!(name, "search");
1175 assert_eq!(args["q"], "rust");
1176 }
1177 _ => panic!("expected FunctionCall"),
1178 }
1179 }
1180
1181 #[test]
1182 fn test_boundary_whitespace_gemma4_preserved() {
1183 let text = "pre \n<|tool_call>call:get_weather{<|\"|>city<|\"|>:<|\"|>Tokyo<|\"|>}<tool_call|>\n post";
1184 let parts = parse_text_tool_calls(text).unwrap();
1185 assert_eq!(parts.len(), 3);
1186 assert!(matches!(&parts[0], Part::Text { text } if text == "pre \n"));
1187 match &parts[1] {
1188 Part::FunctionCall { name, args, .. } => {
1189 assert_eq!(name, "get_weather");
1190 assert_eq!(args["city"], "Tokyo");
1191 }
1192 _ => panic!("expected FunctionCall"),
1193 }
1194 assert!(matches!(&parts[2], Part::Text { text } if text == "\n post"));
1195 }
1196
1197 #[test]
1198 fn test_boundary_whitespace_action_tag_preserved() {
1199 let text = "pre \n<|action_start|> {\"name\": \"search\", \"arguments\": {\"q\": \"rust\"}} <|action_end|>\n post";
1200 let parts = parse_text_tool_calls(text).unwrap();
1201 assert_eq!(parts.len(), 3);
1202 assert!(matches!(&parts[0], Part::Text { text } if text == "pre \n"));
1203 match &parts[1] {
1204 Part::FunctionCall { name, args, .. } => {
1205 assert_eq!(name, "search");
1206 assert_eq!(args["q"], "rust");
1207 }
1208 _ => panic!("expected FunctionCall"),
1209 }
1210 assert!(matches!(&parts[2], Part::Text { text } if text == "\n post"));
1211 }
1212}