1use serde_json::Value;
55
56use super::super::error::NormalizationError;
57use super::super::parser::{ParserOutput, ToolCallParser};
58use crate::domain::agent::ToolCall;
59use crate::domain::dialect::{BodyCodec, DialectSpec};
60
61#[derive(Default, Debug)]
64struct ChannelState {
65 pending: String,
67 inside: bool,
69 body: String,
71}
72
73#[derive(Copy, Clone)]
75enum Channel {
76 Text,
77 Reasoning,
78}
79
80#[derive(Debug)]
83pub(crate) struct DelimitedToolCallParser {
84 spec: DialectSpec,
86 text: ChannelState,
87 reasoning: ChannelState,
88 next_id: u32,
91}
92
93impl DelimitedToolCallParser {
94 #[must_use]
96 pub(crate) fn new(spec: DialectSpec) -> Self {
97 Self {
98 spec,
99 text: ChannelState::default(),
100 reasoning: ChannelState::default(),
101 next_id: 0,
102 }
103 }
104
105 fn mint_id(&mut self) -> String {
107 let n = self.next_id;
108 self.next_id = self.next_id.saturating_add(1);
109 format!("{}{n}", self.spec.id_prefix)
110 }
111
112 fn scan(&mut self, channel: Channel, chunk: &str) -> ParserOutput {
118 let mut out = ParserOutput::default();
119
120 let open = self.spec.tool_open.clone();
124 let close = self.spec.tool_close.clone();
125 let codecs = self.spec.body_codecs.clone();
126
127 let mut state = match channel {
131 Channel::Text => std::mem::take(&mut self.text),
132 Channel::Reasoning => std::mem::take(&mut self.reasoning),
133 };
134
135 state.pending.push_str(chunk);
136
137 loop {
138 if state.inside {
139 if let Some(p) = state.pending.find(&*close) {
140 state.body.push_str(&state.pending[..p]);
141 finalize_tool_call(&codecs, &state.body, &mut out, || self.mint_id());
142 state.body.clear();
143 state.inside = false;
144 state.pending.drain(..p + close.len());
145 continue;
146 }
147 let keep = partial_suffix_len(state.pending.as_bytes(), close.as_bytes());
148 let flush_to = state.pending.len() - keep;
149 state.body.push_str(&state.pending[..flush_to]);
150 state.pending.drain(..flush_to);
151 break;
152 }
153
154 if let Some(p) = state.pending.find(&*open) {
156 forward(&mut out, channel, &state.pending[..p]);
157 state.pending.drain(..p + open.len());
158 state.inside = true;
159 continue;
160 }
161 let keep = partial_suffix_len(state.pending.as_bytes(), open.as_bytes());
162 let flush_to = state.pending.len() - keep;
163 forward(&mut out, channel, &state.pending[..flush_to]);
164 state.pending.drain(..flush_to);
165 break;
166 }
167
168 match channel {
169 Channel::Text => self.text = state,
170 Channel::Reasoning => self.reasoning = state,
171 }
172 out
173 }
174
175 fn flush_channel(&mut self, channel: Channel) -> ParserOutput {
177 let mut out = ParserOutput::default();
178 let state = match channel {
179 Channel::Text => std::mem::take(&mut self.text),
180 Channel::Reasoning => std::mem::take(&mut self.reasoning),
181 };
182 if state.inside {
183 let mut partial = state.body;
187 partial.push_str(&state.pending);
188 out.errors
189 .push(NormalizationError::unclosed_tool_call(partial));
190 } else {
191 forward(&mut out, channel, &state.pending);
193 }
194 out
195 }
196}
197
198impl ToolCallParser for DelimitedToolCallParser {
199 fn push_text(&mut self, chunk: &str) -> ParserOutput {
200 self.scan(Channel::Text, chunk)
201 }
202
203 fn push_reasoning(&mut self, chunk: &str) -> ParserOutput {
204 self.scan(Channel::Reasoning, chunk)
205 }
206
207 fn finish(&mut self) -> ParserOutput {
208 let mut a = self.flush_channel(Channel::Text);
209 let b = self.flush_channel(Channel::Reasoning);
210 a.forward_text.push_str(&b.forward_text);
211 a.forward_reasoning.push_str(&b.forward_reasoning);
212 a.tool_calls.extend(b.tool_calls);
213 a.errors.extend(b.errors);
214 a
215 }
216}
217
218fn forward(out: &mut ParserOutput, channel: Channel, bytes: &str) {
224 if bytes.is_empty() {
225 return;
226 }
227 match channel {
228 Channel::Text => out.forward_text.push_str(bytes),
229 Channel::Reasoning => out.forward_reasoning.push_str(bytes),
230 }
231}
232
233fn finalize_tool_call(
251 codecs: &[BodyCodec],
252 body: &str,
253 out: &mut ParserOutput,
254 mut mint_id: impl FnMut() -> String,
255) {
256 let trimmed = body.trim();
257 for codec in codecs {
258 match codec {
259 BodyCodec::Json => {
260 if let Some(call) = parse_json_body(trimmed, &mut mint_id) {
261 out.tool_calls.push(call);
262 return;
263 }
264 }
265 BodyCodec::FunctionXml => {
266 if let Some(calls) = parse_function_xml_body(trimmed, &mut mint_id) {
267 out.tool_calls.extend(calls);
268 return;
269 }
270 }
271 }
272 }
273 if codecs.contains(&BodyCodec::Json)
280 && let Some(repaired) = crate::normalize::coerce::coerce_json_object(trimmed)
281 && let Some(call) = parse_json_body(&repaired, &mut mint_id)
282 {
283 out.tool_calls.push(call);
284 return;
285 }
286
287 let error = if codecs.contains(&BodyCodec::FunctionXml) && trimmed.starts_with("<function=") {
288 NormalizationError::malformed_function_xml(body.to_owned())
289 } else {
290 NormalizationError::malformed_tool_call(body.to_owned())
291 };
292 out.errors.push(error);
293}
294
295fn parse_json_body(body: &str, mint_id: &mut impl FnMut() -> String) -> Option<ToolCall> {
297 let parsed: Value = serde_json::from_str(body).ok()?;
298 let obj = parsed.as_object()?;
299 let name = obj.get("name").and_then(Value::as_str)?.to_owned();
300 let arguments = obj
301 .get("arguments")
302 .cloned()
303 .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
304 Some(ToolCall {
305 id: mint_id(),
306 name,
307 arguments,
308 })
309}
310
311fn parse_function_xml_body(
328 body: &str,
329 mint_id: &mut impl FnMut() -> String,
330) -> Option<Vec<ToolCall>> {
331 let mut calls = Vec::new();
332 let mut cursor = body.trim();
333
334 while !cursor.is_empty() {
335 let after_open = cursor.strip_prefix("<function=")?;
336 let name_end = after_open.find('>')?;
337 let name = after_open[..name_end].trim();
338 if name.is_empty() {
339 return None;
340 }
341 let after_name = &after_open[name_end + 1..];
342
343 let close_at = find_own_close(after_name, "</function>", "<function=")?;
350 let inner = after_name[..close_at].trim();
351 let after_function = &after_name[close_at + "</function>".len()..];
352
353 let mut args = serde_json::Map::new();
354 let mut param_cursor = inner;
355 while !param_cursor.is_empty() {
356 param_cursor = param_cursor.trim_start();
357 if param_cursor.is_empty() {
358 break;
359 }
360 let after_param = param_cursor.strip_prefix("<parameter=")?;
361 let key_end = after_param.find('>')?;
362 let key = after_param[..key_end].trim().to_owned();
363 if key.is_empty() {
364 return None;
365 }
366 let rest = &after_param[key_end + 1..];
367 let close_at = find_own_close(rest, "</parameter>", "<parameter=")?;
368 let raw_value = rest[..close_at].trim();
369 args.insert(key, parse_param_value(raw_value));
370 param_cursor = &rest[close_at + "</parameter>".len()..];
371 }
372
373 calls.push(ToolCall {
374 id: mint_id(),
375 name: name.to_owned(),
376 arguments: Value::Object(args),
377 });
378
379 cursor = after_function.trim_start();
380 }
381
382 (!calls.is_empty()).then_some(calls)
383}
384
385fn find_own_close(rest: &str, close: &str, next_open: &str) -> Option<usize> {
400 let boundary = rest.find(next_open).unwrap_or(rest.len());
401 rest[..boundary].rfind(close)
402}
403
404fn parse_param_value(raw: &str) -> Value {
414 if raw.is_empty() {
415 return Value::String(String::new());
416 }
417 if let Ok(v) = serde_json::from_str::<Value>(raw) {
418 return v;
419 }
420 Value::String(raw.to_owned())
421}
422
423fn partial_suffix_len(buf: &[u8], marker: &[u8]) -> usize {
427 if marker.len() < 2 {
428 return 0;
429 }
430 let max = std::cmp::min(buf.len(), marker.len() - 1);
431 for n in (1..=max).rev() {
432 if buf.ends_with(&marker[..n]) {
433 return n;
434 }
435 }
436 0
437}
438
439#[cfg(test)]
444mod tests {
445 use super::*;
446 use crate::normalize::NormalizationErrorKind;
447 use serde_json::json;
448
449 fn qwen() -> DelimitedToolCallParser {
452 DelimitedToolCallParser::new(DialectSpec::qwen_xml())
453 }
454
455 fn collect(p: &mut DelimitedToolCallParser, chunks: &[&str]) -> ParserOutput {
456 let mut total = ParserOutput::default();
457 for c in chunks {
458 let o = p.push_text(c);
459 total.forward_text.push_str(&o.forward_text);
460 total.forward_reasoning.push_str(&o.forward_reasoning);
461 total.tool_calls.extend(o.tool_calls);
462 total.errors.extend(o.errors);
463 }
464 let f = p.finish();
465 total.forward_text.push_str(&f.forward_text);
466 total.forward_reasoning.push_str(&f.forward_reasoning);
467 total.tool_calls.extend(f.tool_calls);
468 total.errors.extend(f.errors);
469 total
470 }
471
472 #[test]
475 fn a_fenced_tool_call_body_is_repaired_instead_of_discarded() {
476 let mut p = qwen();
477 let out = collect(
478 &mut p,
479 &[
480 "<tool_call>\n```json\n",
481 r#"{"name":"read_file","arguments":{"path":"a"}}"#,
482 "\n```\n</tool_call>",
483 ],
484 );
485 assert!(
486 out.errors.is_empty(),
487 "the turn should no longer be given up: {:?}",
488 out.errors
489 );
490 assert_eq!(out.tool_calls.len(), 1);
491 assert_eq!(out.tool_calls[0].name, "read_file");
492 assert_eq!(out.tool_calls[0].arguments, json!({"path": "a"}));
493 }
494
495 #[test]
498 fn an_unrepairable_body_still_reports_the_original_error() {
499 let mut p = qwen();
500 let out = collect(&mut p, &["<tool_call>", "not json at all", "</tool_call>"]);
501 assert!(out.tool_calls.is_empty());
502 assert_eq!(out.errors.len(), 1);
503 assert!(matches!(
504 out.errors[0].kind,
505 NormalizationErrorKind::MalformedToolCallJson { .. }
506 ));
507 }
508
509 #[test]
512 fn a_call_truncated_mid_string_is_not_invented() {
513 let mut p = qwen();
514 let out = collect(
515 &mut p,
516 &[
517 "<tool_call>",
518 r#"{"name":"read_file","arguments":{"path":"/etc/ho"#,
519 "</tool_call>",
520 ],
521 );
522 assert!(
523 out.tool_calls.is_empty(),
524 "a truncated argument must never become a dispatched call"
525 );
526 assert_eq!(out.errors.len(), 1);
527 }
528
529 #[test]
530 fn passthrough_with_no_markup() {
531 let mut p = qwen();
532 let out = collect(&mut p, &["hello ", "world"]);
533 assert_eq!(out.forward_text, "hello world");
534 assert!(out.tool_calls.is_empty());
535 assert!(out.errors.is_empty());
536 }
537
538 #[test]
539 fn extracts_simple_tool_call_from_text() {
540 let mut p = qwen();
541 let out = collect(
542 &mut p,
543 &[r#"before<tool_call>{"name":"foo","arguments":{"x":1}}</tool_call>after"#],
544 );
545 assert_eq!(out.forward_text, "beforeafter");
546 assert_eq!(out.tool_calls.len(), 1);
547 assert_eq!(out.tool_calls[0].id, "call_qwen_0");
548 assert_eq!(out.tool_calls[0].name, "foo");
549 assert_eq!(out.tool_calls[0].arguments, json!({"x": 1}));
550 assert!(out.errors.is_empty());
551 }
552
553 #[test]
554 fn open_tag_straddles_chunk_boundary() {
555 let mut p = qwen();
556 let out = collect(
557 &mut p,
558 &[
559 "before<tool",
560 "_call>",
561 r#"{"name":"foo","arguments":{}}"#,
562 "</tool_call>",
563 "after",
564 ],
565 );
566 assert_eq!(out.forward_text, "beforeafter");
567 assert_eq!(out.tool_calls.len(), 1);
568 assert_eq!(out.tool_calls[0].name, "foo");
569 }
570
571 #[test]
572 fn close_tag_straddles_chunk_boundary() {
573 let mut p = qwen();
574 let out = collect(
575 &mut p,
576 &[
577 "<tool_call>",
578 r#"{"name":"foo","arguments":{}}</tool"#,
579 "_call>tail",
580 ],
581 );
582 assert_eq!(out.forward_text, "tail");
583 assert_eq!(out.tool_calls.len(), 1);
584 assert_eq!(out.tool_calls[0].name, "foo");
585 }
586
587 #[test]
588 fn one_byte_at_a_time_still_works() {
589 let mut p = qwen();
590 let s = r#"x<tool_call>{"name":"f","arguments":{"a":2}}</tool_call>y"#;
591 let chunks: Vec<String> = s.chars().map(|c| c.to_string()).collect();
592 let refs: Vec<&str> = chunks.iter().map(String::as_str).collect();
593 let out = collect(&mut p, &refs);
594 assert_eq!(out.forward_text, "xy");
595 assert_eq!(out.tool_calls.len(), 1);
596 assert_eq!(out.tool_calls[0].arguments, json!({"a": 2}));
597 }
598
599 #[test]
600 fn tool_call_in_reasoning_channel_is_extracted() {
601 let mut p = qwen();
602 let chunk = r#"thinking <tool_call>{"name":"foo","arguments":{}}</tool_call> done"#;
603 let out = p.push_reasoning(chunk);
604 let f = p.finish();
605 assert_eq!(out.forward_reasoning, "thinking done");
606 assert_eq!(out.tool_calls.len(), 1);
607 assert_eq!(out.tool_calls[0].name, "foo");
608 assert!(f.is_empty());
609 }
610
611 #[test]
612 fn malformed_json_emits_error() {
613 let mut p = qwen();
614 let out = collect(&mut p, &["<tool_call>not json</tool_call>"]);
615 assert!(out.tool_calls.is_empty());
616 assert_eq!(out.errors.len(), 1);
617 assert!(matches!(
618 out.errors[0].kind,
619 crate::normalize::error::NormalizationErrorKind::MalformedToolCallJson { .. }
620 ));
621 }
622
623 #[test]
624 fn missing_name_field_is_malformed() {
625 let mut p = qwen();
626 let out = collect(&mut p, &[r#"<tool_call>{"arguments":{}}</tool_call>"#]);
627 assert!(out.tool_calls.is_empty());
628 assert_eq!(out.errors.len(), 1);
629 }
630
631 #[test]
632 fn missing_arguments_defaults_to_empty_object() {
633 let mut p = qwen();
634 let out = collect(&mut p, &[r#"<tool_call>{"name":"foo"}</tool_call>"#]);
635 assert_eq!(out.tool_calls.len(), 1);
636 assert_eq!(out.tool_calls[0].arguments, json!({}));
637 assert!(out.errors.is_empty());
638 }
639
640 #[test]
641 fn unclosed_tag_at_end_yields_error() {
642 let mut p = qwen();
643 let _ = p.push_text(r#"hello <tool_call>{"name":"foo""#);
644 let f = p.finish();
645 assert_eq!(f.errors.len(), 1);
646 assert!(matches!(
647 f.errors[0].kind,
648 crate::normalize::error::NormalizationErrorKind::UnclosedToolCallTag { .. }
649 ));
650 assert!(f.tool_calls.is_empty());
651 }
652
653 #[test]
654 fn multiple_tool_calls_get_distinct_ids() {
655 let mut p = qwen();
656 let out = collect(
657 &mut p,
658 &[
659 r#"<tool_call>{"name":"a","arguments":{}}</tool_call>"#,
660 r#"<tool_call>{"name":"b","arguments":{}}</tool_call>"#,
661 ],
662 );
663 assert_eq!(out.tool_calls.len(), 2);
664 assert_eq!(out.tool_calls[0].id, "call_qwen_0");
665 assert_eq!(out.tool_calls[1].id, "call_qwen_1");
666 }
667
668 #[test]
669 fn partial_marker_lookalike_is_eventually_flushed() {
670 let mut p = qwen();
673 let mid = p.push_text("<tool");
674 assert_eq!(mid.forward_text, "");
675 let f = p.finish();
676 assert_eq!(f.forward_text, "<tool");
677 }
678
679 #[test]
680 fn partial_suffix_len_finds_longest_overlap() {
681 assert_eq!(partial_suffix_len(b"abc<tool", b"<tool_call>"), 5);
682 assert_eq!(partial_suffix_len(b"abc<", b"<tool_call>"), 1);
683 assert_eq!(partial_suffix_len(b"abc", b"<tool_call>"), 0);
684 assert_eq!(partial_suffix_len(b"<tool_call>", b"<tool_call>"), 0);
687 assert_eq!(partial_suffix_len(b"</tool_call><", b"<tool_call>"), 1);
689 }
690
691 #[test]
697 fn extracts_function_xml_body_with_string_param() {
698 let mut p = qwen();
699 let body = "<tool_call>\n<function=grep>\n<parameter=regex>\ngglib\\s+q\n</parameter>\n</function>\n</tool_call>";
700 let out = collect(&mut p, &[body]);
701 assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
702 assert_eq!(out.tool_calls.len(), 1);
703 assert_eq!(out.tool_calls[0].name, "grep");
704 assert_eq!(
705 out.tool_calls[0].arguments,
706 json!({ "regex": "gglib\\s+q" })
707 );
708 }
709
710 #[test]
711 fn function_xml_body_with_multiple_params() {
712 let mut p = qwen();
713 let body = concat!(
714 "<tool_call><function=read_file>",
715 "<parameter=path>src/main.rs</parameter>",
716 "<parameter=start_line>1</parameter>",
717 "<parameter=end_line>20</parameter>",
718 "</function></tool_call>",
719 );
720 let out = collect(&mut p, &[body]);
721 assert!(out.errors.is_empty());
722 assert_eq!(out.tool_calls.len(), 1);
723 assert_eq!(out.tool_calls[0].name, "read_file");
724 assert_eq!(
725 out.tool_calls[0].arguments,
726 json!({ "path": "src/main.rs", "start_line": 1, "end_line": 20 })
727 );
728 }
729
730 #[test]
731 fn function_xml_body_with_json_object_param() {
732 let mut p = qwen();
733 let body = r#"<tool_call><function=run><parameter=opts>{"a":1,"b":[2,3]}</parameter></function></tool_call>"#;
734 let out = collect(&mut p, &[body]);
735 assert!(out.errors.is_empty());
736 assert_eq!(out.tool_calls.len(), 1);
737 assert_eq!(
738 out.tool_calls[0].arguments,
739 json!({ "opts": { "a": 1, "b": [2, 3] } })
740 );
741 }
742
743 #[test]
744 fn function_xml_body_streamed_byte_by_byte() {
745 let mut p = qwen();
746 let s = "<tool_call><function=grep><parameter=regex>x</parameter></function></tool_call>";
747 let chunks: Vec<String> = s.chars().map(|c| c.to_string()).collect();
748 let refs: Vec<&str> = chunks.iter().map(String::as_str).collect();
749 let out = collect(&mut p, &refs);
750 assert!(out.errors.is_empty());
751 assert_eq!(out.tool_calls.len(), 1);
752 assert_eq!(out.tool_calls[0].name, "grep");
753 assert_eq!(out.tool_calls[0].arguments, json!({ "regex": "x" }));
754 }
755
756 #[test]
757 fn function_xml_body_without_parameters_yields_empty_args() {
758 let mut p = qwen();
759 let body = "<tool_call><function=ping></function></tool_call>";
760 let out = collect(&mut p, &[body]);
761 assert!(out.errors.is_empty());
762 assert_eq!(out.tool_calls.len(), 1);
763 assert_eq!(out.tool_calls[0].name, "ping");
764 assert_eq!(out.tool_calls[0].arguments, json!({}));
765 }
766
767 #[test]
771 fn multiple_function_blocks_in_one_wrapper_are_all_extracted() {
772 let mut p = qwen();
773 let body = concat!(
774 "<tool_call>",
775 "<function=get_weather><parameter=city>Paris</parameter></function>",
776 "<function=get_time><parameter=zone>UTC</parameter></function>",
777 "</tool_call>",
778 );
779 let out = collect(&mut p, &[body]);
780 assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
781 assert_eq!(out.tool_calls.len(), 2);
782 assert_eq!(out.tool_calls[0].name, "get_weather");
783 assert_eq!(out.tool_calls[0].arguments, json!({"city": "Paris"}));
784 assert_eq!(out.tool_calls[1].name, "get_time");
785 assert_eq!(out.tool_calls[1].arguments, json!({"zone": "UTC"}));
786 assert_ne!(
787 out.tool_calls[0].id, out.tool_calls[1].id,
788 "each call in the block needs its own ID"
789 );
790 }
791
792 #[test]
799 fn a_parameter_value_containing_the_literal_close_marker_does_not_truncate() {
800 let mut p = qwen();
801 let body = concat!(
802 "<tool_call><function=write_doc>",
803 "<parameter=text>Use </parameter> to close a param</parameter>",
804 "<parameter=lang>en</parameter>",
805 "</function></tool_call>",
806 );
807 let out = collect(&mut p, &[body]);
808 assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
809 assert_eq!(out.tool_calls.len(), 1);
810 assert_eq!(
811 out.tool_calls[0].arguments,
812 json!({"text": "Use </parameter> to close a param", "lang": "en"})
813 );
814 }
815
816 #[test]
820 fn a_parameter_value_containing_the_literal_function_close_does_not_truncate() {
821 let mut p = qwen();
822 let body = concat!(
823 "<tool_call>",
824 "<function=write_doc><parameter=text>end with </function> tag</parameter></function>",
825 "<function=ping></function>",
826 "</tool_call>",
827 );
828 let out = collect(&mut p, &[body]);
829 assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
830 assert_eq!(out.tool_calls.len(), 2);
831 assert_eq!(
832 out.tool_calls[0].arguments,
833 json!({"text": "end with </function> tag"})
834 );
835 assert_eq!(out.tool_calls[1].name, "ping");
836 }
837
838 #[test]
842 fn malformed_function_xml_gets_its_own_error_kind() {
843 let mut p = qwen();
844 let out = collect(
845 &mut p,
846 &["<tool_call><function=oops(no closing angle</tool_call>"],
847 );
848 assert!(out.tool_calls.is_empty());
849 assert_eq!(out.errors.len(), 1);
850 assert!(matches!(
851 out.errors[0].kind,
852 crate::normalize::error::NormalizationErrorKind::MalformedFunctionXml { .. }
853 ));
854 }
855
856 #[test]
863 fn parameter_values_that_look_like_json_literals_are_coerced_not_kept_as_strings() {
864 let mut p = qwen();
865 let body = concat!(
866 "<tool_call><function=configure>",
867 "<parameter=enabled>true</parameter>",
868 "<parameter=count>123</parameter>",
869 "<parameter=label>plain text</parameter>",
870 "</function></tool_call>",
871 );
872 let out = collect(&mut p, &[body]);
873 assert!(out.errors.is_empty());
874 assert_eq!(
875 out.tool_calls[0].arguments,
876 json!({"enabled": true, "count": 123, "label": "plain text"}),
877 "bool- and number-shaped strings coerce; only non-JSON-shaped text stays a string"
878 );
879 }
880
881 fn derived() -> DialectSpec {
889 DialectSpec {
890 id: crate::domain::dialect::DERIVED_DIALECT_ID.to_owned(),
891 tool_open: "«TC»".to_owned(),
892 tool_close: "«/TC»".to_owned(),
893 body_codecs: vec![BodyCodec::Json],
894 emission: crate::domain::dialect::EmissionProfile::default(),
895 id_prefix: crate::domain::dialect::DERIVED_ID_PREFIX.to_owned(),
896 }
897 }
898
899 #[test]
900 fn custom_marker_spec_extracts_calls_with_its_own_id_prefix() {
901 let mut p = DelimitedToolCallParser::new(derived());
902 let out = collect(
903 &mut p,
904 &[r#"before«TC»{"name":"foo","arguments":{"x":1}}«/TC»after"#],
905 );
906 assert_eq!(out.forward_text, "beforeafter");
907 assert_eq!(out.tool_calls.len(), 1);
908 assert_eq!(out.tool_calls[0].id, "call_dialect_0");
909 assert_eq!(out.tool_calls[0].name, "foo");
910 assert!(out.errors.is_empty());
911 }
912
913 #[test]
914 fn custom_marker_spec_survives_byte_at_a_time_chunking() {
915 let mut p = DelimitedToolCallParser::new(derived());
916 let s = r#"x«TC»{"name":"f","arguments":{"a":2}}«/TC»y"#;
917 let chunks: Vec<String> = s.chars().map(|c| c.to_string()).collect();
921 let refs: Vec<&str> = chunks.iter().map(String::as_str).collect();
922 let out = collect(&mut p, &refs);
923 assert_eq!(out.forward_text, "xy");
924 assert_eq!(out.tool_calls.len(), 1);
925 assert_eq!(out.tool_calls[0].arguments, json!({"a": 2}));
926 }
927
928 #[test]
929 fn fenced_spec_with_identical_open_and_close_markers_works() {
930 let spec = DialectSpec {
931 tool_open: "@@TOOL@@".to_owned(),
932 tool_close: "@@TOOL@@".to_owned(),
933 ..derived()
934 };
935 let mut p = DelimitedToolCallParser::new(spec);
936 let out = collect(
937 &mut p,
938 &[r#"a@@TOOL@@{"name":"f","arguments":{}}@@TOOL@@b"#],
939 );
940 assert_eq!(out.forward_text, "ab");
941 assert_eq!(out.tool_calls.len(), 1);
942 assert!(out.errors.is_empty());
943 }
944
945 #[test]
949 fn json_only_spec_rejects_function_xml_with_the_json_error_kind() {
950 let spec = DialectSpec {
951 tool_open: "<tool_call>".to_owned(),
952 tool_close: "</tool_call>".to_owned(),
953 ..derived()
954 };
955 let mut p = DelimitedToolCallParser::new(spec);
956 let out = collect(
957 &mut p,
958 &["<tool_call><function=grep><parameter=regex>x</parameter></function></tool_call>"],
959 );
960 assert!(out.tool_calls.is_empty());
961 assert_eq!(out.errors.len(), 1);
962 assert!(matches!(
963 out.errors[0].kind,
964 crate::normalize::error::NormalizationErrorKind::MalformedToolCallJson { .. }
965 ));
966 }
967
968 #[test]
973 fn render_call_output_round_trips_through_a_parser_of_the_same_spec() {
974 for spec in [DialectSpec::qwen_xml(), derived()] {
975 let emission = spec.render_call("read_file", &json!({"path": "a.rs"}));
976 let mut p = DelimitedToolCallParser::new(spec);
977 let out = collect(&mut p, &[emission.as_str()]);
978 assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
979 assert_eq!(out.tool_calls.len(), 1);
980 assert_eq!(out.tool_calls[0].name, "read_file");
981 assert_eq!(out.tool_calls[0].arguments, json!({"path": "a.rs"}));
982 assert_eq!(out.forward_text, "");
983 }
984 }
985}