Skip to content

LLM Providers

源码版本v2026.7.20

Responsabilidad

Traducir «nombre de provider (nvidia, kimi, openai, openrouter…)» a «hostname concreto, preprocesamiento de mensajes, extra_body, lista de modelos». Hermes no usa herencia para abstraer los providers, sino un dataclass ProviderProfile + un conjunto de hooks; el profile se registra como plugin, logrando «declarar = integrar». Soporta Nous Portal, OpenRouter, OpenAI y endpoints personalizados.

Motivo de diseño

¿Por qué un dataclass ProviderProfile + hooks en lugar de class OpenAIProvider(BaseProvider)? Hermes integra 30+ providers; en la inmensa mayoría, la diferencia es solo base_url, el header de auth y un par de campos especiales. Una jerarquía obliga a cada subclase a sobrescribir un montón de métodos y el 90% acaba con overrides vacíos. El dataclass + hooks convierte «declarar 7 campos + sobrescribir 1 hook» en toda la integración; la lógica más retorcida (p. ej. dónde van los campos de reasoning en Kimi) se resuelve reescribiendo el hook.

Los profiles van en plugins/model-providers/<name>/ y no en providers/<name>.py: el directorio de plugin trae un manifiesto plugin.yaml y submódulos, y lo puede sobrescribir el user con la misma estructura en $HERMES_HOME/plugins/ (last-writer-wins) sin necesidad de forkear el repo para parchear un provider interno. Los archivos sueltos de providers/*.py son un vestigio de back-compat.

Archivos clave

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 es triestado: None usa el default del caller; OMIT_TEMPERATURE no envía el campo; un valor concreto lo sobrescribe. supports_vision_tool_messages por defecto es True; el MiMo de Xiaomi acepta user messages multimodales pero rechaza tool content en formato list, así que hay que ponerlo a False, si no 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 por defecto es un dict vacío; las subclases lo sobrescriben bajo demanda. build_api_kwargs_extras reparte el campo en dos rutas — extra_body y api_kwargs top-level —: en OpenRouter, reasoning va en extra_body; en Kimi, reasoning_effort es top-level.

  • fetch_models:175-232 — hook: lista de modelos disponibles para ese provider. _profile_user_agent() devuelve hermes-cli/<version> en lugar de Python-urllib/<ver>; algunos providers llevan WAF por delante que devuelven 403 al UA por defecto.
  • doc del módulo:9-28 — describe el mecanismo de descubrimiento perezoso (escanea plugins solo en la primera llamada)
  • register_provider:53-65 — registra un 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)

Descubrimiento perezoso: la primera llamada a get_provider_profile dispara _discover_providers(), que escanea tres sitios — bundled, user plugins y archivos sueltos legacy. La tabla de alias hace que nombres sinónimos como kimi / moonshot apunten al mismo profile.

Flujo de datos

  1. init_agent:276 recibe el nombre del provider configurado por el usuario.
  2. Llama a get_provider_profile:65 (lo que dispara el descubrimiento perezoso la primera vez: escanea plugins/model-providers/<name>/, lo importa y llama a register_provider).
  3. Una vez obtenido el ProviderProfile:
  4. El bucle principal usa la petición ensamblada para llamar a la API del provider (subbucle de reintentos:1227).
  5. El extra_body de un provider personalizado se fusiona en _merge_custom_provider_extra_body:257.

Límites y fallos

  • El profile no existe: si la config lleva un nombre de provider desconocido, get_provider_profile devuelve None; el bucle principal cae a un provider genérico de chat_completions y el picker, al no tener fallback_models, muestra una lista vacía.
  • Sin soporte de visión: con supports_vision=False, las imágenes en los tool_result no se meten tal cual en el tool message; con supports_vision_tool_messages=False, usar content tipo list devuelve 400 — hay que degradar la imagen o convertirla a base64.
  • fetch_models falla: el endpoint /models de algunos providers lleva WAF (el UA Python-urllib recibe 403). fetch_models usa el UA hermes-cli/<version> para esquivarlo; si aun así falla, cae a fallback_models — aquí solo deberían figurar modelos agentic, los que no soportan tool-calling provocan que el picker elija mal.
  • Plugin de user pisa bundled: register_provider es last-writer-wins; un plugin de user con el mismo nombre que uno bundled lo sobrescribe. Los providers/<name>.py legacy, al importarse los últimos, también pueden pisar — footgun; el código nuevo va por el directorio de plugins.

Resumen

La abstracción de provider se resume en «profile + hooks», no «herencia». Añadir un provider solo requiere crear un subdirectorio en plugins/model-providers/, declarar un ProviderProfile y llamar a register_provider, sin tocar la base. El descubrimiento perezoso mantiene el arranque rápido y la carga bajo demanda.

Sitio de aprendizaje comunitario no oficial. Basado en el código fuente de NousResearch/hermes-agent (licencia MIT).