@kairyou/agent-tools 0.8.0 → 0.9.0
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/README.md +27 -11
- package/README.zh-CN.md +24 -9
- package/dist/usage/core.mjs +100 -20
- package/integrations/usage/lib/format.mjs +79 -5
- package/integrations/usage/lib/routes.mjs +42 -15
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -26,7 +26,9 @@ Generate a Conventional Commits message from staged changes and wait for user co
|
|
|
26
26
|
npx -y skills@latest add kairyou/agent-tools --skill at-commit -g -y
|
|
27
27
|
```
|
|
28
28
|
|
|
29
|
-
|
|
29
|
+
Usage:
|
|
30
|
+
|
|
31
|
+
- `/at-commit [<language>]` — language for the commit description (Conventional Commits tokens stay in English)
|
|
30
32
|
|
|
31
33
|
### at-review
|
|
32
34
|
|
|
@@ -36,7 +38,9 @@ Review changes for correctness bugs, regressions, convention violations, and hig
|
|
|
36
38
|
npx -y skills@latest add kairyou/agent-tools --skill at-review -g -y
|
|
37
39
|
```
|
|
38
40
|
|
|
39
|
-
|
|
41
|
+
Usage:
|
|
42
|
+
|
|
43
|
+
- `/at-review [--fix] [<pr|branch|path>]` — reports findings; `--fix` also applies them
|
|
40
44
|
|
|
41
45
|
### at-simplify
|
|
42
46
|
|
|
@@ -46,7 +50,9 @@ Refactor changes to reduce duplication, lower complexity, and improve code quali
|
|
|
46
50
|
npx -y skills@latest add kairyou/agent-tools --skill at-simplify -g -y
|
|
47
51
|
```
|
|
48
52
|
|
|
49
|
-
|
|
53
|
+
Usage:
|
|
54
|
+
|
|
55
|
+
- `/at-simplify [<pr|branch|path>]`
|
|
50
56
|
|
|
51
57
|
### at-zentao
|
|
52
58
|
|
|
@@ -109,9 +115,10 @@ compatibility and configuration.
|
|
|
109
115
|
For API relay / gateway setups: shows the relay's balance / quota inside the
|
|
110
116
|
agent, so when you pay per use or have plan limits you always know how much you
|
|
111
117
|
have spent and how much is left — without opening the gateway console.
|
|
112
|
-
Works with API-key usage endpoints exposed by
|
|
113
|
-
|
|
114
|
-
|
|
118
|
+
Works with API-key usage endpoints exposed by relay software such as Sub2API,
|
|
119
|
+
One API (including OneHub and DoneHub), New API, and Claude Code Hub. It also
|
|
120
|
+
supports the OpenRouter platform. Compatibility depends on the gateway version
|
|
121
|
+
and whether the corresponding usage endpoint is enabled.
|
|
115
122
|
|
|
116
123
|
```bash
|
|
117
124
|
npx -y @kairyou/agent-tools@latest usage -a claude codex opencode
|
|
@@ -138,7 +145,9 @@ endpoint and key — and tune `providerUsage` in `~/.agent-tools/config.jsonc`:
|
|
|
138
145
|
```jsonc
|
|
139
146
|
{
|
|
140
147
|
"providerUsage": {
|
|
141
|
-
|
|
148
|
+
// auto | sub2api | openai-compatible | one-api | one-hub |
|
|
149
|
+
// done-hub | new-api | claude-code-hub | openrouter | <custom-route-id>
|
|
150
|
+
"preset": "auto",
|
|
142
151
|
"days": 30, // how many recent days of spend to count
|
|
143
152
|
"debug": false // true: log probes to ~/.agent-tools/logs/usage-debug.log
|
|
144
153
|
}
|
|
@@ -152,15 +161,22 @@ custom route id is also accepted.
|
|
|
152
161
|
Output examples:
|
|
153
162
|
|
|
154
163
|
```text
|
|
155
|
-
#
|
|
164
|
+
# Plan limits (sub2api / openai-compatible).
|
|
156
165
|
D $0.0/$100 | W $0.0/$300 | Exp 07-08
|
|
157
166
|
|
|
158
|
-
#
|
|
167
|
+
# Multi-window limits (claude-code-hub).
|
|
168
|
+
5h $2.1/$10.0 | D $8.0/$20.0 | T $19.0/$100 | Exp 08-31
|
|
169
|
+
|
|
170
|
+
# Balance and usage (one-api / one-hub / done-hub / new-api / openrouter).
|
|
171
|
+
balance $15.0 | used $5.0/$20.0
|
|
172
|
+
|
|
173
|
+
# Wallet and recent spend (sub2api).
|
|
159
174
|
balance $362 | today $61.7 | 30d $566
|
|
160
175
|
```
|
|
161
176
|
|
|
162
|
-
Fields: `D/W/M` are daily/weekly/monthly spend against
|
|
163
|
-
the plan expiry; `balance` is wallet credit; `today` and `30d`
|
|
177
|
+
Fields: `5h/D/W/M/T` are five-hour/daily/weekly/monthly/total spend against
|
|
178
|
+
limits; `Exp` is the plan expiry; `balance` is wallet credit; `today` and `30d`
|
|
179
|
+
are API spend.
|
|
164
180
|
|
|
165
181
|
#### Custom gateway routes
|
|
166
182
|
|
package/README.zh-CN.md
CHANGED
|
@@ -24,7 +24,9 @@ npx -y skills@latest add kairyou/agent-tools --skill <name...> -g -y
|
|
|
24
24
|
npx -y skills@latest add kairyou/agent-tools --skill at-commit -g -y
|
|
25
25
|
```
|
|
26
26
|
|
|
27
|
-
|
|
27
|
+
用法:
|
|
28
|
+
|
|
29
|
+
- `/at-commit [<语言>]` — 指定 commit description 的语言(Conventional Commits 语法部分保持英文)
|
|
28
30
|
|
|
29
31
|
### at-review
|
|
30
32
|
|
|
@@ -34,7 +36,9 @@ npx -y skills@latest add kairyou/agent-tools --skill at-commit -g -y
|
|
|
34
36
|
npx -y skills@latest add kairyou/agent-tools --skill at-review -g -y
|
|
35
37
|
```
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
用法:
|
|
40
|
+
|
|
41
|
+
- `/at-review [--fix] [<pr|分支|路径>]` — 输出审查结果; 加 `--fix` 则同时应用修复
|
|
38
42
|
|
|
39
43
|
### at-simplify
|
|
40
44
|
|
|
@@ -44,7 +48,9 @@ npx -y skills@latest add kairyou/agent-tools --skill at-review -g -y
|
|
|
44
48
|
npx -y skills@latest add kairyou/agent-tools --skill at-simplify -g -y
|
|
45
49
|
```
|
|
46
50
|
|
|
47
|
-
|
|
51
|
+
用法:
|
|
52
|
+
|
|
53
|
+
- `/at-simplify [<pr|分支|路径>]`
|
|
48
54
|
|
|
49
55
|
### at-zentao
|
|
50
56
|
|
|
@@ -104,8 +110,9 @@ npx -y @kairyou/agent-tools@latest statusline -a claude
|
|
|
104
110
|
|
|
105
111
|
面向使用 API 中转的场景: 在 agent 内直接显示中转网关的余额/额度, 按量付费或
|
|
106
112
|
有套餐限额时, 随时知道花了多少, 还剩多少, 不用切出去登录网关后台.
|
|
107
|
-
支持 Sub2API, One API, New API 与
|
|
108
|
-
|
|
113
|
+
支持 Sub2API, One API (包括 OneHub 与 DoneHub), New API 与 Claude Code Hub
|
|
114
|
+
等中转程序提供的 API Key 用量接口, 也支持 OpenRouter 平台. 具体兼容性取决于
|
|
115
|
+
网关版本及其是否开放相应接口.
|
|
109
116
|
|
|
110
117
|
```bash
|
|
111
118
|
npx -y @kairyou/agent-tools@latest usage -a claude codex opencode
|
|
@@ -130,7 +137,9 @@ provider 的 `base_url` 和密钥; Claude Code: 读取 `ANTHROPIC_BASE_URL` 与
|
|
|
130
137
|
```jsonc
|
|
131
138
|
{
|
|
132
139
|
"providerUsage": {
|
|
133
|
-
|
|
140
|
+
// auto | sub2api | openai-compatible | one-api | one-hub |
|
|
141
|
+
// done-hub | new-api | claude-code-hub | openrouter | <自定义 route id>
|
|
142
|
+
"preset": "auto",
|
|
134
143
|
"days": 30, // 统计最近多少天的消耗
|
|
135
144
|
"debug": false // true: 探测过程写入 ~/.agent-tools/logs/usage-debug.log
|
|
136
145
|
}
|
|
@@ -143,14 +152,20 @@ provider 的 `base_url` 和密钥; Claude Code: 读取 `ANTHROPIC_BASE_URL` 与
|
|
|
143
152
|
显示效果示例:
|
|
144
153
|
|
|
145
154
|
```text
|
|
146
|
-
#
|
|
155
|
+
# 套餐限额 (sub2api / openai-compatible).
|
|
147
156
|
D $0.0/$100 | W $0.0/$300 | Exp 07-08
|
|
148
157
|
|
|
149
|
-
#
|
|
158
|
+
# 多窗口限额 (claude-code-hub).
|
|
159
|
+
5h $2.1/$10.0 | D $8.0/$20.0 | T $19.0/$100 | Exp 08-31
|
|
160
|
+
|
|
161
|
+
# 余额与用量 (one-api / one-hub / done-hub / new-api / openrouter).
|
|
162
|
+
balance $15.0 | used $5.0/$20.0
|
|
163
|
+
|
|
164
|
+
# 钱包与近期消耗 (sub2api).
|
|
150
165
|
balance $362 | today $61.7 | 30d $566
|
|
151
166
|
```
|
|
152
167
|
|
|
153
|
-
字段含义: `D/W/M`
|
|
168
|
+
字段含义: `5h/D/W/M/T` 是 5 小时/日/周/月/总消耗与上限, `Exp` 是套餐到期日,
|
|
154
169
|
`balance` 是钱包余额, `today` / `30d` 是今日与近 30 天 API 消耗.
|
|
155
170
|
|
|
156
171
|
#### 自定义网关路由
|
package/dist/usage/core.mjs
CHANGED
|
@@ -1261,6 +1261,7 @@ async function requestJson(url, options = {}) {
|
|
|
1261
1261
|
}
|
|
1262
1262
|
|
|
1263
1263
|
// integrations/usage/lib/format.mjs
|
|
1264
|
+
var ONE_API_HARD_LIMIT_SENTINEL_USD = 1e6;
|
|
1264
1265
|
function pickNumber(obj, keys) {
|
|
1265
1266
|
for (const key of keys) {
|
|
1266
1267
|
const value = obj?.[key];
|
|
@@ -1344,21 +1345,39 @@ async function formatQuota(data) {
|
|
|
1344
1345
|
const keys = Object.keys(root || {}).slice(0, 4).join(", ");
|
|
1345
1346
|
return keys ? `received (${keys})` : `checked ${unit}`;
|
|
1346
1347
|
}
|
|
1347
|
-
|
|
1348
|
+
function newApiTokenQuota(data) {
|
|
1348
1349
|
const root = usageRoot(data);
|
|
1349
1350
|
const unlimited = root?.unlimited_quota === true || root?.unlimitedQuota === true;
|
|
1350
|
-
const quota = pickNumber(root, [
|
|
1351
|
-
|
|
1352
|
-
|
|
1351
|
+
const quota = pickNumber(root, [
|
|
1352
|
+
"quota",
|
|
1353
|
+
"limit",
|
|
1354
|
+
"total_quota",
|
|
1355
|
+
"totalQuota",
|
|
1356
|
+
"total_granted",
|
|
1357
|
+
"totalGranted"
|
|
1358
|
+
]);
|
|
1359
|
+
const used = pickNumber(root, ["used_quota", "usedQuota", "used", "total_used", "totalUsed"]);
|
|
1360
|
+
let remaining = pickNumber(root, [
|
|
1361
|
+
"remain_quota",
|
|
1362
|
+
"remainQuota",
|
|
1363
|
+
"remaining",
|
|
1364
|
+
"balance",
|
|
1365
|
+
"total_available",
|
|
1366
|
+
"totalAvailable"
|
|
1367
|
+
]);
|
|
1353
1368
|
if (remaining === void 0 && quota !== void 0 && used !== void 0) {
|
|
1354
1369
|
remaining = Math.max(0, quota - used);
|
|
1355
1370
|
}
|
|
1371
|
+
return { unlimited, quota, used, remaining };
|
|
1372
|
+
}
|
|
1373
|
+
async function formatNewApiTokenLine(data) {
|
|
1374
|
+
const { unlimited, quota, used, remaining } = newApiTokenQuota(data);
|
|
1356
1375
|
if (!unlimited && quota === void 0 && used === void 0 && remaining === void 0) {
|
|
1357
1376
|
throw new Error("NewAPI token usage payload has no quota fields");
|
|
1358
1377
|
}
|
|
1359
1378
|
const parts = [];
|
|
1360
1379
|
if (unlimited) parts.push("unlimited");
|
|
1361
|
-
if (remaining !== void 0) parts.push(`balance ${await formatNewApiQuota(remaining)}`);
|
|
1380
|
+
if (!unlimited && remaining !== void 0) parts.push(`balance ${await formatNewApiQuota(remaining)}`);
|
|
1362
1381
|
if (used !== void 0 && quota !== void 0) {
|
|
1363
1382
|
parts.push(`used ${await formatNewApiQuota(used)}/${await formatNewApiQuota(quota)}`);
|
|
1364
1383
|
} else if (used !== void 0) {
|
|
@@ -1384,8 +1403,51 @@ function formatOpenRouterLine(data) {
|
|
|
1384
1403
|
return parts.join(" | ");
|
|
1385
1404
|
}
|
|
1386
1405
|
function formatOneApiBillingLine(limit, used) {
|
|
1406
|
+
if (!hasSpendableOneApiLimit(limit)) return `used ${formatMoney(used)}`;
|
|
1387
1407
|
return `balance ${formatMoney(Math.max(0, limit - used))} | used ${formatMoney(used)}/${formatMoney(limit)}`;
|
|
1388
1408
|
}
|
|
1409
|
+
function hasSpendableOneApiLimit(limit) {
|
|
1410
|
+
return Number.isFinite(limit) && limit >= 0 && limit < ONE_API_HARD_LIMIT_SENTINEL_USD;
|
|
1411
|
+
}
|
|
1412
|
+
function effectiveClaudeCodeHubWindow(root, suffix) {
|
|
1413
|
+
const keyLimit = pickNumber(root, [`keyLimit${suffix}Usd`]);
|
|
1414
|
+
if (keyLimit !== void 0 && keyLimit > 0) {
|
|
1415
|
+
return { limit: keyLimit, used: pickNumber(root, [`keyCurrent${suffix}Usd`]) };
|
|
1416
|
+
}
|
|
1417
|
+
const userLimit = pickNumber(root, [`userLimit${suffix}Usd`]);
|
|
1418
|
+
return {
|
|
1419
|
+
limit: userLimit !== void 0 && userLimit > 0 ? userLimit : void 0,
|
|
1420
|
+
used: pickNumber(root, [`userCurrent${suffix}Usd`])
|
|
1421
|
+
};
|
|
1422
|
+
}
|
|
1423
|
+
function earliestDate(...values) {
|
|
1424
|
+
return values.map((value) => ({ value, time: Date.parse(value) })).filter((entry) => entry.value && Number.isFinite(entry.time)).sort((left, right) => left.time - right.time)[0]?.value;
|
|
1425
|
+
}
|
|
1426
|
+
function formatClaudeCodeHubLine(data) {
|
|
1427
|
+
const root = usageRoot(data);
|
|
1428
|
+
const windows = [
|
|
1429
|
+
["5h", "5h"],
|
|
1430
|
+
["D", "Daily"],
|
|
1431
|
+
["W", "Weekly"],
|
|
1432
|
+
["M", "Monthly"],
|
|
1433
|
+
["T", "Total"]
|
|
1434
|
+
];
|
|
1435
|
+
const parts = [];
|
|
1436
|
+
for (const [label, suffix] of windows) {
|
|
1437
|
+
const { limit, used } = effectiveClaudeCodeHubWindow(root, suffix);
|
|
1438
|
+
if (limit !== void 0 && used !== void 0) {
|
|
1439
|
+
parts.push(`${label} ${formatMoney(used)}/${formatMoney(limit)}`);
|
|
1440
|
+
}
|
|
1441
|
+
}
|
|
1442
|
+
if (parts.length === 0) {
|
|
1443
|
+
const totalUsed = pickNumber(root, ["keyCurrentTotalUsd", "userCurrentTotalUsd"]);
|
|
1444
|
+
if (totalUsed !== void 0) parts.push(`used ${formatMoney(totalUsed)}`);
|
|
1445
|
+
}
|
|
1446
|
+
const expires = shortDate(earliestDate(root?.expiresAt, root?.userExpiresAt));
|
|
1447
|
+
if (expires) parts.push(`Exp ${expires}`);
|
|
1448
|
+
if (parts.length === 0) throw new Error("Claude Code Hub quota payload has no quota fields");
|
|
1449
|
+
return parts.join(" | ");
|
|
1450
|
+
}
|
|
1389
1451
|
function formatQuotaLimitedLine(root) {
|
|
1390
1452
|
const quota = root?.quota || {};
|
|
1391
1453
|
const limit = pickNumber(quota, ["limit", "quota"]);
|
|
@@ -1466,13 +1528,7 @@ async function fetchNewApiTokenUsage(context) {
|
|
|
1466
1528
|
key: context.key,
|
|
1467
1529
|
name: "New API token usage"
|
|
1468
1530
|
});
|
|
1469
|
-
const
|
|
1470
|
-
const quota = pickNumber(root, ["quota", "limit", "total_quota", "totalQuota"]);
|
|
1471
|
-
const used = pickNumber(root, ["used_quota", "usedQuota", "used"]);
|
|
1472
|
-
let remaining = pickNumber(root, ["remain_quota", "remainQuota", "remaining", "balance"]);
|
|
1473
|
-
if (remaining === void 0 && quota !== void 0 && used !== void 0) {
|
|
1474
|
-
remaining = Math.max(0, quota - used);
|
|
1475
|
-
}
|
|
1531
|
+
const { quota, used, remaining } = newApiTokenQuota(json);
|
|
1476
1532
|
const scale = await newApiQuotaScale();
|
|
1477
1533
|
const quotaForWarning = scale ? quota / scale : quota;
|
|
1478
1534
|
const usedForWarning = scale ? used / scale : used;
|
|
@@ -1506,13 +1562,16 @@ async function fetchOneApiBillingUsage(context) {
|
|
|
1506
1562
|
throw new Error("One API billing payload has no quota fields");
|
|
1507
1563
|
}
|
|
1508
1564
|
const used = usageCents / 100;
|
|
1565
|
+
const spendableLimit = hasSpendableOneApiLimit(limit);
|
|
1509
1566
|
const normalized = {
|
|
1510
|
-
mode: "quota_limited",
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1567
|
+
mode: spendableLimit ? "quota_limited" : "unrestricted",
|
|
1568
|
+
...spendableLimit ? {
|
|
1569
|
+
quota: {
|
|
1570
|
+
limit,
|
|
1571
|
+
used,
|
|
1572
|
+
remaining: Math.max(0, limit - used)
|
|
1573
|
+
}
|
|
1574
|
+
} : { used },
|
|
1516
1575
|
unit: "USD",
|
|
1517
1576
|
source: "oneapi-billing",
|
|
1518
1577
|
raw: { subscription, usage }
|
|
@@ -1524,6 +1583,19 @@ async function fetchOneApiBillingUsage(context) {
|
|
|
1524
1583
|
normalized
|
|
1525
1584
|
);
|
|
1526
1585
|
}
|
|
1586
|
+
async function fetchClaudeCodeHubUsage(context) {
|
|
1587
|
+
const base = cleanBaseUrl(context.baseUrl).replace(/\/api\/v1$/i, "").replace(/\/v1$/i, "");
|
|
1588
|
+
const json = await requestJson(joinUrl(base, "/api/v1/me/quota"), {
|
|
1589
|
+
key: context.key,
|
|
1590
|
+
name: "Claude Code Hub quota"
|
|
1591
|
+
});
|
|
1592
|
+
return usageResult(
|
|
1593
|
+
context,
|
|
1594
|
+
"claude-code-hub",
|
|
1595
|
+
formatClaudeCodeHubLine(json),
|
|
1596
|
+
json
|
|
1597
|
+
);
|
|
1598
|
+
}
|
|
1527
1599
|
async function fetchOpenRouterUsage(context) {
|
|
1528
1600
|
const base = cleanBaseUrl(context.baseUrl).includes("/api/v1") ? cleanBaseUrl(context.baseUrl) : joinUrl(serviceRoot(context.baseUrl), "/api/v1");
|
|
1529
1601
|
const endpoints = [
|
|
@@ -1562,6 +1634,11 @@ var USAGE_ROUTES = {
|
|
|
1562
1634
|
id: "openrouter",
|
|
1563
1635
|
path: "/api/v1/key",
|
|
1564
1636
|
run: fetchOpenRouterUsage
|
|
1637
|
+
},
|
|
1638
|
+
"claude-code-hub": {
|
|
1639
|
+
id: "claude-code-hub",
|
|
1640
|
+
path: "/api/v1/me/quota",
|
|
1641
|
+
run: fetchClaudeCodeHubUsage
|
|
1565
1642
|
}
|
|
1566
1643
|
};
|
|
1567
1644
|
var CUSTOM_ROUTE_HELPERS = { requestJson, agentConfig };
|
|
@@ -1634,12 +1711,15 @@ async function usageRouteIds(context) {
|
|
|
1634
1711
|
"openai-compatible": ["v1-usage"],
|
|
1635
1712
|
"new-api": ["newapi-token"],
|
|
1636
1713
|
"one-api": ["oneapi-billing"],
|
|
1637
|
-
"
|
|
1714
|
+
"one-hub": ["oneapi-billing"],
|
|
1715
|
+
"done-hub": ["oneapi-billing"],
|
|
1716
|
+
"openrouter": ["openrouter"],
|
|
1717
|
+
"claude-code-hub": ["claude-code-hub"]
|
|
1638
1718
|
};
|
|
1639
1719
|
if (routes[preset]) return routes[preset];
|
|
1640
1720
|
if (preset !== "auto") return (await routeRegistry())[preset] ? [preset] : [];
|
|
1641
1721
|
const customIds = (await customRoutes()).map((route) => route.id);
|
|
1642
|
-
const builtinIds = hostIncludes(context.baseUrl, "openrouter.ai") ? ["openrouter"] : ["v1-usage", "newapi-token", "oneapi-billing"];
|
|
1722
|
+
const builtinIds = hostIncludes(context.baseUrl, "openrouter.ai") ? ["openrouter"] : ["v1-usage", "newapi-token", "oneapi-billing", "claude-code-hub"];
|
|
1643
1723
|
return [.../* @__PURE__ */ new Set([...customIds, ...builtinIds])];
|
|
1644
1724
|
}
|
|
1645
1725
|
async function cachedUsageRoute(context, registry) {
|
|
@@ -4,6 +4,8 @@
|
|
|
4
4
|
|
|
5
5
|
import { newApiQuotaScale, providerUsageDays } from "./config.mjs";
|
|
6
6
|
|
|
7
|
+
const ONE_API_HARD_LIMIT_SENTINEL_USD = 1_000_000;
|
|
8
|
+
|
|
7
9
|
export function pickNumber(obj, keys) {
|
|
8
10
|
for (const key of keys) {
|
|
9
11
|
const value = obj?.[key];
|
|
@@ -112,15 +114,34 @@ export async function formatQuota(data) {
|
|
|
112
114
|
return keys ? `received (${keys})` : `checked ${unit}`;
|
|
113
115
|
}
|
|
114
116
|
|
|
115
|
-
export
|
|
117
|
+
export function newApiTokenQuota(data) {
|
|
116
118
|
const root = usageRoot(data);
|
|
117
119
|
const unlimited = root?.unlimited_quota === true || root?.unlimitedQuota === true;
|
|
118
|
-
const quota = pickNumber(root, [
|
|
119
|
-
|
|
120
|
-
|
|
120
|
+
const quota = pickNumber(root, [
|
|
121
|
+
"quota",
|
|
122
|
+
"limit",
|
|
123
|
+
"total_quota",
|
|
124
|
+
"totalQuota",
|
|
125
|
+
"total_granted",
|
|
126
|
+
"totalGranted",
|
|
127
|
+
]);
|
|
128
|
+
const used = pickNumber(root, ["used_quota", "usedQuota", "used", "total_used", "totalUsed"]);
|
|
129
|
+
let remaining = pickNumber(root, [
|
|
130
|
+
"remain_quota",
|
|
131
|
+
"remainQuota",
|
|
132
|
+
"remaining",
|
|
133
|
+
"balance",
|
|
134
|
+
"total_available",
|
|
135
|
+
"totalAvailable",
|
|
136
|
+
]);
|
|
121
137
|
if (remaining === undefined && quota !== undefined && used !== undefined) {
|
|
122
138
|
remaining = Math.max(0, quota - used);
|
|
123
139
|
}
|
|
140
|
+
return { unlimited, quota, used, remaining };
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
export async function formatNewApiTokenLine(data) {
|
|
144
|
+
const { unlimited, quota, used, remaining } = newApiTokenQuota(data);
|
|
124
145
|
|
|
125
146
|
if (!unlimited && quota === undefined && used === undefined && remaining === undefined) {
|
|
126
147
|
throw new Error("NewAPI token usage payload has no quota fields");
|
|
@@ -128,7 +149,7 @@ export async function formatNewApiTokenLine(data) {
|
|
|
128
149
|
|
|
129
150
|
const parts = [];
|
|
130
151
|
if (unlimited) parts.push("unlimited");
|
|
131
|
-
if (remaining !== undefined) parts.push(`balance ${await formatNewApiQuota(remaining)}`);
|
|
152
|
+
if (!unlimited && remaining !== undefined) parts.push(`balance ${await formatNewApiQuota(remaining)}`);
|
|
132
153
|
if (used !== undefined && quota !== undefined) {
|
|
133
154
|
parts.push(`used ${await formatNewApiQuota(used)}/${await formatNewApiQuota(quota)}`);
|
|
134
155
|
} else if (used !== undefined) {
|
|
@@ -158,9 +179,62 @@ export function formatOpenRouterLine(data) {
|
|
|
158
179
|
}
|
|
159
180
|
|
|
160
181
|
export function formatOneApiBillingLine(limit, used) {
|
|
182
|
+
if (!hasSpendableOneApiLimit(limit)) return `used ${formatMoney(used)}`;
|
|
161
183
|
return `balance ${formatMoney(Math.max(0, limit - used))} | used ${formatMoney(used)}/${formatMoney(limit)}`;
|
|
162
184
|
}
|
|
163
185
|
|
|
186
|
+
export function hasSpendableOneApiLimit(limit) {
|
|
187
|
+
return Number.isFinite(limit) && limit >= 0 && limit < ONE_API_HARD_LIMIT_SENTINEL_USD;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
function effectiveClaudeCodeHubWindow(root, suffix) {
|
|
191
|
+
const keyLimit = pickNumber(root, [`keyLimit${suffix}Usd`]);
|
|
192
|
+
if (keyLimit !== undefined && keyLimit > 0) {
|
|
193
|
+
return { limit: keyLimit, used: pickNumber(root, [`keyCurrent${suffix}Usd`]) };
|
|
194
|
+
}
|
|
195
|
+
const userLimit = pickNumber(root, [`userLimit${suffix}Usd`]);
|
|
196
|
+
return {
|
|
197
|
+
limit: userLimit !== undefined && userLimit > 0 ? userLimit : undefined,
|
|
198
|
+
used: pickNumber(root, [`userCurrent${suffix}Usd`]),
|
|
199
|
+
};
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
function earliestDate(...values) {
|
|
203
|
+
return values
|
|
204
|
+
.map((value) => ({ value, time: Date.parse(value) }))
|
|
205
|
+
.filter((entry) => entry.value && Number.isFinite(entry.time))
|
|
206
|
+
.sort((left, right) => left.time - right.time)[0]?.value;
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
export function formatClaudeCodeHubLine(data) {
|
|
210
|
+
const root = usageRoot(data);
|
|
211
|
+
const windows = [
|
|
212
|
+
["5h", "5h"],
|
|
213
|
+
["D", "Daily"],
|
|
214
|
+
["W", "Weekly"],
|
|
215
|
+
["M", "Monthly"],
|
|
216
|
+
["T", "Total"],
|
|
217
|
+
];
|
|
218
|
+
const parts = [];
|
|
219
|
+
|
|
220
|
+
for (const [label, suffix] of windows) {
|
|
221
|
+
const { limit, used } = effectiveClaudeCodeHubWindow(root, suffix);
|
|
222
|
+
if (limit !== undefined && used !== undefined) {
|
|
223
|
+
parts.push(`${label} ${formatMoney(used)}/${formatMoney(limit)}`);
|
|
224
|
+
}
|
|
225
|
+
}
|
|
226
|
+
|
|
227
|
+
if (parts.length === 0) {
|
|
228
|
+
const totalUsed = pickNumber(root, ["keyCurrentTotalUsd", "userCurrentTotalUsd"]);
|
|
229
|
+
if (totalUsed !== undefined) parts.push(`used ${formatMoney(totalUsed)}`);
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const expires = shortDate(earliestDate(root?.expiresAt, root?.userExpiresAt));
|
|
233
|
+
if (expires) parts.push(`Exp ${expires}`);
|
|
234
|
+
if (parts.length === 0) throw new Error("Claude Code Hub quota payload has no quota fields");
|
|
235
|
+
return parts.join(" | ");
|
|
236
|
+
}
|
|
237
|
+
|
|
164
238
|
function formatQuotaLimitedLine(root) {
|
|
165
239
|
const quota = root?.quota || {};
|
|
166
240
|
const limit = pickNumber(quota, ["limit", "quota"]);
|
|
@@ -24,9 +24,12 @@ import {
|
|
|
24
24
|
usageRoot,
|
|
25
25
|
hasV1UsageFields,
|
|
26
26
|
formatQuota,
|
|
27
|
+
newApiTokenQuota,
|
|
27
28
|
formatNewApiTokenLine,
|
|
28
29
|
formatOneApiBillingLine,
|
|
30
|
+
hasSpendableOneApiLimit,
|
|
29
31
|
formatOpenRouterLine,
|
|
32
|
+
formatClaudeCodeHubLine,
|
|
30
33
|
} from "./format.mjs";
|
|
31
34
|
import { readRouteCache } from "./cache.mjs";
|
|
32
35
|
|
|
@@ -66,13 +69,7 @@ async function fetchNewApiTokenUsage(context) {
|
|
|
66
69
|
key: context.key,
|
|
67
70
|
name: "New API token usage",
|
|
68
71
|
});
|
|
69
|
-
const
|
|
70
|
-
const quota = pickNumber(root, ["quota", "limit", "total_quota", "totalQuota"]);
|
|
71
|
-
const used = pickNumber(root, ["used_quota", "usedQuota", "used"]);
|
|
72
|
-
let remaining = pickNumber(root, ["remain_quota", "remainQuota", "remaining", "balance"]);
|
|
73
|
-
if (remaining === undefined && quota !== undefined && used !== undefined) {
|
|
74
|
-
remaining = Math.max(0, quota - used);
|
|
75
|
-
}
|
|
72
|
+
const { quota, used, remaining } = newApiTokenQuota(json);
|
|
76
73
|
const scale = await newApiQuotaScale();
|
|
77
74
|
const quotaForWarning = scale ? quota / scale : quota;
|
|
78
75
|
const usedForWarning = scale ? used / scale : used;
|
|
@@ -109,13 +106,16 @@ async function fetchOneApiBillingUsage(context) {
|
|
|
109
106
|
throw new Error("One API billing payload has no quota fields");
|
|
110
107
|
}
|
|
111
108
|
const used = usageCents / 100;
|
|
109
|
+
const spendableLimit = hasSpendableOneApiLimit(limit);
|
|
112
110
|
const normalized = {
|
|
113
|
-
mode: "quota_limited",
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
111
|
+
mode: spendableLimit ? "quota_limited" : "unrestricted",
|
|
112
|
+
...(spendableLimit ? {
|
|
113
|
+
quota: {
|
|
114
|
+
limit,
|
|
115
|
+
used,
|
|
116
|
+
remaining: Math.max(0, limit - used),
|
|
117
|
+
},
|
|
118
|
+
} : { used }),
|
|
119
119
|
unit: "USD",
|
|
120
120
|
source: "oneapi-billing",
|
|
121
121
|
raw: { subscription, usage },
|
|
@@ -128,6 +128,24 @@ async function fetchOneApiBillingUsage(context) {
|
|
|
128
128
|
);
|
|
129
129
|
}
|
|
130
130
|
|
|
131
|
+
// Claude Code Hub exposes self-scoped quota windows to the same API key used
|
|
132
|
+
// for model requests. The endpoint accepts the existing Bearer authentication.
|
|
133
|
+
async function fetchClaudeCodeHubUsage(context) {
|
|
134
|
+
const base = cleanBaseUrl(context.baseUrl)
|
|
135
|
+
.replace(/\/api\/v1$/i, "")
|
|
136
|
+
.replace(/\/v1$/i, "");
|
|
137
|
+
const json = await requestJson(joinUrl(base, "/api/v1/me/quota"), {
|
|
138
|
+
key: context.key,
|
|
139
|
+
name: "Claude Code Hub quota",
|
|
140
|
+
});
|
|
141
|
+
return usageResult(
|
|
142
|
+
context,
|
|
143
|
+
"claude-code-hub",
|
|
144
|
+
formatClaudeCodeHubLine(json),
|
|
145
|
+
json
|
|
146
|
+
);
|
|
147
|
+
}
|
|
148
|
+
|
|
131
149
|
// OpenRouter exposes normal API-key usage at /api/v1/key. Some accounts also
|
|
132
150
|
// expose credits at /api/v1/credits; keep this route isolated because
|
|
133
151
|
// OpenRouter's base URL already includes /api/v1, unlike New API.
|
|
@@ -173,6 +191,11 @@ const USAGE_ROUTES = {
|
|
|
173
191
|
path: "/api/v1/key",
|
|
174
192
|
run: fetchOpenRouterUsage,
|
|
175
193
|
},
|
|
194
|
+
"claude-code-hub": {
|
|
195
|
+
id: "claude-code-hub",
|
|
196
|
+
path: "/api/v1/me/quota",
|
|
197
|
+
run: fetchClaudeCodeHubUsage,
|
|
198
|
+
},
|
|
176
199
|
};
|
|
177
200
|
|
|
178
201
|
// User-authored gateway routes, declared in config.jsonc:
|
|
@@ -256,7 +279,8 @@ async function routeRegistry() {
|
|
|
256
279
|
return registry;
|
|
257
280
|
}
|
|
258
281
|
|
|
259
|
-
// Presets select API-key usage protocols
|
|
282
|
+
// Presets primarily select API-key usage protocols. Brand aliases map to a
|
|
283
|
+
// shared protocol when the upstream gateway exposes the same endpoints.
|
|
260
284
|
async function usageRouteIds(context) {
|
|
261
285
|
const preset = await usagePreset();
|
|
262
286
|
const routes = {
|
|
@@ -264,7 +288,10 @@ async function usageRouteIds(context) {
|
|
|
264
288
|
"openai-compatible": ["v1-usage"],
|
|
265
289
|
"new-api": ["newapi-token"],
|
|
266
290
|
"one-api": ["oneapi-billing"],
|
|
291
|
+
"one-hub": ["oneapi-billing"],
|
|
292
|
+
"done-hub": ["oneapi-billing"],
|
|
267
293
|
"openrouter": ["openrouter"],
|
|
294
|
+
"claude-code-hub": ["claude-code-hub"],
|
|
268
295
|
};
|
|
269
296
|
if (routes[preset]) return routes[preset];
|
|
270
297
|
|
|
@@ -275,7 +302,7 @@ async function usageRouteIds(context) {
|
|
|
275
302
|
const customIds = (await customRoutes()).map((route) => route.id);
|
|
276
303
|
const builtinIds = hostIncludes(context.baseUrl, "openrouter.ai")
|
|
277
304
|
? ["openrouter"]
|
|
278
|
-
: ["v1-usage", "newapi-token", "oneapi-billing"];
|
|
305
|
+
: ["v1-usage", "newapi-token", "oneapi-billing", "claude-code-hub"];
|
|
279
306
|
return [...new Set([...customIds, ...builtinIds])];
|
|
280
307
|
}
|
|
281
308
|
|
package/package.json
CHANGED