API Server 把 Hermes 的模型、Memory、Skills 和终端工具交给另一个系统调用。一次 curl 返回答案只证明网络接通;上线还必须证明错误 key 被拒绝、长任务可追踪、重复请求不会重复执行、浏览器 Origin 被限制,以及管理员能停止入口。
本课只在独立 api-lab-741 Profile 和 127.0.0.1:8642 上测试,固定输入为 API-CANARY-741。完成 A01–A14 前不绑定 0.0.0.0,也不接真实前端。
本站验证范围: 2026-10-07 已在 Hermes v0.21.5 上只用 127.0.0.1 实跑 A01–A14,A03 的启动方式已按新版本更正,记录见文末。
下载两份材料
运行卡只记录 key 的保管位置、owner 和轮换时间,不记录 key 本身。命令使用当前 shell 的 API_SERVER_KEY;录屏和 CI 日志应屏蔽 header。
先决定是否需要 API
| 需求 | 入口 |
|---|---|
| 人在终端里交互 | CLI / TUI |
| Telegram 等消息入口 | Messaging Gateway |
| 固定周期任务 | Cron |
| OpenAI-compatible 客户端、内部服务主动调用 | API Server |
| Desktop 或远程 Dashboard 后端 | hermes serve,这是另一套 JSON-RPC/WebSocket 服务,默认 9119 |
旧教程经常把 hermes serve 和本页 API Server 混为一谈。OpenAI-compatible API Server 是 Gateway 中的一个 platform,默认端口 8642,通过 API_SERVER_ENABLED=true 后运行 hermes gateway 启动。
先按 Profiles P01–P12 建立独立 Profile。Profile 会隔离 Hermes 状态,但不会自动限制 local terminal backend 的宿主机文件权限;API 调用可能触发完整工具集,因此要先缩小 toolset 与 cwd。
A01:建立独立 Profile 与固定环境
hermes profile create api-lab-741 --clone \
--description "Loopback API acceptance lab; no production writes"
hermes -p api-lab-741 doctor
hermes -p api-lab-741 dump
检查该 Profile 没有生产 Cron、消息渠道或不需要的写入 Skill。为它设置一个临时 terminal.cwd,只放无敏感测试文件。
A02:配置 loopback、key 与并发上限
在 ~/.hermes/profiles/api-lab-741/.env 写入:
API_SERVER_ENABLED=true
API_SERVER_HOST=127.0.0.1
API_SERVER_PORT=8642
API_SERVER_KEY=<由密码管理器生成的随机测试值>
API_SERVER_MODEL_NAME=api-lab-741
在同一 Profile 的 config.yaml 设置首轮并发上限:
gateway:
api_server:
max_concurrent_runs: 1
API key 在 loopback 上也必填。环境变量优先于 config.yaml;避免两个位置写不同 key/port 后排查错误。CORS 此时保持未配置。
A03:前台启动并确认监听主体
hermes -p api-lab-741 gateway run
v0.21.5 起这条命令会被拒绝(退出码 78):“Profile ‘api-lab-741’ does not get a gateway of its own. Exactly one gateway per host…”。新版本要求一台机器只有一个宿主 Gateway,由它服务所有 Profile。它给出的路线(重装宿主 Gateway、migrate --multiplex、install --force、gateway.standalone: true)都会改动正在运行的生产 Gateway。纯测试时,本站的做法是把 Profile 复制到 ~/.hermes/profiles/ 之外的目录,再用 HERMES_HOME=<该目录> hermes gateway run 前台运行。它会与宿主 Gateway 并排运行、互不接管;首次启动要先装一套隔离的 Python 运行时,约 2 分钟。
启动日志应显示 API Server 监听 http://127.0.0.1:8642。另开终端检查端口对应进程和 Profile,不能只看到“8642 已占用”就假定是本次 Gateway。
A04:区分 liveness 与 readiness
curl -i http://127.0.0.1:8642/health
curl -i http://127.0.0.1:8642/health/detailed \
-H "Authorization: Bearer $API_SERVER_KEY"
/health 是公开 liveness,预期 200 与 {"status":"ok"};它不运行 readiness 检查。/health/detailed 是鉴权后的有界状态,检查 active Profile 的 config、state DB、model、disk、Gateway、active runs 等,且不返回 key、路径、命令或原始错误。
Detailed readiness 即使 degraded 也使用 HTTP 200。自动监控必须解析顶层 status 与 readiness.checks,不能只看状态码。
A05:做三次鉴权负例
curl -i http://127.0.0.1:8642/v1/models
curl -i http://127.0.0.1:8642/v1/models \
-H "Authorization: Bearer definitely-wrong"
curl -i http://127.0.0.1:8642/v1/models \
-H "Authorization: Bearer $API_SERVER_KEY"
缺失与错误 bearer 必须是 401,正确 key 才能读取模型。若错误 key 得到业务数据,立即停止 Gateway,不继续后续测试。
A06:先发现能力,再写客户端
curl -sS http://127.0.0.1:8642/v1/capabilities \
-H "Authorization: Bearer $API_SERVER_KEY" | jq .
curl -sS http://127.0.0.1:8642/v1/models \
-H "Authorization: Bearer $API_SERVER_KEY" | jq .
curl -sS http://127.0.0.1:8642/v1/toolsets \
-H "Authorization: Bearer $API_SERVER_KEY" | jq .
Capabilities 是客户端的契约入口:确认 chat_completions、responses_api、run_submission、run_status、run_events_sse、run_stop 等实际 feature,再决定展示哪些按钮。/v1/models 只广告稳定 alias,不是 provider 全量目录;Hermes-aware UI 才使用 /api/model/options。
在运行卡抄录 API platform 最终解析出的 toolsets。它们可能包含 terminal/file 等完整能力,不能因请求长得像 OpenAI API 就把它当成纯文本模型。
A07:验证无状态 Chat Completions
curl -sS http://127.0.0.1:8642/v1/chat/completions \
-H "Authorization: Bearer $API_SERVER_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"api-lab-741","messages":[{"role":"user","content":"Reply with exactly API-CANARY-741 and nothing else."}],"stream":false}' | jq .
记录 HTTP 状态、response id、实际 model、唯一输出和 usage。Chat Completions 是无状态接口,每次都应携带完整 messages。裸 model 默认可能被当稳定 alias 并回退到 Gateway 默认;只有显式 provider 或启用 direct_model_requests 才能让 OpenAI-compatible 请求直接选择其他模型。
A08:验证 Responses 的服务端多轮
第一次请求:
curl -sS http://127.0.0.1:8642/v1/responses \
-H "Authorization: Bearer $API_SERVER_KEY" \
-H "Content-Type: application/json" \
-d '{"model":"api-lab-741","input":"Remember only for this conversation: API-CANARY-741.","store":true}' | jq .
保存实际 resp_...,第二次用 previous_response_id 询问金丝雀。服务端会重建包含 tool calls/results 的链路,并把多轮记录为同一 Session。也可用固定 conversation 名让服务端自动链到该 conversation 最新 response。
Stored responses 在 SQLite 中跨 Gateway 重启保留,但最多 100 条并按 LRU 淘汰。不要把它当无限期业务数据库。DELETE /v1/responses/{id} 后再 GET 应按真实结果记录。
A09:创建可追踪 Run
为本次实验生成一个唯一、不可复用的幂等键,例如 api-lab-741-<uuid>:
curl -i -X POST http://127.0.0.1:8642/v1/runs \
-H "Authorization: Bearer $API_SERVER_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: api-lab-741-<uuid>" \
-d '{"input":"Reply with exactly API-CANARY-741 and nothing else.","session_id":"api-lab-741-session"}'
创建返回 run_id 与 started。用精确 ID 轮询:
curl -sS http://127.0.0.1:8642/v1/runs/<run-id> \
-H "Authorization: Bearer $API_SERVER_KEY" | jq .
追到 completed、failed 或 cancelled,再记录 output 与 usage。POST 接受不等于执行完成。
A10:证明相同重试不会创建第二个 Run
用 同一 Idempotency-Key 和完全相同 JSON 重发 A09。预期 HTTP 202、相同 run_id,并包含 Idempotency-Replayed: true。该 reservation 在启动工作前持久保存,Gateway 重启和 terminal state 后仍能重放;key 按认证 Profile/credential 隔离,并在最后状态更新后保留 24 小时。
随后用同一 key 改一个字符,预期 HTTP 409 与 idempotency_key_conflict。无 Idempotency-Key 的每次 POST 都创建新 run,因此客户端超时重试必须自己携带唯一键。
A11:验证 Events 与 Stop
创建一个足够长但无副作用的测试 run,然后:
curl -N http://127.0.0.1:8642/v1/runs/<run-id>/events \
-H "Authorization: Bearer $API_SERVER_KEY"
curl -sS -X POST http://127.0.0.1:8642/v1/runs/<run-id>/stop \
-H "Authorization: Bearer $API_SERVER_KEY"
Stop 先返回 stopping,只有 executor 退出后状态才变为 cancelled。不能在收到 stopping 时释放同一业务资源或宣告取消完成。SSE 断开不会取消 run;未消费的 event buffer 五分钟后过期,但仍可轮询状态、停止或审批。
A12:验证 CORS allowlist 正反例
只有浏览器必须直连时才配置:
API_SERVER_CORS_ORIGINS=http://localhost:3000
重启同一 Profile Gateway 后分别发送 allowlisted 与陌生 Origin 的 preflight:
curl -i -X OPTIONS http://127.0.0.1:8642/v1/runs \
-H "Origin: http://localhost:3000" \
-H "Access-Control-Request-Method: POST" \
-H "Access-Control-Request-Headers: authorization,content-type,idempotency-key"
curl -i -X OPTIONS http://127.0.0.1:8642/v1/runs \
-H "Origin: https://untrusted.invalid" \
-H "Access-Control-Request-Method: POST"
只有允许 Origin 应得到匹配的 Access-Control-Allow-Origin。CORS 管浏览器读取,不是鉴权;服务端脚本不受浏览器 CORS 保护,仍必须验证 bearer。
A13:验证并发上限和 Profile 路由
在 max_concurrent_runs: 1 时保持一个长 run 运行,再提交第二个,预期 HTTP 429 Too many concurrent runs (max 1)。客户端应退避,不能无界立即重试;0 会禁用上限,不适合作为首轮配置。
若启用 gateway.multiplex_profiles,共享 listener 通过 /p/<profile>/... 路由。每个 named Profile 必须使用自己的 API_SERVER_KEY;default key 对 named prefix 应返回 401。Run ID 也按 Profile 隔离,在另一个 Profile 查询返回 404。先在单 Profile 完成 A01–A12,再增加这组矩阵。
A14:撤销入口并复查端口
- 停止
api-lab-741Gateway。 - 确认 8642 不再监听,
/health不可达。 - 从 Profile 配置移除
API_SERVER_ENABLED和测试 CORS。 - 轮换或销毁测试 key。
- Export Profile,记录归档 SHA-256。
- 删除临时 Profile,再确认 default active。
如果对接系统已经保存 base URL/key,还要先撤销调用方配置,避免它持续产生 401 与重试流量。
本站实测记录(2026-10-07,Hermes v0.21.5)
环境:macOS 26.4(Apple Silicon),Hermes v0.21.5(2026.9.24),模型 glm-5.3-flash(zai),测试 Profile aclab 及临时 Profile,均不连接消息平台;你的 default Profile 与正在运行的 Gateway 未改动。 测试 Profile 从 aclab 克隆,max_concurrent_runs: 1,只绑定 127.0.0.1:8642;测试 key 用 openssl rand 现场生成,测完即销毁。

