実行時のプロバイダー解決
目次
Hermes には、共通のプロバイダー解決処理があり、次のどこからでも使われます。
- CLI
- ゲートウェイ
- cron のジョブ
- ACP
- 補助モデルの呼び出し
主な実装は次のファイルです。
hermes_cli/runtime_provider.py— 資格情報の解決、独自エンドポイントの実行時解決hermes_cli/auth.py— プロバイダーの登録簿とresolve_provider()hermes_cli/model_switch.py— CLI とゲートウェイで共通の/model切り替え処理agent/auxiliary_client.py— 補助モデルの振り分けproviders/— 抽象基底クラスと登録簿の入口 (ProviderProfile、register_provider、get_provider_profile、list_providers)plugins/model-providers/<name>/— プロバイダーごとのプラグイン (同梱)。api_mode、base_url、env_vars、fallback_modelsを宣言し、最初に使われた時点で自分を登録簿へ登録します。$HERMES_HOME/plugins/model-providers/<name>/に置いたユーザー側のプラグインは、同じ名前の同梱プラグインより優先されます。
providers/ の get_provider_profile() は、プロバイダー ID を渡すと ProviderProfile を返します。runtime_provider.py は解決のたびにこれを呼び、正式な base_url、優先順つきの env_vars、api_mode、fallback_models を受け取ります。同じ情報をあちこちのファイルに書き写さずに済むわけです。plugins/model-providers/<your-provider>/ (または $HERMES_HOME/plugins/model-providers/<your-provider>/) に新しいプラグインを置いて register_provider() を呼べば、それだけで runtime_provider.py が拾ってくれます。解決処理そのものに分岐を足す必要はありません。
新しいプロバイダーを一級市民として足したい場合は、このページと合わせて プロバイダーを追加する と モデルプロバイダープラグインの手引き も読んでください。
chat completions が返す reasoning の形
OpenAI 互換のリレーは、reasoning や reasoning_content を文字列で返すこともあれば、 テキストパートの辞書、あるいはテキストパートと文字列断片が混じった配列で返すこともあります。 Hermes はこうしたフィールドを、メインのストリーム、Relay の記録、同期・非同期の補助ストリーム、 完了したレスポンスからの reasoning 抽出のどれでも、文字列として扱う前に平坦化します。 断片がもともと持っている空白はそのまま残り、平坦化のときにフィールドの中へ区切りを足すことはありません。 メインのストリームと Relay の記録では、太字の reasoning 見出しがそろっている箇所の段落の切れ目は これまでどおり残ります。reasoning は目に見える回答とは分けたままです。
delta.reasoning_details は、プロバイダーごとの中身を解釈しない記録が並んだリストです。 メインのストリームでも Relay の記録でも、これらの記録は届いた順にそのまま追加され、 平坦化も、まとめ直しも、中身の書き換えもしません。プロバイダーは最後の delta でまとめて出しても、 複数の delta に分けて出してもかまいません。新しい記録がないチャンクではこのフィールドを省きます。 集めた記録はレスポンスの正規化とアシスタントメッセージの保存を通り、入れ子になった署名付きのデータも含めて セッションの再生まで残ります。
解決の優先順位
おおまかには、プロバイダーは次の順で決まります。
- CLI や実行時に明示された指定
config.yamlのモデル・プロバイダー設定- 環境変数
- プロバイダーごとの既定値、または自動解決
この順番には意味があります。ふだんの実行では、保存されたモデル・プロバイダーの選択こそが正しい値だ、と Hermes は考えるからです。こうしておけば、シェルに残った古い export が、hermes model で最後に選んだエンドポイントを黙って上書きしてしまうことがありません。
プロバイダー
いま扱えるプロバイダーの系統は次のとおりです (同梱されている全部は plugins/model-providers/ を見てください)。
- AI Gateway (Vercel)
- OpenRouter
- Nous Portal
- OpenAI Codex
- Copilot / Copilot ACP
- Anthropic (ネイティブ)
- Google / Gemini (
gemini) - Alibaba / DashScope (
alibaba、alibaba-coding-plan) - DeepSeek
- Z.AI
- Kimi / Moonshot (
kimi-coding、kimi-coding-cn) - MiniMax (
minimax、minimax-cn、minimax-oauth) - Kilo Code
- Hugging Face
- OpenCode Zen / OpenCode Go
- AWS Bedrock
- Azure Foundry
- NVIDIA NIM
- xAI (Grok)
- Arcee
- GMI Cloud
- StepFun
- Qwen OAuth
- Xiaomi
- Ollama Cloud
- LM Studio
- Tencent TokenHub
- Custom (
provider: custom) — OpenAI 互換のエンドポイントなら何でも扱える、一級市民のプロバイダー - 名前付きの独自プロバイダー (config.yaml の
providers:辞書。古いcustom_providersのリストも、互換のためにいまも読まれます)
実行時解決が返すもの
解決処理は、たとえば次のような値を返します。
providerapi_modebase_urlapi_keysource- 有効期限や更新の情報など、プロバイダー固有の付帯情報
これが効いてくる場面
この解決処理があるおかげで、Hermes は認証と実行時の判断を次のあいだで共有できます。
hermes chat- ゲートウェイでのメッセージ処理
- まっさらなセッションで動く cron のジョブ
- ACP のエディターセッション
- 補助モデルの仕事
AI Gateway
~/.hermes/.env に AI_GATEWAY_API_KEY を書き、--provider ai-gateway を付けて実行します。Hermes はゲートウェイの /models エンドポイントから使えるモデルを取ってきて、ツールを使える言語モデルだけに絞り込みます。
OpenRouter・AI Gateway・OpenAI 互換の独自ベース URL
複数のプロバイダーの鍵がある場合 (たとえば OPENROUTER_API_KEY、AI_GATEWAY_API_KEY、OPENAI_API_KEY が同居しているとき)、独自エンドポイントへ間違った鍵を送ってしまわないための処理が Hermes には入っています。
それぞれの API キーは、自分のベース URL だけに使われます。
OPENROUTER_API_KEYはopenrouter.aiのエンドポイントにしか送られませんAI_GATEWAY_API_KEYはai-gateway.vercel.shのエンドポイントにしか送られませんOPENAI_API_KEYは独自エンドポイント向け、および最後の受け皿として使われます
Hermes は次の 2 つも区別します。
- こちらが選んだ本物の独自エンドポイント
- 独自エンドポイントを設定していないときに使われる、OpenRouter への切り替え経路
この区別がとくに効くのは、次のような場面です。
- 手元で動かすモデルサーバー
- OpenRouter でも AI Gateway でもない、OpenAI 互換の API
- セットアップをやり直さずにプロバイダーを切り替えるとき
- 設定に保存した独自エンドポイントを、いまのシェルに
OPENAI_BASE_URLが export されていなくても使い続けたいとき
Anthropic のネイティブ経路
Anthropic はもう「OpenRouter 経由でだけ使うもの」ではありません。
解決の結果が anthropic になったとき、Hermes は次を使います。
api_mode = anthropic_messages- Anthropic の Messages API そのもの
- 変換のための
agent/anthropic_adapter.py
ネイティブの Anthropic では、資格情報の解決は、更新できる Claude Code の資格情報を、環境変数へ写したトークンより優先するようになりました。実際には次のように動きます。
- 更新できる認証情報を含む Claude Code の資格情報ファイルがあれば、そちらを優先します
- 手で入れた
ANTHROPIC_TOKENやCLAUDE_CODE_OAUTH_TOKENの値も、明示的な上書きとしていまも効きます - ネイティブの Messages API を呼ぶ前に、Anthropic の資格情報の更新を先に済ませます
- 401 が返ったときは、Anthropic のクライアントを作り直して一度だけやり直す、という受け皿も残しています
OpenAI Codex の経路
Codex は Responses API を使う別経路です。
api_mode = codex_responses- 専用の資格情報の解決と、認証情報の保管への対応
- 再開したセッションに残っていた Codex の推論項目(
encrypted_content)が拒否されたとき(400 のinvalid_encrypted_contentでも、401 のtoken_expiredでも)は、資格情報の更新やプールの切り替えに進む前に、保存していた項目を外して一度だけ送り直し、自力で立て直します。その除去のあとに生成された推論は引き続き送り直します。送り直しを止めるのは、同じセッションで2回目に拒否されたときだけです
補助モデルの振り分け
次のような補助的な仕事は、
- 画像の読み取り
- Web から抜き出した内容の要約
- コンテキスト圧縮のための要約
- スキルハブの操作
- MCP の補助的な操作
- メモリーの書き出し
会話に使うメインのモデルとは別に、自前のプロバイダー・モデルを指定できます。
補助的な仕事にプロバイダー main を指定すると、Hermes はふだんのチャットと同じ共通経路でそれを解決します。実際には次のようになります。
- 環境変数で指定した独自エンドポイントがそのまま効きます
hermes modelやconfig.yamlで保存した独自エンドポイントも効きます- 保存された本物の独自エンドポイントなのか、OpenRouter への切り替えなのかを、補助側でも見分けられます
予備のモデル
Hermes では、予備のプロバイダーの連なりを設定できます。(provider, model) を並べたリストで、メインのモデルがエラーになったときに上から順に試されます。古い形式である 1 組だけの fallback_model 辞書も、互換のためにいまも受け付けます (最初の書き込み時に新しい形式へ移ります)。
内部ではどう動くか
- 保存:
AIAgent.__init__がfallback_modelの辞書を保持し、_fallback_activated = Falseにします。
- 切り替わるきっかけ:
_try_activate_fallback()(中身はagent/chat_completion_helpers.pyのtry_activate_fallback()に渡されます) が、ターン処理の 3 か所 (agent/turn_api_error.py、agent/turn_response_check.py、agent/turn_recovery.py) から呼ばれます。
- API の応答が壊れていて (choices が None、本文が無いなど)、やり直しの上限に達したとき
- やり直しても意味のないクライアント側のエラー (HTTP 401、403、404) が出たとき
- 一時的なエラー (HTTP 429、500、502、503) でやり直しの上限に達したとき
- 切り替えの流れ (
_try_activate_fallback):
- すでに切り替え済み、または設定が無ければ、その場で
Falseを返します auxiliary_client.pyのresolve_provider_client()を呼び、認証まで整えた新しいクライアントを作りますapi_modeを決めます。openai-codex ならcodex_responses、anthropic ならanthropic_messages、それ以外はchat_completionsです- その場で差し替えます。対象は
self.model、self.provider、self.base_url、self.api_mode、self.client、self._client_kwargsです - anthropic に切り替える場合は、OpenAI 互換ではなく Anthropic のネイティブクライアントを作ります
- プロンプトキャッシュを使うかどうかを判断し直します (OpenRouter 上の Claude 系モデルでは有効になります)
_fallback_activated = Trueにして、二度目が起きないようにします- やり直し回数を 0 に戻し、処理を続けます
- 設定の読み込み:
- CLI:
hermes_cli/fallback_config.get_fallback_chain()で予備の連なりを読み、AIAgent(fallback_model=...)に渡します - ゲートウェイ:
gateway/run_config_loaders.py._load_fallback_model()がconfig.yamlを読み、AIAgentに渡します - 検証:
providerとmodelの両方が空でないことが条件で、そうでなければ予備の仕組みは無効になります
予備が働かないもの
- サブエージェントへの委任 (
tools/delegate_tool.py): サブエージェントは親のプロバイダーは受け継ぎますが、予備の設定は受け継ぎません - 補助的な仕事: 自前のプロバイダー自動判定の連なりを使います (上の「補助モデルの振り分け」を参照)
プロバイダーやモデルを固定していない cron のジョブでは予備が 働きます。run_job() が config.yaml から fallback_providers (または古い fallback_model) を読み、AIAgent(fallback_model=...) に渡します。ゲートウェイの _load_fallback_model() と同じやり方です。自前の provider / model / base_url を持つジョブには予備の連なりが付きません。固定した委任先の子エージェントと同じ決まりです。cron の内部 も参照してください。
テストの範囲
予備の動きは、いくつかのテスト一式で確かめています。
tests/agent/test_fallback_credential_isolation.py— メインと予備のあいだで資格情報が混ざらないことtests/hermes_cli/test_fallback_cmd.py— CLI の/fallbackコマンド