Разрешение провайдера в рантайме

В Hermes есть общий резолвер провайдеров, который работает во всех компонентах:

  • CLI
  • gateway
  • cron-задачи
  • ACP
  • вызовы вспомогательных моделей

Основная реализация:

  • hermes_cli/runtime_provider.py — разрешение учётных данных, _resolve_custom_runtime()
  • hermes_cli/auth.py — реестр провайдеров, resolve_provider()
  • hermes_cli/model_switch.py — общий пайплайн переключения /model (CLI + gateway)
  • agent/auxiliary_client.py — маршрутизация вспомогательных моделей
  • providers/ — ABC и точки входа реестра (ProviderProfile, register_provider, get_provider_profile, list_providers)
  • plugins/model-providers/<name>/ — плагины для каждого провайдера (встроенные), которые объявляют api_mode, base_url, env_vars, fallback_models и регистрируются в реестре при первом обращении. Пользовательские плагины по пути $HERMES_HOME/plugins/model-providers/<name>/ перекрывают встроенные с тем же именем.

get_provider_profile() в providers/ возвращает ProviderProfile для заданного идентификатора провайдера. runtime_provider.py вызывает эту функцию в момент разрешения и получает канонический base_url, приоритетный список env_vars, api_mode и fallback_models — без дублирования этих данных в нескольких файлах. Чтобы runtime_provider.py подхватил новый плагин, достаточно добавить его в plugins/model-providers/<your-provider>/ (или $HERMES_HOME/plugins/model-providers/<your-provider>/) с вызовом register_provider(). Менять сам резолвер не нужно.

Если вы добавляете нового инференс-провайдера первого класса, читайте Adding Providers и руководство Model Provider Plugin вместе с этой страницей.

Приоритет разрешения

На верхнем уровне разрешение провайдера идёт в таком порядке:

  1. явный запрос из CLI/рантайма
  2. конфигурация модели/провайдера в config.yaml
  3. переменные окружения
  4. дефолты провайдера или автоматическое разрешение

Порядок здесь принципиален: для обычных запусков Hermes считает источником истины сохранённый выбор модели и провайдера. Иначе устаревший экспорт в оболочке мог бы молча перезаписать endpoint, который пользователь в последний раз выбрал через hermes model.

Провайдеры

Текущие семейства провайдеров (полный встроенный список — в plugins/model-providers/):

  • OpenRouter
  • Nous Portal
  • OpenAI Codex
  • Copilot / Copilot ACP
  • Anthropic (native)
  • Google / Gemini (gemini, google-gemini-cli)
  • Alibaba / DashScope (alibaba, alibaba-coding-plan)
  • DeepSeek
  • Z.AI
  • Kimi / Moonshot (kimi-coding, kimi-coding-cn)
  • MiniMax (minimax, minimax-cn, minimax-oauth)
  • Kilo Code
  • Hugging Face
  • OpenCode Zen / OpenCode Go
  • AWS Bedrock
  • Azure Foundry
  • NVIDIA NIM
  • xAI (Grok)
  • Arcee
  • GMI Cloud
  • StepFun
  • Qwen OAuth
  • Xiaomi
  • Ollama Cloud
  • LM Studio
  • Tencent TokenHub
  • Custom (provider: custom) — провайдер первого класса для любого OpenAI-совместимого endpoint
  • Именованные кастомные провайдеры (список custom_providers в config.yaml)

Результат разрешения в рантайме

Резолвер возвращает такие данные:

  • provider
  • api_mode
  • base_url
  • api_key
  • source
  • специфичные для провайдера метаданные: информация об истечении срока действия и обновлении токена

Зачем это нужно

Благодаря этому резолверу Hermes держит единую логику аутентификации и рантайма сразу в нескольких местах:

  • hermes chat
  • обработке сообщений в gateway
  • cron-задачах, выполняемых в новых сессиях
  • сессиях редактора ACP
  • задачах вспомогательных моделей

OpenRouter и кастомные OpenAI-совместимые base URL

Hermes умеет не отдавать чужой API-ключ кастомному endpoint, когда ключей несколько (например, OPENROUTER_API_KEY и OPENAI_API_KEY).

API-ключ каждого провайдера привязан к его собственному base URL:

  • OPENROUTER_API_KEY отправляется только на endpoint openrouter.ai
  • OPENAI_API_KEY используется для кастомных endpoint и как резервный вариант

Hermes также разграничивает:

  • реальный кастомный endpoint, выбранный пользователем
  • резервный путь через OpenRouter, когда кастомный endpoint не настроен

Это разграничение особенно важно для:

  • локальных серверов моделей
  • OpenAI-совместимых API, не связанных с OpenRouter
  • смены провайдеров без повторного запуска настройки
  • кастомных endpoint, сохранённых в конфиге — они продолжают работать, даже если OPENAI_BASE_URL не экспортирован в текущей оболочке

