Files
hermes-agent/providers
kshitijk4poor 040a7d6e7c feat: provider modules — ProviderProfile ABC, 29 providers, fetch_models, transport single-path
Introduces providers/ as the single source of truth for every inference
provider. All 29 providers declared with correct data cross-checked against
auth.py, runtime_provider.py and auxiliary_client.py.

Providers covered:
  chat_completions: openrouter, nous, kimi-coding, kimi-coding-cn, qwen-oauth,
    nvidia, deepseek, zai, stepfun, arcee, huggingface, xiaomi, ollama-cloud,
    kilocode, alibaba, opencode-zen, opencode-go, custom, vercel (ai-gateway),
    copilot, gemini, google-gemini-cli
  codex_responses: xai, openai-codex
  anthropic_messages: anthropic, minimax, minimax-cn
  bedrock_converse: bedrock
  chat_completions (ACP subprocess): copilot-acp

Key additions vs prior commit:
- Cross-checked ALL env_vars against auth.py (fixed copilot, zai, kimi-coding,
  arcee, alibaba, ollama-cloud)
- Cross-checked ALL aliases against auth.py _PROVIDER_ALIASES (added 21 missing:
  kimi-cn, moonshot-cn, kimi-for-coding, claude-code, github, github-model,
  qwen-cli, huggingface-hub, x.ai, lmstudio/vllm/llamacpp variants, go,
  opencode-go-sub, kilo-gateway)
- Fixed auth_type mismatches (bedrock: aws_sdk, copilot: copilot)
- Fixed copilot-acp api_mode to match runtime_provider.py (chat_completions)
- Added 4 missing default_aux_model values (stepfun, minimax, minimax-cn, ollama-cloud)
- fetch_models() on every profile (default hits base_url/models with Bearer auth)
- models_url field for non-standard catalog URLs (OpenRouter public endpoint)
- Transport registry _discovered guard (fixes xdist partial-registry poisoning)
- Copilot ACP client relocated agent/ -> acp_adapter/
- run_agent.py: _PROFILE_ACTIVE_PROVIDERS module-level, dead is_nvidia_nim removed
- providers/README.md contributor guide

Closes part of #14418. Remaining activation in #14515.
2026-04-28 01:50:23 +05:30
..

providers/

Single source of truth for every inference provider Hermes knows about.

Each provider is declared once here as a ProviderProfile. Every other layer — auth resolution, transport kwargs, model listing, runtime routing — reads from these profiles instead of maintaining its own parallel data.


Directory layout

providers/
├── base.py           ProviderProfile dataclass + OMIT_TEMPERATURE sentinel
├── __init__.py       Registry: register_provider(), get_provider_profile()
├── README.md         This file
│
├── # Simple providers — just identity + auth + endpoint
├── alibaba.py        Alibaba Cloud DashScope
├── arcee.py          Arcee AI
├── bedrock.py        AWS Bedrock  (api_mode=bedrock_converse)
├── deepseek.py       DeepSeek
├── huggingface.py    Hugging Face Inference API
├── kilocode.py       Kilo Code
├── minimax.py        MiniMax (international + CN)
├── nvidia.py         NVIDIA NIM  (default_max_tokens=16384)
├── ollama_cloud.py   Ollama Cloud
├── stepfun.py        StepFun
├── xiaomi.py         Xiaomi MiMo
├── xai.py            xAI Grok  (api_mode=codex_responses)
├── zai.py            Z.AI / GLM
│
├── # Medium — one or two quirks
├── anthropic.py      Native Anthropic  (x-api-key header, api_mode=anthropic_messages)
├── copilot.py        GitHub Copilot  (auth_type=copilot, reasoning per model)
├── copilot_acp.py    Copilot ACP subprocess  (api_mode=copilot_acp)
├── custom.py         Custom/Ollama local  (think=false, num_ctx)
├── gemini.py         Google Gemini AI Studio + Cloud Code OAuth
├── kimi.py           Kimi Coding  (OMIT_TEMPERATURE, thinking, dual endpoint)
├── openai_codex.py   OpenAI Codex OAuth  (api_mode=codex_responses)
├── opencode.py       OpenCode Zen + Go  (per-model api_mode routing)
│
├── # Complex — subclasses with multiple overrides
├── nous.py           Nous Portal  (tags, attribution, reasoning omit-when-disabled)
├── openrouter.py     OpenRouter  (provider preferences, public model fetch)
├── qwen.py           Qwen OAuth  (message normalization, cache_control, vl_hires)
└── vercel.py         Vercel AI Gateway  (attribution headers, reasoning passthrough)

