File size: 14,157 Bytes
645efc2
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
---

sidebar_position: 10
title: "Model Provider Plugins"
description: "How to build a model provider (inference backend) plugin for Hermes Agent"
---


# Building a Model Provider Plugin

Model provider plugins declare an inference backend β€” an OpenAI-compatible endpoint, an Anthropic Messages server, a Codex-style Responses API, or a Bedrock-native surface β€” that Hermes can route `AIAgent` calls through. Every built-in provider (OpenRouter, Anthropic, GMI, DeepSeek, Nvidia, …) ships as one of these plugins. Third parties can add their own by dropping a directory under `$HERMES_HOME/plugins/model-providers/` with zero changes to the repo.

:::tip
Model provider plugins are the third kind of **provider plugin**. The others are [Memory Provider Plugins](/developer-guide/memory-provider-plugin) (cross-session knowledge) and [Context Engine Plugins](/developer-guide/context-engine-plugin) (context compression strategies). All three follow the same "drop a directory, declare a profile, no repo edits" pattern.
:::

## How discovery works

`providers/__init__.py._discover_providers()` runs lazily the first time any code calls `get_provider_profile()` or `list_providers()`. Discovery order:

1. **Bundled plugins** β€” `<repo>/plugins/model-providers/<name>/` β€” ship with Hermes
2. **User plugins** β€” `$HERMES_HOME/plugins/model-providers/<name>/` β€” drop in any directory; no restart required for subsequent sessions
3. **Legacy single-file** β€” `<repo>/providers/<name>.py` β€” back-compat for out-of-tree editable installs

**User plugins override bundled plugins of the same name** because `register_provider()` is last-writer-wins. Drop a `$HERMES_HOME/plugins/model-providers/gmi/` directory to replace the built-in GMI profile without touching the repo.

## Directory structure

```

plugins/model-providers/my-provider/

β”œβ”€β”€ __init__.py       # Calls register_provider(profile) at module-level

β”œβ”€β”€ plugin.yaml       # kind: model-provider + metadata (optional but recommended)

└── README.md         # Setup instructions (optional)

```

The only required file is `__init__.py`. `plugin.yaml` is used by `hermes plugins` for introspection and by the general PluginManager to route the plugin to the right loader; without it, the general loader falls back to a source-text heuristic.

## Minimal example β€” a simple API-key provider

```python

# plugins/model-providers/acme-inference/__init__.py

from providers import register_provider

from providers.base import ProviderProfile



acme = ProviderProfile(

    name="acme-inference",

    aliases=("acme",),

    display_name="Acme Inference",

    description="Acme β€” OpenAI-compatible direct API",

    signup_url="https://acme.example.com/keys",

    env_vars=("ACME_API_KEY", "ACME_BASE_URL"),

    base_url="https://api.acme.example.com/v1",

    auth_type="api_key",

    default_aux_model="acme-small-fast",

    fallback_models=(

        "acme-large-v3",

        "acme-medium-v3",

        "acme-small-fast",

    ),

)



register_provider(acme)

```

```yaml

# plugins/model-providers/acme-inference/plugin.yaml

name: acme-inference

kind: model-provider

version: 1.0.0

description: Acme Inference β€” OpenAI-compatible direct API

author: Your Name

```

That's it. After dropping these two files, the following **auto-wire** with no other edits:

| Integration | Where | What it gets |
|---|---|---|
| Credential resolution | `hermes_cli/auth.py` | `PROVIDER_REGISTRY["acme-inference"]` populated from profile |
| `--provider` CLI flag | `hermes_cli/main.py` | Accepts `acme-inference` |
| `hermes model` picker | `hermes_cli/models.py` | Appears in `CANONICAL_PROVIDERS`, model list fetched from `{base_url}/models` |
| `hermes doctor` | `hermes_cli/doctor.py` | Health check for `ACME_API_KEY` + `{base_url}/models` probe |
| `hermes setup` | `hermes_cli/config.py` | `ACME_API_KEY` appears in `OPTIONAL_ENV_VARS` and the setup wizard |
| URL reverse-mapping | `agent/model_metadata.py` | Hostname β†’ provider name for auto-detection |
| Auxiliary model | `agent/auxiliary_client.py` | Uses `default_aux_model` for compression / summarization |
| Runtime resolution | `hermes_cli/runtime_provider.py` | Returns correct `base_url`, `api_key`, `api_mode` |
| Transport | `agent/transports/chat_completions.py` | Profile path generates kwargs via `prepare_messages` / `build_extra_body` / `build_api_kwargs_extras` |

## ProviderProfile fields

Full definition in `providers/base.py`. The most useful ones:

