Files
new-api-key-usage/docs/upstream/NewAPI-sk-key接口文档.md
T
skycurtain e1c6d2153e feat(server): 同步引擎与 seed(阶段 4)
- 引擎:tick(5 分钟 + 防重入 + 启动即跑)· 单轮串行遍历 active key · 日志整批单事务入库(ON CONFLICT DO NOTHING + log_synced_at 取请求发起时刻)· 额度采集(上游字段全组,独立 try)· QPU 采样(基线 / 值变化才追加)· 每轮摘要与超期 warning
- 401 状态机:N=3 摘除(disabled_source=auto_401);403/500/网络不计数;解密失败跳过不计数
- 纯逻辑抽层:policy(计数 / 合并判定)· dedupe(去重键与内容指纹)· log-row(日志映射,与 seed 共用)
- seed 脚本(幂等):假 key + 984 条日志 + QPU 基线 + 示例购买
- 实测:401 端到端 1→2→3 摘除且摘除后不再轮询;幂等重跑新增 0 条
- 实证发现:错误行 request_id 不唯一(8 组 ×3)——记入实施计划「待定问题 #5」与 upstream 文档,待作者裁决
- AGENTS.md / 根 package.json 增 seed 命令
2026-09-15 00:46:46 +08:00

479 lines
20 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
# NewAPI sk-key 查询接口文档
> 基于源码实证调研(QuantumNous/new-api),整理所有可通过 `sk-` API key 直接访问的查询接口。
>
> 调研日期:2025-07 月 · 源码版本:main 分支
---
## 0. 通用说明
### 认证方式
所有接口均使用 API key 鉴权,在请求头中携带:
```
Authorization: Bearer sk-xxxxxxxxxxxx
```
**不需要**账号登录态(access token / session)。这是仅凭 sk-key 即可查询的关键前提。
### BaseURL
即 NewAPI 站点地址,例如 `https://your-newapi-instance.com`。所有路径前不带多余前缀,直接拼接。
### 两种鉴权中间件
| 中间件 | 说明 |
|--------|------|
| `TokenAuth()` | 标准版:校验 key 有效、未过期、有剩余额度。过期或耗尽的 key 会被拒绝。 |
| `TokenAuthReadOnly()` | 宽松版:只校验 key 存在、token 未被显式禁用、用户未封禁。**key 已过期、额度耗尽仍可查询**;**显式禁用(Disabled)的 key 会被 401 拒绝**(2026-09-13 源码复核,`middleware/auth.go` 注释原文:only explicitly disabled tokens are denied)。 |
> `/api/usage/token` 和 `/api/log/token` 用的是 `TokenAuthReadOnly()`,所以 key 额度耗尽、甚至过期后,仍能查到自己的用量记录;但 key 被管理员**显式禁用**后会收到 401(见上表)。
### 已确认的换算参数
| 参数 | 值 | 来源 |
|------|-----|------|
| `QuotaPerUnit` | 500000 | 实测验证:接口① `total_used` ÷ 500000 = 接口④ `total_usage` ÷ 100 |
### 关键开关:DisplayTokenStatEnabled
| 接口 | 受开关影响? |
|------|-------------|
| billing 接口(③④⑤⑥) | ✅ 受影响。开启 -> 返回 token 级额度;关闭 -> 回退到账号级额度(暴露整个账号余额) |
| `/api/usage/token`、`/api/log/token` | ❌ 不受影响,恒返回 token 级数据 |
**实测已确认当前平台开关为开启状态**(billing 接口返回的 `hard_limit_usd` 是 token 级占位值,非账号总额)。
开关位于后台:设置 -> 计费设置(Billing)-> "货币与显示"(Currency & Display)-> `Display Token Statistics`。仅管理员可改。
---
## 1. 接口清单总览
| 序号 | 方法 | 路径 | 鉴权 | 用途 |
|------|------|------|------|------|
| ① | GET | `/api/usage/token` | TokenAuthReadOnly | 查 token 的累计已用/剩余/总额(原始 quota) |
| ② | GET | `/api/log/token` | TokenAuthReadOnly | 查该 key 最近 1000 条调用日志 |
| ③ | GET | `/v1/dashboard/billing/subscription` | TokenAuth | 查总额度上限(OpenAI 兼容,美元) |
| ④ | GET | `/v1/dashboard/billing/usage` | TokenAuth | 查累计已用(OpenAI 兼容,美分) |
| ⑤ | GET | `/dashboard/billing/subscription` | TokenAuth | 同 ③,不带 /v1 前缀 |
| ⑥ | GET | `/dashboard/billing/usage` | TokenAuth | 同 ④,不带 /v1 前缀 |
| ⑦ | GET | `/v1/models` | TokenAuth | 列出可用模型(OpenAI 格式) |
| ⑧ | GET | `/v1/models/:model` | TokenAuth | 查单个模型详情 |
| ⑨ | GET | `/v1beta/models` | TokenAuth | 列出可用模型(Gemini 格式) |
| ⑩ | GET | `/v1beta/openai/models` | TokenAuth | 列出可用模型(OpenAI 格式) |
> ③和⑤是同一个接口,④和⑥是同一个接口,只是路径前缀不同(为兼容不同客户端写法)。测一组即可。
>
> ⑦~⑩是模型列表接口,与用量统计无关,列出仅为完整覆盖。
---
## 2. 用量/额度查询接口详解
### ① GET `/api/usage/token` - token 级用量汇总
**鉴权**:`TokenAuthReadOnly()`(宽松版:key 过期/额度耗尽也能查;被显式禁用则 401)
**请求参数**:无
**请求示例**:
```
GET /api/usage/token
Authorization: Bearer sk-xxxx
```
**返回结构**(源码:`controller/token.go` -> `GetTokenUsage`):
```json
{
"code": true,
"message": "ok",
"data": {
"object": "token_usage",
"name": "令牌名称",
"total_granted": 6012,
"total_used": 673419457,
"total_available": -673413445,
"unlimited_quota": true,
"model_limits": {},
"model_limits_enabled": false,
"expires_at": 0
}
}
```
**字段说明**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `name` | string | 令牌名称 |
| `total_granted` | int | 总额度 = RemainQuota + UsedQuota(原始 quota 单位) |
| `total_used` | int | **累计已用**(原始 quota 单位)。从 key 创建以来累计,非当月。换算美元:`total_used ÷ 500000` |
| `total_available` | int | 剩余 = total_granted − total_used。无限额度时为负数,无实际意义 |
| `unlimited_quota` | bool | 是否无限额度。true 时 total_granted/total_available 无实际意义 |
| `model_limits` | object | 模型限制(map) |
| `model_limits_enabled` | bool | 是否启用模型限制 |
| `expires_at` | int | 过期时间(Unix 秒),0 = 永不过期 |
**注意事项**:
- `total_used` 是**累计值**,从 key 创建以来累计,不是当月用量。
- 额度单位是**原始 quota 整数**,需自行除以 `QuotaPerUnit`(默认 500000)换算美元。
- 禁用再解禁不会重置 `total_used`(源码验证:`Update()` 的 `.Select(...)` 列表不含 `used_quota`)。
---
### ② GET `/api/log/token` - token 级调用日志
**鉴权**:`TokenAuthReadOnly()`
**请求参数**:无(不支持任何 query 参数)
**请求示例**:
```
GET /api/log/token
Authorization: Bearer sk-xxxx
```
**返回结构**(源码:`controller/log.go:62` -> `GetLogByKey`):
```json
{
"success": true,
"message": "",
"data": [
{
"id": 123456,
"user_id": 12,
"created_at": 1751328000,
"type": 2,
"content": "模型倍率 1.0,分组倍率 1.0",
"username": "用户名",
"token_name": "令牌名",
"model_name": "gpt-4o",
"quota": 2500,
"prompt_tokens": 1500,
"completion_tokens": 500,
"use_time": 2,
"is_stream": true,
"channel": 0,
"channel_name": "",
"token_id": 100,
"group": "default",
"ip": "",
"request_id": "20260911113652…",
"other": "{\"billing_source\":\"subscription\",\"cache_tokens\":29056,\"frt\":3931,\"model_ratio\":0.075,\"request_path\":\"/v1/messages\",\"stream_status\":{\"status\":\"ok\"},…}"
}
]
}
```
**字段说明**(共 21 个字段;已按实测响应 + 上游源码校正,2026-09-11):
| 字段 | 类型 | 说明 |
|------|------|------|
| `id` | int | 展示序号,**每次从 1 重编**——跨请求无意义,绝不能用于去重 |
| `user_id` | int | 账号 ID(同一实例内恒定) |
| `created_at` | int | 调用时间(UTC Unix 秒) |
| `type` | int | 日志类型(完整取值见下);token 日志实见 2=消费、5=错误 |
| `content` | string | 消费记录为空串;错误记录为错误详情文本 |
| `username` | string | 账号用户名 |
| `token_name` | string | 令牌名 |
| `model_name` | string | 调用的模型名 |
| `quota` | int | 本次消耗的 quota(换算美元:`quota ÷ 500000`);错误记录为 0 |
| `prompt_tokens` | int | 输入 token 数;错误记录为 0 |
| `completion_tokens` | int | 输出 token 数;错误记录为 0 |
| `use_time` | int | 耗时(秒);错误记录为 0 |
| `is_stream` | bool | 是否流式 |
| `channel` | int | 渠道 ID |
| `channel_name` | string | 渠道名(普通用户视角为空串) |
| `token_id` | int | 上游 token ID(对账用) |
| `group` | string | 分组名(默认 `default`) |
| `ip` | string | 请求 IP(普通用户视角为空串) |
| `request_id` | string | 请求 ID(格式:`yyyyMMddHHmmss` 前缀 + 随机串);JSON tag 为 `omitempty`,但源码写入时有兜底保证非空——**去重唯一依据**。**补充实测(2026-09-15)**:错误场景下 `request_id` **不保证行级唯一**——1000 条实测样本中有 8 个 `request_id` 各对应 3 条 `type=5` 错误记录(同一请求的多次失败 / 重试,`content` 的 `status_code` 可不同);按 `request_id` 去重会把它们合并为 1 条(消费类 `type=2` 样本中未观察到重复) |
| `upstream_request_id` | string | 上游请求 ID;JSON tag 为 `omitempty`——**空值时不出现、有值时才出现**(实测样本中缺席);消费方需按「可选字段」处理 |
| `other` | string | JSON 字符串,扩展信息(结构见下) |
**`type` 完整取值(上游源码 `model/log.go`;常量注释明确「刻意不用 iota,以保证枚举值永不改变」)**:
| 值 | 常量 | 含义 | 可能出现在 token 日志? |
|----|------|------|------------------------|
| 0 | `LogTypeUnknown` | 未知 | 兜底值 |
| 1 | `LogTypeTopup` | 充值 | 否(账号级,无 token_id) |
| 2 | `LogTypeConsume` | 消费 | **是**(实测) |
| 3 | `LogTypeManage` | 管理操作 | 否(审计类) |
| 4 | `LogTypeSystem` | 系统 | 否 |
| 5 | `LogTypeError` | 错误 | **是**(实测) |
| 6 | `LogTypeRefund` | 退款 | 可能(任务计费链路带 token_id) |
| 7 | `LogTypeLogin` | 登录 | 否(账号级) |
> 本接口按 `token_id` 过滤、不按 `type` 过滤——凡是「带 token_id 写入」的类型都可能返回。
**`other` 字段(JSON 字符串,内容随 `type` 变化,实测 2026-09-11)**:
- 消费记录(`type=2`):`billing_source`、`cache_ratio`、`cache_tokens`、`completion_ratio`、`frt`、`group_ratio`、`model_price`、`model_ratio`、`reasoning_effort`、`request_conversion`、`request_path`、`stream_status`(对象)、`subscription_*`(订阅维度,共 8 个)、`user_group_ratio`、`wallet_quota_deducted`
- 错误记录(`type=5`):`error_code`、`error_type`、`request_path`、`status_code`
- 注:普通用户视角下,服务端会先对 `other` 做一层「可见性过滤」再返回——上表为过滤后实际可见的键
**关键限制**:
- **硬上限 1000 条**:固定返回最近 1000 条日志(`Limit(common.MaxRecentItems)`,`MaxRecentItems = 1000`,定义于 `common/constants.go`)。
- **不支持任何请求参数**:`GetLogByKey` 不读任何 `c.Query()`,没有分页(page/limit/offset),没有时间过滤(start_timestamp/end_timestamp)。
- **固定按 `id desc` 排序**:即最新 1000 条。
- **超过 1000 条的旧记录无法获取**:无法翻页,无法扩大 limit。
- **当月调用超 1000 次时,此接口无法覆盖完整当月数据**。
---
### ③ GET `/v1/dashboard/billing/subscription` - 总额度上限
**鉴权**:`TokenAuth()`
**请求参数**:无
**请求示例**:
```
GET /v1/dashboard/billing/subscription
Authorization: Bearer sk-xxxx
```
**返回结构**(源码:`controller/billing.go:13` -> `GetSubscription`):
```json
{
"object": "billing_subscription",
"has_payment_method": true,
"soft_limit_usd": 100000000,
"hard_limit_usd": 100000000,
"system_hard_limit_usd": 100000000,
"access_until": 0
}
```
**字段说明**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `hard_limit_usd` | float | **总额度上限**(美元)。无限额度 key 返回 `100000000`(占位值,无实际意义) |
| `soft_limit_usd` | float | 软上限(同 hard_limit_usd) |
| `system_hard_limit_usd` | float | 系统上限(同 hard_limit_usd) |
| `access_until` | int | token 过期时间(Unix 秒),0 = 永不过期 |
**注意事项**:
- 受 `DisplayTokenStatEnabled` 开关影响。开启时返回 token 级额度;关闭时返回账号级额度(会暴露整个账号余额)。
- 无限额度 key 返回 `100000000`(一亿)作为占位值,无参考意义。
- 受 `quota_display_type` 影响:USD 模式直接是美元;CNY 模式会做货币转换;Tokens 模式是原始 token 数。
---
### ④ GET `/v1/dashboard/billing/usage` - 累计已用
**鉴权**:`TokenAuth()`
**请求参数**:无
**请求示例**:
```
GET /v1/dashboard/billing/usage
Authorization: Bearer sk-xxxx
```
**返回结构**(源码:`controller/billing.go:75` -> `GetUsage`):
```json
{
"object": "list",
"total_usage": 134683.8914
}
```
**字段说明**:
| 字段 | 类型 | 说明 |
|------|------|------|
| `total_usage` | float | **累计已用**(单位:**美分**)。换算美元:`total_usage ÷ 100` |
**实测验证**:
```
total_usage = 134683.8914 美分 ÷ 100 = 1346.84 美元
与接口① total_used(673419457) ÷ 500000 = 1346.84 美元 完全吻合
```
**注意事项**:
- `total_usage` 是**累计值**(从 key 创建至今),不是当月用量。
- 受 `DisplayTokenStatEnabled` 开关影响(开启时为 token 级)。
- 受 `quota_display_type` 影响(USD 模式才是美元,否则需另行换算)。
- `hard_limit_usd`(接口③)− `total_usage ÷ 100`(接口④)= 剩余额度(仅有限额 key 有意义)。
---
## 3. 模型列表接口(与用量无关)
> 以下接口 sk-key 可访问,但返回的是模型列表,与用量统计无关。列出仅为完整覆盖。
### ⑦ GET `/v1/models`
返回当前 key 可用的模型列表,OpenAI 兼容格式。
```json
{
"object": "list",
"data": [
{
"id": "gpt-4o",
"object": "model",
"created": 1686935000,
"owned_by": "openai"
}
]
}
```
### ⑧ GET `/v1/models/:model`
返回单个模型详情。将 `:model` 替换为模型名,如 `/v1/models/gpt-4o`。
> **已知限制**:此接口与 `/v1/models` 数据源不同。`/v1/models` 查数据库 `abilities` 表(反映渠道实际配置的模型),而此接口查代码内硬编码的静态模型列表(`openAIModelsMap`)。当渠道配了新模型或自定义模型名但代码未收录时,会出现"列表里有但单查返回 `model_not_found`"的现象。不影响实际模型调用。
### ⑨ GET `/v1beta/models`
返回模型列表(Gemini 兼容格式)。
### ⑩ GET `/v1beta/openai/models`
返回模型列表(OpenAI 格式,Gemini 路由下)。
---
## 4. 确认不存在的接口(排除清单)
以下接口在源码中**不存在**或**不可用 sk-key 访问**,测试时应返回 404 或 401:
### 4.1 不存在的接口(返回 404)
| 路径 | 状态 |
|------|------|
| `/v1/usage` | 不存在 |
| `/v1/quota` | 不存在 |
| `/api/token/self` | 不存在 |
### 4.2 存在但需要登录态的接口(sk-key 访问返回 401)
这些接口支持时间范围查询和 `token_name` 过滤,但**必须用账号登录态**(access token + `New-Api-User` header),sk-key 无法访问:
| 路径 | 鉴权 | 能力 |
|------|------|------|
| `/api/log/self` | `UserAuth()` | 支持 `start_timestamp`、`end_timestamp`、`token_name` 过滤,返回日志明细 |
| `/api/log/self/stat` | `UserAuth()` | **支持时间范围 + token_name,返回 quota 汇总**(调 `SumUsedQuota`) |
| `/api/data/self` | `UserAuth()` | 按日聚合的看板数据 |
| `/api/token/` (GET/POST/PUT/DELETE) | `UserAuth()` | token 的增删改查 |
| `/api/user/self` | `UserAuth()` | 用户自身信息 |
> **特别说明**:`/api/log/self/stat` 是 NewAPI 中最适合算"当月用量"的接口--传当月起止时间 + `token_name` 即可返回该 key 当月消耗的 quota 总和。但它需要登录态,是"按时间段统计用量"难以纯客户端实现的核心障碍。
### 4.3 参数被忽略的情况
| 请求 | 实际行为 |
|------|---------|
| `/api/log/token?p=2` | 参数被忽略,仍返回最近 1000 条 |
| `/api/log/token?limit=5000` | 参数被忽略,仍返回最近 1000 条 |
| `/api/log/token?start_timestamp=1751328000` | 参数被忽略,仍返回最近 1000 条 |
| `/v1/dashboard/billing/usage?start_date=2025-07-01` | 参数被忽略 |
---
## 5. 用量换算速查
### 核心换算公式
```
原始 quota ÷ 500000 = 美元
```
### 消费记录的 `quota` 是怎么算出来的(实测反推,2026-09-13)
按 tokens 计费的模型,`quota` 在**请求时**由「倍率 × 用量」算出并固化进日志(此后不再变化):
```
quota = ( (prompt_tokens − cache_tokens) ← 提示 tokens(未命中缓存部分)
+ cache_tokens × cache_ratio ← 缓存命中部分按折扣
+ completion_tokens × completion_ratio ) ← 补全 tokens 按倍率
× model_ratio ← 模型倍率
× group_ratio ← 分组倍率
```
- 验证:6/6 条实测日志逐条吻合(差值仅为四舍五入)
- 按次计费模型走 `model_price` 分支:`quota = model_price × QuotaPerUnit × group_ratio`(源码 `relay/helper/price.go`)
- **公式中没有 `QuotaPerUnit`**:倍率与美元的锚定(`model_ratio = 1` ⇔ `$0.002 / 1K tokens`)已隐含它——倍率制下改 `QuotaPerUnit` 等于全站重定标;美元定价路径(`model_price` / 表达式 `$/1M`)才显式乘 `QuotaPerUnit`
### 实测数据验证
| 来源 | 原始值 | 换算 | 结果 |
|------|--------|------|------|
| 接口① `total_used` | 673,419,457 quota | ÷ 500000 | 1346.84 美元 |
| 接口④ `total_usage` | 134,683.8914 美分 | ÷ 100 | 1346.84 美元 |
两者完全吻合,确认 `QuotaPerUnit = 500000`。
---
## 6. 关键限制总结
### 6.1 时间维度
- **所有 sk-key 接口返回的用量都是累计值**(从 key 创建至今),没有任何接口直接返回"当月用量"。
- 唯一带时间戳的是 `/api/log/token`,但硬限 1000 条且不支持时间过滤。
- 支持时间范围的接口(`/api/log/self`、`/api/log/self/stat`)需要登录态,sk-key 无法访问。
### 6.2 日志条数
- `/api/log/token` 硬上限 **1000 条**(`MaxRecentItems = 1000`,定义于 `common/constants.go`)。
- 不支持分页、不支持时间过滤、不支持改变 limit。
- 月调用量超过 1000 次时,此接口无法覆盖完整当月数据。
### 6.3 写操作
- **sk-key 没有任何写接口**(没有 POST/PUT/DELETE 能改 token 自身)。
- token 的增删改查(`/api/token/`)全部需要登录态 `UserAuth()`。
- 无法通过 sk-key 重置 `used_quota`(没有"重置用量"接口,`Update()` 的 Select 列表不含 `used_quota`)。
### 6.4 禁用/解禁行为
- 禁用 token 只改 `status` 字段,**不重置 `used_quota`**,不清零 `remain_quota`,不丢历史日志。
- 解禁后 `used_quota`、`remain_quota`、历史日志全部原封不动。
- 唯一让 `used_quota` 归零的途径是**删除 token 后新建**(产生全新 Id/Key,旧日志按旧 token_id 关联,新 token 查不到旧日志)。
- 解禁保护:额度耗尽(`Exhausted`)且 `RemainQuota ≤ 0` 且非无限额度的 token 不能直接解禁,需先充值;已过期的 token 也不能直接解禁,需改过期时间。
---
## 7. 源码参考
| 关注点 | 文件路径 | 关键函数/位置 |
|--------|---------|--------------|
| 路由注册 | `router/api-router.go` | usageRoute、logRoute 组 |
| 路由注册 | `router/dashboard.go:14-23` | billing 路由组 |
| 路由注册 | `router/relay-router.go` | models 路由 |
| API key 鉴权 | `middleware/auth.go:171` | `TokenAuthReadOnly()` |
| API key 鉴权 | `middleware/auth.go` | `TokenAuth()` / `SetupContextForToken` |
| token 用量查询 | `controller/token.go` | `GetTokenUsage` |
| 日志查询 | `controller/log.go:62` | `GetLogByKey` |
| 日志查询(model) | `model/log.go` | `GetLogByTokenId`(Limit 1000) |
| billing 查询 | `controller/billing.go:13` | `GetSubscription` |
| billing 查询 | `controller/billing.go:75` | `GetUsage` |
| 显示开关 | `common/constants.go` | `DisplayTokenStatEnabled` |
| 日志上限 | `common/constants.go` | `MaxRecentItems = 1000` |
| 配额单位 | `common/constants.go` | `QuotaPerUnit`(默认 500000) |
| token 更新 | `model/token.go` | `Update()`(Select 不含 used_quota) |
| 额度增减 | `model/token.go` | `increaseTokenQuota` / `decreaseTokenQuota` |
> 仓库地址:https://github.com/QuantumNous/new-api