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:
nesquena-hermes
2026-06-20 19:25:51 +00:00
parent fbafe1e623
commit 85e3be6a42
2 changed files with 60 additions and 2 deletions
+39 -2
View File
@@ -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,