Skip to content

LLM Providers

源码版本v2026.7.20

Responsabilité

Traduire « nom de provider (nvidia, kimi, openai, openrouter…) » en « hostname concret, prétraitement des messages, extra_body, liste de modèles ». Hermes n'utilise pas une hiérarchie d'héritage pour abstraire les providers, mais une dataclass ProviderProfile + un ensemble de hooks ; le profil est enregistré comme plugin (plugin), ce qui se traduit par « déclarer c'est câbler ». Prend en charge Nous Portal, OpenRouter, OpenAI et endpoints personnalisés.

Mot de conception

Pourquoi une dataclass ProviderProfile + des hooks plutôt que class OpenAIProvider(BaseProvider) ? Hermes intègre plus de 30 providers, et dans la grande majorité des cas la différence se résume à base_url, un header d'auth et quelques champs spéciaux. Une hiérarchie d'héritage forcerait chaque sous-classe à overrider un paquet de méthodes, dont 90 % ne seraient que des implémentations vides. Dataclass + hooks permet « déclarer 7 champs + overrider 1 hook » pour boucler l'intégration ; les logiques plus complexes (comme la position du champ reasoning chez Kimi) se rabattent sur une réécriture du hook.

Le profil vit dans plugins/model-providers/<name>/ plutôt que dans providers/<name>.py : le répertoire de plugin embarque un manifeste plugin.yaml et des sous-modules, il est surchargé à l'identique par le $HERMES_HOME/plugins/ utilisateur (last-writer-wins), ce qui permet de patcher un provider intégré sans forker le dépôt. Les fichiers uniques providers/*.py sont un legacy de rétro-compatibilité.

Fichiers clés

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 est tri-state : None utilise la valeur par défaut du caller, OMIT_TEMPERATURE n'envoie pas le champ, une valeur concrète surcharge. supports_vision_tool_messages vaut True par défaut ; le MiMo de Xiaomi accepte les user messages multimodaux mais rejette le contenu de type list dans les tool messages, il faut le mettre à False, sinon 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 renvoie un dict vide par défaut, les sous-classes overrident au besoin. build_api_kwargs_extras éclate les champs en deux canaux, extra_body et api_kwargs top-level : pour OpenRouter reasoning va dans extra_body, pour Kimi reasoning_effort est top-level.

  • fetch_models:175-232 — hook : récupère la liste des modèles disponibles du provider. _profile_user_agent() renvoie hermes-cli/<version> plutôt que Python-urllib/<ver>, certains providers ont un WAF devant qui 403 l'UA par défaut.
  • doc module:9-28 — décrit le mécanisme de découverte paresseuse (scan des plugins au premier appel)
  • register_provider:53-65 — enregistre un 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)

Découverte paresseuse : c'est le premier get_provider_profile qui déclenche _discover_providers(), qui scanne trois endroits — bundled, plugins utilisateur et fichiers uniques legacy. La table d'alias fait que kimi / moonshot et autres synonymes pointent vers le même profil.

Flux de données

  1. init_agent:276 récupère le nom de provider configuré par l'utilisateur.
  2. Appelle get_provider_profile:65 (premier appel déclenche la découverte paresseuse : scan de plugins/model-providers/<name>/, import et register_provider).
  3. Après obtention du ProviderProfile :
  4. La boucle principale appelle l'API du provider avec la requête assemblée (sous-boucle de retry:1227).
  5. L'extra_body des providers personnalisés est fusionné dans _merge_custom_provider_extra_body:257.

Limites et échecs

  • Profil inexistant : la config déclare un nom de provider inconnu, get_provider_profile renvoie None, la boucle principale retombe sur un provider générique chat_completions, et le picker ne trouve aucun fallback_models — affichage vide.
  • Vision non supportée : quand supports_vision=False, les images des tool_results ne sont pas collées telles quelles dans le tool message ; quand supports_vision_tool_messages=False, envoyer un contenu de type list 400, il faut dégrader l'image ou la convertir en base64.
  • Échec de fetch_models : le endpoint /models de certains providers est derrière un WAF (l'UA Python-urllib par défaut se prend un 403). fetch_models utilise l'UA hermes-cli/<version> pour contourner, et retombe sur fallback_models en cas d'échec — seuls les modèles agentic devraient figurer là, sinon le picker se trompe.
  • Plugin utilisateur surchargeant le bundled : register_provider est last-writer-wins, le plugin utilisateur écrase le bundled homonyme importé après. Les providers/<name>.py legacy, importés en dernier, peuvent aussi surcharger — un footgun, le nouveau code doit passer par le répertoire de plugins.

Résumé

Les mots-clés de l'abstraction provider sont « profil + hooks » plutôt qu'« héritage ». Ajouter un provider consiste à créer un sous-répertoire dans plugins/model-providers/, déclarer un ProviderProfile et appeler register_provider, sans toucher à la base. La découverte paresseuse garantit un démarrage rapide et un chargement à la demande.

Site d'apprentissage communautaire non officiel. Basé sur le code source de NousResearch/hermes-agent (licence MIT).