Пулы учётных данных

Пул учётных данных позволяет зарегистрировать сразу несколько API-ключей или OAuth-токенов для одного и того же провайдера. Как только один ключ упирается в лимит запросов или в квоту биллинга, Hermes сам переключается на следующий рабочий ключ — сессия продолжается, и менять провайдера не приходится.

Это не то же самое, что запасные провайдеры: те переключают вас на другого провайдера целиком. Пул крутит ключи внутри одного провайдера, а запасной провайдер — это аварийный переход между разными. Сперва пробуется пул; и только когда все его ключи исчерпаны, в дело вступает запасной провайдер.

внимание
Ротация ключа сбрасывает кэш промпта

Кэши промптов на стороне провайдера (Anthropic, OpenAI, OpenRouter) привязаны к аккаунту/API-ключу, которым был сделан запрос. Когда пул переключается на другой ключ посреди сессии, у нового ключа нет закэшированного префикса для вашего диалога — следующий запрос перечитывает всю историю по полной цене за входные токены, а возврат на прежний ключ позже означает ещё одно полное перечитывание, если только TTL кэша у того ключа ещё не истёк. Ротация сохраняет сессию — в этом её смысл, — но на длинных диалогах каждая ротация обходится в один полноценный проход по всему контексту по полной цене.

совет

Пулы учётных данных пригодятся прежде всего там, где провайдер работает по API-ключам (OpenRouter, Anthropic). Одного OAuth через Nous Portal хватает на 300+ моделей, так что большинству пользователей на Portal пул вообще не нужен.

Как это работает

