@1930dev/opencode-usage 0.3.3 → 0.3.5
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 +28 -3
- package/dist/cli.js +212 -13
- package/dist/tui.js +92 -12
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -58,6 +58,10 @@ opencode-usage usage --pct # add the % BUDGET column (--by provider
|
|
|
58
58
|
opencode-usage providers
|
|
59
59
|
opencode-usage providers --no-net # cached only
|
|
60
60
|
|
|
61
|
+
# What the providers themselves answer about their budget
|
|
62
|
+
opencode-usage probe # one free-model call per provider
|
|
63
|
+
opencode-usage probe --json
|
|
64
|
+
|
|
61
65
|
# Model ranking by intelligence per blended dollar
|
|
62
66
|
opencode-usage top
|
|
63
67
|
opencode-usage top --limit 10 --json
|
|
@@ -124,7 +128,11 @@ Normalized percentage of budget consumed per provider, from these sources (in pr
|
|
|
124
128
|
- `opencode-go` (Zen): rolling 5h / weekly / monthly % (binding window)
|
|
125
129
|
- `openrouter`: credits used / total credits
|
|
126
130
|
- `orcarouter`: spend since top-up; a per-key credit cap turns into a %
|
|
127
|
-
- `zai`: coding plan quota
|
|
131
|
+
- `zai`: coding plan quota when the key has an active GLM plan; otherwise the
|
|
132
|
+
wallet balance (fallback)
|
|
133
|
+
- `snowflake-cortex`: tokens in the last 30d from the account usage views
|
|
134
|
+
(SQL API); the percentage comes from a `snowflake-cortex` line in
|
|
135
|
+
`budgets.json`
|
|
128
136
|
|
|
129
137
|
2. **Documented limits** — published quotas, used when the provider reports none.
|
|
130
138
|
Each one is read against the period it resets on: a daily limit against today,
|
|
@@ -142,8 +150,8 @@ Normalized percentage of budget consumed per provider, from these sources (in pr
|
|
|
142
150
|
- `cloudflare-workers-ai`: 10k neurons/day (free tier, resets 00:00 UTC)
|
|
143
151
|
- `nvidia`: credit allowance retired — the trial is rate-limited per model
|
|
144
152
|
- `opencode`: no spend API for API keys
|
|
145
|
-
- `
|
|
146
|
-
|
|
153
|
+
- `zai`: a key without a coding plan shows its wallet balance (live), and the
|
|
154
|
+
percentage comes from a `zai` line in `budgets.json`
|
|
147
155
|
|
|
148
156
|
3. **budgets.json** — your monthly USD per provider, read against what the
|
|
149
157
|
provider cost so far this calendar month:
|
|
@@ -157,6 +165,23 @@ Normalized percentage of budget consumed per provider, from these sources (in pr
|
|
|
157
165
|
|
|
158
166
|
Providers without any source show `—`.
|
|
159
167
|
|
|
168
|
+
## Free-model probe (`probe`)
|
|
169
|
+
|
|
170
|
+
`probe` sends one minimal request against a free model per connected provider
|
|
171
|
+
and reports what the answer reveals about the budget: a rate-limit header, a
|
|
172
|
+
"no credits" error, or the meter itself (`usage.neurons` for Cloudflare). It
|
|
173
|
+
reads the key from opencode's auth store and never prints it. Providers covered:
|
|
174
|
+
|
|
175
|
+
- `cloudflare-workers-ai` — the response carries neurons per request (the free
|
|
176
|
+
tier's unit), so the probe answers whether the account can still run
|
|
177
|
+
- `nvidia` — an HTTP 200 means the free per-model allowance still works
|
|
178
|
+
- `orcarouter` — the `free` model answers `rate_limit_error` with a
|
|
179
|
+
`retry-after`, and credits raise the cap
|
|
180
|
+
- `zai` — an HTTP 429 with `code 1113` means the balance is empty
|
|
181
|
+
|
|
182
|
+
`opencode` exposes no chat API for keys and `snowflake-cortex` is read through
|
|
183
|
+
its SQL quota, so both are skipped.
|
|
184
|
+
|
|
160
185
|
## Ranking (`top`)
|
|
161
186
|
|
|
162
187
|
Models ranked by intelligence index per blended dollar:
|
package/dist/cli.js
CHANGED
|
@@ -284,12 +284,39 @@ function parseZai(data) {
|
|
|
284
284
|
raw: data
|
|
285
285
|
};
|
|
286
286
|
}
|
|
287
|
+
function parseZaiWallet(data) {
|
|
288
|
+
const failed = data.success === false || data.code !== undefined && data.code !== 0 && data.code !== 200;
|
|
289
|
+
if (failed) {
|
|
290
|
+
return { provider: "zai", ok: false, detail: data.msg ?? "wallet endpoint error", windows: [] };
|
|
291
|
+
}
|
|
292
|
+
const balance = data.data?.availableBalance ?? data.data?.rechargeAmount ?? 0;
|
|
293
|
+
const spend = data.data?.totalSpendAmount ?? 0;
|
|
294
|
+
const windows = [
|
|
295
|
+
{ label: "balance", percentUsed: 0, detail: `$${balance.toFixed(2)}` },
|
|
296
|
+
{ label: "spend", percentUsed: 0, detail: `$${spend.toFixed(2)}` }
|
|
297
|
+
];
|
|
298
|
+
const today = data.data?.todaySpendAmount;
|
|
299
|
+
if (typeof today === "number") {
|
|
300
|
+
windows.push({ label: "today", percentUsed: 0, detail: `$${today.toFixed(2)}` });
|
|
301
|
+
}
|
|
302
|
+
return {
|
|
303
|
+
provider: "zai",
|
|
304
|
+
ok: true,
|
|
305
|
+
detail: `balance $${balance.toFixed(2)}`,
|
|
306
|
+
windows,
|
|
307
|
+
raw: data
|
|
308
|
+
};
|
|
309
|
+
}
|
|
310
|
+
var QUOTA_URL = "https://api.z.ai/api/monitor/usage/quota/limit";
|
|
311
|
+
var WALLET_URL = "https://api.z.ai/api/biz/account/query-customer-account-report";
|
|
287
312
|
async function fetchZai(key) {
|
|
288
313
|
try {
|
|
289
|
-
const
|
|
290
|
-
|
|
291
|
-
|
|
292
|
-
|
|
314
|
+
const auth = { Authorization: `Bearer ${key}`, Accept: "application/json" };
|
|
315
|
+
const data = await getJson(QUOTA_URL, auth);
|
|
316
|
+
if (data.code !== undefined && data.code !== 0) {
|
|
317
|
+
const wallet = await getJson(WALLET_URL, auth);
|
|
318
|
+
return parseZaiWallet(wallet);
|
|
319
|
+
}
|
|
293
320
|
return parseZai(data);
|
|
294
321
|
} catch (err) {
|
|
295
322
|
return { provider: "zai", ok: false, detail: err.message, windows: [] };
|
|
@@ -338,11 +365,13 @@ function parseOrcaRouter(usage, subscription) {
|
|
|
338
365
|
const cap = hard > 0 && hard < NO_CAP ? hard : soft > 0 && soft < NO_CAP ? soft : 0;
|
|
339
366
|
const capped = cap > 0;
|
|
340
367
|
const pct = capped ? spent / cap * 100 : 0;
|
|
368
|
+
const hint = capped ? "" : " (no cap set)";
|
|
369
|
+
const detail = `$${spent.toFixed(2)} spent${hint}`;
|
|
341
370
|
return {
|
|
342
371
|
provider: "orcarouter",
|
|
343
372
|
ok: true,
|
|
344
|
-
detail
|
|
345
|
-
windows: [{ label: "spend", percentUsed: pct, detail: `$${spent.toFixed(2)}` }],
|
|
373
|
+
detail,
|
|
374
|
+
windows: [{ label: "spend", percentUsed: pct, detail: `$${spent.toFixed(2)}${hint}` }],
|
|
346
375
|
budget: capped ? { percentUsed: pct, label: `$${cap}` } : undefined,
|
|
347
376
|
raw: { usage, subscription }
|
|
348
377
|
};
|
|
@@ -358,6 +387,56 @@ async function fetchOrcaRouter(key) {
|
|
|
358
387
|
}
|
|
359
388
|
}
|
|
360
389
|
|
|
390
|
+
// packages/core/src/quota/snowflake.ts
|
|
391
|
+
var SQL_API = (account) => `https://${account}.snowflakecomputing.com/api/v2/statements`;
|
|
392
|
+
var STATEMENT = `SELECT
|
|
393
|
+
SUM(CASE WHEN START_TIME >= DATE_TRUNC('day', CURRENT_TIMESTAMP())
|
|
394
|
+
AND START_TIME < DATEADD('day', 1, DATE_TRUNC('day', CURRENT_TIMESTAMP()))
|
|
395
|
+
THEN TOKENS ELSE 0 END) AS today_tokens,
|
|
396
|
+
SUM(TOKENS) AS month_tokens
|
|
397
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_REST_API_USAGE_HISTORY
|
|
398
|
+
WHERE START_TIME >= DATEADD('day', -30, CURRENT_TIMESTAMP())`;
|
|
399
|
+
var fmtTokens = (n) => n.toLocaleString("en-US");
|
|
400
|
+
function parseSnowflake(row) {
|
|
401
|
+
const today = Number(row?.[0] ?? 0);
|
|
402
|
+
const month = Number(row?.[1] ?? 0);
|
|
403
|
+
return {
|
|
404
|
+
provider: "snowflake-cortex",
|
|
405
|
+
ok: true,
|
|
406
|
+
detail: `${fmtTokens(month)} tokens/30d`,
|
|
407
|
+
windows: [
|
|
408
|
+
{ label: "tokens/mo", percentUsed: 0, detail: `${fmtTokens(month)}` },
|
|
409
|
+
{ label: "today", percentUsed: 0, detail: `${fmtTokens(today)}` }
|
|
410
|
+
],
|
|
411
|
+
raw: { row }
|
|
412
|
+
};
|
|
413
|
+
}
|
|
414
|
+
async function fetchSnowflake(key, metadata) {
|
|
415
|
+
const account = String(metadata?.account ?? "").toUpperCase();
|
|
416
|
+
if (!account) {
|
|
417
|
+
return { provider: "snowflake-cortex", ok: false, detail: "metadata.account is required", windows: [] };
|
|
418
|
+
}
|
|
419
|
+
try {
|
|
420
|
+
const res = await fetch(SQL_API(account), {
|
|
421
|
+
method: "POST",
|
|
422
|
+
headers: {
|
|
423
|
+
Authorization: `Bearer ${key}`,
|
|
424
|
+
"Content-Type": "application/json",
|
|
425
|
+
Accept: "application/json"
|
|
426
|
+
},
|
|
427
|
+
body: JSON.stringify({ statement: STATEMENT, timeout: 30 })
|
|
428
|
+
});
|
|
429
|
+
const body = await res.json().catch(() => null);
|
|
430
|
+
if (!res.ok) {
|
|
431
|
+
return { provider: "snowflake-cortex", ok: false, detail: body?.message ?? `HTTP ${res.status}`, windows: [] };
|
|
432
|
+
}
|
|
433
|
+
const rows = Array.isArray(body?.data) ? body.data : [];
|
|
434
|
+
return parseSnowflake(rows[0]);
|
|
435
|
+
} catch (err) {
|
|
436
|
+
return { provider: "snowflake-cortex", ok: false, detail: err.message, windows: [] };
|
|
437
|
+
}
|
|
438
|
+
}
|
|
439
|
+
|
|
361
440
|
// packages/core/src/providers.ts
|
|
362
441
|
var TTL_MS = 15 * 60 * 1000;
|
|
363
442
|
async function readCache() {
|
|
@@ -381,7 +460,8 @@ var FETCHERS = {
|
|
|
381
460
|
"github-copilot": fetchCopilot,
|
|
382
461
|
zai: fetchZai,
|
|
383
462
|
amd: fetchAmd,
|
|
384
|
-
orcarouter: fetchOrcaRouter
|
|
463
|
+
orcarouter: fetchOrcaRouter,
|
|
464
|
+
"snowflake-cortex": fetchSnowflake
|
|
385
465
|
};
|
|
386
466
|
async function providerStatuses(opts) {
|
|
387
467
|
const auth = await readAuth();
|
|
@@ -402,7 +482,7 @@ async function providerStatuses(opts) {
|
|
|
402
482
|
const secret = authSecret(auth[p]);
|
|
403
483
|
if (!fetcher || !secret)
|
|
404
484
|
return null;
|
|
405
|
-
return fetcher(secret);
|
|
485
|
+
return fetcher(secret, auth[p].metadata);
|
|
406
486
|
}));
|
|
407
487
|
for (const q of live) {
|
|
408
488
|
if (q)
|
|
@@ -425,7 +505,7 @@ async function providerStatuses(opts) {
|
|
|
425
505
|
}));
|
|
426
506
|
}
|
|
427
507
|
// packages/core/src/report.ts
|
|
428
|
-
function
|
|
508
|
+
function fmtTokens2(n) {
|
|
429
509
|
if (n >= 1e9)
|
|
430
510
|
return `${(n / 1e9).toFixed(1)}B`;
|
|
431
511
|
if (n >= 1e6)
|
|
@@ -446,6 +526,15 @@ function fmtAgo(ms, now = Date.now()) {
|
|
|
446
526
|
return `${h}h ago`;
|
|
447
527
|
return `${Math.floor(h / 24)}d ago`;
|
|
448
528
|
}
|
|
529
|
+
function fmtRetry(seconds) {
|
|
530
|
+
if (seconds < 60)
|
|
531
|
+
return `${seconds}s`;
|
|
532
|
+
if (seconds < 3600)
|
|
533
|
+
return `${Math.round(seconds / 60)}m`;
|
|
534
|
+
if (seconds < 86400)
|
|
535
|
+
return `${Math.ceil(seconds / 3600)}h`;
|
|
536
|
+
return `${Math.ceil(seconds / 86400)}d`;
|
|
537
|
+
}
|
|
449
538
|
function pad(s, n) {
|
|
450
539
|
return s.length >= n ? s : s + " ".repeat(n - s.length);
|
|
451
540
|
}
|
|
@@ -467,8 +556,8 @@ function usageTable(rows, totals, sinceLabel, groupBy, pct) {
|
|
|
467
556
|
const row = [
|
|
468
557
|
r.group,
|
|
469
558
|
String(r.messages),
|
|
470
|
-
|
|
471
|
-
|
|
559
|
+
fmtTokens2(r.tokensInput),
|
|
560
|
+
fmtTokens2(r.tokensOutput),
|
|
472
561
|
fmtCost(r.cost)
|
|
473
562
|
];
|
|
474
563
|
if (pct) {
|
|
@@ -485,7 +574,7 @@ function usageTable(rows, totals, sinceLabel, groupBy, pct) {
|
|
|
485
574
|
[sources: ${[...pct.values()].map((p) => `${p.source}${p.label ? `(${p.label})` : ""}`).filter((v, i, a) => a.indexOf(v) === i).join(", ")}]` : "";
|
|
486
575
|
return `${t}
|
|
487
576
|
|
|
488
|
-
TOTAL since ${sinceLabel}: ${totals.messages} msgs, ${
|
|
577
|
+
TOTAL since ${sinceLabel}: ${totals.messages} msgs, ${fmtTokens2(totals.tokensInput)} in / ${fmtTokens2(totals.tokensOutput)} out, ${fmtCost(totals.cost)} est.${sourceNote}`;
|
|
489
578
|
}
|
|
490
579
|
function providersTable(statuses, local) {
|
|
491
580
|
const headers = ["PROVIDER", "QUOTA", "USAGE 7D (LOCAL)", "LAST USED"];
|
|
@@ -498,6 +587,18 @@ function providersTable(statuses, local) {
|
|
|
498
587
|
});
|
|
499
588
|
return table(headers, rows);
|
|
500
589
|
}
|
|
590
|
+
function probeTable(results) {
|
|
591
|
+
const headers = ["PROVIDER", "TAUGHT", "SIGNAL", "RETRY"];
|
|
592
|
+
const rows = results.map((r) => {
|
|
593
|
+
if (r.status === 0)
|
|
594
|
+
return [r.provider, "error", r.message ?? "no signal", "\u2014"];
|
|
595
|
+
const taught = r.ok ? "yes" : "no";
|
|
596
|
+
const signal = r.ok ? r.usage && Object.keys(r.usage).length > 0 ? Object.entries(r.usage).map(([k, v]) => `${k}=${v}`).join(", ") : "http ok" : r.message || (r.code ? `code ${r.code}` : `HTTP ${r.status}`);
|
|
597
|
+
const retry = r.retryAfterSeconds !== undefined ? fmtRetry(r.retryAfterSeconds) : "\u2014";
|
|
598
|
+
return [r.provider, taught, signal, retry];
|
|
599
|
+
});
|
|
600
|
+
return table(headers, rows);
|
|
601
|
+
}
|
|
501
602
|
// packages/core/src/ranking/fetch.ts
|
|
502
603
|
import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
|
|
503
604
|
import path3 from "path";
|
|
@@ -735,7 +836,7 @@ var PROVIDER_LIMITS = [
|
|
|
735
836
|
limit: 100,
|
|
736
837
|
tier: "paid",
|
|
737
838
|
source: "https://docs.snowflake.com/en/sql-reference/account-usage/cortex_ai_functions_usage_history",
|
|
738
|
-
note: "
|
|
839
|
+
note: "Billed per token (Service Consumption Table). Live tokens come from ACCOUNT_USAGE.CORTEX_REST_API_USAGE_HISTORY through the SQL API with the stored JWT; the percentage needs a snowflake-cortex line in budgets.json."
|
|
739
840
|
},
|
|
740
841
|
{
|
|
741
842
|
provider: "zai",
|
|
@@ -795,6 +896,89 @@ function resolvePct(provider, sources) {
|
|
|
795
896
|
}
|
|
796
897
|
return { pct: 0, source: "none" };
|
|
797
898
|
}
|
|
899
|
+
// packages/core/src/probe.ts
|
|
900
|
+
function numericUsage(u) {
|
|
901
|
+
if (typeof u !== "object" || u === null)
|
|
902
|
+
return;
|
|
903
|
+
const out = {};
|
|
904
|
+
for (const [k, v] of Object.entries(u)) {
|
|
905
|
+
if (typeof v === "number")
|
|
906
|
+
out[k] = v;
|
|
907
|
+
}
|
|
908
|
+
return Object.keys(out).length > 0 ? out : undefined;
|
|
909
|
+
}
|
|
910
|
+
function redact(text, key) {
|
|
911
|
+
return key.length > 6 ? text.split(key).join("[redacted]") : text;
|
|
912
|
+
}
|
|
913
|
+
var chatBody = (model) => JSON.stringify({ model, messages: [{ role: "user", content: "ping" }], max_tokens: 4 });
|
|
914
|
+
async function probeChat(p, key) {
|
|
915
|
+
let res;
|
|
916
|
+
try {
|
|
917
|
+
res = await fetch(p.url, {
|
|
918
|
+
method: "POST",
|
|
919
|
+
headers: {
|
|
920
|
+
"Content-Type": "application/json",
|
|
921
|
+
Accept: "application/json",
|
|
922
|
+
Authorization: `Bearer ${key}`
|
|
923
|
+
},
|
|
924
|
+
body: p.body
|
|
925
|
+
});
|
|
926
|
+
} catch (err) {
|
|
927
|
+
return { provider: p.name, status: 0, ok: false, message: redact(err.message, key) };
|
|
928
|
+
}
|
|
929
|
+
const text = await res.text();
|
|
930
|
+
let parsed = null;
|
|
931
|
+
try {
|
|
932
|
+
parsed = JSON.parse(text);
|
|
933
|
+
} catch {
|
|
934
|
+
parsed = null;
|
|
935
|
+
}
|
|
936
|
+
const obj = parsed ?? {};
|
|
937
|
+
const err = obj.error;
|
|
938
|
+
const firstErr = Array.isArray(obj.errors) ? obj.errors[0] : undefined;
|
|
939
|
+
const message = redact(err?.message ?? err?.detail ?? firstErr?.message ?? "", key);
|
|
940
|
+
const rawRetry = res.headers.get("retry-after");
|
|
941
|
+
const headerRetry = rawRetry !== null && rawRetry !== "" ? Number(rawRetry) : Number.NaN;
|
|
942
|
+
const retry = Number.isFinite(headerRetry) ? headerRetry : err?.metadata?.retry_after_seconds;
|
|
943
|
+
const usage = p.extractUsage ? p.extractUsage(obj) : numericUsage(obj.usage);
|
|
944
|
+
return {
|
|
945
|
+
provider: p.name,
|
|
946
|
+
status: res.status,
|
|
947
|
+
ok: res.status >= 200 && res.status < 300,
|
|
948
|
+
...err?.code ?? firstErr?.code ? { code: err?.code ?? firstErr?.code } : {},
|
|
949
|
+
...message ? { message } : {},
|
|
950
|
+
...Number.isFinite(retry) ? { retryAfterSeconds: retry } : {},
|
|
951
|
+
...usage ? { usage } : {}
|
|
952
|
+
};
|
|
953
|
+
}
|
|
954
|
+
var PROBERS = {
|
|
955
|
+
zai: (key) => probeChat({ name: "zai", url: "https://api.z.ai/api/paas/v4/chat/completions", body: chatBody("glm-4.5-air") }, key),
|
|
956
|
+
nvidia: (key) => probeChat({ name: "nvidia", url: "https://integrate.api.nvidia.com/v1/chat/completions", body: chatBody("meta/llama-3.2-11b-vision-instruct") }, key),
|
|
957
|
+
orcarouter: (key) => probeChat({ name: "orcarouter", url: "https://api.orcarouter.ai/v1/chat/completions", body: chatBody("orcarouter/free") }, key),
|
|
958
|
+
"cloudflare-workers-ai": (key, meta) => {
|
|
959
|
+
const account = String(meta?.accountId ?? "");
|
|
960
|
+
if (!account) {
|
|
961
|
+
return Promise.resolve({ provider: "cloudflare-workers-ai", status: 0, ok: false, message: "metadata.accountId is required" });
|
|
962
|
+
}
|
|
963
|
+
return probeChat({
|
|
964
|
+
name: "cloudflare-workers-ai",
|
|
965
|
+
url: `https://api.cloudflare.com/client/v4/accounts/${account}/ai/run/@cf/meta/llama-3.1-8b-fast-v2`,
|
|
966
|
+
body: JSON.stringify({ prompt: "ping", max_tokens: 4 }),
|
|
967
|
+
extractUsage: (parsed) => numericUsage(typeof parsed === "object" && parsed !== null && "result" in parsed ? parsed.result.usage : undefined)
|
|
968
|
+
}, key);
|
|
969
|
+
}
|
|
970
|
+
};
|
|
971
|
+
async function runProbes() {
|
|
972
|
+
const auth = await readAuth();
|
|
973
|
+
const results = await Promise.all(Object.entries(auth).sort().map(async ([name, entry]) => {
|
|
974
|
+
const prober = PROBERS[name];
|
|
975
|
+
const key = authSecret(entry);
|
|
976
|
+
if (!prober || !key)
|
|
977
|
+
return null;
|
|
978
|
+
return prober(key, entry.metadata);
|
|
979
|
+
}));
|
|
980
|
+
return results.filter((r) => r !== null);
|
|
981
|
+
}
|
|
798
982
|
// packages/core/src/snapshot.ts
|
|
799
983
|
async function withConnectedProviders(rows) {
|
|
800
984
|
let connected;
|
|
@@ -868,6 +1052,7 @@ var USAGE = `opencode-usage \u2014 usage tracking and model ranking for opencode
|
|
|
868
1052
|
USAGE
|
|
869
1053
|
opencode-usage usage [--since 7d] [--by provider|model|day|project|agent] [--today] [--pct] [--json]
|
|
870
1054
|
opencode-usage providers [--no-net] [--json]
|
|
1055
|
+
opencode-usage probe [--json]
|
|
871
1056
|
opencode-usage top [--limit 20] [--json]
|
|
872
1057
|
|
|
873
1058
|
OPTIONS
|
|
@@ -957,6 +1142,18 @@ async function cmdProviders(flags, json) {
|
|
|
957
1142
|
db.close();
|
|
958
1143
|
}
|
|
959
1144
|
}
|
|
1145
|
+
async function cmdProbe(flags, json) {
|
|
1146
|
+
const results = await runProbes();
|
|
1147
|
+
if (json) {
|
|
1148
|
+
console.log(JSON.stringify({ probes: results }, null, 2));
|
|
1149
|
+
return;
|
|
1150
|
+
}
|
|
1151
|
+
const auth = await readAuth();
|
|
1152
|
+
const withoutProbe = Object.entries(auth).filter(([, e]) => e.key || e.access).map(([p]) => p).filter((p) => !["cloudflare-workers-ai", "nvidia", "orcarouter", "zai"].includes(p)).sort();
|
|
1153
|
+
console.log(probeTable(results));
|
|
1154
|
+
if (withoutProbe.length > 0)
|
|
1155
|
+
console.log(`no free-model probe: ${withoutProbe.join(", ")}`);
|
|
1156
|
+
}
|
|
960
1157
|
var TOP_HEADERS = ["MODEL", "NAME", "IQ", "CODING", "$/M", "IQ/$"];
|
|
961
1158
|
var TOP_WIDTHS = [28, 26, 5, 7, 8, 6];
|
|
962
1159
|
function topLine(cells) {
|
|
@@ -994,6 +1191,8 @@ async function run(argv) {
|
|
|
994
1191
|
await cmdUsage(flags, json);
|
|
995
1192
|
else if (cmd === "providers")
|
|
996
1193
|
await cmdProviders(flags, json);
|
|
1194
|
+
else if (cmd === "probe")
|
|
1195
|
+
await cmdProbe(flags, json);
|
|
997
1196
|
else if (cmd === "top")
|
|
998
1197
|
await cmdTop(flags, json);
|
|
999
1198
|
else
|
package/dist/tui.js
CHANGED
|
@@ -274,12 +274,39 @@ function parseZai(data) {
|
|
|
274
274
|
raw: data
|
|
275
275
|
};
|
|
276
276
|
}
|
|
277
|
+
function parseZaiWallet(data) {
|
|
278
|
+
const failed = data.success === false || data.code !== undefined && data.code !== 0 && data.code !== 200;
|
|
279
|
+
if (failed) {
|
|
280
|
+
return { provider: "zai", ok: false, detail: data.msg ?? "wallet endpoint error", windows: [] };
|
|
281
|
+
}
|
|
282
|
+
const balance = data.data?.availableBalance ?? data.data?.rechargeAmount ?? 0;
|
|
283
|
+
const spend = data.data?.totalSpendAmount ?? 0;
|
|
284
|
+
const windows = [
|
|
285
|
+
{ label: "balance", percentUsed: 0, detail: `$${balance.toFixed(2)}` },
|
|
286
|
+
{ label: "spend", percentUsed: 0, detail: `$${spend.toFixed(2)}` }
|
|
287
|
+
];
|
|
288
|
+
const today = data.data?.todaySpendAmount;
|
|
289
|
+
if (typeof today === "number") {
|
|
290
|
+
windows.push({ label: "today", percentUsed: 0, detail: `$${today.toFixed(2)}` });
|
|
291
|
+
}
|
|
292
|
+
return {
|
|
293
|
+
provider: "zai",
|
|
294
|
+
ok: true,
|
|
295
|
+
detail: `balance $${balance.toFixed(2)}`,
|
|
296
|
+
windows,
|
|
297
|
+
raw: data
|
|
298
|
+
};
|
|
299
|
+
}
|
|
300
|
+
var QUOTA_URL = "https://api.z.ai/api/monitor/usage/quota/limit";
|
|
301
|
+
var WALLET_URL = "https://api.z.ai/api/biz/account/query-customer-account-report";
|
|
277
302
|
async function fetchZai(key) {
|
|
278
303
|
try {
|
|
279
|
-
const
|
|
280
|
-
|
|
281
|
-
|
|
282
|
-
|
|
304
|
+
const auth = { Authorization: `Bearer ${key}`, Accept: "application/json" };
|
|
305
|
+
const data = await getJson(QUOTA_URL, auth);
|
|
306
|
+
if (data.code !== undefined && data.code !== 0) {
|
|
307
|
+
const wallet = await getJson(WALLET_URL, auth);
|
|
308
|
+
return parseZaiWallet(wallet);
|
|
309
|
+
}
|
|
283
310
|
return parseZai(data);
|
|
284
311
|
} catch (err) {
|
|
285
312
|
return { provider: "zai", ok: false, detail: err.message, windows: [] };
|
|
@@ -328,11 +355,13 @@ function parseOrcaRouter(usage, subscription) {
|
|
|
328
355
|
const cap = hard > 0 && hard < NO_CAP ? hard : soft > 0 && soft < NO_CAP ? soft : 0;
|
|
329
356
|
const capped = cap > 0;
|
|
330
357
|
const pct = capped ? spent / cap * 100 : 0;
|
|
358
|
+
const hint = capped ? "" : " (no cap set)";
|
|
359
|
+
const detail = `$${spent.toFixed(2)} spent${hint}`;
|
|
331
360
|
return {
|
|
332
361
|
provider: "orcarouter",
|
|
333
362
|
ok: true,
|
|
334
|
-
detail
|
|
335
|
-
windows: [{ label: "spend", percentUsed: pct, detail: `$${spent.toFixed(2)}` }],
|
|
363
|
+
detail,
|
|
364
|
+
windows: [{ label: "spend", percentUsed: pct, detail: `$${spent.toFixed(2)}${hint}` }],
|
|
336
365
|
budget: capped ? { percentUsed: pct, label: `$${cap}` } : undefined,
|
|
337
366
|
raw: { usage, subscription }
|
|
338
367
|
};
|
|
@@ -348,6 +377,56 @@ async function fetchOrcaRouter(key) {
|
|
|
348
377
|
}
|
|
349
378
|
}
|
|
350
379
|
|
|
380
|
+
// packages/core/src/quota/snowflake.ts
|
|
381
|
+
var SQL_API = (account) => `https://${account}.snowflakecomputing.com/api/v2/statements`;
|
|
382
|
+
var STATEMENT = `SELECT
|
|
383
|
+
SUM(CASE WHEN START_TIME >= DATE_TRUNC('day', CURRENT_TIMESTAMP())
|
|
384
|
+
AND START_TIME < DATEADD('day', 1, DATE_TRUNC('day', CURRENT_TIMESTAMP()))
|
|
385
|
+
THEN TOKENS ELSE 0 END) AS today_tokens,
|
|
386
|
+
SUM(TOKENS) AS month_tokens
|
|
387
|
+
FROM SNOWFLAKE.ACCOUNT_USAGE.CORTEX_REST_API_USAGE_HISTORY
|
|
388
|
+
WHERE START_TIME >= DATEADD('day', -30, CURRENT_TIMESTAMP())`;
|
|
389
|
+
var fmtTokens = (n) => n.toLocaleString("en-US");
|
|
390
|
+
function parseSnowflake(row) {
|
|
391
|
+
const today = Number(row?.[0] ?? 0);
|
|
392
|
+
const month = Number(row?.[1] ?? 0);
|
|
393
|
+
return {
|
|
394
|
+
provider: "snowflake-cortex",
|
|
395
|
+
ok: true,
|
|
396
|
+
detail: `${fmtTokens(month)} tokens/30d`,
|
|
397
|
+
windows: [
|
|
398
|
+
{ label: "tokens/mo", percentUsed: 0, detail: `${fmtTokens(month)}` },
|
|
399
|
+
{ label: "today", percentUsed: 0, detail: `${fmtTokens(today)}` }
|
|
400
|
+
],
|
|
401
|
+
raw: { row }
|
|
402
|
+
};
|
|
403
|
+
}
|
|
404
|
+
async function fetchSnowflake(key, metadata) {
|
|
405
|
+
const account = String(metadata?.account ?? "").toUpperCase();
|
|
406
|
+
if (!account) {
|
|
407
|
+
return { provider: "snowflake-cortex", ok: false, detail: "metadata.account is required", windows: [] };
|
|
408
|
+
}
|
|
409
|
+
try {
|
|
410
|
+
const res = await fetch(SQL_API(account), {
|
|
411
|
+
method: "POST",
|
|
412
|
+
headers: {
|
|
413
|
+
Authorization: `Bearer ${key}`,
|
|
414
|
+
"Content-Type": "application/json",
|
|
415
|
+
Accept: "application/json"
|
|
416
|
+
},
|
|
417
|
+
body: JSON.stringify({ statement: STATEMENT, timeout: 30 })
|
|
418
|
+
});
|
|
419
|
+
const body = await res.json().catch(() => null);
|
|
420
|
+
if (!res.ok) {
|
|
421
|
+
return { provider: "snowflake-cortex", ok: false, detail: body?.message ?? `HTTP ${res.status}`, windows: [] };
|
|
422
|
+
}
|
|
423
|
+
const rows = Array.isArray(body?.data) ? body.data : [];
|
|
424
|
+
return parseSnowflake(rows[0]);
|
|
425
|
+
} catch (err) {
|
|
426
|
+
return { provider: "snowflake-cortex", ok: false, detail: err.message, windows: [] };
|
|
427
|
+
}
|
|
428
|
+
}
|
|
429
|
+
|
|
351
430
|
// packages/core/src/providers.ts
|
|
352
431
|
var TTL_MS = 15 * 60 * 1000;
|
|
353
432
|
async function readCache() {
|
|
@@ -371,7 +450,8 @@ var FETCHERS = {
|
|
|
371
450
|
"github-copilot": fetchCopilot,
|
|
372
451
|
zai: fetchZai,
|
|
373
452
|
amd: fetchAmd,
|
|
374
|
-
orcarouter: fetchOrcaRouter
|
|
453
|
+
orcarouter: fetchOrcaRouter,
|
|
454
|
+
"snowflake-cortex": fetchSnowflake
|
|
375
455
|
};
|
|
376
456
|
async function providerStatuses(opts) {
|
|
377
457
|
const auth = await readAuth();
|
|
@@ -392,7 +472,7 @@ async function providerStatuses(opts) {
|
|
|
392
472
|
const secret = authSecret(auth[p]);
|
|
393
473
|
if (!fetcher || !secret)
|
|
394
474
|
return null;
|
|
395
|
-
return fetcher(secret);
|
|
475
|
+
return fetcher(secret, auth[p].metadata);
|
|
396
476
|
}));
|
|
397
477
|
for (const q of live) {
|
|
398
478
|
if (q)
|
|
@@ -500,7 +580,7 @@ var PROVIDER_LIMITS = [
|
|
|
500
580
|
limit: 100,
|
|
501
581
|
tier: "paid",
|
|
502
582
|
source: "https://docs.snowflake.com/en/sql-reference/account-usage/cortex_ai_functions_usage_history",
|
|
503
|
-
note: "
|
|
583
|
+
note: "Billed per token (Service Consumption Table). Live tokens come from ACCOUNT_USAGE.CORTEX_REST_API_USAGE_HISTORY through the SQL API with the stored JWT; the percentage needs a snowflake-cortex line in budgets.json."
|
|
504
584
|
},
|
|
505
585
|
{
|
|
506
586
|
provider: "zai",
|
|
@@ -655,7 +735,7 @@ function columnsFor(inner) {
|
|
|
655
735
|
const budget = Math.max(BUDGET_MIN, inner - used);
|
|
656
736
|
return { cols: [...base, { title: "BUDGET", width: budget, align: "left" }], bar: wide ? WIDE_BAR : COMPACT_BAR };
|
|
657
737
|
}
|
|
658
|
-
function
|
|
738
|
+
function fmtTokens2(n) {
|
|
659
739
|
if (n >= 1e6)
|
|
660
740
|
return `${(n / 1e6).toFixed(1)}M`;
|
|
661
741
|
if (n >= 1000)
|
|
@@ -821,8 +901,8 @@ function leadCells(cols, r) {
|
|
|
821
901
|
return row(cols, [
|
|
822
902
|
r.provider,
|
|
823
903
|
String(r.messages),
|
|
824
|
-
|
|
825
|
-
|
|
904
|
+
fmtTokens2(r.tokensInput),
|
|
905
|
+
fmtTokens2(r.tokensOutput),
|
|
826
906
|
`$${r.cost.toFixed(2)}`
|
|
827
907
|
]).trimEnd().padEnd(upToBudget) + " ";
|
|
828
908
|
}
|