Skip to content

Commit bfcbe9d

Browse files
committed
feat(agent): classify model families
1 parent 31f34c5 commit bfcbe9d

1 file changed

Lines changed: 118 additions & 0 deletions

File tree

crates/agent/src/lib.rs

Lines changed: 118 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3,6 +3,22 @@ use std::collections::HashMap;
33
use codewhale_config::ProviderKind;
44
use serde::{Deserialize, Serialize};
55

6+
/// High-level model family used for shared identity affordances across clients.
7+
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
8+
pub enum ModelFamily {
9+
DeepSeek,
10+
Anthropic,
11+
OpenAI,
12+
Google,
13+
Meta,
14+
Mistral,
15+
Qwen,
16+
Grok,
17+
Cohere,
18+
GptOss,
19+
Inferencer,
20+
}
21+
622
/// Metadata for a single model entry in the registry.
723
///
824
/// Each model has a canonical `id` used by the provider, a list of `aliases`
@@ -545,6 +561,58 @@ fn normalize(value: &str) -> String {
545561
value.trim().to_ascii_lowercase()
546562
}
547563

564+
#[must_use]
565+
/// Classify a model identifier by its underlying model family.
566+
pub fn model_family(model_id: &str) -> ModelFamily {
567+
let normalized = normalize(model_id);
568+
if normalized.is_empty() {
569+
return ModelFamily::Inferencer;
570+
}
571+
572+
if normalized.contains("deepseek") {
573+
return ModelFamily::DeepSeek;
574+
}
575+
if normalized.contains("claude") || normalized.contains("anthropic") {
576+
return ModelFamily::Anthropic;
577+
}
578+
if normalized.contains("gpt-oss") || normalized.contains("gpt_oss") {
579+
return ModelFamily::GptOss;
580+
}
581+
if normalized.starts_with("gpt-")
582+
|| normalized.contains("/gpt-")
583+
|| normalized.contains("openai/")
584+
{
585+
return ModelFamily::OpenAI;
586+
}
587+
if normalized.contains("gemini")
588+
|| normalized.contains("gemma")
589+
|| normalized.contains("google/")
590+
{
591+
return ModelFamily::Google;
592+
}
593+
if normalized.contains("llama") || normalized.contains("meta-") || normalized.contains("meta/")
594+
{
595+
return ModelFamily::Meta;
596+
}
597+
if normalized.contains("mistral")
598+
|| normalized.contains("mixtral")
599+
|| normalized.contains("codestral")
600+
{
601+
return ModelFamily::Mistral;
602+
}
603+
if normalized.contains("qwen") {
604+
return ModelFamily::Qwen;
605+
}
606+
if normalized.contains("grok") {
607+
return ModelFamily::Grok;
608+
}
609+
if normalized.contains("cohere") || normalized.contains("command-r") {
610+
return ModelFamily::Cohere;
611+
}
612+
613+
ModelFamily::Inferencer
614+
}
615+
548616
fn model_matches(model: &ModelInfo, requested: &str) -> bool {
549617
let requested = normalize(requested);
550618
normalize(&model.id) == requested
@@ -840,4 +908,54 @@ mod tests {
840908
assert_eq!(resolved.resolved.provider, ProviderKind::Deepseek);
841909
assert_eq!(resolved.resolved.id, "deepseek-v4-flash");
842910
}
911+
912+
#[test]
913+
fn model_family_classifies_known_model_ids() {
914+
assert_eq!(model_family("deepseek-v4-pro"), ModelFamily::DeepSeek);
915+
assert_eq!(model_family("openai/gpt-5.4"), ModelFamily::OpenAI);
916+
assert_eq!(
917+
model_family("anthropic/claude-opus-4-7"),
918+
ModelFamily::Anthropic
919+
);
920+
assert_eq!(
921+
model_family("meta-llama/llama-3.3-70b-instruct"),
922+
ModelFamily::Meta
923+
);
924+
assert_eq!(model_family("Qwen/Qwen3-Coder"), ModelFamily::Qwen);
925+
}
926+
927+
#[test]
928+
fn model_family_uses_underlying_model_for_router_ids() {
929+
assert_eq!(
930+
model_family("groq/llama-3.3-70b-versatile"),
931+
ModelFamily::Meta
932+
);
933+
assert_eq!(
934+
model_family("openrouter/openai/gpt-5.4"),
935+
ModelFamily::OpenAI
936+
);
937+
assert_eq!(
938+
model_family("fireworks/accounts/fireworks/models/deepseek-v4-pro"),
939+
ModelFamily::DeepSeek
940+
);
941+
}
942+
943+
#[test]
944+
fn model_family_covers_prominent_google_and_mistral_model_names() {
945+
assert_eq!(model_family("google/gemma-3-27b-it"), ModelFamily::Google);
946+
assert_eq!(
947+
model_family("mistralai/mixtral-8x22b"),
948+
ModelFamily::Mistral
949+
);
950+
assert_eq!(model_family("codestral-latest"), ModelFamily::Mistral);
951+
}
952+
953+
#[test]
954+
fn model_family_falls_back_to_inferencer_for_unknown_models() {
955+
assert_eq!(
956+
model_family("custom-gateway/my-private-model"),
957+
ModelFamily::Inferencer
958+
);
959+
assert_eq!(model_family(""), ModelFamily::Inferencer);
960+
}
843961
}

0 commit comments

Comments
 (0)