use std::collections::HashMap;
use std::sync::OnceLock;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ModelRegistration {
pub family: &'static str,
pub id_substrings: &'static [&'static str],
pub reasoning_open: Option<&'static str>,
pub reasoning_close: Option<&'static str>,
pub tool_open: Option<&'static str>,
pub tool_close: Option<&'static str>,
pub tool_preamble: Option<&'static str>,
}
impl ModelRegistration {
pub fn matches(&self, model_id: &str) -> bool {
let lower = model_id.to_ascii_lowercase();
self.id_substrings
.iter()
.any(|s| lower.contains(&s.to_ascii_lowercase()))
}
pub fn has_reasoning(&self) -> bool {
self.reasoning_open.is_some() && self.reasoning_close.is_some()
}
pub fn has_tools(&self) -> bool {
self.tool_open.is_some() && self.tool_close.is_some()
}
pub fn tool_call_gbnf(
&self,
fn_name: &str,
params_schema: &serde_json::Value,
shape: GrammarShape,
) -> Result<String, String> {
match self.family {
"gemma4" => {
gemma4_tool_call_gbnf(fn_name, params_schema, shape).map_err(|e| e.to_string())
}
"qwen35" => {
qwen35_tool_call_gbnf(fn_name, params_schema, shape).map_err(|e| e.to_string())
}
"deepseek4" => {
deepseek4_tool_call_gbnf(fn_name, params_schema, shape).map_err(|e| e.to_string())
}
other => Err(format!(
"tool_call_gbnf: no per-model grammar emitter for family '{}'",
other
)),
}
}
pub fn parallel_call_separator(&self) -> &'static str {
match self.family {
"gemma4" => "",
"qwen35" => "\n",
"deepseek4" => "\n",
_ => "",
}
}
pub fn auto_lazy_multi_fn_inter_call(&self) -> &'static str {
match self.family {
"gemma4" => "<|tool_call>",
"qwen35" => "\n<tool_call>\n",
"deepseek4" => "\n",
_ => "",
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum GrammarShape {
SingleBody,
OneOrMoreCalls { parallel: bool },
OneOrMoreCallsBodyOnly { parallel: bool },
}
pub const GEMMA4: ModelRegistration = ModelRegistration {
family: "gemma4",
id_substrings: &["gemma-4", "gemma4"],
reasoning_open: Some("<|channel>"),
reasoning_close: Some("<channel|>"),
tool_open: Some("<|tool_call>"),
tool_close: Some("<tool_call|>"),
tool_preamble: None,
};
pub const QWEN35: ModelRegistration = ModelRegistration {
family: "qwen35",
id_substrings: &["qwen3.5", "qwen3.6", "qwen35", "qwen36"],
reasoning_open: Some("<think>"),
reasoning_close: Some("</think>"),
tool_open: Some("<tool_call>"),
tool_close: Some("</tool_call>"),
tool_preamble: None,
};
pub const DEEPSEEK4: ModelRegistration = ModelRegistration {
family: "deepseek4",
id_substrings: &["deepseek-v4", "deepseek v4", "deepseek4", "deepseek_v4"],
reasoning_open: Some("<think>"),
reasoning_close: Some("</think>"),
tool_open: Some("<|DSML|tool_calls>"),
tool_close: Some("</|DSML|tool_calls>"),
tool_preamble: None,
};
pub const BUILTIN_REGISTRATIONS: &[ModelRegistration] = &[GEMMA4, QWEN35, DEEPSEEK4];
static REGISTRY: OnceLock<std::sync::RwLock<Vec<ModelRegistration>>> = OnceLock::new();
fn reg() -> &'static std::sync::RwLock<Vec<ModelRegistration>> {
REGISTRY.get_or_init(|| std::sync::RwLock::new(BUILTIN_REGISTRATIONS.to_vec()))
}
pub fn find_for(model_id: &str) -> Option<ModelRegistration> {
let guard = reg().read().unwrap();
for r in guard.iter() {
if r.matches(model_id) {
return Some(r.clone());
}
}
None
}
pub fn list_families() -> Vec<String> {
let guard = reg().read().unwrap();
guard.iter().map(|r| r.family.to_string()).collect()
}
pub fn register(entry: ModelRegistration) {
reg().write().unwrap().push(entry);
}
#[derive(Debug, Clone)]
pub struct ReasoningSplitter {
open_marker: &'static str,
close_marker: &'static str,
in_reasoning: bool,
tail_buf: String,
tail_cap: usize,
}
impl ReasoningSplitter {
pub fn from_registration(reg: &ModelRegistration) -> Option<Self> {
Self::from_registration_forced(reg, false)
}
pub fn from_registration_forced(reg: &ModelRegistration, forced_open: bool) -> Option<Self> {
let (open, close) = match (reg.reasoning_open, reg.reasoning_close) {
(Some(o), Some(c)) if !o.is_empty() && !c.is_empty() => (o, c),
_ => return None,
};
let cap = open.len().max(close.len()).max(1);
Some(Self {
open_marker: open,
close_marker: close,
in_reasoning: forced_open,
tail_buf: String::with_capacity(cap * 2),
tail_cap: cap,
})
}
pub fn feed(&mut self, fragment: &str) -> Vec<(SplitSlot, String)> {
let mut out: Vec<(SplitSlot, String)> = Vec::new();
let mut scan = std::mem::take(&mut self.tail_buf);
scan.push_str(fragment);
let mut scan_cursor = 0usize;
let mut out_cursor = 0usize;
loop {
let active_marker = if self.in_reasoning {
self.close_marker
} else {
self.open_marker
};
match scan[scan_cursor..].find(active_marker) {
Some(rel) => {
let marker_start = scan_cursor + rel;
let slot = if self.in_reasoning {
SplitSlot::Reasoning
} else {
SplitSlot::Content
};
if marker_start > out_cursor {
out.push((slot, scan[out_cursor..marker_start].to_string()));
}
self.in_reasoning = !self.in_reasoning;
scan_cursor = marker_start + active_marker.len();
out_cursor = scan_cursor;
}
None => {
let total_len = scan.len();
let emit_end = total_len.saturating_sub(self.tail_cap);
if emit_end > out_cursor {
let emit_end = snap_down_char_boundary(&scan, emit_end);
if emit_end > out_cursor {
let slot = if self.in_reasoning {
SplitSlot::Reasoning
} else {
SplitSlot::Content
};
out.push((slot, scan[out_cursor..emit_end].to_string()));
out_cursor = emit_end;
}
}
self.tail_buf = scan[out_cursor..].to_string();
break;
}
}
}
out
}
pub fn finish(&mut self) -> Option<(SplitSlot, String)> {
if self.tail_buf.is_empty() {
return None;
}
let slot = if self.in_reasoning {
SplitSlot::Reasoning
} else {
SplitSlot::Content
};
let text = std::mem::take(&mut self.tail_buf);
Some((slot, text))
}
pub fn in_reasoning(&self) -> bool {
self.in_reasoning
}
}
fn snap_down_char_boundary(s: &str, mut idx: usize) -> usize {
while idx > 0 && !s.is_char_boundary(idx) {
idx -= 1;
}
idx
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum SplitSlot {
Content,
Reasoning,
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ToolCallEvent {
Content(String),
ToolCallOpen,
ToolCallText(String),
ToolCallClose,
}
fn family_resync_markers(open_marker: &str) -> &'static [&'static str] {
match open_marker {
"<|tool_call>" => &[
"<|tool_response>",
"<tool_response|>",
"<|channel>",
"<channel|>",
"<|turn>",
"<turn|>",
],
"<tool_call>" => &[
"<think>", "</think>",
],
_ => &[],
}
}
#[derive(Debug, Clone)]
pub struct ToolCallSplitter {
open_marker: &'static str,
close_marker: &'static str,
in_call_resync: &'static [&'static str],
in_tool_call: bool,
tail_buf: String,
tail_cap: usize,
}
impl ToolCallSplitter {
pub fn from_registration(reg: &ModelRegistration) -> Option<Self> {
let (open, close) = match (reg.tool_open, reg.tool_close) {
(Some(o), Some(c)) if !o.is_empty() && !c.is_empty() => (o, c),
_ => return None,
};
let in_call_resync = family_resync_markers(open);
let mut cap = open.len().max(close.len()).max(1);
for m in in_call_resync {
cap = cap.max(m.len());
}
Some(Self {
open_marker: open,
close_marker: close,
in_call_resync,
in_tool_call: false,
tail_buf: String::with_capacity(cap * 2),
tail_cap: cap,
})
}
pub fn feed(&mut self, fragment: &str) -> Vec<ToolCallEvent> {
let mut out: Vec<ToolCallEvent> = Vec::new();
let mut scan = std::mem::take(&mut self.tail_buf);
scan.push_str(fragment);
let mut scan_cursor = 0usize;
let mut out_cursor = 0usize;
loop {
#[derive(Clone, Copy)]
enum Hit {
StateFlip(&'static str),
Swallow(&'static str),
}
let hit: Option<(usize, Hit)> = if self.in_tool_call {
let mut best: Option<(usize, Hit)> = scan[scan_cursor..]
.find(self.close_marker)
.map(|r| (r, Hit::StateFlip(self.close_marker)));
for &resync in self.in_call_resync {
if let Some(r) = scan[scan_cursor..].find(resync) {
match best {
None => best = Some((r, Hit::StateFlip(resync))),
Some((b, _)) if r < b => best = Some((r, Hit::StateFlip(resync))),
_ => {}
}
}
}
best
} else {
let mut best: Option<(usize, Hit)> = scan[scan_cursor..]
.find(self.open_marker)
.map(|r| (r, Hit::StateFlip(self.open_marker)));
if let Some(r) = scan[scan_cursor..].find(self.close_marker) {
match best {
None => best = Some((r, Hit::Swallow(self.close_marker))),
Some((b, _)) if r < b => best = Some((r, Hit::Swallow(self.close_marker))),
_ => {}
}
}
for &resync in self.in_call_resync {
if let Some(r) = scan[scan_cursor..].find(resync) {
match best {
None => best = Some((r, Hit::Swallow(resync))),
Some((b, _)) if r < b => best = Some((r, Hit::Swallow(resync))),
_ => {}
}
}
}
best
};
match hit {
Some((rel, kind)) => {
let marker_start = scan_cursor + rel;
let marker = match kind {
Hit::StateFlip(m) | Hit::Swallow(m) => m,
};
if marker_start > out_cursor {
let text = scan[out_cursor..marker_start].to_string();
if self.in_tool_call {
out.push(ToolCallEvent::ToolCallText(text));
} else {
out.push(ToolCallEvent::Content(text));
}
}
match kind {
Hit::StateFlip(_) => {
if self.in_tool_call {
out.push(ToolCallEvent::ToolCallClose);
} else {
out.push(ToolCallEvent::ToolCallOpen);
}
self.in_tool_call = !self.in_tool_call;
}
Hit::Swallow(_) => {
}
}
scan_cursor = marker_start + marker.len();
out_cursor = scan_cursor;
}
None => {
let total_len = scan.len();
let emit_end = total_len.saturating_sub(self.tail_cap);
if emit_end > out_cursor {
let emit_end = snap_down_char_boundary(&scan, emit_end);
if emit_end > out_cursor {
let text = scan[out_cursor..emit_end].to_string();
if self.in_tool_call {
out.push(ToolCallEvent::ToolCallText(text));
} else {
out.push(ToolCallEvent::Content(text));
}
out_cursor = emit_end;
}
}
self.tail_buf = scan[out_cursor..].to_string();
break;
}
}
}
out
}
pub fn finish(&mut self) -> Option<ToolCallEvent> {
if self.tail_buf.is_empty() {
return None;
}
let text = std::mem::take(&mut self.tail_buf);
if self.in_tool_call {
Some(ToolCallEvent::ToolCallText(text))
} else {
Some(ToolCallEvent::Content(text))
}
}
pub fn in_tool_call(&self) -> bool {
self.in_tool_call
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct ParsedToolCall {
pub name: String,
pub arguments_json: String,
}
const ALL_FAMILY_LEAK_MARKERS: &[&str] = &[
"<|channel>",
"<channel|>",
"<|tool_call>",
"<tool_call|>",
"<|tool_response>",
"<tool_response|>",
"<|turn>",
"<turn|>",
"<think>",
"</think>",
"<tool_call>",
"</tool_call>",
"<|DSML|tool_calls>",
"</|DSML|tool_calls>",
"<|DSML|invoke",
"</|DSML|invoke>",
"<|DSML|parameter",
"</|DSML|parameter>",
];
pub fn scrub_special_tokens(body: &str) -> String {
let mut out = body.to_string();
for m in ALL_FAMILY_LEAK_MARKERS {
if out.contains(m) {
out = out.replace(m, "");
}
}
out
}
pub fn is_valid_tool_name(name: &str) -> bool {
!name.is_empty()
&& name
.chars()
.all(|c| c.is_ascii_alphanumeric() || c == '_' || c == '-')
}
pub fn parse_tool_call_body(reg: &ModelRegistration, body: &str) -> Option<ParsedToolCall> {
parse_tool_call_bodies(reg, body)?.into_iter().next()
}
pub fn parse_tool_call_bodies(reg: &ModelRegistration, body: &str) -> Option<Vec<ParsedToolCall>> {
match reg.family {
"gemma4" => parse_gemma4_tool_call(body).map(|call| vec![call]),
"qwen35" => parse_qwen35_tool_call(body).map(|call| vec![call]),
"deepseek4" => crate::core::deepseek_v4_encoding::parse_tool_calls_body(body)
.ok()
.map(|calls| {
calls
.into_iter()
.map(|call| ParsedToolCall {
name: call.function.name,
arguments_json: call.function.arguments,
})
.collect()
}),
_ => None,
}
}
fn parse_gemma4_tool_call(body: &str) -> Option<ParsedToolCall> {
let body = body.trim();
let rest = body.strip_prefix("call:")?;
let brace_start = rest.find('{')?;
let name = rest[..brace_start].trim().to_string();
if !is_valid_tool_name(&name) {
return None;
}
let after_open = &rest[brace_start + 1..];
let close_idx = after_open.rfind('}')?;
let kv_str = &after_open[..close_idx];
let kvs = split_gemma4_top_level(kv_str);
let mut args = serde_json::Map::new();
for kv in kvs {
let (k, v) = split_gemma4_kv_once(kv)?;
let key = k.trim().to_string();
if key.is_empty() {
return None;
}
args.insert(key, gemma4_value_to_json(v.trim()));
}
let arguments_json = serde_json::to_string(&serde_json::Value::Object(args)).ok()?;
Some(ParsedToolCall {
name,
arguments_json,
})
}
fn split_gemma4_top_level(s: &str) -> Vec<&str> {
let mut out = Vec::new();
let mut start = 0usize;
let mut in_str = false;
let mut depth: usize = 0;
let bytes = s.as_bytes();
let mut i = 0usize;
while i < bytes.len() {
if bytes[i..].starts_with(b"<|\"|>") {
in_str = !in_str;
i += 5;
continue;
}
if !in_str {
match bytes[i] {
b'{' | b'[' => depth += 1,
b'}' | b']' => depth = depth.saturating_sub(1),
b',' if depth == 0 => {
out.push(&s[start..i]);
start = i + 1;
}
_ => {}
}
}
i += 1;
}
if start < s.len() {
out.push(&s[start..]);
}
out
}
fn split_gemma4_kv_once(s: &str) -> Option<(&str, &str)> {
let mut in_str = false;
let mut depth: usize = 0;
let bytes = s.as_bytes();
let mut i = 0usize;
while i < bytes.len() {
if bytes[i..].starts_with(b"<|\"|>") {
in_str = !in_str;
i += 5;
continue;
}
if !in_str {
match bytes[i] {
b'{' | b'[' => depth += 1,
b'}' | b']' => depth = depth.saturating_sub(1),
b':' if depth == 0 => return Some((&s[..i], &s[i + 1..])),
_ => {}
}
}
i += 1;
}
None
}
fn gemma4_value_to_json(v: &str) -> serde_json::Value {
let v = v.trim();
if let Some(stripped) = v
.strip_prefix("<|\"|>")
.and_then(|s| s.strip_suffix("<|\"|>"))
{
return serde_json::Value::String(stripped.to_string());
}
if v.len() >= 2 && v.starts_with('{') && v.ends_with('}') {
let inner = &v[1..v.len() - 1];
let mut map = serde_json::Map::new();
if !inner.trim().is_empty() {
for kv in split_gemma4_top_level(inner) {
match split_gemma4_kv_once(kv) {
Some((k, val)) => {
let key = k.trim();
if key.is_empty() {
return serde_json::Value::String(v.to_string());
}
map.insert(key.to_string(), gemma4_value_to_json(val));
}
None => return serde_json::Value::String(v.to_string()),
}
}
}
return serde_json::Value::Object(map);
}
if v.len() >= 2 && v.starts_with('[') && v.ends_with(']') {
let inner = &v[1..v.len() - 1];
if inner.trim().is_empty() {
return serde_json::Value::Array(Vec::new());
}
return serde_json::Value::Array(
split_gemma4_top_level(inner)
.into_iter()
.map(gemma4_value_to_json)
.collect(),
);
}
if let Ok(num) = v.parse::<i64>() {
return serde_json::Value::from(num);
}
if let Ok(num) = v.parse::<f64>() {
return serde_json::Value::from(num);
}
match v {
"true" => serde_json::Value::Bool(true),
"false" => serde_json::Value::Bool(false),
"null" => serde_json::Value::Null,
_ => serde_json::Value::String(v.to_string()),
}
}
fn parse_qwen35_tool_call(body: &str) -> Option<ParsedToolCall> {
let body = body.trim();
let after_func = body.strip_prefix("<function=")?;
let name_end = after_func.find('>')?;
let name = after_func[..name_end].trim().to_string();
if !is_valid_tool_name(&name) {
return None;
}
let after_name_close = &after_func[name_end + 1..];
let func_close = after_name_close.rfind("</function>")?;
let inner = after_name_close[..func_close].trim();
let mut args = serde_json::Map::new();
let mut cursor = 0usize;
while cursor < inner.len() {
let Some(rel_open) = inner[cursor..].find("<parameter=") else {
break;
};
let p_open = cursor + rel_open;
let key_start = p_open + "<parameter=".len();
let Some(rel_gt) = inner[key_start..].find('>') else {
break;
};
let key_end = key_start + rel_gt;
let key = inner[key_start..key_end].trim().to_string();
let val_start = key_end + 1;
let Some(rel_close) = inner[val_start..].find("</parameter>") else {
break;
};
let val_end = val_start + rel_close;
let val_raw = inner[val_start..val_end].trim();
let json_val: serde_json::Value = match serde_json::from_str(val_raw) {
Ok(v) => v,
Err(_) => serde_json::Value::String(val_raw.to_string()),
};
args.insert(key, json_val);
cursor = val_end + "</parameter>".len();
}
let arguments_json = serde_json::to_string(&serde_json::Value::Object(args)).ok()?;
Some(ParsedToolCall {
name,
arguments_json,
})
}
pub fn make_reasoning_splitter(
reg: &ModelRegistration,
forced_open: bool,
) -> Option<ReasoningSplitter> {
ReasoningSplitter::from_registration_forced(reg, forced_open)
}
pub fn prompt_seeds_reasoning_open(rendered: &str, reg: &ModelRegistration) -> bool {
match reg.reasoning_open {
Some(open) if !open.trim_end().is_empty() => rendered.trim_end().ends_with(open.trim_end()),
_ => false,
}
}
pub fn split_full_output(reg: &ModelRegistration, full_text: &str) -> (String, Option<String>) {
split_full_output_forced(reg, full_text, false)
}
pub fn split_full_output_forced(
reg: &ModelRegistration,
full_text: &str,
forced_open: bool,
) -> (String, Option<String>) {
let mut splitter = match make_reasoning_splitter(reg, forced_open) {
Some(s) => s,
None => return (full_text.to_string(), None),
};
let mut content = String::new();
let mut reasoning = String::new();
for (slot, frag) in splitter.feed(full_text) {
match slot {
SplitSlot::Content => content.push_str(&frag),
SplitSlot::Reasoning => reasoning.push_str(&frag),
}
}
if let Some((slot, frag)) = splitter.finish() {
match slot {
SplitSlot::Content => content.push_str(&frag),
SplitSlot::Reasoning => reasoning.push_str(&frag),
}
}
(
content,
if reasoning.is_empty() {
None
} else {
Some(reasoning)
},
)
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum EmitterError {
TooManyRequiredKeys { fn_name: String, count: usize },
UnsupportedSchemaFeature {
fn_name: String,
param_path: String,
feature: String,
},
}
const MAX_REQUIRED_KEYS: usize = 8;
const MAX_NESTED_DEPTH: usize = 32;
const MAX_NESTED_PROPERTIES: usize = 32;
impl std::fmt::Display for EmitterError {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
EmitterError::TooManyRequiredKeys { fn_name, count } => write!(
f,
"function '{}' has {} required parameters; ADR-005 wave-2.7 \
limits required keys to {} (SOTA bound: O(2^N) permutation \
grammar, 256 rules worst-case); reduce the required set or \
split the tool",
fn_name, count, MAX_REQUIRED_KEYS
),
EmitterError::UnsupportedSchemaFeature {
fn_name,
param_path,
feature,
} => write!(
f,
"function '{}' parameter '{}' uses unsupported schema feature \
'{}'; the iter-231b nested compiler supports scalars, enums, \
type-unions, anyOf/oneOf, object (properties/required<=8/\
additionalProperties) and array (single-schema items); \
rewrite the parameter schema or drop the feature",
fn_name, param_path, feature
),
}
}
}
fn gbnf_literal(s: &str) -> String {
let mut out = String::with_capacity(s.len() + 2);
out.push('"');
for c in s.chars() {
match c {
'\r' => out.push_str("\\r"),
'\n' => out.push_str("\\n"),
'"' => out.push_str("\\\""),
'\\' => out.push_str("\\\\"),
_ => out.push(c),
}
}
out.push('"');
out
}
fn gemma4_value_gbnf(
fn_name: &str,
param_name: &str,
schema: &serde_json::Value,
rules: &mut Vec<(String, String)>,
rule_counter: &mut u32,
) -> Result<String, EmitterError> {
let obj = match schema.as_object() {
Some(o) => o,
None => {
return Ok(GEMMA4_TOP_ANY_VAL.to_string());
}
};
if let Some(serde_json::Value::Array(values)) = obj.get("enum") {
let alts: Vec<String> = values
.iter()
.filter_map(|v| v.as_str())
.map(|s| {
format!(
"{} {} {}",
gbnf_literal("<|\"|>"),
gbnf_literal(s),
gbnf_literal("<|\"|>")
)
})
.collect();
if !alts.is_empty() {
return Ok(format!("( {} )", alts.join(" | ")));
}
}
let schema_type = obj.get("type").and_then(|t| t.as_str()).unwrap_or("");
match schema_type {
"string" => {
match compile_pattern(
fn_name,
&format!("/{}", param_name),
obj.get("pattern"),
crate::serve::api::grammar::regex_gbnf::Surface::GemmaMarkerString,
)? {
Some(body) => Ok(format!(
"{} {} {}",
gbnf_literal("<|\"|>"),
body,
gbnf_literal("<|\"|>")
)),
None => Ok("gemma4-str-val".to_string()),
}
}
"integer" => Ok("gemma4-int-val".to_string()),
"number" => Ok("gemma4-num-val".to_string()),
"boolean" => Ok("gemma4-bool-val".to_string()),
"null" => Ok("gemma4-null-val".to_string()),
"array" | "object" => gemma4_nested_value_rule(
fn_name,
&format!("/{}", param_name),
schema,
rules,
rule_counter,
1,
),
_ => {
Ok(GEMMA4_TOP_ANY_VAL.to_string())
}
}
}
const GEMMA4_TOP_ANY_VAL: &str = "( gemma4-any-val | gemma4-json-obj | gemma4-json-arr )";
fn gemma4_nested_value_rule(
fn_name: &str,
path: &str,
schema: &serde_json::Value,
rules: &mut Vec<(String, String)>,
rule_counter: &mut u32,
depth: usize,
) -> Result<String, EmitterError> {
if depth > MAX_NESTED_DEPTH {
return Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: format!("nesting depth > {}", MAX_NESTED_DEPTH),
});
}
let obj = match schema.as_object() {
Some(o) => o,
None => return Ok("gemma4-json-val".to_string()),
};
for feat in [
"allOf",
"$ref",
"$defs",
"not",
"if",
"then",
"else",
"dependentSchemas",
"patternProperties",
"propertyNames",
"contains",
] {
if obj.contains_key(feat) {
return Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: feat.to_string(),
});
}
}
for comb in ["anyOf", "oneOf"] {
if let Some(serde_json::Value::Array(subs)) = obj.get(comb) {
if subs.is_empty() {
return Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: format!("empty {}", comb),
});
}
let mut alts: Vec<String> = Vec::with_capacity(subs.len());
for (i, s) in subs.iter().enumerate() {
alts.push(gemma4_nested_value_rule(
fn_name,
&format!("{}/{}/{}", path, comb, i),
s,
rules,
rule_counter,
depth + 1,
)?);
}
return Ok(format!("( {} )", alts.join(" | ")));
}
}
if let Some(serde_json::Value::Array(values)) = obj.get("enum") {
if values.is_empty() {
return Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: "empty enum".to_string(),
});
}
let mut alts: Vec<String> = Vec::with_capacity(values.len());
for v in values {
match v {
serde_json::Value::String(s) => alts.push(format!(
"{} {} {}",
gbnf_literal("<|\"|>"),
gbnf_literal(s),
gbnf_literal("<|\"|>")
)),
serde_json::Value::Number(_) | serde_json::Value::Bool(_) => {
let text = serde_json::to_string(v).map_err(|e| {
EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: format!("enum serialize: {}", e),
}
})?;
alts.push(gbnf_literal(&text));
}
serde_json::Value::Null => alts.push(gbnf_literal("null")),
_ => {
return Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: "container enum value".to_string(),
});
}
}
}
return Ok(format!("( {} )", alts.join(" | ")));
}
match obj.get("type") {
None => Ok("gemma4-json-val".to_string()),
Some(serde_json::Value::Array(types)) => {
let mut alts: Vec<String> = Vec::with_capacity(types.len());
for (i, t) in types.iter().enumerate() {
let Some(tstr) = t.as_str() else {
return Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: "non-string type union entry".to_string(),
});
};
let mut stub = serde_json::Map::new();
stub.insert("type".into(), serde_json::Value::String(tstr.into()));
alts.push(gemma4_nested_value_rule(
fn_name,
&format!("{}/type/{}", path, i),
&serde_json::Value::Object(stub),
rules,
rule_counter,
depth + 1,
)?);
}
Ok(format!("( {} )", alts.join(" | ")))
}
Some(serde_json::Value::String(t)) => match t.as_str() {
"string" => {
match compile_pattern(
fn_name,
path,
obj.get("pattern"),
crate::serve::api::grammar::regex_gbnf::Surface::GemmaMarkerString,
)? {
Some(body) => Ok(format!(
"{} {} {}",
gbnf_literal("<|\"|>"),
body,
gbnf_literal("<|\"|>")
)),
None => Ok("gemma4-str-val".to_string()),
}
}
"integer" => Ok("gemma4-int-val".to_string()),
"number" => Ok("gemma4-num-val".to_string()),
"boolean" => Ok("gemma4-bool-val".to_string()),
"null" => Ok("gemma4-null-val".to_string()),
"object" => gemma4_nested_object(fn_name, path, obj, rules, rule_counter, depth),
"array" => gemma4_nested_array(fn_name, path, obj, rules, rule_counter, depth),
other => Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: format!("type '{}'", other),
}),
},
Some(_) => Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: "non-string type".to_string(),
}),
}
}
fn gemma4_nested_object(
fn_name: &str,
path: &str,
obj: &serde_json::Map<String, serde_json::Value>,
rules: &mut Vec<(String, String)>,
rule_counter: &mut u32,
depth: usize,
) -> Result<String, EmitterError> {
let properties = obj.get("properties").and_then(|p| p.as_object());
let additional_closed = matches!(
obj.get("additionalProperties"),
Some(serde_json::Value::Bool(false))
);
let props = match properties {
Some(p) if !p.is_empty() => p,
_ => {
return Ok(if additional_closed {
r#""{" "}""#.to_string()
} else {
"gemma4-json-obj".to_string()
});
}
};
let required_set: std::collections::HashSet<&str> = obj
.get("required")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
.unwrap_or_default();
if required_set.len() > MAX_REQUIRED_KEYS {
return Err(EmitterError::TooManyRequiredKeys {
fn_name: format!("{} (nested {})", fn_name, path),
count: required_set.len(),
});
}
if props.len() > MAX_NESTED_PROPERTIES {
return Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: format!("> {} properties", MAX_NESTED_PROPERTIES),
});
}
let mut req_kv: Vec<String> = Vec::new();
let mut opt_kv: Vec<String> = Vec::new();
let mut sorted_keys: Vec<&String> = props.keys().collect();
sorted_keys.sort();
for key in sorted_keys {
let val_body = gemma4_nested_value_rule(
fn_name,
&format!("{}/properties/{}", path, key),
&props[key],
rules,
rule_counter,
depth + 1,
)?;
*rule_counter += 1;
let kv_name = format!("g4n-{}-kv", *rule_counter);
rules.push((
kv_name.clone(),
format!("{} \":\" {}", gbnf_literal(key), val_body),
));
if required_set.contains(key.as_str()) {
req_kv.push(kv_name);
} else {
opt_kv.push(kv_name);
}
}
let extra_kv: Option<String> = if additional_closed {
None
} else {
Some(r#"gemma4-json-key ":" gemma4-json-val"#.to_string())
};
build_nested_obj_body("g4n", req_kv, opt_kv, extra_kv, rules, rule_counter)
.map(|inner| format!(r#""{{" {} "}}""#, inner))
}
fn gemma4_nested_array(
fn_name: &str,
path: &str,
obj: &serde_json::Map<String, serde_json::Value>,
rules: &mut Vec<(String, String)>,
rule_counter: &mut u32,
depth: usize,
) -> Result<String, EmitterError> {
match obj.get("items") {
None => Ok("gemma4-json-arr".to_string()),
Some(serde_json::Value::Object(_)) => {
let item_rule = gemma4_nested_value_rule(
fn_name,
&format!("{}/items", path),
obj.get("items").expect("items checked above"),
rules,
rule_counter,
depth + 1,
)?;
Ok(format!(r#""[" ( {0} ("," {0})* )? "]""#, item_rule))
}
Some(serde_json::Value::Array(_)) => Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: "tuple-form items".to_string(),
}),
Some(_) => Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: "non-object items".to_string(),
}),
}
}
fn gemma4_tool_call_gbnf(
fn_name: &str,
params_schema: &serde_json::Value,
shape: GrammarShape,
) -> Result<String, EmitterError> {
let mut rules: Vec<(String, String)> = Vec::new();
let mut rule_counter: u32 = 0;
rules.push((
"gemma4-str-char".to_string(),
r#"[^<\\] | [\\] [^\x00-\x1F]"#.to_string(),
));
rules.push((
"gemma4-str-val".to_string(),
format!(
"{} gemma4-str-char* {}",
gbnf_literal("<|\"|>"),
gbnf_literal("<|\"|>")
),
));
rules.push((
"gemma4-int-val".to_string(),
r#""-"? ([0] | [1-9] [0-9]{0,15})"#.to_string(),
));
rules.push((
"gemma4-num-val".to_string(),
r#""-"? ([0] | [1-9] [0-9]{0,15}) ("." [0-9]{1,16})? ([eE] [-+]? [0-9]{1,16})?"#
.to_string(),
));
rules.push((
"gemma4-bool-val".to_string(),
r#""true" | "false""#.to_string(),
));
rules.push(("gemma4-null-val".to_string(), r#""null""#.to_string()));
rules.push((
"gemma4-any-val".to_string(),
r#"gemma4-str-val | gemma4-num-val | gemma4-bool-val | gemma4-null-val"#.to_string(),
));
rules.push((
"gemma4-json-key".to_string(),
r#"[^,:{}\[\]<]+"#.to_string(),
));
rules.push((
"gemma4-json-obj".to_string(),
r#""{" ("}" | gemma4-json-key ":" gemma4-json-val ("," gemma4-json-key ":" gemma4-json-val)* "}")"#.to_string(),
));
rules.push((
"gemma4-json-arr".to_string(),
r#""[" ("]" | gemma4-json-val ("," gemma4-json-val)* "]")"#.to_string(),
));
rules.push((
"gemma4-json-val".to_string(),
"gemma4-str-val | gemma4-num-val | gemma4-bool-val | gemma4-null-val | gemma4-json-obj | gemma4-json-arr".to_string(),
));
let required_set: std::collections::HashSet<String> = params_schema
.as_object()
.and_then(|o| o.get("required"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
if required_set.len() > MAX_REQUIRED_KEYS {
return Err(EmitterError::TooManyRequiredKeys {
fn_name: fn_name.to_string(),
count: required_set.len(),
});
}
let properties = params_schema
.as_object()
.and_then(|o| o.get("properties"))
.and_then(|p| p.as_object());
let prefix_lit = gbnf_literal(&format!("call:{}", fn_name));
let open_lit = gbnf_literal("{");
let close_lit = gbnf_literal("}");
let comma_lit = gbnf_literal(",");
let single_body: String = if let Some(props) = properties {
if props.is_empty() {
format!("{} {} {}", prefix_lit, open_lit, close_lit)
} else {
let mut required_kv_names: Vec<String> = Vec::new();
let mut optional_kv_names: Vec<String> = Vec::new();
let mut sorted_keys: Vec<&String> = props.keys().collect();
sorted_keys.sort();
for key in &sorted_keys {
let val_schema = &props[*key];
let val_rule = gemma4_value_gbnf(
fn_name,
key.as_str(),
val_schema,
&mut rules,
&mut rule_counter,
)?;
let key_lit = gbnf_literal(key.as_str());
let kv_body = format!("{} {} {}", key_lit, gbnf_literal(":"), val_rule);
let kv_name = format!("gemma4-kv-{}", sanitize_rule_name_local(key));
rules.push((kv_name.clone(), kv_body));
if required_set.contains(*key) {
required_kv_names.push(kv_name);
} else {
optional_kv_names.push(kv_name);
}
}
let kv_body_rule = if required_kv_names.is_empty() {
let all_names: Vec<String> = optional_kv_names.clone();
let alts = all_names.join(" | ");
let kv_item_rule = "gemma4-kv-item".to_string();
rules.push((kv_item_rule.clone(), format!("( {} )", alts)));
let kv_list_rule = "gemma4-kv-list".to_string();
rules.push((
kv_list_rule.clone(),
format!("{} ( {} {} )*", kv_item_rule, comma_lit, kv_item_rule),
));
kv_list_rule
} else {
let req_top = build_gemma4_required_permutation(
fn_name,
&required_kv_names,
&comma_lit,
&mut rules,
);
if optional_kv_names.is_empty() {
req_top
} else {
let alts = optional_kv_names.join(" | ");
let opt_item_rule = "gemma4-opt-item".to_string();
rules.push((opt_item_rule.clone(), format!("( {} )", alts)));
let kv_list_rule = "gemma4-kv-list".to_string();
rules.push((
kv_list_rule.clone(),
format!("{} ( {} {} )*", req_top, comma_lit, opt_item_rule),
));
kv_list_rule
}
};
format!("{} {} {} {}", prefix_lit, open_lit, kv_body_rule, close_lit)
}
} else {
rules.push(("gemma4-any-kv-char".to_string(), r#"[^}]"#.to_string()));
format!(
"{} {} gemma4-any-kv-char* {}",
prefix_lit, open_lit, close_lit
)
};
let root_body = match shape {
GrammarShape::SingleBody => single_body.clone(),
GrammarShape::OneOrMoreCalls { parallel } => {
let open_marker = gbnf_literal("<|tool_call>");
let close_marker = gbnf_literal("<tool_call|>");
let g4_call_rule = "gemma4-call".to_string();
rules.push((
g4_call_rule.clone(),
format!("{} {} {}", open_marker, single_body, close_marker),
));
if parallel {
format!("{} {}*", g4_call_rule, g4_call_rule)
} else {
g4_call_rule
}
}
GrammarShape::OneOrMoreCallsBodyOnly { parallel } => {
let open_marker = gbnf_literal("<|tool_call>");
let close_marker = gbnf_literal("<tool_call|>");
if parallel {
let g4_call_rule = "gemma4-call".to_string();
rules.push((
g4_call_rule.clone(),
format!("{} {} {}", open_marker, single_body, close_marker),
));
format!("{} {} {}*", single_body, close_marker, g4_call_rule)
} else {
format!("{} {}", single_body, close_marker)
}
}
};
rules.push(("root".to_string(), root_body));
let mut out = String::new();
for (name, body) in &rules {
if name == "root" {
out.push_str(&format!("root ::= {}\n", body));
break;
}
}
for (name, body) in &rules {
if name != "root" {
out.push_str(&format!("{} ::= {}\n", name, body));
}
}
Ok(out)
}
fn build_gemma4_required_permutation(
slug: &str,
required_kv_names: &[String],
comma_lit: &str,
rules: &mut Vec<(String, String)>,
) -> String {
let mut sorted = required_kv_names.to_vec();
sorted.sort();
let name_parts: Vec<String> = sorted
.iter()
.map(|n| n.trim_start_matches("gemma4-kv-").to_string())
.collect();
let rule_name = format!(
"g4req-{}-{}",
sanitize_rule_name_local(slug),
name_parts.join("-")
);
if rules.iter().any(|(n, _)| n == &rule_name) {
return rule_name;
}
if sorted.len() == 1 {
rules.push((rule_name.clone(), sorted[0].clone()));
return rule_name;
}
rules.push((rule_name.clone(), String::new()));
let mut alts: Vec<String> = Vec::new();
for (i, kv_name) in sorted.iter().enumerate() {
let remaining: Vec<String> = sorted
.iter()
.enumerate()
.filter(|(j, _)| *j != i)
.map(|(_, s)| s.clone())
.collect();
let rest = build_gemma4_required_permutation(slug, &remaining, comma_lit, rules);
alts.push(format!("{} {} {}", kv_name, comma_lit, rest));
}
let body = alts.join(" | ");
for (n, b) in rules.iter_mut() {
if n == &rule_name {
*b = body;
break;
}
}
rule_name
}
fn qwen35_tool_call_gbnf(
fn_name: &str,
params_schema: &serde_json::Value,
shape: GrammarShape,
) -> Result<String, EmitterError> {
let mut rules: Vec<(String, String)> = Vec::new();
rules.push((
"qwen35-str-char".to_string(),
r#"[^<\\] | [\\] [^\x00-\x1F]"#.to_string(),
));
rules.push(("qwen35-str-val".to_string(), "qwen35-str-char*".to_string()));
rules.push((
"qwen35-int-val".to_string(),
r#""-"? ([0] | [1-9] [0-9]{0,15})"#.to_string(),
));
rules.push((
"qwen35-num-val".to_string(),
r#""-"? ([0] | [1-9] [0-9]{0,15}) ("." [0-9]{1,16})? ([eE] [-+]? [0-9]{1,16})?"#
.to_string(),
));
rules.push((
"qwen35-bool-val".to_string(),
r#""true" | "false""#.to_string(),
));
rules.push(("qwen35-null-val".to_string(), r#""null""#.to_string()));
rules.push((
"qwen35-any-val".to_string(),
r#"qwen35-str-val | qwen35-num-val | qwen35-bool-val | qwen35-null-val"#.to_string(),
));
rules.push((
"qwen35-json-char".to_string(),
r#"[^<"\\\x00-\x1F] | [\\] (["\\/bfnrt] | [u] [0-9a-fA-F]{4})"#.to_string(),
));
rules.push((
"qwen35-json-str".to_string(),
r#""\"" qwen35-json-char* "\"""#.to_string(),
));
rules.push((
"qwen35-json-obj".to_string(),
r#""{" ("}" | qwen35-json-str ":" qwen35-json-val ("," qwen35-json-str ":" qwen35-json-val)* "}")"#.to_string(),
));
rules.push((
"qwen35-json-arr".to_string(),
r#""[" ("]" | qwen35-json-val ("," qwen35-json-val)* "]")"#.to_string(),
));
rules.push((
"qwen35-json-val".to_string(),
"qwen35-json-str | qwen35-num-val | qwen35-bool-val | qwen35-null-val | qwen35-json-obj | qwen35-json-arr".to_string(),
));
let required_set: std::collections::HashSet<String> = params_schema
.as_object()
.and_then(|o| o.get("required"))
.and_then(|v| v.as_array())
.map(|arr| {
arr.iter()
.filter_map(|v| v.as_str().map(String::from))
.collect()
})
.unwrap_or_default();
if required_set.len() > MAX_REQUIRED_KEYS {
return Err(EmitterError::TooManyRequiredKeys {
fn_name: fn_name.to_string(),
count: required_set.len(),
});
}
let properties = params_schema
.as_object()
.and_then(|o| o.get("properties"))
.and_then(|p| p.as_object());
let func_open_lit = gbnf_literal(&format!("<function={}>", fn_name));
let func_close_lit = gbnf_literal("</function>");
let newline_lit = gbnf_literal("\n");
let single_body: String = if let Some(props) = properties {
if props.is_empty() {
format!("{} {}", func_open_lit, func_close_lit)
} else {
let mut required_block_names: Vec<String> = Vec::new();
let mut optional_block_names: Vec<String> = Vec::new();
let mut rule_counter: u32 = 0;
let mut sorted_keys: Vec<&String> = props.keys().collect();
sorted_keys.sort();
for key in &sorted_keys {
let val_schema = &props[*key];
let val_rule = qwen35_value_rule(
fn_name,
key.as_str(),
val_schema,
&mut rules,
&mut rule_counter,
)?;
let param_open_lit = gbnf_literal(&format!("<parameter={}>", key));
let param_close_lit = gbnf_literal("</parameter>");
let block_body = format!(
"{} {} {} {} {} {}",
param_open_lit,
newline_lit,
val_rule,
newline_lit,
param_close_lit,
newline_lit
);
let block_name = format!("qwen35-param-{}", sanitize_rule_name_local(key));
rules.push((block_name.clone(), block_body));
if required_set.contains(*key) {
required_block_names.push(block_name);
} else {
optional_block_names.push(block_name);
}
}
let param_body_rule = if required_block_names.is_empty() {
let alts = optional_block_names.join(" | ");
let param_item_rule = "qwen35-param-item".to_string();
rules.push((param_item_rule.clone(), format!("( {} )", alts)));
let param_list_rule = "qwen35-param-list".to_string();
rules.push((param_list_rule.clone(), format!("{}*", param_item_rule)));
param_list_rule
} else {
let req_top =
build_qwen35_required_permutation(fn_name, &required_block_names, &mut rules);
if optional_block_names.is_empty() {
req_top
} else {
let alts = optional_block_names.join(" | ");
let opt_item_rule = "qwen35-opt-item".to_string();
rules.push((opt_item_rule.clone(), format!("( {} )", alts)));
let param_list_rule = "qwen35-param-list".to_string();
rules.push((
param_list_rule.clone(),
format!("{} {}*", req_top, opt_item_rule),
));
param_list_rule
}
};
format!(
"{} {} {} {}",
func_open_lit, newline_lit, param_body_rule, func_close_lit
)
}
} else {
rules.push((
"qwen35-inner-char".to_string(),
r#"[^<\\] | [\\] [^\x00-\x1F]"#.to_string(),
));
format!("{} qwen35-inner-char* {}", func_open_lit, func_close_lit)
};
let root_body = match shape {
GrammarShape::SingleBody => single_body.clone(),
GrammarShape::OneOrMoreCalls { parallel } => {
let open_marker = gbnf_literal("<tool_call>");
let close_marker = gbnf_literal("</tool_call>");
let qwen_call_rule = "qwen35-call".to_string();
rules.push((
qwen_call_rule.clone(),
format!(
"{} {} {} {} {}",
open_marker, newline_lit, single_body, newline_lit, close_marker
),
));
if parallel {
format!("{} ( {} {} )*", qwen_call_rule, newline_lit, qwen_call_rule)
} else {
qwen_call_rule
}
}
GrammarShape::OneOrMoreCallsBodyOnly { parallel } => {
let open_marker = gbnf_literal("<tool_call>");
let close_marker = gbnf_literal("</tool_call>");
if parallel {
let qwen_call_rule = "qwen35-call".to_string();
rules.push((
qwen_call_rule.clone(),
format!(
"{} {} {} {} {}",
open_marker, newline_lit, single_body, newline_lit, close_marker
),
));
format!(
"{} {} {} ( {} {} )*",
single_body, newline_lit, close_marker, newline_lit, qwen_call_rule
)
} else {
format!("{} {} {}", single_body, newline_lit, close_marker)
}
}
};
rules.push(("root".to_string(), root_body));
let mut out = String::new();
for (name, body) in &rules {
if name == "root" {
out.push_str(&format!("root ::= {}\n", body));
break;
}
}
for (name, body) in &rules {
if name != "root" {
out.push_str(&format!("{} ::= {}\n", name, body));
}
}
Ok(out)
}
fn deepseek4_tool_call_gbnf(
fn_name: &str,
params_schema: &serde_json::Value,
shape: GrammarShape,
) -> Result<String, EmitterError> {
let mut rules: Vec<(String, String)> = vec![
(
"dsml-json-char".into(),
r#"[^"\\\x00-\x1F] | [\\] (["\\/bfnrt] | "u" [0-9a-fA-F]{4})"#.into(),
),
(
"dsml-json-str".into(),
r#""\"" dsml-json-char* "\"""#.into(),
),
(
"dsml-json-num".into(),
r#""-"? ([0] | [1-9] [0-9]{0,15}) ("." [0-9]{1,16})? ([eE] [-+]? [0-9]{1,16})?"#.into(),
),
(
"dsml-json-obj".into(),
r#""{" ("}" | dsml-json-str ":" dsml-json-val ("," dsml-json-str ":" dsml-json-val)* "}")"#.into(),
),
(
"dsml-json-arr".into(),
r#""[" ("]" | dsml-json-val ("," dsml-json-val)* "]")"#.into(),
),
(
"dsml-json-val".into(),
r#"dsml-json-str | dsml-json-num | "true" | "false" | "null" | dsml-json-obj | dsml-json-arr"#.into(),
),
(
"dsml-string-char".into(),
r#"[^<\\] | [\\] [^\x00-\x1F]"#.into(),
),
("dsml-string-val".into(), "dsml-string-char*".into()),
];
let newline = gbnf_literal("\n");
let parameter_close = gbnf_literal("</|DSML|parameter>");
let required_set: std::collections::HashSet<String> = params_schema
.as_object()
.and_then(|object| object.get("required"))
.and_then(serde_json::Value::as_array)
.map(|items| {
items
.iter()
.filter_map(serde_json::Value::as_str)
.map(ToOwned::to_owned)
.collect()
})
.unwrap_or_default();
if required_set.len() > MAX_REQUIRED_KEYS {
return Err(EmitterError::TooManyRequiredKeys {
fn_name: fn_name.to_string(),
count: required_set.len(),
});
}
let mut required_parameter_rules = Vec::new();
let mut optional_parameter_rules = Vec::new();
if let Some(properties) = params_schema
.as_object()
.and_then(|object| object.get("properties"))
.and_then(serde_json::Value::as_object)
{
let mut keys = properties.keys().collect::<Vec<_>>();
keys.sort();
for key in keys {
let schema = &properties[key];
let is_string = schema
.get("type")
.and_then(serde_json::Value::as_str)
.is_some_and(|kind| kind == "string");
let value_rule = if is_string {
"dsml-string-val"
} else {
"dsml-json-val"
};
let open = gbnf_literal(&format!(
"<|DSML|parameter name=\"{}\" string=\"{}\">",
key,
if is_string { "true" } else { "false" }
));
let rule_name = format!("dsml-param-{}", sanitize_rule_name_local(key));
rules.push((
rule_name.clone(),
format!("{} {} {} {}", open, value_rule, parameter_close, newline),
));
if required_set.contains(key.as_str()) {
required_parameter_rules.push(rule_name);
} else {
optional_parameter_rules.push(rule_name);
}
}
}
let parameter_sequence =
if required_parameter_rules.is_empty() && optional_parameter_rules.is_empty() {
None
} else if required_parameter_rules.is_empty() {
let item = "dsml-optional-param".to_string();
rules.push((
item.clone(),
format!("( {} )", optional_parameter_rules.join(" | ")),
));
let list = "dsml-param-list".to_string();
rules.push((list.clone(), format!("{}*", item)));
Some(list)
} else {
let required =
build_dsml_required_permutation(fn_name, &required_parameter_rules, &mut rules);
if optional_parameter_rules.is_empty() {
Some(required)
} else {
let item = "dsml-optional-param".to_string();
rules.push((
item.clone(),
format!("( {} )", optional_parameter_rules.join(" | ")),
));
let list = "dsml-param-list".to_string();
rules.push((list.clone(), format!("{} {}*", required, item)));
Some(list)
}
};
let invoke_open = gbnf_literal(&format!("<|DSML|invoke name=\"{}\">", fn_name));
let invoke_close = gbnf_literal("</|DSML|invoke>");
let invoke = if let Some(parameters) = parameter_sequence {
format!(
"{} {} {} {}",
invoke_open, newline, parameters, invoke_close
)
} else {
format!("{} {} {} {}", invoke_open, newline, newline, invoke_close)
};
rules.push(("dsml-invoke".into(), invoke));
let open = gbnf_literal("<|DSML|tool_calls>");
let close = gbnf_literal("</|DSML|tool_calls>");
let full_body = match shape {
GrammarShape::SingleBody => "dsml-invoke".to_string(),
GrammarShape::OneOrMoreCalls { parallel } => {
let invokes = if parallel {
format!("dsml-invoke ( {} dsml-invoke )*", newline)
} else {
"dsml-invoke".to_string()
};
format!("{} {} {} {} {}", open, newline, invokes, newline, close)
}
GrammarShape::OneOrMoreCallsBodyOnly { parallel } => {
let invokes = if parallel {
format!("dsml-invoke ( {} dsml-invoke )*", newline)
} else {
"dsml-invoke".to_string()
};
format!("{} {} {} {}", newline, invokes, newline, close)
}
};
rules.push(("root".into(), full_body));
let mut output = String::new();
output.push_str(&format!("root ::= {}\n", rules.last().unwrap().1));
for (name, body) in rules.into_iter().filter(|(name, _)| name != "root") {
output.push_str(&format!("{} ::= {}\n", name, body));
}
Ok(output)
}
fn build_dsml_required_permutation(
fn_name: &str,
required: &[String],
rules: &mut Vec<(String, String)>,
) -> String {
let mut sorted = required.to_vec();
sorted.sort();
if sorted.len() == 1 {
return sorted[0].clone();
}
let suffix = sorted
.iter()
.map(|name| name.trim_start_matches("dsml-param-"))
.collect::<Vec<_>>()
.join("-");
let rule_name = format!(
"dsml-required-{}-{}",
sanitize_rule_name_local(fn_name),
suffix
);
if rules.iter().any(|(name, _)| name == &rule_name) {
return rule_name;
}
rules.push((rule_name.clone(), String::new()));
let alternatives = sorted
.iter()
.enumerate()
.map(|(index, item)| {
let remainder = sorted
.iter()
.enumerate()
.filter(|(candidate, _)| *candidate != index)
.map(|(_, value)| value.clone())
.collect::<Vec<_>>();
let tail = build_dsml_required_permutation(fn_name, &remainder, rules);
format!("{} {}", item, tail)
})
.collect::<Vec<_>>()
.join(" | ");
let (_, body) = rules
.iter_mut()
.find(|(name, _)| name == &rule_name)
.expect("inserted DSML permutation rule");
*body = alternatives;
rule_name
}
fn build_qwen35_required_permutation(
slug: &str,
required_block_names: &[String],
rules: &mut Vec<(String, String)>,
) -> String {
let mut sorted = required_block_names.to_vec();
sorted.sort();
let name_parts: Vec<String> = sorted
.iter()
.map(|n| n.trim_start_matches("qwen35-param-").to_string())
.collect();
let rule_name = format!(
"q35req-{}-{}",
sanitize_rule_name_local(slug),
name_parts.join("-")
);
if rules.iter().any(|(n, _)| n == &rule_name) {
return rule_name;
}
if sorted.len() == 1 {
rules.push((rule_name.clone(), sorted[0].clone()));
return rule_name;
}
rules.push((rule_name.clone(), String::new()));
let mut alts: Vec<String> = Vec::new();
for (i, block_name) in sorted.iter().enumerate() {
let remaining: Vec<String> = sorted
.iter()
.enumerate()
.filter(|(j, _)| *j != i)
.map(|(_, s)| s.clone())
.collect();
let rest = build_qwen35_required_permutation(slug, &remaining, rules);
alts.push(format!("{} {}", block_name, rest));
}
let body = alts.join(" | ");
for (n, b) in rules.iter_mut() {
if n == &rule_name {
*b = body;
break;
}
}
rule_name
}
fn qwen35_value_rule(
fn_name: &str,
param_name: &str,
schema: &serde_json::Value,
rules: &mut Vec<(String, String)>,
rule_counter: &mut u32,
) -> Result<String, EmitterError> {
let obj = match schema.as_object() {
Some(o) => o,
None => return Ok(QWEN35_TOP_ANY_VAL.to_string()),
};
if let Some(serde_json::Value::Array(values)) = obj.get("enum") {
let alts: Vec<String> = values
.iter()
.filter_map(|v| v.as_str())
.map(|s| gbnf_literal(s))
.collect();
if !alts.is_empty() {
return Ok(format!("( {} )", alts.join(" | ")));
}
}
let schema_type = obj.get("type").and_then(|t| t.as_str()).unwrap_or("");
match schema_type {
"string" => {
match compile_pattern(
fn_name,
&format!("/{}", param_name),
obj.get("pattern"),
crate::serve::api::grammar::regex_gbnf::Surface::QwenRawString,
)? {
Some(body) => Ok(body),
None => Ok("qwen35-str-val".to_string()),
}
}
"integer" => Ok("qwen35-int-val".to_string()),
"number" => Ok("qwen35-num-val".to_string()),
"boolean" => Ok("qwen35-bool-val".to_string()),
"null" => Ok("qwen35-null-val".to_string()),
"array" | "object" => qwen35_nested_value_rule(
fn_name,
&format!("/{}", param_name),
schema,
rules,
rule_counter,
1,
),
_ => Ok(QWEN35_TOP_ANY_VAL.to_string()),
}
}
const QWEN35_TOP_ANY_VAL: &str = "( qwen35-any-val | qwen35-json-obj | qwen35-json-arr )";
fn compile_pattern(
fn_name: &str,
path: &str,
pattern: Option<&serde_json::Value>,
surface: crate::serve::api::grammar::regex_gbnf::Surface,
) -> Result<Option<String>, EmitterError> {
let Some(pat) = pattern.and_then(|p| p.as_str()) else {
return Ok(None);
};
match crate::serve::api::grammar::regex_gbnf::regex_to_gbnf_body(pat, surface) {
Ok(body) => Ok(Some(body)),
Err(e) => Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: format!("pattern {:?}: {}", pat, e.0),
}),
}
}
fn qwen35_nested_value_rule(
fn_name: &str,
path: &str,
schema: &serde_json::Value,
rules: &mut Vec<(String, String)>,
rule_counter: &mut u32,
depth: usize,
) -> Result<String, EmitterError> {
if depth > MAX_NESTED_DEPTH {
return Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: format!("nesting depth > {}", MAX_NESTED_DEPTH),
});
}
let obj = match schema.as_object() {
Some(o) => o,
None => return Ok("qwen35-json-val".to_string()),
};
for feat in [
"allOf",
"$ref",
"$defs",
"not",
"if",
"then",
"else",
"dependentSchemas",
"patternProperties",
"propertyNames",
"contains",
] {
if obj.contains_key(feat) {
return Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: feat.to_string(),
});
}
}
for comb in ["anyOf", "oneOf"] {
if let Some(serde_json::Value::Array(subs)) = obj.get(comb) {
if subs.is_empty() {
return Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: format!("empty {}", comb),
});
}
let mut alts: Vec<String> = Vec::with_capacity(subs.len());
for (i, s) in subs.iter().enumerate() {
alts.push(qwen35_nested_value_rule(
fn_name,
&format!("{}/{}/{}", path, comb, i),
s,
rules,
rule_counter,
depth + 1,
)?);
}
return Ok(format!("( {} )", alts.join(" | ")));
}
}
if let Some(serde_json::Value::Array(values)) = obj.get("enum") {
if values.is_empty() {
return Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: "empty enum".to_string(),
});
}
let mut alts: Vec<String> = Vec::with_capacity(values.len());
for v in values {
match v {
serde_json::Value::String(_)
| serde_json::Value::Number(_)
| serde_json::Value::Bool(_)
| serde_json::Value::Null => {
let text = serde_json::to_string(v).map_err(|e| {
EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: format!("enum serialize: {}", e),
}
})?;
alts.push(gbnf_literal(&text));
}
_ => {
return Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: "container enum value".to_string(),
});
}
}
}
return Ok(format!("( {} )", alts.join(" | ")));
}
match obj.get("type") {
None => Ok("qwen35-json-val".to_string()),
Some(serde_json::Value::Array(types)) => {
let mut alts: Vec<String> = Vec::with_capacity(types.len());
for (i, t) in types.iter().enumerate() {
let Some(tstr) = t.as_str() else {
return Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: "non-string type union entry".to_string(),
});
};
let mut stub = serde_json::Map::new();
stub.insert("type".into(), serde_json::Value::String(tstr.into()));
alts.push(qwen35_nested_value_rule(
fn_name,
&format!("{}/type/{}", path, i),
&serde_json::Value::Object(stub),
rules,
rule_counter,
depth + 1,
)?);
}
Ok(format!("( {} )", alts.join(" | ")))
}
Some(serde_json::Value::String(t)) => match t.as_str() {
"string" => {
match compile_pattern(
fn_name,
path,
obj.get("pattern"),
crate::serve::api::grammar::regex_gbnf::Surface::QwenJsonString,
)? {
Some(body) => Ok(format!(
"{} {} {}",
gbnf_literal("\""),
body,
gbnf_literal("\"")
)),
None => Ok("qwen35-json-str".to_string()),
}
}
"integer" => Ok("qwen35-int-val".to_string()),
"number" => Ok("qwen35-num-val".to_string()),
"boolean" => Ok("qwen35-bool-val".to_string()),
"null" => Ok("qwen35-null-val".to_string()),
"object" => qwen35_nested_object(fn_name, path, obj, rules, rule_counter, depth),
"array" => qwen35_nested_array(fn_name, path, obj, rules, rule_counter, depth),
other => Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: format!("type '{}'", other),
}),
},
Some(_) => Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: "non-string type".to_string(),
}),
}
}
fn qwen35_nested_object(
fn_name: &str,
path: &str,
obj: &serde_json::Map<String, serde_json::Value>,
rules: &mut Vec<(String, String)>,
rule_counter: &mut u32,
depth: usize,
) -> Result<String, EmitterError> {
let properties = obj.get("properties").and_then(|p| p.as_object());
let additional_closed = matches!(
obj.get("additionalProperties"),
Some(serde_json::Value::Bool(false))
);
let props = match properties {
Some(p) if !p.is_empty() => p,
_ => {
return Ok(if additional_closed {
r#""{" "}""#.to_string()
} else {
"qwen35-json-obj".to_string()
});
}
};
let required_set: std::collections::HashSet<&str> = obj
.get("required")
.and_then(|v| v.as_array())
.map(|arr| arr.iter().filter_map(|v| v.as_str()).collect())
.unwrap_or_default();
if required_set.len() > MAX_REQUIRED_KEYS {
return Err(EmitterError::TooManyRequiredKeys {
fn_name: format!("{} (nested {})", fn_name, path),
count: required_set.len(),
});
}
if props.len() > MAX_NESTED_PROPERTIES {
return Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: format!("> {} properties", MAX_NESTED_PROPERTIES),
});
}
let mut req_kv: Vec<String> = Vec::new();
let mut opt_kv: Vec<String> = Vec::new();
let mut sorted_keys: Vec<&String> = props.keys().collect();
sorted_keys.sort();
for key in sorted_keys {
let val_body = qwen35_nested_value_rule(
fn_name,
&format!("{}/properties/{}", path, key),
&props[key],
rules,
rule_counter,
depth + 1,
)?;
*rule_counter += 1;
let kv_name = format!("q35n-{}-kv", *rule_counter);
let key_json = serde_json::to_string(key)
.unwrap_or_else(|_| format!("\"{}\"", key.replace('"', "\\\"")));
rules.push((
kv_name.clone(),
format!("{} \":\" {}", gbnf_literal(&key_json), val_body),
));
if required_set.contains(key.as_str()) {
req_kv.push(kv_name);
} else {
opt_kv.push(kv_name);
}
}
let extra_kv: Option<String> = if additional_closed {
None
} else {
Some(r#"qwen35-json-str ":" qwen35-json-val"#.to_string())
};
build_nested_obj_body("q35n", req_kv, opt_kv, extra_kv, rules, rule_counter)
.map(|inner| format!(r#""{{" {} "}}""#, inner))
}
fn qwen35_nested_array(
fn_name: &str,
path: &str,
obj: &serde_json::Map<String, serde_json::Value>,
rules: &mut Vec<(String, String)>,
rule_counter: &mut u32,
depth: usize,
) -> Result<String, EmitterError> {
match obj.get("items") {
None => Ok("qwen35-json-arr".to_string()),
Some(serde_json::Value::Object(_)) => {
let item_rule = qwen35_nested_value_rule(
fn_name,
&format!("{}/items", path),
obj.get("items").expect("items checked above"),
rules,
rule_counter,
depth + 1,
)?;
Ok(format!(r#""[" ( {0} ("," {0})* )? "]""#, item_rule))
}
Some(serde_json::Value::Array(_)) => Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: "tuple-form items".to_string(),
}),
Some(_) => Err(EmitterError::UnsupportedSchemaFeature {
fn_name: fn_name.to_string(),
param_path: path.to_string(),
feature: "non-object items".to_string(),
}),
}
}
fn build_nested_obj_body(
prefix: &str,
req_kv: Vec<String>,
opt_kv: Vec<String>,
extra_kv: Option<String>,
rules: &mut Vec<(String, String)>,
rule_counter: &mut u32,
) -> Result<String, EmitterError> {
let comma = r#"",""#;
let mut opt_items: Vec<String> = opt_kv;
if let Some(eb) = &extra_kv {
opt_items.push(format!("( {} )", eb));
}
if req_kv.is_empty() {
if opt_items.is_empty() {
return Ok(String::new());
}
*rule_counter += 1;
let item_name = format!("{}-{}-item", prefix, *rule_counter);
rules.push((item_name.clone(), format!("( {} )", opt_items.join(" | "))));
Ok(format!("( {} ( {} {} )* )?", item_name, comma, item_name))
} else {
let req_top = build_nested_kv_permutation(
&format!("{}-{}", prefix, *rule_counter),
&req_kv,
comma,
rules,
);
if opt_items.is_empty() {
Ok(req_top)
} else {
*rule_counter += 1;
let opt_name = format!("{}-{}-opt", prefix, *rule_counter);
rules.push((opt_name.clone(), format!("( {} )", opt_items.join(" | "))));
Ok(format!("{} ( {} {} )*", req_top, comma, opt_name))
}
}
}
fn build_nested_kv_permutation(
set_name: &str,
item_names: &[String],
separator_lit: &str,
rules: &mut Vec<(String, String)>,
) -> String {
let mut sorted = item_names.to_vec();
sorted.sort();
if sorted.len() == 1 {
return sorted[0].clone();
}
let rule_name = format!("{}-{}", set_name, sorted.join("-"));
if rules.iter().any(|(n, _)| n == &rule_name) {
return rule_name;
}
let mut alts: Vec<String> = Vec::new();
for (i, item) in sorted.iter().enumerate() {
let remaining: Vec<String> = sorted
.iter()
.enumerate()
.filter(|(j, _)| *j != i)
.map(|(_, s)| s.clone())
.collect();
let rest = build_nested_kv_permutation(set_name, &remaining, separator_lit, rules);
alts.push(format!("{} {} {}", item, separator_lit, rest));
}
rules.push((rule_name.clone(), alts.join(" | ")));
rule_name
}
fn sanitize_rule_name_local(raw: &str) -> String {
let mut out = String::with_capacity(raw.len());
for c in raw.chars() {
if c.is_ascii_alphanumeric() || c == '-' {
out.push(c);
} else {
out.push('-');
}
}
if out.is_empty() {
out.push('x');
}
out
}
#[allow(dead_code)]
const _COMPILE_REFERENCES: fn() -> HashMap<String, ModelRegistration> = || HashMap::new();
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn iter230_b1_prompt_seeds_reasoning_open_truth_table() {
assert!(prompt_seeds_reasoning_open(
"<|im_start|>assistant\n<think>\n",
&QWEN35
));
assert!(!prompt_seeds_reasoning_open(
"<|im_start|>assistant\n<think>\n\n</think>\n\n",
&QWEN35
));
assert!(!prompt_seeds_reasoning_open("<|turn>model\n", &GEMMA4));
assert!(!prompt_seeds_reasoning_open(
"<|turn>model\n<|channel>thought\n<channel|>",
&GEMMA4
));
let no_markers = ModelRegistration {
family: "none",
id_substrings: &["x"],
reasoning_open: None,
reasoning_close: None,
tool_open: None,
tool_close: None,
tool_preamble: None,
};
assert!(!prompt_seeds_reasoning_open("<think>\n", &no_markers));
}
#[test]
fn iter230_b1_seeded_open_close_marker_across_fragments() {
let mut sp = make_reasoning_splitter(&QWEN35, true).unwrap();
let mut reasoning = String::new();
let mut content = String::new();
for frag in ["I think", " therefore</th", "ink>I am"] {
for (slot, s) in sp.feed(frag) {
match slot {
SplitSlot::Reasoning => reasoning.push_str(&s),
SplitSlot::Content => content.push_str(&s),
}
}
}
if let Some((slot, s)) = sp.finish() {
match slot {
SplitSlot::Reasoning => reasoning.push_str(&s),
SplitSlot::Content => content.push_str(&s),
}
}
assert_eq!(reasoning, "I think therefore");
assert_eq!(content, "I am");
}
#[test]
fn iter230_b1_seeded_open_no_close_finish_routes_to_reasoning() {
let (content, reasoning) =
split_full_output_forced(&QWEN35, "endless pondering with no close", true);
assert_eq!(content, "");
assert_eq!(
reasoning.as_deref(),
Some("endless pondering with no close")
);
}
#[test]
fn iter230_b1_unseeded_byte_identical_to_legacy() {
let text = "Sure! <think>step by step</think>The answer is 42.";
let legacy = split_full_output(&QWEN35, text);
let forced_false = split_full_output_forced(&QWEN35, text, false);
assert_eq!(legacy, forced_false);
assert_eq!(legacy.0, "Sure! The answer is 42.");
assert_eq!(legacy.1.as_deref(), Some("step by step"));
}
#[test]
fn iter230_b1_redundant_open_while_seeded_is_reasoning_text() {
let (content, reasoning) =
split_full_output_forced(&QWEN35, "<think>abc</think>done", true);
assert_eq!(reasoning.as_deref(), Some("<think>abc"));
assert_eq!(content, "done");
}
#[test]
fn iter230_b2_factory_only_construction() {
let direct = format!("{}::{}", "ReasoningSplitter", "from_registration");
let modules: [(&str, &str); 4] = [
("engine.rs", include_str!("engine.rs")),
("engine_qwen35.rs", include_str!("engine_qwen35.rs")),
("engine_qwen3vl.rs", include_str!("engine_qwen3vl.rs")),
("handlers.rs", include_str!("handlers.rs")),
];
for (name, src) in modules {
assert_eq!(
src.matches(&direct).count(),
0,
"{name}: direct ReasoningSplitter construction found — \
use registry::make_reasoning_splitter(reg, forced_open) \
so the iter-230 forced-open seed is threaded"
);
}
}
#[test]
fn gemma4_matches_real_model_ids() {
assert!(GEMMA4.matches("gemma-4-26B-A4B-it-ara-abliterated-dwq"));
assert!(GEMMA4.matches("gemma-4-27b-it"));
assert!(GEMMA4.matches("GEMMA-4-test"));
}
#[test]
fn qwen35_matches_family_ids() {
assert!(QWEN35.matches("qwen3.5-27b"));
assert!(QWEN35.matches("qwen3.6-35b-a3b-abliterix"));
assert!(QWEN35.matches("Qwen35-14B-chat"));
}
#[test]
fn non_matching_model_id_returns_none() {
assert!(find_for("llama-3.2-1b").is_none());
assert!(find_for("unknown-model").is_none());
}
#[test]
fn gemma4_has_reasoning_and_tools() {
assert!(GEMMA4.has_reasoning());
assert!(GEMMA4.has_tools());
assert_eq!(GEMMA4.reasoning_open, Some("<|channel>"));
assert_eq!(GEMMA4.reasoning_close, Some("<channel|>"));
}
#[test]
fn gemma4_reasoning_markers_match_chat_template_emission() {
assert_eq!(GEMMA4.reasoning_open, Some("<|channel>"));
assert_eq!(GEMMA4.reasoning_close, Some("<channel|>"));
assert_eq!(GEMMA4.reasoning_open.unwrap(), "<|channel>");
assert_eq!(GEMMA4.reasoning_close.unwrap(), "<channel|>");
}
#[test]
fn qwen35_has_different_reasoning_markers() {
assert_ne!(GEMMA4.reasoning_open, QWEN35.reasoning_open);
assert_ne!(GEMMA4.reasoning_close, QWEN35.reasoning_close);
}
#[test]
fn register_appends_and_wins_over_builtin_on_substring_overlap() {
register(ModelRegistration {
family: "custom",
id_substrings: &["test_register_overlap"],
reasoning_open: Some("<R>"),
reasoning_close: Some("</R>"),
tool_open: None,
tool_close: None,
tool_preamble: None,
});
let found = find_for("test_register_overlap-001").expect("found");
assert_eq!(found.family, "custom");
}
fn split(reg: &ModelRegistration, s: &str) -> Vec<(SplitSlot, String)> {
let mut sp = ReasoningSplitter::from_registration(reg).unwrap();
let mut out = sp.feed(s);
if let Some(tail) = sp.finish() {
out.push(tail);
}
coalesce(&out)
}
#[test]
fn splitter_no_markers_all_content() {
let out = split(&GEMMA4, "hello world");
assert_eq!(out, vec![(SplitSlot::Content, "hello world".into())]);
}
#[test]
fn splitter_single_reasoning_span() {
let out = split(&GEMMA4, "pre <|channel>because<channel|> post");
assert_eq!(
out,
vec![
(SplitSlot::Content, "pre ".into()),
(SplitSlot::Reasoning, "because".into()),
(SplitSlot::Content, " post".into()),
]
);
}
#[test]
fn splitter_open_without_close_reasoning_continues_to_end() {
let out = split(&GEMMA4, "pre <|channel>still thinking");
assert_eq!(
out,
vec![
(SplitSlot::Content, "pre ".into()),
(SplitSlot::Reasoning, "still thinking".into()),
]
);
}
#[test]
fn splitter_marker_spans_fragment_boundary() {
let mut sp = ReasoningSplitter::from_registration(&GEMMA4).unwrap();
let a = sp.feed("before <|chan");
let b = sp.feed("nel>reasoning<channel|>after");
let c = sp.finish();
let mut all: Vec<(SplitSlot, String)> = Vec::new();
all.extend(a);
all.extend(b);
if let Some(t) = c {
all.push(t);
}
let joined = coalesce(&all);
assert_eq!(
joined,
vec![
(SplitSlot::Content, "before ".into()),
(SplitSlot::Reasoning, "reasoning".into()),
(SplitSlot::Content, "after".into()),
]
);
}
#[test]
fn splitter_multiple_reasoning_spans() {
let out = split(&GEMMA4, "a<|channel>b<channel|>c<|channel>d<channel|>e");
let joined = coalesce(&out);
assert_eq!(
joined,
vec![
(SplitSlot::Content, "a".into()),
(SplitSlot::Reasoning, "b".into()),
(SplitSlot::Content, "c".into()),
(SplitSlot::Reasoning, "d".into()),
(SplitSlot::Content, "e".into()),
]
);
}
#[test]
fn splitter_gemma4_realistic_thought_channel_emission() {
let out = split(
&GEMMA4,
"<|channel>thought\nlet me compute 73 * 47<channel|>The answer is 3431",
);
let joined = coalesce(&out);
assert_eq!(
joined,
vec![
(
SplitSlot::Reasoning,
"thought\nlet me compute 73 * 47".into()
),
(SplitSlot::Content, "The answer is 3431".into()),
]
);
}
#[test]
fn splitter_qwen_markers_distinct_from_gemma() {
let out = split(&QWEN35, "hi <think>pondering</think> there");
let joined = coalesce(&out);
assert_eq!(
joined,
vec![
(SplitSlot::Content, "hi ".into()),
(SplitSlot::Reasoning, "pondering".into()),
(SplitSlot::Content, " there".into()),
]
);
}
#[test]
fn splitter_does_not_split_utf8_at_fragment_end() {
let mut sp = ReasoningSplitter::from_registration(&GEMMA4).unwrap();
let _ = sp.feed("hello α");
let _ = sp.feed("β world");
let _ = sp.finish();
}
#[test]
fn split_full_output_helper_returns_both_slots() {
let (content, reasoning) = split_full_output(
&GEMMA4,
"a <|channel>r1<channel|> b <|channel>r2<channel|> c",
);
assert_eq!(content, "a b c");
assert_eq!(reasoning.as_deref(), Some("r1r2"));
}
#[test]
fn split_full_output_no_markers_returns_none_reasoning() {
let (content, reasoning) = split_full_output(&GEMMA4, "just plain content");
assert_eq!(content, "just plain content");
assert_eq!(reasoning, None);
}
#[test]
fn split_full_output_preserves_tool_call_markers_in_content() {
let raw_stream =
"<think>I should call get_weather</think>OK, calling now: \
<tool_call>\n<function=get_weather><parameter=city>Paris</parameter></function>\n</tool_call>";
let (content, reasoning) = split_full_output(&QWEN35, raw_stream);
assert_eq!(
reasoning.as_deref(),
Some("I should call get_weather"),
"iter C non-streaming contract: reasoning span must be \
extracted verbatim, markers swallowed"
);
assert!(
!content.contains("<think>") && !content.contains("</think>"),
"iter C non-streaming contract: reasoning markers must NOT \
leak into content slot; got content: {content:?}"
);
assert!(
content.contains("<tool_call>") && content.contains("</tool_call>"),
"iter C+B-2 composition: tool-call markers MUST be preserved \
in the content slot for the downstream ToolCallSplitter to \
consume; ReasoningSplitter must not touch them. got content: {content:?}"
);
assert!(
content.contains("OK, calling now:"),
"iter C non-streaming contract: post-reasoning natural-language \
preamble must be preserved verbatim; got content: {content:?}"
);
}
#[test]
fn split_full_output_pure_reasoning_returns_empty_content() {
let (content, reasoning) =
split_full_output(&QWEN35, "<think>only thinking, no answer</think>");
assert_eq!(
reasoning.as_deref(),
Some("only thinking, no answer"),
"pure-reasoning input must produce a populated reasoning slot"
);
assert_eq!(
content, "",
"pure-reasoning input (no content after </think>) must produce \
empty content slot; got: {content:?}"
);
}
#[test]
fn list_families_includes_builtins() {
let fams = list_families();
assert!(fams.iter().any(|f| f == "gemma4"));
assert!(fams.iter().any(|f| f == "qwen35"));
}
#[test]
fn gemma4_tool_call_markers_match_chat_template_emission() {
assert_eq!(GEMMA4.tool_open, Some("<|tool_call>"));
assert_eq!(GEMMA4.tool_close, Some("<tool_call|>"));
}
fn tcfeed(reg: &ModelRegistration, s: &str) -> Vec<ToolCallEvent> {
let mut sp = ToolCallSplitter::from_registration(reg).unwrap();
let mut out = sp.feed(s);
if let Some(tail) = sp.finish() {
out.push(tail);
}
out
}
fn tc_coalesce(v: &[ToolCallEvent]) -> Vec<ToolCallEvent> {
let mut out: Vec<ToolCallEvent> = Vec::new();
for ev in v {
if let (Some(last), ev) = (out.last_mut(), ev) {
match (last, ev) {
(ToolCallEvent::Content(a), ToolCallEvent::Content(b)) => {
a.push_str(b);
continue;
}
(ToolCallEvent::ToolCallText(a), ToolCallEvent::ToolCallText(b)) => {
a.push_str(b);
continue;
}
_ => {}
}
}
out.push(ev.clone());
}
out
}
#[test]
fn tool_call_splitter_no_markers_all_content() {
let out = tcfeed(&GEMMA4, "hello world");
assert_eq!(out, vec![ToolCallEvent::Content("hello world".into())]);
}
#[test]
fn tool_call_splitter_single_call_gemma4_markers() {
let out = tcfeed(&GEMMA4, "pre <|tool_call>call:f{x:1}<tool_call|> post");
let joined = tc_coalesce(&out);
assert_eq!(
joined,
vec![
ToolCallEvent::Content("pre ".into()),
ToolCallEvent::ToolCallOpen,
ToolCallEvent::ToolCallText("call:f{x:1}".into()),
ToolCallEvent::ToolCallClose,
ToolCallEvent::Content(" post".into()),
]
);
}
#[test]
fn tool_call_splitter_qwen35_markers_distinct() {
let out = tcfeed(
&QWEN35,
"pre <tool_call>\n<function=f><parameter=x>\n1\n</parameter></function>\n</tool_call> post",
);
let joined = tc_coalesce(&out);
assert_eq!(joined.len(), 5, "got {joined:?}");
match (&joined[0], &joined[1], &joined[3], &joined[4]) {
(
ToolCallEvent::Content(a),
ToolCallEvent::ToolCallOpen,
ToolCallEvent::ToolCallClose,
ToolCallEvent::Content(b),
) => {
assert_eq!(a, "pre ");
assert_eq!(b, " post");
}
other => panic!("unexpected event sequence: {other:?}"),
}
}
#[test]
fn tool_call_splitter_marker_spans_fragment_boundary() {
let mut sp = ToolCallSplitter::from_registration(&GEMMA4).unwrap();
let a = sp.feed("before <|tool");
let b = sp.feed("_call>call:f{a:1}<tool_call");
let c = sp.feed("|>after");
let d = sp.finish();
let mut all: Vec<ToolCallEvent> = Vec::new();
all.extend(a);
all.extend(b);
all.extend(c);
if let Some(t) = d {
all.push(t);
}
let joined = tc_coalesce(&all);
assert_eq!(
joined,
vec![
ToolCallEvent::Content("before ".into()),
ToolCallEvent::ToolCallOpen,
ToolCallEvent::ToolCallText("call:f{a:1}".into()),
ToolCallEvent::ToolCallClose,
ToolCallEvent::Content("after".into()),
]
);
}
#[test]
fn iter219c_in_call_tool_response_aborts_with_synthetic_close() {
let out = tcfeed(
&GEMMA4,
"<|tool_call>call:get_current<|tool_response>call:get_current_weather{x:1}<tool_call|>",
);
let joined = tc_coalesce(&out);
assert!(
matches!(joined.as_slice(),
[
ToolCallEvent::ToolCallOpen,
ToolCallEvent::ToolCallText(t1),
ToolCallEvent::ToolCallClose,
..
] if t1 == "call:get_current"
),
"iter-219c: splitter MUST abort on in-call <|tool_response> with \
ToolCallText(partial body) + synthetic ToolCallClose. Got: {joined:#?}"
);
for ev in &joined {
match ev {
ToolCallEvent::ToolCallText(t) | ToolCallEvent::Content(t) => {
assert!(
!t.contains("<|tool_response>") && !t.contains("<tool_response|>"),
"iter-219c: resync marker leaked into text event: {ev:#?}"
);
}
_ => {}
}
}
}
#[test]
fn iter219c_in_call_channel_marker_aborts() {
let out = tcfeed(
&GEMMA4,
"<|tool_call>call:f<|channel>thought<channel|>{x:1}<tool_call|>",
);
let joined = tc_coalesce(&out);
let saw_clean_partial = joined.windows(3).any(|w| {
matches!(w,
[
ToolCallEvent::ToolCallOpen,
ToolCallEvent::ToolCallText(t),
ToolCallEvent::ToolCallClose,
] if t == "call:f"
)
});
assert!(
saw_clean_partial,
"iter-219c: <|channel> mid-call MUST trigger abort; expected \
ToolCallText(\"call:f\") + synthetic ToolCallClose; got: {joined:#?}"
);
for ev in &joined {
if let ToolCallEvent::ToolCallText(t) = ev {
assert!(
!t.contains("<|channel>"),
"iter-219c: <|channel> leaked into ToolCallText: {ev:#?}"
);
}
}
}
#[test]
fn iter219c_close_before_resync_still_wins() {
let out = tcfeed(
&GEMMA4,
"<|tool_call>call:f{x:1}<tool_call|><|tool_response>tool_result<tool_response|>",
);
let joined = tc_coalesce(&out);
assert!(
joined.len() >= 4,
"iter-219c happy path: expected ≥4 events; got {joined:#?}"
);
match (&joined[0], &joined[1], &joined[2]) {
(
ToolCallEvent::ToolCallOpen,
ToolCallEvent::ToolCallText(t),
ToolCallEvent::ToolCallClose,
) if t == "call:f{x:1}" => {}
_ => panic!("iter-219c: close_marker MUST win over later resync; got: {joined:#?}"),
}
}
#[test]
fn tool_call_splitter_open_without_close_finishes_in_call() {
let out = tcfeed(&GEMMA4, "<|tool_call>call:f{a:1");
let joined = tc_coalesce(&out);
assert_eq!(
joined,
vec![
ToolCallEvent::ToolCallOpen,
ToolCallEvent::ToolCallText("call:f{a:1".into()),
]
);
}
#[test]
fn tool_call_splitter_no_registration_returns_none() {
let none_reg = ModelRegistration {
family: "no-tools",
id_substrings: &["test"],
reasoning_open: None,
reasoning_close: None,
tool_open: None,
tool_close: None,
tool_preamble: None,
};
assert!(ToolCallSplitter::from_registration(&none_reg).is_none());
}
#[test]
fn parse_gemma4_simple_string_arg() {
let parsed = parse_tool_call_body(
&GEMMA4,
"call:get_current_weather{location:<|\"|>Paris<|\"|>}",
)
.expect("parse");
assert_eq!(parsed.name, "get_current_weather");
let v: serde_json::Value = serde_json::from_str(&parsed.arguments_json).expect("arg JSON");
assert_eq!(v["location"], "Paris");
}
#[test]
fn parse_gemma4_multi_arg_string_and_enum() {
let parsed = parse_tool_call_body(
&GEMMA4,
"call:f{location:<|\"|>San Francisco<|\"|>,unit:<|\"|>celsius<|\"|>}",
)
.expect("parse");
assert_eq!(parsed.name, "f");
let v: serde_json::Value = serde_json::from_str(&parsed.arguments_json).expect("arg JSON");
assert_eq!(v["location"], "San Francisco");
assert_eq!(v["unit"], "celsius");
}
#[test]
fn parse_gemma4_numeric_and_bool_args() {
let parsed = parse_tool_call_body(&GEMMA4, "call:f{count:42,enabled:true,ratio:1.5}")
.expect("parse");
let v: serde_json::Value = serde_json::from_str(&parsed.arguments_json).expect("arg JSON");
assert_eq!(v["count"], 42);
assert_eq!(v["enabled"], true);
assert_eq!(v["ratio"], 1.5);
}
#[test]
fn parse_gemma4_string_with_comma_inside_quotes() {
let parsed = parse_tool_call_body(
&GEMMA4,
"call:f{addr:<|\"|>1, Main St<|\"|>,city:<|\"|>NYC<|\"|>}",
)
.expect("parse");
let v: serde_json::Value = serde_json::from_str(&parsed.arguments_json).expect("arg JSON");
assert_eq!(v["addr"], "1, Main St");
assert_eq!(v["city"], "NYC");
}
#[test]
fn parse_gemma4_empty_args() {
let parsed = parse_tool_call_body(&GEMMA4, "call:noop{}").expect("parse");
assert_eq!(parsed.name, "noop");
assert_eq!(parsed.arguments_json, "{}");
}
#[test]
fn parse_gemma4_invalid_returns_none() {
assert!(parse_tool_call_body(&GEMMA4, "garbage{}").is_none());
assert!(parse_tool_call_body(&GEMMA4, "call:f").is_none());
assert!(parse_tool_call_body(&GEMMA4, "call:{}").is_none());
}
#[test]
fn parse_qwen35_function_with_parameters() {
let parsed = parse_tool_call_body(
&QWEN35,
"<function=get_current_weather>\n<parameter=location>\nParis\n</parameter>\n</function>",
)
.expect("parse");
assert_eq!(parsed.name, "get_current_weather");
let v: serde_json::Value = serde_json::from_str(&parsed.arguments_json).expect("arg JSON");
assert_eq!(v["location"], "Paris");
}
#[test]
fn parse_qwen35_function_with_jsonish_value() {
let parsed = parse_tool_call_body(
&QWEN35,
"<function=set>\n<parameter=count>\n42\n</parameter>\n</function>",
)
.expect("parse");
let v: serde_json::Value = serde_json::from_str(&parsed.arguments_json).expect("arg JSON");
assert_eq!(v["count"], 42);
}
#[test]
fn parse_qwen35_invalid_returns_none() {
assert!(parse_tool_call_body(&QWEN35, "garbage").is_none());
assert!(parse_tool_call_body(&QWEN35, "<function=>").is_none());
}
fn coalesce(v: &[(SplitSlot, String)]) -> Vec<(SplitSlot, String)> {
let mut out: Vec<(SplitSlot, String)> = Vec::new();
for (slot, s) in v {
if let Some(last) = out.last_mut() {
if last.0 == *slot {
last.1.push_str(s);
continue;
}
}
out.push((*slot, s.clone()));
}
out
}
fn grammar_runtime_for_gbnf(gbnf: &str) -> crate::serve::api::grammar::sampler::GrammarRuntime {
let g = crate::serve::api::grammar::parser::parse(gbnf)
.unwrap_or_else(|e| panic!("parse GBNF:\n{}\nerror: {}", gbnf, e));
let rid = g
.rule_id("root")
.unwrap_or_else(|| panic!("no root rule in GBNF:\n{}", gbnf));
crate::serve::api::grammar::sampler::GrammarRuntime::new(g, rid)
.unwrap_or_else(|| panic!("GrammarRuntime::new returned None for GBNF:\n{}", gbnf))
}
fn gemma4_runtime(
fn_name: &str,
schema_json: &str,
) -> crate::serve::api::grammar::sampler::GrammarRuntime {
let schema: serde_json::Value = serde_json::from_str(schema_json).unwrap();
let gbnf = GEMMA4
.tool_call_gbnf(fn_name, &schema, GrammarShape::SingleBody)
.unwrap_or_else(|e| panic!("tool_call_gbnf error: {}", e));
grammar_runtime_for_gbnf(&gbnf)
}
fn qwen35_runtime(
fn_name: &str,
schema_json: &str,
) -> crate::serve::api::grammar::sampler::GrammarRuntime {
let schema: serde_json::Value = serde_json::from_str(schema_json).unwrap();
let gbnf = QWEN35
.tool_call_gbnf(fn_name, &schema, GrammarShape::SingleBody)
.unwrap_or_else(|e| panic!("tool_call_gbnf error: {}", e));
grammar_runtime_for_gbnf(&gbnf)
}
fn deepseek4_runtime(
fn_name: &str,
schema_json: &str,
shape: GrammarShape,
) -> crate::serve::api::grammar::sampler::GrammarRuntime {
let schema: serde_json::Value = serde_json::from_str(schema_json).unwrap();
let gbnf = DEEPSEEK4
.tool_call_gbnf(fn_name, &schema, shape)
.unwrap_or_else(|error| panic!("tool_call_gbnf error: {error}"));
grammar_runtime_for_gbnf(&gbnf)
}
#[test]
fn deepseek4_registration_and_multi_invoke_parser_are_openai_compatible() {
let registration = find_for("DeepSeek-V4-Flash-0731").expect("DeepSeek registration");
assert_eq!(registration.family, "deepseek4");
assert_eq!(
find_for("Deepseek v4 Flash 0731 Source")
.expect("converted general.name registration")
.family,
"deepseek4"
);
let body = "\n<|DSML|invoke name=\"read_file\">\n<|DSML|parameter name=\"path\" string=\"true\">src/main.rs</|DSML|parameter>\n</|DSML|invoke>\n<|DSML|invoke name=\"run_tests\">\n<|DSML|parameter name=\"all\" string=\"false\">true</|DSML|parameter>\n</|DSML|invoke>\n";
let calls = parse_tool_call_bodies(®istration, body).expect("parse DSML block");
assert_eq!(calls.len(), 2);
assert_eq!(calls[0].name, "read_file");
assert_eq!(
serde_json::from_str::<serde_json::Value>(&calls[0].arguments_json).unwrap(),
serde_json::json!({"path": "src/main.rs"})
);
assert_eq!(calls[1].name, "run_tests");
assert_eq!(
serde_json::from_str::<serde_json::Value>(&calls[1].arguments_json).unwrap(),
serde_json::json!({"all": true})
);
}
#[test]
fn deepseek4_required_grammar_enforces_required_parameter() {
let schema = r#"{
"type": "object",
"properties": {
"path": {"type": "string"},
"line": {"type": "integer"}
},
"required": ["path"]
}"#;
let mut accepted = deepseek4_runtime(
"read_file",
schema,
GrammarShape::OneOrMoreCalls { parallel: false },
);
let valid = "<|DSML|tool_calls>\n<|DSML|invoke name=\"read_file\">\n<|DSML|parameter name=\"path\" string=\"true\">src/lib.rs</|DSML|parameter>\n</|DSML|invoke>\n</|DSML|tool_calls>";
assert!(accepted.accept_bytes(valid.as_bytes()));
assert!(accepted.is_accepted());
let mut rejected = deepseek4_runtime(
"read_file",
schema,
GrammarShape::OneOrMoreCalls { parallel: false },
);
let missing = "<|DSML|tool_calls>\n<|DSML|invoke name=\"read_file\">\n<|DSML|parameter name=\"line\" string=\"false\">7</|DSML|parameter>\n</|DSML|invoke>\n</|DSML|tool_calls>";
let alive = rejected.accept_bytes(missing.as_bytes());
assert!(!(alive && rejected.is_accepted()));
}
#[test]
fn deepseek4_parallel_grammar_uses_one_outer_block() {
let schema = r#"{"type":"object","properties":{}}"#;
let mut runtime = deepseek4_runtime(
"ping",
schema,
GrammarShape::OneOrMoreCalls { parallel: true },
);
let calls = "<|DSML|tool_calls>\n<|DSML|invoke name=\"ping\">\n\n</|DSML|invoke>\n<|DSML|invoke name=\"ping\">\n\n</|DSML|invoke>\n</|DSML|tool_calls>";
assert!(runtime.accept_bytes(calls.as_bytes()));
assert!(runtime.is_accepted());
}
#[test]
fn gemma4_tool_call_grammar_accepts_canonical_emission() {
let schema = r#"{
"type": "object",
"properties": {
"location": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
}
}"#;
let mut rt = gemma4_runtime("get_weather", schema);
let input = b"call:get_weather{location:<|\"|>SF<|\"|>,unit:<|\"|>fahrenheit<|\"|>}";
assert!(rt.accept_bytes(input), "canonical emission rejected");
assert!(rt.is_accepted(), "not accepted at end");
}
#[test]
fn gemma4_tool_call_grammar_accepts_reversed_key_order() {
let schema = r#"{
"type": "object",
"properties": {
"location": {"type": "string"},
"unit": {"type": "string"}
}
}"#;
let mut rt = gemma4_runtime("get_weather", schema);
let input = b"call:get_weather{unit:<|\"|>celsius<|\"|>,location:<|\"|>London<|\"|>}";
assert!(rt.accept_bytes(input), "reversed key order rejected");
assert!(rt.is_accepted(), "not accepted at end");
}
#[test]
fn gemma4_tool_call_grammar_accepts_numeric_arg() {
let schema = r#"{
"type": "object",
"properties": {
"count": {"type": "integer"},
"enabled": {"type": "boolean"}
}
}"#;
let mut rt = gemma4_runtime("do_thing", schema);
let input = b"call:do_thing{count:42,enabled:true}";
assert!(rt.accept_bytes(input), "numeric+boolean args rejected");
assert!(rt.is_accepted(), "not accepted");
}
#[test]
fn gemma4_tool_call_grammar_rejects_malformed_wrapper_prefix() {
let schema = r#"{
"type": "object",
"properties": {"location": {"type": "string"}}
}"#;
let mut rt = gemma4_runtime("get_weather", schema);
let input = b"call_:get_weather{location:<|\"|>SF<|\"|>}";
let ok = rt.accept_bytes(input);
assert!(
!(ok && rt.is_accepted()),
"malformed prefix accepted (should reject)"
);
}
#[test]
fn gemma4_tool_call_grammar_rejects_wrong_delimiter() {
let schema = r#"{
"type": "object",
"properties": {"location": {"type": "string"}}
}"#;
let mut rt = gemma4_runtime("get_weather", schema);
let input = b"call:get_weather(SF)";
let ok = rt.accept_bytes(input);
assert!(
!(ok && rt.is_accepted()),
"wrong delimiter accepted (should reject)"
);
}
#[test]
fn gemma4_tool_call_grammar_empty_args_accepted() {
let schema = r#"{"type": "object", "properties": {}}"#;
let mut rt = gemma4_runtime("noop", schema);
let input = b"call:noop{}";
assert!(rt.accept_bytes(input), "empty args form rejected");
assert!(rt.is_accepted(), "not accepted");
}
#[test]
fn iter219b_grammar_exhaust_after_close_marker_is_terminal() {
let schema_json = r#"{"type":"object","properties":{"x":{"type":"integer"}}}"#;
let schema_v: serde_json::Value = serde_json::from_str(schema_json).unwrap();
let gbnf = GEMMA4
.tool_call_gbnf(
"f",
&schema_v,
GrammarShape::OneOrMoreCallsBodyOnly { parallel: false },
)
.expect("tool_call_gbnf");
let mut rt = grammar_runtime_for_gbnf(&gbnf);
assert!(
rt.accept_bytes(b"call:f{x:1}<tool_call|>"),
"canonical body+close rejected"
);
assert!(
rt.is_accepted(),
"post-close: rule must be in an accepting state"
);
let mut clone_lf = rt.clone();
let alive_after_lf = clone_lf.accept_bytes(b"\n");
assert!(
!alive_after_lf,
"iter-219b: grammar must terminate on trailing `\\n` after close \
marker; HEAD's `space ::= | \" \" | \"\\n\"{{1,2}} [ \\t]{{0,20}}` \
allows up to 22 trailing whitespace bytes which prevents \
`is_dead` from flipping. Drop the trailing ` space` from \
OneOrMoreCallsBodyOnly{{false}} + SingleBody root_body."
);
assert!(
clone_lf.is_dead(),
"is_dead must flip to true after rejected continuation post-close"
);
let mut clone_lt = rt.clone();
let alive_after_lt = clone_lt.accept_bytes(b"<");
assert!(
!alive_after_lt,
"iter-219b: post-close grammar must reject `<` (start of any \
special-token leak like <|tool_response>)"
);
assert!(
clone_lt.is_dead(),
"is_dead must flip on rejected `<` after close"
);
}
#[test]
fn qwen35_tool_call_grammar_accepts_canonical_emission() {
let schema = r#"{
"type": "object",
"properties": {
"location": {"type": "string"},
"unit": {"type": "string"}
}
}"#;
let mut rt = qwen35_runtime("get_weather", schema);
let input = b"<function=get_weather>\n<parameter=location>\nParis\n</parameter>\n<parameter=unit>\ncelsius\n</parameter>\n</function>";
assert!(rt.accept_bytes(input), "canonical Qwen35 emission rejected");
assert!(rt.is_accepted(), "not accepted at end");
}
#[test]
fn qwen35_tool_call_grammar_accepts_reversed_param_order() {
let schema = r#"{
"type": "object",
"properties": {
"location": {"type": "string"},
"unit": {"type": "string"}
}
}"#;
let mut rt = qwen35_runtime("get_weather", schema);
let input = b"<function=get_weather>\n<parameter=unit>\nfahrenheit\n</parameter>\n<parameter=location>\nSF\n</parameter>\n</function>";
assert!(rt.accept_bytes(input), "reversed param order rejected");
assert!(rt.is_accepted(), "not accepted");
}
#[test]
fn qwen35_tool_call_grammar_rejects_malformed_wrapper() {
let schema = r#"{
"type": "object",
"properties": {"location": {"type": "string"}}
}"#;
let mut rt = qwen35_runtime("get_weather", schema);
let input =
b"[function=get_weather]\n[parameter=location]\nParis\n[/parameter]\n[/function]";
let ok = rt.accept_bytes(input);
assert!(
!(ok && rt.is_accepted()),
"malformed wrapper accepted (should reject)"
);
}
#[test]
fn qwen35_tool_call_grammar_accepts_empty_params() {
let schema = r#"{"type": "object", "properties": {}}"#;
let mut rt = qwen35_runtime("ping", schema);
let input = b"<function=ping></function>";
assert!(rt.accept_bytes(input), "empty params form rejected");
assert!(rt.is_accepted(), "not accepted");
}
#[test]
fn qwen35_grammar_accepted_output_is_parseable() {
let schema = r#"{
"type": "object",
"properties": {
"location": {"type": "string"},
"unit": {"type": "string"}
}
}"#;
let body = "<function=get_weather>\n<parameter=location>\nParis\n</parameter>\n<parameter=unit>\ncelsius\n</parameter>\n</function>";
let mut rt = qwen35_runtime("get_weather", schema);
assert!(rt.accept_bytes(body.as_bytes()), "grammar rejected body");
assert!(rt.is_accepted());
let parsed = parse_tool_call_body(&QWEN35, body).expect("parse_tool_call_body failed");
assert_eq!(parsed.name, "get_weather");
let v: serde_json::Value = serde_json::from_str(&parsed.arguments_json).unwrap();
assert_eq!(v["location"], "Paris");
assert_eq!(v["unit"], "celsius");
}
#[test]
fn gemma4_grammar_accepted_output_is_parseable() {
let schema = r#"{
"type": "object",
"properties": {
"location": {"type": "string"},
"unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
}
}"#;
let body = "call:get_weather{location:<|\"|>San Francisco<|\"|>,unit:<|\"|>celsius<|\"|>}";
let mut rt = gemma4_runtime("get_weather", schema);
assert!(rt.accept_bytes(body.as_bytes()), "grammar rejected body");
assert!(rt.is_accepted());
let parsed = parse_tool_call_body(&GEMMA4, body).expect("parse_tool_call_body failed");
assert_eq!(parsed.name, "get_weather");
let v: serde_json::Value = serde_json::from_str(&parsed.arguments_json).unwrap();
assert_eq!(v["location"], "San Francisco");
assert_eq!(v["unit"], "celsius");
}
#[test]
fn unknown_family_tool_call_gbnf_returns_err() {
let unknown = ModelRegistration {
family: "unknown_llama",
id_substrings: &["unknown_llama"],
reasoning_open: None,
reasoning_close: None,
tool_open: None,
tool_close: None,
tool_preamble: None,
};
let schema: serde_json::Value = serde_json::json!({});
let result = unknown.tool_call_gbnf("f", &schema, GrammarShape::SingleBody);
assert!(result.is_err(), "expected Err for unknown family");
assert!(result.unwrap_err().contains("unknown_llama"));
}
#[test]
fn b1_gemma4_required_present_accept() {
let schema = r#"{
"type": "object",
"properties": {
"city": {"type": "string"},
"units": {"type": "string"}
},
"required": ["city"]
}"#;
let mut rt = gemma4_runtime("get_weather", schema);
let input = b"call:get_weather{city:<|\"|>Paris<|\"|>,units:<|\"|>metric<|\"|>}";
assert!(
rt.accept_bytes(input),
"required key present should be accepted"
);
assert!(rt.is_accepted());
}
#[test]
fn b1_gemma4_required_missing_reject() {
let schema = r#"{
"type": "object",
"properties": {
"city": {"type": "string"},
"units": {"type": "string"}
},
"required": ["city"]
}"#;
let mut rt = gemma4_runtime("get_weather", schema);
let input = b"call:get_weather{units:<|\"|>metric<|\"|>}";
let ok = rt.accept_bytes(input);
assert!(
!(ok && rt.is_accepted()),
"missing required key must be rejected"
);
}
#[test]
fn b1_gemma4_required_permuted_accept() {
let schema = r#"{
"type": "object",
"properties": {
"a": {"type": "integer"},
"b": {"type": "integer"}
},
"required": ["a", "b"]
}"#;
let mut rt = gemma4_runtime("add", schema);
let input = b"call:add{b:2,a:1}";
assert!(
rt.accept_bytes(input),
"permuted required keys must be accepted"
);
assert!(rt.is_accepted());
}
#[test]
fn b1_gemma4_too_many_required_keys_err() {
let mut props = serde_json::Map::new();
let mut required = Vec::new();
for i in 0..9usize {
let k = format!("key{}", i);
props.insert(k.clone(), serde_json::json!({"type": "string"}));
required.push(serde_json::Value::String(k));
}
let schema = serde_json::json!({
"type": "object",
"properties": props,
"required": required
});
let result = GEMMA4.tool_call_gbnf("f", &schema, GrammarShape::SingleBody);
assert!(result.is_err(), "9 required keys must return Err");
let msg = result.unwrap_err();
assert!(
msg.contains("9"),
"error message should mention count: {}",
msg
);
assert!(
msg.contains("8"),
"error message should mention cap: {}",
msg
);
}
#[test]
fn nine_required_keys_in_gemma_tool_call_gbnf_returns_too_many_required_keys() {
let mut props = serde_json::Map::new();
let mut required = Vec::new();
for i in 0..9usize {
let k = format!("p{}", i);
props.insert(k.clone(), serde_json::json!({"type": "integer"}));
required.push(serde_json::Value::String(k));
}
let schema = serde_json::json!({
"type": "object",
"properties": props,
"required": required
});
let result = GEMMA4.tool_call_gbnf("tool9", &schema, GrammarShape::SingleBody);
assert!(
result.is_err(),
"9 required keys must return TooManyRequiredKeys"
);
let msg = result.unwrap_err();
assert!(msg.contains("9"), "error must mention count 9: {}", msg);
assert!(msg.contains("8"), "error must mention cap 8: {}", msg);
}
#[test]
fn eight_required_keys_in_gemma_tool_call_gbnf_compiles_ok() {
let mut props = serde_json::Map::new();
let mut required = Vec::new();
for i in 0..8usize {
let k = format!("p{}", i);
props.insert(k.clone(), serde_json::json!({"type": "integer"}));
required.push(serde_json::Value::String(k));
}
let schema = serde_json::json!({
"type": "object",
"properties": props,
"required": required
});
let result = GEMMA4.tool_call_gbnf("tool8", &schema, GrammarShape::SingleBody);
assert!(result.is_ok(), "8 required keys must compile without error");
}
#[test]
fn b1_qwen35_required_present_accept() {
let schema = r#"{
"type": "object",
"properties": {
"city": {"type": "string"},
"units": {"type": "string"}
},
"required": ["city"]
}"#;
let mut rt = qwen35_runtime("get_weather", schema);
let input = b"<function=get_weather>\n<parameter=city>\nParis\n</parameter>\n<parameter=units>\nmetric\n</parameter>\n</function>";
assert!(
rt.accept_bytes(input),
"required key present should be accepted"
);
assert!(rt.is_accepted());
}
#[test]
fn b1_qwen35_required_missing_reject() {
let schema = r#"{
"type": "object",
"properties": {
"city": {"type": "string"},
"units": {"type": "string"}
},
"required": ["city"]
}"#;
let mut rt = qwen35_runtime("get_weather", schema);
let input = b"<function=get_weather>\n<parameter=units>\nmetric\n</parameter>\n</function>";
let ok = rt.accept_bytes(input);
assert!(
!(ok && rt.is_accepted()),
"missing required key must be rejected"
);
}
#[test]
fn b1_qwen35_required_permuted_accept() {
let schema = r#"{
"type": "object",
"properties": {
"a": {"type": "integer"},
"b": {"type": "integer"}
},
"required": ["a", "b"]
}"#;
let mut rt = qwen35_runtime("add", schema);
let input = b"<function=add>\n<parameter=b>\n2\n</parameter>\n<parameter=a>\n1\n</parameter>\n</function>";
assert!(
rt.accept_bytes(input),
"permuted required keys must be accepted"
);
assert!(rt.is_accepted());
}
#[test]
fn b1_qwen35_too_many_required_keys_err() {
let mut props = serde_json::Map::new();
let mut required = Vec::new();
for i in 0..9usize {
let k = format!("key{}", i);
props.insert(k.clone(), serde_json::json!({"type": "string"}));
required.push(serde_json::Value::String(k));
}
let schema = serde_json::json!({
"type": "object",
"properties": props,
"required": required
});
let result = QWEN35.tool_call_gbnf("f", &schema, GrammarShape::SingleBody);
assert!(result.is_err(), "9 required keys must return Err");
let msg = result.unwrap_err();
assert!(
msg.contains("9"),
"error message should mention count: {}",
msg
);
assert!(
msg.contains("8"),
"error message should mention cap: {}",
msg
);
}
#[test]
fn nine_required_keys_in_qwen35_tool_call_gbnf_returns_too_many_required_keys() {
let mut props = serde_json::Map::new();
let mut required = Vec::new();
for i in 0..9usize {
let k = format!("q{}", i);
props.insert(k.clone(), serde_json::json!({"type": "string"}));
required.push(serde_json::Value::String(k));
}
let schema = serde_json::json!({
"type": "object",
"properties": props,
"required": required
});
let result = QWEN35.tool_call_gbnf("qtool9", &schema, GrammarShape::SingleBody);
assert!(
result.is_err(),
"9 required keys must return TooManyRequiredKeys"
);
let msg = result.unwrap_err();
assert!(msg.contains("9"), "error must mention count 9: {}", msg);
assert!(msg.contains("8"), "error must mention cap 8: {}", msg);
}
#[test]
fn eight_required_keys_in_qwen35_tool_call_gbnf_compiles_ok() {
let mut props = serde_json::Map::new();
let mut required = Vec::new();
for i in 0..8usize {
let k = format!("q{}", i);
props.insert(k.clone(), serde_json::json!({"type": "string"}));
required.push(serde_json::Value::String(k));
}
let schema = serde_json::json!({
"type": "object",
"properties": props,
"required": required
});
let result = QWEN35.tool_call_gbnf("qtool8", &schema, GrammarShape::SingleBody);
assert!(result.is_ok(), "8 required keys must compile without error");
}
#[test]
fn iter231a_gemma4_array_param_compiles_and_accepts_nested_value() {
let schema = r#"{
"type": "object",
"properties": {
"tags": {"type": "array"},
"name": {"type": "string"}
}
}"#;
let mut rt = gemma4_runtime("tag_item", schema);
let input = b"call:tag_item{name:<|\"|>x<|\"|>,tags:[<|\"|>a<|\"|>,<|\"|>b<|\"|>,1,true,null,[2,{k:<|\"|>v<|\"|>}]]}";
assert!(rt.accept_bytes(input), "nested array value rejected");
assert!(rt.is_accepted(), "not accepted at end");
}
#[test]
fn iter231a_gemma4_object_param_compiles_and_accepts_nested_value() {
let schema = r#"{
"type": "object",
"properties": {
"config": {"type": "object"}
}
}"#;
let mut rt = gemma4_runtime("configure", schema);
let input = b"call:configure{config:{model:<|\"|>sonnet<|\"|>,retries:3,nested:{on:true,ids:[1,2]}}}";
assert!(rt.accept_bytes(input), "nested object value rejected");
assert!(rt.is_accepted(), "not accepted at end");
}
#[test]
fn iter231a_qwen35_array_param_compiles_and_accepts_nested_value() {
let schema = r#"{
"type": "object",
"properties": {
"tags": {"type": "array"}
}
}"#;
let mut rt = qwen35_runtime("tag_item", schema);
let input = b"<function=tag_item>\n<parameter=tags>\n[\"a\",\"b\",1,true,null,[2,{\"k\":\"v\"}]]\n</parameter>\n</function>";
assert!(rt.accept_bytes(input), "nested JSON array rejected");
assert!(rt.is_accepted(), "not accepted at end");
}
#[test]
fn iter231a_qwen35_object_param_compiles_and_accepts_nested_value() {
let schema = r#"{
"type": "object",
"properties": {
"metadata": {"type": "object"}
}
}"#;
let mut rt = qwen35_runtime("set_meta", schema);
let input = b"<function=set_meta>\n<parameter=metadata>\n{\"model\":\"sonnet\",\"retries\":3,\"nested\":{\"on\":true,\"ids\":[1,2]}}\n</parameter>\n</function>";
assert!(rt.accept_bytes(input), "nested JSON object rejected");
assert!(rt.is_accepted(), "not accepted at end");
}
#[test]
fn iter231a_qwen35_mcp_tool_with_freeform_object_param_compiles_and_runs() {
let schema = r#"{
"type": "object",
"properties": {
"agentType": {"type": "string"},
"config": {"type": "object"},
"memoryDimension": {"type": "integer"},
"model": {"type": "string", "enum": ["haiku", "sonnet", "opus"]},
"task": {"type": "string"}
}
}"#;
let mut rt = qwen35_runtime("agent_spawn", schema);
let input = b"<function=agent_spawn>\n<parameter=agentType>\ncoder\n</parameter>\n<parameter=config>\n{\"maxTurns\":50,\"env\":{\"KEY\":\"value\"},\"nested\":[1,{\"x\":true}]}\n</parameter>\n<parameter=memoryDimension>\n384\n</parameter>\n<parameter=model>\nsonnet\n</parameter>\n<parameter=task>\nimplement feature\n</parameter>\n</function>";
assert!(rt.accept_bytes(input), "MCP-shaped call rejected");
assert!(rt.is_accepted(), "not accepted at end");
}
#[test]
fn iter231a_qwen35_object_param_rejects_angle_bracket_in_json_string() {
let schema = r#"{
"type": "object",
"properties": {
"config": {"type": "object"}
}
}"#;
let mut rt = qwen35_runtime("configure", schema);
let input = b"<function=configure>\n<parameter=config>\n{\"a\":\"<bad\"}\n</parameter>\n</function>";
let ok = rt.accept_bytes(input);
assert!(
!(ok && rt.is_accepted()),
"JSON string containing `<` accepted (must reject — close-tag safety)"
);
}
#[test]
fn iter231a_scalar_params_unaffected() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"name": {"type": "string"},
"count": {"type": "integer"},
"enabled": {"type": "boolean"}
}
});
assert!(
GEMMA4
.tool_call_gbnf("f", &schema, GrammarShape::SingleBody)
.is_ok(),
"scalars must compile"
);
assert!(
QWEN35
.tool_call_gbnf("f", &schema, GrammarShape::SingleBody)
.is_ok(),
"scalars must compile"
);
}
#[test]
fn iter231b_qwen35_nested_object_required_keys_any_order() {
let schema = r#"{
"type": "object",
"properties": {
"server": {
"type": "object",
"properties": {
"host": {"type": "string"},
"port": {"type": "integer"},
"tls": {"type": "boolean"}
},
"required": ["host", "port"]
}
}
}"#;
let mut rt = qwen35_runtime("connect", schema);
let input = b"<function=connect>\n<parameter=server>\n{\"host\":\"example.com\",\"port\":443}\n</parameter>\n</function>";
assert!(rt.accept_bytes(input), "declared-order rejected");
assert!(rt.is_accepted());
let mut rt2 = qwen35_runtime("connect", schema);
let input2 = b"<function=connect>\n<parameter=server>\n{\"port\":443,\"host\":\"example.com\",\"tls\":true}\n</parameter>\n</function>";
assert!(rt2.accept_bytes(input2), "reversed required order rejected");
assert!(rt2.is_accepted());
}
#[test]
fn iter231b_qwen35_nested_object_missing_required_rejected() {
let schema = r#"{
"type": "object",
"properties": {
"server": {
"type": "object",
"properties": {
"host": {"type": "string"},
"port": {"type": "integer"}
},
"required": ["host", "port"]
}
}
}"#;
let mut rt = qwen35_runtime("connect", schema);
let input = b"<function=connect>\n<parameter=server>\n{\"host\":\"example.com\"}\n</parameter>\n</function>";
let ok = rt.accept_bytes(input);
assert!(
!(ok && rt.is_accepted()),
"nested object missing a required key accepted (must reject)"
);
}
#[test]
fn iter231b_qwen35_nested_object_additional_properties_gate() {
let closed = r#"{
"type": "object",
"properties": {
"cfg": {
"type": "object",
"properties": {"a": {"type": "integer"}},
"required": ["a"],
"additionalProperties": false
}
}
}"#;
let mut rt = qwen35_runtime("f", closed);
let input =
b"<function=f>\n<parameter=cfg>\n{\"a\":1,\"zzz\":2}\n</parameter>\n</function>";
let ok = rt.accept_bytes(input);
assert!(
!(ok && rt.is_accepted()),
"additionalProperties:false accepted an undeclared key"
);
let open = r#"{
"type": "object",
"properties": {
"cfg": {
"type": "object",
"properties": {"a": {"type": "integer"}},
"required": ["a"]
}
}
}"#;
let mut rt2 = qwen35_runtime("f", open);
assert!(
rt2.accept_bytes(input),
"open object rejected an extra key (wildcard tail must accept)"
);
assert!(rt2.is_accepted());
}
#[test]
fn iter231b_qwen35_nested_array_typed_items_enforced() {
let schema = r#"{
"type": "object",
"properties": {
"tags": {"type": "array", "items": {"type": "string"}}
}
}"#;
let mut rt = qwen35_runtime("tag", schema);
let good = b"<function=tag>\n<parameter=tags>\n[\"a\",\"b\"]\n</parameter>\n</function>";
assert!(rt.accept_bytes(good), "string items rejected");
assert!(rt.is_accepted());
let mut rt2 = qwen35_runtime("tag", schema);
let bad = b"<function=tag>\n<parameter=tags>\n[1,2]\n</parameter>\n</function>";
let ok = rt2.accept_bytes(bad);
assert!(
!(ok && rt2.is_accepted()),
"array with items:string accepted integer items (must reject)"
);
}
#[test]
fn iter231b_qwen35_nested_enum_and_anyof() {
let schema = r#"{
"type": "object",
"properties": {
"cfg": {
"type": "object",
"properties": {
"mode": {"enum": ["fast", "safe"]},
"limit": {"anyOf": [{"type": "integer"}, {"type": "null"}]}
},
"required": ["mode"],
"additionalProperties": false
}
}
}"#;
let mut rt = qwen35_runtime("f", schema);
let good = b"<function=f>\n<parameter=cfg>\n{\"mode\":\"fast\",\"limit\":3}\n</parameter>\n</function>";
assert!(rt.accept_bytes(good), "enum+anyOf valid value rejected");
assert!(rt.is_accepted());
let mut rt2 = qwen35_runtime("f", schema);
let bad = b"<function=f>\n<parameter=cfg>\n{\"mode\":\"warp\"}\n</parameter>\n</function>";
let ok = rt2.accept_bytes(bad);
assert!(!(ok && rt2.is_accepted()), "undeclared enum value accepted");
let mut rt3 = qwen35_runtime("f", schema);
let bad2 = b"<function=f>\n<parameter=cfg>\n{\"mode\":\"safe\",\"limit\":3.5}\n</parameter>\n</function>";
let ok2 = rt3.accept_bytes(bad2);
assert!(
!(ok2 && rt3.is_accepted()),
"anyOf-mismatched value accepted on closed object"
);
}
#[test]
fn iter231b_qwen35_open_object_wildcard_tail_documented() {
let schema = r#"{
"type": "object",
"properties": {
"cfg": {
"type": "object",
"properties": {
"mode": {"enum": ["fast", "safe"]},
"limit": {"type": "integer"}
},
"required": ["mode"]
}
}
}"#;
let mut rt = qwen35_runtime("f", schema);
let input = b"<function=f>\n<parameter=cfg>\n{\"mode\":\"safe\",\"limit\":3.5}\n</parameter>\n</function>";
assert!(
rt.accept_bytes(input) && rt.is_accepted(),
"open-object wildcard tail must accept (documented CFG limitation)"
);
let mut rt2 = qwen35_runtime("f", schema);
let bad = b"<function=f>\n<parameter=cfg>\n{\"mode\":\"warp\",\"limit\":3}\n</parameter>\n</function>";
assert!(
!(rt2.accept_bytes(bad) && rt2.is_accepted()),
"required-key enum mismatch must reject even on open objects"
);
}
#[test]
fn iter231b_qwen35_unsupported_nested_feature_errors() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"cfg": {"type": "object", "properties": {"x": {"$ref": "#/$defs/t"}}}
}
});
let err = QWEN35
.tool_call_gbnf("f", &schema, GrammarShape::SingleBody)
.expect_err("$ref must error");
let msg = err.to_string();
assert!(msg.contains("$ref"), "error must name the feature: {}", msg);
assert!(
msg.contains("/cfg/properties/x"),
"error must carry the dot-path: {}",
msg
);
}
#[test]
fn iter231b_qwen35_nested_nine_required_keys_errors() {
let props: serde_json::Map<String, serde_json::Value> = (0..9)
.map(|i| (format!("k{}", i), serde_json::json!({"type": "integer"})))
.collect();
let schema = serde_json::json!({
"type": "object",
"properties": {
"cfg": {
"type": "object",
"properties": props,
"required": ["k0","k1","k2","k3","k4","k5","k6","k7","k8"]
}
}
});
let err = QWEN35
.tool_call_gbnf("f", &schema, GrammarShape::SingleBody)
.expect_err("9 nested required keys must error");
assert!(
err.contains("9 required parameters"),
"error must carry the required-key count: {}",
err
);
assert!(
err.contains("/cfg"),
"error must carry the nested path: {}",
err
);
}
#[test]
fn iter231b_gemma4_nested_object_required_keys_any_order() {
let schema = r#"{
"type": "object",
"properties": {
"server": {
"type": "object",
"properties": {
"host": {"type": "string"},
"port": {"type": "integer"}
},
"required": ["host", "port"]
}
}
}"#;
let mut rt = gemma4_runtime("connect", schema);
let input = b"call:connect{server:{port:443,host:<|\"|>example.com<|\"|>}}";
assert!(rt.accept_bytes(input), "reversed required order rejected");
assert!(rt.is_accepted());
}
#[test]
fn iter231b_gemma4_nested_missing_required_and_typed_items_rejected() {
let schema = r#"{
"type": "object",
"properties": {
"server": {
"type": "object",
"properties": {
"host": {"type": "string"},
"port": {"type": "integer"}
},
"required": ["host", "port"]
},
"tags": {"type": "array", "items": {"type": "string"}}
}
}"#;
let mut rt = gemma4_runtime("connect", schema);
let input = b"call:connect{server:{host:<|\"|>example.com<|\"|>}}";
let ok = rt.accept_bytes(input);
assert!(
!(ok && rt.is_accepted()),
"missing nested required accepted"
);
let mut rt2 = gemma4_runtime("connect", schema);
let input2 = b"call:connect{server:{host:<|\"|>h<|\"|>,port:1},tags:[1,2]}";
let ok2 = rt2.accept_bytes(input2);
assert!(
!(ok2 && rt2.is_accepted()),
"typed array accepted wrong item type"
);
}
#[test]
fn iter231b_gemma4_parser_nested_arguments_round_trip() {
let body = "call:configure{config:{model:<|\"|>sonnet<|\"|>,retries:3,nested:{on:true,ids:[1,2]}},task:<|\"|>do it<|\"|>}";
let parsed = parse_gemma4_tool_call(body).expect("nested call must parse");
assert_eq!(parsed.name, "configure");
let args: serde_json::Value =
serde_json::from_str(&parsed.arguments_json).expect("arguments_json valid JSON");
assert_eq!(
args,
serde_json::json!({
"config": {
"model": "sonnet",
"retries": 3,
"nested": {"on": true, "ids": [1, 2]}
},
"task": "do it"
})
);
}
#[test]
fn iter231b_gemma4_parser_commas_inside_nested_values_do_not_split() {
let body = "call:f{tags:[<|\"|>a,b<|\"|>,2],note:<|\"|>x,y<|\"|>}";
let parsed = parse_gemma4_tool_call(body).expect("must parse");
let args: serde_json::Value =
serde_json::from_str(&parsed.arguments_json).expect("arguments_json valid JSON");
assert_eq!(
args,
serde_json::json!({
"tags": ["a,b", 2],
"note": "x,y"
})
);
}
#[test]
fn iter231b_qwen35_parser_nested_arguments_round_trip() {
let body = "<function=configure>\n<parameter=config>\n{\"model\":\"sonnet\",\"nested\":{\"ids\":[1,2]}}\n</parameter>\n</function>";
let parsed = parse_qwen35_tool_call(body).expect("nested call must parse");
assert_eq!(parsed.name, "configure");
let args: serde_json::Value =
serde_json::from_str(&parsed.arguments_json).expect("arguments_json valid JSON");
assert_eq!(
args,
serde_json::json!({"config": {"model": "sonnet", "nested": {"ids": [1, 2]}}})
);
}
#[test]
fn iter231b_untyped_params_accept_structured_values() {
let schema = r#"{"type": "object", "properties": {"payload": {}}}"#;
let mut rt = qwen35_runtime("f", schema);
let q_input =
b"<function=f>\n<parameter=payload>\n{\"k\":[1,2]}\n</parameter>\n</function>";
assert!(
rt.accept_bytes(q_input),
"qwen untyped structured value rejected"
);
assert!(rt.is_accepted());
let mut rt2 = gemma4_runtime("f", schema);
let g_input = b"call:f{payload:{k:[1,2]}}";
assert!(
rt2.accept_bytes(g_input),
"gemma untyped structured value rejected"
);
assert!(rt2.is_accepted());
}
#[test]
fn iter231c_qwen35_pattern_on_array_items_enforced() {
let schema = r#"{
"type": "object",
"properties": {
"argv": {
"type": "array",
"items": {"type": "string", "pattern": "^[a-z][a-z0-9-]*$"}
}
}
}"#;
let mut rt = qwen35_runtime("cli_help", schema);
let good = b"<function=cli_help>\n<parameter=argv>\n[\"ruflo\",\"claude-flow\",\"a\",\"z9-x\"]\n</parameter>\n</function>";
assert!(rt.accept_bytes(good), "conforming argv rejected");
assert!(rt.is_accepted());
let mut rt2 = qwen35_runtime("cli_help", schema);
let bad = b"<function=cli_help>\n<parameter=argv>\n[\"Bad\"]\n</parameter>\n</function>";
let ok = rt2.accept_bytes(bad);
assert!(
!(ok && rt2.is_accepted()),
"pattern-violating item accepted"
);
let mut rt3 = qwen35_runtime("cli_help", schema);
let bad2 = b"<function=cli_help>\n<parameter=argv>\n[\"-x\"]\n</parameter>\n</function>";
assert!(
!(rt3.accept_bytes(bad2) && rt3.is_accepted()),
"dash-start item accepted"
);
}
#[test]
fn iter231c_qwen35_pattern_quantifier_and_alternation() {
let schema = r#"{
"type": "object",
"properties": {
"year": {"type": "string", "pattern": "^\\d{4}$"},
"method": {"type": "string", "pattern": "^(get|post|delete)$"}
},
"required": ["year"]
}"#;
let mut rt = qwen35_runtime("f", schema);
let good = b"<function=f>\n<parameter=year>\n2026\n</parameter>\n<parameter=method>\npost\n</parameter>\n</function>";
assert!(
rt.accept_bytes(good),
"valid quantifier/alternation strings rejected"
);
assert!(rt.is_accepted());
let mut rt2 = qwen35_runtime("f", schema);
let bad = b"<function=f>\n<parameter=year>\n202\n</parameter>\n</function>";
assert!(
!(rt2.accept_bytes(bad) && rt2.is_accepted()),
"3-digit year accepted"
);
let mut rt3 = qwen35_runtime("f", schema);
let bad2 = b"<function=f>\n<parameter=year>\n2026\n</parameter>\n<parameter=method>\npatch\n</parameter>\n</function>";
assert!(
!(rt3.accept_bytes(bad2) && rt3.is_accepted()),
"non-alternation method accepted"
);
}
#[test]
fn iter231c_qwen35_unanchored_pattern_is_contains() {
let schema = r#"{
"type": "object",
"properties": {
"text": {"type": "string", "pattern": "needle"}
}
}"#;
let mut rt = qwen35_runtime("f", schema);
let input = b"<function=f>\n<parameter=text>\n\"a haystack with needle inside\"\n</parameter>\n</function>";
assert!(rt.accept_bytes(input), "contains-match rejected");
assert!(rt.is_accepted());
let mut rt2 = qwen35_runtime("f", schema);
let bad = b"<function=f>\n<parameter=text>\n\"nothing here\"\n</parameter>\n</function>";
assert!(
!(rt2.accept_bytes(bad) && rt2.is_accepted()),
"non-containing accepted"
);
}
#[test]
fn iter231c_gemma4_pattern_between_markers() {
let schema = r#"{
"type": "object",
"properties": {
"argv": {
"type": "array",
"items": {"type": "string", "pattern": "^[a-z][a-z0-9-]*$"}
}
}
}"#;
let mut rt = gemma4_runtime("cli_help", schema);
let good = b"call:cli_help{argv:[<|\"|>ruflo<|\"|>,<|\"|>claude-flow<|\"|>]}";
assert!(rt.accept_bytes(good), "conforming argv rejected (gemma)");
assert!(rt.is_accepted());
let mut rt2 = gemma4_runtime("cli_help", schema);
let bad = b"call:cli_help{argv:[<|\"|>Bad<|\"|>]}";
assert!(
!(rt2.accept_bytes(bad) && rt2.is_accepted()),
"pattern-violating item accepted (gemma)"
);
}
#[test]
fn iter231c_qwen35_toplevel_string_pattern() {
let schema = r#"{
"type": "object",
"properties": {
"subcommand": {"type": "string", "pattern": "^[a-z][a-z0-9-]*$"}
}
}"#;
let mut rt = qwen35_runtime("cli", schema);
let good = b"<function=cli>\n<parameter=subcommand>\nstatus\n</parameter>\n</function>";
assert!(
rt.accept_bytes(good),
"valid top-level pattern string rejected"
);
assert!(rt.is_accepted());
let mut rt2 = qwen35_runtime("cli", schema);
let bad = b"<function=cli>\n<parameter=subcommand>\nStatus\n</parameter>\n</function>";
assert!(
!(rt2.accept_bytes(bad) && rt2.is_accepted()),
"invalid top-level pattern string accepted"
);
}
#[test]
fn iter231c_nonregular_pattern_errors_honestly() {
let schema = serde_json::json!({
"type": "object",
"properties": {
"x": {"type": "string", "pattern": "^(a)\\1$"}
}
});
let err = QWEN35
.tool_call_gbnf("f", &schema, GrammarShape::SingleBody)
.expect_err("backreference must error");
assert!(
err.contains("backreference"),
"error names the feature: {}",
err
);
assert!(err.contains("/x"), "error carries the path: {}", err);
}
#[test]
fn b6_gemma4_str_char_accepts_backslash_sequence() {
let schema = r#"{
"type": "object",
"properties": {"path": {"type": "string"}}
}"#;
let mut rt = gemma4_runtime("read_file", schema);
let input = "call:read_file{path:<|\"|>C:\\Users\\test<|\"|>}";
assert!(
rt.accept_bytes(input.as_bytes()),
"backslash sequence in Gemma string must be accepted"
);
assert!(rt.is_accepted());
}
#[test]
fn b6_qwen35_trailing_newline_on_last_param_required() {
let schema = r#"{
"type": "object",
"properties": {
"location": {"type": "string"}
}
}"#;
let mut rt = qwen35_runtime("weather", schema);
let correct = b"<function=weather>\n<parameter=location>\nParis\n</parameter>\n</function>";
assert!(
rt.accept_bytes(correct),
"trailing \\n after </parameter> must be accepted"
);
assert!(rt.is_accepted(), "must be accepted");
}
#[test]
fn b6_qwen35_no_trailing_newline_rejected() {
let schema = r#"{
"type": "object",
"properties": {
"location": {"type": "string"}
}
}"#;
let mut rt = qwen35_runtime("weather", schema);
let wrong = b"<function=weather>\n<parameter=location>\nParis\n</parameter></function>";
let ok = rt.accept_bytes(wrong);
assert!(
!(ok && rt.is_accepted()),
"emission without trailing \\n after </parameter> must be rejected"
);
}
fn gemma4_required_runtime(
fn_name: &str,
schema_json: &str,
parallel: bool,
) -> crate::serve::api::grammar::sampler::GrammarRuntime {
let schema: serde_json::Value = serde_json::from_str(schema_json).unwrap();
let gbnf = GEMMA4
.tool_call_gbnf(fn_name, &schema, GrammarShape::OneOrMoreCalls { parallel })
.unwrap_or_else(|e| panic!("tool_call_gbnf error: {}", e));
grammar_runtime_for_gbnf(&gbnf)
}
fn qwen35_required_runtime(
fn_name: &str,
schema_json: &str,
parallel: bool,
) -> crate::serve::api::grammar::sampler::GrammarRuntime {
let schema: serde_json::Value = serde_json::from_str(schema_json).unwrap();
let gbnf = QWEN35
.tool_call_gbnf(fn_name, &schema, GrammarShape::OneOrMoreCalls { parallel })
.unwrap_or_else(|e| panic!("tool_call_gbnf error: {}", e));
grammar_runtime_for_gbnf(&gbnf)
}
#[test]
fn gemma4_required_grammar_accepts_marker_wrapped_call() {
let schema = r#"{
"type": "object",
"properties": {"location": {"type": "string"}}
}"#;
let mut rt = gemma4_required_runtime("get_weather", schema, false);
let input = b"<|tool_call>call:get_weather{location:<|\"|>SF<|\"|>}<tool_call|>";
assert!(
rt.accept_bytes(input),
"eager-grammar runtime must accept marker-wrapped call"
);
assert!(rt.is_accepted(), "not accepted at end");
}
#[test]
fn required_eager_grammar_masks_non_marker_first_token() {
use crate::serve::api::grammar::mask;
let schema = r#"{
"type": "object",
"properties": {"location": {"type": "string"}}
}"#;
let rt = gemma4_required_runtime("get_weather", schema, false);
assert!(
!rt.is_awaiting_trigger(),
"eager grammar runtime must NOT be in awaiting_trigger state"
);
let token_bytes: Vec<Vec<u8>> = vec![
b"<".to_vec(), b"<|tool_call>".to_vec(), b"hello".to_vec(), b"the".to_vec(), b" ".to_vec(), b"\n".to_vec(), b"call:".to_vec(), b"".to_vec(), ];
let mut logits = vec![0.0_f32; token_bytes.len()];
let masked = mask::mask_invalid_tokens(&rt, &token_bytes, &mut logits);
assert_eq!(masked, 5, "expected 5 masked tokens, logits = {:?}", logits);
assert!(
logits[0].is_finite(),
"token 0 (`<`) must survive — prefixes open marker"
);
assert!(
logits[1].is_finite(),
"token 1 (`<|tool_call>`) must survive — full open marker"
);
assert!(!logits[2].is_finite(), "token 2 (`hello`) must be masked");
assert!(!logits[3].is_finite(), "token 3 (`the`) must be masked");
assert!(!logits[4].is_finite(), "token 4 (` `) must be masked");
assert!(!logits[5].is_finite(), "token 5 (`\\n`) must be masked");
assert!(
!logits[6].is_finite(),
"token 6 (`call:`) must be masked — body bytes only legal AFTER open marker"
);
assert!(
logits[7].is_finite(),
"token 7 (empty bytes) is exempt from mask per mask.rs:77-80 (EOS contract)"
);
}
#[test]
fn auto_lazy_grammar_allows_preamble_content() {
use crate::serve::api::grammar::mask;
let schema = r#"{
"type": "object",
"properties": {"location": {"type": "string"}}
}"#;
let mut rt = gemma4_runtime("get_weather", schema);
rt.set_awaiting_trigger(true);
assert!(rt.is_awaiting_trigger());
let token_bytes: Vec<Vec<u8>> = vec![
b"<".to_vec(),
b"<|tool_call>".to_vec(),
b"hello".to_vec(),
b"the".to_vec(),
b" ".to_vec(),
b"\n".to_vec(),
b"call:".to_vec(),
];
let mut logits = vec![0.0_f32; token_bytes.len()];
let masked = mask::mask_invalid_tokens(&rt, &token_bytes, &mut logits);
assert_eq!(
masked, 0,
"lazy/awaiting_trigger runtime must mask zero tokens"
);
for (i, l) in logits.iter().enumerate() {
assert!(
l.is_finite(),
"token {} masked under awaiting_trigger; logits = {:?}",
i,
logits
);
}
}
#[test]
fn qwen35_required_grammar_accepts_marker_wrapped_call() {
let schema = r#"{
"type": "object",
"properties": {"location": {"type": "string"}}
}"#;
let mut rt = qwen35_required_runtime("get_weather", schema, false);
let input = b"<tool_call>\n<function=get_weather>\n<parameter=location>\nParis\n</parameter>\n</function>\n</tool_call>";
assert!(
rt.accept_bytes(input),
"eager Qwen35 grammar must accept canonical single-call emission"
);
assert!(rt.is_accepted());
}
#[test]
fn gemma4_required_grammar_rejects_second_call_when_parallel_false() {
let schema = r#"{
"type": "object",
"properties": {"location": {"type": "string"}}
}"#;
let mut rt = gemma4_required_runtime("get_weather", schema, false);
let first = b"<|tool_call>call:get_weather{location:<|\"|>SF<|\"|>}<tool_call|>";
assert!(rt.accept_bytes(first));
assert!(rt.is_accepted(), "first call must reach accepted state");
let second_open = b"<|tool_call>";
let alive = rt.accept_bytes(second_open);
assert!(
!alive || rt.is_dead(),
"second `<|tool_call>` must be rejected under parallel=false"
);
}
#[test]
fn gemma4_parallel_grammar_accepts_two_calls() {
let schema = r#"{
"type": "object",
"properties": {"location": {"type": "string"}}
}"#;
let mut rt = gemma4_required_runtime("get_weather", schema, true);
let input = b"<|tool_call>call:get_weather{location:<|\"|>SF<|\"|>}<tool_call|>\
<|tool_call>call:get_weather{location:<|\"|>NYC<|\"|>}<tool_call|>";
assert!(
rt.accept_bytes(input),
"parallel Gemma 4 grammar must accept two back-to-back calls"
);
assert!(rt.is_accepted(), "two-call sequence not in accepting state");
}
#[test]
fn qwen35_parallel_grammar_accepts_two_calls_separated_by_newline() {
let schema = r#"{
"type": "object",
"properties": {"location": {"type": "string"}}
}"#;
let mut rt = qwen35_required_runtime("get_weather", schema, true);
let input = b"<tool_call>\n<function=get_weather>\n<parameter=location>\nParis\n</parameter>\n</function>\n</tool_call>\n<tool_call>\n<function=get_weather>\n<parameter=location>\nLondon\n</parameter>\n</function>\n</tool_call>";
assert!(
rt.accept_bytes(input),
"parallel Qwen35 grammar must accept two calls with `\\n` separator"
);
assert!(rt.is_accepted(), "two-call sequence not accepted");
}
#[test]
fn qwen35_parallel_grammar_rejects_calls_without_newline_separator() {
let schema = r#"{
"type": "object",
"properties": {"location": {"type": "string"}}
}"#;
let mut rt = qwen35_required_runtime("get_weather", schema, true);
let input = b"<tool_call>\n<function=get_weather>\n<parameter=location>\nParis\n</parameter>\n</function>\n</tool_call><tool_call>\n<function=get_weather>\n<parameter=location>\nLondon\n</parameter>\n</function>\n</tool_call>";
let alive = rt.accept_bytes(input);
assert!(
!alive || !rt.is_accepted(),
"Qwen parallel grammar accepted two calls without `\\n` separator (should reject)"
);
}
#[test]
fn qwen35_required_grammar_rejects_second_call_when_parallel_false() {
let schema = r#"{
"type": "object",
"properties": {"location": {"type": "string"}}
}"#;
let mut rt = qwen35_required_runtime("get_weather", schema, false);
let first = b"<tool_call>\n<function=get_weather>\n<parameter=location>\nParis\n</parameter>\n</function>\n</tool_call>";
assert!(rt.accept_bytes(first));
assert!(rt.is_accepted(), "first call must reach accepted state");
let second = b"\n<tool_call>";
let alive = rt.accept_bytes(second);
assert!(
!alive || rt.is_dead(),
"second call open must be rejected under parallel=false"
);
}
}