@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 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 the usage APIs of Sub2API, NewAPI/OneAPI-family panels (OneHub,
113
- DoneHub, Veloera, ...), and OpenRouter; compatibility can vary with a
114
- deployment's version and auth scheme.
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 | new-api | veloera | openrouter | ...
144
- "userId": "", // some NewAPI/Veloera panels require your panel user id
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, NewAPI/OneAPI 系面板 (OneHub, DoneHub, Veloera 等)
106
- 与 OpenRouter 的用量接口; 同类网关的部署版本和鉴权方式不同, 兼容性可能有差异.
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 | new-api | veloera | openrouter | ...
134
- "userId": "", // 部分 NewAPI/Veloera 面板需要填面板用户 id
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 认证的中转), 可以自己写路由模块并在
@@ -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 || value > MAX_USAGE_DAYS) return DEFAULT_USAGE_DAYS;
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 panelQuotaScale(kind) {
1392
- return kind === "veloera" ? 1e6 : DEFAULT_NEW_API_QUOTA_SCALE;
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: "NewAPI token usage"
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 fetchPanelUserSelfUsage(context) {
1527
- const preset = await usagePreset();
1528
- const kind = preset === "auto" ? "new-api" : preset;
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: "panel /api/user/self",
1532
- headers: await panelUserHeaders()
1482
+ name: "One API billing subscription"
1533
1483
  });
1534
- const root = usageRoot(json);
1535
- if (pickNumber(root, ["quota"]) === void 0 && pickNumber(root, ["used_quota", "usedQuota"]) === void 0) {
1536
- await debugLog({
1537
- source: "panel /api/user/self",
1538
- payloadKeys: Object.keys(root || {}).slice(0, 20),
1539
- success: root?.success,
1540
- message: root?.message || root?.error?.message || ""
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 scale = panelQuotaScale(kind);
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: total === void 0 ? void 0 : total / scale,
1552
- used: used === void 0 ? void 0 : used / scale,
1553
- remaining: remaining === void 0 ? void 0 : remaining / scale
1497
+ limit,
1498
+ used,
1499
+ remaining: Math.max(0, limit - used)
1554
1500
  },
1555
1501
  unit: "USD",
1556
- source: "panel-user-self",
1557
- raw: json
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
- "sub2api-auth-me",
1585
- `API | balance ${formatMoney(balance)}`,
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
- "panel-user-self": {
1624
- id: "panel-user-self",
1625
- path: "/api/user/self",
1626
- run: fetchPanelUserSelfUsage
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", "sub2api-auth-me"],
1618
+ "sub2api": ["v1-usage"],
1701
1619
  "openai-compatible": ["v1-usage"],
1702
- "new-api": ["newapi-token", "panel-user-self"],
1703
- "one-api": ["newapi-token", "panel-user-self"],
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", "sub2api-auth-me", "newapi-token", "panel-user-self"];
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 || value > MAX_USAGE_DAYS) return DEFAULT_USAGE_DAYS;
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, DEFAULT_NEW_API_QUOTA_SCALE } from "./config.mjs";
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 panelQuotaScale(kind) {
163
- return kind === "veloera" ? 1000000 : DEFAULT_NEW_API_QUOTA_SCALE;
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
- // NewAPI / OneAPI family panels: use the current API key as Bearer auth and
67
- // query token usage from the service root rather than the /v1 OpenAI-compatible
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: "NewAPI token usage",
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
- // NewAPI / OneAPI / OneHub / DoneHub / Veloera panel session endpoint, based on
100
- // Metapi's platform handling. This works when PROVIDER_USAGE_API_KEY is a panel
101
- // access/session token, or when the site accepts the API key for /api/user/self.
102
- async function fetchPanelUserSelfUsage(context) {
103
- const preset = await usagePreset();
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: "panel /api/user/self",
108
- headers: await panelUserHeaders(),
100
+ name: "One API billing subscription",
109
101
  });
110
- const root = usageRoot(json);
111
- if (pickNumber(root, ["quota"]) === undefined && pickNumber(root, ["used_quota", "usedQuota"]) === undefined) {
112
- await debugLog({
113
- source: "panel /api/user/self",
114
- payloadKeys: Object.keys(root || {}).slice(0, 20),
115
- success: root?.success,
116
- message: root?.message || root?.error?.message || "",
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 scale = panelQuotaScale(kind);
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: total === undefined ? undefined : total / scale,
132
- used: used === undefined ? undefined : used / scale,
133
- remaining: remaining === undefined ? undefined : remaining / scale,
115
+ limit,
116
+ used,
117
+ remaining: Math.max(0, limit - used),
134
118
  },
135
119
  unit: "USD",
136
- source: "panel-user-self",
137
- raw: json,
120
+ source: "oneapi-billing",
121
+ raw: { subscription, usage },
138
122
  };
139
123
  return usageResult(
140
124
  context,
141
- "panel-user-self",
142
- await formatPanelUserSelfLine(context.label, json, kind),
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 NewAPI/OneAPI.
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
- "panel-user-self": {
215
- id: "panel-user-self",
216
- path: "/api/user/self",
217
- run: fetchPanelUserSelfUsage,
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/anyrouter.mjs"] }
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 are probe-order aliases over the routes above, not separate
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", "sub2api-auth-me"],
263
+ "sub2api": ["v1-usage"],
316
264
  "openai-compatible": ["v1-usage"],
317
- "new-api": ["newapi-token", "panel-user-self"],
318
- "one-api": ["newapi-token", "panel-user-self"],
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", "sub2api-auth-me", "newapi-token", "panel-user-self"];
278
+ : ["v1-usage", "newapi-token", "oneapi-billing"];
338
279
  return [...new Set([...customIds, ...builtinIds])];
339
280
  }
340
281
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@kairyou/agent-tools",
3
- "version": "0.6.0",
3
+ "version": "0.7.0",
4
4
  "description": "Reusable Agent Skills and installable integrations (statusline, provider usage, vision) for Codex, Claude Code, and opencode.",
5
5
  "license": "MIT",
6
6
  "engines": {