#[path = "dialects/gemma3_tool_code/mod.rs"]
mod gemma3_tool_code;
#[path = "dialects/openai.rs"]
mod openai;
mod dispatch;
mod error;
mod registry;
use serde::{Deserialize, Serialize};
pub(crate) use dispatch::{
DetectScore, DialectRequest, FramedToolResult, ToolDialect, correlate_tool_results,
};
pub use error::{DialectError, DialectErrorKind};
pub(crate) use gemma3_tool_code::Gemma3ToolCodeDialect;
pub(crate) use openai::OpenAiDialect;
pub use registry::ToolDialectRegistry;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ToolDialectId {
#[serde(rename = "openai")]
OpenAi,
#[serde(rename = "gemma3_tool_code")]
Gemma3ToolCode,
}
impl std::fmt::Display for ToolDialectId {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ToolDialectId::OpenAi => f.write_str("openai"),
ToolDialectId::Gemma3ToolCode => f.write_str("gemma3_tool_code"),
}
}
}
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
#[non_exhaustive]
pub enum ToolsMode {
#[serde(rename = "native")]
Native,
#[serde(rename = "emulated")]
Emulated,
}
impl std::fmt::Display for ToolsMode {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
match self {
ToolsMode::Native => f.write_str("native"),
ToolsMode::Emulated => f.write_str("emulated"),
}
}
}
impl ToolDialectId {
#[must_use]
pub fn tools_mode(&self) -> ToolsMode {
match self {
ToolDialectId::OpenAi => ToolsMode::Native,
ToolDialectId::Gemma3ToolCode => ToolsMode::Emulated,
}
}
}
#[derive(Debug, Clone, Default)]
#[non_exhaustive]
pub struct DialectEvidence {
pub supports_tool_calls: Option<bool>,
pub supports_tool_calls_authoritative: bool,
pub chat_template: Option<String>,
pub model_id: Option<String>,
pub source: Option<String>,
}
impl DialectEvidence {
#[must_use]
pub fn new(
supports_tool_calls: Option<bool>,
chat_template: Option<String>,
model_id: Option<String>,
source: Option<String>,
) -> Self {
Self {
supports_tool_calls,
supports_tool_calls_authoritative: false,
chat_template,
model_id,
source,
}
}
#[must_use]
pub fn authoritative_tool_support(mut self, authoritative: bool) -> Self {
self.supports_tool_calls_authoritative = authoritative;
self
}
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn empty_evidence_fails_resolve() {
let registry = ToolDialectRegistry::builtin();
let evidence = DialectEvidence::default();
let error = registry
.resolve(&evidence)
.expect_err("empty evidence must fail to resolve");
assert_eq!(error.kind(), DialectErrorKind::NoMatch);
}
#[test]
fn openai_scores_when_supports_tool_calls() {
let registry = ToolDialectRegistry::builtin();
let evidence = DialectEvidence {
supports_tool_calls: Some(true),
..Default::default()
};
let id = registry.resolve(&evidence).expect("should resolve");
assert_eq!(id, ToolDialectId::OpenAi);
}
#[test]
fn openai_scores_chatml_tool_template_without_native_flag() {
let registry = ToolDialectRegistry::builtin();
let evidence = DialectEvidence {
supports_tool_calls: Some(false),
chat_template: Some(
"<|im_start|>system\n{%- if tools %}<tool_call>{%- endif %}<|im_end|>".to_string(),
),
model_id: Some("qwen3.5-9b".to_string()),
..Default::default()
};
let id = registry.resolve(&evidence).expect("should resolve");
assert_eq!(id, ToolDialectId::OpenAi);
}
#[test]
fn openai_scores_mistral_tools_template_without_native_flag() {
let registry = ToolDialectRegistry::builtin();
let evidence = DialectEvidence {
supports_tool_calls: Some(false),
chat_template: Some(
"[SYSTEM_PROMPT]x[/SYSTEM_PROMPT][AVAILABLE_TOOLS][][/AVAILABLE_TOOLS][INST][TOOL_CALLS][TOOL_RESULTS]"
.to_string(),
),
model_id: Some("mistral-small".to_string()),
..Default::default()
};
let id = registry.resolve(&evidence).expect("should resolve");
assert_eq!(id, ToolDialectId::OpenAi);
}
#[test]
fn authoritative_negative_is_never_overridden_by_template() {
let registry = ToolDialectRegistry::builtin();
let evidence = DialectEvidence {
supports_tool_calls: Some(false),
supports_tool_calls_authoritative: true,
chat_template: Some(
"<|im_start|>system\n<tool_call>{%- endif %}<|im_end|>".to_string(),
),
model_id: Some("qwen-mystery".to_string()),
..Default::default()
};
let error = registry
.resolve(&evidence)
.expect_err("authoritative negative must not select a native dialect");
assert_eq!(error.kind(), DialectErrorKind::NoMatch);
}
#[test]
fn single_marker_template_does_not_select_native() {
let registry = ToolDialectRegistry::builtin();
for template in [
"<|im_start|>system\nplease tool_call something<|im_end|>", "[AVAILABLE_TOOLS][]", "[TOOL_CALLS]", ] {
let evidence = DialectEvidence {
supports_tool_calls: Some(false),
chat_template: Some(template.to_string()),
model_id: Some("mystery".to_string()),
..Default::default()
};
assert_eq!(
registry.resolve(&evidence).map_err(|e| e.kind()),
Err(DialectErrorKind::NoMatch),
"template {template:?} must not select a dialect",
);
}
}
#[test]
fn gemma_scores_when_no_native_tools_and_template() {
let registry = ToolDialectRegistry::builtin();
let evidence = DialectEvidence {
supports_tool_calls: Some(false),
chat_template: Some("<start_of_turn>user\n".to_string()),
model_id: Some("gemma-3-27b-it".to_string()),
..Default::default()
};
let id = registry.resolve(&evidence).expect("should resolve");
assert_eq!(id, ToolDialectId::Gemma3ToolCode);
}
#[test]
fn dialect_id_serde_round_trip() {
let openai = ToolDialectId::OpenAi;
let json = serde_json::to_string(&openai).unwrap();
assert_eq!(json, "\"openai\"");
let parsed: ToolDialectId = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, openai);
let gemma = ToolDialectId::Gemma3ToolCode;
let json = serde_json::to_string(&gemma).unwrap();
assert_eq!(json, "\"gemma3_tool_code\"");
let parsed: ToolDialectId = serde_json::from_str(&json).unwrap();
assert_eq!(parsed, gemma);
}
#[test]
fn tools_mode_serde_round_trip() {
let native = ToolsMode::Native;
assert_eq!(serde_json::to_string(&native).unwrap(), "\"native\"");
let emulated = ToolsMode::Emulated;
assert_eq!(serde_json::to_string(&emulated).unwrap(), "\"emulated\"");
}
#[test]
fn dialect_id_tools_mode_mapping() {
assert_eq!(ToolDialectId::OpenAi.tools_mode(), ToolsMode::Native);
assert_eq!(
ToolDialectId::Gemma3ToolCode.tools_mode(),
ToolsMode::Emulated
);
}
}