@kairyou/agent-tools 0.8.0 → 0.10.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 +57 -113
- package/README.zh-CN.md +55 -103
- package/dist/usage/cli.mjs +5 -3
- package/dist/usage/codex-hook.mjs +18 -10
- package/dist/usage/core.mjs +275 -114
- package/docs/en/custom-gateway-routes.md +53 -0
- package/docs/en/repository-structure.md +20 -0
- package/docs/zh-CN/custom-gateway-routes.md +47 -0
- package/docs/zh-CN/repository-structure.md +20 -0
- package/integrations/usage/codex-hook.mjs +19 -10
- package/integrations/usage/core.mjs +60 -12
- package/integrations/usage/lib/cache.mjs +152 -70
- package/integrations/usage/lib/config.mjs +11 -10
- package/integrations/usage/lib/format.mjs +79 -5
- package/integrations/usage/lib/http.mjs +5 -2
- package/integrations/usage/lib/routes.mjs +42 -15
- package/package.json +2 -1
- package/scripts/install.mjs +13 -4
- package/scripts/publish.mjs +6 -2
- package/scripts/release.mjs +8 -6
package/dist/usage/core.mjs
CHANGED
|
@@ -1,7 +1,8 @@
|
|
|
1
1
|
#!/usr/bin/env node
|
|
2
2
|
|
|
3
3
|
// integrations/usage/core.mjs
|
|
4
|
-
import { pathToFileURL as pathToFileURL2 } from "node:url";
|
|
4
|
+
import { fileURLToPath, pathToFileURL as pathToFileURL2 } from "node:url";
|
|
5
|
+
import { spawn } from "node:child_process";
|
|
5
6
|
|
|
6
7
|
// integrations/usage/lib/config.mjs
|
|
7
8
|
import { readFile, writeFile, mkdir, open, stat } from "node:fs/promises";
|
|
@@ -875,9 +876,10 @@ var AUTH_PATH = join(CODEX_HOME, "auth.json");
|
|
|
875
876
|
var CODEX_CONFIG_PATH = join(CODEX_HOME, "config.toml");
|
|
876
877
|
var AGENT_CONFIG_PATH = join(AGENT_TOOLS_HOME, "config.jsonc");
|
|
877
878
|
var DEBUG_PATH = join(AGENT_TOOLS_HOME, "logs", "usage-debug.log");
|
|
878
|
-
var
|
|
879
|
-
var
|
|
880
|
-
var
|
|
879
|
+
var CACHE_DIR = join(AGENT_TOOLS_HOME, "cache");
|
|
880
|
+
var ROUTE_CACHE_PATH = join(CACHE_DIR, "usage-routes.json");
|
|
881
|
+
var SNAPSHOT_PATH = join(CACHE_DIR, "usage-snapshot.json");
|
|
882
|
+
var REFRESH_STATE_PATH = join(CACHE_DIR, "usage-refresh-state.json");
|
|
881
883
|
var DEFAULT_USAGE_DAYS = 30;
|
|
882
884
|
var MAX_USAGE_DAYS = 90;
|
|
883
885
|
var DEFAULT_NEW_API_QUOTA_SCALE = 5e5;
|
|
@@ -944,10 +946,8 @@ async function usagePreset() {
|
|
|
944
946
|
const config = await agentConfig();
|
|
945
947
|
return String(process.env.PROVIDER_USAGE_PRESET || config.preset || "auto").toLowerCase();
|
|
946
948
|
}
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
return Number.isFinite(raw) && raw >= 0 ? raw : 6e4;
|
|
950
|
-
}
|
|
949
|
+
var HOOK_SNAPSHOT_MAX_AGE_MS = 10 * 6e4;
|
|
950
|
+
var REFRESH_INTERVAL_MS = 6e4;
|
|
951
951
|
async function newApiQuotaScale() {
|
|
952
952
|
const config = await agentConfig();
|
|
953
953
|
const scale = Number(config.newApiQuotaScale || DEFAULT_NEW_API_QUOTA_SCALE);
|
|
@@ -995,114 +995,161 @@ function hostIncludes(baseUrl, value) {
|
|
|
995
995
|
}
|
|
996
996
|
|
|
997
997
|
// integrations/usage/lib/cache.mjs
|
|
998
|
-
import { writeFile as writeFile2, mkdir as mkdir2 } from "node:fs/promises";
|
|
999
|
-
import {
|
|
1000
|
-
|
|
1001
|
-
var
|
|
1002
|
-
var
|
|
1003
|
-
async function
|
|
998
|
+
import { writeFile as writeFile2, mkdir as mkdir2, open as open2, unlink, rename, stat as stat2, utimes } from "node:fs/promises";
|
|
999
|
+
import { createHash, randomUUID } from "node:crypto";
|
|
1000
|
+
import { dirname as dirname2, join as join2 } from "node:path";
|
|
1001
|
+
var CACHE_VERSION = 1;
|
|
1002
|
+
var WRITE_LOCK_PATH = join2(CACHE_DIR, "usage-cache-write.lock");
|
|
1003
|
+
async function readJsonCache(path, field) {
|
|
1004
1004
|
try {
|
|
1005
|
-
const raw = await readTextIfExists(
|
|
1006
|
-
|
|
1007
|
-
const parsed = JSON.parse(raw);
|
|
1005
|
+
const raw = await readTextIfExists(path);
|
|
1006
|
+
const entries = raw.trim() ? JSON.parse(raw)?.[field] : null;
|
|
1008
1007
|
return {
|
|
1009
|
-
version:
|
|
1010
|
-
|
|
1008
|
+
version: CACHE_VERSION,
|
|
1009
|
+
[field]: entries && typeof entries === "object" ? entries : {}
|
|
1011
1010
|
};
|
|
1012
1011
|
} catch {
|
|
1013
|
-
return { version:
|
|
1012
|
+
return { version: CACHE_VERSION, [field]: {} };
|
|
1014
1013
|
}
|
|
1015
1014
|
}
|
|
1016
|
-
async function
|
|
1015
|
+
async function updateJsonCache(path, field, source, mutate) {
|
|
1016
|
+
const release = await acquireWriteLock();
|
|
1017
|
+
if (!release) {
|
|
1018
|
+
await debugLog({ source, skipped: "cache write lock unavailable" });
|
|
1019
|
+
return;
|
|
1020
|
+
}
|
|
1017
1021
|
try {
|
|
1018
|
-
const cache = await
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
source: result.source,
|
|
1024
|
-
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1025
|
-
};
|
|
1026
|
-
await mkdir2(dirname2(ROUTE_CACHE_PATH), { recursive: true });
|
|
1027
|
-
await writeFile2(ROUTE_CACHE_PATH, `${JSON.stringify(cache, null, 2)}
|
|
1022
|
+
const cache = await readJsonCache(path, field);
|
|
1023
|
+
mutate(cache[field]);
|
|
1024
|
+
await mkdir2(dirname2(path), { recursive: true });
|
|
1025
|
+
const temp = `${path}.${process.pid}.tmp`;
|
|
1026
|
+
await writeFile2(temp, `${JSON.stringify(cache, null, 2)}
|
|
1028
1027
|
`);
|
|
1028
|
+
await rename(temp, path);
|
|
1029
1029
|
} catch (error) {
|
|
1030
|
-
await debugLog({ source
|
|
1030
|
+
await debugLog({ source, error: error.message });
|
|
1031
|
+
} finally {
|
|
1032
|
+
await release();
|
|
1031
1033
|
}
|
|
1032
1034
|
}
|
|
1033
|
-
async function
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
|
|
1037
|
-
|
|
1038
|
-
|
|
1039
|
-
|
|
1040
|
-
|
|
1035
|
+
async function readRouteCache() {
|
|
1036
|
+
return await readJsonCache(ROUTE_CACHE_PATH, "routes");
|
|
1037
|
+
}
|
|
1038
|
+
async function rememberUsageRoute(context, route, result) {
|
|
1039
|
+
await updateJsonCache(ROUTE_CACHE_PATH, "routes", "route-cache", (routes) => {
|
|
1040
|
+
routes[usageRouteCacheKey(context.baseUrl)] = {
|
|
1041
|
+
route: route.id,
|
|
1042
|
+
path: route.path,
|
|
1043
|
+
source: result.source,
|
|
1044
|
+
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1041
1045
|
};
|
|
1042
|
-
}
|
|
1043
|
-
return { version: SNAPSHOT_VERSION, items: {} };
|
|
1044
|
-
}
|
|
1046
|
+
});
|
|
1045
1047
|
}
|
|
1046
1048
|
async function readUsageSnapshot(context) {
|
|
1047
|
-
const cache = await
|
|
1049
|
+
const cache = await readJsonCache(SNAPSHOT_PATH, "items");
|
|
1048
1050
|
return cache.items[usageRouteCacheKey(context.baseUrl)] || null;
|
|
1049
1051
|
}
|
|
1050
1052
|
async function rememberUsageSnapshot(context, result) {
|
|
1051
1053
|
if (!result?.text) return;
|
|
1052
|
-
|
|
1053
|
-
|
|
1054
|
-
const key = usageRouteCacheKey(context.baseUrl);
|
|
1055
|
-
cache.items[key] = {
|
|
1054
|
+
await updateJsonCache(SNAPSHOT_PATH, "items", "snapshot-cache", (items) => {
|
|
1055
|
+
items[usageRouteCacheKey(context.baseUrl)] = {
|
|
1056
1056
|
text: result.text,
|
|
1057
1057
|
source: result.source,
|
|
1058
1058
|
baseUrl: context.baseUrl,
|
|
1059
1059
|
updatedAt: (/* @__PURE__ */ new Date()).toISOString()
|
|
1060
1060
|
};
|
|
1061
|
-
|
|
1062
|
-
await writeFile2(SNAPSHOT_PATH, `${JSON.stringify(cache, null, 2)}
|
|
1063
|
-
`);
|
|
1064
|
-
} catch (error) {
|
|
1065
|
-
await debugLog({ source: "snapshot-cache", error: error.message });
|
|
1066
|
-
}
|
|
1067
|
-
}
|
|
1068
|
-
async function readRefreshState() {
|
|
1069
|
-
try {
|
|
1070
|
-
const raw = await readTextIfExists(REFRESH_STATE_PATH);
|
|
1071
|
-
if (!raw.trim()) return { version: REFRESH_STATE_VERSION, items: {} };
|
|
1072
|
-
const parsed = JSON.parse(raw);
|
|
1073
|
-
return {
|
|
1074
|
-
version: REFRESH_STATE_VERSION,
|
|
1075
|
-
items: parsed?.items && typeof parsed.items === "object" ? parsed.items : {}
|
|
1076
|
-
};
|
|
1077
|
-
} catch {
|
|
1078
|
-
return { version: REFRESH_STATE_VERSION, items: {} };
|
|
1079
|
-
}
|
|
1061
|
+
});
|
|
1080
1062
|
}
|
|
1081
1063
|
async function rememberRefreshState(context, patch) {
|
|
1082
|
-
|
|
1083
|
-
const state = await readRefreshState();
|
|
1064
|
+
await updateJsonCache(REFRESH_STATE_PATH, "items", "refresh-state", (items) => {
|
|
1084
1065
|
const key = usageRouteCacheKey(context.baseUrl);
|
|
1085
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1066
|
+
items[key] = { ...items[key] || {}, ...patch, baseUrl: context.baseUrl };
|
|
1067
|
+
});
|
|
1068
|
+
}
|
|
1069
|
+
async function canRefreshUsage(context, minIntervalMs) {
|
|
1070
|
+
if (minIntervalMs <= 0) return true;
|
|
1071
|
+
const state = await readJsonCache(REFRESH_STATE_PATH, "items");
|
|
1072
|
+
const item = state.items[usageRouteCacheKey(context.baseUrl)] || {};
|
|
1073
|
+
const latest = Math.max(
|
|
1074
|
+
...[item.lastStartedAt, item.lastSuccessAt, item.lastFailureAt].map((value) => Date.parse(value || "")).filter(Number.isFinite),
|
|
1075
|
+
0
|
|
1076
|
+
);
|
|
1077
|
+
return latest === 0 || Date.now() - latest >= minIntervalMs;
|
|
1078
|
+
}
|
|
1079
|
+
async function tryLock(lockPath, staleMs, details = {}) {
|
|
1080
|
+
await mkdir2(dirname2(lockPath), { recursive: true });
|
|
1081
|
+
const token = randomUUID();
|
|
1082
|
+
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1083
|
+
try {
|
|
1084
|
+
const handle = await open2(lockPath, "wx");
|
|
1085
|
+
try {
|
|
1086
|
+
await handle.writeFile(
|
|
1087
|
+
`${JSON.stringify({ token, pid: process.pid, ...details, startedAt: (/* @__PURE__ */ new Date()).toISOString() })}
|
|
1088
|
+
`
|
|
1089
|
+
);
|
|
1090
|
+
} finally {
|
|
1091
|
+
await handle.close();
|
|
1092
|
+
}
|
|
1093
|
+
const heartbeat = setInterval(() => {
|
|
1094
|
+
const now = /* @__PURE__ */ new Date();
|
|
1095
|
+
utimes(lockPath, now, now).catch(() => {
|
|
1096
|
+
});
|
|
1097
|
+
}, Math.max(50, Math.floor(staleMs / 3)));
|
|
1098
|
+
heartbeat.unref();
|
|
1099
|
+
return async () => {
|
|
1100
|
+
clearInterval(heartbeat);
|
|
1101
|
+
try {
|
|
1102
|
+
const held = JSON.parse(await readTextIfExists(lockPath));
|
|
1103
|
+
if (held?.token === token) await unlink(lockPath);
|
|
1104
|
+
} catch {
|
|
1105
|
+
}
|
|
1106
|
+
};
|
|
1107
|
+
} catch (error) {
|
|
1108
|
+
if (error?.code !== "EEXIST" || attempt > 0) return null;
|
|
1109
|
+
const age = await stat2(lockPath).then(
|
|
1110
|
+
({ mtimeMs }) => Date.now() - mtimeMs,
|
|
1111
|
+
() => Infinity
|
|
1112
|
+
// vanished under us: retry immediately
|
|
1113
|
+
);
|
|
1114
|
+
if (age <= staleMs) return null;
|
|
1115
|
+
try {
|
|
1116
|
+
const claimed = `${lockPath}.${token}`;
|
|
1117
|
+
await rename(lockPath, claimed);
|
|
1118
|
+
await unlink(claimed).catch(() => {
|
|
1119
|
+
});
|
|
1120
|
+
} catch {
|
|
1121
|
+
return null;
|
|
1122
|
+
}
|
|
1123
|
+
}
|
|
1095
1124
|
}
|
|
1125
|
+
return null;
|
|
1126
|
+
}
|
|
1127
|
+
function refreshLockPath(context) {
|
|
1128
|
+
const key = usageRouteCacheKey(context.baseUrl);
|
|
1129
|
+
const hash = createHash("sha256").update(key).digest("hex").slice(0, 16);
|
|
1130
|
+
return join2(CACHE_DIR, `usage-refresh-${hash}.lock`);
|
|
1131
|
+
}
|
|
1132
|
+
async function acquireUsageRefreshLease(context, leaseMs = 6e4) {
|
|
1133
|
+
return await tryLock(refreshLockPath(context), leaseMs, { baseUrl: context.baseUrl });
|
|
1134
|
+
}
|
|
1135
|
+
async function acquireWriteLock({ timeoutMs = 500, staleMs = 5e3 } = {}) {
|
|
1136
|
+
const deadline = Date.now() + timeoutMs;
|
|
1137
|
+
do {
|
|
1138
|
+
const release = await tryLock(WRITE_LOCK_PATH, staleMs);
|
|
1139
|
+
if (release) return release;
|
|
1140
|
+
await new Promise((resolve) => setTimeout(resolve, 10));
|
|
1141
|
+
} while (Date.now() < deadline);
|
|
1142
|
+
return null;
|
|
1096
1143
|
}
|
|
1097
1144
|
|
|
1098
1145
|
// integrations/usage/lib/routes.mjs
|
|
1099
1146
|
import { readdir } from "node:fs/promises";
|
|
1100
|
-
import { basename, extname, isAbsolute, join as
|
|
1147
|
+
import { basename, extname, isAbsolute, join as join3 } from "node:path";
|
|
1101
1148
|
import { pathToFileURL } from "node:url";
|
|
1102
1149
|
|
|
1103
1150
|
// integrations/usage/lib/http.mjs
|
|
1104
1151
|
import { createContext, runInContext } from "node:vm";
|
|
1105
|
-
var
|
|
1152
|
+
var DEFAULT_REQUEST_TIMEOUT_MS = 1e4;
|
|
1106
1153
|
var SHIELD_USER_AGENT = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/132.0.0.0 Safari/537.36";
|
|
1107
1154
|
function shortPreview(text) {
|
|
1108
1155
|
return String(text || "").replace(/\s+/g, " ").trim().slice(0, 220);
|
|
@@ -1203,7 +1250,7 @@ function mergeSetCookiePairs(cookieHeader, setCookieHeaders) {
|
|
|
1203
1250
|
return merged;
|
|
1204
1251
|
}
|
|
1205
1252
|
async function requestJson(url, options = {}) {
|
|
1206
|
-
const { key = "", headers = {}, name = "usage", timeoutMs =
|
|
1253
|
+
const { key = "", headers = {}, name = "usage", timeoutMs = DEFAULT_REQUEST_TIMEOUT_MS } = options;
|
|
1207
1254
|
let cookieHeader = "";
|
|
1208
1255
|
for (let attempt = 0; attempt < 2; attempt += 1) {
|
|
1209
1256
|
const controller = new AbortController();
|
|
@@ -1261,6 +1308,7 @@ async function requestJson(url, options = {}) {
|
|
|
1261
1308
|
}
|
|
1262
1309
|
|
|
1263
1310
|
// integrations/usage/lib/format.mjs
|
|
1311
|
+
var ONE_API_HARD_LIMIT_SENTINEL_USD = 1e6;
|
|
1264
1312
|
function pickNumber(obj, keys) {
|
|
1265
1313
|
for (const key of keys) {
|
|
1266
1314
|
const value = obj?.[key];
|
|
@@ -1344,21 +1392,39 @@ async function formatQuota(data) {
|
|
|
1344
1392
|
const keys = Object.keys(root || {}).slice(0, 4).join(", ");
|
|
1345
1393
|
return keys ? `received (${keys})` : `checked ${unit}`;
|
|
1346
1394
|
}
|
|
1347
|
-
|
|
1395
|
+
function newApiTokenQuota(data) {
|
|
1348
1396
|
const root = usageRoot(data);
|
|
1349
1397
|
const unlimited = root?.unlimited_quota === true || root?.unlimitedQuota === true;
|
|
1350
|
-
const quota = pickNumber(root, [
|
|
1351
|
-
|
|
1352
|
-
|
|
1398
|
+
const quota = pickNumber(root, [
|
|
1399
|
+
"quota",
|
|
1400
|
+
"limit",
|
|
1401
|
+
"total_quota",
|
|
1402
|
+
"totalQuota",
|
|
1403
|
+
"total_granted",
|
|
1404
|
+
"totalGranted"
|
|
1405
|
+
]);
|
|
1406
|
+
const used = pickNumber(root, ["used_quota", "usedQuota", "used", "total_used", "totalUsed"]);
|
|
1407
|
+
let remaining = pickNumber(root, [
|
|
1408
|
+
"remain_quota",
|
|
1409
|
+
"remainQuota",
|
|
1410
|
+
"remaining",
|
|
1411
|
+
"balance",
|
|
1412
|
+
"total_available",
|
|
1413
|
+
"totalAvailable"
|
|
1414
|
+
]);
|
|
1353
1415
|
if (remaining === void 0 && quota !== void 0 && used !== void 0) {
|
|
1354
1416
|
remaining = Math.max(0, quota - used);
|
|
1355
1417
|
}
|
|
1418
|
+
return { unlimited, quota, used, remaining };
|
|
1419
|
+
}
|
|
1420
|
+
async function formatNewApiTokenLine(data) {
|
|
1421
|
+
const { unlimited, quota, used, remaining } = newApiTokenQuota(data);
|
|
1356
1422
|
if (!unlimited && quota === void 0 && used === void 0 && remaining === void 0) {
|
|
1357
1423
|
throw new Error("NewAPI token usage payload has no quota fields");
|
|
1358
1424
|
}
|
|
1359
1425
|
const parts = [];
|
|
1360
1426
|
if (unlimited) parts.push("unlimited");
|
|
1361
|
-
if (remaining !== void 0) parts.push(`balance ${await formatNewApiQuota(remaining)}`);
|
|
1427
|
+
if (!unlimited && remaining !== void 0) parts.push(`balance ${await formatNewApiQuota(remaining)}`);
|
|
1362
1428
|
if (used !== void 0 && quota !== void 0) {
|
|
1363
1429
|
parts.push(`used ${await formatNewApiQuota(used)}/${await formatNewApiQuota(quota)}`);
|
|
1364
1430
|
} else if (used !== void 0) {
|
|
@@ -1384,8 +1450,51 @@ function formatOpenRouterLine(data) {
|
|
|
1384
1450
|
return parts.join(" | ");
|
|
1385
1451
|
}
|
|
1386
1452
|
function formatOneApiBillingLine(limit, used) {
|
|
1453
|
+
if (!hasSpendableOneApiLimit(limit)) return `used ${formatMoney(used)}`;
|
|
1387
1454
|
return `balance ${formatMoney(Math.max(0, limit - used))} | used ${formatMoney(used)}/${formatMoney(limit)}`;
|
|
1388
1455
|
}
|
|
1456
|
+
function hasSpendableOneApiLimit(limit) {
|
|
1457
|
+
return Number.isFinite(limit) && limit >= 0 && limit < ONE_API_HARD_LIMIT_SENTINEL_USD;
|
|
1458
|
+
}
|
|
1459
|
+
function effectiveClaudeCodeHubWindow(root, suffix) {
|
|
1460
|
+
const keyLimit = pickNumber(root, [`keyLimit${suffix}Usd`]);
|
|
1461
|
+
if (keyLimit !== void 0 && keyLimit > 0) {
|
|
1462
|
+
return { limit: keyLimit, used: pickNumber(root, [`keyCurrent${suffix}Usd`]) };
|
|
1463
|
+
}
|
|
1464
|
+
const userLimit = pickNumber(root, [`userLimit${suffix}Usd`]);
|
|
1465
|
+
return {
|
|
1466
|
+
limit: userLimit !== void 0 && userLimit > 0 ? userLimit : void 0,
|
|
1467
|
+
used: pickNumber(root, [`userCurrent${suffix}Usd`])
|
|
1468
|
+
};
|
|
1469
|
+
}
|
|
1470
|
+
function earliestDate(...values) {
|
|
1471
|
+
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;
|
|
1472
|
+
}
|
|
1473
|
+
function formatClaudeCodeHubLine(data) {
|
|
1474
|
+
const root = usageRoot(data);
|
|
1475
|
+
const windows = [
|
|
1476
|
+
["5h", "5h"],
|
|
1477
|
+
["D", "Daily"],
|
|
1478
|
+
["W", "Weekly"],
|
|
1479
|
+
["M", "Monthly"],
|
|
1480
|
+
["T", "Total"]
|
|
1481
|
+
];
|
|
1482
|
+
const parts = [];
|
|
1483
|
+
for (const [label, suffix] of windows) {
|
|
1484
|
+
const { limit, used } = effectiveClaudeCodeHubWindow(root, suffix);
|
|
1485
|
+
if (limit !== void 0 && used !== void 0) {
|
|
1486
|
+
parts.push(`${label} ${formatMoney(used)}/${formatMoney(limit)}`);
|
|
1487
|
+
}
|
|
1488
|
+
}
|
|
1489
|
+
if (parts.length === 0) {
|
|
1490
|
+
const totalUsed = pickNumber(root, ["keyCurrentTotalUsd", "userCurrentTotalUsd"]);
|
|
1491
|
+
if (totalUsed !== void 0) parts.push(`used ${formatMoney(totalUsed)}`);
|
|
1492
|
+
}
|
|
1493
|
+
const expires = shortDate(earliestDate(root?.expiresAt, root?.userExpiresAt));
|
|
1494
|
+
if (expires) parts.push(`Exp ${expires}`);
|
|
1495
|
+
if (parts.length === 0) throw new Error("Claude Code Hub quota payload has no quota fields");
|
|
1496
|
+
return parts.join(" | ");
|
|
1497
|
+
}
|
|
1389
1498
|
function formatQuotaLimitedLine(root) {
|
|
1390
1499
|
const quota = root?.quota || {};
|
|
1391
1500
|
const limit = pickNumber(quota, ["limit", "quota"]);
|
|
@@ -1466,13 +1575,7 @@ async function fetchNewApiTokenUsage(context) {
|
|
|
1466
1575
|
key: context.key,
|
|
1467
1576
|
name: "New API token usage"
|
|
1468
1577
|
});
|
|
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
|
-
}
|
|
1578
|
+
const { quota, used, remaining } = newApiTokenQuota(json);
|
|
1476
1579
|
const scale = await newApiQuotaScale();
|
|
1477
1580
|
const quotaForWarning = scale ? quota / scale : quota;
|
|
1478
1581
|
const usedForWarning = scale ? used / scale : used;
|
|
@@ -1506,13 +1609,16 @@ async function fetchOneApiBillingUsage(context) {
|
|
|
1506
1609
|
throw new Error("One API billing payload has no quota fields");
|
|
1507
1610
|
}
|
|
1508
1611
|
const used = usageCents / 100;
|
|
1612
|
+
const spendableLimit = hasSpendableOneApiLimit(limit);
|
|
1509
1613
|
const normalized = {
|
|
1510
|
-
mode: "quota_limited",
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
1614
|
+
mode: spendableLimit ? "quota_limited" : "unrestricted",
|
|
1615
|
+
...spendableLimit ? {
|
|
1616
|
+
quota: {
|
|
1617
|
+
limit,
|
|
1618
|
+
used,
|
|
1619
|
+
remaining: Math.max(0, limit - used)
|
|
1620
|
+
}
|
|
1621
|
+
} : { used },
|
|
1516
1622
|
unit: "USD",
|
|
1517
1623
|
source: "oneapi-billing",
|
|
1518
1624
|
raw: { subscription, usage }
|
|
@@ -1524,6 +1630,19 @@ async function fetchOneApiBillingUsage(context) {
|
|
|
1524
1630
|
normalized
|
|
1525
1631
|
);
|
|
1526
1632
|
}
|
|
1633
|
+
async function fetchClaudeCodeHubUsage(context) {
|
|
1634
|
+
const base = cleanBaseUrl(context.baseUrl).replace(/\/api\/v1$/i, "").replace(/\/v1$/i, "");
|
|
1635
|
+
const json = await requestJson(joinUrl(base, "/api/v1/me/quota"), {
|
|
1636
|
+
key: context.key,
|
|
1637
|
+
name: "Claude Code Hub quota"
|
|
1638
|
+
});
|
|
1639
|
+
return usageResult(
|
|
1640
|
+
context,
|
|
1641
|
+
"claude-code-hub",
|
|
1642
|
+
formatClaudeCodeHubLine(json),
|
|
1643
|
+
json
|
|
1644
|
+
);
|
|
1645
|
+
}
|
|
1527
1646
|
async function fetchOpenRouterUsage(context) {
|
|
1528
1647
|
const base = cleanBaseUrl(context.baseUrl).includes("/api/v1") ? cleanBaseUrl(context.baseUrl) : joinUrl(serviceRoot(context.baseUrl), "/api/v1");
|
|
1529
1648
|
const endpoints = [
|
|
@@ -1562,6 +1681,11 @@ var USAGE_ROUTES = {
|
|
|
1562
1681
|
id: "openrouter",
|
|
1563
1682
|
path: "/api/v1/key",
|
|
1564
1683
|
run: fetchOpenRouterUsage
|
|
1684
|
+
},
|
|
1685
|
+
"claude-code-hub": {
|
|
1686
|
+
id: "claude-code-hub",
|
|
1687
|
+
path: "/api/v1/me/quota",
|
|
1688
|
+
run: fetchClaudeCodeHubUsage
|
|
1565
1689
|
}
|
|
1566
1690
|
};
|
|
1567
1691
|
var CUSTOM_ROUTE_HELPERS = { requestJson, agentConfig };
|
|
@@ -1605,11 +1729,11 @@ async function loadCustomRoutes() {
|
|
|
1605
1729
|
const specs = Array.isArray(config.routes) ? config.routes : [];
|
|
1606
1730
|
for (const spec of specs) {
|
|
1607
1731
|
if (typeof spec !== "string" || !spec.trim()) continue;
|
|
1608
|
-
const file = isAbsolute(spec) ? spec :
|
|
1732
|
+
const file = isAbsolute(spec) ? spec : join3(AGENT_TOOLS_HOME, spec);
|
|
1609
1733
|
const route = await loadRouteModule(file, spec);
|
|
1610
1734
|
if (route) routes.push(route);
|
|
1611
1735
|
}
|
|
1612
|
-
const packagedDir =
|
|
1736
|
+
const packagedDir = join3(AGENT_TOOLS_HOME, "dist", "usage", "routes");
|
|
1613
1737
|
let packaged = [];
|
|
1614
1738
|
try {
|
|
1615
1739
|
packaged = (await readdir(packagedDir)).filter((n) => n.endsWith(".mjs")).sort();
|
|
@@ -1617,7 +1741,7 @@ async function loadCustomRoutes() {
|
|
|
1617
1741
|
packaged = [];
|
|
1618
1742
|
}
|
|
1619
1743
|
for (const name of packaged) {
|
|
1620
|
-
const route = await loadRouteModule(
|
|
1744
|
+
const route = await loadRouteModule(join3(packagedDir, name), `dist/usage/routes/${name}`);
|
|
1621
1745
|
if (route && !routes.some((existing) => existing.id === route.id)) routes.push(route);
|
|
1622
1746
|
}
|
|
1623
1747
|
return routes;
|
|
@@ -1634,12 +1758,15 @@ async function usageRouteIds(context) {
|
|
|
1634
1758
|
"openai-compatible": ["v1-usage"],
|
|
1635
1759
|
"new-api": ["newapi-token"],
|
|
1636
1760
|
"one-api": ["oneapi-billing"],
|
|
1637
|
-
"
|
|
1761
|
+
"one-hub": ["oneapi-billing"],
|
|
1762
|
+
"done-hub": ["oneapi-billing"],
|
|
1763
|
+
"openrouter": ["openrouter"],
|
|
1764
|
+
"claude-code-hub": ["claude-code-hub"]
|
|
1638
1765
|
};
|
|
1639
1766
|
if (routes[preset]) return routes[preset];
|
|
1640
1767
|
if (preset !== "auto") return (await routeRegistry())[preset] ? [preset] : [];
|
|
1641
1768
|
const customIds = (await customRoutes()).map((route) => route.id);
|
|
1642
|
-
const builtinIds = hostIncludes(context.baseUrl, "openrouter.ai") ? ["openrouter"] : ["v1-usage", "newapi-token", "oneapi-billing"];
|
|
1769
|
+
const builtinIds = hostIncludes(context.baseUrl, "openrouter.ai") ? ["openrouter"] : ["v1-usage", "newapi-token", "oneapi-billing", "claude-code-hub"];
|
|
1643
1770
|
return [.../* @__PURE__ */ new Set([...customIds, ...builtinIds])];
|
|
1644
1771
|
}
|
|
1645
1772
|
async function cachedUsageRoute(context, registry) {
|
|
@@ -1768,7 +1895,7 @@ function normalizeUsageContext(input) {
|
|
|
1768
1895
|
|
|
1769
1896
|
// integrations/usage/core.mjs
|
|
1770
1897
|
function parseArgs(argv) {
|
|
1771
|
-
const opts = { mode: "hook", agent: "codex" };
|
|
1898
|
+
const opts = { mode: "hook", agent: "codex", silent: false };
|
|
1772
1899
|
let modeSet = false;
|
|
1773
1900
|
for (let i = 0; i < argv.length; i += 1) {
|
|
1774
1901
|
const arg = argv[i];
|
|
@@ -1776,6 +1903,8 @@ function parseArgs(argv) {
|
|
|
1776
1903
|
opts.agent = argv[++i];
|
|
1777
1904
|
} else if (arg.startsWith("--agent=")) {
|
|
1778
1905
|
opts.agent = arg.slice("--agent=".length);
|
|
1906
|
+
} else if (arg === "--silent") {
|
|
1907
|
+
opts.silent = true;
|
|
1779
1908
|
} else if (!arg.startsWith("-") && !modeSet) {
|
|
1780
1909
|
opts.mode = arg;
|
|
1781
1910
|
modeSet = true;
|
|
@@ -1851,27 +1980,59 @@ async function queryProviderUsage(input, options = {}) {
|
|
|
1851
1980
|
return await queryUsageContext(normalizeUsageContext(input), options);
|
|
1852
1981
|
}
|
|
1853
1982
|
async function refresh(agent = "codex") {
|
|
1854
|
-
|
|
1983
|
+
const context = await usageContext(agent);
|
|
1984
|
+
const release = await acquireUsageRefreshLease(context);
|
|
1985
|
+
if (!release) return { skipped: true, text: "" };
|
|
1986
|
+
try {
|
|
1987
|
+
if (!await canRefreshUsage(context, REFRESH_INTERVAL_MS)) return { skipped: true, text: "" };
|
|
1988
|
+
await rememberRefreshState(context, { lastStartedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
1989
|
+
return await queryUsageContext(context, { agent, rememberSnapshot: true });
|
|
1990
|
+
} finally {
|
|
1991
|
+
await release();
|
|
1992
|
+
}
|
|
1993
|
+
}
|
|
1994
|
+
async function cachedUsage(context) {
|
|
1995
|
+
const cached = await readUsageSnapshot(context);
|
|
1996
|
+
const ageMs = cached?.updatedAt ? Date.now() - Date.parse(cached.updatedAt) : Infinity;
|
|
1997
|
+
return cached?.text && Number.isFinite(ageMs) ? { ...cached, ageMs } : null;
|
|
1855
1998
|
}
|
|
1856
1999
|
async function queryAgentProviderUsage(agent = "codex", { maxAgeMs = 0 } = {}) {
|
|
1857
2000
|
const context = await usageContext(agent);
|
|
1858
2001
|
if (maxAgeMs > 0) {
|
|
1859
|
-
const cached = await
|
|
1860
|
-
|
|
1861
|
-
if (cached?.text && age < maxAgeMs) return { ...cached, cached: true };
|
|
2002
|
+
const cached = await cachedUsage(context);
|
|
2003
|
+
if (cached && cached.ageMs < maxAgeMs) return { ...cached, cached: true };
|
|
1862
2004
|
}
|
|
1863
2005
|
return await queryUsageContext(context, { agent, rememberSnapshot: true });
|
|
1864
2006
|
}
|
|
2007
|
+
function scheduleRefresh(agent) {
|
|
2008
|
+
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), "refresh", "--agent", agent], {
|
|
2009
|
+
cwd: process.cwd(),
|
|
2010
|
+
env: process.env,
|
|
2011
|
+
detached: true,
|
|
2012
|
+
stdio: "ignore",
|
|
2013
|
+
windowsHide: true
|
|
2014
|
+
});
|
|
2015
|
+
child.on("error", () => {
|
|
2016
|
+
});
|
|
2017
|
+
child.unref();
|
|
2018
|
+
}
|
|
2019
|
+
async function hook(agent, { silent = false } = {}) {
|
|
2020
|
+
const context = await usageContext(agent);
|
|
2021
|
+
const cached = await cachedUsage(context);
|
|
2022
|
+
if ((!cached || cached.ageMs >= REFRESH_INTERVAL_MS) && await canRefreshUsage(context, REFRESH_INTERVAL_MS)) {
|
|
2023
|
+
scheduleRefresh(agent);
|
|
2024
|
+
}
|
|
2025
|
+
hookOut(silent ? "" : cached && cached.ageMs < HOOK_SNAPSHOT_MAX_AGE_MS ? cached.text : "");
|
|
2026
|
+
}
|
|
1865
2027
|
async function main() {
|
|
1866
2028
|
try {
|
|
1867
2029
|
if (mode === "refresh") {
|
|
1868
2030
|
await refresh(cli.agent);
|
|
1869
|
-
} else if (mode === "print"
|
|
1870
|
-
const result = await
|
|
2031
|
+
} else if (mode === "print") {
|
|
2032
|
+
const result = await queryAgentProviderUsage(cli.agent);
|
|
1871
2033
|
textOut(result?.text || "");
|
|
1872
2034
|
} else if (mode === "hook") {
|
|
1873
|
-
|
|
1874
|
-
hookOut(result?.text || "");
|
|
2035
|
+
await hook(cli.agent, { silent: cli.silent });
|
|
1875
2036
|
} else {
|
|
1876
2037
|
throw new Error(`unknown mode: ${mode}`);
|
|
1877
2038
|
}
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
# Custom gateway routes
|
|
2
|
+
|
|
3
|
+
Advanced guide for [provider usage](../../README.md#provider-usage): write your own
|
|
4
|
+
usage probe for relays the built-in presets cannot reach (e.g. cookie-authenticated
|
|
5
|
+
gateways) without modifying package code.
|
|
6
|
+
|
|
7
|
+
## Declare a route
|
|
8
|
+
|
|
9
|
+
Write a route module and list it in `providerUsage.routes` (paths resolve against
|
|
10
|
+
`~/.agent-tools`). Declared routes are probed first; setting `"preset"` to a route
|
|
11
|
+
id selects it directly.
|
|
12
|
+
|
|
13
|
+
```jsonc
|
|
14
|
+
{
|
|
15
|
+
"providerUsage": {
|
|
16
|
+
"routes": [
|
|
17
|
+
"custom/my-gateway.mjs",
|
|
18
|
+
"custom/another-gateway.mjs"
|
|
19
|
+
],
|
|
20
|
+
"myGateway": { "username": "me", "password": "..." }
|
|
21
|
+
}
|
|
22
|
+
}
|
|
23
|
+
```
|
|
24
|
+
|
|
25
|
+
## Route module API
|
|
26
|
+
|
|
27
|
+
```js
|
|
28
|
+
// ~/.agent-tools/custom/my-gateway.mjs
|
|
29
|
+
export const meta = { id: "my-gateway" }; // optional; id defaults to the file name
|
|
30
|
+
|
|
31
|
+
export async function run(context, { requestJson, agentConfig }) {
|
|
32
|
+
// context: { baseUrl, key, providerName, provider, label }
|
|
33
|
+
const { myGateway = {} } = await agentConfig(); // the providerUsage object; custom keys welcome
|
|
34
|
+
|
|
35
|
+
const login = await fetch(`${context.baseUrl}/api/user/login`, {
|
|
36
|
+
method: "POST",
|
|
37
|
+
headers: { "content-type": "application/json" },
|
|
38
|
+
body: JSON.stringify({ username: myGateway.username, password: myGateway.password }),
|
|
39
|
+
});
|
|
40
|
+
const session = await login.json();
|
|
41
|
+
|
|
42
|
+
// requestJson parses JSON and throws on non-2xx responses; pass custom
|
|
43
|
+
// authorization or cookie headers here when needed.
|
|
44
|
+
const me = await requestJson(`${context.baseUrl}/api/user/self`, {
|
|
45
|
+
headers: { authorization: `Bearer ${session?.data?.accessToken}` },
|
|
46
|
+
});
|
|
47
|
+
return { text: `balance ¥${me?.data?.balance}` };
|
|
48
|
+
}
|
|
49
|
+
```
|
|
50
|
+
|
|
51
|
+
`text` is a free-form string; return `{ text }` on success, throw to fall through
|
|
52
|
+
to the next route. Enable `providerUsage.debug` to log probe failures to
|
|
53
|
+
`~/.agent-tools/logs/usage-debug.log`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# Repository structure
|
|
2
|
+
|
|
3
|
+
```text
|
|
4
|
+
agent-tools/
|
|
5
|
+
├── .claude-plugin/ # Claude Code/plugin ecosystem manifest.
|
|
6
|
+
├── .codex-plugin/ # Codex plugin manifest.
|
|
7
|
+
├── integrations/ # Installable capabilities, one directory each.
|
|
8
|
+
│ ├── statusline/ # Agent status line: branch, model, usage.
|
|
9
|
+
│ ├── usage/ # Provider balance / quota display.
|
|
10
|
+
│ └── vision/ # Cross-model image understanding.
|
|
11
|
+
├── skills/ # Reusable Agent Skills.
|
|
12
|
+
│ ├── workflow/ # Workflow-oriented skills.
|
|
13
|
+
│ │ ├── at-commit/ # Conventional Commit message skill.
|
|
14
|
+
│ │ ├── at-review/ # Review changes for bugs and regressions.
|
|
15
|
+
│ │ └── at-simplify/ # Reduce complexity and duplication in changes.
|
|
16
|
+
│ └── integrations/ # Skills that integrate external systems.
|
|
17
|
+
│ └── at-zentao/ # ZenTao bug/task fixing workflow.
|
|
18
|
+
├── docs/ # Advanced guides and contributor reference.
|
|
19
|
+
└── scripts/ # Install, sync, validation, and maintenance scripts.
|
|
20
|
+
```
|