Создание плагина провайдера моделей

Плагины провайдеров моделей объявляют бэкенд для инференса — OpenAI-совместимую точку входа, сервер Anthropic Messages, API Responses в стиле Codex или нативный интерфейс Bedrock — через который Hermes может направлять вызовы AIAgent. Все встроенные провайдеры (OpenRouter, Anthropic, GMI, DeepSeek, Nvidia, …) поставляются как такие плагины. Сторонние разработчики могут добавить свои, разместив директорию в $HERMES_HOME/plugins/model-providers/ без каких-либо изменений в репозитории.

Плагины провайдеров моделей — это третий вид **провайдерских плагинов**. Остальные — [Memory Provider Plugins](/docs/developer-guide/memory-provider-plugin) (знания между сессиями) и [Context Engine Plugins](/docs/developer-guide/context-engine-plugin) (стратегии сжатия контекста). Все три следуют одному принципу: «создай директорию, объяви профиль, без правок в репозитории».

Как работает обнаружение

providers/__init__.py._discover_providers() выполняется лениво при первом вызове get_provider_profile() или list_providers(). Порядок обнаружения:

  1. Встроенные плагины<repo>/plugins/model-providers/<name>/ — поставляются с Hermes

  2. Пользовательские плагины$HERMES_HOME/plugins/model-providers/<name>/ — можно разместить в любой директории; перезагрузка для последующих сессий не требуется

  3. Устаревшие однофайловые<repo>/providers/<name>.py — обратная совместимость для внешних редактируемых установок

Пользовательские плагины переопределяют встроенные с тем же именем, поскольку register_provider() работает по принципу «последний записавший побеждает». Разместите директорию $HERMES_HOME/plugins/model-providers/gmi/, чтобы заменить встроенный профиль GMI без изменения репозитория.

Структура директории

plugins/model-providers/my-provider/
├── __init__.py       # Calls register_provider(profile) at module-level
├── plugin.yaml       # kind: model-provider + metadata (optional but recommended)
└── README.md         # Setup instructions (optional)

Единственный обязательный файл — __init__.py. plugin.yaml используется hermes plugins для интроспекции и общим PluginManager для маршрутизации плагина к нужному загрузчику; без него общий загрузчик использует эвристику на основе исходного текста.

Минимальный пример — простой провайдер с API-ключом

# plugins/model-providers/acme-inference/__init__.py
from providers import register_provider
from providers.base import ProviderProfile

acme = ProviderProfile(
    name="acme-inference",
    aliases=("acme",),
    display_name="Acme Inference",
    description="Acme — OpenAI-compatible direct API",
    signup_url="https://acme.example.com/keys",
    env_vars=("ACME_API_KEY", "ACME_BASE_URL"),
    base_url="https://api.acme.example.com/v1",
    auth_type="api_key",
    default_aux_model="acme-small-fast",
    fallback_models=(
        "acme-large-v3",
        "acme-medium-v3",
        "acme-small-fast",
    ),
)

register_provider(acme)
# plugins/model-providers/acme-inference/plugin.yaml
name: acme-inference
kind: model-provider
version: 1.0.0
description: Acme Inference — OpenAI-compatible direct API
author: Your Name

Вот и всё. После размещения этих двух файлов следующее автоматически связывается без каких-либо других правок:

Интеграция Где Что получает
Разрешение учётных данных hermes_cli/auth.py PROVIDER_REGISTRY["acme-inference"] заполняется из профиля
--provider флаг CLI hermes_cli/main.py Принимает acme-inference
Выбор hermes model hermes_cli/models.py Появляется в CANONICAL_PROVIDERS, список моделей загружается из {base_url}/models
hermes doctor hermes_cli/doctor.py Проверка работоспособности для ACME_API_KEY + запрос {base_url}/models
hermes setup hermes_cli/config.py ACME_API_KEY появляется в OPTIONAL_ENV_VARS и мастере настройки
Обратное сопоставление URL agent/model_metadata.py Имя хоста → имя провайдера для автоопределения
Вспомогательная модель agent/auxiliary_client.py Использует default_aux_model для сжатия / суммаризации
Разрешение во время выполнения hermes_cli/runtime_provider.py Возвращает корректные base_url, api_key, api_mode
Транспорт agent/transports/chat_completions.py Путь профиля генерирует kwargs через prepare_messages / build_extra_body / build_api_kwargs_extras

Поля ProviderProfile

Полное определение в providers/base.py. Наиболее полезные:

