计费 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-DD
  • end 早于 start
  • limit 不是正整数(小于 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 全局唯一,可以建为成本明细表的主键,避免补跑脚本时重复入库。