use serde::{Deserialize, Serialize};
use std::sync::atomic::{AtomicU64, Ordering};
use crate::grammar::{GrammarEngine, GrammarError};
use metrale_grammar::CompiledGrammar;
static TOOL_CALL_COUNTER: AtomicU64 = AtomicU64::new(0);
fn next_tool_call_id() -> String {
let id = TOOL_CALL_COUNTER.fetch_add(1, Ordering::Relaxed);
format!("call_{id:016x}")
}
fn is_tool_name_or_namespace_char(c: char) -> bool {
c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.' | ':')
}
fn is_tool_name_component(s: &str) -> bool {
!s.is_empty()
&& s.chars()
.all(|c| c.is_ascii_alphanumeric() || matches!(c, '_' | '-' | '.'))
}
fn is_normalized_tool_name(name: &str) -> bool {
!name.is_empty() && !name.contains(':')
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ToolDefinition {
#[serde(rename = "type")]
pub tool_type: String,
pub function: FunctionDefinition,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct FunctionDefinition {
pub name: String,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub description: Option<String>,
#[serde(default, skip_serializing_if = "Option::is_none")]
pub parameters: Option<serde_json::Value>,
}
#[derive(Debug, Clone, Deserialize)]
#[serde(untagged)]
pub enum ToolChoice {
Mode(String),
Specific {
function: ToolChoiceFunction,
},
}
#[derive(Debug, Clone, Deserialize)]
pub struct ToolChoiceFunction {
pub name: String,
}
impl ToolChoice {
pub fn is_none(&self) -> bool {
matches!(self, Self::Mode(s) if s == "none")
}
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct IncomingToolCall {
#[serde(default)]
pub id: Option<String>,
pub function: IncomingFunction,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct IncomingFunction {
pub name: String,
pub arguments: String,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct ToolCall {
pub id: String,
#[serde(rename = "type")]
pub call_type: String,
pub function: FunctionCall,
}
#[derive(Debug, Clone, Deserialize, Serialize)]
pub struct FunctionCall {
pub name: String,
pub arguments: String,
}
#[derive(Debug, Clone, Serialize)]
pub struct ChunkToolCall {
pub index: usize,
#[serde(skip_serializing_if = "Option::is_none")]
pub id: Option<String>,
#[serde(rename = "type", skip_serializing_if = "Option::is_none")]
pub call_type: Option<String>,
pub function: ChunkFunction,
}
#[derive(Debug, Clone, Serialize)]
pub struct ChunkFunction {
#[serde(skip_serializing_if = "Option::is_none")]
pub name: Option<String>,
pub arguments: String,
}
#[derive(Copy, Clone)]
pub struct LeakMarkers {
pub orphan_open: &'static [&'static str],
pub close: &'static [&'static str],
pub envelope_open: &'static [&'static str],
pub envelope_close: &'static [&'static str],
}
impl LeakMarkers {
pub const EMPTY: Self = Self {
orphan_open: &[],
close: &[],
envelope_open: &[],
envelope_close: &[],
};
}
pub use prompt_levers::PromptLevers;
pub trait ToolCallParser: Send + Sync {
fn name(&self) -> &str;
fn system_prompt(
&self,
tools: &[ToolDefinition],
tool_choice: &ToolChoice,
levers: &PromptLevers,
) -> String;
fn format_tool_calls(&self, calls: &[IncomingToolCall]) -> String;
fn format_tool_response(&self, content: &str) -> String {
format!("<tool_response>\n{content}\n</tool_response>")
}
fn leak_markers(&self) -> LeakMarkers {
LeakMarkers::EMPTY
}
fn compile_tool_grammar(
&self,
_engine: &mut GrammarEngine,
_tools: &[ToolDefinition],
_use_triggers: bool,
) -> Option<Result<CompiledGrammar, GrammarError>> {
None
}
fn has_tool_grammar(&self) -> bool {
false
}
fn param_value_close_delim(&self) -> Option<&'static str> {
None
}
fn broken_opener_stop_strings(&self) -> &'static [&'static str] {
&[]
}
fn wants_typed_arguments(&self) -> bool {
false
}
fn promotes_bare_call_names(&self) -> bool {
false
}
fn call_policy(&self) -> CallPolicy {
CallPolicy::Repairing
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum CallPolicy {
Repairing,
FailClosed { keepalive: std::time::Duration },
}
impl std::fmt::Display for dyn ToolCallParser {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
write!(f, "{}", self.name())
}
}
#[derive(Debug, Clone, Copy)]
pub enum ToolCallFormat {
Hermes,
Qwen3Coder,
Qwen3Xml,
Gemma4,
Mistral,
MinimaxXml,
DeepseekV4,
BareJson,
PoolsideV1,
Glm47,
}
impl std::str::FromStr for ToolCallFormat {
type Err = String;
fn from_str(s: &str) -> Result<Self, Self::Err> {
match s {
"hermes" => Ok(Self::Hermes),
"qwen3_coder" => Ok(Self::Qwen3Coder),
"qwen3_xml" => Ok(Self::Qwen3Xml),
"gemma4" => Ok(Self::Gemma4),
"mistral" => Ok(Self::Mistral),
"minimax_xml" => Ok(Self::MinimaxXml),
"deepseek_v4" | "deepseek_v41" | "dsml" => Ok(Self::DeepseekV4),
"bare_json" => Ok(Self::BareJson),
"poolside_v1" => Ok(Self::PoolsideV1),
"glm47" => Ok(Self::Glm47),
other => Err(format!(
"Unknown tool call parser '{other}'. Supported: hermes, qwen3_coder, qwen3_xml, gemma4, mistral, minimax_xml, deepseek_v4, bare_json, poolside_v1, glm47",
)),
}
}
}
impl ToolCallFormat {
pub fn into_parser(self) -> Box<dyn ToolCallParser> {
match self {
Self::Hermes => Box::new(HermesParser),
Self::Qwen3Coder => Box::new(Qwen3CoderParser),
Self::Qwen3Xml => Box::new(Qwen3XmlParser),
Self::Gemma4 => Box::new(Gemma4Parser),
Self::Mistral => Box::new(MistralNativeParser),
Self::MinimaxXml => Box::new(MinimaxXmlParser),
Self::DeepseekV4 => Box::new(DeepseekV4DsmlParser),
Self::BareJson => Box::new(BareJsonParser),
Self::PoolsideV1 => Box::new(PoolsideV1Parser),
Self::Glm47 => Box::new(Glm47Parser),
}
}
pub fn has_grammar(self) -> bool {
self.into_parser().has_tool_grammar()
}
pub fn name(self) -> &'static str {
match self {
Self::Hermes => "hermes",
Self::Qwen3Coder => "qwen3_coder",
Self::Qwen3Xml => "qwen3_xml",
Self::Gemma4 => "gemma4",
Self::Mistral => "mistral",
Self::MinimaxXml => "minimax_xml",
Self::DeepseekV4 => "deepseek_v4",
Self::BareJson => "bare_json",
Self::PoolsideV1 => "poolside_v1",
Self::Glm47 => "glm47",
}
}
}
mod bare_json;
mod deepseek_v4_dsml;
mod fuzzy_match;
mod gemma4;
pub mod glm47;
mod helpers_a;
mod helpers_b;
mod hermes;
mod minimax_xml;
mod mistral;
mod parse_dispatch;
mod parse_single_a;
mod parse_single_b;
mod parse_tools_tag;
mod pipeline;
mod pipeline_helpers;
mod poolside_v1;
mod prompt_levers;
mod qwen3_coder;
mod qwen3_xml;
mod streaming;
mod streaming_emit;
mod streaming_flush;
mod streaming_glm47;
mod streaming_impl;
mod type_coerce;
pub(crate) mod validation;
pub use bare_json::*;
pub use deepseek_v4_dsml::*;
pub use gemma4::*;
pub use glm47::{Glm47Parser, Verdict, parse_glm47_answer};
use helpers_a::*;
pub(crate) use helpers_b::append_tool_choice_instruction;
use helpers_b::*;
pub use hermes::*;
pub use minimax_xml::*;
pub use mistral::*;
pub use parse_dispatch::*;
use parse_single_a::*;
use parse_single_b::*;
use parse_tools_tag::*;
pub use pipeline::*;
use pipeline_helpers::*;
pub use poolside_v1::*;
pub use qwen3_coder::*;
pub use qwen3_xml::*;
pub use streaming::*;
pub use type_coerce::coerce_all;
pub use validation::*;
#[cfg(test)]
mod tests;