Skip to content

feat(ops): hermes-provider-fix — stop Hermes routing through OpenRouter - #124

Open
Deesmo wants to merge 4 commits into
mainfrom
cursor/fix-hermes-openrouter-provider-routing-34c7
Open

feat(ops): hermes-provider-fix — stop Hermes routing through OpenRouter#124
Deesmo wants to merge 4 commits into
mainfrom
cursor/fix-hermes-openrouter-provider-routing-34c7

Conversation

@Deesmo

@Deesmo Deesmo commented Aug 24, 2026

Copy link
Copy Markdown
Owner

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 unset model.provider in this order:

# Check Result
1 explicit CLI --api-key / --base-url OpenRouter
2 model.provider from config.yaml that provider
3 OPENAI_API_KEY or OPENROUTER_API_KEY present OpenRouter
4 OpenRouter credential in auth.json OpenRouter
5 any other provider's key (Anthropic, Gemini, xAI, …) that provider

Step 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.0 with four direct keys and no OpenRouter key at all, by calling Hermes' own resolver:

resolve_requested_provider()  -> 'auto'
resolve_provider('auto')      -> 'openrouter'
resolve_runtime_provider()    -> provider=openrouter
                                 base_url=https://openrouter.ai/api/v1
                                 api_key=<the OPENAI_API_KEY>

Two related traps, also covered:

  • model.provider: openai is not the OpenAI API. openai is an alias for openrouter in hermes_cli.providers.ALIASES, and the resolver separately rejects it as an unknown provider. The direct provider is openai-api.
  • auxiliary.<task>.provider, delegation.provider and fallback_providers stay 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/.env is never written. An explicit model.provider just stops the OpenRouter key from winning auto-detection; it remains available on demand. Verified with a key present:

default session      -> anthropic  https://api.anthropic.com
explicit openrouter  -> openrouter https://openrouter.ai/api/v1   key resolves: True

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.

  • Diagnosis runs inside the Hermes interpreter and calls Hermes' own resolve_requested_provider() / resolve_runtime_provider(), so it reports what Hermes actually does instead of re-implementing the precedence rules. It additionally scans model.base_url, every auxiliary.* override and its fallback_chain, delegation.provider, fallback_providers / legacy fallback_model, OPENROUTER_API_KEY / OPENAI_BASE_URL / OPENROUTER_BASE_URL, and OpenRouter credentials in auth.json.
  • --verify makes one cheap authenticated GET per 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.
  • --apply backs up config.yaml to a timestamped file and writes only through hermes 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 the hermes launcher's shebang — so plain python3 works, including piped straight from curl.

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.0 install (PyPI wheel in a venv) driving synthetic HERMES_HOME fixtures, including one mirroring the Hermes Desktop layout.

Config-variant sweep — diagnosis matches Hermes' own resolver in every case:

Variant Hermes resolves Tool reports
no model.provider openrouter blocker, explains precedence step 3
provider: auto openrouter blocker
provider: openai raises Unknown provider blocker, explains the alias
provider: openrouter openrouter blocker
provider: anthropic anthropic clean

End-to-end on the reported scenario: openrouter + OpenAI key → --applyanthropic / api.anthropic.com / ANTHROPIC_API_KEY, confirmed by re-running the resolver. Repeated with an OpenRouter key also present: same result, key retained, .env mtime unchanged. Kitchen-sink config with every escape hatch set at once (aggregator base_url, four pinned auxiliary slots, aux fallback_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 (0 clean/applied, 1 blockers, 2 cannot run); refuses to write when no direct key verifies; rejects --provider openai with the alias explanation; graceful on an empty HERMES_HOME and when run with an interpreter that lacks hermes_cli; no false positives from Hermes' merged config defaults (user config is read separately); no key material in any output across all modes.

pyflakes clean, and the repo security gate passes.

Open in Web Open in Cursor 

cursoragent and others added 2 commits August 24, 2026 18:01
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>
@Deesmo
Deesmo marked this pull request as ready for review August 24, 2026 18:03

@cursor cursor Bot left a comment

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Cursor Bugbot has reviewed your changes using high effort and found 4 potential issues.

Fix All in Cursor

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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.

Fix in Cursor Fix in Web

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`.")

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

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

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

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."
),
)

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

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)
Fix in Cursor Fix in Web

Reviewed by Cursor Bugbot for commit 3df81d6. Configure here.

cursoragent and others added 2 commits August 24, 2026 18:09
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.
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

2 participants