mirror of
https://github.com/nesquena/hermes-webui.git
synced 2026-07-07 08:10:45 +00:00
fix(#4555): preserve /api/models aliases contract on disk-cache fallback
Codex ship-gate (SILENT): both disk loaders dropped `aliases` (the save path never persisted them), so a disk-cache hit — strict cold-path OR the new stale fallback — served /api/models with empty aliases, silently breaking `/model <alias>` slash-command resolution (static/commands.js resolves slash aliases only from /api/models.aliases). Add _model_aliases_from_config() helper and reconstruct aliases from current config in BOTH loaders (fixes the pre-existing strict-loader gap too). Adds a regression test. Co-authored-by: starship-s <starship-s@users.noreply.github.com>
This commit is contained in:
+39
-2
@@ -4601,17 +4601,46 @@ def _load_models_cache_from_disk() -> dict | None:
|
||||
if not _is_loadable_disk_cache(cache):
|
||||
return None
|
||||
# Strip the disk-only metadata before returning, so the in-memory
|
||||
# cache shape stays exactly what the rest of the code expects.
|
||||
# cache shape stays exactly what the rest of the code expects. The
|
||||
# disk save path does not persist `aliases`, so reconstruct them from
|
||||
# current config to keep the /api/models.aliases contract intact (a
|
||||
# disk-cache hit must not silently drop `/model <alias>` resolution).
|
||||
return {
|
||||
"active_provider": cache["active_provider"],
|
||||
"default_model": cache["default_model"],
|
||||
"configured_model_badges": cache["configured_model_badges"],
|
||||
"groups": cache["groups"],
|
||||
"aliases": (
|
||||
cache["aliases"]
|
||||
if isinstance(cache.get("aliases"), dict)
|
||||
else _model_aliases_from_config()
|
||||
),
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
|
||||
def _model_aliases_from_config() -> dict[str, str]:
|
||||
"""Build the normalized model-alias map from current config.
|
||||
|
||||
Mirrors the alias construction used by the live and static catalog paths so
|
||||
the `/api/models.aliases` contract is consistent across every catalog source
|
||||
(live, static, and the stale-disk fallback, which can't read aliases from a
|
||||
disk cache that never persisted them).
|
||||
"""
|
||||
try:
|
||||
raw_aliases = cfg.get("model", {}).get("aliases", {})
|
||||
if isinstance(raw_aliases, dict):
|
||||
return {
|
||||
str(k).strip(): str(v).strip()
|
||||
for k, v in raw_aliases.items()
|
||||
if k and v
|
||||
}
|
||||
except Exception:
|
||||
pass
|
||||
return {}
|
||||
|
||||
|
||||
def _load_stale_models_cache_from_disk() -> dict | None:
|
||||
"""Load a shape-valid stale /api/models disk cache for timeout fallback only.
|
||||
|
||||
@@ -4636,12 +4665,20 @@ def _load_stale_models_cache_from_disk() -> dict | None:
|
||||
if cache.get("_schema_version") != _MODELS_CACHE_SCHEMA_VERSION:
|
||||
return None
|
||||
aliases = cache.get("aliases")
|
||||
if not isinstance(aliases, dict):
|
||||
# The disk cache save path does not persist `aliases`, so a cache
|
||||
# read back from disk lacks them. Defaulting to {} would silently
|
||||
# break `/model <alias>` slash-command resolution (static/commands.js
|
||||
# resolves slash aliases only from /api/models.aliases) for the
|
||||
# duration of the over-budget stale fallback. Reconstruct from
|
||||
# current config, mirroring the live/static catalog alias build.
|
||||
aliases = _model_aliases_from_config()
|
||||
return {
|
||||
"active_provider": cache["active_provider"],
|
||||
"default_model": cache["default_model"],
|
||||
"configured_model_badges": cache["configured_model_badges"],
|
||||
"groups": cache["groups"],
|
||||
"aliases": aliases if isinstance(aliases, dict) else {},
|
||||
"aliases": aliases,
|
||||
}
|
||||
except Exception:
|
||||
return None
|
||||
|
||||
@@ -401,6 +401,27 @@ def test_load_stale_models_cache_from_disk_rejects_cross_schema(
|
||||
assert cfg._load_stale_models_cache_from_disk() is None
|
||||
|
||||
|
||||
def test_load_stale_models_cache_reconstructs_aliases_from_config(
|
||||
monkeypatch,
|
||||
isolate_models_catalog_state,
|
||||
):
|
||||
"""When the disk cache lacks aliases (the save path never persisted them),
|
||||
the stale loader must reconstruct them from current config so /model <alias>
|
||||
slash-command resolution keeps working during the over-budget fallback."""
|
||||
payload = _build_stale_disk_cache_payload()
|
||||
payload.pop("aliases", None)
|
||||
models_cache_path = isolate_models_catalog_state["models_cache_path"]
|
||||
monkeypatch.setattr(cfg, "_get_models_cache_path", lambda: models_cache_path)
|
||||
models_cache_path.write_text(json.dumps(payload), encoding="utf-8")
|
||||
monkeypatch.setattr(
|
||||
cfg, "cfg", {"model": {"aliases": {"fast": "ollama-cloud/chat-1"}}}, raising=False
|
||||
)
|
||||
|
||||
stale = cfg._load_stale_models_cache_from_disk()
|
||||
assert stale is not None
|
||||
assert stale["aliases"] == {"fast": "ollama-cloud/chat-1"}
|
||||
|
||||
|
||||
def test_default_group_survives_only_as_emergency_last_resort(
|
||||
monkeypatch,
|
||||
isolate_models_catalog_state,
|
||||
|
||||
Reference in New Issue
Block a user