LLM Providers
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
OMIT_TEMPERATURE:21— Sentinel-Objekt, bedeutet «temperature nicht senden» (Kimi überlässt das dem Server)@dataclass ProviderProfile:38-88— Provider-Metadaten (name, aliases, base_url, auth, temperature-Strategie …):
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 = Nonefixed_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».
get_hostname / prepare_messages:98-118— Hooks: welcher Host bedient den Request, wie werden Nachrichten vorverarbeitetbuild_extra_body:119-175— Hook: provider-spezifische Request-Body-Felder:
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()gibthermes-cli/<version>stattPython-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:
_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.
get_provider_profile:65-76— Lookup nach Name oder Aliaslist_providers:76-91— listet alle Profileplugins/model-providers/-Verzeichnis— tatsächliche Profildefinitionen (ein Unterverzeichnis pro Provider)
Datenfluss
init_agent:276erhält den konfigurierten Provider-Namen und ruftget_provider_profile:65auf (löst beim ersten Mal Lazy-Discovery aus: Scan vonplugins/model-providers/<name>/, Import undregister_provider).- Nach Erhalt des
ProviderProfile:get_hostname(providers/base.py:98) bestimmt den Anfrage-Hostprepare_messages(providers/base.py:111) präpariert dieapi_messagesausbuild_turn_context:268build_extra_body(providers/base.py:119) fügt provider-spezifische Felder hinzutemperaturefolgt der Profil-Strategie (None / OMIT_TEMPERATURE / konkreter Wert)
- Die Hauptschleife ruft mit dem zusammengebauten Request die Provider-API auf (
Wiederholungs-Teilschleife:1227). - Der
extra_bodyeines Custom-Providers wird in_merge_custom_provider_extra_body:257gemergt.
Grenzen und Fehler
- Profil existiert nicht: Steht in der Config ein unbekannter Provider-Name, gibt
get_provider_profileNonezurück; die Hauptschleife fällt auf einen generischenchat_completions-Provider zurück, und der Picker erhält keinefallback_modelsund zeigt eine leere Liste. - Vision nicht unterstützt: Ist
supports_vision=False, wird eintool_result-Bild nicht unverändert in die Tool-Nachricht gesteckt; istsupports_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-UAPython-urllibwird mit 403 abgewiesen).fetch_modelsverwendet den UAhermes-cli/<version>als Workaround; schlägt es fehl, fällt es auffallback_modelszurück — dort sollten nur agentic Modelle stehen, sonst wählt der Picker falsch. - User-Plugin überschreibt bundled:
register_providerist last-writer-wins; ein User-Plugin mit gleichem Namen überschreibt das bundled nachträglich. Auch das legacyproviders/<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.