#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum Vendor {
Deepseek,
Other,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum ModelKind {
Chat,
Reasoner,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct ModelFamily {
pub vendor: Vendor,
pub kind: ModelKind,
pub supports_system_prompt: bool,
pub supports_tools: bool,
}
impl ModelFamily {
pub fn is_deepseek_chat(&self) -> bool {
self.vendor == Vendor::Deepseek && self.kind == ModelKind::Chat
}
}
pub fn resolve_family(provider: &str, model_id: &str) -> ModelFamily {
let provider = provider.to_ascii_lowercase();
let model = model_id.to_ascii_lowercase();
let vendor = if provider == "deepseek" || model.contains("deepseek") {
Vendor::Deepseek
} else {
Vendor::Other
};
let kind = if model.contains("reasoner") || has_r1_token(&model) {
ModelKind::Reasoner
} else {
ModelKind::Chat
};
let crippled_reasoner = vendor == Vendor::Deepseek && kind == ModelKind::Reasoner;
ModelFamily {
vendor,
kind,
supports_system_prompt: !crippled_reasoner,
supports_tools: !crippled_reasoner,
}
}
fn has_r1_token(model: &str) -> bool {
model
.split(|c: char| !c.is_ascii_alphanumeric())
.any(|tok| tok == "r1")
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn deepseek_chat_v4_is_chat_with_full_capabilities() {
let f = resolve_family("deepseek", "deepseek-v4-pro");
assert_eq!(f.vendor, Vendor::Deepseek);
assert_eq!(f.kind, ModelKind::Chat);
assert!(f.supports_system_prompt);
assert!(f.supports_tools);
assert!(f.is_deepseek_chat());
}
#[test]
fn deepseek_reasoner_loses_system_prompt_and_tools() {
let f = resolve_family("deepseek", "deepseek-reasoner");
assert_eq!(f.vendor, Vendor::Deepseek);
assert_eq!(f.kind, ModelKind::Reasoner);
assert!(!f.supports_system_prompt);
assert!(!f.supports_tools);
assert!(!f.is_deepseek_chat());
}
#[test]
fn deepseek_r1_id_is_detected_as_reasoner() {
let f = resolve_family("deepseek", "deepseek-r1");
assert_eq!(f.vendor, Vendor::Deepseek);
assert_eq!(f.kind, ModelKind::Reasoner);
}
#[test]
fn openrouter_passthrough_detected_by_model_id() {
let f = resolve_family("openrouter", "deepseek/deepseek-chat");
assert_eq!(f.vendor, Vendor::Deepseek);
assert_eq!(f.kind, ModelKind::Chat);
assert!(f.is_deepseek_chat());
}
#[test]
fn openrouter_passthrough_reasoner_detected() {
let f = resolve_family("openrouter", "deepseek/deepseek-r1");
assert_eq!(f.vendor, Vendor::Deepseek);
assert_eq!(f.kind, ModelKind::Reasoner);
assert!(!f.supports_tools);
}
#[test]
fn case_insensitive() {
let f = resolve_family("DeepSeek", "DEEPSEEK-V4-FLASH");
assert_eq!(f.vendor, Vendor::Deepseek);
assert_eq!(f.kind, ModelKind::Chat);
}
#[test]
fn openai_is_other_vendor_chat() {
let f = resolve_family("openai", "gpt-4o");
assert_eq!(f.vendor, Vendor::Other);
assert_eq!(f.kind, ModelKind::Chat);
assert!(f.supports_system_prompt);
assert!(f.supports_tools);
assert!(!f.is_deepseek_chat());
}
#[test]
fn other_vendor_reasoner_keeps_system_prompt_and_tools() {
let f = resolve_family("openai", "o3-reasoner");
assert_eq!(f.vendor, Vendor::Other);
assert_eq!(f.kind, ModelKind::Reasoner);
assert!(f.supports_system_prompt);
assert!(f.supports_tools);
}
#[test]
fn anthropic_is_other_chat() {
let f = resolve_family("anthropic", "claude-sonnet-4-6");
assert_eq!(f.vendor, Vendor::Other);
assert_eq!(f.kind, ModelKind::Chat);
}
#[test]
fn r1_substring_does_not_false_positive_mid_token() {
let f = resolve_family("openai", "super1-model");
assert_eq!(f.kind, ModelKind::Chat);
}
}