Hermes Agent Wiki 非公式・日本語wiki

モデルプロバイダプラグインを作る

目次

モデルプロバイダプラグインは推論バックエンドを申告するものです。OpenAI 互換のエンドポイント、Anthropic Messages のサーバー、Codex 形式の Responses API、Bedrock ネイティブの窓口などを申告すると、Hermes は AIAgent の呼び出しをそこへ流せるようになります。組み込みのプロバイダ(OpenRouter、Anthropic、GMI、DeepSeek、Nvidia など)はすべてこの形のプラグインとして同梱されています。第三者も $HERMES_HOME/plugins/model-providers/ の下にディレクトリを置くだけで、リポジトリには一切手を入れずに自分のプロバイダを追加できます。

探索のしくみ

providers/__init__.py._discover_providers() は、どこかのコードが最初に get_provider_profile()list_providers() を呼んだ時点で遅延実行されます。探索の順序は次のとおりです。

  1. 同梱のプラグイン<repo>/plugins/model-providers/<name>/ — Hermes に付属します
  2. 利用者のプラグイン$HERMES_HOME/plugins/model-providers/<name>/ — 好きなディレクトリを置くだけで、次のセッションからは再起動なしで有効です
  3. 従来の単一ファイル形式<repo>/providers/<name>.py — リポジトリ外の editable インストール向けの後方互換です

同じ名前なら、利用者のプラグインが同梱のプラグインを上書きします。 register_provider() は後から書いたほうが勝つ作りだからです。$HERMES_HOME/plugins/model-providers/gmi/ というディレクトリを置けば、リポジトリに触らずに組み込みの GMI のプロファイルを差し替えられます。

ディレクトリ構成

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)

必須のファイルは __init__.py だけです。plugin.yamlhermes plugins が中身を調べるときと、汎用の PluginManager がプラグインを正しい読み込み処理へ振り分けるときに使われます。無い場合、汎用の読み込み処理はソースコードの文面から推測して判断します。

最小の例 — API キーだけの単純なプロバイダ

# 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)
# 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

これで終わりです。この 2 つのファイルを置くだけで、以下が他に何も編集せずに 自動でつながります

つながる先 場所 得られるもの
認証情報の解決 hermes_cli/auth.py PROVIDER_REGISTRY["acme-inference"] がプロファイルから埋まります
--provider の CLI フラグ hermes_cli/main.py acme-inference を受け付けます
hermes model のモデル選択画面 hermes_cli/models.py CANONICAL_PROVIDERS に現れ、モデル一覧は {base_url}/models から取得されます
hermes doctor hermes_cli/doctor.py ACME_API_KEY の確認と {base_url}/models への疎通チェック
hermes setup hermes_cli/config.py ACME_API_KEYOPTIONAL_ENV_VARS と初期設定の案内に現れます
URL からの逆引き agent/model_metadata.py ホスト名からプロバイダ名を自動で判定します
補助モデル agent/auxiliary_client.py 圧縮や要約に default_aux_model を使います
実行時の解決 hermes_cli/runtime_provider.py 正しい base_urlapi_keyapi_mode を返します
通信層 agent/transports/chat_completions.py プロファイル経由の経路が prepare_messages / build_extra_body / build_api_kwargs_extras から引数を組み立てます

ProviderProfile の項目

完全な定義は providers/base.py にあります。よく使うものは次のとおりです。

