| name | hermes-openrouter-model-picker |
| description | Fix missing models in Hermes /model picker. |
| version | 1.0.0 |
| author | ajax |
| license | MIT |
| platforms | ["linux","macos","windows"] |
| metadata | {"hermes":{"tags":["hermes","openrouter","models","model-picker","cache","cron","troubleshooting"],"related_skills":["hermes-agent"]}} |
Hermes OpenRouter Model Picker
Two jobs:
- Diagnose a model that exists on OpenRouter but won't show in
/model.
- Replace the ~43-model curated picker with the N newest models, newest first.
Both hinge on one fact most people miss, so read the next section before touching anything.
The two-cache gotcha (root cause of ~every "model is missing" report)
/model renders the INTERSECTION of two independent caches. A model must be in
BOTH or it silently vanishes. No error, no warning.
| # | File | Role | TTL |
|---|
| 1 | $HERMES_HOME/cache/model_catalog.json | curated manifest — what's allowed | 1h (model_catalog.ttl_hours) |
| 2 | $HERMES_HOME/provider_models_cache.json | live /v1/models ids — what's available | 1h TTL, 7-day stale-serve |
Cache #2 is the trap. In hermes_cli/models.py:
_PROVIDER_MODELS_CACHE_TTL = 3600
_PROVIDER_MODELS_STALE_SERVE_MAX = 7 * 24 * 3600
An expired entry whose credential fingerprint still matches is served
immediately from disk while a daemon thread refetches in the background. That
thread only rewrites the disk file — it never updates the list already handed to the
picker. Net effect: a newly-released model can be correct in cache #1 and still not
render for up to 7 days.
hermes model --refresh does NOT fix this. It clears the picker/catalog caches,
not provider_models_cache.json. This is the single most common wrong turn.
Ordering also comes from cache #1 — the picker preserves manifest order, which is why
overriding the manifest is what gives you a chronological list.
Part 1 — Diagnose a missing model
Never trust the config; check both caches. Substitute the real model id.
curl -s https://openrouter.ai/api/v1/models \
| python3 -c "
import json,sys
want='z-ai/glm-5.3'
for m in json.load(sys.stdin)['data']:
if m['id']==want:
sp=m.get('supported_parameters') or []
print(want,'exists | tools:', 'tools' in sp)
break
else: print(want,'NOT on OpenRouter')"
If tools: False, stop — Hermes requires tool calling and deliberately hides such
models (selecting one fails at runtime). That is correct behavior, not a bug.
cd /usr/local/lib/hermes-agent && ./venv/bin/python -c "
import json, os
from hermes_cli.model_catalog import get_curated_openrouter_models
home=os.environ.get('HERMES_HOME') or os.path.expanduser('~/.hermes')
cur=[m for m,_ in (get_curated_openrouter_models() or [])]
live=json.load(open(f'{home}/provider_models_cache.json')).get('openrouter',{}).get('models',[])
w='z-ai/glm-5.3'
print('in manifest (cache #1):', w in cur)
print('in live cache (cache #2):', w in live)
"
- Missing from #1 → manifest is stale/curated-out. Refresh it, or override it (Part 2).
- Missing from #2 → the 7-day stale-serve trap. Fix below.
- Missing from both → do the #2 fix first, then recheck #1.
cd /usr/local/lib/hermes-agent && ./venv/bin/python -c "
from hermes_cli import models as M
M.clear_provider_models_cache('openrouter')
ids = M.cached_provider_model_ids('openrouter') # forces live re-fetch
print('rebuilt:', len(ids), 'models')
"
cd /usr/local/lib/hermes-agent && ./venv/bin/python -c "
from hermes_cli import models as M
from hermes_cli.model_catalog import reset_cache
reset_cache(); M._openrouter_catalog_cache=None
lst=M.fetch_openrouter_models(force_refresh=True)
ids=[m for m,_ in lst]
print('entries:',len(lst))
for w in ('z-ai/glm-5.3','google/gemini-3.7-flash'):
print(' ',w,'->','OK' if w in ids else 'MISSING')
"
A live TUI holds warm in-process copies. After fixing on disk, /reload or start a
fresh session.
Part 2 — Chronological picker (newest N models)
model_catalog.providers.<provider>.url overrides the curated list for one provider.
Point it at a locally-generated manifest and you control both membership and order.
Override fetches skip the disk cache and re-read on every picker open, so the file
is always as fresh as its last regeneration.
file:// URLs work — they pass _fetch_manifest + schema validation.
Ship scripts/openrouter_chrono_catalog.py (in this skill) and wire it:
cp scripts/openrouter_chrono_catalog.py ~/.hermes/scripts/
python3 ~/.hermes/scripts/openrouter_chrono_catalog.py 100
hermes config set model_catalog.providers.openrouter.url \
"file://$HOME/.hermes/cache/openrouter_chrono.json"
Filtering the script applies (~417 raw → 100):
- tool-calling required — Hermes hard-requires it; listing others is a trap
- drop
:batch — async-only endpoints, unusable for interactive chat
- drop
~alias — floating pointers (~z-ai/glm-latest) that duplicate a concrete id
The script sorts by the created field (every entry has one) and writes a
schema-version-1 manifest. Release date goes in description, though Hermes overwrites
that with its own recommended/free/default badge on some rows — picker-side, unavoidable.
Crucially the script also busts cache #2 whenever top-100 membership changes.
Regenerating the manifest alone reintroduces the exact bug in Part 1.
Keep it current with cron
cronjob(action='create', name='openrouter-chrono-catalog-refresh',
schedule='0 */6 * * *', script='openrouter_chrono_catalog.py',
no_agent=True, deliver='local',
prompt='Refresh the chronological OpenRouter model manifest.')
no_agent=True → scheduler runs the script and delivers stdout verbatim. Zero tokens.
Pitfalls
hermes model --refresh doesn't clear provider_models_cache.json. Biggest time sink.
- Never hand-edit
config.yaml — use hermes config set; a stray indent breaks the live gateway.
- Cron
script takes a bare filename relative to ~/.hermes/scripts/. Absolute paths are
rejected, and argv can't be passed — for flags like --quiet, ship a .sh wrapper.
- Use the Hermes venv (
/usr/local/lib/hermes-agent/venv/bin/python) for hermes_cli
imports; system python3 lacks the deps. Keep cron scripts dependency-free (stdlib only)
so they run under plain python3 — do JSON surgery instead of importing hermes_cli.
- Don't chain long diagnostics with
&&/heredocs in one terminal call — the parser
blocklist trips on oversized inline payloads. One command per call, or execute_code.
- Bust only your provider's key in
provider_models_cache.json; other providers
(xai-oauth, gemini, …) share the file. Verify they survive.
- Verify end-to-end, not per-cache. Both caches can look right while the intersection
is still wrong. Always finish with the Step 4
fetch_openrouter_models check.
- A model absent because
tools: False is working as intended — don't "fix" it.
Verification checklist
fetch_openrouter_models() returns the expected count.
- Target model ids present in that list.
- No
:batch / ~alias ids leaked (chronological mode).
- Other providers still present in
provider_models_cache.json.
- Cron job's first manual run exits
ok (cronjob action='run').