@kairyou/agent-tools 0.9.0 → 0.10.1
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 +40 -110
- package/README.zh-CN.md +39 -100
- package/dist/usage/cli.mjs +5 -3
- package/dist/usage/codex-hook.mjs +18 -10
- package/dist/usage/core.mjs +224 -98
- 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 +56 -4
- package/integrations/usage/lib/http.mjs +5 -2
- 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();
|
|
@@ -1262,6 +1309,14 @@ async function requestJson(url, options = {}) {
|
|
|
1262
1309
|
|
|
1263
1310
|
// integrations/usage/lib/format.mjs
|
|
1264
1311
|
var ONE_API_HARD_LIMIT_SENTINEL_USD = 1e6;
|
|
1312
|
+
var RESET_TIME_FIELDS = Object.freeze({
|
|
1313
|
+
// Sub2API rate-limit entries returned alongside quota usage.
|
|
1314
|
+
SUB2API_RATE_LIMIT: "reset_at",
|
|
1315
|
+
// Sub2API /v1/usage response: subscription weekly-window anchor.
|
|
1316
|
+
SUB2API_SUBSCRIPTION_WEEKLY_START: "weekly_window_start",
|
|
1317
|
+
// OpenRouter /api/v1/key response: absolute key-limit reset.
|
|
1318
|
+
OPENROUTER_KEY_LIMIT: "limit_reset"
|
|
1319
|
+
});
|
|
1265
1320
|
function pickNumber(obj, keys) {
|
|
1266
1321
|
for (const key of keys) {
|
|
1267
1322
|
const value = obj?.[key];
|
|
@@ -1290,6 +1345,39 @@ function shortDate(value) {
|
|
|
1290
1345
|
const match = String(value).match(/^(\d{4})-(\d{2})-(\d{2})/);
|
|
1291
1346
|
return match ? `${match[2]}-${match[3]}` : "";
|
|
1292
1347
|
}
|
|
1348
|
+
function timestampMs(value) {
|
|
1349
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
1350
|
+
return value < 1e12 ? value * 1e3 : value;
|
|
1351
|
+
}
|
|
1352
|
+
const parsed = Date.parse(value);
|
|
1353
|
+
return Number.isFinite(parsed) ? parsed : void 0;
|
|
1354
|
+
}
|
|
1355
|
+
function compactDurationUntil(value) {
|
|
1356
|
+
const resetMs = timestampMs(value);
|
|
1357
|
+
if (resetMs === void 0) return "";
|
|
1358
|
+
const remainingMs = resetMs - Date.now();
|
|
1359
|
+
if (remainingMs <= 0) return "";
|
|
1360
|
+
let seconds = Math.floor(remainingMs / 1e3);
|
|
1361
|
+
if (seconds <= 0) return "0m";
|
|
1362
|
+
const days = Math.floor(seconds / 86400);
|
|
1363
|
+
seconds %= 86400;
|
|
1364
|
+
const hours = Math.floor(seconds / 3600);
|
|
1365
|
+
seconds %= 3600;
|
|
1366
|
+
const minutes = Math.floor(seconds / 60);
|
|
1367
|
+
if (days) return `${days}d${hours}h`;
|
|
1368
|
+
if (hours) return `${hours}h${minutes}m`;
|
|
1369
|
+
return `${minutes}m`;
|
|
1370
|
+
}
|
|
1371
|
+
function rateLimitResetAt(entry) {
|
|
1372
|
+
const value = entry?.[RESET_TIME_FIELDS.SUB2API_RATE_LIMIT];
|
|
1373
|
+
return timestampMs(value) === void 0 ? void 0 : value;
|
|
1374
|
+
}
|
|
1375
|
+
function weeklyResetAt(subscription) {
|
|
1376
|
+
const startMs = timestampMs(
|
|
1377
|
+
subscription?.[RESET_TIME_FIELDS.SUB2API_SUBSCRIPTION_WEEKLY_START]
|
|
1378
|
+
);
|
|
1379
|
+
return startMs === void 0 ? void 0 : startMs + 7 * 86400 * 1e3;
|
|
1380
|
+
}
|
|
1293
1381
|
function hasSubscriptionLimits(root) {
|
|
1294
1382
|
const sub = root?.subscription || {};
|
|
1295
1383
|
return [
|
|
@@ -1390,7 +1478,7 @@ function formatOpenRouterLine(data) {
|
|
|
1390
1478
|
const limit = pickNumber(root, ["limit", "limit_remaining", "total_credits"]);
|
|
1391
1479
|
const remaining = pickNumber(root, ["limit_remaining", "remaining_credits"]);
|
|
1392
1480
|
const used = pickNumber(root, ["usage", "total_usage", "spend"]);
|
|
1393
|
-
const reset = root?.
|
|
1481
|
+
const reset = compactDurationUntil(root?.[RESET_TIME_FIELDS.OPENROUTER_KEY_LIMIT]);
|
|
1394
1482
|
const parts = [];
|
|
1395
1483
|
if (remaining !== void 0) parts.push(`balance ${formatMoney(remaining)}`);
|
|
1396
1484
|
if (used !== void 0 && limit !== void 0 && limit !== remaining) {
|
|
@@ -1398,7 +1486,7 @@ function formatOpenRouterLine(data) {
|
|
|
1398
1486
|
} else if (used !== void 0) {
|
|
1399
1487
|
parts.push(`used ${formatMoney(used)}`);
|
|
1400
1488
|
}
|
|
1401
|
-
if (reset) parts.push(
|
|
1489
|
+
if (reset) parts.push(`\u27F3${reset}`);
|
|
1402
1490
|
if (parts.length === 0) throw new Error("OpenRouter payload has no usage fields");
|
|
1403
1491
|
return parts.join(" | ");
|
|
1404
1492
|
}
|
|
@@ -1464,7 +1552,8 @@ function formatQuotaLimitedLine(root) {
|
|
|
1464
1552
|
const window = entry?.window;
|
|
1465
1553
|
const rateLimit = pickNumber(entry, ["limit"]);
|
|
1466
1554
|
const rateUsed = pickNumber(entry, ["used"]);
|
|
1467
|
-
|
|
1555
|
+
const reset = compactDurationUntil(rateLimitResetAt(entry));
|
|
1556
|
+
return window && rateLimit !== void 0 && rateUsed !== void 0 ? `${window} ${formatMoney(rateUsed)}/${formatMoney(rateLimit)}${reset ? ` \u27F3${reset}` : ""}` : "";
|
|
1468
1557
|
}).filter(Boolean);
|
|
1469
1558
|
if (rateParts.length > 0) parts.push(rateParts.join(", "));
|
|
1470
1559
|
}
|
|
@@ -1491,9 +1580,12 @@ function formatUsageLine(root) {
|
|
|
1491
1580
|
const monthlyLimit = pickNumber(sub, ["monthly_limit_usd"]);
|
|
1492
1581
|
const monthlyUsage = pickNumber(sub, ["monthly_usage_usd"]);
|
|
1493
1582
|
const expires = shortDate(sub.expires_at);
|
|
1583
|
+
const weeklyReset = compactDurationUntil(weeklyResetAt(sub));
|
|
1494
1584
|
const parts = [];
|
|
1495
1585
|
if (dailyLimit > 0 && dailyUsage !== void 0) parts.push(`D ${formatMoney(dailyUsage)}/${formatMoney(dailyLimit)}`);
|
|
1496
|
-
if (weeklyLimit > 0 && weeklyUsage !== void 0)
|
|
1586
|
+
if (weeklyLimit > 0 && weeklyUsage !== void 0) {
|
|
1587
|
+
parts.push(`W ${formatMoney(weeklyUsage)}/${formatMoney(weeklyLimit)}${weeklyReset ? ` \u27F3${weeklyReset}` : ""}`);
|
|
1588
|
+
}
|
|
1497
1589
|
if (monthlyLimit > 0 && monthlyUsage !== void 0) parts.push(`M ${formatMoney(monthlyUsage)}/${formatMoney(monthlyLimit)}`);
|
|
1498
1590
|
if (expires) parts.push(`Exp ${expires}`);
|
|
1499
1591
|
return parts.join(" | ");
|
|
@@ -1682,11 +1774,11 @@ async function loadCustomRoutes() {
|
|
|
1682
1774
|
const specs = Array.isArray(config.routes) ? config.routes : [];
|
|
1683
1775
|
for (const spec of specs) {
|
|
1684
1776
|
if (typeof spec !== "string" || !spec.trim()) continue;
|
|
1685
|
-
const file = isAbsolute(spec) ? spec :
|
|
1777
|
+
const file = isAbsolute(spec) ? spec : join3(AGENT_TOOLS_HOME, spec);
|
|
1686
1778
|
const route = await loadRouteModule(file, spec);
|
|
1687
1779
|
if (route) routes.push(route);
|
|
1688
1780
|
}
|
|
1689
|
-
const packagedDir =
|
|
1781
|
+
const packagedDir = join3(AGENT_TOOLS_HOME, "dist", "usage", "routes");
|
|
1690
1782
|
let packaged = [];
|
|
1691
1783
|
try {
|
|
1692
1784
|
packaged = (await readdir(packagedDir)).filter((n) => n.endsWith(".mjs")).sort();
|
|
@@ -1694,7 +1786,7 @@ async function loadCustomRoutes() {
|
|
|
1694
1786
|
packaged = [];
|
|
1695
1787
|
}
|
|
1696
1788
|
for (const name of packaged) {
|
|
1697
|
-
const route = await loadRouteModule(
|
|
1789
|
+
const route = await loadRouteModule(join3(packagedDir, name), `dist/usage/routes/${name}`);
|
|
1698
1790
|
if (route && !routes.some((existing) => existing.id === route.id)) routes.push(route);
|
|
1699
1791
|
}
|
|
1700
1792
|
return routes;
|
|
@@ -1848,7 +1940,7 @@ function normalizeUsageContext(input) {
|
|
|
1848
1940
|
|
|
1849
1941
|
// integrations/usage/core.mjs
|
|
1850
1942
|
function parseArgs(argv) {
|
|
1851
|
-
const opts = { mode: "hook", agent: "codex" };
|
|
1943
|
+
const opts = { mode: "hook", agent: "codex", silent: false };
|
|
1852
1944
|
let modeSet = false;
|
|
1853
1945
|
for (let i = 0; i < argv.length; i += 1) {
|
|
1854
1946
|
const arg = argv[i];
|
|
@@ -1856,6 +1948,8 @@ function parseArgs(argv) {
|
|
|
1856
1948
|
opts.agent = argv[++i];
|
|
1857
1949
|
} else if (arg.startsWith("--agent=")) {
|
|
1858
1950
|
opts.agent = arg.slice("--agent=".length);
|
|
1951
|
+
} else if (arg === "--silent") {
|
|
1952
|
+
opts.silent = true;
|
|
1859
1953
|
} else if (!arg.startsWith("-") && !modeSet) {
|
|
1860
1954
|
opts.mode = arg;
|
|
1861
1955
|
modeSet = true;
|
|
@@ -1931,27 +2025,59 @@ async function queryProviderUsage(input, options = {}) {
|
|
|
1931
2025
|
return await queryUsageContext(normalizeUsageContext(input), options);
|
|
1932
2026
|
}
|
|
1933
2027
|
async function refresh(agent = "codex") {
|
|
1934
|
-
|
|
2028
|
+
const context = await usageContext(agent);
|
|
2029
|
+
const release = await acquireUsageRefreshLease(context);
|
|
2030
|
+
if (!release) return { skipped: true, text: "" };
|
|
2031
|
+
try {
|
|
2032
|
+
if (!await canRefreshUsage(context, REFRESH_INTERVAL_MS)) return { skipped: true, text: "" };
|
|
2033
|
+
await rememberRefreshState(context, { lastStartedAt: (/* @__PURE__ */ new Date()).toISOString() });
|
|
2034
|
+
return await queryUsageContext(context, { agent, rememberSnapshot: true });
|
|
2035
|
+
} finally {
|
|
2036
|
+
await release();
|
|
2037
|
+
}
|
|
2038
|
+
}
|
|
2039
|
+
async function cachedUsage(context) {
|
|
2040
|
+
const cached = await readUsageSnapshot(context);
|
|
2041
|
+
const ageMs = cached?.updatedAt ? Date.now() - Date.parse(cached.updatedAt) : Infinity;
|
|
2042
|
+
return cached?.text && Number.isFinite(ageMs) ? { ...cached, ageMs } : null;
|
|
1935
2043
|
}
|
|
1936
2044
|
async function queryAgentProviderUsage(agent = "codex", { maxAgeMs = 0 } = {}) {
|
|
1937
2045
|
const context = await usageContext(agent);
|
|
1938
2046
|
if (maxAgeMs > 0) {
|
|
1939
|
-
const cached = await
|
|
1940
|
-
|
|
1941
|
-
if (cached?.text && age < maxAgeMs) return { ...cached, cached: true };
|
|
2047
|
+
const cached = await cachedUsage(context);
|
|
2048
|
+
if (cached && cached.ageMs < maxAgeMs) return { ...cached, cached: true };
|
|
1942
2049
|
}
|
|
1943
2050
|
return await queryUsageContext(context, { agent, rememberSnapshot: true });
|
|
1944
2051
|
}
|
|
2052
|
+
function scheduleRefresh(agent) {
|
|
2053
|
+
const child = spawn(process.execPath, [fileURLToPath(import.meta.url), "refresh", "--agent", agent], {
|
|
2054
|
+
cwd: process.cwd(),
|
|
2055
|
+
env: process.env,
|
|
2056
|
+
detached: true,
|
|
2057
|
+
stdio: "ignore",
|
|
2058
|
+
windowsHide: true
|
|
2059
|
+
});
|
|
2060
|
+
child.on("error", () => {
|
|
2061
|
+
});
|
|
2062
|
+
child.unref();
|
|
2063
|
+
}
|
|
2064
|
+
async function hook(agent, { silent = false } = {}) {
|
|
2065
|
+
const context = await usageContext(agent);
|
|
2066
|
+
const cached = await cachedUsage(context);
|
|
2067
|
+
if ((!cached || cached.ageMs >= REFRESH_INTERVAL_MS) && await canRefreshUsage(context, REFRESH_INTERVAL_MS)) {
|
|
2068
|
+
scheduleRefresh(agent);
|
|
2069
|
+
}
|
|
2070
|
+
hookOut(silent ? "" : cached && cached.ageMs < HOOK_SNAPSHOT_MAX_AGE_MS ? cached.text : "");
|
|
2071
|
+
}
|
|
1945
2072
|
async function main() {
|
|
1946
2073
|
try {
|
|
1947
2074
|
if (mode === "refresh") {
|
|
1948
2075
|
await refresh(cli.agent);
|
|
1949
|
-
} else if (mode === "print"
|
|
1950
|
-
const result = await
|
|
2076
|
+
} else if (mode === "print") {
|
|
2077
|
+
const result = await queryAgentProviderUsage(cli.agent);
|
|
1951
2078
|
textOut(result?.text || "");
|
|
1952
2079
|
} else if (mode === "hook") {
|
|
1953
|
-
|
|
1954
|
-
hookOut(result?.text || "");
|
|
2080
|
+
await hook(cli.agent, { silent: cli.silent });
|
|
1955
2081
|
} else {
|
|
1956
2082
|
throw new Error(`unknown mode: ${mode}`);
|
|
1957
2083
|
}
|
|
@@ -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
|
+
```
|
|
@@ -0,0 +1,47 @@
|
|
|
1
|
+
# 自定义网关路由
|
|
2
|
+
|
|
3
|
+
[Provider usage](../../README.zh-CN.md#provider-usage) 的高级指南: 为内置 preset 覆盖不到的中转(比如 cookie 认证的网关)编写自己的用量探测, 无需修改包内代码.
|
|
4
|
+
|
|
5
|
+
## 声明路由
|
|
6
|
+
|
|
7
|
+
编写路由模块, 并在 `providerUsage.routes` 里声明(相对 `~/.agent-tools` 解析). 声明的路由优先探测; `"preset"` 填路由 id 可直接选中.
|
|
8
|
+
|
|
9
|
+
```jsonc
|
|
10
|
+
{
|
|
11
|
+
"providerUsage": {
|
|
12
|
+
"routes": [
|
|
13
|
+
"custom/my-gateway.mjs",
|
|
14
|
+
"custom/another-gateway.mjs"
|
|
15
|
+
],
|
|
16
|
+
"myGateway": { "username": "me", "password": "..." }
|
|
17
|
+
}
|
|
18
|
+
}
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
## 路由模块 API
|
|
22
|
+
|
|
23
|
+
```js
|
|
24
|
+
// ~/.agent-tools/custom/my-gateway.mjs
|
|
25
|
+
export const meta = { id: "my-gateway" }; // 可选; id 默认取文件名
|
|
26
|
+
|
|
27
|
+
export async function run(context, { requestJson, agentConfig }) {
|
|
28
|
+
// context: { baseUrl, key, providerName, provider, label }
|
|
29
|
+
const { myGateway = {} } = await agentConfig(); // providerUsage 对象, 自定义键随意加
|
|
30
|
+
|
|
31
|
+
const login = await fetch(`${context.baseUrl}/api/user/login`, {
|
|
32
|
+
method: "POST",
|
|
33
|
+
headers: { "content-type": "application/json" },
|
|
34
|
+
body: JSON.stringify({ username: myGateway.username, password: myGateway.password }),
|
|
35
|
+
});
|
|
36
|
+
const session = await login.json();
|
|
37
|
+
|
|
38
|
+
// requestJson 会解析 JSON, 并在非 2xx 响应时抛错; 需要时可在这里传入
|
|
39
|
+
// authorization、cookie 等自定义认证 header.
|
|
40
|
+
const me = await requestJson(`${context.baseUrl}/api/user/self`, {
|
|
41
|
+
headers: { authorization: `Bearer ${session?.data?.accessToken}` },
|
|
42
|
+
});
|
|
43
|
+
return { text: `balance ¥${me?.data?.balance}` };
|
|
44
|
+
}
|
|
45
|
+
```
|
|
46
|
+
|
|
47
|
+
`text` 是自由字符串; 成功返回 `{ text }`, 抛错则回落到下一条路由. 开启 `providerUsage.debug` 后, 探测失败会记录到 `~/.agent-tools/logs/usage-debug.log`.
|
|
@@ -0,0 +1,20 @@
|
|
|
1
|
+
# 仓库结构
|
|
2
|
+
|
|
3
|
+
```text
|
|
4
|
+
agent-tools/
|
|
5
|
+
├── .claude-plugin/ # Claude Code/plugin 生态的 manifest.
|
|
6
|
+
├── .codex-plugin/ # Codex plugin manifest.
|
|
7
|
+
├── integrations/ # 可安装的 capability, 一个一目录.
|
|
8
|
+
│ ├── statusline/ # Agent 状态栏: 分支, 模型, 用量.
|
|
9
|
+
│ ├── usage/ # Provider 余额/额度显示.
|
|
10
|
+
│ └── vision/ # 跨模型识图.
|
|
11
|
+
├── skills/ # 可复用的 Agent Skills.
|
|
12
|
+
│ ├── workflow/ # 工作流类 skills.
|
|
13
|
+
│ │ ├── at-commit/ # 生成 Conventional Commits message.
|
|
14
|
+
│ │ ├── at-review/ # 审查改动中的 bug 与回归风险.
|
|
15
|
+
│ │ └── at-simplify/ # 减少改动中的冗余和复杂度.
|
|
16
|
+
│ └── integrations/ # 对接外部系统的 skills.
|
|
17
|
+
│ └── at-zentao/ # 禅道 bug/task 修复工作流.
|
|
18
|
+
├── docs/ # 高级指南和贡献者参考.
|
|
19
|
+
└── scripts/ # 安装, 同步, 校验和仓库维护脚本.
|
|
20
|
+
```
|