Нативный путь Anthropic

Anthropic больше не ограничивается маршрутом «через OpenRouter».

Когда разрешение провайдера выбирает anthropic, Hermes использует:

  • api_mode = anthropic_messages
  • нативный Anthropic Messages API
  • agent/anthropic_adapter.py для трансляции

Для нативного Anthropic разрешение учётных данных теперь предпочитает обновляемые учётные данные Claude Code скопированным env-токенам, если есть и те, и другие. На практике это означает:

  • файлы учётных данных Claude Code считаются предпочтительным источником, если в них есть обновляемая авторизация
  • ручные значения ANTHROPIC_TOKEN / CLAUDE_CODE_OAUTH_TOKEN по-прежнему работают как явные переопределения
  • перед вызовами нативного Messages API Hermes заранее обновляет учётные данные Anthropic
  • при 401 Hermes как запасной путь ещё раз повторяет попытку, пересобрав клиент Anthropic

Путь OpenAI Codex

Codex использует отдельный путь через Responses API:

  • api_mode = codex_responses
  • выделенное разрешение учётных данных и поддержка хранилища аутентификации

Маршрутизация вспомогательных моделей

Вспомогательные задачи, такие как:

  • обработка изображений
  • суммаризация при веб-извлечении
  • сжатие контекста
  • операции хаба навыков
  • вспомогательные операции MCP
  • сброс памяти

— могут идти по собственной маршрутизации провайдера/модели, отдельной от основной модели диалога.

Если вспомогательная задача настроена с провайдером main, Hermes разрешает её через тот же общий путь рантайма, что и обычный чат. На практике это означает:

  • кастомные endpoint из переменных окружения продолжают работать
  • кастомные endpoint, сохранённые через hermes model / config.yaml, тоже работают
  • маршрутизация вспомогательных задач различает реальный сохранённый кастомный endpoint и резервный путь через OpenRouter

Резервные модели

Hermes поддерживает настраиваемую цепочку резервных провайдеров — список пар (provider, model), которые перебираются по порядку при ошибках основной модели. Устаревший формат fallback_model с одной парой по-прежнему принимается ради обратной совместимости (и мигрируется при первой записи).

Как это устроено внутри

  1. Хранение: AIAgent.__init__ сохраняет словарь fallback_model и ставит _fallback_activated = False.

  2. Точки срабатывания: _try_activate_fallback() вызывается из трёх мест основного цикла повторных попыток в run_agent.py:

    • После исчерпания попыток при некорректных ответах API (None choices, отсутствующий контент)
    • При неповторяемых ошибках клиента (HTTP 401, 403, 404)
    • После исчерпания попыток при транзиентных ошибках (HTTP 429, 500, 502, 503)
  3. Процесс активации (_try_activate_fallback):

    • Сразу возвращает False, если режим уже активирован или не настроен
    • Вызывает resolve_provider_client() из auxiliary_client.py, чтобы собрать новый клиент с нужной авторизацией
    • Определяет api_mode: codex_responses для openai-codex, anthropic_messages для anthropic, chat_completions для всего остального
    • Заменяет значения in-place: self.model, self.provider, self.base_url, self.api_mode, self.client, self._client_kwargs
    • Для резервного Anthropic: создаёт нативный клиент Anthropic вместо OpenAI-совместимого
    • Заново оценивает кэширование промптов (включается для моделей Claude на OpenRouter)
    • Ставит _fallback_activated = True — это не даёт сработать повторно
    • Сбрасывает счётчик попыток в 0 и продолжает цикл
  4. Конфигурационный поток:

    • CLI: cli.py читает CLI_CONFIG["fallback_model"] → передаёт в AIAgent(fallback_model=...)
    • Gateway: gateway/run.py._load_fallback_model() читает config.yaml → передаёт в AIAgent
    • Валидация: оба ключа provider и model должны быть непустыми, иначе резервный режим отключается

Что НЕ поддерживает резервный режим

  • Делегирование субагентам (tools/delegate_tool.py): субагенты наследуют провайдера родителя, но не конфигурацию резервного режима
  • Вспомогательные задачи: используют собственную независимую цепочку автоопределения провайдера (см. раздел «Маршрутизация вспомогательных моделей» выше)

Cron-задачи резервный режим поддерживают: run_job() читает fallback_providers (или устаревший fallback_model) из config.yaml и передаёт в AIAgent(fallback_model=...), следуя тому же паттерну, что и _load_fallback_model() в gateway. См. Cron Internals.

Тестовое покрытие

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

  • tests/run_agent/test_fallback_credential_isolation.py — изоляция учётных данных между основным и резервным провайдером
  • tests/hermes_cli/test_fallback_cmd.py — команда /fallback в CLI
  • tests/gateway/test_fallback_eviction.py — вытеснение неработающих провайдеров в gateway

Связанная документация

ESC