feat(ops): hermes-provider-fix — stop Hermes routing through OpenRouter - #124
feat(ops): hermes-provider-fix — stop Hermes routing through OpenRouter#124Deesmo wants to merge 4 commits into
Conversation
Co-authored-by: Deesmo <Deesmo@users.noreply.github.com>
Hermes Agent resolves an unset model.provider by checking OPENAI_API_KEY /
OPENROUTER_API_KEY before the Anthropic, Gemini and xAI keys, and treats a
match as "use OpenRouter". An OpenAI key in ~/.hermes/.env is therefore enough
to send every request to openrouter.ai -- authenticated with that OpenAI key --
while the direct-provider keys in the same file are never reached.
Verified against hermes-agent==0.19.0 by calling its own resolver:
resolve_provider('auto') -> 'openrouter', api_key = the OPENAI_API_KEY.
Two related traps this also covers: 'openai' is an alias for openrouter in
hermes_cli.providers.ALIASES (the direct provider is 'openai-api'), and
auxiliary/delegation/fallback slots stay pinned to OpenRouter independently of
the main chat model.
The tool runs diagnosis inside the Hermes interpreter and calls Hermes' own
resolve_runtime_provider(), so it reports actual routing rather than a
re-implementation of the precedence rules. --verify probes each provider's own
API to see which keys work; --apply backs up config.yaml and writes only
through `hermes config set/unset`, then re-resolves to prove the result.
API key values are never printed, logged or written, and ~/.hermes/.env is
never modified.
Co-authored-by: Deesmo <Deesmo@users.noreply.github.com>
There was a problem hiding this comment.
Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.
Bugbot Autofix prepared fixes for all 4 issues found in the latest run.
- ✅ Fixed: Verify uses wrong key precedence
- collect_keys now lets process environment values override ~/.hermes/.env (and the primary path uses Hermes' own load_hermes_dotenv), matching Hermes 0.19.0 precedence instead of preferring stale file keys.
- ✅ Fixed: Apply reports success after write failures
- apply_fix now aggregates every run_config result, prints an explicit failure message instead of Done when any write fails, and main exits 2 when --apply writes fail.
- ✅ Fixed: Verify skips Hermes-resolved secrets
- collect_keys now runs load_hermes_dotenv inside the Hermes interpreter and reads back only the provider key variables, so secret-source (Bitwarden/1Password) keys are resolved exactly as Hermes resolves them, with the flat reader kept only as a fallback.
- ✅ Fixed: Auto-fixed findings still need manual edit
- Both the OPENROUTER_API_KEY auto-detect warning and the auth.json OpenRouter-pool warning now set auto_fixed=True, so they no longer appear as manual work after --apply sets model.provider.
You can send follow-ups to the cloud agent here.
Comment @cursor review or bugbot run to trigger another review on this PR
Reviewed by Cursor Bugbot for commit 3df81d6. Configure here.
| for name, value in os.environ.items(): | ||
| if value.strip(): | ||
| keys.setdefault(name, value.strip()) | ||
| return keys |
There was a problem hiding this comment.
Verify uses wrong key precedence
High Severity
collect_keys loads ~/.hermes/.env first and then uses setdefault for process env, so file values win. Hermes 0.19.0 does the opposite (load_hermes_dotenv keeps existing env vars). --verify can therefore probe a stale file key while Hermes uses a rotated shell/systemd key, which can block --apply or pick the wrong provider.
Reviewed by Cursor Bugbot for commit 3df81d6. Configure here.
| return | ||
|
|
||
| _report_post_apply(setup) | ||
| print("Done. Cross-check with `hermes doctor`, then start a session with `hermes`.") |
There was a problem hiding this comment.
Apply reports success after write failures
High Severity
apply_fix ignores run_config’s boolean result, continues after failed hermes config set/unset (including when the CLI is missing), then prints Done and main still exits 0. Operators can believe routing was fixed while config is unchanged or only partially updated.
Additional Locations (2)
Reviewed by Cursor Bugbot for commit 3df81d6. Configure here.
| for name, value in os.environ.items(): | ||
| if value.strip(): | ||
| keys.setdefault(name, value.strip()) | ||
| return keys |
There was a problem hiding this comment.
Verify skips Hermes-resolved secrets
Medium Severity
Live key checks read only the parent process env and a flat .env file. They never run load_hermes_dotenv / SecretSource, so Bitwarden and 1Password keys that Hermes resolves at startup look absent or are probed as literal op:// placeholders. The recommended --verify --apply path then refuses or mis-selects providers for those installs.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 3df81d6. Configure here.
| "Setting model.provider explicitly (--apply does this) makes the key inert. " | ||
| "Remove it from ~/.hermes/.env only if you never want OpenRouter as a manual option." | ||
| ), | ||
| ) |
There was a problem hiding this comment.
Auto-fixed findings still need manual edit
Low Severity
The OPENROUTER_API_KEY and auth.json OpenRouter-pool findings say --apply makes them inert by setting model.provider, but they leave auto_fixed false. After a successful apply they still appear under “needs your judgement,” pushing unnecessary manual cleanup.
Additional Locations (1)
Reviewed by Cursor Bugbot for commit 3df81d6. Configure here.
The OpenRouter key does not need to be removed. An explicit model.provider makes it inert -- it stops winning auto-detection but stays in ~/.hermes/.env and still resolves when deliberately requested. The previous wording implied deleting it was an option worth considering; say plainly that it is not needed. Verified with a key present: default sessions resolve to anthropic / api.anthropic.com while resolve_runtime_provider(requested="openrouter") still returns openrouter with a usable key, and .env is never written. Also make the tool usable where it actually has to run: - locate the interpreter via the `hermes` launcher's shebang, so pipx, Homebrew and relocated venvs work, not just ~/.hermes/hermes-agent/venv - tolerate being piped into python (no __file__), for a one-line invocation Co-authored-by: Deesmo <Deesmo@users.noreply.github.com>
…n write errors - collect_keys now resolves keys inside the Hermes interpreter via load_hermes_dotenv, matching Hermes 0.19.0 precedence (process env beats ~/.hermes/.env) and external secret sources; the fallback reader uses the same precedence instead of letting file values win. - apply_fix propagates run_config failures: a failed or missing hermes CLI no longer prints Done, and main exits 2 when --apply writes fail. - The OPENROUTER_API_KEY and auth.json pool warnings are marked auto_fixed, since setting model.provider (which --apply does) makes them inert.


