Skip to content

LLM Providers

源码版本v2026.7.20

Verantwortung

Übersetzt einen «Provider-Namen (provider name) (nvidia, kimi, openai, openrouter …)» in «konkreten Hostnamen, Nachrichten-Vorverarbeitung, extra_body, Modellliste». Hermes abstrahiert Provider nicht über Vererbung, sondern über die ProviderProfile-Dataclass + einen Satz Hooks; das Profil wird als Plugin (plugin) registriert – «deklarieren heißt anbinden». Unterstützt werden Nous Portal, OpenRouter, OpenAI und eigene Endpunkte.

Designmotiv

Warum eine ProviderProfile-Dataclass + Hooks statt class OpenAIProvider(BaseProvider)? Hermes dockt 30+ Provider an; bei den meisten unterscheidet sich nur base_url, der Auth-Header und wenige Sonderfelder. Ein Vererbungssystem würde jede Unterklasse zwingen, einen Haufen Methoden zu überschreiben, und 90 % der Unterklassen bestünden nur aus leeren Implementierungen. Dataclass + Hooks erlauben «7 Felder deklarieren + 1 Hook überschreiben» als fertige Anbindung; komplexe Logik (etwa die Position des reasoning-Felds bei Kimi) wird über einen neu geschriebenen Hook gelöst.

Profile liegen in plugins/model-providers/<name>/ statt in providers/<name>.py: Ein Plugin-Verzeichnis bringt eine plugin.yaml-Manifest und Submodule mit und wird vom User unter $HERMES_HOME/plugins/ mit gleicher Struktur überdeckt (last-writer-wins), ohne den Repo forken zu müssen, um einen eingebauten Provider zu patchen. Die Einzeldatei providers/*.py ist ein Back-compat-Historie.

Schlüsseldateien

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 ist dreiwertig: None — Caller-Standard verwenden; OMIT_TEMPERATURE — Feld nicht senden; konkreter Wert — überschreibt. supports_vision_tool_messages ist defaultmäßig True; Xiaomis MiMo akzeptiert multimodale User-Nachrichten, weist aber list-type Tool-Content zurück und muss auf False gesetzt werden, sonst kommt eine 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 ist defaultmäßig ein leeres Dict, Unterklassen überschreiben bei Bedarf. build_api_kwargs_extras splittet Felder in extra_body und top-level api_kwargs: OpenRouter-reasoning geht ins extra_body, bei Kimi ist reasoning_effort top-level.

  • fetch_models:175-232 — Hook: holt die für diesen Provider verfügbare Modellliste. _profile_user_agent() gibt hermes-cli/<version> statt Python-urllib/<ver> zurück; manche Provider haben vorab einen WAF, der den Default-UA mit 403 abweist.
  • Modul-Dokumentation:9-28 — beschreibt den Lazy-Discovery-Mechanismus (Scan erst beim ersten Aufruf)
  • register_provider:53-65 — registriert ein Profil:
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)

Lazy-Discovery: Erst der erste Aufruf von get_provider_profile stößt _discover_providers() an und scannt drei Quellen — bundled, user plugins, legacy Einzeldatei. Die Alias-Tabelle zeigt synonyme Namen wie kimi / moonshot auf dasselbe Profil.

Datenfluss

  1. init_agent:276 erhält den konfigurierten Provider-Namen und ruft get_provider_profile:65 auf (löst beim ersten Mal Lazy-Discovery aus: Scan von plugins/model-providers/<name>/, Import und register_provider).
  2. Nach Erhalt des ProviderProfile:
  3. Die Hauptschleife ruft mit dem zusammengebauten Request die Provider-API auf (Wiederholungs-Teilschleife:1227).
  4. Der extra_body eines Custom-Providers wird in _merge_custom_provider_extra_body:257 gemergt.

Grenzen und Fehler

  • Profil existiert nicht: Steht in der Config ein unbekannter Provider-Name, gibt get_provider_profile None zurück; die Hauptschleife fällt auf einen generischen chat_completions-Provider zurück, und der Picker erhält keine fallback_models und zeigt eine leere Liste.
  • Vision nicht unterstützt: Ist supports_vision=False, wird ein tool_result-Bild nicht unverändert in die Tool-Nachricht gesteckt; ist supports_vision_tool_messages=False, liefert list-type Content eine 400 — das Bild muss degradiert oder nach base64 gewandelt werden.
  • fetch_models schlägt fehl: Manche Provider haben vor dem /models-Endpunkt einen WAF (Default-UA Python-urllib wird mit 403 abgewiesen). fetch_models verwendet den UA hermes-cli/<version> als Workaround; schlägt es fehl, fällt es auf fallback_models zurück — dort sollten nur agentic Modelle stehen, sonst wählt der Picker falsch.
  • User-Plugin überschreibt bundled: register_provider ist last-writer-wins; ein User-Plugin mit gleichem Namen überschreibt das bundled nachträglich. Auch das legacy providers/<name>.py, das zuletzt importiert wird, kann überschreiben — eine Footgun, neuer Code geht über das Plugin-Verzeichnis.

Zusammenfassung

Leitgedanke der Provider-Abstraktion ist «Profil + Hooks» statt «Vererbung». Ein neuer Provider bedeutet nur ein Unterverzeichnis in plugins/model-providers/, in dem ein ProviderProfile deklariert und per register_provider angemeldet wird – ohne Änderung an base. Lazy-Discovery hält den Start schnell und lädt nach Bedarf.

Inoffizielle Community-Lernseite. Basiert auf dem MIT-lizenzierten NousResearch/hermes-agent-Quellcode.