1use adk_core::Part;
22
23pub fn contains_tool_call_tag(text: &str) -> bool {
25 text.contains("<tool_call>")
26 || text.contains("<|tool_call>")
27 || text.contains("<|python_tag|>")
28 || text.contains("[TOOL_CALLS]")
29 || text.contains("<|tool▁call")
30 || text.contains("<|action_start|>")
31 || (text.contains("```json") && text.contains("\"name\""))
32}
33
34pub fn parse_text_tool_calls(text: &str) -> Option<Vec<Part>> {
42 if !contains_tool_call_tag(text) {
43 return None;
44 }
45
46 let mut parts = Vec::new();
47
48 if let Some(parsed) = parse_qwen_format(text, &mut parts) {
50 return Some(parsed);
51 }
52
53 if let Some(parsed) = parse_llama_format(text, &mut parts) {
55 return Some(parsed);
56 }
57
58 if let Some(parsed) = parse_mistral_nemo_format(text, &mut parts) {
60 return Some(parsed);
61 }
62
63 if let Some(parsed) = parse_deepseek_format(text) {
65 return Some(parsed);
66 }
67
68 if let Some(parsed) = parse_gemma4_format(text) {
70 return Some(parsed);
71 }
72
73 if let Some(parsed) = parse_action_tag_format(text) {
75 return Some(parsed);
76 }
77
78 None
79}
80
81fn parse_qwen_format(text: &str, _parts: &mut Vec<Part>) -> Option<Vec<Part>> {
87 let mut result = Vec::new();
88 let mut remaining = text;
89
90 loop {
91 let start = remaining.find("<tool_call>")?;
92
93 let before = &remaining[..start];
95 if !before.is_empty() {
96 result.push(Part::Text { text: before.to_string() });
97 }
98
99 let after_open = &remaining[start + "<tool_call>".len()..];
100 let end = after_open.find("</tool_call>")?;
101 let inner = after_open[..end].trim();
102
103 if let Some(part) = parse_json_tool_call(inner) {
105 result.push(part);
106 }
107 else if let Some(part) = parse_function_tag(inner) {
109 result.push(part);
110 } else {
111 result.push(Part::Text {
113 text: remaining[start..start + "<tool_call>".len() + end + "</tool_call>".len()]
114 .to_string(),
115 });
116 }
117
118 remaining = &after_open[end + "</tool_call>".len()..];
119 if remaining.trim().is_empty() || !remaining.contains("<tool_call>") {
120 let trailing = remaining;
121 if !trailing.is_empty() {
122 result.push(Part::Text { text: trailing.to_string() });
123 }
124 break;
125 }
126 }
127
128 if result.is_empty() { None } else { Some(result) }
129}
130
131fn parse_function_tag(inner: &str) -> Option<Part> {
133 let func_start = inner.find("<function=")?;
134 let after_eq = &inner[func_start + "<function=".len()..];
135 let name_end = after_eq.find('>')?;
136 let name = after_eq[..name_end].trim().to_string();
137
138 let body_start = name_end + 1;
139 let func_end = after_eq.find("</function>")?;
140 let body = after_eq[body_start..func_end].trim();
141
142 let args = if body.is_empty() {
143 serde_json::json!({})
144 } else {
145 serde_json::from_str(body).unwrap_or_else(|_| serde_json::json!({}))
146 };
147
148 Some(Part::FunctionCall { name, args, id: None, thought_signature: None })
149}
150
151fn parse_json_tool_call(json_str: &str) -> Option<Part> {
154 let value: serde_json::Value = serde_json::from_str(json_str).ok()?;
155 let obj = value.as_object()?;
156
157 let name =
158 obj.get("name").or_else(|| obj.get("function")).and_then(|v| v.as_str())?.to_string();
159
160 let args = obj
161 .get("arguments")
162 .or_else(|| obj.get("parameters"))
163 .cloned()
164 .unwrap_or(serde_json::json!({}));
165
166 Some(Part::FunctionCall { name, args, id: None, thought_signature: None })
167}
168
169fn parse_llama_format(text: &str, _parts: &mut Vec<Part>) -> Option<Vec<Part>> {
171 let tag = "<|python_tag|>";
172 let start = text.find(tag)?;
173
174 let mut result = Vec::new();
175 let before = &text[..start];
176 if !before.is_empty() {
177 result.push(Part::Text { text: before.to_string() });
178 }
179
180 let json_str = text[start + tag.len()..].trim();
181 if let Some(part) = parse_json_tool_call(json_str) {
182 result.push(part);
183 } else {
184 return None;
185 }
186
187 Some(result)
188}
189
190fn parse_mistral_nemo_format(text: &str, _parts: &mut Vec<Part>) -> Option<Vec<Part>> {
192 let tag = "[TOOL_CALLS]";
193 let start = text.find(tag)?;
194
195 let mut result = Vec::new();
196 let before = &text[..start];
197 if !before.is_empty() {
198 result.push(Part::Text { text: before.to_string() });
199 }
200
201 let json_str = text[start + tag.len()..].trim();
202 let arr: Vec<serde_json::Value> = serde_json::from_str(json_str).ok()?;
204 for item in &arr {
205 let obj = item.as_object()?;
206 let name = obj.get("name").and_then(|v| v.as_str())?.to_string();
207 let args = obj
208 .get("arguments")
209 .or_else(|| obj.get("parameters"))
210 .cloned()
211 .unwrap_or(serde_json::json!({}));
212 result.push(Part::FunctionCall { name, args, id: None, thought_signature: None });
213 }
214
215 if result.is_empty() { None } else { Some(result) }
216}
217
218fn parse_deepseek_format(text: &str) -> Option<Vec<Part>> {
223 let fence_start = text.find("```json")?;
224 let json_start = fence_start + "```json".len();
225 let after_fence = &text[json_start..];
226 let fence_end = after_fence.find("```")?;
227 let json_str = after_fence[..fence_end].trim();
228
229 let mut result = Vec::new();
230 let before = &text[..fence_start];
231 if !before.is_empty() {
232 result.push(Part::Text { text: before.to_string() });
233 }
234
235 if let Some(part) = parse_json_tool_call(json_str) {
237 result.push(part);
238 } else if let Ok(arr) = serde_json::from_str::<Vec<serde_json::Value>>(json_str) {
239 for item in &arr {
240 if let Some(obj) = item.as_object() {
241 let name = obj
242 .get("name")
243 .or_else(|| obj.get("function"))
244 .and_then(|v| v.as_str())?
245 .to_string();
246 let args = obj
247 .get("arguments")
248 .or_else(|| obj.get("parameters"))
249 .cloned()
250 .unwrap_or(serde_json::json!({}));
251 result.push(Part::FunctionCall { name, args, id: None, thought_signature: None });
252 }
253 }
254 } else {
255 return None;
256 }
257
258 if result.is_empty() { None } else { Some(result) }
259}
260
261fn parse_gemma4_format(text: &str) -> Option<Vec<Part>> {
266 let mut result = Vec::new();
267 let mut remaining = text;
268
269 loop {
270 let start = remaining.find("<|tool_call>")?;
271 let before = &remaining[..start];
272 if !before.is_empty() {
273 result.push(Part::Text { text: before.to_string() });
274 }
275
276 let after_open = &remaining[start + "<|tool_call>".len()..];
277 let end = after_open.find("<tool_call|>")?;
278 let inner = after_open[..end].trim();
279
280 if let Some(call_body) = inner.strip_prefix("call:") {
282 let brace_start = call_body.find('{');
283 let name = if let Some(bs) = brace_start {
284 call_body[..bs].trim().to_string()
285 } else {
286 call_body.trim().to_string()
287 };
288
289 let args = if let Some(bs) = brace_start {
290 let args_raw = &call_body[bs..];
291 let json_str = args_raw.replace("<|\"|>", "\"");
293 serde_json::from_str(&json_str).unwrap_or(serde_json::json!({}))
294 } else {
295 serde_json::json!({})
296 };
297
298 result.push(Part::FunctionCall { name, args, id: None, thought_signature: None });
299 }
300
301 remaining = &after_open[end + "<tool_call|>".len()..];
302 if remaining.trim().is_empty() || !remaining.contains("<|tool_call>") {
303 let trailing = remaining;
304 if !trailing.is_empty() {
305 result.push(Part::Text { text: trailing.to_string() });
306 }
307 break;
308 }
309 }
310
311 if result.is_empty() { None } else { Some(result) }
312}
313
314fn parse_action_tag_format(text: &str) -> Option<Vec<Part>> {
319 let start_tag = "<|action_start|>";
320 let end_tag = "<|action_end|>";
321
322 let start = text.find(start_tag)?;
323 let mut result = Vec::new();
324
325 let before = &text[..start];
326 if !before.is_empty() {
327 result.push(Part::Text { text: before.to_string() });
328 }
329
330 let after_open = &text[start + start_tag.len()..];
331 let end = after_open.find(end_tag)?;
332 let inner = after_open[..end].trim();
333
334 if let Some(part) = parse_json_tool_call(inner) {
335 result.push(part);
336 } else {
337 return None;
338 }
339
340 let trailing = &after_open[end + end_tag.len()..];
341 if !trailing.is_empty() {
342 result.push(Part::Text { text: trailing.to_string() });
343 }
344
345 Some(result)
346}
347
348const TOOL_CALL_PREFIXES: &[&str] = &[
352 "<tool_call",
353 "<|tool_call>",
354 "<|python_tag|",
355 "[TOOL_CALLS]",
356 "<|action_start|>",
357 "<\u{ff5c}\u{2581}tool", ];
359
360const MAX_BUFFER_SIZE: usize = 4096;
362
363pub struct ToolCallBuffer {
386 buffer: String,
387 buffering: bool,
388}
389
390pub enum BufferAction {
392 Emit(Vec<Part>),
394 Buffering,
396}
397
398impl ToolCallBuffer {
399 pub fn new() -> Self {
401 Self { buffer: String::new(), buffering: false }
402 }
403
404 pub fn push(&mut self, text: &str) -> BufferAction {
409 self.buffer.push_str(text);
410
411 if self.buffering {
412 if self.has_complete_tool_call() {
414 return self.try_parse_and_emit();
415 }
416 if self.buffer.len() > MAX_BUFFER_SIZE {
418 return self.flush_as_emit();
419 }
420 BufferAction::Buffering
421 } else {
422 if self.starts_tool_call_prefix() {
424 self.buffering = true;
425 if self.has_complete_tool_call() {
427 return self.try_parse_and_emit();
428 }
429 BufferAction::Buffering
430 } else if self.has_partial_prefix() {
431 self.buffering = true;
433 BufferAction::Buffering
434 } else {
435 self.flush_as_emit()
437 }
438 }
439 }
440
441 pub fn flush(&mut self) -> Vec<Part> {
444 if self.buffer.is_empty() {
445 return Vec::new();
446 }
447
448 if let Some(parts) = parse_text_tool_calls(&self.buffer) {
450 self.buffer.clear();
451 self.buffering = false;
452 return parts;
453 }
454
455 let text = std::mem::take(&mut self.buffer);
457 self.buffering = false;
458 if text.is_empty() { Vec::new() } else { vec![Part::Text { text }] }
459 }
460
461 fn starts_tool_call_prefix(&self) -> bool {
462 TOOL_CALL_PREFIXES.iter().any(|prefix| self.buffer.contains(prefix))
463 }
464
465 fn has_partial_prefix(&self) -> bool {
466 let buf = &self.buffer;
468 for prefix in TOOL_CALL_PREFIXES {
469 let prefix_chars: Vec<char> = prefix.chars().collect();
471 for i in 1..prefix_chars.len() {
472 let partial: String = prefix_chars[..i].iter().collect();
473 if buf.ends_with(&partial) {
474 return true;
475 }
476 }
477 }
478 false
479 }
480
481 fn has_complete_tool_call(&self) -> bool {
482 (self.buffer.contains("<tool_call>") && self.buffer.contains("</tool_call>"))
483 || (self.buffer.contains("<|tool_call>") && self.buffer.contains("<tool_call|>"))
484 || (self.buffer.contains("<|python_tag|>")
485 && self.buffer.contains('\n')
486 && self.buffer.len() > "<|python_tag|>".len() + 5)
487 || (self.buffer.contains("[TOOL_CALLS]")
488 && self.buffer.contains(']')
489 && self.buffer.rfind(']') > self.buffer.find("[TOOL_CALLS]").map(|i| i + 12))
490 || (self.buffer.contains("```json") && self.buffer.matches("```").count() >= 2)
491 || (self.buffer.contains("<|action_start|>") && self.buffer.contains("<|action_end|>"))
492 }
493
494 fn try_parse_and_emit(&mut self) -> BufferAction {
495 if let Some(parts) = parse_text_tool_calls(&self.buffer) {
496 self.buffer.clear();
497 self.buffering = false;
498 BufferAction::Emit(parts)
499 } else {
500 self.flush_as_emit()
502 }
503 }
504
505 fn flush_as_emit(&mut self) -> BufferAction {
506 let text = std::mem::take(&mut self.buffer);
507 self.buffering = false;
508 if text.is_empty() {
509 BufferAction::Emit(Vec::new())
510 } else {
511 BufferAction::Emit(vec![Part::Text { text }])
512 }
513 }
514}
515
516impl Default for ToolCallBuffer {
517 fn default() -> Self {
518 Self::new()
519 }
520}
521
522#[cfg(test)]
523mod tests {
524 use super::*;
525
526 #[test]
527 fn test_qwen_json_format() {
528 let text =
529 r#"<tool_call>{"name": "get_weather", "arguments": {"city": "Tokyo"}}</tool_call>"#;
530 let parts = parse_text_tool_calls(text).unwrap();
531 assert_eq!(parts.len(), 1);
532 match &parts[0] {
533 Part::FunctionCall { name, args, .. } => {
534 assert_eq!(name, "get_weather");
535 assert_eq!(args["city"], "Tokyo");
536 }
537 _ => panic!("expected FunctionCall"),
538 }
539 }
540
541 #[test]
542 fn test_qwen_function_tag_format() {
543 let text = r#"<tool_call><function=screenshot></function></tool_call>"#;
544 let parts = parse_text_tool_calls(text).unwrap();
545 assert_eq!(parts.len(), 1);
546 match &parts[0] {
547 Part::FunctionCall { name, args, .. } => {
548 assert_eq!(name, "screenshot");
549 assert_eq!(*args, serde_json::json!({}));
550 }
551 _ => panic!("expected FunctionCall"),
552 }
553 }
554
555 #[test]
556 fn test_qwen_function_tag_with_args() {
557 let text = r#"<tool_call><function=get_weather>{"city": "Tokyo"}</function></tool_call>"#;
558 let parts = parse_text_tool_calls(text).unwrap();
559 assert_eq!(parts.len(), 1);
560 match &parts[0] {
561 Part::FunctionCall { name, args, .. } => {
562 assert_eq!(name, "get_weather");
563 assert_eq!(args["city"], "Tokyo");
564 }
565 _ => panic!("expected FunctionCall"),
566 }
567 }
568
569 #[test]
570 fn test_text_before_tool_call() {
571 let text = r#"Let me check that for you.
572<tool_call>{"name": "search", "arguments": {"query": "rust"}}</tool_call>"#;
573 let parts = parse_text_tool_calls(text).unwrap();
574 assert_eq!(parts.len(), 2);
575 assert!(matches!(&parts[0], Part::Text { text } if text.contains("check that")));
576 assert!(matches!(&parts[1], Part::FunctionCall { name, .. } if name == "search"));
577 }
578
579 #[test]
580 fn test_multiple_tool_calls() {
581 let text = r#"<tool_call>{"name": "a", "arguments": {}}</tool_call>
586<tool_call>{"name": "b", "arguments": {"x": 1}}</tool_call>"#;
587 let parts = parse_text_tool_calls(text).unwrap();
588 assert_eq!(parts.len(), 3);
589 assert!(matches!(&parts[0], Part::FunctionCall { name, .. } if name == "a"));
590 assert!(matches!(&parts[1], Part::Text { text } if text == "\n"));
591 assert!(matches!(&parts[2], Part::FunctionCall { name, .. } if name == "b"));
592 }
593
594 #[test]
595 fn test_llama_format() {
596 let text = r#"<|python_tag|>{"name": "get_weather", "parameters": {"city": "NYC"}}"#;
597 let parts = parse_text_tool_calls(text).unwrap();
598 assert_eq!(parts.len(), 1);
599 match &parts[0] {
600 Part::FunctionCall { name, args, .. } => {
601 assert_eq!(name, "get_weather");
602 assert_eq!(args["city"], "NYC");
603 }
604 _ => panic!("expected FunctionCall"),
605 }
606 }
607
608 #[test]
609 fn test_mistral_nemo_format() {
610 let text = r#"[TOOL_CALLS][{"name": "search", "arguments": {"q": "rust"}}]"#;
611 let parts = parse_text_tool_calls(text).unwrap();
612 assert_eq!(parts.len(), 1);
613 match &parts[0] {
614 Part::FunctionCall { name, args, .. } => {
615 assert_eq!(name, "search");
616 assert_eq!(args["q"], "rust");
617 }
618 _ => panic!("expected FunctionCall"),
619 }
620 }
621
622 #[test]
623 fn test_no_tool_call_returns_none() {
624 assert!(parse_text_tool_calls("Hello, how can I help?").is_none());
625 assert!(parse_text_tool_calls("").is_none());
626 }
627
628 #[test]
629 fn test_contains_tool_call_tag() {
630 assert!(contains_tool_call_tag("<tool_call>"));
631 assert!(contains_tool_call_tag("text <tool_call> more"));
632 assert!(contains_tool_call_tag("<|python_tag|>"));
633 assert!(contains_tool_call_tag("[TOOL_CALLS]"));
634 assert!(!contains_tool_call_tag("normal text"));
635 }
636
637 #[test]
640 fn test_buffer_normal_text_emits_immediately() {
641 let mut buf = ToolCallBuffer::new();
642 match buf.push("Hello world") {
643 BufferAction::Emit(parts) => {
644 assert_eq!(parts.len(), 1);
645 assert!(matches!(&parts[0], Part::Text { text } if text == "Hello world"));
646 }
647 BufferAction::Buffering => panic!("should emit immediately"),
648 }
649 }
650
651 #[test]
652 fn test_buffer_complete_tool_call_in_one_chunk() {
653 let mut buf = ToolCallBuffer::new();
654 let text = r#"<tool_call>{"name": "search", "arguments": {"q": "rust"}}</tool_call>"#;
655 match buf.push(text) {
656 BufferAction::Emit(parts) => {
657 assert_eq!(parts.len(), 1);
658 assert!(matches!(&parts[0], Part::FunctionCall { name, .. } if name == "search"));
659 }
660 BufferAction::Buffering => panic!("should emit parsed tool call"),
661 }
662 }
663
664 #[test]
665 fn test_buffer_tool_call_split_across_chunks() {
666 let mut buf = ToolCallBuffer::new();
667
668 assert!(matches!(buf.push("<tool_call>"), BufferAction::Buffering));
670
671 assert!(matches!(
673 buf.push(r#"{"name": "get_weather", "arguments": {"city": "Tokyo"}}"#),
674 BufferAction::Buffering
675 ));
676
677 match buf.push("</tool_call>") {
679 BufferAction::Emit(parts) => {
680 assert_eq!(parts.len(), 1);
681 assert!(
682 matches!(&parts[0], Part::FunctionCall { name, .. } if name == "get_weather")
683 );
684 }
685 BufferAction::Buffering => panic!("should emit after closing tag"),
686 }
687 }
688
689 #[test]
690 fn test_buffer_text_then_tool_call() {
691 let mut buf = ToolCallBuffer::new();
692
693 match buf.push("Let me check. ") {
695 BufferAction::Emit(parts) => {
696 assert_eq!(parts.len(), 1);
697 assert!(matches!(&parts[0], Part::Text { .. }));
698 }
699 BufferAction::Buffering => panic!("should emit text"),
700 }
701
702 let tc = r#"<tool_call>{"name": "search", "arguments": {}}</tool_call>"#;
704 match buf.push(tc) {
705 BufferAction::Emit(parts) => {
706 assert_eq!(parts.len(), 1);
707 assert!(matches!(&parts[0], Part::FunctionCall { name, .. } if name == "search"));
708 }
709 BufferAction::Buffering => panic!("should emit tool call"),
710 }
711 }
712
713 #[test]
714 fn test_buffer_flush_incomplete_as_text() {
715 let mut buf = ToolCallBuffer::new();
716 assert!(matches!(buf.push("<tool_call>partial"), BufferAction::Buffering));
717
718 let parts = buf.flush();
720 assert_eq!(parts.len(), 1);
721 assert!(matches!(&parts[0], Part::Text { text } if text.contains("<tool_call>")));
722 }
723
724 #[test]
725 fn test_buffer_flush_empty() {
726 let mut buf = ToolCallBuffer::new();
727 let parts = buf.flush();
728 assert!(parts.is_empty());
729 }
730
731 #[test]
732 fn test_buffer_partial_prefix_detection() {
733 let mut buf = ToolCallBuffer::new();
734 assert!(matches!(buf.push("<tool"), BufferAction::Buffering));
736 assert!(matches!(buf.push("_call>"), BufferAction::Buffering));
738 match buf.push(r#"{"name":"x","arguments":{}}</tool_call>"#) {
740 BufferAction::Emit(parts) => {
741 assert_eq!(parts.len(), 1);
742 assert!(matches!(&parts[0], Part::FunctionCall { name, .. } if name == "x"));
743 }
744 BufferAction::Buffering => panic!("should emit"),
745 }
746 }
747
748 #[test]
751 fn test_deepseek_json_fence() {
752 let text = "```json\n{\"name\": \"search\", \"arguments\": {\"q\": \"rust\"}}\n```";
753 let parts = parse_text_tool_calls(text).unwrap();
754 assert_eq!(parts.len(), 1);
755 match &parts[0] {
756 Part::FunctionCall { name, args, .. } => {
757 assert_eq!(name, "search");
758 assert_eq!(args["q"], "rust");
759 }
760 _ => panic!("expected FunctionCall"),
761 }
762 }
763
764 #[test]
765 fn test_deepseek_with_text_before() {
766 let text = "I'll search for that.\n```json\n{\"name\": \"search\", \"arguments\": {\"q\": \"rust\"}}\n```\n<|tool▁call▁end|>";
767 let parts = parse_text_tool_calls(text).unwrap();
768 assert!(!parts.is_empty());
769 let has_fn_call =
770 parts.iter().any(|p| matches!(p, Part::FunctionCall { name, .. } if name == "search"));
771 assert!(has_fn_call);
772 }
773
774 #[test]
777 fn test_gemma4_simple() {
778 let text = "<|tool_call>call:get_weather{}<tool_call|>";
779 let parts = parse_text_tool_calls(text).unwrap();
780 assert_eq!(parts.len(), 1);
781 match &parts[0] {
782 Part::FunctionCall { name, .. } => assert_eq!(name, "get_weather"),
783 _ => panic!("expected FunctionCall"),
784 }
785 }
786
787 #[test]
788 fn test_gemma4_with_args() {
789 let text = "<|tool_call>call:get_weather{<|\"|>city<|\"|>:<|\"|>Tokyo<|\"|>}<tool_call|>";
790 let parts = parse_text_tool_calls(text).unwrap();
791 assert_eq!(parts.len(), 1);
792 match &parts[0] {
793 Part::FunctionCall { name, args, .. } => {
794 assert_eq!(name, "get_weather");
795 assert_eq!(args["city"], "Tokyo");
796 }
797 _ => panic!("expected FunctionCall"),
798 }
799 }
800
801 #[test]
804 fn test_action_tags() {
805 let text = "<|action_start|>{\"name\": \"search\", \"arguments\": {\"q\": \"rust\"}}<|action_end|>";
806 let parts = parse_text_tool_calls(text).unwrap();
807 assert_eq!(parts.len(), 1);
808 match &parts[0] {
809 Part::FunctionCall { name, args, .. } => {
810 assert_eq!(name, "search");
811 assert_eq!(args["q"], "rust");
812 }
813 _ => panic!("expected FunctionCall"),
814 }
815 }
816
817 #[test]
818 fn test_action_tags_with_surrounding_text() {
819 let text = "Let me look that up. <|action_start|>{\"name\": \"search\", \"arguments\": {}}<|action_end|> Done.";
820 let parts = parse_text_tool_calls(text).unwrap();
821 assert!(parts.len() >= 2); let has_fn_call =
823 parts.iter().any(|p| matches!(p, Part::FunctionCall { name, .. } if name == "search"));
824 assert!(has_fn_call);
825 }
826
827 fn stream_and_reconstruct(deltas: &[&str]) -> String {
832 let mut buf = ToolCallBuffer::new();
833 let mut out = String::new();
834 let mut collect = |parts: Vec<Part>| {
835 for part in parts {
836 if let Part::Text { text } = part {
837 out.push_str(&text);
838 }
839 }
840 };
841 for delta in deltas {
842 if let BufferAction::Emit(parts) = buf.push(delta) {
843 collect(parts);
844 }
845 }
846 collect(buf.flush());
847 out
848 }
849
850 #[test]
851 fn test_regression_markdown_structure_reconstructs() {
852 let reconstructed =
855 stream_and_reconstruct(&["Heading", "\n\n", "- first", "\n", "- second"]);
856 assert_eq!(reconstructed, "Heading\n\n- first\n- second");
857 }
858
859 #[test]
860 fn test_regression_single_space_reconstructs() {
861 let reconstructed = stream_and_reconstruct(&["Hello", " ", "world"]);
864 assert_eq!(reconstructed, "Hello world");
865 }
866
867 #[test]
868 fn test_flush_as_emit_whitespace_only_emits_text() {
869 let mut buf = ToolCallBuffer::new();
872 match buf.push("\n\n") {
873 BufferAction::Emit(parts) => {
874 assert_eq!(parts.len(), 1);
875 assert!(matches!(&parts[0], Part::Text { text } if text == "\n\n"));
876 }
877 BufferAction::Buffering => panic!("whitespace-only delta should emit"),
878 }
879 }
880
881 #[test]
882 fn test_flush_whitespace_only_emits_text() {
883 let mut buf = ToolCallBuffer::new();
886 buf.buffer.push_str(" \n\t");
889 buf.buffering = true;
890 let parts = buf.flush();
891 assert_eq!(parts.len(), 1);
892 assert!(matches!(&parts[0], Part::Text { text } if text == " \n\t"));
893 }
894
895 #[test]
898 fn test_boundary_whitespace_qwen_preserved() {
899 let text = "lead \n<tool_call> {\"name\": \"search\", \"arguments\": {\"q\": \"rust\"}} </tool_call>\n trail";
902 let parts = parse_text_tool_calls(text).unwrap();
903 assert_eq!(parts.len(), 3);
904 assert!(matches!(&parts[0], Part::Text { text } if text == "lead \n"));
905 match &parts[1] {
906 Part::FunctionCall { name, args, .. } => {
907 assert_eq!(name, "search");
908 assert_eq!(args["q"], "rust");
909 }
910 _ => panic!("expected FunctionCall"),
911 }
912 assert!(matches!(&parts[2], Part::Text { text } if text == "\n trail"));
913 }
914
915 #[test]
916 fn test_boundary_whitespace_llama_preserved() {
917 let text = "pre \n<|python_tag|> {\"name\": \"get_weather\", \"parameters\": {\"city\": \"NYC\"}}";
918 let parts = parse_text_tool_calls(text).unwrap();
919 assert_eq!(parts.len(), 2);
920 assert!(matches!(&parts[0], Part::Text { text } if text == "pre \n"));
921 match &parts[1] {
922 Part::FunctionCall { name, args, .. } => {
923 assert_eq!(name, "get_weather");
924 assert_eq!(args["city"], "NYC");
925 }
926 _ => panic!("expected FunctionCall"),
927 }
928 }
929
930 #[test]
931 fn test_boundary_whitespace_mistral_nemo_preserved() {
932 let text = "pre \n[TOOL_CALLS] [{\"name\": \"search\", \"arguments\": {\"q\": \"rust\"}}]";
933 let parts = parse_text_tool_calls(text).unwrap();
934 assert_eq!(parts.len(), 2);
935 assert!(matches!(&parts[0], Part::Text { text } if text == "pre \n"));
936 match &parts[1] {
937 Part::FunctionCall { name, args, .. } => {
938 assert_eq!(name, "search");
939 assert_eq!(args["q"], "rust");
940 }
941 _ => panic!("expected FunctionCall"),
942 }
943 }
944
945 #[test]
946 fn test_boundary_whitespace_deepseek_preserved() {
947 let text = "pre \n```json\n{\"name\": \"search\", \"arguments\": {\"q\": \"rust\"}}\n```";
948 let parts = parse_text_tool_calls(text).unwrap();
949 assert_eq!(parts.len(), 2);
950 assert!(matches!(&parts[0], Part::Text { text } if text == "pre \n"));
951 match &parts[1] {
952 Part::FunctionCall { name, args, .. } => {
953 assert_eq!(name, "search");
954 assert_eq!(args["q"], "rust");
955 }
956 _ => panic!("expected FunctionCall"),
957 }
958 }
959
960 #[test]
961 fn test_boundary_whitespace_gemma4_preserved() {
962 let text = "pre \n<|tool_call>call:get_weather{<|\"|>city<|\"|>:<|\"|>Tokyo<|\"|>}<tool_call|>\n post";
963 let parts = parse_text_tool_calls(text).unwrap();
964 assert_eq!(parts.len(), 3);
965 assert!(matches!(&parts[0], Part::Text { text } if text == "pre \n"));
966 match &parts[1] {
967 Part::FunctionCall { name, args, .. } => {
968 assert_eq!(name, "get_weather");
969 assert_eq!(args["city"], "Tokyo");
970 }
971 _ => panic!("expected FunctionCall"),
972 }
973 assert!(matches!(&parts[2], Part::Text { text } if text == "\n post"));
974 }
975
976 #[test]
977 fn test_boundary_whitespace_action_tag_preserved() {
978 let text = "pre \n<|action_start|> {\"name\": \"search\", \"arguments\": {\"q\": \"rust\"}} <|action_end|>\n post";
979 let parts = parse_text_tool_calls(text).unwrap();
980 assert_eq!(parts.len(), 3);
981 assert!(matches!(&parts[0], Part::Text { text } if text == "pre \n"));
982 match &parts[1] {
983 Part::FunctionCall { name, args, .. } => {
984 assert_eq!(name, "search");
985 assert_eq!(args["q"], "rust");
986 }
987 _ => panic!("expected FunctionCall"),
988 }
989 assert!(matches!(&parts[2], Part::Text { text } if text == "\n post"));
990 }
991}