API サーバー
目次
- まずは動かす
- 1. API サーバーを有効にする
- 2. ゲートウェイを起動する
- 3. フロントエンドをつなぐ
- 会話と最終結果の扱い
- 冪等キー
- エンドポイント
- POST /v1/chat/completions
- POST /v1/responses
- GET /v1/responses/\{id\}
- DELETE /v1/responses/\{id\}
- GET /v1/models
- GET /api/model/options
- GET /v1/capabilities
- ブラウザ拡張からの操作
- 手元の API での登録
- 操作役とやり取りする枠
- リクエストごとのモデルの指定
- GET /health
- GET /health/detailed
- Runs の API(逐次送りと相性のよい別の道)
- POST /v1/runs
- GET /v1/runs/\{run_id\}
- GET /v1/runs/\{run_id\}/events
- POST /v1/runs/\{run_id\}/resolve-unknown
- POST /v1/runs/\{run_id\}/stop
- POST /v1/runs/\{run_id\}/approval
- POST /v1/runs/\{run_id\}/clarify
- Jobs の API(裏で動く予定の仕事)
- GET /api/jobs
- POST /api/jobs
- 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 でのセッションの操作)
- スキルと道具立ての一覧
- 長期の記憶の切り分け(X-Hermes-Session-Key)
- システムプロンプトの扱い
- 認証
- プロファイルごとの振り分け(/p/
/…) - 設定
- 環境変数
- config.yaml
- 同時に走る実行の上限
- previous_response_id でつなぐときの履歴の大きさ
- 安全のためのヘッダー
- CORS
- つながるフロントエンド
- プロファイルで複数の利用者に対応する
- 制限
- 中継の使い方
API サーバーは、hermes-agent を OpenAI 互換の HTTP のエンドポイントとして公開します。OpenAI の形式を話せるフロントエンドなら何でも、つまり Open WebUI、LobeChat、LibreChat、NextChat、ChatBox をはじめ何百というものが、hermes-agent につないでこれを裏側として使えます。
エージェントは、持っている道具ひとそろい(ターミナル、ファイル操作、ウェブ検索、記憶、スキル)を使ってリクエストに応え、最終的な返答を返します。逐次送りのときは、ツールの進み具合を示すしるしが文中に現れるので、フロントエンド側はエージェントがいま何をしているかを見せられます。
まずは動かす
1. API サーバーを有効にする
~/.hermes/.env に次を足します。
API_SERVER_ENABLED=true
API_SERVER_KEY=change-me-local-dev
# Optional: only if a browser must call Hermes directly
# API_SERVER_CORS_ORIGINS=http://localhost:30002. ゲートウェイを起動する
hermes gatewayこう表示されます。
[API Server] API server listening on http://127.0.0.1:86423. フロントエンドをつなぐ
OpenAI 互換のクライアントを http://localhost:8642/v1 に向けます。
# Test with 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 との連携ガイド を見てください。
会話と最終結果の扱い
API へのリクエストは、ゲートウェイの永続的な受け付けキューを通ります。構造を持った画像の内容は、実行の最後まで構造を保ったまま扱われます。 ゲートウェイのスラッシュコマンドとして解釈されることはありません。
/v1/runsと/v1/responsesは、
X-Hermes-Session-Key で宣言された会話を保ちます。セッションや返答の連鎖を明示した場合はそちらが優先され、 その会話の識別が別のヘッダーへ移ることはありません。
/v1/runsのconversation_historyが空でなければ、明示した
session_id があってもそちらが正になります。それ以外の場合、セッションの続きは、ターンを実行する時点で、 先に並んでいた作業が終わってから履歴を読み込みます。
- run のポーリングと SSE は、
cancelledかfailedかで食い違いません。Responses も同じ
最終ステータスと response.cancelled / response.failed イベントを使い、診断用の文が 含まれていても変わりません。Chat Completions は、中断されたターンには finish_reason: "cancelled"、 失敗には "error" を返し、成功を示す "stop" は返しません。
- セッションチャットの返答には
statusが付き、そのストリームも対応する run の最終イベントを
伝えます。アシスタントの締めくくりのフレームが来たこと自体は、実行が成功した証拠にはなりません。
冪等キー
POST /v1/chat/completions と POST /v1/responses は Idempotency-Key ヘッダーを受け付けます。 キーで何が保証されるかは、実行モードによって変わります。
- 正準モード(ゲートウェイがセッションの管理役を動かしている場合): キーは、受け付けを恒久的に見分ける印になります。
そのキーを持つ最初のリクエストが受け付けられて一度だけ実行され、以後そのキーはその受け付けのものになります。 キーは認証済みの API のプロファイルと資格情報ごとに分けられるので、別のプロファイルや、入れ替えたあとの API_SERVER_KEY で同じクライアント側のキーを使えば、新しいリクエストとして扱われます。この節の残りは、このモードの説明です。
- 従来モード(セッションの管理役がない場合): ストリーミングでないリクエストは、メモリ上の 5 分間の
キャッシュで重複が除かれます。ストリーミングのリクエストは重複が除かれません。
/v1/runs のキーは専用の台帳を持ち、24 時間保たれます(下の Runs の API を参照)。
まったく同じやり直しと衝突。 やり直しが「まったく同じ」と見なされるのは、同じキーを、同じ識別項目と 同じ宛先の指定で繰り返したときです。識別項目を変えたり、キーを別のセッションへ送ったりすると衝突になり、 admission_conflict で断られて何も実行されません。断りがどう届くかは、リクエストによって違います。
- Responses(JSON でもストリーミングでも)とストリーミングでない Chat Completions: HTTP 409
とエラーコード admission_conflict が返ります。
- ストリーミングの Chat Completions: ストリームはすでに HTTP 200 で開いているので、
断りは最後のチャンクとして届きます。そこには finish_reason: "error" と、error.message の "admission_conflict" が入り、そのあとに data: [DONE] が続きます。ステータスではなく、このチャンクを確かめてください。
識別項目はエンドポイントによって違います。
- Responses:
input、instructions、previous_response_id、conversation、
conversation_history、model、provider、model_options、tools。送った場合は truncation も含みます。
- Chat Completions: 最後のユーザーメッセージ、それより前のメッセージ(
X-Hermes-Session-Id を送った場合は、履歴はセッションから取られるので除きます)、 システムプロンプト、そして要求したモデル、プロバイダー、model_options といったターンの設定です。 tools と tool_choice は含まれません。これらだけを変えたやり直しは、衝突にならずにもとの答えが再生されます。
受け取り方はどちらのエンドポイントでも識別項目に入りません。やり直しで JSON と stream: true を 切り替えてもかまわず、同じ答えが求めた形で返ります。
再生と重複の防止は別のものです。 まったく同じやり直しでもとの答えが再生されるのは、サーバーがまだ そのキーのリクエストの識別情報と、再生用の記録、または答えを組み立て直せる最終状態の受け付けを持っている間だけです。 再生ができなくなっても、受け付けの証拠が残っている間は、そのキーが再び実行されることはありません。 やり直しは衝突として断られ、何も実行されません。Responses のやり直しでは、代わりに HTTP 409 unknown_execution が返ることがあります。これは、保存先がそのキーの予約(たとえば古いリリースが 記録したもの)は持っているのに答えを持っていないので、最初の試みが実行されたかどうかが 分からない、という意味です。新しいキーで仕事を送り直してよいという意味ではありません。最初の試みは すでに実行されているかもしれません。
セッションを削除すると(DELETE /api/sessions/{id})、その受け付けは削除済みの印として残ります。 削除から 7 日たつと整理の対象になりますが、実際に消えるのは整理の処理が走ったときだけで、それまでキーは次のようにふるまいます。
- Chat Completions: まったく同じやり直しは、セッションを作り直さずにもとの答えを再生します。
そのキーをほかのセッションへ送ると、衝突として断られます。 削除済みの印が整理されたあとは、そのキーで有効な新しい宛先に新しい仕事を頼めます。
- Responses: 再生には、レスポンスの保存先がそのキーの識別情報を持っていることも必要なので、
7 日より前に再生できなくなることがあります。確定した識別情報は上限のある数だけ保たれ、 DELETE /v1/responses/{id} を呼ぶとすぐにひとつ消えます。識別情報がなくなったキーは、 生きているセッションに対しても 409 admission_conflict を返します。識別情報がなくなり、削除済みの印も 整理されたあとは、そのキーで新しい仕事を頼めます。
明示した X-Hermes-Session-Id が別のものに付け替えられることはありません。削除したセッションの id は、 整理されたあとも使われないままです。そこに識別情報が残っているキー付きのまったく同じやり直しは 再生され、内容を変えたものは衝突として断られます。そこに識別情報が残っていないリクエスト、 たとえば新しいリクエストや、識別情報が整理されたあとのやり直しは、 not_found で断られます。
X-Hermes-Session-Id のない Chat Completions のリクエストは、システムプロンプトと最初のユーザーメッセージから セッションを決めます。削除したセッションの id は二度と使われないので、削除した会話と同じ始まり方の新しい会話には 新しいセッションが割り当てられ、そのあとのターンも、再起動のあとを含めて同じそのセッションに届きます。削除した会話の ターンをキー付きでまったく同じにやり直した場合は移されず、上に書いたとおり再生されます。
エンドポイント
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 の部品の配列として送れます。遠隔の http(s) の URL も、data:image/... の URL も使えます。
{
"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)と、画像以外の data: の URL は 400 unsupported_content_type を返します。
逐次送り("stream": true): Server-Sent Events(SSE)で、返答をトークンごとの塊にして返します。Chat Completions では標準の chat.completion.chunk のイベントに加えて、ツールの開始を見せるための Hermes 独自の hermes.tool.progress イベントを使います。Responses では、response.created、response.output_text.delta、response.output_item.added、response.output_item.done、response.completed といった OpenAI Responses のイベント型を使います。
どの SSE の流れ(Chat Completions、Responses、/api/sessions/{id}/chat/stream、/v1/runs/{id}/events)でも、10 秒間イベントが送られなかったときは : keepalive というコメント行を出します。これで、時間のかかるツール呼び出しの途中でクライアント側の無通信タイムアウトが働くことはありません。標準的な SSE クライアントはコメント行を無視します。自前でパースする場合は、: で始まる行を読み飛ばしてください。
逐次送りでのツールの進み具合:
- Chat Completions: Hermes は
event: hermes.tool.progressを出します。保存されるアシスタントの文章を汚さずに、ツールの開始を見せるためです。名前付きの SSE イベントを受け付けない厳密な OpenAI クライアントでは、gateway.platforms.api_server.tool_progress_events: false(既定はtrue)でこのフレームを止められます。本文の塊には影響しません。この止め方が効くのは Chat Completions だけです。/v1/runs/{id}/eventsは常にツールのイベントを出します。/v1/capabilitiesのtool_progress_events機能が示しているのはこちらです。 - Responses: Hermes は SSE の流れのなかで、仕様どおりの
function_callとfunction_call_outputという出力項目を出します。クライアント側はツールの様子を構造化した画面として、その場で描けます。
モデルの思考(モデルが実際に思考を出し、適用される reasoning の設定がそれを許しているときにだけ出ます。入力の側で止めるには model_options.reasoning.enabled: false を指定します):
- Chat Completions: 思考の差分は
choices[0].delta.reasoning_contentの塊として届きます(Open WebUI、opencode、Vercel AI SDK が思考の欄として表示する DeepSeek 風の項目です)。答えの文章は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の項目は入力では無視され、中身のない利用者の発言として読まれることはありません。 - この対応は
GET /v1/capabilitiesでfeatures.reasoning_streaming: trueとして示されます。
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" で再生されるだけで、クライアントがこれから実行すべき保留中の呼び出しではありません。
"stream": true のときは、往復の途中でアシスタントが差しはさむ補足(openai-codex のバックエンドが出す phase="commentary" の前置きや、モデルがツールの呼び出しと並べて書く文章)が、それ自体で完結した 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 も data:image/... の URL も使えます。
{
"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)と、画像以外の data: の URL は 400 unsupported_content_type を返します。
previous_response_id で何往復もする
レスポンスを数珠つなぎにすれば、ツールの呼び出しも含めた文脈を往復のあいだ保てます。
{
"input": "Now show me the README",
"previous_response_id": "resp_abc123"
}サーバーは、保存されたレスポンスのつながりから会話の全体を組み直します。それまでのツールの呼び出しと結果はすべて残ります。つながったリクエストは同じセッションも共有するので、何往復もの会話がダッシュボードやセッションの履歴では 1 件として見えます。
各レスポンスの output に並ぶのは、その往復の項目だけです(その回の function_call と function_call_output、それに最後の message)。前の往復のツールの呼び出しが混ざることはありません。呼び出しの前に Hermes が渡された履歴を直した場合(続けて並んだ assistant や user の項目をまとめたり、相手のいないツールの結果を落としたり)や、つながりの途中で履歴を圧縮した場合も同じです。保存されるのは直したあとの記録なので、往復のたびに履歴がもう 1 つぶん増えることはありません。
名前を付けた会話
レスポンスの ID を追いかける代わりに、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\}
保存済みのレスポンスを ID で取り出します。
DELETE /v1/responses/\{id\}
保存済みのレスポンスを消します。
GET /v1/models
エージェントを、使えるモデルとして一覧に出します。表に出るモデル名は、既定ではプロファイルの名前です(既定のプロファイルなら hermes-agent)。たいていのフロントエンドが、モデルを見つけるためにこれを必要とします。
/v1/models は、あえて軽い OpenAI 互換の窓口にしてあります。Hermes が振り分けられる提供元とモデルの組み合わせを すべて並べたりはしませんし、 価格や機能の情報を足したりもしません。
GET /api/model/options
Hermes を分かっているクライアントなら、ダッシュボードや TUI が使っているのと同じ、 選り分けられた提供元とモデルの一覧を求められます。この経路は API サーバーのふつうの bearer 認証を使い、OpenAI 互換の /v1/models の応答には入れるべきでない情報、つまり提供元の行、 モデルの機能の手がかり、価格の情報を返します。
curl \
-H "Authorization: Bearer $API_SERVER_KEY" \
"http://127.0.0.1:8642/api/model/options"この中身は、ダッシュボードの Models のページや TUI の model.options の RPC が使っているのと同じ土台です。認証済みの提供元、選り分けられたモデルの 一覧、モデルごとの価格、モデルの機能の手がかりが返ります。
ふつうに開いたときは、自作の提供元に対してあえて控えめに動きます。Hermes が確かめに行くのは いま選ばれている自作のエンドポイントだけで、古くなった、あるいは止まっている保存済みの エンドポイントが選択画面を止めてしまわないようにするためです。はっきり更新を求めると、 すべてを確かめに行き、提供元のモデルのキャッシュも捨てます。
curl \
-H "Authorization: Bearer $API_SERVER_KEY" \
"http://127.0.0.1:8642/api/model/options?refresh=1"OpenAI 互換のクライアントが、chat や responses のリクエストで送り返すモデル名だけを必要と しているなら /v1/models を使ってください。認証済みの画面が、Hermes ならではの もっと詳しい選択用の情報を必要とするなら /api/model/options です。
GET /v1/capabilities
API サーバーの安定した窓口を、外部の画面や指揮役、プラグインの橋渡しが機械的に読める形で説明します。
{
"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
}
}ダッシュボードやブラウザの画面、管理の仕組みをつなぐときは、この経路を使ってください。動いている Hermes の版が実行・逐次送り・取り消し・セッションの継続に対応しているかを、Python の内部に踏み込まずに調べられます。
ブラウザ拡張からの操作
Hermes は、いまの Hermes のセッションに結びついたブラウザのセッションを操る、認証済みの拡張へ ブラウザ関連のツールを振り分けられます。この機能は既定では無効です。使うには browser.extension_control.enabled を true にしてください。
browser:
extension_control:
enabled: true手元の API の経路にも、API サーバーの bearer キーが要ります。操作役として登録できるのは、 すでにあるサーバーのセッションに対してだけです。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、好きなスクリプトの実行、コンソールへの アクセス、アップロード、画像の抽出、視覚の機能は、この操作のやり取りには含まれません。
リクエストに結びついた操作役がいないとき、あるいは機能が無効なときは、Hermes はこれまでどおりの ブラウザの裏側を使い続けます。ゲートウェイが操作役の主体と通信路の系統をリクエストに結びつけたら、 その拡張の道筋が正となります。操作役がいない、どれか決まらない、切れている、力不足、といった場合は 黙って別の手元やクラウドのブラウザに乗り換えたりせず、閉じる側に倒れて失敗します。 どの操作役かがはっきり決まったあとは、その結果もエラーも正であり、 Hermes が同じ操作を別の裏側でやり直すことはありません。
手元の API での登録
protocol_version、session_id、controller_id、browser_profile_id、そして求める
capabilities を添えて、認証済みの POST /v1/browser-control/register を送ります。
- Hermes は 30 秒で切れる一度きりの引換券と、絞り込まれてサーバー側に結びつけられた操作の範囲を返します。
hermes-browser-control-v1と
hermes-browser-control-ticket.<ticket> の 2 つの WebSocket の副プロトコルを添えて GET /v1/browser-control/ws を開きます。
引換券をクエリ文字列で受け付けることはありません。知らない、期限切れ、使い回し、形が おかしい引換券は、WebSocket に切り替わる前にはじかれます。
操作役とやり取りする枠
Hermes は browser.controller.command という枠を送ります。中身は command_id、 action、書き換えられない arguments、ブラウザと操作役の ID、そしてもとになった tool_call_id です。操作役は browser.controller.result で、同じ command_id、 きっちり真偽値の ok、そして result か error のどちらかを返します。 取り消しや時間切れでは browser.controller.cancel が出て、遅れて届いた結果は捨てられます。
思わぬ接続の切断があると操作役は切断中の扱いになり、すでに走っている仕事はそれぞれの もとの期限まで保たれます。同じ主体、プロファイル、セッション、操作役の ID、ブラウザの プロファイル、通信路の身元でつなぎ直せば、保留になっていた取り消しが片づくまで新しい仕事を 入れないまま、通信路だけが入れ替わります。つなぎ直しのときに取り決めた機能が変わることは あります。機能は身元を決める項目ではないからです。同じ認証済みセッションの道筋で操作役の ID や ブラウザのプロファイルが違えば、それは丸ごとの入れ替えです。後継が使えるようになる前に、 古い保留中の仕事は取り消されます。意図してきっぱり切り離したいときは、認証済みの操作役の通信路で browser.controller.detach を送ってください。保留中の仕事がその場で取り消されます。 ただ接続を閉じただけなら、復帰できる切断として扱われます。
認証済みのダッシュボードの通信路も、登録・結果・生存確認・機能・持ち主の扱いを、 自分の Gateway の RPC とイベントの経路で同じように備えています。どちらの通信路でも、 主体、プロファイル、セッション、操作役、ブラウザのプロファイル、通信路の系統、機能のすべてが 一意にぴたりと一致してはじめて選ばれます。いったん選ばれたら、操作役の失敗は正であり、 別のブラウザの裏側でやり直されることはありません。
リクエストごとのモデルの指定
認証済みのクライアントは、次を送ることで Hermes の既定のモデルの選び方を リクエストごとに上書きできます。
model— この往復で使いたいモデルの IDprovider— この往復で資格情報と実行環境を決めるための、Hermes の提供元の識別名model_options— このリクエストの範囲でだけ効く、推論の深さやサービスの等級の指定
同じリクエストの項目は、次で受け付けられます。
POST /v1/chat/completionsPOST /v1/responsesPOST /v1/runsPOST /api/sessions/{session_id}/chatPOST /api/sessions/{session_id}/chat/stream
優先の順番は決まっています。
- セッションの
/modelによる上書き(そのセッションにすでにあれば) - リクエストの
modelが設定済みの経路の別名に当たるときに選ばれる、
固定の gateway.platforms.api_server.model_routes の対応づけ
- 経路の別名に当たらないときの、リクエストの
model/providerそのもの - ゲートウェイ全体の設定や、環境変数の既定値
どのモデルや提供元が選ばれても、model_options はそのリクエストの範囲にとどまります。 設定済みの model_routes の別名と食い違う provider を送ってきたリクエストは、 経路の資格情報を黙って別の提供元と混ぜたりせず、400 ではじかれます。
OpenAI 互換のエンドポイントで model だけを送る場合は、こちらで許可が要ります。 ふつうの OpenAI のクライアントはモデル名(gpt-4o など)を決め打ちしがちで、いまある構成の多くは それがゲートウェイの既定に落ちることを当てにしています。そのため POST /v1/chat/completions と POST /v1/responses では、provider を伴わずに送られた model の値は、次を有効にしない限り無視されます。
gateway:
platforms:
api_server:
direct_model_requests: trueprovider をはっきり書いたリクエストと、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"} を返します。/v1/ の接頭辞を前提とする OpenAI 互換のクライアントのために、GET /v1/health でも使えます。
GET /health/detailed
監視や管理の仕組みのための、認証済みの準備状況の確認です。いま使っているプロファイルの設定、 状態のデータベース、設定されたモデル、ディスクの空き、ゲートウェイとプラットフォームの状態、 動いている API の実行、待ちになっているプロセスの完了、動いている委任について、限られた形で 状態を報告します。返るのは状態と件数だけで、設定の値、資格情報、パス、コマンド、待ち行列の 中身、生のエラーは出しません。
公開されている /health の経路は軽い生存確認のままで、準備状況の確認は走らせません。 準備状況が芳しくない結果でも HTTP は 200 です。いちばん上の status と readiness.checks の項目を見てください。
Runs の API(逐次送りと相性のよい別の道)
/v1/chat/completions と /v1/responses に加えて、長い会話のための runs の API があります。クライアント側で逐次送りを自分でさばくのではなく、進み具合のイベントを購読したいときのためのものです。
POST /v1/runs
エージェントの実行を新しく作ります。進み具合のイベントを購読するのに使える run_id が返ります。
{
"run_id": "run_abc123",
"status": "started"
}実行は、単純な input の文字列と、任意の session_id、instructions、conversation_history、previous_response_id を受け付けます。session_id を渡すと、Hermes がそれを実行の状態に出すので、外部の画面が自前の会話の ID と突き合わせられます。
安全にやり直せる形で作りたいときは、Idempotency-Key のヘッダー(見える ASCII 文字で 1〜255 文字)を送ってください。Hermes は仕事を始める前に、この鍵を確実に押さえます。まったく同じ内容でやり直せば、もとの run_id が HTTP 202 と Idempotency-Replayed: true とともに返ります。これはゲートウェイを再起動したあとでも、実行が完了・失敗・取り消しになったあとでも同じです。同じ鍵を違う JSON の中身で使い回すと、HTTP 409 とコード idempotency_key_conflict が返ります。鍵は認証済みの API のプロファイルや資格情報ごとに分けられ、最後に状態が変わってから 24 時間保たれます。クライアント側は他人に推し量れない固有の鍵を使い、関係のない操作で使い回さないでください。このヘッダーがないリクエストは従来どおりの動きで、必ず新しい実行を作ります。
ゲートウェイが受け付けられなかった実行は鍵を押さえたまま残らないので、まったく同じ内容でやり直しても安全です。プロファイルが 後片付けや引退の最中のとき、または状態データベースのロックが書き込みの時間切れを過ぎても解けないときは、POST /v1/runs と実行の操作は HTTP 503(runtime_draining または storage_unavailable)を Retry-After 付きで返します。 操作の本文の形が崩れていれば 400 invalid_params を返します。持ち主・世代・識別を理由に断る場合は、 これまでどおり 409 です。
session_id がすでにある Hermes のセッションを指していて、conversation_history も previous_response_id も渡されていないときは、そのセッションのいま使われている記録を 読み込みます。セッションの順番待ちの仕組みが、同時に書こうとするものを一列に並べ、 待たされたあとには記録を読み直します。
API のセッションは API 専用のセッションです。 ここでの session_id と、 X-Hermes-Session-Id のヘッダー(/v1/chat/completions と /v1/responses で使うもの)が 指せるのは、API サーバーが作ったセッションか、これから作るセッションだけです。CLI、TUI、Desktop、ACP、 メッセージングの会話といった、ほかの入り口が持っているセッションには結び付けられません。そうしたリクエストは、 受け付けの前に HTTP 409 とエラーコード permission_denied で断られます。実行は作られず、 Idempotency-Key の予約も残らず、まったく同じ内容でやり直しても再び 409 が返ります。これは、 API サーバーが他人の記録の上で、生きている持ち主と並行して自前のエージェントを走らせていた以前の動きに代わるものです。 ほかの入り口の会話で作業したいときは、OpenAI 互換の経路ではなく、ゲートウェイの WebSocket (共有セッションへの prompt.submit)からつないでください。
GET /v1/runs/\{run_id\}
いまの実行の状態を問い合わせます。SSE の接続をつなぎっぱなしにせずに状態だけ知りたいダッシュボードや、画面の移動のあとでつなぎ直す画面に向いています。
{
"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)になったあとも、問い合わせと画面の突き合わせのために状態はしばらく残ります。実行中にゲートウェイが終了した場合、その実行はエージェントに停止を求めるより先に interrupted(エラーは Gateway shutdown interrupted the run.、終了時のできごとは run.interrupted)として書き残されます。そのため、永続的な実行が再起動をまたいで running のまま残ることはなく、中断されたターンから遅れて届いた結果がこれを上書きすることもありません。ただし、正規の実行で、ターンの途中に持ち主が再起動したものは unknown を返します。これは終わりの状態ではありません。そのセッションの後続のターンは、POST /v1/runs/{run_id}/resolve-unknown(後述)で確認が取れるまで待ちます。
ゲートウェイがまだ後片付けの最中のとき(ターンが動いている状態での hermes gateway stop / restart、または SIGTERM)は、終わっていない実行のすべてに、新しいターンの受け付けをやめた時点から shutdown_requested_at(Unix 秒)が付きます。ターンはまだ処理されているので status は running のままですが、この項目が見えた側は、プロセスが終わりに向かっていて、遅くとも後片付けに使える時間が尽きた時点でその実行が interrupted で終わることが分かります。終わりの状態になった実行に、この項目が後から付くことはありません。
GET /v1/runs/\{run_id\}/events
その実行のツールの進み具合、トークンの差分、節目のできごとを Server-Sent Events で流します。状態を失わずにつないだり離れたりしたい、ダッシュボードや作り込んだクライアントのためのものです。
往復の途中でアシスタントが差しはさむ補足(openai-codex のバックエンドが出す phase="commentary" の 前置きや、モデルがツールの呼び出しと並べて書く文章)は、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 が 0 以外だった、構造化された {"error": ...} の結果が返った、承認が拒まれた、といった場合で、結果が JSON 文字列で届いても、 解析済みのオブジェクトで届いても同じように判定します。完了時の preview は結果の本文(構造化された結果は JSON に変換したもの)で、秘密情報の伏せ字処理を必ず通したうえで 500 文字に切り詰めます。そのため クライアントは、上限のないツールの中身を受け取らなくても、承認の拒否(BLOCKED: ...)と通常の失敗を見分けられます。
エージェントが裏で動く子エージェントに仕事を任せたときは、 subagent.start と subagent.complete という節目のできごとも流れます。だからクライアント側は、 子が働いているあいだ実行が黙り込むのではなく、任せた結果を(時間切れや失敗も含めて) 見られます。subagent.complete の中身には、子の状態、要約、かかった時間、トークンと費用の 数字、突き合わせ用の child_session_id、そして所属するひとまとまりの delegation_id (同時や入れ子の分岐でも区別が付くように)が入ります。自由に書ける項目は、プロセスの外に出る前に 必ず秘密を伏せる処理を通ります。子のツールごとのできごと (subagent.tool や進み具合の刻み)は、あえて流しません。画面には量が多すぎるからです。 細かい実況は、子ごとの実況の記録ファイルを見てください。これらのできごとは、親のストリームが 開いているあいだだけ流れます。離れて完了したものが遅れて届いても、終わった実行の SSE ストリームを 再び開いたり、その終了状態を変えたりはしません。
切り離された結果とセッションの履歴
裏で動く委任は、サーバー側のセッションの履歴を読み込む続きの往復があってはじめて働きます。つまり Chat Completions でのはっきりした X-Hermes-Session-Id、Hermes 本来の /api/sessions/{id}/chat のリクエスト、あるいはセッションの履歴を使う Runs のリクエストのいずれかです。ヘッダーのない Chat Completions、Responses の数珠つなぎ、そして previous_response_id や呼び出し側が渡した 履歴を使う Runs のリクエストは、代わりに委任を同期で実行し、もとの往復のなかで結果を返します。 リクエストの内容からセッション ID をただ導き出しただけでは、切り離した配達は働きません。
再開できるリクエストでは、委任のまとまりごとに完了が 1 回だけ保存されます。これは GET /api/sessions/{id}/messages から取り出せますし、次に実際にクライアントが往復するときの セッションの履歴にも入ります。やり直しても同じ結果が二重に入ることはなく、途中の失敗を知らせる ものは別の身元を持ちます。配達は、クライアントの往復がセッションの占有権を持っているあいだは 待たされ、圧縮による続きにも従います。Chat Completions は、あなたが渡したはっきりしたセッション ID を JSON でも逐次送りでもそのまま返すので、圧縮のあともその ID を送り続けてください。
完了が求められていないモデルの往復を勝手に始めることはなく、保留中の人による確認を素通りする こともありません。次の往復はクライアント側のものです。自分で持っている履歴のスナップショットを 使い続けるクライアントは、サーバー側の配達の記録がそのスナップショットに混ざることを期待するので はなく、同期の委任を使ってください。
読まれなかったイベントの控えは 5 分で捨てられます。離れていったクライアントのせいで メモリが際限なく増えないようにするためです。これで消えるのは通信の状態だけです。まだ走っている 実行は、実際にその仕事が終わるまで、状態の問い合わせ・承認・停止の操作・同時実行の 数え上げから見え続けます。つながっている SSE の購読者は、これまでどおり受け取り続けます。
POST /v1/runs/\{run_id\}/resolve-unknown
権限を持つ持ち主が再起動したあと、すでに引き受けられていたターンは実行の結果が分からなくなり、 GET /v1/runs/{run_id} はそれを "status": "unknown"(終わりの状態ではありません)と返します。Hermes は、失われた先頭を繰り返し実行する危険を冒さず、 そのセッションの後続の作業を止めておきます。その本文から admission_id と execution_generation を正確に読み取り、失われた実行が終わらないことを 確認として送ってください。
{
"admission_id": "adm_abc123",
"execution_generation": 7
}このリクエストは、ほかの実行の操作と同じく認証され、実行の持ち主の範囲に限られます。 ホスト型ルームから呼ぶ場合は、既存の stop の許可が要ります。成功した応答は、 正規の最終受領(outcome: "interrupted")に run_id を加えたものです。これにより FIFO が一度だけ解放され、結果の分からない先頭を繰り返すことなく、後ろに並んでいた作業の実行が予定されます。 すでに確定した解決とまったく同じものを繰り返すと、同じ受領(200)が返るので、応答が失われたあとのやり直しも安全です。 世代が古いか整数でない場合、またはいま結果不明の状態ではない実行の場合は、409 stale_generation が返ります。 経路の実行に属さない受け付け ID には 409 not_found が返ります。 本文はこの 2 つの項目だけをちょうど含んでいる必要があり、そうでなければサーバーは 400 invalid_params を返します。
ホスト型ルームの本拠は、メンバーの unknown の実行を次の問い合わせのときにこの方法で解決し、 ルームの作業を中断済み("member gateway restarted")として落ち着かせます。古いリリースのルームの本拠は unknown の状態を知らないので、更新されるまでは食い違った実行の状態を伝えます。
この回復の操作は、正規のセッション権限が有効なときにだけ、features.run_unknown_resolution として、 また run_unknown_resolution のエンドポイントとして公開されます。従来の API 実行モードでは公開されません。通常の POST /v1/runs/{run_id}/stop はあえて別のままで、結果の分からない受け付けに対しては 409 unknown_execution を返します。
この版で作られた受け付けでは、外からは中身の見えない実行の持ち主の範囲が、正規の受け付けと一体で 保存され、終わりの状態も含めて、その受け付けの操作と状態の寿命のあいだ保たれます。これはサーバー内部の状態であり、 bearer の資格情報でも、モデルへの入力でも、API の応答の項目でもありません。鍵のないリクエストは これまでどおり冪等ではないので、本文が同じでも別々の実行が作られます。 以前の鍵のない受け付けには、保存された持ち主の範囲がないため、アダプターの再起動のあとも安全側に倒れて失敗し続けます。 後から安全に補うことはできません。以前の鍵付きの 実行は、既存の冪等性の台帳を使い続けます。セッションを明示的に引退させると、 正規の受け付けの中身と、回復できる持ち主が消されます。 やり直しの台帳と正規の受け付けは別々のデータベースのトランザクションなので、 この回復の操作は、全体として厳密に一度だけ実行されることを約束するものではありません。
POST /v1/runs/\{run_id\}/stop
走っているエージェントの往復に割り込みます。この経路はすぐに {"status": "stopping"} を返し、その裏で Hermes が、いま動いているエージェントに次の安全な切れ目で止まるよう頼みます。 実行は、その仕事が実際に終わるまで stopping として追われ、そのあと cancelled に落ち着きます。停止を頼んだからといって、まだ走っている働き手が 隠されることはありません。
POST /v1/runs/\{run_id\}/approval
人の判断を待っている実行について、保留中の承認に答えます(たとえば、承認の方針でせき止められたツールの呼び出しです)。本文には承認の判断を入れます。判断が記録されると、実行が再開します。この経路は /v1/capabilities で run_approval という機能として公開されているので、外部の画面は承認の問いかけを出す前に対応しているかを調べられます。
正規のゲートウェイの持ち主がいる場合、GET /v1/runs/{run_id} は pending_controls を出します。使う prompt_id と execution_generation は、 つながっている WebSocket の閲覧側と同じです。いまの識別を正確に添えて答えてください。
{"request_id": "<prompt_id>", "execution_generation": 3, "choice": "once"}正規の操作は、識別のない答えやまとめての答えを受け付けません。古い世代、 ほかの prompt の ID、すでに決着した実行には HTTP 409 が返ります。既存の実行の認証と、 ホスト型ルームでの承認の制限は、これまでどおり適用されます。
POST /v1/runs/\{run_id\}/clarify
正規の確認の問いには、request_id、execution_generation、 answer(文字列)で答えます。この認証付きのエンドポイントと WebSocket の clarify.respond は、 待っている同じツールに答えを渡します。答えても、推論のターンがもう一度送られることはありません。 MCP の信頼ゲートでの同意(trust: untrusted と設定したサーバーにある、書き込みのできるツール)も同じ形で現れます。実行は approval.request のイベントを出し、この経路で答えが出るまで waiting_for_approval で待ちます(once ならツールを実行し、deny なら止めます)。
Jobs の API(裏で動く予定の仕事)
サーバーは、予定された裏側のエージェントの実行を離れたところから管理するための、軽い仕事の作成・取得・更新・削除の窓口を備えています。どの経路も同じ 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 でのセッションの操作)
外部の画面は、ダッシュボードを立ち上げなくても REST から Hermes のセッションを扱えます。どの経路も 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 を付けると、画像の添付をインラインの data URI ではなく [image] というプレースホルダーとして返します。やり取りの記録が数キロバイトで届くので、ネットワーク越しに読むクライアントに向いています |
POST |
/api/sessions/{id}/fork |
SessionDB の系譜をたどってセッションを枝分かれさせます(CLI の /branch と同じ考え方です) |
POST |
/api/sessions/{id}/chat |
エージェントの往復を 1 回、待ち合わせる形で走らせます |
POST |
/api/sessions/{id}/chat/stream |
往復 1 回を 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_* の項目でこの窓口の全体を知らせるので、外部の画面は対応を調べたうえで安全に別の手に切り替えられます。chat と chat/stream の中身では、文中の画像にも対応しています(複数の形式を扱える経路です)。
# fork a session and run one turn
curl -X POST http://localhost:8642/api/sessions/$ID/fork \
-H "Authorization: Bearer $API_SERVER_KEY" \
-d '{"title": "explore alt path"}'
# stream a turn over 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 の一覧を返します。どちらも /v1/capabilities の endpoints.* に載っています。
長期の記憶の切り分け(X-Hermes-Session-Key)
Open WebUI のように複数の利用者がいるフロントエンドでは、長期の記憶(Honcho など)のために、経路ごとに変わらない識別子が要ります。それは、記録に紐づいていて /new のたびに変わる X-Hermes-Session-Id とは別のものでなければなりません。/v1/chat/completions、/v1/responses、/v1/runs に X-Hermes-Session-Key を渡すと、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" で対応していることを知らせます。この鍵がないと、Honcho の per-session のやり方では session_id ごとに違う切り分けになります。Hermes がこれまでしていたのと、まさに同じ動きです。
自動の想起は会話の記録に沿って働きます。メモリプロバイダーはセッションごとに一度だけ初期化され、リクエストをまたいで保たれます。そのため、続きのセッション(X-Hermes-Session-Id、previous_response_id、または X-Hermes-Session-Key で宣言した会話)は、プロバイダーが前のターンのあとに用意した想起を受け取ります。Telegram や Discord のチャットとまったく同じです。続きの指定がないリクエストは新しいセッションを始めるので、新しい CLI セッションの最初のターンと同じく、用意されたものはまだ何もありません。使われていないセッションは、ゲートウェイのエージェントキャッシュと同じ待機時間(agent.agent_cache.idle_ttl_secs、既定は1時間)が過ぎるとプロバイダーを手放します。
システムプロンプトの扱い
フロントエンドが system のメッセージ(Chat Completions)や instructions の項目(Responses API)を送ってくると、hermes-agent はそれを自分の中核のシステムプロンプトの上に重ねます。エージェントはツールも記憶もスキルもすべて保ったままで、フロントエンドのシステムプロンプトは追加の指示として効きます。
つまり、能力を失わずにフロントエンドごとの振る舞いを整えられるということです。
- Open WebUI のシステムプロンプト: 「あなたは Python の専門家です。必ず型ヒントを付けてください。」
- それでもエージェントには、ターミナル、ファイルのツール、ウェブ検索、記憶などが残ります。
認証
Authorization のヘッダーによる bearer トークンの認証です。
Authorization: Bearer ***鍵は環境変数 API_SERVER_KEY で設定します。ブラウザから Hermes を直接呼ぶ必要があるなら、API_SERVER_CORS_ORIGINS にもはっきりと許可する一覧を設定してください。
プロファイルごとの振り分け(/p/<profile>/…)
プロファイルごとのゲートウェイの振り分けが 有効なとき(gateway.multiplex_profiles)、ひとつの待ち受けが /p/<profile>/ という URL の 接頭辞ですべてのプロファイルを引き受けます。そして認証は、振り分けられた プロファイルに結びつきます。
/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 から始めた実行も含みます)。 別のプロファイルの実行の ID には 403 ではなく必ず 404 を返します。
設定
環境変数
| 変数 | 既定 | 説明 |
|---|---|---|
API_SERVER_ENABLED |
false |
API サーバーを有効にします |
API_SERVER_PORT |
8642 |
HTTP サーバーのポート |
API_SERVER_HOST |
127.0.0.1 |
待ち受けるアドレス(既定では手元だけ) |
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 = verbatimport、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 のスケジューラを通るため、この上限ではなく 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 への 1 回の書き込みが数百 KB になることもあります。gateway.api_server.history_tool_output_max_chars(既定は 0 で、そのまま保存します)を指定すると、保存される履歴のなかのツールの出力ひとつずつと、文字列で渡されたツールの引数ひとつずつを、その文字数で頭打ちにできます。それより長いものは先頭だけを残し、...[N more chars] という印を付けて切ります。利用者とアシスタントの文章には手を付けません。response.completed の中身や、順次届く SSE のできごとにも影響しません。保存された履歴は、次につないだ往復でモデルが見るものでもあります。つまりこの上限を使うと、モデルに再生される内容も短くなります。往復をまたいでツールの出力がまるごと必要な進め方なら、0 のままにしてください。
安全のためのヘッダー
どのレスポンスにも、安全のためのヘッダーが入ります。
X-Content-Type-Options: nosniff— 中身の種類を勝手に推し量られるのを防ぎますReferrer-Policy: no-referrer— どこから来たかが漏れるのを防ぎます
CORS
API サーバーは、既定ではブラウザ向けの CORS を有効にしません。
ブラウザから直接つなぐなら、許可する一覧をはっきり書きます。
API_SERVER_CORS_ORIGINS=http://localhost:3000,http://127.0.0.1:3000CORS を有効にすると、こうなります。
- 事前確認のレスポンスに
Access-Control-Max-Age: 600が入ります(10 分のキャッシュ) - SSE の逐次送りのレスポンスにも CORS のヘッダーが入るので、ブラウザの EventSource のクライアントがきちんと動きます
X-Hermes-Session-Idが許されるリクエストのヘッダーになります。許可した出どころのブラウザから、会話の続きを求められるようになります。Idempotency-Keyが許されるリクエストのヘッダーになります。重複を避けるために送れます(キーが何をカバーし、どれくらい保たれるかは冪等キーを参照してください)
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(設定・記憶・スキルが別々のもの)を渡すには、プロファイルを使います。
# Create a profile per user
hermes profile create alice
hermes profile create bob
# Configure each profile's API server on a different port. API_SERVER_* are env
# vars (not config.yaml keys), so write them to each profile's .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
# Start each profile's gateway
hermes -p alice gateway &
hermes -p bob gateway &それぞれのプロファイルの API サーバーは、そのプロファイル名をモデルの ID として自動で知らせます。
http://localhost:8643/v1/models→ モデルalicehttp://localhost:8644/v1/models→ モデルbob
Open WebUI では、それぞれを別のつなぎ先として足します。モデルの一覧には alice と bob が別々のモデルとして並び、それぞれの裏にはすっかり切り離された Hermes がいます。詳しくは Open WebUI のガイド を見てください。
制限
- レスポンスの保存 — 保存されたレスポンス(
previous_response_idのためのもの)は SQLite に残り、ゲートウェイを再起動しても消えません。保存できるのは最大 100 件です(古いものから捨てられます)。 - ファイルのアップロードには対応していません —
/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 サーバーに向けて設定されていると、そちらは自前でエージェントを動かす代わりに、すべてのメッセージをここへ送ってきます。おかげで、役割を分けた構成にできます。たとえば Matrix の端末間の暗号化を受け持つ Docker のコンテナが、ホスト側のエージェントへ中継するといった形です。
設定の手順は Matrix の中継の使い方 を見てください。