GET /v1/balance 返回 Router One API Key 所属账号的预付余额,单位 USD,这样盯着钱包的可以是一台服务器,而不是一个盯着控制台的人。响应里有三个数:balance 是现在就能花的金额(已包含赠送余额),reserved_balance 是网关为进行中的请求暂时预留的金额,total_balance 是两者之和。告警看 balance,对账看 total_balance。下面依次是:按自己的消耗速度定阈值、给并发造成的短暂下探做防抖、两份可以直接复制的监控脚本,以及为什么 one-api / new-api 自带的余额按钮干不了这件事。
请求与响应
一个 GET,没有请求体,也没有查询参数。账号下任意一把普通 API Key 都可以用,传法和调用模型时一样:
curl -sS https://api.router.one/v1/balance \
-H "Authorization: Bearer $ROUTER_ONE_API_KEY"
{
"object": "balance",
"currency": "USD",
"balance": 12.345678,
"reserved_balance": 0.5,
"total_balance": 12.845678
}
| 字段 | 含义 | 用在哪里 |
|---|---|---|
object | 固定为 balance | 确认解析到的是正确的响应 |
currency | 固定为 USD | 告警文案里标注币种 |
balance | 当前可消费金额,已包含赠送余额 | 低余额告警 |
reserved_balance | 为进行中的请求预留的金额;每个请求结算后,多预留的部分退回 balance | 解释短暂下探 |
total_balance | balance + reserved_balance | 对账与观察长期趋势 |
对监控来说有三点要紧。这个数属于账号而不是某一把 Key:用哪把 Key 查,结果都一样。响应头是 Cache-Control: private, no-store,每次轮询拿到的都是实时值。这个接口有独立限流,每把 Key 每分钟 60 次,和这把 Key 的推理限流分开计数:轮询不会占用生产流量的额度,生产流量再忙也不会把监控挤掉。接口参考:GET /v1/balance。
按自己的消耗速度定阈值
「低于 $10 就告警」这种固定数字,对个人项目是几周的余量,对高峰期的中转站只是几分钟。应该自己推出来:
- 在 Dashboard 的用量视图里看最近的花费。取忙的一天,不要取平均,换算成每小时多少美元。
- 想清楚一个人从看到告警到完成充值需要几小时。把夜里和周末算进去,通常是 12 到 48 小时,而不是 1 小时。
- 阈值 = 每小时花费 × 这个小时数。高峰期每小时约 $3、反应窗口 24 小时,就应该在低于 $72 时告警。
- 流量涨了就重算。流量少的时候定下的阈值,到真正要用的时候已经偏低。
balance 为什么会下探,怎样防抖
请求进行中时,网关会把预估费用暂时预留在 reserved_balance;请求结算后,多预留的部分退回 balance。并发高的时候同时有很多笔预留,balance 会短暂低于结算后的数值,而 total_balance 保持平稳。只看一次低读数就告警的监控,报出来的是流量,不是钱。两个办法,可以一起用:
- 要求连续多次偏低。
balance连续 2–3 次轮询都低于阈值才告警。预留会随请求结算而释放;真正快花完的钱包会一直低。 - 把信号拆成两路。 慢信号「今天该充值了」用
total_balance对比阈值,因为预留不会改变它;快信号用balance对比一个小得多的下限。真正打断流量的是后者:可消费金额不够覆盖一个请求的初始预留时,即使total_balance更高,这个请求也会以 HTTP 402 失败(402 诊断)。
每 1–5 分钟轮询一次就够了。限流允许的频率远高于此,但余额变化没有那么快:连续三次的防抖加上 2 分钟的间隔,大约六分钟就能确认一次真实的不足。
监控一:cron 里的 bash + curl + jq
Key 和 webhook 地址都放在环境变量里。webhook 收到的是通用的 {"text": "..."} JSON,请按你的聊天或告警工具要求的格式修改。
#!/usr/bin/env bash
# balance-check.sh:由 cron 运行,例如每 5 分钟一次:
# */5 * * * * . /etc/router-one-monitor.env && /usr/local/bin/balance-check.sh
# (env 文件里是两行 export,权限 chmod 600)
set -euo pipefail
: "${ROUTER_ONE_API_KEY:?set ROUTER_ONE_API_KEY}"
: "${ALERT_WEBHOOK_URL:?set ALERT_WEBHOOK_URL}"
THRESHOLD_USD="${THRESHOLD_USD:-25}" # 你自己的数:每小时花费 x 需要的余量小时数
STRIKES_NEEDED="${STRIKES_NEEDED:-3}" # 连续几次偏低才告警
STATE_FILE="${STATE_FILE:-/var/tmp/router-one-balance.strikes}"
body=$(curl -fsS --max-time 10 \
-H "Authorization: Bearer $ROUTER_ONE_API_KEY" \
https://api.router.one/v1/balance)
balance=$(jq -er '.balance' <<<"$body")
total=$(jq -er '.total_balance' <<<"$body")
if jq -en --argjson b "$balance" --argjson t "$THRESHOLD_USD" '$b < $t' >/dev/null; then
strikes=$(( $(cat "$STATE_FILE" 2>/dev/null || echo 0) + 1 ))
else
strikes=0
fi
echo "$strikes" > "$STATE_FILE"
# 每轮偏低只告警一次;充值后计数归零,重新生效。
if [ "$strikes" -eq "$STRIKES_NEEDED" ]; then
jq -n --arg text "Router One balance low: $balance USD spendable, $total USD total (threshold $THRESHOLD_USD USD)" '{text: $text}' |
curl -fsS --max-time 10 -H "Content-Type: application/json" -d @- "$ALERT_WEBHOOK_URL" >/dev/null
fi
curl -f 会让 401、429 或服务不可用时以非零状态退出,cron 自己的失败通知就能发现监控已经失明。告警只在连续次数达到设定值的那一次发出,而不是每五分钟一条、直到有人充值。
监控二:带退避的 Python 轮询
只用标准库。遇到 429、5xx 和网络错误按指数退避重试,401 不重试,连续三轮检查失败时单独发一条告警。
#!/usr/bin/env python3
"""轮询 GET /v1/balance;余额持续偏低时推送到 webhook。"""
import json, os, time, urllib.error, urllib.request
API_KEY = os.environ["ROUTER_ONE_API_KEY"]
WEBHOOK = os.environ["ALERT_WEBHOOK_URL"]
THRESHOLD = float(os.environ.get("THRESHOLD_USD", "25"))
INTERVAL = float(os.environ.get("POLL_SECONDS", "120")) # 1-5 分钟一次就够
STRIKES_NEEDED = 3 # 连续几次偏低才告警
def get_balance():
req = urllib.request.Request(
"https://api.router.one/v1/balance",
headers={"Authorization": f"Bearer {API_KEY}"},
)
delay = 2
for _ in range(6):
try:
with urllib.request.urlopen(req, timeout=10) as res:
return json.load(res)
except urllib.error.HTTPError as err:
if err.code != 429 and err.code < 500:
raise # 401 等 4xx:去修 Key,重试没有用
except urllib.error.URLError:
pass # 网络抖动:重试
time.sleep(delay) # 指数退避:2、4、8、16、32 秒
delay = min(delay * 2, 60)
raise RuntimeError("no answer from /v1/balance after 6 attempts")
def notify(text):
data = json.dumps({"text": text}).encode()
req = urllib.request.Request(WEBHOOK, data=data, headers={"Content-Type": "application/json"})
urllib.request.urlopen(req, timeout=10).close()
strikes = failures = 0
while True:
try:
b = get_balance()
failures = 0
strikes = strikes + 1 if b["balance"] < THRESHOLD else 0
if strikes == STRIKES_NEEDED: # 每轮偏低只告警一次
notify(f"Router One balance low: {b['balance']:.2f} USD spendable, "
f"{b['total_balance']:.2f} USD total (threshold {THRESHOLD:.2f} USD)")
except Exception as exc: # 监控失明本身也是事故
failures += 1
if failures == 3:
notify(f"Router One balance check failing: {exc}")
time.sleep(INTERVAL)
用你平时守护其他常驻进程的方式来运行它。检查失败的含义是「未知」,不是「为零」。
转售方与中转站:这个数字保护的是什么
在一个账号上做转售或跑中转站,其实有两种上限,回答的是两个不同的问题。
每个客户的上限,是各把 Key 的 maxSpend 消费上限。 钱包由账号下所有 Key 共用,所以 /v1/balance 说不出某个客户还剩多少。这件事归 Key 管:每个客户或分组一把 Key,各自设置 maxSpend;某个客户触顶时,只有那把 Key 返回 HTTP 402,钱包和其他 Key 不受影响。这套做法见用带消费上限的 Key 做转售。配置页面:转售方概览、批发 LLM API、白标 LLM API,one-api / new-api 用户另见中转站指南。
/v1/balance 保护的是整个生意。 钱包本身不够支付一个请求时,模型调用会在所有 Key 上同时以 HTTP 402 失败,不管请求来自哪个客户,也不管这个客户自己的上限还剩多少。对你的客户来说这就是全站故障,而且没有任何一把 Key 的上限会提前提醒你,因为没有哪把 Key 出现异常。要告警的正是这件事,上面的阈值也是按它来定的:看的是所有客户流量的总和。
用一把专门的监控 Key。 监控从不调用模型,这把 Key 不需要任何花费:给它设一个很小的 maxSpend。即使泄漏也刷不出用量,轮换它也不会碰到客户的 Key 和中转站渠道用的 Key。
中转软件自带的余额按钮管不到这里。 one-api(点击渠道的余额单元格)和 new-api(「更新余额」按钮)跑的都是 controller/channel-billing.go 里的查询逻辑。对类型为 OpenAI 的渠道(以及自定义渠道 Custom),它向渠道的 base URL 发两个 GET 请求:先 /v1/dashboard/billing/subscription,再 /v1/dashboard/billing/usage,然后用 hard_limit_usd 减去 total_usage / 100 得出余额。这两个是 OpenAI 的旧版计费路径;one-api 自己的渠道页也已经提示:OpenAI 渠道不再支持通过 key 获取余额。Router One 没有实现它们,也没有实现 /dashboard/billing/credit_grants,所以这个按钮读不到这个钱包:任何非 200 的响应都会变成错误文本 status code: <n>,渠道里保存的余额保持不变。没有专属查询逻辑的渠道类型会返回「尚未实现」,在 one-api 里也包括单独的「OpenAI 兼容」类型。new-api 的定时刷新(CHANNEL_UPDATE_FREQUENCY)跑的是同一套查询,查询出错的渠道会被直接跳过,所以它同样不会告警。截至 2026-09-19 的 new-api main 分支,「高级自定义」渠道类型可以自己配置余额路由,但只有响应是带数字 total_available 的 credit_summary 对象时才会保存数值;其他 JSON(包括上面的 balance 对象)只会显示在「无法识别余额响应」下面,不会保存。所以请在中转站旁边跑上面两份监控之一,并用它自己的 Key。
这个接口不做什么,以及它的错误
- 不报单把 Key 的花费。 它不会告诉你某把 Key 的
maxSpend已经用了多少。单 Key 花费在 Dashboard → Logs 和用量视图里:按 Key 追踪 LLM API 成本、LLM 成本追踪。 - 不报订阅配额。 它只报预付钱包,不报订阅套餐的请求配额(/pricing)。
- 没有充值 API。 充值由账号所有者在控制台完成:支付宝或银行卡走同一个托管收银台,或者用 USDT/USDC。没有任何接口可以加钱,也不会自动充值;告警的任务就是及时通知到人。
- 没有历史。 每次调用只返回一个实时值。想要余额曲线,就自己保存轮询结果;花了多少、是哪把 Key 花的,到 Dashboard → Logs 和用量视图里对账。
错误体是 OpenAI 风格的 {"error":{"message","type","code","request_id"}};联系支持时请带上 request_id。Key 缺失或无效返回 401 AUTH_INVALID_API_KEY(authentication_error):不要重试,去修 Key。同一把 Key 每分钟超过 60 次返回 429 RATE_LIMIT_EXCEEDED(rate_limit_error):退避后重试,并检查是不是多个监控共用了一把 Key。在模型调用上,钱的问题返回 402,速度的问题返回 429;这一点和官方 OpenAI API 不同,那边额度耗尽表现为 429 insufficient_quota(对比)。遇到 429 退避重试;遇到 402 不要重试,去充值。
落地步骤:在 router.one 的 Dashboard → API Keys 里建一把专用 Key,设一个很小的 maxSpend,和 webhook 地址一起放进环境变量,再把上面两份监控之一加进定时任务。
常见问题
低余额告警应该看哪个字段? balance。它是现在就能花的金额,已包含赠送余额。total_balance 还加上了为进行中请求预留的金额,所以更平稳、更适合对账;但当可消费金额不够覆盖一个请求的初始预留时,这个请求会以 402 失败。
/v1/balance 可以多久轮询一次?会占用推理限流吗? 这个接口有独立限流,每把 Key 每分钟 60 次,所以轮询不会占用这把 Key 的推理限流。每 1–5 分钟一次就够了。超过限制会返回 429 RATE_LIMIT_EXCEEDED,退避后重试即可。
每把 Key 或每个客户有自己的余额吗? 没有。余额是账号的预付钱包,账号下每把 Key 查到的都是同一个数。每个客户的上限是各把 Key 的 maxSpend 消费上限,单 Key 花费在 Dashboard → Logs 里。/v1/balance 防的是共用钱包被花空、所有 Key 一起返回 402 的情况。
one-api 或 new-api 的余额按钮能用在 Router One 渠道上吗? OpenAI 类型和自定义类型的渠道不行。这个按钮请求的是 /v1/dashboard/billing/subscription 和 /v1/dashboard/billing/usage,也就是 OpenAI 的旧版计费路径,Router One 没有实现,所以查询会以状态码错误结束,渠道里保存的余额不会变化。请改用一个小的 cron 任务轮询 /v1/balance。