fix(nous): make "thinking off" stick on a cold start

Portal reasoning capabilities were held only in memory, so a process that
had not yet fetched them answered "unknown" — and on that answer the Nous
profile drops the disable rather than risk a 400. A short-lived process
(`hermes -p`, a cron job, a freshly booted gateway) is always in that
state, so every one of those runs silently ignored "thinking off" and
billed the user for reasoning they had turned off.

The parsed catalog is now mirrored to `cache/reasoning_caps.json`, keyed
by the URL it came from, and hydrated on a cold lookup without touching
the network. Every picker and pricing fetch already pulls that same
document, so they seed the mirror for free.

The catalog URL itself now resolves through the same ladder as the rest
of the Nous catalog reads (`NOUS_INFERENCE_BASE_URL` → credential base →
production) instead of being pinned to production, which had a staging
profile deciding the reasoning-mandatory question from prod's answers.
Keying the mirror by URL keeps those deployments apart.
This commit is contained in:
Brooklyn Nicholson
2026-08-19 23:19:27 -05:00
committed by brooklyn!
parent 6851841112
commit 9c0cd1add2
4 changed files with 491 additions and 72 deletions
+215 -70
View File
@@ -27,7 +27,7 @@ if TYPE_CHECKING:
from hermes_cli import __version__ as _HERMES_VERSION
from hermes_cli.urllib_security import open_credentialed_url, url_origin
from utils import base_url_host_matches
from utils import atomic_json_write, base_url_host_matches
logger = logging.getLogger(__name__)
@@ -1600,6 +1600,124 @@ _openrouter_reasoning_caps_cache: dict[str, Optional[dict[str, Any]]] | None = N
_openrouter_reasoning_caps_failed_at: float | None = None
# ── Disk mirror ────────────────────────────────────────────────────────
#
# The in-process caches are always cold in a short-lived process, and every
# consumer is on a hot path that must never block on HTTP — so without a disk
# copy, `hermes -p`, a cron job, or a freshly booted gateway answers
# "capability unknown" for its whole first turn and falls back to the
# conservative wire shape. Persisting the parsed catalog makes every run after
# the first correct from its first turn.
#
# One file holds every catalog, keyed by the URL it came from: OpenRouter and
# the Nous Portal list different models, and a staging Portal must not answer
# for production.
_REASONING_CAPS_DISK_TTL_SECONDS = 24 * 3600
def _reasoning_caps_disk_path() -> Path:
from hermes_constants import get_hermes_home
return get_hermes_home() / "cache" / "reasoning_caps.json"
def _read_reasoning_caps_disk() -> dict[str, Any]:
try:
with _reasoning_caps_disk_path().open(encoding="utf-8") as fh:
data = json.load(fh)
except Exception:
return {}
return data if isinstance(data, dict) else {}
def _load_reasoning_caps_disk(
url: str,
) -> tuple[Optional[dict[str, Optional[dict[str, Any]]]], float]:
"""Return ``(caps, age_seconds)`` for *url*, or ``(None, 0.0)``."""
entry = _read_reasoning_caps_disk().get(url)
if not isinstance(entry, dict):
return None, 0.0
caps = entry.get("caps")
if not isinstance(caps, dict) or not caps:
return None, 0.0
try:
age = max(0.0, time.time() - float(entry.get("ts") or 0))
except (TypeError, ValueError):
age = float(_REASONING_CAPS_DISK_TTL_SECONDS)
return {str(mid): model_caps for mid, model_caps in caps.items()}, age
def _save_reasoning_caps_disk(
url: str, caps: dict[str, Optional[dict[str, Any]]]
) -> None:
"""Merge *url*'s catalog into the shared disk mirror, atomically."""
try:
data = _read_reasoning_caps_disk()
data[url] = {"ts": time.time(), "caps": caps}
path = _reasoning_caps_disk_path()
path.parent.mkdir(parents=True, exist_ok=True)
atomic_json_write(path, data, indent=0, separators=(",", ":"))
except Exception as exc:
logger.debug("Failed to save reasoning-caps disk cache: %s", exc)
def _warm_reasoning_caps_async(refresh) -> None:
"""Run *refresh* in a background thread. Fire-and-forget.
Called from hot paths that found the cache cold or the disk copy stale, so
the next call — or, via the disk mirror, the next process — benefits
without this turn ever blocking on HTTP. Callers own the once-per-process
guard; the fetch keeps its own failure TTL. Skipped under pytest, where a
mid-suite background fetch would make cache state, and therefore test
behavior, timing-dependent.
"""
if os.environ.get("PYTEST_CURRENT_TEST"):
return
threading.Thread(
target=refresh, name="reasoning-caps-warm", daemon=True
).start()
def _hydrate_reasoning_caps_from_disk(url: str, refresh):
"""The disk copy of *url*'s catalog, queueing *refresh* when it's stale.
A copy past its TTL is still returned — a stale verdict beats no verdict,
and reasoning capabilities change rarely — with a background refresh so
the next run is current.
"""
caps, age = _load_reasoning_caps_disk(url)
if caps is None:
return None
if age >= _REASONING_CAPS_DISK_TTL_SECONDS:
_warm_reasoning_caps_async(refresh)
return caps
def _seed_reasoning_caps(
url: str, items: Any
) -> Optional[dict[str, Optional[dict[str, Any]]]]:
"""Parse a ``/v1/models`` ``data`` array and mirror it for *url*.
Takes the payload rather than fetching it, so the picker and pricing
fetches — which pull the same document a capability fetch would — leave the
mirror warm at no network cost. None when the array has no usable entries,
which callers remember as a failure rather than caching as empty.
"""
if not isinstance(items, list):
return None
caps_by_id: dict[str, Optional[dict[str, Any]]] = {}
for item in items:
if not isinstance(item, dict):
continue
mid = str(item.get("id") or "").strip()
if not mid:
continue
caps_by_id[mid] = parse_openrouter_reasoning_capabilities(item)
if not caps_by_id:
return None
_save_reasoning_caps_disk(url, caps_by_id)
return caps_by_id
def _fetch_reasoning_caps_catalog(
url: str, timeout: float
) -> Optional[dict[str, Optional[dict[str, Any]]]]:
@@ -1619,39 +1737,32 @@ def _fetch_reasoning_caps_catalog(
payload = json.loads(resp.read().decode())
except Exception:
return None
items = payload.get("data")
if not isinstance(items, list):
return None
caps_by_id: dict[str, Optional[dict[str, Any]]] = {}
for item in items:
if not isinstance(item, dict):
continue
mid = str(item.get("id") or "").strip()
if not mid:
continue
caps_by_id[mid] = parse_openrouter_reasoning_capabilities(item)
return caps_by_id or None
return _seed_reasoning_caps(url, payload.get("data"))
def _fetch_openrouter_reasoning_caps(timeout: float = 6.0) -> Optional[dict[str, Optional[dict[str, Any]]]]:
_OPENROUTER_CATALOG_URL = "https://openrouter.ai/api/v1/models"
def _fetch_openrouter_reasoning_caps(
timeout: float = 6.0, *, force: bool = False
) -> Optional[dict[str, Optional[dict[str, Any]]]]:
"""Fetch + cache per-model reasoning capabilities from the live catalog.
Returns None (without poisoning the cache) when the catalog is
unreachable so callers can retry later and fall back in the meantime.
Failed fetches are remembered for 60 seconds so hot per-turn callers
don't pay an HTTP round-trip on every call while offline.
don't pay an HTTP round-trip on every call while offline. *force* refetches
past a cache populated from the disk mirror.
"""
global _openrouter_reasoning_caps_cache, _openrouter_reasoning_caps_failed_at
if _openrouter_reasoning_caps_cache is not None:
if _openrouter_reasoning_caps_cache is not None and not force:
return _openrouter_reasoning_caps_cache
if (
_openrouter_reasoning_caps_failed_at is not None
and (time.monotonic() - _openrouter_reasoning_caps_failed_at) < 60
):
return None
caps_by_id = _fetch_reasoning_caps_catalog(
"https://openrouter.ai/api/v1/models", timeout
)
caps_by_id = _fetch_reasoning_caps_catalog(_OPENROUTER_CATALOG_URL, timeout)
if caps_by_id is None:
_openrouter_reasoning_caps_failed_at = time.monotonic()
return None
@@ -1659,6 +1770,10 @@ def _fetch_openrouter_reasoning_caps(timeout: float = 6.0) -> Optional[dict[str,
return caps_by_id
def _refresh_openrouter_reasoning_caps() -> None:
_fetch_openrouter_reasoning_caps(force=True)
def openrouter_model_reasoning_capabilities(
model_id: Optional[str],
*,
@@ -1679,14 +1794,15 @@ def openrouter_model_reasoning_capabilities(
By default this is a CACHE-ONLY lookup — safe on per-request hot paths
(never blocks on HTTP). The cache is populated for free whenever
``fetch_openrouter_models()`` runs (model picker, setup), by the
non-blocking ``warm_openrouter_reasoning_caps_async()`` warmer, or by
passing ``allow_fetch=True`` from non-latency-sensitive callers.
``fetch_openrouter_models()`` runs (model picker, setup), from the disk
mirror a previous run left behind, by the non-blocking
``warm_openrouter_reasoning_caps_async()`` warmer, or by passing
``allow_fetch=True`` from non-latency-sensitive callers.
"""
model = str(model_id or "").strip()
if not model:
return None
caps_by_id = _openrouter_reasoning_caps_cache
caps_by_id = _openrouter_caps_cached()
if caps_by_id is None and allow_fetch:
caps_by_id = _fetch_openrouter_reasoning_caps(timeout=timeout)
if caps_by_id is None:
@@ -1694,29 +1810,28 @@ def openrouter_model_reasoning_capabilities(
return caps_by_id.get(model)
_openrouter_caps_disk_checked = False
_openrouter_caps_warm_started = False
def warm_openrouter_reasoning_caps_async() -> None:
"""Warm the reasoning-capability cache in a background thread.
def _openrouter_caps_cached() -> Optional[dict[str, Optional[dict[str, Any]]]]:
"""Cache-only OpenRouter caps: memory, else the disk mirror. Never HTTP."""
global _openrouter_reasoning_caps_cache, _openrouter_caps_disk_checked
if _openrouter_reasoning_caps_cache is None and not _openrouter_caps_disk_checked:
_openrouter_caps_disk_checked = True
_openrouter_reasoning_caps_cache = _hydrate_reasoning_caps_from_disk(
_OPENROUTER_CATALOG_URL, _refresh_openrouter_reasoning_caps
)
return _openrouter_reasoning_caps_cache
Fire-and-forget: called from hot paths that found the cache cold so the
NEXT call benefits, without ever blocking a turn on HTTP. One warm
attempt per process (the fetch has its own 60s failure TTL). Skipped
under pytest — a mid-suite background fetch would make cache state, and
therefore test behavior, timing-dependent.
"""
def warm_openrouter_reasoning_caps_async() -> None:
"""Warm the OpenRouter reasoning-capability cache in the background."""
global _openrouter_caps_warm_started
if _openrouter_caps_warm_started or _openrouter_reasoning_caps_cache is not None:
return
if os.environ.get("PYTEST_CURRENT_TEST"):
if _openrouter_caps_warm_started or _openrouter_caps_cached() is not None:
return
_openrouter_caps_warm_started = True
threading.Thread(
target=_fetch_openrouter_reasoning_caps,
name="openrouter-reasoning-caps-warm",
daemon=True,
).start()
_warm_reasoning_caps_async(_refresh_openrouter_reasoning_caps)
# Nous Portal serves OpenRouter's catalog schema, so the same parser and
@@ -1724,22 +1839,32 @@ def warm_openrouter_reasoning_caps_async() -> None:
# list different models (and different capabilities for shared ids).
_nous_reasoning_caps_cache: dict[str, Optional[dict[str, Any]]] | None = None
_nous_reasoning_caps_failed_at: float | None = None
_nous_caps_warm_started = False
def _fetch_nous_reasoning_caps(timeout: float = 6.0) -> Optional[dict[str, Optional[dict[str, Any]]]]:
def nous_catalog_url() -> str:
"""The Portal ``/v1/models`` URL for the endpoint we actually talk to.
Resolved through the documented ladder rather than pinned to production —
``NOUS_INFERENCE_BASE_URL`` → resolved credential base → prod — so a
staging profile reads staging's capabilities. Reading prod's would answer
the reasoning-mandatory question for the wrong deployment.
"""
return f"{_resolve_nous_pricing_credentials()[1]}/v1/models"
def _fetch_nous_reasoning_caps(
timeout: float = 6.0, *, force: bool = False
) -> Optional[dict[str, Optional[dict[str, Any]]]]:
"""Nous Portal counterpart of :func:`_fetch_openrouter_reasoning_caps`."""
global _nous_reasoning_caps_cache, _nous_reasoning_caps_failed_at
if _nous_reasoning_caps_cache is not None:
if _nous_reasoning_caps_cache is not None and not force:
return _nous_reasoning_caps_cache
if (
_nous_reasoning_caps_failed_at is not None
and (time.monotonic() - _nous_reasoning_caps_failed_at) < 60
):
return None
caps_by_id = _fetch_reasoning_caps_catalog(
f"{_DEFAULT_NOUS_INFERENCE_BASE}/v1/models", timeout
)
caps_by_id = _fetch_reasoning_caps_catalog(nous_catalog_url(), timeout)
if caps_by_id is None:
_nous_reasoning_caps_failed_at = time.monotonic()
return None
@@ -1747,6 +1872,10 @@ def _fetch_nous_reasoning_caps(timeout: float = 6.0) -> Optional[dict[str, Optio
return caps_by_id
def _refresh_nous_reasoning_caps() -> None:
_fetch_nous_reasoning_caps(force=True)
def nous_model_reasoning_capabilities(
model_id: Optional[str],
*,
@@ -1762,7 +1891,7 @@ def nous_model_reasoning_capabilities(
model = str(model_id or "").strip()
if not model:
return None
caps_by_id = _nous_reasoning_caps_cache
caps_by_id = _nous_caps_cached()
if caps_by_id is None and allow_fetch:
caps_by_id = _fetch_nous_reasoning_caps(timeout=timeout)
if caps_by_id is None:
@@ -1770,19 +1899,33 @@ def nous_model_reasoning_capabilities(
return caps_by_id.get(model)
_nous_caps_disk_checked = False
_nous_caps_warm_started = False
def _nous_caps_cached() -> Optional[dict[str, Optional[dict[str, Any]]]]:
"""Cache-only Portal caps: memory, else the disk mirror. Never HTTP.
Guarded to one attempt per process because naming the catalog means
resolving Portal credentials, which can itself reach the network to
refresh a token — far too expensive for a caller that runs every turn.
"""
global _nous_reasoning_caps_cache, _nous_caps_disk_checked
if _nous_reasoning_caps_cache is None and not _nous_caps_disk_checked:
_nous_caps_disk_checked = True
_nous_reasoning_caps_cache = _hydrate_reasoning_caps_from_disk(
nous_catalog_url(), _refresh_nous_reasoning_caps
)
return _nous_reasoning_caps_cache
def warm_nous_reasoning_caps_async() -> None:
"""Nous Portal counterpart of :func:`warm_openrouter_reasoning_caps_async`."""
global _nous_caps_warm_started
if _nous_caps_warm_started or _nous_reasoning_caps_cache is not None:
return
if os.environ.get("PYTEST_CURRENT_TEST"):
if _nous_caps_warm_started or _nous_caps_cached() is not None:
return
_nous_caps_warm_started = True
threading.Thread(
target=_fetch_nous_reasoning_caps,
name="nous-reasoning-caps-warm",
daemon=True,
).start()
_warm_reasoning_caps_async(_refresh_nous_reasoning_caps)
# Canonical low→high ordering used for nearest-level clamping. Kept as an
@@ -1835,7 +1978,7 @@ def fetch_openrouter_models(
try:
req = urllib.request.Request(
"https://openrouter.ai/api/v1/models",
_OPENROUTER_CATALOG_URL,
headers={"Accept": "application/json"},
)
with _urlopen_model_catalog_request(req, timeout=timeout) as resp:
@@ -1856,16 +1999,14 @@ def fetch_openrouter_models(
continue
live_by_id[mid] = item
# Free warm-up for the reasoning-capability cache: this is the same
# payload _fetch_openrouter_reasoning_caps would fetch, so parse it once
# here and hot-path callers (openrouter_model_reasoning_capabilities)
# never need their own HTTP round-trip.
# Free warm-up for the reasoning-capability cache: this is the same payload
# _fetch_openrouter_reasoning_caps would fetch, so parse it once here and
# hot-path callers (openrouter_model_reasoning_capabilities) never need
# their own HTTP round-trip.
global _openrouter_reasoning_caps_cache
if _openrouter_reasoning_caps_cache is None and live_by_id:
_openrouter_reasoning_caps_cache = {
mid: parse_openrouter_reasoning_capabilities(item)
for mid, item in live_by_id.items()
}
seeded = _seed_reasoning_caps(_OPENROUTER_CATALOG_URL, live_items)
if _openrouter_reasoning_caps_cache is None and seeded is not None:
_openrouter_reasoning_caps_cache = seeded
curated: list[tuple[str, str]] = []
silent_default = get_preferred_silent_default_model("openrouter")
@@ -2208,6 +2349,11 @@ def fetch_models_with_pricing(
except Exception:
return _cache_catalog(cache_key, {})
# Same document the reasoning-capability fetch would pull, and every
# picker/pricing surface goes through here — mirror it so a later hot-path
# lookup (and the next process) has an answer without its own round-trip.
_seed_reasoning_caps(url, payload.get("data"))
result: dict[str, dict[str, Any]] = {}
for item in payload.get("data", []):
mid = item.get("id")
@@ -2342,6 +2488,10 @@ def _resolve_nous_pricing_credentials() -> tuple[str, str]:
pass
base_url = (env_base or creds_base or _DEFAULT_NOUS_INFERENCE_BASE).rstrip("/")
# Credential bases arrive with or without the ``/v1`` suffix. Callers
# append their own path, so hand back the bare origin.
if base_url.endswith("/v1"):
base_url = base_url[:-3]
return (api_key, base_url)
@@ -2365,14 +2515,9 @@ def get_pricing_for_provider(provider: str, *, force_refresh: bool = False) -> d
if normalized == "nous":
api_key, base_url = _resolve_nous_pricing_credentials()
if base_url:
# Nous base_url typically looks like https://inference-api.nousresearch.com/v1
# We need the part before /v1 for our fetch function
stripped = base_url.rstrip("/")
if stripped.endswith("/v1"):
stripped = stripped[:-3]
return fetch_models_with_pricing(
api_key=api_key,
base_url=stripped,
base_url=base_url,
force_refresh=force_refresh,
# Sale chrome (pricing.original) is Nous Portal-only.
include_sale_original=True,
@@ -36,10 +36,14 @@ _CATALOG = (
@pytest.fixture
def cold_cache(monkeypatch):
"""A freshly started process that has never mirrored a catalog to disk."""
import hermes_cli.models as models_mod
monkeypatch.setattr(models_mod, "_nous_reasoning_caps_cache", None)
monkeypatch.setattr(models_mod, "_nous_reasoning_caps_failed_at", None)
monkeypatch.setattr(models_mod, "_nous_caps_disk_checked", False)
monkeypatch.setattr(models_mod, "_nous_caps_warm_started", False)
models_mod._reasoning_caps_disk_path().unlink(missing_ok=True)
return models_mod
@@ -76,7 +80,24 @@ class TestNousModelReasoningCapabilities:
# urllib title-cases header names.
assert seen[0].get_header("User-agent")
assert "nousresearch.com" in seen[0].full_url
def test_catalog_read_follows_the_configured_endpoint(
self, cold_cache, monkeypatch
):
"""Capabilities come from the deployment we actually talk to.
Pinned to production, a staging profile would take its
reasoning-mandatory verdicts from a different deployment's catalog.
"""
from hermes_cli.models import nous_catalog_url
monkeypatch.setenv(
"NOUS_INFERENCE_BASE_URL", "https://staging.nousresearch.com/v1"
)
assert nous_catalog_url() == "https://staging.nousresearch.com/v1/models"
monkeypatch.delenv("NOUS_INFERENCE_BASE_URL")
assert nous_catalog_url().endswith("/v1/models")
def test_unlisted_and_empty_models_return_none(self, cold_cache, monkeypatch):
from hermes_cli.models import nous_model_reasoning_capabilities
@@ -0,0 +1,234 @@
"""The reasoning-capability disk mirror.
Every consumer of these capabilities sits on a per-request hot path that must
never block on HTTP, so a process whose in-memory cache is cold answers
"unknown" — and on that answer the Nous profile drops a "thinking off" disable
rather than risk a 400. A short-lived process (``hermes -p``, a cron job, a
freshly booted gateway) is ALWAYS cold, so without a disk copy that fallback is
the only behavior those runs ever get and the user keeps paying for reasoning
they turned off.
These tests pin the mirror that makes every run after the first correct from
its first turn.
"""
import json
import pytest
import hermes_cli.models as models_mod
_CATALOG = json.dumps({
"data": [
{
"id": "deepseek/deepseek-v4-pro",
"supported_parameters": ["reasoning", "tools"],
"reasoning": {"mandatory": False},
},
{
"id": "arcee-ai/trinity-large-thinking",
"supported_parameters": ["reasoning"],
"reasoning": {"mandatory": True},
},
]
}).encode()
def _response(body: bytes):
class _Resp:
def __enter__(self):
return self
def __exit__(self, exc_type, exc, tb):
return False
def read(self):
return body
return _Resp()
@pytest.fixture
def cold_process(monkeypatch):
"""Put the module in the state a freshly started process would be in."""
def _reset():
for name in (
"_nous_reasoning_caps_cache",
"_nous_reasoning_caps_failed_at",
"_openrouter_reasoning_caps_cache",
"_openrouter_reasoning_caps_failed_at",
):
monkeypatch.setattr(models_mod, name, None)
for name in (
"_nous_caps_disk_checked",
"_nous_caps_warm_started",
"_openrouter_caps_disk_checked",
"_openrouter_caps_warm_started",
):
monkeypatch.setattr(models_mod, name, False)
_reset()
return _reset
@pytest.fixture
def offline():
"""Fail the test if anything reaches for the network."""
def _boom(req, *, timeout):
raise AssertionError(f"must not fetch: {req.full_url}")
return _boom
def test_fetched_catalog_answers_a_later_process_offline(
cold_process, offline, monkeypatch
):
"""The whole point: run once online, and the next run starts out correct.
Without the mirror this second lookup is the reported bug — an unknown
verdict, and a silently-ignored "thinking off" for the whole turn.
"""
monkeypatch.setattr(
models_mod, "_urlopen_model_catalog_request",
lambda req, *, timeout: _response(_CATALOG),
)
assert models_mod.nous_model_reasoning_capabilities(
"deepseek/deepseek-v4-pro", allow_fetch=True
) is not None
cold_process()
monkeypatch.setattr(models_mod, "_urlopen_model_catalog_request", offline)
caps = models_mod.nous_model_reasoning_capabilities("deepseek/deepseek-v4-pro")
assert caps["mandatory"] is False
mandatory = models_mod.nous_model_reasoning_capabilities(
"arcee-ai/trinity-large-thinking"
)
assert mandatory["mandatory"] is True
def test_mirror_is_keyed_by_catalog_url(cold_process, offline, monkeypatch):
"""One catalog's verdicts must never answer for another's.
The Portal and OpenRouter list different models, and a staging Portal
answering for production would decide the reasoning-mandatory question for
the wrong deployment.
"""
monkeypatch.setattr(
models_mod, "_urlopen_model_catalog_request",
lambda req, *, timeout: _response(_CATALOG),
)
models_mod.nous_model_reasoning_capabilities(
"deepseek/deepseek-v4-pro", allow_fetch=True
)
cold_process()
monkeypatch.setattr(models_mod, "_urlopen_model_catalog_request", offline)
assert models_mod.openrouter_model_reasoning_capabilities(
"deepseek/deepseek-v4-pro"
) is None
def test_staging_portal_does_not_read_productions_mirror(
cold_process, offline, monkeypatch
):
monkeypatch.setattr(
models_mod, "_urlopen_model_catalog_request",
lambda req, *, timeout: _response(_CATALOG),
)
models_mod.nous_model_reasoning_capabilities(
"deepseek/deepseek-v4-pro", allow_fetch=True
)
cold_process()
monkeypatch.setenv("NOUS_INFERENCE_BASE_URL", "https://staging.nousresearch.com")
monkeypatch.setattr(models_mod, "_urlopen_model_catalog_request", offline)
assert models_mod.nous_model_reasoning_capabilities(
"deepseek/deepseek-v4-pro"
) is None
def test_stale_copy_is_still_served(cold_process, offline, monkeypatch):
"""A stale verdict beats no verdict — capabilities change rarely.
Refusing to read an aged mirror would put every long-idle install back on
the cold-start fallback it exists to prevent.
"""
url = models_mod.nous_catalog_url()
models_mod._save_reasoning_caps_disk(
url, {"deepseek/deepseek-v4-pro": {"supports_reasoning": True, "mandatory": False}}
)
raw = json.loads(models_mod._reasoning_caps_disk_path().read_text())
raw[url]["ts"] = 0 # epoch — far past any TTL
models_mod._reasoning_caps_disk_path().write_text(json.dumps(raw))
cold_process()
monkeypatch.setattr(models_mod, "_urlopen_model_catalog_request", offline)
caps = models_mod.nous_model_reasoning_capabilities("deepseek/deepseek-v4-pro")
assert caps["mandatory"] is False
def test_unreadable_mirror_degrades_to_unknown(cold_process, offline, monkeypatch):
"""A corrupt file answers "unknown", never raises into the request path."""
path = models_mod._reasoning_caps_disk_path()
path.parent.mkdir(parents=True, exist_ok=True)
path.write_text("{ this is not json")
monkeypatch.setattr(models_mod, "_urlopen_model_catalog_request", offline)
assert models_mod.nous_model_reasoning_capabilities("deepseek/deepseek-v4-pro") is None
def test_pricing_fetch_seeds_the_mirror(cold_process, offline, monkeypatch):
"""The picker's pricing call already holds the catalog — mirror it for free.
Every surface that renders prices goes through here, so the common case
never pays a second round-trip to learn the same thing.
"""
monkeypatch.setattr(models_mod, "_pricing_cache", {})
monkeypatch.setattr(models_mod, "_pricing_cache_retry_after", {})
monkeypatch.setattr(
models_mod, "_urlopen_model_catalog_request",
lambda req, *, timeout: _response(_CATALOG),
)
models_mod.fetch_models_with_pricing(
base_url="https://inference-api.nousresearch.com"
)
cold_process()
monkeypatch.setattr(models_mod, "_urlopen_model_catalog_request", offline)
caps = models_mod.nous_model_reasoning_capabilities("deepseek/deepseek-v4-pro")
assert caps is not None
def test_a_missing_mirror_is_looked_for_once_per_process(cold_process, monkeypatch):
"""Coming up empty must not re-cost the lookup on every later turn.
This path runs once per request, and naming the Portal catalog resolves
Portal credentials — which can itself reach the network to refresh a
token. Both halves have to be paid at most once.
"""
counts = {"url": 0, "read": 0}
real_url = models_mod.nous_catalog_url
real_read = models_mod._read_reasoning_caps_disk
def _counting_url():
counts["url"] += 1
return real_url()
def _counting_read():
counts["read"] += 1
return real_read()
monkeypatch.setattr(models_mod, "nous_catalog_url", _counting_url)
monkeypatch.setattr(models_mod, "_read_reasoning_caps_disk", _counting_read)
for _ in range(5):
models_mod.nous_model_reasoning_capabilities("deepseek/deepseek-v4-pro")
assert counts == {"url": 1, "read": 1}
+20 -1
View File
@@ -88,7 +88,26 @@ def test_resolve_nous_pricing_credentials_honors_inference_env_override(monkeypa
)
api_key, base_url = models_mod._resolve_nous_pricing_credentials()
assert api_key == ""
assert base_url == "https://stg-inference-api.nousresearch.com/v1"
# The bare origin, whichever form the override was written in: callers
# append their own path (``/v1/models``), so a suffix here would double up.
assert base_url == "https://stg-inference-api.nousresearch.com"
def test_resolve_nous_pricing_credentials_normalizes_either_suffix(monkeypatch):
"""``/v1`` on the override is optional and must not change the result."""
monkeypatch.setattr(
"hermes_cli.auth.resolve_nous_runtime_credentials", lambda: None
)
for override in (
"https://stg-inference-api.nousresearch.com",
"https://stg-inference-api.nousresearch.com/",
"https://stg-inference-api.nousresearch.com/v1",
"https://stg-inference-api.nousresearch.com/v1/",
):
monkeypatch.setenv("NOUS_INFERENCE_BASE_URL", override)
assert models_mod._resolve_nous_pricing_credentials()[1] == (
"https://stg-inference-api.nousresearch.com"
)
def test_a_failed_catalog_fetch_is_not_cached_forever(monkeypatch):