@@ -3,6 +3,22 @@ use std::collections::HashMap;
33use codewhale_config:: ProviderKind ;
44use 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+
548616fn 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