1010from google .genai import types as genai_types
1111
1212from utils import log as logging
13+ from utils .llm_models import (
14+ ANTHROPIC_MODEL ,
15+ ANTHROPIC_MODEL_DISPLAY ,
16+ GOOGLE_MODEL ,
17+ GOOGLE_MODEL_DISPLAY ,
18+ OPENAI_MODEL ,
19+ OPENAI_MODEL_DISPLAY ,
20+ XAI_MODEL ,
21+ XAI_MODEL_DISPLAY ,
22+ )
1323
1424
1525def _is_placeholder (key ):
@@ -372,13 +382,14 @@ def format_error(self, error: Exception) -> str:
372382}
373383
374384# Model registry: single source of truth for all model configuration.
375- # Each entry contains everything needed by both backend and frontend.
376- # To add/update a model, only change this dict — frontend dropdowns are populated from it.
385+ # Registry keys are stable vendor slugs so model upgrades never rename keys —
386+ # to upgrade a model, only change the IDs/display names in utils/llm_models.py.
387+ # Frontend dropdowns are populated from this dict.
377388_DEFAULT_MODELS = {
378- "opus " : {
389+ "anthropic " : {
379390 "provider_class" : AnthropicProvider ,
380- "model_id" : "claude-opus-4-6" ,
381- "display_name" : "Claude Opus 4.6" ,
391+ "model_id" : ANTHROPIC_MODEL ,
392+ "display_name" : ANTHROPIC_MODEL_DISPLAY ,
382393 "vendor" : "anthropic" ,
383394 "vendor_display" : "Anthropic" ,
384395 "order" : 1 ,
@@ -387,39 +398,61 @@ def format_error(self, error: Exception) -> str:
387398 "max_tokens" : 16384 ,
388399 },
389400 },
390- "gpt-5.2 " : {
401+ "openai " : {
391402 "provider_class" : OpenAIProvider ,
392- "model_id" : "gpt-5.2" ,
393- "display_name" : "GPT 5.2" ,
403+ "model_id" : OPENAI_MODEL ,
404+ "display_name" : OPENAI_MODEL_DISPLAY ,
394405 "vendor" : "openai" ,
395406 "vendor_display" : "OpenAI" ,
396407 "order" : 2 ,
397408 "thinking_config" : {
398409 "reasoning_effort" : "high" ,
399410 },
400411 },
401- "gemini-3 " : {
412+ "google " : {
402413 "provider_class" : GeminiProvider ,
403- "model_id" : "gemini-3-pro-preview" ,
404- "display_name" : "Gemini 3 Pro" ,
414+ "model_id" : GOOGLE_MODEL ,
415+ "display_name" : GOOGLE_MODEL_DISPLAY ,
405416 "vendor" : "google" ,
406417 "vendor_display" : "Google" ,
407418 "order" : 3 ,
408419 "thinking_config" : {
409420 "thinking_budget" : - 1 ,
410421 },
411422 },
412- "grok-4.1 " : {
423+ "xai " : {
413424 "provider_class" : XAIProvider ,
414- "model_id" : "grok-4-1-fast-non-reasoning" ,
415- "display_name" : "Grok 4.1 Fast" ,
425+ "model_id" : XAI_MODEL ,
426+ "display_name" : XAI_MODEL_DISPLAY ,
416427 "vendor" : "xai" ,
417428 "vendor_display" : "xAI" ,
418429 "order" : 4 ,
419- "thinking_model_id" : "grok-4-1-fast-reasoning" ,
420430 },
421431}
422432
433+ # Legacy registry keys from before keys became vendor slugs (Aug 2026). Old
434+ # mobile clients and stored user preferences still send these — resolve them
435+ # instead of falling back to the default model. Never reuse these as new keys.
436+ MODEL_ALIASES = {
437+ # Legacy Ask AI keys
438+ "opus" : "anthropic" ,
439+ "gpt-5.2" : "openai" ,
440+ "gemini-3" : "google" ,
441+ "grok-4.1" : "xai" ,
442+ # Legacy briefing keys
443+ "haiku" : "anthropic" ,
444+ "gpt-5-mini" : "openai" ,
445+ "gemini-flash-lite" : "google" ,
446+ "grok-4.1-fast" : "xai" ,
447+ }
448+
449+
450+ def _resolve_key (model_name , registry ):
451+ """Resolve a model key against a registry, mapping legacy keys via MODEL_ALIASES."""
452+ if model_name in registry :
453+ return model_name
454+ return MODEL_ALIASES .get (model_name , model_name )
455+
423456
424457def _load_models ():
425458 """Load models, applying settings override if ASK_AI_MODELS is defined.
@@ -462,15 +495,23 @@ def _load_models():
462495
463496MODELS = _load_models ()
464497VALID_MODELS = list (MODELS .keys ())
465- DEFAULT_MODEL = getattr (settings , "ASK_AI_MODEL" , "opus" )
498+ DEFAULT_MODEL = _resolve_key ( getattr (settings , "ASK_AI_MODEL" , "anthropic" ), MODELS )
466499
467500# MODEL_VENDORS includes both current and historical models for metrics tracking.
468501# When retiring a model, remove it from MODELS above but keep it here.
469502MODEL_VENDORS = {
470503 ** {key : m ["vendor" ] for key , m in MODELS .items ()},
471504 # Historical models (kept for metrics)
505+ "opus" : "anthropic" ,
506+ "haiku" : "anthropic" ,
507+ "gpt-5.2" : "openai" ,
508+ "gpt-5-mini" : "openai" ,
472509 "gpt-5.1" : "openai" ,
473510 "gpt-4.1" : "openai" ,
511+ "gemini-3" : "google" ,
512+ "gemini-flash-lite" : "google" ,
513+ "grok-4.1" : "xai" ,
514+ "grok-4.1-fast" : "xai" ,
474515 "grok-4" : "xai" ,
475516}
476517
@@ -502,6 +543,7 @@ def get_provider(model_name: str, thinking: bool = False) -> tuple[LLMProvider,
502543 Returns:
503544 Tuple of (provider_instance, model_id, thinking_config)
504545 """
546+ model_name = _resolve_key (model_name , MODELS )
505547 if model_name not in MODELS :
506548 model_name = DEFAULT_MODEL
507549
@@ -516,36 +558,37 @@ def get_provider(model_name: str, thinking: bool = False) -> tuple[LLMProvider,
516558
517559
518560# Briefing model registry: cheap models optimized for daily briefing generation.
519- # Separate from the Ask AI models (which use flagship models).
561+ # Keys are the same stable vendor slugs as the Ask AI registry, and both tiers
562+ # now point at the same cheap models from utils/llm_models.py.
520563_DEFAULT_BRIEFING_MODELS = {
521- "haiku " : {
564+ "anthropic " : {
522565 "provider_class" : AnthropicProvider ,
523- "model_id" : "claude-haiku-4-5" ,
524- "display_name" : "Claude Haiku" ,
566+ "model_id" : ANTHROPIC_MODEL ,
567+ "display_name" : ANTHROPIC_MODEL_DISPLAY ,
525568 "vendor" : "anthropic" ,
526569 "vendor_display" : "Anthropic" ,
527570 "order" : 1 ,
528571 },
529- "gpt-5-mini " : {
572+ "openai " : {
530573 "provider_class" : OpenAIProvider ,
531- "model_id" : "gpt-5-mini" ,
532- "display_name" : "GPT 5 Mini" ,
574+ "model_id" : OPENAI_MODEL ,
575+ "display_name" : OPENAI_MODEL_DISPLAY ,
533576 "vendor" : "openai" ,
534577 "vendor_display" : "OpenAI" ,
535578 "order" : 2 ,
536579 },
537- "gemini-flash-lite " : {
580+ "google " : {
538581 "provider_class" : GeminiProvider ,
539- "model_id" : "gemini-2.5-flash-lite" ,
540- "display_name" : "Gemini Flash Lite" ,
582+ "model_id" : GOOGLE_MODEL ,
583+ "display_name" : GOOGLE_MODEL_DISPLAY ,
541584 "vendor" : "google" ,
542585 "vendor_display" : "Google" ,
543586 "order" : 3 ,
544587 },
545- "grok-4.1-fast " : {
588+ "xai " : {
546589 "provider_class" : XAIProvider ,
547- "model_id" : "grok-4-1-fast-non-reasoning" ,
548- "display_name" : "Grok 4.1 Fast" ,
590+ "model_id" : XAI_MODEL ,
591+ "display_name" : XAI_MODEL_DISPLAY ,
549592 "vendor" : "xai" ,
550593 "vendor_display" : "xAI" ,
551594 "order" : 4 ,
@@ -589,7 +632,19 @@ def _load_briefing_models():
589632
590633BRIEFING_MODELS = _load_briefing_models ()
591634VALID_BRIEFING_MODELS = list (BRIEFING_MODELS .keys ())
592- DEFAULT_BRIEFING_MODEL = getattr (settings , "BRIEFING_MODEL" , "haiku" )
635+ DEFAULT_BRIEFING_MODEL = _resolve_key (getattr (settings , "BRIEFING_MODEL" , "anthropic" ), BRIEFING_MODELS )
636+
637+
638+ def resolve_briefing_model_key (model_name : Optional [str ]) -> Optional [str ]:
639+ """Resolve a stored/POSTed briefing model key to a current registry key.
640+
641+ Maps legacy keys ("haiku") to vendor keys ("anthropic"). Returns None when
642+ the key isn't recognized so callers can fall back to the server default.
643+ """
644+ if not model_name :
645+ return None
646+ model_name = _resolve_key (model_name , BRIEFING_MODELS )
647+ return model_name if model_name in BRIEFING_MODELS else None
593648
594649
595650def get_briefing_models_for_frontend () -> list :
@@ -613,7 +668,6 @@ def get_briefing_models_for_frontend() -> list:
613668
614669def get_briefing_provider (model_name : str ) -> tuple [LLMProvider , str ]:
615670 """Get a provider instance and model ID for the given briefing model name."""
616- if not model_name or model_name not in BRIEFING_MODELS :
617- model_name = DEFAULT_BRIEFING_MODEL
671+ model_name = resolve_briefing_model_key (model_name ) or DEFAULT_BRIEFING_MODEL
618672 model = BRIEFING_MODELS [model_name ]
619673 return model ["provider_class" ](), model ["model_id" ]
0 commit comments