@guangnao/agent-hub
v1.1.5
Published
Pool subscription-authorized agent nodes behind one OpenAI/Anthropic-compatible /v1 endpoint. Star topology: a central hub + outbound-connected worker nodes, each running a local subscription-auth proxy against its own claude/codex login.
Maintainers
Readme
@guangnao/agent-hub
把多台已授权 claude / codex 的机器聚合成一个 OpenAI / Anthropic 兼容的 /v1 端点。
/v1 任何 OpenAI/Anthropic 客户端指过来即用。
星型拓扑(不是网状 P2P):一个中心 hub + 若干出站连上来的工作节点。每个节点在本机跑一个轻量本地代理、读该机的 claude/codex 订阅登录、字节级转发(Claude 走原生 /v1/messages、保留原生 tool_use)。订阅凭证永不离开节点,只回传结果。
客户端 ──/v1(OpenAI+Anthropic)──▶ hub ──wss──▶ 节点A / 节点B / 节点C …
调度 (各自的 claude/codex 订阅)⚠️ 合规:把多个个人订阅池化对外,很可能违反 Anthropic/OpenAI 的个人使用条款(封号风险)。自用集群风险低、对外服务风险高 —— 自行斟酌。 🔒 安全:hub 能支配所有节点的额度 —— 生产务必设
AGENT_HUB_API_KEY(客户端)+AGENT_HUB_NODE_TOKEN(节点),并用防火墙/安全组收紧端口。
安装
npm i -g @guangnao/agent-hub # 得到 agent-hub 命令服务器上免 sudo 全局安装(EACCES)
共享机(Amazon Linux 2023 / Debian …)上 Node 的全局目录归 root,npm i -g 会 EACCES … mkdir '/usr/lib/node_modules/@guangnao'。这没法由包自动修复 —— 权限错误发生在任何包脚本运行之前,且包不该擅自改你的 npm/shell 配置。装 @guangnao/agent-hub 之前先跑一次下面这段(幂等、可重复跑、不会卡),把全局前缀挪到家目录,之后正常 npm i -g 即可:
mkdir -p ~/.npm-global
npm config set prefix ~/.npm-global
grep -q '.npm-global/bin' ~/.bashrc || echo 'export PATH=$HOME/.npm-global/bin:$PATH' >> ~/.bashrc
export PATH=$HOME/.npm-global/bin:$PATH # 当前 shell 立即生效;~/.bashrc 覆盖新开的 shell
npm i -g @guangnao/agent-hub # 从此免 sudo跑过这段后,agent-hub start 缺 pm2 时的自动 npm i -g pm2 也会装进 ~/.npm-global,同样免 sudo、不卡。
替代:sudo npm i -g @guangnao/agent-hub(装成 root),或用 nvm/fnm(其全局目录本就在你名下)。
要求 Node ≥ 20.12。
快速开始
1) 起中心 hub(放服务器)。 生产用 start 挂到 pm2(后台、关终端不停、崩溃自拉起):
PORT=8790 HOST=0.0.0.0 agent-hub start # 不带 AGENT_HUB_API_KEY 时自动生成并落盘
# → pm2 启动 name=agent-hub;就绪后【完整打印 API key】,方便复制:
# API key (generated + saved to ~/.agent-hub/config.json):
# sk-…(48 hex)
agent-hub stop # 停止并从 pm2 删除
agent-hub serve # 前台运行(容器/调试用,不经 pm2)API key:
start未设AGENT_HUB_API_KEY时自动生成sk-…并存到~/.agent-hub/config.json(AGENT_HUB_CONFIG改路径,权限 0600),下次启动不变、每次都完整打印。设了AGENT_HUB_API_KEY则以环境变量为准(不落盘)。serve/state同样按 env > 持久化解析,所以本机agent-hub state免手动带 key。要换一把:agent-hub new apikey生成新 key 并落盘替换旧的——运行中的 hub 不会自动生效,重启才应用(agent-hub stop && agent-hub start)。 服务器健壮性:家目录缺失/只读也不会卡 —— 写入按AGENT_HUB_CONFIG → ~/.agent-hub → $TMPDIR/agent-hub逐个降级,全失败则只警告不报错(key 本次仍可用,按提示设AGENT_HUB_API_KEY或可写的AGENT_HUB_CONFIG即可固定)。start会在缺 pm2 时自动npm i -g pm2;其余环境变量(PORT/HOST/AGENT_HUB_NODE_TOKEN/AGENT_HUB_NODE_RPM)由 pm2 从当前 shell 继承。
查看各节点统计(名称/模型数/容量/在途/近 1min 用量/首字节延迟/冷却):
AGENT_HUB_API_KEY=sk-xxx agent-hub state http://<hub-ip>:8790 # 缺省读 AGENT_HUB_URL,再缺省 127.0.0.1:87902) 起一个节点(放已授权 claude/codex 的机器)。
agent-hub node http://<hub-ip>:8790 # 前台常驻;托给 pm2/systemd 等你自己的进程管理即可开机自启
AGENT_HUB_NODE_TOKEN=node-secret AGENT_HUB_NODE_NAME=my-node agent-hub node http://<hub-ip>:8790凭证不离机——节点只读本机的 claude/codex 登录态,向 hub 只回传结果。任何客户端工具都可以在自己的启动流程里调用这条命令,实现"检测到本机有可用订阅就自动接入"。
3) 客户端接入。 把任意 OpenAI/Anthropic 客户端指向 hub 的 /v1:
# OpenAI 兼容
curl http://<hub-ip>:8790/v1/chat/completions \
-H 'authorization: Bearer sk-xxx' -H 'content-type: application/json' \
-d '{"model":"gpt-5-codex","messages":[{"role":"user","content":"hi"}],"stream":true}'
# Anthropic 兼容(原生 tool_use)
curl http://<hub-ip>:8790/v1/messages \
-H 'x-api-key: sk-xxx' -H 'anthropic-version: 2023-06-01' -H 'content-type: application/json' \
-d '{"model":"claude-sonnet-4-5","max_tokens":256,"stream":true,"messages":[{"role":"user","content":"hi"}]}'
# 全池模型目录
curl http://<hub-ip>:8790/v1/models -H 'authorization: Bearer sk-xxx'端点(hub)
鉴权范围:只有 /v1/*(会消耗某个节点 subscription 额度的面)受 AGENT_HUB_API_KEY 保护;/health、/state、/metrics 是纯运维可见性、不设 key(和 Prometheus 抓取端点同一惯例)——对外暴露的话请靠防火墙/安全组收紧,别指望 key。
| 方法 · 路径 | 说明 |
|---|---|
| POST /v1/messages | Anthropic 面 → 有该模型的节点(Claude 原生透传)·需 key |
| POST /v1/chat/completions | OpenAI 面 ·需 key |
| POST /v1/responses | OpenAI Responses 面(Codex 原生)·需 key |
| GET /v1/models | 全池模型并集({object:'list',data:[…]})·需 key |
| GET /health | {status, nodes, models, inflight} |
| GET /state | 逐节点统计:{totals, nodes:[{id,name,models,capacity,inflight,used1m,cooldownMs,ttfbMs}]} —— agent-hub state 用它 |
| GET /metrics | Prometheus 文本(agent_p2p_nodes / models / inflight / nodes_cooldown / jobs_{dispatched,served,failed,ratelimited}_total) |
调度(公平优先 → 合规 → 速度):byModel 倒排索引取"有该模型"的节点后,按容量归一化的近 60s 用量最少者派发 —— 均衡摊派,绝不过度消耗同一节点的订阅额度;平手时看在途数、再看首字节延迟(轻微偏好快节点)。合规护栏:可选每节点每分钟配额上限(AGENT_HUB_NODE_RPM)在触限前就避让;节点上游返回 429/529 → 冷却退避(15s 起翻倍、封顶 5min)暂时移出轮转,成功后恢复。若节点识别出这是账号会话/用量上限(小时级重置,不是瞬时限流)会带上真实的 Retry-After,冷却时长按它来、不再被 5 分钟封顶——避免账号明明还没恢复,5 分钟一到又把它派出去撞一次。跳过满载(inflight ≥ capacity 背压);匹配节点全忙/限流 → 503(可重试);目录未命中回退任意节点(本地代理真校验)。客户端断开 → 通知节点取消;节点掉线 → 挂起请求回 502。
规模化(单进程可撑数千节点):倒排索引使选节点为 O(该模型节点数) 而非全表扫;/v1/models 直接读索引键,不每请求重算并集;单一心跳巡检(ws 原生 ping,死连 terminate)取代"每连接一个定时器";节点上下线超过 50 个后转为 5s 聚合日志(免刷屏)。再往上(数万节点)→ 起多实例 hub 挂 L4 负载均衡、节点/客户端一起打散(无状态分片,天然容灾);仅当需"跨实例全局最优选节点"才引入 Redis 共享注册表。
账号池规模建议:每个账号(节点)的订阅本身有硬上限——Claude/Codex 的会话额度通常是小时级滚动窗口,一旦某个账号打满,在它重置前这个账号完全不可用,跟 hub/调度逻辑无关,加节点数、调参数都绕不开这一点。池子里只有一个账号时,这个账号一打满,/v1/* 就会开始返回 503 all matching nodes are busy or rate-limited——不是 bug,是池子里真的没有可用账号了。实用建议:
- 账号数 ≥ 预期峰值并发 + 冗余:至少备 2 个账号起步,高峰期负载越高、越该多备几个——某个账号打满时,其余账号能顶上,请求方基本无感。
- 主动限速优于被动冷却:给每个节点设
AGENT_HUB_NODE_RPM,值设成略低于账号订阅的真实速率上限——在真正撞上账号级会话上限之前就提前避让,而不是等打满了再冷却等重置。 - 观察
agent-hub state的COOLDOWN列——如果频繁看到某个节点冷却时长是几十分钟到几小时级(而不是十几秒到几分钟),说明它在被真实打满,不是偶发限流,该考虑给它设更低的AGENT_HUB_NODE_RPM,或者往池子里再加账号。
配置(环境变量)
hub(agent-hub serve):
| 变量 | 默认 | 说明 |
|---|---|---|
| HOST / PORT | 0.0.0.0 / 8790 | 绑定地址 / 端口 |
| AGENT_HUB_API_KEY | 自动生成 | 客户端鉴权(x-api-key / Bearer)。start 未设时自动生成并存 ~/.agent-hub/config.json,下次不变;serve 直跑未设且无持久化 = 开放 |
| AGENT_HUB_NODE_TOKEN | (空) | 节点注册口令;空 = 任意节点可注册 |
| AGENT_HUB_NODE_RPM | 0(不限) | 每节点每分钟派发上限(合规护栏);设成略低于订阅真实限额,触限前即避让 |
| AGENT_HUB_JOB_TIMEOUT_MS | 1800000 | 单 job 硬超时兜底:节点始终不回该 job → 巡检回收 |
| AGENT_HUB_UNHEALTHY_MS | 5000 | 节点报错后的降级窗口,期间避开调度直到一次成功 |
| AGENT_HUB_LOG_JSON | 关闭 | 结构化日志:1 = 每行 JSON(便于日志聚合);默认人类可读 |
节点(agent-hub node <hubUrl>):
| 变量 | 默认 | 说明 |
|---|---|---|
| AGENT_HUB_URL | (无) | node 子命令缺省的 <hubUrl>,免每次都写 |
| AGENT_HUB_NODE_TOKEN | (空) | 必须与 hub 一致(hub 设了才需要) |
| AGENT_HUB_NODE_NAME | 主机名 | 节点显示名 |
| AGENT_HUB_IDLE_TIMEOUT_MS | 120000 | 单 job 空闲超时(首字节前 / 流中途无字节达到此值 → 判上游挂起,abort 回 504 释放容量) |
| AGENT_HUB_MODEL_REFRESH_MS | 300000 | 模型目录定期重拉周期;订阅换档/重新登录后免重连即可刷新 |
凭证来源(节点本机):Claude 读 ~/.claude 登录或 CLAUDE_CODE_OAUTH_TOKEN;Codex 读 ~/.codex/auth.json(codex login)。
通信 / 可靠性
节点主动出站 wss 长连到 hub(NAT/防火墙友好,只需出站),一条连接多路复用多个 job(按 id 关联),响应分块 base64 回传(无跨块截断)。心跳保活、断线指数退避重连;job 有取消/掉线兜底。
编程式用法
@guangnao/agent-hub 除了 CLI,也是一个包:createHub/startNode 都是普通函数,可以直接 import 进你自己的工具——中心和节点各自都能这么用,不一定要经过 agent-hub 命令行。
起中心(createHub)
等价于 agent-hub serve,但配置项直接传参、不走环境变量;返回一个 node:http Server。
import { createHub } from '@guangnao/agent-hub';
const hub = createHub({ port: 8790, apiKey: 'sk-xxx', nodeToken: 'node-secret' });
// hub.close() 即可关闭(会先停收新连接、清定时器)HubOptions(全部可选):
| 字段 | 说明 |
|---|---|
| port / host | 缺省 8790 / 0.0.0.0 |
| apiKey | 客户端鉴权;''/缺省 = 开放 |
| nodeToken | 节点注册口令;''/缺省 = 任意节点可注册 |
| nodeRpm | 每节点每分钟派发上限(合规护栏);缺省不限 |
| jobMaxMs | 单 job 硬超时兜底;缺省 30min |
| unhealthyMs | 节点报错后的降级窗口;缺省 5s |
| logJson | true → 结构化 JSON 日志;缺省人类可读 |
| log | 自定义日志 sink((line: string) => void);缺省写 stdout |
起一个节点(startNode)——把"接入池子"嵌进你自己的工具
这是把 agent-hub 当开发包用的主要场景:不用另开一个 agent-hub node 子进程,直接在你自己的程序里 import { startNode } 调用,服务在启动时检测到本机有可用的 claude/codex 订阅,省掉一层子进程管理,状态、日志、优雅退出都由调用方掌控。
import { startNode } from '@guangnao/agent-hub';
const ac = new AbortController();
await startNode({
hubUrl: 'http://hub:8790',
token: 'node-secret',
onlyIfCredentialed: true, // 本机没有可用的 claude/codex 凭证 → 静默不接入,直接 resolve(适合"能接就接、不能就算了"的自动化)
status: false, // 关掉终端实时状态行(嵌入到自己的 UI 里时通常想自己控制输出)
log: (line) => myLogger.info(line), // 缺省写 stderr;传自己的 logger 就接管
signal: ac.signal, // 传了才会在 abort 时 resolve——不传则常驻直到进程退出
});
// ac.abort() → 优雅下线(关 ws 长连 + 本地代理端口),Promise 随之 resolveNodeOptions:
| 字段 | 说明 |
|---|---|
| hubUrl | 必填,hub 地址 |
| token | 节点注册口令;需与 hub 的 nodeToken 一致 |
| name | 节点显示名;缺省主机名 |
| onlyIfCredentialed | true 时:本机拉不出任何 claude/codex 模型 → 不注册、直接 resolve(不报错、不重试) |
| status | 是否在 stderr 画实时状态行;缺省按 process.stderr.isTTY |
| log | 自定义日志回调;缺省写 stderr |
| signal | 传入后,abort() 触发优雅停止(关 ws + 本地代理)并 resolve;不传则 startNode 常驻不返回 |
凭证读取(onlyIfCredentialed/上报模型目录都靠它):Claude 走 ~/.claude 登录或 CLAUDE_CODE_OAUTH_TOKEN;Codex 走 ~/.codex/auth.json(codex login)——和节点 CLI 的凭证来源完全一致,因为底层是同一份实现。
许可
MIT。
