Files
hermes-agent/providers
teknium1 13fe9c7171 feat(providers): external-process provider support for standalone model-provider plugins (from #105863)
The provider-agnostic half of PR #105863, so a CLI-driven subscription provider can ship as a
standalone `kind: model-provider` plugin instead of a bundled one:

- ProviderProfile: `native_reasoning_details_type`, `model_aliases`, `get_model_context_length`,
  `get_usage_cost`, `setup_status`, `discover_models` hooks (all default None / no-op).
- Chat Completions transport: provider-native `reasoning_details` carriers follow only their
  declaring profile; standard records still replay on OpenRouter-style routes, strict routes
  drop the field wholesale (#70233). Relay/stream accumulate `delta.reasoning_details` verbatim.
- `hermes model`: the generic plugin flow gates an external-process row on the CLI's own login
  status (inline `login_command` on a TTY), offers `discover_models()` rows with per-row notes,
  and never writes config when the executable is missing.
- `/model` and the pickers: process providers list their live catalog merged with the pinned
  one, declared aliases/ids resolve inside the provider, and validation accepts a listed id
  without probing `process://`.
- Delegation keeps the selected external-process provider and protocol for the child.
- Model metadata / usage pricing consult the profile's bound and cost hooks first.
- Desktop: `[1m]` renders as a "1M" tag and hyphenated Anthropic versions read "Haiku 4.5".

The bespoke `_model_flow_external_process` and hard-coded `hermes_cli/main.py` paths from the
PR were dropped in favour of main's `_model_flow_plugin_provider`.

Co-authored-by: unsupportedpastels <unsupportedpastels@users.noreply.github.com>
2026-09-20 14:29:39 -07:00
..
…

providers/

Registry and ABC for every inference provider Hermes knows about.

Each provider is declared once as a ProviderProfile. Every other layer — auth resolution, transport kwargs, model listing, runtime routing — reads from these profiles instead of maintaining its own parallel data.


Layout

providers/
├── base.py         ProviderProfile dataclass + OMIT_TEMPERATURE sentinel
├── __init__.py     Registry: register_provider(), get_provider_profile(), list_providers()
└── README.md       This file

The profiles themselves live as plugins under plugins/model-providers/<name>/ (bundled in this repo) and $HERMES_HOME/plugins/model-providers/<name>/ (per-user overrides). The registry in providers/__init__.py lazily discovers them the first time any consumer calls get_provider_profile() or list_providers(). See plugins/model-providers/README.md for the plugin contract and examples.


How it wires in

The registry is populated on first access. After that, every downstream layer reads from it:

  • hermes_cli/auth.py extends PROVIDER_REGISTRY with every api-key profile it sees (skipping copilot, kimi-coding, kimi-coding-cn, zai, openrouter, custom — those need bespoke token resolution).
  • hermes_cli/models.py extends CANONICAL_PROVIDERS and calls profile.fetch_models() inside provider_model_ids().
  • hermes_cli/doctor.py adds a /models health check for each auth_type="api_key" profile.
  • hermes_cli/config.py injects every env_var into OPTIONAL_ENV_VARS so the setup wizard knows about it.
  • hermes_cli/runtime_provider.py reads profile.api_mode as a fallback when URL detection finds nothing.
  • agent/model_metadata.py maps hostname → provider via profile.get_hostname().
  • agent/auxiliary_client.py reads profile.default_aux_model first before falling back to the legacy hardcoded dict.
  • agent/transports/chat_completions.py::_build_kwargs_from_profile() invokes profile.prepare_messages(), profile.build_extra_body(), and profile.build_api_kwargs_extras() on every call.
  • run_agent.py passes provider_profile=<ProviderProfile> so the transport takes the profile path instead of the legacy flag path.

Adding a provider

See plugins/model-providers/README.md — drop a new directory there (or under $HERMES_HOME/plugins/model-providers/ for a private plugin).


Hooks you can override on ProviderProfile

Hook Purpose
get_hostname() URL-based detection — default derives from base_url.
prepare_messages(msgs) Provider-specific message preprocessing (Qwen normalises to list-of-parts, injects cache_control).
build_extra_body(**ctx) Provider-specific extra_body (OpenRouter provider prefs, Gemini thinking_config).
build_api_kwargs_extras(**ctx) (extra_body_additions, top_level_kwargs) — Kimi puts reasoning_effort top-level, Qwen splits enable_thinking/thinking_budget.
supported_reasoning_efforts(model) Declared per-model reasoning-effort vocabulary for gateways that 400 on unknown levels (Ramp Router reads its live catalog). None = defer to transport defaults, () = model takes no reasoning params, tuple = clamp target. Must be cache-only — called on the request hot path.
fetch_models(*, api_key) Live catalog fetch — default hits {models_url or base_url}/models with Bearer auth. Override for no-REST providers (Bedrock), OAuth catalogs (Anthropic), or public catalogs (OpenRouter).

Configuration fields

Full reference in providers/base.py dataclass definition.