2323
2424log = logging .getLogger (__name__ )
2525
26+ # ---------------------------------------------------------------------------
27+ # Model capability tiers
28+ # ---------------------------------------------------------------------------
29+ # Tier 4: orchestrator — multi-agent coordination, complex planning, 20+ tools
30+ # Tier 3: autonomous — independent multi-step execution, 10+ tools, self-correction
31+ # Tier 2: tool-use — reliable tool calling, structured instructions, single-task
32+ # Tier 1: conversational — text in/out, unreliable with tools
33+
34+ TIER_LABELS : dict [int , str ] = {
35+ 4 : "orchestrator" ,
36+ 3 : "autonomous" ,
37+ 2 : "tool-use" ,
38+ 1 : "conversational" ,
39+ }
40+
41+ # Known model → tier mappings. Prefix matching is used: the longest prefix
42+ # that matches wins. Unknown models default to tier 2.
43+ _MODEL_TIERS : dict [str , int ] = {
44+ # Anthropic
45+ "claude-opus-4" : 4 ,
46+ "claude-sonnet-4" : 3 ,
47+ "claude-haiku-4" : 2 ,
48+ "claude-sonnet-3.5" : 3 ,
49+ "claude-haiku-3.5" : 2 ,
50+ # OpenAI
51+ "o3" : 4 ,
52+ "o1" : 3 ,
53+ "gpt-4o" : 3 ,
54+ "gpt-4-turbo" : 3 ,
55+ "gpt-4" : 3 ,
56+ "gpt-3.5" : 2 ,
57+ # Google
58+ "gemini-2.5-pro" : 4 ,
59+ "gemini-2.5-flash" : 3 ,
60+ "gemini-2.0-flash" : 3 ,
61+ "gemini-2.0-pro" : 4 ,
62+ "gemini-1.5-pro" : 3 ,
63+ "gemini-1.5-flash" : 2 ,
64+ }
65+
66+ _DEFAULT_TIER = 2
67+
68+
69+ def get_model_tier (model_id : str ) -> int :
70+ """Return the capability tier (1-4) for a model ID.
71+
72+ Uses longest-prefix matching against _MODEL_TIERS.
73+ Unknown models default to tier 2 (tool-use).
74+ """
75+ if not model_id :
76+ return _DEFAULT_TIER
77+ lower = model_id .lower ()
78+ best_match = 0
79+ best_tier = _DEFAULT_TIER
80+ for prefix , tier in _MODEL_TIERS .items ():
81+ if lower .startswith (prefix .lower ()) and len (prefix ) > best_match :
82+ best_match = len (prefix )
83+ best_tier = tier
84+ return best_tier
85+
86+
87+ def get_tier_label (tier : int ) -> str :
88+ """Return the human-readable label for a tier number."""
89+ return TIER_LABELS .get (tier , "unknown" )
90+
2691
2792class ProviderManager :
2893 """Provider configuration, resolution, probing, and lifecycle."""
@@ -341,7 +406,13 @@ def get_available_models(self, provider_name: str, *, require_active: bool = Fal
341406 {"id" : "claude-haiku-4-5" , "name" : "Claude Haiku 4.5" },
342407 ],
343408 }
344- return _PROVIDER_MODELS .get (provider_name , [])
409+ models = _PROVIDER_MODELS .get (provider_name , [])
410+ # Enrich each model entry with capability tier
411+ for m in models :
412+ tier = get_model_tier (m ["id" ])
413+ m ["capability_tier" ] = tier
414+ m ["tier_label" ] = get_tier_label (tier )
415+ return models
345416
346417 # ------------------------------------------------------------------
347418 # Provider management (config UI)
@@ -374,15 +445,23 @@ async def provider_list(self) -> list[dict[str, Any]]:
374445 else :
375446 setup_hint = None
376447
448+ models = self .get_available_models (pid )
449+ # Derive orchestrator_capable from model tiers (backward compat)
450+ max_tier = max ((m .get ("capability_tier" , _DEFAULT_TIER ) for m in models ), default = _DEFAULT_TIER ) if models else _DEFAULT_TIER
451+ # Fall back to the static flag for providers with no model list
452+ orch_capable = max_tier >= 4 if models else info .get ("orchestrator_capable" , False )
453+
377454 entry : dict [str , Any ] = {
378455 "id" : pid ,
379456 "name" : info ["name" ],
380457 "description" : info ["description" ],
381458 "auth_type" : info ["auth_type" ],
382459 "configured" : configured ,
383460 "active" : is_active ,
384- "orchestrator_capable" : info .get ("orchestrator_capable" , False ),
385- "models" : self .get_available_models (pid ),
461+ "orchestrator_capable" : orch_capable ,
462+ "max_capability_tier" : max_tier ,
463+ "max_tier_label" : get_tier_label (max_tier ),
464+ "models" : models ,
386465 "setup_hint" : setup_hint ,
387466 }
388467
@@ -406,17 +485,22 @@ async def provider_list(self) -> list[dict[str, Any]]:
406485 pconfig = self ._config .providers .get (pid )
407486 base_url = pconfig .base_url if pconfig else ""
408487
488+ default_model = pconfig .default_model if pconfig else ""
489+ custom_tier = get_model_tier (default_model )
490+
409491 entry = {
410492 "id" : pid ,
411493 "name" : pid ,
412494 "description" : f"Custom OpenAI-compatible provider ({ base_url } )" ,
413495 "auth_type" : "api_key" ,
414496 "configured" : True ,
415497 "active" : is_active ,
416- "orchestrator_capable" : True ,
498+ "orchestrator_capable" : custom_tier >= 4 ,
499+ "max_capability_tier" : custom_tier ,
500+ "max_tier_label" : get_tier_label (custom_tier ),
417501 "custom" : True ,
418502 "base_url" : base_url ,
419- "default_model" : pconfig . default_model if pconfig else "" ,
503+ "default_model" : default_model ,
420504 "models" : [],
421505 "setup_hint" : None ,
422506 }
0 commit comments