@kairyou/agent-tools 0.4.0 → 0.5.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 +4 -3
- package/README.zh-CN.md +3 -3
- package/dist/statusline/claude-statusline.mjs +866 -61
- package/dist/usage/core.mjs +1199 -377
- package/integrations/statusline/claude-statusline.mjs +6 -65
- package/integrations/usage/core.mjs +13 -1103
- package/integrations/usage/lib/cache.mjs +110 -0
- package/integrations/usage/lib/config.mjs +99 -0
- package/integrations/usage/lib/context.mjs +138 -0
- package/integrations/usage/lib/format.mjs +265 -0
- package/integrations/usage/lib/http.mjs +186 -0
- package/integrations/usage/lib/routes.mjs +265 -0
- package/integrations/usage/lib/urls.mjs +48 -0
- package/package.json +1 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
// On-disk state under ~/.agent-tools/cache: the last working route per
|
|
2
|
+
// gateway, the latest usage snapshot, and refresh bookkeeping.
|
|
3
|
+
|
|
4
|
+
import { writeFile, mkdir } from "node:fs/promises";
|
|
5
|
+
import { dirname } from "node:path";
|
|
6
|
+
import {
|
|
7
|
+
ROUTE_CACHE_PATH,
|
|
8
|
+
SNAPSHOT_PATH,
|
|
9
|
+
REFRESH_STATE_PATH,
|
|
10
|
+
readTextIfExists,
|
|
11
|
+
debugLog,
|
|
12
|
+
} from "./config.mjs";
|
|
13
|
+
import { usageRouteCacheKey } from "./urls.mjs";
|
|
14
|
+
|
|
15
|
+
const ROUTE_CACHE_VERSION = 1;
|
|
16
|
+
const SNAPSHOT_VERSION = 1;
|
|
17
|
+
const REFRESH_STATE_VERSION = 1;
|
|
18
|
+
|
|
19
|
+
export async function readRouteCache() {
|
|
20
|
+
try {
|
|
21
|
+
const raw = await readTextIfExists(ROUTE_CACHE_PATH);
|
|
22
|
+
if (!raw.trim()) return { version: ROUTE_CACHE_VERSION, routes: {} };
|
|
23
|
+
const parsed = JSON.parse(raw);
|
|
24
|
+
return {
|
|
25
|
+
version: ROUTE_CACHE_VERSION,
|
|
26
|
+
routes: parsed?.routes && typeof parsed.routes === "object" ? parsed.routes : {},
|
|
27
|
+
};
|
|
28
|
+
} catch {
|
|
29
|
+
return { version: ROUTE_CACHE_VERSION, routes: {} };
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export async function rememberUsageRoute(context, route, result) {
|
|
34
|
+
try {
|
|
35
|
+
const cache = await readRouteCache();
|
|
36
|
+
const key = usageRouteCacheKey(context.baseUrl);
|
|
37
|
+
cache.routes[key] = {
|
|
38
|
+
route: route.id,
|
|
39
|
+
path: route.path,
|
|
40
|
+
source: result.source,
|
|
41
|
+
updatedAt: new Date().toISOString(),
|
|
42
|
+
};
|
|
43
|
+
await mkdir(dirname(ROUTE_CACHE_PATH), { recursive: true });
|
|
44
|
+
await writeFile(ROUTE_CACHE_PATH, `${JSON.stringify(cache, null, 2)}\n`);
|
|
45
|
+
} catch (error) {
|
|
46
|
+
await debugLog({ source: "route-cache", error: error.message });
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
async function readSnapshotCache() {
|
|
51
|
+
try {
|
|
52
|
+
const raw = await readTextIfExists(SNAPSHOT_PATH);
|
|
53
|
+
if (!raw.trim()) return { version: SNAPSHOT_VERSION, items: {} };
|
|
54
|
+
const parsed = JSON.parse(raw);
|
|
55
|
+
return {
|
|
56
|
+
version: SNAPSHOT_VERSION,
|
|
57
|
+
items: parsed?.items && typeof parsed.items === "object" ? parsed.items : {},
|
|
58
|
+
};
|
|
59
|
+
} catch {
|
|
60
|
+
return { version: SNAPSHOT_VERSION, items: {} };
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
export async function rememberUsageSnapshot(context, result) {
|
|
65
|
+
if (!result?.text) return;
|
|
66
|
+
try {
|
|
67
|
+
const cache = await readSnapshotCache();
|
|
68
|
+
const key = usageRouteCacheKey(context.baseUrl);
|
|
69
|
+
cache.items[key] = {
|
|
70
|
+
text: result.text,
|
|
71
|
+
source: result.source,
|
|
72
|
+
baseUrl: context.baseUrl,
|
|
73
|
+
updatedAt: new Date().toISOString(),
|
|
74
|
+
};
|
|
75
|
+
await mkdir(dirname(SNAPSHOT_PATH), { recursive: true });
|
|
76
|
+
await writeFile(SNAPSHOT_PATH, `${JSON.stringify(cache, null, 2)}\n`);
|
|
77
|
+
} catch (error) {
|
|
78
|
+
await debugLog({ source: "snapshot-cache", error: error.message });
|
|
79
|
+
}
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
async function readRefreshState() {
|
|
83
|
+
try {
|
|
84
|
+
const raw = await readTextIfExists(REFRESH_STATE_PATH);
|
|
85
|
+
if (!raw.trim()) return { version: REFRESH_STATE_VERSION, items: {} };
|
|
86
|
+
const parsed = JSON.parse(raw);
|
|
87
|
+
return {
|
|
88
|
+
version: REFRESH_STATE_VERSION,
|
|
89
|
+
items: parsed?.items && typeof parsed.items === "object" ? parsed.items : {},
|
|
90
|
+
};
|
|
91
|
+
} catch {
|
|
92
|
+
return { version: REFRESH_STATE_VERSION, items: {} };
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
export async function rememberRefreshState(context, patch) {
|
|
97
|
+
try {
|
|
98
|
+
const state = await readRefreshState();
|
|
99
|
+
const key = usageRouteCacheKey(context.baseUrl);
|
|
100
|
+
state.items[key] = {
|
|
101
|
+
...(state.items[key] || {}),
|
|
102
|
+
...patch,
|
|
103
|
+
baseUrl: context.baseUrl,
|
|
104
|
+
};
|
|
105
|
+
await mkdir(dirname(REFRESH_STATE_PATH), { recursive: true });
|
|
106
|
+
await writeFile(REFRESH_STATE_PATH, `${JSON.stringify(state, null, 2)}\n`);
|
|
107
|
+
} catch (error) {
|
|
108
|
+
await debugLog({ source: "refresh-state", error: error.message });
|
|
109
|
+
}
|
|
110
|
+
}
|
|
@@ -0,0 +1,99 @@
|
|
|
1
|
+
// Paths, constants, config.jsonc access, and debug logging shared by the
|
|
2
|
+
// usage runtime modules.
|
|
3
|
+
|
|
4
|
+
import { readFile, writeFile, mkdir } from "node:fs/promises";
|
|
5
|
+
import { existsSync } from "node:fs";
|
|
6
|
+
import { dirname, join } from "node:path";
|
|
7
|
+
import { homedir } from "node:os";
|
|
8
|
+
import { parse as parseJsonc } from "jsonc-parser";
|
|
9
|
+
|
|
10
|
+
export const CODEX_HOME = process.env.CODEX_HOME || join(homedir(), ".codex");
|
|
11
|
+
export const AGENT_TOOLS_HOME = process.env.AGENT_TOOLS_HOME || join(homedir(), ".agent-tools");
|
|
12
|
+
export const AUTH_PATH = join(CODEX_HOME, "auth.json");
|
|
13
|
+
export const CODEX_CONFIG_PATH = join(CODEX_HOME, "config.toml");
|
|
14
|
+
export const AGENT_CONFIG_PATH = join(AGENT_TOOLS_HOME, "config.jsonc");
|
|
15
|
+
export const DEBUG_PATH = join(AGENT_TOOLS_HOME, "logs", "usage-debug.log");
|
|
16
|
+
export const ROUTE_CACHE_PATH = join(AGENT_TOOLS_HOME, "cache", "usage-routes.json");
|
|
17
|
+
export const SNAPSHOT_PATH = join(AGENT_TOOLS_HOME, "cache", "usage-snapshot.json");
|
|
18
|
+
export const REFRESH_STATE_PATH = join(AGENT_TOOLS_HOME, "cache", "usage-refresh-state.json");
|
|
19
|
+
export const DEFAULT_USAGE_DAYS = 30;
|
|
20
|
+
export const MAX_USAGE_DAYS = 90;
|
|
21
|
+
export const DEFAULT_NEW_API_QUOTA_SCALE = 500000;
|
|
22
|
+
|
|
23
|
+
export async function readJson(path) {
|
|
24
|
+
return JSON.parse(await readFile(path, "utf8"));
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
export async function readTextIfExists(path) {
|
|
28
|
+
if (!existsSync(path)) return "";
|
|
29
|
+
return readFile(path, "utf8");
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
let agentConfigCache;
|
|
33
|
+
export async function agentConfig() {
|
|
34
|
+
if (agentConfigCache) return agentConfigCache;
|
|
35
|
+
try {
|
|
36
|
+
const raw = await readTextIfExists(AGENT_CONFIG_PATH);
|
|
37
|
+
if (!raw.trim()) {
|
|
38
|
+
agentConfigCache = {};
|
|
39
|
+
return agentConfigCache;
|
|
40
|
+
}
|
|
41
|
+
const errors = [];
|
|
42
|
+
const parsed = parseJsonc(raw.replace(/^\uFEFF/, ""), errors, { allowTrailingComma: true });
|
|
43
|
+
agentConfigCache = (errors.length === 0 && parsed?.providerUsage) || {};
|
|
44
|
+
} catch {
|
|
45
|
+
agentConfigCache = {};
|
|
46
|
+
}
|
|
47
|
+
return agentConfigCache;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
export async function debugLog(event) {
|
|
51
|
+
const config = await agentConfig();
|
|
52
|
+
if (process.env.PROVIDER_USAGE_DEBUG !== "1" && config.debug !== true) return;
|
|
53
|
+
await mkdir(dirname(DEBUG_PATH), { recursive: true });
|
|
54
|
+
const line = JSON.stringify({
|
|
55
|
+
at: new Date().toISOString(),
|
|
56
|
+
...event,
|
|
57
|
+
});
|
|
58
|
+
await writeFile(DEBUG_PATH, `${line}\n`, { flag: "a" });
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
export async function providerUsageDays() {
|
|
62
|
+
const config = await agentConfig();
|
|
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;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
export async function usagePreset() {
|
|
69
|
+
const config = await agentConfig();
|
|
70
|
+
return String(process.env.PROVIDER_USAGE_PRESET || config.preset || "auto").toLowerCase();
|
|
71
|
+
}
|
|
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
|
+
export async function newApiQuotaScale() {
|
|
96
|
+
const config = await agentConfig();
|
|
97
|
+
const scale = Number(config.newApiQuotaScale || DEFAULT_NEW_API_QUOTA_SCALE);
|
|
98
|
+
return Number.isFinite(scale) && scale > 0 ? scale : 0;
|
|
99
|
+
}
|
|
@@ -0,0 +1,138 @@
|
|
|
1
|
+
// Discovers the active relay endpoint and credentials for each agent.
|
|
2
|
+
|
|
3
|
+
import { existsSync } from "node:fs";
|
|
4
|
+
import {
|
|
5
|
+
AUTH_PATH,
|
|
6
|
+
CODEX_CONFIG_PATH,
|
|
7
|
+
readJson,
|
|
8
|
+
readTextIfExists,
|
|
9
|
+
} from "./config.mjs";
|
|
10
|
+
|
|
11
|
+
function stripInlineComment(value) {
|
|
12
|
+
let inSingle = false;
|
|
13
|
+
let inDouble = false;
|
|
14
|
+
for (let i = 0; i < value.length; i += 1) {
|
|
15
|
+
const char = value[i];
|
|
16
|
+
const prev = value[i - 1];
|
|
17
|
+
if (char === "'" && !inDouble) inSingle = !inSingle;
|
|
18
|
+
if (char === '"' && !inSingle && prev !== "\\") inDouble = !inDouble;
|
|
19
|
+
if (char === "#" && !inSingle && !inDouble) return value.slice(0, i).trim();
|
|
20
|
+
}
|
|
21
|
+
return value.trim();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
function parseTomlLite(source) {
|
|
25
|
+
const root = {};
|
|
26
|
+
let current = root;
|
|
27
|
+
for (const rawLine of source.split(/\r?\n/)) {
|
|
28
|
+
const line = rawLine.trim();
|
|
29
|
+
if (!line || line.startsWith("#")) continue;
|
|
30
|
+
|
|
31
|
+
const table = line.match(/^\[([^\]]+)\]$/);
|
|
32
|
+
if (table) {
|
|
33
|
+
current = root;
|
|
34
|
+
for (const part of table[1].split(".")) {
|
|
35
|
+
const key = part.replace(/^['"]|['"]$/g, "");
|
|
36
|
+
current[key] ||= {};
|
|
37
|
+
current = current[key];
|
|
38
|
+
}
|
|
39
|
+
continue;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
const eq = line.indexOf("=");
|
|
43
|
+
if (eq === -1) continue;
|
|
44
|
+
const key = line.slice(0, eq).trim();
|
|
45
|
+
const rawValue = stripInlineComment(line.slice(eq + 1));
|
|
46
|
+
current[key] = parseTomlValue(rawValue);
|
|
47
|
+
}
|
|
48
|
+
return root;
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function parseTomlValue(value) {
|
|
52
|
+
if ((value.startsWith('"') && value.endsWith('"')) || (value.startsWith("'") && value.endsWith("'"))) {
|
|
53
|
+
return value.slice(1, -1);
|
|
54
|
+
}
|
|
55
|
+
if (value === "true") return true;
|
|
56
|
+
if (value === "false") return false;
|
|
57
|
+
return value;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
function activeProvider(config) {
|
|
61
|
+
const providerName = config.model_provider || "openai";
|
|
62
|
+
const provider = config.model_providers?.[providerName] || {};
|
|
63
|
+
return { providerName, provider };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
function apiKeyFor(auth, provider) {
|
|
67
|
+
if (process.env.PROVIDER_USAGE_API_KEY) return process.env.PROVIDER_USAGE_API_KEY;
|
|
68
|
+
if (process.env.SUB2API_API_KEY) return process.env.SUB2API_API_KEY;
|
|
69
|
+
if (provider.env_key && process.env[provider.env_key]) return process.env[provider.env_key];
|
|
70
|
+
if (auth.OPENAI_API_KEY) return auth.OPENAI_API_KEY;
|
|
71
|
+
if (process.env.OPENAI_API_KEY) return process.env.OPENAI_API_KEY;
|
|
72
|
+
return "";
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function apiKeyForClaude() {
|
|
76
|
+
return (
|
|
77
|
+
process.env.PROVIDER_USAGE_API_KEY ||
|
|
78
|
+
process.env.ANTHROPIC_AUTH_TOKEN ||
|
|
79
|
+
process.env.ANTHROPIC_API_KEY ||
|
|
80
|
+
""
|
|
81
|
+
);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function providerLabel(providerName, provider) {
|
|
85
|
+
return String(provider.name || providerName || "API").toUpperCase();
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function contextForCodex() {
|
|
89
|
+
const auth = existsSync(AUTH_PATH) ? await readJson(AUTH_PATH) : {};
|
|
90
|
+
const codexConfig = parseTomlLite(await readTextIfExists(CODEX_CONFIG_PATH));
|
|
91
|
+
const { providerName, provider } = activeProvider(codexConfig);
|
|
92
|
+
const baseUrl =
|
|
93
|
+
process.env.PROVIDER_USAGE_BASE_URL ||
|
|
94
|
+
process.env.SUB2API_BASE_URL ||
|
|
95
|
+
process.env.OPENAI_BASE_URL ||
|
|
96
|
+
provider.base_url ||
|
|
97
|
+
"";
|
|
98
|
+
const key = apiKeyFor(auth, provider);
|
|
99
|
+
return {
|
|
100
|
+
providerName,
|
|
101
|
+
provider,
|
|
102
|
+
baseUrl,
|
|
103
|
+
key,
|
|
104
|
+
label: providerLabel(providerName, provider),
|
|
105
|
+
};
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
function contextForClaude() {
|
|
109
|
+
const baseUrl =
|
|
110
|
+
process.env.PROVIDER_USAGE_BASE_URL ||
|
|
111
|
+
process.env.ANTHROPIC_BASE_URL ||
|
|
112
|
+
"";
|
|
113
|
+
return {
|
|
114
|
+
providerName: "claude",
|
|
115
|
+
provider: { name: "Claude" },
|
|
116
|
+
baseUrl,
|
|
117
|
+
key: apiKeyForClaude(),
|
|
118
|
+
label: "Claude",
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
export async function usageContext(agent) {
|
|
123
|
+
return agent === "claude" ? contextForClaude() : await contextForCodex();
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
export function normalizeUsageContext(input) {
|
|
127
|
+
const providerName = String(input?.providerName || "provider");
|
|
128
|
+
const provider = input?.provider && typeof input.provider === "object"
|
|
129
|
+
? input.provider
|
|
130
|
+
: { name: providerName };
|
|
131
|
+
return {
|
|
132
|
+
providerName,
|
|
133
|
+
provider,
|
|
134
|
+
baseUrl: String(input?.baseUrl || ""),
|
|
135
|
+
key: String(input?.key || ""),
|
|
136
|
+
label: String(input?.label || provider.name || providerName),
|
|
137
|
+
};
|
|
138
|
+
}
|
|
@@ -0,0 +1,265 @@
|
|
|
1
|
+
// Turns gateway payloads into the compact one-line usage message.
|
|
2
|
+
|
|
3
|
+
import { newApiQuotaScale, providerUsageDays, DEFAULT_NEW_API_QUOTA_SCALE } from "./config.mjs";
|
|
4
|
+
|
|
5
|
+
export function pickNumber(obj, keys) {
|
|
6
|
+
for (const key of keys) {
|
|
7
|
+
const value = obj?.[key];
|
|
8
|
+
if (typeof value === "number" && Number.isFinite(value)) return value;
|
|
9
|
+
if (typeof value === "string" && value.trim() && Number.isFinite(Number(value))) return Number(value);
|
|
10
|
+
}
|
|
11
|
+
return undefined;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
export function formatMoney(value) {
|
|
15
|
+
return `$${value.toFixed(value >= 100 ? 0 : 1)}`;
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function formatMaybeMoney(value, unit = "USD") {
|
|
19
|
+
if (unit === "USD" || unit === "$") return formatMoney(value);
|
|
20
|
+
return `${value.toLocaleString("en-US", { maximumFractionDigits: 1 })} ${unit}`;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
function usageParts() {
|
|
24
|
+
return ["API"];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
async function formatNewApiQuota(value) {
|
|
28
|
+
const scale = await newApiQuotaScale();
|
|
29
|
+
if (Number.isFinite(scale) && scale > 0) return formatMoney(value / scale);
|
|
30
|
+
return value.toLocaleString("en-US", { maximumFractionDigits: 0 });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
export function usageRoot(data) {
|
|
34
|
+
return data?.data && typeof data.data === "object" ? data.data : data;
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
function shortDate(value) {
|
|
38
|
+
if (!value) return "";
|
|
39
|
+
const match = String(value).match(/^(\d{4})-(\d{2})-(\d{2})/);
|
|
40
|
+
return match ? `${match[2]}-${match[3]}` : "";
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function hasSubscriptionLimits(root) {
|
|
44
|
+
const sub = root?.subscription || {};
|
|
45
|
+
return [
|
|
46
|
+
"daily_limit_usd",
|
|
47
|
+
"weekly_limit_usd",
|
|
48
|
+
"monthly_limit_usd",
|
|
49
|
+
"daily_usage_usd",
|
|
50
|
+
"weekly_usage_usd",
|
|
51
|
+
"monthly_usage_usd",
|
|
52
|
+
].some((key) => pickNumber(sub, [key]) !== undefined);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
function isQuotaLimitedUsage(root) {
|
|
56
|
+
return root?.mode === "quota_limited" || root?.quota;
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
function isSubscriptionUsage(root) {
|
|
60
|
+
return hasSubscriptionLimits(root) || (root?.mode === "unrestricted" && root?.subscription);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
function isWalletUsage(root) {
|
|
64
|
+
const planName = String(root?.planName || "");
|
|
65
|
+
return (
|
|
66
|
+
(root?.mode === "unrestricted" && !isSubscriptionUsage(root)) ||
|
|
67
|
+
planName.includes("钱包") ||
|
|
68
|
+
planName.toLowerCase().includes("wallet") ||
|
|
69
|
+
(pickNumber(root, ["balance"]) !== undefined && !hasSubscriptionLimits(root))
|
|
70
|
+
);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
export function hasV1UsageFields(root) {
|
|
74
|
+
return (
|
|
75
|
+
hasSubscriptionLimits(root) ||
|
|
76
|
+
pickNumber(root, [
|
|
77
|
+
"balance",
|
|
78
|
+
"remaining",
|
|
79
|
+
"remain",
|
|
80
|
+
"available",
|
|
81
|
+
"hard_limit_usd",
|
|
82
|
+
"hard_limit",
|
|
83
|
+
"total_granted",
|
|
84
|
+
"quota",
|
|
85
|
+
"total_usage",
|
|
86
|
+
"used",
|
|
87
|
+
"usage",
|
|
88
|
+
]) !== undefined ||
|
|
89
|
+
pickNumber(root?.quota, ["limit", "quota", "used", "quota_used", "remaining"]) !== undefined ||
|
|
90
|
+
pickNumber(root?.usage?.today, ["actual_cost", "cost"]) !== undefined ||
|
|
91
|
+
(Array.isArray(root?.daily_usage) && root.daily_usage.length > 0)
|
|
92
|
+
);
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
export async function formatQuota(label, data) {
|
|
96
|
+
const root = usageRoot(data);
|
|
97
|
+
const unit = root?.unit || "USD";
|
|
98
|
+
const remaining = pickNumber(root, ["remaining"]);
|
|
99
|
+
const hardLimit = pickNumber(root, ["hard_limit_usd", "hard_limit", "total_granted", "quota"]);
|
|
100
|
+
const used = pickNumber(root, ["total_usage", "used", "usage"]);
|
|
101
|
+
const balance = pickNumber(root, ["balance", "remaining", "remain", "available"]);
|
|
102
|
+
|
|
103
|
+
if (isQuotaLimitedUsage(root)) return formatQuotaLimitedLine(label, root);
|
|
104
|
+
if (isSubscriptionUsage(root)) return formatUsageLine(label, root);
|
|
105
|
+
if (isWalletUsage(root)) return await formatWalletLine(label, root);
|
|
106
|
+
if (remaining !== undefined) return formatUsageLine(label, root);
|
|
107
|
+
if (balance !== undefined) return `API | balance ${formatMaybeMoney(balance, unit)}`;
|
|
108
|
+
if (hardLimit !== undefined && used !== undefined) {
|
|
109
|
+
return `API | remaining ${formatMaybeMoney(Math.max(0, hardLimit - used), unit)}`;
|
|
110
|
+
}
|
|
111
|
+
if (hardLimit !== undefined) return `API | total ${formatMaybeMoney(hardLimit, unit)}`;
|
|
112
|
+
|
|
113
|
+
const keys = Object.keys(root || {}).slice(0, 4).join(", ");
|
|
114
|
+
return keys ? `API | received (${keys})` : `API | checked ${unit}`;
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
export async function formatNewApiTokenLine(label, data) {
|
|
118
|
+
const root = usageRoot(data);
|
|
119
|
+
const unlimited = root?.unlimited_quota === true || root?.unlimitedQuota === true;
|
|
120
|
+
const quota = pickNumber(root, ["quota", "limit", "total_quota", "totalQuota"]);
|
|
121
|
+
const used = pickNumber(root, ["used_quota", "usedQuota", "used"]);
|
|
122
|
+
let remaining = pickNumber(root, ["remain_quota", "remainQuota", "remaining", "balance"]);
|
|
123
|
+
if (remaining === undefined && quota !== undefined && used !== undefined) {
|
|
124
|
+
remaining = Math.max(0, quota - used);
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
if (!unlimited && quota === undefined && used === undefined && remaining === undefined) {
|
|
128
|
+
throw new Error("NewAPI token usage payload has no quota fields");
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const parts = usageParts();
|
|
132
|
+
if (unlimited) parts.push("unlimited");
|
|
133
|
+
if (remaining !== undefined) parts.push(`balance ${await formatNewApiQuota(remaining)}`);
|
|
134
|
+
if (used !== undefined && quota !== undefined) {
|
|
135
|
+
parts.push(`used ${await formatNewApiQuota(used)}/${await formatNewApiQuota(quota)}`);
|
|
136
|
+
} else if (used !== undefined) {
|
|
137
|
+
parts.push(`used ${await formatNewApiQuota(used)}`);
|
|
138
|
+
}
|
|
139
|
+
return parts.join(" | ");
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
export function formatOpenRouterLine(label, data) {
|
|
143
|
+
const root = usageRoot(data);
|
|
144
|
+
const limit = pickNumber(root, ["limit", "limit_remaining", "total_credits"]);
|
|
145
|
+
const remaining = pickNumber(root, ["limit_remaining", "remaining_credits"]);
|
|
146
|
+
const used = pickNumber(root, ["usage", "total_usage", "spend"]);
|
|
147
|
+
const reset = root?.limit_reset || root?.reset_at ? shortDate(root.limit_reset || root.reset_at) : "";
|
|
148
|
+
const parts = usageParts();
|
|
149
|
+
|
|
150
|
+
if (remaining !== undefined) parts.push(`balance ${formatMoney(remaining)}`);
|
|
151
|
+
if (used !== undefined && limit !== undefined && limit !== remaining) {
|
|
152
|
+
parts.push(`used ${formatMoney(used)}/${formatMoney(limit)}`);
|
|
153
|
+
} else if (used !== undefined) {
|
|
154
|
+
parts.push(`used ${formatMoney(used)}`);
|
|
155
|
+
}
|
|
156
|
+
if (reset) parts.push(`Reset ${reset}`);
|
|
157
|
+
|
|
158
|
+
if (parts.length === 1) throw new Error("OpenRouter payload has no usage fields");
|
|
159
|
+
return parts.join(" | ");
|
|
160
|
+
}
|
|
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(" | ");
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
function formatQuotaLimitedLine(label, root) {
|
|
204
|
+
const quota = root?.quota || {};
|
|
205
|
+
const limit = pickNumber(quota, ["limit", "quota"]);
|
|
206
|
+
const used = pickNumber(quota, ["used", "quota_used"]);
|
|
207
|
+
const remaining = pickNumber(quota, ["remaining"]) ?? pickNumber(root, ["remaining"]);
|
|
208
|
+
const parts = usageParts();
|
|
209
|
+
|
|
210
|
+
if (limit !== undefined && used !== undefined) {
|
|
211
|
+
parts.push(`Q ${formatMoney(used)}/${formatMoney(limit)}`);
|
|
212
|
+
} else if (remaining !== undefined) {
|
|
213
|
+
parts.push(`remaining ${formatMoney(remaining)}`);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
if (Array.isArray(root?.rate_limits) && root.rate_limits.length > 0) {
|
|
217
|
+
const rateParts = root.rate_limits
|
|
218
|
+
.map((entry) => {
|
|
219
|
+
const window = entry?.window;
|
|
220
|
+
const rateLimit = pickNumber(entry, ["limit"]);
|
|
221
|
+
const rateUsed = pickNumber(entry, ["used"]);
|
|
222
|
+
return window && rateLimit !== undefined && rateUsed !== undefined
|
|
223
|
+
? `${window} ${formatMoney(rateUsed)}/${formatMoney(rateLimit)}`
|
|
224
|
+
: "";
|
|
225
|
+
})
|
|
226
|
+
.filter(Boolean);
|
|
227
|
+
if (rateParts.length > 0) parts.push(rateParts.join(", "));
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
return parts.join(" | ");
|
|
231
|
+
}
|
|
232
|
+
|
|
233
|
+
async function formatWalletLine(label, root) {
|
|
234
|
+
const balance = pickNumber(root, ["balance", "remaining", "remain", "available"]);
|
|
235
|
+
const todayCost = pickNumber(root?.usage?.today, ["actual_cost", "cost"]);
|
|
236
|
+
const recentUsage = Array.isArray(root?.daily_usage)
|
|
237
|
+
? root.daily_usage.reduce((sum, day) => sum + (pickNumber(day, ["actual_cost", "cost"]) || 0), 0)
|
|
238
|
+
: undefined;
|
|
239
|
+
|
|
240
|
+
const parts = usageParts();
|
|
241
|
+
if (balance !== undefined) parts.push(`balance ${formatMoney(balance)}`);
|
|
242
|
+
if (todayCost !== undefined) parts.push(`today ${formatMoney(todayCost)}`);
|
|
243
|
+
if (recentUsage !== undefined && root.daily_usage.length > 0) {
|
|
244
|
+
parts.push(`${await providerUsageDays()}d ${formatMoney(recentUsage)}`);
|
|
245
|
+
}
|
|
246
|
+
return parts.join(" | ");
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
function formatUsageLine(label, root) {
|
|
250
|
+
const sub = root?.subscription || {};
|
|
251
|
+
const dailyLimit = pickNumber(sub, ["daily_limit_usd"]);
|
|
252
|
+
const dailyUsage = pickNumber(sub, ["daily_usage_usd"]);
|
|
253
|
+
const weeklyLimit = pickNumber(sub, ["weekly_limit_usd"]);
|
|
254
|
+
const weeklyUsage = pickNumber(sub, ["weekly_usage_usd"]);
|
|
255
|
+
const monthlyLimit = pickNumber(sub, ["monthly_limit_usd"]);
|
|
256
|
+
const monthlyUsage = pickNumber(sub, ["monthly_usage_usd"]);
|
|
257
|
+
const expires = shortDate(sub.expires_at);
|
|
258
|
+
|
|
259
|
+
const parts = usageParts();
|
|
260
|
+
if (dailyLimit > 0 && dailyUsage !== undefined) parts.push(`D ${formatMoney(dailyUsage)}/${formatMoney(dailyLimit)}`);
|
|
261
|
+
if (weeklyLimit > 0 && weeklyUsage !== undefined) parts.push(`W ${formatMoney(weeklyUsage)}/${formatMoney(weeklyLimit)}`);
|
|
262
|
+
if (monthlyLimit > 0 && monthlyUsage !== undefined) parts.push(`M ${formatMoney(monthlyUsage)}/${formatMoney(monthlyLimit)}`);
|
|
263
|
+
if (expires) parts.push(`Exp ${expires}`);
|
|
264
|
+
return parts.join(" | ");
|
|
265
|
+
}
|