Skip to content

Commit b753e2e

Browse files
samuelclayclaude
andcommitted
Upgrade AI models to each vendor's cheap tier and centralize model IDs behind stable vendor keys
Claude Haiku 4.5, GPT-5.6 Luna, Gemini 3.6 Flash, and Grok 4.6 everywhere (Archive Assistant stays Sonnet, bumped to Sonnet 5). Model IDs now live in one place per platform: utils/llm_models.py on web, AskAIModels.swift on iOS, AskAiModels.kt on Android. Registry keys are stable vendor slugs with aliases for legacy keys, so old clients and saved preferences keep working. Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
1 parent 9acb0f8 commit b753e2e

31 files changed

Lines changed: 314 additions & 140 deletions

apps/archive_assistant/models.py

Lines changed: 3 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,8 @@
99

1010
import mongoengine as mongo
1111

12+
from utils.llm_models import ANTHROPIC_ARCHIVE_MODEL
13+
1214

1315
class MArchiveConversation(mongo.Document):
1416
"""
@@ -54,7 +56,7 @@ class MArchiveQuery(mongo.Document):
5456
response_date = mongo.DateTimeField()
5557

5658
# Metadata
57-
model = mongo.StringField(max_length=64, default="claude-sonnet-4-5")
59+
model = mongo.StringField(max_length=64, default=ANTHROPIC_ARCHIVE_MODEL)
5860
duration_ms = mongo.IntField() # Time to generate response
5961
tokens_used = mongo.IntField()
6062

apps/archive_assistant/tasks.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -16,15 +16,12 @@
1616
from django.conf import settings
1717
from django.contrib.auth.models import User
1818

19-
from apps.archive_assistant.models import (
20-
MArchiveAssistantUsage,
21-
MArchiveConversation,
22-
MArchiveQuery,
23-
)
19+
from apps.archive_assistant.models import MArchiveAssistantUsage, MArchiveConversation, MArchiveQuery
2420
from apps.archive_assistant.prompts import get_system_prompt
2521
from apps.archive_assistant.tools import ARCHIVE_TOOLS, execute_tool
2622
from utils import log as logging
2723
from utils.llm_costs import LLMCostTracker
24+
from utils.llm_models import ANTHROPIC_ARCHIVE_MODEL
2825

2926
# Character limit for non-premium users before truncation
3027
FREE_RESPONSE_CHAR_LIMIT = 300
@@ -37,7 +34,7 @@ def get_redis_pubsub_connection():
3734

3835
@shared_task(name="archive-assistant-query")
3936
def process_archive_query(
40-
user_id, conversation_id, query_id, query_text, model="claude-sonnet-4-5", is_premium_archive=True
37+
user_id, conversation_id, query_id, query_text, model=ANTHROPIC_ARCHIVE_MODEL, is_premium_archive=True
4138
):
4239
"""
4340
Process an Archive Assistant query using Claude with tools.

apps/archive_assistant/views.py

Lines changed: 4 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,15 @@
1212
from django.views.decorators.csrf import csrf_exempt
1313
from django.views.decorators.http import require_http_methods
1414

15-
from apps.archive_assistant.models import (
16-
MArchiveAssistantUsage,
17-
MArchiveConversation,
18-
MArchiveQuery,
19-
)
15+
from apps.archive_assistant.models import MArchiveAssistantUsage, MArchiveConversation, MArchiveQuery
2016
from apps.archive_assistant.prompts import get_suggested_questions
2117
from apps.archive_assistant.tasks import process_archive_query
2218
from apps.archive_extension.models import MArchivedStory
2319
from apps.archive_extension.utils import format_datetime_utc
2420
from apps.profile.models import Profile
2521
from utils import json_functions as json
2622
from utils import log as logging
23+
from utils.llm_models import ANTHROPIC_ARCHIVE_MODEL
2724
from utils.user_functions import ajax_login_required, get_user
2825

2926

@@ -51,7 +48,7 @@ def submit_query(request):
5148
POST params:
5249
query: The question to ask (required)
5350
conversation_id: Existing conversation ID (optional, creates new if not provided)
54-
model: Model to use (optional, defaults to claude-sonnet-4-5)
51+
model: Model to use (optional, defaults to ANTHROPIC_ARCHIVE_MODEL in utils/llm_models.py)
5552
5653
Returns:
5754
{
@@ -72,7 +69,7 @@ def submit_query(request):
7269

7370
query_text = request.POST.get("query", "").strip()
7471
conversation_id = request.POST.get("conversation_id", "").strip()
75-
model = request.POST.get("model", "claude-sonnet-4-5")
72+
model = request.POST.get("model", ANTHROPIC_ARCHIVE_MODEL)
7673

7774
# Check if user has premium archive for full responses
7875
profile = Profile.objects.get(user=user)

apps/archive_extension/tasks.py

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -14,6 +14,7 @@
1414

1515
from utils import json_functions as json
1616
from utils import log as logging
17+
from utils.llm_models import ANTHROPIC_MODEL
1718

1819

1920
def _publish_category_update(user_id, archive_id, categories):
@@ -212,7 +213,7 @@ def _categorize_single_archive(archive, user_categories=None):
212213
)
213214

