From 8b66bc88dfabc18d24a82d437fbcf97b29da88dd Mon Sep 17 00:00:00 2001 From: kshitijk4poor <82637225+kshitijk4poor@users.noreply.github.com> Date: Wed, 9 Sep 2026 18:27:36 +0530 Subject: [PATCH] refactor(models): OpenRouter disk cache reuses catalog TTL and atomic JSON cache helpers --- hermes_cli/models.py | 59 +++++++++++++++++++++----------------------- 1 file changed, 28 insertions(+), 31 deletions(-) diff --git a/hermes_cli/models.py b/hermes_cli/models.py index 89cc7c7ce1..96071d4cc0 100644 --- a/hermes_cli/models.py +++ b/hermes_cli/models.py @@ -147,49 +147,46 @@ def _custom_provider_ssl_context(base_url: str): # Process-lifetime picker lists refreshed from the live catalogs (see fetch_*_models). _openrouter_catalog_cache: list[tuple[str, str]] | None = None -# On-disk TTL for the curated OpenRouter picker catalog. The in-memory -# ``_openrouter_catalog_cache`` is per-process, so without a disk cache every -# cold picker open re-downloads the full ~686KB /api/v1/models catalog -# (~1.4-7s). Persisting the *curated* result (post-filter) lets fresh -# processes serve the picker from disk within the TTL instead. -_OPENROUTER_CATALOG_DISK_TTL = 3600.0 # 1h, matches provider-models cache +# The in-memory ``_openrouter_catalog_cache`` is per-process, so without a disk cache every cold +# picker open re-downloads the full ~686KB /api/v1/models catalog. The *curated* result +# (post-filter) is persisted under the same TTL the catalog manifest uses, so both layers go +# stale together. -def _openrouter_catalog_disk_path() -> "Path": - """Disk path for the persisted curated OpenRouter catalog.""" +def _openrouter_catalog_disk_ttl() -> float: + from hermes_cli.model_catalog import DEFAULT_TTL_MINUTES + + return DEFAULT_TTL_MINUTES * 60.0 + + +def _openrouter_catalog_disk_path() -> Path: from hermes_constants import get_hermes_home + return get_hermes_home() / "cache" / "openrouter_curated_catalog.json" def _read_openrouter_catalog_disk() -> list[tuple[str, str]] | None: - """Return the last-known-good curated catalog from disk if fresh, else None.""" - try: - path = _openrouter_catalog_disk_path() - with open(path, encoding="utf-8") as fh: - obj = json.load(fh) - if time.time() - float(obj.get("fetched_at", 0)) > _OPENROUTER_CATALOG_DISK_TTL: - return None - items = obj.get("curated") - if not isinstance(items, list): - return None - out: list[tuple[str, str]] = [] - for it in items: - if isinstance(it, (list, tuple)) and len(it) == 2: - out.append((str(it[0]), str(it[1]))) - return out or None - except Exception: + """Fresh curated catalog from disk, or None (missing, corrupt, expired, or empty).""" + obj = _read_json_cache(_openrouter_catalog_disk_path()) + if obj is None: return None + try: + if time.time() - float(obj.get("fetched_at", 0)) > _openrouter_catalog_disk_ttl(): + return None + except (TypeError, ValueError): + return None + items = obj.get("curated") + if not isinstance(items, list): + return None + out = [(str(it[0]), str(it[1])) for it in items if isinstance(it, (list, tuple)) and len(it) == 2] + return out or None def _write_openrouter_catalog_disk(curated: list[tuple[str, str]]) -> None: - """Persist the curated catalog so the next cold picker open is instant.""" try: - path = _openrouter_catalog_disk_path() - path.parent.mkdir(parents=True, exist_ok=True) - tmp = path.with_suffix(".tmp") - with open(tmp, "w", encoding="utf-8") as fh: - json.dump({"fetched_at": time.time(), "curated": [list(c) for c in curated]}, fh) - os.replace(tmp, path) + _write_json_cache( + _openrouter_catalog_disk_path(), + {"fetched_at": time.time(), "curated": [list(c) for c in curated]}) except Exception as exc: logger.debug("openrouter curated catalog disk write failed: %s", exc) _ai_gateway_catalog_cache: list[tuple[str, str]] | None = None