mod coercion;
mod history;
mod render;
mod xml;
pub use coercion::{ToolArgShape, build_arg_shapes, is_string_only_schema};
pub use history::encode_inband_tool_history;
pub use render::{render_inband_tool_prompt, render_tool_catalog};
use crate::messages::{AssistantMessage, ToolCall};
use crate::tools::Tool;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Dialect {
Glm,
Hermes,
Kimi,
Xml,
Anthropic,
Deepseek,
Harmony,
Qwen3,
Gemini,
Gemma,
Minimax,
}
pub const FALLBACK_DIALECT: Dialect = Dialect::Xml;
impl Dialect {
pub fn from_name(name: &str) -> Option<Self> {
Some(match name.trim().to_ascii_lowercase().as_str() {
"glm" => Dialect::Glm,
"hermes" => Dialect::Hermes,
"kimi" => Dialect::Kimi,
"xml" => Dialect::Xml,
"anthropic" => Dialect::Anthropic,
"deepseek" => Dialect::Deepseek,
"harmony" => Dialect::Harmony,
"qwen3" | "qwen" => Dialect::Qwen3,
"gemini" => Dialect::Gemini,
"gemma" => Dialect::Gemma,
"minimax" => Dialect::Minimax,
_ => return None,
})
}
pub fn as_str(self) -> &'static str {
match self {
Dialect::Glm => "glm",
Dialect::Hermes => "hermes",
Dialect::Kimi => "kimi",
Dialect::Xml => "xml",
Dialect::Anthropic => "anthropic",
Dialect::Deepseek => "deepseek",
Dialect::Harmony => "harmony",
Dialect::Qwen3 => "qwen3",
Dialect::Gemini => "gemini",
Dialect::Gemma => "gemma",
Dialect::Minimax => "minimax",
}
}
pub fn preferred_for_model(model_id: &str) -> Self {
let id = model_id.to_ascii_lowercase();
if id.contains("anthropic") || id.contains("claude") {
Dialect::Anthropic
} else if id.contains("glm") {
Dialect::Glm
} else if id.contains("gemma") {
Dialect::Gemma
} else if id.contains("gemini") {
Dialect::Gemini
} else if id.contains("kimi") || id.contains("moonshot") {
Dialect::Kimi
} else if id.contains("qwen") {
Dialect::Qwen3
} else if id.contains("deepseek") {
Dialect::Deepseek
} else if id.contains("minimax") {
Dialect::Minimax
} else if id.contains("gpt-oss") || id.contains("openai") || id.contains("gpt-") {
Dialect::Harmony
} else {
FALLBACK_DIALECT
}
}
pub fn prompt(self) -> String {
xml::xml_prompt()
}
pub fn render_tool_calls(self, calls: &[ToolCall], tools: &[Tool]) -> String {
xml::render_tool_calls(calls, tools)
}
pub fn render_tool_results(self, results: &[RenderedToolResult]) -> String {
xml::render_tool_results(results)
}
pub fn render_thinking(self, text: &str) -> String {
xml::render_thinking(text)
}
pub fn parse(self, text: &str, tools: &[Tool]) -> Vec<ScanSegment> {
xml::parse(text, tools)
}
pub fn parse_assistant_message(
self,
message: &AssistantMessage,
tools: &[Tool],
) -> AssistantMessage {
xml::parse_assistant_message(message, tools)
}
}
#[derive(Debug, Clone)]
pub struct RenderedToolResult {
pub id: String,
pub name: String,
pub index: usize,
pub text: String,
pub is_error: bool,
}
#[derive(Debug, Clone, PartialEq)]
pub enum ScanSegment {
Text(String),
Thinking(String),
ToolCall(ToolCall),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn dialect_round_trips_through_str() {
for d in [
Dialect::Glm,
Dialect::Hermes,
Dialect::Kimi,
Dialect::Xml,
Dialect::Anthropic,
Dialect::Deepseek,
Dialect::Harmony,
Dialect::Qwen3,
Dialect::Gemini,
Dialect::Gemma,
Dialect::Minimax,
] {
assert_eq!(Dialect::from_name(d.as_str()), Some(d), "{d:?}");
}
}
#[test]
fn dialect_from_name_is_case_insensitive_and_trims() {
assert_eq!(Dialect::from_name(" XML "), Some(Dialect::Xml));
assert_eq!(Dialect::from_name("Qwen"), Some(Dialect::Qwen3));
assert_eq!(Dialect::from_name("nope"), None);
}
#[test]
fn preferred_dialect_matches_families() {
assert_eq!(
Dialect::preferred_for_model("claude-sonnet-4"),
Dialect::Anthropic
);
assert_eq!(
Dialect::preferred_for_model("anthropic/claude-opus"),
Dialect::Anthropic
);
assert_eq!(
Dialect::preferred_for_model("gemini-2.5-pro"),
Dialect::Gemini
);
assert_eq!(Dialect::preferred_for_model("gemma-3-27b"), Dialect::Gemma);
assert_eq!(Dialect::preferred_for_model("qwen3-coder"), Dialect::Qwen3);
assert_eq!(
Dialect::preferred_for_model("deepseek-v3"),
Dialect::Deepseek
);
assert_eq!(
Dialect::preferred_for_model("gpt-oss-120b"),
Dialect::Harmony
);
assert_eq!(Dialect::preferred_for_model("glm-4.6"), Dialect::Glm);
assert_eq!(Dialect::preferred_for_model("kimi-k2"), Dialect::Kimi);
assert_eq!(Dialect::preferred_for_model("minimax-m2"), Dialect::Minimax);
assert_eq!(
Dialect::preferred_for_model("llama-3.3-70b"),
FALLBACK_DIALECT
);
}
#[test]
fn gemma_wins_over_gemini_prefix() {
assert_eq!(Dialect::preferred_for_model("gemma-2-9b"), Dialect::Gemma);
}
}