@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 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 (needs an active GLM coding plan)
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
- - `snowflake-cortex`: 100 credits/month, billed per token
146
- - `zai`: no coding plan on this key
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 data = await getJson("https://api.z.ai/api/monitor/usage/quota/limit", {
290
- Authorization: `Bearer ${key}`,
291
- Accept: "application/json"
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: `$${spent.toFixed(2)} spent`,
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 fmtTokens(n) {
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
- fmtTokens(r.tokensInput),
471
- fmtTokens(r.tokensOutput),
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, ${fmtTokens(totals.tokensInput)} in / ${fmtTokens(totals.tokensOutput)} out, ${fmtCost(totals.cost)} est.${sourceNote}`;
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: "Cortex AI Functions bill per token (credits per million tokens, Service Consumption Table). Usage lives in ACCOUNT_USAGE views, which the API key alone cannot read."
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 data = await getJson("https://api.z.ai/api/monitor/usage/quota/limit", {
280
- Authorization: `Bearer ${key}`,
281
- Accept: "application/json"
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: `$${spent.toFixed(2)} spent`,
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: "Cortex AI Functions bill per token (credits per million tokens, Service Consumption Table). Usage lives in ACCOUNT_USAGE views, which the API key alone cannot read."
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 fmtTokens(n) {
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
- fmtTokens(r.tokensInput),
825
- fmtTokens(r.tokensOutput),
904
+ fmtTokens2(r.tokensInput),
905
+ fmtTokens2(r.tokensOutput),
826
906
  `$${r.cost.toFixed(2)}`
827
907
  ]).trimEnd().padEnd(upToBudget) + " ";
828
908
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@1930dev/opencode-usage",
3
- "version": "0.3.3",
3
+ "version": "0.3.5",
4
4
  "description": "Usage tracking, budget percentages and model ranking for opencode — CLI plus a /usage TUI plugin",
5
5
  "type": "module",
6
6
  "bin": {