fix(model-routing): keep selected models strict (#5801)

Co-authored-by: Alexandre Teixeira <111787685+alteixeira20@users.noreply.github.com>
This commit is contained in:
RaresKeY
2026-08-12 14:10:07 +01:00
committed by GitHub
co-authored by Alexandre Teixeira
parent 53869d194d
commit b52296471b
14 changed files with 417 additions and 103 deletions
+11 -11
View File
@@ -21,6 +21,7 @@ from src import agent_runs
from src.model_context import estimate_tokens
from src.chat_helpers import coerce_message_and_session
from src.endpoint_resolver import normalize_base as _normalize_base, build_chat_url
from src.foreground_model_routing import build_foreground_model_candidates
from src.session_search import search_session_messages
from src.prompt_security import untrusted_context_message
from core.exceptions import SessionNotFoundError
@@ -1399,14 +1400,14 @@ def setup_chat_routes(
thinking_response = ""
last_metrics = None
# Configured fallback chain for the default chat model. Tried in
# order if the session's primary model fails before producing
# output. Resolved once per request.
try:
from src.endpoint_resolver import resolve_chat_fallback_candidates
_fallback_candidates = resolve_chat_fallback_candidates(owner=_user)
except Exception:
_fallback_candidates = []
# Foreground Chat and Agent requests use one owner-aware policy
# boundary. Legacy `default_model_fallbacks` data is not eligible.
_foreground_candidates = build_foreground_model_candidates(
sess.endpoint_url,
sess.model,
sess.headers,
owner=_user,
)
# Send model name early so the frontend can show it during streaming
_model_suffix = "Research" if effective_do_research else None
@@ -1522,9 +1523,8 @@ def setup_chat_routes(
_actual_model = None
# ── Chat mode: call stream_llm directly, NO tools, NO document access ──
try:
_chat_candidates = [(sess.endpoint_url, sess.model, sess.headers)] + _fallback_candidates
async for chunk in stream_llm_with_fallback(
_chat_candidates,
_foreground_candidates,
messages,
temperature=ctx.preset.temperature,
# Respect the preset; 0/unset = let the server decide (no
@@ -1710,7 +1710,7 @@ def setup_chat_routes(
disabled_tools=disabled_tools if disabled_tools else None,
tool_policy=tool_policy,
owner=_user,
fallbacks=_fallback_candidates,
fallbacks=_foreground_candidates[1:],
plan_mode=plan_mode,
approved_plan=approved_plan or None,
workspace=workspace or None,
+4 -4
View File
@@ -5327,9 +5327,9 @@ def setup_email_routes():
# Build a candidate chain so a stale session-stored API key
# (the most common cause of "authentication failed" here)
# doesn't kill AI Reply outright — fall through to the
# user's Utility / Default endpoints AND their configured
# fallback chains. Dedupe by url+model so we don't retry
# the same broken endpoint.
# user's Utility / Default endpoints and the active Utility
# fallback chain. The retired default-fallback hook stays empty.
# Dedupe by url+model so we don't retry the same broken endpoint.
from src.llm_core import llm_call_async_with_fallback
from src.endpoint_resolver import (
resolve_utility_fallback_candidates,
@@ -5358,7 +5358,7 @@ def setup_email_routes():
_add(_d_url, _d_model, _d_headers)
except Exception:
pass
# Configured fallback chains last.
# Active Utility fallbacks, then the retired default hook.
for cand in resolve_utility_fallback_candidates(owner=owner) or []:
_add(*cand)
for cand in resolve_chat_fallback_candidates(owner=owner) or []:
+2 -32
View File
@@ -46,10 +46,11 @@ _ENDPOINT_SETTING_FIELDS = {
}
_ENDPOINT_FALLBACK_FIELDS = {
"default_model_fallbacks": "Default Model Fallbacks",
"utility_model_fallbacks": "Utility Model Fallbacks",
"vision_model_fallbacks": "Vision Model Fallbacks",
}
# `default_model_fallbacks` is intentionally absent. The legacy data remains
# stored as-is even when an endpoint is removed, but no longer affects routing.
def _speech_settings_using_endpoint(settings: dict, ep_id: str) -> list:
@@ -2437,7 +2438,6 @@ def setup_model_routes(model_discovery):
_user_prefs = _load_for_user(_user) or {}
ep_id = (_user_prefs.get("default_endpoint_id") or "").strip()
model = (_user_prefs.get("default_model") or "").strip()
_fallbacks = _user_prefs.get("default_model_fallbacks") or []
# If user has no personal default, fall back to global default
# But only based on the "share_defaults_with_users" flag
# (only if share_defaults_with_users is enabled)
@@ -2446,12 +2446,9 @@ def setup_model_routes(model_discovery):
ep_id = settings.get("default_endpoint_id", "")
if not model:
model = settings.get("default_model", "")
if not _fallbacks:
_fallbacks = settings.get("default_model_fallbacks") or []
else:
ep_id = settings.get("default_endpoint_id", "")
model = settings.get("default_model", "")
_fallbacks = settings.get("default_model_fallbacks") or []
db = SessionLocal()
try:
ep = None
@@ -2466,33 +2463,6 @@ def setup_model_routes(model_discovery):
if _user and not _is_admin:
ep_q = owner_filter(ep_q, ModelEndpoint, _user)
ep = ep_q.first()
# Configured fallback chain — when the chosen default endpoint is
# gone/disabled, honor the user's configured `default_model_fallbacks`
# in order BEFORE arbitrarily grabbing the first enabled endpoint.
# (Previously this jumped straight to "first enabled", which is why
# deleting/changing the main endpoint silently reassigned the default
# chat to some unrelated endpoint instead of the fallback.)
if not ep:
for entry in _fallbacks:
if not isinstance(entry, dict):
continue
fid = (entry.get("endpoint_id") or "").strip()
if not fid:
continue
cand_q = db.query(ModelEndpoint).filter(
ModelEndpoint.id == fid, ModelEndpoint.is_enabled == True
)
if _user and not _is_admin:
cand_q = owner_filter(cand_q, ModelEndpoint, _user)
cand = cand_q.first()
if cand:
ep = cand
# Use the fallback entry's model. Reset even when empty
# so we don't carry the prior endpoint's stale model onto
# this fallback — the cached-models lookup below then
# fills it from the fallback endpoint.
model = (entry.get("model") or "").strip()
break
# Last resort: first enabled endpoint owned by THIS user. Do not
# include null-owner/shared endpoints here: a brand-new user with
# no explicit default should not auto-open a pending chat using an