Reworks the salvaged OpenCode Free provider to match the tier's real auth contract (verified live 2026-08-21): the Zen relay serves free models ANONYMOUSLY and 401s any unrecognized bearer, so the provider now declares no credentials at all and routes every model through the shared keyless machinery from the Ox Alpha fix (empty Authorization default header overriding the SDK bearer). On top of the salvaged base: - auth.py: no api_key_env_vars; drop the keyed-auth special case - runtime_provider.py: restore the plain fail-closed path (opencode-free never reaches it — the keyless runtime resolves first) - models.py: opencode-free joins the opencode family (prefix stripping, Zen endpoint routing incl. muse->responses); keyless predicate extended with unsuffixed free slugs (big-pickle); free runtime pins EVERY opencode-free model keyless; curated catalog replaces the models.dev cost==0 filter (it lags reality: deepseek-v4-flash-free stayed 'free' there after its promo ended and the relay began 401ing it — delisted) - agent_runtime_helpers.py: replace the httpx transport-sharing auth-strip wrapper with the shared header policy (no proxy-mount loss) - model_setup_flows.py: skip the API-key prompt for opencode-free - plugin profile: keyless headers, no env vars - .env.example + providers.md: keyless docs (no OPENCODE_FREE_API_KEY) - tests rewritten to the keyless contract, incl. catalog-membership invariant (every curated model must satisfy the keyless predicate) E2E: full AIAgent turns with zero keys complete on x-preview-f-free via provider opencode-free and alias 'free', incl. a real terminal tool round-trip; muse routes to /v1/responses; picker lists 8 keyless models.
Model Provider Plugins
Each subdirectory is a self-contained provider profile plugin. The
directory layout mirrors plugins/platforms/:
plugins/model-providers/
├── openrouter/
│ ├── __init__.py # registers the ProviderProfile
│ └── plugin.yaml # manifest: name, kind, version, description
├── anthropic/
│ ├── __init__.py
│ └── plugin.yaml
└── ...
How discovery works
providers/__init__.py._discover_providers() scans this directory (and
$HERMES_HOME/plugins/model-providers/) the first time anything calls
get_provider_profile() or list_providers(). Each __init__.py is
imported and expected to call providers.register_provider(profile).
User plugins at $HERMES_HOME/plugins/model-providers/<name>/ override
bundled plugins of the same name — last-writer-wins in
register_provider(). Drop a file there to replace a built-in.
Adding a new provider
-
Create
plugins/model-providers/<your_provider>/__init__.py:from providers import register_provider from providers.base import ProviderProfile my_provider = ProviderProfile( name="your-provider", aliases=("alias1", "alias2"), display_name="Your Provider", description="One-line description shown in the setup picker", signup_url="https://your-provider.example.com/keys", env_vars=("YOUR_PROVIDER_API_KEY", "YOUR_PROVIDER_BASE_URL"), base_url="https://api.your-provider.example.com/v1", default_aux_model="your-cheap-model", ) register_provider(my_provider) -
Create
plugins/model-providers/<your_provider>/plugin.yaml:name: your-provider-profile kind: model-provider version: 1.0.0 description: Short sentence about the provider author: Your Name
Nothing else needs to change. auth.py, config.py, models.py,
doctor.py, model_metadata.py, runtime_provider.py, and the
chat_completions transport all auto-wire from the registry.
Non-trivial profiles
Override the ProviderProfile hooks in a subclass for per-provider
quirks — see plugins/model-providers/openrouter/__init__.py for
build_extra_body and build_api_kwargs_extras examples, and
plugins/model-providers/gemini/__init__.py for thinking_config
translation.