| Field | Type | Purpose |
|---|---|---|
| `name` | str | Canonical id β€” matches `model.provider` in `config.yaml` and the `--provider` flag |
| `aliases` | `tuple[str, ...]` | Alternative names resolved by `get_provider_profile()` (e.g. `grok` β†’ `xai`) |
| `api_mode` | str | `chat_completions` \| `codex_responses` \| `anthropic_messages` \| `bedrock_converse` |
| `display_name` | str | Human label shown in `hermes model` picker |
| `description` | str | Picker subtitle |
| `signup_url` | str | Shown during first-run setup ("get an API key here") |
| `env_vars` | `tuple[str, ...]` | API-key env vars in priority order; a final `*_BASE_URL` entry is used as the user base-URL override |
| `base_url` | str | Default inference endpoint |
| `models_url` | str | Explicit catalog URL (falls back to `{base_url}/models`) |
| `auth_type` | str | `api_key` \| `oauth_device_code` \| `oauth_external` \| `copilot` \| `aws_sdk` \| `external_process` |
| `fallback_models` | `tuple[str, ...]` | Curated list shown when live catalog fetch fails |
| `default_headers` | `dict[str, str]` | Sent on every request (e.g. Copilot's `Editor-Version`) |
| `fixed_temperature` | Any | `None` = use caller's value; `OMIT_TEMPERATURE` sentinel = don't send temperature at all (Kimi) |
| `default_max_tokens` | `int \| None` | Provider-level max_tokens cap (Nvidia: 16384) |

| `default_aux_model` | str | Cheap model for auxiliary tasks (compression, vision, summarization) |



## Overridable hooks



Subclass `ProviderProfile` for non-trivial quirks:



```python

from typing import Any

from providers.base import ProviderProfile



class AcmeProfile(ProviderProfile):

    def prepare_messages(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
        """Provider-specific message preprocessing. Runs after codex

        sanitization, before developer-role swap. Default: pass-through."""

        # Example: Qwen normalizes plain-text content to a list-of-parts

        # array and injects cache_control; Kimi rewrites tool-call JSON

        return messages


    def build_extra_body(self, *, session_id=None, **context) -> dict:

        """Provider-specific extra_body fields merged into the API call.

        Context includes: session_id, provider_preferences, model, base_url,

        reasoning_config. Default: empty dict."""

        # Example: OpenRouter's provider-preferences block,

        # Gemini's thinking_config translation.

        return {}


    def build_api_kwargs_extras(self, *, reasoning_config=None, **context):

        """Returns (extra_body_additions, top_level_kwargs). Needed when some

        fields go top-level (Kimi's reasoning_effort, OpenRouter's verbosity for

        adaptive Anthropic models) and some go in extra_body (OpenRouter's

        reasoning dict). Default: ({}, {})."""

        return {}, {}


    def fetch_models(self, *, api_key=None, base_url=None, timeout=8.0) -> list[str] | None:

        """Live catalog fetch. Default hits {models_url or base_url}/models with

        Bearer auth. Override for: custom auth (Anthropic), no REST endpoint

        (Bedrock β†’ None), or public/unauthenticated catalogs (OpenRouter)."""

        return super().fetch_models(api_key=api_key, base_url=base_url, timeout=timeout)

```


## Hook reference examples

Look at these bundled plugins for idioms:

| Plugin | Why look |
|---|---|
| `plugins/model-providers/openrouter/` | Aggregator with provider preferences, public model catalog |
| `plugins/model-providers/gemini/` | `thinking_config` translation (native + OpenAI-compat nested forms) |
| `plugins/model-providers/kimi-coding/` | `OMIT_TEMPERATURE`, `extra_body.thinking`, top-level `reasoning_effort` |
| `plugins/model-providers/qwen-oauth/` | Message normalization, `cache_control` injection, VL high-res |
| `plugins/model-providers/nous/` | Attribution tags, "omit reasoning when disabled" |
| `plugins/model-providers/custom/` | Ollama `num_ctx` + `think: false` quirks |
| `plugins/model-providers/bedrock/` | `api_mode="bedrock_converse"`, `fetch_models` returns None (no REST endpoint) |

## User overrides β€” replace a built-in without editing the repo

Say you want to point `gmi` at your private staging endpoint for testing. Create `~/.hermes/plugins/model-providers/gmi/__init__.py`:

```python

from providers import register_provider

from providers.base import ProviderProfile



register_provider(ProviderProfile(

    name="gmi",

    aliases=("gmi-cloud", "gmicloud"),

    env_vars=("GMI_API_KEY",),

    base_url="https://gmi-staging.internal.example.com/v1",

    auth_type="api_key",

    default_aux_model="google/gemini-3.1-flash-lite-preview",

))

```

Next session, `get_provider_profile("gmi").base_url` returns the staging URL. No repo patch, no rebuild. Because user plugins are discovered after bundled ones, the user `register_provider()` call wins.

## api_mode selection



Four values are recognized. Hermes picks one based on:



1. User explicit override (`config.yaml` `model.api_mode` when set)
2. OpenCode's per-model dispatch (`opencode_model_api_mode` for Zen and Go)
3. URL auto-detection β€” `/anthropic` suffix β†’ `anthropic_messages`, `api.openai.com` β†’ `codex_responses`, `api.x.ai` β†’ `codex_responses`, `/coding` on Kimi domains β†’ `chat_completions`
4. **Profile `api_mode`** as a fallback when URL detection finds nothing

5. Default `chat_completions`



Set `profile.api_mode` to match the default your provider ships β€” it acts as a hint. User URL overrides still win.



## Auth types



| `auth_type` | Meaning | Who uses it |

|---|---|---|

| `api_key` | Single env var carries a static API key | Most providers |

| `oauth_device_code` | Device-code OAuth flow | β€” |

| `oauth_external` | User signs in elsewhere, tokens land in `auth.json` | Anthropic OAuth, MiniMax OAuth, Qwen Portal, Nous Portal |

| `copilot` | GitHub Copilot token refresh cycle | `copilot` plugin only |

| `aws_sdk` | AWS SDK credential chain (IAM role, profile, env) | `bedrock` plugin only |

| `external_process` | Auth handled by a subprocess the agent spawns | `copilot-acp` plugin only |



`auth_type` gates which codepaths treat your provider as a "simple api-key provider" β€” if it's not `api_key`, the PluginManager still records the manifest but Hermes' CLI-level automation (doctor checks, `--provider` flag, setup wizard delegation) may skip over it.



## Discovery timing



Provider discovery is **lazy** β€” triggered by the first `get_provider_profile()` or `list_providers()` call in the process. In practice this happens early at startup (`auth.py` module load extends `PROVIDER_REGISTRY` eagerly). If you need to verify your plugin loaded, run:

```bash

hermes doctor

```

β€” a successful `auth_type="api_key"` profile appears under the Provider Connectivity section with a `/models` probe.

For programmatic inspection:

```python

from providers import list_providers

for p in list_providers():

    print(p.name, p.base_url, p.api_mode)

```

## Testing your plugin

Point `HERMES_HOME` at a temp directory so you don't pollute your real config:

```bash

export HERMES_HOME=/tmp/hermes-plugin-test

mkdir -p $HERMES_HOME/plugins/model-providers/my-provider

cat > $HERMES_HOME/plugins/model-providers/my-provider/__init__.py <<'EOF'

from providers import register_provider

from providers.base import ProviderProfile

register_provider(ProviderProfile(

    name="my-provider",

    env_vars=("MY_API_KEY",),

    base_url="https://api.my-provider.example.com/v1",

    auth_type="api_key",

))

EOF



export MY_API_KEY=your-test-key

hermes -z "hello" --provider my-provider -m some-model

```

## General PluginManager integration

The general `PluginManager` (the thing `hermes plugins` operates on) **sees** model-provider plugins but does not import them β€” `providers/__init__.py` owns their lifecycle. The manager records the manifest for introspection and categorizes by `kind: model-provider`. When you drop an unlabeled user plugin into `$HERMES_HOME/plugins/` that happens to call `register_provider` with a `ProviderProfile`, the manager auto-coerces it to `kind: model-provider` via a source-text heuristic β€” so the plugin still routes correctly even without `plugin.yaml`.

## Distribute via pip

Like any Hermes plugin, model providers can ship as a pip package. Add an entry point to your `pyproject.toml`:

```toml

[project.entry-points."hermes_agent.plugins"]

acme-inference = "acme_hermes_plugin:register"

```

…where `acme_hermes_plugin:register` is a function that calls `register_provider(profile)`. The general PluginManager picks up entry-point plugins during `discover_and_load()`. For `kind: model-provider` pip plugins, you still need to declare the kind in your manifest (or rely on the source-text heuristic).

See [Building a Hermes Plugin](/developer-guide/plugins#distribute-via-pip) for the full entry-points setup.

## Related pages

- [Provider Runtime](/developer-guide/provider-runtime) β€” resolution precedence + where each layer reads the profile
- [Adding Providers](/developer-guide/adding-providers) β€” end-to-end checklist for new inference backends (covers both the fast plugin path and the full CLI/auth integration)
- [Memory Provider Plugins](/developer-guide/memory-provider-plugin)
- [Context Engine Plugins](/developer-guide/context-engine-plugin)
- [Building a Hermes Plugin](/developer-guide/plugins) β€” general plugin authoring