refactor(agent): models_dev — _quietly/_refresh_locked helpers, pack provider table, trim docstrings

This commit is contained in:
Teknium
2026-09-02 18:29:12 -07:00
parent 9fc1f90f97
commit 2382f18dd4
+138 -237
View File
@@ -1,13 +1,11 @@
"""Models.dev registry integration — primary database for providers and models.
Fetches https://models.dev/api.json. Resolution order: in-memory cache (fresh, or
stale served immediately while one background daemon thread refreshes) → disk
cache (~/.hermes/models_dev_cache.json, any age) → network, only when no cache
exists at all. Failed refreshes back off 5 min process-wide. Refreshes use ETag
conditional GET whenever a servable registry is held (a 304 re-confirms without
re-downloading ~2 MB). Hot paths pass ``allow_network=False`` and never do I/O.
A corrupt/empty disk cache is quarantined, never served as ``{}``. The URL can
be overridden via ``models_dev.url`` in config.yaml (mirrors)."""
Resolution: in-memory cache (fresh, or stale served while one background daemon
thread refreshes) → disk cache (~/.hermes/models_dev_cache.json, any age) →
network only when no cache exists. Failed refreshes back off 5 min process-wide.
Refreshes use ETag conditional GET when a servable registry is held. Hot paths
pass ``allow_network=False`` and never do I/O. A corrupt/empty disk cache is
quarantined, never served as ``{}``. ``models_dev.url`` in config.yaml = mirror."""
import json
import logging
@@ -37,12 +35,9 @@ _models_dev_refresh_lock = threading.Lock()
_models_dev_refresh_in_flight = False
# --- Dataclasses -----------------------------------------------------------
@dataclass
class ModelInfo:
"""Full metadata for a single model from models.dev."""
id: str
name: str
family: str
@@ -99,7 +94,6 @@ class ModelInfo:
@dataclass
class ProviderInfo:
"""Full metadata for a provider from models.dev."""
id: str # models.dev provider ID
name: str # display name
env: Tuple[str, ...] # env var names for API key
@@ -111,7 +105,6 @@ class ProviderInfo:
@dataclass
class ModelCapabilities:
"""Structured capability metadata for a model from models.dev."""
supports_tools: bool = True
supports_vision: bool = False
supports_reasoning: bool = False
@@ -120,50 +113,26 @@ class ModelCapabilities:
model_family: str = ""
# --- Provider ID mapping: Hermes ↔ models.dev ------------------------------
# Hermes provider names → models.dev provider IDs
PROVIDER_TO_MODELS_DEV: Dict[str, str] = {
"openrouter": "openrouter",
"novita": "novita-ai",
"anthropic": "anthropic",
"openai": "openai",
"openai-codex": "openai",
"zai": "zai",
"kimi": "kimi-for-coding",
"kimi-coding": "kimi-for-coding",
"moonshot": "kimi-for-coding",
"stepfun": "stepfun",
"kimi-coding-cn": "kimi-for-coding",
"minimax": "minimax",
"minimax-oauth": "minimax",
"minimax-cn": "minimax-cn",
"deepseek": "deepseek",
"alibaba": "alibaba",
"qwen-oauth": "alibaba",
"copilot": "github-copilot",
"ai-gateway": "vercel",
"opencode-zen": "opencode",
"opencode-go": "opencode-go",
"kilocode": "kilo",
"fireworks": "fireworks-ai",
"huggingface": "huggingface",
"gemini": "google",
"google": "google",
"openrouter": "openrouter", "novita": "novita-ai", "anthropic": "anthropic",
"openai": "openai", "openai-codex": "openai", "zai": "zai",
"kimi": "kimi-for-coding", "kimi-coding": "kimi-for-coding",
"moonshot": "kimi-for-coding", "stepfun": "stepfun",
"kimi-coding-cn": "kimi-for-coding", "minimax": "minimax",
"minimax-oauth": "minimax", "minimax-cn": "minimax-cn", "deepseek": "deepseek",
"alibaba": "alibaba", "qwen-oauth": "alibaba", "copilot": "github-copilot",
"ai-gateway": "vercel", "opencode-zen": "opencode",
"opencode-go": "opencode-go", "kilocode": "kilo", "fireworks": "fireworks-ai",
"huggingface": "huggingface", "gemini": "google", "google": "google",
"xai": "xai",
"xai-oauth": "xai", # OAuth is a transport path for the same xAI catalog
"xiaomi": "xiaomi",
"nvidia": "nvidia",
"xiaomi": "xiaomi", "nvidia": "nvidia",
# Meta Model API (Muse Spark, api.meta.ai): models.dev keys it "meta", the
# Hermes provider is "meta-ai"; both aliases are needed or muse-spark-*
# falls back to the generic 256K default instead of its true 1M window.
"meta-ai": "meta",
"meta": "meta",
"groq": "groq",
"mistral": "mistral",
"togetherai": "togetherai",
"perplexity": "perplexity",
"cohere": "cohere",
"meta-ai": "meta", "meta": "meta", "groq": "groq", "mistral": "mistral",
"togetherai": "togetherai", "perplexity": "perplexity", "cohere": "cohere",
"ollama-cloud": "ollama-cloud",
}
@@ -195,8 +164,6 @@ def _cfg_get(*keys: str, default: Any) -> Any:
return default
# --- Disk cache + ETag sidecar ---------------------------------------------
def _hermes_path(name: str) -> Path:
from hermes_constants import get_hermes_home
return get_hermes_home() / name
@@ -210,34 +177,36 @@ def _get_etag_path() -> Path:
return _hermes_path("models_dev_cache.etag")
def _quietly(what: str, fn, default=None):
"""Run *fn*; on any exception log ``"Failed to <what>: %s"`` at debug and return *default*."""
try:
return fn()
except Exception as e:
logger.debug("Failed to %s: %s", what, e)
return default
def _load_etag() -> str:
"""Last-known ETag from disk, or "" if missing."""
try:
def read() -> str:
etag_path = _get_etag_path()
if etag_path.exists():
return etag_path.read_text(encoding="utf-8").strip()
except Exception as e:
logger.debug("Failed to load models.dev ETag: %s", e)
return ""
return etag_path.read_text(encoding="utf-8").strip() if etag_path.exists() else ""
return _quietly("load models.dev ETag", read, "")
def _save_etag(etag: str) -> None:
try:
def write() -> None:
etag_path = _get_etag_path()
etag_path.parent.mkdir(parents=True, exist_ok=True)
atomic_write_text(etag_path, etag)
except Exception as e:
logger.debug("Failed to save models.dev ETag: %s", e)
_quietly("save models.dev ETag", write)
def _clear_etag() -> None:
"""Delete the ETag sidecar so the next fetch is unconditional: an
If-None-Match without a servable cache invites a 304 that leaves the
process with no data at all."""
try:
_get_etag_path().unlink(missing_ok=True)
except Exception as e:
logger.debug("Failed to clear models.dev ETag: %s", e)
_quietly("clear models.dev ETag", lambda: _get_etag_path().unlink(missing_ok=True))
def _get_models_dev_url() -> str:
@@ -264,10 +233,7 @@ def _load_disk_cache() -> Dict[str, Any]:
data = json.load(f)
if _validate_registry(data):
return data
logger.warning(
"models.dev disk cache is corrupt or empty; "
"quarantining (will refetch from network)"
)
logger.warning("models.dev disk cache is corrupt or empty; quarantining (will refetch from network)")
_quarantine_corrupt_cache(cache_path)
except Exception as e:
logger.warning("Failed to load models.dev disk cache; quarantining: %s", e)
@@ -279,13 +245,10 @@ def _load_disk_cache() -> Dict[str, Any]:
def _quarantine_corrupt_cache(cache_path: Path) -> None:
"""Rename a rejected cache aside and drop its ETag sidecar.
Renaming makes the rejection a one-time event — otherwise every hot-path
call that finds the in-memory cache empty re-parses and re-warns until a
network fetch succeeds. The sidecar vouches for a registry we no longer
hold, so it goes too.
"""
"""Rename a rejected cache aside and drop its ETag sidecar. Renaming makes the
rejection a one-time event — otherwise every hot-path call that finds the
in-memory cache empty re-parses and re-warns until a network fetch succeeds.
The sidecar vouches for a registry we no longer hold, so it goes too."""
try:
cache_path.rename(cache_path.with_suffix(".json.corrupt"))
except Exception as e:
@@ -294,47 +257,37 @@ def _quarantine_corrupt_cache(cache_path: Path) -> None:
def _disk_cache_age_seconds() -> Optional[float]:
"""Age of the disk cache file in seconds, or None if missing/unreadable.
An mtime in the future (clock skew) is also None — unknown freshness — so
callers fall through to the network rather than trusting it forever.
"""
try:
"""Age of the disk cache file in seconds, or None if missing/unreadable. An
mtime in the future (clock skew) is also None — unknown freshness — so
callers fall through to the network rather than trusting it forever."""
def stat() -> Optional[float]:
cache_path = _get_cache_path()
if not cache_path.exists():
return None
age = time.time() - cache_path.stat().st_mtime
return age if age >= 0 else None
except Exception as e:
logger.debug("Failed to stat models.dev disk cache: %s", e)
return None
return _quietly("stat models.dev disk cache", stat)
def _save_disk_cache(data: Dict[str, Any], etag: str = "") -> None:
"""Save the registry atomically, plus the ETag sidecar when non-empty."""
try:
atomic_json_write(_get_cache_path(), data, indent=None, separators=(",", ":"))
except Exception as e:
logger.debug("Failed to save models.dev disk cache: %s", e)
_quietly("save models.dev disk cache", lambda: atomic_json_write(
_get_cache_path(), data, indent=None, separators=(",", ":")))
if etag:
_save_etag(etag)
# --- Network refresh (all state mutation happens under _models_dev_fetch_lock)
# Network refresh: all state mutation happens under _models_dev_fetch_lock.
class _NotModified(Exception):
"""Server returned 304 Not Modified — existing cache is still valid."""
def _fetch_models_dev_from_network(*, conditional: bool = False) -> Tuple[Dict[str, Any], str]:
"""Fetch the live registry; returns ``(registry, etag)`` (etag "" if none).
``conditional`` sends ``If-None-Match`` with the sidecar's ETag and raises
``_NotModified`` on 304. Pass True ONLY while holding
``_models_dev_fetch_lock`` AND a servable registry — a conditional request
without one invites a 304 that leaves the process with no data.
Raises on network errors and on an empty/invalid payload.
"""
Raises on network errors and on an empty/invalid payload. ``conditional``
sends ``If-None-Match`` with the sidecar's ETag and raises ``_NotModified``
on 304 — pass True ONLY while holding ``_models_dev_fetch_lock`` AND a
servable registry, or a 304 leaves the process with no data."""
headers: Dict[str, str] = {}
if conditional and (etag := _load_etag()):
headers["If-None-Match"] = etag
@@ -352,11 +305,8 @@ def _fetch_models_dev_from_network(*, conditional: bool = False) -> Tuple[Dict[s
def _mark_stale_cache_grace() -> None:
"""Give stale cache data a 5-minute in-memory grace before retrying refresh.
Only ever moves the timestamp forward, so a background refresh that
completed between the caller's staleness check and this call keeps its
fresh timestamp.
"""
Only ever moves the timestamp forward, so a background refresh that completed
between the caller's staleness check and this call keeps its fresh stamp."""
global _models_dev_cache_time
grace_time = time.time() - _MODELS_DEV_CACHE_TTL + _MODELS_DEV_RETRY_DELAY
if grace_time > _models_dev_cache_time:
@@ -372,11 +322,9 @@ def _serve_stale(msg: str, *args: Any) -> Dict[str, Any]:
def _commit_registry(data: Dict[str, Any], *, etag: str = "", where: str) -> None:
"""Persist a fetched registry: disk + in-mem + clear backoff.
Callers hold ``_models_dev_fetch_lock`` so a failing refresh on one path
can never stomp state a succeeding refresh on the other just committed.
"""
"""Persist a fetched registry: disk + in-mem + clear backoff. Callers hold
``_models_dev_fetch_lock`` so a failing refresh on one path can never stomp
state a succeeding refresh on the other just committed."""
global _models_dev_cache, _models_dev_cache_time, _models_dev_retry_after
_save_disk_cache(data, etag)
_models_dev_cache = data
@@ -398,10 +346,8 @@ def _confirm_cache_not_modified(*, where: str) -> None:
# and arm the normal backoff rather than marking {} "fresh".
_clear_etag()
_models_dev_retry_after = time.time() + _MODELS_DEV_RETRY_DELAY
logger.warning(
"models.dev returned 304 but no cached registry is held (%s); "
"cleared ETag sidecar, will refetch unconditionally", where,
)
logger.warning("models.dev returned 304 but no cached registry is held (%s); "
"cleared ETag sidecar, will refetch unconditionally", where)
return
_models_dev_cache_time = time.time()
_models_dev_retry_after = 0
@@ -412,9 +358,22 @@ def _note_refresh_failure(exc: Exception, *, where: str) -> None:
"""Arm the process-wide 5-minute backoff. Caller holds the lock."""
global _models_dev_retry_after
_models_dev_retry_after = time.time() + _MODELS_DEV_RETRY_DELAY
logger.debug(
"models.dev refresh failed (%s); retry suppressed for %ds: %s", where, _MODELS_DEV_RETRY_DELAY, exc,
)
logger.debug("models.dev refresh failed (%s); retry suppressed for %ds: %s", where, _MODELS_DEV_RETRY_DELAY, exc)
def _refresh_locked(where: str) -> Optional[Dict[str, Any]]:
"""One conditional fetch + state update. Caller holds ``_models_dev_fetch_lock``.
Returns the registry to serve, or None when the fetch failed (backoff armed)."""
try:
data, etag = _fetch_models_dev_from_network(conditional=bool(_models_dev_cache))
_commit_registry(data, etag=etag, where=where)
return data
except _NotModified:
_confirm_cache_not_modified(where=where)
return _models_dev_cache
except Exception as e:
_note_refresh_failure(e, where=where)
return None
def _background_refresh_models_dev() -> None:
@@ -426,14 +385,7 @@ def _background_refresh_models_dev() -> None:
# mid-fetch by a concurrent force_refresh and the two paths can't
# double-download. Hot-path callers never touch this lock.
with _models_dev_fetch_lock:
data, etag = _fetch_models_dev_from_network(conditional=bool(_models_dev_cache))
_commit_registry(data, etag=etag, where="background")
except _NotModified:
with _models_dev_fetch_lock:
_confirm_cache_not_modified(where="background")
except Exception as e:
with _models_dev_fetch_lock:
_note_refresh_failure(e, where="background")
_refresh_locked("background")
finally:
with _models_dev_refresh_lock:
_models_dev_refresh_in_flight = False
@@ -463,17 +415,13 @@ def _start_background_refresh_models_dev() -> None:
def fetch_models_dev(force_refresh: bool = False, *, allow_network: bool = True) -> Dict[str, Any]:
"""Fetch the models.dev registry (dict keyed by provider ID; {} on failure).
Cache hierarchy when ``force_refresh=False``: fresh in-memory → stale
in-memory (returned immediately, refreshed in one background daemon thread
— stale data beats a foreground timeout) → disk cache of any age (a stale
one triggers the same background refresh; corrupt/empty is rejected) →
singleflight foreground network fetch. Any failed refresh suppresses
automatic refreshes for 5 minutes.
``force_refresh=True`` (``hermes config refresh``) bypasses the cache fast
paths and the backoff, falling back to cached data only if the call fails.
``allow_network=False`` returns any memory/disk cache regardless of age and
never makes a request — for latency-sensitive paths.
Cache hierarchy: fresh in-memory → stale in-memory (served now, refreshed in
one background daemon thread — stale beats a foreground timeout) → disk of any
age (stale triggers the same background refresh) → singleflight foreground
fetch. A failed refresh suppresses automatic refreshes for 5 minutes.
``force_refresh=True`` bypasses the cache fast paths and the backoff, falling
back to cached data only if the call fails. ``allow_network=False`` returns
any memory/disk cache regardless of age and never makes a request.
"""
global _models_dev_cache, _models_dev_cache_time, _models_dev_retry_after
@@ -498,15 +446,11 @@ def fetch_models_dev(force_refresh: bool = False, *, allow_network: bool = True)
if disk_age is not None and (disk_data := _load_disk_cache()):
_models_dev_cache = disk_data
if disk_age >= _MODELS_DEV_CACHE_TTL:
return _serve_stale(
"Using stale models.dev disk cache (age=%.0fs); refreshing in background", disk_age,
)
return _serve_stale("Using stale models.dev disk cache (age=%.0fs); refreshing in background", disk_age)
# Anchor the in-mem TTL to the file's age so an aging cache isn't
# extended by another full TTL.
_models_dev_cache_time = time.time() - disk_age
logger.debug(
"Loaded models.dev from fresh disk cache (%d providers, age=%.0fs)", len(disk_data), disk_age,
)
logger.debug("Loaded models.dev from fresh disk cache (%d providers, age=%.0fs)", len(disk_data), disk_age)
return _models_dev_cache
# Process-wide backoff: don't make every caller retry an unreachable
# endpoint while no usable cache exists.
@@ -523,15 +467,9 @@ def fetch_models_dev(force_refresh: bool = False, *, allow_network: bool = True)
if force_refresh and not _models_dev_cache and (disk := _load_disk_cache()):
_models_dev_cache = disk
_models_dev_cache_time = 0 # servable but not fresh
try:
data, etag = _fetch_models_dev_from_network(conditional=bool(_models_dev_cache))
_commit_registry(data, etag=etag, where="foreground")
return data
except _NotModified:
_confirm_cache_not_modified(where="foreground")
return _models_dev_cache
except Exception as e:
_note_refresh_failure(e, where="foreground")
served = _refresh_locked("foreground")
if served is not None:
return served
# Stage 5: network failed — serve any stale memory/disk cache. Freshness
# stays expired; the retry-after timestamp gates the next attempt.
if not _models_dev_cache:
@@ -542,8 +480,6 @@ def fetch_models_dev(force_refresh: bool = False, *, allow_network: bool = True)
return _models_dev_cache
# --- Catalog access helpers ------------------------------------------------
def _fetch_registry(allow_network: bool) -> Dict[str, Any]:
# Keep the zero-argument call on the allow_network path: dozens of test
# sites monkeypatch fetch_models_dev with zero-arg lambdas.
@@ -559,31 +495,26 @@ def _registry_provider(mdev_id: str, allow_network: bool) -> Optional[Dict[str,
def _registry_models(mdev_id: str, *, allow_network: bool) -> Optional[Dict[str, Any]]:
"""The ``models`` dict of a models.dev provider entry, or None."""
provider_data = _registry_provider(mdev_id, allow_network)
if provider_data is None:
return None
models = provider_data.get("models", {})
models = provider_data.get("models", {}) if provider_data is not None else None
return models if isinstance(models, dict) else None
def _get_provider_models(provider: str, *, allow_network: bool = False) -> Optional[Dict[str, Any]]:
"""Resolve a Hermes provider ID to its models dict, or None if unknown.
``allow_network`` defaults to False — hot-path callers must never block."""
mdev_provider_id = PROVIDER_TO_MODELS_DEV.get(provider)
if not mdev_provider_id:
return None
return _registry_models(mdev_provider_id, allow_network=allow_network)
mdev_id = PROVIDER_TO_MODELS_DEV.get(provider)
return _registry_models(mdev_id, allow_network=allow_network) if mdev_id else None
def _iter_model_entries(models: Dict[str, Any], model: str, *, suffix_fallback: bool = True):
"""Yield ``(model_id, entry)`` candidates: exact, case-insensitive, then
(optionally) ``:cloud``/``-cloud`` suffixed forms.
The suffix fallback exists because some providers (e.g. ollama-cloud)
store ``kimi-k2.6:cloud`` while the live API returns the bare name;
without it context lookup falls through to stale OpenRouter metadata and
trips the 64k minimum-context guard. Every consumer shares this order so a
suffix-keyed catalog model counts as KNOWN for ``model_overrides``
fill-gap ``_default`` semantics.
Suffix fallback: some providers (ollama-cloud) store ``kimi-k2.6:cloud``
while the live API returns the bare name; without it context lookup falls to
stale OpenRouter metadata and trips the 64k minimum-context guard. Every
consumer shares this order so a suffix-keyed catalog model counts as KNOWN
for ``model_overrides`` fill-gap ``_default`` semantics.
"""
candidates = [model]
if suffix_fallback:
@@ -618,12 +549,10 @@ def lookup_models_dev_context(provider: str, model: str, *, allow_network: bool
"""Context window in tokens for provider+model, or None if not found.
An EXPLICIT ``model_overrides`` entry wins over the catalog; ``_default``
entries fill the gap only when the catalog has no answer (the supported
self-unblock path for models with wrong/missing context in models.dev).
Catalog entries with context=0 are skipped in favour of later candidates.
``allow_network`` defaults to False — this runs every turn and must never
block.
"""
fills the gap only when the catalog has no answer (the self-unblock path for
wrong/missing context in models.dev). Catalog entries with context=0 are
skipped in favour of later candidates. ``allow_network`` defaults to False —
this runs every turn and must never block."""
override_ctx = _override_context_window(provider, model)
if override_ctx is not None:
return override_ctx
@@ -636,21 +565,14 @@ def lookup_models_dev_context(provider: str, model: str, *, allow_network: bool
return _default_override_context(provider)
# --- Per-model metadata overrides (config.yaml → model_overrides) ----------
#
# Canonical override schema (the ONLY key space consumers accept):
# context_window, max_output_tokens, supports_tools, supports_vision,
# supports_reasoning, model_family
#
# ``model_overrides.<provider>.<model_id>`` is an explicit override that always
# wins over the catalog for the fields it sets (partial patch).
# ``model_overrides.<provider>._default`` / ``model_overrides._default`` are
# FILL-GAP defaults: they apply ONLY to models the catalog does not know and
# never displace catalog data — a ``_default: {context_window: 128000}`` cannot
# clamp every catalog-known model of a provider.
#
# Provider keys accept the Hermes provider id or the models.dev provider id.
# Model ids match exactly, then case-insensitively (mirroring catalog lookup).
# Per-model overrides (config.yaml → model_overrides). Canonical schema (the ONLY
# key space consumers accept): context_window, max_output_tokens, supports_tools,
# supports_vision, supports_reasoning, model_family.
# ``<provider>.<model_id>`` is an explicit partial patch that always wins over the
# catalog. ``<provider>._default`` / top-level ``_default`` are FILL-GAP defaults:
# they apply ONLY to models the catalog does not know and never displace catalog
# data. Provider keys accept the Hermes or models.dev id; model ids match exactly,
# then case-insensitively (mirroring catalog lookup).
_OVERRIDE_WARNED_KEYS: set = set()
@@ -664,12 +586,10 @@ _UNKNOWN_MODEL_BASE: Dict[str, Any] = {
def _load_model_overrides() -> Dict[str, Any]:
"""The ``model_overrides`` config section ({} on any failure).
Deliberately not memoized: ``load_config_readonly()`` is already
(mtime, size)-cached upstream, and an ``id(cfg)``-keyed layer here can
serve stale overrides after a reload when CPython reuses the dict address.
"""
"""The ``model_overrides`` config section ({} on any failure). Deliberately not
memoized: ``load_config_readonly()`` is already (mtime, size)-cached upstream,
and an ``id(cfg)``-keyed layer can serve stale overrides after a reload when
CPython reuses the dict address."""
return _dict_or_empty(_cfg_get("model_overrides", default={}))
@@ -738,20 +658,15 @@ def _override_int(override: Dict[str, Any], key: str) -> Optional[int]:
warn_key = (key, repr(raw))
if warn_key not in _OVERRIDE_WARNED_KEYS:
_OVERRIDE_WARNED_KEYS.add(warn_key)
logger.warning(
"model_overrides: ignoring invalid %s value %r (expected a positive integer)", key, raw,
)
logger.warning("model_overrides: ignoring invalid %s value %r (expected a positive integer)", key, raw)
return None
def _override_context_window(provider: str, model: str) -> Optional[int]:
"""EXPLICITLY overridden context_window, or None.
Explicit-only on purpose: this runs early in the resolution chain
(agent/model_metadata.py, before custom_providers and live probes) where
a ``_default`` must not preempt more specific sources; fill-gap defaults
apply later in ``lookup_models_dev_context`` once the catalog has missed.
"""
"""EXPLICITLY overridden context_window, or None. Explicit-only on purpose:
this runs early in the resolution chain (agent/model_metadata.py, before
custom_providers and live probes) where a ``_default`` must not preempt more
specific sources; fill-gap defaults apply in ``lookup_models_dev_context``."""
ov = _explicit_model_override(provider, model)
return _override_int(ov, "context_window") if ov is not None else None
@@ -809,17 +724,15 @@ def _merge_catalog_entry_with_override(raw: Dict[str, Any], override: Dict[str,
def _apply_overrides(provider: str, model: str, entry: Optional[Dict[str, Any]]) -> Optional[Dict[str, Any]]:
"""Catalog *entry* patched by its override; ``_UNKNOWN_MODEL_BASE`` patched
by a fill-gap override on a catalog miss; None when neither exists.
The override is selected AFTER the catalog lookup: _default only fills misses."""
"""*entry* patched by its override; ``_UNKNOWN_MODEL_BASE`` patched by a
fill-gap override on a catalog miss (selected AFTER lookup: _default only
fills misses); None when neither exists."""
override = _override_for(provider, model, catalog_hit=entry is not None)
if override is None:
return entry
return _merge_catalog_entry_with_override(entry if entry is not None else _UNKNOWN_MODEL_BASE, override)
# --- Model capability metadata ---------------------------------------------
def _entry_supports_vision(entry: Dict[str, Any]) -> bool:
"""Prefer explicit ``modalities.input`` (the older ``attachment`` flag can be
stale or too broad for image routing); fall back to it only when the input
@@ -833,12 +746,10 @@ def _entry_supports_vision(entry: Dict[str, Any]) -> bool:
def get_model_capabilities(provider: str, model: str, *, allow_network: bool = False) -> Optional[ModelCapabilities]:
"""Capability metadata from the models.dev cache, or None if unresolvable.
EXPLICIT ``model_overrides`` entries patch catalog values for the fields
they set; ``_default`` entries fill the gap only for models the catalog
does not know. Unspecified fields fall through to the catalog value, or to
safe defaults (tools on, vision/reasoning off, 200K/8K) when absent.
``allow_network`` defaults to False — vision/image routing is a hot path.
"""
EXPLICIT ``model_overrides`` patch catalog fields; ``_default`` fills the gap
only for models the catalog does not know. Unspecified fields fall through to
the catalog, or to safe defaults (tools on, vision/reasoning off, 200K/8K).
``allow_network`` defaults to False — vision/image routing is a hot path."""
models = _get_provider_models(provider, allow_network=allow_network)
entry = _find_model_entry(models, model) if models is not None else None
raw = _apply_overrides(provider, model, entry)
@@ -890,9 +801,8 @@ _GOOGLE_HIDDEN_MODELS = frozenset({
def _should_hide_from_provider_catalog(provider: str, model_id: str) -> bool:
provider_lower = (provider or "").strip().lower()
model_lower = (model_id or "").strip().lower()
return provider_lower in {"gemini", "google"} and model_lower in _GOOGLE_HIDDEN_MODELS
return ((provider or "").strip().lower() in {"gemini", "google"}
and (model_id or "").strip().lower() in _GOOGLE_HIDDEN_MODELS)
def list_agentic_models(provider: str, *, allow_network: bool = True) -> List[str]:
@@ -911,31 +821,27 @@ def list_agentic_models(provider: str, *, allow_network: bool = True) -> List[st
]
# --- Rich dataclass constructors + queries ---------------------------------
def _parse_model_info(model_id: str, raw: Dict[str, Any], provider_id: str) -> ModelInfo:
"""Convert a raw models.dev model entry dict into a ModelInfo dataclass."""
cost = _dict_or_empty(raw.get("cost"))
modalities = _dict_or_empty(raw.get("modalities"))
input_mods = modalities.get("input") or []
output_mods = modalities.get("output") or []
def _mods(key: str) -> Tuple[str, ...]:
mods = modalities.get(key) or []
return tuple(mods) if isinstance(mods, list) else ()
def _cost(key: str) -> Optional[float]:
return float(cost[key]) if key in cost and cost[key] is not None else None
return float(cost[key]) if cost.get(key) is not None else None
return ModelInfo(
id=model_id,
name=raw.get("name", "") or model_id,
family=raw.get("family", "") or "",
provider_id=provider_id,
reasoning=bool(raw.get("reasoning", False)),
tool_call=bool(raw.get("tool_call", False)),
attachment=bool(raw.get("attachment", False)),
temperature=bool(raw.get("temperature", False)),
structured_output=bool(raw.get("structured_output", False)),
open_weights=bool(raw.get("open_weights", False)),
input_modalities=tuple(input_mods) if isinstance(input_mods, list) else (),
output_modalities=tuple(output_mods) if isinstance(output_mods, list) else (),
**{k: bool(raw.get(k, False)) for k in (
"reasoning", "tool_call", "attachment", "temperature", "structured_output", "open_weights")},
input_modalities=_mods("input"),
output_modalities=_mods("output"),
context_window=_extract_limit(raw, "context") or 0,
max_output=_extract_limit(raw, "output") or 0,
max_input=_extract_limit(raw, "input"),
@@ -966,8 +872,7 @@ def _parse_provider_info(provider_id: str, raw: Dict[str, Any]) -> ProviderInfo:
def get_provider_info(provider_id: str, *, allow_network: bool = True) -> Optional[ProviderInfo]:
"""Provider metadata by Hermes or models.dev ID, or None if not cataloged.
``allow_network`` defaults to True — the primary caller is interactive
setup (``resolve_provider_full``). Hot-path callers pass False."""
``allow_network`` defaults to True — primary caller is interactive setup."""
mdev_id = PROVIDER_TO_MODELS_DEV.get(provider_id, provider_id)
raw = _registry_provider(mdev_id, allow_network)
return _parse_provider_info(mdev_id, raw) if raw is not None else None
@@ -975,13 +880,9 @@ def get_provider_info(provider_id: str, *, allow_network: bool = True) -> Option
def get_model_info(provider_id: str, model_id: str, *, allow_network: bool = False) -> Optional[ModelInfo]:
"""Full model metadata by Hermes or models.dev provider ID (exact match,
then case-insensitive), or None if not found.
``model_overrides`` use the same canonical schema as every other consumer:
EXPLICIT entries patch known catalog models; ``_default`` entries fill the
gap only for models the catalog does not know.
``allow_network`` defaults to False — cost guard and inventory are hot paths.
"""
then case-insensitive), or None if not found. EXPLICIT ``model_overrides``
patch known catalog models; ``_default`` fills the gap only for unknown ones.
``allow_network`` defaults to False — cost guard and inventory are hot paths."""
mdev_id = PROVIDER_TO_MODELS_DEV.get(provider_id, provider_id)
models = _registry_models(mdev_id, allow_network=allow_network)
mid, entry = model_id, None