Academy → Developer GuideOfficial documentation · Arabic guidance

Model Provider Plugins

إضافات مزوّد النماذج

Advanced9 min readLesson 194 questions✓ 2026-08-18
Before you read

What this page is, and what it holds.

This page covers Model Provider Plugins. You will use hermes plugins and hermes doctor here; about 9 minutes to read. The priciest model is not always best for your task. Compare on one task and set a spend cap.

14sections
10code examples
4tables
2commands
1,546source words
The official one-line description

How to build a model provider (inference backend) plugin for Hermes Agent

What you will be able to do

Outcomes taken from this page, not a template.

  • Understand what المزوّد والنموذج is and when you need it.
  • Run hermes plugins and hermes doctor and understand what happens next.
  • Read the table and take only the row that applies to you.
  • Set ACME_API_KEY in the right place.
Identifiers you will meet

Exactly as they appear in Hermes.

Commands
  • hermes plugins
  • hermes doctor
Environment variables
  • ACME_API_KEY
  • ACME_BASE_URL
  • GMI_API_KEY
  • PROVIDER_REGISTRY
  • HERMES_HOME
Page map

Jump to the part you need.

  1. 01How discovery works
  2. 02Directory structure
  3. 03Minimal example — a simple API-key provider
  4. 04ProviderProfile fields
  5. 05Overridable hooks
  6. 06Hook reference examples
  7. 07User overrides — replace a built-in without editing the repo
  8. 08apimode selection
  9. 09Auth types
  10. 10Discovery timing
  11. 11Testing your plugin
  12. 12General PluginManager integration
  13. 13Distribute via pip
  14. 14Related pages
The full official page

Nothing summarised away.

The documentation body below is reproduced from the official source so commands and identifiers stay exact. Each section carries a short note describing what it contains.

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.

How discovery works

Explains the idea itself. Read it slowly; the later sections build on it.

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

Commands you type in a terminal. Understand what one does before copying it. Commands here: hermes plugins.

Text4 lines
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

A lookup table. Do not read it all; find the row that applies to you.

Python22 lines
# 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)
YAML6 lines
# 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:

IntegrationWhereWhat it gets
Credential resolutionhermes_cli/auth.pyPROVIDER_REGISTRY["acme-inference"] populated from profile
--provider CLI flaghermes_cli/main.pyAccepts acme-inference
hermes model pickerhermes_cli/models.pyAppears in CANONICAL_PROVIDERS, model list fetched from {base_url}/models
hermes doctorhermes_cli/doctor.pyHealth check for ACME_API_KEY + {base_url}/models probe
hermes setuphermes_cli/config.pyACME_API_KEY appears in OPTIONAL_ENV_VARS and the setup wizard
URL reverse-mappingagent/model_metadata.pyHostname → provider name for auto-detection
Auxiliary modelagent/auxiliary_client.pyUses default_aux_model for compression / summarization
Runtime resolutionhermes_cli/runtime_provider.pyReturns correct base_url, api_key, api_mode
Transportagent/transports/chat_completions.pyProfile path generates kwargs via prepare_messages / build_extra_body / build_api_kwargs_extras

ProviderProfile fields

A lookup table. Do not read it all; find the row that applies to you.

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