Why
Hermes Desktop kept asking for OpenRouter credits even though Anthropic, OpenAI, xAI and Gemini keys were already configured. This is not a credit problem — it is a provider-resolution problem.
hermes_cli.auth.resolve_provider()resolves an unsetmodel.providerin this order:--api-key/--base-urlmodel.providerfromconfig.yamlOPENAI_API_KEYorOPENROUTER_API_KEYpresentauth.jsonStep 3 runs before step 5, so an OpenAI key alone sends every request to
openrouter.ai— and Hermes uses that OpenAI key as the OpenRouter bearer token. The Anthropic, Gemini and xAI keys in the same file are never reached, and OpenRouter has no balance tied to an OpenAI key.Reproduced against
hermes-agent==0.19.0with four direct keys and no OpenRouter key at all, by calling Hermes' own resolver:Two related traps, also covered:
model.provider: openaiis not the OpenAI API.openaiis an alias foropenrouterinhermes_cli.providers.ALIASES, and the resolver separately rejects it as an unknown provider. The direct provider isopenai-api.auxiliary.<task>.provider,delegation.providerandfallback_providersstay pinned to OpenRouter independently of the main chat model, so fixing only the main model leaves side traffic on the aggregator.The OpenRouter key stays
Nothing is deleted and
~/.hermes/.envis never written. An explicitmodel.providerjust stops the OpenRouter key from winning auto-detection; it remains available on demand. Verified with a key present:What this adds
scripts/hermes-provider-fix/— a stdlib-only operator utility for a local Hermes install. Unrelated to the Arch Tools API; it sits alongside the other operator scripts.resolve_requested_provider()/resolve_runtime_provider(), so it reports what Hermes actually does instead of re-implementing the precedence rules. It additionally scansmodel.base_url, everyauxiliary.*override and itsfallback_chain,delegation.provider,fallback_providers/ legacyfallback_model,OPENROUTER_API_KEY/OPENAI_BASE_URL/OPENROUTER_BASE_URL, and OpenRouter credentials inauth.json.--verifymakes one cheap authenticatedGETper provider against its own model catalogue. xAI and Gemini answer a bad key with HTTP 400 rather than 401, so their bodies are inspected too.--applybacks upconfig.yamlto a timestamped file and writes only throughhermes config set/hermes config unset(Hermes' own YAML-safe path — no hand-rolled YAML editing), translates the model id with Hermes'normalize_model_for_provider()/get_default_model_for_provider(), clears the OpenRouter overrides, then re-resolves to prove the new routing.It finds Hermes in any install layout — the standard
~/.hermes/hermes-agent/venv, a root-mode/usr/local/lib/hermes-agent, or anything else (pipx, Homebrew, a relocated venv) via thehermeslauncher's shebang — so plainpython3works, including piped straight fromcurl.Deliberate limits: it never edits
~/.hermes/.env; a fallback chain that mixes OpenRouter and direct entries is reported for manual editing rather than removed wholesale; API key values are never printed, logged or written.Verification
Tested against a real
hermes-agent==0.19.0install (PyPI wheel in a venv) driving syntheticHERMES_HOMEfixtures, including one mirroring the Hermes Desktop layout.Config-variant sweep — diagnosis matches Hermes' own resolver in every case:
model.provideropenrouterprovider: autoopenrouterprovider: openaiprovider: openrouteropenrouterprovider: anthropicanthropicEnd-to-end on the reported scenario:
openrouter+ OpenAI key →--apply→anthropic/api.anthropic.com/ANTHROPIC_API_KEY, confirmed by re-running the resolver. Repeated with an OpenRouter key also present: same result, key retained,.envmtime unchanged. Kitchen-sink config with every escape hatch set at once (aggregatorbase_url, four pinned auxiliary slots, auxfallback_chain,delegation,fallback_providers, plus a pooled OpenRouter credential) is fully cleaned in one pass.Also checked: idempotent re-runs report clean; correct exit codes (
0clean/applied,1blockers,2cannot run); refuses to write when no direct key verifies; rejects--provider openaiwith the alias explanation; graceful on an emptyHERMES_HOMEand when run with an interpreter that lackshermes_cli; no false positives from Hermes' merged config defaults (user config is read separately); no key material in any output across all modes.pyflakesclean, and the repo security gate passes.