Поле Тип Назначение
name str Канонический ID — соответствует значениям --provider и HERMES_INFERENCE_PROVIDER
aliases tuple[str, ...] Альтернативные имена, разрешаемые через get_provider_profile() (например, grokxai)
api_mode str chat_completions | codex_responses | anthropic_messages | bedrock_converse
display_name str Человекочитаемое название, отображаемое в выборе hermes model
description str Подзаголовок в выборе
signup_url str Показывается при первой настройке («получить API-ключ здесь»)
env_vars tuple[str, ...] Переменные окружения для API-ключа в порядке приоритета; последняя запись *_BASE_URL используется как пользовательское переопределение base-URL
base_url str Конечная точка инференса по умолчанию
models_url str Явный URL каталога моделей (по умолчанию {base_url}/models)
auth_type str api_key | oauth_device_code | oauth_external | copilot | aws_sdk | external_process
fallback_models tuple[str, ...] Кураторский список, отображаемый когда не удаётся загрузить каталог моделей
default_headers dict[str, str] Отправляются с каждым запросом (например, Copilot's Editor-Version)
fixed_temperature Any None = использовать значение вызывающего; sentinel OMIT_TEMPERATURE = не отправлять temperature (Kimi)
default_max_tokens int | None Ограничение max_tokens на уровне провайдера (Nvidia: 16384)
default_aux_model str Дешёвая модель для вспомогательных задач (сжатие, vision, суммаризация)

Переопределяемые хуки

Унаследуйтесь от ProviderProfile для нестандартных особенностей:

from typing import Any
from providers.base import ProviderProfile

class AcmeProfile(ProviderProfile):
    def prepare_messages(self, messages: list[dict[str, Any]]) -> list[dict[str, Any]]:
        """Provider-specific message preprocessing. Runs after codex
        sanitization, before developer-role swap. Default: pass-through."""
        # Example: Qwen normalizes plain-text content to a list-of-parts
        # array and injects cache_control; Kimi rewrites tool-call JSON
        return messages

    def build_extra_body(self, *, session_id=None, **context) -> dict:
        """Provider-specific extra_body fields merged into the API call.
        Context includes: session_id, provider_preferences, model, base_url,
        reasoning_config. Default: empty dict."""
        # Example: OpenRouter's provider-preferences block,
        # Gemini's thinking_config translation.
        return {}

    def build_api_kwargs_extras(self, *, reasoning_config=None, **context):
        """Returns (extra_body_additions, top_level_kwargs). Needed when some
        fields go top-level (Kimi's reasoning_effort) and some go in extra_body
        (OpenRouter's reasoning dict). Default: ({}, {})."""
        return {}, {}

    def fetch_models(self, *, api_key=None, timeout=8.0) -> list[str] | None:
        """Live catalog fetch. Default hits {models_url or base_url}/models with
        Bearer auth. Override for: custom auth (Anthropic), no REST endpoint
        (Bedrock → None), or public/unauthenticated catalogs (OpenRouter)."""
        return super().fetch_models(api_key=api_key, timeout=timeout)

Примеры хуков для справки

Посмотрите на эти встроенные плагины для изучения идиом:

Плагин Почему стоит посмотреть
plugins/model-providers/openrouter/ Агрегатор с предпочтениями провайдеров, публичный каталог моделей
plugins/model-providers/gemini/ Трансляция thinking_config (нативные и OpenAI-совместимые вложенные формы)
plugins/model-providers/kimi-coding/ OMIT_TEMPERATURE, extra_body.thinking, верхнеуровневый reasoning_effort
plugins/model-providers/qwen-oauth/ Нормализация сообщений, внедрение cache_control, VL high-res
plugins/model-providers/nous/ Теги атрибуции, «опустить reasoning, когда отключён»
plugins/model-providers/custom/ Ollama num_ctx + особенности think: false
plugins/model-providers/bedrock/ api_mode="bedrock_converse", fetch_models возвращает None (нет REST endpoint)

Пользовательские переопределения — замена встроенного без правки репозитория

Допустим, вы хотите направить gmi на ваш частный staging endpoint для тестирования. Создайте ~/.hermes/plugins/model-providers/gmi/__init__.py:

from providers import register_provider
from providers.base import ProviderProfile

register_provider(ProviderProfile(
    name="gmi",
    aliases=("gmi-cloud", "gmicloud"),
    env_vars=("GMI_API_KEY",),
    base_url="https://gmi-staging.internal.example.com/v1",
    auth_type="api_key",
    default_aux_model="google/gemini-3.1-flash-lite-preview",
))

В следующей сессии get_provider_profile("gmi").base_url вернёт staging URL. Никаких патчей репозитория, никакой пересборки. Поскольку пользовательские плагины обнаруживаются после встроенных, вызов пользовательского register_provider() побеждает.

Выбор api_mode

Распознаются четыре значения. Hermes выбирает одно на основе:

  1. Явное переопределение пользователя (model.api_mode в config.yaml)

  2. Диспетчеризация по модели OpenCode (opencode_model_api_mode для Zen и Go)

  3. Автоопределение по URL — суффикс /anthropicanthropic_messages, api.openai.comcodex_responses, api.x.aicodex_responses, /coding на доменах Kimi → chat_completions

  4. Profile api_mode как запасной вариант, когда определение по URL ничего не находит

  5. По умолчанию chat_completions

Установите profile.api_mode в соответствии со значением по умолчанию вашего провайдера — это служит подсказкой. Пользовательские переопределения URL всё равно имеют приоритет.

Типы аутентификации

auth_type Значение Кто использует
api_key Одна переменная окружения содержит статический API-ключ Большинство провайдеров
oauth_device_code OAuth-поток с кодом устройства
oauth_external Пользователь входит в систему в другом месте, токены сохраняются в auth.json Anthropic OAuth, MiniMax OAuth, Gemini Cloud Code, Qwen Portal, Nous Portal
copilot Цикл обновления токенов GitHub Copilot Только плагин copilot
aws_sdk Цепочка учётных данных AWS SDK (IAM-роль, профиль, окружение) Только плагин bedrock
external_process Аутентификация обрабатывается подпроцессом, запускаемым агентом Только плагин copilot-acp

auth_type определяет, какие пути кода обрабатывают ваш провайдер как «простой провайдер с API-ключом» — если это не api_key, PluginManager всё равно записывает манифест, но автоматизация на уровне CLI Hermes (проверки doctor, флаг --provider, делегирование мастеру настройки) может его пропустить.

Время обнаружения

Обнаружение провайдеров — ленивое — запускается при первом вызове get_provider_profile() или list_providers() в процессе. На практике это происходит рано при запуске (загрузка модуля auth.py расширяет PROVIDER_REGISTRY с нетерпением). Если вам нужно проверить, что ваш плагин загрузился, выполните:

hermes doctor

— успешный профиль с auth_type="api_key" отображается в секции Provider Connectivity с запросом /models.

Для программной проверки:

from providers import list_providers
for p in list_providers():
    print(p.name, p.base_url, p.api_mode)

Тестирование вашего плагина

Укажите HERMES_HOME на временную директорию, чтобы не загрязнять вашу реальную конфигурацию:

export HERMES_HOME=/tmp/hermes-plugin-test
mkdir -p $HERMES_HOME/plugins/model-providers/my-provider
cat > $HERMES_HOME/plugins/model-providers/my-provider/__init__.py <<'EOF'
from providers import register_provider
from providers.base import ProviderProfile
register_provider(ProviderProfile(
    name="my-provider",
    env_vars=("MY_API_KEY",),
    base_url="https://api.my-provider.example.com/v1",
    auth_type="api_key",
))
EOF

export MY_API_KEY=your-test-key
hermes -z "hello" --provider my-provider -m some-model

Интеграция с общим PluginManager

Общий PluginManager (тот, с которым работает hermes plugins) видит плагины провайдеров моделей, но не импортирует их — providers/__init__.py управляет их жизненным циклом. Менеджер записывает манифест для интроспекции и категоризирует по kind: model-provider. Когда вы помещаете немаркированный пользовательский плагин в $HERMES_HOME/plugins/, который вызывает register_provider с ProviderProfile, менеджер автоматически приводит его к kind: model-provider с помощью эвристики на основе исходного текста — так что плагин всё равно маршрутизируется корректно даже без plugin.yaml.

Распространение через pip

Как и любой плагин Hermes, провайдеры моделей могут поставляться как pip-пакет. Добавьте точку входа в ваш pyproject.toml:

[project.entry-points."hermes.plugins"]
acme-inference = "acme_hermes_plugin:register"

…где acme_hermes_plugin:register — это функция, вызывающая register_provider(profile). Общий PluginManager подхватывает плагины с точками входа во время discover_and_load(). Для pip-плагинов с kind: model-provider вам всё равно нужно объявить kind в манифесте (или полагаться на эвристику исходного текста).

См. Building a Hermes Plugin для полной настройки точек входа.