mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-06-17 02:05:22 -04:00
fix(models): clear deleted endpoint fallback refs (#1207)
This commit is contained in:
+82
-22
@@ -33,6 +33,19 @@ _SPEECH_ENDPOINT_SETTINGS = (
|
|||||||
("stt_provider", "stt_model", "base", "Speech to Text"),
|
("stt_provider", "stt_model", "base", "Speech to Text"),
|
||||||
)
|
)
|
||||||
|
|
||||||
|
_ENDPOINT_SETTING_FIELDS = {
|
||||||
|
"default_endpoint_id": ("default_model", "Default Model"),
|
||||||
|
"utility_endpoint_id": ("utility_model", "Utility Model"),
|
||||||
|
"research_endpoint_id": ("research_model", "Deep Research"),
|
||||||
|
"task_endpoint_id": ("task_model", "Background Tasks"),
|
||||||
|
}
|
||||||
|
|
||||||
|
_ENDPOINT_FALLBACK_FIELDS = {
|
||||||
|
"default_model_fallbacks": "Default Model Fallbacks",
|
||||||
|
"utility_model_fallbacks": "Utility Model Fallbacks",
|
||||||
|
"vision_model_fallbacks": "Vision Model Fallbacks",
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
def _speech_settings_using_endpoint(settings: dict, ep_id: str) -> list:
|
def _speech_settings_using_endpoint(settings: dict, ep_id: str) -> list:
|
||||||
"""Return speech settings that reference a model endpoint."""
|
"""Return speech settings that reference a model endpoint."""
|
||||||
@@ -56,6 +69,58 @@ def _clear_speech_settings_for_endpoint(settings: dict, ep_id: str) -> list:
|
|||||||
return cleared
|
return cleared
|
||||||
|
|
||||||
|
|
||||||
|
def _endpoint_settings_using_endpoint(settings: dict, ep_id: str, *, include_speech: bool = False) -> list:
|
||||||
|
"""Return labels for settings and fallback chains that reference an endpoint."""
|
||||||
|
affected = []
|
||||||
|
for ep_key, (_, label) in _ENDPOINT_SETTING_FIELDS.items():
|
||||||
|
if (settings.get(ep_key) or "") == ep_id:
|
||||||
|
affected.append(label)
|
||||||
|
for fallback_key, label in _ENDPOINT_FALLBACK_FIELDS.items():
|
||||||
|
chain = settings.get(fallback_key) or []
|
||||||
|
if any(isinstance(entry, dict) and (entry.get("endpoint_id") or "") == ep_id for entry in chain):
|
||||||
|
affected.append(label)
|
||||||
|
if include_speech:
|
||||||
|
affected.extend(_speech_settings_using_endpoint(settings, ep_id))
|
||||||
|
return affected
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_endpoint_settings_for_endpoint(settings: dict, ep_id: str, *, include_speech: bool = False) -> list:
|
||||||
|
"""Remove an endpoint from direct settings and model fallback chains."""
|
||||||
|
cleared = []
|
||||||
|
for ep_key, (model_key, label) in _ENDPOINT_SETTING_FIELDS.items():
|
||||||
|
if (settings.get(ep_key) or "") == ep_id:
|
||||||
|
settings[ep_key] = ""
|
||||||
|
settings[model_key] = ""
|
||||||
|
cleared.append(label)
|
||||||
|
for fallback_key, label in _ENDPOINT_FALLBACK_FIELDS.items():
|
||||||
|
chain = settings.get(fallback_key)
|
||||||
|
if not isinstance(chain, list):
|
||||||
|
continue
|
||||||
|
kept = [
|
||||||
|
entry for entry in chain
|
||||||
|
if not (isinstance(entry, dict) and (entry.get("endpoint_id") or "") == ep_id)
|
||||||
|
]
|
||||||
|
if len(kept) != len(chain):
|
||||||
|
settings[fallback_key] = kept
|
||||||
|
cleared.append(label)
|
||||||
|
if include_speech:
|
||||||
|
cleared.extend(_clear_speech_settings_for_endpoint(settings, ep_id))
|
||||||
|
return cleared
|
||||||
|
|
||||||
|
|
||||||
|
def _clear_user_pref_endpoint_refs(all_prefs: dict, ep_id: str) -> int:
|
||||||
|
"""Remove endpoint references from scoped or legacy-flat user preferences."""
|
||||||
|
if not isinstance(all_prefs, dict):
|
||||||
|
return 0
|
||||||
|
users = all_prefs.get("_users")
|
||||||
|
pref_sets = users.values() if isinstance(users, dict) else [all_prefs]
|
||||||
|
cleared_users = 0
|
||||||
|
for prefs in pref_sets:
|
||||||
|
if isinstance(prefs, dict) and _clear_endpoint_settings_for_endpoint(prefs, ep_id):
|
||||||
|
cleared_users += 1
|
||||||
|
return cleared_users
|
||||||
|
|
||||||
|
|
||||||
# Loopback hosts a user might type for a local model server (LM Studio,
|
# Loopback hosts a user might type for a local model server (LM Studio,
|
||||||
# llama.cpp, vLLM, …). Inside Docker these point at the *container*, not the
|
# llama.cpp, vLLM, …). Inside Docker these point at the *container*, not the
|
||||||
# host the server actually runs on.
|
# host the server actually runs on.
|
||||||
@@ -1454,38 +1519,31 @@ def setup_model_routes(model_discovery):
|
|||||||
finally:
|
finally:
|
||||||
db.close()
|
db.close()
|
||||||
|
|
||||||
# ── Settings fields that store an endpoint ID ──
|
|
||||||
_EP_SETTING_FIELDS = {
|
|
||||||
"default_endpoint_id": ("default_model", "Default Model"),
|
|
||||||
"utility_endpoint_id": ("utility_model", "Utility Model"),
|
|
||||||
"research_endpoint_id": ("research_model", "Deep Research"),
|
|
||||||
"task_endpoint_id": ("task_model", "Background Tasks"),
|
|
||||||
}
|
|
||||||
|
|
||||||
def _settings_using_endpoint(ep_id: str) -> list:
|
def _settings_using_endpoint(ep_id: str) -> list:
|
||||||
"""Return human-readable labels for settings that reference this endpoint."""
|
"""Return human-readable labels for settings that reference this endpoint."""
|
||||||
settings = _load_settings()
|
return _endpoint_settings_using_endpoint(_load_settings(), ep_id, include_speech=True)
|
||||||
affected = []
|
|
||||||
for ep_key, (_, label) in _EP_SETTING_FIELDS.items():
|
|
||||||
if (settings.get(ep_key) or "") == ep_id:
|
|
||||||
affected.append(label)
|
|
||||||
affected.extend(_speech_settings_using_endpoint(settings, ep_id))
|
|
||||||
return affected
|
|
||||||
|
|
||||||
def _clear_settings_for_endpoint(ep_id: str) -> list:
|
def _clear_settings_for_endpoint(ep_id: str) -> list:
|
||||||
"""Clear all settings that reference this endpoint. Returns list of cleared labels."""
|
"""Clear all settings that reference this endpoint. Returns list of cleared labels."""
|
||||||
settings = _load_settings()
|
settings = _load_settings()
|
||||||
cleared = []
|
cleared = _clear_endpoint_settings_for_endpoint(settings, ep_id, include_speech=True)
|
||||||
for ep_key, (model_key, label) in _EP_SETTING_FIELDS.items():
|
|
||||||
if (settings.get(ep_key) or "") == ep_id:
|
|
||||||
settings[ep_key] = ""
|
|
||||||
settings[model_key] = ""
|
|
||||||
cleared.append(label)
|
|
||||||
cleared.extend(_clear_speech_settings_for_endpoint(settings, ep_id))
|
|
||||||
if cleared:
|
if cleared:
|
||||||
_save_settings(settings)
|
_save_settings(settings)
|
||||||
return cleared
|
return cleared
|
||||||
|
|
||||||
|
def _clear_user_prefs_for_endpoint(ep_id: str) -> int:
|
||||||
|
"""Clear per-user endpoint selections and fallback chains."""
|
||||||
|
try:
|
||||||
|
from routes.prefs_routes import _load as _load_prefs, _save as _save_prefs
|
||||||
|
all_prefs = _load_prefs()
|
||||||
|
cleared_users = _clear_user_pref_endpoint_refs(all_prefs, ep_id)
|
||||||
|
if cleared_users:
|
||||||
|
_save_prefs(all_prefs)
|
||||||
|
return cleared_users
|
||||||
|
except Exception as e:
|
||||||
|
logger.warning("Failed to clear user prefs for endpoint %s: %s", ep_id, e)
|
||||||
|
return 0
|
||||||
|
|
||||||
def _session_uses_endpoint_url(session_url: str, base_url: str) -> bool:
|
def _session_uses_endpoint_url(session_url: str, base_url: str) -> bool:
|
||||||
if not session_url or not base_url:
|
if not session_url or not base_url:
|
||||||
return False
|
return False
|
||||||
@@ -1550,6 +1608,7 @@ def setup_model_routes(model_discovery):
|
|||||||
raise HTTPException(404, "Endpoint not found")
|
raise HTTPException(404, "Endpoint not found")
|
||||||
# Clean up any settings that reference this endpoint
|
# Clean up any settings that reference this endpoint
|
||||||
cleared = _clear_settings_for_endpoint(ep_id)
|
cleared = _clear_settings_for_endpoint(ep_id)
|
||||||
|
cleared_user_preferences = _clear_user_prefs_for_endpoint(ep_id)
|
||||||
cleared_sessions = _clear_sessions_for_endpoint(db, ep.base_url)
|
cleared_sessions = _clear_sessions_for_endpoint(db, ep.base_url)
|
||||||
cleared_loaded_sessions = _clear_loaded_sessions_for_endpoint(ep.base_url)
|
cleared_loaded_sessions = _clear_loaded_sessions_for_endpoint(ep.base_url)
|
||||||
db.delete(ep)
|
db.delete(ep)
|
||||||
@@ -1559,6 +1618,7 @@ def setup_model_routes(model_discovery):
|
|||||||
return {
|
return {
|
||||||
"deleted": True,
|
"deleted": True,
|
||||||
"cleared_settings": cleared,
|
"cleared_settings": cleared,
|
||||||
|
"cleared_user_preferences": cleared_user_preferences,
|
||||||
"cleared_sessions": cleared_sessions,
|
"cleared_sessions": cleared_sessions,
|
||||||
"cleared_loaded_sessions": cleared_loaded_sessions,
|
"cleared_loaded_sessions": cleared_loaded_sessions,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,6 +35,9 @@ from routes.model_routes import (
|
|||||||
_truthy,
|
_truthy,
|
||||||
_speech_settings_using_endpoint,
|
_speech_settings_using_endpoint,
|
||||||
_clear_speech_settings_for_endpoint,
|
_clear_speech_settings_for_endpoint,
|
||||||
|
_endpoint_settings_using_endpoint,
|
||||||
|
_clear_endpoint_settings_for_endpoint,
|
||||||
|
_clear_user_pref_endpoint_refs,
|
||||||
_PROVIDER_CURATED,
|
_PROVIDER_CURATED,
|
||||||
)
|
)
|
||||||
from src.llm_core import ANTHROPIC_MODELS
|
from src.llm_core import ANTHROPIC_MODELS
|
||||||
@@ -67,6 +70,74 @@ def test_clear_speech_endpoint_settings_resets_tts_and_stt():
|
|||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
|
def test_endpoint_cleanup_removes_primary_and_fallback_references():
|
||||||
|
settings = {
|
||||||
|
"default_endpoint_id": "dead",
|
||||||
|
"default_model": "primary",
|
||||||
|
"default_model_fallbacks": [
|
||||||
|
{"endpoint_id": "dead", "model": "fallback-a"},
|
||||||
|
{"endpoint_id": "keep", "model": "fallback-b"},
|
||||||
|
],
|
||||||
|
"utility_model_fallbacks": [{"endpoint_id": "dead", "model": "utility"}],
|
||||||
|
"vision_model_fallbacks": [{"endpoint_id": "dead", "model": "vision"}],
|
||||||
|
"stt_provider": "endpoint:dead",
|
||||||
|
"stt_model": "whisper",
|
||||||
|
}
|
||||||
|
|
||||||
|
assert _endpoint_settings_using_endpoint(settings, "dead", include_speech=True) == [
|
||||||
|
"Default Model",
|
||||||
|
"Default Model Fallbacks",
|
||||||
|
"Utility Model Fallbacks",
|
||||||
|
"Vision Model Fallbacks",
|
||||||
|
"Speech to Text",
|
||||||
|
]
|
||||||
|
assert _clear_endpoint_settings_for_endpoint(settings, "dead", include_speech=True) == [
|
||||||
|
"Default Model",
|
||||||
|
"Default Model Fallbacks",
|
||||||
|
"Utility Model Fallbacks",
|
||||||
|
"Vision Model Fallbacks",
|
||||||
|
"Speech to Text",
|
||||||
|
]
|
||||||
|
assert settings["default_endpoint_id"] == ""
|
||||||
|
assert settings["default_model"] == ""
|
||||||
|
assert settings["default_model_fallbacks"] == [
|
||||||
|
{"endpoint_id": "keep", "model": "fallback-b"},
|
||||||
|
]
|
||||||
|
assert settings["utility_model_fallbacks"] == []
|
||||||
|
assert settings["vision_model_fallbacks"] == []
|
||||||
|
assert settings["stt_provider"] == "disabled"
|
||||||
|
assert settings["stt_model"] == "base"
|
||||||
|
|
||||||
|
|
||||||
|
def test_endpoint_cleanup_updates_scoped_and_legacy_user_prefs():
|
||||||
|
scoped = {
|
||||||
|
"_users": {
|
||||||
|
"alice": {
|
||||||
|
"utility_endpoint_id": "dead",
|
||||||
|
"utility_model": "utility",
|
||||||
|
"vision_model_fallbacks": [{"endpoint_id": "dead", "model": "vision"}],
|
||||||
|
},
|
||||||
|
"bob": {
|
||||||
|
"default_endpoint_id": "keep",
|
||||||
|
"default_model": "chat",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
assert _clear_user_pref_endpoint_refs(scoped, "dead") == 1
|
||||||
|
assert scoped["_users"]["alice"] == {
|
||||||
|
"utility_endpoint_id": "",
|
||||||
|
"utility_model": "",
|
||||||
|
"vision_model_fallbacks": [],
|
||||||
|
}
|
||||||
|
assert scoped["_users"]["bob"]["default_endpoint_id"] == "keep"
|
||||||
|
|
||||||
|
legacy = {
|
||||||
|
"default_model_fallbacks": [{"endpoint_id": "dead", "model": "chat"}],
|
||||||
|
}
|
||||||
|
assert _clear_user_pref_endpoint_refs(legacy, "dead") == 1
|
||||||
|
assert legacy["default_model_fallbacks"] == []
|
||||||
|
|
||||||
|
|
||||||
# ── _match_provider_curated ──
|
# ── _match_provider_curated ──
|
||||||
|
|
||||||
class TestMatchProviderCurated:
|
class TestMatchProviderCurated:
|
||||||
|
|||||||
Reference in New Issue
Block a user