The remote model catalog (website/static/api/model-catalog.json) now labels
exactly one entry per provider block with "default": true — z-ai/glm-5.2 for
both OpenRouter and Nous Portal. That labeled entry is the model Hermes
silently lands on when the user never picked one, and it can be rotated by
editing the manifest alone: no release needed.
- model_catalog.py: get_default_model_from_cache() reads the label from the
in-process/disk cache only — never triggers a network fetch, so hot
resolution paths (agent build, gateway session setup) stay network-free.
- models.py: get_preferred_silent_default_model() resolves catalog label
first, PREFERRED_SILENT_DEFAULT_MODEL constant second (offline/fresh
install). _PROVIDER_SILENT_DEFAULT_OVERRIDES dict replaced by
_SILENT_DEFAULT_PROVIDERS routing through the shared resolver.
fetch_openrouter_models() preserves the "default" badge through live
/v1/models refreshes so the picker shows it.
- scripts/build_model_catalog.py: generator emits the default label so
regeneration can't drop it.
- website/docs/reference/model-catalog.md: schema documents the new field.
- Salvaged from PR #61141 (@HumphreySun98): bare-provider /model switches
(/model nous) route through the cost-safe default instead of curated
entry [0].
- tests: catalog-label precedence, constant fallback, stale-label fallback,
cache-only (no network) guarantee, and a shipped-manifest contract test
pinning the labeled entry to PREFERRED_SILENT_DEFAULT_MODEL.
E2E (temp HERMES_HOME): fresh-install constant fallback, shipped-manifest
label read, release-free rotation (relabeled cache -> new default across
models.py, tui_gateway, and gateway empty-model paths) all verified.
`detect_static_provider_for_model` handles a bare provider name typed as a
model (e.g. `/model nous`) by returning `_PROVIDER_MODELS[provider][0]` — the
first curated entry. For metered aggregators whose curated list is ordered
most-capable-first (Nous Portal), entry [0] is the priciest flagship, so
`/model nous` silently switched to it.
This is exactly the billing footgun `_PROVIDER_SILENT_DEFAULT_OVERRIDES` /
`get_default_model_for_provider` exist to prevent (per their docstring, a
missing model "escalated to Opus and billed 863 requests before the user
noticed"). The non-interactive fallback already routes through that cost-safe
helper; the interactive `/model <provider>` path did not.
Route this path through `get_default_model_for_provider` too. Providers
without a silent-default override are unchanged (the helper returns
`models[0]`), so only overridden providers (currently `nous`) change — from
the flagship to the low-cost default.
Adds regression tests: `/model nous` resolves to the cost-safe default, and
non-overridden providers still resolve to their first catalog model.
When a user starts a chat without ever selecting a model (GUI Chat App
onboarding, provider-set-but-model-missing config, empty model.default),
every silent fallback path resolved to the first curated catalog entry —
anthropic/claude-fable-5, the priciest flagship. Users were silently
billed for the most expensive model without opting in.
- hermes_cli/models.py: add PREFERRED_SILENT_DEFAULT_MODEL (z-ai/glm-5.2)
+ pick_silent_default_model() helper; point the nous silent-default
override at it and add an openrouter override (previously resolved to
"" and let downstream paths land on the flagship).
- hermes_cli/web_server.py: /api/model/recommended-default (the endpoint
the Desktop onboarding confirm card reads) now picks GLM-5.2 when the
provider's list carries it instead of blindly taking entry [0].
- tui_gateway/server.py: _resolve_model()'s last-resort literal was
anthropic/claude-sonnet-4; now PREFERRED_SILENT_DEFAULT_MODEL.
- tests: update empty-model fallback tests for the new contract.
Assert get_default_model_for_provider("nous") never returns the priciest
catalog entry (anthropic/claude-opus-4.8) and that an override pointing at a
model absent from the catalog falls back to catalog order. Regression for the
silent flagship-billing footgun.
Remove unused imports (F401) and duplicate/shadowed import
redefinitions (F811) across the codebase using ruff's safe
autofixes. No behavioral changes -- imports only.
- ~1400 safe autofixes applied across 644 files (net -1072 lines)
- __init__.py re-exports preserved (excluded from F401 removal so
public re-export surfaces stay intact)
- Re-exports that are imported or monkeypatched by tests but look
unused in their defining module are kept with explicit # noqa:
F401 (gateway/run.py load_dotenv; run_agent re-exports from
agent.message_sanitization, agent.context_compressor,
agent.retry_utils, agent.prompt_builder, agent.process_bootstrap,
agent.codex_responses_adapter)
- Unsafe F841 (unused-variable) fixes deliberately skipped -- those
can change behavior when the RHS has side effects
- ruff lints remain disabled in pyproject.toml (only PLW1514 is
selected); this is a one-time cleanup, not a config change
Verification:
- python -m compileall: clean
- pytest --collect-only: all 27161 tests collect (zero import errors)
- core entry points import clean (run_agent, model_tools, cli,
toolsets, hermes_state, batch_runner, gateway)
- static scan: every name any test imports directly from an edited
module still resolves
When a user configures a provider (e.g. `hermes auth add openai-codex`)
but never selects a model via `hermes model`, the gateway and CLI would
pass an empty model string to the API, causing:
'Codex Responses request model must be a non-empty string'
Now both gateway (_resolve_session_agent_runtime) and CLI
(_ensure_runtime_credentials) detect an empty model and fill it from
the provider's first catalog entry in _PROVIDER_MODELS. This covers
all providers that have a static model list (openai-codex, anthropic,
gemini, copilot, etc.).
The fix is conservative: it only triggers when model is truly empty
and a known provider was resolved. Explicit model choices are never
overridden.