@1930dev/opencode-usage 0.3.3 → 0.3.4

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
@@ -124,7 +124,11 @@ Normalized percentage of budget consumed per provider, from these sources (in pr
124
124
  - `opencode-go` (Zen): rolling 5h / weekly / monthly % (binding window)
125
125
  - `openrouter`: credits used / total credits
126
126
  - `orcarouter`: spend since top-up; a per-key credit cap turns into a %
127
- - `zai`: coding plan quota (needs an active GLM coding plan)
127
+ - `zai`: coding plan quota when the key has an active GLM plan; otherwise the
128
+ wallet balance (fallback)
129
+ - `snowflake-cortex`: tokens in the last 30d from the account usage views
130
+ (SQL API); the percentage comes from a `snowflake-cortex` line in
131
+ `budgets.json`
128
132
 
129
133
  2. **Documented limits** — published quotas, used when the provider reports none.
130
134
  Each one is read against the period it resets on: a daily limit against today,
@@ -142,8 +146,8 @@ Normalized percentage of budget consumed per provider, from these sources (in pr
142
146
  - `cloudflare-workers-ai`: 10k neurons/day (free tier, resets 00:00 UTC)
143
147
  - `nvidia`: credit allowance retired — the trial is rate-limited per model
144
148
  - `opencode`: no spend API for API keys
145
- - `snowflake-cortex`: 100 credits/month, billed per token
146
- - `zai`: no coding plan on this key
149
+ - `zai`: a key without a coding plan shows its wallet balance (live), and the
150
+ percentage comes from a `zai` line in `budgets.json`
147
151
 
148
152
  3. **budgets.json** — your monthly USD per provider, read against what the
149
153
  provider cost so far this calendar month:
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)
@@ -467,8 +547,8 @@ function usageTable(rows, totals, sinceLabel, groupBy, pct) {
467
547
  const row = [
468
548
  r.group,
469
549
  String(r.messages),
470
- fmtTokens(r.tokensInput),
471
- fmtTokens(r.tokensOutput),
550
+ fmtTokens2(r.tokensInput),
551
+ fmtTokens2(r.tokensOutput),
472
552
  fmtCost(r.cost)
473
553
  ];
474
554
  if (pct) {
@@ -485,7 +565,7 @@ function usageTable(rows, totals, sinceLabel, groupBy, pct) {
485
565
  [sources: ${[...pct.values()].map((p) => `${p.source}${p.label ? `(${p.label})` : ""}`).filter((v, i, a) => a.indexOf(v) === i).join(", ")}]` : "";
486
566
  return `${t}
487
567
 
488
- TOTAL since ${sinceLabel}: ${totals.messages} msgs, ${fmtTokens(totals.tokensInput)} in / ${fmtTokens(totals.tokensOutput)} out, ${fmtCost(totals.cost)} est.${sourceNote}`;
568
+ TOTAL since ${sinceLabel}: ${totals.messages} msgs, ${fmtTokens2(totals.tokensInput)} in / ${fmtTokens2(totals.tokensOutput)} out, ${fmtCost(totals.cost)} est.${sourceNote}`;
489
569
  }
490
570
  function providersTable(statuses, local) {
491
571
  const headers = ["PROVIDER", "QUOTA", "USAGE 7D (LOCAL)", "LAST USED"];
@@ -735,7 +815,7 @@ var PROVIDER_LIMITS = [
735
815
  limit: 100,
736
816
  tier: "paid",
737
817
  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."
818
+ 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
819
  },
740
820
  {
741
821
  provider: "zai",
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.4",
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": {