| 步骤 | 实测结果 |
|---|---|
| A03 | hermes -p api-lab-741 gateway run 被拒绝(见上文);改用独立 HERMES_HOME 启动,只监听 127.0.0.1:8642 |
| A04 | /health 200,{"status":"ok","platform":"hermes-agent","version":"0.21.5"};/health/detailed 无 key 401,有 key 时 7 项检查全部 ok |
| A05 | 缺失 key 401,错误 key 401(gateway_auth_failed),正确 key 200 |
| A06 | capabilities 全部开启(只有 admin_config_rw: false);/v1/models 只有 api-lab-741 |
| A07 | 200,内容精确为 API-CANARY-741,响应头带 X-Hermes-Session-Id |
| A08 | 带 previous_response_id 的第二轮答出金丝雀,不带链的对照请求答 UNKNOWN;DELETE 200 后 GET 404 |
| A09–A10 | 创建 202 → completed;同键同内容重放得到相同 run_id 和 Idempotency-Replayed: true;同键改内容 409 idempotency_key_conflict;不带键创建新 run |
| A13 | 运行中再提交 → 429 “Too many concurrent runs (max 1)“ |
| A11 | stop 返回 stopping,随后变为 cancelled;事件流 run.cancelled,原因 interrupted_during_api_call |
| A12 | 白名单 Origin 的预检得到匹配的 Access-Control-Allow-Origin;陌生 Origin 的预检 403,陌生 Origin 的普通请求也直接 403 |
| A14 | 停止后 8642 释放、/health 不可达;临时 Profile、隔离目录和 key 全部删除,宿主 Gateway 一直在运行 |
两点成本与风险提醒:
- 一句话请求也有 13,630 个输入 token,因为完整的 Agent 系统提示和工具定义每次都会发送。好在其中 13,568 个命中了缓存。按 token 计费的客户端,要按这个量级估算成本。
- API 默认开放全部工具集:web、browser、terminal、file、code_execution、cronjob、delegation,连克隆带过来的 homeassistant 插件也在里面。持有 key 的调用方等于能在这台机器上执行命令、创建定时任务。上线前先在 Profile 里收窄 toolsets。另外,Chat Completions 的响应会带
reasoning_content,直接透传给前端会暴露模型的思考过程。
上线必须填写的责任表
| 问题 | 必须有明确答案 |
|---|---|
| 谁能调用 | 服务身份、owner、key 轮换与离职交接 |
| Agent 能做什么 | Profile、cwd、Toolsets、Skills、审批规则 |
| 如何重试 | Idempotency-Key 生命周期与 409/429 处理 |
| 如何观察 | liveness、detailed readiness、run terminal state、usage |
| 如何停止 | stop 的最终 cancelled、Gateway 停止、key 撤销 |
| 浏览器是否直连 | 无需则关闭 CORS;需要则精确 Origin |
完成检查
- A01–A14 均有真实状态码、ID、JSON 字段或端口证据。
- 缺 key 与错误 key 都被拒绝,详细 readiness 没有泄露 Secret。
- Chat、Responses、Runs 三种接口的状态模型没有混用。
- 相同幂等请求返回同一 run,不同 payload 返回 409。
- Stop 被追踪到最终 cancelled,第二个并发请求得到 429。
- 非 allowlist Origin 没有得到跨域读取许可。
- Gateway 已停止,端口关闭,测试 key 与 Profile 已撤销。
需要把 API 对接 Open WebUI 时,应沿用同一份 A01–A14 表,而不是只填写 base URL。需要定时执行时,转到 Cron 生命周期实验,不要让外部客户端自己模拟 scheduler。