@ponydev/dsh-model-usage 1.0.5 → 1.0.6
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +12 -0
- package/README.md +6 -1
- package/docs/providers.md +67 -0
- package/lib/adapters.js +204 -3
- package/lib/client.js +3 -2
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,17 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 1.0.6 — 2026-10-20
|
|
4
|
+
|
|
5
|
+
### 新增
|
|
6
|
+
- **供应商覆盖对齐 DSH provider 目录(43 个)**:
|
|
7
|
+
- 新增适配器:OpenCode Go(`zen/go/v1/usage`,5h/周/月)、Moonshot/Kimi 平台余额、MiniMax Token Plan(5h/周)、Xiaomi MiMo 余额、GitHub Copilot 配额(Premium/Chat/补全)
|
|
8
|
+
- `deepseek` 路由也映射到 DeepSeek 余额适配器
|
|
9
|
+
- 新增「每月限额」窗口标签
|
|
10
|
+
- `docs/providers.md`:完整覆盖矩阵(真实额度 / 订阅待确认 / 无公开接口 / customQuota 接入方式)
|
|
11
|
+
|
|
12
|
+
### 变更
|
|
13
|
+
- 无公开额度接口的 provider(Anthropic/OpenAI/Google/Bedrock/Azure/Groq 等)保持隐藏,不产生错误卡片
|
|
14
|
+
|
|
3
15
|
## 1.0.5 — 2026-10-20
|
|
4
16
|
|
|
5
17
|
### 新增
|
package/README.md
CHANGED
|
@@ -37,7 +37,7 @@ dsh plugin --profile web add @ponydev/dsh-model-usage
|
|
|
37
37
|
dsh plugin --profile web add link:/path/to/dsh-model-usage
|
|
38
38
|
```
|
|
39
39
|
|
|
40
|
-
##
|
|
40
|
+
## 供应商支持(完整矩阵见 [docs/providers.md](docs/providers.md))
|
|
41
41
|
|
|
42
42
|
| 供应商 | 数据接口 | 凭据引用 |
|
|
43
43
|
|---|---|---|
|
|
@@ -46,6 +46,11 @@ dsh plugin --profile web add link:/path/to/dsh-model-usage
|
|
|
46
46
|
| GLM Coding Plan(国内 / 国际) | `open.bigmodel.cn` / `api.z.ai` 额度接口(5 小时 / 每周 / **每月 MCP 工具次数**) | `ZAI_CODING_CN_API_KEY` / `ZAI_API_KEY` |
|
|
47
47
|
| OpenRouter | `openrouter.ai/api/v1/credits`(兜底 `/api/v1/key`) | `OPENROUTER_API_KEY` |
|
|
48
48
|
| Kimi Coding Plan | `api.kimi.com/coding/v1/usages` | `KIMI_CODING_API_KEY` / `KIMI_API_KEY` / `MOONSHOT_API_KEY` |
|
|
49
|
+
| Moonshot / Kimi 平台 | `api.moonshot.{ai,cn}/v1/users/me/balance` | `MOONSHOT_API_KEY` / `MOONSHOT_CN_API_KEY` |
|
|
50
|
+
| MiniMax Token Plan | `api.minimax.io` / `api.minimaxi.com` `/v1/token_plan/remains` | `MINIMAX_API_KEY` / `MINIMAX_CN_API_KEY` |
|
|
51
|
+
| OpenCode Go | `opencode.ai/zen/go/v1/usage` | `OPENCODE_API_KEY` |
|
|
52
|
+
| Xiaomi MiMo | `platform.xiaomimimo.com/api/v1/balance` | `XIAOMI_API_KEY` |
|
|
53
|
+
| GitHub Copilot | `api.github.com/copilot_internal/user` | `GITHUB_TOKEN` |
|
|
49
54
|
| 其他任意供应商 | `customQuota` 配置(URL + JSON 路径映射) | 自定义 `keyRef` |
|
|
50
55
|
|
|
51
56
|
`customQuota` 示例(余额式):
|
|
@@ -0,0 +1,67 @@
|
|
|
1
|
+
# 供应商覆盖矩阵
|
|
2
|
+
|
|
3
|
+
DSH(`@deepseek-ai/dsh` 0.2.0-rc.2)的 pi-ai provider 目录共 43 个 provider。本插件按「**有真实额度接口才显示,没有就隐藏**」的原则覆盖。
|
|
4
|
+
|
|
5
|
+
图例:
|
|
6
|
+
- ✅ **真实额度**:调用服务商官方接口,显示余额或窗口剩余
|
|
7
|
+
- 🟡 **订阅/接口待确认**:接口存在但需要特定 Key 类型,失败时优雅隐藏
|
|
8
|
+
- ⬜ **无公开接口**:服务商未提供可用 API Key 查询的余额接口 → 不显示卡片(不会报错)
|
|
9
|
+
- 🔧 **customQuota**:可用配置接入(URL + JSON 路径映射)
|
|
10
|
+
|
|
11
|
+
## 已支持(真实额度)
|
|
12
|
+
|
|
13
|
+
| Provider id | 数据接口 | 凭据引用 | 展示 |
|
|
14
|
+
|---|---|---|---|
|
|
15
|
+
| `deepseek-official` / `deepseek` | `api.deepseek.com/user/balance` | `DEEPSEEK_API_KEY` | 余额(总/充值/赠金) |
|
|
16
|
+
| `deepseek-account` | DSH `deepseekAccount` 服务 | 登录态 | 账号钱包 |
|
|
17
|
+
| `zai` / `zai-coding-cn` | `open.bigmodel.cn` / `api.z.ai` 额度接口 | `ZAI_CODING_CN_API_KEY` / `ZAI_API_KEY` | 5 小时 / 每周 / **每月 MCP 工具次数** |
|
|
18
|
+
| `openrouter` | `openrouter.ai/api/v1/credits`(兜底 `/api/v1/key`) | `OPENROUTER_API_KEY` | credits 余额 |
|
|
19
|
+
| `kimi-coding` / `kimi-coding-cn` | `api.kimi.com/coding/v1/usages` | `KIMI_CODING_API_KEY` / `KIMI_API_KEY` | 周 + 滚动窗口 |
|
|
20
|
+
| `moonshotai` / `moonshotai-cn` | `api.moonshot.{ai,cn}/v1/users/me/balance` | `MOONSHOT_API_KEY` / `MOONSHOT_CN_API_KEY` | 平台余额 |
|
|
21
|
+
| `minimax` / `minimax-cn` | `api.minimax.io` / `api.minimaxi.com` `/v1/token_plan/remains` | `MINIMAX_API_KEY` / `MINIMAX_CN_API_KEY` | 5h + 周(订阅 Key) |
|
|
22
|
+
| `opencode-go` | `opencode.ai/zen/go/v1/usage` | `OPENCODE_API_KEY` | 5h / 周 / 月 |
|
|
23
|
+
| `xiaomi*`(含 token-plan) | `platform.xiaomimimo.com/api/v1/balance` | `XIAOMI_API_KEY` | 余额 |
|
|
24
|
+
| `github-copilot` | `api.github.com/copilot_internal/user` | `GITHUB_TOKEN` / `GH_TOKEN` | Premium / Chat / 补全 配额 |
|
|
25
|
+
|
|
26
|
+
## 无公开余额接口(隐藏,不报错)
|
|
27
|
+
|
|
28
|
+
| Provider id | 说明 |
|
|
29
|
+
|---|---|
|
|
30
|
+
| `anthropic` | 无用户 Key 余额接口(组织用量需 Admin Key + 控制台) |
|
|
31
|
+
| `openai`、`openai-codex` | 用量接口需组织 Admin Key / ChatGPT OAuth,DSH 的普通 Key 不可用 |
|
|
32
|
+
| `google`、`google-vertex` | 需 GCP 项目计费 API,非 Key 级 |
|
|
33
|
+
| `amazon-bedrock` | 需 AWS IAM/CloudWatch 计费 |
|
|
34
|
+
| `azure-openai-responses` | 需 Azure 订阅计费 API |
|
|
35
|
+
| `groq`、`cerebras`、`fireworks`、`together`、`baseten`、`nvidia`、`huggingface`、`mistral`、`meta`、`cloudflare-ai-gateway`、`cloudflare-workers-ai`、`vercel-ai-gateway`、`radius`、`ant-ling`、`opencode` | 无 Key 级余额/配额查询接口 |
|
|
36
|
+
| `qwen-token-plan` / `-cn` / `-individual` | 阿里云百炼暂未提供 Token Plan 用量 OpenAPI(控制台可见) |
|
|
37
|
+
|
|
38
|
+
> 这些 provider 的卡片**直接隐藏**,不影响插件运行;需要展示时用 `customQuota` 配置接入,或提 issue 附接口文档。
|
|
39
|
+
|
|
40
|
+
## 用 customQuota 接入任意供应商
|
|
41
|
+
|
|
42
|
+
```yaml
|
|
43
|
+
config:
|
|
44
|
+
customQuota:
|
|
45
|
+
my-provider:
|
|
46
|
+
url: https://api.example.com/quota
|
|
47
|
+
auth: bearer # bearer | raw | x-api-key | none
|
|
48
|
+
keyRef: MY_PROVIDER_API_KEY
|
|
49
|
+
balance: { path: data.balance, currency: CNY }
|
|
50
|
+
# 或窗口式
|
|
51
|
+
windows:
|
|
52
|
+
- key: rolling
|
|
53
|
+
label: 5h
|
|
54
|
+
usedPath: data.used
|
|
55
|
+
limitPath: data.limit
|
|
56
|
+
resetAtPath: data.resetAt
|
|
57
|
+
resetAtUnit: ms # ms | s | iso
|
|
58
|
+
```
|
|
59
|
+
|
|
60
|
+
## 新增适配器
|
|
61
|
+
|
|
62
|
+
1. 在 `lib/adapters.js` 新增 `{ id, refs, read(env) }`
|
|
63
|
+
2. `adapterFor()` 加路由匹配(注意顺序:更具体的路由在前,如 `kimi-coding` 先于 `kimi`)
|
|
64
|
+
3. 返回 `{ status:'ok', balance }` 或 `{ status:'ok', windows:[{key,label,usedPct,resetAt,countBased?,remaining?,limit?}] }`
|
|
65
|
+
4. 失败一律:`needs-credential` / `unauthorized` / `unsupported`(不抛未捕获异常)
|
|
66
|
+
5. 只允许写死白名单域名,错误信息必须脱敏
|
|
67
|
+
6. 更新本文档矩阵 + `package.json` version(patch/minor)+ `CHANGELOG.md`
|
package/lib/adapters.js
CHANGED
|
@@ -419,6 +419,201 @@ const openrouter = {
|
|
|
419
419
|
},
|
|
420
420
|
}
|
|
421
421
|
|
|
422
|
+
// OpenCode Go(订阅):GET https://opencode.ai/zen/go/v1/usage Bearer
|
|
423
|
+
// 响应 { usage: { rolling: { percent, resetsAt }, weekly: {...}, monthly: {...} } }
|
|
424
|
+
const opencodeGo = {
|
|
425
|
+
id: 'opencode-go',
|
|
426
|
+
refs: (cfg, route) => [cfg.credentials[route] || 'OPENCODE_API_KEY'],
|
|
427
|
+
async read(env) {
|
|
428
|
+
const key = await resolveKey(env)
|
|
429
|
+
if (!key) return { status: 'needs-credential', credential: { refs: env.refs, configured: false } }
|
|
430
|
+
const res = await fetchT('https://opencode.ai/zen/go/v1/usage', { headers: { authorization: 'Bearer ' + key, accept: 'application/json' } }, 10000)
|
|
431
|
+
if (res.status === 401 || res.status === 403) throw err('model-usage/unauthorized', 'HTTP ' + res.status + '(OpenCode Key 无效)')
|
|
432
|
+
if (!res.ok) throw err('model-usage/http', 'HTTP ' + res.status)
|
|
433
|
+
const data = await res.json()
|
|
434
|
+
const u = (data && data.usage) || {}
|
|
435
|
+
const windows = []
|
|
436
|
+
for (const [key2, label] of [['rolling', '5h'], ['weekly', '周'], ['monthly', '月']]) {
|
|
437
|
+
const it = u[key2]
|
|
438
|
+
if (!it || typeof it !== 'object') continue
|
|
439
|
+
const pct = Number(it.percent)
|
|
440
|
+
if (!Number.isFinite(pct)) continue
|
|
441
|
+
const resetRaw = it.resetsAt
|
|
442
|
+
const resetAt = typeof resetRaw === 'string' ? Date.parse(resetRaw) || null : (Number(resetRaw) || null)
|
|
443
|
+
windows.push({ key: key2 === 'rolling' ? 'rolling' : key2 === 'weekly' ? 'weekly' : 'monthly', label, usedPct: clampPct(pct), resetAt })
|
|
444
|
+
}
|
|
445
|
+
if (!windows.length) return { status: 'unsupported', error: { code: 'model-usage/shape', message: 'OpenCode Go 未返回 usage 窗口' } }
|
|
446
|
+
return { status: 'ok', fetchedAt: Date.now(), credential: { refs: env.refs, configured: true }, windows }
|
|
447
|
+
},
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
// Moonshot / Kimi 平台余额:GET https://api.moonshot.{cn,ai}/v1/users/me/balance Bearer
|
|
451
|
+
// 响应 { data: { available_balance, voucher_balance, cash_balance } }
|
|
452
|
+
const moonshotBalance = {
|
|
453
|
+
id: 'moonshot-balance',
|
|
454
|
+
refs: (cfg, route) => [cfg.credentials[route] || (/cn$/.test(route) ? 'MOONSHOT_CN_API_KEY' : 'MOONSHOT_API_KEY'), 'MOONSHOT_API_KEY', 'KIMI_API_KEY', 'MOONSHOT_CN_API_KEY'],
|
|
455
|
+
async read(env) {
|
|
456
|
+
const key = await resolveKey(env)
|
|
457
|
+
if (!key) return { status: 'needs-credential', credential: { refs: env.refs, configured: false } }
|
|
458
|
+
const cn = /cn$/.test(env.route || '')
|
|
459
|
+
const host = cn ? 'https://api.moonshot.cn' : 'https://api.moonshot.ai'
|
|
460
|
+
const res = await fetchT(host + '/v1/users/me/balance', { headers: { authorization: 'Bearer ' + key, accept: 'application/json' } }, 10000)
|
|
461
|
+
if (res.status === 401 || res.status === 403) throw err('model-usage/unauthorized', 'HTTP ' + res.status + '(Moonshot Key 无效)')
|
|
462
|
+
if (!res.ok) throw err('model-usage/http', 'HTTP ' + res.status)
|
|
463
|
+
const body = await res.json()
|
|
464
|
+
const d = (body && body.data) || body || {}
|
|
465
|
+
const pick = (k) => { const v = Number(d[k]); return Number.isFinite(v) ? v : null }
|
|
466
|
+
const available = pick('available_balance')
|
|
467
|
+
if (available == null) return { status: 'unsupported', error: { code: 'model-usage/shape', message: 'Moonshot 未返回 available_balance' } }
|
|
468
|
+
const cash = pick('cash_balance')
|
|
469
|
+
const voucher = pick('voucher_balance')
|
|
470
|
+
const cur = cn ? 'CNY' : 'USD'
|
|
471
|
+
return {
|
|
472
|
+
status: 'ok',
|
|
473
|
+
fetchedAt: Date.now(),
|
|
474
|
+
credential: { refs: env.refs, configured: true },
|
|
475
|
+
balance: {
|
|
476
|
+
currency: cur,
|
|
477
|
+
total: available.toFixed(2),
|
|
478
|
+
toppedUp: cash != null ? cash.toFixed(2) : '',
|
|
479
|
+
granted: voucher != null ? voucher.toFixed(2) : '',
|
|
480
|
+
available: available > 0,
|
|
481
|
+
},
|
|
482
|
+
}
|
|
483
|
+
},
|
|
484
|
+
}
|
|
485
|
+
|
|
486
|
+
// MiniMax Token Plan:GET {api.minimax.io|api.minimaxi.com}/v1/token_plan/remains Bearer
|
|
487
|
+
// 响应 model_remains[] / models[]:current_interval_remaining_percent、current_weekly_remaining_percent、
|
|
488
|
+
// remains_time(毫秒)、weekly_remains_time、end_time / weekly_end_time
|
|
489
|
+
const minimaxPlan = {
|
|
490
|
+
id: 'minimax-plan',
|
|
491
|
+
refs: (cfg, route) => [cfg.credentials[route] || (/cn$/.test(route) ? 'MINIMAX_CN_API_KEY' : 'MINIMAX_API_KEY')],
|
|
492
|
+
async read(env) {
|
|
493
|
+
const key = await resolveKey(env)
|
|
494
|
+
if (!key) return { status: 'needs-credential', credential: { refs: env.refs, configured: false } }
|
|
495
|
+
const cn = /cn$/.test(env.route || '')
|
|
496
|
+
const url = cn ? 'https://api.minimaxi.com/v1/token_plan/remains' : 'https://api.minimax.io/v1/token_plan/remains'
|
|
497
|
+
const res = await fetchT(url, { headers: { authorization: 'Bearer ' + key, accept: 'application/json' } }, 10000)
|
|
498
|
+
if (res.status === 401 || res.status === 403) throw err('model-usage/unauthorized', 'HTTP ' + res.status + '(需要 MiniMax 订阅 Key,不是按量付费 Key)')
|
|
499
|
+
if (!res.ok) throw err('model-usage/http', 'HTTP ' + res.status)
|
|
500
|
+
const body = await res.json()
|
|
501
|
+
const base = (body && body.base_resp) || {}
|
|
502
|
+
if (Number(base.status_code) && Number(base.status_code) !== 0) {
|
|
503
|
+
return { status: 'unsupported', error: { code: 'model-usage/biz', message: String(base.status_msg || base.status_code) } }
|
|
504
|
+
}
|
|
505
|
+
const rows = [body.model_remains, body.models, body.data, body.data && body.data.model_remains].find((x) => Array.isArray(x) && x.length)
|
|
506
|
+
if (!rows) return { status: 'unsupported', error: { code: 'model-usage/shape', message: 'MiniMax 未返回 model_remains' } }
|
|
507
|
+
const n = (row, keys) => {
|
|
508
|
+
for (const k of keys) { const v = Number(row[k]); if (Number.isFinite(v)) return v }
|
|
509
|
+
return null
|
|
510
|
+
}
|
|
511
|
+
// 取第一条有配额的行(多模型时展示主模型)
|
|
512
|
+
let picked = null
|
|
513
|
+
for (const row of rows) {
|
|
514
|
+
if (!row || typeof row !== 'object') continue
|
|
515
|
+
const intervalPct = n(row, ['current_interval_remaining_percent', 'interval_remaining_percent', 'current_interval_remain_percent'])
|
|
516
|
+
const weeklyPct = n(row, ['current_weekly_remaining_percent', 'weekly_remaining_percent', 'current_weekly_remain_percent'])
|
|
517
|
+
if (intervalPct == null && weeklyPct == null) continue
|
|
518
|
+
picked = { row, intervalPct, weeklyPct }
|
|
519
|
+
break
|
|
520
|
+
}
|
|
521
|
+
if (!picked) return { status: 'unsupported', error: { code: 'model-usage/shape', message: 'MiniMax 配额字段缺失' } }
|
|
522
|
+
const now = Date.now()
|
|
523
|
+
const remainsToReset = (row, keys) => {
|
|
524
|
+
const ms = n(row, keys)
|
|
525
|
+
return ms == null ? null : now + ms
|
|
526
|
+
}
|
|
527
|
+
const endAt = (row, keys) => {
|
|
528
|
+
const v = n(row, keys)
|
|
529
|
+
if (v == null) return null
|
|
530
|
+
return v < 1e12 ? v * 1000 : v
|
|
531
|
+
}
|
|
532
|
+
const windows = []
|
|
533
|
+
if (picked.intervalPct != null) {
|
|
534
|
+
windows.push({
|
|
535
|
+
key: 'rolling', label: '5h',
|
|
536
|
+
usedPct: clampPct(100 - picked.intervalPct),
|
|
537
|
+
resetAt: endAt(picked.row, ['end_time', 'current_interval_end_time', 'interval_end_time']) || remainsToReset(picked.row, ['remains_time', 'current_interval_remains_time', 'interval_remains_time']),
|
|
538
|
+
})
|
|
539
|
+
}
|
|
540
|
+
if (picked.weeklyPct != null) {
|
|
541
|
+
windows.push({
|
|
542
|
+
key: 'weekly', label: '周',
|
|
543
|
+
usedPct: clampPct(100 - picked.weeklyPct),
|
|
544
|
+
resetAt: endAt(picked.row, ['weekly_end_time', 'current_weekly_end_time']) || remainsToReset(picked.row, ['weekly_remains_time', 'current_weekly_remains_time']),
|
|
545
|
+
})
|
|
546
|
+
}
|
|
547
|
+
const plan = (typeof (picked.row.subscribe_title || picked.row.plan_name || body.plan_name) === 'string') ? String(picked.row.subscribe_title || picked.row.plan_name || body.plan_name) : ''
|
|
548
|
+
return { status: 'ok', fetchedAt: Date.now(), credential: { refs: env.refs, configured: true }, plan, windows }
|
|
549
|
+
},
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
// Xiaomi MiMo 余额:GET https://platform.xiaomimimo.com/api/v1/balance Bearer
|
|
553
|
+
const xiaomiBalance = {
|
|
554
|
+
id: 'xiaomi-balance',
|
|
555
|
+
refs: (cfg, route) => [cfg.credentials[route] || 'XIAOMI_API_KEY'],
|
|
556
|
+
async read(env) {
|
|
557
|
+
const key = await resolveKey(env)
|
|
558
|
+
if (!key) return { status: 'needs-credential', credential: { refs: env.refs, configured: false } }
|
|
559
|
+
const res = await fetchT('https://platform.xiaomimimo.com/api/v1/balance', { headers: { authorization: 'Bearer ' + key, accept: 'application/json' } }, 10000)
|
|
560
|
+
if (res.status === 401 || res.status === 403) throw err('model-usage/unauthorized', 'HTTP ' + res.status + '(Xiaomi Key 无效)')
|
|
561
|
+
if (!res.ok) throw err('model-usage/http', 'HTTP ' + res.status)
|
|
562
|
+
const body = await res.json()
|
|
563
|
+
const d = (body && (body.data || body.result)) || body || {}
|
|
564
|
+
const val = [d.balance, d.remaining, d.available_balance, d.total_balance].map(Number).find((v) => Number.isFinite(v))
|
|
565
|
+
if (val == null) return { status: 'unsupported', error: { code: 'model-usage/shape', message: 'Xiaomi 未返回余额字段' } }
|
|
566
|
+
return {
|
|
567
|
+
status: 'ok',
|
|
568
|
+
fetchedAt: Date.now(),
|
|
569
|
+
credential: { refs: env.refs, configured: true },
|
|
570
|
+
balance: { currency: 'CNY', total: val.toFixed(2), toppedUp: '', granted: '', available: val > 0 },
|
|
571
|
+
}
|
|
572
|
+
},
|
|
573
|
+
}
|
|
574
|
+
|
|
575
|
+
// GitHub Copilot:GET https://api.github.com/copilot_internal/user Bearer(GitHub token)
|
|
576
|
+
// 响应 quota_snapshots: { premium_interactions|chat|completions: { entitlement, remaining, percent_remaining } }
|
|
577
|
+
const githubCopilot = {
|
|
578
|
+
id: 'github-copilot',
|
|
579
|
+
refs: (cfg, route) => [cfg.credentials[route] || 'GITHUB_TOKEN', 'GH_TOKEN', 'GITHUB_COPILOT_TOKEN'],
|
|
580
|
+
async read(env) {
|
|
581
|
+
const key = await resolveKey(env)
|
|
582
|
+
if (!key) return { status: 'needs-credential', credential: { refs: env.refs, configured: false } }
|
|
583
|
+
const res = await fetchT('https://api.github.com/copilot_internal/user', {
|
|
584
|
+
headers: { authorization: 'Bearer ' + key, accept: 'application/json', 'user-agent': 'dsh-model-usage' },
|
|
585
|
+
}, 10000)
|
|
586
|
+
if (res.status === 401 || res.status === 403) throw err('model-usage/unauthorized', 'HTTP ' + res.status + '(需要 GitHub Copilot token)')
|
|
587
|
+
if (!res.ok) throw err('model-usage/http', 'HTTP ' + res.status)
|
|
588
|
+
const body = await res.json()
|
|
589
|
+
const snap = body && body.quota_snapshots
|
|
590
|
+
if (!snap || typeof snap !== 'object') return { status: 'unsupported', error: { code: 'model-usage/shape', message: 'Copilot 未返回 quota_snapshots(非 Copilot 账号或接口改版)' } }
|
|
591
|
+
const windows = []
|
|
592
|
+
let resetAt = null
|
|
593
|
+
const qr = Number(body.quota_reset_date)
|
|
594
|
+
if (Number.isFinite(qr)) resetAt = qr < 1e12 ? qr * 1000 : qr
|
|
595
|
+
for (const [key2, label] of [['premium_interactions', 'Premium'], ['chat', 'Chat'], ['completions', '补全']]) {
|
|
596
|
+
const it = snap[key2]
|
|
597
|
+
if (!it || typeof it !== 'object') continue
|
|
598
|
+
const pctRemain = Number(it.percent_remaining)
|
|
599
|
+
const remain = Number(it.remaining)
|
|
600
|
+
const ent = Number(it.entitlement)
|
|
601
|
+
let usedPct = null
|
|
602
|
+
let countBased = false
|
|
603
|
+
let remaining = null
|
|
604
|
+
let limit = null
|
|
605
|
+
if (Number.isFinite(pctRemain)) usedPct = clampPct(100 - pctRemain)
|
|
606
|
+
else if (Number.isFinite(remain) && Number.isFinite(ent) && ent > 0) usedPct = clampPct(((ent - remain) / ent) * 100)
|
|
607
|
+
if (Number.isFinite(remain) && Number.isFinite(ent)) { countBased = true; remaining = remain; limit = ent }
|
|
608
|
+
if (usedPct == null) continue
|
|
609
|
+
windows.push({ key: key2, label, usedPct, resetAt, countBased, remaining, limit })
|
|
610
|
+
}
|
|
611
|
+
if (!windows.length) return { status: 'unsupported', error: { code: 'model-usage/shape', message: 'Copilot 配额为空' } }
|
|
612
|
+
const plan = typeof body.copilot_plan === 'string' ? body.copilot_plan : ''
|
|
613
|
+
return { status: 'ok', fetchedAt: Date.now(), credential: { refs: env.refs, configured: true }, plan, windows }
|
|
614
|
+
},
|
|
615
|
+
}
|
|
616
|
+
|
|
422
617
|
// 通用自定义额度:任何供应商都能通过 cordis.patch.yml 的 customQuota 配置接入
|
|
423
618
|
// customQuota: { [route]: { url, auth:'bearer'|'raw'|'x-api-key'|'none', keyRef, headers,
|
|
424
619
|
// balance:{path,currencyPath,currency}, windows:[{key,label,usedPath,limitPath,resetAtPath,resetAtUnit}] } }
|
|
@@ -477,11 +672,17 @@ const customQuota = {
|
|
|
477
672
|
}
|
|
478
673
|
|
|
479
674
|
export function adapterFor(route, cfg) {
|
|
480
|
-
if (route
|
|
675
|
+
if (!route) return null
|
|
676
|
+
if (route === 'deepseek-official' || route === 'deepseek') return deepseekBalance
|
|
481
677
|
if (route === 'deepseek-account') return deepseekAccount
|
|
482
678
|
if (route === 'zai-coding-cn' || route === 'zai') return glmCoding
|
|
483
|
-
if (
|
|
484
|
-
if (
|
|
679
|
+
if (/^openrouter/i.test(route)) return openrouter
|
|
680
|
+
if (/^kimi-coding/i.test(route)) return kimiCoding
|
|
681
|
+
if (/^(moonshot|kimi)/i.test(route)) return moonshotBalance
|
|
682
|
+
if (/^minimax/i.test(route)) return minimaxPlan
|
|
683
|
+
if (/^opencode-go/i.test(route)) return opencodeGo
|
|
684
|
+
if (/^(xiaomi|mimo)/i.test(route)) return xiaomiBalance
|
|
685
|
+
if (/^github-copilot/i.test(route)) return githubCopilot
|
|
485
686
|
if (cfg && cfg.customQuota && cfg.customQuota[route] && cfg.customQuota[route].url) return customQuota
|
|
486
687
|
return null
|
|
487
688
|
}
|
package/lib/client.js
CHANGED
|
@@ -43,7 +43,7 @@ window.__ModuleLoader__.load({
|
|
|
43
43
|
spendTitle: '今日实际消耗', spentWord: '支出', rechargedWord: '充值', windowSpentWord: '额度消耗', thTime: '时间', thInput: '输入', thOutput: '输出', thCache: '缓存读', thDur: '耗时', thTtft: '首字', noLog: '暂无请求记录',
|
|
44
44
|
shortAccount: '账号', remainCompact: '剩 {n}%', remainPrefix: '剩 ', remainSuffix: '', statsError: '统计加载失败', retry: '重试', emptyData: '暂无数据', emptyUsage: '今天还没有调用记录',
|
|
45
45
|
kpiRequests: '模型调用', kpiTokens: '消耗 Tokens', kpiCacheHit: '缓存命中率',
|
|
46
|
-
limitMcp: '每月 MCP 工具', remainCalls: '剩余 {n}/{m} 次',
|
|
46
|
+
limitMcp: '每月 MCP 工具', limitMonthly: '每月限额', remainCalls: '剩余 {n}/{m} 次',
|
|
47
47
|
groupCurrent: '当前会话在用', groupSub: '订阅窗口', groupApi: 'API 余额', groupWallet: '账号钱包', groupOther: '其他',
|
|
48
48
|
trendTitle: '使用趋势',
|
|
49
49
|
thProvider: '供应商', thModel: '模型', thRequests: '请求数', thTokens: 'Tokens',
|
|
@@ -79,7 +79,7 @@ window.__ModuleLoader__.load({
|
|
|
79
79
|
spendTitle: 'Today\'s actual usage', spentWord: 'Spent', rechargedWord: 'Recharged', windowSpentWord: 'Quota used', thTime: 'Time', thInput: 'Input', thOutput: 'Output', thCache: 'Cache', thDur: 'Duration', thTtft: 'TTFT', noLog: 'No requests yet',
|
|
80
80
|
shortAccount: 'Account', remainCompact: '{n}% left', remainPrefix: '', remainSuffix: ' left', statsError: 'Failed to load stats', retry: 'Retry', emptyData: 'No data', emptyUsage: 'No calls today yet',
|
|
81
81
|
kpiRequests: 'Model calls', kpiTokens: 'Tokens', kpiCacheHit: 'Cache hit rate',
|
|
82
|
-
limitMcp: 'MCP tools / month', remainCalls: '{n}/{m} calls left',
|
|
82
|
+
limitMcp: 'MCP tools / month', limitMonthly: 'Monthly', remainCalls: '{n}/{m} calls left',
|
|
83
83
|
groupCurrent: 'In use this session', groupSub: 'Subscription windows', groupApi: 'API balance', groupWallet: 'Account wallet', groupOther: 'Other',
|
|
84
84
|
trendTitle: 'Usage trend',
|
|
85
85
|
thProvider: 'Provider', thModel: 'Model', thRequests: 'Requests', thTokens: 'Tokens',
|
|
@@ -502,6 +502,7 @@ window.__ModuleLoader__.load({
|
|
|
502
502
|
if (w.key === 'rolling') return tr('limitRolling')
|
|
503
503
|
if (w.key === 'weekly') return tr('limitWeekly')
|
|
504
504
|
if (w.key === 'mcp') return tr('limitMcp')
|
|
505
|
+
if (w.key === 'monthly') return tr('limitMonthly')
|
|
505
506
|
return w.label || ''
|
|
506
507
|
}
|
|
507
508
|
function WinRow({ w, tr }) {
|