跳到主要内容
Router One
返回博客

LLM API 余额监控:用 GET /v1/balance 在钱包花空前告警

发布作者Router One 团队方法说明

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_balancebalance + reserved_balance对账与观察长期趋势

对监控来说有三点要紧。这个数属于账号而不是某一把 Key:用哪把 Key 查,结果都一样。响应头是 Cache-Control: private, no-store,每次轮询拿到的都是实时值。这个接口有独立限流,每把 Key 每分钟 60 次,和这把 Key 的推理限流分开计数:轮询不会占用生产流量的额度,生产流量再忙也不会把监控挤掉。接口参考:GET /v1/balance

按自己的消耗速度定阈值

「低于 $10 就告警」这种固定数字,对个人项目是几周的余量,对高峰期的中转站只是几分钟。应该自己推出来:

  1. 在 Dashboard 的用量视图里看最近的花费。取忙的一天,不要取平均,换算成每小时多少美元。
  2. 想清楚一个人从看到告警到完成充值需要几小时。把夜里和周末算进去,通常是 12 到 48 小时,而不是 1 小时。
  3. 阈值 = 每小时花费 × 这个小时数。高峰期每小时约 $3、反应窗口 24 小时,就应该在低于 $72 时告警。
  4. 流量涨了就重算。流量少的时候定下的阈值,到真正要用的时候已经偏低。

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_availablecredit_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_KEYauthentication_error):不要重试,去修 Key。同一把 Key 每分钟超过 60 次返回 429 RATE_LIMIT_EXCEEDEDrate_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。

相关权威页面

这篇文章归入「LLM API 网关与路由」主题,以下页面作为商业页、配置文档、证据页和信任事实源。

商业主页面Router One API 网关承接统一模型调用、路由、fallback、预算和观测的产品首页。API 文档Router One API 文档OpenAI 兼容端点、CLI 配置和模型调用示例。证据页智能路由方法论路由信号、模型与请求 ID,以及客户侧 trace 的字段边界。对比页OpenRouter 替代方案专业对比全球模型目录与中国友好路由、支付能力的差异。信任页可引用事实表面向搜索爬虫、AI 答案引擎和客户的稳定事实源。数据留存数据留存政策prompt/completion 留存边界和请求元数据政策。网关页面统一 LLM API 网关一个 OpenAI 兼容端点接入整个模型目录,含路由、fallback 与预算控制。路由页面智能模型路由候选排序如何使用延迟、公示成本与可靠性信号。故障转移页面LLM 供应商故障转移什么样的请求才符合在另一条健康供应商路由上重试的条件。可观测页面逐请求 Trace 日志每个请求的模型与请求 ID、Token、延迟、状态与报错。兼容性页面OpenAI 兼容端点沿用 OpenAI SDK,只改 base URL 即可触达各个模型系列。成本追踪页面LLM 成本追踪按 Key、按模型、按请求的花费归因,配合硬性消费上限。转售方页面在 Router One 上搭你自己的 API 服务带消费上限的客户 Key、按 Key 的用量归因,以及明确的「不提供」清单。客户端接入SDK 与客户端配置指南把任意编程 agent、SDK、聊天客户端或 LLM 应用平台指向同一个端点,每个都有专属指南。模型对比模型价格与上下文两两对比每百万 token 单价、上下文窗口与能力,渲染自实时模型目录。

相关阅读