Разрешение провайдера в рантайме
В 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 вместе с этой страницей.
Приоритет разрешения
На верхнем уровне разрешение провайдера идёт в таком порядке:
- явный запрос из CLI/рантайма
- конфигурация модели/провайдера в
config.yaml - переменные окружения
- дефолты провайдера или автоматическое разрешение
Порядок здесь принципиален: для обычных запусков 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)
Результат разрешения в рантайме
Резолвер возвращает такие данные:
providerapi_modebase_urlapi_keysource- специфичные для провайдера метаданные: информация об истечении срока действия и обновлении токена
Зачем это нужно
Благодаря этому резолверу 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отправляется только на endpointopenrouter.aiOPENAI_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 с одной парой по-прежнему принимается ради обратной совместимости (и мигрируется при первой записи).
Как это устроено внутри
-
Хранение:
AIAgent.__init__сохраняет словарьfallback_modelи ставит_fallback_activated = False. -
Точки срабатывания:
_try_activate_fallback()вызывается из трёх мест основного цикла повторных попыток вrun_agent.py:- После исчерпания попыток при некорректных ответах API (None choices, отсутствующий контент)
- При неповторяемых ошибках клиента (HTTP 401, 403, 404)
- После исчерпания попыток при транзиентных ошибках (HTTP 429, 500, 502, 503)
-
Процесс активации (
_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 и продолжает цикл
- Сразу возвращает
-
Конфигурационный поток:
- CLI:
cli.pyчитаетCLI_CONFIG["fallback_model"]→ передаёт вAIAgent(fallback_model=...) - Gateway:
gateway/run.py._load_fallback_model()читаетconfig.yaml→ передаёт вAIAgent - Валидация: оба ключа
providerиmodelдолжны быть непустыми, иначе резервный режим отключается
- CLI:
Что НЕ поддерживает резервный режим
- Делегирование субагентам (
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в CLItests/gateway/test_fallback_eviction.py— вытеснение неработающих провайдеров в gateway