1use superfluid_abi::Status;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
6pub enum TurnState {
7 AfterInput,
8 MidTurn,
9 AfterFinishedTurn,
10}
11
12#[derive(Debug, Clone, Default)]
13pub struct ToolCallMsg {
14 pub id: String,
15 pub name: String,
16 pub arguments: String,
17}
18
19#[derive(Debug, Clone, Default)]
20pub struct ChatMessage {
21 pub role: u32,
22 pub content: String,
23 pub parts: Vec<ContentPart>,
24 pub tool_calls: Vec<ToolCallMsg>,
25 pub tool_call_id: Option<String>,
26 pub name: Option<String>,
27 pub reasoning: Option<String>,
28}
29
30#[derive(Debug, Clone, PartialEq, Eq)]
31pub enum ContentPart {
32 Text(String),
33 Image { blob: String },
34 Audio { blob: String },
35}
36
37impl ChatMessage {
38 pub fn new(role: u32, content: impl Into<String>) -> ChatMessage {
39 ChatMessage { role, content: content.into(), ..Default::default() }
40 }
41}
42
43pub(crate) fn thinking_probe_contexts() -> Vec<(Vec<ChatMessage>, Vec<String>)> {
44 let user = ChatMessage::new(crate::wal::role::USER, "probe");
45 let system = ChatMessage::new(crate::wal::role::SYSTEM, "probe");
46 let tool = r#"{"type":"function","function":{"name":"probe","description":"probe","parameters":{"type":"object","properties":{}}}}"#;
47 vec![
48 (vec![user.clone()], Vec::new()),
49 (vec![system.clone(), user.clone()], Vec::new()),
50 (vec![system, user], vec![tool.to_string()]),
51 ]
52}
53
54#[cfg_attr(not(feature = "basert"), allow(dead_code))]
55pub(crate) fn thinking_switch_changes<T: PartialEq>(
56 render: impl Fn(&[ChatMessage], &[String], bool) -> Option<T>,
57) -> bool {
58 thinking_probe_contexts().iter().any(|(msgs, tools)| {
59 match (render(msgs, tools, true), render(msgs, tools, false)) {
60 (Some(on), Some(off)) => on != off,
61 (Some(_), None) | (None, Some(_)) => true,
62 (None, None) => false,
63 }
64 })
65}
66
67pub trait TextCodec: Send {
68 fn encode(&self, text: &str) -> Vec<u32>;
69
70 fn encode_content(&self, text: &str) -> Vec<u32> {
71 self.encode(text)
72 }
73
74 fn encode_pieces(&self, pieces: &[(&str, bool)]) -> Vec<u32> {
75 let mut out = Vec::new();
76 for (text, content) in pieces {
77 out.extend(if *content { self.encode_content(text) } else { self.encode(text) });
78 }
79 out
80 }
81 fn token_bytes(&self, token: u32) -> Vec<u8>;
82
83 fn decode(&self, tokens: &[u32]) -> String {
84 let mut s = Utf8Stream::default();
85 let mut out = String::new();
86 for &t in tokens {
87 out.push_str(&s.push(&self.token_bytes(t)));
88 }
89 out.push_str(&s.flush());
90 out
91 }
92
93 fn stream_prologue(&self) -> Vec<u32> {
94 Vec::new()
95 }
96
97 fn behavior_fingerprint(&self) -> Option<superfluid_fingerprint::BehaviorFingerprint> {
98 None
99 }
100
101 fn chat_template_source(&self) -> Option<String> {
102 None
103 }
104
105 fn render_message(&self, _role: u32, _text: &str) -> Option<Vec<u32>> {
106 None
107 }
108
109 fn generation_prefix(&self, _state: TurnState) -> Option<Vec<u32>> {
110 None
111 }
112
113 fn trailing_generation_prompt(
114 &self,
115 _messages: &[ChatMessage],
116 _tools: &[String],
117 kwargs: &serde_json::Map<String, serde_json::Value>,
118 span: &[u32],
119 ) -> usize {
120 let thinking = kwargs.get("enable_thinking").and_then(|v| v.as_bool());
121 [
122 self.generation_prefix_with(TurnState::AfterInput, thinking),
123 self.generation_prefix(TurnState::AfterInput),
124 ]
125 .into_iter()
126 .flatten()
127 .find(|op| !op.is_empty() && span.len() > op.len() && span.ends_with(op))
128 .map_or(0, |op| op.len())
129 }
130
131 fn empty_think_tokens(&self) -> Option<Vec<u32>> {
132 None
133 }
134
135 fn generation_prefix_with(&self, state: TurnState, thinking: Option<bool>) -> Option<Vec<u32>> {
136 let mut op = self.generation_prefix(state)?;
137 if thinking == Some(false) {
138 if let Some(block) = self.empty_think_tokens() {
139 op.extend(block);
140 }
141 }
142 Some(op)
143 }
144
145 fn render_system_full(
146 &self,
147 system: Option<&str>,
148 tool_jsons: &[String],
149 _kwargs: &serde_json::Map<String, serde_json::Value>,
150 ) -> Option<Vec<u32>> {
151 self.render_system_with_tools(system, tool_jsons)
152 }
153
154 fn supports_reasoning_effort(&self) -> bool {
155 false
156 }
157
158 fn supports_enable_thinking(&self) -> bool {
159 false
160 }
161
162 fn reasoning_effort_levels(&self) -> Vec<&'static str> {
163 Vec::new()
164 }
165
166 fn validate_template_kwargs(
167 &self,
168 _kwargs: &serde_json::Map<String, serde_json::Value>,
169 ) -> Result<(), String> {
170 Ok(())
171 }
172
173 fn render_prompt_structured_with(
174 &self,
175 messages: &[ChatMessage],
176 tools: &[String],
177 _kwargs: &serde_json::Map<String, serde_json::Value>,
178 ) -> Option<Vec<u32>> {
179 self.render_prompt_structured(messages, tools)
180 }
181
182 fn template_refusal(
183 &self,
184 _messages: &[ChatMessage],
185 _tools: &[String],
186 _kwargs: &serde_json::Map<String, serde_json::Value>,
187 ) -> Option<String> {
188 None
189 }
190
191 fn turn_separator(&self) -> Option<Vec<u32>> {
192 None
193 }
194
195 fn channel_markers(&self) -> Vec<(u32, u32, u32)> {
196 Vec::new()
197 }
198
199 fn render_system_with_tools(
200 &self,
201 _system: Option<&str>,
202 _tool_jsons: &[String],
203 ) -> Option<Vec<u32>> {
204 None
205 }
206
207 fn render_tool_result(&self, _name: &str, _content: &str) -> Option<Vec<u32>> {
208 None
209 }
210
211 fn render_assistant_with_tool_calls(
212 &self,
213 _content: &str,
214 _calls: &[(String, String)],
215 ) -> Option<Vec<u32>> {
216 None
217 }
218
219 fn render_assistant_with_tool_calls_after_query(
223 &self,
224 content: &str,
225 calls: &[(String, String)],
226 ) -> Option<Vec<u32>> {
227 self.render_assistant_with_tool_calls(content, calls)
228 }
229
230 fn render_fim(&self, _prefix: &str, _suffix: &str, _mode: u8) -> Option<Vec<u32>> {
231 None
232 }
233
234 fn render_turn_with_tokens(
235 &self,
236 _role: u32,
237 _pre_text: &str,
238 _inner: &[u32],
239 _post_text: &str,
240 ) -> Option<Vec<u32>> {
241 None
242 }
243
244 fn render_block(&self, role: u32, kind: u32, payload: &str) -> Option<Vec<u32>> {
245 let text = project_block_text(kind, payload)?;
246 if text.is_empty() {
247 return Some(Vec::new());
248 }
249 self.render_message(role, &text)
250 }
251
252 fn channelizer(&self) -> Box<dyn Channelizer> {
253 Box::new(Segmenter::new(self.channel_markers()))
254 }
255
256 fn turn_terminators(&self) -> Vec<u32> {
257 Vec::new()
258 }
259
260 fn render_prompt_structured(
261 &self,
262 messages: &[ChatMessage],
263 tools: &[String],
264 ) -> Option<Vec<u32>> {
265 let flat: Vec<(u32, String)> =
266 messages.iter().map(|m| (m.role, m.content.clone())).collect();
267 self.render_prompt(&flat, tools)
268 }
269
270 fn render_prompt(&self, messages: &[(u32, String)], tools: &[String]) -> Option<Vec<u32>> {
271 let mut out = self.stream_prologue();
272 let mut tools_emitted = tools.is_empty();
273 for (role, text) in messages {
274 if !tools_emitted && *role == crate::wal::role::SYSTEM {
275 out.extend(self.render_system_with_tools(Some(text), tools)?);
276 tools_emitted = true;
277 } else {
278 out.extend(self.render_message(*role, text)?);
279 }
280 }
281 if !tools_emitted {
282 let mut with = self.render_system_with_tools(None, tools)?;
283 with.extend(std::mem::take(&mut out));
284 out = with;
285 }
286 if let Some(op) = self.generation_prefix(TurnState::AfterInput) {
287 out.extend(op);
288 }
289 Some(out)
290 }
291
292 fn call_grammar(&self, tool_jsons: &[String]) -> Option<String> {
293 self.tool_envelope().call_grammar(tool_jsons)
294 }
295
296 fn renders_per_message(&self) -> bool {
297 true
298 }
299
300 fn tool_envelope(&self) -> ToolEnvelope {
301 ToolEnvelope::Json
302 }
303
304 fn tool_fragment_mode(&self) -> crate::tool_fragment::ToolFragmentMode {
305 match self.tool_envelope() {
306 ToolEnvelope::Json => crate::tool_fragment::ToolFragmentMode::Json,
307 _ => crate::tool_fragment::ToolFragmentMode::None,
308 }
309 }
310
311 fn tool_call_delimiters(&self) -> Option<(String, String)> {
312 let (open, close, _) = self
313 .channel_markers()
314 .into_iter()
315 .find(|&(_, _, ch)| ch == crate::wal::channel::TOOL_CALL)?;
316 Some((self.marker_literal(open), self.marker_literal(close)))
317 }
318
319 fn marker_literal(&self, token: u32) -> String {
320 self.decode(&[token])
321 }
322
323 fn structural_tag(&self, tool_jsons: &[String], at_least_one: bool) -> Option<String> {
324 let (begin, end) = self.tool_call_delimiters()?;
325 self.tool_envelope().structural_tag(tool_jsons, &begin, &end, at_least_one)
326 }
327
328 fn response_format_tag(&self, json_schema: &str) -> Option<String> {
329 let _ = json_schema;
330 None
331 }
332
333 fn parse_tool_call(&self, raw: &str) -> Option<(String, String)> {
334 self.parse_tool_call_with(raw, None)
335 }
336
337 fn parse_tool_call_with(&self, raw: &str, schemas: Option<&ToolSchemas>) -> Option<(String, String)> {
338 self.tool_envelope().parse_call(raw, schemas)
339 }
340
341 fn named_tool_call(&self, raw: &str) -> Option<(String, String)> {
342 self.tool_envelope().named_call(raw)
343 }
344}
345
346#[derive(Debug, Clone, PartialEq)]
347pub struct Run {
348 pub span: Vec<u32>,
349 pub text: Vec<u32>,
350 pub channel: u32,
351 pub closes: bool,
352}
353
354pub(crate) fn repair_json_object(raw: &str) -> Option<String> {
355 let unfenced = strip_code_fence(raw).trim();
356 let mut attempts: Vec<String> = vec![unfenced.to_string()];
357 if let Some(obj) = first_json_object(unfenced) {
358 attempts.push(obj.to_string());
359 }
360 for i in 0..attempts.len() {
361 let a = attempts[i].clone();
362 let fixed = drop_trailing_commas(&a);
363 if fixed != a {
364 attempts.push(fixed.clone());
365 }
366 if let Some(closed) = close_unterminated(&a) {
367 attempts.push(closed.clone());
368 let closed_fixed = drop_trailing_commas(&closed);
369 if closed_fixed != closed {
370 attempts.push(closed_fixed);
371 }
372 }
373 }
374 attempts.iter().find_map(|a| match serde_json::from_str::<serde_json::Value>(a) {
375 Ok(v) if v.is_object() => Some(v.to_string()),
376 _ => None,
377 })
378}
379
380fn strip_code_fence(raw: &str) -> &str {
381 let t = raw.trim();
382 let Some(rest) = t.strip_prefix("```") else { return t };
383 let rest = match rest.find('\n') {
384 Some(i) => &rest[i + 1..],
385 None => rest,
386 };
387 rest.strip_suffix("```").unwrap_or(rest).trim()
388}
389
390fn first_json_object(raw: &str) -> Option<&str> {
391 let start = raw.find('{')?;
392 let (mut depth, mut in_str, mut esc) = (0i32, false, false);
393 for (i, c) in raw.char_indices().skip_while(|(i, _)| *i < start) {
394 if esc {
395 esc = false;
396 continue;
397 }
398 match c {
399 '\\' if in_str => esc = true,
400 '"' => in_str = !in_str,
401 '{' if !in_str => depth += 1,
402 '}' if !in_str => {
403 depth -= 1;
404 if depth == 0 {
405 return raw.get(start..i + c.len_utf8());
406 }
407 }
408 _ => {}
409 }
410 }
411 None
412}
413
414fn close_unterminated(raw: &str) -> Option<String> {
415 let mut stack: Vec<char> = Vec::new();
416 let (mut in_str, mut esc) = (false, false);
417 for c in raw.chars() {
418 if esc {
419 esc = false;
420 continue;
421 }
422 match c {
423 '\\' if in_str => esc = true,
424 '"' => in_str = !in_str,
425 '{' | '[' if !in_str => stack.push(c),
426 '}' | ']' if !in_str => {
427 stack.pop()?;
428 }
429 _ => {}
430 }
431 }
432 if stack.is_empty() && !in_str {
433 return None;
434 }
435 let mut out = raw.to_string();
436 if esc {
437 out.pop();
438 }
439 if in_str {
440 out.push('"');
441 }
442 while let Some(open) = stack.pop() {
443 out.push(if open == '{' { '}' } else { ']' });
444 }
445 Some(out)
446}
447
448fn drop_trailing_commas(raw: &str) -> String {
449 let mut out = String::with_capacity(raw.len());
450 let (mut in_str, mut esc) = (false, false);
451 let chars: Vec<char> = raw.chars().collect();
452 for (i, &c) in chars.iter().enumerate() {
453 if esc {
454 esc = false;
455 out.push(c);
456 continue;
457 }
458 match c {
459 '\\' if in_str => {
460 esc = true;
461 out.push(c);
462 }
463 '"' => {
464 in_str = !in_str;
465 out.push(c);
466 }
467 ',' if !in_str => {
468 let next = chars[i + 1..].iter().find(|c| !c.is_whitespace());
469 if matches!(next, Some('}') | Some(']')) {
470 continue;
471 }
472 out.push(c);
473 }
474 _ => out.push(c),
475 }
476 }
477 out
478}
479
480fn call_fields(v: &serde_json::Value) -> Option<(String, String)> {
481 let v = v.get("function").filter(|f| f.is_object()).unwrap_or(v);
482 let name = v.get("name")?.as_str()?.trim().to_string();
483 if name.is_empty() {
484 return None;
485 }
486 let args = v
487 .get("arguments")
488 .or_else(|| v.get("parameters"))
489 .or_else(|| v.get("args"))
490 .or_else(|| v.get("input"));
491 let args = match args {
492 None => "{}".to_string(),
493 Some(serde_json::Value::String(s)) => {
494 let inner: serde_json::Value = serde_json::from_str(s).ok()?;
495 inner.to_string()
496 }
497 Some(other) => other.to_string(),
498 };
499 Some((name, args))
500}
501
502pub fn parse_json_tool_call(raw: &str) -> Option<(String, String)> {
503 let unfenced = strip_code_fence(raw);
504 let mut attempts: Vec<String> = vec![unfenced.to_string()];
505 if let Some(obj) = first_json_object(unfenced) {
506 attempts.push(obj.to_string());
507 }
508 for i in 0..attempts.len() {
509 let a = attempts[i].clone();
510 let fixed = drop_trailing_commas(&a);
511 if fixed != a {
512 attempts.push(fixed.clone());
513 }
514 if let Some(closed) = close_unterminated(&a) {
515 attempts.push(closed.clone());
516 let closed_fixed = drop_trailing_commas(&closed);
517 if closed_fixed != closed {
518 attempts.push(closed_fixed);
519 }
520 }
521 }
522 for a in attempts {
523 if let Ok(v) = serde_json::from_str::<serde_json::Value>(a.trim()) {
524 if let Some(call) = call_fields(&v) {
525 return Some(call);
526 }
527 }
528 }
529 None
530}
531const GEMMA_QUOTE: [char; 5] = ['<', '|', '"', '|', '>'];
532
533fn gemma_at(cs: &[char], p: usize, pat: &[char]) -> bool {
534 p + pat.len() <= cs.len() && cs[p..p + pat.len()] == *pat
535}
536
537fn gemma_skip_ws(cs: &[char], p: &mut usize) {
538 while *p < cs.len() && matches!(cs[*p], ' ' | '\n' | '\t') {
539 *p += 1;
540 }
541}
542
543fn gemma_parse_key(cs: &[char], p: &mut usize) -> String {
544 let start = *p;
545 while *p < cs.len() && !matches!(cs[*p], ':' | ',' | '}' | ']') {
546 *p += 1;
547 }
548 cs[start..*p].iter().collect()
549}
550
551fn gemma_parse_string(cs: &[char], p: &mut usize) -> String {
552 *p += GEMMA_QUOTE.len();
553 let mut i = *p;
554 while i < cs.len() && !gemma_at(cs, i, &GEMMA_QUOTE) {
555 i += 1;
556 }
557 if i >= cs.len() {
558 let v: String = cs[*p..].iter().collect();
559 *p = cs.len();
560 return v;
561 }
562 let v: String = cs[*p..i].iter().collect();
563 *p = i + GEMMA_QUOTE.len();
564 v
565}
566
567const GEMMA_MAX_DEPTH: usize = 128;
568
569fn gemma_parse_value(cs: &[char], p: &mut usize, in_array: bool) -> serde_json::Value {
570 gemma_parse_value_at(cs, p, in_array, 0)
571}
572
573fn gemma_parse_value_at(cs: &[char], p: &mut usize, in_array: bool, depth: usize) -> serde_json::Value {
574 use serde_json::Value;
575 gemma_skip_ws(cs, p);
576 if *p >= cs.len() {
577 return Value::Null;
578 }
579 if depth > GEMMA_MAX_DEPTH {
580 *p = cs.len();
581 return Value::Null;
582 }
583 if gemma_at(cs, *p, &GEMMA_QUOTE) {
584 return Value::String(gemma_parse_string(cs, p));
585 }
586 if cs[*p] == '{' {
587 *p += 1;
588 let mut obj = serde_json::Map::new();
589 while *p < cs.len() && cs[*p] != '}' {
590 while *p < cs.len() && matches!(cs[*p], ' ' | ',' | '\n') {
591 *p += 1;
592 }
593 if *p >= cs.len() || cs[*p] == '}' {
594 break;
595 }
596 let mut k = gemma_parse_key(cs, p);
597 while k.ends_with(' ') || k.ends_with('\n') {
598 k.pop();
599 }
600 if *p >= cs.len() || cs[*p] != ':' {
601 break;
602 }
603 *p += 1;
604 let v = gemma_parse_value_at(cs, p, false, depth + 1);
605 obj.insert(k, v);
606 }
607 if *p < cs.len() && cs[*p] == '}' {
608 *p += 1;
609 }
610 return Value::Object(obj);
611 }
612 if cs[*p] == '[' {
613 *p += 1;
614 let mut arr = Vec::new();
615 while *p < cs.len() && cs[*p] != ']' {
616 while *p < cs.len() && matches!(cs[*p], ' ' | ',' | '\n') {
617 *p += 1;
618 }
619 if *p >= cs.len() || cs[*p] == ']' {
620 break;
621 }
622 arr.push(gemma_parse_value_at(cs, p, true, depth + 1));
623 }
624 if *p < cs.len() && cs[*p] == ']' {
625 *p += 1;
626 }
627 return Value::Array(arr);
628 }
629 let start = *p;
630 let (mut brace, mut bracket) = (0i32, 0i32);
631 while *p < cs.len() {
632 match cs[*p] {
633 '{' => brace += 1,
634 '}' => {
635 if brace == 0 {
636 break;
637 }
638 brace -= 1;
639 }
640 '[' => bracket += 1,
641 ']' => {
642 if bracket == 0 {
643 break;
644 }
645 bracket -= 1;
646 }
647 ',' if brace == 0 && bracket == 0 => {
648 if in_array {
649 break;
650 }
651 let mut q = *p + 1;
652 while q < cs.len() && matches!(cs[q], ' ' | '\n' | '\t') {
653 q += 1;
654 }
655 let ks = q;
656 while q < cs.len() && (cs[q].is_ascii_alphanumeric() || cs[q] == '_') {
657 q += 1;
658 }
659 if q > ks && q < cs.len() && cs[q] == ':' {
660 break;
661 }
662 }
663 _ => {}
664 }
665 *p += 1;
666 }
667 let lit: String = cs[start..*p].iter().collect();
668 let t = lit.trim();
669 match t {
670 "true" => Value::Bool(true),
671 "false" => Value::Bool(false),
672 "null" | "" => Value::Null,
673 _ => {
674 if let Ok(i) = t.parse::<i64>() {
675 return Value::Number(i.into());
676 }
677 if let Ok(f) = t.parse::<f64>() {
678 if let Some(n) = serde_json::Number::from_f64(f) {
679 return Value::Number(n);
680 }
681 }
682 Value::String(t.to_string())
683 }
684 }
685}
686
687pub fn trailing_open_channel(tokens: &[u32], markers: &[(u32, u32, u32)]) -> u32 {
688 let mut cur = crate::wal::channel::TEXT;
689 for &t in tokens {
690 if cur == crate::wal::channel::TEXT {
691 if let Some((_, _, ch)) = markers.iter().find(|(o, _, _)| *o == t) {
692 cur = *ch;
693 }
694 } else if markers.iter().any(|(_, c, ch)| *c == t && *ch == cur) {
695 cur = crate::wal::channel::TEXT;
696 }
697 }
698 cur
699}
700
701pub fn primed_channelizer(codec: &dyn TextCodec, prompt: &[u32]) -> Box<dyn Channelizer> {
702 let mut chan = codec.channelizer();
703 let open = trailing_open_channel(prompt, &codec.channel_markers());
704 if open != crate::wal::channel::TEXT {
705 chan.prime(open);
706 }
707 chan
708}
709
710#[cfg(test)]
711mod trailing_channel_tests {
712 use super::trailing_open_channel;
713 use crate::wal::channel;
714
715 #[test]
716 fn reports_the_channel_a_prompt_leaves_open() {
717 let m = [(10, 11, channel::REASONING), (20, 21, channel::TOOL_CALL)];
718 assert_eq!(trailing_open_channel(&[1, 2, 3], &m), channel::TEXT);
719 assert_eq!(trailing_open_channel(&[1, 2, 10, 5], &m), channel::REASONING);
720 assert_eq!(trailing_open_channel(&[1, 10, 5, 11, 5], &m), channel::TEXT);
721 assert_eq!(trailing_open_channel(&[10, 7, 11, 8, 20, 9, 21, 10], &m), channel::REASONING);
722 assert_eq!(trailing_open_channel(&[10, 21], &m), channel::REASONING);
723 }
724
725 #[test]
726 fn primed_channelizer_starts_where_the_prompt_left_off() {
727 use super::{primed_channelizer, MockChatCodec, MOCK_THINK_CLOSE, MOCK_THINK_OPEN};
728 let codec = MockChatCodec;
729 let generated = [7u32, 8, MOCK_THINK_CLOSE, 9];
730 let open = primed_channelizer(&codec, &[1, 2, MOCK_THINK_OPEN]).split(&generated);
731 assert_eq!(open[0].channel, channel::REASONING, "{open:?}");
732 assert_eq!(open.last().unwrap().channel, channel::TEXT, "{open:?}");
733 let closed =
734 primed_channelizer(&codec, &[1, MOCK_THINK_OPEN, MOCK_THINK_CLOSE]).split(&generated[..2]);
735 assert!(closed.iter().all(|r| r.channel == channel::TEXT), "{closed:?}");
736 }
737}
738
739#[derive(Clone)]
740pub struct Delims {
741 name_open: String,
742 name_close: String,
743 key_open: String,
744 key_close: String,
745 val_close: String,
746}
747
748pub(crate) const S_NAME: &str = "N1nfx";
749pub(crate) const S_K1: &str = "K1kax";
750pub(crate) const S_V1: &str = "V1vax";
751pub(crate) const S_K2: &str = "J2kbx";
752pub(crate) const S_V2: &str = "U2vbx";
753
754pub(crate) fn common_prefix(a: &str, b: &str) -> String {
755 let mut end = 0;
756 for (ca, cb) in a.chars().zip(b.chars()) {
757 if ca != cb {
758 break;
759 }
760 end += ca.len_utf8();
761 }
762 a[..end].to_string()
763}
764
765pub(crate) fn common_suffix(a: &str, b: &str) -> String {
766 let mut n = 0;
767 for (ca, cb) in a.chars().rev().zip(b.chars().rev()) {
768 if ca != cb {
769 break;
770 }
771 n += ca.len_utf8();
772 }
773 a[a.len() - n..].to_string()
774}
775
776pub(crate) fn learn_delims(two: &str, zero: Option<&str>, frame: Option<(&str, &str)>) -> Option<Delims> {
777 let at = |s: &str| two.find(s);
778 let inm = at(S_NAME)?;
779 let mut pairs = [
780 (at(S_K1)?, S_K1.len(), at(S_V1)?, S_V1.len()),
781 (at(S_K2)?, S_K2.len(), at(S_V2)?, S_V2.len()),
782 ];
783 pairs.sort_by_key(|p| p.0);
784 let (ika, kalen, iva, valen) = pairs[0];
785 let (ikb, kblen, ivb, vblen) = pairs[1];
786 if !(ika < iva && iva < ikb && ikb < ivb) {
787 return None;
788 }
789
790 let anchor_end = frame
791 .and_then(|(o, _)| two[..inm].rfind(o).map(|i| i + o.len()))
792 .unwrap_or_else(|| two[..inm].rfind('\n').map(|i| i + 1).unwrap_or(0));
793 let name_open = two[anchor_end..inm].trim().to_string();
794
795 let after_name_two = &two[inm + S_NAME.len()..];
796 let name_close = match zero.and_then(|z| z.find(S_NAME).map(|i| &z[i + S_NAME.len()..])) {
797 Some(after_name_zero) => common_prefix(after_name_two, after_name_zero),
798 None => {
799 after_name_two[..after_name_two.find(&two[ika..ika + kalen]).unwrap_or(after_name_two.len())].to_string()
800 }
801 };
802
803 let mut key_open = common_suffix(&two[..ika], &two[..ikb]);
804 let key_close = common_prefix(&two[ika + kalen..], &two[ikb + kblen..]);
805 let val_close = common_prefix(&two[iva + valen..], &two[ivb + vblen..]);
806
807 while !name_close.is_empty() && key_open.starts_with(&name_close) {
808 key_open = key_open[name_close.len()..].to_string();
809 }
810
811 if name_open.is_empty() || name_close.is_empty() || key_open.is_empty() || key_close.is_empty() {
812 return None;
813 }
814 Some(Delims {
815 name_open,
816 name_close,
817 key_open,
818 key_close,
819 val_close,
820 })
821}
822
823pub(crate) fn typed_value(s: &str) -> serde_json::Value {
824 let t = s.trim();
825 match serde_json::from_str::<serde_json::Value>(t) {
826 Ok(v @ (serde_json::Value::Object(_) | serde_json::Value::Array(_))) => v,
827 Ok(v @ (serde_json::Value::Number(_) | serde_json::Value::Bool(_) | serde_json::Value::Null))
828 if !t.is_empty() =>
829 {
830 v
831 }
832 _ => serde_json::Value::String(s.to_string()),
833 }
834}
835
836impl Delims {
837 pub(crate) fn structural_tag(
838 &self,
839 tool_jsons: &[String],
840 begin: &str,
841 end: &str,
842 at_least_one: bool,
843 ) -> Option<String> {
844 if tool_jsons.is_empty() || begin.is_empty() || end.is_empty() || !at_least_one {
845 return None;
846 }
847 let mut tags = Vec::new();
848 for t in tool_jsons {
849 let v: serde_json::Value = serde_json::from_str(t).ok()?;
850 let f = v.get("function").unwrap_or(&v);
851 let name = f.get("name").and_then(|n| n.as_str())?;
852 let params = f
853 .get("parameters")
854 .cloned()
855 .unwrap_or_else(|| serde_json::json!({ "type": "object" }));
856 tags.push(serde_json::json!({
857 "type": "tag",
858 "begin": begin,
859 "content": self.call_body_format(name, ¶ms),
860 "end": end,
861 }));
862 }
863 Some(wrap_structural_tag(serde_json::json!({
864 "type": "triggered_tags",
865 "triggers": [begin],
866 "tags": tags,
867 "at_least_one": at_least_one,
868 "stop_after_first": stop_after_first(at_least_one),
869 })))
870 }
871
872 fn call_body_format(&self, name: &str, params: &serde_json::Value) -> serde_json::Value {
873 let props = params.get("properties").and_then(|p| p.as_object());
874 let required: Vec<String> = params
875 .get("required")
876 .and_then(|r| r.as_array())
877 .map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect())
878 .unwrap_or_default();
879 let param = |key: &str| -> serde_json::Value {
880 let schema = props.and_then(|p| p.get(key));
881 let open = format!("{}{key}{}", self.key_open, self.key_close);
882 if self.val_close.is_empty() {
883 let schema = schema.cloned().unwrap_or_else(|| serde_json::json!({ "type": "string" }));
884 return serde_json::json!({
885 "type": "sequence",
886 "elements": [
887 { "type": "const_string", "value": open },
888 value_format(&schema, ATEM_STRING),
889 ]
890 });
891 }
892 serde_json::json!({
893 "type": "tag",
894 "begin": open,
895 "content": glm_value_format(schema, params),
896 "end": self.val_close,
897 })
898 };
899 let all: Vec<String> = props.map(|p| p.keys().cloned().collect()).unwrap_or_default();
900 let mut elements = vec![
901 serde_json::json!({ "type": "regex", "pattern": "\\s*" }),
902 serde_json::json!({
903 "type": "const_string",
904 "value": format!("{}{name}{}", self.name_open, self.name_close),
905 }),
906 ];
907 for key in &all {
908 let e = param(key);
909 if required.contains(key) {
910 elements.push(e);
911 } else {
912 elements.push(serde_json::json!({ "type": "optional", "content": e }));
913 }
914 }
915 elements.push(serde_json::json!({ "type": "any_text" }));
916 serde_json::json!({ "type": "sequence", "elements": elements })
917 }
918
919 fn value(&self, name: &str, key: &str, raw: &str, schemas: Option<&ToolSchemas>) -> serde_json::Value {
920 let prop = schemas.and_then(|s| s.property(name, key));
921 let root = schemas.and_then(|s| s.params(name)).unwrap_or(&serde_json::Value::Null);
922 declared_value(raw, prop, root, StringUnion::Verbatim)
923 }
924
925 pub(crate) fn parse(&self, raw: &str, schemas: Option<&ToolSchemas>) -> Option<(String, String)> {
926 let i = raw.find(&self.name_open)?;
927 let j = i + self.name_open.len();
928 let ne = raw[j..].find(&self.name_close).unwrap_or(raw.len() - j);
929 let name = raw[j..j + ne].trim();
930 if name.is_empty() || name.contains('<') || name.contains(char::is_whitespace) {
931 return None;
932 }
933 let mut args = serde_json::Map::new();
934 let mut cursor = j + ne;
935 while let Some(k) = raw[cursor..].find(&self.key_open) {
936 let ks = cursor + k + self.key_open.len();
937 let ke = match raw[ks..].find(&self.key_close) {
938 Some(e) => e,
939 None => break,
940 };
941 let key = raw[ks..ks + ke].trim().to_string();
942 let vs = ks + ke + self.key_close.len();
943 let ve = if self.val_close.is_empty() {
944 raw.len() - vs
945 } else {
946 raw[vs..].find(&self.val_close).unwrap_or(raw.len() - vs)
947 };
948 let val = &raw[vs..vs + ve];
949 if !key.is_empty() {
950 let v = self.value(name, &key, val, schemas);
951 args.insert(key, v);
952 }
953 cursor = vs + ve;
954 }
955 Some((name.to_string(), serde_json::Value::Object(args).to_string()))
956 }
957
958 #[cfg(test)]
959 pub(crate) fn for_test(name_open: &str, name_close: &str, key_open: &str, key_close: &str, val_close: &str) -> Self {
960 Self {
961 name_open: name_open.into(),
962 name_close: name_close.into(),
963 key_open: key_open.into(),
964 key_close: key_close.into(),
965 val_close: val_close.into(),
966 }
967 }
968
969 pub(crate) fn peek_name(&self, raw: &str) -> Option<String> {
970 let i = raw.find(&self.name_open)?;
971 let j = i + self.name_open.len();
972 let ne = raw[j..].find(&self.name_close)?;
973 let name = raw[j..j + ne].trim();
974 if name.is_empty() || name.contains('<') || name.contains(char::is_whitespace) {
975 return None;
976 }
977 Some(name.to_string())
978 }
979
980 pub(crate) fn closed_args(
981 &self,
982 raw: &str,
983 cursor: &mut Option<usize>,
984 schemas: Option<&ToolSchemas>,
985 ) -> Vec<(String, serde_json::Value)> {
986 let mut out = Vec::new();
987 let name = schemas.and_then(|_| self.peek_name(raw)).unwrap_or_default();
988 if cursor.is_none() {
989 let Some(i) = raw.find(&self.name_open) else {
990 return out;
991 };
992 let j = i + self.name_open.len();
993 let Some(ne) = raw[j..].find(&self.name_close) else {
994 return out;
995 };
996 *cursor = Some(j + ne);
997 }
998 let cursor = cursor.as_mut().expect("set above");
999 while let Some(k) = raw[*cursor..].find(&self.key_open) {
1000 let ks = *cursor + k + self.key_open.len();
1001 let Some(ke) = raw[ks..].find(&self.key_close) else {
1002 break;
1003 };
1004 let key = raw[ks..ks + ke].trim().to_string();
1005 let vs = ks + ke + self.key_close.len();
1006 if self.val_close.is_empty() {
1007 break;
1008 }
1009 let Some(ve) = raw[vs..].find(&self.val_close) else {
1010 break;
1011 };
1012 if !key.is_empty() {
1013 let v = self.value(&name, &key, &raw[vs..vs + ve], schemas);
1014 out.push((key, v));
1015 }
1016 *cursor = vs + ve;
1017 }
1018 out
1019 }
1020}
1021
1022pub fn parse_gemma_tool_call(raw: &str) -> Option<(String, String)> {
1023 let mut body = raw.trim();
1024 if let Some(i) = body.find(GEMMA_TOOL_OPEN) {
1025 body = &body[i + GEMMA_TOOL_OPEN.len()..];
1026 }
1027 if let Some(i) = body.find(GEMMA_TOOL_CLOSE) {
1028 body = &body[..i];
1029 }
1030 let rest = body.trim().strip_prefix("call:")?;
1031 let brace = rest.find('{')?;
1032 let name = rest[..brace].trim().to_string();
1033 if name.is_empty() {
1034 return None;
1035 }
1036 let cs: Vec<char> = rest.chars().collect();
1037 let mut p = rest[..brace].chars().count();
1038 let v = gemma_parse_value(&cs, &mut p, false);
1039 if v.is_null() {
1040 return Some((name, "{}".to_string()));
1041 }
1042 Some((name, serde_json::to_string(&v).ok()?))
1043}
1044
1045pub fn function_tool(json: &str) -> Result<String, String> {
1049 use serde_json::{json, Value};
1050 let Value::Object(mut tool) =
1051 serde_json::from_str::<Value>(json).map_err(|e| format!("not JSON: {e}"))?
1052 else {
1053 return Err("not a JSON object".into());
1054 };
1055 let named = |f: &serde_json::Map<String, Value>| -> Result<(), String> {
1056 match f.get("name").and_then(Value::as_str) {
1057 Some(n) if !n.is_empty() => {}
1058 _ => return Err("it has no name".into()),
1059 }
1060 match f.get("parameters") {
1061 None | Some(Value::Object(_)) => Ok(()),
1062 Some(_) => Err("its parameters are not a JSON schema object".into()),
1063 }
1064 };
1065 if tool.get("type").is_some_and(|t| t != "function") {
1066 return Err("its type is not \"function\"".into());
1067 }
1068 if let Some(f) = tool.get("function") {
1069 let Value::Object(f) = f else {
1070 return Err("its function is not an object".into());
1071 };
1072 named(f)?;
1073 return Ok(Value::Object(tool).to_string());
1074 }
1075 tool.remove("type");
1076 let function = match tool.remove("input_schema") {
1077 Some(schema) => json!({
1078 "name": tool.get("name").cloned().unwrap_or(Value::Null),
1079 "description": tool.get("description").and_then(Value::as_str).unwrap_or_default(),
1080 "parameters": schema,
1081 }),
1082 None => Value::Object(tool),
1083 };
1084 named(function.as_object().expect("built as an object"))?;
1085 Ok(json!({"type": "function", "function": function}).to_string())
1086}
1087
1088#[derive(Debug, Clone, Default)]
1089pub struct ToolSchemas(std::collections::HashMap<String, ToolSchema>);
1090
1091#[derive(Debug, Clone)]
1092struct ToolSchema {
1093 root: serde_json::Value,
1094 args: serde_json::Value,
1095}
1096
1097impl ToolSchemas {
1098 pub fn from_values<'a>(tools: impl IntoIterator<Item = &'a serde_json::Value>) -> ToolSchemas {
1099 let mut m = std::collections::HashMap::new();
1100 for t in tools {
1101 let f = t.get("function").unwrap_or(t);
1102 let Some(name) = f.get("name").and_then(|n| n.as_str()) else { continue };
1103 let params = f
1104 .get("parameters")
1105 .or_else(|| f.get("input_schema"))
1106 .cloned()
1107 .unwrap_or(serde_json::Value::Null);
1108 let args = resolve_local_ref(¶ms, ¶ms).into_owned();
1109 m.insert(name.to_string(), ToolSchema { root: params, args });
1110 }
1111 ToolSchemas(m)
1112 }
1113
1114 pub fn from_jsons(tools: &[String]) -> ToolSchemas {
1115 let vals: Vec<serde_json::Value> = tools.iter().filter_map(|t| serde_json::from_str(t).ok()).collect();
1116 ToolSchemas::from_values(vals.iter())
1117 }
1118
1119 pub fn of_values(tools: &[serde_json::Value]) -> Option<std::sync::Arc<ToolSchemas>> {
1120 (!tools.is_empty()).then(|| std::sync::Arc::new(ToolSchemas::from_values(tools.iter())))
1121 }
1122
1123 pub fn params(&self, tool: &str) -> Option<&serde_json::Value> {
1124 self.0.get(tool).map(|t| &t.root)
1125 }
1126
1127 pub fn args_schema(&self, tool: &str) -> Option<&serde_json::Value> {
1128 self.0.get(tool).map(|t| &t.args)
1129 }
1130
1131 pub fn property(&self, tool: &str, key: &str) -> Option<&serde_json::Value> {
1132 self.args_schema(tool)?.get("properties")?.get(key)
1133 }
1134
1135 pub fn is_empty(&self) -> bool {
1136 self.0.is_empty()
1137 }
1138}
1139
1140fn resolve_local_ref<'a>(p: &'a serde_json::Value, root: &'a serde_json::Value) -> std::borrow::Cow<'a, serde_json::Value> {
1141 use std::borrow::Cow;
1142 let mut cur: Cow<'a, serde_json::Value> = Cow::Borrowed(p);
1143 for _ in 0..16 {
1144 let Some(r) = cur.get("$ref").and_then(|r| r.as_str()) else { break };
1145 let Some(target) = r.strip_prefix('#').and_then(|ptr| root.pointer(ptr)) else { break };
1146 let siblings: Vec<(String, serde_json::Value)> = cur
1147 .as_object()
1148 .map(|o| o.iter().filter(|(k, _)| *k != "$ref").map(|(k, v)| (k.clone(), v.clone())).collect())
1149 .unwrap_or_default();
1150 if siblings.is_empty() {
1151 cur = Cow::Borrowed(target);
1152 continue;
1153 }
1154 let mut merged = target.clone();
1155 if let Some(obj) = merged.as_object_mut() {
1156 for (k, v) in siblings {
1157 match (k.as_str(), obj.get_mut(&k), v) {
1158 ("required", Some(serde_json::Value::Array(have)), serde_json::Value::Array(add)) => {
1159 for x in add {
1160 if !have.contains(&x) {
1161 have.push(x);
1162 }
1163 }
1164 }
1165 ("properties", Some(serde_json::Value::Object(have)), serde_json::Value::Object(add)) => {
1166 have.extend(add);
1167 }
1168 (_, _, v) => {
1169 obj.insert(k, v);
1170 }
1171 }
1172 }
1173 }
1174 cur = Cow::Owned(merged);
1175 }
1176 cur
1177}
1178
1179fn static_type_name(t: &str) -> Option<&'static str> {
1180 ["null", "boolean", "integer", "number", "string", "array", "object"].into_iter().find(|n| *n == t)
1181}
1182
1183fn json_type_name(v: &serde_json::Value) -> &'static str {
1184 match v {
1185 serde_json::Value::Null => "null",
1186 serde_json::Value::Bool(_) => "boolean",
1187 serde_json::Value::Number(n) if n.is_i64() || n.is_u64() => "integer",
1188 serde_json::Value::Number(_) => "number",
1189 serde_json::Value::String(_) => "string",
1190 serde_json::Value::Array(_) => "array",
1191 serde_json::Value::Object(_) => "object",
1192 }
1193}
1194
1195fn schema_types(p: &serde_json::Value, root: &serde_json::Value) -> Vec<&'static str> {
1196 fn walk(p: &serde_json::Value, root: &serde_json::Value, depth: u32, out: &mut Vec<&'static str>) {
1197 if depth > 16 {
1198 return;
1199 }
1200 let p = resolve_local_ref(p, root);
1201 match p.get("type") {
1202 Some(serde_json::Value::String(t)) => out.extend(static_type_name(t)),
1203 Some(serde_json::Value::Array(ts)) => {
1204 out.extend(ts.iter().filter_map(|t| t.as_str()).filter_map(static_type_name))
1205 }
1206 _ => {}
1207 }
1208 for k in ["anyOf", "oneOf", "allOf"] {
1209 if let Some(alts) = p.get(k).and_then(|a| a.as_array()) {
1210 for a in alts {
1211 walk(a, root, depth + 1, out);
1212 }
1213 }
1214 }
1215 if let Some(vals) = p.get("enum").and_then(|e| e.as_array()) {
1216 for v in vals {
1217 out.push(json_type_name(v));
1218 }
1219 }
1220 if let Some(c) = p.get("const") {
1221 out.push(json_type_name(c));
1222 }
1223 }
1224 let mut out = Vec::new();
1225 walk(p, root, 0, &mut out);
1226 out
1227}
1228
1229fn json_is_one_of(v: &serde_json::Value, types: &[&str]) -> bool {
1230 use serde_json::Value as J;
1231 types.iter().any(|t| match (*t, v) {
1232 ("null", J::Null) | ("boolean", J::Bool(_)) | ("array", J::Array(_)) | ("object", J::Object(_)) => true,
1233 ("number", J::Number(_)) => true,
1234 ("integer", J::Number(n)) => n.is_i64() || n.is_u64() || n.as_f64().is_some_and(|f| f.fract() == 0.0),
1235 _ => false,
1236 })
1237}
1238
1239#[derive(Clone, Copy, PartialEq, Eq, Debug)]
1240enum StringUnion {
1241 Typed,
1242 #[cfg_attr(not(any(feature = "basert", test)), allow(dead_code))]
1243 Verbatim,
1244}
1245
1246fn declared_value(
1247 raw: &str,
1248 prop: Option<&serde_json::Value>,
1249 root: &serde_json::Value,
1250 union: StringUnion,
1251) -> serde_json::Value {
1252 let types = prop.map(|p| schema_types(p, root)).unwrap_or_default();
1253 if types.is_empty() {
1254 return typed_value(raw);
1255 }
1256 let text = types.contains(&"string");
1257 let padded = raw.len() != raw.trim().len();
1258 if text && (padded || union == StringUnion::Verbatim) {
1259 return serde_json::Value::String(raw.to_string());
1260 }
1261 let t = raw.trim();
1262 let parsed = serde_json::from_str::<serde_json::Value>(t).ok().or_else(|| {
1263 let py = match t {
1264 "True" => serde_json::Value::Bool(true),
1265 "False" => serde_json::Value::Bool(false),
1266 "None" => serde_json::Value::Null,
1267 _ => return None,
1268 };
1269 (!text && json_is_one_of(&py, &types)).then_some(py)
1270 });
1271 match parsed {
1272 Some(j) if !j.is_string() && json_is_one_of(&j, &types) => j,
1273 _ if text => serde_json::Value::String(raw.to_string()),
1274 Some(j) => j,
1275 None => serde_json::Value::String(raw.to_string()),
1276 }
1277}
1278
1279fn glm_value(raw: &str, prop: Option<&serde_json::Value>, root: &serde_json::Value) -> serde_json::Value {
1280 declared_value(raw, prop, root, StringUnion::Typed)
1281}
1282
1283fn glm_bare_name(name: &str) -> bool {
1284 !name.is_empty() && !name.contains(char::is_whitespace) && !name.contains(['{', '}', '<', '>', '"'])
1285}
1286
1287pub fn parse_glm_tool_call(raw: &str, schemas: Option<&ToolSchemas>) -> Option<(String, String)> {
1288 const AK_O: &str = GLM_ARG_KEY_OPEN;
1289 const AK_C: &str = GLM_ARG_KEY_CLOSE;
1290 const AV_O: &str = GLM_ARG_VALUE_OPEN;
1291 const AV_C: &str = GLM_ARG_VALUE_CLOSE;
1292 let mut body = raw;
1293 if let Some(i) = body.find("<tool_call>") {
1294 body = &body[i + "<tool_call>".len()..];
1295 }
1296 if let Some(i) = body.find("</tool_call>") {
1297 body = &body[..i];
1298 }
1299 let first_key = body.find(AK_O);
1300 let name = body[..first_key.unwrap_or(body.len())].trim();
1301 if !glm_bare_name(name) {
1302 return None;
1303 }
1304 let mut args = serde_json::Map::new();
1305 let mut pos = first_key;
1306 while let Some(p) = pos {
1307 let k_start = p + AK_O.len();
1308 let k_end = k_start + body[k_start..].find(AK_C)?;
1309 let key = body[k_start..k_end].trim();
1310 let after_key = k_end + AK_C.len();
1311 let v_open = after_key + body[after_key..].find(AV_O)?;
1312 if !body[after_key..v_open].trim().is_empty() {
1313 return None;
1314 }
1315 let v_start = v_open + AV_O.len();
1316 let v_end = v_start + body[v_start..].find(AV_C)?;
1317 if !key.is_empty() {
1318 let prop = schemas.and_then(|s| s.property(name, key));
1319 let root = schemas.and_then(|s| s.params(name)).unwrap_or(&serde_json::Value::Null);
1320 args.insert(key.to_string(), glm_value(&body[v_start..v_end], prop, root));
1321 }
1322 let after = &body[v_end + AV_C.len()..];
1323 let next = after.trim_start();
1324 if next.is_empty() {
1325 break;
1326 }
1327 if !next.starts_with(AK_O) {
1328 return None;
1329 }
1330 pos = Some(body.len() - next.len());
1331 }
1332 if let Some(required) = schemas
1333 .and_then(|s| s.args_schema(name))
1334 .and_then(|p| p.get("required"))
1335 .and_then(|r| r.as_array())
1336 {
1337 if required.iter().filter_map(|k| k.as_str()).any(|k| !args.contains_key(k)) {
1338 return None;
1339 }
1340 }
1341 Some((name.to_string(), serde_json::Value::Object(args).to_string()))
1342}
1343
1344const GLM_ARG_KEY_OPEN: &str = "<arg_key>";
1345const GLM_ARG_KEY_CLOSE: &str = "</arg_key>";
1346const GLM_ARG_VALUE_OPEN: &str = "<arg_value>";
1347const GLM_ARG_VALUE_CLOSE: &str = "</arg_value>";
1348
1349fn glm_value_format(schema: Option<&serde_json::Value>, params: &serde_json::Value) -> serde_json::Value {
1350 let any_text = serde_json::json!({ "type": "any_text" });
1351 let Some(schema) = schema else { return any_text };
1352 let schema = resolve_local_ref(schema, params);
1353 if let Some(vals) = fixed_values(&schema, params, 0) {
1354 let elems: Vec<serde_json::Value> = vals
1355 .into_iter()
1356 .map(|v| match v {
1357 serde_json::Value::String(s) => serde_json::json!({ "type": "const_string", "value": s }),
1358 other => serde_json::json!({ "type": "const_string", "value": python_json(&other) }),
1359 })
1360 .collect();
1361 return if elems.len() == 1 {
1362 elems.into_iter().next().unwrap_or(any_text)
1363 } else {
1364 serde_json::json!({ "type": "or", "elements": elems })
1365 };
1366 }
1367 let types = schema_types(&schema, params);
1368 if types.is_empty() || types.contains(&"string") {
1369 return any_text;
1370 }
1371 let mut schema = schema.into_owned();
1372 if let Some(obj) = schema.as_object_mut() {
1373 for defs in ["$defs", "definitions"] {
1374 if let (Some(d), false) = (params.get(defs), obj.contains_key(defs)) {
1375 obj.insert(defs.to_string(), d.clone());
1376 }
1377 }
1378 }
1379 serde_json::json!({ "type": "json_schema", "json_schema": schema })
1380}
1381
1382fn fixed_values(schema: &serde_json::Value, root: &serde_json::Value, depth: u32) -> Option<Vec<serde_json::Value>> {
1383 if depth > 16 {
1384 return None;
1385 }
1386 let schema = resolve_local_ref(schema, root);
1387 if let Some(c) = schema.get("const") {
1388 return Some(vec![c.clone()]);
1389 }
1390 if let Some(vals) = schema.get("enum").and_then(|e| e.as_array()).filter(|v| !v.is_empty()) {
1391 return Some(vals.clone());
1392 }
1393 for k in ["anyOf", "oneOf"] {
1394 if let Some(alts) = schema.get(k).and_then(|a| a.as_array()).filter(|a| !a.is_empty()) {
1395 let mut out: Vec<serde_json::Value> = Vec::new();
1396 for a in alts {
1397 for v in fixed_values(a, root, depth + 1)? {
1398 if !out.contains(&v) {
1399 out.push(v);
1400 }
1401 }
1402 }
1403 return Some(out);
1404 }
1405 }
1406 None
1407}
1408
1409fn python_json(v: &serde_json::Value) -> String {
1410 match v {
1411 serde_json::Value::Array(a) => format!("[{}]", a.iter().map(python_json).collect::<Vec<_>>().join(", ")),
1412 serde_json::Value::Object(o) => format!(
1413 "{{{}}}",
1414 o.iter()
1415 .map(|(k, v)| format!("{}: {}", serde_json::Value::String(k.clone()), python_json(v)))
1416 .collect::<Vec<_>>()
1417 .join(", ")
1418 ),
1419 scalar => scalar.to_string(),
1420 }
1421}
1422
1423fn glm_call_body_format(name: &str, params: &serde_json::Value) -> serde_json::Value {
1424 let args = resolve_local_ref(params, params);
1425 let props = args.get("properties").and_then(|p| p.as_object());
1426 let required: Vec<String> = args
1427 .get("required")
1428 .and_then(|r| r.as_array())
1429 .map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect())
1430 .unwrap_or_default();
1431 let arg = |key: &str, schema: Option<&serde_json::Value>| {
1432 serde_json::json!({
1433 "type": "sequence",
1434 "elements": [
1435 { "type": "const_string", "value": format!("{GLM_ARG_KEY_OPEN}{key}{GLM_ARG_KEY_CLOSE}") },
1436 {
1437 "type": "tag",
1438 "begin": GLM_ARG_VALUE_OPEN,
1439 "content": glm_value_format(schema, params),
1440 "end": GLM_ARG_VALUE_CLOSE,
1441 },
1442 ]
1443 })
1444 };
1445 let mut elements = vec![serde_json::json!({ "type": "const_string", "value": name })];
1446 for (key, schema) in props.into_iter().flatten() {
1447 if required.contains(key) {
1448 elements.push(arg(key, Some(schema)));
1449 } else {
1450 elements.push(serde_json::json!({ "type": "optional", "content": arg(key, Some(schema)) }));
1451 }
1452 }
1453 for key in required.iter().filter(|k| !props.is_some_and(|p| p.contains_key(k.as_str()))) {
1454 elements.push(arg(key, None));
1455 }
1456 serde_json::json!({ "type": "sequence", "elements": elements })
1457}
1458
1459fn wrap_structural_tag(format: serde_json::Value) -> String {
1460 serde_json::json!({ "type": "structural_tag", "format": format }).to_string()
1461}
1462
1463fn stop_after_first(at_least_one: bool) -> bool {
1464 at_least_one
1465}
1466
1467fn value_format(schema: &serde_json::Value, string_pattern: &str) -> serde_json::Value {
1468 if let Some(vals) = schema.get("enum").and_then(|e| e.as_array()) {
1469 let elems: Vec<serde_json::Value> = vals
1470 .iter()
1471 .map(|v| {
1472 let lit = v.as_str().map(|s| s.to_string()).unwrap_or_else(|| v.to_string());
1473 serde_json::json!({ "type": "const_string", "value": lit })
1474 })
1475 .collect();
1476 if !elems.is_empty() {
1477 return serde_json::json!({ "type": "or", "elements": elems });
1478 }
1479 }
1480 match schema.get("type").and_then(|t| t.as_str()).unwrap_or("string") {
1481 "integer" => serde_json::json!({ "type": "regex", "pattern": r"-?[0-9]+" }),
1482 "number" => serde_json::json!({ "type": "regex", "pattern": r"-?[0-9]+(\.[0-9]+)?" }),
1483 "boolean" => serde_json::json!({
1484 "type": "or",
1485 "elements": [
1486 { "type": "const_string", "value": "true" },
1487 { "type": "const_string", "value": "false" },
1488 ]
1489 }),
1490 "array" => serde_json::json!({
1491 "type": "regex",
1492 "pattern": r"\[[^{}\[\]\n]*\]"
1493 }),
1494 "object" => serde_json::json!({ "type": "regex", "pattern": r"\{[^{}\n]*\}" }),
1495 _ => serde_json::json!({ "type": "regex", "pattern": string_pattern }),
1496 }
1497}
1498
1499const GEMMA_STRING: &str = r"[A-Za-z0-9_.:/=@+-][A-Za-z0-9 _.:/=@+-]*";
1500
1501const ATEM_STRING: &str = r"[A-Za-z0-9_.:/=@+,(){}\[\]!?'-][A-Za-z0-9 _.:/=@+,(){}\[\]!?'-]*";
1502
1503fn gemma_value_format(schema: &serde_json::Value) -> serde_json::Value {
1504 value_format(schema, GEMMA_STRING)
1505}
1506
1507fn atem_call_body_format(name: &str, params: &serde_json::Value) -> serde_json::Value {
1508 let props = params.get("properties").and_then(|p| p.as_object());
1509 let required: Vec<String> = params
1510 .get("required")
1511 .and_then(|r| r.as_array())
1512 .map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect())
1513 .unwrap_or_default();
1514 let param = |key: &str| -> serde_json::Value {
1515 let schema = props
1516 .and_then(|p| p.get(key))
1517 .cloned()
1518 .unwrap_or_else(|| serde_json::json!({ "type": "string" }));
1519 serde_json::json!({
1520 "type": "sequence",
1521 "elements": [
1522 { "type": "const_string", "value": format!("<atem:parameter name=\"{key}\">") },
1523 value_format(&schema, ATEM_STRING),
1524 { "type": "const_string", "value": "</atem:parameter>\n" },
1525 ]
1526 })
1527 };
1528 let all: Vec<String> = props.map(|p| p.keys().cloned().collect()).unwrap_or_default();
1529
1530 let mut elements = vec![serde_json::json!({
1531 "type": "const_string",
1532 "value": format!("\n<atem:invoke name=\"{name}\">\n"),
1533 })];
1534 for key in &all {
1535 let e = param(key);
1536 if required.contains(key) {
1537 elements.push(e);
1538 } else {
1539 elements.push(serde_json::json!({ "type": "optional", "content": e }));
1540 }
1541 }
1542 elements.push(serde_json::json!({ "type": "const_string", "value": "</atem:invoke>\n" }));
1543 serde_json::json!({ "type": "sequence", "elements": elements })
1544}
1545
1546fn gemma_call_body_format(name: &str, params: &serde_json::Value) -> serde_json::Value {
1547 let props = params.get("properties").and_then(|p| p.as_object());
1548 let required: Vec<String> = params
1549 .get("required")
1550 .and_then(|r| r.as_array())
1551 .map(|a| a.iter().filter_map(|v| v.as_str().map(String::from)).collect())
1552 .unwrap_or_default();
1553
1554 let pair = |key: &str| -> serde_json::Value {
1555 let schema = props
1556 .and_then(|p| p.get(key))
1557 .cloned()
1558 .unwrap_or_else(|| serde_json::json!({ "type": "string" }));
1559 serde_json::json!({
1560 "type": "sequence",
1561 "elements": [
1562 { "type": "const_string", "value": format!("{key}:") },
1563 gemma_value_format(&schema),
1564 ]
1565 })
1566 };
1567
1568 let mut all: Vec<String> = props.map(|p| p.keys().cloned().collect()).unwrap_or_default();
1569 all.sort();
1570
1571 let mut elements = vec![serde_json::json!({
1572 "type": "const_string",
1573 "value": format!("call:{name}{{"),
1574 })];
1575 let mut first = true;
1576 for key in all.iter().filter(|k| required.contains(k)) {
1577 if !first {
1578 elements.push(serde_json::json!({ "type": "const_string", "value": "," }));
1579 }
1580 first = false;
1581 elements.push(pair(key));
1582 }
1583 elements.push(serde_json::json!({ "type": "const_string", "value": "}" }));
1584 serde_json::json!({ "type": "sequence", "elements": elements })
1585}
1586
1587pub const GEMMA_TOOL_OPEN: &str = "<|tool_call>";
1588pub const GEMMA_TOOL_CLOSE: &str = "<tool_call|>";
1589
1590pub const GEMMA_TOOL_RESPONSE_OPEN: &str = "<|tool_response>";
1591pub const GEMMA_TOOL_RESPONSE_CLOSE: &str = "<tool_response|>";
1592
1593#[derive(Debug, Clone, Copy, PartialEq, Eq, Default)]
1594pub enum ToolEnvelope {
1595 #[default]
1596 Json,
1597 Atem,
1598 Harmony,
1599 Gemma,
1600 Glm,
1601}
1602
1603impl ToolEnvelope {
1604 pub fn from_name(s: &str) -> Option<ToolEnvelope> {
1605 match s {
1606 "json" => Some(ToolEnvelope::Json),
1607 "atem" => Some(ToolEnvelope::Atem),
1608 "gemma" => Some(ToolEnvelope::Gemma),
1609 "harmony" => Some(ToolEnvelope::Harmony),
1610 "glm" => Some(ToolEnvelope::Glm),
1611 _ => None,
1612 }
1613 }
1614
1615 pub fn names() -> &'static [&'static str] {
1616 &["json", "atem", "gemma", "harmony", "glm"]
1617 }
1618
1619 pub fn name(&self) -> &'static str {
1620 match self {
1621 ToolEnvelope::Json => "json",
1622 ToolEnvelope::Atem => "atem",
1623 ToolEnvelope::Gemma => "gemma",
1624 ToolEnvelope::Harmony => "harmony",
1625 ToolEnvelope::Glm => "glm",
1626 }
1627 }
1628
1629 pub fn parse_call(&self, raw: &str, schemas: Option<&ToolSchemas>) -> Option<(String, String)> {
1630 match self {
1631 ToolEnvelope::Json => parse_json_tool_call(raw),
1632 ToolEnvelope::Atem => crate::atem::parse_atem_call(raw),
1633 ToolEnvelope::Harmony => crate::harmony::parse_harmony_tool_call(raw),
1634 ToolEnvelope::Gemma => parse_gemma_tool_call(raw),
1635 ToolEnvelope::Glm => parse_glm_tool_call(raw, schemas),
1636 }
1637 }
1638
1639 pub fn named_call(&self, raw: &str) -> Option<(String, String)> {
1640 match self {
1641 ToolEnvelope::Harmony => crate::harmony::named_call(raw),
1642 ToolEnvelope::Json | ToolEnvelope::Atem | ToolEnvelope::Gemma | ToolEnvelope::Glm => None,
1643 }
1644 }
1645
1646 pub fn structural_tag(
1647 &self,
1648 tool_jsons: &[String],
1649 begin: &str,
1650 end: &str,
1651 at_least_one: bool,
1652 ) -> Option<String> {
1653 if tool_jsons.is_empty() || begin.is_empty() || end.is_empty() {
1654 return None;
1655 }
1656 match self {
1657 ToolEnvelope::Harmony => None,
1658 ToolEnvelope::Atem => {
1659 let mut tags = Vec::new();
1660 for t in tool_jsons {
1661 let v: serde_json::Value = serde_json::from_str(t).ok()?;
1662 let f = v.get("function").unwrap_or(&v);
1663 let name = f.get("name").and_then(|n| n.as_str())?;
1664 let params = f
1665 .get("parameters")
1666 .cloned()
1667 .unwrap_or_else(|| serde_json::json!({ "type": "object" }));
1668 tags.push(serde_json::json!({
1669 "type": "tag",
1670 "begin": begin,
1671 "content": atem_call_body_format(name, ¶ms),
1672 "end": end,
1673 }));
1674 }
1675 Some(wrap_structural_tag(serde_json::json!({
1676 "type": "triggered_tags",
1677 "triggers": [begin],
1678 "tags": tags,
1679 "at_least_one": at_least_one,
1680 "stop_after_first": stop_after_first(at_least_one),
1681 })))
1682 }
1683 ToolEnvelope::Glm => {
1684 if !at_least_one {
1685 return None;
1686 }
1687 let mut tags = Vec::new();
1688 for t in tool_jsons {
1689 let v: serde_json::Value = serde_json::from_str(t).ok()?;
1690 let f = v.get("function").unwrap_or(&v);
1691 let name = f.get("name").and_then(|n| n.as_str())?;
1692 let params = f
1693 .get("parameters")
1694 .cloned()
1695 .unwrap_or_else(|| serde_json::json!({ "type": "object" }));
1696 tags.push(serde_json::json!({
1697 "type": "tag",
1698 "begin": begin,
1699 "content": glm_call_body_format(name, ¶ms),
1700 "end": end,
1701 }));
1702 }
1703 Some(wrap_structural_tag(serde_json::json!({
1704 "type": "triggered_tags",
1705 "triggers": [begin],
1706 "tags": tags,
1707 "at_least_one": at_least_one,
1708 "stop_after_first": stop_after_first(at_least_one),
1709 })))
1710 }
1711 ToolEnvelope::Gemma => {
1712 if !at_least_one {
1713 return None;
1714 }
1715 let mut tags = Vec::new();
1716 for t in tool_jsons {
1717 let v: serde_json::Value = serde_json::from_str(t).ok()?;
1718 let f = v.get("function").unwrap_or(&v);
1719 let name = f.get("name").and_then(|n| n.as_str())?;
1720 let params = f
1721 .get("parameters")
1722 .cloned()
1723 .unwrap_or_else(|| serde_json::json!({ "type": "object" }));
1724 tags.push(serde_json::json!({
1725 "type": "tag",
1726 "begin": begin,
1727 "content": gemma_call_body_format(name, ¶ms),
1728 "end": end,
1729 }));
1730 }
1731 Some(wrap_structural_tag(serde_json::json!({
1732 "type": "triggered_tags",
1733 "triggers": [begin],
1734 "tags": tags,
1735 "at_least_one": at_least_one,
1736 "stop_after_first": stop_after_first(at_least_one),
1737 })))
1738 }
1739 ToolEnvelope::Json => {
1740 let mut tags = Vec::new();
1741 for t in tool_jsons {
1742 let v: serde_json::Value = serde_json::from_str(t).ok()?;
1743 let f = v.get("function").unwrap_or(&v);
1744 let name = f.get("name").and_then(|n| n.as_str())?;
1745 let params = f
1746 .get("parameters")
1747 .cloned()
1748 .unwrap_or_else(|| serde_json::json!({ "type": "object" }));
1749 tags.push(serde_json::json!({
1750 "type": "tag",
1751 "begin": begin,
1752 "content": {
1753 "type": "json_schema",
1754 "json_schema": {
1755 "type": "object",
1756 "properties": {
1757 "name": { "const": name },
1758 "arguments": params,
1759 },
1760 "required": ["name", "arguments"],
1761 "additionalProperties": false,
1762 },
1763 },
1764 "end": end,
1765 }));
1766 }
1767 Some(wrap_structural_tag(serde_json::json!({
1768 "type": "triggered_tags",
1769 "triggers": [begin],
1770 "tags": tags,
1771 "at_least_one": at_least_one,
1772 "stop_after_first": stop_after_first(at_least_one),
1773 })))
1774 }
1775 }
1776 }
1777
1778 pub fn call_grammar(&self, tool_jsons: &[String]) -> Option<String> {
1779 match self {
1780 ToolEnvelope::Atem => None,
1781 ToolEnvelope::Gemma => None,
1782 ToolEnvelope::Harmony => None,
1783 ToolEnvelope::Glm => None,
1784 ToolEnvelope::Json => {
1785 if tool_jsons.is_empty() {
1786 return None;
1787 }
1788 let mut variants = Vec::new();
1789 for t in tool_jsons {
1790 let v: serde_json::Value = serde_json::from_str(t).ok()?;
1791 let f = v.get("function").unwrap_or(&v);
1792 let name = f.get("name").and_then(|n| n.as_str())?;
1793 let params = f
1794 .get("parameters")
1795 .cloned()
1796 .unwrap_or_else(|| serde_json::json!({ "type": "object" }));
1797 variants.push(serde_json::json!({
1798 "type": "object",
1799 "properties": { "name": { "const": name }, "arguments": params },
1800 "required": ["name", "arguments"],
1801 "additionalProperties": false,
1802 }));
1803 }
1804 let schema = if variants.len() == 1 {
1805 variants.pop().unwrap()
1806 } else {
1807 serde_json::json!({ "anyOf": variants })
1808 };
1809 Some(schema.to_string())
1810 }
1811 }
1812 }
1813}
1814
1815pub fn tool_envelope_override_conflict(codec: &dyn TextCodec, env: ToolEnvelope) -> Option<String> {
1816 let own = codec.tool_envelope();
1817 if env == own {
1818 return None;
1819 }
1820 if env == ToolEnvelope::Glm
1821 && codec
1822 .chat_template_source()
1823 .is_some_and(|t| t.contains(GLM_ARG_KEY_OPEN) && t.contains(GLM_ARG_VALUE_OPEN))
1824 {
1825 return None;
1826 }
1827 Some(format!(
1828 "--tool-call-parser {} conflicts with this model's own tool wire ({}). The dialect renders \
1829 its instructions from the checkpoint's template, so the override would instruct the model \
1830 in one format while parsing another. Drop the flag to use the derived wire{}.",
1831 env.name(),
1832 own.name(),
1833 if env == ToolEnvelope::Glm {
1834 " (`glm` is accepted only where the chat template itself spells the GLM \
1835 <arg_key>/<arg_value> wire)"
1836 } else {
1837 ""
1838 }
1839 ))
1840}
1841
1842pub struct WithToolEnvelope {
1843 inner: Box<dyn TextCodec + Send + Sync>,
1844 envelope: ToolEnvelope,
1845}
1846
1847impl WithToolEnvelope {
1848 pub fn new(inner: Box<dyn TextCodec + Send + Sync>, envelope: ToolEnvelope) -> Self {
1849 WithToolEnvelope { inner, envelope }
1850 }
1851}
1852
1853impl TextCodec for WithToolEnvelope {
1854 fn tool_envelope(&self) -> ToolEnvelope {
1855 self.envelope
1856 }
1857 fn renders_per_message(&self) -> bool {
1858 self.inner.renders_per_message()
1859 }
1860 fn tool_call_delimiters(&self) -> Option<(String, String)> {
1861 self.inner.tool_call_delimiters()
1862 }
1863 fn marker_literal(&self, token: u32) -> String {
1864 self.inner.marker_literal(token)
1865 }
1866 fn encode(&self, text: &str) -> Vec<u32> {
1867 self.inner.encode(text)
1868 }
1869 fn encode_content(&self, text: &str) -> Vec<u32> {
1870 self.inner.encode_content(text)
1871 }
1872 fn encode_pieces(&self, pieces: &[(&str, bool)]) -> Vec<u32> {
1873 self.inner.encode_pieces(pieces)
1874 }
1875 fn token_bytes(&self, token: u32) -> Vec<u8> {
1876 self.inner.token_bytes(token)
1877 }
1878 fn chat_template_source(&self) -> Option<String> {
1879 self.inner.chat_template_source()
1880 }
1881 fn decode(&self, tokens: &[u32]) -> String {
1882 self.inner.decode(tokens)
1883 }
1884 fn stream_prologue(&self) -> Vec<u32> {
1885 self.inner.stream_prologue()
1886 }
1887 fn behavior_fingerprint(&self) -> Option<superfluid_fingerprint::BehaviorFingerprint> {
1888 self.inner.behavior_fingerprint()
1889 }
1890 fn render_message(&self, role: u32, text: &str) -> Option<Vec<u32>> {
1891 self.inner.render_message(role, text)
1892 }
1893 fn generation_prefix(&self, state: TurnState) -> Option<Vec<u32>> {
1894 self.inner.generation_prefix(state)
1895 }
1896 fn generation_prefix_with(&self, state: TurnState, thinking: Option<bool>) -> Option<Vec<u32>> {
1897 self.inner.generation_prefix_with(state, thinking)
1898 }
1899 fn turn_separator(&self) -> Option<Vec<u32>> {
1900 self.inner.turn_separator()
1901 }
1902 fn channel_markers(&self) -> Vec<(u32, u32, u32)> {
1903 self.inner.channel_markers()
1904 }
1905 fn response_format_tag(&self, json_schema: &str) -> Option<String> {
1906 self.inner.response_format_tag(json_schema)
1907 }
1908 fn render_system_with_tools(
1909 &self,
1910 system: Option<&str>,
1911 tool_jsons: &[String],
1912 ) -> Option<Vec<u32>> {
1913 self.inner.render_system_with_tools(system, tool_jsons)
1914 }
1915 fn render_tool_result(&self, name: &str, content: &str) -> Option<Vec<u32>> {
1916 self.inner.render_tool_result(name, content)
1917 }
1918 fn render_assistant_with_tool_calls(
1919 &self,
1920 content: &str,
1921 calls: &[(String, String)],
1922 ) -> Option<Vec<u32>> {
1923 self.inner.render_assistant_with_tool_calls(content, calls)
1924 }
1925 fn render_assistant_with_tool_calls_after_query(
1926 &self,
1927 content: &str,
1928 calls: &[(String, String)],
1929 ) -> Option<Vec<u32>> {
1930 self.inner.render_assistant_with_tool_calls_after_query(content, calls)
1931 }
1932 fn render_fim(&self, prefix: &str, suffix: &str, mode: u8) -> Option<Vec<u32>> {
1933 self.inner.render_fim(prefix, suffix, mode)
1934 }
1935 fn render_turn_with_tokens(
1936 &self,
1937 role: u32,
1938 pre_text: &str,
1939 inner: &[u32],
1940 post_text: &str,
1941 ) -> Option<Vec<u32>> {
1942 self.inner.render_turn_with_tokens(role, pre_text, inner, post_text)
1943 }
1944 fn render_block(&self, role: u32, kind: u32, payload: &str) -> Option<Vec<u32>> {
1945 self.inner.render_block(role, kind, payload)
1946 }
1947 fn channelizer(&self) -> Box<dyn Channelizer> {
1948 self.inner.channelizer()
1949 }
1950 fn render_prompt(&self, messages: &[(u32, String)], tools: &[String]) -> Option<Vec<u32>> {
1951 self.inner.render_prompt(messages, tools)
1952 }
1953 fn turn_terminators(&self) -> Vec<u32> {
1954 self.inner.turn_terminators()
1955 }
1956 fn render_prompt_structured(
1957 &self,
1958 messages: &[ChatMessage],
1959 tools: &[String],
1960 ) -> Option<Vec<u32>> {
1961 self.inner.render_prompt_structured(messages, tools)
1962 }
1963 fn render_prompt_structured_with(
1964 &self,
1965 messages: &[ChatMessage],
1966 tools: &[String],
1967 kwargs: &serde_json::Map<String, serde_json::Value>,
1968 ) -> Option<Vec<u32>> {
1969 self.inner.render_prompt_structured_with(messages, tools, kwargs)
1970 }
1971 fn template_refusal(
1972 &self,
1973 messages: &[ChatMessage],
1974 tools: &[String],
1975 kwargs: &serde_json::Map<String, serde_json::Value>,
1976 ) -> Option<String> {
1977 self.inner.template_refusal(messages, tools, kwargs)
1978 }
1979 fn render_system_full(
1980 &self,
1981 system: Option<&str>,
1982 tool_jsons: &[String],
1983 kwargs: &serde_json::Map<String, serde_json::Value>,
1984 ) -> Option<Vec<u32>> {
1985 self.inner.render_system_full(system, tool_jsons, kwargs)
1986 }
1987 fn validate_template_kwargs(
1988 &self,
1989 kwargs: &serde_json::Map<String, serde_json::Value>,
1990 ) -> Result<(), String> {
1991 self.inner.validate_template_kwargs(kwargs)
1992 }
1993 fn supports_reasoning_effort(&self) -> bool {
1994 self.inner.supports_reasoning_effort()
1995 }
1996 fn reasoning_effort_levels(&self) -> Vec<&'static str> {
1997 self.inner.reasoning_effort_levels()
1998 }
1999 fn supports_enable_thinking(&self) -> bool {
2000 self.inner.supports_enable_thinking()
2001 }
2002 fn empty_think_tokens(&self) -> Option<Vec<u32>> {
2003 self.inner.empty_think_tokens()
2004 }
2005}
2006
2007pub trait Channelizer: Send {
2008 fn split(&mut self, tokens: &[u32]) -> Vec<Run>;
2009 fn current_channel(&self) -> u32;
2010 fn prime(&mut self, _channel: u32) {}
2011}
2012
2013pub mod fim_mode {
2014 pub const PSM: u8 = 0;
2015 pub const SPM: u8 = 1;
2016}
2017
2018pub const MODEL_VISIBILITY_VERSION: u32 = 2;
2019
2020pub fn project_block_text(kind: u32, payload: &str) -> Option<String> {
2021 use crate::wal::block_kind;
2022 let v: serde_json::Value = serde_json::from_str(payload).unwrap_or(serde_json::Value::Null);
2023 let str_of = |k: &str| v.get(k).and_then(|x| x.as_str()).unwrap_or("").to_string();
2024 let cap = |mut t: String, max: usize| {
2025 if t.len() > max {
2026 let mut cut = max;
2027 while !t.is_char_boundary(cut) {
2028 cut -= 1;
2029 }
2030 t.truncate(cut);
2031 t.push_str("\n…[truncated]");
2032 }
2033 t
2034 };
2035 Some(match kind {
2036 block_kind::FILE_DIFF => {
2037 let path = str_of("path");
2038 let diff = str_of("diff");
2039 cap(format!("```diff\n# {path}\n{diff}\n```"), 16 * 1024)
2040 }
2041 block_kind::ARTIFACT => {
2042 let title = str_of("title");
2043 let content = str_of("content");
2044 cap(format!("[artifact: {title}]\n{content}"), 16 * 1024)
2045 }
2046 block_kind::CITATION => {
2047 let title = str_of("title");
2048 let url = str_of("url");
2049 let quote = str_of("quote");
2050 if quote.is_empty() {
2051 format!("[citation] {title} — {url}")
2052 } else {
2053 format!("[citation] {title} — {url}\n> {quote}")
2054 }
2055 }
2056 block_kind::PROGRESS => String::new(),
2057 block_kind::DIAGNOSTIC => {
2058 let severity = str_of("severity");
2059 let message = str_of("message");
2060 let path = str_of("path");
2061 let line = v.get("line").and_then(|x| x.as_u64()).unwrap_or(0);
2062 if path.is_empty() {
2063 format!("[diagnostic {severity}] {message}")
2064 } else {
2065 format!("[diagnostic {severity}] {path}:{line}: {message}")
2066 }
2067 }
2068 block_kind::WORKSPACE_REF => format!("@{}", str_of("path")),
2069 block_kind::IMAGE => String::new(),
2070 _ => return None,
2071 })
2072}
2073
2074pub struct Segmenter {
2075 pairs: Vec<(u32, u32, u32)>,
2076 active: Option<usize>,
2077 names: Vec<(u32, Vec<u32>)>,
2078 name_at: Option<usize>,
2079}
2080
2081impl Segmenter {
2082 pub fn new(pairs: Vec<(u32, u32, u32)>) -> Segmenter {
2083 Segmenter {
2084 pairs,
2085 active: None,
2086 names: Vec::new(),
2087 name_at: None,
2088 }
2089 }
2090
2091 pub fn with_names(mut self, names: Vec<(u32, Vec<u32>)>) -> Segmenter {
2092 self.names = names;
2093 self
2094 }
2095
2096 fn name_of(&self, open: u32) -> Option<&[u32]> {
2097 self.names.iter().find(|(o, _)| *o == open).map(|(_, n)| n.as_slice())
2098 }
2099
2100 fn takes_name(&mut self, open: u32, t: u32) -> bool {
2101 let (Some(k), Some(name)) = (self.name_at, self.name_of(open)) else {
2102 return false;
2103 };
2104 let named = name.get(k) == Some(&t);
2105 let len = name.len();
2106 self.name_at = (named && k + 1 < len).then_some(k + 1);
2107 named
2108 }
2109
2110 fn is_marker(&self, t: u32) -> bool {
2111 self.pairs.iter().any(|&(o, c, _)| o == t || c == t)
2112 }
2113}
2114
2115impl Segmenter {
2116 fn pair_for(&self, channel: u32) -> Option<usize> {
2117 self.pairs.iter().position(|&(_, _, c)| c == channel)
2118 }
2119}
2120
2121impl Channelizer for Segmenter {
2122 fn split(&mut self, tokens: &[u32]) -> Vec<Run> {
2123 if self.pairs.is_empty() {
2124 return vec![Run {
2125 span: tokens.to_vec(),
2126 text: tokens.to_vec(),
2127 channel: crate::wal::channel::TEXT,
2128 closes: false,
2129 }];
2130 }
2131 let mut out: Vec<Run> = Vec::new();
2132 let mut run: Vec<u32> = Vec::new();
2133 let mut text: Vec<u32> = Vec::new();
2134 let mut run_channel = self.current_channel();
2135 fn flush(
2136 run: &mut Vec<u32>,
2137 text: &mut Vec<u32>,
2138 ch: u32,
2139 closes: bool,
2140 out: &mut Vec<Run>,
2141 ) {
2142 if !run.is_empty() {
2143 out.push(Run {
2144 span: std::mem::take(run),
2145 text: std::mem::take(text),
2146 channel: ch,
2147 closes,
2148 });
2149 }
2150 }
2151 for &t in tokens {
2152 match self.active {
2153 None => {
2154 if let Some(i) = self.pairs.iter().position(|&(open, _, _)| open == t) {
2155 flush(&mut run, &mut text, run_channel, false, &mut out);
2156 self.active = Some(i);
2157 self.name_at = self.name_of(t).map(|_| 0);
2158 run_channel = self.pairs[i].2;
2159 run.push(t);
2160 } else {
2161 run.push(t);
2162 if !self.is_marker(t) {
2163 text.push(t);
2164 }
2165 }
2166 }
2167 Some(i) => {
2168 if self.pairs[i].1 == t {
2169 run.push(t);
2170 flush(&mut run, &mut text, run_channel, true, &mut out);
2171 self.active = None;
2172 self.name_at = None;
2173 run_channel = crate::wal::channel::TEXT;
2174 } else {
2175 run.push(t);
2176 let named = self.takes_name(self.pairs[i].0, t);
2177 if !named && !self.is_marker(t) {
2178 text.push(t);
2179 }
2180 }
2181 }
2182 }
2183 }
2184 flush(&mut run, &mut text, run_channel, false, &mut out);
2185 out
2186 }
2187
2188 fn current_channel(&self) -> u32 {
2189 self.active
2190 .map(|i| self.pairs[i].2)
2191 .unwrap_or(crate::wal::channel::TEXT)
2192 }
2193
2194 fn prime(&mut self, channel: u32) {
2195 if let Some(i) = self.pair_for(channel) {
2196 self.active = Some(i);
2197 self.name_at = None;
2198 }
2199 }
2200}
2201
2202pub struct MockCodec;
2203
2204const MOCK_BASE: u32 = 0x100;
2205
2206impl TextCodec for MockCodec {
2207 fn encode(&self, text: &str) -> Vec<u32> {
2208 text.bytes().map(|b| MOCK_BASE + b as u32).collect()
2209 }
2210
2211 fn token_bytes(&self, token: u32) -> Vec<u8> {
2212 if (MOCK_BASE..MOCK_BASE + 256).contains(&token) {
2213 vec![(token - MOCK_BASE) as u8]
2214 } else {
2215 Vec::new()
2216 }
2217 }
2218}
2219
2220pub struct MockChatCodec;
2221
2222pub const MOCK_TURN_END: u32 = MOCK_BASE + 0x1E;
2223pub const MOCK_THINK_OPEN: u32 = MOCK_BASE + 0x02;
2224pub const MOCK_THINK_CLOSE: u32 = MOCK_BASE + 0x03;
2225pub const MOCK_TOOL_OPEN: u32 = MOCK_BASE + 0x04;
2226pub const MOCK_TOOL_CLOSE: u32 = MOCK_BASE + 0x05;
2227
2228impl TextCodec for MockChatCodec {
2229 fn encode(&self, text: &str) -> Vec<u32> {
2230 MockCodec.encode(text)
2231 }
2232 fn encode_content(&self, text: &str) -> Vec<u32> {
2233 MockCodec
2234 .encode(text)
2235 .into_iter()
2236 .map(|t| {
2237 if [MOCK_TURN_END, MOCK_THINK_OPEN, MOCK_THINK_CLOSE, MOCK_TOOL_OPEN, MOCK_TOOL_CLOSE]
2238 .contains(&t)
2239 {
2240 MOCK_BASE + b'?' as u32
2241 } else {
2242 t
2243 }
2244 })
2245 .collect()
2246 }
2247 fn token_bytes(&self, token: u32) -> Vec<u8> {
2248 MockCodec.token_bytes(token)
2249 }
2250 fn behavior_fingerprint(&self) -> Option<superfluid_fingerprint::BehaviorFingerprint> {
2251 Some(superfluid_fingerprint::BehaviorFingerprint {
2252 codec_id: "mock-chat".into(),
2253 codec_version: 2,
2254 tokenizer_hash: superfluid_fingerprint::vocab_hash(2 * MOCK_BASE, |t| {
2255 MockCodec.token_bytes(t)
2256 }),
2257 marker_digest: superfluid_fingerprint::BehaviorFingerprint::marker_digest_of(
2258 &self.channel_markers(),
2259 ),
2260 sampling_impl_version: 1,
2261 block_projection_version: MODEL_VISIBILITY_VERSION,
2262 grammar_version: 0,
2263 speculation: None,
2264 })
2265 }
2266 fn render_message(&self, role: u32, text: &str) -> Option<Vec<u32>> {
2267 let mut span = MockCodec.encode(&format!("<{role}>"));
2268 span.extend(self.encode_content(text));
2269 span.push(MOCK_TURN_END);
2270 Some(span)
2271 }
2272 fn render_turn_with_tokens(
2273 &self,
2274 role: u32,
2275 pre_text: &str,
2276 inner: &[u32],
2277 post_text: &str,
2278 ) -> Option<Vec<u32>> {
2279 let mut span = MockCodec.encode(&format!("<{role}>"));
2280 span.extend(self.encode_content(pre_text));
2281 span.extend_from_slice(inner);
2282 span.extend(self.encode_content(post_text));
2283 span.push(MOCK_TURN_END);
2284 Some(span)
2285 }
2286 fn render_fim(&self, prefix: &str, suffix: &str, mode: u8) -> Option<Vec<u32>> {
2287 const FIM_PRE: u32 = 4_000_000;
2288 const FIM_SUF: u32 = 4_000_001;
2289 const FIM_MID: u32 = 4_000_002;
2290 let mut span = Vec::new();
2291 if mode == fim_mode::SPM {
2292 span.extend([FIM_PRE, FIM_SUF]);
2293 span.extend(MockCodec.encode(suffix));
2294 span.push(FIM_MID);
2295 span.extend(MockCodec.encode(prefix));
2296 } else {
2297 span.push(FIM_PRE);
2298 span.extend(MockCodec.encode(prefix));
2299 span.push(FIM_SUF);
2300 span.extend(MockCodec.encode(suffix));
2301 span.push(FIM_MID);
2302 }
2303 Some(span)
2304 }
2305 fn generation_prefix(&self, state: TurnState) -> Option<Vec<u32>> {
2306 match state {
2307 TurnState::AfterInput => Some(MockCodec.encode("<a>")),
2308 TurnState::MidTurn => None,
2309 TurnState::AfterFinishedTurn => {
2310 let mut s = vec![MOCK_TURN_END];
2311 s.extend(MockCodec.encode("<a>"));
2312 Some(s)
2313 }
2314 }
2315 }
2316 fn channel_markers(&self) -> Vec<(u32, u32, u32)> {
2317 vec![
2318 (
2319 MOCK_THINK_OPEN,
2320 MOCK_THINK_CLOSE,
2321 crate::wal::channel::REASONING,
2322 ),
2323 (
2324 MOCK_TOOL_OPEN,
2325 MOCK_TOOL_CLOSE,
2326 crate::wal::channel::TOOL_CALL,
2327 ),
2328 ]
2329 }
2330 fn render_system_with_tools(
2331 &self,
2332 system: Option<&str>,
2333 tool_jsons: &[String],
2334 ) -> Option<Vec<u32>> {
2335 let mut span = MockCodec.encode("<0>");
2336 span.extend(self.encode_content(system.unwrap_or("")));
2337 for t in tool_jsons {
2338 span.extend(MockCodec.encode(t));
2339 }
2340 span.push(MOCK_TURN_END);
2341 Some(span)
2342 }
2343 fn render_tool_result(&self, _name: &str, content: &str) -> Option<Vec<u32>> {
2344 let mut span = MockCodec.encode("<r>");
2345 span.extend(self.encode_content(content));
2346 span.push(MOCK_TURN_END);
2347 Some(span)
2348 }
2349 fn render_assistant_with_tool_calls(
2350 &self,
2351 content: &str,
2352 calls: &[(String, String)],
2353 ) -> Option<Vec<u32>> {
2354 let mut span = MockCodec.encode("<a>");
2355 span.extend(self.encode_content(content));
2356 for (name, args) in calls {
2357 span.push(MOCK_TOOL_OPEN);
2358 span.extend(MockCodec.encode(&format!(
2359 "{{\"name\": \"{name}\", \"arguments\": {args}}}"
2360 )));
2361 span.push(MOCK_TOOL_CLOSE);
2362 }
2363 span.push(MOCK_TURN_END);
2364 Some(span)
2365 }
2366}
2367
2368pub struct MockTemplateCodec;
2369
2370pub const MOCK_IMAGE_MARKER: u32 = 999;
2371
2372impl TextCodec for MockTemplateCodec {
2373 fn encode(&self, text: &str) -> Vec<u32> {
2374 MockCodec.encode(text)
2375 }
2376 fn decode(&self, tokens: &[u32]) -> String {
2377 MockCodec.decode(tokens)
2378 }
2379 fn token_bytes(&self, token: u32) -> Vec<u8> {
2380 MockCodec.token_bytes(token)
2381 }
2382 fn renders_per_message(&self) -> bool {
2383 false
2384 }
2385 fn render_prompt_structured_with(
2386 &self,
2387 messages: &[ChatMessage],
2388 tools: &[String],
2389 _kwargs: &serde_json::Map<String, serde_json::Value>,
2390 ) -> Option<Vec<u32>> {
2391 let mut span = Vec::new();
2392 for m in messages {
2393 span.extend(MockCodec.encode(&format!("<{}>", m.role)));
2394 if m.parts.is_empty() {
2395 span.extend(MockCodec.encode(&m.content));
2396 } else {
2397 for p in &m.parts {
2398 match p {
2399 ContentPart::Text(t) => span.extend(MockCodec.encode(t)),
2400 ContentPart::Image { .. } => span.push(MOCK_IMAGE_MARKER),
2401 ContentPart::Audio { .. } => span.push(MOCK_IMAGE_MARKER),
2402 }
2403 }
2404 }
2405 span.push(MOCK_TURN_END);
2406 }
2407 for t in tools {
2408 span.extend(MockCodec.encode(t));
2409 }
2410 span.extend(MockCodec.encode("<a>"));
2411 Some(span)
2412 }
2413}
2414
2415pub struct MockThinkingTemplateCodec;
2416
2417impl TextCodec for MockThinkingTemplateCodec {
2418 fn encode(&self, text: &str) -> Vec<u32> {
2419 MockCodec.encode(text)
2420 }
2421 fn decode(&self, tokens: &[u32]) -> String {
2422 MockCodec.decode(tokens)
2423 }
2424 fn token_bytes(&self, token: u32) -> Vec<u8> {
2425 MockCodec.token_bytes(token)
2426 }
2427 fn renders_per_message(&self) -> bool {
2428 false
2429 }
2430 fn channel_markers(&self) -> Vec<(u32, u32, u32)> {
2431 MockChatCodec.channel_markers()
2432 }
2433 fn render_prompt_structured_with(
2434 &self,
2435 messages: &[ChatMessage],
2436 tools: &[String],
2437 kwargs: &serde_json::Map<String, serde_json::Value>,
2438 ) -> Option<Vec<u32>> {
2439 let mut span = MockTemplateCodec.render_prompt_structured_with(messages, tools, kwargs)?;
2440 span.push(MOCK_THINK_OPEN);
2441 Some(span)
2442 }
2443 fn trailing_generation_prompt(
2444 &self,
2445 _messages: &[ChatMessage],
2446 _tools: &[String],
2447 _kwargs: &serde_json::Map<String, serde_json::Value>,
2448 span: &[u32],
2449 ) -> usize {
2450 let mut opener = MockCodec.encode("<a>");
2451 opener.push(MOCK_THINK_OPEN);
2452 if span.len() > opener.len() && span.ends_with(&opener) {
2453 opener.len()
2454 } else {
2455 0
2456 }
2457 }
2458}
2459
2460pub struct NoCodec;
2461
2462impl TextCodec for NoCodec {
2463 fn encode(&self, _text: &str) -> Vec<u32> {
2464 Vec::new()
2465 }
2466 fn token_bytes(&self, _token: u32) -> Vec<u8> {
2467 Vec::new()
2468 }
2469}
2470
2471#[derive(Default)]
2472pub struct Utf8Stream {
2473 buf: Vec<u8>,
2474}
2475
2476impl Utf8Stream {
2477 pub fn push(&mut self, bytes: &[u8]) -> String {
2478 self.buf.extend_from_slice(bytes);
2479 let mut out = String::new();
2480 loop {
2481 match std::str::from_utf8(&self.buf) {
2482 Ok(s) => {
2483 out.push_str(s);
2484 self.buf.clear();
2485 return out;
2486 }
2487 Err(e) => {
2488 let valid = e.valid_up_to();
2489 out.push_str(std::str::from_utf8(&self.buf[..valid]).expect("checked"));
2492 match e.error_len() {
2493 Some(n) => {
2494 out.push('\u{FFFD}');
2495 self.buf.drain(..valid + n);
2496 }
2497 None => {
2498 self.buf.drain(..valid);
2499 return out;
2500 }
2501 }
2502 }
2503 }
2504 }
2505 }
2506
2507 pub fn flush(&mut self) -> String {
2508 if self.buf.is_empty() {
2509 return String::new();
2510 }
2511 self.buf.clear();
2512 "\u{FFFD}".to_string()
2513 }
2514}
2515
2516pub struct BundleCodec {
2517 handle: std::sync::Arc<dyn superfluid_engine::Tokenizer>,
2518 vocab_hash: u64,
2519 markers: std::sync::OnceLock<superfluid_engine::Markers>,
2520}
2521
2522impl BundleCodec {
2523 #[cfg(feature = "basert")]
2524 pub fn load(model_path: &std::path::Path) -> Result<BundleCodec, Status> {
2525 let handle =
2526 superfluid_engine_ffi::TokenizerHandle::load(model_path).map_err(|_| Status::Fatal)?;
2527 Ok(Self::from_tokenizer(std::sync::Arc::new(handle)))
2528 }
2529
2530 pub fn from_tokenizer(tok: std::sync::Arc<dyn superfluid_engine::Tokenizer>) -> BundleCodec {
2531 let vocab_hash = superfluid_fingerprint::vocab_hash(tok.vocab_size(), |t| tok.token_bytes(t));
2532 BundleCodec {
2533 handle: tok,
2534 vocab_hash,
2535 markers: std::sync::OnceLock::new(),
2536 }
2537 }
2538
2539 fn markers(&self) -> &superfluid_engine::Markers {
2540 self.markers.get_or_init(|| superfluid_engine::Markers::of(self.handle.as_ref()))
2541 }
2542
2543 pub(crate) fn tokenizer(&self) -> std::sync::Arc<dyn superfluid_engine::Tokenizer> {
2544 std::sync::Arc::clone(&self.handle)
2545 }
2546
2547 pub(crate) fn fingerprint(&self, codec_id: &str, markers: &[(u32, u32, u32)])
2548 -> superfluid_fingerprint::BehaviorFingerprint {
2549 superfluid_fingerprint::BehaviorFingerprint {
2550 codec_id: codec_id.to_string(),
2551 codec_version: 2,
2552 tokenizer_hash: self.vocab_hash,
2553 marker_digest: superfluid_fingerprint::BehaviorFingerprint::marker_digest_of(markers),
2554 sampling_impl_version: 1,
2555 block_projection_version: MODEL_VISIBILITY_VERSION,
2556 grammar_version: 0,
2557 speculation: None,
2558 }
2559 }
2560}
2561
2562impl TextCodec for BundleCodec {
2563 fn encode(&self, text: &str) -> Vec<u32> {
2564 self.handle.encode(text)
2565 }
2566 fn encode_content(&self, text: &str) -> Vec<u32> {
2567 self.handle
2568 .encode_plain(text)
2569 .unwrap_or_else(|| self.markers().encode_plain(self.handle.as_ref(), text))
2570 }
2571 fn encode_pieces(&self, pieces: &[(&str, bool)]) -> Vec<u32> {
2572 self.handle
2573 .encode_pieces(pieces)
2574 .unwrap_or_else(|| self.markers().encode_pieces(self.handle.as_ref(), pieces))
2575 }
2576 fn token_bytes(&self, token: u32) -> Vec<u8> {
2577 self.handle.token_bytes(token)
2578 }
2579 fn marker_literal(&self, token: u32) -> String {
2580 let text = self.decode(&[token]);
2581 if !text.is_empty() {
2582 return text;
2583 }
2584 self.handle
2585 .special_tokens()
2586 .into_iter()
2587 .find(|(_, id)| *id == token)
2588 .map(|(s, _)| s)
2589 .unwrap_or_default()
2590 }
2591 fn behavior_fingerprint(&self) -> Option<superfluid_fingerprint::BehaviorFingerprint> {
2592 Some(self.fingerprint("raw", &[]))
2593 }
2594 fn chat_template_source(&self) -> Option<String> {
2595 let source = self.handle.chat_template_jinja();
2596 (!source.trim().is_empty()).then_some(source)
2597 }
2598 fn render_fim(&self, prefix: &str, suffix: &str, mode: u8) -> Option<Vec<u32>> {
2599 let bos = self.handle.bos_token();
2600 let atomic = |m: &str| -> bool {
2601 let mut t = self.encode(m);
2602 if bos.is_some() && t.first() == bos.as_ref() && t.len() > 1 {
2603 t.remove(0);
2604 }
2605 t.len() == 1
2606 };
2607 const PRE: &str = "<|fim_prefix|>";
2608 const SUF: &str = "<|fim_suffix|>";
2609 const MID: &str = "<|fim_middle|>";
2610 if !(atomic(PRE) && atomic(SUF) && atomic(MID)) {
2611 return None;
2612 }
2613 let pieces: [(&str, bool); 5] = if mode == fim_mode::SPM {
2614 [(PRE, false), (SUF, false), (suffix, true), (MID, false), (prefix, true)]
2615 } else {
2616 [(PRE, false), (prefix, true), (SUF, false), (suffix, true), (MID, false)]
2617 };
2618 Some(self.encode_pieces(&pieces))
2619 }
2620}
2621
2622#[cfg(feature = "basert")]
2623pub fn chatml_codec(model: &std::path::Path) -> Result<ChatMlCodec, &'static str> {
2624 let tok = superfluid_engine_ffi::TokenizerHandle::load(model).map_err(|_| "tokenizer load failed")?;
2625 chatml_codec_from_tokenizer(std::sync::Arc::new(tok))
2626}
2627
2628pub fn chatml_codec_from_tokenizer(
2629 tok: std::sync::Arc<dyn superfluid_engine::Tokenizer>,
2630) -> Result<ChatMlCodec, &'static str> {
2631 let codec = ChatMlCodec::from_tokenizer(std::sync::Arc::clone(&tok))
2632 .map_err(|_| "tokenizer lacks the ChatML markers")?;
2633 if let Ok(t) = crate::template_codec::TemplateCodec::from_tokenizer(tok) {
2634 if !codec.frames_like(&t) {
2635 return Err(
2636 "this model's chat template does not frame turns the way the curated ChatML \
2637 dialect does, so it would send every turn in a shape the checkpoint was not \
2638 trained on. Use --dialect auto or --dialect template.",
2639 );
2640 }
2641 }
2642 Ok(codec)
2643}
2644
2645#[cfg(feature = "basert")]
2646pub fn auto_codec(
2647 model: &std::path::Path,
2648) -> Result<(Box<dyn TextCodec + Send + Sync>, &'static str), &'static str> {
2649 let tok = superfluid_engine_ffi::TokenizerHandle::load(model)
2650 .map_err(|_| "no curated codec, no usable chat template, and no raw tokenizer")?;
2651 Ok(auto_codec_from_tokenizer(std::sync::Arc::new(tok)))
2652}
2653
2654pub fn auto_codec_from_tokenizer(
2655 tok: std::sync::Arc<dyn superfluid_engine::Tokenizer>,
2656) -> (Box<dyn TextCodec + Send + Sync>, &'static str) {
2657 let tmpl = crate::template_codec::TemplateCodec::from_tokenizer(std::sync::Arc::clone(&tok)).ok();
2658 let curated = chatml_codec_from_tokenizer(std::sync::Arc::clone(&tok)).ok();
2659 if let Some(c) = curated {
2660 (Box::new(c), "chatml")
2661 } else if let Some(c) = tmpl {
2662 (Box::new(c), "template")
2663 } else {
2664 (Box::new(BundleCodec::from_tokenizer(tok)), "raw")
2665 }
2666}
2667
2668pub(crate) const CONTENT_STAND_IN: &str = "BASERTCONTENTSTANDIN9f2c";
2669
2670pub(crate) fn split_once_exact<'a>(text: &'a str, needle: &str) -> Option<(&'a str, &'a str)> {
2671 let (before, after) = text.split_once(needle)?;
2672 (!after.contains(needle)).then_some((before, after))
2673}
2674
2675pub(crate) fn stand_in_key(n: usize) -> String {
2676 format!("{CONTENT_STAND_IN}{n}Z")
2677}
2678
2679pub(crate) fn stand_in_strings(v: &mut serde_json::Value, subs: &mut Vec<(String, String)>) {
2680 match v {
2681 serde_json::Value::String(text) => {
2682 let key = stand_in_key(subs.len());
2683 subs.push((key.clone(), std::mem::take(text)));
2684 *text = key;
2685 }
2686 serde_json::Value::Array(items) => items.iter_mut().for_each(|i| stand_in_strings(i, subs)),
2687 serde_json::Value::Object(map) => map.values_mut().for_each(|i| stand_in_strings(i, subs)),
2688 _ => {}
2689 }
2690}
2691
2692fn place_known_form(rest: &str, original: &str, follows: impl Fn(&str) -> bool) -> Option<usize> {
2693 let fits = |form: &str| rest.starts_with(form) && follows(&rest[form.len()..]);
2694 if fits(original) {
2695 return Some(original.len());
2696 }
2697 let trimmed = original.trim();
2698 if fits(trimmed) {
2699 return Some(trimmed.len());
2700 }
2701 let quoted = serde_json::to_string(original).unwrap_or_default();
2702 let json = quoted.get(1..quoted.len().saturating_sub(1)).unwrap_or("");
2703 let html_safe = json
2704 .replace('<', "\\u003c")
2705 .replace('>', "\\u003e")
2706 .replace('&', "\\u0026")
2707 .replace('\'', "\\u0027");
2708 if fits(&html_safe) {
2709 return Some(html_safe.len());
2710 }
2711 if fits(json) {
2712 return Some(json.len());
2713 }
2714 None
2715}
2716
2717#[derive(Debug, Clone, Copy, PartialEq, Eq)]
2718pub(crate) enum Placement {
2719 SearchFraming,
2720 KnownFormsOnly,
2721}
2722
2723pub(crate) fn split_around_stand_ins(
2724 stand_ins: &str,
2725 subs: &[(String, String)],
2726 real: &str,
2727 placement: Placement,
2728) -> Option<Vec<(String, bool)>> {
2729 let mut hits: Vec<(usize, usize)> = Vec::with_capacity(subs.len());
2730 for (i, (key, _)) in subs.iter().enumerate() {
2731 let mut it = stand_ins.match_indices(key.as_str());
2732 match (it.next(), it.next()) {
2733 (Some((pos, _)), None) => hits.push((pos, i)),
2734 _ => return None,
2735 }
2736 }
2737 hits.sort_unstable();
2738 let mut pieces: Vec<(String, bool)> = Vec::with_capacity(2 * hits.len() + 1);
2739 let (mut si_at, mut real_at) = (0usize, 0usize);
2740 for (n, (pos, i)) in hits.iter().enumerate() {
2741 let framing = &stand_ins[si_at..*pos];
2742 if !real[real_at..].starts_with(framing) {
2743 return None;
2744 }
2745 real_at += framing.len();
2746 si_at = pos + subs[*i].0.len();
2747 let last = n + 1 == hits.len();
2748 let next_end = hits.get(n + 1).map(|(p, _)| *p).unwrap_or(stand_ins.len());
2749 let next_framing = &stand_ins[si_at..next_end];
2750 let rest = &real[real_at..];
2751 let follows = |after: &str| -> bool {
2752 if last {
2753 after == next_framing
2754 } else {
2755 after.starts_with(next_framing)
2756 }
2757 };
2758 let known = place_known_form(rest, &subs[*i].1, follows);
2759 let content_end = match known {
2760 Some(end) => end,
2761 None if placement == Placement::KnownFormsOnly => return None,
2762 None if last => {
2763 if !rest.ends_with(next_framing) {
2764 return None;
2765 }
2766 rest.len() - next_framing.len()
2767 }
2768 None => {
2769 if next_framing.is_empty() {
2770 return None;
2771 }
2772 let mut it = rest.match_indices(next_framing);
2773 match (it.next(), it.next()) {
2774 (Some((p, _)), None) => p,
2775 _ => return None,
2776 }
2777 }
2778 };
2779 pieces.push((framing.to_string(), false));
2780 pieces.push((rest[..content_end].to_string(), true));
2781 real_at += content_end;
2782 }
2783 pieces.push((stand_ins[si_at..].to_string(), false));
2784 Some(pieces)
2785}
2786
2787#[cfg(test)]
2788mod glm_call_tests {
2789 use super::*;
2790
2791 fn args(raw: &str) -> (String, serde_json::Value) {
2792 args_with(raw, None)
2793 }
2794
2795 fn args_with(raw: &str, schemas: Option<&ToolSchemas>) -> (String, serde_json::Value) {
2796 let (n, a) = parse_glm_tool_call(raw, schemas).expect("parses");
2797 (n, serde_json::from_str(&a).unwrap())
2798 }
2799
2800 fn schemas(tools: serde_json::Value) -> ToolSchemas {
2801 ToolSchemas::from_values(tools.as_array().unwrap().iter())
2802 }
2803
2804 #[test]
2805 fn reads_glm_bodies_framed_or_not() {
2806 let (n, a) = args("get_weather<arg_key>city</arg_key><arg_value>Paris</arg_value>");
2807 assert_eq!(n, "get_weather");
2808 assert_eq!(a, serde_json::json!({"city": "Paris"}));
2809 let (n2, a2) = args("<tool_call>get_weather<arg_key>city</arg_key><arg_value>Paris</arg_value></tool_call>");
2810 assert_eq!((n2, a2), (n, a));
2811 }
2812
2813 #[test]
2814 fn non_strings_come_back_typed_strings_stay_raw() {
2815 let (_, a) = args(
2816 "f<arg_key>n</arg_key><arg_value>3</arg_value><arg_key>flag</arg_key><arg_value>true</arg_value>\
2817 <arg_key>o</arg_key><arg_value>{\"x\": [1, 2]}</arg_value><arg_key>s</arg_key><arg_value>\"quoted\"</arg_value>\
2818 <arg_key>txt</arg_key><arg_value>a < b & {not json</arg_value>",
2819 );
2820 assert_eq!(
2821 a,
2822 serde_json::json!({"n": 3, "flag": true, "o": {"x": [1, 2]}, "s": "\"quoted\"", "txt": "a < b & {not json"})
2823 );
2824 }
2825
2826 #[test]
2827 fn declared_strings_stay_strings_and_declared_types_are_honoured() {
2828 let s = schemas(serde_json::json!([{
2829 "type": "function",
2830 "function": {"name": "f", "parameters": {"type": "object", "properties": {
2831 "id": {"type": "string"}, "flag": {"type": "string"}, "nil": {"type": "string"},
2832 "obj": {"type": "string"},
2833 "n": {"type": "integer"}, "on": {"type": "boolean"}, "list": {"type": "array"},
2834 "maybe": {"type": ["integer", "null"]}, "either": {"anyOf": [{"type": "integer"}, {"type": "string"}]},
2835 "pick": {"enum": ["1", "2"]},
2836 "loose": {}
2837 }}}
2838 }]));
2839 let raw = "f<arg_key>id</arg_key><arg_value>3</arg_value><arg_key>flag</arg_key><arg_value>true</arg_value>\
2840 <arg_key>nil</arg_key><arg_value>null</arg_value><arg_key>obj</arg_key><arg_value>{\"a\": 1}</arg_value>\
2841 <arg_key>n</arg_key><arg_value>42</arg_value><arg_key>on</arg_key><arg_value>false</arg_value>\
2842 <arg_key>list</arg_key><arg_value>[1, \"x\"]</arg_value><arg_key>maybe</arg_key><arg_value>null</arg_value>\
2843 <arg_key>either</arg_key><arg_value>7</arg_value><arg_key>pick</arg_key><arg_value>2</arg_value>\
2844 <arg_key>loose</arg_key><arg_value>5</arg_value><arg_key>undeclared</arg_key><arg_value>false</arg_value>";
2845 let (_, a) = args_with(raw, Some(&s));
2846 assert_eq!(
2847 a,
2848 serde_json::json!({
2849 "id": "3", "flag": "true", "nil": "null", "obj": "{\"a\": 1}",
2850 "n": 42, "on": false, "list": [1, "x"], "maybe": null, "either": 7, "pick": "2",
2851 "loose": 5, "undeclared": false
2852 })
2853 );
2854 let (_, b) = args_with("g<arg_key>id</arg_key><arg_value>3</arg_value>", Some(&s));
2855 assert_eq!(b, serde_json::json!({"id": 3}));
2856 let anth = schemas(serde_json::json!([{"name": "f", "input_schema": {"properties": {"id": {"type": "string"}}}}]));
2857 assert_eq!(args_with("f<arg_key>id</arg_key><arg_value>3</arg_value>", Some(&anth)).1, serde_json::json!({"id": "3"}));
2858 }
2859
2860 #[test]
2861 fn string_values_keep_their_boundary_whitespace() {
2862 let raw = "write<arg_key>content</arg_key><arg_value>\n indented\n</arg_value>\
2863 <arg_key>pad</arg_key><arg_value> x </arg_value><arg_key>n</arg_key><arg_value> 3 </arg_value>";
2864 let (_, a) = args(raw);
2865 assert_eq!(a, serde_json::json!({"content": "\n indented\n", "pad": " x ", "n": 3}));
2866 let s = schemas(serde_json::json!([{"name": "write", "parameters": {"properties": {"n": {"type": "string"}}}}]));
2867 assert_eq!(args_with(raw, Some(&s)).1["n"], " 3 ");
2868 let (n, a) = args("<tool_call>f\n<arg_key>k</arg_key>\n<arg_value>v</arg_value>\n</tool_call>");
2869 assert_eq!((n.as_str(), a), ("f", serde_json::json!({"k": "v"})));
2870 }
2871
2872 #[test]
2873 fn a_call_cut_inside_an_argument_is_not_a_call() {
2874 for cut in [
2875 "f<arg_key>path</arg_key><arg_value>/etc/pas",
2876 "f<arg_key>path</arg_key><arg_value>",
2877 "f<arg_key>path</arg_key>",
2878 "f<arg_key>pa",
2879 "f<arg_key>",
2880 "f<arg_key>a</arg_key><arg_value>1</arg_value><arg_key>b</arg_key><arg_value>par",
2881 "f<arg_key>a</arg_key><arg_value>1</arg_value><arg_key>b",
2882 ] {
2883 assert_eq!(parse_glm_tool_call(cut, None), None, "{cut:?}");
2884 }
2885 assert!(parse_glm_tool_call("f<arg_key>a</arg_key><arg_value>1</arg_value>", None).is_some());
2886 assert_eq!(
2887 parse_glm_tool_call("f<arg_key>a</arg_key>junk<arg_value>1</arg_value>", None),
2888 None
2889 );
2890 for junk in [
2891 "f<arg_key>a</arg_key><arg_value>1</arg_value>junk",
2892 "f<arg_key>a</arg_key><arg_value>1</arg_value>junk<arg_key>b</arg_key><arg_value>2</arg_value>",
2893 ] {
2894 assert_eq!(parse_glm_tool_call(junk, None), None, "{junk:?}");
2895 }
2896 assert!(parse_glm_tool_call("f<arg_key>a</arg_key><arg_value>1</arg_value>\n<arg_key>b</arg_key><arg_value>2</arg_value>\n", None).is_some());
2897 }
2898
2899 #[test]
2900 fn refs_and_non_string_value_sets_are_honoured() {
2901 let tool = serde_json::json!({"name": "f", "parameters": {
2902 "type": "object",
2903 "$defs": {"Id": {"type": "string"}, "Level": {"enum": [1, 2, 3]}, "Alias": {"$ref": "#/$defs/Id"}},
2904 "properties": {
2905 "id": {"$ref": "#/$defs/Id"}, "alias": {"$ref": "#/$defs/Alias"},
2906 "level": {"$ref": "#/$defs/Level"}, "one": {"const": 1}, "mixed": {"enum": ["a", 2, null]},
2907 "either": {"anyOf": [{"$ref": "#/$defs/Id"}, {"type": "null"}]}
2908 }
2909 }});
2910 let s = schemas(serde_json::json!([tool.clone()]));
2911 let raw = "f<arg_key>id</arg_key><arg_value>3</arg_value><arg_key>alias</arg_key><arg_value>true</arg_value>\
2912 <arg_key>level</arg_key><arg_value>2</arg_value><arg_key>one</arg_key><arg_value>1</arg_value>\
2913 <arg_key>mixed</arg_key><arg_value>a</arg_value><arg_key>either</arg_key><arg_value>null</arg_value>";
2914 assert_eq!(
2915 args_with(raw, Some(&s)).1,
2916 serde_json::json!({"id": "3", "alias": "true", "level": 2, "one": 1, "mixed": "a", "either": null})
2917 );
2918
2919 let tag: serde_json::Value = serde_json::from_str(
2920 &ToolEnvelope::Glm.structural_tag(&[tool.to_string()], "<tool_call>", "</tool_call>", true).unwrap(),
2921 )
2922 .unwrap();
2923 let el = tag["format"]["tags"][0]["content"]["elements"].as_array().unwrap().clone();
2924 let value = |i: usize| el[i]["content"]["elements"][1]["content"].clone();
2925 assert_eq!(value(1)["type"], "any_text", "id: a string through its ref");
2926 assert_eq!(value(2)["type"], "any_text", "alias: a ref to a ref to a string");
2927 assert_eq!(
2928 value(3),
2929 serde_json::json!({"type": "or", "elements": [
2930 {"type": "const_string", "value": "1"}, {"type": "const_string", "value": "2"}, {"type": "const_string", "value": "3"}
2931 ]})
2932 );
2933 assert_eq!(value(4), serde_json::json!({"type": "const_string", "value": "1"}));
2934 assert_eq!(
2935 value(5),
2936 serde_json::json!({"type": "or", "elements": [
2937 {"type": "const_string", "value": "a"}, {"type": "const_string", "value": "2"}, {"type": "const_string", "value": "null"}
2938 ]})
2939 );
2940 assert_eq!(python_json(&serde_json::json!({"k": [1, "é"], "n": null})), "{\"k\": [1, \"é\"], \"n\": null}");
2941 }
2942
2943 #[test]
2944 fn a_root_level_ref_names_the_argument_object() {
2945 let tool = serde_json::json!({"name": "f", "parameters": {
2946 "$ref": "#/$defs/Args",
2947 "$defs": {
2948 "Args": {"type": "object", "properties": {"id": {"$ref": "#/$defs/Id"}, "n": {"type": "integer"}}, "required": ["id"]},
2949 "Id": {"type": "string"}
2950 }
2951 }});
2952 let s = schemas(serde_json::json!([tool.clone()]));
2953 assert_eq!(parse_glm_tool_call("f", Some(&s)), None, "`id` is required");
2954 assert_eq!(
2955 args_with("f<arg_key>id</arg_key><arg_value>3</arg_value><arg_key>n</arg_key><arg_value>4</arg_value>", Some(&s)).1,
2956 serde_json::json!({"id": "3", "n": 4})
2957 );
2958 let tag: serde_json::Value = serde_json::from_str(
2959 &ToolEnvelope::Glm.structural_tag(&[tool.to_string()], "<tool_call>", "</tool_call>", true).unwrap(),
2960 )
2961 .unwrap();
2962 let el = tag["format"]["tags"][0]["content"]["elements"].as_array().unwrap().clone();
2963 assert_eq!(el[1]["elements"][0]["value"], "<arg_key>id</arg_key>", "required, so not optional");
2964 assert_eq!(el[1]["elements"][1]["content"]["type"], "any_text");
2965 assert_eq!(el[2]["type"], "optional");
2966 assert_eq!(el[2]["content"]["elements"][1]["content"]["json_schema"]["type"], "integer");
2967 }
2968
2969 #[test]
2970 fn ref_siblings_composed_sets_and_padded_nullable_strings() {
2971 let tool = serde_json::json!({"name": "f", "parameters": {
2972 "$ref": "#/$defs/Args",
2973 "required": ["extra"],
2974 "properties": {"extra": {"type": "string"}},
2975 "$defs": {
2976 "Args": {"type": "object", "properties": {"id": {"type": "string"}}, "required": ["id"]},
2977 "Mode": {"type": "string"}
2978 }
2979 }});
2980 let s = schemas(serde_json::json!([tool.clone()]));
2981 assert_eq!(
2982 parse_glm_tool_call("f<arg_key>id</arg_key><arg_value>1</arg_value>", Some(&s)),
2983 None,
2984 "`extra`, required beside the $ref, is missing"
2985 );
2986 assert!(parse_glm_tool_call(
2987 "f<arg_key>id</arg_key><arg_value>1</arg_value><arg_key>extra</arg_key><arg_value>x</arg_value>",
2988 Some(&s)
2989 )
2990 .is_some());
2991 let tag: serde_json::Value = serde_json::from_str(
2992 &ToolEnvelope::Glm.structural_tag(&[tool.to_string()], "<tool_call>", "</tool_call>", true).unwrap(),
2993 )
2994 .unwrap();
2995 let keys: Vec<String> = tag["format"]["tags"][0]["content"]["elements"]
2996 .as_array()
2997 .unwrap()
2998 .iter()
2999 .filter_map(|e| e["elements"][0]["value"].as_str().map(String::from))
3000 .collect();
3001 assert_eq!(keys, ["<arg_key>id</arg_key>", "<arg_key>extra</arg_key>"], "both required");
3002
3003 let tool = serde_json::json!({"name": "g", "parameters": {"type": "object",
3004 "$defs": {"Mode": {"type": "string"}},
3005 "properties": {
3006 "mode": {"$ref": "#/$defs/Mode", "enum": ["r", "w"]},
3007 "pick": {"anyOf": [{"const": "a"}, {"oneOf": [{"const": "b"}, {"enum": [1]}]}]},
3008 "open": {"anyOf": [{"const": "a"}, {"type": "string"}]},
3009 "maybe": {"type": ["string", "null"]}
3010 },
3011 "required": ["mode", "pick", "open", "maybe"]
3012 }});
3013 let tag: serde_json::Value = serde_json::from_str(
3014 &ToolEnvelope::Glm.structural_tag(&[tool.to_string()], "<tool_call>", "</tool_call>", true).unwrap(),
3015 )
3016 .unwrap();
3017 let el = tag["format"]["tags"][0]["content"]["elements"].as_array().unwrap().clone();
3018 let value = |i: usize| el[i]["elements"][1]["content"].clone();
3019 let consts = |vs: &[&str]| {
3020 serde_json::json!({"type": "or", "elements": vs.iter().map(|v| serde_json::json!({"type": "const_string", "value": v})).collect::<Vec<_>>()})
3021 };
3022 assert_eq!(value(1), consts(&["r", "w"]));
3023 assert_eq!(value(2), consts(&["a", "b", "1"]));
3024 assert_eq!(value(3)["type"], "any_text", "an open alternative keeps it open");
3025
3026 let s = schemas(serde_json::json!([tool]));
3027 let read = |v: &str| {
3028 args_with(
3029 &format!("g<arg_key>mode</arg_key><arg_value>r</arg_value><arg_key>pick</arg_key><arg_value>a</arg_value>\
3030 <arg_key>open</arg_key><arg_value>a</arg_value><arg_key>maybe</arg_key><arg_value>{v}</arg_value>"),
3031 Some(&s),
3032 )
3033 .1["maybe"]
3034 .clone()
3035 };
3036 assert_eq!(read("null"), serde_json::Value::Null, "tojson's null");
3037 assert_eq!(read(" null "), " null ", "padded: written raw, so the string");
3038 assert_eq!(read("\nnull"), "\nnull");
3039 }
3040
3041 #[test]
3042 fn real_glm_5_3_calls_read_exactly() {
3043 let s = schemas(serde_json::json!([
3044 {"type": "function", "function": {"name": "write", "parameters": {"type": "object",
3045 "properties": {"filePath": {"type": "string"}, "content": {"type": "string"}}, "required": ["filePath", "content"]}}},
3046 {"type": "function", "function": {"name": "bash", "parameters": {"type": "object",
3047 "properties": {"command": {"type": "string"}, "timeout": {"type": "integer"}, "description": {"type": "string"}},
3048 "required": ["command", "description"]}}},
3049 {"type": "function", "function": {"name": "todowrite", "parameters": {"type": "object",
3050 "properties": {"todos": {"type": "array", "items": {"type": "object"}}}, "required": ["todos"]}}},
3051 {"type": "function", "function": {"name": "set_port", "parameters": {"type": "object",
3052 "properties": {"port": {"type": "string"}, "enabled": {"type": "boolean"}}, "required": ["port", "enabled"]}}}
3053 ]));
3054 let cases = [
3055 (
3056 "set_port<arg_key>port</arg_key><arg_value>8080</arg_value><arg_key>enabled</arg_key><arg_value>true</arg_value>",
3057 serde_json::json!({"port": "8080", "enabled": true}),
3058 ),
3059 (
3060 "write<arg_key>filePath</arg_key><arg_value>/tmp/demo/app.py</arg_value><arg_key>content</arg_key><arg_value>from flask import Flask, jsonify\n\napp = Flask(__name__)\n\n\n@app.route(\"/\")\ndef index():\n return jsonify(message=\"Hello, World!\")\n\n\nif __name__ == \"__main__\":\n app.run(host=\"0.0.0.0\", port=5000)\n</arg_value>",
3061 serde_json::json!({"filePath": "/tmp/demo/app.py", "content": "from flask import Flask, jsonify\n\napp = Flask(__name__)\n\n\n@app.route(\"/\")\ndef index():\n return jsonify(message=\"Hello, World!\")\n\n\nif __name__ == \"__main__\":\n app.run(host=\"0.0.0.0\", port=5000)\n"}),
3062 ),
3063 (
3064 "todowrite<arg_key>todos</arg_key><arg_value>[{\"content\": \"Read and analyze the RFI PDF (requirements, deadlines, submission format)\", \"status\": \"in_progress\", \"priority\": \"high\"}, {\"content\": \"Extract key questions and map them to our capabilities/evidence\", \"status\": \"pending\", \"priority\": \"high\"}]</arg_value>",
3065 serde_json::json!({"todos": [
3066 {"content": "Read and analyze the RFI PDF (requirements, deadlines, submission format)", "status": "in_progress", "priority": "high"},
3067 {"content": "Extract key questions and map them to our capabilities/evidence", "status": "pending", "priority": "high"}
3068 ]}),
3069 ),
3070 (
3071 "bash<arg_key>command</arg_key><arg_value>ls -lh ./inputs</arg_value><arg_key>description</arg_key><arg_value>List files in ./inputs with sizes</arg_value>",
3072 serde_json::json!({"command": "ls -lh ./inputs", "description": "List files in ./inputs with sizes"}),
3073 ),
3074 ];
3075 for (body, want) in cases {
3076 assert_eq!(args_with(body, Some(&s)).1, want, "{body}");
3077 }
3078 }
3079
3080 #[test]
3081 fn required_arguments_must_be_present_when_the_schema_is_known() {
3082 let s = schemas(serde_json::json!([
3083 {"name": "write_file", "parameters": {"type": "object",
3084 "properties": {"path": {"type": "string"}, "content": {"type": "string"}, "mode": {"type": "string"}},
3085 "required": ["path", "content"]}},
3086 {"name": "list_files", "parameters": {"type": "object", "properties": {"dir": {"type": "string"}}}}
3087 ]));
3088 for cut in [
3089 "<tool_call>write_file",
3090 "write_file<arg_key>path</arg_key><arg_value>/a</arg_value>",
3091 "write_file<arg_key>content</arg_key><arg_value>x</arg_value><arg_key>mode</arg_key><arg_value>w</arg_value>",
3092 ] {
3093 assert_eq!(parse_glm_tool_call(cut, Some(&s)), None, "{cut:?}");
3094 }
3095 assert!(parse_glm_tool_call(
3096 "write_file<arg_key>path</arg_key><arg_value>/a</arg_value><arg_key>content</arg_key><arg_value></arg_value>",
3097 Some(&s)
3098 )
3099 .is_some());
3100 assert_eq!(parse_glm_tool_call("list_files", Some(&s)), Some(("list_files".into(), "{}".into())));
3101 assert!(parse_glm_tool_call("write_file", None).is_some());
3102 }
3103
3104 #[test]
3105 fn no_arguments_and_bad_names() {
3106 assert_eq!(parse_glm_tool_call("<tool_call>ping</tool_call>", None), Some(("ping".into(), "{}".into())));
3107 assert_eq!(parse_glm_tool_call("<tool_call>\nping\n</tool_call>", None), Some(("ping".into(), "{}".into())));
3108 assert_eq!(parse_glm_tool_call("{\"name\": \"f\"}", None), None, "JSON is not a GLM body");
3109 assert_eq!(parse_glm_tool_call(" ", None), None);
3110 assert_eq!(parse_glm_tool_call("<tool_call>I cannot call that</tool_call>", None), None);
3111 assert_eq!(
3112 parse_glm_tool_call("I will call f<arg_key>a</arg_key><arg_value>1</arg_value>", None),
3113 None
3114 );
3115 assert_eq!(
3116 parse_glm_tool_call("mcp__fs.read-file<arg_key>a</arg_key><arg_value>1</arg_value>", None).map(|c| c.0),
3117 Some("mcp__fs.read-file".into())
3118 );
3119 }
3120
3121 #[test]
3122 fn forced_choice_gets_a_structural_tag_and_auto_stays_free() {
3123 let tool = serde_json::json!({"type": "function", "function": {"name": "write_file", "parameters": {
3124 "type": "object",
3125 "properties": {"path": {"type": "string"}, "mode": {"enum": ["w", "a"]}, "lines": {"type": "integer"}},
3126 "required": ["path"]
3127 }}})
3128 .to_string();
3129 let e = ToolEnvelope::Glm;
3130 assert_eq!(e.structural_tag(std::slice::from_ref(&tool), "<tool_call>", "</tool_call>", false), None);
3131 let tag: serde_json::Value = serde_json::from_str(
3132 &e.structural_tag(std::slice::from_ref(&tool), "<tool_call>", "</tool_call>", true).expect("forced tag"),
3133 )
3134 .unwrap();
3135 assert_eq!(tag["type"], "structural_tag");
3136 let f = &tag["format"];
3137 assert_eq!((f["type"].as_str(), f["at_least_one"].as_bool()), (Some("triggered_tags"), Some(true)));
3138 assert_eq!(f["stop_after_first"], true);
3139 assert_eq!(f["triggers"], serde_json::json!(["<tool_call>"]));
3140 let t = &f["tags"][0];
3141 assert_eq!((t["begin"].as_str(), t["end"].as_str()), (Some("<tool_call>"), Some("</tool_call>")));
3142 let el = t["content"]["elements"].as_array().unwrap();
3143 assert_eq!(el[0], serde_json::json!({"type": "const_string", "value": "write_file"}));
3144 assert_eq!(el[1]["elements"][0]["value"], "<arg_key>path</arg_key>");
3145 assert_eq!(el[1]["elements"][1]["begin"], "<arg_value>");
3146 assert_eq!(el[1]["elements"][1]["content"]["type"], "any_text");
3147 assert_eq!(el[1]["elements"][1]["end"], "</arg_value>");
3148 assert_eq!(el[2]["type"], "optional");
3149 assert_eq!(el[2]["content"]["elements"][1]["content"]["type"], "or");
3150 assert_eq!(el[3]["content"]["elements"][1]["content"]["type"], "json_schema");
3151 assert_eq!(el[3]["content"]["elements"][1]["content"]["json_schema"], serde_json::json!({"type": "integer"}));
3152 assert_eq!(e.call_grammar(&[tool]), None);
3153
3154 let tool = serde_json::json!({"name": "op", "parameters": {
3155 "type": "object",
3156 "$defs": {"P": {"type": "object", "properties": {"x": {"type": "integer"}}}},
3157 "properties": {"kind": {"type": "string", "const": "move"}, "to": {"type": "array", "items": {"$ref": "#/$defs/P"}}},
3158 "required": ["kind", "to", "why"]
3159 }})
3160 .to_string();
3161 let tag: serde_json::Value =
3162 serde_json::from_str(&e.structural_tag(&[tool], "<tool_call>", "</tool_call>", true).unwrap()).unwrap();
3163 let el = tag["format"]["tags"][0]["content"]["elements"].as_array().unwrap().clone();
3164 assert_eq!(el[1]["elements"][1]["content"], serde_json::json!({"type": "const_string", "value": "move"}));
3165 assert_eq!(el[2]["elements"][1]["content"]["json_schema"]["$defs"]["P"]["type"], "object");
3166 assert_eq!(el[3]["elements"][0]["value"], "<arg_key>why</arg_key>");
3167 assert_eq!(el[3]["elements"][1]["content"]["type"], "any_text");
3168 }
3169
3170 #[test]
3171 fn glm_override_is_taken_where_the_template_spells_the_glm_wire() {
3172 struct T(Option<&'static str>);
3173 impl TextCodec for T {
3174 fn encode(&self, text: &str) -> Vec<u32> {
3175 MockCodec.encode(text)
3176 }
3177 fn token_bytes(&self, token: u32) -> Vec<u8> {
3178 MockCodec.token_bytes(token)
3179 }
3180 fn chat_template_source(&self) -> Option<String> {
3181 self.0.map(str::to_string)
3182 }
3183 }
3184 let glm = T(Some(include_str!("../../../tests/fixtures/chat_templates/glm_5_3.jinja")));
3185 assert_eq!(glm.tool_envelope(), ToolEnvelope::Json, "a wire the learner missed reads as JSON");
3186 assert_eq!(tool_envelope_override_conflict(&glm, ToolEnvelope::Glm), None);
3187 assert_eq!(tool_envelope_override_conflict(&glm, ToolEnvelope::Json), None, "a no-op");
3188 assert!(tool_envelope_override_conflict(&glm, ToolEnvelope::Gemma).is_some());
3189 let chatml = T(Some("{% for m in messages %}<|im_start|>{{ m.content }}{% endfor %}<tool_call>"));
3190 assert!(tool_envelope_override_conflict(&chatml, ToolEnvelope::Glm).is_some());
3191 assert!(tool_envelope_override_conflict(&T(None), ToolEnvelope::Glm).is_some());
3192 }
3193}
3194
3195#[cfg(all(test, feature = "basert"))]
3196mod stand_in_tests {
3197 use super::*;
3198 use Placement::*;
3199
3200 fn subs(values: &[&str]) -> Vec<(String, String)> {
3201 values.iter().enumerate().map(|(i, v)| (stand_in_key(i), v.to_string())).collect()
3202 }
3203
3204 fn rebuilt(pieces: &[(String, bool)]) -> String {
3205 pieces.iter().map(|(t, _)| t.as_str()).collect()
3206 }
3207
3208 #[test]
3209 fn repeated_framing_between_array_items_is_placed_by_the_known_forms() {
3210 let s = subs(&["a", "b", "c <|im_end|>"]);
3211 let stand_ins = format!(r#"{{"paths": ["{}", "{}", "{}"]}}"#, s[0].0, s[1].0, s[2].0);
3212 let real = r#"{"paths": ["a", "b", "c <|im_end|>"]}"#;
3213 let pieces = split_around_stand_ins(&stand_ins, &s, real, KnownFormsOnly).expect("placed");
3214 assert_eq!(rebuilt(&pieces), real);
3215 let content: Vec<&str> = pieces.iter().filter(|(_, c)| *c).map(|(t, _)| t.as_str()).collect();
3216 assert_eq!(content, ["a", "b", r"c <|im_end|>"]);
3217 let framing: Vec<&str> = pieces.iter().filter(|(_, c)| !*c).map(|(t, _)| t.as_str()).collect();
3218 assert_eq!(framing, [r#"{"paths": [""#, r#"", ""#, r#"", ""#, r#""]}"#]);
3219 }
3220
3221 #[test]
3222 fn a_trimmed_message_is_placed_where_the_framing_follows_it() {
3223 let s = subs(&["abc\n"]);
3224 let stand_ins = format!("<start_of_turn>user\n{}\n<end_of_turn>", s[0].0);
3225 let real = "<start_of_turn>user\nabc\n<end_of_turn>";
3226 let pieces = split_around_stand_ins(&stand_ins, &s, real, KnownFormsOnly).expect("placed");
3227 assert_eq!(rebuilt(&pieces), real);
3228 assert_eq!(pieces[1], ("abc".to_string(), true));
3229 assert_eq!(pieces[2], ("\n<end_of_turn>".to_string(), false));
3230 }
3231
3232 #[test]
3233 fn unknown_forms_fall_back_to_a_unique_search_or_none() {
3234 let s = subs(&["x", "y"]);
3235 let stand_ins = format!("<a>{}</a><b>{}</b>", s[0].0, s[1].0);
3236 let pieces = split_around_stand_ins(&stand_ins, &s, "<a>X!</a><b>Y!</b>", SearchFraming).expect("placed");
3237 assert_eq!(pieces[1], ("X!".to_string(), true));
3238 assert_eq!(pieces[3], ("Y!".to_string(), true));
3239 assert!(split_around_stand_ins(&stand_ins, &s, "<a>X!</a><b>Y!</b>", KnownFormsOnly).is_none());
3240 assert!(split_around_stand_ins(&stand_ins, &s, "<a>X</a><b></a><b>Y</b>", SearchFraming).is_none());
3241 assert!(split_around_stand_ins("<a></a>", &s, "<a>x</a>", SearchFraming).is_none());
3242 }
3243
3244 #[test]
3245 fn framing_a_template_emits_only_for_the_real_text_is_never_content() {
3246 let s = subs(&["<tool_response>r</tool_response>"]);
3247 let stand_ins = format!("<|im_start|>user\n{}<|im_end|>", s[0].0);
3248 let real = "<|im_start|>user\n<wrapped><tool_response>r</tool_response></wrapped><|im_end|>";
3249 assert!(split_around_stand_ins(&stand_ins, &s, real, KnownFormsOnly).is_none());
3250 let pieces = split_around_stand_ins(&stand_ins, &s, real, SearchFraming).expect("placed");
3251 assert_eq!(pieces[1].0, "<wrapped><tool_response>r</tool_response></wrapped>");
3252 }
3253
3254 #[test]
3255 fn known_forms_cover_tojson_and_trim() {
3256 let always = |_: &str| true;
3257 let original = " <x> & 'y'\n";
3258 assert_eq!(place_known_form(" <x> & 'y'\n|", original, always), Some(original.len()));
3259 assert_eq!(place_known_form("<x> & 'y'|", original, always), Some("<x> & 'y'".len()));
3260 let html_safe = r" <x> & 'y'\n";
3261 assert_eq!(place_known_form(&format!("{html_safe}|"), original, always), Some(html_safe.len()));
3262 assert_eq!(place_known_form(" <x> & 'y'\\n|", original, always), Some(" <x> & 'y'\\n".len()));
3263 assert_eq!(place_known_form("other", "plain", always), None);
3264 assert_eq!(place_known_form("abc\n<e>", "abc\n", |after: &str| after == "\n<e>"), Some(3));
3265 }
3266}
3267
3268#[derive(Debug, Clone, Copy)]
3269enum Piece<'a> {
3270 Framing(&'a str),
3271 Content(&'a str),
3272}
3273
3274impl<'a> Piece<'a> {
3275 fn as_pair(&self) -> (&'a str, bool) {
3276 match *self {
3277 Piece::Framing(t) => (t, false),
3278 Piece::Content(t) => (t, true),
3279 }
3280 }
3281}
3282
3283fn pieces_of(owned: &[(String, bool)]) -> Vec<Piece<'_>> {
3284 owned
3285 .iter()
3286 .map(|(s, content)| {
3287 if *content {
3288 Piece::Content(s)
3289 } else {
3290 Piece::Framing(s)
3291 }
3292 })
3293 .collect()
3294}
3295
3296pub struct ChatMlCodec {
3297 inner: BundleCodec,
3298 im_start: u32,
3299 im_end: u32,
3300 newline: u32,
3301 think_open: Option<u32>,
3302 think_close: Option<u32>,
3303 tool_open: u32,
3304 tool_close: u32,
3305 think: ThinkSuffixes,
3306 template: Option<Box<crate::template_codec::TemplateCodec>>,
3307 derive_tool_wire: bool,
3308 thinking_switch: bool,
3309 assistant_openers: std::sync::OnceLock<AssistantOpeners>,
3312}
3313
3314#[derive(Debug, Clone, Default, PartialEq, Eq)]
3318struct AssistantOpeners {
3319 in_history: String,
3320 last: String,
3321}
3322
3323#[derive(Debug, Clone, PartialEq, Eq)]
3324struct ThinkSuffixes {
3325 default: Vec<u32>,
3326 on: Vec<u32>,
3327 off: Vec<u32>,
3328}
3329
3330impl ChatMlCodec {
3331 #[cfg(feature = "basert")]
3332 pub fn load(model_path: &std::path::Path) -> Result<ChatMlCodec, Status> {
3333 let tok = superfluid_engine_ffi::TokenizerHandle::load(model_path).map_err(|_| Status::Fatal)?;
3334 Self::from_tokenizer(std::sync::Arc::new(tok))
3335 }
3336
3337 pub fn from_tokenizer(
3338 tok: std::sync::Arc<dyn superfluid_engine::Tokenizer>,
3339 ) -> Result<ChatMlCodec, Status> {
3340 let inner = BundleCodec::from_tokenizer(std::sync::Arc::clone(&tok));
3341 let one = |s: &str| -> Result<u32, Status> {
3342 let t = inner.encode(s);
3343 if t.len() == 1 {
3344 Ok(t[0])
3345 } else {
3346 Err(Status::Unsupported)
3347 }
3348 };
3349 let tmpl = crate::template_codec::TemplateCodec::from_tokenizer(tok).ok();
3350 let (think_open, think_close) = match (one("<think>"), one("</think>")) {
3351 (Ok(o), Ok(c)) => (Some(o), Some(c)),
3352 _ => (None, None),
3353 };
3354 let mut codec = ChatMlCodec {
3355 im_start: one("<|im_start|>")?,
3356 im_end: one("<|im_end|>")?,
3357 newline: one("\n")?,
3358 think_open,
3359 think_close,
3360 tool_open: one("<tool_call>")?,
3361 tool_close: one("</tool_call>")?,
3362 inner,
3363 think: ThinkSuffixes { default: Vec::new(), on: Vec::new(), off: Vec::new() },
3364 derive_tool_wire: tmpl.as_ref().is_some_and(|t| !t.tool_body_is_json()),
3365 template: tmpl.map(Box::new),
3366 thinking_switch: false,
3367 assistant_openers: std::sync::OnceLock::new(),
3368 };
3369 codec.think = codec.derive_think_suffixes().unwrap_or_else(|| codec.assumed_think_suffixes());
3370 codec.thinking_switch = thinking_switch_changes(|msgs, tools, on| {
3371 let mut kw = serde_json::Map::new();
3372 kw.insert("enable_thinking".into(), serde_json::Value::Bool(on));
3373 codec.render_prompt_structured_with(msgs, tools, &kw)
3374 });
3375 Ok(codec)
3376 }
3377
3378 pub fn with_thinking(mut self, enabled: bool) -> ChatMlCodec {
3379 self.think.default = if enabled { self.think.on.clone() } else { self.think.off.clone() };
3380 self
3381 }
3382
3383 fn derive_think_suffixes(&self) -> Option<ThinkSuffixes> {
3384 let t = self.template.as_deref()?;
3385 let (open, close) = (self.think_open?, self.think_close?);
3386 let mut opener = vec![self.im_start];
3387 opener.extend(self.inner.encode("assistant\n"));
3388 let probe = [ChatMessage::new(crate::wal::role::USER, "probe".to_string())];
3389 let suffix = |thinking: Option<bool>| -> Option<Vec<u32>> {
3390 let mut kw = serde_json::Map::new();
3391 if let Some(on) = thinking {
3392 kw.insert("enable_thinking".to_string(), serde_json::Value::Bool(on));
3393 }
3394 let with = t.render_prompt_structured_with(&probe, &[], &kw)?;
3395 let without = t.render_turns_structured_with(&probe, &[], &kw)?;
3396 let s = with.strip_prefix(without.as_slice())?.strip_prefix(opener.as_slice())?;
3397 let markup_only = s.iter().all(|&tok| {
3398 tok == open
3399 || tok == close
3400 || self.inner.token_bytes(tok).iter().all(u8::is_ascii_whitespace)
3401 });
3402 markup_only.then(|| s.to_vec())
3403 };
3404 Some(ThinkSuffixes { default: suffix(None)?, on: suffix(Some(true))?, off: suffix(Some(false))? })
3405 }
3406
3407 fn reopen_for(&self, mut prompt: Vec<u32>, kwargs: &serde_json::Map<String, serde_json::Value>) -> Vec<u32> {
3408 let Some(thinking) = kwargs.get("enable_thinking").and_then(|v| v.as_bool()) else {
3409 return prompt;
3410 };
3411 let unset = self.generation_prefix_with(TurnState::AfterInput, None);
3412 let want = self.generation_prefix_with(TurnState::AfterInput, Some(thinking));
3413 if let (Some(unset), Some(want)) = (unset, want) {
3414 if prompt.ends_with(&unset) {
3415 prompt.truncate(prompt.len() - unset.len());
3416 prompt.extend(want);
3417 }
3418 }
3419 prompt
3420 }
3421
3422 fn assumed_think_suffixes(&self) -> ThinkSuffixes {
3423 let off = if self.think_open.is_some() {
3424 self.inner.encode("<think>\n\n</think>\n\n")
3425 } else {
3426 Vec::new()
3427 };
3428 ThinkSuffixes { default: Vec::new(), on: Vec::new(), off }
3429 }
3430
3431 fn role_str(role: u32) -> &'static str {
3432 match role {
3433 crate::wal::role::SYSTEM => "system",
3434 crate::wal::role::ASSISTANT => "assistant",
3435 _ => "user",
3436 }
3437 }
3438
3439 fn inserted_text(
3440 t: &crate::template_codec::TemplateCodec,
3441 with: &[u32],
3442 without: &[u32],
3443 ) -> Option<String> {
3444 let pre = with
3445 .iter()
3446 .zip(without.iter())
3447 .take_while(|(a, b)| a == b)
3448 .count();
3449 let suf = with[pre..]
3450 .iter()
3451 .rev()
3452 .zip(without[pre..].iter().rev())
3453 .take_while(|(a, b)| a == b)
3454 .count();
3455 if with.len() <= pre + suf {
3456 return None;
3457 }
3458 let text = t.decode(&with[pre..with.len() - suf]);
3459 let trimmed = text.trim();
3460 (!trimmed.is_empty()).then(|| trimmed.to_string())
3461 }
3462
3463 fn tool_source(&self) -> Option<&crate::template_codec::TemplateCodec> {
3464 if self.derive_tool_wire {
3465 self.template.as_deref()
3466 } else {
3467 None
3468 }
3469 }
3470
3471 fn turn(&self, role: u32, pieces: &[Piece<'_>]) -> Vec<u32> {
3472 let role_line = format!("{}\n", Self::role_str(role));
3473 let mut body: Vec<(&str, bool)> = Vec::with_capacity(pieces.len() + 1);
3474 body.push((&role_line, false));
3475 body.extend(pieces.iter().map(Piece::as_pair));
3476 let mut span = vec![self.im_start];
3477 span.extend(self.inner.encode_pieces(&body));
3478 span.push(self.im_end);
3479 span.push(self.newline);
3480 span
3481 }
3482
3483 fn derived_assistant_openers(&self) -> AssistantOpeners {
3488 let Some(t) = self.template.as_deref() else { return AssistantOpeners::default() };
3489 let before_content = |msgs: &[ChatMessage]| -> Option<String> {
3490 let text = self.inner.decode(&t.render_turns_structured(msgs, &[])?);
3491 let at = text.find(CONTENT_STAND_IN)?;
3492 let head = &text[..at];
3493 let role = format!("{}\n", Self::role_str(crate::wal::role::ASSISTANT));
3494 Some(head[head.rfind(&role)? + role.len()..].to_string())
3495 };
3496 let user = ChatMessage::new(crate::wal::role::USER, "x");
3497 let turn = ChatMessage::new(crate::wal::role::ASSISTANT, CONTENT_STAND_IN);
3498 AssistantOpeners {
3499 in_history: before_content(&[user.clone(), turn.clone(), ChatMessage::new(crate::wal::role::USER, "w")])
3500 .unwrap_or_default(),
3501 last: before_content(&[user, turn]).unwrap_or_default(),
3502 }
3503 }
3504
3505 fn assistant_openers(&self) -> &AssistantOpeners {
3506 self.assistant_openers.get_or_init(|| self.derived_assistant_openers())
3507 }
3508
3509 fn opened(&self, turn: Vec<u32>, opener: &str) -> Vec<u32> {
3512 if opener.is_empty() {
3513 return turn;
3514 }
3515 let role_line = self.inner.encode_pieces(&[(&format!("{}\n", Self::role_str(crate::wal::role::ASSISTANT)), false)]);
3516 let head = 1 + role_line.len();
3517 if turn.len() < head || turn[0] != self.im_start || turn[1..head] != role_line[..] {
3518 return turn;
3519 }
3520 let mut span = turn[..head].to_vec();
3521 span.extend(self.inner.encode_pieces(&[(opener, false)]));
3522 span.extend_from_slice(&turn[head..]);
3523 span
3524 }
3525
3526 fn frames_like(&self, t: &crate::template_codec::TemplateCodec) -> bool {
3527 let probe = [
3528 ChatMessage::new(crate::wal::role::USER, "x"),
3529 ChatMessage::new(crate::wal::role::ASSISTANT, "y"),
3530 ];
3531 match t.render_turns_structured(&probe, &[]) {
3532 None => false,
3533 Some(toks) => toks.contains(&self.im_start) && toks.contains(&self.im_end),
3534 }
3535 }
3536
3537 fn derived_system_body(
3538 &self,
3539 t: &crate::template_codec::TemplateCodec,
3540 system: Option<&str>,
3541 tools: &[String],
3542 kwargs: &serde_json::Map<String, serde_json::Value>,
3543 ) -> Option<Vec<(String, bool)>> {
3544 let body_for = |sys: &str| -> Option<String> {
3545 let msgs = [
3546 ChatMessage::new(crate::wal::role::SYSTEM, sys),
3547 ChatMessage::new(crate::wal::role::USER, "x"),
3548 ];
3549 let toks = t.render_turns_structured_with(&msgs, tools, kwargs)?;
3550 let open = toks.iter().position(|&x| x == self.im_start)?;
3551 let end = toks[open..].iter().position(|&x| x == self.im_end)? + open;
3552 let body = &toks[open + 1..end];
3553 let text = self.inner.decode(body);
3554 let role = format!("{}\n", Self::role_str(crate::wal::role::SYSTEM));
3555 Some(text.strip_prefix(&role).unwrap_or(&text).to_string())
3556 };
3557 let non_empty = |pieces: Vec<(String, bool)>| -> Option<Vec<(String, bool)>> {
3558 let joined: String = pieces.iter().map(|(s, _)| s.as_str()).collect();
3559 (!joined.trim().is_empty()).then_some(pieces)
3560 };
3561 let Some(sys) = system.filter(|s| !s.is_empty()) else {
3562 return non_empty(vec![(body_for("")?, false)]);
3563 };
3564 let reference = body_for(sys)?;
3565 let body = body_for(CONTENT_STAND_IN)?;
3566 let placed = split_once_exact(&body, CONTENT_STAND_IN).and_then(|(before, after)| {
3567 [sys, sys.trim()]
3568 .into_iter()
3569 .find(|used| {
3570 reference.len() == before.len() + used.len() + after.len()
3571 && reference.starts_with(before)
3572 && reference.ends_with(after)
3573 && &reference[before.len()..before.len() + used.len()] == *used
3574 })
3575 .map(|used| {
3576 vec![
3577 (before.to_string(), false),
3578 (used.to_string(), true),
3579 (after.to_string(), false),
3580 ]
3581 })
3582 });
3583 match placed {
3584 Some(pieces) => non_empty(pieces),
3585 None => {
3586 tracing::debug!(
3587 "chat template does not carry the system text verbatim; \
3588 rendering it as framing"
3589 );
3590 non_empty(vec![(reference, false)])
3591 }
3592 }
3593 }
3594
3595 fn derived_tool_instructions(
3596 t: &crate::template_codec::TemplateCodec,
3597 tool_jsons: &[String],
3598 ) -> Option<String> {
3599 let msgs = [
3600 ChatMessage::new(crate::wal::role::SYSTEM, ""),
3601 ChatMessage::new(crate::wal::role::USER, "x"),
3602 ];
3603 let with = t.render_turns_structured(&msgs, tool_jsons)?;
3604 let without = t.render_turns_structured(&msgs, &[])?;
3605 Self::inserted_text(t, &with, &without)
3606 }
3607
3608 fn render_without_calls(t: &crate::template_codec::TemplateCodec, content: &str) -> Option<Vec<u32>> {
3609 let user = ChatMessage::new(crate::wal::role::USER, "x");
3610 let plain = ChatMessage::new(crate::wal::role::ASSISTANT, content);
3611 t.render_turns_structured(&[user, plain], &[])
3612 }
3613
3614 fn derived_call_block_against(
3615 t: &crate::template_codec::TemplateCodec,
3616 content: &str,
3617 calls: &[(String, String)],
3618 without: &[u32],
3619 ) -> Option<String> {
3620 let user = ChatMessage::new(crate::wal::role::USER, "x");
3621 let mut calling = ChatMessage::new(crate::wal::role::ASSISTANT, content);
3622 for (name, args) in calls {
3623 calling.tool_calls.push(ToolCallMsg {
3624 id: String::new(),
3625 name: name.clone(),
3626 arguments: args.clone(),
3627 });
3628 }
3629 let with = t.render_turns_structured(&[user, calling], &[])?;
3630 Self::inserted_text(t, &with, without)
3631 }
3632
3633 fn derived_call_pieces(
3634 t: &crate::template_codec::TemplateCodec,
3635 content: &str,
3636 calls: &[(String, String)],
3637 ) -> Option<Vec<(String, bool)>> {
3638 let without = Self::render_without_calls(t, content)?;
3639 let block = Self::derived_call_block_against(t, content, calls, &without)?;
3640 let mut subs: Vec<(String, String)> = Vec::new();
3641 let stand_in_calls: Vec<(String, String)> = calls
3642 .iter()
3643 .map(|(name, args)| {
3644 let args = match serde_json::from_str::<serde_json::Value>(args) {
3645 Ok(mut v) => {
3646 stand_in_strings(&mut v, &mut subs);
3647 v.to_string()
3648 }
3649 Err(_) => args.clone(),
3650 };
3651 (name.clone(), args)
3652 })
3653 .collect();
3654 if subs.is_empty() {
3655 return Some(vec![(block, false)]);
3656 }
3657 let with_stand_ins = Self::derived_call_block_against(t, content, &stand_in_calls, &without)?;
3658 Some(split_around_stand_ins(&with_stand_ins, &subs, &block, Placement::SearchFraming).unwrap_or_else(|| {
3659 tracing::debug!(
3660 "chat template does not carry the call arguments verbatim; \
3661 the call block is rendered as framing"
3662 );
3663 vec![(block, false)]
3664 }))
3665 }
3666}
3667
3668impl TextCodec for ChatMlCodec {
3669 fn encode(&self, text: &str) -> Vec<u32> {
3670 self.inner.encode(text)
3671 }
3672 fn token_bytes(&self, token: u32) -> Vec<u8> {
3673 self.inner.token_bytes(token)
3674 }
3675 fn chat_template_source(&self) -> Option<String> {
3676 self.inner.chat_template_source()
3677 }
3678 fn behavior_fingerprint(&self) -> Option<superfluid_fingerprint::BehaviorFingerprint> {
3679 let base = if self.think.default.is_empty() {
3680 "chatml"
3681 } else if self.think.default == self.think.off {
3682 "chatml/nothink"
3683 } else {
3684 "chatml/think"
3685 };
3686 let id = match &self.template {
3687 Some(t) => format!("{base}/tmpl:{}", t.render_identity()),
3688 None => base.to_string(),
3689 };
3690 Some(self.inner.fingerprint(&id, &self.channel_markers()))
3691 }
3692 fn encode_content(&self, text: &str) -> Vec<u32> {
3693 self.inner.encode_content(text)
3694 }
3695 fn encode_pieces(&self, pieces: &[(&str, bool)]) -> Vec<u32> {
3696 self.inner.encode_pieces(pieces)
3697 }
3698 fn render_message(&self, role: u32, text: &str) -> Option<Vec<u32>> {
3699 let turn = self.turn(role, &[Piece::Content(text)]);
3700 if role == crate::wal::role::ASSISTANT {
3701 return Some(self.opened(turn, &self.assistant_openers().in_history));
3702 }
3703 Some(turn)
3704 }
3705 fn render_turn_with_tokens(
3706 &self,
3707 role: u32,
3708 pre_text: &str,
3709 inner: &[u32],
3710 post_text: &str,
3711 ) -> Option<Vec<u32>> {
3712 let role_line = format!("{}\n", Self::role_str(role));
3713 let mut span = vec![self.im_start];
3714 span.extend(self.inner.encode_pieces(&[(&role_line, false), (pre_text, true)]));
3715 span.extend_from_slice(inner);
3716 if !post_text.is_empty() {
3717 span.extend(self.inner.encode_content(post_text));
3718 }
3719 span.push(self.im_end);
3720 span.push(self.newline);
3721 Some(span)
3722 }
3723 fn render_fim(&self, prefix: &str, suffix: &str, mode: u8) -> Option<Vec<u32>> {
3724 self.inner.render_fim(prefix, suffix, mode)
3725 }
3726 fn empty_think_tokens(&self) -> Option<Vec<u32>> {
3727 self.think_open?;
3728 Some(self.inner.encode("<think>\n\n</think>\n\n"))
3729 }
3730 fn generation_prefix(&self, state: TurnState) -> Option<Vec<u32>> {
3731 self.generation_prefix_with(state, None)
3732 }
3733 fn generation_prefix_with(&self, state: TurnState, thinking: Option<bool>) -> Option<Vec<u32>> {
3734 let suffix = match thinking {
3735 None => &self.think.default,
3736 Some(true) => &self.think.on,
3737 Some(false) => &self.think.off,
3738 };
3739 let opener = || {
3740 let mut s = vec![self.im_start];
3741 s.extend(self.inner.encode("assistant\n"));
3742 s.extend_from_slice(suffix);
3743 s
3744 };
3745 match state {
3746 TurnState::AfterInput => Some(opener()),
3747 TurnState::MidTurn => None,
3748 TurnState::AfterFinishedTurn => {
3749 let mut s = vec![self.newline];
3750 s.extend(opener());
3751 Some(s)
3752 }
3753 }
3754 }
3755 fn channel_markers(&self) -> Vec<(u32, u32, u32)> {
3756 let mut markers = Vec::new();
3757 if let (Some(o), Some(c)) = (self.think_open, self.think_close) {
3758 markers.push((o, c, crate::wal::channel::REASONING));
3759 }
3760 markers.push((
3761 self.tool_open,
3762 self.tool_close,
3763 crate::wal::channel::TOOL_CALL,
3764 ));
3765 markers
3766 }
3767
3768 fn turn_separator(&self) -> Option<Vec<u32>> {
3769 Some(vec![self.newline])
3770 }
3771
3772 fn render_system_with_tools(
3773 &self,
3774 system: Option<&str>,
3775 tool_jsons: &[String],
3776 ) -> Option<Vec<u32>> {
3777 let derived = self
3778 .tool_source()
3779 .and_then(|t| Self::derived_tool_instructions(t, tool_jsons));
3780 let mut pieces: Vec<Piece<'_>> = Vec::new();
3781 if let Some(sys) = system {
3782 pieces.push(Piece::Content(sys));
3783 pieces.push(Piece::Framing("\n\n"));
3784 }
3785 if let Some(tools_text) = derived {
3786 pieces.push(Piece::Framing(&tools_text));
3787 return Some(self.turn(crate::wal::role::SYSTEM, &pieces));
3788 }
3789 let mut body = String::from(
3790 "# Tools\n\nYou may call one or more functions to assist with the user query.\n\nYou are provided with function signatures within <tools></tools> XML tags:\n<tools>",
3791 );
3792 for t in tool_jsons {
3793 body.push('\n');
3794 body.push_str(t);
3795 }
3796 body.push_str(
3797 "\n</tools>\n\nFor each function call, return a json object with function name and arguments within <tool_call></tool_call> XML tags:\n<tool_call>\n{\"name\": <function-name>, \"arguments\": <args-json-object>}\n</tool_call>",
3798 );
3799 pieces.push(Piece::Framing(&body));
3800 Some(self.turn(crate::wal::role::SYSTEM, &pieces))
3801 }
3802
3803 fn tool_envelope(&self) -> ToolEnvelope {
3804 match self.tool_source() {
3805 Some(t) => t.tool_envelope(),
3806 None => ToolEnvelope::Json,
3807 }
3808 }
3809
3810 fn tool_call_delimiters(&self) -> Option<(String, String)> {
3811 match self.tool_source() {
3812 Some(t) => t.tool_call_delimiters(),
3813 None => Some((self.marker_literal(self.tool_open), self.marker_literal(self.tool_close))),
3814 }
3815 }
3816 fn marker_literal(&self, token: u32) -> String {
3817 self.inner.marker_literal(token)
3818 }
3819
3820 fn structural_tag(&self, tool_jsons: &[String], at_least_one: bool) -> Option<String> {
3821 match self.tool_source() {
3822 Some(t) => t.structural_tag(tool_jsons, at_least_one),
3823 None => {
3824 let (begin, end) = self.tool_call_delimiters()?;
3825 self.tool_envelope()
3826 .structural_tag(tool_jsons, &begin, &end, at_least_one)
3827 }
3828 }
3829 }
3830
3831 fn call_grammar(&self, tool_jsons: &[String]) -> Option<String> {
3832 match self.tool_source() {
3833 Some(t) => t.call_grammar(tool_jsons),
3834 None => self.tool_envelope().call_grammar(tool_jsons),
3835 }
3836 }
3837
3838 fn named_tool_call(&self, raw: &str) -> Option<(String, String)> {
3839 self.tool_source()?.named_tool_call(raw)
3840 }
3841
3842 fn parse_tool_call_with(&self, raw: &str, schemas: Option<&ToolSchemas>) -> Option<(String, String)> {
3843 match self.tool_source() {
3844 Some(t) => t.parse_tool_call_with(raw, schemas),
3845 None => parse_json_tool_call(raw),
3846 }
3847 }
3848
3849 fn validate_template_kwargs(
3850 &self,
3851 kwargs: &serde_json::Map<String, serde_json::Value>,
3852 ) -> Result<(), String> {
3853 match (&self.template, kwargs.is_empty()) {
3854 (Some(t), false) => t.validate_template_kwargs(kwargs),
3855 _ => Ok(()),
3856 }
3857 }
3858
3859 fn supports_reasoning_effort(&self) -> bool {
3860 self.template.as_deref().is_some_and(|t| t.supports_reasoning_effort())
3861 }
3862
3863 fn reasoning_effort_levels(&self) -> Vec<&'static str> {
3864 self.template.as_deref().map(|t| t.reasoning_effort_levels()).unwrap_or_default()
3865 }
3866
3867 fn supports_enable_thinking(&self) -> bool {
3868 self.thinking_switch
3869 }
3870
3871 fn render_system_full(
3872 &self,
3873 system: Option<&str>,
3874 tool_jsons: &[String],
3875 kwargs: &serde_json::Map<String, serde_json::Value>,
3876 ) -> Option<Vec<u32>> {
3877 let derived = self
3878 .template
3879 .as_deref()
3880 .and_then(|t| self.derived_system_body(t, system, tool_jsons, kwargs));
3881 match (derived, tool_jsons.is_empty()) {
3882 (Some(body), _) => Some(self.turn(crate::wal::role::SYSTEM, &pieces_of(&body))),
3883 (None, true) => match system {
3884 Some(text) if !text.is_empty() => {
3885 self.render_message(crate::wal::role::SYSTEM, text)
3886 }
3887 _ => Some(Vec::new()),
3888 },
3889 (None, false) => self.render_system_with_tools(system, tool_jsons),
3890 }
3891 }
3892
3893 fn render_prompt_structured_with(
3894 &self,
3895 messages: &[ChatMessage],
3896 tools: &[String],
3897 kwargs: &serde_json::Map<String, serde_json::Value>,
3898 ) -> Option<Vec<u32>> {
3899 let sys_text = messages
3900 .iter()
3901 .find(|m| m.role == crate::wal::role::SYSTEM)
3902 .map(|m| m.content.as_str());
3903 let derived = self
3904 .template
3905 .as_deref()
3906 .and_then(|t| self.derived_system_body(t, sys_text, tools, kwargs));
3907 let Some(body) = derived else {
3908 return self.render_prompt_structured(messages, tools).map(|p| self.reopen_for(p, kwargs));
3909 };
3910 let mut out = self.stream_prologue();
3911 let mut derived_emitted = false;
3912 for m in messages {
3913 if !derived_emitted && m.role == crate::wal::role::SYSTEM {
3914 out.extend(self.turn(crate::wal::role::SYSTEM, &pieces_of(&body)));
3915 derived_emitted = true;
3916 } else {
3917 out.extend(self.render_message(m.role, &m.content)?);
3918 }
3919 }
3920 if !derived_emitted {
3921 let mut with = self.turn(crate::wal::role::SYSTEM, &pieces_of(&body));
3922 with.extend(std::mem::take(&mut out));
3923 out = with;
3924 }
3925 if let Some(op) = self.generation_prefix(TurnState::AfterInput) {
3926 out.extend(op);
3927 }
3928 Some(self.reopen_for(out, kwargs))
3929 }
3930
3931 fn render_tool_result(&self, _name: &str, content: &str) -> Option<Vec<u32>> {
3932 Some(self.turn(
3933 crate::wal::role::USER,
3934 &[
3935 Piece::Framing("<tool_response>\n"),
3936 Piece::Content(content),
3937 Piece::Framing("\n</tool_response>"),
3938 ],
3939 ))
3940 }
3941
3942 fn render_assistant_with_tool_calls(
3943 &self,
3944 content: &str,
3945 calls: &[(String, String)],
3946 ) -> Option<Vec<u32>> {
3947 let turn = self.assistant_tool_turn(content, calls)?;
3948 Some(self.opened(turn, &self.assistant_openers().in_history))
3949 }
3950
3951 fn render_assistant_with_tool_calls_after_query(
3952 &self,
3953 content: &str,
3954 calls: &[(String, String)],
3955 ) -> Option<Vec<u32>> {
3956 let turn = self.assistant_tool_turn(content, calls)?;
3957 Some(self.opened(turn, &self.assistant_openers().last))
3958 }
3959}
3960
3961impl ChatMlCodec {
3962 fn assistant_tool_turn(&self, content: &str, calls: &[(String, String)]) -> Option<Vec<u32>> {
3965 let derived = self
3966 .tool_source()
3967 .and_then(|t| Self::derived_call_pieces(t, content, calls));
3968 if let Some(block) = derived {
3969 let sep = if content.is_empty() { "" } else { "\n" };
3970 let mut pieces = vec![Piece::Content(content), Piece::Framing(sep)];
3971 pieces.extend(pieces_of(&block));
3972 return Some(self.turn(crate::wal::role::ASSISTANT, &pieces));
3973 }
3974 let mut span = vec![self.im_start];
3975 span.extend(self.inner.encode_pieces(&[("assistant\n", false), (content, true)]));
3976 for (i, (name, args)) in calls.iter().enumerate() {
3977 if (i == 0 && !content.is_empty()) || i > 0 {
3978 span.push(self.newline);
3979 }
3980 span.push(self.tool_open);
3981 span.extend(self.inner.encode_content(&format!(
3982 "\n{{\"name\": \"{name}\", \"arguments\": {args}}}\n"
3983 )));
3984 span.push(self.tool_close);
3985 }
3986 span.push(self.im_end);
3987 span.push(self.newline);
3988 Some(span)
3989 }
3990}
3991
3992#[cfg(test)]
3993mod tests {
3994 use super::*;
3995
3996 fn switch(template: &str) -> bool {
3997 let mut env = minijinja::Environment::new();
3998 env.add_function("raise_exception", |m: String| -> Result<minijinja::Value, minijinja::Error> {
3999 Err(minijinja::Error::new(minijinja::ErrorKind::InvalidOperation, m))
4000 });
4001 env.add_template("chat", template).unwrap();
4002 let t = env.get_template("chat").unwrap();
4003 thinking_switch_changes(|msgs, tools, on| {
4004 let messages: Vec<_> = msgs
4005 .iter()
4006 .map(|m| {
4007 let role = if m.role == crate::wal::role::SYSTEM { "system" } else { "user" };
4008 minijinja::context! { role => role, content => m.content.clone() }
4009 })
4010 .collect();
4011 let tools: Vec<serde_json::Value> = tools.iter().map(|t| serde_json::from_str(t).unwrap()).collect();
4012 t.render(minijinja::context! {
4013 messages => messages,
4014 tools => tools,
4015 add_generation_prompt => true,
4016 enable_thinking => on,
4017 })
4018 .ok()
4019 })
4020 }
4021
4022 const TURNS: &str = "{% for m in messages %}<|im_start|>{{ m.role }}\n{{ m.content }}<|im_end|>\n{% endfor %}";
4023
4024 #[test]
4025 fn a_template_that_branches_on_enable_thinking_has_the_switch() {
4026 let qwen = format!(
4027 "{TURNS}{{% if add_generation_prompt %}}<|im_start|>assistant\n\
4028 {{% if enable_thinking is defined and enable_thinking is false %}}<think>\n\n</think>\n\n{{% endif %}}{{% endif %}}"
4029 );
4030 assert!(switch(&qwen));
4031 let strict = format!("{{% if not enable_thinking %}}{{{{ raise_exception('thinking is required') }}}}{{% endif %}}{TURNS}");
4032 assert!(switch(&strict));
4033 let with_tools = format!("{{% if tools and enable_thinking %}}<think>{{% endif %}}{TURNS}");
4034 assert!(switch(&with_tools));
4035 let with_system =
4036 format!("{{% if messages[0].role == 'system' and enable_thinking %}}<think>{{% endif %}}{TURNS}");
4037 assert!(switch(&with_system));
4038 }
4039
4040 #[test]
4041 fn a_template_that_only_names_enable_thinking_has_no_switch() {
4042 for template in [
4043 format!("{{# enable_thinking is not supported #}}{TURNS}"),
4044 format!("{TURNS}{{{{ 'enable_thinking' if false }}}}"),
4045 format!("{{% set enable_thinking = true %}}{{% if enable_thinking %}}{{% endif %}}{TURNS}"),
4046 ] {
4047 assert!(!switch(&template), "{template}");
4048 }
4049 }
4050
4051 fn parse(raw: &str) -> Option<(String, serde_json::Value)> {
4052 let (n, a) = parse_gemma_tool_call(raw)?;
4053 Some((n, serde_json::from_str(&a).unwrap()))
4054 }
4055
4056 #[test]
4057 fn gemma_call_with_markers_parses() {
4058 let (name, args) = parse("<|tool_call>call:bash{command:ls -R}<tool_call|>").unwrap();
4059 assert_eq!(name, "bash");
4060 assert_eq!(args, serde_json::json!({"command": "ls -R"}));
4061 }
4062
4063 #[test]
4064 fn gemma_call_without_markers_parses() {
4065 let (name, args) = parse("call:bash{command:ls -R}").unwrap();
4066 assert_eq!(name, "bash");
4067 assert_eq!(args, serde_json::json!({"command": "ls -R"}));
4068 }
4069
4070 #[test]
4071 fn gemma_bare_value_may_contain_a_comma() {
4072 let (_, args) = parse("call:grep{pattern:hello, world, path:notes.txt}").unwrap();
4073 assert_eq!(args, serde_json::json!({"pattern": "hello, world", "path": "notes.txt"}));
4074 }
4075
4076 #[test]
4077 fn gemma_scalars_keep_their_types() {
4078 let (_, args) =
4079 parse("call:f{a:true, b:false, c:5, d:1.5, e:null, s:plain text}").unwrap();
4080 assert_eq!(
4081 args,
4082 serde_json::json!({"a": true, "b": false, "c": 5, "d": 1.5, "e": null, "s": "plain text"})
4083 );
4084 }
4085
4086 #[test]
4087 fn gemma_quoted_string_form_parses() {
4088 let (_, args) = parse("call:f{a:<|\"|>x, y} z<|\"|>}").unwrap();
4089 assert_eq!(args, serde_json::json!({"a": "x, y} z"}));
4090 }
4091
4092 #[test]
4093 fn gemma_nested_object_parses() {
4094 let (_, args) = parse("call:f{o:{k:v}}").unwrap();
4095 assert_eq!(args, serde_json::json!({"o": {"k": "v"}}));
4096 }
4097
4098 #[test]
4099 fn gemma_array_elements_split_on_commas() {
4100 let (_, args) = parse("call:f{l:[1, 2, 3]}").unwrap();
4101 assert_eq!(args, serde_json::json!({"l": [1, 2, 3]}));
4102 }
4103
4104 #[test]
4105 fn gemma_array_of_strings_keeps_each_element() {
4106 let (_, args) = parse("call:f{l:[alpha, beta gamma, delta]}").unwrap();
4107 assert_eq!(args, serde_json::json!({"l": ["alpha", "beta gamma", "delta"]}));
4108 }
4109
4110 #[test]
4111 fn gemma_object_value_may_still_contain_a_comma() {
4112 let (_, args) = parse("call:f{pattern:hello, world, path:n.txt}").unwrap();
4113 assert_eq!(args, serde_json::json!({"pattern": "hello, world", "path": "n.txt"}));
4114 }
4115
4116 #[test]
4117 fn gemma_integers_stay_integers() {
4118 let (_, args) = parse("call:f{n:42}").unwrap();
4119 assert_eq!(args, serde_json::json!({"n": 42}));
4120 assert_eq!(serde_json::to_string(&args).unwrap(), r#"{"n":42}"#);
4121 }
4122
4123 #[test]
4124 fn gemma_no_args_is_an_empty_object_not_a_failure() {
4125 let (name, args) = parse("call:now{}").unwrap();
4126 assert_eq!(name, "now");
4127 assert_eq!(args, serde_json::json!({}));
4128 }
4129
4130 #[test]
4131 fn gemma_rejects_what_is_not_a_call() {
4132 assert!(parse_gemma_tool_call("I will run ls -R for you.").is_none());
4133 assert!(parse_gemma_tool_call(r#"{"name":"bash","arguments":{}}"#).is_none());
4134 assert!(parse_gemma_tool_call("call:bash").is_none());
4135 assert!(parse_gemma_tool_call("call:{command:ls}").is_none());
4136 }
4137
4138 #[test]
4139 fn gemma_truncated_body_still_names_the_call() {
4140 let (name, args) = parse("<|tool_call>call:bash{command:ls -R").unwrap();
4141 assert_eq!(name, "bash");
4142 assert_eq!(args, serde_json::json!({"command": "ls -R"}));
4143 }
4144
4145 #[test]
4146 fn gemma_call_grammar_orders_arguments_as_the_template_renders_them() {
4147 let params = serde_json::json!({"type": "object",
4148 "properties": {"pattern": {"type": "string"}, "path": {"type": "string"}},
4149 "required": ["pattern", "path"]});
4150 let body = gemma_call_body_format("grep", ¶ms);
4151 let keys: Vec<String> = body["elements"]
4152 .as_array()
4153 .unwrap()
4154 .iter()
4155 .filter_map(|e| e["elements"][0]["value"].as_str().map(String::from))
4156 .collect();
4157 assert_eq!(keys, vec!["path:", "pattern:"], "{body}");
4158 }
4159
4160 #[test]
4161 fn gemma_envelope_is_selected_by_name_and_round_trips() {
4162 let e = ToolEnvelope::from_name("gemma").expect("gemma envelope available");
4163 assert_eq!(e.name(), "gemma");
4164 assert!(ToolEnvelope::names().contains(&"gemma"));
4165 let (name, _) = e.parse_call("<|tool_call>call:bash{command:ls}<tool_call|>", None).unwrap();
4166 assert_eq!(name, "bash");
4167 assert!(e.call_grammar(&[r#"{"function":{"name":"bash"}}"#.to_string()]).is_none());
4168 }
4169
4170 #[test]
4171 fn every_structural_tag_is_a_structural_tag_document() {
4172 let tool = r#"{"name":"f","parameters":{"type":"object","properties":{"a":{"type":"string"}},"required":["a"]}}"#.to_string();
4173 let envelopes = vec![ToolEnvelope::Json, ToolEnvelope::Gemma, ToolEnvelope::Atem];
4174 for e in envelopes {
4175 let Some(tag) =
4176 e.structural_tag(std::slice::from_ref(&tool), "<|tool_call>", "<tool_call|>", true)
4177 else {
4178 continue;
4179 };
4180 let v: serde_json::Value = serde_json::from_str(&tag).expect("tag is JSON");
4181 assert_eq!(
4182 v.get("type").and_then(|t| t.as_str()),
4183 Some("structural_tag"),
4184 "{} tag root type: {tag}",
4185 e.name()
4186 );
4187 assert!(
4188 v.get("format").is_some(),
4189 "{} tag has no `format` field — xgrammar rejects such a document, and the caller then decodes unconstrained: {tag}",
4190 e.name()
4191 );
4192 }
4193 }
4194
4195 #[test]
4196 fn gemma_structural_tag_frames_the_call_without_json() {
4197 let tool = r#"{"name":"grep","parameters":{"type":"object","properties":{"pattern":{"type":"string"},"n":{"type":"integer"},"deep":{"type":"boolean"}},"required":["pattern","n","deep"]}}"#.to_string();
4198 let tag = ToolEnvelope::Gemma
4199 .structural_tag(&[tool], "<|tool_call>", "<tool_call|>", true)
4200 .expect("gemma tag");
4201 let v: serde_json::Value = serde_json::from_str(&tag).unwrap();
4202 let f = &v["format"];
4203 assert_eq!(f["type"], "triggered_tags");
4204 assert_eq!(f["at_least_one"], true, "tool_choice=required must reach the tag");
4205 assert_eq!(f["stop_after_first"], true, "a forced call is one call");
4206 assert_eq!(f["triggers"][0], "<|tool_call>");
4207 let content = &f["tags"][0]["content"];
4208 assert_eq!(content["type"], "sequence");
4209 let dump = content.to_string();
4210 assert!(dump.contains(r#""value":"call:grep{""#), "call prefix: {dump}");
4211 assert!(!dump.contains("json_schema"), "gemma body must not be JSON: {dump}");
4212 assert!(dump.contains(r#""value":"n:""#), "integer key framed: {dump}");
4213 assert!(dump.contains("-?[0-9]+"), "integer value pattern: {dump}");
4214 assert!(dump.contains(r#""value":"true""#), "boolean rendered as const: {dump}");
4215 }
4216
4217 #[test]
4218 fn a_dialect_without_terminators_is_unaffected() {
4219 assert!(MockChatCodec.turn_terminators().is_empty());
4220 }
4221
4222 #[test]
4223 fn json_envelope_still_rejects_the_gemma_body() {
4224 assert!(parse_json_tool_call("call:bash{command:ls -R}").is_none());
4225 }
4226
4227 #[test]
4228 fn utf8_stream_reassembles_split_characters() {
4229 let mut s = Utf8Stream::default();
4230 assert_eq!(s.push(b"caf"), "caf");
4231 assert_eq!(s.push(&[0xC3]), "");
4232 assert_eq!(s.push(&[0xA9]), "\u{e9}");
4233 assert_eq!(s.flush(), "");
4234 }
4235
4236 #[test]
4237 fn utf8_stream_replaces_invalid_and_flushes_partial() {
4238 let mut s = Utf8Stream::default();
4239 assert_eq!(s.push(&[0xFF, b'a']), "\u{FFFD}a");
4240 assert_eq!(s.push(&[0xE2, 0x82]), "");
4241 assert_eq!(s.flush(), "\u{FFFD}");
4242 }
4243
4244 fn shape(runs: &[Run]) -> Vec<(Vec<u32>, u32, bool)> {
4245 runs.iter()
4246 .map(|r| (r.span.clone(), r.channel, r.closes))
4247 .collect()
4248 }
4249
4250 #[test]
4251 fn segmenter_partitions_at_markers_across_slices() {
4252 let think = (100, 101, crate::wal::channel::REASONING);
4253 let tool = (200, 201, crate::wal::channel::TOOL_CALL);
4254 let mut seg = Segmenter::new(vec![think, tool]);
4255 let runs = seg.split(&[1, 2, 100, 3]);
4256 assert_eq!(
4257 shape(&runs),
4258 vec![
4259 (vec![1, 2], crate::wal::channel::TEXT, false),
4260 (vec![100, 3], crate::wal::channel::REASONING, false),
4261 ]
4262 );
4263 let runs = seg.split(&[4, 101, 200, 5, 201, 6]);
4264 assert_eq!(
4265 shape(&runs),
4266 vec![
4267 (vec![4, 101], crate::wal::channel::REASONING, true),
4268 (vec![200, 5, 201], crate::wal::channel::TOOL_CALL, true),
4269 (vec![6], crate::wal::channel::TEXT, false),
4270 ]
4271 );
4272 let input = vec![100, 200, 7, 101, 8];
4273 let runs = seg.split(&input);
4274 let concat: Vec<u32> = runs.iter().flat_map(|r| r.span.clone()).collect();
4275 assert_eq!(concat, input);
4276 assert_eq!(runs[0].channel, crate::wal::channel::REASONING);
4277 }
4278
4279 #[test]
4280 fn a_named_block_keeps_its_name_out_of_the_text() {
4281 use crate::wal::channel;
4282 const OPEN: u32 = 100;
4283 const CLOSE: u32 = 101;
4284 let (thought, nl) = (50, 51);
4285 let named = || Segmenter::new(vec![(OPEN, CLOSE, channel::REASONING)]).with_names(vec![(OPEN, vec![thought, nl])]);
4286 let texts = |runs: Vec<Run>| -> Vec<(u32, Vec<u32>, bool)> { runs.into_iter().map(|r| (r.channel, r.text, r.closes)).collect() };
4287
4288 let whole = [OPEN, thought, nl, 7, 8, CLOSE, 9];
4289 let runs = named().split(&whole);
4290 assert_eq!(runs.iter().flat_map(|r| r.span.clone()).collect::<Vec<_>>(), whole, "spans partition the stream");
4291 assert_eq!(texts(runs), vec![(channel::REASONING, vec![7, 8], true), (channel::TEXT, vec![9], false)]);
4292
4293 let mut seg = named();
4294 let mut runs = seg.split(&[OPEN, thought]);
4295 runs.extend(seg.split(&[nl, 7, CLOSE]));
4296 let reasoning: Vec<u32> = runs.iter().filter(|r| r.channel == channel::REASONING).flat_map(|r| r.text.clone()).collect();
4297 assert_eq!(reasoning, vec![7]);
4298
4299 let runs = named().split(&[OPEN, thought, 7, nl, CLOSE]);
4300 assert_eq!(texts(runs), vec![(channel::REASONING, vec![7, nl], true)]);
4301
4302 let mut seg = named();
4303 seg.split(&[OPEN, thought, nl, 7, CLOSE]);
4304 assert_eq!(texts(seg.split(&[OPEN, thought, nl, 8, CLOSE])), vec![(channel::REASONING, vec![8], true)]);
4305 let mut primed = named();
4306 primed.prime(channel::REASONING);
4307 assert_eq!(texts(primed.split(&[thought, CLOSE])), vec![(channel::REASONING, vec![thought], true)]);
4308 }
4309
4310 #[test]
4311 fn segmenter_without_markers_is_single_channel() {
4312 let mut seg = Segmenter::new(Vec::new());
4313 let runs = seg.split(&[1, 2, 3]);
4314 assert_eq!(
4315 shape(&runs),
4316 vec![(vec![1, 2, 3], crate::wal::channel::TEXT, false)]
4317 );
4318 }
4319
4320 #[test]
4321 fn mock_codec_round_trips_multibyte_text() {
4322 let c = MockCodec;
4323 let text = "héllo — ok";
4324 let toks = c.encode(text);
4325 assert_eq!(c.decode(&toks), text);
4326 }
4327}
4328
4329#[cfg(test)]
4330mod learned_tool_wire_tests {
4331 use super::{learn_delims, S_K1, S_K2, S_NAME, S_V1, S_V2};
4332
4333 fn render(name_open: &str, name_close: &str, key_open: &str, key_close: &str, val_close: &str, args: &[(&str, &str)]) -> String {
4334 let mut s = String::from(name_open);
4335 s.push_str(S_NAME);
4336 s.push_str(name_close);
4337 for (k, v) in args {
4338 s.push_str(key_open);
4339 s.push_str(k);
4340 s.push_str(key_close);
4341 s.push_str(v);
4342 s.push_str(val_close);
4343 }
4344 s
4345 }
4346
4347 #[test]
4348 fn learns_a_qwen_coder_xml_body_and_inverts_it() {
4349 let frame = ("<tool_call>", "</tool_call>");
4350 let two = render("<function=", ">\n", "<parameter=", ">\n", "\n</parameter>\n", &[(S_K1, S_V1), (S_K2, S_V2)]) + "</function>";
4351 let zero = render("<function=", ">\n", "", "", "", &[]) + "</function>";
4352 let d = learn_delims(&two, Some(&(zero)), Some(frame)).expect("learns");
4353 let raw = "<function=bash>\n<parameter=command>\nls -la /tmp\n</parameter>\n<parameter=timeout>\n30\n</parameter>\n</function>";
4354 let (name, args) = d.parse(raw, None).expect("parses");
4355 assert_eq!(name, "bash");
4356 let v: serde_json::Value = serde_json::from_str(&args).unwrap();
4357 assert_eq!(v["command"], "ls -la /tmp");
4358 assert_eq!(v["timeout"], 30);
4359 let cut = "<function=read>\n<parameter=path>\n/a\n</parameter>\n<parameter=limit>\n5";
4360 let (n2, a2) = d.parse(cut, None).unwrap();
4361 assert_eq!(n2, "read");
4362 let v2: serde_json::Value = serde_json::from_str(&a2).unwrap();
4363 assert_eq!(v2["path"], "/a");
4364 }
4365
4366 #[test]
4367 fn learns_a_zero_arg_call_terminator() {
4368 let two = render("<function=", ">\n", "<parameter=", ">\n", "\n</parameter>\n", &[(S_K1, S_V1), (S_K2, S_V2)]) + "</function>";
4369 let zero = render("<function=", ">\n", "", "", "", &[]) + "</function>";
4370 let d = learn_delims(&two, Some(&zero), Some(("<tool_call>", "</tool_call>"))).unwrap();
4371 let (name, args) = d.parse("<function=now>\n</function>", None).expect("zero-arg call");
4372 assert_eq!(name, "now");
4373 assert_eq!(args, "{}");
4374 }
4375
4376 fn qwen_delims() -> super::Delims {
4377 let two = render("<function=", ">\n", "<parameter=", ">\n", "\n</parameter>\n", &[(S_K1, S_V1), (S_K2, S_V2)]) + "</function>";
4378 let zero = render("<function=", ">\n", "", "", "", &[]) + "</function>";
4379 learn_delims(&two, Some(&zero), Some(("<tool_call>", "</tool_call>"))).expect("learns")
4380 }
4381
4382 fn serve_schemas() -> super::ToolSchemas {
4383 super::ToolSchemas::from_values(
4384 serde_json::json!([{"type": "function", "function": {"name": "serve", "parameters": {
4385 "type": "object",
4386 "properties": {
4387 "port": {"type": "string"}, "host": {"type": "string"},
4388 "workers": {"type": "integer"}, "ratio": {"type": "number"},
4389 "debug": {"type": "boolean"}, "verbose": {"type": "boolean"},
4390 "tags": {"type": "array", "items": {"type": "string"}},
4391 "env": {"type": "object"},
4392 "id": {"anyOf": [{"type": "string"}, {"type": "integer"}]},
4393 "note": {"type": ["string", "null"]},
4394 "limit": {"$ref": "#/$defs/Count"},
4395 "mode": {"enum": ["1", "2"]},
4396 "loose": {}
4397 },
4398 "$defs": {"Count": {"type": "integer"}}
4399 }}}])
4400 .as_array()
4401 .unwrap(),
4402 )
4403 }
4404
4405 fn parsed(d: &super::Delims, raw: &str, schemas: Option<&super::ToolSchemas>) -> serde_json::Value {
4406 let (name, args) = d.parse(raw, schemas).expect("parses");
4407 assert_eq!(name, "serve");
4408 serde_json::from_str(&args).unwrap()
4409 }
4410
4411 #[test]
4412 fn learned_xml_wire_types_values_from_the_declared_schema() {
4413 let d = qwen_delims();
4414 let s = serve_schemas();
4415 let raw = "<function=serve>\n\
4416 <parameter=port>\n8080\n</parameter>\n\
4417 <parameter=host>\nnull\n</parameter>\n\
4418 <parameter=workers>\n4\n</parameter>\n\
4419 <parameter=ratio>\n0.5\n</parameter>\n\
4420 <parameter=debug>\ntrue\n</parameter>\n\
4421 <parameter=verbose>\nFalse\n</parameter>\n\
4422 <parameter=tags>\n[\"a\", \"b c\"]\n</parameter>\n\
4423 <parameter=env>\n{\"K\": 1}\n</parameter>\n\
4424 <parameter=id>\n42\n</parameter>\n\
4425 <parameter=note>\nnull\n</parameter>\n\
4426 <parameter=limit>\n10\n</parameter>\n\
4427 <parameter=mode>\n2\n</parameter>\n\
4428 <parameter=loose>\n7\n</parameter>\n\
4429 <parameter=undeclared>\ntrue\n</parameter>\n\
4430 </function>";
4431 assert_eq!(
4432 parsed(&d, raw, Some(&s)),
4433 serde_json::json!({
4434 "port": "8080", "host": "null",
4435 "workers": 4, "ratio": 0.5, "debug": true, "verbose": false,
4436 "tags": ["a", "b c"], "env": {"K": 1},
4437 "id": "42", "note": "null",
4438 "limit": 10, "mode": "2",
4439 "loose": 7, "undeclared": true
4440 })
4441 );
4442 let odd = parsed(&d, "<function=serve>\n<parameter=workers>\nfour\n</parameter>\n</function>", Some(&s));
4443 assert_eq!(odd, serde_json::json!({"workers": "four"}));
4444 }
4445
4446 #[test]
4447 fn learned_xml_wire_without_a_schema_keeps_the_heuristic() {
4448 let d = qwen_delims();
4449 let raw = "<function=serve>\n<parameter=port>\n8080\n</parameter>\n<parameter=host>\nlocalhost\n</parameter>\n</function>";
4450 assert_eq!(parsed(&d, raw, None), serde_json::json!({"port": 8080, "host": "localhost"}));
4451 let other = super::ToolSchemas::from_values(
4452 serde_json::json!([{"name": "other", "parameters": {"properties": {"port": {"type": "string"}}}}])
4453 .as_array()
4454 .unwrap(),
4455 );
4456 assert_eq!(parsed(&d, raw, Some(&other)), serde_json::json!({"port": 8080, "host": "localhost"}));
4457 }
4458
4459 #[test]
4460 fn learned_xml_wire_keeps_payload_whitespace() {
4461 let d = qwen_delims();
4462 let s = super::ToolSchemas::from_values(
4463 serde_json::json!([{"name": "write", "parameters": {"properties": {
4464 "path": {"type": "string"}, "content": {"type": "string"}
4465 }}}])
4466 .as_array()
4467 .unwrap(),
4468 );
4469 let raw = "<function=write>\n<parameter=path>\n/tmp/a.py\n</parameter>\n\
4470 <parameter=content>\n def f():\n return 1\n\n</parameter>\n</function>";
4471 for schemas in [Some(&s), None] {
4472 let (_, args) = d.parse(raw, schemas).unwrap();
4473 let v: serde_json::Value = serde_json::from_str(&args).unwrap();
4474 assert_eq!(v["path"], "/tmp/a.py");
4475 assert_eq!(v["content"], " def f():\n return 1\n", "schemas={}", schemas.is_some());
4476 }
4477 }
4478
4479 #[test]
4480 fn closed_args_type_from_the_same_schema() {
4481 let d = qwen_delims();
4482 let s = serve_schemas();
4483 let raw = "<function=serve>\n<parameter=port>\n8080\n</parameter>\n<parameter=workers>\n4\n</parameter>\n<parameter=id>\n9";
4484 let mut cursor = None;
4485 let got = d.closed_args(raw, &mut cursor, Some(&s));
4486 assert_eq!(
4487 got,
4488 vec![("port".to_string(), serde_json::json!("8080")), ("workers".to_string(), serde_json::json!(4))]
4489 );
4490 let mut cursor = None;
4491 assert_eq!(d.closed_args(raw, &mut cursor, None)[0].1, serde_json::json!(8080));
4492 }
4493
4494 #[cfg(feature = "basert")]
4495 #[test]
4496 fn learned_xml_wire_forced_tag_is_one_call_of_free_text_values() {
4497 let d = qwen_delims();
4498 let tool = serde_json::json!({"type": "function", "function": {"name": "write", "parameters": {
4499 "type": "object",
4500 "properties": {"content": {"type": "string"}, "mode": {"type": "integer"}},
4501 "required": ["content"]
4502 }}})
4503 .to_string();
4504 let tag: serde_json::Value = serde_json::from_str(
4505 &d.structural_tag(std::slice::from_ref(&tool), "<tool_call>", "</tool_call>", true).expect("forced tag"),
4506 )
4507 .unwrap();
4508 let f = &tag["format"];
4509 assert_eq!(f["at_least_one"], true);
4510 assert_eq!(f["stop_after_first"], true);
4511 let el = f["tags"][0]["content"]["elements"].as_array().unwrap();
4512 assert_eq!(el[1]["value"], "<function=write>\n<");
4513 let content = &el[2];
4514 assert_eq!(
4515 content,
4516 &serde_json::json!({
4517 "type": "tag", "begin": "parameter=content>\n",
4518 "content": {"type": "any_text"}, "end": "\n</parameter>\n<"
4519 })
4520 );
4521 let mode = &el[3]["content"];
4522 assert_eq!(el[3]["type"], "optional");
4523 assert_eq!(mode["begin"], "parameter=mode>\n");
4524 assert_eq!(mode["content"]["type"], "json_schema");
4525 assert_eq!(mode["content"]["json_schema"]["type"], "integer");
4526 assert_eq!(el.last().unwrap(), &serde_json::json!({"type": "any_text"}));
4527 }
4528
4529 #[test]
4530 fn bails_when_the_body_has_no_repeating_key_opener() {
4531 let two = format!("call:{S_NAME}{{{S_K1}:{S_V1},{S_K2}:{S_V2}}}");
4532 assert!(learn_delims(&two, None, Some(("<|tool_call>", "<tool_call|>"))).is_none());
4533 }
4534}