@kairyou/agent-tools 0.6.0 → 0.7.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 +9 -6
- package/README.zh-CN.md +7 -5
- package/dist/usage/core.mjs +33 -122
- package/integrations/usage/lib/config.mjs +2 -24
- package/integrations/usage/lib/format.mjs +3 -40
- package/integrations/usage/lib/routes.mjs +37 -96
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -109,9 +109,9 @@ and never touch your edits or comments.
|
|
|
109
109
|
For API relay / gateway setups: shows the relay's balance / quota inside the
|
|
110
110
|
agent, so when you pay per use or have plan limits you always know how much you
|
|
111
111
|
have spent and how much is left — without opening the gateway console.
|
|
112
|
-
Works with
|
|
113
|
-
|
|
114
|
-
|
|
112
|
+
Works with API-key usage endpoints exposed by Sub2API, One API, New API, and
|
|
113
|
+
OpenRouter. Compatibility depends on the gateway version and whether the
|
|
114
|
+
corresponding usage endpoint is enabled.
|
|
115
115
|
|
|
116
116
|
```bash
|
|
117
117
|
npx -y @kairyou/agent-tools@latest usage -a claude
|
|
@@ -140,14 +140,17 @@ endpoint and key — and tune `providerUsage` in `~/.agent-tools/config.jsonc`:
|
|
|
140
140
|
```jsonc
|
|
141
141
|
{
|
|
142
142
|
"providerUsage": {
|
|
143
|
-
"preset": "auto", // sub2api |
|
|
144
|
-
"
|
|
145
|
-
"days": 30, // spend window for the "30d" field (max 90)
|
|
143
|
+
"preset": "auto", // auto | sub2api | one-api | new-api | openrouter | <custom-route-id>
|
|
144
|
+
"days": 30, // how many recent days of spend to count
|
|
146
145
|
"debug": false // true: log probes to ~/.agent-tools/logs/usage-debug.log
|
|
147
146
|
}
|
|
148
147
|
}
|
|
149
148
|
```
|
|
150
149
|
|
|
150
|
+
Keep `preset` set to `auto` for automatic detection. Select a specific protocol
|
|
151
|
+
only when you know which usage endpoint the gateway exposes; a configured
|
|
152
|
+
custom route id is also accepted.
|
|
153
|
+
|
|
151
154
|
#### Custom gateway routes
|
|
152
155
|
|
|
153
156
|
For gateways the built-in probes cannot reach (e.g. cookie-authenticated
|
package/README.zh-CN.md
CHANGED
|
@@ -102,8 +102,8 @@ npx -y @kairyou/agent-tools@latest statusline -a claude
|
|
|
102
102
|
|
|
103
103
|
面向使用 API 中转的场景: 在 agent 内直接显示中转网关的余额/额度, 按量付费或
|
|
104
104
|
有套餐限额时, 随时知道花了多少, 还剩多少, 不用切出去登录网关后台.
|
|
105
|
-
支持 Sub2API,
|
|
106
|
-
|
|
105
|
+
支持 Sub2API, One API, New API 与 OpenRouter 提供的 API Key 用量接口.
|
|
106
|
+
具体兼容性取决于网关版本及其是否开放相应接口.
|
|
107
107
|
|
|
108
108
|
```bash
|
|
109
109
|
npx -y @kairyou/agent-tools@latest usage -a claude
|
|
@@ -130,14 +130,16 @@ provider 的 `base_url` 和密钥; Claude Code: 读取 `ANTHROPIC_BASE_URL` 与
|
|
|
130
130
|
```jsonc
|
|
131
131
|
{
|
|
132
132
|
"providerUsage": {
|
|
133
|
-
"preset": "auto", // sub2api |
|
|
134
|
-
"
|
|
135
|
-
"days": 30, // "30d" 字段的统计窗口(最大 90)
|
|
133
|
+
"preset": "auto", // auto | sub2api | one-api | new-api | openrouter | <自定义 route id>
|
|
134
|
+
"days": 30, // 统计最近多少天的消耗
|
|
136
135
|
"debug": false // true: 探测过程写入 ~/.agent-tools/logs/usage-debug.log
|
|
137
136
|
}
|
|
138
137
|
}
|
|
139
138
|
```
|
|
140
139
|
|
|
140
|
+
保持 `preset: "auto"` 即可自动探测. 只有明确知道网关开放的是哪种用量协议时,
|
|
141
|
+
才指定相应的内置 preset 或已配置的自定义 route id.
|
|
142
|
+
|
|
141
143
|
#### 自定义网关路由
|
|
142
144
|
|
|
143
145
|
内置探测覆盖不到的网关(比如 cookie 认证的中转), 可以自己写路由模块并在
|
package/dist/usage/core.mjs
CHANGED
|
@@ -919,33 +919,13 @@ async function debugLog(event) {
|
|
|
919
919
|
async function providerUsageDays() {
|
|
920
920
|
const config = await agentConfig();
|
|
921
921
|
const value = Number(process.env.PROVIDER_USAGE_DAYS || config.days || DEFAULT_USAGE_DAYS);
|
|
922
|
-
if (!Number.isInteger(value) || value <= 0
|
|
923
|
-
return value;
|
|
922
|
+
if (!Number.isInteger(value) || value <= 0) return DEFAULT_USAGE_DAYS;
|
|
923
|
+
return Math.min(value, MAX_USAGE_DAYS);
|
|
924
924
|
}
|
|
925
925
|
async function usagePreset() {
|
|
926
926
|
const config = await agentConfig();
|
|
927
927
|
return String(process.env.PROVIDER_USAGE_PRESET || config.preset || "auto").toLowerCase();
|
|
928
928
|
}
|
|
929
|
-
async function panelUserId() {
|
|
930
|
-
const config = await agentConfig();
|
|
931
|
-
const raw = process.env.PROVIDER_USAGE_USER_ID || config.userId || "";
|
|
932
|
-
const parsed = Number.parseInt(String(raw), 10);
|
|
933
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
934
|
-
}
|
|
935
|
-
async function panelUserHeaders() {
|
|
936
|
-
const userId = await panelUserId();
|
|
937
|
-
if (!userId) return {};
|
|
938
|
-
const value = String(userId);
|
|
939
|
-
return {
|
|
940
|
-
"New-API-User": value,
|
|
941
|
-
"Veloera-User": value,
|
|
942
|
-
"voapi-user": value,
|
|
943
|
-
"User-id": value,
|
|
944
|
-
"X-User-Id": value,
|
|
945
|
-
"Rix-Api-User": value,
|
|
946
|
-
"neo-api-user": value
|
|
947
|
-
};
|
|
948
|
-
}
|
|
949
929
|
function snapshotTtlMs() {
|
|
950
930
|
const raw = Number(process.env.AGENT_TOOLS_USAGE_SNAPSHOT_TTL_MS);
|
|
951
931
|
return Number.isFinite(raw) && raw >= 0 ? raw : 6e4;
|
|
@@ -1388,36 +1368,8 @@ function formatOpenRouterLine(label, data) {
|
|
|
1388
1368
|
if (parts.length === 1) throw new Error("OpenRouter payload has no usage fields");
|
|
1389
1369
|
return parts.join(" | ");
|
|
1390
1370
|
}
|
|
1391
|
-
function
|
|
1392
|
-
return
|
|
1393
|
-
}
|
|
1394
|
-
function panelQuotaLooksRemaining(kind) {
|
|
1395
|
-
return ["new-api", "anyrouter", "agentrouter", "done-hub", "donehub"].includes(kind);
|
|
1396
|
-
}
|
|
1397
|
-
async function formatPanelUserSelfLine(label, data, kind) {
|
|
1398
|
-
const root = usageRoot(data);
|
|
1399
|
-
const scale = panelQuotaScale(kind);
|
|
1400
|
-
const quota = pickNumber(root, ["quota"]);
|
|
1401
|
-
const used = pickNumber(root, ["used_quota", "usedQuota"]);
|
|
1402
|
-
const todayIncome = pickNumber(root, ["today_income", "todayIncome"]);
|
|
1403
|
-
const todayUsed = pickNumber(root, ["today_quota_consumption", "todayQuotaConsumption"]);
|
|
1404
|
-
if (quota === void 0 && used === void 0) {
|
|
1405
|
-
throw new Error("panel /api/user/self payload has no quota fields");
|
|
1406
|
-
}
|
|
1407
|
-
const quotaUsd = quota === void 0 ? void 0 : quota / scale;
|
|
1408
|
-
const usedUsd = used === void 0 ? void 0 : used / scale;
|
|
1409
|
-
const remainingUsd = panelQuotaLooksRemaining(kind) ? quotaUsd : quotaUsd === void 0 || usedUsd === void 0 ? quotaUsd : Math.max(0, quotaUsd - usedUsd);
|
|
1410
|
-
const totalUsd = panelQuotaLooksRemaining(kind) ? quotaUsd === void 0 || usedUsd === void 0 ? quotaUsd : quotaUsd + usedUsd : quotaUsd;
|
|
1411
|
-
const parts = usageParts();
|
|
1412
|
-
if (remainingUsd !== void 0) parts.push(`balance ${formatMoney(remainingUsd)}`);
|
|
1413
|
-
if (usedUsd !== void 0 && totalUsd !== void 0) {
|
|
1414
|
-
parts.push(`used ${formatMoney(usedUsd)}/${formatMoney(totalUsd)}`);
|
|
1415
|
-
} else if (usedUsd !== void 0) {
|
|
1416
|
-
parts.push(`used ${formatMoney(usedUsd)}`);
|
|
1417
|
-
}
|
|
1418
|
-
if (todayUsed !== void 0) parts.push(`today ${formatMoney(todayUsed / scale)}`);
|
|
1419
|
-
if (todayIncome !== void 0) parts.push(`income ${formatMoney(todayIncome / scale)}`);
|
|
1420
|
-
return parts.join(" | ");
|
|
1371
|
+
function formatOneApiBillingLine(limit, used) {
|
|
1372
|
+
return `API | balance ${formatMoney(Math.max(0, limit - used))} | used ${formatMoney(used)}/${formatMoney(limit)}`;
|
|
1421
1373
|
}
|
|
1422
1374
|
function formatQuotaLimitedLine(label, root) {
|
|
1423
1375
|
const quota = root?.quota || {};
|
|
@@ -1497,7 +1449,7 @@ async function fetchV1Usage(context) {
|
|
|
1497
1449
|
async function fetchNewApiTokenUsage(context) {
|
|
1498
1450
|
const json = await requestJson(joinUrl(serviceRoot(context.baseUrl), "/api/usage/token/"), {
|
|
1499
1451
|
key: context.key,
|
|
1500
|
-
name: "
|
|
1452
|
+
name: "New API token usage"
|
|
1501
1453
|
});
|
|
1502
1454
|
const root = usageRoot(json);
|
|
1503
1455
|
const quota = pickNumber(root, ["quota", "limit", "total_quota", "totalQuota"]);
|
|
@@ -1523,66 +1475,37 @@ async function fetchNewApiTokenUsage(context) {
|
|
|
1523
1475
|
};
|
|
1524
1476
|
return usageResult(context, "newapi-token", await formatNewApiTokenLine(context.label, json), normalized);
|
|
1525
1477
|
}
|
|
1526
|
-
async function
|
|
1527
|
-
const
|
|
1528
|
-
const
|
|
1529
|
-
const json = await requestJson(joinUrl(serviceRoot(context.baseUrl), "/api/user/self"), {
|
|
1478
|
+
async function fetchOneApiBillingUsage(context) {
|
|
1479
|
+
const base = serviceRoot(context.baseUrl);
|
|
1480
|
+
const subscription = await requestJson(joinUrl(base, "/v1/dashboard/billing/subscription"), {
|
|
1530
1481
|
key: context.key,
|
|
1531
|
-
name: "
|
|
1532
|
-
headers: await panelUserHeaders()
|
|
1482
|
+
name: "One API billing subscription"
|
|
1533
1483
|
});
|
|
1534
|
-
const
|
|
1535
|
-
|
|
1536
|
-
|
|
1537
|
-
|
|
1538
|
-
|
|
1539
|
-
|
|
1540
|
-
|
|
1541
|
-
|
|
1484
|
+
const usage = await requestJson(joinUrl(base, "/v1/dashboard/billing/usage"), {
|
|
1485
|
+
key: context.key,
|
|
1486
|
+
name: "One API billing usage"
|
|
1487
|
+
});
|
|
1488
|
+
const limit = pickNumber(subscription, ["hard_limit_usd", "hardLimitUsd"]);
|
|
1489
|
+
const usageCents = pickNumber(usage, ["total_usage", "totalUsage"]);
|
|
1490
|
+
if (limit === void 0 || usageCents === void 0) {
|
|
1491
|
+
throw new Error("One API billing payload has no quota fields");
|
|
1542
1492
|
}
|
|
1543
|
-
const
|
|
1544
|
-
const quota = pickNumber(root, ["quota"]);
|
|
1545
|
-
const used = pickNumber(root, ["used_quota", "usedQuota"]);
|
|
1546
|
-
const remaining = panelQuotaLooksRemaining(kind) ? quota : quota === void 0 || used === void 0 ? quota : Math.max(0, quota - used);
|
|
1547
|
-
const total = panelQuotaLooksRemaining(kind) ? quota === void 0 || used === void 0 ? quota : quota + used : quota;
|
|
1493
|
+
const used = usageCents / 100;
|
|
1548
1494
|
const normalized = {
|
|
1549
1495
|
mode: "quota_limited",
|
|
1550
1496
|
quota: {
|
|
1551
|
-
limit
|
|
1552
|
-
used
|
|
1553
|
-
remaining:
|
|
1497
|
+
limit,
|
|
1498
|
+
used,
|
|
1499
|
+
remaining: Math.max(0, limit - used)
|
|
1554
1500
|
},
|
|
1555
1501
|
unit: "USD",
|
|
1556
|
-
source: "
|
|
1557
|
-
raw:
|
|
1558
|
-
};
|
|
1559
|
-
return usageResult(
|
|
1560
|
-
context,
|
|
1561
|
-
"panel-user-self",
|
|
1562
|
-
await formatPanelUserSelfLine(context.label, json, kind),
|
|
1563
|
-
normalized
|
|
1564
|
-
);
|
|
1565
|
-
}
|
|
1566
|
-
async function fetchSub2ApiAuthMeUsage(context) {
|
|
1567
|
-
const json = await requestJson(joinUrl(serviceRoot(context.baseUrl), "/api/v1/auth/me"), {
|
|
1568
|
-
key: context.key,
|
|
1569
|
-
name: "Sub2API auth/me"
|
|
1570
|
-
});
|
|
1571
|
-
const root = usageRoot(json);
|
|
1572
|
-
const balance = pickNumber(root, ["balance"]);
|
|
1573
|
-
if (balance === void 0) throw new Error("Sub2API auth/me payload has no balance field");
|
|
1574
|
-
const normalized = {
|
|
1575
|
-
mode: "unrestricted",
|
|
1576
|
-
planName: root?.username || root?.email || context.label || "Sub2API",
|
|
1577
|
-
balance,
|
|
1578
|
-
unit: "USD",
|
|
1579
|
-
source: "sub2api-auth-me",
|
|
1580
|
-
raw: json
|
|
1502
|
+
source: "oneapi-billing",
|
|
1503
|
+
raw: { subscription, usage }
|
|
1581
1504
|
};
|
|
1582
1505
|
return usageResult(
|
|
1583
1506
|
context,
|
|
1584
|
-
"
|
|
1585
|
-
|
|
1507
|
+
"oneapi-billing",
|
|
1508
|
+
formatOneApiBillingLine(limit, used),
|
|
1586
1509
|
normalized
|
|
1587
1510
|
);
|
|
1588
1511
|
}
|
|
@@ -1610,20 +1533,15 @@ var USAGE_ROUTES = {
|
|
|
1610
1533
|
path: "/v1/usage",
|
|
1611
1534
|
run: fetchV1Usage
|
|
1612
1535
|
},
|
|
1613
|
-
"sub2api-auth-me": {
|
|
1614
|
-
id: "sub2api-auth-me",
|
|
1615
|
-
path: "/api/v1/auth/me",
|
|
1616
|
-
run: fetchSub2ApiAuthMeUsage
|
|
1617
|
-
},
|
|
1618
1536
|
"newapi-token": {
|
|
1619
1537
|
id: "newapi-token",
|
|
1620
1538
|
path: "/api/usage/token/",
|
|
1621
1539
|
run: fetchNewApiTokenUsage
|
|
1622
1540
|
},
|
|
1623
|
-
"
|
|
1624
|
-
id: "
|
|
1625
|
-
path: "/
|
|
1626
|
-
run:
|
|
1541
|
+
"oneapi-billing": {
|
|
1542
|
+
id: "oneapi-billing",
|
|
1543
|
+
path: "/v1/dashboard/billing/subscription",
|
|
1544
|
+
run: fetchOneApiBillingUsage
|
|
1627
1545
|
},
|
|
1628
1546
|
"openrouter": {
|
|
1629
1547
|
id: "openrouter",
|
|
@@ -1697,23 +1615,16 @@ async function routeRegistry() {
|
|
|
1697
1615
|
async function usageRouteIds(context) {
|
|
1698
1616
|
const preset = await usagePreset();
|
|
1699
1617
|
const routes = {
|
|
1700
|
-
"sub2api": ["v1-usage"
|
|
1618
|
+
"sub2api": ["v1-usage"],
|
|
1701
1619
|
"openai-compatible": ["v1-usage"],
|
|
1702
|
-
"new-api": ["newapi-token"
|
|
1703
|
-
"one-api": ["
|
|
1704
|
-
"onehub": ["newapi-token", "panel-user-self"],
|
|
1705
|
-
"one-hub": ["newapi-token", "panel-user-self"],
|
|
1706
|
-
"donehub": ["newapi-token", "panel-user-self"],
|
|
1707
|
-
"done-hub": ["newapi-token", "panel-user-self"],
|
|
1708
|
-
"veloera": ["panel-user-self", "newapi-token"],
|
|
1709
|
-
"anyrouter": ["newapi-token", "panel-user-self", "v1-usage"],
|
|
1710
|
-
"agentrouter": ["newapi-token", "panel-user-self", "v1-usage"],
|
|
1620
|
+
"new-api": ["newapi-token"],
|
|
1621
|
+
"one-api": ["oneapi-billing"],
|
|
1711
1622
|
"openrouter": ["openrouter"]
|
|
1712
1623
|
};
|
|
1713
1624
|
if (routes[preset]) return routes[preset];
|
|
1714
1625
|
if (preset !== "auto") return (await routeRegistry())[preset] ? [preset] : [];
|
|
1715
1626
|
const customIds = (await customRoutes()).map((route) => route.id);
|
|
1716
|
-
const builtinIds = hostIncludes(context.baseUrl, "openrouter.ai") ? ["openrouter"] : ["v1-usage", "
|
|
1627
|
+
const builtinIds = hostIncludes(context.baseUrl, "openrouter.ai") ? ["openrouter"] : ["v1-usage", "newapi-token", "oneapi-billing"];
|
|
1717
1628
|
return [.../* @__PURE__ */ new Set([...customIds, ...builtinIds])];
|
|
1718
1629
|
}
|
|
1719
1630
|
async function cachedUsageRoute(context, registry) {
|
|
@@ -61,8 +61,8 @@ export async function debugLog(event) {
|
|
|
61
61
|
export async function providerUsageDays() {
|
|
62
62
|
const config = await agentConfig();
|
|
63
63
|
const value = Number(process.env.PROVIDER_USAGE_DAYS || config.days || DEFAULT_USAGE_DAYS);
|
|
64
|
-
if (!Number.isInteger(value) || value <= 0
|
|
65
|
-
return value;
|
|
64
|
+
if (!Number.isInteger(value) || value <= 0) return DEFAULT_USAGE_DAYS;
|
|
65
|
+
return Math.min(value, MAX_USAGE_DAYS);
|
|
66
66
|
}
|
|
67
67
|
|
|
68
68
|
export async function usagePreset() {
|
|
@@ -70,28 +70,6 @@ export async function usagePreset() {
|
|
|
70
70
|
return String(process.env.PROVIDER_USAGE_PRESET || config.preset || "auto").toLowerCase();
|
|
71
71
|
}
|
|
72
72
|
|
|
73
|
-
export async function panelUserId() {
|
|
74
|
-
const config = await agentConfig();
|
|
75
|
-
const raw = process.env.PROVIDER_USAGE_USER_ID || config.userId || "";
|
|
76
|
-
const parsed = Number.parseInt(String(raw), 10);
|
|
77
|
-
return Number.isFinite(parsed) && parsed > 0 ? parsed : null;
|
|
78
|
-
}
|
|
79
|
-
|
|
80
|
-
export async function panelUserHeaders() {
|
|
81
|
-
const userId = await panelUserId();
|
|
82
|
-
if (!userId) return {};
|
|
83
|
-
const value = String(userId);
|
|
84
|
-
return {
|
|
85
|
-
"New-API-User": value,
|
|
86
|
-
"Veloera-User": value,
|
|
87
|
-
"voapi-user": value,
|
|
88
|
-
"User-id": value,
|
|
89
|
-
"X-User-Id": value,
|
|
90
|
-
"Rix-Api-User": value,
|
|
91
|
-
"neo-api-user": value,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
|
|
95
73
|
// Passive callers (the codex hook fires per prompt; several sessions may run
|
|
96
74
|
// at once) reuse a fresh snapshot instead of hitting the gateway every time.
|
|
97
75
|
// Same knob the statusline uses; 0 disables.
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
// Turns gateway payloads into the compact one-line usage message.
|
|
2
2
|
|
|
3
|
-
import { newApiQuotaScale, providerUsageDays
|
|
3
|
+
import { newApiQuotaScale, providerUsageDays } from "./config.mjs";
|
|
4
4
|
|
|
5
5
|
export function pickNumber(obj, keys) {
|
|
6
6
|
for (const key of keys) {
|
|
@@ -159,45 +159,8 @@ export function formatOpenRouterLine(label, data) {
|
|
|
159
159
|
return parts.join(" | ");
|
|
160
160
|
}
|
|
161
161
|
|
|
162
|
-
export function
|
|
163
|
-
return
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
export function panelQuotaLooksRemaining(kind) {
|
|
167
|
-
return ["new-api", "anyrouter", "agentrouter", "done-hub", "donehub"].includes(kind);
|
|
168
|
-
}
|
|
169
|
-
|
|
170
|
-
export async function formatPanelUserSelfLine(label, data, kind) {
|
|
171
|
-
const root = usageRoot(data);
|
|
172
|
-
const scale = panelQuotaScale(kind);
|
|
173
|
-
const quota = pickNumber(root, ["quota"]);
|
|
174
|
-
const used = pickNumber(root, ["used_quota", "usedQuota"]);
|
|
175
|
-
const todayIncome = pickNumber(root, ["today_income", "todayIncome"]);
|
|
176
|
-
const todayUsed = pickNumber(root, ["today_quota_consumption", "todayQuotaConsumption"]);
|
|
177
|
-
|
|
178
|
-
if (quota === undefined && used === undefined) {
|
|
179
|
-
throw new Error("panel /api/user/self payload has no quota fields");
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
const quotaUsd = quota === undefined ? undefined : quota / scale;
|
|
183
|
-
const usedUsd = used === undefined ? undefined : used / scale;
|
|
184
|
-
const remainingUsd = panelQuotaLooksRemaining(kind)
|
|
185
|
-
? quotaUsd
|
|
186
|
-
: (quotaUsd === undefined || usedUsd === undefined ? quotaUsd : Math.max(0, quotaUsd - usedUsd));
|
|
187
|
-
const totalUsd = panelQuotaLooksRemaining(kind)
|
|
188
|
-
? (quotaUsd === undefined || usedUsd === undefined ? quotaUsd : quotaUsd + usedUsd)
|
|
189
|
-
: quotaUsd;
|
|
190
|
-
|
|
191
|
-
const parts = usageParts();
|
|
192
|
-
if (remainingUsd !== undefined) parts.push(`balance ${formatMoney(remainingUsd)}`);
|
|
193
|
-
if (usedUsd !== undefined && totalUsd !== undefined) {
|
|
194
|
-
parts.push(`used ${formatMoney(usedUsd)}/${formatMoney(totalUsd)}`);
|
|
195
|
-
} else if (usedUsd !== undefined) {
|
|
196
|
-
parts.push(`used ${formatMoney(usedUsd)}`);
|
|
197
|
-
}
|
|
198
|
-
if (todayUsed !== undefined) parts.push(`today ${formatMoney(todayUsed / scale)}`);
|
|
199
|
-
if (todayIncome !== undefined) parts.push(`income ${formatMoney(todayIncome / scale)}`);
|
|
200
|
-
return parts.join(" | ");
|
|
162
|
+
export function formatOneApiBillingLine(limit, used) {
|
|
163
|
+
return `API | balance ${formatMoney(Math.max(0, limit - used))} | used ${formatMoney(used)}/${formatMoney(limit)}`;
|
|
201
164
|
}
|
|
202
165
|
|
|
203
166
|
function formatQuotaLimitedLine(label, root) {
|
|
@@ -8,7 +8,6 @@ import {
|
|
|
8
8
|
AGENT_TOOLS_HOME,
|
|
9
9
|
agentConfig,
|
|
10
10
|
usagePreset,
|
|
11
|
-
panelUserHeaders,
|
|
12
11
|
newApiQuotaScale,
|
|
13
12
|
providerUsageDays,
|
|
14
13
|
debugLog,
|
|
@@ -22,15 +21,12 @@ import {
|
|
|
22
21
|
} from "./urls.mjs";
|
|
23
22
|
import {
|
|
24
23
|
pickNumber,
|
|
25
|
-
formatMoney,
|
|
26
24
|
usageRoot,
|
|
27
25
|
hasV1UsageFields,
|
|
28
26
|
formatQuota,
|
|
29
27
|
formatNewApiTokenLine,
|
|
28
|
+
formatOneApiBillingLine,
|
|
30
29
|
formatOpenRouterLine,
|
|
31
|
-
formatPanelUserSelfLine,
|
|
32
|
-
panelQuotaScale,
|
|
33
|
-
panelQuotaLooksRemaining,
|
|
34
30
|
} from "./format.mjs";
|
|
35
31
|
import { readRouteCache } from "./cache.mjs";
|
|
36
32
|
|
|
@@ -63,13 +59,12 @@ async function fetchV1Usage(context) {
|
|
|
63
59
|
return usageResult(context, "v1-usage", await formatQuota(context.label, json), json);
|
|
64
60
|
}
|
|
65
61
|
|
|
66
|
-
//
|
|
67
|
-
//
|
|
68
|
-
// path.
|
|
62
|
+
// New API exposes a read-only usage endpoint authenticated by the same relay
|
|
63
|
+
// API key used for model requests.
|
|
69
64
|
async function fetchNewApiTokenUsage(context) {
|
|
70
65
|
const json = await requestJson(joinUrl(serviceRoot(context.baseUrl), "/api/usage/token/"), {
|
|
71
66
|
key: context.key,
|
|
72
|
-
name: "
|
|
67
|
+
name: "New API token usage",
|
|
73
68
|
});
|
|
74
69
|
const root = usageRoot(json);
|
|
75
70
|
const quota = pickNumber(root, ["quota", "limit", "total_quota", "totalQuota"]);
|
|
@@ -96,84 +91,46 @@ async function fetchNewApiTokenUsage(context) {
|
|
|
96
91
|
return usageResult(context, "newapi-token", await formatNewApiTokenLine(context.label, json), normalized);
|
|
97
92
|
}
|
|
98
93
|
|
|
99
|
-
//
|
|
100
|
-
//
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
const
|
|
104
|
-
const kind = preset === "auto" ? "new-api" : preset;
|
|
105
|
-
const json = await requestJson(joinUrl(serviceRoot(context.baseUrl), "/api/user/self"), {
|
|
94
|
+
// One API's legacy OpenAI billing endpoints use the same relay API key as
|
|
95
|
+
// model requests. Subscription reports the total quota; usage reports cents.
|
|
96
|
+
async function fetchOneApiBillingUsage(context) {
|
|
97
|
+
const base = serviceRoot(context.baseUrl);
|
|
98
|
+
const subscription = await requestJson(joinUrl(base, "/v1/dashboard/billing/subscription"), {
|
|
106
99
|
key: context.key,
|
|
107
|
-
name: "
|
|
108
|
-
headers: await panelUserHeaders(),
|
|
100
|
+
name: "One API billing subscription",
|
|
109
101
|
});
|
|
110
|
-
const
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
102
|
+
const usage = await requestJson(joinUrl(base, "/v1/dashboard/billing/usage"), {
|
|
103
|
+
key: context.key,
|
|
104
|
+
name: "One API billing usage",
|
|
105
|
+
});
|
|
106
|
+
const limit = pickNumber(subscription, ["hard_limit_usd", "hardLimitUsd"]);
|
|
107
|
+
const usageCents = pickNumber(usage, ["total_usage", "totalUsage"]);
|
|
108
|
+
if (limit === undefined || usageCents === undefined) {
|
|
109
|
+
throw new Error("One API billing payload has no quota fields");
|
|
118
110
|
}
|
|
119
|
-
const
|
|
120
|
-
const quota = pickNumber(root, ["quota"]);
|
|
121
|
-
const used = pickNumber(root, ["used_quota", "usedQuota"]);
|
|
122
|
-
const remaining = panelQuotaLooksRemaining(kind)
|
|
123
|
-
? quota
|
|
124
|
-
: (quota === undefined || used === undefined ? quota : Math.max(0, quota - used));
|
|
125
|
-
const total = panelQuotaLooksRemaining(kind)
|
|
126
|
-
? (quota === undefined || used === undefined ? quota : quota + used)
|
|
127
|
-
: quota;
|
|
111
|
+
const used = usageCents / 100;
|
|
128
112
|
const normalized = {
|
|
129
113
|
mode: "quota_limited",
|
|
130
114
|
quota: {
|
|
131
|
-
limit
|
|
132
|
-
used
|
|
133
|
-
remaining:
|
|
115
|
+
limit,
|
|
116
|
+
used,
|
|
117
|
+
remaining: Math.max(0, limit - used),
|
|
134
118
|
},
|
|
135
119
|
unit: "USD",
|
|
136
|
-
source: "
|
|
137
|
-
raw:
|
|
120
|
+
source: "oneapi-billing",
|
|
121
|
+
raw: { subscription, usage },
|
|
138
122
|
};
|
|
139
123
|
return usageResult(
|
|
140
124
|
context,
|
|
141
|
-
"
|
|
142
|
-
|
|
143
|
-
normalized
|
|
144
|
-
);
|
|
145
|
-
}
|
|
146
|
-
|
|
147
|
-
// Sub2API exposes user balance as USD at /api/v1/auth/me. Newer deployments may
|
|
148
|
-
// also expose richer subscription summaries through /v1/usage, so this route is
|
|
149
|
-
// a fallback for deployments where /v1/usage is unavailable.
|
|
150
|
-
async function fetchSub2ApiAuthMeUsage(context) {
|
|
151
|
-
const json = await requestJson(joinUrl(serviceRoot(context.baseUrl), "/api/v1/auth/me"), {
|
|
152
|
-
key: context.key,
|
|
153
|
-
name: "Sub2API auth/me",
|
|
154
|
-
});
|
|
155
|
-
const root = usageRoot(json);
|
|
156
|
-
const balance = pickNumber(root, ["balance"]);
|
|
157
|
-
if (balance === undefined) throw new Error("Sub2API auth/me payload has no balance field");
|
|
158
|
-
const normalized = {
|
|
159
|
-
mode: "unrestricted",
|
|
160
|
-
planName: root?.username || root?.email || context.label || "Sub2API",
|
|
161
|
-
balance,
|
|
162
|
-
unit: "USD",
|
|
163
|
-
source: "sub2api-auth-me",
|
|
164
|
-
raw: json,
|
|
165
|
-
};
|
|
166
|
-
return usageResult(
|
|
167
|
-
context,
|
|
168
|
-
"sub2api-auth-me",
|
|
169
|
-
`API | balance ${formatMoney(balance)}`,
|
|
125
|
+
"oneapi-billing",
|
|
126
|
+
formatOneApiBillingLine(limit, used),
|
|
170
127
|
normalized
|
|
171
128
|
);
|
|
172
129
|
}
|
|
173
130
|
|
|
174
131
|
// OpenRouter exposes normal API-key usage at /api/v1/key. Some accounts also
|
|
175
132
|
// expose credits at /api/v1/credits; keep this route isolated because
|
|
176
|
-
// OpenRouter's base URL already includes /api/v1, unlike
|
|
133
|
+
// OpenRouter's base URL already includes /api/v1, unlike New API.
|
|
177
134
|
async function fetchOpenRouterUsage(context) {
|
|
178
135
|
const base = cleanBaseUrl(context.baseUrl).includes("/api/v1")
|
|
179
136
|
? cleanBaseUrl(context.baseUrl)
|
|
@@ -201,20 +158,15 @@ const USAGE_ROUTES = {
|
|
|
201
158
|
path: "/v1/usage",
|
|
202
159
|
run: fetchV1Usage,
|
|
203
160
|
},
|
|
204
|
-
"sub2api-auth-me": {
|
|
205
|
-
id: "sub2api-auth-me",
|
|
206
|
-
path: "/api/v1/auth/me",
|
|
207
|
-
run: fetchSub2ApiAuthMeUsage,
|
|
208
|
-
},
|
|
209
161
|
"newapi-token": {
|
|
210
162
|
id: "newapi-token",
|
|
211
163
|
path: "/api/usage/token/",
|
|
212
164
|
run: fetchNewApiTokenUsage,
|
|
213
165
|
},
|
|
214
|
-
"
|
|
215
|
-
id: "
|
|
216
|
-
path: "/
|
|
217
|
-
run:
|
|
166
|
+
"oneapi-billing": {
|
|
167
|
+
id: "oneapi-billing",
|
|
168
|
+
path: "/v1/dashboard/billing/subscription",
|
|
169
|
+
run: fetchOneApiBillingUsage,
|
|
218
170
|
},
|
|
219
171
|
"openrouter": {
|
|
220
172
|
id: "openrouter",
|
|
@@ -224,7 +176,7 @@ const USAGE_ROUTES = {
|
|
|
224
176
|
};
|
|
225
177
|
|
|
226
178
|
// User-authored gateway routes, declared in config.jsonc:
|
|
227
|
-
// "providerUsage": { "routes": ["custom/
|
|
179
|
+
// "providerUsage": { "routes": ["custom/my-gateway.mjs"] }
|
|
228
180
|
// Paths resolve against ~/.agent-tools. Each module exports
|
|
229
181
|
// `export async function run(context, helpers)` plus an optional
|
|
230
182
|
// `export const meta = { id }` (id defaults to the file name). Broken modules
|
|
@@ -304,25 +256,14 @@ async function routeRegistry() {
|
|
|
304
256
|
return registry;
|
|
305
257
|
}
|
|
306
258
|
|
|
307
|
-
// Presets
|
|
308
|
-
// protocols (e.g. anyrouter/agentrouter just try the NewAPI panel endpoints
|
|
309
|
-
// and /v1/usage in a different order).
|
|
310
|
-
// They do not provide panel session-cookie authentication; only endpoints that
|
|
311
|
-
// accept the configured Bearer key can succeed.
|
|
259
|
+
// Presets select API-key usage protocols, not hosted gateway brands.
|
|
312
260
|
async function usageRouteIds(context) {
|
|
313
261
|
const preset = await usagePreset();
|
|
314
262
|
const routes = {
|
|
315
|
-
"sub2api": ["v1-usage"
|
|
263
|
+
"sub2api": ["v1-usage"],
|
|
316
264
|
"openai-compatible": ["v1-usage"],
|
|
317
|
-
"new-api": ["newapi-token"
|
|
318
|
-
"one-api": ["
|
|
319
|
-
"onehub": ["newapi-token", "panel-user-self"],
|
|
320
|
-
"one-hub": ["newapi-token", "panel-user-self"],
|
|
321
|
-
"donehub": ["newapi-token", "panel-user-self"],
|
|
322
|
-
"done-hub": ["newapi-token", "panel-user-self"],
|
|
323
|
-
"veloera": ["panel-user-self", "newapi-token"],
|
|
324
|
-
"anyrouter": ["newapi-token", "panel-user-self", "v1-usage"],
|
|
325
|
-
"agentrouter": ["newapi-token", "panel-user-self", "v1-usage"],
|
|
265
|
+
"new-api": ["newapi-token"],
|
|
266
|
+
"one-api": ["oneapi-billing"],
|
|
326
267
|
"openrouter": ["openrouter"],
|
|
327
268
|
};
|
|
328
269
|
if (routes[preset]) return routes[preset];
|
|
@@ -334,7 +275,7 @@ async function usageRouteIds(context) {
|
|
|
334
275
|
const customIds = (await customRoutes()).map((route) => route.id);
|
|
335
276
|
const builtinIds = hostIncludes(context.baseUrl, "openrouter.ai")
|
|
336
277
|
? ["openrouter"]
|
|
337
|
-
: ["v1-usage", "
|
|
278
|
+
: ["v1-usage", "newapi-token", "oneapi-billing"];
|
|
338
279
|
return [...new Set([...customIds, ...builtinIds])];
|
|
339
280
|
}
|
|
340
281
|
|
package/package.json
CHANGED