计费 API(企业对账)
Upthos Relay 提供一组只读的 /v1/billing/* 接口,用于企业内部计费、对账和成本归因。三套端点共用同一鉴权方式,所有金额字段统一以人民币(CNY)用户实付价返回,可直接接入财务系统或自建成本看板。
适用场景
- 预算监控:定时拉取
/balance,在余额低于阈值时触发告警 / 自动充值。 - 每条调用归因:在业务侧记录响应头
x-request-id,再通过/usage反查每条调用的实际成本,写入内部成本归因表。 - 多团队分摊:以
api_key_id为维度对/usage.cost求和,按团队/项目分摊费用。 - 价格快照:定时拉取
/prices,留存effective_at价格快照,方便事后回溯。
鉴权
与 /v1/messages 一致,三种鉴权头任选其一(按优先级):
Authorization: Bearer sk-...
x-api-key: sk-...
x-goog-api-key: sk-...
API Key 必须分配到一个分组才能调用。三个端点都不区分平台分组,但返回内容会按当前 Key 所属分组的费率换算。
GET /v1/billing/balance
返回当前 API Key 的可用余额、Key 配额、订阅余量。
响应
{
"ok": true,
"account_id": 12345,
"key_id": 100,
"group_id": 7,
"currency": "CNY",
"available_balance": 376.55,
"wallet": {
"balance": 1234.56,
"credit_limit": 0
},
"key_quota": {
"limit_cny": 500.00,
"used_cny": 123.45,
"remaining_cny": 376.55
},
"subscription": {
"plan": "codex_standard",
"period_end": "2026-06-01T00:00:00Z",
"monthly_remaining_cny": 800.00
},
"key_status": "active",
"expires_at": "2026-12-31T00:00:00Z",
"updated_at": "2026-05-07T12:00:00Z"
}
上例中 Key 配额剩余
376.55 CNY比钱包余额1234.56 CNY更紧,所以available_balance = 376.55。注意key_quota.*字段直接来自 DB 原值(apiKey.quota是平台 $ 单位,与 CNY 1:1 等价),不会再乘当前分组费率。
字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
account_id |
int | 账户 ID(即 users.id) |
key_id |
int | API Key ID |
group_id |
int | null | Key 所属分组 ID;未分组时为 null |
currency |
string | 固定为 "CNY" |
available_balance |
number | 可用余额(CNY),= min(wallet, key_quota_remain, subscription_monthly_remain),是计费侧实际可消费的下限 |
wallet.balance |
number | 钱包余额(CNY) |
wallet.credit_limit |
number | 信用额度(CNY),暂未启用,固定为 0 |
key_quota |
object | null | 当 Key 设置了配额上限时返回;未设置则字段缺失 |
key_quota.limit_cny / used_cny / remaining_cny |
number | 配额上限 / 已用 / 剩余。apiKey.quota 是平台 $ 单位(与 CNY 1:1 等价,与钱包 balance 同单位),三者直接是 DB 原值,不做任何换算 |
subscription |
object | null | 当存在生效的订阅时返回 |
subscription.plan |
string | 订阅套餐名(如 codex_standard、claude_starter) |
subscription.period_end |
RFC3339 | 当前订阅周期结束时间 |
subscription.monthly_remaining_cny |
number | 本周期月度剩余额度。同样是平台 $ 单位 = CNY 1:1,例如 Claude Max 订阅 monthly_limit_usd=2000 → monthly_remaining_cny 上限为 2000,没有进一步乘 group rate |
key_status |
string | Key 状态:active / disabled / expired |
expires_at |
RFC3339 | null | Key 过期时间;永不过期则缺失 |
updated_at |
RFC3339 | 本次响应生成时间 |
可用余额怎么算:网关在每次扣费时取
available_balance = min(钱包余额, Key 配额剩余, 订阅月度剩余)。任意一项见底都会让整个 Key 不可用——客户端只看available_balance即可判断「还能不能调」。
GET /v1/billing/prices
返回当前 API Key 所属分组可调用的每个模型的 CNY 用户实付单价。
响应
{
"ok": true,
"currency": "CNY",
"group_id": 7,
"data": [
{
"model": "gpt-5.5",
"provider": "openai",
"group_id": 7,
"currency": "CNY",
"input_price_per_1m_tokens": 1.50,
"output_price_per_1m_tokens": 7.50,
"billing_unit": "token",
"price_available": true
},
{
"model": "claude-sonnet-4-6",
"provider": "anthropic",
"group_id": 7,
"currency": "CNY",
"input_price_per_1m_tokens": 16.50,
"output_price_per_1m_tokens": 82.50,
"cache_read_price_per_1m_tokens": 1.65,
"cache_write_price_per_1m_tokens": 20.625,
"billing_unit": "token",
"price_available": true
}
],
"effective_at": "2026-05-07T12:00:00Z"
}
字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
currency |
string | 固定 "CNY" |
group_id |
int | null | 当前 Key 所属分组 ID |
data[].model |
string | 模型 ID(与调用时使用的 model 字段一致) |
data[].provider |
string | 上游平台:openai / anthropic / google 等 |
data[].input_price_per_1m_tokens |
number | 输入价(CNY / 1M tokens) |
data[].output_price_per_1m_tokens |
number | 输出价(CNY / 1M tokens) |
data[].cache_read_price_per_1m_tokens |
number | Prompt 缓存命中价;仅当上游披露且非 0 时存在 |
data[].cache_write_price_per_1m_tokens |
number | Prompt 缓存写入价;仅当上游披露且非 0 时存在 |
data[].billing_unit |
string | 当前固定为 "token" |
data[].price_available |
bool | 是否在内部价目表里有匹配记录;false 表示模型存在但价目缺失(一般是新模型上架未完成定价) |
effective_at |
RFC3339 | 本次价目快照时间 |
缓存建议
价目变更频率低(一般以周为单位)。客户端可以按 effective_at 自行做 ETag 式比较,建议至少缓存 5 分钟,避免每次调用都拉一次价目。如果业务场景对价格变动敏感(如自动套利 / 跨厂比价),缩短到 1 分钟即可。
我们刻意不暴露
cost_ratio、rate_multiplier、上游 USD 原价等字段——只给最终 CNY 用户价。
GET /v1/billing/usage
按时间范围和游标分页返回当前 API Key 的调用流水,含每条调用的实际计费金额。
查询参数
| 参数 | 类型 | 默认值 | 说明 |
|---|---|---|---|
start |
YYYY-MM-DD |
7 天前 | 起始日(含当天 00:00:00 UTC) |
end |
YYYY-MM-DD |
今天 | 结束日(含当天 23:59:59 UTC) |
cursor |
string | (空) | 上一页响应中的 next_cursor,首页留空 |
limit |
int | 100 | 单页条数,默认 100;< 1 或非数字返回 400,> 1000 会被静默截断为 1000 |
参数错误的 4 种情形均返回 400 invalid_request_error:
start/end不是合法的YYYY-MM-DDend早于startlimit不是正整数(小于 1 或无法解析;大于 1000 不报错,会被截断为 1000)cursor不是上一次返回的有效游标(解码失败)
响应
{
"ok": true,
"currency": "CNY",
"data": [
{
"request_id": "req-abc",
"created_at": "2026-05-07T12:00:00Z",
"api_key_id": 100,
"group_id": 7,
"model": "claude-sonnet-4-6",
"requested_model": "claude-sonnet-4-6",
"upstream_model": "claude-sonnet-4-6-20250929",
"input_tokens": 1000,
"output_tokens": 500,
"cost": 0.0578,
"currency": "CNY",
"status": "success",
"duration_ms": 1234
}
],
"next_cursor": "MTcyMjk5..."
}
字段说明
| 字段 | 类型 | 说明 |
|---|---|---|
request_id |
string | 与响应头 x-request-id 一致,做对账主键 |
created_at |
RFC3339 | 调用产生的时间(UTC) |
api_key_id |
int | 调用时使用的 API Key ID |
group_id |
int | null | 调用时 Key 所属分组 ID |
model |
string | 网关侧最终结算用的模型 ID |
requested_model |
string | 客户端请求里写的 model 字段(如做了 model_mapping,会与 model 不同) |
upstream_model |
string | 上游真正调用的模型 ID(含日期后缀,如 claude-sonnet-4-6-20250929) |
input_tokens / output_tokens |
int | 输入 / 输出 token 计数 |
cache_creation_tokens / cache_read_tokens |
int | Prompt 缓存写 / 读 token;为 0 时省略 |
cost |
number | 该条调用的 CNY 用户实付金额(已按调用时的费率快照换算,历史费率变更不影响) |
currency |
string | 固定 "CNY" |
status |
string | 当前固定 "success"(usage_logs 只记成功调用,错误流水在 ops_error_logs 中独立维护) |
duration_ms |
int | 上游响应耗时(毫秒);缺失时省略 |
next_cursor |
string | 下一页游标。为空 / 字段缺失即代表已经到末尾 |
游标分页用法
# 第一页
curl 'https://relay.upthos.com/v1/billing/usage?start=2026-05-01&end=2026-05-07&limit=200' \
-H "Authorization: Bearer $UPTHOS_API_KEY"
# 后续页:把上一次响应里的 next_cursor 原样回传
curl 'https://relay.upthos.com/v1/billing/usage?start=2026-05-01&end=2026-05-07&limit=200&cursor=MTcyMjk5...' \
-H "Authorization: Bearer $UPTHOS_API_KEY"
直到 next_cursor 为空或字段不存在,即可结束循环。
响应头
所有 /v1 端点(不仅是 billing)都会带上以下响应头,对账时按需采集:
| Header | 含义 |
|---|---|
x-request-id |
单次调用的 UUID。对账主键,与 /billing/usage.data[].request_id 完全一致。客户端如果在请求头里携带 x-request-id,网关会原样回填。 |
x-upthos-key-id |
当前 API Key 的 id(十进制) |
x-upthos-group-id |
当前 Key 所属分组的 id(十进制);Key 未分组时该 header 不返回 |
x-upthos-billing-currency |
固定 CNY,提示后续 billing 端点的金额口径 |
字段单位约定
- 所有金额字段(
available_balance/wallet.balance/key_quota.limit_cny/subscription.monthly_remaining_cny/cost等)统一为 CNY 用户实付价。 - Token 计数(
input_tokens/output_tokens/cache_*_tokens)为原始整数,不做单位换算。 - 时间字段统一为 RFC3339 / ISO 8601 格式,时区一律 UTC(
Z结尾)。
三个金额维度共享同一计费单位:
wallet.balance、key_quota.*、subscription.monthly_remaining_cny共享同一计费单位——即用户实际支付的 CNY 量(也就是平台 $ 显示口径,1:1 等价于人民币)。Prices 接口的价格则是从上游真实 USD 换算后的 CNY 单价(× group_rate)。同一调用消耗 ¥X 价格的 token 后,wallet/quota/sub 的剩余值就减少 X,换算路径自动闭合。关于 CNY 与 USD 的换算口径:我们的成本侧采用
$1 ≈ ¥1的隐式 1:1 换算(再 × 当前分组rate_multiplier)。Stripe 的display_fx_rate(≈6.82)仅用于美元卡支付时把美元入账为人民币余额,不参与计费扣减,也不参与本接口的金额换算。/v1/billing/prices的单价 = 上游 USD 价 × group rate;/v1/billing/balance的 wallet/quota/sub 三项已经是平台 $ = CNY 单位,不再做任何换算。
错误码对照
错误响应沿用 Anthropic 错误格式:
{
"type": "error",
"error": {
"type": "authentication_error",
"message": "Invalid API key"
}
}
| HTTP | error.type |
含义 |
|---|---|---|
| 401 | authentication_error |
API Key 缺失或无效 |
| 403 | permission_error |
Key 未分配分组、被禁用或权限不足 |
| 400 | invalid_request_error |
请求参数错误(坏日期 / 坏 limit / 坏 cursor / end < start) |
| 404 | not_found_error |
路径或资源不存在 |
| 429 | rate_limit_error |
触发速率 / 并发限制 |
| 500 | api_error |
网关内部错误 |
对账最佳实践
模式 1:按 request_id 反查单次调用成本
业务侧调一次 /v1/messages,从响应头拿到 x-request-id,写入业务日志。事后如果需要查这条调用的真实成本,按时间窗口拉 /usage 然后用 request_id 关联即可:
import os, requests
BASE = "https://relay.upthos.com"
HEADERS = {"Authorization": f"Bearer {os.environ['UPTHOS_API_KEY']}"}
# 1) 业务调用,记录 x-request-id
r = requests.post(
f"{BASE}/v1/messages",
headers={**HEADERS, "anthropic-version": "2023-06-01"},
json={
"model": "claude-sonnet-4-6",
"max_tokens": 256,
"messages": [{"role": "user", "content": "hi"}],
},
)
req_id = r.headers["x-request-id"]
log_to_business_db(req_id=req_id, biz_action="some-feature")
# 2) 事后按时间窗口反查
def find_cost(req_id, day):
cursor = ""
while True:
params = {"start": day, "end": day, "limit": 1000}
if cursor:
params["cursor"] = cursor
resp = requests.get(f"{BASE}/v1/billing/usage", headers=HEADERS, params=params).json()
for row in resp["data"]:
if row["request_id"] == req_id:
return row["cost"] # CNY
cursor = resp.get("next_cursor", "")
if not cursor:
return None
模式 2:每日定时对账 + 团队分摊
每天凌晨拉前一日全部流水,按 api_key_id 求和写入内部成本归因表:
#!/usr/bin/env bash
# nightly-reconcile.sh — 每天 02:00 跑
set -euo pipefail
YESTERDAY=$(date -u -d 'yesterday' +%F) # macOS: $(date -u -v-1d +%F)
CURSOR=""
while :; do
if [[ -n "$CURSOR" ]]; then
URL="https://relay.upthos.com/v1/billing/usage?start=${YESTERDAY}&end=${YESTERDAY}&limit=1000&cursor=${CURSOR}"
else
URL="https://relay.upthos.com/v1/billing/usage?start=${YESTERDAY}&end=${YESTERDAY}&limit=1000"
fi
RESP=$(curl -sS "$URL" -H "Authorization: Bearer $UPTHOS_API_KEY")
echo "$RESP" | jq -c '.data[] | {request_id, api_key_id, model, cost, created_at}' \
>> "/var/log/upthos/usage-${YESTERDAY}.jsonl"
CURSOR=$(echo "$RESP" | jq -r '.next_cursor // ""')
[[ -z "$CURSOR" ]] && break
done
# 落库后按 api_key_id 聚合到内部成本归因表
psql "$DSN" -c "
COPY staging_usage(request_id, api_key_id, model, cost, created_at)
FROM '/var/log/upthos/usage-${YESTERDAY}.jsonl' (FORMAT json);
INSERT INTO daily_cost_attribution(day, api_key_id, total_cny)
SELECT '${YESTERDAY}'::date, api_key_id, SUM(cost)
FROM staging_usage
WHERE created_at::date = '${YESTERDAY}'::date
GROUP BY api_key_id
ON CONFLICT (day, api_key_id) DO UPDATE SET total_cny = EXCLUDED.total_cny;
"
幂等小贴士:
request_id全局唯一,可以建为成本明细表的主键,避免补跑脚本时重复入库。