1use regex::Regex;
12use std::{collections::HashSet, sync::LazyLock};
13
14#[derive(Debug, Clone)]
16pub struct ParsedToolCall {
17 pub name: String,
18 pub arguments: serde_json::Value,
19 pub tool_call_id: Option<String>,
20}
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
25pub enum ToolProtocolEnvelopeKind {
26 ToolCalls,
27 ToolCallsAlias,
28 FunctionCall,
29 ToolResult,
30 ResponsesFunctionCall,
31 TaggedToolCall,
32}
33
34fn parse_arguments_value(raw: Option<&serde_json::Value>) -> serde_json::Value {
35 let initial = match raw {
36 Some(serde_json::Value::String(s)) => serde_json::from_str::<serde_json::Value>(s)
37 .unwrap_or_else(|_| serde_json::Value::Object(serde_json::Map::new())),
38 Some(value) => value.clone(),
39 None => serde_json::Value::Object(serde_json::Map::new()),
40 };
41 unwrap_nested_json_strings(initial)
42}
43
44fn unwrap_nested_json_strings(value: serde_json::Value) -> serde_json::Value {
49 match value {
50 serde_json::Value::Object(map) => {
51 let mut out = serde_json::Map::with_capacity(map.len());
52 for (k, v) in map {
53 out.insert(k, unwrap_nested_json_strings(v));
54 }
55 serde_json::Value::Object(out)
56 }
57 serde_json::Value::Array(items) => {
58 serde_json::Value::Array(items.into_iter().map(unwrap_nested_json_strings).collect())
59 }
60 serde_json::Value::String(s) => {
61 let trimmed = s.trim_start();
62 if trimmed.starts_with('{') || trimmed.starts_with('[') {
63 match serde_json::from_str::<serde_json::Value>(&s) {
64 Ok(parsed) => unwrap_nested_json_strings(parsed),
65 Err(_) => serde_json::Value::String(s),
66 }
67 } else {
68 serde_json::Value::String(s)
69 }
70 }
71 other => other,
72 }
73}
74
75fn parse_tool_call_id(
76 root: &serde_json::Value,
77 function: Option<&serde_json::Value>,
78) -> Option<String> {
79 function
80 .and_then(|func| func.get("id"))
81 .or_else(|| root.get("id"))
82 .or_else(|| root.get("tool_call_id"))
83 .or_else(|| root.get("call_id"))
84 .and_then(serde_json::Value::as_str)
85 .map(str::trim)
86 .filter(|id| !id.is_empty())
87 .map(ToString::to_string)
88}
89
90pub fn canonicalize_json_for_tool_signature(value: &serde_json::Value) -> serde_json::Value {
91 match value {
92 serde_json::Value::Object(map) => {
93 let mut keys: Vec<String> = map.keys().cloned().collect();
94 keys.sort_unstable();
95 let mut ordered = serde_json::Map::new();
96 for key in keys {
97 if let Some(child) = map.get(&key) {
98 ordered.insert(key, canonicalize_json_for_tool_signature(child));
99 }
100 }
101 serde_json::Value::Object(ordered)
102 }
103 serde_json::Value::Array(items) => serde_json::Value::Array(
104 items
105 .iter()
106 .map(canonicalize_json_for_tool_signature)
107 .collect(),
108 ),
109 _ => value.clone(),
110 }
111}
112
113fn parse_tool_call_value(value: &serde_json::Value) -> Option<ParsedToolCall> {
114 if let Some(function) = value.get("function") {
115 let tool_call_id = parse_tool_call_id(value, Some(function));
116 let raw_name = function
117 .get("name")
118 .and_then(|v| v.as_str())
119 .unwrap_or("")
120 .trim();
121 let name = map_tool_name_alias(raw_name).to_string();
122 if !name.is_empty() {
123 let arguments = parse_arguments_value(
124 function
125 .get("arguments")
126 .or_else(|| function.get("parameters")),
127 );
128 return Some(ParsedToolCall {
129 name,
130 arguments,
131 tool_call_id,
132 });
133 }
134 }
135
136 let tool_call_id = parse_tool_call_id(value, None);
137 let raw_name = value
138 .get("name")
139 .and_then(|v| v.as_str())
140 .unwrap_or("")
141 .trim();
142 let name = map_tool_name_alias(raw_name).to_string();
143
144 if name.is_empty() {
145 return None;
146 }
147
148 let arguments =
149 parse_arguments_value(value.get("arguments").or_else(|| value.get("parameters")));
150 Some(ParsedToolCall {
151 name,
152 arguments,
153 tool_call_id,
154 })
155}
156
157fn parse_tool_calls_from_json_value(value: &serde_json::Value) -> Vec<ParsedToolCall> {
158 let mut calls = Vec::new();
159
160 if let Some(tool_calls) = value.get("tool_calls").and_then(|v| v.as_array()) {
161 for call in tool_calls {
162 if let Some(parsed) = parse_tool_call_value(call) {
163 calls.push(parsed);
164 }
165 }
166
167 if !calls.is_empty() {
168 return calls;
169 }
170 }
171
172 if let Some(array) = value.as_array() {
173 for item in array {
174 if let Some(parsed) = parse_tool_call_value(item) {
175 calls.push(parsed);
176 }
177 }
178 return calls;
179 }
180
181 if let Some(parsed) = parse_tool_call_value(value) {
182 calls.push(parsed);
183 }
184
185 calls
186}
187
188fn has_non_empty_string(value: &serde_json::Value, key: &str) -> bool {
189 value
190 .get(key)
191 .and_then(serde_json::Value::as_str)
192 .is_some_and(|s| !s.trim().is_empty())
193}
194
195fn has_arguments_signal(value: &serde_json::Value) -> bool {
196 value.get("arguments").is_some() || value.get("parameters").is_some()
197}
198
199fn looks_like_tool_call_object(value: &serde_json::Value) -> bool {
200 if let Some(function) = value.get("function").and_then(serde_json::Value::as_object) {
201 let function = serde_json::Value::Object(function.clone());
202 return has_non_empty_string(&function, "name") && has_arguments_signal(&function);
203 }
204
205 has_non_empty_string(value, "name") && has_arguments_signal(value)
206}
207
208fn tool_call_array_has_protocol_shape(value: &serde_json::Value, key: &str) -> bool {
209 value
210 .get(key)
211 .and_then(serde_json::Value::as_array)
212 .is_some_and(|items| !items.is_empty() && items.iter().any(looks_like_tool_call_object))
213}
214
215fn has_tool_protocol_object_signal(value: &serde_json::Value) -> bool {
216 let Some(object) = value.as_object() else {
217 return false;
218 };
219
220 let has_args = has_arguments_signal(value);
221 let has_call_id = has_non_empty_string(value, "id")
222 || has_non_empty_string(value, "call_id")
223 || has_non_empty_string(value, "tool_call_id");
224
225 object
226 .get("function")
227 .and_then(serde_json::Value::as_object)
228 .is_some()
229 || (has_non_empty_string(value, "name") && has_args)
230 || (has_args && has_call_id)
231}
232
233fn tool_call_array_has_malformed_protocol_signal(value: &serde_json::Value, key: &str) -> bool {
234 value
235 .get(key)
236 .and_then(serde_json::Value::as_array)
237 .is_some_and(|items| !items.is_empty() && items.iter().any(has_tool_protocol_object_signal))
238}
239
240fn classify_tool_protocol_json_value(
241 value: &serde_json::Value,
242) -> Option<ToolProtocolEnvelopeKind> {
243 if value
244 .get("type")
245 .and_then(serde_json::Value::as_str)
246 .is_some_and(|ty| ty == "function_call")
247 && has_non_empty_string(value, "name")
248 && (has_arguments_signal(value) || has_non_empty_string(value, "call_id"))
249 {
250 return Some(ToolProtocolEnvelopeKind::ResponsesFunctionCall);
251 }
252
253 if tool_call_array_has_protocol_shape(value, "tool_calls") {
254 return Some(ToolProtocolEnvelopeKind::ToolCalls);
255 }
256
257 if tool_call_array_has_protocol_shape(value, "toolcalls") {
258 return Some(ToolProtocolEnvelopeKind::ToolCallsAlias);
259 }
260
261 if value
262 .get("function_call")
263 .is_some_and(looks_like_tool_call_object)
264 {
265 return Some(ToolProtocolEnvelopeKind::FunctionCall);
266 }
267
268 if has_non_empty_string(value, "tool_call_id")
269 && (value.get("content").is_some()
270 || value.get("result").is_some()
271 || value.get("output").is_some())
272 {
273 return Some(ToolProtocolEnvelopeKind::ToolResult);
274 }
275
276 None
277}
278
279fn json_value_mentions_known_tool(
280 value: &serde_json::Value,
281 known_tool_names: &HashSet<String>,
282) -> bool {
283 if known_tool_names.is_empty() {
284 return false;
285 }
286
287 let Some(object) = value.as_object() else {
288 return value.as_array().is_some_and(|items| {
289 items
290 .iter()
291 .any(|item| json_value_mentions_known_tool(item, known_tool_names))
292 });
293 };
294
295 let name_matches = |candidate: Option<&serde_json::Value>| {
296 candidate
297 .and_then(serde_json::Value::as_str)
298 .map(str::trim)
299 .filter(|name| !name.is_empty())
300 .is_some_and(|name| known_tool_names.contains(&name.to_ascii_lowercase()))
301 };
302
303 if name_matches(object.get("name")) {
304 return true;
305 }
306
307 if let Some(function) = object
308 .get("function")
309 .and_then(serde_json::Value::as_object)
310 {
311 let function = serde_json::Value::Object(function.clone());
312 if json_value_mentions_known_tool(&function, known_tool_names) {
313 return true;
314 }
315 }
316
317 if let Some(function_call) = object.get("function_call")
318 && json_value_mentions_known_tool(function_call, known_tool_names)
319 {
320 return true;
321 }
322
323 ["tool_calls", "toolcalls"].iter().any(|key| {
324 object
325 .get(*key)
326 .and_then(serde_json::Value::as_array)
327 .is_some_and(|items| {
328 items
329 .iter()
330 .any(|item| json_value_mentions_known_tool(item, known_tool_names))
331 })
332 })
333}
334
335pub fn tool_protocol_envelope_mentions_known_tool(
336 text: &str,
337 known_tool_names: &HashSet<String>,
338) -> bool {
339 if known_tool_names.is_empty() {
340 return false;
341 }
342
343 let trimmed = text.trim();
344 if trimmed.is_empty() {
345 return false;
346 }
347
348 if let Some(body) = json_fence_body(trimmed) {
349 return tool_protocol_envelope_mentions_known_tool(body, known_tool_names);
350 }
351
352 if starts_with_tool_protocol_tag_or_fence(trimmed) || contains_tool_protocol_tag_marker(trimmed)
353 {
354 let (_, calls) = parse_tool_calls(trimmed);
355 if calls
356 .iter()
357 .any(|call| known_tool_names.contains(&call.name.to_ascii_lowercase()))
358 {
359 return true;
360 }
361 }
362
363 serde_json::from_str::<serde_json::Value>(trimmed)
364 .is_ok_and(|value| json_value_mentions_known_tool(&value, known_tool_names))
365}
366
367fn has_malformed_tool_protocol_json_signal(value: &serde_json::Value) -> bool {
368 tool_call_array_has_malformed_protocol_signal(value, "tool_calls")
372 || tool_call_array_has_malformed_protocol_signal(value, "toolcalls")
373 || value
374 .get("function_call")
375 .is_some_and(has_tool_protocol_object_signal)
376 || (value
377 .get("type")
378 .and_then(serde_json::Value::as_str)
379 .is_some_and(|ty| ty == "function_call")
380 && (has_non_empty_string(value, "name")
381 || has_non_empty_string(value, "call_id")
382 || has_arguments_signal(value)))
383 || (has_non_empty_string(value, "tool_call_id")
384 && (value.get("content").is_some()
385 || value.get("result").is_some()
386 || value.get("output").is_some()))
387}
388
389fn starts_with_tool_protocol_tag_or_fence(text: &str) -> bool {
390 let lower = text.trim_start().to_ascii_lowercase();
391 lower.starts_with("<tool_call")
392 || lower.starts_with("<toolcall")
393 || lower.starts_with("<tool-call")
394 || lower.starts_with("<invoke")
395 || lower.starts_with("<functioncall")
396 || lower.starts_with("<function_call")
397 || starts_with_tool_protocol_fence_lower(&lower)
398 || lower.starts_with("[tool_call]")
399}
400
401fn starts_with_tool_protocol_fence(text: &str) -> bool {
402 let lower = text.trim_start().to_ascii_lowercase();
403 starts_with_tool_protocol_fence_lower(&lower)
404}
405
406fn starts_with_tool_protocol_fence_lower(lower: &str) -> bool {
407 lower.starts_with("```tool_call")
408 || lower.starts_with("```toolcall")
409 || lower.starts_with("```tool-call")
410 || lower.starts_with("```invoke")
411 || starts_with_tool_name_fence_lower(lower)
412}
413
414fn starts_with_tool_name_fence_lower(lower: &str) -> bool {
415 let Some(rest) = lower.strip_prefix("```tool") else {
416 return false;
417 };
418 matches!(rest.chars().next(), Some(c) if c.is_whitespace() && c != '\n' && c != '\r')
419}
420
421fn contains_tool_protocol_tag_marker(text: &str) -> bool {
422 let lower = text.to_ascii_lowercase();
423 lower.contains("<tool_call")
424 || lower.contains("<toolcall")
425 || lower.contains("<tool-call")
426 || lower.contains("<invoke")
427 || lower.contains("<functioncall")
428 || lower.contains("<function_call")
429 || lower.contains("```tool_call")
430 || lower.contains("```toolcall")
431 || lower.contains("```tool-call")
432 || lower.contains("```invoke")
433 || lower.contains("```tool ")
434 || lower.contains("[tool_call]")
435}
436
437pub fn looks_like_tool_protocol_example(text: &str) -> bool {
438 let trimmed = text.trim();
439 if trimmed.is_empty() {
440 return false;
441 }
442
443 if let Some((body, visible_text)) = leading_json_fence_body_and_trailing_text(trimmed)
444 && classify_tool_protocol_envelope(body).is_some()
445 && has_example_context(visible_text)
446 {
447 return true;
448 }
449
450 if starts_with_tool_protocol_fence(trimmed) || contains_tool_protocol_tag_marker(trimmed) {
451 let (visible_text, calls) = parse_tool_calls(trimmed);
452 if !calls.is_empty() && has_example_context(&visible_text) {
453 return true;
454 }
455 }
456
457 false
458}
459
460fn has_example_context(text: &str) -> bool {
461 let lower = text.to_ascii_lowercase();
462 lower.contains("example")
463 || lower.contains("sample")
464 || lower.contains("示例")
465 || lower.contains("例如")
468 || lower.contains("比如")
469 || lower.contains("举例")
470 || lower.contains("例子")
471 || lower.contains("比方说")
472 || lower.contains("譬如")
473}
474
475fn leading_json_fence_body_and_trailing_text(trimmed: &str) -> Option<(&str, &str)> {
476 let rest = trimmed.strip_prefix("```")?;
477 let first_newline = rest.find('\n')?;
478 let language = rest[..first_newline].trim().trim_end_matches('\r');
479 if !language.eq_ignore_ascii_case("json") {
480 return None;
481 }
482
483 let body_with_close = &rest[first_newline + 1..];
484 let close_start = body_with_close.find("```")?;
485 let body = body_with_close[..close_start].trim();
486 let trailing = body_with_close[close_start + 3..].trim();
487 (!body.is_empty() && !trailing.is_empty()).then_some((body, trailing))
488}
489
490pub fn contains_tool_protocol_tag_call(text: &str) -> bool {
491 if !contains_tool_protocol_tag_marker(text) || looks_like_tool_protocol_example(text) {
492 return false;
493 }
494
495 let (_, calls) = parse_tool_calls(text);
496 !calls.is_empty()
497}
498
499fn classify_tagged_tool_protocol_envelope(text: &str) -> Option<ToolProtocolEnvelopeKind> {
500 if !starts_with_tool_protocol_tag_or_fence(text) {
501 return None;
502 }
503 if looks_like_tool_protocol_example(text) {
504 return None;
505 }
506
507 let is_fence = starts_with_tool_protocol_fence(text);
508 let (visible_text, calls) = parse_tool_calls(text);
509 (!calls.is_empty() && (is_fence || visible_text.trim().is_empty()))
510 .then_some(ToolProtocolEnvelopeKind::TaggedToolCall)
511}
512
513fn looks_like_malformed_tagged_tool_protocol_envelope(text: &str) -> bool {
514 if !starts_with_tool_protocol_tag_or_fence(text) {
515 return false;
516 }
517 if looks_like_tool_protocol_example(text) {
518 return false;
519 }
520
521 let (visible_text, calls) = parse_tool_calls(text);
522 if !calls.is_empty() || !visible_text.trim().is_empty() {
523 return false;
524 }
525
526 let lower = text.to_ascii_lowercase();
527 lower.contains("arguments")
528 || lower.contains("parameters")
529 || lower.contains("function")
530 || lower.contains("name")
531 || lower.contains("call_id")
532 || lower.contains("tool_call_id")
533}
534
535fn has_malformed_tool_protocol_text_signal(text: &str) -> bool {
536 let trimmed = text.trim_start();
537 let lower = trimmed.to_ascii_lowercase();
538 let json_like =
539 trimmed.starts_with('{') || trimmed.starts_with('[') || lower.starts_with("```json");
540 if !json_like {
541 return false;
542 }
543
544 let has_tool_result_shape = text.contains("\"tool_call_id\"")
547 && (text.contains("\"content\"")
548 || text.contains("\"result\"")
549 || text.contains("\"output\""));
550 let has_protocol_container = text.contains("\"tool_calls\"")
551 || text.contains("\"toolcalls\"")
552 || text.contains("\"function_call\"");
553 let has_arguments = text.contains("\"arguments\"") || text.contains("\"parameters\"");
554 let has_call_id = text.contains("\"call_id\"") || text.contains("\"tool_call_id\"");
555
556 has_tool_result_shape || (has_protocol_container && has_arguments && has_call_id)
557}
558
559fn malformed_text_mentions_known_tool(text: &str, known_tool_names: &HashSet<String>) -> bool {
560 if known_tool_names.is_empty() {
561 return false;
562 }
563
564 static JSON_NAME_FIELD_RE: LazyLock<Regex> =
565 LazyLock::new(|| Regex::new(r#""name"\s*:\s*"([^"]+)""#).unwrap());
566
567 JSON_NAME_FIELD_RE.captures_iter(text).any(|cap| {
568 cap.get(1)
569 .map(|name| name.as_str().trim().to_ascii_lowercase())
570 .is_some_and(|name| known_tool_names.contains(&name))
571 })
572}
573
574fn has_malformed_tool_protocol_text_signal_for_known_tools(
575 text: &str,
576 known_tool_names: &HashSet<String>,
577) -> bool {
578 if has_malformed_tool_protocol_text_signal(text) {
579 return true;
580 }
581
582 let trimmed = text.trim_start();
583 let lower = trimmed.to_ascii_lowercase();
584 let json_like =
585 trimmed.starts_with('{') || trimmed.starts_with('[') || lower.starts_with("```json");
586 if !json_like {
587 return false;
588 }
589
590 let has_protocol_container = text.contains("\"tool_calls\"")
591 || text.contains("\"toolcalls\"")
592 || text.contains("\"function_call\"");
593 let has_arguments = text.contains("\"arguments\"") || text.contains("\"parameters\"");
594
595 has_protocol_container
596 && has_arguments
597 && malformed_text_mentions_known_tool(text, known_tool_names)
598}
599
600fn json_fence_body(trimmed: &str) -> Option<&str> {
601 let rest = trimmed.strip_prefix("```")?;
602 let first_newline = rest.find('\n')?;
603 let language = rest[..first_newline].trim().trim_end_matches('\r');
604 if !language.eq_ignore_ascii_case("json") {
605 return None;
606 }
607
608 let body_with_close = &rest[first_newline + 1..];
609 let close_start = body_with_close.rfind("```")?;
610 if !body_with_close[close_start + 3..].trim().is_empty() {
611 return None;
612 }
613 Some(body_with_close[..close_start].trim())
614}
615
616pub fn classify_tool_protocol_envelope(text: &str) -> Option<ToolProtocolEnvelopeKind> {
617 let trimmed = text.trim();
618 if trimmed.is_empty() {
619 return None;
620 }
621
622 if let Some(kind) = classify_tagged_tool_protocol_envelope(trimmed) {
623 return Some(kind);
624 }
625
626 if let Some(body) = json_fence_body(trimmed) {
627 return classify_tool_protocol_envelope(body);
628 }
629
630 let value = serde_json::from_str::<serde_json::Value>(trimmed).ok()?;
631 classify_tool_protocol_json_value(&value)
632}
633
634pub fn looks_like_tool_protocol_envelope(text: &str) -> bool {
635 let trimmed = text.trim();
636 if trimmed.is_empty() {
637 return false;
638 }
639
640 if classify_tool_protocol_envelope(trimmed).is_some() {
641 return true;
642 }
643
644 if let Some(body) = json_fence_body(trimmed) {
645 return looks_like_tool_protocol_envelope(body);
646 }
647
648 serde_json::from_str::<serde_json::Value>(trimmed)
649 .is_ok_and(|value| has_malformed_tool_protocol_json_signal(&value))
650}
651
652pub fn looks_like_malformed_tool_protocol_envelope(text: &str) -> bool {
653 let trimmed = text.trim();
654 if looks_like_tool_protocol_example(trimmed) {
655 return false;
656 }
657
658 if looks_like_malformed_tagged_tool_protocol_envelope(trimmed) {
659 return true;
660 }
661
662 let lower = trimmed.to_ascii_lowercase();
663 let json_like =
664 trimmed.starts_with('{') || trimmed.starts_with('[') || lower.starts_with("```json");
665 if trimmed.is_empty() || !json_like {
666 return false;
667 }
668
669 if let Some(body) = json_fence_body(trimmed) {
670 return looks_like_malformed_tool_protocol_envelope(body);
671 }
672
673 if serde_json::from_str::<serde_json::Value>(trimmed).is_ok() {
674 return false;
675 }
676
677 has_malformed_tool_protocol_text_signal(trimmed)
678}
679
680pub fn looks_like_malformed_tool_protocol_envelope_for_known_tools(
681 text: &str,
682 known_tool_names: &HashSet<String>,
683) -> bool {
684 let trimmed = text.trim();
685 if looks_like_tool_protocol_example(trimmed) {
686 return false;
687 }
688
689 if looks_like_malformed_tool_protocol_envelope(trimmed) {
690 return true;
691 }
692
693 let lower = trimmed.to_ascii_lowercase();
694 let json_like =
695 trimmed.starts_with('{') || trimmed.starts_with('[') || lower.starts_with("```json");
696 if trimmed.is_empty() || !json_like {
697 return false;
698 }
699
700 if let Some(body) = json_fence_body(trimmed) {
701 return looks_like_malformed_tool_protocol_envelope_for_known_tools(body, known_tool_names);
702 }
703
704 if serde_json::from_str::<serde_json::Value>(trimmed).is_ok() {
705 return false;
706 }
707
708 has_malformed_tool_protocol_text_signal_for_known_tools(trimmed, known_tool_names)
709}
710
711fn is_xml_meta_tag(tag: &str) -> bool {
712 let normalized = tag.to_ascii_lowercase();
713 matches!(
714 normalized.as_str(),
715 "tool_call"
716 | "toolcall"
717 | "tool-call"
718 | "invoke"
719 | "thinking"
720 | "thought"
721 | "analysis"
722 | "reasoning"
723 | "reflection"
724 )
725}
726
727static XML_OPEN_TAG_RE: LazyLock<Regex> =
729 LazyLock::new(|| Regex::new(r"<([a-zA-Z_][a-zA-Z0-9_-]*)>").unwrap());
730
731static MINIMAX_INVOKE_RE: LazyLock<Regex> = LazyLock::new(|| {
734 Regex::new(r#"(?is)<invoke\b[^>]*\bname\s*=\s*(?:"([^"]+)"|'([^']+)')[^>]*>(.*?)</invoke>"#)
735 .unwrap()
736});
737
738static MINIMAX_PARAMETER_RE: LazyLock<Regex> = LazyLock::new(|| {
739 Regex::new(
740 r#"(?is)<parameter\b[^>]*\bname\s*=\s*(?:"([^"]+)"|'([^']+)')[^>]*>(.*?)</parameter>"#,
741 )
742 .unwrap()
743});
744
745fn extract_xml_pairs(input: &str) -> Vec<(&str, &str)> {
748 let mut results = Vec::new();
749 let mut search_start = 0;
750 while let Some(open_cap) = XML_OPEN_TAG_RE.captures(&input[search_start..]) {
751 let full_open = open_cap.get(0).unwrap();
752 let tag_name = open_cap.get(1).unwrap().as_str();
753 let open_end = search_start + full_open.end();
754
755 let closing_tag = format!("</{tag_name}>");
756 if let Some(close_pos) = input[open_end..].find(&closing_tag) {
757 let inner = &input[open_end..open_end + close_pos];
758 results.push((tag_name, inner.trim()));
759 search_start = open_end + close_pos + closing_tag.len();
760 } else {
761 search_start = open_end;
762 }
763 }
764 results
765}
766
767fn parse_xml_tool_calls(xml_content: &str) -> Option<Vec<ParsedToolCall>> {
772 let mut calls = Vec::new();
773 let trimmed = xml_content.trim();
774
775 if !trimmed.starts_with('<') || !trimmed.contains('>') {
776 return None;
777 }
778
779 for (tool_name_str, inner_content) in extract_xml_pairs(trimmed) {
780 let tool_name = tool_name_str.to_string();
781 if is_xml_meta_tag(&tool_name) {
782 continue;
783 }
784
785 if inner_content.is_empty() {
786 continue;
787 }
788
789 let mut args = serde_json::Map::new();
790
791 if let Some(first_json) = extract_json_values(inner_content).into_iter().next() {
792 match first_json {
793 serde_json::Value::Object(object_args) => {
794 args = object_args;
795 }
796 other => {
797 args.insert("value".to_string(), other);
798 }
799 }
800 } else {
801 for (key_str, value) in extract_xml_pairs(inner_content) {
802 let key = key_str.to_string();
803 if is_xml_meta_tag(&key) {
804 continue;
805 }
806 if !value.is_empty() {
807 args.insert(key, serde_json::Value::String(value.to_string()));
808 }
809 }
810
811 if args.is_empty() {
812 args.insert(
813 "content".to_string(),
814 serde_json::Value::String(inner_content.to_string()),
815 );
816 }
817 }
818
819 calls.push(ParsedToolCall {
820 name: tool_name,
821 arguments: serde_json::Value::Object(args),
822 tool_call_id: None,
823 });
824 }
825
826 if calls.is_empty() { None } else { Some(calls) }
827}
828
829fn parse_minimax_invoke_calls(response: &str) -> Option<(String, Vec<ParsedToolCall>)> {
831 let mut calls = Vec::new();
832 let mut text_parts = Vec::new();
833 let mut last_end = 0usize;
834
835 for cap in MINIMAX_INVOKE_RE.captures_iter(response) {
836 let Some(full_match) = cap.get(0) else {
837 continue;
838 };
839
840 let before = response[last_end..full_match.start()].trim();
841 if !before.is_empty() {
842 text_parts.push(before.to_string());
843 }
844
845 let name = cap
846 .get(1)
847 .or_else(|| cap.get(2))
848 .map(|m| m.as_str().trim())
849 .filter(|v| !v.is_empty());
850 let body = cap.get(3).map(|m| m.as_str()).unwrap_or("").trim();
851 last_end = full_match.end();
852
853 let Some(name) = name else {
854 continue;
855 };
856
857 let mut args = serde_json::Map::new();
858 for param_cap in MINIMAX_PARAMETER_RE.captures_iter(body) {
859 let key = param_cap
860 .get(1)
861 .or_else(|| param_cap.get(2))
862 .map(|m| m.as_str().trim())
863 .unwrap_or_default();
864 if key.is_empty() {
865 continue;
866 }
867 let value = param_cap
868 .get(3)
869 .map(|m| m.as_str().trim())
870 .unwrap_or_default();
871 if value.is_empty() {
872 continue;
873 }
874
875 let parsed = extract_json_values(value).into_iter().next();
876 args.insert(
877 key.to_string(),
878 parsed.unwrap_or_else(|| serde_json::Value::String(value.to_string())),
879 );
880 }
881
882 if args.is_empty() {
883 if let Some(first_json) = extract_json_values(body).into_iter().next() {
884 match first_json {
885 serde_json::Value::Object(obj) => args = obj,
886 other => {
887 args.insert("value".to_string(), other);
888 }
889 }
890 } else if !body.is_empty() {
891 args.insert(
892 "content".to_string(),
893 serde_json::Value::String(body.to_string()),
894 );
895 }
896 }
897
898 calls.push(ParsedToolCall {
899 name: name.to_string(),
900 arguments: serde_json::Value::Object(args),
901 tool_call_id: None,
902 });
903 }
904
905 if calls.is_empty() {
906 return None;
907 }
908
909 let after = response[last_end..].trim();
910 if !after.is_empty() {
911 text_parts.push(after.to_string());
912 }
913
914 let text = text_parts
915 .join("\n")
916 .replace("<minimax:tool_call>", "")
917 .replace("</minimax:tool_call>", "")
918 .replace("<minimax:toolcall>", "")
919 .replace("</minimax:toolcall>", "")
920 .trim()
921 .to_string();
922
923 Some((text, calls))
924}
925
926const TOOL_CALL_OPEN_TAGS: [&str; 7] = [
927 "<tool_call>",
928 "<tool_calls>",
929 "<toolcall>",
930 "<tool-call>",
931 "<invoke>",
932 "<minimax:tool_call>",
933 "<minimax:toolcall>",
934];
935
936const TOOL_CALL_CLOSE_TAGS: [&str; 7] = [
937 "</tool_call>",
938 "</tool_calls>",
939 "</toolcall>",
940 "</tool-call>",
941 "</invoke>",
942 "</minimax:tool_call>",
943 "</minimax:toolcall>",
944];
945
946fn find_first_tag<'a>(haystack: &str, tags: &'a [&'a str]) -> Option<(usize, &'a str)> {
947 tags.iter()
948 .filter_map(|tag| haystack.find(tag).map(|idx| (idx, *tag)))
949 .min_by_key(|(idx, _)| *idx)
950}
951
952fn extract_first_json_value_with_end(input: &str) -> Option<(serde_json::Value, usize)> {
953 let trimmed = input.trim_start();
954 let trim_offset = input.len().saturating_sub(trimmed.len());
955
956 for (byte_idx, ch) in trimmed.char_indices() {
957 if ch != '{' && ch != '[' {
958 continue;
959 }
960
961 let slice = &trimmed[byte_idx..];
962 let mut stream = serde_json::Deserializer::from_str(slice).into_iter::<serde_json::Value>();
963 if let Some(Ok(value)) = stream.next() {
964 let consumed = stream.byte_offset();
965 if consumed > 0 {
966 return Some((value, trim_offset + byte_idx + consumed));
967 }
968 }
969 }
970
971 None
972}
973
974fn strip_leading_close_tags(mut input: &str) -> &str {
975 loop {
976 let trimmed = input.trim_start();
977 if !trimmed.starts_with("</") {
978 return trimmed;
979 }
980
981 let Some(close_end) = trimmed.find('>') else {
982 return "";
983 };
984 input = &trimmed[close_end + 1..];
985 }
986}
987
988fn extract_json_values(input: &str) -> Vec<serde_json::Value> {
998 let mut values = Vec::new();
999 let trimmed = input.trim();
1000 if trimmed.is_empty() {
1001 return values;
1002 }
1003
1004 if let Ok(value) = serde_json::from_str::<serde_json::Value>(trimmed) {
1005 values.push(value);
1006 return values;
1007 }
1008
1009 let char_positions: Vec<(usize, char)> = trimmed.char_indices().collect();
1010 let mut idx = 0;
1011 while idx < char_positions.len() {
1012 let (byte_idx, ch) = char_positions[idx];
1013 if ch == '{' || ch == '[' {
1014 let slice = &trimmed[byte_idx..];
1015 let mut stream =
1016 serde_json::Deserializer::from_str(slice).into_iter::<serde_json::Value>();
1017 if let Some(Ok(value)) = stream.next() {
1018 let consumed = stream.byte_offset();
1019 if consumed > 0 {
1020 values.push(value);
1021 let next_byte = byte_idx + consumed;
1022 while idx < char_positions.len() && char_positions[idx].0 < next_byte {
1023 idx += 1;
1024 }
1025 continue;
1026 }
1027 }
1028 }
1029 idx += 1;
1030 }
1031
1032 values
1033}
1034
1035fn find_json_end(input: &str) -> Option<usize> {
1037 let trimmed = input.trim_start();
1038 let offset = input.len() - trimmed.len();
1039
1040 if !trimmed.starts_with('{') {
1041 return None;
1042 }
1043
1044 let mut depth = 0;
1045 let mut in_string = false;
1046 let mut escape_next = false;
1047
1048 for (i, ch) in trimmed.char_indices() {
1049 if escape_next {
1050 escape_next = false;
1051 continue;
1052 }
1053
1054 match ch {
1055 '\\' if in_string => escape_next = true,
1056 '"' => in_string = !in_string,
1057 '{' if !in_string => depth += 1,
1058 '}' if !in_string => {
1059 depth -= 1;
1060 if depth == 0 {
1061 return Some(offset + i + ch.len_utf8());
1062 }
1063 }
1064 _ => {}
1065 }
1066 }
1067
1068 None
1069}
1070
1071fn parse_xml_attribute_tool_calls(response: &str) -> Vec<ParsedToolCall> {
1081 let mut calls = Vec::new();
1082
1083 static INVOKE_RE: LazyLock<Regex> = LazyLock::new(|| {
1085 Regex::new(r#"(?s)<invoke\s+name="([^"]+)"[^>]*>(.*?)</invoke>"#).unwrap()
1086 });
1087
1088 static PARAM_RE: LazyLock<Regex> = LazyLock::new(|| {
1090 Regex::new(r#"<parameter\s+name="([^"]+)"[^>]*>([^<]*)</parameter>"#).unwrap()
1091 });
1092
1093 for cap in INVOKE_RE.captures_iter(response) {
1094 let tool_name = cap.get(1).map(|m| m.as_str()).unwrap_or("");
1095 let inner = cap.get(2).map(|m| m.as_str()).unwrap_or("");
1096
1097 if tool_name.is_empty() {
1098 continue;
1099 }
1100
1101 let mut arguments = serde_json::Map::new();
1102
1103 for param_cap in PARAM_RE.captures_iter(inner) {
1104 let param_name = param_cap.get(1).map(|m| m.as_str()).unwrap_or("");
1105 let param_value = param_cap.get(2).map(|m| m.as_str()).unwrap_or("");
1106
1107 if !param_name.is_empty() {
1108 arguments.insert(
1109 param_name.to_string(),
1110 serde_json::Value::String(param_value.to_string()),
1111 );
1112 }
1113 }
1114
1115 if !arguments.is_empty() {
1116 calls.push(ParsedToolCall {
1117 name: map_tool_name_alias(tool_name).to_string(),
1118 arguments: serde_json::Value::Object(arguments),
1119 tool_call_id: None,
1120 });
1121 }
1122 }
1123
1124 calls
1125}
1126
1127fn parse_perl_style_tool_calls(response: &str) -> Vec<ParsedToolCall> {
1142 let mut calls = Vec::new();
1143
1144 static PERL_RE: LazyLock<Regex> = LazyLock::new(|| {
1147 Regex::new(r"(?s)(?:\[TOOL_CALL\]|TOOL_CALL)\s*\{(.+?)\}\}\s*(?:\[/TOOL_CALL\]|/TOOL_CALL)")
1148 .unwrap()
1149 });
1150
1151 static TOOL_NAME_RE: LazyLock<Regex> =
1153 LazyLock::new(|| Regex::new(r#"tool\s*=>\s*"([^"]+)""#).unwrap());
1154
1155 static ARGS_BLOCK_RE: LazyLock<Regex> =
1159 LazyLock::new(|| Regex::new(r"(?s)args\s*=>\s*\{(.+?)(?:\}|$)").unwrap());
1160
1161 static ARGS_RE: LazyLock<Regex> =
1163 LazyLock::new(|| Regex::new(r#"--(\w+)\s+"([^"]+)""#).unwrap());
1164
1165 for cap in PERL_RE.captures_iter(response) {
1166 let content = cap.get(1).map(|m| m.as_str()).unwrap_or("");
1167
1168 let tool_name = TOOL_NAME_RE
1170 .captures(content)
1171 .and_then(|c| c.get(1))
1172 .map(|m| m.as_str())
1173 .unwrap_or("");
1174
1175 if tool_name.is_empty() {
1176 continue;
1177 }
1178
1179 let args_block = ARGS_BLOCK_RE
1181 .captures(content)
1182 .and_then(|c| c.get(1))
1183 .map(|m| m.as_str())
1184 .unwrap_or("");
1185
1186 let mut arguments = serde_json::Map::new();
1187
1188 for arg_cap in ARGS_RE.captures_iter(args_block) {
1189 let key = arg_cap.get(1).map(|m| m.as_str()).unwrap_or("");
1190 let value = arg_cap.get(2).map(|m| m.as_str()).unwrap_or("");
1191
1192 if !key.is_empty() {
1193 arguments.insert(
1194 key.to_string(),
1195 serde_json::Value::String(value.to_string()),
1196 );
1197 }
1198 }
1199
1200 if !arguments.is_empty() {
1201 calls.push(ParsedToolCall {
1202 name: map_tool_name_alias(tool_name).to_string(),
1203 arguments: serde_json::Value::Object(arguments),
1204 tool_call_id: None,
1205 });
1206 }
1207 }
1208
1209 calls
1210}
1211
1212fn parse_function_call_tool_calls(response: &str) -> Vec<ParsedToolCall> {
1221 let mut calls = Vec::new();
1222
1223 static FUNC_RE: LazyLock<Regex> = LazyLock::new(|| {
1225 Regex::new(r"(?s)<FunctionCall>\s*(\w+)\s*<code>([^<]+)</code>\s*</FunctionCall>").unwrap()
1226 });
1227
1228 for cap in FUNC_RE.captures_iter(response) {
1229 let tool_name = cap.get(1).map(|m| m.as_str()).unwrap_or("");
1230 let args_text = cap.get(2).map(|m| m.as_str()).unwrap_or("");
1231
1232 if tool_name.is_empty() {
1233 continue;
1234 }
1235
1236 let mut arguments = serde_json::Map::new();
1238 for line in args_text.lines() {
1239 let line = line.trim();
1240 if let Some(pos) = line.find('>') {
1241 let key = line[..pos].trim();
1242 let value = line[pos + 1..].trim();
1243 if !key.is_empty() && !value.is_empty() {
1244 arguments.insert(
1245 key.to_string(),
1246 serde_json::Value::String(value.to_string()),
1247 );
1248 }
1249 }
1250 }
1251
1252 if !arguments.is_empty() {
1253 calls.push(ParsedToolCall {
1254 name: map_tool_name_alias(tool_name).to_string(),
1255 arguments: serde_json::Value::Object(arguments),
1256 tool_call_id: None,
1257 });
1258 }
1259 }
1260
1261 calls
1262}
1263
1264fn map_tool_name_alias(tool_name: &str) -> &str {
1268 let tool_name = tool_name
1275 .rsplit_once('.')
1276 .map(|(_, suffix)| suffix)
1277 .unwrap_or(tool_name);
1278 match tool_name {
1279 "shell" | "bash" | "sh" | "exec" | "command" | "cmd" | "browser_open" | "browser"
1281 | "web_search" => "shell",
1282 "send_message" | "sendmessage" => "message_send",
1284 "fileread" | "file_read" | "readfile" | "read_file" | "file" => "file_read",
1286 "filewrite" | "file_write" | "writefile" | "write_file" => "file_write",
1287 "filelist" | "file_list" | "listfiles" | "list_files" => "file_list",
1288 "memoryrecall" | "memory_recall" | "recall" | "memrecall" => "memory_recall",
1290 "memorystore" | "memory_store" | "store" | "memstore" => "memory_store",
1291 "memoryforget" | "memory_forget" | "forget" | "memforget" => "memory_forget",
1292 "http_request" | "http" | "fetch" | "curl" | "wget" => "http_request",
1294 _ => tool_name,
1295 }
1296}
1297
1298fn build_curl_command(url: &str) -> Option<String> {
1299 if !(url.starts_with("http://") || url.starts_with("https://")) {
1300 return None;
1301 }
1302
1303 if url.chars().any(char::is_whitespace) {
1304 return None;
1305 }
1306
1307 let escaped = url.replace('\'', r#"'\\''"#);
1308 Some(format!("curl -s '{}'", escaped))
1309}
1310
1311fn parse_glm_style_tool_calls(text: &str) -> Vec<(String, serde_json::Value, Option<String>)> {
1312 let mut calls = Vec::new();
1313
1314 for line in text.lines() {
1315 let line = line.trim();
1316 if line.is_empty() {
1317 continue;
1318 }
1319
1320 if let Some(pos) = line.find('/') {
1322 let tool_part = &line[..pos];
1323 let rest = &line[pos + 1..];
1324
1325 if tool_part.chars().all(|c| c.is_alphanumeric() || c == '_') {
1326 let tool_name = map_tool_name_alias(tool_part);
1327
1328 if let Some(gt_pos) = rest.find('>') {
1329 let param_name = rest[..gt_pos].trim();
1330 let value = rest[gt_pos + 1..].trim();
1331
1332 let arguments = match tool_name {
1333 "shell" => {
1334 if param_name == "url" {
1335 let Some(command) = build_curl_command(value) else {
1336 continue;
1337 };
1338 serde_json::json!({ "command": command })
1339 } else if value.starts_with("http://") || value.starts_with("https://")
1340 {
1341 if let Some(command) = build_curl_command(value) {
1342 serde_json::json!({ "command": command })
1343 } else {
1344 serde_json::json!({ "command": value })
1345 }
1346 } else {
1347 serde_json::json!({ "command": value })
1348 }
1349 }
1350 "http_request" => {
1351 serde_json::json!({"url": value, "method": "GET"})
1352 }
1353 _ => serde_json::json!({ param_name: value }),
1354 };
1355
1356 calls.push((tool_name.to_string(), arguments, Some(line.to_string())));
1357 continue;
1358 }
1359
1360 if rest.starts_with('{')
1361 && let Ok(json_args) = serde_json::from_str::<serde_json::Value>(rest)
1362 {
1363 calls.push((tool_name.to_string(), json_args, Some(line.to_string())));
1364 }
1365 }
1366 }
1367 }
1368
1369 calls
1370}
1371
1372fn default_param_for_tool(tool: &str) -> &'static str {
1378 match tool {
1379 "shell" | "bash" | "sh" | "exec" | "command" | "cmd" => "command",
1380 "file_read" | "fileread" | "readfile" | "read_file" | "file" | "file_write"
1382 | "filewrite" | "writefile" | "write_file" | "file_edit" | "fileedit" | "editfile"
1383 | "edit_file" | "file_list" | "filelist" | "listfiles" | "list_files" => "path",
1384 "memory_recall" | "memoryrecall" | "recall" | "memrecall" | "memory_forget"
1386 | "memoryforget" | "forget" | "memforget" | "web_search_tool" | "web_search"
1387 | "websearch" | "search" => "query",
1388 "memory_store" | "memorystore" | "store" | "memstore" => "content",
1389 "http_request" | "http" | "fetch" | "curl" | "wget" | "browser_open" | "browser" => "url",
1391 _ => "input",
1392 }
1393}
1394
1395fn parse_glm_shortened_body(body: &str) -> Option<ParsedToolCall> {
1407 let body = body.trim();
1408 if body.is_empty() {
1409 return None;
1410 }
1411
1412 let function_style = body.find('(').and_then(|open| {
1413 if body.ends_with(')') && open > 0 {
1414 Some((body[..open].trim(), body[open + 1..body.len() - 1].trim()))
1415 } else {
1416 None
1417 }
1418 });
1419
1420 let (tool_raw, value_part) = if let Some((tool, args)) = function_style {
1424 (tool, args)
1425 } else if body.contains("=\"") {
1426 let split_pos = body.find(|c: char| c.is_whitespace()).unwrap_or(body.len());
1428 let tool = body[..split_pos].trim();
1429 let attrs = body[split_pos..]
1430 .trim()
1431 .trim_end_matches("/>")
1432 .trim_end_matches('>')
1433 .trim_end_matches('/')
1434 .trim();
1435 (tool, attrs)
1436 } else if let Some(gt_pos) = body.find('>') {
1437 let tool = body[..gt_pos].trim();
1439 let value = body[gt_pos + 1..].trim();
1440 let value = value.trim_end_matches("/>").trim_end_matches('/').trim();
1442 (tool, value)
1443 } else {
1444 return None;
1445 };
1446
1447 let tool_raw = tool_raw.trim_end_matches(|c: char| c.is_whitespace());
1449 if tool_raw.is_empty() || !tool_raw.chars().all(|c| c.is_alphanumeric() || c == '_') {
1450 return None;
1451 }
1452
1453 let tool_name = map_tool_name_alias(tool_raw);
1454
1455 if value_part.contains("=\"") {
1457 let mut args = serde_json::Map::new();
1458 let mut rest = value_part;
1460 while let Some(eq_pos) = rest.find("=\"") {
1461 let key_start = rest[..eq_pos]
1462 .rfind(|c: char| c.is_whitespace())
1463 .map(|p| p + 1)
1464 .unwrap_or(0);
1465 let key = rest[key_start..eq_pos]
1466 .trim()
1467 .trim_matches(|c: char| c == ',' || c == ';');
1468 let after_quote = &rest[eq_pos + 2..];
1469 if let Some(end_quote) = after_quote.find('"') {
1470 let value = &after_quote[..end_quote];
1471 if !key.is_empty() {
1472 args.insert(
1473 key.to_string(),
1474 serde_json::Value::String(value.to_string()),
1475 );
1476 }
1477 rest = &after_quote[end_quote + 1..];
1478 } else {
1479 break;
1480 }
1481 }
1482 if !args.is_empty() {
1483 return Some(ParsedToolCall {
1484 name: tool_name.to_string(),
1485 arguments: serde_json::Value::Object(args),
1486 tool_call_id: None,
1487 });
1488 }
1489 }
1490
1491 if value_part.contains('\n') {
1493 let mut args = serde_json::Map::new();
1494 for line in value_part.lines() {
1495 let line = line.trim();
1496 if line.is_empty() {
1497 continue;
1498 }
1499 if let Some(colon_pos) = line.find(':') {
1500 let key = line[..colon_pos].trim();
1501 let value = line[colon_pos + 1..].trim();
1502 if !key.is_empty() && !value.is_empty() {
1503 let json_value = match value {
1505 "true" | "yes" => serde_json::Value::Bool(true),
1506 "false" | "no" => serde_json::Value::Bool(false),
1507 _ => serde_json::Value::String(value.to_string()),
1508 };
1509 args.insert(key.to_string(), json_value);
1510 }
1511 }
1512 }
1513 if !args.is_empty() {
1514 return Some(ParsedToolCall {
1515 name: tool_name.to_string(),
1516 arguments: serde_json::Value::Object(args),
1517 tool_call_id: None,
1518 });
1519 }
1520 }
1521
1522 if !value_part.is_empty() {
1524 let param = default_param_for_tool(tool_raw);
1525 let arguments = match tool_name {
1526 "shell" => {
1527 if value_part.starts_with("http://") || value_part.starts_with("https://") {
1528 if let Some(cmd) = build_curl_command(value_part) {
1529 serde_json::json!({ "command": cmd })
1530 } else {
1531 serde_json::json!({ "command": value_part })
1532 }
1533 } else {
1534 serde_json::json!({ "command": value_part })
1535 }
1536 }
1537 "http_request" => serde_json::json!({"url": value_part, "method": "GET"}),
1538 _ => serde_json::json!({ param: value_part }),
1539 };
1540 return Some(ParsedToolCall {
1541 name: tool_name.to_string(),
1542 arguments,
1543 tool_call_id: None,
1544 });
1545 }
1546
1547 None
1548}
1549
1550pub fn parse_tool_calls(response: &str) -> (String, Vec<ParsedToolCall>) {
1575 let cleaned = strip_think_tags(response);
1580 let response = cleaned.as_str();
1581
1582 let mut text_parts = Vec::new();
1583 let mut calls = Vec::new();
1584 let mut remaining = response;
1585
1586 if let Ok(json_value) = serde_json::from_str::<serde_json::Value>(response.trim()) {
1589 calls = parse_tool_calls_from_json_value(&json_value);
1590 if !calls.is_empty() {
1591 if let Some(content) = json_value.get("content").and_then(|v| v.as_str())
1593 && !content.trim().is_empty()
1594 {
1595 text_parts.push(content.trim().to_string());
1596 }
1597 return (text_parts.join("\n"), calls);
1598 }
1599 }
1600
1601 if let Some((minimax_text, minimax_calls)) = parse_minimax_invoke_calls(response)
1602 && !minimax_calls.is_empty()
1603 {
1604 return (minimax_text, minimax_calls);
1605 }
1606
1607 while let Some((start, open_tag)) = find_first_tag(remaining, &TOOL_CALL_OPEN_TAGS) {
1609 let before = &remaining[..start];
1611 if !before.trim().is_empty() {
1612 text_parts.push(before.trim().to_string());
1613 }
1614
1615 let Some(close_tag) = (match open_tag {
1616 "<tool_call>" => Some("</tool_call>"),
1617 "<tool_calls>" => Some("</tool_calls>"),
1618 "<toolcall>" => Some("</toolcall>"),
1619 "<tool-call>" => Some("</tool-call>"),
1620 "<invoke>" => Some("</invoke>"),
1621 "<minimax:tool_call>" => Some("</minimax:tool_call>"),
1622 "<minimax:toolcall>" => Some("</minimax:toolcall>"),
1623 _ => None,
1624 }) else {
1625 break;
1626 };
1627
1628 let after_open = &remaining[start + open_tag.len()..];
1629 if let Some(close_idx) = after_open.find(close_tag) {
1630 let inner = &after_open[..close_idx];
1631 let mut parsed_any = false;
1632
1633 let json_values = extract_json_values(inner);
1635 for value in json_values {
1636 let parsed_calls = parse_tool_calls_from_json_value(&value);
1637 if !parsed_calls.is_empty() {
1638 parsed_any = true;
1639 calls.extend(parsed_calls);
1640 }
1641 }
1642
1643 if !parsed_any && let Some(xml_calls) = parse_xml_tool_calls(inner) {
1645 calls.extend(xml_calls);
1646 parsed_any = true;
1647 }
1648
1649 if !parsed_any {
1650 if let Some(glm_call) = parse_glm_shortened_body(inner) {
1652 calls.push(glm_call);
1653 parsed_any = true;
1654 }
1655 }
1656
1657 if !parsed_any {
1658 ::zeroclaw_log::record!(
1659 WARN,
1660 ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note)
1661 .with_outcome(::zeroclaw_log::EventOutcome::Unknown),
1662 "Malformed <tool_call>: expected tool-call object in tag body (JSON/XML/GLM)"
1663 );
1664 }
1665
1666 remaining = &after_open[close_idx + close_tag.len()..];
1667 } else {
1668 let mut resolved = false;
1671 if let Some((cross_idx, cross_tag)) = find_first_tag(after_open, &TOOL_CALL_CLOSE_TAGS)
1672 {
1673 let inner = &after_open[..cross_idx];
1674 let mut parsed_any = false;
1675
1676 let json_values = extract_json_values(inner);
1678 for value in json_values {
1679 let parsed_calls = parse_tool_calls_from_json_value(&value);
1680 if !parsed_calls.is_empty() {
1681 parsed_any = true;
1682 calls.extend(parsed_calls);
1683 }
1684 }
1685
1686 if !parsed_any && let Some(xml_calls) = parse_xml_tool_calls(inner) {
1688 calls.extend(xml_calls);
1689 parsed_any = true;
1690 }
1691
1692 if !parsed_any && let Some(glm_call) = parse_glm_shortened_body(inner) {
1694 calls.push(glm_call);
1695 parsed_any = true;
1696 }
1697
1698 if parsed_any {
1699 remaining = &after_open[cross_idx + cross_tag.len()..];
1700 resolved = true;
1701 }
1702 }
1703
1704 if resolved {
1705 continue;
1706 }
1707
1708 if let Some(json_end) = find_json_end(after_open)
1711 && let Ok(value) =
1712 serde_json::from_str::<serde_json::Value>(&after_open[..json_end])
1713 {
1714 let parsed_calls = parse_tool_calls_from_json_value(&value);
1715 if !parsed_calls.is_empty() {
1716 calls.extend(parsed_calls);
1717 remaining = strip_leading_close_tags(&after_open[json_end..]);
1718 continue;
1719 }
1720 }
1721
1722 if let Some((value, consumed_end)) = extract_first_json_value_with_end(after_open) {
1723 let parsed_calls = parse_tool_calls_from_json_value(&value);
1724 if !parsed_calls.is_empty() {
1725 calls.extend(parsed_calls);
1726 remaining = strip_leading_close_tags(&after_open[consumed_end..]);
1727 continue;
1728 }
1729 }
1730
1731 let glm_input = after_open.trim();
1734 if let Some(glm_call) = parse_glm_shortened_body(glm_input) {
1735 calls.push(glm_call);
1736 remaining = "";
1737 continue;
1738 }
1739
1740 remaining = &remaining[start..];
1741 break;
1742 }
1743 }
1744
1745 if calls.is_empty() {
1749 static MD_TOOL_CALL_RE: LazyLock<Regex> = LazyLock::new(|| {
1750 Regex::new(
1751 r"(?s)```(?:tool[_-]?call|invoke)\s*\n(.*?)(?:```|</tool[_-]?call>|</toolcall>|</invoke>|</minimax:toolcall>)",
1752 )
1753 .unwrap()
1754 });
1755 let mut md_text_parts: Vec<String> = Vec::new();
1756 let mut last_end = 0;
1757
1758 for cap in MD_TOOL_CALL_RE.captures_iter(response) {
1759 let full_match = cap.get(0).unwrap();
1760 let before = &response[last_end..full_match.start()];
1761 if !before.trim().is_empty() {
1762 md_text_parts.push(before.trim().to_string());
1763 }
1764 let inner = &cap[1];
1765 let json_values = extract_json_values(inner);
1766 for value in json_values {
1767 let parsed_calls = parse_tool_calls_from_json_value(&value);
1768 calls.extend(parsed_calls);
1769 }
1770 last_end = full_match.end();
1771 }
1772
1773 if !calls.is_empty() {
1774 let after = &response[last_end..];
1775 if !after.trim().is_empty() {
1776 md_text_parts.push(after.trim().to_string());
1777 }
1778 text_parts = md_text_parts;
1779 remaining = "";
1780 }
1781 }
1782
1783 if calls.is_empty() {
1786 static MD_TOOL_NAME_RE: LazyLock<Regex> =
1787 LazyLock::new(|| Regex::new(r"(?s)```tool\s+(\w+)\s*\n(.*?)(?:```|$)").unwrap());
1788 let mut md_text_parts: Vec<String> = Vec::new();
1789 let mut last_end = 0;
1790
1791 for cap in MD_TOOL_NAME_RE.captures_iter(response) {
1792 let full_match = cap.get(0).unwrap();
1793 let before = &response[last_end..full_match.start()];
1794 if !before.trim().is_empty() {
1795 md_text_parts.push(before.trim().to_string());
1796 }
1797 let tool_name = &cap[1];
1798 let inner = &cap[2];
1799
1800 let json_values = extract_json_values(inner);
1802 if json_values.is_empty() {
1803 ::zeroclaw_log::record!(WARN, ::zeroclaw_log::Event::new(module_path!(), ::zeroclaw_log::Action::Note).with_outcome(::zeroclaw_log::EventOutcome::Unknown).with_attrs(::serde_json::json!({"tool_name": tool_name, "inner": inner.chars().take(100).collect::<String>()})), "Found ```tool <name> block but could not parse JSON arguments");
1805 } else {
1806 for value in json_values {
1807 let arguments = if value.is_object() {
1808 value
1809 } else {
1810 serde_json::Value::Object(serde_json::Map::new())
1811 };
1812 calls.push(ParsedToolCall {
1813 name: tool_name.to_string(),
1814 arguments,
1815 tool_call_id: None,
1816 });
1817 }
1818 }
1819 last_end = full_match.end();
1820 }
1821
1822 if !calls.is_empty() {
1823 let after = &response[last_end..];
1824 if !after.trim().is_empty() {
1825 md_text_parts.push(after.trim().to_string());
1826 }
1827 text_parts = md_text_parts;
1828 remaining = "";
1829 }
1830 }
1831
1832 if calls.is_empty() {
1839 let xml_calls = parse_xml_attribute_tool_calls(remaining);
1840 if !xml_calls.is_empty() {
1841 let mut cleaned_text = remaining.to_string();
1842 for call in xml_calls {
1843 calls.push(call);
1844 if let Some(start) = cleaned_text.find("<minimax:toolcall>")
1846 && let Some(end) = cleaned_text.find("</minimax:toolcall>")
1847 {
1848 let end_pos = end + "</minimax:toolcall>".len();
1849 if end_pos <= cleaned_text.len() {
1850 cleaned_text =
1851 format!("{}{}", &cleaned_text[..start], &cleaned_text[end_pos..]);
1852 }
1853 }
1854 }
1855 if !cleaned_text.trim().is_empty() {
1856 text_parts.push(cleaned_text.trim().to_string());
1857 }
1858 remaining = "";
1859 }
1860 }
1861
1862 if calls.is_empty() {
1870 let perl_calls = parse_perl_style_tool_calls(remaining);
1871 if !perl_calls.is_empty() {
1872 let mut cleaned_text = remaining.to_string();
1873 for call in perl_calls {
1874 calls.push(call);
1875 while let Some(start) = cleaned_text.find("TOOL_CALL") {
1877 if let Some(end) = cleaned_text.find("/TOOL_CALL") {
1878 let end_pos = end + "/TOOL_CALL".len();
1879 if end_pos <= cleaned_text.len() {
1880 cleaned_text =
1881 format!("{}{}", &cleaned_text[..start], &cleaned_text[end_pos..]);
1882 }
1883 } else {
1884 break;
1885 }
1886 }
1887 }
1888 if !cleaned_text.trim().is_empty() {
1889 text_parts.push(cleaned_text.trim().to_string());
1890 }
1891 remaining = "";
1892 }
1893 }
1894
1895 if calls.is_empty() {
1900 let func_calls = parse_function_call_tool_calls(remaining);
1901 if !func_calls.is_empty() {
1902 let mut cleaned_text = remaining.to_string();
1903 for call in func_calls {
1904 calls.push(call);
1905 while let Some(start) = cleaned_text.find("<FunctionCall>") {
1907 if let Some(end) = cleaned_text.find("</FunctionCall>") {
1908 let end_pos = end + "</FunctionCall>".len();
1909 if end_pos <= cleaned_text.len() {
1910 cleaned_text =
1911 format!("{}{}", &cleaned_text[..start], &cleaned_text[end_pos..]);
1912 }
1913 } else {
1914 break;
1915 }
1916 }
1917 }
1918 if !cleaned_text.trim().is_empty() {
1919 text_parts.push(cleaned_text.trim().to_string());
1920 }
1921 remaining = "";
1922 }
1923 }
1924
1925 if calls.is_empty() {
1927 let glm_calls = parse_glm_style_tool_calls(remaining);
1928 if !glm_calls.is_empty() {
1929 let mut cleaned_text = remaining.to_string();
1930 for (name, args, raw) in &glm_calls {
1931 calls.push(ParsedToolCall {
1932 name: name.clone(),
1933 arguments: args.clone(),
1934 tool_call_id: None,
1935 });
1936 if let Some(r) = raw {
1937 cleaned_text = cleaned_text.replace(r, "");
1938 }
1939 }
1940 if !cleaned_text.trim().is_empty() {
1941 text_parts.push(cleaned_text.trim().to_string());
1942 }
1943 remaining = "";
1944 }
1945 }
1946
1947 if !remaining.trim().is_empty() {
1959 text_parts.push(remaining.trim().to_string());
1960 }
1961
1962 (text_parts.join("\n"), calls)
1963}
1964
1965pub fn strip_think_tags(s: &str) -> String {
1970 let mut result = String::with_capacity(s.len());
1971 let mut rest = s;
1972 loop {
1973 if let Some(start) = rest.find("<think>") {
1974 result.push_str(&rest[..start]);
1975 if let Some(end) = rest[start..].find("</think>") {
1976 rest = &rest[start + end + "</think>".len()..];
1977 } else {
1978 break;
1980 }
1981 } else {
1982 result.push_str(rest);
1983 break;
1984 }
1985 }
1986 result.trim().to_string()
1987}
1988
1989pub fn strip_tool_result_blocks(text: &str) -> String {
1992 static TOOL_RESULT_RE: LazyLock<Regex> =
1993 LazyLock::new(|| Regex::new(r"(?s)<tool_result[^>]*>.*?</tool_result>").unwrap());
1994 static THINKING_RE: LazyLock<Regex> =
1995 LazyLock::new(|| Regex::new(r"(?s)<thinking>.*?</thinking>").unwrap());
1996 static THINK_RE: LazyLock<Regex> =
1997 LazyLock::new(|| Regex::new(r"(?s)<think>.*?</think>").unwrap());
1998 static TOOL_RESULTS_PREFIX_RE: LazyLock<Regex> =
1999 LazyLock::new(|| Regex::new(r"(?m)^\[Tool results\]\s*\n?").unwrap());
2000 static EXCESS_BLANK_LINES_RE: LazyLock<Regex> =
2001 LazyLock::new(|| Regex::new(r"\n{3,}").unwrap());
2002
2003 let result = TOOL_RESULT_RE.replace_all(text, "");
2004 let result = THINKING_RE.replace_all(&result, "");
2005 let result = THINK_RE.replace_all(&result, "");
2006 let result = TOOL_RESULTS_PREFIX_RE.replace_all(&result, "");
2007 let result = EXCESS_BLANK_LINES_RE.replace_all(result.trim(), "\n\n");
2008
2009 result.trim().to_string()
2010}
2011
2012pub fn detect_tool_call_parse_issue(
2013 response: &str,
2014 parsed_calls: &[ParsedToolCall],
2015) -> Option<String> {
2016 if !parsed_calls.is_empty() {
2017 return None;
2018 }
2019
2020 let trimmed = response.trim();
2021 if trimmed.is_empty() {
2022 return None;
2023 }
2024
2025 if looks_like_tool_protocol_envelope(trimmed) {
2026 return Some(
2027 "response resembled an internal tool protocol envelope but no valid tool call could be parsed"
2028 .into(),
2029 );
2030 }
2031
2032 if let Ok(value) = serde_json::from_str::<serde_json::Value>(trimmed) {
2033 return has_malformed_tool_protocol_json_signal(&value).then(|| {
2034 "response resembled an internal tool protocol envelope but no valid tool call could be parsed"
2035 .into()
2036 });
2037 }
2038
2039 if has_malformed_tool_protocol_text_signal(trimmed) {
2040 return Some(
2041 "response resembled an internal tool protocol envelope but no valid tool call could be parsed"
2042 .into(),
2043 );
2044 }
2045
2046 let contains_tool_payload_marker = trimmed.contains("<tool_call")
2047 || trimmed.contains("<toolcall")
2048 || trimmed.contains("<tool-call")
2049 || trimmed.contains("```tool_call")
2050 || trimmed.contains("```toolcall")
2051 || trimmed.contains("```tool-call")
2052 || trimmed.contains("```tool file_")
2053 || trimmed.contains("```tool shell")
2054 || trimmed.contains("```tool web_")
2055 || trimmed.contains("```tool memory_")
2056 || trimmed.contains("```tool ") || trimmed.contains("TOOL_CALL")
2058 || trimmed.contains("[TOOL_CALL]")
2059 || trimmed.contains("<FunctionCall>");
2060
2061 if contains_tool_payload_marker {
2062 if looks_like_tool_protocol_example(trimmed) {
2063 return None;
2064 }
2065 if contains_tool_protocol_tag_call(trimmed) {
2066 return Some(
2067 "response resembled a tool-call payload but no valid tool call could be parsed"
2068 .into(),
2069 );
2070 }
2071
2072 let (visible_text, recovered_calls) = parse_tool_calls(trimmed);
2073 if !recovered_calls.is_empty() && !visible_text.trim().is_empty() {
2074 return None;
2075 }
2076 if !recovered_calls.is_empty() || visible_text.trim().is_empty() {
2077 return Some(
2078 "response resembled a tool-call payload but no valid tool call could be parsed"
2079 .into(),
2080 );
2081 }
2082 }
2083
2084 if looks_like_malformed_tool_protocol_envelope(trimmed) {
2085 Some("response resembled a tool-call payload but no valid tool call could be parsed".into())
2086 } else {
2087 None
2088 }
2089}
2090
2091pub fn build_native_assistant_history_from_parsed_calls(
2092 text: &str,
2093 tool_calls: &[ParsedToolCall],
2094 reasoning_content: Option<&str>,
2095) -> Option<String> {
2096 if tool_calls.is_empty() {
2101 return None;
2102 }
2103
2104 let calls_json = tool_calls
2105 .iter()
2106 .map(|tc| {
2107 Some(serde_json::json!({
2108 "id": tc.tool_call_id.clone()?,
2109 "name": tc.name,
2110 "arguments": serde_json::to_string(&tc.arguments).unwrap_or_else(|_| "{}".to_string()),
2111 }))
2112 })
2113 .collect::<Option<Vec<_>>>()?;
2114
2115 let content = if text.trim().is_empty() {
2116 serde_json::Value::Null
2117 } else {
2118 serde_json::Value::String(text.trim().to_string())
2119 };
2120
2121 let mut obj = serde_json::json!({
2122 "content": content,
2123 "tool_calls": calls_json,
2124 });
2125
2126 if let Some(rc) = reasoning_content {
2127 obj.as_object_mut().unwrap().insert(
2128 "reasoning_content".to_string(),
2129 serde_json::Value::String(rc.to_string()),
2130 );
2131 }
2132
2133 Some(obj.to_string())
2134}
2135
2136#[cfg(test)]
2137mod tests {
2138 use super::*;
2139
2140 #[test]
2141 fn build_native_assistant_history_returns_none_for_empty_calls() {
2142 let result = build_native_assistant_history_from_parsed_calls("answer text", &[], None);
2147 assert!(
2148 result.is_none(),
2149 "expected None for empty tool_calls slice, got {result:?}"
2150 );
2151 }
2152
2153 #[test]
2154 fn build_native_assistant_history_returns_none_for_empty_calls_with_reasoning() {
2155 let result = build_native_assistant_history_from_parsed_calls(
2160 "answer text",
2161 &[],
2162 Some("deep thought"),
2163 );
2164 assert!(result.is_none());
2165 }
2166
2167 #[test]
2168 fn build_native_assistant_history_emits_tool_calls_when_non_empty() {
2169 let calls = vec![ParsedToolCall {
2173 name: "shell".into(),
2174 arguments: serde_json::json!({"command": "pwd"}),
2175 tool_call_id: Some("call_1".into()),
2176 }];
2177 let result = build_native_assistant_history_from_parsed_calls("answer", &calls, None);
2178 let s = result.expect("Some(_) for non-empty tool_calls");
2179 let parsed: serde_json::Value = serde_json::from_str(&s).unwrap();
2180 assert_eq!(parsed["content"].as_str(), Some("answer"));
2181 let arr = parsed["tool_calls"].as_array().expect("tool_calls array");
2182 assert_eq!(arr.len(), 1);
2183 assert_eq!(arr[0]["name"].as_str(), Some("shell"));
2184 }
2185
2186 #[test]
2187 fn parse_arguments_value_unwraps_nested_object_string() {
2188 let raw = serde_json::json!({
2189 "service": "gmail",
2190 "params": "{\"maxResults\":3}"
2191 });
2192 let out = parse_arguments_value(Some(&raw));
2193 assert_eq!(out["service"], serde_json::json!("gmail"));
2194 assert_eq!(out["params"], serde_json::json!({"maxResults": 3}));
2195 }
2196
2197 #[test]
2198 fn parse_arguments_value_unwraps_nested_array_string() {
2199 let raw = serde_json::json!({ "items": "[1,2,3]" });
2200 let out = parse_arguments_value(Some(&raw));
2201 assert_eq!(out["items"], serde_json::json!([1, 2, 3]));
2202 }
2203
2204 #[test]
2205 fn parse_arguments_value_leaves_non_json_strings_alone() {
2206 let raw = serde_json::json!({
2207 "greeting": "hello",
2208 "answer": "42",
2209 "truthy": "true",
2210 "broken": "{not json"
2211 });
2212 let out = parse_arguments_value(Some(&raw));
2213 assert_eq!(out["greeting"], serde_json::json!("hello"));
2214 assert_eq!(out["answer"], serde_json::json!("42"));
2215 assert_eq!(out["truthy"], serde_json::json!("true"));
2216 assert_eq!(out["broken"], serde_json::json!("{not json"));
2217 }
2218
2219 #[test]
2220 fn parse_arguments_value_handles_double_encoding() {
2221 let inner = r#"{"params":"{\"maxResults\":3}"}"#;
2222 let raw = serde_json::Value::String(inner.to_string());
2223 let out = parse_arguments_value(Some(&raw));
2224 assert_eq!(out["params"], serde_json::json!({"maxResults": 3}));
2225 }
2226
2227 #[test]
2228 fn parse_tool_call_value_handles_gemini_double_encoded_params() {
2229 let inner = r#"{"service":"gmail","resource":"users","sub_resource":"messages","method":"list","params":"{\"maxResults\":3}"}"#;
2230 let call_json = serde_json::json!({
2231 "function": {
2232 "name": "google_workspace",
2233 "arguments": inner
2234 }
2235 });
2236 let parsed = parse_tool_call_value(&call_json).expect("expected a parsed call");
2237 assert_eq!(parsed.name, "google_workspace");
2238 assert_eq!(
2239 parsed.arguments["params"],
2240 serde_json::json!({"maxResults": 3})
2241 );
2242 assert_eq!(
2243 parsed.arguments["sub_resource"],
2244 serde_json::json!("messages")
2245 );
2246 }
2247
2248 #[test]
2249 fn parse_tool_calls_extracts_multiple_calls() {
2250 let response = r#"<tool_call>
2251{"name": "file_read", "arguments": {"path": "a.txt"}}
2252</tool_call>
2253<tool_call>
2254{"name": "file_read", "arguments": {"path": "b.txt"}}
2255</tool_call>"#;
2256
2257 let (_, calls) = parse_tool_calls(response);
2258 assert_eq!(calls.len(), 2);
2259 assert_eq!(calls[0].name, "file_read");
2260 assert_eq!(calls[1].name, "file_read");
2261 }
2262
2263 #[test]
2264 fn parse_tool_calls_returns_text_only_when_no_calls() {
2265 let response = "Just a normal response with no tools.";
2266 let (text, calls) = parse_tool_calls(response);
2267 assert_eq!(text, "Just a normal response with no tools.");
2268 assert!(calls.is_empty());
2269 }
2270
2271 #[test]
2272 fn parse_tool_calls_handles_malformed_json() {
2273 let response = r#"<tool_call>
2274not valid json
2275</tool_call>
2276Some text after."#;
2277
2278 let (text, calls) = parse_tool_calls(response);
2279 assert!(calls.is_empty());
2280 assert!(text.contains("Some text after."));
2281 }
2282
2283 #[test]
2284 fn parse_tool_calls_text_before_and_after() {
2285 let response = r#"Before text.
2286<tool_call>
2287{"name": "shell", "arguments": {"command": "echo hi"}}
2288</tool_call>
2289After text."#;
2290
2291 let (text, calls) = parse_tool_calls(response);
2292 assert!(text.contains("Before text."));
2293 assert!(text.contains("After text."));
2294 assert_eq!(calls.len(), 1);
2295 }
2296
2297 #[test]
2298 fn parse_tool_calls_handles_openai_format() {
2299 let response = r#"{"content": "Let me check that for you.", "tool_calls": [{"type": "function", "function": {"name": "shell", "arguments": "{\"command\": \"ls -la\"}"}}]}"#;
2301
2302 let (text, calls) = parse_tool_calls(response);
2303 assert_eq!(text, "Let me check that for you.");
2304 assert_eq!(calls.len(), 1);
2305 assert_eq!(calls[0].name, "shell");
2306 assert_eq!(
2307 calls[0].arguments.get("command").unwrap().as_str().unwrap(),
2308 "ls -la"
2309 );
2310 }
2311
2312 #[test]
2313 fn parse_tool_calls_handles_openai_format_multiple_calls() {
2314 let response = r#"{"tool_calls": [{"type": "function", "function": {"name": "file_read", "arguments": "{\"path\": \"a.txt\"}"}}, {"type": "function", "function": {"name": "file_read", "arguments": "{\"path\": \"b.txt\"}"}}]}"#;
2315
2316 let (_, calls) = parse_tool_calls(response);
2317 assert_eq!(calls.len(), 2);
2318 assert_eq!(calls[0].name, "file_read");
2319 assert_eq!(calls[1].name, "file_read");
2320 }
2321
2322 #[test]
2323 fn parse_tool_calls_openai_format_without_content() {
2324 let response = r#"{"tool_calls": [{"type": "function", "function": {"name": "memory_recall", "arguments": "{}"}}]}"#;
2326
2327 let (text, calls) = parse_tool_calls(response);
2328 assert!(text.is_empty()); assert_eq!(calls.len(), 1);
2330 assert_eq!(calls[0].name, "memory_recall");
2331 }
2332
2333 #[test]
2334 fn parse_tool_calls_preserves_openai_tool_call_ids() {
2335 let response = r#"{"tool_calls":[{"id":"call_42","function":{"name":"shell","arguments":"{\"command\":\"pwd\"}"}}]}"#;
2336 let (_, calls) = parse_tool_calls(response);
2337 assert_eq!(calls.len(), 1);
2338 assert_eq!(calls[0].tool_call_id.as_deref(), Some("call_42"));
2339 }
2340
2341 #[test]
2342 fn parse_tool_calls_handles_markdown_json_inside_tool_call_tag() {
2343 let response = r#"<tool_call>
2344```json
2345{"name": "file_write", "arguments": {"path": "test.py", "content": "print('ok')"}}
2346```
2347</tool_call>"#;
2348
2349 let (text, calls) = parse_tool_calls(response);
2350 assert!(text.is_empty());
2351 assert_eq!(calls.len(), 1);
2352 assert_eq!(calls[0].name, "file_write");
2353 assert_eq!(
2354 calls[0].arguments.get("path").unwrap().as_str().unwrap(),
2355 "test.py"
2356 );
2357 }
2358
2359 #[test]
2360 fn parse_tool_calls_handles_noisy_tool_call_tag_body() {
2361 let response = r#"<tool_call>
2362I will now call the tool with this payload:
2363{"name": "shell", "arguments": {"command": "pwd"}}
2364</tool_call>"#;
2365
2366 let (text, calls) = parse_tool_calls(response);
2367 assert!(text.is_empty());
2368 assert_eq!(calls.len(), 1);
2369 assert_eq!(calls[0].name, "shell");
2370 assert_eq!(
2371 calls[0].arguments.get("command").unwrap().as_str().unwrap(),
2372 "pwd"
2373 );
2374 }
2375
2376 #[test]
2377 fn parse_tool_calls_handles_tool_call_inline_attributes_with_send_message_alias() {
2378 let response = r#"<tool_call>send_message channel="user_channel" message="Hello! How can I assist you today?"</tool_call>"#;
2379
2380 let (text, calls) = parse_tool_calls(response);
2381 assert!(text.is_empty());
2382 assert_eq!(calls.len(), 1);
2383 assert_eq!(calls[0].name, "message_send");
2384 assert_eq!(
2385 calls[0].arguments.get("channel").unwrap().as_str().unwrap(),
2386 "user_channel"
2387 );
2388 assert_eq!(
2389 calls[0].arguments.get("message").unwrap().as_str().unwrap(),
2390 "Hello! How can I assist you today?"
2391 );
2392 }
2393
2394 #[test]
2395 fn parse_tool_calls_handles_tool_call_function_style_arguments() {
2396 let response = r#"<tool_call>message_send(channel="general", message="test")</tool_call>"#;
2397
2398 let (text, calls) = parse_tool_calls(response);
2399 assert!(text.is_empty());
2400 assert_eq!(calls.len(), 1);
2401 assert_eq!(calls[0].name, "message_send");
2402 assert_eq!(
2403 calls[0].arguments.get("channel").unwrap().as_str().unwrap(),
2404 "general"
2405 );
2406 assert_eq!(
2407 calls[0].arguments.get("message").unwrap().as_str().unwrap(),
2408 "test"
2409 );
2410 }
2411
2412 #[test]
2413 fn parse_tool_calls_handles_xml_nested_tool_payload() {
2414 let response = r#"<tool_call>
2415<memory_recall>
2416<query>project roadmap</query>
2417</memory_recall>
2418</tool_call>"#;
2419
2420 let (text, calls) = parse_tool_calls(response);
2421 assert!(text.is_empty());
2422 assert_eq!(calls.len(), 1);
2423 assert_eq!(calls[0].name, "memory_recall");
2424 assert_eq!(
2425 calls[0].arguments.get("query").unwrap().as_str().unwrap(),
2426 "project roadmap"
2427 );
2428 }
2429
2430 #[test]
2431 fn parse_tool_calls_handles_plural_tool_calls_wrapper() {
2432 let (text, calls) = parse_tool_calls(
2436 "<tool_calls>\n{\"name\":\"myserver__some_tool\",\"arguments\":{\"key\":\"value\"}}\n</tool_calls>",
2437 );
2438 assert_eq!(calls.len(), 1);
2439 assert_eq!(calls[0].name, "myserver__some_tool");
2440 assert_eq!(
2441 calls[0].arguments.get("key").unwrap().as_str().unwrap(),
2442 "value"
2443 );
2444 assert!(text.is_empty());
2445 }
2446
2447 #[test]
2448 fn parse_tool_calls_ignores_xml_thinking_wrapper() {
2449 let response = r#"<tool_call>
2450<thinking>Need to inspect memory first</thinking>
2451<memory_recall>
2452<query>recent deploy notes</query>
2453</memory_recall>
2454</tool_call>"#;
2455
2456 let (text, calls) = parse_tool_calls(response);
2457 assert!(text.is_empty());
2458 assert_eq!(calls.len(), 1);
2459 assert_eq!(calls[0].name, "memory_recall");
2460 assert_eq!(
2461 calls[0].arguments.get("query").unwrap().as_str().unwrap(),
2462 "recent deploy notes"
2463 );
2464 }
2465
2466 #[test]
2467 fn parse_tool_calls_handles_xml_with_json_arguments() {
2468 let response = r#"<tool_call>
2469<shell>{"command":"pwd"}</shell>
2470</tool_call>"#;
2471
2472 let (text, calls) = parse_tool_calls(response);
2473 assert!(text.is_empty());
2474 assert_eq!(calls.len(), 1);
2475 assert_eq!(calls[0].name, "shell");
2476 assert_eq!(
2477 calls[0].arguments.get("command").unwrap().as_str().unwrap(),
2478 "pwd"
2479 );
2480 }
2481
2482 #[test]
2483 fn parse_tool_calls_handles_markdown_tool_call_fence() {
2484 let response = r#"I'll check that.
2485```tool_call
2486{"name": "shell", "arguments": {"command": "pwd"}}
2487```
2488Done."#;
2489
2490 let (text, calls) = parse_tool_calls(response);
2491 assert_eq!(calls.len(), 1);
2492 assert_eq!(calls[0].name, "shell");
2493 assert_eq!(
2494 calls[0].arguments.get("command").unwrap().as_str().unwrap(),
2495 "pwd"
2496 );
2497 assert!(text.contains("I'll check that."));
2498 assert!(text.contains("Done."));
2499 assert!(!text.contains("```tool_call"));
2500 }
2501
2502 #[test]
2503 fn parse_tool_calls_handles_markdown_tool_call_hybrid_close_tag() {
2504 let response = r#"Preface
2505```tool-call
2506{"name": "shell", "arguments": {"command": "date"}}
2507</tool_call>
2508Tail"#;
2509
2510 let (text, calls) = parse_tool_calls(response);
2511 assert_eq!(calls.len(), 1);
2512 assert_eq!(calls[0].name, "shell");
2513 assert_eq!(
2514 calls[0].arguments.get("command").unwrap().as_str().unwrap(),
2515 "date"
2516 );
2517 assert!(text.contains("Preface"));
2518 assert!(text.contains("Tail"));
2519 assert!(!text.contains("```tool-call"));
2520 }
2521
2522 #[test]
2523 fn parse_tool_calls_handles_markdown_invoke_fence() {
2524 let response = r#"Checking.
2525```invoke
2526{"name": "shell", "arguments": {"command": "date"}}
2527```
2528Done."#;
2529
2530 let (text, calls) = parse_tool_calls(response);
2531 assert_eq!(calls.len(), 1);
2532 assert_eq!(calls[0].name, "shell");
2533 assert_eq!(
2534 calls[0].arguments.get("command").unwrap().as_str().unwrap(),
2535 "date"
2536 );
2537 assert!(text.contains("Checking."));
2538 assert!(text.contains("Done."));
2539 }
2540
2541 #[test]
2542 fn parse_tool_calls_handles_tool_name_fence_format() {
2543 let response = r#"I'll write a test file.
2545```tool file_write
2546{"path": "/home/user/test.txt", "content": "Hello world"}
2547```
2548Done."#;
2549
2550 let (text, calls) = parse_tool_calls(response);
2551 assert_eq!(calls.len(), 1);
2552 assert_eq!(calls[0].name, "file_write");
2553 assert_eq!(
2554 calls[0].arguments.get("path").unwrap().as_str().unwrap(),
2555 "/home/user/test.txt"
2556 );
2557 assert!(text.contains("I'll write a test file."));
2558 assert!(text.contains("Done."));
2559 }
2560
2561 #[test]
2562 fn parse_tool_calls_handles_tool_name_fence_shell() {
2563 let response = r#"```tool shell
2565{"command": "ls -la"}
2566```"#;
2567
2568 let (_text, calls) = parse_tool_calls(response);
2569 assert_eq!(calls.len(), 1);
2570 assert_eq!(calls[0].name, "shell");
2571 assert_eq!(
2572 calls[0].arguments.get("command").unwrap().as_str().unwrap(),
2573 "ls -la"
2574 );
2575 }
2576
2577 #[test]
2578 fn parse_tool_calls_handles_multiple_tool_name_fences() {
2579 let response = r#"First, I'll write a file.
2581```tool file_write
2582{"path": "/tmp/a.txt", "content": "A"}
2583```
2584Then read it.
2585```tool file_read
2586{"path": "/tmp/a.txt"}
2587```
2588Done."#;
2589
2590 let (text, calls) = parse_tool_calls(response);
2591 assert_eq!(calls.len(), 2);
2592 assert_eq!(calls[0].name, "file_write");
2593 assert_eq!(calls[1].name, "file_read");
2594 assert!(text.contains("First, I'll write a file."));
2595 assert!(text.contains("Then read it."));
2596 assert!(text.contains("Done."));
2597 }
2598
2599 #[test]
2600 fn parse_tool_calls_handles_toolcall_tag_alias() {
2601 let response = r#"<toolcall>
2602{"name": "shell", "arguments": {"command": "date"}}
2603</toolcall>"#;
2604
2605 let (text, calls) = parse_tool_calls(response);
2606 assert!(text.is_empty());
2607 assert_eq!(calls.len(), 1);
2608 assert_eq!(calls[0].name, "shell");
2609 assert_eq!(
2610 calls[0].arguments.get("command").unwrap().as_str().unwrap(),
2611 "date"
2612 );
2613 }
2614
2615 #[test]
2616 fn parse_tool_calls_handles_tool_dash_call_tag_alias() {
2617 let response = r#"<tool-call>
2618{"name": "shell", "arguments": {"command": "whoami"}}
2619</tool-call>"#;
2620
2621 let (text, calls) = parse_tool_calls(response);
2622 assert!(text.is_empty());
2623 assert_eq!(calls.len(), 1);
2624 assert_eq!(calls[0].name, "shell");
2625 assert_eq!(
2626 calls[0].arguments.get("command").unwrap().as_str().unwrap(),
2627 "whoami"
2628 );
2629 }
2630
2631 #[test]
2632 fn parse_tool_calls_handles_invoke_tag_alias() {
2633 let response = r#"<invoke>
2634{"name": "shell", "arguments": {"command": "uptime"}}
2635</invoke>"#;
2636
2637 let (text, calls) = parse_tool_calls(response);
2638 assert!(text.is_empty());
2639 assert_eq!(calls.len(), 1);
2640 assert_eq!(calls[0].name, "shell");
2641 assert_eq!(
2642 calls[0].arguments.get("command").unwrap().as_str().unwrap(),
2643 "uptime"
2644 );
2645 }
2646
2647 #[test]
2648 fn parse_tool_calls_handles_minimax_invoke_parameter_format() {
2649 let response = r#"<minimax:tool_call>
2650<invoke name="shell">
2651<parameter name="command">sqlite3 /tmp/test.db ".tables"</parameter>
2652</invoke>
2653</minimax:tool_call>"#;
2654
2655 let (text, calls) = parse_tool_calls(response);
2656 assert!(text.is_empty());
2657 assert_eq!(calls.len(), 1);
2658 assert_eq!(calls[0].name, "shell");
2659 assert_eq!(
2660 calls[0].arguments.get("command").unwrap().as_str().unwrap(),
2661 r#"sqlite3 /tmp/test.db ".tables""#
2662 );
2663 }
2664
2665 #[test]
2666 fn parse_tool_calls_handles_minimax_invoke_with_surrounding_text() {
2667 let response = r#"Preface
2668<minimax:tool_call>
2669<invoke name='http_request'>
2670<parameter name='url'>https://example.com</parameter>
2671<parameter name='method'>GET</parameter>
2672</invoke>
2673</minimax:tool_call>
2674Tail"#;
2675
2676 let (text, calls) = parse_tool_calls(response);
2677 assert!(text.contains("Preface"));
2678 assert!(text.contains("Tail"));
2679 assert_eq!(calls.len(), 1);
2680 assert_eq!(calls[0].name, "http_request");
2681 assert_eq!(
2682 calls[0].arguments.get("url").unwrap().as_str().unwrap(),
2683 "https://example.com"
2684 );
2685 assert_eq!(
2686 calls[0].arguments.get("method").unwrap().as_str().unwrap(),
2687 "GET"
2688 );
2689 }
2690
2691 #[test]
2692 fn parse_tool_calls_handles_minimax_toolcall_alias_and_cross_close_tag() {
2693 let response = r#"<tool_call>
2694{"name":"shell","arguments":{"command":"date"}}
2695</minimax:toolcall>"#;
2696
2697 let (text, calls) = parse_tool_calls(response);
2698 assert!(text.is_empty());
2699 assert_eq!(calls.len(), 1);
2700 assert_eq!(calls[0].name, "shell");
2701 assert_eq!(
2702 calls[0].arguments.get("command").unwrap().as_str().unwrap(),
2703 "date"
2704 );
2705 }
2706
2707 #[test]
2708 fn parse_tool_calls_handles_perl_style_tool_call_blocks() {
2709 let response = r#"TOOL_CALL
2710{tool => "shell", args => { --command "uname -a" }}}
2711/TOOL_CALL"#;
2712
2713 let calls = parse_perl_style_tool_calls(response);
2714 assert_eq!(calls.len(), 1);
2715 assert_eq!(calls[0].name, "shell");
2716 assert_eq!(
2717 calls[0].arguments.get("command").unwrap().as_str().unwrap(),
2718 "uname -a"
2719 );
2720 }
2721
2722 #[test]
2723 fn parse_tool_calls_handles_square_bracket_tool_call_blocks() {
2724 let response =
2725 r#"[TOOL_CALL]{tool => "shell", args => {--command "echo hello"}}[/TOOL_CALL]"#;
2726
2727 let calls = parse_perl_style_tool_calls(response);
2728 assert_eq!(calls.len(), 1);
2729 assert_eq!(calls[0].name, "shell");
2730 assert_eq!(
2731 calls[0].arguments.get("command").unwrap().as_str().unwrap(),
2732 "echo hello"
2733 );
2734 }
2735
2736 #[test]
2737 fn parse_tool_calls_handles_square_bracket_multiline() {
2738 let response = r#"[TOOL_CALL]
2739{tool => "file_read", args => {
2740 --path "/tmp/test.txt"
2741 --description "Read test file"
2742}}
2743[/TOOL_CALL]"#;
2744
2745 let calls = parse_perl_style_tool_calls(response);
2746 assert_eq!(calls.len(), 1);
2747 assert_eq!(calls[0].name, "file_read");
2748 assert_eq!(
2749 calls[0].arguments.get("path").unwrap().as_str().unwrap(),
2750 "/tmp/test.txt"
2751 );
2752 assert_eq!(
2753 calls[0]
2754 .arguments
2755 .get("description")
2756 .unwrap()
2757 .as_str()
2758 .unwrap(),
2759 "Read test file"
2760 );
2761 }
2762
2763 #[test]
2764 fn parse_tool_calls_recovers_unclosed_tool_call_with_json() {
2765 let response = r#"I will call the tool now.
2766<tool_call>
2767{"name": "shell", "arguments": {"command": "uptime -p"}}"#;
2768
2769 let (text, calls) = parse_tool_calls(response);
2770 assert!(text.contains("I will call the tool now."));
2771 assert_eq!(calls.len(), 1);
2772 assert_eq!(calls[0].name, "shell");
2773 assert_eq!(
2774 calls[0].arguments.get("command").unwrap().as_str().unwrap(),
2775 "uptime -p"
2776 );
2777 }
2778
2779 #[test]
2780 fn parse_tool_calls_recovers_mismatched_close_tag() {
2781 let response = r#"<tool_call>
2782{"name": "shell", "arguments": {"command": "uptime"}}
2783</arg_value>"#;
2784
2785 let (text, calls) = parse_tool_calls(response);
2786 assert!(text.is_empty());
2787 assert_eq!(calls.len(), 1);
2788 assert_eq!(calls[0].name, "shell");
2789 assert_eq!(
2790 calls[0].arguments.get("command").unwrap().as_str().unwrap(),
2791 "uptime"
2792 );
2793 }
2794
2795 #[test]
2796 fn parse_tool_calls_recovers_cross_alias_closing_tags() {
2797 let response = r#"<toolcall>
2798{"name": "shell", "arguments": {"command": "date"}}
2799</tool_call>"#;
2800
2801 let (text, calls) = parse_tool_calls(response);
2802 assert!(text.is_empty());
2803 assert_eq!(calls.len(), 1);
2804 assert_eq!(calls[0].name, "shell");
2805 }
2806
2807 #[test]
2808 fn parse_tool_calls_rejects_raw_tool_json_without_tags() {
2809 let response = r#"Sure, creating the file now.
2813{"name": "file_write", "arguments": {"path": "hello.py", "content": "print('hello')"}}"#;
2814
2815 let (text, calls) = parse_tool_calls(response);
2816 assert!(text.contains("Sure, creating the file now."));
2817 assert_eq!(
2818 calls.len(),
2819 0,
2820 "Raw JSON without wrappers should not be parsed"
2821 );
2822 }
2823
2824 #[test]
2825 fn parse_tool_calls_handles_empty_tool_result() {
2826 let response = r#"I'll run that command.
2828<tool_result name="shell">
2829
2830</tool_result>
2831Done."#;
2832 let (text, calls) = parse_tool_calls(response);
2833 assert!(text.contains("Done."));
2834 assert!(calls.is_empty());
2835 }
2836
2837 #[test]
2838 fn strip_tool_result_blocks_removes_single_block() {
2839 let input = r#"<tool_result name="memory_recall" status="ok">
2840{"matches":["hello"]}
2841</tool_result>
2842Here is my answer."#;
2843 assert_eq!(strip_tool_result_blocks(input), "Here is my answer.");
2844 }
2845
2846 #[test]
2847 fn strip_tool_result_blocks_removes_multiple_blocks() {
2848 let input = r#"<tool_result name="memory_recall" status="ok">
2849{"matches":[]}
2850</tool_result>
2851<tool_result name="shell" status="ok">
2852done
2853</tool_result>
2854Final answer."#;
2855 assert_eq!(strip_tool_result_blocks(input), "Final answer.");
2856 }
2857
2858 #[test]
2859 fn strip_tool_result_blocks_removes_prefix() {
2860 let input =
2861 "[Tool results]\n<tool_result name=\"shell\" status=\"ok\">\nok\n</tool_result>\nDone.";
2862 assert_eq!(strip_tool_result_blocks(input), "Done.");
2863 }
2864
2865 #[test]
2866 fn strip_tool_result_blocks_removes_thinking() {
2867 let input = "<thinking>\nLet me think...\n</thinking>\nHere is the answer.";
2868 assert_eq!(strip_tool_result_blocks(input), "Here is the answer.");
2869 }
2870
2871 #[test]
2872 fn strip_tool_result_blocks_removes_think_tags() {
2873 let input = "<think>\nLet me reason...\n</think>\nHere is the answer.";
2874 assert_eq!(strip_tool_result_blocks(input), "Here is the answer.");
2875 }
2876
2877 #[test]
2878 fn parse_tool_calls_strips_think_before_tool_call() {
2879 let response = "<think>I need to list files to understand the project</think>\n<tool_call>\n{\"name\":\"shell\",\"arguments\":{\"command\":\"ls\"}}\n</tool_call>";
2882 let (text, calls) = parse_tool_calls(response);
2883 assert_eq!(
2884 calls.len(),
2885 1,
2886 "should parse tool call after stripping think tags"
2887 );
2888 assert_eq!(calls[0].name, "shell");
2889 assert_eq!(
2890 calls[0].arguments.get("command").unwrap().as_str().unwrap(),
2891 "ls"
2892 );
2893 assert!(text.is_empty(), "think content should not appear as text");
2894 }
2895
2896 #[test]
2897 fn parse_tool_calls_strips_think_only_returns_empty() {
2898 let response = "<think>Just thinking, no action needed</think>";
2901 let (text, calls) = parse_tool_calls(response);
2902 assert!(calls.is_empty());
2903 assert!(text.is_empty());
2904 }
2905
2906 #[test]
2907 fn parse_tool_calls_handles_qwen_think_with_multiple_tool_calls() {
2908 let response = "<think>I need to check two things</think>\n<tool_call>\n{\"name\":\"shell\",\"arguments\":{\"command\":\"date\"}}\n</tool_call>\n<tool_call>\n{\"name\":\"shell\",\"arguments\":{\"command\":\"pwd\"}}\n</tool_call>";
2909 let (_, calls) = parse_tool_calls(response);
2910 assert_eq!(calls.len(), 2);
2911 assert_eq!(
2912 calls[0].arguments.get("command").unwrap().as_str().unwrap(),
2913 "date"
2914 );
2915 assert_eq!(
2916 calls[1].arguments.get("command").unwrap().as_str().unwrap(),
2917 "pwd"
2918 );
2919 }
2920
2921 #[test]
2922 fn strip_tool_result_blocks_preserves_clean_text() {
2923 let input = "Hello, this is a normal response.";
2924 assert_eq!(strip_tool_result_blocks(input), input);
2925 }
2926
2927 #[test]
2928 fn strip_tool_result_blocks_returns_empty_for_only_tags() {
2929 let input = "<tool_result name=\"memory_recall\" status=\"ok\">\n{}\n</tool_result>";
2930 assert_eq!(strip_tool_result_blocks(input), "");
2931 }
2932
2933 #[test]
2934 fn parse_arguments_value_handles_null() {
2935 let value = serde_json::json!(null);
2937 let result = parse_arguments_value(Some(&value));
2938 assert!(result.is_null());
2939 }
2940
2941 #[test]
2942 fn parse_tool_calls_handles_empty_tool_calls_array() {
2943 let response = r#"{"content": "Hello", "tool_calls": []}"#;
2945 let (text, calls) = parse_tool_calls(response);
2946 assert!(text.contains("Hello"));
2948 assert!(calls.is_empty());
2949 }
2950
2951 #[test]
2952 fn detect_tool_call_parse_issue_flags_malformed_payloads() {
2953 let response =
2954 "<tool_call>{\"name\":\"shell\",\"arguments\":{\"command\":\"pwd\"}</tool_call>";
2955 let issue = detect_tool_call_parse_issue(response, &[]);
2956 assert!(
2957 issue.is_some(),
2958 "malformed tool payload should be flagged for diagnostics"
2959 );
2960 }
2961
2962 #[test]
2963 fn detect_tool_call_parse_issue_ignores_normal_text() {
2964 let issue = detect_tool_call_parse_issue("Thanks, done.", &[]);
2965 assert!(issue.is_none());
2966 }
2967
2968 #[test]
2969 fn detect_tool_call_parse_issue_ignores_empty_tool_calls_array() {
2970 let issue = detect_tool_call_parse_issue(r#"{"content":"Hello","tool_calls":[]}"#, &[]);
2971 assert!(issue.is_none());
2972 }
2973
2974 #[test]
2975 fn detect_tool_call_parse_issue_ignores_json_fenced_business_tool_calls() {
2976 let response = r#"```json
2977{"tool_calls":[{"service":"billing","count":2}]}
2978```"#;
2979 let issue = detect_tool_call_parse_issue(response, &[]);
2980 assert!(issue.is_none());
2981 }
2982
2983 #[test]
2984 fn detect_tool_call_parse_issue_ignores_tool_call_fenced_example() {
2985 let response = r#"```tool_call
2986{"name":"shell","arguments":{"command":"pwd"}}
2987```
2988This is an example, not an invocation."#;
2989
2990 let issue = detect_tool_call_parse_issue(response, &[]);
2991
2992 assert!(issue.is_none());
2993 }
2994
2995 #[test]
2996 fn detect_tool_call_parse_issue_flags_standalone_tool_call_fence() {
2997 let response = r#"```tool_call
2998{"name":"shell","arguments":{"command":"pwd"}}
2999```"#;
3000
3001 let issue = detect_tool_call_parse_issue(response, &[]);
3002
3003 assert!(issue.is_some());
3004 }
3005
3006 #[test]
3007 fn detect_tool_call_parse_issue_ignores_tool_call_tag_example() {
3008 let response = r#"<tool_call>
3009{"name":"shell","arguments":{"command":"pwd"}}
3010</tool_call>
3011This is an example, not an invocation."#;
3012
3013 let issue = detect_tool_call_parse_issue(response, &[]);
3014
3015 assert!(issue.is_none());
3016 }
3017
3018 #[test]
3019 fn detect_tool_call_parse_issue_flags_tagged_tool_call_with_trailing_text() {
3020 let response = r#"<tool_call>
3021{"name":"shell","arguments":{"command":"pwd"}}
3022</tool_call>
3023Done."#;
3024
3025 let issue = detect_tool_call_parse_issue(response, &[]);
3026
3027 assert!(issue.is_some());
3028 }
3029
3030 #[test]
3031 fn detect_tool_call_parse_issue_flags_json_fenced_tool_protocol() {
3032 let response = r#"```json
3033{"tool_calls":[{"name":"shell","arguments":{"command":"pwd"}}]}
3034```"#;
3035 let issue = detect_tool_call_parse_issue(response, &[]);
3036 assert!(issue.is_some());
3037 }
3038
3039 #[test]
3040 fn detect_tool_call_parse_issue_flags_malformed_tool_result_envelope() {
3041 let response = r#"{"tool_call_id":"call_1","content":"raw tool output""#;
3042 let issue = detect_tool_call_parse_issue(response, &[]);
3043 assert!(issue.is_some());
3044 }
3045
3046 #[test]
3047 fn detect_tool_call_parse_issue_ignores_malformed_tool_call_id_only_json() {
3048 let response = r#"{"tool_call_id":"support-case-1""#;
3049 let issue = detect_tool_call_parse_issue(response, &[]);
3050 assert!(issue.is_none());
3051 }
3052
3053 #[test]
3054 fn detect_tool_call_parse_issue_flags_malformed_nonempty_tool_calls_array() {
3055 let issue = detect_tool_call_parse_issue(
3056 r#"{"content":null,"tool_calls":[{"call_id":"call_1","arguments":"{}"}]}"#,
3057 &[],
3058 );
3059 assert!(issue.is_some());
3060 }
3061
3062 #[test]
3063 fn detect_tool_call_parse_issue_ignores_malformed_business_tool_calls_without_call_id() {
3064 for response in [
3065 r#"{"tool_calls":[{"name":"support_case","arguments":{"id":"A1"}}"#,
3066 r#"{"toolcalls":[{"name":"support_case","arguments":{"id":"A1"}}"#,
3067 ] {
3068 let issue = detect_tool_call_parse_issue(response, &[]);
3069
3070 assert!(
3071 issue.is_none(),
3072 "business JSON without a tool call id must not be treated as internal protocol: {response}"
3073 );
3074 assert!(
3075 !looks_like_malformed_tool_protocol_envelope(response),
3076 "business JSON without a tool call id must not be classified as malformed protocol: {response}"
3077 );
3078 }
3079 }
3080
3081 #[test]
3082 fn looks_like_tool_protocol_envelope_flags_malformed_nonempty_tool_calls_array() {
3083 assert!(looks_like_tool_protocol_envelope(
3084 r#"{"content":null,"tool_calls":[{"call_id":"call_1","arguments":"{}"}]}"#
3085 ));
3086 assert!(!looks_like_tool_protocol_envelope(
3087 r#"{"content":"Hello","tool_calls":[]}"#
3088 ));
3089 }
3090
3091 #[test]
3092 fn classify_tool_protocol_envelope_flags_internal_json_variants() {
3093 assert_eq!(
3094 classify_tool_protocol_envelope(
3095 r#"{"content":null,"tool_calls":[{"id":"call_1","name":"shell","arguments":"{}"}]}"#
3096 ),
3097 Some(ToolProtocolEnvelopeKind::ToolCalls)
3098 );
3099 assert_eq!(
3100 classify_tool_protocol_envelope(
3101 r#"{"toolcalls":[{"name":"shell","arguments":{"command":"pwd"}}]}"#
3102 ),
3103 Some(ToolProtocolEnvelopeKind::ToolCallsAlias)
3104 );
3105 assert_eq!(
3106 classify_tool_protocol_envelope(r#"{"tool_calls":[{"name":"shell","arguments":{}}]}"#),
3107 Some(ToolProtocolEnvelopeKind::ToolCalls)
3108 );
3109 assert_eq!(
3110 classify_tool_protocol_envelope(r#"{"toolcalls":[{"name":"shell","arguments":{}}]}"#),
3111 Some(ToolProtocolEnvelopeKind::ToolCallsAlias)
3112 );
3113 assert_eq!(
3114 classify_tool_protocol_envelope(
3115 r#"{"function_call":{"name":"shell","arguments":"{\"command\":\"pwd\"}"}}"#
3116 ),
3117 Some(ToolProtocolEnvelopeKind::FunctionCall)
3118 );
3119 assert_eq!(
3120 classify_tool_protocol_envelope(
3121 r#"{"tool_call_id":"call_1","content":"command output"}"#
3122 ),
3123 Some(ToolProtocolEnvelopeKind::ToolResult)
3124 );
3125 assert_eq!(
3126 classify_tool_protocol_envelope(
3127 r#"{"type":"function_call","call_id":"call_1","name":"shell","arguments":"{}"}"#
3128 ),
3129 Some(ToolProtocolEnvelopeKind::ResponsesFunctionCall)
3130 );
3131 assert_eq!(
3132 classify_tool_protocol_envelope(
3133 r#"```json
3134{"tool_calls":[{"name":"shell","arguments":{"command":"pwd"}}]}
3135```"#
3136 ),
3137 Some(ToolProtocolEnvelopeKind::ToolCalls)
3138 );
3139 }
3140
3141 #[test]
3142 fn classify_tool_protocol_envelope_preserves_tool_call_examples() {
3143 let fenced_example = r#"```tool_call
3144{"name":"shell","arguments":{"command":"pwd"}}
3145```
3146This is an example, not an invocation."#;
3147 let embedded_fenced_example = r#"Here is an example:
3148```tool_call
3149{"name":"shell","arguments":{"command":"pwd"}}
3150```"#;
3151 let embedded_fenced_example_cn = r#"例如:
3152```tool_call
3153{"name":"shell","arguments":{"command":"pwd"}}
3154```"#;
3155 let tag_example = r#"<tool_call>
3156{"name":"shell","arguments":{"command":"pwd"}}
3157</tool_call>
3158This is an example, not an invocation."#;
3159 let tag_example_cn = r#"比如:
3160<tool_call>
3161{"name":"shell","arguments":{"command":"pwd"}}
3162</tool_call>"#;
3163
3164 assert_eq!(classify_tool_protocol_envelope(fenced_example), None);
3165 assert!(!looks_like_tool_protocol_envelope(fenced_example));
3166 assert_eq!(
3167 classify_tool_protocol_envelope(embedded_fenced_example),
3168 None
3169 );
3170 assert!(!looks_like_tool_protocol_envelope(embedded_fenced_example));
3171 assert!(looks_like_tool_protocol_example(embedded_fenced_example));
3172 assert_eq!(
3173 classify_tool_protocol_envelope(embedded_fenced_example_cn),
3174 None
3175 );
3176 assert!(!looks_like_tool_protocol_envelope(
3177 embedded_fenced_example_cn
3178 ));
3179 assert!(looks_like_tool_protocol_example(embedded_fenced_example_cn));
3180 assert_eq!(classify_tool_protocol_envelope(tag_example), None);
3181 assert!(!looks_like_tool_protocol_envelope(tag_example));
3182 assert_eq!(classify_tool_protocol_envelope(tag_example_cn), None);
3183 assert!(!looks_like_tool_protocol_envelope(tag_example_cn));
3184 assert!(looks_like_tool_protocol_example(tag_example_cn));
3185 }
3186
3187 #[test]
3188 fn contains_tool_protocol_tag_call_flags_embedded_tool_call_fences() {
3189 let embedded = r#"Let me call it:
3190```tool_call
3191{"name":"shell","arguments":{"command":"pwd"}}
3192```
3193Done."#;
3194
3195 assert!(contains_tool_protocol_tag_call(embedded));
3196 }
3197
3198 #[test]
3199 fn classify_tool_protocol_envelope_flags_standalone_tool_fences() {
3200 let tool_call_fence = r#"```tool_call
3201{"name":"shell","arguments":{"command":"pwd"}}
3202```"#;
3203 let invoke_fence = r#"```invoke
3204{"name":"shell","arguments":{"command":"pwd"}}
3205```"#;
3206 let tool_name_fence = r#"```tool shell
3207{"command":"pwd"}
3208```"#;
3209
3210 assert_eq!(
3211 classify_tool_protocol_envelope(tool_call_fence),
3212 Some(ToolProtocolEnvelopeKind::TaggedToolCall)
3213 );
3214 assert!(looks_like_tool_protocol_envelope(tool_call_fence));
3215 assert_eq!(
3216 classify_tool_protocol_envelope(invoke_fence),
3217 Some(ToolProtocolEnvelopeKind::TaggedToolCall)
3218 );
3219 assert!(looks_like_tool_protocol_envelope(invoke_fence));
3220 assert_eq!(
3221 classify_tool_protocol_envelope(tool_name_fence),
3222 Some(ToolProtocolEnvelopeKind::TaggedToolCall)
3223 );
3224 assert!(looks_like_tool_protocol_envelope(tool_name_fence));
3225 }
3226
3227 #[test]
3228 fn classify_tool_protocol_envelope_preserves_top_level_arrays_without_protocol_marker() {
3229 assert!(!looks_like_tool_protocol_envelope(
3230 r#"[{"service":"billing","count":2}]"#
3231 ));
3232
3233 assert!(!looks_like_tool_protocol_envelope(
3234 r#"[{"name":"shell","arguments":{}}]"#
3235 ));
3236 }
3237
3238 #[test]
3239 fn classify_tool_protocol_envelope_preserves_top_level_schema_array() {
3240 let schema = r#"[{"name":"planner","parameters":{"goal":"string"}}]"#;
3241
3242 assert_eq!(classify_tool_protocol_envelope(schema), None);
3243 assert!(!looks_like_tool_protocol_envelope(schema));
3244 }
3245
3246 #[test]
3247 fn classify_tool_protocol_envelope_preserves_plain_user_json() {
3248 let profile = r#"{"name":"profile","parameters":{"timezone":"UTC"}}"#;
3249 assert_eq!(classify_tool_protocol_envelope(profile), None);
3250 assert!(!looks_like_tool_protocol_envelope(profile));
3251 }
3252
3253 #[test]
3254 fn looks_like_tool_protocol_envelope_preserves_plain_json_with_similar_keys() {
3255 let config = r#"{"function_call":false,"description":"disable the feature"}"#;
3256 assert!(!looks_like_tool_protocol_envelope(config));
3257
3258 let audit_log = r#"{"tool_calls":[{"service":"billing","count":2}]}"#;
3259 assert!(!looks_like_tool_protocol_envelope(audit_log));
3260
3261 let queued_case =
3262 r#"{"tool_calls":[{"id":"case-1","status":"queued","service":"billing"}]}"#;
3263 assert!(!looks_like_tool_protocol_envelope(queued_case));
3264
3265 let named_record =
3266 r#"{"tool_calls":[{"name":"planner","status":"queued","service":"workflow"}]}"#;
3267 assert!(!looks_like_tool_protocol_envelope(named_record));
3268 }
3269
3270 #[test]
3271 fn parse_tool_calls_handles_whitespace_only_name() {
3272 let value = serde_json::json!({"function": {"name": " ", "arguments": {}}});
3274 let result = parse_tool_call_value(&value);
3275 assert!(result.is_none());
3276 }
3277
3278 #[test]
3279 fn parse_tool_calls_handles_empty_string_arguments() {
3280 let value = serde_json::json!({"name": "test", "arguments": ""});
3282 let result = parse_tool_call_value(&value);
3283 assert!(result.is_some());
3284 assert_eq!(result.unwrap().name, "test");
3285 }
3286
3287 #[test]
3288 fn parse_arguments_value_handles_invalid_json_string() {
3289 let value = serde_json::Value::String("not valid json".to_string());
3291 let result = parse_arguments_value(Some(&value));
3292 assert!(result.is_object());
3293 assert!(result.as_object().unwrap().is_empty());
3294 }
3295
3296 #[test]
3297 fn parse_arguments_value_handles_none() {
3298 let result = parse_arguments_value(None);
3300 assert!(result.is_object());
3301 assert!(result.as_object().unwrap().is_empty());
3302 }
3303
3304 #[test]
3305 fn parse_tool_calls_from_json_value_handles_empty_array() {
3306 let value = serde_json::json!({"tool_calls": []});
3308 let result = parse_tool_calls_from_json_value(&value);
3309 assert!(result.is_empty());
3310 }
3311
3312 #[test]
3313 fn parse_tool_calls_from_json_value_handles_missing_tool_calls() {
3314 let value = serde_json::json!({"name": "test", "arguments": {}});
3316 let result = parse_tool_calls_from_json_value(&value);
3317 assert_eq!(result.len(), 1);
3318 }
3319
3320 #[test]
3321 fn parse_tool_calls_from_json_value_handles_top_level_array() {
3322 let value = serde_json::json!([
3324 {"name": "tool_a", "arguments": {}},
3325 {"name": "tool_b", "arguments": {}}
3326 ]);
3327 let result = parse_tool_calls_from_json_value(&value);
3328 assert_eq!(result.len(), 2);
3329 }
3330
3331 #[test]
3332 fn parse_glm_style_browser_open_url() {
3333 let response = "browser_open/url>https://example.com";
3334 let calls = parse_glm_style_tool_calls(response);
3335 assert_eq!(calls.len(), 1);
3336 assert_eq!(calls[0].0, "shell");
3337 assert!(calls[0].1["command"].as_str().unwrap().contains("curl"));
3338 assert!(
3339 calls[0].1["command"]
3340 .as_str()
3341 .unwrap()
3342 .contains("example.com")
3343 );
3344 }
3345
3346 #[test]
3347 fn parse_glm_style_shell_command() {
3348 let response = "shell/command>ls -la";
3349 let calls = parse_glm_style_tool_calls(response);
3350 assert_eq!(calls.len(), 1);
3351 assert_eq!(calls[0].0, "shell");
3352 assert_eq!(calls[0].1["command"], "ls -la");
3353 }
3354
3355 #[test]
3356 fn parse_glm_style_http_request() {
3357 let response = "http_request/url>https://api.example.com/data";
3358 let calls = parse_glm_style_tool_calls(response);
3359 assert_eq!(calls.len(), 1);
3360 assert_eq!(calls[0].0, "http_request");
3361 assert_eq!(calls[0].1["url"], "https://api.example.com/data");
3362 assert_eq!(calls[0].1["method"], "GET");
3363 }
3364
3365 #[test]
3366 fn parse_glm_style_ignores_plain_url() {
3367 let response = "https://example.com/api";
3370 let calls = parse_glm_style_tool_calls(response);
3371 assert!(
3372 calls.is_empty(),
3373 "plain URL must not be parsed as tool call"
3374 );
3375 }
3376
3377 #[test]
3378 fn parse_glm_style_json_args() {
3379 let response = r#"shell/{"command": "echo hello"}"#;
3380 let calls = parse_glm_style_tool_calls(response);
3381 assert_eq!(calls.len(), 1);
3382 assert_eq!(calls[0].0, "shell");
3383 assert_eq!(calls[0].1["command"], "echo hello");
3384 }
3385
3386 #[test]
3387 fn parse_glm_style_multiple_calls() {
3388 let response = r#"shell/command>ls
3389browser_open/url>https://example.com"#;
3390 let calls = parse_glm_style_tool_calls(response);
3391 assert_eq!(calls.len(), 2);
3392 }
3393
3394 #[test]
3395 fn parse_glm_style_tool_call_integration() {
3396 let response = "Checking...\nbrowser_open/url>https://example.com\nDone";
3398 let (text, calls) = parse_tool_calls(response);
3399 assert_eq!(calls.len(), 1);
3400 assert_eq!(calls[0].name, "shell");
3401 assert!(text.contains("Checking"));
3402 assert!(text.contains("Done"));
3403 }
3404
3405 #[test]
3406 fn parse_glm_style_rejects_non_http_url_param() {
3407 let response = "browser_open/url>javascript:alert(1)";
3408 let calls = parse_glm_style_tool_calls(response);
3409 assert!(calls.is_empty());
3410 }
3411
3412 #[test]
3413 fn parse_tool_calls_handles_unclosed_tool_call_tag() {
3414 let response = "<tool_call>{\"name\":\"shell\",\"arguments\":{\"command\":\"pwd\"}}\nDone";
3415 let (text, calls) = parse_tool_calls(response);
3416 assert_eq!(calls.len(), 1);
3417 assert_eq!(calls[0].name, "shell");
3418 assert_eq!(calls[0].arguments["command"], "pwd");
3419 assert_eq!(text, "Done");
3420 }
3421
3422 #[test]
3423 fn parse_tool_calls_empty_input_returns_empty() {
3424 let (text, calls) = parse_tool_calls("");
3425 assert!(calls.is_empty(), "empty input should produce no tool calls");
3426 assert!(text.is_empty(), "empty input should produce no text");
3427 }
3428
3429 #[test]
3430 fn parse_tool_calls_whitespace_only_returns_empty_calls() {
3431 let (text, calls) = parse_tool_calls(" \n\t ");
3432 assert!(calls.is_empty());
3433 assert!(text.is_empty() || text.trim().is_empty());
3434 }
3435
3436 #[test]
3437 fn parse_tool_calls_nested_xml_tags_handled() {
3438 let response = r#"<tool_call><tool_call>{"name":"echo","arguments":{"msg":"hi"}}</tool_call></tool_call>"#;
3440 let (_text, calls) = parse_tool_calls(response);
3441 assert!(
3443 !calls.is_empty(),
3444 "nested XML tags should still yield at least one tool call"
3445 );
3446 }
3447
3448 #[test]
3449 fn parse_tool_calls_truncated_json_no_panic() {
3450 let response = r#"<tool_call>{"name":"shell","arguments":{"command":"ls"</tool_call>"#;
3452 let (_text, _calls) = parse_tool_calls(response);
3453 }
3455
3456 #[test]
3457 fn parse_tool_calls_empty_json_object_in_tag() {
3458 let response = "<tool_call>{}</tool_call>";
3459 let (_text, calls) = parse_tool_calls(response);
3460 assert!(
3462 calls.is_empty(),
3463 "empty JSON object should not produce a tool call"
3464 );
3465 }
3466
3467 #[test]
3468 fn parse_tool_calls_closing_tag_only_returns_text() {
3469 let response = "Some text </tool_call> more text";
3470 let (text, calls) = parse_tool_calls(response);
3471 assert!(
3472 calls.is_empty(),
3473 "closing tag only should not produce calls"
3474 );
3475 assert!(
3476 !text.is_empty(),
3477 "text around orphaned closing tag should be preserved"
3478 );
3479 }
3480
3481 #[test]
3482 fn parse_tool_calls_very_large_arguments_no_panic() {
3483 let large_arg = "x".repeat(100_000);
3484 let response = format!(
3485 r#"<tool_call>{{"name":"echo","arguments":{{"message":"{}"}}}}</tool_call>"#,
3486 large_arg
3487 );
3488 let (_text, calls) = parse_tool_calls(&response);
3489 assert_eq!(calls.len(), 1, "large arguments should still parse");
3490 assert_eq!(calls[0].name, "echo");
3491 }
3492
3493 #[test]
3494 fn parse_tool_calls_special_characters_in_arguments() {
3495 let response = r#"<tool_call>{"name":"echo","arguments":{"message":"hello \"world\" <>&'\n\t"}}</tool_call>"#;
3496 let (_text, calls) = parse_tool_calls(response);
3497 assert_eq!(calls.len(), 1);
3498 assert_eq!(calls[0].name, "echo");
3499 }
3500
3501 #[test]
3502 fn parse_tool_calls_text_with_embedded_json_not_extracted() {
3503 let response = r#"Here is some data: {"name":"echo","arguments":{"message":"hi"}} end."#;
3505 let (_text, calls) = parse_tool_calls(response);
3506 assert!(
3507 calls.is_empty(),
3508 "raw JSON in text without tags should not be extracted"
3509 );
3510 }
3511
3512 #[test]
3513 fn parse_tool_calls_multiple_formats_mixed() {
3514 let response = r#"I'll help you with that.
3516
3517<tool_call>
3518{"name":"shell","arguments":{"command":"echo hello"}}
3519</tool_call>
3520
3521Let me check the result."#;
3522 let (text, calls) = parse_tool_calls(response);
3523 assert_eq!(
3524 calls.len(),
3525 1,
3526 "should extract one tool call from mixed content"
3527 );
3528 assert_eq!(calls[0].name, "shell");
3529 assert!(
3530 text.contains("help you"),
3531 "text before tool call should be preserved"
3532 );
3533 }
3534
3535 #[test]
3536 fn parse_tool_calls_cross_alias_close_tag_with_json() {
3537 let input = r#"<tool_call>{"name": "shell", "arguments": {"command": "ls"}}</invoke>"#;
3539 let (text, calls) = parse_tool_calls(input);
3540 assert_eq!(calls.len(), 1);
3541 assert_eq!(calls[0].name, "shell");
3542 assert_eq!(calls[0].arguments["command"], "ls");
3543 assert!(text.is_empty());
3544 }
3545
3546 #[test]
3547 fn parse_tool_calls_cross_alias_close_tag_with_glm_shortened() {
3548 let input = "<tool_call>shell>uname -a</invoke>";
3550 let (text, calls) = parse_tool_calls(input);
3551 assert_eq!(calls.len(), 1);
3552 assert_eq!(calls[0].name, "shell");
3553 assert_eq!(calls[0].arguments["command"], "uname -a");
3554 assert!(text.is_empty());
3555 }
3556
3557 #[test]
3558 fn parse_tool_calls_glm_shortened_body_in_matched_tags() {
3559 let input = "<tool_call>shell>pwd</tool_call>";
3561 let (text, calls) = parse_tool_calls(input);
3562 assert_eq!(calls.len(), 1);
3563 assert_eq!(calls[0].name, "shell");
3564 assert_eq!(calls[0].arguments["command"], "pwd");
3565 assert!(text.is_empty());
3566 }
3567
3568 #[test]
3569 fn parse_tool_calls_glm_yaml_style_in_tags() {
3570 let input = "<tool_call>shell>\ncommand: date\napproved: true</invoke>";
3572 let (text, calls) = parse_tool_calls(input);
3573 assert_eq!(calls.len(), 1);
3574 assert_eq!(calls[0].name, "shell");
3575 assert_eq!(calls[0].arguments["command"], "date");
3576 assert_eq!(calls[0].arguments["approved"], true);
3577 assert!(text.is_empty());
3578 }
3579
3580 #[test]
3581 fn parse_tool_calls_attribute_style_in_tags() {
3582 let input = r#"<tool_call>shell command="date" /></tool_call>"#;
3584 let (text, calls) = parse_tool_calls(input);
3585 assert_eq!(calls.len(), 1);
3586 assert_eq!(calls[0].name, "shell");
3587 assert_eq!(calls[0].arguments["command"], "date");
3588 assert!(text.is_empty());
3589 }
3590
3591 #[test]
3592 fn parse_tool_calls_file_read_shortened_in_cross_alias() {
3593 let input = r#"<tool_call>file_read path=".env" /></invoke>"#;
3595 let (text, calls) = parse_tool_calls(input);
3596 assert_eq!(calls.len(), 1);
3597 assert_eq!(calls[0].name, "file_read");
3598 assert_eq!(calls[0].arguments["path"], ".env");
3599 assert!(text.is_empty());
3600 }
3601
3602 #[test]
3603 fn parse_tool_calls_unclosed_glm_shortened_no_close_tag() {
3604 let input = "<tool_call>shell>ls -la";
3606 let (text, calls) = parse_tool_calls(input);
3607 assert_eq!(calls.len(), 1);
3608 assert_eq!(calls[0].name, "shell");
3609 assert_eq!(calls[0].arguments["command"], "ls -la");
3610 assert!(text.is_empty());
3611 }
3612
3613 #[test]
3614 fn parse_tool_calls_text_before_cross_alias() {
3615 let input = "Let me check that.\n<tool_call>shell>uname -a</invoke>\nDone.";
3617 let (text, calls) = parse_tool_calls(input);
3618 assert_eq!(calls.len(), 1);
3619 assert_eq!(calls[0].name, "shell");
3620 assert_eq!(calls[0].arguments["command"], "uname -a");
3621 assert!(text.contains("Let me check that."));
3622 assert!(text.contains("Done."));
3623 }
3624
3625 #[test]
3626 fn parse_glm_shortened_body_url_to_curl() {
3627 let call = parse_glm_shortened_body("shell>https://example.com/api").unwrap();
3629 assert_eq!(call.name, "shell");
3630 let cmd = call.arguments["command"].as_str().unwrap();
3631 assert!(cmd.contains("curl"));
3632 assert!(cmd.contains("example.com"));
3633 }
3634
3635 #[test]
3636 fn parse_glm_shortened_body_browser_open_maps_to_shell_command() {
3637 let call = parse_glm_shortened_body("browser_open>https://example.com").unwrap();
3640 assert_eq!(call.name, "shell");
3641 let cmd = call.arguments["command"].as_str().unwrap();
3642 assert!(cmd.contains("curl"));
3643 assert!(cmd.contains("example.com"));
3644 }
3645
3646 #[test]
3647 fn parse_glm_shortened_body_memory_recall() {
3648 let call = parse_glm_shortened_body("memory_recall>recent meetings").unwrap();
3650 assert_eq!(call.name, "memory_recall");
3651 assert_eq!(call.arguments["query"], "recent meetings");
3652 }
3653
3654 #[test]
3655 fn parse_glm_shortened_body_function_style_alias_maps_to_message_send() {
3656 let call =
3657 parse_glm_shortened_body(r#"sendmessage(channel="alerts", message="hi")"#).unwrap();
3658 assert_eq!(call.name, "message_send");
3659 assert_eq!(call.arguments["channel"], "alerts");
3660 assert_eq!(call.arguments["message"], "hi");
3661 }
3662
3663 #[test]
3664 fn parse_glm_shortened_body_rejects_empty() {
3665 assert!(parse_glm_shortened_body("").is_none());
3666 assert!(parse_glm_shortened_body(" ").is_none());
3667 }
3668
3669 #[test]
3670 fn parse_glm_shortened_body_rejects_invalid_tool_name() {
3671 assert!(parse_glm_shortened_body("not-a-tool>value").is_none());
3673 assert!(parse_glm_shortened_body("tool name>value").is_none());
3674 }
3675
3676 #[test]
3677 fn build_native_assistant_history_from_parsed_calls_includes_reasoning_content() {
3678 let calls = vec![ParsedToolCall {
3679 name: "shell".into(),
3680 arguments: serde_json::json!({"command": "pwd"}),
3681 tool_call_id: Some("call_2".into()),
3682 }];
3683 let result = build_native_assistant_history_from_parsed_calls(
3684 "answer",
3685 &calls,
3686 Some("deep thought"),
3687 );
3688 assert!(result.is_some());
3689 let parsed: serde_json::Value = serde_json::from_str(result.as_deref().unwrap()).unwrap();
3690 assert_eq!(parsed["content"].as_str(), Some("answer"));
3691 assert_eq!(parsed["reasoning_content"].as_str(), Some("deep thought"));
3692 assert!(parsed["tool_calls"].is_array());
3693 }
3694
3695 #[test]
3696 fn build_native_assistant_history_from_parsed_calls_omits_reasoning_content_when_none() {
3697 let calls = vec![ParsedToolCall {
3698 name: "shell".into(),
3699 arguments: serde_json::json!({"command": "pwd"}),
3700 tool_call_id: Some("call_2".into()),
3701 }];
3702 let result = build_native_assistant_history_from_parsed_calls("answer", &calls, None);
3703 assert!(result.is_some());
3704 let parsed: serde_json::Value = serde_json::from_str(result.as_deref().unwrap()).unwrap();
3705 assert_eq!(parsed["content"].as_str(), Some("answer"));
3706 assert!(parsed.get("reasoning_content").is_none());
3707 }
3708
3709 #[test]
3717 fn parse_tool_call_value_handles_missing_name_field() {
3718 let value = serde_json::json!({"function": {"arguments": {}}});
3719 let result = parse_tool_call_value(&value);
3720 assert!(result.is_none());
3721 }
3722
3723 #[test]
3724 fn parse_tool_call_value_handles_top_level_name() {
3725 let value = serde_json::json!({"name": "test_tool", "arguments": {}});
3726 let result = parse_tool_call_value(&value);
3727 assert!(result.is_some());
3728 assert_eq!(result.unwrap().name, "test_tool");
3729 }
3730
3731 #[test]
3732 fn parse_tool_call_value_accepts_top_level_parameters_alias() {
3733 let value = serde_json::json!({
3734 "name": "schedule",
3735 "parameters": {"action": "create", "message": "test"}
3736 });
3737 let result = parse_tool_call_value(&value).expect("tool call should parse");
3738 assert_eq!(result.name, "schedule");
3739 assert_eq!(
3740 result.arguments.get("action").and_then(|v| v.as_str()),
3741 Some("create")
3742 );
3743 }
3744
3745 #[test]
3746 fn parse_tool_call_value_accepts_function_parameters_alias() {
3747 let value = serde_json::json!({
3748 "function": {
3749 "name": "shell",
3750 "parameters": {"command": "date"}
3751 }
3752 });
3753 let result = parse_tool_call_value(&value).expect("tool call should parse");
3754 assert_eq!(result.name, "shell");
3755 assert_eq!(
3756 result.arguments.get("command").and_then(|v| v.as_str()),
3757 Some("date")
3758 );
3759 }
3760
3761 #[test]
3762 fn parse_tool_call_value_preserves_tool_call_id_aliases() {
3763 let value = serde_json::json!({
3764 "call_id": "legacy_1",
3765 "function": {
3766 "name": "shell",
3767 "arguments": {"command": "date"}
3768 }
3769 });
3770 let result = parse_tool_call_value(&value).expect("tool call should parse");
3771 assert_eq!(result.tool_call_id.as_deref(), Some("legacy_1"));
3772 }
3773
3774 #[test]
3775 fn extract_json_values_handles_empty_string() {
3776 let result = extract_json_values("");
3777 assert!(result.is_empty());
3778 }
3779
3780 #[test]
3781 fn extract_json_values_handles_whitespace_only() {
3782 let result = extract_json_values(
3783 "
3784 ",
3785 );
3786 assert!(result.is_empty());
3787 }
3788
3789 #[test]
3790 fn extract_json_values_handles_multiple_objects() {
3791 let input = r#"{"a": 1}{"b": 2}{"c": 3}"#;
3792 let result = extract_json_values(input);
3793 assert_eq!(result.len(), 3);
3794 }
3795
3796 #[test]
3797 fn extract_json_values_handles_arrays() {
3798 let input = r#"[1, 2, 3]{"key": "value"}"#;
3799 let result = extract_json_values(input);
3800 assert_eq!(result.len(), 2);
3801 }
3802
3803 #[test]
3804 fn map_tool_name_alias_direct_coverage() {
3805 assert_eq!(map_tool_name_alias("bash"), "shell");
3806 assert_eq!(map_tool_name_alias("filelist"), "file_list");
3807 assert_eq!(map_tool_name_alias("memorystore"), "memory_store");
3808 assert_eq!(map_tool_name_alias("memoryforget"), "memory_forget");
3809 assert_eq!(map_tool_name_alias("http"), "http_request");
3810 assert_eq!(
3811 map_tool_name_alias("totally_unknown_tool"),
3812 "totally_unknown_tool"
3813 );
3814 }
3815
3816 #[test]
3817 fn map_tool_name_alias_strips_dotted_namespaces() {
3818 assert_eq!(map_tool_name_alias("default_api.file_read"), "file_read");
3820 assert_eq!(map_tool_name_alias("tools.shell"), "shell");
3821
3822 assert_eq!(
3826 map_tool_name_alias("google_workspace.search_gmail_messages"),
3827 "search_gmail_messages"
3828 );
3829
3830 assert_eq!(map_tool_name_alias("a.b.c.final"), "final");
3832
3833 assert_eq!(map_tool_name_alias("default_api.bash"), "shell");
3835
3836 assert_eq!(map_tool_name_alias("file_read"), "file_read");
3838 }
3839
3840 #[test]
3841 fn default_param_for_tool_coverage() {
3842 assert_eq!(default_param_for_tool("shell"), "command");
3843 assert_eq!(default_param_for_tool("bash"), "command");
3844 assert_eq!(default_param_for_tool("file_read"), "path");
3845 assert_eq!(default_param_for_tool("memory_recall"), "query");
3846 assert_eq!(default_param_for_tool("memory_store"), "content");
3847 assert_eq!(default_param_for_tool("web_search_tool"), "query");
3848 assert_eq!(default_param_for_tool("web_search"), "query");
3849 assert_eq!(default_param_for_tool("search"), "query");
3850 assert_eq!(default_param_for_tool("http_request"), "url");
3851 assert_eq!(default_param_for_tool("browser_open"), "url");
3852 assert_eq!(default_param_for_tool("unknown_tool"), "input");
3853 }
3854}