Your request
  → Pick key from pool (round_robin / least_used / fill_first / random)
  → Send to provider
  → 429 rate limit?
      → Plan/usage limit reached (e.g. ChatGPT/Codex "usage limit reached")?
          → Rotate to next pool key immediately (no retry — the cap won't clear on retry)
      → Generic / transient 429?
          → Retry same key once (transient blip)
          → Second 429 → rotate to next pool key
      → All keys exhausted → fallback_model (different provider)
  → 402 billing error?
      → Immediately rotate to next pool key (1h cooldown)
  → 401 auth expired?
      → Try refreshing the token (OAuth)
      → Refresh failed → rotate to next pool key
  → 400 "model is not supported when using Codex with a ChatGPT account"?
      → Bench this key for that model only, rotate to the next key (other models stay usable)
      → Every key rejects the model → fallback_model; the model is skipped for the session
  → HTTP 200 but `response.status: failed` (ChatGPT/Codex reports usage limits this way)?
      → Same rules as above, keyed on the embedded error code/message:
        quota/billing/auth → pool rotation first, provider fallback only once the pool is exhausted;
        content-policy and other failures → no rotation
  → Success → continue normally

Быстрый старт

Если API-ключ уже прописан в .env, Hermes подхватит его автоматически — получится пул из одного ключа. Чтобы ротация заработала, добавьте ещё ключи:

# Add a second OpenRouter key
hermes auth add openrouter --api-key sk-or-v1-your-second-key

# ...или пусть ключ выпустит вход через браузер (OpenRouter OAuth PKCE; хранится как обычный API-ключ)
hermes auth add openrouter --type oauth

# Add a second Anthropic key
hermes auth add anthropic --type api-key --api-key sk-ant-api03-your-second-key

# Add an Anthropic OAuth credential (requires Claude Max plan + extra usage credits)
hermes auth add anthropic --type oauth
# Opens browser for OAuth login

Посмотреть свои пулы:

hermes auth list

Вывод:

openrouter (2 credentials):
  #1  OPENROUTER_API_KEY   api_key id=ab12cd34 priority=0 env:OPENROUTER_API_KEY ←
  #2  backup-key           api_key id=ef56gh78 priority=1 manual

anthropic (3 credentials):
  #1  hermes_pkce          oauth   id=ab12cd34 priority=0 hermes_pkce ←
  #2  claude_code          oauth   id=cd34ef56 priority=1 claude_code
  #3  ANTHROPIC_API_KEY    api_key id=ef56gh78 priority=2 env:ANTHROPIC_API_KEY

Стрелка ← отмечает, какие учётные данные выбраны сейчас. Поле id= — идентификатор записи, который принимает hermes auth remove <provider> <target>, когда метка неоднозначна, а priority= — порядок, в котором пул пробует записи при стратегии fill_first.

Интерактивное управление

Запустите hermes auth без подкоманды — откроется интерактивный мастер:

hermes auth

Он покажет полный статус пулов и предложит меню:

What would you like to do?
  1. Add a credential
  2. Remove a credential
  3. Reset cooldowns for a provider
  4. Set rotation strategy for a provider
  5. Exit

У провайдеров, которые поддерживают и API-ключи, и OAuth (Anthropic, Nous, Codex), при добавлении спросят, какой тип нужен:

anthropic supports both API keys and OAuth login.
  1. API key (paste a key from the provider dashboard)
  2. OAuth login (authenticate via browser)
Type [1/2]:

Каждый вход через hermes auth add openai-codex становится отдельной записью пула, но независимо ротируются только разные аккаунты OpenAI: два входа в один и тот же аккаунт делят на стороне апстрима одно семейство токенов, поэтому OpenAI отзывает более старый, а вторая запись квоты не добавляет. Hermes предупреждает об этом при добавлении (warning: this login is the same OpenAI account as openai-codex credential #N) — войдите в другой аккаунт или оставьте одну запись.

Команды CLI

КомандаОписание
hermes authИнтерактивный мастер управления пулами
hermes auth listПоказать все пулы и учётные данные
hermes auth list <provider>Показать пул конкретного провайдера
hermes auth add <provider>Добавить учётные данные (спросит тип и ключ)
hermes auth add <provider> --type api-key --api-key <key>Добавить API-ключ без интерактива
hermes auth add <provider> --type oauthДобавить OAuth-данные через вход в браузере
hermes auth add openai-codex --browserТолько для Codex: вход через браузерный поток authorization-code + PKCE на localhost:1455 вместо кода устройства по умолчанию (для организаций, где гранты device-code отключены); если порт занят, откатывается на код устройства. Для всех входов Codex включается по умолчанию через auth.codex_login_flow: browser
hermes auth add <provider> --priority 0Добавить запись и поставить её первой в порядке fill_first
hermes auth priority <provider> <target> <n>Перевести запись на приоритет n (0 — пробуется первой); остальные перенумеровываются
hermes auth remove <provider> <index>Удалить запись по индексу (нумерация с единицы)
hermes auth reset <provider>Сбросить все кулдауны и статусы исчерпания (действует и на работающие сессии: живой шлюз или чат подхватит сброс при следующем запросе, а не запишет обратно свой устаревший кулдаун)
hermes auth reset <provider> <target>Снять кулдаун с одной записи — по индексу, id или метке
hermes auth refresh <provider> [target]Обновить токены одних OAuth-данных и вернуть их в ротацию (подтверждает, что грант жив; квоту перепроверит следующий запрос)

У Nous команда auth refresh работает только с синглтоном device_code из логина. Независимые аккаунты пула Nous отклоняются до обновления, их токены и кулдауны остаются как были. Обновить синглтон можно повторной аутентификацией hermes auth add nous --type oauth; независимый аккаунт это не обновляет. У остальных провайдеров поддержка обновления по источникам осталась прежней.

Стратегии ротации

Позиции приоритета считаются с нуля и упираются в границы пула; цели указываются одноимённо выводу — индексом с единицы, ID записи или однозначной точной меткой. auth add --priority заодно ставит на место и существующую запись, обновлённую повторной аутентификацией. У Anthropic ручные учётные данные держатся впереди засеянных, поэтому команда сообщает фактическую позицию, если это правило её изменило. Другие стратегии могут приоритет перебивать, а порядок не перепривязывает учётные данные, которые уже держит идущая сессия.

Каждый успешный выбор из пула увеличивает request_count, независимо от стратегии. Поиски ради обновления и подглядывания не считаются. Чтение статуса (hermes doctor, строки провайдеров в выборе /model, карточки авторизации в дашборде) — это подглядывание: оно никогда не обновляет, не ротирует и не отправляет на скамейку записи пула, так что сбой токен-эндпоинта, пока открыт выбор модели, не спрячет провайдера, который продолжает обслуживать запросы. Это счётчики выбора, а не суммы для биллинга и не число всех запросов к модели: закэшированные учётные данные обслуживают несколько запросов. Счётчики живут в памяти до следующей и без того происходящей записи пула — ротации, исчерпания, обновления, административной правки, — так что записи на диск на каждый выбор не добавляется.

Настраиваются через hermes auth → «Set rotation strategy» либо в config.yaml:

credential_pool_strategies:
  openrouter: round_robin
  anthropic: least_used
СтратегияПоведение
fill_first (по умолчанию)Держаться первого рабочего ключа, пока он не исчерпается, потом перейти к следующему; порядок задаёт priority каждой записи (меняется командой hermes auth priority)
round_robinПеребирать ключи по кругу, переключаясь после каждого выбора
least_usedБрать ключ с наименьшим числом запросов
randomСлучайный выбор среди рабочих ключей

Понижение рабочей записи в приоритете

Пул отправляет запись на скамейку только после того, как провайдер её отклонил (429/402/401). Если запись нужна вам в другом месте — например, вход Codex, недельное окно которого вы бережёте для интерактивной работы, — и вы не хотите, чтобы шлюз выбирал её первой ещё до того, как она иссякнет, не удаляйте её, а переместите в конец порядка fill_first:

hermes auth list openai-codex                 # find the index, id or label
hermes auth priority openai-codex 1 99        # 1-based index, entry id, or exact label; large n = last
hermes auth priority openai-codex work-seat 0 # ...and back to the front later

hermes auth priority <provider> <target> <priority> переставляет одну запись, перенумеровывает остальные и сохраняет новый порядок в auth.json. Пониженная запись остаётся рабочей: она не исчерпана, поэтому проверка сброса квоты Codex её не трогает, а hermes auth reset снимать нечего; по таймеру она не обновляется и по-прежнему используется, как только все записи перед ней окажутся на скамейке. Сессии, которые уже держат запись, остаются на ней до ротации; новые сессии (и следующий запуск шлюза) следуют новому порядку.

Восстановление после ошибок

Разные ошибки пул обрабатывает по-разному:

ОшибкаПоведениеКулдаун
429 Rate LimitОдин повтор на том же ключе (вдруг сбой временный). Второй подряд 429 — переход на следующий ключ1 час
402 Billing/QuotaСразу переход на следующий ключ1 час
401 Auth ExpiredСначала попытка обновить OAuth-токен. Переход — только если обновление не удалось5 минут
400 Codex model entitlement (The '<model>' model is not supported when using Codex with a ChatGPT account.)Ключ отправляется на скамейку только для отклонённой модели, и идёт переход на следующий; остальные модели продолжают пользоваться этим ключом. Другие 400 ротацию не запускают никогдаДо hermes auth reset (по модели; право на модель — свойство тарифа, а не окна)
Все ключи исчерпаныПереход на fallback_model, если он задан—

Значения reset_at, присланные провайдером, переопределяют эти кулдауны по умолчанию.

Флаг has_retried_429 сбрасывается при каждом успешном вызове API, поэтому единичный временный 429 не запускает ротацию.

Скамейка по квоте временна и для живой сессии. Когда 429/402 уводит сессию с записи, в начале каждого хода эта сессия проверяет, вернулась ли запись в ротацию, и переходит обратно, как только кулдаун снят, — тот же выбор сделала бы и новая сессия. Поэтому долгий чат (шлюз держит агентов в кэше) возвращается на место по подписке, когда её окно снова откроется, а не оплачивает до конца своих дней тарифицируемый фолбэк. Скамейка по 401 такого не запускает; явное переключение через /model отменяет запланированный возврат.

У Anthropic 429 считаются по моделям. Anthropic применяет лимиты запросов на каждую модель отдельно, поэтому общий 429 по одной модели Claude отправляет учётные данные в кулдаун только для этой модели — тот же ключ продолжает обслуживать все остальные модели Claude; ANTHROPIC_API_KEY и заимствованные токены Claude Code подчиняются тому же помодельному кулдауну. Ошибки оплаты (402, исчерпан лимит использования) и авторизации (401) по-прежнему снимают с ротации учётные данные целиком.

Мёртвый OAuth-вход попадает в отчёт, а не на скамейку. Когда refresh-токен отклонён окончательно (invalid_grant, invalid_token, refresh_token_reused — токен отозван, или другая программа с тем же входом успела его повернуть первой, — или, в случае Nous, у профиля нет ни входа в Portal, ни пары токенов для обновления), пул пишет одно WARNING с именем записи и командой починки (hermes auth add <provider>), а учётные данные покидают ротацию — помечаются dead или удаляются, если лишь зеркалили файл токена, который пул только что очистил, — пока вы не войдёте заново. Это касается OAuth-входов Anthropic, Codex, xAI и Nous в равной мере. Мёртвые учётные данные никогда не возвращаются в ротацию по таймеру, так что потерянный вход всплывёт в логе один раз, а не будет молча падать каждый час.

Все входы Codex в постоянно работающем домашнем каталоге мертвы: войдите заново, а не ждите подхвата. Hermes автоматически импортирует ~/.codex/auth.json из Codex CLI только ради починки уже имеющегося входа — когда его собственное обновление записи openai-codex не удалось, а auth.adopt_external_logins включён (см. Заимствованные логины CLI). Если в пуле все записи Codex dead (или удалены), чинить уже нечего, и headless-шлюз или cron-профиль остаётся без учётных данных Codex, пока вы не выполните hermes auth add openai-codex в этом домашнем каталоге (hermes -p <profile> auth add openai-codex для именованного профиля) — команда интерактивно предложит импорт из Codex CLI. Профили, которым нужен один общий вход, могут указывать на один и тот же HERMES_HOME, а не держать каждый свою копию одноразового refresh-токена.

Запись на кулдауне или мёртвая запись — это не пустая установка. Когда настроенный профиль запускает CLI, а его единственная запись на скамейке или в карантине, при старте печатается причина сбоя и, для скамейки, оставшееся время кулдауна (а для мёртвой записи — команда повторного входа hermes auth add <provider>). Мастер первого запуска «No inference provider is configured yet» предлагается, только когда резолвер не находит вообще ничего настроенного.

Пулы для своих эндпоинтов

У собственных эндпоинтов, совместимых с OpenAI (Together.ai, RunPod, локальные серверы), есть свои пулы. Ключом служит имя эндпоинта из словаря providers: в config.yaml (или устаревшего списка custom_providers, который автоматически мигрируется).

Когда вы настраиваете свой эндпоинт через hermes model, имя генерируется автоматически — что-то вроде «Together.ai» или «Local (localhost:8080)». Это имя и становится ключом пула.

# After setting up a custom endpoint via hermes model:
hermes auth list
# Shows:
#   Together.ai (1 credential):
#     #1  config key    api_key config:Together.ai ←

# Add a second key for the same endpoint:
hermes auth add Together.ai --api-key sk-together-second-key

Пулы своих эндпоинтов хранятся в auth.json внутри ключа credential_pool с префиксом custom::

{
  "credential_pool": {
    "openrouter": [...],
    "custom:together.ai": [...]
  }
}

Автообнаружение

Hermes сам находит учётные данные из разных источников и заполняет пул при старте:

ИсточникПримерПодхватывается автоматически?
Переменные окруженияOPENROUTER_API_KEY, ANTHROPIC_API_KEYДа
Нумерованные соседи в окруженииOPENROUTER_API_KEY_2, OPENROUTER_API_KEY_3, …Да (см. ниже)
OAuth-токены (auth.json)Codex device code, Nous device codeДа
Учётные данные Claude Code~/.claude/.credentials.jsonДа (Anthropic)
Hermes PKCE OAuth~/.hermes/auth.jsonДа (Anthropic)
Конфиг своего эндпоинтаmodel.api_key в config.yamlДа (свои эндпоинты)
Ручные записиДобавлены через hermes auth addСохраняются в auth.json

Автоматически подхваченные записи обновляются при каждой загрузке пула: убрали переменную окружения — соответствующая запись в пуле вычистится сама. Ручные записи (через hermes auth add) автоматически не удаляются никогда.

Несколько ключей из окружения

Хотите несколько ключей для провайдера, не складывая ни один из них в auth.json? Пронумеруйте их. Рядом с NVIDIA_API_KEY задайте NVIDIA_API_KEY_2, NVIDIA_API_KEY_3, … в shell, .env или менеджере секретов (Bitwarden Secrets, Vault, …) — при следующей загрузке каждый станет отдельной записью пула, без команд и без конфига. Обнаружение останавливается на первом пропущенном номере, так что случайный _5 без _4 игнорируется. В связке с credential_pool_strategies их можно ротировать:

credential_pool_strategies:
  nvidia: round_robin

Заимствованные секреты рантайма (например, переменные окружения, ссылки на Bitwarden, Vault, keyring, systemd, а также значения из своего конфига) на границе auth.json остаются только ссылками. Разрешённое значение Hermes держит в памяти на время текущего запуска, но на диск кладёт лишь метаданные: ссылку на источник, метку, статус, счётчики запросов и необратимый отпечаток. Ручные записи и состояние OAuth/device-code, которым владеет сам Hermes, хранят те токены, что нужны им для обновления.

Делегирование и общий пул у субагентов

Когда агент порождает субагентов через delegate_task, родительский пул учётных данных автоматически передаётся потомкам:

  • Тот же провайдер — потомок получает весь родительский пул и может ротировать ключи при лимитах
  • Другой провайдер — потомок берёт собственный пул этого провайдера (если он настроен)
  • Пул не настроен — потомок откатывается на унаследованный одиночный API-ключ

Так субагенты получают ту же устойчивость к лимитам, что и родитель, без какой-либо дополнительной настройки. Аренда учётных данных под каждую задачу не даёт потомкам мешать друг другу, когда они ротируют ключи параллельно.

Потокобезопасность

Все изменения состояния пула (select(), mark_exhausted_and_rotate(), try_refresh_current(), mark_used()) идут под блокировкой потоков. Это нужно для безопасного одновременного доступа, когда шлюз ведёт несколько чат-сессий сразу.

Между процессами (множество субагентов, шлюз рядом с CLI, cron-задачи) обновления OAuth выстраиваются в очередь через файловую блокировку на auth.json. Когда общий OAuth-грант истекает под множеством параллельных процессов, обновление выполняет ровно один из них; остальные видят, что токен на диске больше не совпадает с тем, который дал сбой, и принимают его вместо того, чтобы снова прокручивать одноразовый refresh-токен. Процесс, проигравший гонку за блокировку, оставляет свою запись здоровой и повторяет попытку — состязание за блокировку никогда не записывается как сбой учётных данных. Сессия или процесс, у которых ещё старая копия пула, никогда не записывают её токены поверх пары, которую с тех пор повернул кто-то другой: на диске остаётся более новая пара, и её подхватывают.

Архитектура

Полную схему потока данных смотрите в файле docs/credential-pool-flow.excalidraw в репозитории.

Пул учётных данных встраивается на уровне разрешения провайдера:

  1. agent/credential_pool.py — менеджер пула: хранение, выбор, ротация, кулдауны; за разрешение цели под блокировкой, сброс, добавление, удаление и правку приоритетов отвечает agent/credential_pool_admin.py; помодельные кулдауны Anthropic по 429 живут в agent/credential_pool_model_cooldowns.py
  2. hermes_cli/auth_commands.py — команды CLI и интерактивный мастер
  3. hermes_cli/runtime_provider.py — разрешение учётных данных с учётом пула
  4. agent/turn_api_error.py — восстановление после ошибок: 429/402/401 → ротация пула → запасной вариант

Хранение

Состояние пула лежит в ~/.hermes/auth.json под ключом credential_pool:

{
  "version": 1,
  "credential_pool": {
    "openrouter": [
      {
        "id": "abc123",
        "label": "OPENROUTER_API_KEY",
        "auth_type": "api_key",
        "priority": 0,
        "source": "env:OPENROUTER_API_KEY",
        "secret_source": "bitwarden",
        "secret_fingerprint": "sha256:12ab34cd56ef7890",
        "last_status": "ok",
        "request_count": 142
      }
    ],
    "anthropic": [
      {
        "id": "manual1",
        "label": "personal-api-key",
        "auth_type": "api_key",
        "priority": 0,
        "source": "manual",
        "access_token": "sk-ant-api03-..."
      }
    ]
  }
}

Запись OpenRouter выше заимствована из внешнего источника, поэтому сам ключ в auth.json не хранится. А вот запись Anthropic добавлена в хранилище Hermes вручную и намеренно, так что её токен можно сохранить на диск.

Строка env: при каждой загрузке заново наполняется из окружения, и имя переменной не обязано быть одним из тех, что Hermes объявляет для провайдера: нумерованные соседи (OPENROUTER_API_KEY_2, см. Автообнаружение) появляются здесь сами, а строка, написанная вручную и указывающая на любую другую переменную, заполняется тем же способом — секрет при этом в auth.json не попадает никогда.

Стратегии хранятся в config.yaml (не в auth.json):

credential_pool_strategies:
  openrouter: round_robin
  anthropic: least_used
перевод и редактура:

обновлено

⌕ ESC