> https://router.one/zh/blog/llm-api-balance-monitoring 的 Markdown 镜像，供 AI 助手与爬虫使用。Router One 是 OpenAI 兼容的统一 LLM API 网关。
> 发布：2026-09-19 · 作者：Router One Team

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

_用 GET /v1/balance 在服务端监控预付余额：三个字段怎么看、按消耗速度定阈值、并发下探怎样防抖，附 bash 与 Python 监控脚本，并说明 one-api / new-api 自带的余额按钮为什么读不到这个钱包。_

`GET /v1/balance` 返回 Router One API Key 所属账号的预付余额，单位 USD，这样盯着钱包的可以是一台服务器，而不是一个盯着控制台的人。响应里有三个数：`balance` 是现在就能花的金额（已包含赠送余额），`reserved_balance` 是网关为进行中的请求暂时预留的金额，`total_balance` 是两者之和。告警看 `balance`，对账看 `total_balance`。下面依次是：按自己的消耗速度定阈值、给并发造成的短暂下探做防抖、两份可以直接复制的监控脚本，以及为什么 one-api / new-api 自带的余额按钮干不了这件事。

## 请求与响应

一个 GET，没有请求体，也没有查询参数。账号下任意一把普通 API Key 都可以用，传法和调用模型时一样：

```bash
curl -sS https://api.router.one/v1/balance \
  -H "Authorization: Bearer $ROUTER_ONE_API_KEY"
```

```json
{
  "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](https://router.one/zh/docs/account/getBalance)。

## 按自己的消耗速度定阈值

「低于 $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 诊断](https://router.one/zh/llm-api-error-codes)）。

每 1–5 分钟轮询一次就够了。限流允许的频率远高于此，但余额变化没有那么快：连续三次的防抖加上 2 分钟的间隔，大约六分钟就能确认一次真实的不足。

## 监控一：cron 里的 bash + curl + jq

Key 和 webhook 地址都放在环境变量里。webhook 收到的是通用的 `{"text": "..."}` JSON，请按你的聊天或告警工具要求的格式修改。

```bash
#!/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 不重试，连续三轮检查失败时单独发一条告警。

```python
#!/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 做转售](https://router.one/zh/blog/resell-llm-api-spend-capped-keys)。配置页面：[转售方概览](https://router.one/zh/llm-api-reseller)、[批发 LLM API](https://router.one/zh/wholesale-llm-api)、[白标 LLM API](https://router.one/zh/white-label-llm-api)，one-api / new-api 用户另见[中转站指南](https://router.one/zh/llm-api-relay-station)。

**/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 成本](https://router.one/zh/blog/track-llm-api-costs-per-key)、[LLM 成本追踪](https://router.one/zh/llm-cost-tracking)。
- **不报订阅配额。** 它只报预付钱包，不报订阅套餐的请求配额（[/pricing](https://router.one/zh/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`（[对比](https://router.one/zh/openai-insufficient-quota)）。遇到 429 退避重试；遇到 402 不要重试，去充值。

落地步骤：在 [router.one](https://router.one/zh) 的 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。

## 相关页面

- 本页规范地址：https://router.one/zh/blog/llm-api-balance-monitoring
- LLM API 网关与路由：https://router.one/zh/llm-api-gateway
- 全部博客文章：https://router.one/zh/blog
- 模型与每模型 token 价格：https://router.one/zh/models （markdown：https://router.one/zh/models.md ）
- 定价：https://router.one/zh/pricing
- API 文档（markdown）：https://router.one/zh/docs.md
- 公司事实（markdown）：https://router.one/zh/facts/company.md
