fix: invalidate model cache on catalog changes

This commit is contained in:
Michael Lam 2026-05-16 22:23:24 -07:00
parent 603183a301
commit cdbb785037
3 changed files with 76 additions and 1 deletions

View file

@ -2,6 +2,10 @@
## [Unreleased]
### Fixed
- **PR #2445** by @Michaelyklam (fixes #2443) — `/api/models` now fingerprints model-catalog inputs as part of its persisted cache metadata, so server-side catalog additions and Codex local catalog changes invalidate `models_cache.json` immediately instead of waiting for the 24-hour TTL or manual cache deletion.
## [v0.51.82] — 2026-05-17 — Release BF (stage-375 — 2-PR batch — table renderer pipe protection + Catppuccin appearance skin)
### Added

View file

@ -11,6 +11,7 @@ Discovery order for all paths:
import collections
import copy
import hashlib
import json
import logging
import os
@ -2205,11 +2206,45 @@ def _models_cache_file_fingerprint(path: Path) -> dict:
return fingerprint
def _models_cache_catalog_fingerprint() -> dict:
"""Return non-secret model-catalog identity metadata for cache invalidation.
The /api/models payload is not only a function of user config/auth files.
It also depends on the provider/model catalog baked into this module and on
small local catalogs such as Codex's models_cache.json. Keep this cheap and
deterministic so a server restart after catalog changes does not keep
serving an otherwise-valid persisted models_cache.json until the 24h TTL
expires (#2443).
"""
catalog_payload = {
"provider_models": _PROVIDER_MODELS,
"provider_display": _PROVIDER_DISPLAY,
}
try:
encoded = json.dumps(
catalog_payload,
sort_keys=True,
separators=(",", ":"),
ensure_ascii=True,
default=str,
).encode("utf-8")
provider_catalog_sha = hashlib.sha256(encoded).hexdigest()
except Exception:
provider_catalog_sha = "unavailable"
codex_home = Path(os.getenv("CODEX_HOME", "").strip() or (HOME / ".codex")).expanduser()
return {
"provider_catalog_sha256": provider_catalog_sha,
"codex_models_cache": _models_cache_file_fingerprint(codex_home / "models_cache.json"),
}
def _models_cache_source_fingerprint() -> dict:
"""Return the current config/auth-store fingerprint for /api/models cache."""
"""Return the current config/auth/catalog fingerprint for /api/models cache."""
return {
"config_yaml": _models_cache_file_fingerprint(_get_config_path()),
"auth_json": _models_cache_file_fingerprint(_get_auth_store_path()),
"catalog": _models_cache_catalog_fingerprint(),
}

View file

@ -142,3 +142,39 @@ def test_disk_models_cache_still_loads_when_auth_and_config_sources_are_unchange
result = config.get_available_models()
assert result == fresh_opencode
def test_memory_models_cache_invalidates_when_static_catalog_changes(tmp_path, monkeypatch):
_configure_isolated_sources(tmp_path, monkeypatch, "opencode-go")
stale_opencode = _valid_models_cache("opencode-go", "glm-5.1")
with config._available_models_cache_lock:
config._available_models_cache = stale_opencode
config._available_models_cache_ts = time.monotonic()
config._available_models_cache_source_fingerprint = config._models_cache_source_fingerprint()
updated_models = list(config._PROVIDER_MODELS["opencode-go"])
updated_models.append({"id": "new-catalog-model", "label": "New Catalog Model"})
monkeypatch.setitem(config._PROVIDER_MODELS, "opencode-go", updated_models)
result = config.get_available_models()
opencode_group = next(g for g in result["groups"] if g.get("provider_id") == "opencode-go")
assert any(m.get("id") == "new-catalog-model" for m in opencode_group["models"])
def test_disk_models_cache_invalidates_when_static_catalog_changes(tmp_path, monkeypatch):
_configure_isolated_sources(tmp_path, monkeypatch, "opencode-go")
stale_opencode = _valid_models_cache("opencode-go", "glm-5.1")
config._save_models_cache_to_disk(stale_opencode)
assert config._models_cache_path.exists()
updated_models = list(config._PROVIDER_MODELS["opencode-go"])
updated_models.append({"id": "new-disk-catalog-model", "label": "New Disk Catalog Model"})
monkeypatch.setitem(config._PROVIDER_MODELS, "opencode-go", updated_models)
_reset_memory_cache()
result = config.get_available_models()
assert result != stale_opencode
opencode_group = next(g for g in result["groups"] if g.get("provider_id") == "opencode-go")
assert any(m.get("id") == "new-disk-catalog-model" for m in opencode_group["models"])