API-сервер
API-сервер отдаёт hermes-agent наружу как OpenAI-совместимый HTTP-эндпоинт. Любой фронтенд, который говорит на формате OpenAI — Open WebUI, LobeChat, LibreChat, NextChat, ChatBox и сотни других — подключается к hermes-agent и использует его как бэкенд.
Агент обрабатывает запросы со всем своим набором инструментов (терминал, операции с файлами, веб-поиск, память, навыки) и возвращает итоговый ответ. При стриминге индикаторы прогресса инструментов идут прямо в потоке, так что фронтенд показывает, чем агент занят прямо сейчас.
Чтобы API-сервер приносил пользу, самому Hermes нужны настроенный провайдер и бэкенды инструментов. Подписка Nous Portal закрывает и то и другое — 300+ моделей плюс веб/изображения/TTS/браузер через Tool Gateway. Выполните hermes setup --portal один раз перед запуском API-сервера, и фронтенды вроде Open WebUI или LobeChat получат полностью оснащённый инструментами бэкенд.
Быстрый старт
1. Включите API-сервер
Добавьте в ~/.hermes/.env:
API_SERVER_ENABLED=true
API_SERVER_KEY=change-me-local-dev
# Опционально: только если браузер должен обращаться к Hermes напрямую
# API_SERVER_CORS_ORIGINS=http://localhost:3000
2. Запустите шлюз
hermes gateway
Вы увидите:
[API Server] API server listening on http://127.0.0.1:8642
3. Подключите фронтенд
Направьте любой OpenAI-совместимый клиент на http://localhost:8642/v1:
# Проверка через curl
curl http://localhost:8642/v1/chat/completions \
-H "Authorization: Bearer change-me-local-dev" \
-H "Content-Type: application/json" \
-d '{"model": "hermes-agent", "messages": [{"role": "user", "content": "Hello!"}]}'
Либо подключите Open WebUI, LobeChat или любой другой фронтенд — пошаговые инструкции смотрите в руководстве по интеграции Open WebUI.
Эндпоинты
POST /v1/chat/completions
Стандартный формат OpenAI Chat Completions. Без состояния — полная переписка передаётся в каждом запросе через массив messages.
Запрос:
{
"model": "hermes-agent",
"messages": [
{"role": "system", "content": "You are a Python expert."},
{"role": "user", "content": "Write a fibonacci function"}
],
"stream": false
}
Ответ:
{
"id": "chatcmpl-abc123",
"object": "chat.completion",
"created": 1710000000,
"model": "hermes-agent",
"choices": [{
"index": 0,
"message": {"role": "assistant", "content": "Here's a fibonacci function..."},
"finish_reason": "stop"
}],
"usage": {"prompt_tokens": 50, "completion_tokens": 200, "total_tokens": 250}
}
Изображения во входных данных: пользовательские сообщения могут передавать content массивом из частей text и image_url. Поддерживаются и удалённые URL по http(s), и URL вида data:image/...:
{
"model": "hermes-agent",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "What is in this image?"},
{"type": "image_url", "image_url": {"url": "https://example.com/cat.png", "detail": "high"}}
]
}
]
}
Загруженные файлы (file / input_file / file_id) и URL data:, не являющиеся изображениями, возвращают 400 unsupported_content_type.
Стриминг ("stream": true): возвращает Server-Sent Events (SSE) с кусками ответа токен за токеном. Для Chat Completions в потоке используются стандартные события chat.completion.chunk плюс собственное событие Hermes hermes.tool.progress для UX начала работы инструмента. Для Responses в потоке используются типы событий OpenAI Responses, такие как response.created, response.output_text.delta, response.output_item.added, response.output_item.done и response.completed.
Все SSE-потоки (Chat Completions, Responses, /api/sessions/{id}/chat/stream, /v1/runs/{id}/events) отправляют строку-комментарий : keepalive, если за 10 секунд не было ни одного события, — так долгие вызовы инструментов не упираются в таймауты простоя на стороне клиента. Стандартные SSE-клиенты комментарии игнорируют; собственный парсер должен пропускать строки, начинающиеся с :.
Прогресс инструментов в потоках:
- Chat Completions: Hermes отправляет
event: hermes.tool.progress, чтобы показать начало работы инструмента, не засоряя сохраняемый текст ассистента. Строгие OpenAI-клиенты, которые спотыкаются на именованных SSE-событиях, могут отключить эти кадры черезgateway.platforms.api_server.tool_progress_events: false(по умолчаниюtrue); куски контента это не затрагивает. Отключение действует только на Chat Completions —/v1/runs/{id}/eventsвсегда отправляет события инструментов, и именно это описывает возможностьtool_progress_eventsв/v1/capabilities. - Responses: Hermes отправляет элементы вывода
function_callиfunction_call_outputпо спецификации прямо во время SSE-потока, поэтому клиенты могут рисовать структурированный UI инструментов в реальном времени.
Рассуждения модели (отправляются, только если модель действительно рассуждает и итоговый конфиг reasoning это разрешает; отключение на стороне входа — model_options.reasoning.enabled: false):
- Chat Completions: дельты рассуждений приходят кусками
choices[0].delta.reasoning_content(поле в стиле DeepSeek, которое Open WebUI, opencode и Vercel AI SDK показывают как блок размышлений); текст ответа остаётся вdelta.content. - Responses: каждый всплеск размышлений — это элемент вывода
reasoningпо спецификации:response.output_item.added(item.type: "reasoning"),response.reasoning_summary_part.added,response.reasoning_summary_text.delta…response.reasoning_summary_text.done,response.reasoning_summary_part.done,response.output_item.done. Он закрывается до того, как откроется следующий элемент message илиfunction_call, и повторяется в выводеresponse.completedкак{"id": "rs_…", "type": "reasoning", "status": "completed", "summary": [{"type": "summary_text", "text": "…"}]}.sequence_numberмонотонно растёт через события рассуждений, текста и инструментов. - Без стриминга:
/v1/chat/completionsвозвращает рассуждения хода вchoices[0].message.reasoning_content;/v1/responsesвозвращает те же элементы выводаreasoningперед message (и перед элементамиfunction_callэтого шага), в том числе при повторе черезGET /v1/responses/{id}. - Отправлять список
outputпредыдущего ответа обратно как следующийinput(так делают клиенты Responses SDK) можно: элементыreasoningна входе игнорируются, а не разбираются как пустые ходы пользователя. - Поддержка объявляется как
features.reasoning_streaming: trueвGET /v1/capabilities.
POST /v1/responses
Формат OpenAI Responses API. Поддерживает серверное состояние переписки через previous_response_id — сервер хранит полную историю разговора (включая вызовы инструментов и их результаты), так что многоходовой контекст сохраняется без управления им со стороны клиента.
Запрос:
{
"model": "hermes-agent",
"input": "What files are in my project?",
"instructions": "You are a helpful coding assistant.",
"store": true
}
Ответ:
{
"id": "resp_abc123",
"object": "response",
"status": "completed",
"model": "hermes-agent",
"output": [
{"type": "function_call", "status": "completed", "name": "terminal", "arguments": "{\"command\": \"ls\"}", "call_id": "call_1"},
{"type": "function_call_output", "status": "completed", "call_id": "call_1", "output": "README.md src/ tests/"},
{"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "Your project has..."}]}
],
"usage": {"input_tokens": 50, "output_tokens": 200, "total_tokens": 250}
}
Вызовы инструментов в массиве output уже выполнены на стороне сервера агентом Hermes — они воспроизводятся с "status": "completed" для структурированного UI инструментов, а не как ожидающие вызовы для выполнения клиентом.
При "stream": true промежуточные комментарии ассистента посреди хода (преамбулы прогресса phase="commentary" у бэкенда openai-codex или текст, который модель пишет рядом со своими вызовами инструментов) приходят отдельным завершённым элементом вывода message с "phase": "commentary" (response.output_item.added + response.output_item.done, и он же перечислен в response.completed). В итоговый элемент ответа он не вливается никогда, поэтому клиенты могут показывать его как живой прогресс и пропускать при сборке ответа. Приватные рассуждения в этот элемент не попадают. display.interim_assistant_messages: false (или переопределение display.platforms.api_server) подавляет его на всех поверхностях API-сервера.
Изображения во входных данных: input[].content может содержать части input_text и input_image. Поддерживаются и удалённые URL, и URL вида data:image/...:
{
"model": "hermes-agent",
"input": [
{
"role": "user",
"content": [
{"type": "input_text", "text": "Describe this screenshot."},
{"type": "input_image", "image_url": "data:image/png;base64,iVBORw0K..."}
]
}
]
}
Загруженные файлы (input_file / file_id) и URL data:, не являющиеся изображениями, возвращают 400 unsupported_content_type.
Многоходовой режим с previous_response_id
Сцепляйте ответы, чтобы держать полный контекст (включая вызовы инструментов) между ходами:
{
"input": "Now show me the README",
"previous_response_id": "resp_abc123"
}
Сервер восстанавливает всю переписку из сохранённой цепочки ответов — все прежние вызовы инструментов и их результаты сохранены. Сцепленные запросы делят и одну сессию, поэтому многоходовые разговоры показываются как одна запись в дашборде и истории сессий.
output каждого ответа перечисляет только элементы этого хода (его записи function_call / function_call_output и итоговый message), но никогда не вызовы инструментов прежних ходов — в том числе когда Hermes перед вызовом починил переданную историю (слил подряд идущие элементы assistant или user, выкинул осиротевшие результаты инструментов) или сжал её посреди цепочки. Сохранённая цепочка — это и есть починенный транскрипт, так что история не прирастает второй копией на каждом ходе.
Именованные разговоры
Используйте параметр conversation вместо отслеживания идентификаторов ответов:
{"input": "Hello", "conversation": "my-project"}
{"input": "What's in src/?", "conversation": "my-project"}
{"input": "Run the tests", "conversation": "my-project"}
Сервер автоматически сцепляет запрос с последним ответом в этом разговоре. Похоже на команду /title для сессий шлюза.
GET /v1/responses/{id}
Получить ранее сохранённый ответ по идентификатору.
DELETE /v1/responses/{id}
Удалить сохранённый ответ.
GET /v1/models
Выдаёт агента как доступную модель. Анонсируемое имя модели по умолчанию совпадает с именем профиля (или hermes-agent для профиля по умолчанию). Большинству фронтендов оно нужно для обнаружения моделей.
/v1/models намеренно остаётся дешёвой OpenAI-совместимой поверхностью. Он не перечисляет все связки «провайдер + модель», куда Hermes умеет маршрутизировать, и не обогащает ответ ценами и возможностями.
GET /api/model/options
Клиенты, знающие про Hermes, могут запросить тот же отобранный перечень провайдеров и моделей, каким пользуются дашборд и TUI. Маршрут работает через обычную bearer-аутентификацию API-сервера и возвращает строки провайдеров, подсказки о возможностях моделей и метаданные о ценах — всему этому не место в OpenAI-совместимом ответе /v1/models:
curl \
-H "Authorization: Bearer $API_SERVER_KEY" \
"http://127.0.0.1:8642/api/model/options"
На этой же основе работают страница Models в дашборде и RPC model.options в TUI. Ответ содержит аутентифицированных провайдеров, отобранные списки моделей, цены по каждой модели и подсказки о её возможностях.
Обычное открытие намеренно осторожно к пользовательским провайдерам: Hermes опрашивает только выбранный сейчас пользовательский эндпоинт, чтобы устаревший или выключенный сохранённый адрес не подвешивал выбор. Явное обновление переключает на полный опрос и сбрасывает кэш моделей провайдера:
curl \
-H "Authorization: Bearer $API_SERVER_KEY" \
"http://127.0.0.1:8642/api/model/options?refresh=1"
Берите /v1/models, когда OpenAI-совместимому клиенту нужно лишь имя модели, чтобы вернуть его в запросах chat/responses. Берите /api/model/options, когда аутентифицированному интерфейсу нужны богатые метаданные выбора, специфичные для Hermes.
GET /v1/capabilities
Возвращает машиночитаемое описание стабильной поверхности API-сервера для внешних UI, оркестраторов и мостов-плагинов.
{
"object": "hermes.api_server.capabilities",
"platform": "hermes-agent",
"model": "hermes-agent",
"auth": {"type": "bearer", "required": true},
"features": {
"chat_completions": true,
"responses_api": true,
"run_submission": true,
"run_status": true,
"run_events_sse": true,
"run_stop": true,
"reasoning_streaming": true
}
}
Обращайтесь к этому эндпоинту при интеграции дашбордов, браузерных UI или панелей управления — так они узнают, поддерживает ли запущенная версия Hermes прогоны, стриминг, отмену и непрерывность сессий, не завязываясь на приватные внутренности Python.
Управление через расширение браузера
Hermes умеет пропускать браузерные инструменты через аутентифицированное расширение, которое управляет сессией браузера, привязанной к текущей сессии Hermes. По умолчанию функция выключена; чтобы её задействовать, поставьте browser.extension_control.enabled в true:
browser:
extension_control:
enabled: true
Локальному пути через API дополнительно нужен bearer-ключ API-сервера. Контроллер регистрируется только для уже существующей серверной сессии. Принципала контроллера Hermes выводит из аутентифицированного состояния сервера, а присланный клиентом principal_id игнорирует.
Актуальный контракт смотрите через GET /v1/capabilities. Объект browser_extension_control сообщает, включена ли функция, версию протокола, имена транспортов и точный список разрешённых возможностей:
controller.noop
browser_back
browser_click
browser_navigate
browser_press
browser_screenshot
browser_scroll
browser_snapshot
browser_tab_activate
browser_tabs
browser_type
Запрошенные возможности за пределами этого списка отсекаются. Сырой CDP, произвольное выполнение скриптов, доступ к консоли, загрузка файлов, извлечение изображений и vision в протокол контроллера не входят.
Когда у запроса нет привязанной идентичности контроллера — или когда функция отключена, — Hermes продолжает работать с прежним браузерным бэкендом. Но как только шлюз привяжет к запросу принципала контроллера и семейство транспортов, главным становится канал расширения: отсутствующий, неоднозначный, отвалившийся или неспособный выполнить действие контроллер даёт отказ вместо молчаливого переключения на другой локальный или облачный браузер. После того как выбран конкретный контроллер, его результат или ошибка окончательны — то же действие через другой бэкенд Hermes не повторяет.
Регистрация через локальный API
- Отправьте аутентифицированный
POST /v1/browser-control/registerс полямиprotocol_version,session_id,controller_id,browser_profile_idи запрашиваемымиcapabilities. - Hermes вернёт одноразовый тикет с TTL 30 секунд и отфильтрованную область контроллера, привязанную к серверу.
- Откройте
GET /v1/browser-control/wsсразу с двумя подпротоколами WebSocket:hermes-browser-control-v1иhermes-browser-control-ticket.<ticket>.
В строке запроса тикет не принимается никогда. Неизвестные, просроченные, повторно использованные или битые тикеты отбрасываются ещё до апгрейда соединения до WebSocket.
Фреймы контроллера
Hermes шлёт фреймы browser.controller.command с полями command_id, action, неизменяемыми arguments, идентификаторами браузера и контроллера, а также исходным tool_call_id. Контроллер отвечает фреймом browser.controller.result — тот же command_id, строго булев ok и либо result, либо error. Отмена и таймаут порождают browser.controller.cancel; опоздавшие результаты игнорируются.
Неожиданный обрыв сокета помечает контроллер офлайновым, но уже запущенная работа сохраняется до исходного дедлайна каждой команды. Переподключение с теми же принципалом, профилем, сессией, идентификатором контроллера, профилем браузера и транспортной идентичностью обновляет транспорт, не принимая новой работы, пока не будут разосланы отложенные отмены. Согласованные возможности при таком переподключении могут измениться — полем идентичности они не считаются. Другой идентификатор контроллера или другой профиль браузера в той же аутентифицированной сессии означает жёсткую замену: старая незавершённая работа отменяется, и только потом преемник становится маршрутизируемым. Для намеренного жёсткого отсоединения пошлите browser.controller.detach по аутентифицированному транспорту контроллера — это немедленно отменит всю ожидающую работу. Простое закрытие сокета трактуется как обрыв, после которого можно восстановиться.
Аутентифицированный транспорт дашборда даёт ту же семантику регистрации, результатов, heartbeat, возможностей и владения — через свой канал Gateway RPC/событий. В обоих транспортах для выбора нужно одно однозначное точное совпадение по принципалу, профилю, сессии, контроллеру, профилю браузера, семейству транспортов и возможности. А когда контроллер уже выбран, его отказ окончателен и никогда не повторяется через другой браузерный бэкенд.
Выбор модели на уровне запроса
Аутентифицированные клиенты могут переопределить выбор модели Hermes для конкретного запроса, отправив:
model— идентификатор целевой модели для этого ходаprovider— слаг провайдера Hermes, по которому подбираются учётные данные и рантайм на этот ходmodel_options— управление рассуждениями и уровнем сервиса в границах запроса
Те же поля принимают:
POST /v1/chat/completionsPOST /v1/responsesPOST /v1/runsPOST /api/sessions/{session_id}/chatPOST /api/sessions/{session_id}/chat/stream
Приоритет детерминирован:
- Переопределение
/modelв сессии, если оно у сессии уже есть - Статическое сопоставление
gateway.platforms.api_server.model_routes, выбранное, когдаmodelиз запроса совпал с настроенным псевдонимом маршрута - Прямые
model/providerиз запроса, если ни один псевдоним не подошёл - Глобальные умолчания из конфига шлюза и окружения
model_options остаётся в границах запроса независимо от того, чья модель и провайдер победили. Если запрос присылает provider, конфликтующий с настроенным псевдонимом model_routes, Hermes отвечает 400, а не подмешивает молча учётные данные маршрута к другому провайдеру.
Голые значения model на OpenAI-совместимых эндпоинтах включаются отдельно. Обычные OpenAI-клиенты сплошь и рядом зашивают имена моделей (gpt-4o и прочие), а существующие развёртывания рассчитывают, что такие имена откатятся к умолчанию шлюза. Поэтому на POST /v1/chat/completions и POST /v1/responses значение model, присланное БЕЗ provider, игнорируется — пока вы не включите:
gateway:
platforms:
api_server:
direct_model_requests: true
Запросы с явным provider — а также нативные для Hermes /v1/runs и эндпоинты чата в сессии — уважают запрошенную модель независимо от этого флага.
Пример:
{
"model": "MiniMax-M3",
"provider": "minimax",
"model_options": {
"reasoning_effort": "high",
"service_tier": "priority"
},
"messages": [
{"role": "user", "content": "Summarize the repo status."}
]
}
GET /health
Проверка здоровья. Возвращает {"status": "ok"}. Доступна также по GET /v1/health для OpenAI-совместимых клиентов, которые ждут префикс /v1/.
GET /health/detailed
Аутентифицированная проверка готовности для мониторинга и панелей управления. Отдаёт ограниченный по объёму статус: конфиг активного профиля, база состояния, настроенная модель, свободное место на диске, состояние шлюза и платформ, активные прогоны API, ожидающие завершения процессы и активные делегирования. В ответе — статусы и счётчики, но не значения конфига, не учётные данные, не пути, не команды, не содержимое очередей и не сырые ошибки.
Публичный маршрут /health остаётся дешёвой проверкой живости и проверок готовности не запускает. Деградировавшая готовность всё равно отвечает HTTP 200 — смотрите на поле status верхнего уровня и на readiness.checks.
Runs API (удобная альтернатива со стримингом)
Помимо /v1/chat/completions и /v1/responses, сервер отдаёт API runs — для длинных сессий, где клиент хочет подписаться на события прогресса, а не управлять стримингом сам.
POST /v1/runs
Создать новый прогон агента. Возвращает run_id, по которому можно подписаться на события прогресса.
{
"run_id": "run_abc123",
"status": "started"
}
Прогоны принимают простую строку input и опциональные session_id, instructions, conversation_history или previous_response_id. Если задан session_id, Hermes выводит его в статусе прогона, чтобы внешние UI могли сопоставить прогоны со своими идентификаторами разговоров.
Чтобы можно было безопасно повторять запрос на создание, отправляйте заголовок Idempotency-Key (1–255 видимых ASCII-символов). Hermes надёжно резервирует ключ ещё до начала работы. Идентичный повтор возвращает исходный run_id с HTTP 202 и Idempotency-Replayed: true — в том числе после перезапуска шлюза и после того, как прогон уже завершился, упал или был отменён. Повтор с тем же ключом, но другим JSON-телом запроса возвращает HTTP 409 с кодом idempotency_key_conflict. Ключи изолированы по аутентифицированному профилю/учётным данным API и хранятся 24 часа после последнего обновления статуса; используйте уникальные непредсказуемые ключи и не переиспользуйте их для несвязанных операций. Запросы без этого заголовка ведут себя по-старому и всегда создают новый прогон.
Когда session_id указывает на существующую сессию Hermes и явные conversation_history или previous_response_id не переданы, прогон подгружает активный транскрипт этой сессии. Аренда хода сессии сериализует параллельную запись и обновляет транскрипт после ожидания при конфликте.
GET /v1/runs/{run_id}
Опросить текущее состояние прогона. Удобно для дашбордов, которым нужен статус без удержания открытого SSE-соединения, и для UI, которые переподключаются после навигации.
{
"object": "hermes.run",
"run_id": "run_abc123",
"status": "completed",
"session_id": "space-session",
"model": "hermes-agent",
"output": "Done.",
"usage": {"input_tokens": 50, "output_tokens": 200, "total_tokens": 250, "cache_read_tokens": 40, "cache_write_tokens": 0},
"runtime": {"provider": "openai", "model": "gpt-5", "route_source": "global"}
}
model повторяет то, что просил запрос. У завершённого прогона runtime — пара провайдер/модель, которая на самом деле обслужила ход: после переключения на резервного провайдера там будет резервная пара, так что сборщик данных для учёта затрат отнесёт прогон к правильному провайдеру. usage.cache_read_tokens / usage.cache_write_tokens — чтения и записи кэша промптов в сессии, чтобы закэшированный ввод не считался по полной цене. У runtime та же форма, что в /v1/chat/completions и /v1/responses: route_source говорит, как был выбран рантайм (global, raw_request, model_routes), а запрос, в котором назван model/provider, получает ещё и requested: {provider, model}, чтобы запрошенную и обслужившую пары можно было сравнить. Событие run.completed в потоке событий несёт те же поля usage и runtime.
Статусы недолго держатся после финальных состояний (completed, failed, cancelled или interrupted) — для опроса и согласования состояния в UI. Если шлюз завершается, пока прогон активен, прогон сохраняется как interrupted (ошибка Gateway shutdown interrupted the run., финальное событие run.interrupted) ещё до того, как агента попросят остановиться, так что долговечный прогон никогда не переживает перезапуск в статусе running; запоздалый результат прерванного хода его не перезапишет.
Пока шлюз ещё доделывает текущую работу (hermes gateway stop/restart или SIGTERM с ходом в процессе), каждый незавершённый прогон дополнительно несёт shutdown_requested_at (секунды Unix) с того момента, как новые ходы начали отклоняться. status остаётся running, потому что ход ещё обслуживается; опрашивающий клиент, увидевший это поле, знает, что процесс уходит и прогон завершится interrupted самое позднее по истечении бюджета на завершение. Завершённые прогоны этого поля не получают никогда.
GET /v1/runs/{run_id}/events
Поток Server-Sent Events с прогрессом вызовов инструментов, дельтами токенов и событиями жизненного цикла прогона. Сделан для дашбордов и толстых клиентов, которым нужно подключаться и отключаться, не теряя состояния.
Промежуточные комментарии ассистента посреди хода — преамбулы прогресса phase="commentary"
у бэкенда openai-codex или текст, который модель пишет рядом со своими вызовами
инструментов, — приходят как message.interim (text, already_streamed): тот же контракт,
что у шлюза TUI. already_streamed: true означает, что текст уже ушёл и как message.delta,
так что клиенты, которые отрисовывают дельты, могут его пропустить. Итоговый ответ
по-прежнему приходит только в run.completed; приватные рассуждения в message.interim
не превращаются никогда. Включается через display.interim_assistant_messages (по умолчанию true).
События жизненного цикла инструментов несут tool.started (tool, preview аргументов) и
tool.completed (tool, duration в секундах, error и preview результата). Флаг
error отражает исход самого инструмента — ненулевой exit_code терминала, структурированный
результат {"error": ...}, отклонённое подтверждение — независимо от того, пришёл результат JSON-строкой или
уже разобранным объектом. preview при завершении — это текст результата (структурированные результаты
кодируются в JSON), пропущенный через принудительное маскирование секретов и обрезанный до 500 символов, так что
клиент отличит отказ в подтверждении (BLOCKED: ...) от обычного сбоя, не получая
неограниченную полезную нагрузку инструмента.
Когда агент делегирует работу фоновым субагентам, в поток приходят и события жизненного цикла subagent.start и subagent.complete — клиент видит исход делегирования, включая таймауты и сбои, вместо тишины на время работы дочернего агента. В полезной нагрузке subagent.complete едут статус дочернего агента, сводка, длительность, цифры по токенам и стоимости, child_session_id для сопоставления, а также delegation_id пачки, к которой относится дочерний агент (чтобы параллельные и вложенные веерные запуски не сливались); свободный текст перед выходом из процесса проходит принудительное вычищение секретов. События по каждому инструменту дочернего агента (subagent.tool, тики прогресса) намеренно не пробрасываются — это шум на весь интерфейс; для пошаговой картины есть живые файлы транскриптов по каждому дочернему агенту. Эти события доступны, пока открыт поток родителя: запоздавшее отсоединённое завершение уже закрытый SSE-поток прогона не открывает заново и его терминальный статус не меняет.
Отсоединённые результаты и история сессии
Фоновому делегированию нужно продолжение, которое читает историю сессии на сервере: явный X-Hermes-Session-Id в Chat Completions, нативный запрос /api/sessions/{id}/chat или запрос Runs, работающий с историей сессии. Chat Completions без заголовка, цепочки Responses и запросы Runs с previous_response_id или собственной историей от вызывающей стороны выполняют делегирование синхронно и возвращают результат в том же ходе. Просто вывести ID сессии из содержимого запроса недостаточно, чтобы включить отсоединённую доставку.
У запросов, которые можно продолжить, завершение сохраняется один раз на единицу делегирования. Оно доступно через GET /api/sessions/{id}/messages и в истории сессии на следующем настоящем ходе клиента. Повторы тот же результат заново не вставляют, а промежуточные уведомления о сбое задачи живут под своими идентификаторами. Доставка ждёт, пока аренда сессии принадлежит ходу клиента, и следует за продолжениями после сжатия. Chat Completions возвращает переданный вами явный ID сессии и в JSON, и в потоковом ответе; продолжайте отправлять этот ID даже после сжатия.
Завершение никогда не начинает ход модели само и не обходит ожидающее подтверждение от человека. Следующий ход — за клиентом. Клиентам, которые продолжают работать со своими снимками истории, стоит брать синхронное делегирование, а не ждать, что серверная строка доставки вольётся в эти снимки.
Непрочитанные буферы событий протухают через пять минут, чтобы отвалившийся клиент не растил память бесконечно. Истекает только транспортное состояние: прогон, который ещё выполняется, остаётся виден опросу статуса, подтверждениям, управлению остановкой и учёту параллельности — до тех пор, пока работа исполнителя действительно не завершится. Подключённый подписчик SSE продолжает вычитывать поток как обычно.
POST /v1/runs/{run_id}/stop
Прервать текущий ход агента. Эндпоинт сразу возвращает {"status": "stopping"}, пока Hermes просит активного агента остановиться в ближайшей безопасной точке прерывания.
Прогон продолжает числиться как stopping, пока работа исполнителя не завершится, и только потом переходит в cancelled: запрос остановки никогда не прячет ещё живого воркера.
POST /v1/runs/{run_id}/approval
Разрешить ожидающее подтверждение для прогона, который встал на человеческое решение (например, вызов инструмента, закрытый политикой согласования). Тело запроса содержит решение об одобрении; прогон возобновляется, как только решение зафиксировано. Эндпоинт анонсируется в /v1/capabilities как возможность run_approval, чтобы внешние UI определяли поддержку перед тем, как показывать пользователю запрос на подтверждение.
Согласие на уровне доверия MCP — вызов инструмента с возможностью записи на сервере с trust: untrusted — проходит тем же путём: прогон отправляет событие approval.request и встаёт в waiting_for_approval, пока этот эндпоинт не разрешит его (once запускает инструмент, deny блокирует).
Jobs API (фоновые задачи по расписанию)
Сервер отдаёт лёгкий CRUD-интерфейс задач для управления фоновыми прогонами агента по расписанию с удалённого клиента. Все эндпоинты закрыты той же bearer-авторизацией.
GET /api/jobs
Список всех задач по расписанию.
POST /api/jobs
Создать новую задачу по расписанию. Тело принимает ту же форму, что и hermes cron — промпт, расписание, навыки, переопределение провайдера, цель доставки.
GET /api/jobs/{job_id}
Получить определение одной задачи и состояние её последнего запуска.
PATCH /api/jobs/{job_id}
Обновить поля существующей задачи (промпт, расписание и т. д.). Частичные обновления сливаются.
DELETE /api/jobs/{job_id}
Удалить задачу. Заодно отменяет любой выполняющийся прогон.
POST /api/jobs/{job_id}/pause
Приостановить задачу без удаления. Метки следующего запуска по расписанию замораживаются до возобновления.
POST /api/jobs/{job_id}/resume
Возобновить ранее приостановленную задачу.
POST /api/jobs/{job_id}/run
Запустить задачу немедленно, вне расписания.
Sessions API (управление сессиями через REST)
Внешние UI могут управлять сессиями Hermes через REST, не поднимая дашборд. Все эндпоинты закрыты ключом API_SERVER_KEY и живут под /api/sessions/*.
| Метод | Путь | Описание |
|---|---|---|
GET | /api/sessions | Список сессий (с пагинацией — limit, offset, source, include_children) |
POST | /api/sessions | Создать пустую сессию |
GET | /api/sessions/{id} | Прочитать метаданные сессии |
PATCH | /api/sessions/{id} | Обновить заголовок или end_reason |
DELETE | /api/sessions/{id} | Удалить сессию |
GET | /api/sessions/{id}/messages | История сообщений сессии. include_compacted=true возвращает и ходы, которые архивировало сжатие контекста (по умолчанию — только живой транскрипт). inline_images=false рисует вложенные изображения заглушками [image] вместо встроенных data URI — транскрипт весит килобайты, что удобно клиентам, читающим по сети |
POST | /api/sessions/{id}/fork | Ответвить сессию через родословную SessionDB (совпадает с семантикой CLI /branch) |
POST | /api/sessions/{id}/chat | Выполнить один синхронный ход агента |
POST | /api/sessions/{id}/chat/stream | SSE-обёртка над одним ходом — отправляет события assistant.delta, assistant.commentary (комментарий посреди хода: message_id, text, already_streamed; в assistant.completed не вливается никогда), tool.started, tool.completed, tool.failed (инструмент завершился с ошибкой), а затем завершающее run.completed / run.failed / run.cancelled, соответствующее тому, как закончился ход (см. Терминальный статус прогона) |
/v1/capabilities анонсирует всю поверхность через флаги возможностей session_* и записи endpoints.session_*, чтобы внешние UI определяли поддержку и безопасно откатывались. Изображения во входных данных поддерживаются в полезной нагрузке chat и chat/stream (путь с поддержкой мультимодальности).
# ответвить сессию и выполнить один ход
curl -X POST http://localhost:8642/api/sessions/$ID/fork \
-H "Authorization: Bearer $API_SERVER_KEY" \
-d '{"title": "explore alt path"}'
# стримить ход по SSE
curl -N -X POST http://localhost:8642/api/sessions/$ID/chat/stream \
-H "Authorization: Bearer $API_SERVER_KEY" \
-d '{"input": "what files changed in the last hour?"}'
Обнаружение навыков и наборов инструментов
GET /v1/skills и GET /v1/toolsets позволяют внешним клиентам детерминированно перечислить возможности агента через REST, а не выспрашивать их у модели. Оба эндпоинта только на чтение и закрыты ключом API_SERVER_KEY.
curl http://localhost:8642/v1/skills \
-H "Authorization: Bearer $API_SERVER_KEY"
# → [{"name": "github-pr-workflow", "description": "...", "category": "..."}, ...]
curl http://localhost:8642/v1/toolsets \
-H "Authorization: Bearer $API_SERVER_KEY"
# → [{"name": "core", "label": "...", "description": "...", "enabled": true,
# "configured": true, "tools": ["read_file", "write_file", ...]}, ...]
/v1/skills возвращает те же метаданные, которые хаб навыков использует внутри себя. /v1/toolsets возвращает наборы инструментов, разрешённые для платформы api_server, с конкретным списком tools, в который раскрывается каждый набор. Оба анонсируются под endpoints.* в /v1/capabilities.
Область долговременной памяти (X-Hermes-Session-Key)
Многопользовательским фронтендам вроде Open WebUI нужен стабильный идентификатор на канал для долговременной памяти (Honcho и т. п.), независимый от привязанного к транскрипту X-Hermes-Session-Id (который меняется по /new). Передайте X-Hermes-Session-Key в /v1/chat/completions, /v1/responses или /v1/runs, и Hermes протянет его до AIAgent(gateway_session_key=...), где провайдер памяти Honcho выведет из него стабильную область.
POST /v1/chat/completions HTTP/1.1
Authorization: Bearer ***
X-Hermes-Session-Id: transcript-alpha
X-Hermes-Session-Key: agent:main:webui:dm:user-42
Правила: максимум 256 символов; управляющие символы (\r, \n, \x00) отвергаются; значение возвращается обратно в ответах (JSON + SSE). /v1/capabilities анонсирует поддержку через "session_key_header": "X-Hermes-Session-Key". Без этого ключа стратегия per-session у Honcho даёт разную область на каждый session_id — ровно так, как Hermes вёл себя раньше.
Автоматическое припоминание следует за транскриптом: провайдер памяти инициализируется один раз на сессию и сохраняется между запросами, так что продолженная сессия (X-Hermes-Session-Id, previous_response_id или объявленный разговор X-Hermes-Session-Key) получает то, что провайдер подготовил после предыдущего хода, — ровно как чат в Telegram или Discord. Запрос без какого-либо продолжения начинает новую сессию и, как первый ход любой новой сессии CLI, пока ничего подготовленного не имеет. Простаивающие сессии освобождают своего провайдера через тот же TTL простоя, что и кэш агентов шлюза (agent.agent_cache.idle_ttl_secs, по умолчанию один час).
Обработка системного промпта
Когда фронтенд присылает сообщение system (Chat Completions) или поле instructions (Responses API), hermes-agent накладывает его поверх своего основного системного промпта. Агент сохраняет все свои инструменты, память и навыки — системный промпт фронтенда добавляет дополнительные указания.
Так можно настроить поведение под конкретный фронтенд, не теряя возможностей:
- Системный промпт Open WebUI: «You are a Python expert. Always include type hints.»
- У агента по-прежнему есть терминал, файловые инструменты, веб-поиск, память и т. д.
Аутентификация
Bearer-токен через заголовок Authorization:
Authorization: Bearer ***
Ключ задаётся переменной окружения API_SERVER_KEY. Если нужно, чтобы браузер обращался к Hermes напрямую, дополнительно задайте API_SERVER_CORS_ORIGINS с явным списком разрешений.
Маршрутизация по профилям (/p/<profile>/…)
Когда мультипрофильная маршрутизация шлюза включена (gateway.multiplex_profiles), общий слушатель обслуживает каждый профиль по префиксу URL /p/<profile>/ — и аутентификация привязана к маршрутизируемому профилю:
- Запросы к
/p/<profile>/v1/...обязаны предъявлять собственныйAPI_SERVER_KEYэтого профиля (из~/.hermes/profiles/<profile>/.env). Ключ слушателя по умолчанию на префиксах именованных профилей отвергается. - Маршруты без префикса и
/p/default/...продолжают работать с ключом профиля по умолчанию. - Именованный профиль без собственного
API_SERVER_KEYзакрывается наглухо — его префикс недоступен, пока ключ не задан. - Прогоны привязаны к профилю:
/v1/runs/{run_id}и его маршрутыevents,stop,steerиapprovalотвечают только тому профилю, который прогон создал (включая прогоны, запущенные через/api/sessions/{id}/chat/stream); идентификатор чужого прогона вернёт404, а не403.
До этой правки корректный ключ профиля по умолчанию принимался на любом префиксе /p/<profile>/. Если вы полагались на один общий ключ для всех префиксов профилей, пропишите отдельный API_SERVER_KEY в .env каждого профиля — переиспользованные ключи по умолчанию на именованных префиксах теперь возвращают 401.
API-сервер даёт полный доступ к набору инструментов hermes-agent, включая команды терминала. API_SERVER_KEY обязателен для любого развёртывания, в том числе при привязке к локальному 127.0.0.1 по умолчанию. Держите API_SERVER_CORS_ORIGINS узким, чтобы контролировать доступ из браузера, когда вы явно разрешаете браузерных клиентов.
Конфигурация
Переменные окружения
| Переменная | По умолчанию | Описание |
|---|---|---|
API_SERVER_ENABLED | false | Включить API-сервер |
API_SERVER_PORT | 8642 | Порт HTTP-сервера |
API_SERVER_HOST | 127.0.0.1 | Адрес привязки (по умолчанию только localhost) |
API_SERVER_KEY | (обязательно) | Bearer-токен для аутентификации |
API_SERVER_CORS_ORIGINS | (нет) | Разрешённые браузерные источники через запятую |
API_SERVER_MODEL_NAME | (имя профиля) | Имя модели в /v1/models. По умолчанию имя профиля или hermes-agent для профиля по умолчанию. |
config.yaml
Те же настройки могут жить в ~/.hermes/config.yaml во вложенной секции gateway.api_server::
gateway:
api_server:
enabled: true
port: 8642
host: 127.0.0.1
key: your-secret-key
cors_origins: http://localhost:3000
model_name: my-hermes
max_concurrent_runs: 10 # concurrent-run cap; 0 disables the limit
history_tool_output_max_chars: 0 # cap tool outputs in stored /v1/responses history; 0 = verbatim
port, key, host, cors_origins и model_name автоматически прокидываются в настройки extra платформы, поэтому ведут себя ровно как их аналоги в переменных окружения API_SERVER_*. Переменные окружения приоритетнее значений из config.yaml. Блок принимается и под gateway.platforms.api_server:, и в секции верхнего уровня platforms.api_server:.
Предел параллельных прогонов
API-сервер ограничивает, сколько прогонов агента выполняется одновременно — суммарно на эндпоинтах, которые запускают прогон напрямую: OpenAI-совместимых, Runs и чата сессий (POST /api/sessions/{id}/chat и его вариант /stream, по которым ходят личные сообщения агентов между машинами). Прогоны, запущенные cron (POST /api/jobs/{id}/run, POST /api/cron/fire), идут через планировщик cron и подчиняются его собственным лимитам, а не этому пределу. Предел читается из gateway.api_server.max_concurrent_runs (по умолчанию 10; 0 снимает ограничение, отрицательные значения приводятся к 0). Когда предел выбран, новые запросы на запуск отклоняются с HTTP 429 и текстом Too many concurrent runs (max N) — клиенту следует отступить и повторить позже.
Размер сохранённой истории для цепочек previous_response_id
Каждый сохранённый снимок /v1/responses содержит всю накопленную историю разговора (именно её воспроизводят цепочки previous_response_id и conversation), включая дословный вывод каждого инструмента. Поэтому разговор с несколькими большими выводами инструментов может сделать одну запись в response_store.db размером в несколько сотен КБ. Задайте gateway.api_server.history_tool_output_max_chars (по умолчанию 0 = хранить дословно), чтобы ограничить каждый вывод инструмента и каждый строковый аргумент вызова инструмента в сохранённой истории этим числом символов; всё, что длиннее, обрезается до начала с маркером ...[N more chars]. Текст пользователя и ассистента не трогается никогда, а полезная нагрузка response.completed и инкрементальные SSE-события не затрагиваются. Поскольку сохранённая история — это то, что модель видит на следующем ходе цепочки, включение ограничения урезает и то, что модели воспроизводится заново, — оставьте 0, если вашему процессу нужны полные выводы инструментов между ходами.
Заголовки безопасности
Все ответы содержат заголовки безопасности:
X-Content-Type-Options: nosniff— отключает угадывание MIME-типаReferrer-Policy: no-referrer— предотвращает утечку реферера
CORS
API-сервер по умолчанию не включает браузерный CORS.
Для прямого доступа из браузера задайте явный список разрешений:
API_SERVER_CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000
Когда CORS включён:
- Предварительные (preflight) ответы содержат
Access-Control-Max-Age: 600(кэш на 10 минут) - SSE-потоки содержат CORS-заголовки, чтобы браузерные клиенты EventSource работали корректно
X-Hermes-Session-Idвходит в разрешённые заголовки запроса, поэтому браузеры с разрешённого origin могут запрашивать продолжение сессииIdempotency-Keyвходит в разрешённые заголовки запроса — клиенты могут отправлять его для дедупликации (ответы кэшируются по ключу на 5 минут)
Большинству описанных фронтендов, таких как Open WebUI, CORS не нужен вовсе: они подключаются по схеме сервер-сервер.
Совместимые фронтенды
Подходит любой фронтенд с поддержкой формата OpenAI API. Проверенные и описанные интеграции:
| Фронтенд | Звёзды | Подключение |
|---|---|---|
| Open WebUI | 126k | Есть полное руководство |
| LobeChat | 73k | Эндпоинт кастомного провайдера |
| LibreChat | 34k | Кастомный эндпоинт в librechat.yaml |
| AnythingLLM | 56k | Универсальный провайдер OpenAI |
| NextChat | 87k | Переменная окружения BASE_URL |
| ChatBox | 39k | Настройка API Host |
| Jan | 26k | Конфигурация удалённой модели |
| HF Chat-UI | 8k | OPENAI_BASE_URL |
| big-AGI | 7k | Кастомный эндпоинт |
| OpenAI Python SDK | — | OpenAI(base_url="http://localhost:8642/v1") |
| curl | — | Прямые HTTP-запросы |
Многопользовательская настройка с профилями
Чтобы выдать нескольким пользователям по своему изолированному экземпляру Hermes (отдельные конфиг, память, навыки), используйте профили:
# Создаём по профилю на пользователя
hermes profile create alice
hermes profile create bob
# Настраиваем API-сервер каждого профиля на отдельном порту. API_SERVER_* — это
# переменные окружения (не ключи config.yaml), поэтому пишем их в .env каждого профиля:
cat >> ~/.hermes/profiles/alice/.env <<EOF
API_SERVER_ENABLED=true
API_SERVER_PORT=8643
API_SERVER_KEY=alice-secret
EOF
cat >> ~/.hermes/profiles/bob/.env <<EOF
API_SERVER_ENABLED=true
API_SERVER_PORT=8644
API_SERVER_KEY=bob-secret
EOF
# Запускаем шлюз каждого профиля
hermes -p alice gateway &
hermes -p bob gateway &
API-сервер каждого профиля автоматически анонсирует имя профиля как идентификатор модели:
http://localhost:8643/v1/models→ модельalicehttp://localhost:8644/v1/models→ модельbob
В Open WebUI добавьте каждый как отдельное подключение. В выпадающем списке моделей появятся alice и bob как разные модели, за каждой стоит полностью изолированный экземпляр Hermes. Подробности — в руководстве по Open WebUI.
Ограничения
- Хранение ответов — сохранённые ответы (для
previous_response_id) лежат в SQLite и переживают перезапуски шлюза. Максимум 100 сохранённых ответов (вытеснение по LRU). - Нет загрузки файлов — изображения во входных данных поддерживаются и в
/v1/chat/completions, и в/v1/responses, но загруженные файлы (file,input_file,file_id) и неизображённые документы через API не поддерживаются. - Простые OpenAI-клиенты по-прежнему видят псевдоним —
/v1/modelsанонсирует стабильный псевдоним Hermes (hermes-agentили имя активного профиля). Более продвинутые клиенты могут слать в запросах явныеproviderиmodel_options.
Режим прокси
API-сервер служит и бэкендом для режима прокси шлюза. Когда другой экземпляр шлюза Hermes настроен с GATEWAY_PROXY_URL, указывающим на этот API-сервер, он пересылает все сообщения сюда вместо запуска собственного агента. Так получаются раздельные развёртывания — например, Docker-контейнер обрабатывает Matrix E2EE и ретранслирует запросы агенту на хосте.
Полное руководство по настройке — в разделе Matrix Proxy Mode.