1use serde_json::Value;
32
33use super::super::error::NormalizationError;
34use super::super::parser::{ParserOutput, ToolCallParser};
35use crate::domain::agent::ToolCall;
36
37const OPEN: &str = "<tool_call>";
39const CLOSE: &str = "</tool_call>";
41
42#[derive(Default, Debug)]
45struct ChannelState {
46 pending: String,
48 inside: bool,
50 body: String,
52}
53
54#[derive(Copy, Clone)]
56enum Channel {
57 Text,
58 Reasoning,
59}
60
61#[derive(Default, Debug)]
63pub struct QwenXmlParser {
64 text: ChannelState,
65 reasoning: ChannelState,
66 next_id: u32,
69}
70
71impl QwenXmlParser {
72 #[must_use]
74 pub fn new() -> Self {
75 Self::default()
76 }
77
78 fn mint_id(&mut self) -> String {
80 let n = self.next_id;
81 self.next_id = self.next_id.saturating_add(1);
82 format!("call_qwen_{n}")
83 }
84
85 fn scan(&mut self, channel: Channel, chunk: &str) -> ParserOutput {
91 let mut out = ParserOutput::default();
92
93 let mut state = match channel {
97 Channel::Text => std::mem::take(&mut self.text),
98 Channel::Reasoning => std::mem::take(&mut self.reasoning),
99 };
100
101 state.pending.push_str(chunk);
102
103 loop {
104 if state.inside {
105 if let Some(p) = state.pending.find(CLOSE) {
106 state.body.push_str(&state.pending[..p]);
107 finalize_tool_call(&state.body, &mut out, || self.mint_id());
108 state.body.clear();
109 state.inside = false;
110 state.pending.drain(..p + CLOSE.len());
111 continue;
112 }
113 let keep = partial_suffix_len(state.pending.as_bytes(), CLOSE.as_bytes());
114 let flush_to = state.pending.len() - keep;
115 state.body.push_str(&state.pending[..flush_to]);
116 state.pending.drain(..flush_to);
117 break;
118 }
119
120 if let Some(p) = state.pending.find(OPEN) {
122 forward(&mut out, channel, &state.pending[..p]);
123 state.pending.drain(..p + OPEN.len());
124 state.inside = true;
125 continue;
126 }
127 let keep = partial_suffix_len(state.pending.as_bytes(), OPEN.as_bytes());
128 let flush_to = state.pending.len() - keep;
129 forward(&mut out, channel, &state.pending[..flush_to]);
130 state.pending.drain(..flush_to);
131 break;
132 }
133
134 match channel {
135 Channel::Text => self.text = state,
136 Channel::Reasoning => self.reasoning = state,
137 }
138 out
139 }
140
141 fn flush_channel(&mut self, channel: Channel) -> ParserOutput {
143 let mut out = ParserOutput::default();
144 let state = match channel {
145 Channel::Text => std::mem::take(&mut self.text),
146 Channel::Reasoning => std::mem::take(&mut self.reasoning),
147 };
148 if state.inside {
149 let mut partial = state.body;
153 partial.push_str(&state.pending);
154 out.errors
155 .push(NormalizationError::unclosed_tool_call(partial));
156 } else {
157 forward(&mut out, channel, &state.pending);
159 }
160 out
161 }
162}
163
164impl ToolCallParser for QwenXmlParser {
165 fn push_text(&mut self, chunk: &str) -> ParserOutput {
166 self.scan(Channel::Text, chunk)
167 }
168
169 fn push_reasoning(&mut self, chunk: &str) -> ParserOutput {
170 self.scan(Channel::Reasoning, chunk)
171 }
172
173 fn finish(&mut self) -> ParserOutput {
174 let mut a = self.flush_channel(Channel::Text);
175 let b = self.flush_channel(Channel::Reasoning);
176 a.forward_text.push_str(&b.forward_text);
177 a.forward_reasoning.push_str(&b.forward_reasoning);
178 a.tool_calls.extend(b.tool_calls);
179 a.errors.extend(b.errors);
180 a
181 }
182}
183
184fn forward(out: &mut ParserOutput, channel: Channel, bytes: &str) {
190 if bytes.is_empty() {
191 return;
192 }
193 match channel {
194 Channel::Text => out.forward_text.push_str(bytes),
195 Channel::Reasoning => out.forward_reasoning.push_str(bytes),
196 }
197}
198
199fn finalize_tool_call(body: &str, out: &mut ParserOutput, mut mint_id: impl FnMut() -> String) {
221 let trimmed = body.trim();
222 if let Some(call) = parse_json_body(trimmed, &mut mint_id) {
223 out.tool_calls.push(call);
224 return;
225 }
226 if let Some(calls) = parse_function_xml_body(trimmed, &mut mint_id) {
227 out.tool_calls.extend(calls);
228 return;
229 }
230 let error = if trimmed.starts_with("<function=") {
231 NormalizationError::malformed_function_xml(body.to_owned())
232 } else {
233 NormalizationError::malformed_tool_call(body.to_owned())
234 };
235 out.errors.push(error);
236}
237
238fn parse_json_body(body: &str, mint_id: &mut impl FnMut() -> String) -> Option<ToolCall> {
240 let parsed: Value = serde_json::from_str(body).ok()?;
241 let obj = parsed.as_object()?;
242 let name = obj.get("name").and_then(Value::as_str)?.to_owned();
243 let arguments = obj
244 .get("arguments")
245 .cloned()
246 .unwrap_or_else(|| Value::Object(serde_json::Map::new()));
247 Some(ToolCall {
248 id: mint_id(),
249 name,
250 arguments,
251 })
252}
253
254fn parse_function_xml_body(
271 body: &str,
272 mint_id: &mut impl FnMut() -> String,
273) -> Option<Vec<ToolCall>> {
274 let mut calls = Vec::new();
275 let mut cursor = body.trim();
276
277 while !cursor.is_empty() {
278 let after_open = cursor.strip_prefix("<function=")?;
279 let name_end = after_open.find('>')?;
280 let name = after_open[..name_end].trim();
281 if name.is_empty() {
282 return None;
283 }
284 let after_name = &after_open[name_end + 1..];
285
286 let close_at = find_own_close(after_name, "</function>", "<function=")?;
293 let inner = after_name[..close_at].trim();
294 let after_function = &after_name[close_at + "</function>".len()..];
295
296 let mut args = serde_json::Map::new();
297 let mut param_cursor = inner;
298 while !param_cursor.is_empty() {
299 param_cursor = param_cursor.trim_start();
300 if param_cursor.is_empty() {
301 break;
302 }
303 let after_param = param_cursor.strip_prefix("<parameter=")?;
304 let key_end = after_param.find('>')?;
305 let key = after_param[..key_end].trim().to_owned();
306 if key.is_empty() {
307 return None;
308 }
309 let rest = &after_param[key_end + 1..];
310 let close_at = find_own_close(rest, "</parameter>", "<parameter=")?;
311 let raw_value = rest[..close_at].trim();
312 args.insert(key, parse_param_value(raw_value));
313 param_cursor = &rest[close_at + "</parameter>".len()..];
314 }
315
316 calls.push(ToolCall {
317 id: mint_id(),
318 name: name.to_owned(),
319 arguments: Value::Object(args),
320 });
321
322 cursor = after_function.trim_start();
323 }
324
325 (!calls.is_empty()).then_some(calls)
326}
327
328fn find_own_close(rest: &str, close: &str, next_open: &str) -> Option<usize> {
343 let boundary = rest.find(next_open).unwrap_or(rest.len());
344 rest[..boundary].rfind(close)
345}
346
347fn parse_param_value(raw: &str) -> Value {
357 if raw.is_empty() {
358 return Value::String(String::new());
359 }
360 if let Ok(v) = serde_json::from_str::<Value>(raw) {
361 return v;
362 }
363 Value::String(raw.to_owned())
364}
365
366fn partial_suffix_len(buf: &[u8], marker: &[u8]) -> usize {
370 if marker.len() < 2 {
371 return 0;
372 }
373 let max = std::cmp::min(buf.len(), marker.len() - 1);
374 for n in (1..=max).rev() {
375 if buf.ends_with(&marker[..n]) {
376 return n;
377 }
378 }
379 0
380}
381
382#[cfg(test)]
387mod tests {
388 use super::*;
389 use serde_json::json;
390
391 fn collect(p: &mut QwenXmlParser, chunks: &[&str]) -> ParserOutput {
392 let mut total = ParserOutput::default();
393 for c in chunks {
394 let o = p.push_text(c);
395 total.forward_text.push_str(&o.forward_text);
396 total.forward_reasoning.push_str(&o.forward_reasoning);
397 total.tool_calls.extend(o.tool_calls);
398 total.errors.extend(o.errors);
399 }
400 let f = p.finish();
401 total.forward_text.push_str(&f.forward_text);
402 total.forward_reasoning.push_str(&f.forward_reasoning);
403 total.tool_calls.extend(f.tool_calls);
404 total.errors.extend(f.errors);
405 total
406 }
407
408 #[test]
409 fn passthrough_with_no_markup() {
410 let mut p = QwenXmlParser::new();
411 let out = collect(&mut p, &["hello ", "world"]);
412 assert_eq!(out.forward_text, "hello world");
413 assert!(out.tool_calls.is_empty());
414 assert!(out.errors.is_empty());
415 }
416
417 #[test]
418 fn extracts_simple_tool_call_from_text() {
419 let mut p = QwenXmlParser::new();
420 let out = collect(
421 &mut p,
422 &[r#"before<tool_call>{"name":"foo","arguments":{"x":1}}</tool_call>after"#],
423 );
424 assert_eq!(out.forward_text, "beforeafter");
425 assert_eq!(out.tool_calls.len(), 1);
426 assert_eq!(out.tool_calls[0].id, "call_qwen_0");
427 assert_eq!(out.tool_calls[0].name, "foo");
428 assert_eq!(out.tool_calls[0].arguments, json!({"x": 1}));
429 assert!(out.errors.is_empty());
430 }
431
432 #[test]
433 fn open_tag_straddles_chunk_boundary() {
434 let mut p = QwenXmlParser::new();
435 let out = collect(
436 &mut p,
437 &[
438 "before<tool",
439 "_call>",
440 r#"{"name":"foo","arguments":{}}"#,
441 "</tool_call>",
442 "after",
443 ],
444 );
445 assert_eq!(out.forward_text, "beforeafter");
446 assert_eq!(out.tool_calls.len(), 1);
447 assert_eq!(out.tool_calls[0].name, "foo");
448 }
449
450 #[test]
451 fn close_tag_straddles_chunk_boundary() {
452 let mut p = QwenXmlParser::new();
453 let out = collect(
454 &mut p,
455 &[
456 "<tool_call>",
457 r#"{"name":"foo","arguments":{}}</tool"#,
458 "_call>tail",
459 ],
460 );
461 assert_eq!(out.forward_text, "tail");
462 assert_eq!(out.tool_calls.len(), 1);
463 assert_eq!(out.tool_calls[0].name, "foo");
464 }
465
466 #[test]
467 fn one_byte_at_a_time_still_works() {
468 let mut p = QwenXmlParser::new();
469 let s = r#"x<tool_call>{"name":"f","arguments":{"a":2}}</tool_call>y"#;
470 let chunks: Vec<String> = s.chars().map(|c| c.to_string()).collect();
471 let refs: Vec<&str> = chunks.iter().map(String::as_str).collect();
472 let out = collect(&mut p, &refs);
473 assert_eq!(out.forward_text, "xy");
474 assert_eq!(out.tool_calls.len(), 1);
475 assert_eq!(out.tool_calls[0].arguments, json!({"a": 2}));
476 }
477
478 #[test]
479 fn tool_call_in_reasoning_channel_is_extracted() {
480 let mut p = QwenXmlParser::new();
481 let chunk = r#"thinking <tool_call>{"name":"foo","arguments":{}}</tool_call> done"#;
482 let out = p.push_reasoning(chunk);
483 let f = p.finish();
484 assert_eq!(out.forward_reasoning, "thinking done");
485 assert_eq!(out.tool_calls.len(), 1);
486 assert_eq!(out.tool_calls[0].name, "foo");
487 assert!(f.is_empty());
488 }
489
490 #[test]
491 fn malformed_json_emits_error() {
492 let mut p = QwenXmlParser::new();
493 let out = collect(&mut p, &["<tool_call>not json</tool_call>"]);
494 assert!(out.tool_calls.is_empty());
495 assert_eq!(out.errors.len(), 1);
496 assert!(matches!(
497 out.errors[0].kind,
498 crate::normalize::error::NormalizationErrorKind::MalformedToolCallJson { .. }
499 ));
500 }
501
502 #[test]
503 fn missing_name_field_is_malformed() {
504 let mut p = QwenXmlParser::new();
505 let out = collect(&mut p, &[r#"<tool_call>{"arguments":{}}</tool_call>"#]);
506 assert!(out.tool_calls.is_empty());
507 assert_eq!(out.errors.len(), 1);
508 }
509
510 #[test]
511 fn missing_arguments_defaults_to_empty_object() {
512 let mut p = QwenXmlParser::new();
513 let out = collect(&mut p, &[r#"<tool_call>{"name":"foo"}</tool_call>"#]);
514 assert_eq!(out.tool_calls.len(), 1);
515 assert_eq!(out.tool_calls[0].arguments, json!({}));
516 assert!(out.errors.is_empty());
517 }
518
519 #[test]
520 fn unclosed_tag_at_end_yields_error() {
521 let mut p = QwenXmlParser::new();
522 let _ = p.push_text(r#"hello <tool_call>{"name":"foo""#);
523 let f = p.finish();
524 assert_eq!(f.errors.len(), 1);
525 assert!(matches!(
526 f.errors[0].kind,
527 crate::normalize::error::NormalizationErrorKind::UnclosedToolCallTag { .. }
528 ));
529 assert!(f.tool_calls.is_empty());
530 }
531
532 #[test]
533 fn multiple_tool_calls_get_distinct_ids() {
534 let mut p = QwenXmlParser::new();
535 let out = collect(
536 &mut p,
537 &[
538 r#"<tool_call>{"name":"a","arguments":{}}</tool_call>"#,
539 r#"<tool_call>{"name":"b","arguments":{}}</tool_call>"#,
540 ],
541 );
542 assert_eq!(out.tool_calls.len(), 2);
543 assert_eq!(out.tool_calls[0].id, "call_qwen_0");
544 assert_eq!(out.tool_calls[1].id, "call_qwen_1");
545 }
546
547 #[test]
548 fn partial_marker_lookalike_is_eventually_flushed() {
549 let mut p = QwenXmlParser::new();
552 let mid = p.push_text("<tool");
553 assert_eq!(mid.forward_text, "");
554 let f = p.finish();
555 assert_eq!(f.forward_text, "<tool");
556 }
557
558 #[test]
559 fn partial_suffix_len_finds_longest_overlap() {
560 assert_eq!(partial_suffix_len(b"abc<tool", b"<tool_call>"), 5);
561 assert_eq!(partial_suffix_len(b"abc<", b"<tool_call>"), 1);
562 assert_eq!(partial_suffix_len(b"abc", b"<tool_call>"), 0);
563 assert_eq!(partial_suffix_len(b"<tool_call>", b"<tool_call>"), 0);
566 assert_eq!(partial_suffix_len(b"</tool_call><", b"<tool_call>"), 1);
568 }
569
570 #[test]
576 fn extracts_function_xml_body_with_string_param() {
577 let mut p = QwenXmlParser::new();
578 let body = "<tool_call>\n<function=grep>\n<parameter=regex>\ngglib\\s+q\n</parameter>\n</function>\n</tool_call>";
579 let out = collect(&mut p, &[body]);
580 assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
581 assert_eq!(out.tool_calls.len(), 1);
582 assert_eq!(out.tool_calls[0].name, "grep");
583 assert_eq!(
584 out.tool_calls[0].arguments,
585 json!({ "regex": "gglib\\s+q" })
586 );
587 }
588
589 #[test]
590 fn function_xml_body_with_multiple_params() {
591 let mut p = QwenXmlParser::new();
592 let body = concat!(
593 "<tool_call><function=read_file>",
594 "<parameter=path>src/main.rs</parameter>",
595 "<parameter=start_line>1</parameter>",
596 "<parameter=end_line>20</parameter>",
597 "</function></tool_call>",
598 );
599 let out = collect(&mut p, &[body]);
600 assert!(out.errors.is_empty());
601 assert_eq!(out.tool_calls.len(), 1);
602 assert_eq!(out.tool_calls[0].name, "read_file");
603 assert_eq!(
604 out.tool_calls[0].arguments,
605 json!({ "path": "src/main.rs", "start_line": 1, "end_line": 20 })
606 );
607 }
608
609 #[test]
610 fn function_xml_body_with_json_object_param() {
611 let mut p = QwenXmlParser::new();
612 let body = r#"<tool_call><function=run><parameter=opts>{"a":1,"b":[2,3]}</parameter></function></tool_call>"#;
613 let out = collect(&mut p, &[body]);
614 assert!(out.errors.is_empty());
615 assert_eq!(out.tool_calls.len(), 1);
616 assert_eq!(
617 out.tool_calls[0].arguments,
618 json!({ "opts": { "a": 1, "b": [2, 3] } })
619 );
620 }
621
622 #[test]
623 fn function_xml_body_streamed_byte_by_byte() {
624 let mut p = QwenXmlParser::new();
625 let s = "<tool_call><function=grep><parameter=regex>x</parameter></function></tool_call>";
626 let chunks: Vec<String> = s.chars().map(|c| c.to_string()).collect();
627 let refs: Vec<&str> = chunks.iter().map(String::as_str).collect();
628 let out = collect(&mut p, &refs);
629 assert!(out.errors.is_empty());
630 assert_eq!(out.tool_calls.len(), 1);
631 assert_eq!(out.tool_calls[0].name, "grep");
632 assert_eq!(out.tool_calls[0].arguments, json!({ "regex": "x" }));
633 }
634
635 #[test]
636 fn function_xml_body_without_parameters_yields_empty_args() {
637 let mut p = QwenXmlParser::new();
638 let body = "<tool_call><function=ping></function></tool_call>";
639 let out = collect(&mut p, &[body]);
640 assert!(out.errors.is_empty());
641 assert_eq!(out.tool_calls.len(), 1);
642 assert_eq!(out.tool_calls[0].name, "ping");
643 assert_eq!(out.tool_calls[0].arguments, json!({}));
644 }
645
646 #[test]
650 fn multiple_function_blocks_in_one_wrapper_are_all_extracted() {
651 let mut p = QwenXmlParser::new();
652 let body = concat!(
653 "<tool_call>",
654 "<function=get_weather><parameter=city>Paris</parameter></function>",
655 "<function=get_time><parameter=zone>UTC</parameter></function>",
656 "</tool_call>",
657 );
658 let out = collect(&mut p, &[body]);
659 assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
660 assert_eq!(out.tool_calls.len(), 2);
661 assert_eq!(out.tool_calls[0].name, "get_weather");
662 assert_eq!(out.tool_calls[0].arguments, json!({"city": "Paris"}));
663 assert_eq!(out.tool_calls[1].name, "get_time");
664 assert_eq!(out.tool_calls[1].arguments, json!({"zone": "UTC"}));
665 assert_ne!(
666 out.tool_calls[0].id, out.tool_calls[1].id,
667 "each call in the block needs its own ID"
668 );
669 }
670
671 #[test]
678 fn a_parameter_value_containing_the_literal_close_marker_does_not_truncate() {
679 let mut p = QwenXmlParser::new();
680 let body = concat!(
681 "<tool_call><function=write_doc>",
682 "<parameter=text>Use </parameter> to close a param</parameter>",
683 "<parameter=lang>en</parameter>",
684 "</function></tool_call>",
685 );
686 let out = collect(&mut p, &[body]);
687 assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
688 assert_eq!(out.tool_calls.len(), 1);
689 assert_eq!(
690 out.tool_calls[0].arguments,
691 json!({"text": "Use </parameter> to close a param", "lang": "en"})
692 );
693 }
694
695 #[test]
699 fn a_parameter_value_containing_the_literal_function_close_does_not_truncate() {
700 let mut p = QwenXmlParser::new();
701 let body = concat!(
702 "<tool_call>",
703 "<function=write_doc><parameter=text>end with </function> tag</parameter></function>",
704 "<function=ping></function>",
705 "</tool_call>",
706 );
707 let out = collect(&mut p, &[body]);
708 assert!(out.errors.is_empty(), "errors: {:?}", out.errors);
709 assert_eq!(out.tool_calls.len(), 2);
710 assert_eq!(
711 out.tool_calls[0].arguments,
712 json!({"text": "end with </function> tag"})
713 );
714 assert_eq!(out.tool_calls[1].name, "ping");
715 }
716
717 #[test]
721 fn malformed_function_xml_gets_its_own_error_kind() {
722 let mut p = QwenXmlParser::new();
723 let out = collect(
724 &mut p,
725 &["<tool_call><function=oops(no closing angle</tool_call>"],
726 );
727 assert!(out.tool_calls.is_empty());
728 assert_eq!(out.errors.len(), 1);
729 assert!(matches!(
730 out.errors[0].kind,
731 crate::normalize::error::NormalizationErrorKind::MalformedFunctionXml { .. }
732 ));
733 }
734
735 #[test]
742 fn parameter_values_that_look_like_json_literals_are_coerced_not_kept_as_strings() {
743 let mut p = QwenXmlParser::new();
744 let body = concat!(
745 "<tool_call><function=configure>",
746 "<parameter=enabled>true</parameter>",
747 "<parameter=count>123</parameter>",
748 "<parameter=label>plain text</parameter>",
749 "</function></tool_call>",
750 );
751 let out = collect(&mut p, &[body]);
752 assert!(out.errors.is_empty());
753 assert_eq!(
754 out.tool_calls[0].arguments,
755 json!({"enabled": true, "count": 123, "label": "plain text"}),
756 "bool- and number-shaped strings coerce; only non-JSON-shaped text stays a string"
757 );
758 }
759}