ProviderProfile fields

@dataclass
class ProviderProfile:
    # Identity
    name: str                    # canonical ID matching hermes_cli/auth.py PROVIDER_REGISTRY
    api_mode: str                # "chat_completions" | "anthropic_messages" |
                                 # "codex_responses" | "bedrock_converse" | "copilot_acp"
    aliases: tuple               # alternate names resolved by get_provider_profile()

    # Auth & endpoints
    env_vars: tuple              # env var names holding the API key, in priority order
    base_url: str                # default inference endpoint
    models_url: str              # explicit models endpoint; falls back to {base_url}/models
                                 # set when the models catalog lives at a different URL
                                 # (e.g. OpenRouter: public /api/v1/models vs /api/v1 inference)
    auth_type: str               # "api_key" | "oauth_device_code" | "oauth_external" |
                                 # "copilot" | "aws" | "external_process"

    # Client-level quirks
    default_headers: dict        # extra HTTP headers sent on every request

    # Request-level quirks
    fixed_temperature: Any       # None = use caller's default; OMIT_TEMPERATURE = don't send
    default_max_tokens: int|None # inject max_tokens when caller omits it
    default_aux_model: str       # cheap model for auxiliary tasks (compression, vision, etc.)
                                 # empty string = use main model (default)

Hooks (override in a subclass)

Method When to override
prepare_messages(messages) Provider needs message pre-processing (Qwen: string → list-of-parts, cache_control)
build_extra_body(*, session_id, **ctx) Provider-specific extra_body fields (Nous: tags, OpenRouter: provider preferences)
build_api_kwargs_extras(*, reasoning_config, **ctx) Returns (extra_body_additions, top_level_kwargs) — use when some fields go to extra_body and some go top-level (Kimi: reasoning_effort top-level; OpenRouter: reasoning in extra_body)
fetch_models(*, api_key, timeout) Custom model listing (Anthropic: x-api-key header; OpenRouter: public endpoint, no auth; Bedrock/copilot-acp: return None)

All hooks have safe defaults — only override what differs from the base.


How to add a new provider

1. Simple (standard OpenAI-compatible endpoint)

# providers/myprovider.py
from providers import register_provider
from providers.base import ProviderProfile

myprovider = ProviderProfile(
    name="myprovider",           # must match id in hermes_cli/auth.py PROVIDER_REGISTRY
    aliases=("my-provider", "myp"),
    api_mode="chat_completions",
    env_vars=("MYPROVIDER_API_KEY",),
    base_url="https://api.myprovider.com/v1",
    auth_type="api_key",
)

register_provider(myprovider)

The default fetch_models() will call GET https://api.myprovider.com/v1/models with Bearer auth automatically. No override needed for standard /v1/models.

2. With quirks (subclass)

# providers/myprovider.py
from typing import Any
from providers import register_provider
from providers.base import ProviderProfile


class MyProviderProfile(ProviderProfile):
    """My provider — custom reasoning header."""

    def build_api_kwargs_extras(
        self,
        *,
        reasoning_config: dict | None = None,
        **ctx: Any,
    ) -> tuple[dict[str, Any], dict[str, Any]]:
        extra_body: dict[str, Any] = {}
        if reasoning_config:
            extra_body["my_reasoning"] = reasoning_config.get("effort", "medium")
        return extra_body, {}

    def fetch_models(
        self,
        *,
        api_key: str | None = None,
        timeout: float = 8.0,
    ) -> list[str] | None:
        # Override only if your endpoint differs from standard /v1/models
        return super().fetch_models(api_key=api_key, timeout=timeout)


