use serde_json::Value;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
pub enum Category {
Code,
Reasoning,
Chat,
ToolCall,
Creative,
Info,
Image,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Complexity {
Quick, Moderate, Deep, }
#[derive(Debug, Clone, PartialEq)]
pub struct Classification {
pub category: Category,
pub complexity: Complexity,
pub needs_tools: bool,
pub has_media_inputs: bool,
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct MediaRequirements {
pub has_media: bool,
pub needs_vision: bool,
pub needs_audio: bool,
}
pub type Tier = u8;
pub struct ModelProfile {
pub name: &'static str,
pub strengths: &'static [Category],
pub tier: Tier,
pub tools: bool,
}
pub static MODEL_PROFILES: &[ModelProfile] = &[
ModelProfile {
name: "Qwen3-235B-A22B-Q4_K_M",
strengths: &[
Category::Code,
Category::Reasoning,
Category::Chat,
Category::Creative,
],
tier: 4,
tools: true,
},
ModelProfile {
name: "Llama-3.1-405B-Instruct-Q2_K",
strengths: &[Category::Chat, Category::Reasoning, Category::Code],
tier: 4,
tools: true,
},
ModelProfile {
name: "MiniMax-M2.5-Q4_K_M",
strengths: &[
Category::Code,
Category::Reasoning,
Category::Chat,
Category::Creative,
Category::ToolCall,
],
tier: 4,
tools: true,
},
ModelProfile {
name: "Qwen2.5-72B-Instruct-Q4_K_M",
strengths: &[Category::Chat, Category::Reasoning, Category::Code],
tier: 3,
tools: true,
},
ModelProfile {
name: "Llama-3.3-70B-Instruct-Q4_K_M",
strengths: &[Category::Chat, Category::ToolCall, Category::Code],
tier: 3,
tools: true,
},
ModelProfile {
name: "DeepSeek-R1-Distill-70B-Q4_K_M",
strengths: &[Category::Reasoning],
tier: 3,
tools: false, },
ModelProfile {
name: "Mixtral-8x22B-Instruct-Q4_K_M",
strengths: &[Category::Chat, Category::Code, Category::Reasoning],
tier: 3,
tools: true,
},
ModelProfile {
name: "Qwen3-32B-Q4_K_M",
strengths: &[Category::Reasoning, Category::Code, Category::Chat],
tier: 3,
tools: true,
},
ModelProfile {
name: "Qwen2.5-Coder-32B-Instruct-Q4_K_M",
strengths: &[Category::Code],
tier: 3,
tools: true,
},
ModelProfile {
name: "DeepSeek-R1-Distill-Qwen-32B-Q4_K_M",
strengths: &[Category::Reasoning],
tier: 3,
tools: false,
},
ModelProfile {
name: "Qwen3-30B-A3B-Q4_K_M",
strengths: &[Category::Chat, Category::Reasoning, Category::Code],
tier: 3,
tools: true,
},
ModelProfile {
name: "Qwen3-Coder-30B-A3B-Instruct-Q4_K_M",
strengths: &[Category::Code, Category::ToolCall],
tier: 3,
tools: true,
},
ModelProfile {
name: "Qwen2.5-32B-Instruct-Q4_K_M",
strengths: &[
Category::Chat,
Category::Reasoning,
Category::Code,
Category::ToolCall,
],
tier: 3,
tools: true,
},
ModelProfile {
name: "Gemma-3-27B-it-Q4_K_M",
strengths: &[Category::Reasoning, Category::Chat],
tier: 3,
tools: false, },
ModelProfile {
name: "Qwen3.5-27B-Q4_K_M",
strengths: &[Category::Code, Category::Reasoning, Category::Chat],
tier: 3,
tools: true,
},
ModelProfile {
name: "Qwen3-Coder-Next-Q4_K_M",
strengths: &[Category::Code, Category::ToolCall, Category::Reasoning],
tier: 4,
tools: true,
},
ModelProfile {
name: "Qwen3.5-9B-Q4_K_M",
strengths: &[Category::Chat, Category::Code],
tier: 2,
tools: false,
},
ModelProfile {
name: "Mistral-Small-3.1-24B-Instruct-Q4_K_M",
strengths: &[Category::Chat, Category::ToolCall],
tier: 2,
tools: true,
},
ModelProfile {
name: "Devstral-Small-2505-Q4_K_M",
strengths: &[Category::Code, Category::ToolCall],
tier: 2,
tools: true,
},
ModelProfile {
name: "GLM-4.7-Flash-Q4_K_M",
strengths: &[Category::Chat, Category::ToolCall],
tier: 2,
tools: true,
},
ModelProfile {
name: "GLM-4-32B-0414-Q4_K_M",
strengths: &[Category::Chat, Category::ToolCall, Category::Code],
tier: 2,
tools: true,
},
ModelProfile {
name: "Llama-4-Scout-Q4_K_M",
strengths: &[Category::Chat, Category::ToolCall],
tier: 2,
tools: true,
},
ModelProfile {
name: "Qwen3-14B-Q4_K_M",
strengths: &[Category::Chat, Category::Reasoning],
tier: 2,
tools: true,
},
ModelProfile {
name: "Qwen2.5-14B-Instruct-Q4_K_M",
strengths: &[Category::Chat],
tier: 2,
tools: true,
},
ModelProfile {
name: "Qwen2.5-Coder-14B-Instruct-Q4_K_M",
strengths: &[Category::Code],
tier: 2,
tools: true,
},
ModelProfile {
name: "DeepSeek-R1-Distill-Qwen-14B-Q4_K_M",
strengths: &[Category::Reasoning],
tier: 2,
tools: false,
},
ModelProfile {
name: "Gemma-3-12B-it-Q4_K_M",
strengths: &[Category::Chat, Category::Reasoning],
tier: 2,
tools: false,
},
ModelProfile {
name: "Qwen3-8B-Q4_K_M",
strengths: &[Category::Chat, Category::Code],
tier: 2,
tools: true,
},
ModelProfile {
name: "Hermes-2-Pro-Mistral-7B-Q4_K_M",
strengths: &[Category::Chat],
tier: 2,
tools: false,
},
ModelProfile {
name: "Qwen2.5-Coder-7B-Instruct-Q4_K_M",
strengths: &[Category::Code],
tier: 2,
tools: true,
},
ModelProfile {
name: "Qwen3-4B-Q4_K_M",
strengths: &[Category::Chat],
tier: 1,
tools: true,
},
ModelProfile {
name: "Qwen2.5-3B-Instruct-Q4_K_M",
strengths: &[Category::Chat],
tier: 1,
tools: true,
},
ModelProfile {
name: "Llama-3.2-3B-Instruct-Q4_K_M",
strengths: &[Category::Chat, Category::ToolCall],
tier: 1,
tools: true,
},
];
pub fn profile_for(model_name: &str) -> Option<&'static ModelProfile> {
if let Some(p) = MODEL_PROFILES.iter().find(|p| p.name == model_name) {
return Some(p);
}
let clean = strip_split_suffix(model_name);
if clean != model_name {
return MODEL_PROFILES.iter().find(|p| p.name == clean);
}
None
}
pub fn strip_split_suffix(name: &str) -> &str {
if let Some(idx) = name.rfind("-of-") {
let after = &name[idx + 4..];
if after.chars().all(|c| c.is_ascii_digit()) && !after.is_empty() {
if let Some(dash) = name[..idx].rfind('-') {
let between = &name[dash + 1..idx];
if between.chars().all(|c| c.is_ascii_digit()) && !between.is_empty() {
return &name[..dash];
}
}
}
}
name
}
pub fn strip_split_suffix_owned(name: &str) -> String {
strip_split_suffix(name).to_string()
}
pub fn classify(body: &Value) -> Classification {
let text = collect_message_text(body);
let lower = text.to_lowercase();
let media = media_requirements(body);
let has_tools_schema = body
.get("tools")
.and_then(|t| t.as_array())
.map(|a| !a.is_empty())
.unwrap_or(false);
let has_tool_blocks = body
.get("messages")
.and_then(|m| m.as_array())
.map(|msgs| {
msgs.iter().any(|msg| {
msg.get("content")
.and_then(|c| c.as_array())
.map(|blocks| {
blocks.iter().any(|b| {
matches!(
b.get("type").and_then(|t| t.as_str()),
Some("tool_use") | Some("tool_result")
)
})
})
.unwrap_or(false)
})
})
.unwrap_or(false);
let needs_tools = has_tools_schema || has_tool_blocks;
let last_user_len = last_user_message_len(body);
let code_signals = [
"```",
"def ",
"fn ",
"func ",
"class ",
"import ",
"function",
"const ",
"let ",
"var ",
"return ",
"write a program",
"write code",
"implement",
"refactor",
"debug",
"fix the bug",
"write a script",
"code review",
"pull request",
"git ",
"compile",
"syntax",
"python",
"javascript",
"typescript",
" rust ",
"golang",
"java ",
"c++",
" ruby ",
" swift ",
"kotlin",
"algorithm",
"binary search",
" sort ",
"regex",
" api ",
" http ",
" sql ",
"database",
" query ",
];
let code_score: usize = code_signals.iter().filter(|s| lower.contains(*s)).count();
let reasoning_signals = [
"prove",
"explain why",
"step by step",
"calculate",
"solve",
"derive",
"what is the probability",
"how many",
"analyze",
"compare and contrast",
"evaluate",
"mathematical",
"theorem",
"equation",
"logic",
"think carefully",
"reason about",
];
let reasoning_score: usize = reasoning_signals
.iter()
.filter(|s| lower.contains(*s))
.count();
let creative_signals = [
"write a story",
"write a poem",
"creative",
"imagine",
"fiction",
"narrative",
"compose",
"brainstorm",
"write a song",
"screenplay",
"dialogue",
];
let creative_score: usize = creative_signals
.iter()
.filter(|s| lower.contains(*s))
.count();
let info_signals = [
"what is",
"who is",
"when did",
"where is",
"how does",
"define ",
"explain ",
"summarize",
"summary",
"overview",
"tell me about",
"describe ",
"what are the",
"list the",
"difference between",
"compare ",
"history of",
];
let info_score: usize = info_signals.iter().filter(|s| lower.contains(*s)).count();
let image_signals = [
"image",
"picture",
"photo",
"draw",
"generate an image",
"visualize",
"diagram",
"screenshot",
"describe this image",
];
let image_score: usize = image_signals.iter().filter(|s| lower.contains(*s)).count();
let deep_signals = [
"architect",
"design a system",
"trade-off",
"tradeoff",
"in depth",
"comprehensive",
"thorough",
"detailed analysis",
"long-term",
"strategy",
"plan for",
"review this codebase",
"rewrite",
"from scratch",
];
let deep_score: usize = deep_signals.iter().filter(|s| lower.contains(*s)).count();
let mut system_code = false;
if let Some(messages) = body.get("messages").and_then(|m| m.as_array()) {
for msg in messages {
if msg.get("role").and_then(|r| r.as_str()) == Some("system")
&& let Some(content) = msg.get("content").and_then(|c| c.as_str())
{
let sys = content.to_lowercase();
if sys.contains("developer") || sys.contains("coding") || sys.contains("programmer")
{
system_code = true;
}
}
}
}
let category = if system_code
|| code_score >= 2
|| (code_score >= 1 && reasoning_score == 0 && creative_score == 0)
{
Category::Code
} else if reasoning_score >= 2 {
Category::Reasoning
} else if creative_score >= 1 {
Category::Creative
} else if media.needs_vision || image_score >= 1 {
Category::Image
} else if needs_tools && code_score == 0 && reasoning_score == 0 && creative_score == 0 {
Category::ToolCall
} else if info_score >= 2 && code_score == 0 {
Category::Info
} else {
Category::Chat
};
let total_messages = body
.get("messages")
.and_then(|m| m.as_array())
.map(|a| a.len())
.unwrap_or(0);
let complexity = if deep_score >= 1 || last_user_len > 500 || total_messages > 10 {
Complexity::Deep
} else if last_user_len < 60 && total_messages <= 2 && reasoning_score == 0 && deep_score == 0 {
Complexity::Quick
} else {
Complexity::Moderate
};
Classification {
category,
complexity,
needs_tools,
has_media_inputs: media.has_media,
}
}
pub fn media_requirements(body: &Value) -> MediaRequirements {
let mut requirements = MediaRequirements::default();
let Some(messages) = body.get("messages").and_then(|m| m.as_array()) else {
return requirements;
};
for msg in messages {
let Some(blocks) = msg.get("content").and_then(|c| c.as_array()) else {
continue;
};
for block in blocks {
let block_type = block
.get("type")
.and_then(|t| t.as_str())
.unwrap_or_default();
match block_type {
"image_url" | "input_image" | "image" => {
requirements.has_media = true;
requirements.needs_vision = true;
}
"audio_url" | "input_audio" | "audio" => {
requirements.has_media = true;
requirements.needs_audio = true;
}
"file" | "input_file" => {
requirements.has_media = true;
}
_ => {
if block.get("image_url").is_some() || block.get("image").is_some() {
requirements.has_media = true;
requirements.needs_vision = true;
}
if block.get("audio_url").is_some() || block.get("audio").is_some() {
requirements.has_media = true;
requirements.needs_audio = true;
}
}
}
}
}
requirements
}
fn last_user_message_len(body: &Value) -> usize {
body.get("messages")
.and_then(|m| m.as_array())
.and_then(|msgs| {
msgs.iter()
.rev()
.find(|m| m.get("role").and_then(|r| r.as_str()) == Some("user"))
})
.map(message_text)
.map(|s| s.len())
.unwrap_or(0)
}
fn collect_message_text(body: &Value) -> String {
let mut text = String::new();
if let Some(messages) = body.get("messages").and_then(|m| m.as_array()) {
for msg in messages {
let content = message_text(msg);
if !content.is_empty() {
text.push_str(&content);
text.push('\n');
}
}
}
text
}
fn message_text(msg: &Value) -> String {
if let Some(s) = msg.get("content").and_then(|c| c.as_str()) {
return s.to_string();
}
if let Some(blocks) = msg.get("content").and_then(|c| c.as_array()) {
let mut out = String::new();
for b in blocks {
if let Some(t) = b.get("text").and_then(|t| t.as_str()) {
out.push_str(t);
out.push('\n');
}
}
return out;
}
String::new()
}
pub fn pick_model_classified<'a>(
classification: &Classification,
available_models: &[(&'a str, f64)],
) -> Option<&'a str> {
if available_models.is_empty() {
return None;
}
let filtered: Vec<(&str, f64)> = if classification.needs_tools {
available_models
.iter()
.filter(|(name, _)| profile_for(name).map(|p| p.tools).unwrap_or(false))
.copied()
.collect()
} else {
available_models.to_vec()
};
let candidates = if filtered.is_empty() {
available_models
} else {
&filtered
};
let category = classification.category;
let mut scored: Vec<(&str, i32)> = candidates
.iter()
.map(|(name, tok_s)| {
let profile = profile_for(name);
let tier = profile.map(|p| p.tier).unwrap_or(1) as i32;
let has_match = profile
.map(|p| p.strengths.contains(&category))
.unwrap_or(false);
let match_bonus = if has_match { 1000 } else { 0 };
let position_bonus = profile
.map(|p| {
p.strengths
.iter()
.position(|s| *s == category)
.map(|i| match i {
0 => 20,
1 => 10,
_ => 5,
})
.unwrap_or(0)
})
.unwrap_or(0);
let tier_bonus = if classification.needs_tools {
tier * 20
} else {
match classification.complexity {
Complexity::Quick => tier * 5, Complexity::Moderate => tier * 10, Complexity::Deep => tier * 15, }
};
let speed_bonus = if classification.needs_tools {
(tok_s / 20.0).min(5.0) as i32
} else {
(tok_s / 5.0).min(20.0) as i32
};
let score = match_bonus + tier_bonus + position_bonus + speed_bonus;
(*name, score)
})
.collect();
scored.sort_by_key(|entry| std::cmp::Reverse(entry.1));
if !classification.needs_tools && scored.len() > 1 {
let best_score = scored[0].1;
let top_tier: Vec<&(&str, i32)> = scored
.iter()
.filter(|(_, s)| best_score - s <= 15)
.collect();
if top_tier.len() > 1 {
let nanos = std::time::SystemTime::now()
.duration_since(std::time::UNIX_EPOCH)
.unwrap_or_default()
.subsec_nanos() as usize;
let pick = top_tier[nanos % top_tier.len()];
return Some(pick.0);
}
}
scored.first().map(|(name, _)| *name)
}
#[cfg(test)]
pub fn pick_model_with_tools<'a>(
category: Category,
available_models: &[(&'a str, f64)],
tools_required: bool,
) -> Option<&'a str> {
pick_model_classified(
&Classification {
category,
complexity: Complexity::Moderate,
needs_tools: tools_required,
has_media_inputs: false,
},
available_models,
)
}
#[cfg(test)]
mod tests {
use super::*;
use serde_json::json;
#[test]
fn test_classify_tool_call() {
let body = json!({
"messages": [{"role": "user", "content": "Run the tests and check the output"}],
"tools": [{"type": "function", "function": {"name": "bash"}}]
});
assert_eq!(classify(&body).category, Category::ToolCall);
}
#[test]
fn test_classify_code() {
let body = json!({
"messages": [
{"role": "user", "content": "Write a Python function to implement binary search and debug any issues"}
]
});
assert_eq!(classify(&body).category, Category::Code);
}
#[test]
fn test_classify_reasoning() {
let body = json!({
"messages": [
{"role": "user", "content": "Prove that the square root of 2 is irrational. Explain step by step."}
]
});
assert_eq!(classify(&body).category, Category::Reasoning);
}
#[test]
fn test_classify_creative() {
let body = json!({
"messages": [
{"role": "user", "content": "Write a story about a robot who learns to paint"}
]
});
assert_eq!(classify(&body).category, Category::Creative);
}
#[test]
fn test_classify_chat_default() {
let body = json!({
"messages": [
{"role": "user", "content": "What's the capital of France?"}
]
});
let cl = classify(&body);
assert_eq!(cl.category, Category::Chat);
assert_eq!(cl.complexity, Complexity::Quick); assert!(!cl.needs_tools);
assert!(!cl.has_media_inputs);
}
#[test]
fn test_classify_deep_analysis() {
let body = json!({
"messages": [
{"role": "user", "content": "Design a system architecture for a distributed database with strong consistency guarantees. Provide a detailed analysis of the trade-offs between CAP theorem constraints and explain how to handle network partitions in depth."}
]
});
let cl = classify(&body);
assert_eq!(cl.complexity, Complexity::Deep);
}
#[test]
fn test_classify_code_with_tools() {
let body = json!({
"messages": [{"role": "user", "content": "Write a Python function to sort a list and debug it"}],
"tools": [{"type": "function", "function": {"name": "bash"}}]
});
let cl = classify(&body);
assert_eq!(cl.category, Category::Code);
assert!(cl.needs_tools);
}
#[test]
fn test_classify_tools_schema_always_needs_tools() {
let body = json!({
"messages": [{"role": "user", "content": "hello"}],
"tools": [{"type": "function", "function": {"name": "bash"}}]
});
let cl = classify(&body);
assert!(cl.needs_tools);
}
#[test]
fn test_classify_tools_schema_with_tool_content() {
let body = json!({
"messages": [{"role": "user", "content": "Read the file and fix the bug"}],
"tools": [{"type": "function", "function": {"name": "read"}}]
});
let cl = classify(&body);
assert!(cl.needs_tools);
}
#[test]
fn test_classify_anthropic_text_blocks_with_tools() {
let body = json!({
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "List files in this directory and read README.md"}
]
}
],
"tools": [{"name": "shell"}]
});
let cl = classify(&body);
assert!(cl.needs_tools);
assert!(matches!(cl.category, Category::Code | Category::ToolCall));
}
#[test]
fn test_classify_anthropic_tool_use_block_sets_needs_tools() {
let body = json!({
"messages": [
{
"role": "assistant",
"content": [
{"type": "tool_use", "id": "toolu_123", "name": "shell", "input": {"command": "ls"}}
]
}
]
});
let cl = classify(&body);
assert!(cl.needs_tools);
}
#[test]
fn test_anthropic_tool_request_prefers_stronger_tool_model() {
let body = json!({
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "List files in this directory and read README.md"}
]
}
],
"tools": [{"name": "shell"}]
});
let cl = classify(&body);
assert!(cl.needs_tools);
let available = vec![("Qwen3-8B-Q4_K_M", 40.0), ("MiniMax-M2.5-Q4_K_M", 20.0)];
let picked = pick_model_classified(&cl, &available);
assert_eq!(picked, Some("MiniMax-M2.5-Q4_K_M"));
}
#[test]
fn test_classify_system_prompt_code() {
let body = json!({
"messages": [
{"role": "system", "content": "You are a senior developer and coding assistant."},
{"role": "user", "content": "Help me with this."}
]
});
assert_eq!(classify(&body).category, Category::Code);
}
#[test]
fn test_media_requirements_detect_audio_block() {
let body = json!({
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "Transcribe this clip"},
{"type": "audio_url", "audio_url": {"url": "mesh://blob/client-1/example"}}
]
}
]
});
let media = media_requirements(&body);
assert!(media.has_media);
assert!(media.needs_audio);
assert!(!media.needs_vision);
assert!(classify(&body).has_media_inputs);
}
#[test]
fn test_media_requirements_detect_image_block() {
let body = json!({
"messages": [
{
"role": "user",
"content": [
{"type": "image_url", "image_url": {"url": "data:image/png;base64,abc"}}
]
}
]
});
let media = media_requirements(&body);
assert!(media.has_media);
assert!(media.needs_vision);
assert!(!media.needs_audio);
assert!(classify(&body).has_media_inputs);
}
#[test]
fn test_pick_model_primary_strength_wins() {
let available = vec![("Qwen3-8B-Q4_K_M", 50.0), ("Qwen3-235B-A22B-Q4_K_M", 20.0)];
let result = pick_model_classified(
&Classification {
category: Category::Chat,
complexity: Complexity::Moderate,
needs_tools: false,
has_media_inputs: false,
},
&available,
);
assert!(result == Some("Qwen3-8B-Q4_K_M") || result == Some("Qwen3-235B-A22B-Q4_K_M"));
}
#[test]
fn test_deep_complexity_prefers_bigger() {
let available = vec![("Qwen3-8B-Q4_K_M", 50.0), ("Qwen3-235B-A22B-Q4_K_M", 20.0)];
let result = pick_model_classified(
&Classification {
category: Category::Chat,
complexity: Complexity::Deep,
needs_tools: false,
has_media_inputs: false,
},
&available,
);
assert!(result == Some("Qwen3-8B-Q4_K_M") || result == Some("Qwen3-235B-A22B-Q4_K_M"));
}
#[test]
fn test_quick_complexity_prefers_smaller() {
let available = vec![
("Qwen3-8B-Q4_K_M", 50.0),
("Qwen2.5-72B-Instruct-Q4_K_M", 10.0),
];
let result = pick_model_classified(
&Classification {
category: Category::Chat,
complexity: Complexity::Quick,
needs_tools: false,
has_media_inputs: false,
},
&available,
);
assert!(result == Some("Qwen3-8B-Q4_K_M") || result == Some("Qwen2.5-72B-Instruct-Q4_K_M"));
}
#[test]
fn test_pick_model_prefers_strength_match() {
let available = vec![
("DeepSeek-R1-Distill-70B-Q4_K_M", 10.0), ("Qwen2.5-72B-Instruct-Q4_K_M", 10.0), ];
let result = pick_model_classified(
&Classification {
category: Category::Reasoning,
complexity: Complexity::Moderate,
needs_tools: false,
has_media_inputs: false,
},
&available,
);
assert!(
result == Some("DeepSeek-R1-Distill-70B-Q4_K_M")
|| result == Some("Qwen2.5-72B-Instruct-Q4_K_M")
);
}
#[test]
fn test_pick_model_code_specialist() {
let available = vec![
("Qwen2.5-Coder-32B-Instruct-Q4_K_M", 15.0),
("Qwen2.5-32B-Instruct-Q4_K_M", 15.0),
];
let result = pick_model_classified(
&Classification {
category: Category::Code,
complexity: Complexity::Moderate,
needs_tools: false,
has_media_inputs: false,
},
&available,
);
assert!(
result == Some("Qwen2.5-Coder-32B-Instruct-Q4_K_M")
|| result == Some("Qwen2.5-32B-Instruct-Q4_K_M")
);
}
#[test]
fn test_pick_model_empty() {
let available: Vec<(&str, f64)> = vec![];
assert_eq!(
pick_model_classified(
&Classification {
category: Category::Chat,
complexity: Complexity::Moderate,
needs_tools: false,
has_media_inputs: false,
},
&available
),
None
);
}
#[test]
fn test_pick_model_unknown_model_still_works() {
let available = vec![("SomeUnknownModel", 30.0)];
let result = pick_model_classified(
&Classification {
category: Category::Chat,
complexity: Complexity::Moderate,
needs_tools: false,
has_media_inputs: false,
},
&available,
);
assert_eq!(result, Some("SomeUnknownModel"));
}
#[test]
fn test_profile_lookup() {
assert!(profile_for("Qwen3-235B-A22B-Q4_K_M").is_some());
assert_eq!(profile_for("Qwen3-235B-A22B-Q4_K_M").unwrap().tier, 4);
assert!(profile_for("nonexistent").is_none());
}
#[test]
fn test_all_profiles_have_strengths() {
for p in MODEL_PROFILES {
assert!(!p.strengths.is_empty(), "{} has no strengths", p.name);
}
}
#[test]
fn test_classify_empty_tools_is_not_tool_call() {
let body = json!({
"messages": [{"role": "user", "content": "hello"}],
"tools": []
});
assert_eq!(classify(&body).category, Category::Chat);
}
#[test]
fn test_strip_split_suffix() {
assert_eq!(
strip_split_suffix("MiniMax-M2.5-Q4_K_M-00001-of-00004"),
"MiniMax-M2.5-Q4_K_M"
);
assert_eq!(
strip_split_suffix("Qwen3-Coder-Next-Q4_K_M-00001-of-00004"),
"Qwen3-Coder-Next-Q4_K_M"
);
assert_eq!(
strip_split_suffix("Hermes-2-Pro-Mistral-7B-Q4_K_M"),
"Hermes-2-Pro-Mistral-7B-Q4_K_M"
);
assert_eq!(strip_split_suffix(""), "");
}
#[test]
fn test_profile_for_split_gguf() {
let p = profile_for("MiniMax-M2.5-Q4_K_M-00001-of-00004");
assert!(p.is_some());
assert_eq!(p.unwrap().name, "MiniMax-M2.5-Q4_K_M");
assert_eq!(p.unwrap().tier, 4);
}
}
#[test]
fn test_tools_filter_prefers_capable() {
let available = vec![
("DeepSeek-R1-Distill-Qwen-32B-Q4_K_M", 10.0), ("Qwen2.5-32B-Instruct-Q4_K_M", 50.0), ];
let result = pick_model_with_tools(Category::Reasoning, &available, false);
assert!(
result == Some("DeepSeek-R1-Distill-Qwen-32B-Q4_K_M")
|| result == Some("Qwen2.5-32B-Instruct-Q4_K_M")
);
let result = pick_model_with_tools(Category::Reasoning, &available, true);
assert_eq!(result, Some("Qwen2.5-32B-Instruct-Q4_K_M"));
}
#[test]
fn test_tools_filter_fallback_when_none_capable() {
let available = vec![
("DeepSeek-R1-Distill-Qwen-32B-Q4_K_M", 10.0), ];
let result = pick_model_with_tools(Category::Reasoning, &available, true);
assert_eq!(result, Some("DeepSeek-R1-Distill-Qwen-32B-Q4_K_M"));
}
#[test]
fn test_agentic_prefers_strongest_model() {
let available = vec![
("Hermes-2-Pro-Mistral-7B-Q4_K_M", 87.0), ("Qwen2.5-Coder-7B-Instruct-Q4_K_M", 85.0), ("Qwen2.5-32B-Instruct-Q4_K_M", 18.0), ];
let cl = Classification {
category: Category::Code,
complexity: Complexity::Moderate,
needs_tools: true,
has_media_inputs: false,
};
let result = pick_model_classified(&cl, &available);
assert_eq!(result, Some("Qwen2.5-32B-Instruct-Q4_K_M"));
}
#[test]
fn test_chat_prefers_fastest_model() {
let available = vec![
("Hermes-2-Pro-Mistral-7B-Q4_K_M", 87.0), ("Qwen2.5-32B-Instruct-Q4_K_M", 18.0), ];
let cl = Classification {
category: Category::Chat,
complexity: Complexity::Quick,
needs_tools: false,
has_media_inputs: false,
};
let result = pick_model_classified(&cl, &available);
assert!(
result == Some("Hermes-2-Pro-Mistral-7B-Q4_K_M")
|| result == Some("Qwen2.5-32B-Instruct-Q4_K_M")
);
}
#[test]
fn test_agentic_deep_strongly_prefers_biggest() {
let available = vec![
("Qwen2.5-Coder-7B-Instruct-Q4_K_M", 85.0), ("MiniMax-M2.5-Q4_K_M", 21.0), ];
let cl = Classification {
category: Category::Code,
complexity: Complexity::Deep,
needs_tools: true,
has_media_inputs: false,
};
let result = pick_model_classified(&cl, &available);
assert_eq!(result, Some("MiniMax-M2.5-Q4_K_M"));
}