Skip to content

LLM Providers

源码版本v2026.7.20

職務

「provider 名(nvidia、kimi、openai、openrouter…)」を「具体的な hostname、メッセージ前処理、extra_body、モデルリスト」に翻訳する。Hermes は継承体系で provider 抽象をするのではなく、ProviderProfile データクラス + 一群のフックを用い、profile をプラグイン (plugin) として登録する。「宣言即ち接入」を実現する。Nous Portal、OpenRouter、OpenAI とカスタム endpoint をサポート。

設計動機

なぜ class OpenAIProvider(BaseProvider) ではなく ProviderProfile データクラス + フックにするのか? hermes は 30+ の provider を接入しており、大多数の違いは base_url、auth header、いくつかの特殊フィールドだけだ。継承体系は各サブクラスに大量のメソッド override を強要し、90% のサブクラスは空実装になる。dataclass + フックなら「7 個のフィールドを宣言 + 1 個のフックを override」するだけで接入が済み、複雑なロジック(Kimi の reasoning フィールドの位置など)は後からフックを書き直せばいい。

profile を providers/<name>.py ではなく plugins/model-providers/<name>/ に置く理由:プラグインディレクトリは plugin.yaml マニフェストとサブモジュールを持ち、ユーザーの $HERMES_HOME/plugins/ の同構造が同じ名前で上書き(last-writer-wins)する。リポジトリを fork せずに組み込み provider にパッチを当てられる。providers/*.py の単一ファイルは後方互換の遺物だ。

主要ファイル

  • OMIT_TEMPERATURE:21 — センチネルオブジェクト、「temperature を送らない」を表す(Kimi は server に任せる)。
  • @dataclass ProviderProfile:38-88 — provider メタデータ(name、aliases、base_url、auth、temperature 戦略…)。
python
OMIT_TEMPERATURE = object()

@dataclass
class ProviderProfile:
    name: str
    api_mode: str = "chat_completions"
    aliases: tuple = ()
    env_vars: tuple = ()
    base_url: str = ""
    auth_type: str = "api_key"
    supports_vision: bool = False
    supports_vision_tool_messages: bool = True
    fallback_models: tuple = ()
    fixed_temperature: Any = None

fixed_temperature は三値:None は caller デフォルトを使う、OMIT_TEMPERATURE はこのフィールドを送らない、具体値は上書きする。supports_vision_tool_messages はデフォルト True だが、Xiaomi MiMo はマルチモーダル user message を受け付けるのに list-type の tool content を拒否する。False にしないと 400 "text is not set" が出る。

python
def build_extra_body(self, *, session_id: str | None = None, **context: Any) -> dict[str, Any]:
    """Merged into the API kwargs extra_body. Default: empty dict."""
    return {}

def build_api_kwargs_extras(self, *, reasoning_config=None, **context) -> tuple[dict, dict]:
    """Returns (extra_body_additions, top_level_kwargs).
    OpenRouter: reasoning in extra_body. Kimi: reasoning_effort is top-level."""
    return {}, {}

build_extra_body はデフォルトで空 dict、サブクラスが必要に応じて override する。build_api_kwargs_extras はフィールドを extra_body と top-level api_kwargs の二系統に分ける:OpenRouter の reasoning は extra_body、Kimi の reasoning_effort は top-level だ。

  • fetch_models:175-232 — フック:当該 provider の利用可能モデル一覧を取得する。_profile_user_agent()Python-urllib/<ver> ではなく hermes-cli/<version> を返す。一部の provider 前段に WAF があり、デフォルト UA だと 403 を食らう。
  • モジュールドキュメント:9-28 — 遅延発見機構(初回呼び出しでプラグイン走査)の説明。
  • register_provider:53-65 — profile を一つ登録する。
python
_REGISTRY: dict[str, ProviderProfile] = {}
_ALIASES: dict[str, str] = {}

def register_provider(profile: ProviderProfile) -> None:
    """Later registrations with the same name replace earlier ones —
    user plugins can override bundled profiles without editing repo code."""
    _REGISTRY[profile.name] = profile
    for alias in profile.aliases:
        _ALIASES[alias] = profile.name

def get_provider_profile(name: str) -> ProviderProfile | None:
    if not _discovered:
        _discover_providers()
    canonical = _ALIASES.get(name, name)
    return _REGISTRY.get(canonical)

遅延発見:初回の get_provider_profile だけが _discover_providers() をトリガーし、bundled + user plugins + legacy 単一ファイルの三箇所を走査する。alias 表により kimi / moonshot などの同義名が同じ profile に向く。

データフロー

  1. init_agent:276 が provider 名を受け取り、get_provider_profile:65 を呼ぶ(初回は遅延発見をトリガー:plugins/model-providers/<name>/ を走査し、インポートして register_provider)。
  2. ProviderProfile を得た後:get_hostname(providers/base.py:98) でホストを決め、prepare_messages(providers/base.py:111) で api_messages を前処理し、build_extra_body(providers/base.py:119) で専用フィールドを追加し、temperature は戦略に従う(None / OMIT_TEMPERATURE / 具体値)。
  3. 主ループは組み立てたリクエストで provider API を呼ぶ(リトライ副ループ:1227)。
  4. カスタム provider の extra_body マージは _merge_custom_provider_extra_body:257 で完了する。

境界と失敗

  • profile が存在しない:設定に未知の provider 名を書くと、get_provider_profileNone を返す。主ループは generic chat_completions provider にフォールバック (fallback) し、picker は fallback_models を取れず空表示になる。
  • vision 非サポート:supports_vision=False のとき tool_result の画像は tool message にそのまま入れない。supports_vision_tool_messages=False のとき list-type content だと 400 になり、画像を降格するか base64 に変える必要がある。
  • fetch_models の失敗:一部の provider の /models エンドポイントには WAF があり(デフォルト Python-urllib UA が 403 になる)。fetch_modelshermes-cli/<version> の UA で回避し、失敗時は fallback_models にフォールバックする——ここに置くべきは agentic モデルだけで、non-tool-calling モデルを置くと picker が誤選する。
  • user plugin による bundled 上書き:register_provider は last-writer-wins で、user plugin は bundled の後に同名で上書きする。legacy の providers/<name>.py も最後に import されれば上書きできる——footgun なので、新規コードはプラグインディレクトリを使う。

まとめ

Provider 抽象のキーワードは「profile + フック」であって「継承」ではない。新規 provider は plugins/model-providers/ にサブディレクトリを作り、ProviderProfile を宣言して register_provider するだけでよく、base を修正する必要はない。遅延発見が起動を速く保ち、必要に応じてロードする。

非公式コミュニティ学習サイト。MIT ライセンスの NousResearch/hermes-agent ソースに基づく。