Пулы учётных данных
Пул учётных данных позволяет зарегистрировать сразу несколько 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 в репозитории.
Пул учётных данных встраивается на уровне разрешения провайдера:
agent/credential_pool.py— менеджер пула: хранение, выбор, ротация, кулдауны; за разрешение цели под блокировкой, сброс, добавление, удаление и правку приоритетов отвечаетagent/credential_pool_admin.py; помодельные кулдауны Anthropic по 429 живут вagent/credential_pool_model_cooldowns.pyhermes_cli/auth_commands.py— команды CLI и интерактивный мастерhermes_cli/runtime_provider.py— разрешение учётных данных с учётом пула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