FieldTypePurpose
namestrCanonical id — matches model.provider in config.yaml and the --provider flag
aliasestuple[str, ...]Alternative names resolved by get_provider_profile() (e.g. grok → xai)
api_modestrchat_completions | codex_responses | anthropic_messages | bedrock_converse
display_namestrHuman label shown in hermes model picker
descriptionstrPicker subtitle
signup_urlstrShown during first-run setup ("get an API key here")
env_varstuple[str, ...]API-key env vars in priority order; a final *_BASE_URL entry is used as the user base-URL override
base_urlstrDefault inference endpoint
models_urlstrExplicit catalog URL (falls back to {base_url}/models)
auth_typestrapi_key | oauth_device_code | oauth_external | copilot | aws_sdk | external_process
fallback_modelstuple[str, ...]Curated list shown when live catalog fetch fails
default_headersdict[str, str]Sent on every request (e.g. Copilot's Editor-Version)
fixed_temperatureAnyNone = use caller's value; OMIT_TEMPERATURE sentinel = don't send temperature at all (Kimi)
default_max_tokensint | NoneProvider-level max_tokens cap (Nvidia: 16384)
default_aux_modelstrCheap model for auxiliary tasks (compression, vision, summarization)

Overridable hooks

Explains the idea itself. Read it slowly; the later sections build on it.

Subclass ProviderProfile for non-trivial quirks:

Python31 lines
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

A lookup table. Do not read it all; find the row that applies to you.

Look at these bundled plugins for idioms:

PluginWhy 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

Settings you configure once. Change one at a time so you can see what each does. Set GMI_API_KEY in your environment, not in the chat.

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

Python11 lines
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.

apimode selection

Explains the idea itself. Read it slowly; the later sections build on it.

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

A lookup table. Do not read it all; find the row that applies to you.

auth_typeMeaningWho uses it
api_keySingle env var carries a static API keyMost providers
oauth_device_codeDevice-code OAuth flow—
oauth_externalUser signs in elsewhere, tokens land in auth.jsonAnthropic OAuth, MiniMax OAuth, Qwen Portal, Nous Portal
copilotGitHub Copilot token refresh cyclecopilot plugin only
aws_sdkAWS SDK credential chain (IAM role, profile, env)bedrock plugin only
external_processAuth handled by a subprocess the agent spawnscopilot-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

Settings you configure once. Change one at a time so you can see what each does. Commands here: hermes doctor. Set PROVIDER_REGISTRY in your environment, not in the chat.

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:

Shell1 line
hermes doctor

— a successful auth_type="api_key" profile appears under the Provider Connectivity section with a /models probe.

For programmatic inspection:

Python3 lines
from providers import list_providers
for p in list_providers():
    print(p.name, p.base_url, p.api_mode)

Testing your plugin

Settings you configure once. Change one at a time so you can see what each does. Set HERMES_HOME in your environment, not in the chat.

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

Shell15 lines
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

Commands you type in a terminal. Understand what one does before copying it. Commands here: hermes plugins.

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

Explains the idea itself. Read it slowly; the later sections build on it.

Model providers can ship as a pip package. Expose an entry point in the hermes_agent.plugins group in your pyproject.toml:

TOML2 lines
[project.entry-points."hermes_agent.plugins"]
acme-inference = "acme_hermes_plugin:register"

The target may be either:

  • a callable (module:func) — invoked with no arguments; it should call register_provider(profile), or
  • a bare module (module) — imported for its module-level register_provider(...) side effect, mirroring the directory-plugin __init__.py contract.

providers/__init__.py discovers these entry points itself — the general PluginManager never invokes provider registration for pip packages (its entry-point path targets register(ctx)-style general plugins, gated by plugins.enabled), so the provider registry does its own scan. Two rules apply:

  • Opt-in required. The same plugins.enabled allow-list (and plugins.disabled deny-list) from config.yaml governs this scan. A pip package is never imported just because it is installed — users must add the entry-point name to plugins.enabled:
YAML3 lines
  plugins:
    enabled:
      - acme-inference
  • Lowest precedence. Entry-point plugins are discovered before filesystem plugins: because register_provider() is last-writer-wins, a bundled or $HERMES_HOME profile of the same name always overrides a pip-installed one. A pip package can add a genuinely new provider, but cannot silently hijack a first-party provider name.

Targets that require arguments (a general plugin's register(ctx)) are skipped by the provider scan — they belong to the PluginManager. A broken entry point is isolated — it is logged at warning level and skipped, and never blocks discovery of the other providers.

See Building a Hermes Plugin for the full entry-points setup.

Knowledge check

4 questions answered by this page alone.

Every option is a real identifier from the Hermes documentation. The wrong ones are real too, just from other pages.

1. In this lesson's table, what is the “Where” for “hermes setup”?
2. Which of these environment variables actually appears in this lesson?
3. Which of these headings does not appear in this lesson?
4. Which configuration key appears in this lesson's examples?