項目 用途
name str 正式な id — config.yamlmodel.provider--provider フラグに一致します
aliases tuple[str, ...] get_provider_profile() が解決してくれる別名(例: grokxai
api_mode str chat_completions \ codex_responses \ anthropic_messages \ bedrock_converse
display_name str hermes model の選択画面に出す表示名
description str 選択画面の副題
signup_url str 初回設定のときに出す案内(「API キーはここで取得」)
env_vars tuple[str, ...] API キーの環境変数を優先順に並べたもの。最後の *_BASE_URL の項目は、利用者がベース URL を上書きするために使われます
base_url str 既定の推論エンドポイント
models_url str カタログの URL を明示するもの(無ければ {base_url}/models
auth_type str api_key \ oauth_device_code \ oauth_external \ copilot \ aws_sdk \ external_process
fallback_models tuple[str, ...] 実際のカタログ取得に失敗したときに見せる、選りすぐりの一覧
default_headers dict[str, str] すべてのリクエストに付けるもの(例: Copilot の Editor-Version
fixed_temperature Any None なら呼び出し側の値を使い、OMIT_TEMPERATURE という目印なら temperature を一切送りません(Kimi)
default_max_tokens `int \ None` プロバイダごとの max_tokens の上限(Nvidia は 16384)
default_aux_model str 補助的な処理(圧縮、画像の読み取り、要約)に使う安価なモデル

上書きできるフック

込み入った癖に対応したいときは ProviderProfile を継承してください。

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)

フックの実例

書き方の参考には、同梱の次のプラグインを見てください。

プラグイン 見どころ
plugins/model-providers/openrouter/ プロバイダの優先設定を持つ集約型、公開されたモデルカタログ
plugins/model-providers/gemini/ thinking_config の変換(ネイティブ形式と OpenAI 互換の入れ子形式の両方)
plugins/model-providers/kimi-coding/ OMIT_TEMPERATUREextra_body.thinking、トップレベルの reasoning_effort
plugins/model-providers/qwen-oauth/ メッセージの正規化、cache_control の差し込み、VL の高解像度対応
plugins/model-providers/nous/ 帰属タグ、「無効なときは reasoning を送らない」処理
plugins/model-providers/custom/ Ollama の num_ctxthink: false の癖
plugins/model-providers/bedrock/ api_mode="bedrock_converse"、REST のエンドポイントが無いため fetch_models は None を返す

利用者による上書き — リポジトリを編集せずに組み込みを差し替える

たとえば試験用に gmi を自分の非公開ステージング環境へ向けたいとします。~/.hermes/plugins/model-providers/gmi/__init__.py を次のように作ります。

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

次のセッションから get_provider_profile("gmi").base_url はステージングの URL を返します。リポジトリへのパッチも再ビルドも要りません。利用者のプラグインは同梱のものより後で見つかるので、利用者側の register_provider() の呼び出しが勝ちます。

api_mode の決まり方

認識される値は 4 つです。Hermes は次の順で決めます。

  1. 利用者による明示的な上書き(config.yamlmodel.api_mode が設定されているとき)
  2. OpenCode のモデルごとの振り分け(Zen と Go 向けの opencode_model_api_mode
  3. URL からの自動判定 — 末尾が /anthropic なら anthropic_messagesapi.openai.com なら codex_responsesapi.x.ai なら codex_responses、Kimi のドメインで /coding なら chat_completions
  4. URL からは何も判定できなかったときの受け皿としての プロファイルの api_mode
  5. 既定の chat_completions

profile.api_mode には、そのプロバイダが標準で提供している方式を設定してください。あくまでヒントとして働き、利用者による URL の上書きのほうが依然として優先されます。

認証の種類

auth_type 意味 使っているもの
api_key 環境変数 1 つに固定の API キーが入る ほとんどのプロバイダ
oauth_device_code デバイスコード方式の OAuth
oauth_external 利用者が別の場所でサインインし、トークンが auth.json に入る Anthropic OAuth、MiniMax OAuth、Qwen Portal、Nous Portal
copilot GitHub Copilot のトークン更新の流れ copilot プラグインのみ
aws_sdk AWS SDK の認証情報の探索(IAM ロール、プロファイル、環境変数) bedrock プラグインのみ
external_process エージェントが起動する別プロセスが認証を担う copilot-acp プラグインのみ

auth_type は、どの処理経路がそのプロバイダを「単純な API キーのプロバイダ」として扱うかを決めます。api_key でない場合、PluginManager はマニフェストを記録はしますが、Hermes の CLI まわりの自動処理(doctor の確認、--provider フラグ、初期設定の案内への引き渡し)は素通りすることがあります。

探索のタイミング

プロバイダの探索は 遅延実行 です。そのプロセスで最初に get_provider_profile()list_providers() が呼ばれたときに走ります。実際には起動の早い段階で起こります(auth.py はモジュールの読み込み時点で PROVIDER_REGISTRY を先に拡張します)。プラグインが読み込まれたか確かめたいときは、次を実行してください。

hermes doctor

auth_type="api_key" のプロファイルがうまく読み込まれていれば、Provider Connectivity の節に /models への疎通チェックとともに現れます。

プログラムから中身を調べるには次のようにします。

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

プラグインを試す

本物の設定を汚さないよう、HERMES_HOME を一時ディレクトリに向けてください。

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

汎用の PluginManager との関係

汎用の PluginManagerhermes plugins が操作する対象)は、モデルプロバイダのプラグインを 認識はします が、読み込みはしません — その面倒を見るのは providers/__init__.py です。マネージャはマニフェストを記録して中身を調べられるようにし、kind: model-provider として分類します。種別を書いていない利用者のプラグインを $HERMES_HOME/plugins/ に置いて、それがたまたま ProviderProfile を伴う register_provider を呼んでいる場合、マネージャはソースコードの文面から推測して kind: model-provider に読み替えます — つまり plugin.yaml が無くても正しく振り分けられます。

pip で配布する

モデルプロバイダは pip のパッケージとして配れます。pyproject.tomlhermes_agent.plugins グループのエントリポイントを公開してください。

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

指定先は次のどちらでもかまいません。

  • 呼び出せるものmodule:func)— 引数なしで呼ばれます。中で

register_provider(profile) を呼んでください。

  • モジュールそのものmodule)— モジュールを読み込んだときの

register_provider(...) の副作用が目的です。ディレクトリ形式のプラグインの __init__.py と同じ考え方です。

これらのエントリポイントは providers/__init__.py が自分で探します — 汎用の PluginManager は pip パッケージについてプロバイダの登録を呼び出しません (マネージャのエントリポイント経路が対象にするのは register(ctx) 形式の汎用プラグインで、 plugins.enabled で制御されます)。そのためプロバイダのレジストリは独自に走査します。 ここには 2 つの決まりがあります。

  • 明示的に有効にする必要があります。 この走査も config.yaml

plugins.enabled 許可リスト(と plugins.disabled 拒否リスト)に従います。 pip のパッケージは、インストールされているというだけで読み込まれることはありません。 利用者がエントリポイントの名前を plugins.enabled に加える必要があります。

plugins:
  enabled:
    - acme-inference
  • 優先度はいちばん低いです。 エントリポイントのプラグインは、ファイルシステム上の

プラグインより 先に 見つかります。register_provider() は後から書いたほうが勝つので、 同じ名前の同梱プロファイルや $HERMES_HOME のプロファイルが、必ず pip で入れたものを 上書きします。pip のパッケージは本当に新しいプロバイダを足すことはできますが、 一次提供のプロバイダ名を黙って乗っ取ることはできません。

引数が必要な指定先(汎用プラグインの register(ctx))は、プロバイダの走査では 読み飛ばされます。そちらは PluginManager の担当です。壊れたエントリポイントは 切り離され、警告として記録して読み飛ばすだけなので、他のプロバイダの探索を 止めることはありません。

エントリポイントの設定全体については Hermes プラグインを作る を参照してください。