1use std::collections::{HashMap, HashSet};
51
52use async_trait::async_trait;
53use openai_protocol::common::Tool;
54use regex::Regex;
55use serde_json::{json, Value};
56
57use crate::{
58 errors::ParserResult,
59 traits::ToolParser,
60 types::{FunctionCall, StreamingParseResult, ToolCall, ToolCallItem},
61};
62
63const TOOLS_OPEN: &str = "<|open|>tools<|sep|>";
64const TOOLS_CLOSE: &str = "<|close|>tools<|sep|>";
65const RESPONSE_OPEN: &str = "<|open|>response<|sep|>";
66const RESPONSE_CLOSE: &str = "<|close|>response<|sep|>";
67const THINK_CLOSE: &str = "<|close|>think<|sep|>";
68
69const TOOLS_SECTION_PREFIXES: [&str; 3] = ["", THINK_CLOSE, RESPONSE_CLOSE];
73
74const OPEN: &str = "<|open|>";
76const CLOSE: &str = "<|close|>";
77const SEP: &str = "<|sep|>";
78
79struct DecodedCall {
85 name: String,
86 arguments: String,
88}
89
90pub struct KimiK3Parser {
96 tools_open_re: Regex,
98 tools_close_re: Regex,
100 response_open_re: Regex,
102 response_close_re: Regex,
104 message_close_re: Regex,
106 call_re: Regex,
108 arg_re: Regex,
110 attr_re: Regex,
112 response_re: Regex,
114
115 buffer: String,
118 sent_content_idx: usize,
121 sent_tool_call_count: usize,
123}
124
125impl KimiK3Parser {
126 pub fn build_structural_tag(tools: &[Tool], at_least_one: bool) -> Value {
176 let call_formats: Vec<Value> = tools
177 .iter()
178 .filter(|tool| !tool.function.name.is_empty())
179 .map(|tool| build_call_format(&tool.function.name, tool.function.parameters_schema()))
180 .collect();
181
182 let call_choice = if call_formats.is_empty() {
183 permissive_call_format()
184 } else {
185 json!({ "type": "or", "elements": call_formats })
186 };
187
188 let content = json!({ "type": "plus", "content": call_choice });
189
190 let tags: Vec<Value> = TOOLS_SECTION_PREFIXES
192 .iter()
193 .map(|prefix| {
194 json!({
195 "begin": format!("{prefix}{TOOLS_OPEN}"),
196 "content": content,
197 "end": TOOLS_CLOSE,
198 })
199 })
200 .collect();
201
202 json!({
203 "format": {
204 "type": "triggered_tags",
205 "triggers": [TOOLS_OPEN, THINK_CLOSE, RESPONSE_CLOSE],
206 "tags": tags,
207 "at_least_one": at_least_one,
208 }
209 })
210 }
211
212 #[expect(
214 clippy::expect_used,
215 reason = "regex patterns are compile-time string literals"
216 )]
217 pub fn new() -> Self {
218 Self {
219 tools_open_re: Regex::new(r"<\|open\|>\s*tools\s*<\|sep\|>")
220 .expect("valid regex"),
221 tools_close_re: Regex::new(r"<\|close\|>\s*tools\s*<\|sep\|>")
222 .expect("valid regex"),
223 response_open_re: Regex::new(r"<\|open\|>\s*response\s*<\|sep\|>")
224 .expect("valid regex"),
225 response_close_re: Regex::new(r"<\|close\|>\s*response\s*<\|sep\|>")
226 .expect("valid regex"),
227 message_close_re: Regex::new(r"<\|close\|>\s*message\s*<\|sep\|>")
228 .expect("valid regex"),
229 call_re: Regex::new(
230 r"(?s)<\|open\|>\s*call\s+(?P<attrs>.*?)<\|sep\|>(?P<body>.*?)<\|close\|>\s*call\s*<\|sep\|>",
231 )
232 .expect("valid regex"),
233 arg_re: Regex::new(
234 r"(?s)<\|open\|>\s*argument\s+(?P<attrs>.*?)<\|sep\|>(?P<val>.*?)<\|close\|>\s*argument\s*<\|sep\|>",
235 )
236 .expect("valid regex"),
237 attr_re: Regex::new(r#"(?P<k>\w+)="(?P<v>[^"]*)""#).expect("valid regex"),
238 response_re: Regex::new(
239 r"(?s)<\|open\|>\s*response\s*<\|sep\|>(?P<c>.*?)<\|close\|>\s*response\s*<\|sep\|>",
240 )
241 .expect("valid regex"),
242 buffer: String::new(),
243 sent_content_idx: 0,
244 sent_tool_call_count: 0,
245 }
246 }
247
248 fn attrs(&self, s: &str) -> HashMap<String, String> {
251 self.attr_re
252 .captures_iter(s)
253 .map(|m| {
254 let key = m.name("k").map_or("", |g| g.as_str()).to_string();
255 let value = m
256 .name("v")
257 .map_or("", |g| g.as_str())
258 .replace(""", "\"")
259 .replace("&", "&");
260 (key, value)
261 })
262 .collect()
263 }
264
265 fn decode_call(&self, attrs: &str, body: &str) -> Option<DecodedCall> {
271 let call_attrs = self.attrs(attrs);
272 let tool_name = call_attrs.get("tool").cloned().unwrap_or_default();
273 if tool_name.is_empty() {
274 return None;
275 }
276
277 let mut arguments = serde_json::Map::new();
278 for arg_match in self.arg_re.captures_iter(body) {
279 let arg_attrs_text = arg_match.name("attrs").map_or("", |g| g.as_str());
280 let arg_attrs = self.attrs(arg_attrs_text);
281 let key = arg_attrs.get("key").cloned().unwrap_or_default();
282 let arg_type = arg_attrs.get("type").map_or("string", String::as_str);
283 let raw_value = arg_match.name("val").map_or("", |g| g.as_str());
284
285 let value = if arg_type == "string" {
286 Value::String(raw_value.to_string())
287 } else {
288 serde_json::from_str::<Value>(raw_value)
289 .unwrap_or_else(|_| Value::String(raw_value.to_string()))
290 };
291 arguments.insert(key, value);
292 }
293
294 let arguments_str = serde_json::to_string(&arguments).unwrap_or_else(|_| "{}".to_string());
295
296 Some(DecodedCall {
297 name: tool_name,
298 arguments: arguments_str,
299 })
300 }
301
302 fn strip_response_content(&self, text: &str) -> Option<String> {
310 let stripped = if let Some(m_open) = self.response_open_re.find(text) {
311 if let Some(m_close) = self.response_close_re.find_at(text, m_open.end()) {
312 text[m_open.end()..m_close.start()].to_string()
313 } else {
314 text[m_open.end()..].to_string()
315 }
316 } else {
317 self.response_close_re.replace_all(text, "").into_owned()
318 };
319 let stripped = self
320 .message_close_re
321 .replace_all(&stripped, "")
322 .into_owned();
323 if stripped.is_empty() {
324 None
325 } else {
326 Some(stripped)
327 }
328 }
329
330 fn content(&self, model_output: &str, before: &str) -> Option<String> {
335 if let Some(m) = self.response_re.captures(model_output) {
336 let c = m.name("c").map_or("", |g| g.as_str());
337 return if c.is_empty() {
338 None
339 } else {
340 Some(c.to_string())
341 };
342 }
343 self.strip_response_content(before)
344 }
345
346 fn extract_response_content(&mut self, current_text: &str) -> Option<String> {
352 let m_open = self.response_open_re.find(current_text);
353 let body_start = m_open.map_or(0, |m| m.end());
354
355 let tools_start = self
356 .tools_open_re
357 .find_at(current_text, body_start)
358 .map(|m| m.start());
359 let response_end = self
360 .response_close_re
361 .find_at(current_text, body_start)
362 .map(|m| m.start());
363
364 let sendable_idx = match (tools_start, response_end) {
365 (Some(a), Some(b)) => a.min(b),
366 (Some(a), None) | (None, Some(a)) => a,
367 (None, None) => {
368 let overlap = partial_tag_overlap(current_text, RESPONSE_OPEN)
369 .max(partial_tag_overlap(current_text, RESPONSE_CLOSE))
370 .max(partial_tag_overlap(current_text, TOOLS_OPEN));
371 current_text.len() - overlap
372 }
373 };
374
375 if sendable_idx <= body_start {
376 return None;
377 }
378 if self.sent_content_idx < body_start {
379 self.sent_content_idx = body_start;
380 }
381 if sendable_idx <= self.sent_content_idx {
382 return None;
383 }
384
385 let content = current_text[self.sent_content_idx..sendable_idx].to_string();
386 self.sent_content_idx = sendable_idx;
387 if content.is_empty() {
388 None
389 } else {
390 Some(content)
391 }
392 }
393
394 fn decode_calls_in_section(&self, section: &str) -> Vec<DecodedCall> {
398 let mut calls = Vec::new();
399 for m in self.call_re.captures_iter(section) {
400 let attrs_text = m.name("attrs").map_or("", |g| g.as_str());
401 let body = m.name("body").map_or("", |g| g.as_str());
402 if let Some(decoded) = self.decode_call(attrs_text, body) {
403 calls.push(decoded);
404 }
405 }
406 calls
407 }
408}
409
410enum ArgShape {
416 Fixed {
418 type_attr: &'static str,
419 value: Value,
420 },
421 Permissive,
424}
425
426fn escape_attr(value: &str) -> String {
429 value.replace('&', "&").replace('"', """)
430}
431
432fn build_call_format(name: &str, params: &Value) -> Value {
435 let esc_name = escape_attr(name);
436 json!({
437 "type": "sequence",
438 "elements": [
439 { "type": "const_string", "value": format!("{OPEN}call tool=\"{esc_name}\" index=\"") },
440 { "type": "regex", "pattern": "[1-9][0-9]*" },
441 { "type": "const_string", "value": format!("\"{SEP}") },
442 build_args_format(params),
443 { "type": "const_string", "value": format!("{CLOSE}call{SEP}") },
444 ]
445 })
446}
447
448fn build_args_format(params: &Value) -> Value {
454 let Some(properties) = params.get("properties").and_then(Value::as_object) else {
455 return permissive_args_format();
456 };
457 if properties.is_empty() {
458 return permissive_args_format();
459 }
460
461 let required: HashSet<&str> = params
462 .get("required")
463 .and_then(Value::as_array)
464 .map(|values| values.iter().filter_map(Value::as_str).collect())
465 .unwrap_or_default();
466
467 let defs = RootDefs::from_params(params);
468
469 let elements: Vec<Value> = properties
470 .iter()
471 .map(|(key, subschema)| {
472 let argument = build_argument_format(key, subschema, defs);
473 if required.contains(key.as_str()) {
474 argument
475 } else {
476 json!({ "type": "optional", "content": argument })
477 }
478 })
479 .collect();
480
481 json!({ "type": "sequence", "elements": elements })
482}
483
484fn build_argument_format(key: &str, subschema: &Value, defs: RootDefs<'_>) -> Value {
487 let esc_key = escape_attr(key);
488 match classify_arg(subschema, defs) {
489 ArgShape::Fixed { type_attr, value } => json!({
490 "type": "sequence",
491 "elements": [
492 { "type": "const_string", "value": format!("{OPEN}argument key=\"{esc_key}\" type=\"{type_attr}\"{SEP}") },
493 value,
494 { "type": "const_string", "value": format!("{CLOSE}argument{SEP}") },
495 ]
496 }),
497 ArgShape::Permissive => json!({
498 "type": "sequence",
499 "elements": [
500 { "type": "const_string", "value": format!("{OPEN}argument key=\"{esc_key}\" type=\"") },
501 { "type": "any_text", "excludes": ["\"", "<|"] },
502 { "type": "const_string", "value": format!("\"{SEP}") },
503 { "type": "any_text", "excludes": [CLOSE, OPEN, SEP] },
504 { "type": "const_string", "value": format!("{CLOSE}argument{SEP}") },
505 ]
506 }),
507 }
508}
509
510#[derive(Clone, Copy, Default)]
519struct RootDefs<'a> {
520 root: Option<&'a Value>,
521 defs: Option<&'a Value>,
522 definitions: Option<&'a Value>,
523}
524
525impl<'a> RootDefs<'a> {
526 fn from_params(params: &'a Value) -> Self {
527 Self {
528 root: Some(params),
529 defs: params.get("$defs"),
530 definitions: params.get("definitions"),
531 }
532 }
533
534 fn is_empty(self) -> bool {
535 self.defs.is_none() && self.definitions.is_none()
536 }
537
538 fn block(self, name: &str) -> Option<&'a Value> {
539 match name {
540 "$defs" => self.defs,
541 "definitions" => self.definitions,
542 _ => None,
543 }
544 }
545
546 fn resolve_pointer(self, pointer: &str) -> Option<&'a Value> {
551 if pointer == "#" || pointer == "#/" {
552 return self.root;
553 }
554 let rest = pointer.strip_prefix("#/")?;
555 let mut segments = rest.split('/');
556 let mut node = self.block(segments.next()?)?;
557 for segment in segments {
558 let key = segment.replace("~1", "/").replace("~0", "~");
560 node = node.get(&key)?;
561 }
562 Some(node)
563 }
564
565 fn resolve_ref(self, subschema: &'a Value) -> Option<&'a Value> {
573 let pointer = subschema.get("$ref")?.as_str()?;
574 self.resolve_pointer(pointer)
575 }
576
577 fn local_refs_resolve(self, schema: &Value) -> bool {
582 match schema {
583 Value::Object(map) => {
584 for (key, value) in map {
585 if matches!(key.as_str(), "$ref" | "$dynamicRef" | "$recursiveRef") {
586 if let Some(pointer) = value.as_str() {
587 if pointer.starts_with('#') && self.resolve_pointer(pointer).is_none() {
588 return false;
589 }
590 }
591 }
592 }
593 map.values().all(|value| self.local_refs_resolve(value))
594 }
595 Value::Array(items) => items.iter().all(|item| self.local_refs_resolve(item)),
596 _ => true,
597 }
598 }
599
600 fn attach(self, subschema: &Value) -> Value {
609 if self.is_empty() || !contains_local_ref(subschema) {
610 return subschema.clone();
611 }
612 let mut out = subschema.clone();
613 let Some(object) = out.as_object_mut() else {
614 return subschema.clone();
615 };
616 for (name, block) in [("$defs", self.defs), ("definitions", self.definitions)] {
617 let Some(Value::Object(root_entries)) = block else {
618 continue;
619 };
620 match object.get_mut(name) {
621 Some(Value::Object(own)) => {
622 for (key, value) in root_entries {
623 own.entry(key.clone()).or_insert_with(|| value.clone());
624 }
625 }
626 Some(_) => {}
627 None => {
628 object.insert(name.to_string(), Value::Object(root_entries.clone()));
629 }
630 }
631 }
632 out
633 }
634}
635
636fn contains_local_ref(schema: &Value) -> bool {
642 match schema {
643 Value::Object(map) => {
644 let refs_locally = map.iter().any(|(key, value)| {
645 matches!(key.as_str(), "$ref" | "$dynamicRef" | "$recursiveRef")
646 && value.as_str().is_some_and(|t| t.starts_with('#'))
647 });
648 refs_locally || map.values().any(contains_local_ref)
649 }
650 Value::Array(items) => items.iter().any(contains_local_ref),
651 _ => false,
652 }
653}
654
655fn classify_arg(subschema: &Value, defs: RootDefs<'_>) -> ArgShape {
660 if !defs.local_refs_resolve(subschema) {
664 return ArgShape::Permissive;
665 }
666
667 let target = defs.resolve_ref(subschema);
672 let keyword = |name: &str| {
673 subschema
674 .get(name)
675 .or_else(|| target.and_then(|schema| schema.get(name)))
676 };
677
678 if let Some(values) = keyword("enum").and_then(Value::as_array) {
680 if !values.is_empty() && values.iter().all(Value::is_string) {
681 let options: Vec<Value> = values
682 .iter()
683 .filter_map(Value::as_str)
684 .map(|v| json!({ "type": "const_string", "value": v }))
685 .collect();
686 return ArgShape::Fixed {
687 type_attr: "string",
688 value: json!({ "type": "or", "elements": options }),
689 };
690 }
691 }
692
693 match keyword("type").and_then(Value::as_str) {
694 Some("string") => ArgShape::Fixed {
696 type_attr: "string",
697 value: json!({ "type": "any_text", "excludes": [CLOSE, OPEN, SEP] }),
698 },
699 Some("integer") | Some("number") => ArgShape::Fixed {
700 type_attr: "number",
701 value: json!({ "type": "json_schema", "json_schema": defs.attach(subschema) }),
702 },
703 Some("boolean") => ArgShape::Fixed {
704 type_attr: "boolean",
705 value: json!({ "type": "json_schema", "json_schema": { "type": "boolean" } }),
706 },
707 Some("object") => ArgShape::Fixed {
708 type_attr: "object",
709 value: json!({ "type": "json_schema", "json_schema": defs.attach(subschema) }),
710 },
711 Some("array") => ArgShape::Fixed {
712 type_attr: "array",
713 value: json!({ "type": "json_schema", "json_schema": defs.attach(subschema) }),
714 },
715 Some("null") => ArgShape::Fixed {
716 type_attr: "null",
717 value: json!({ "type": "const_string", "value": "null" }),
718 },
719 _ => ArgShape::Permissive,
720 }
721}
722
723fn permissive_args_format() -> Value {
726 json!({ "type": "star", "content": permissive_argument_format() })
727}
728
729fn permissive_argument_format() -> Value {
738 json!({
739 "type": "sequence",
740 "elements": [
741 { "type": "const_string", "value": format!("{OPEN}argument key=\"") },
742 { "type": "any_text", "excludes": ["\"", "<|"] },
743 { "type": "const_string", "value": "\" type=\"" },
744 { "type": "any_text", "excludes": ["\"", "<|"] },
745 { "type": "const_string", "value": format!("\"{SEP}") },
746 { "type": "any_text", "excludes": [CLOSE, OPEN, SEP] },
747 { "type": "const_string", "value": format!("{CLOSE}argument{SEP}") },
748 ]
749 })
750}
751
752fn permissive_call_format() -> Value {
754 json!({
755 "type": "sequence",
756 "elements": [
757 { "type": "const_string", "value": format!("{OPEN}call tool=\"") },
758 { "type": "any_text", "excludes": ["\"", "<|"] },
759 { "type": "const_string", "value": "\" index=\"" },
760 { "type": "regex", "pattern": "[1-9][0-9]*" },
761 { "type": "const_string", "value": format!("\"{SEP}") },
762 permissive_args_format(),
763 { "type": "const_string", "value": format!("{CLOSE}call{SEP}") },
764 ]
765 })
766}
767
768fn partial_tag_overlap(text: &str, tag: &str) -> usize {
773 let max_len = text.len().min(tag.len().saturating_sub(1));
774 for n in (1..=max_len).rev() {
775 if text.ends_with(&tag[..n]) {
776 return n;
777 }
778 }
779 0
780}
781
782impl Default for KimiK3Parser {
783 fn default() -> Self {
784 Self::new()
785 }
786}
787
788#[async_trait]
789impl ToolParser for KimiK3Parser {
790 async fn parse_complete(&self, output: &str) -> ParserResult<(String, Vec<ToolCall>)> {
791 let Some(m_open) = self.tools_open_re.find(output) else {
792 let content = self.content(output, output);
793 return Ok((content.unwrap_or_default(), vec![]));
794 };
795
796 let before = &output[..m_open.start()];
797 let start = m_open.end();
798 let section_end = self
799 .tools_close_re
800 .find_at(output, start)
801 .map_or(output.len(), |m| m.start());
802 let section = &output[start..section_end];
803
804 let calls = self
805 .decode_calls_in_section(section)
806 .into_iter()
807 .map(|decoded| ToolCall {
808 function: FunctionCall {
809 name: decoded.name,
810 arguments: decoded.arguments,
811 },
812 })
813 .collect();
814
815 let content = self.content(output, before);
816 Ok((content.unwrap_or_default(), calls))
817 }
818
819 async fn parse_incremental(
820 &mut self,
821 chunk: &str,
822 _tools: &[Tool],
823 ) -> ParserResult<StreamingParseResult> {
824 self.buffer.push_str(chunk);
825 let current_text = self.buffer.clone();
826
827 let content = self.extract_response_content(¤t_text);
828
829 let Some(m_tools) = self.tools_open_re.find(¤t_text) else {
830 return Ok(StreamingParseResult {
831 normal_text: content.unwrap_or_default(),
832 calls: vec![],
833 });
834 };
835
836 let section = ¤t_text[m_tools.end()..];
837 let decoded_calls = self.decode_calls_in_section(section);
838
839 if decoded_calls.len() <= self.sent_tool_call_count {
840 return Ok(StreamingParseResult {
841 normal_text: content.unwrap_or_default(),
842 calls: vec![],
843 });
844 }
845
846 let calls = decoded_calls
847 .iter()
848 .skip(self.sent_tool_call_count)
849 .enumerate()
850 .map(|(i, decoded)| ToolCallItem {
851 tool_index: self.sent_tool_call_count + i,
854 name: Some(decoded.name.clone()),
855 parameters: decoded.arguments.clone(),
856 })
857 .collect();
858 self.sent_tool_call_count = decoded_calls.len();
859
860 Ok(StreamingParseResult {
861 normal_text: content.unwrap_or_default(),
862 calls,
863 })
864 }
865
866 fn has_tool_markers(&self, text: &str) -> bool {
867 self.tools_open_re.is_match(text)
868 }
869
870 fn reset(&mut self) {
871 self.buffer.clear();
872 self.sent_content_idx = 0;
873 self.sent_tool_call_count = 0;
874 }
875}
876
877#[cfg(test)]
878mod tests {
879 use serde_json::Value;
880
881 use super::*;
882
883 fn _arg(key: &str, typ: &str, value: &str) -> String {
884 format!(r#"{OPEN}argument key="{key}" type="{typ}"{SEP}{value}{CLOSE}argument{SEP}"#)
885 }
886
887 fn _call(tool: &str, index: i32, args: &[String]) -> String {
888 let body: String = args.concat();
889 format!(r#"{OPEN}call tool="{tool}" index="{index}"{SEP}{body}{CLOSE}call{SEP}"#)
890 }
891
892 fn _response(content: &str) -> String {
893 format!("{OPEN}response{SEP}{content}{CLOSE}response{SEP}")
894 }
895
896 fn _tools(calls: &[String]) -> String {
897 let body: String = calls.concat();
898 format!("{OPEN}tools{SEP}{body}{CLOSE}tools{SEP}")
899 }
900
901 #[tokio::test]
902 async fn test_parse_complete_response_and_typed_arguments() {
903 let parser = KimiK3Parser::new();
904 let input = _response("answer")
905 + &_tools(&[_call(
906 "calc",
907 1,
908 &[
909 _arg("x", "number", "1"),
910 _arg("flag", "boolean", "true"),
911 _arg("text", "string", "raw"),
912 ],
913 )]);
914
915 let (content, calls) = parser.parse_complete(&input).await.unwrap();
916 assert_eq!(content, "answer");
917 assert_eq!(calls.len(), 1);
918 assert_eq!(calls[0].function.name, "calc");
919 let args: Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
920 assert_eq!(
921 args,
922 serde_json::json!({"x": 1, "flag": true, "text": "raw"})
923 );
924 }
925
926 #[tokio::test]
927 async fn test_parse_complete_unescapes_attributes() {
928 let parser = KimiK3Parser::new();
929 let input = _tools(&[_call(
930 "a&b"c",
931 1,
932 &[_arg("k&q", "string", "v")],
933 )]);
934
935 let (_content, calls) = parser.parse_complete(&input).await.unwrap();
936 assert_eq!(calls.len(), 1);
937 assert_eq!(calls[0].function.name, "a&b\"c");
938 let args: Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
939 assert_eq!(args, serde_json::json!({"k&q": "v"}));
940 }
941
942 #[tokio::test]
943 async fn test_parse_complete_allows_less_than_in_attributes() {
944 let parser = KimiK3Parser::new();
945 let input = _tools(&[_call("calc<beta", 1, &[_arg("foo<bar", "string", "raw")])]);
946
947 let (_content, calls) = parser.parse_complete(&input).await.unwrap();
948 assert_eq!(calls.len(), 1);
949 assert_eq!(calls[0].function.name, "calc<beta");
950 let args: Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
951 assert_eq!(args, serde_json::json!({"foo<bar": "raw"}));
952 }
953
954 #[tokio::test]
955 async fn test_parse_complete_no_tools_channel() {
956 let parser = KimiK3Parser::new();
957 let input = _response("hi");
958
959 let (content, calls) = parser.parse_complete(&input).await.unwrap();
960 assert_eq!(content, "hi");
961 assert_eq!(calls.len(), 0);
962 }
963
964 #[tokio::test]
965 async fn test_parse_complete_whitespace_degraded_markers() {
966 let parser = KimiK3Parser::new();
967 let input = format!("{OPEN} response {SEP}answer{CLOSE} response {SEP}");
968
969 let (content, calls) = parser.parse_complete(&input).await.unwrap();
970 assert_eq!(content, "answer");
971 assert_eq!(calls.len(), 0);
972 }
973
974 #[tokio::test]
975 async fn test_parse_complete_malformed_json_argument_falls_back_to_raw() {
976 let parser = KimiK3Parser::new();
977 let input = _tools(&[_call("calc", 1, &[_arg("x", "number", "not-json")])]);
978
979 let (_content, calls) = parser.parse_complete(&input).await.unwrap();
980 assert_eq!(calls.len(), 1);
981 let args: Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
982 assert_eq!(args, serde_json::json!({"x": "not-json"}));
983 }
984
985 #[tokio::test]
986 async fn test_parse_complete_multiple_tool_calls() {
987 let parser = KimiK3Parser::new();
988 let input = _tools(&[
989 _call("first", 1, &[_arg("a", "number", "1")]),
990 _call("second", 2, &[_arg("b", "number", "2")]),
991 ]);
992
993 let (_content, calls) = parser.parse_complete(&input).await.unwrap();
994 assert_eq!(calls.len(), 2);
995 assert_eq!(calls[0].function.name, "first");
996 assert_eq!(calls[1].function.name, "second");
997 }
998
999 #[tokio::test]
1000 async fn test_decode_call_without_index_attribute() {
1001 let parser = KimiK3Parser::new();
1002 let input = format!(
1003 r#"{OPEN}tools{SEP}{OPEN}call tool="calc"{SEP}{CLOSE}call{SEP}{CLOSE}tools{SEP}"#
1004 );
1005
1006 let (_content, calls) = parser.parse_complete(&input).await.unwrap();
1007 assert_eq!(calls.len(), 1);
1008 assert_eq!(calls[0].function.name, "calc");
1009 assert_eq!(calls[0].function.arguments, "{}");
1010 }
1011
1012 #[test]
1013 fn test_has_tool_markers() {
1014 let parser = KimiK3Parser::new();
1015 assert!(parser.has_tool_markers(&_tools(&[])));
1016 assert!(!parser.has_tool_markers(&_response("hi")));
1017 assert!(!parser.has_tool_markers("plain text"));
1018 }
1019
1020 #[tokio::test]
1021 async fn test_streaming_split_markers_do_not_leak() {
1022 let mut parser = KimiK3Parser::new();
1023 let hi_chunk = format!("{SEP}Hi");
1024 let call_open_chunk = format!(r#"{OPEN}call tool="calc" index="1"{SEP}"#);
1025 let arg_chunk = _arg("x", "number", "1");
1026 let close_call_chunk = format!("{CLOSE}call");
1027
1028 let chunks: [&str; 10] = [
1029 OPEN,
1030 "response",
1031 hi_chunk.as_str(),
1032 OPEN,
1033 "tools",
1034 SEP,
1035 call_open_chunk.as_str(),
1036 arg_chunk.as_str(),
1037 close_call_chunk.as_str(),
1038 SEP,
1039 ];
1040
1041 let mut content = String::new();
1042 let mut calls = Vec::new();
1043 for chunk in chunks {
1044 let result = parser.parse_incremental(chunk, &[]).await.unwrap();
1045 content.push_str(&result.normal_text);
1046 calls.extend(result.calls);
1047 }
1048
1049 assert_eq!(content, "Hi");
1050 assert!(!content.contains(OPEN));
1051 assert!(!content.contains(SEP));
1052 assert_eq!(calls.len(), 1);
1053 assert_eq!(calls[0].name.as_deref(), Some("calc"));
1054 let args: Value = serde_json::from_str(&calls[0].parameters).unwrap();
1055 assert_eq!(args, serde_json::json!({"x": 1}));
1056 }
1057
1058 #[tokio::test]
1059 async fn test_streaming_consumed_response_prefix_no_call_keeps_content() {
1060 let mut parser = KimiK3Parser::new();
1061 let close_response_chunk = format!("response{SEP}");
1062 let chunks: [&str; 4] = ["O", "K", CLOSE, close_response_chunk.as_str()];
1063
1064 let mut content = String::new();
1065 for chunk in chunks {
1066 let result = parser.parse_incremental(chunk, &[]).await.unwrap();
1067 assert!(!result.normal_text.contains(CLOSE));
1068 content.push_str(&result.normal_text);
1069 }
1070 assert_eq!(content, "OK");
1071 }
1072
1073 #[tokio::test]
1074 async fn test_streaming_multiple_tool_calls_increment_tool_index() {
1075 let mut parser = KimiK3Parser::new();
1076 let input = _tools(&[
1077 _call("first", 1, &[_arg("a", "number", "1")]),
1078 _call("second", 2, &[_arg("b", "number", "2")]),
1079 ]);
1080
1081 let result = parser.parse_incremental(&input, &[]).await.unwrap();
1082 assert_eq!(result.calls.len(), 2);
1083 assert_eq!(result.calls[0].tool_index, 0);
1084 assert_eq!(result.calls[1].tool_index, 1);
1085 }
1086
1087 #[tokio::test]
1088 async fn test_streaming_duplicate_xtml_index_uses_ordinal() {
1089 let mut parser = KimiK3Parser::new();
1093 let input = _tools(&[
1094 _call("first", 7, &[_arg("a", "number", "1")]),
1095 _call("second", 7, &[_arg("b", "number", "2")]),
1096 _call("third", 3, &[_arg("c", "number", "3")]),
1097 ]);
1098
1099 let result = parser.parse_incremental(&input, &[]).await.unwrap();
1100 assert_eq!(result.calls.len(), 3);
1101 assert_eq!(result.calls[0].tool_index, 0);
1102 assert_eq!(result.calls[1].tool_index, 1);
1103 assert_eq!(result.calls[2].tool_index, 2);
1104 }
1105
1106 #[tokio::test]
1107 async fn test_reset_clears_streaming_state() {
1108 let mut parser = KimiK3Parser::new();
1109
1110 let primed = format!("{OPEN}response{SEP}Hello{CLOSE}response{SEP}")
1111 + &_tools(&[_call("calc", 1, &[_arg("x", "number", "1")])]);
1112 let primed_result = parser.parse_incremental(&primed, &[]).await.unwrap();
1113 assert_eq!(primed_result.calls.len(), 1);
1114
1115 parser.reset();
1116
1117 let fresh_content = format!("{OPEN}response{SEP}Fresh{CLOSE}response{SEP}");
1118 let content_result = parser.parse_incremental(&fresh_content, &[]).await.unwrap();
1119 assert_eq!(content_result.normal_text, "Fresh");
1120
1121 let fresh_call = _tools(&[_call("calc", 1, &[_arg("x", "number", "2")])]);
1122 let call_result = parser.parse_incremental(&fresh_call, &[]).await.unwrap();
1123 assert_eq!(call_result.calls.len(), 1);
1124 assert_eq!(call_result.calls[0].name.as_deref(), Some("calc"));
1125 }
1126
1127 #[test]
1128 fn test_factory_resolves_moonshot_kimi_k3() {
1129 let factory = crate::factory::ParserFactory::new();
1130 assert_eq!(
1131 factory
1132 .registry()
1133 .resolve_model_to_parser("moonshotai/Kimi-K3"),
1134 Some("kimi_k3".to_string())
1135 );
1136 }
1137
1138 #[test]
1139 fn test_factory_resolves_underscore_kimi_k3() {
1140 let factory = crate::factory::ParserFactory::new();
1141 for id in ["kimi_k3", "Kimi_K3", "moonshotai/Kimi_K3"] {
1142 assert_eq!(
1143 factory.registry().resolve_model_to_parser(id),
1144 Some("kimi_k3".to_string()),
1145 "expected `{id}` to resolve to the kimi_k3 parser"
1146 );
1147 }
1148 }
1149
1150 #[test]
1151 fn test_factory_registers_kimi_k3_structural_tag() {
1152 let factory = crate::factory::ParserFactory::new();
1153 assert!(factory.registry().has_structural_tag("kimi_k3"));
1154 }
1155
1156 fn sample_tools() -> Vec<Tool> {
1157 serde_json::from_value(serde_json::json!([{
1158 "type": "function",
1159 "function": {
1160 "name": "get_weather",
1161 "parameters": {
1162 "type": "object",
1163 "properties": {
1164 "city": {"type": "string"},
1165 "days": {"type": "integer"},
1166 "units": {"type": "string", "enum": ["c", "f"]}
1167 },
1168 "required": ["city", "days"]
1169 }
1170 }
1171 }]))
1172 .unwrap()
1173 }
1174
1175 fn tag_for_params(params: serde_json::Value) -> Value {
1177 let tools: Vec<Tool> = serde_json::from_value(serde_json::json!([{
1178 "type": "function",
1179 "function": { "name": "f", "parameters": params }
1180 }]))
1181 .unwrap();
1182 KimiK3Parser::build_structural_tag(&tools, true)
1183 }
1184
1185 fn embedded_schemas(node: &Value, out: &mut Vec<Value>) {
1187 match node {
1188 Value::Object(map) => {
1189 if map.get("type").and_then(Value::as_str) == Some("json_schema") {
1190 if let Some(schema) = map.get("json_schema") {
1191 out.push(schema.clone());
1192 }
1193 }
1194 for value in map.values() {
1195 embedded_schemas(value, out);
1196 }
1197 }
1198 Value::Array(items) => {
1199 for item in items {
1200 embedded_schemas(item, out);
1201 }
1202 }
1203 _ => {}
1204 }
1205 }
1206
1207 fn schemas_in(tag: &Value) -> Vec<Value> {
1208 let mut out = Vec::new();
1209 embedded_schemas(tag, &mut out);
1210 out
1211 }
1212
1213 #[test]
1223 fn nested_ref_inside_array_items_carries_defs() {
1224 let tag = tag_for_params(serde_json::json!({
1225 "type": "object",
1226 "$defs": { "Tag": {"type": "string"} },
1227 "properties": {
1228 "tags": {"type": "array", "items": {"$ref": "#/$defs/Tag"}}
1229 },
1230 "required": ["tags"]
1231 }));
1232
1233 let schemas = schemas_in(&tag);
1234 assert!(!schemas.is_empty(), "array argument should embed a schema");
1235 for schema in &schemas {
1236 assert!(
1237 schema.get("$defs").is_some(),
1238 "embedded schema must carry $defs: {schema}"
1239 );
1240 }
1241 }
1242
1243 #[test]
1244 fn bare_ref_property_resolves_to_its_definition() {
1245 let tag = tag_for_params(serde_json::json!({
1246 "type": "object",
1247 "$defs": {
1248 "Address": {
1249 "type": "object",
1250 "properties": {"city": {"type": "string"}}
1251 }
1252 },
1253 "properties": { "address": {"$ref": "#/$defs/Address"} },
1254 "required": ["address"]
1255 }));
1256
1257 let rendered = serde_json::to_string(&tag).unwrap();
1260 assert!(
1261 rendered.contains(r#"type=\"object\""#),
1262 "bare $ref should classify as object: {rendered}"
1263 );
1264 assert!(!schemas_in(&tag).is_empty());
1265 }
1266
1267 #[test]
1268 fn recursive_definition_does_not_loop() {
1269 let tag = tag_for_params(serde_json::json!({
1272 "type": "object",
1273 "$defs": {
1274 "Node": {
1275 "type": "object",
1276 "properties": {
1277 "value": {"type": "string"},
1278 "next": {"anyOf": [{"type": "null"}, {"$ref": "#/$defs/Node"}]}
1279 }
1280 }
1281 },
1282 "properties": { "node": {"$ref": "#/$defs/Node"} },
1283 "required": ["node"]
1284 }));
1285
1286 for schema in schemas_in(&tag) {
1287 assert!(
1288 schema.get("$defs").is_some(),
1289 "recursive schema must keep $defs: {schema}"
1290 );
1291 }
1292 }
1293
1294 #[test]
1295 fn pointer_into_a_definition_subpath_resolves() {
1296 let tag = tag_for_params(serde_json::json!({
1298 "type": "object",
1299 "$defs": {
1300 "group": {
1301 "type": "array",
1302 "items": {"type": "object", "properties": {"data": {"type": "string"}}}
1303 }
1304 },
1305 "properties": { "root": {"$ref": "#/$defs/group/items"} },
1306 "required": ["root"]
1307 }));
1308
1309 let rendered = serde_json::to_string(&tag).unwrap();
1310 assert!(
1311 rendered.contains(r#"type=\"object\""#),
1312 "sub-path pointer should resolve to the object it names: {rendered}"
1313 );
1314 }
1315
1316 #[test]
1317 fn draft07_definitions_block_is_also_carried() {
1318 let tag = tag_for_params(serde_json::json!({
1319 "type": "object",
1320 "definitions": { "Item": {"type": "string"} },
1321 "properties": {
1322 "items": {"type": "array", "items": {"$ref": "#/definitions/Item"}}
1323 },
1324 "required": ["items"]
1325 }));
1326
1327 for schema in schemas_in(&tag) {
1328 assert!(
1329 schema.get("definitions").is_some(),
1330 "embedded schema must carry definitions: {schema}"
1331 );
1332 }
1333 }
1334
1335 #[test]
1336 fn schema_without_refs_is_left_untouched() {
1337 let tag = tag_for_params(serde_json::json!({
1338 "type": "object",
1339 "$defs": { "Unused": {"type": "string"} },
1340 "properties": { "payload": {"type": "object", "properties": {"a": {"type": "string"}}} },
1341 "required": ["payload"]
1342 }));
1343
1344 for schema in schemas_in(&tag) {
1345 assert!(
1346 schema.get("$defs").is_none(),
1347 "a schema with no $ref should not be padded with $defs: {schema}"
1348 );
1349 }
1350 }
1351
1352 #[test]
1353 fn dynamic_ref_spelling_also_carries_defs() {
1354 let tag = tag_for_params(serde_json::json!({
1357 "type": "object",
1358 "$defs": { "Item": {"type": "string"} },
1359 "properties": {
1360 "items": {"type": "array", "items": {"$dynamicRef": "#/$defs/Item"}}
1361 },
1362 "required": ["items"]
1363 }));
1364
1365 for schema in schemas_in(&tag) {
1366 assert!(
1367 schema.get("$defs").is_some(),
1368 "$dynamicRef must also pull the block across: {schema}"
1369 );
1370 }
1371 }
1372
1373 #[test]
1374 fn child_defs_block_keeps_root_definitions_too() {
1375 let tag = tag_for_params(serde_json::json!({
1378 "type": "object",
1379 "$defs": { "Root": {"type": "string"} },
1380 "properties": {
1381 "p": {
1382 "type": "object",
1383 "$defs": { "Local": {"type": "number"} },
1384 "properties": { "x": {"$ref": "#/$defs/Root"} }
1385 }
1386 },
1387 "required": ["p"]
1388 }));
1389
1390 let schemas = schemas_in(&tag);
1391 assert!(!schemas.is_empty());
1392 for schema in &schemas {
1393 let defs = schema["$defs"].as_object().expect("block present");
1394 assert!(defs.contains_key("Local"), "own entry kept: {schema}");
1395 assert!(defs.contains_key("Root"), "root entry merged in: {schema}");
1396 }
1397 }
1398
1399 #[test]
1400 fn root_pointer_resolves_to_the_parameter_schema() {
1401 let tag = tag_for_params(serde_json::json!({
1403 "type": "object",
1404 "properties": {
1405 "name": {"type": "string"},
1406 "child": {"$ref": "#"}
1407 },
1408 "required": ["child"]
1409 }));
1410
1411 let rendered = serde_json::to_string(&tag).unwrap();
1412 assert!(
1413 rendered.contains(r#"key=\"child\" type=\"object\""#),
1414 "root pointer should classify as object: {rendered}"
1415 );
1416 }
1417
1418 #[test]
1419 fn sibling_type_wins_over_the_referenced_definition() {
1420 let tag = tag_for_params(serde_json::json!({
1423 "type": "object",
1424 "$defs": { "Payload": {"properties": {"a": {"type": "string"}}} },
1425 "properties": {
1426 "p": {"$ref": "#/$defs/Payload", "type": "object"}
1427 },
1428 "required": ["p"]
1429 }));
1430
1431 let rendered = serde_json::to_string(&tag).unwrap();
1432 assert!(
1433 rendered.contains(r#"key=\"p\" type=\"object\""#),
1434 "sibling type should be honoured: {rendered}"
1435 );
1436 for schema in schemas_in(&tag) {
1438 assert_eq!(schema["$ref"], "#/$defs/Payload");
1439 assert!(schema.get("$defs").is_some());
1440 }
1441 }
1442
1443 #[test]
1444 fn nested_dangling_pointer_falls_back_to_permissive() {
1445 let tag = tag_for_params(serde_json::json!({
1448 "type": "object",
1449 "$defs": { "Tag": {"type": "string"} },
1450 "properties": {
1451 "tags": {"type": "array", "items": {"$ref": "#/$defs/Missing"}}
1452 },
1453 "required": ["tags"]
1454 }));
1455
1456 assert!(
1457 schemas_in(&tag).is_empty(),
1458 "an unresolvable nested pointer must not be embedded"
1459 );
1460 }
1461
1462 #[test]
1463 fn dangling_ref_falls_back_to_permissive() {
1464 let tag = tag_for_params(serde_json::json!({
1467 "type": "object",
1468 "properties": { "x": {"$ref": "#/$defs/Missing"} },
1469 "required": ["x"]
1470 }));
1471 assert!(schemas_in(&tag).is_empty());
1472 }
1473
1474 #[test]
1475 fn test_build_structural_tag_frames_tools_section() {
1476 let tag = KimiK3Parser::build_structural_tag(&sample_tools(), true);
1477 let format = &tag["format"];
1478 assert_eq!(format["type"], "triggered_tags");
1479 assert_eq!(format["triggers"][0], TOOLS_OPEN);
1480 assert_eq!(format["at_least_one"], true);
1481 assert!(format.get("stop_after_first").is_none());
1484
1485 let section = &format["tags"][0];
1486 assert_eq!(section["begin"], TOOLS_OPEN);
1487 assert_eq!(section["end"], TOOLS_CLOSE);
1488 assert_eq!(section["content"]["type"], "plus");
1490 }
1491
1492 #[test]
1493 fn test_build_structural_tag_allows_closing_prefilled_channel() {
1494 let tag = KimiK3Parser::build_structural_tag(&sample_tools(), true);
1499 let format = &tag["format"];
1500
1501 let triggers: Vec<&str> = format["triggers"]
1502 .as_array()
1503 .unwrap()
1504 .iter()
1505 .map(|t| t.as_str().unwrap())
1506 .collect();
1507 assert!(triggers.contains(&THINK_CLOSE), "got: {triggers:?}");
1508 assert!(triggers.contains(&RESPONSE_CLOSE), "got: {triggers:?}");
1509 assert!(triggers.contains(&TOOLS_OPEN), "got: {triggers:?}");
1510
1511 let begins: Vec<&str> = format["tags"]
1512 .as_array()
1513 .unwrap()
1514 .iter()
1515 .map(|t| t["begin"].as_str().unwrap())
1516 .collect();
1517 assert_eq!(
1518 begins,
1519 vec![
1520 TOOLS_OPEN,
1521 &format!("{THINK_CLOSE}{TOOLS_OPEN}")[..],
1522 &format!("{RESPONSE_CLOSE}{TOOLS_OPEN}")[..],
1523 ]
1524 );
1525
1526 for section in format["tags"].as_array().unwrap() {
1528 assert_eq!(section["end"], TOOLS_CLOSE);
1529 assert_eq!(section["content"]["type"], "plus");
1530 }
1531 }
1532
1533 #[tokio::test]
1534 async fn test_parse_complete_handles_think_close_prefixed_tools_section() {
1535 let parser = KimiK3Parser::new();
1538 let input = THINK_CLOSE.to_string()
1539 + &_tools(&[_call("get_weather", 1, &[_arg("city", "string", "SF")])]);
1540
1541 let (_content, calls) = parser.parse_complete(&input).await.unwrap();
1542 assert_eq!(calls.len(), 1);
1543 assert_eq!(calls[0].function.name, "get_weather");
1544 let args: Value = serde_json::from_str(&calls[0].function.arguments).unwrap();
1545 assert_eq!(args, serde_json::json!({"city": "SF"}));
1546 }
1547
1548 #[test]
1549 fn test_build_structural_tag_pins_call_framing_and_tool_name() {
1550 let tag = KimiK3Parser::build_structural_tag(&sample_tools(), true);
1551 let call = &tag["format"]["tags"][0]["content"]["content"]["elements"][0];
1552 assert_eq!(call["type"], "sequence");
1553 let elems = call["elements"].as_array().unwrap();
1554 assert_eq!(
1555 elems[0]["value"],
1556 "<|open|>call tool=\"get_weather\" index=\""
1557 );
1558 assert_eq!(elems[1]["type"], "regex");
1559 assert_eq!(elems[2]["value"], "\"<|sep|>");
1560 assert_eq!(elems[4]["value"], "<|close|>call<|sep|>");
1561 }
1562
1563 #[test]
1564 fn test_build_structural_tag_strict_arguments_by_type() {
1565 let tag = KimiK3Parser::build_structural_tag(&sample_tools(), true);
1566 let call = &tag["format"]["tags"][0]["content"]["content"]["elements"][0];
1567 let args = &call["elements"][3];
1568 assert_eq!(args["type"], "sequence");
1569 let arg_elems = args["elements"].as_array().unwrap();
1570 assert_eq!(arg_elems.len(), 3);
1572
1573 assert_eq!(arg_elems[0]["type"], "sequence");
1575 assert_eq!(
1576 arg_elems[0]["elements"][0]["value"],
1577 "<|open|>argument key=\"city\" type=\"string\"<|sep|>"
1578 );
1579 assert_eq!(arg_elems[0]["elements"][1]["type"], "any_text");
1580
1581 assert_eq!(
1583 arg_elems[1]["elements"][0]["value"],
1584 "<|open|>argument key=\"days\" type=\"number\"<|sep|>"
1585 );
1586 assert_eq!(arg_elems[1]["elements"][1]["type"], "json_schema");
1587
1588 assert_eq!(arg_elems[2]["type"], "optional");
1590 let units = &arg_elems[2]["content"];
1591 assert_eq!(
1592 units["elements"][0]["value"],
1593 "<|open|>argument key=\"units\" type=\"string\"<|sep|>"
1594 );
1595 assert_eq!(units["elements"][1]["type"], "or");
1596 assert_eq!(units["elements"][1]["elements"][0]["value"], "c");
1597 assert_eq!(units["elements"][1]["elements"][1]["value"], "f");
1598 }
1599
1600 #[test]
1601 fn test_build_structural_tag_escapes_attribute_values() {
1602 let tools: Vec<Tool> = serde_json::from_value(serde_json::json!([{
1603 "type": "function",
1604 "function": {
1605 "name": "a&b\"c",
1606 "parameters": {
1607 "type": "object",
1608 "properties": {"k\"q": {"type": "string"}},
1609 "required": ["k\"q"]
1610 }
1611 }
1612 }]))
1613 .unwrap();
1614 let tag = KimiK3Parser::build_structural_tag(&tools, true);
1615 let call = &tag["format"]["tags"][0]["content"]["content"]["elements"][0];
1616 assert_eq!(
1617 call["elements"][0]["value"],
1618 "<|open|>call tool=\"a&b"c\" index=\""
1619 );
1620 let arg = &call["elements"][3]["elements"][0];
1621 assert_eq!(
1622 arg["elements"][0]["value"],
1623 "<|open|>argument key=\"k"q\" type=\"string\"<|sep|>"
1624 );
1625 }
1626
1627 #[test]
1628 fn test_build_structural_tag_falls_back_when_no_properties() {
1629 let tools: Vec<Tool> = serde_json::from_value(serde_json::json!([{
1630 "type": "function",
1631 "function": {"name": "ping", "parameters": {"type": "object"}}
1632 }]))
1633 .unwrap();
1634 let tag = KimiK3Parser::build_structural_tag(&tools, false);
1635 assert_eq!(tag["format"]["at_least_one"], false);
1636 let call = &tag["format"]["tags"][0]["content"]["content"]["elements"][0];
1637 assert_eq!(call["elements"][3]["type"], "star");
1639 }
1640}