use serde_json::{json, Map, Value};
#[derive(Debug, Clone)]
pub struct ModelDefaultsProfile {
pub name: &'static str,
pub pattern: &'static str,
pub native_function_calling: Option<bool>,
pub streaming: Option<bool>,
pub temperature: Option<f32>,
pub max_tokens: Option<usize>,
pub extra_body: Value,
}
#[derive(Debug, Clone, Default, PartialEq, Eq)]
pub struct AppliedFields {
pub native_function_calling: bool,
pub streaming: bool,
pub temperature: bool,
pub max_tokens: bool,
pub extra_body_keys: Vec<String>,
}
impl AppliedFields {
pub fn is_empty(&self) -> bool {
!self.native_function_calling
&& !self.streaming
&& !self.temperature
&& !self.max_tokens
&& self.extra_body_keys.is_empty()
}
pub fn render(&self) -> String {
let mut parts: Vec<String> = Vec::new();
if self.native_function_calling {
parts.push("native_function_calling".to_string());
}
if self.streaming {
parts.push("streaming".to_string());
}
if self.temperature {
parts.push("temperature".to_string());
}
if self.max_tokens {
parts.push("max_tokens".to_string());
}
for k in &self.extra_body_keys {
parts.push(format!("extra_body.{}", k));
}
parts.join(", ")
}
}
pub fn builtin_profiles() -> Vec<ModelDefaultsProfile> {
vec![
ModelDefaultsProfile {
name: "glm-5.2",
pattern: "*glm-5.2*",
native_function_calling: Some(true),
streaming: None,
temperature: Some(1.0),
max_tokens: Some(65536),
extra_body: json!({
"top_p": 0.95,
"chat_template_kwargs": {
"enable_thinking": true,
},
}),
},
ModelDefaultsProfile {
name: "qwen3.6",
pattern: "qwen3.6-*",
native_function_calling: Some(true),
streaming: None,
temperature: Some(0.7),
max_tokens: Some(32768),
extra_body: json!({
"top_p": 0.8,
"top_k": 20,
"min_p": 0.0,
"presence_penalty": 1.5,
"chat_template_kwargs": {
"enable_thinking": true,
"preserve_thinking": true,
},
}),
},
ModelDefaultsProfile {
name: "qwen3.5",
pattern: "qwen3.5-*",
native_function_calling: Some(true),
streaming: None,
temperature: Some(0.6),
max_tokens: Some(32768),
extra_body: json!({
"top_p": 0.95,
"top_k": 20,
"presence_penalty": 0.0,
}),
},
ModelDefaultsProfile {
name: "claude",
pattern: "claude-*",
native_function_calling: Some(true),
streaming: Some(true),
temperature: None,
max_tokens: None,
extra_body: Value::Null,
},
ModelDefaultsProfile {
name: "gpt",
pattern: "gpt-*",
native_function_calling: Some(true),
streaming: Some(true),
temperature: None,
max_tokens: None,
extra_body: Value::Null,
},
]
}
pub fn glob_matches(pattern: &str, model: &str) -> bool {
let pattern = pattern.to_ascii_lowercase();
let model = model.to_ascii_lowercase();
glob_matches_inner(pattern.as_bytes(), model.as_bytes())
}
fn glob_matches_inner(pat: &[u8], s: &[u8]) -> bool {
let (mut i, mut j) = (0usize, 0usize);
let (mut star_i, mut star_j) = (None::<usize>, 0usize);
while j < s.len() {
if i < pat.len() && (pat[i] == b'?' || pat[i] == s[j]) {
i += 1;
j += 1;
} else if i < pat.len() && pat[i] == b'*' {
star_i = Some(i);
star_j = j;
i += 1;
} else if let Some(si) = star_i {
i = si + 1;
star_j += 1;
j = star_j;
} else {
return false;
}
}
while i < pat.len() && pat[i] == b'*' {
i += 1;
}
i == pat.len()
}
pub fn match_profile(model: &str) -> Option<ModelDefaultsProfile> {
builtin_profiles()
.into_iter()
.find(|p| glob_matches(p.pattern, model))
}
pub fn apply_profile(
config: &mut crate::config::Config,
profile: &ModelDefaultsProfile,
user_explicit: &UserExplicitFields,
) -> AppliedFields {
let mut applied = AppliedFields::default();
if !user_explicit.native_function_calling {
if let Some(v) = profile.native_function_calling {
config.agent.native_function_calling = v;
applied.native_function_calling = true;
}
}
if !user_explicit.streaming {
if let Some(v) = profile.streaming {
config.agent.streaming = v;
applied.streaming = true;
}
}
if !user_explicit.temperature {
if let Some(v) = profile.temperature {
config.temperature = v;
applied.temperature = true;
}
}
if !user_explicit.max_tokens {
if let Some(v) = profile.max_tokens {
config.max_tokens = v;
applied.max_tokens = true;
}
}
if let Value::Object(profile_extra) = &profile.extra_body {
let dest = config.extra_body.get_or_insert_with(Map::new);
for (k, v) in profile_extra {
if !user_explicit.extra_body_keys.iter().any(|uk| uk == k) && !dest.contains_key(k) {
dest.insert(k.clone(), v.clone());
applied.extra_body_keys.push(k.clone());
}
}
applied.extra_body_keys.sort();
}
applied
}
#[derive(Debug, Default, Clone)]
pub struct UserExplicitFields {
pub native_function_calling: bool,
pub streaming: bool,
pub temperature: bool,
pub max_tokens: bool,
pub extra_body_keys: Vec<String>,
}
impl UserExplicitFields {
pub fn from_toml(content: &str) -> Self {
let mut out = Self::default();
let table = match toml::from_str::<toml::Value>(content) {
Ok(toml::Value::Table(t)) => t,
_ => return out,
};
if table.contains_key("temperature") {
out.temperature = true;
}
if table.contains_key("max_tokens") {
out.max_tokens = true;
}
if let Some(toml::Value::Table(agent)) = table.get("agent") {
if agent.contains_key("native_function_calling") {
out.native_function_calling = true;
}
if agent.contains_key("streaming") {
out.streaming = true;
}
}
if let Some(toml::Value::Table(extra)) = table.get("extra_body") {
out.extra_body_keys = extra.keys().cloned().collect();
}
out
}
}
#[cfg(test)]
#[path = "../../tests/unit/config/model_profiles/model_profiles_test.rs"]
mod tests;