214215
response = client.messages.create(
215-
model="claude-haiku-4-5",
216+
model=ANTHROPIC_MODEL,
216217
max_tokens=100,
217218
messages=[{"role": "user", "content": prompt}],
218219
)

apps/archive_extension/views.py

Lines changed: 3 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -15,17 +15,14 @@
1515
from django.views.decorators.csrf import csrf_exempt
1616
from django.views.decorators.http import require_http_methods
1717

18-
from apps.archive_extension.blocklist import (
19-
get_blocked_domains,
20-
get_blocked_patterns,
21-
is_blocked,
22-
)
18+
from apps.archive_extension.blocklist import get_blocked_domains, get_blocked_patterns, is_blocked
2319
from apps.archive_extension.matching import match_and_process
2420
from apps.archive_extension.models import MArchivedStory, MArchiveUserSettings
2521
from apps.archive_extension.tasks import categorize_archives, index_archive_for_search
2622
from apps.archive_extension.utils import format_datetime_utc
2723
from utils import json_functions as json
2824
from utils import log as logging
25+
from utils.llm_models import ANTHROPIC_MODEL
2926
from utils.user_functions import ajax_login_required, get_user
3027

3128

@@ -1131,7 +1128,7 @@ def split_category(request):
11311128
]"""
11321129

11331130
response = client.messages.create(
1134-
model="claude-haiku-4-5", max_tokens=2000, messages=[{"role": "user", "content": prompt}]
1131+
model=ANTHROPIC_MODEL, max_tokens=2000, messages=[{"role": "user", "content": prompt}]
11351132
)
11361133

11371134
result_text = response.content[0].text.strip()

apps/ask_ai/models.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class MAskAIResponse(mongo.Document):
1313
story_hash = mongo.StringField(max_length=32)
1414
question_id = mongo.StringField(max_length=64)
1515
custom_question = mongo.StringField()
16-
model = mongo.StringField(max_length=32) # AI model used (opus, gpt-5.2, gemini-3, grok-4.1)
16+
model = mongo.StringField(max_length=32) # AI model used (anthropic, openai, google, xai)
1717
response_z = mongo.BinaryField()
1818
response_metadata = mongo.DictField()
1919
created_at = mongo.DateTimeField(default=datetime.datetime.now)
@@ -56,7 +56,7 @@ def get_cached_response(cls, user_id, story_hash, question_id, custom_question=N
5656
story_hash: Story hash
5757
question_id: Question ID (e.g., "sentence", "bullets")
5858
custom_question: Optional custom question text
59-
model: Optional model name (opus, gpt-5.2, gemini-3, grok-4.1)
59+
model: Optional model name (anthropic, openai, google, xai)
6060
6161
Returns:
6262
MAskAIResponse instance or None
@@ -91,7 +91,7 @@ def create_response(
9191
question_id: Question ID
9292
response_text: Full response text from AI
9393
custom_question: Optional custom question
94-
model: Optional model name (opus, gpt-5.2, gemini-3, grok-4.1)
94+
model: Optional model name (anthropic, openai, google, xai)
9595
metadata: Optional metadata dict (tokens, model, etc.)
9696
9797
Returns:

apps/ask_ai/providers.py

Lines changed: 86 additions & 32 deletions
Original file line numberDiff line numberDiff line change
@@ -10,6 +10,16 @@
1010
from google.genai import types as genai_types
1111

1212
from 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

1525
def _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

424457
def _load_models():
425458
"""Load models, applying settings override if ASK_AI_MODELS is defined.
@@ -462,15 +495,23 @@ def _load_models():
462495

463496
MODELS = _load_models()
464497
VALID_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.
469502
MODEL_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

590633
BRIEFING_MODELS = _load_briefing_models()
591634
VALID_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

595650
def get_briefing_models_for_frontend() -> list:
@@ -613,7 +668,6 @@ def get_briefing_models_for_frontend() -> list:
613668

614669
def 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

Comments
 (0)