myprovider = MyProviderProfile(
    name="myprovider",
    aliases=("myp",),
    env_vars=("MYPROVIDER_API_KEY",),
    base_url="https://api.myprovider.com/v1",
)

register_provider(myprovider)

3. Wire it up

After creating the file, add name to the _PROFILE_ACTIVE_PROVIDERS set in run_agent.py once you've verified parity against the legacy flag path. Start with a simple provider (no message prep, no reasoning quirks) and work up.


fetch_models contract

def fetch_models(
    self,
    *,
    api_key: str | None = None,
    timeout: float = 8.0,
) -> list[str] | None:
    ...
  • Returns list[str]: model IDs from the provider's live endpoint.
  • Returns None: provider doesn't support REST model listing (Bedrock, copilot-acp), or the request failed. Callers must fall back to _PROVIDER_MODELS on None.
  • Never raises — swallow exceptions and return None.
  • Default implementation: GET {base_url}/models with Bearer auth. Works for any standard OpenAI-compatible provider.

Override when:

  • Auth header is not Bearer (Anthropic: x-api-key)
  • Endpoint path differs from /models AND you can't just set models_url (OpenRouter: public endpoint, pass api_key=None explicitly)
  • Response format differs (extra wrapping, non-standard id field)
  • Provider has no REST endpoint (Bedrock, copilot-acp → return None)
  • Filtering needed post-fetch (only tool-capable models, etc.)

Use models_url instead of overriding when the only difference is the URL:

# No subclass needed — just set models_url
myprovider = ProviderProfile(
    name="myprovider",
    base_url="https://api.myprovider.com/v1",
    models_url="https://catalog.myprovider.com/models",  # different host
)

Debugging

Check if a provider resolves

from providers import get_provider_profile

p = get_provider_profile("myprovider")
print(p)           # ProviderProfile(name='myprovider', ...)
print(p.base_url)
print(p.api_mode)

Check all registered providers

from providers import _REGISTRY
print(list(_REGISTRY.keys()))

Test live model fetch

import os
from providers import get_provider_profile

p = get_provider_profile("myprovider")
key = os.getenv("MYPROVIDER_API_KEY")
models = p.fetch_models(api_key=key, timeout=5.0)
print(models)      # list of model IDs, or None on failure

Test alias resolution

from providers import get_provider_profile

# All of these should return the same profile
assert get_provider_profile("openrouter").name == "openrouter"
assert get_provider_profile("or").name == "openrouter"

Run the provider test suite

# From the repo root
source venv/bin/activate
python -m pytest tests/providers/ -v

Check ruff + ty compliance

source venv/bin/activate
ruff format providers/*.py
ruff check providers/*.py --select UP,E,F,I,W
ty check providers/*.py

Common mistakes

Wrong name — must exactly match the id field in hermes_cli/auth.py PROVIDER_REGISTRY. If they diverge, get_provider_profile() will return a profile that doesn't match what runtime_provider.py resolves.

Wrong env_vars — check hermes_cli/auth.py api_key_env_vars for the exact env var names. Mismatch means WS2 auth migration will miss the key.

Wrong base_url — check hermes_cli/auth.py inference_base_url constant. Several providers have version suffixes or non-obvious paths (stepfun: /step_plan/v1, opencode-go: /zen/go/v1).

Skipping api_mode — defaults to chat_completions. Providers that use anthropic_messages, codex_responses, bedrock_converse, or copilot_acp must set it explicitly.

Forgetting register_provider() — auto-discovery runs pkgutil.iter_modules over the package and imports each module, but only if register_provider() is called at module level. Without it the profile is never in _REGISTRY.

fetch_models returning the wrong shape — must return list[str] (plain model IDs), not list[tuple] or list[dict]. Callers expect plain strings.

build_api_kwargs_extras wrong tuple — must return (extra_body_dict, top_level_dict). Returning a flat dict or swapping the order silently sends fields to the wrong place.