@1930dev/opencode-usage 0.1.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.
Files changed (4) hide show
  1. package/README.md +144 -0
  2. package/dist/cli.js +922 -0
  3. package/dist/tui.js +833 -0
  4. package/package.json +58 -0
package/dist/cli.js ADDED
@@ -0,0 +1,922 @@
1
+ #!/usr/bin/env bun
2
+ // @bun
3
+
4
+ // packages/core/src/types.ts
5
+ function parseDuration(input) {
6
+ const m = /^(\d+)\s*(h|d|w)$/.exec(input.trim().toLowerCase());
7
+ if (!m)
8
+ throw new Error(`invalid duration: ${input} (use Nh, Nd or Nw)`);
9
+ const n = Number(m[1]);
10
+ const ms = { h: 3600000, d: 86400000, w: 604800000 }[m[2]];
11
+ return n * ms;
12
+ }
13
+ function startOfDayMs(now = Date.now()) {
14
+ const d = new Date(now);
15
+ d.setHours(0, 0, 0, 0);
16
+ return d.getTime();
17
+ }
18
+ function startOfMonthMs(now = Date.now()) {
19
+ const d = new Date(now);
20
+ d.setDate(1);
21
+ d.setHours(0, 0, 0, 0);
22
+ return d.getTime();
23
+ }
24
+ // packages/core/src/db.ts
25
+ import { Database } from "bun:sqlite";
26
+
27
+ // packages/core/src/config.ts
28
+ import path from "path";
29
+ function home() {
30
+ const h = process.env.HOME;
31
+ if (!h)
32
+ throw new Error("HOME is not set");
33
+ return h;
34
+ }
35
+ function dbPath() {
36
+ return process.env.OPENCODE_DB_PATH ?? path.join(home(), ".local/share/opencode/opencode.db");
37
+ }
38
+ function authPath() {
39
+ return process.env.OPENCODE_AUTH_PATH ?? path.join(home(), ".local/share/opencode/auth.json");
40
+ }
41
+ function cacheDir() {
42
+ return process.env.OPENCODE_USAGE_CACHE ?? path.join(home(), ".cache/opencode-usage");
43
+ }
44
+ function budgetsPath() {
45
+ return process.env.OPENCODE_USAGE_BUDGETS ?? path.join(home(), ".config/opencode-usage/budgets.json");
46
+ }
47
+
48
+ // packages/core/src/db.ts
49
+ function openDb() {
50
+ const db = new Database(dbPath(), { readonly: true });
51
+ db.exec("PRAGMA query_only = true");
52
+ return db;
53
+ }
54
+ var COST = "CAST(json_extract(data,'$.cost') AS REAL)";
55
+ var T_IN = "CAST(json_extract(data,'$.tokens.input') AS INTEGER)";
56
+ var T_OUT = "CAST(json_extract(data,'$.tokens.output') AS INTEGER)";
57
+ var T_REA = "CAST(json_extract(data,'$.tokens.reasoning') AS INTEGER)";
58
+ var T_CR = "CAST(json_extract(data,'$.tokens.cache.read') AS INTEGER)";
59
+ var T_CW = "CAST(json_extract(data,'$.tokens.cache.write') AS INTEGER)";
60
+ var CREATED = "CAST(json_extract(data,'$.time.created') AS INTEGER)";
61
+ var GROUPS = {
62
+ provider: "json_extract(data,'$.providerID')",
63
+ model: "json_extract(data,'$.providerID') || '/' || json_extract(data,'$.modelID')",
64
+ day: "strftime('%Y-%m-%d', (${" + CREATED + "})/1000, 'unixepoch')",
65
+ project: "json_extract(data,'$.path.cwd')",
66
+ agent: "json_extract(data,'$.agent')"
67
+ };
68
+ function usageSince(db, sinceMs, groupBy = "provider") {
69
+ const g = GROUPS[groupBy];
70
+ const rows = db.query(`SELECT ${g} AS grp,
71
+ COALESCE(json_extract(data,'$.providerID'),'?') AS provider,
72
+ COALESCE(json_extract(data,'$.modelID'),'?') AS model,
73
+ COUNT(*) AS messages,
74
+ COALESCE(SUM(${COST}),0) AS cost,
75
+ COALESCE(SUM(${T_IN}),0) AS tokensInput,
76
+ COALESCE(SUM(${T_OUT}),0) AS tokensOutput,
77
+ COALESCE(SUM(${T_REA}),0) AS tokensReasoning,
78
+ COALESCE(SUM(${T_CR}),0) AS tokensCacheRead,
79
+ COALESCE(SUM(${T_CW}),0) AS tokensCacheWrite
80
+ FROM message
81
+ WHERE json_extract(data,'$.role')='assistant'
82
+ AND ${CREATED} >= ?
83
+ GROUP BY grp
84
+ ORDER BY cost DESC`).all(sinceMs);
85
+ return rows.map((r) => ({
86
+ group: String(r.grp),
87
+ provider: String(r.provider),
88
+ model: String(r.model),
89
+ day: "",
90
+ project: "",
91
+ agent: "",
92
+ messages: Number(r.messages),
93
+ cost: Number(r.cost),
94
+ tokensInput: Number(r.tokensInput),
95
+ tokensOutput: Number(r.tokensOutput),
96
+ tokensReasoning: Number(r.tokensReasoning),
97
+ tokensCacheRead: Number(r.tokensCacheRead),
98
+ tokensCacheWrite: Number(r.tokensCacheWrite)
99
+ }));
100
+ }
101
+ function usageTotals(db, sinceMs) {
102
+ const r = db.query(`SELECT COUNT(*) AS messages,
103
+ COALESCE(SUM(${COST}),0) AS cost,
104
+ COALESCE(SUM(${T_IN}),0) AS tokensInput,
105
+ COALESCE(SUM(${T_OUT}),0) AS tokensOutput
106
+ FROM message
107
+ WHERE json_extract(data,'$.role')='assistant' AND ${CREATED} >= ?`).get(sinceMs);
108
+ return {
109
+ messages: Number(r?.messages ?? 0),
110
+ cost: Number(r?.cost ?? 0),
111
+ tokensInput: Number(r?.tokensInput ?? 0),
112
+ tokensOutput: Number(r?.tokensOutput ?? 0)
113
+ };
114
+ }
115
+ function localUsageByProvider(db, sinceMs) {
116
+ const rows = db.query(`SELECT COALESCE(json_extract(data,'$.providerID'),'?') AS provider,
117
+ COUNT(*) AS messages,
118
+ COALESCE(SUM(${COST}),0) AS cost,
119
+ COALESCE(SUM(${T_IN} + ${T_OUT}),0) AS tokens,
120
+ MAX(${CREATED}) AS lastUsedMs
121
+ FROM message
122
+ WHERE json_extract(data,'$.role')='assistant'
123
+ AND ${CREATED} >= ?
124
+ GROUP BY provider`).all(sinceMs);
125
+ const map = new Map;
126
+ for (const r of rows) {
127
+ map.set(String(r.provider), {
128
+ provider: String(r.provider),
129
+ messages: Number(r.messages),
130
+ cost: Number(r.cost),
131
+ tokens: Number(r.tokens),
132
+ lastUsedMs: Number(r.lastUsedMs)
133
+ });
134
+ }
135
+ return map;
136
+ }
137
+ function monthlyUsageByProvider(db, startOfMonthMs2) {
138
+ const rows = db.query(`SELECT COALESCE(json_extract(data,'$.providerID'),'?') AS provider,
139
+ COALESCE(SUM(CAST(json_extract(data,'$.cost') AS REAL)),0) AS cost
140
+ FROM message
141
+ WHERE json_extract(data,'$.role')='assistant'
142
+ AND CAST(json_extract(data,'$.time.created') AS INTEGER) >= ?
143
+ GROUP BY provider`).all(startOfMonthMs2);
144
+ const map = new Map;
145
+ for (const r of rows) {
146
+ map.set(String(r.provider), Number(r.cost));
147
+ }
148
+ return map;
149
+ }
150
+ // packages/core/src/providers.ts
151
+ import { mkdir, readFile as readFile2, writeFile } from "fs/promises";
152
+ import path2 from "path";
153
+
154
+ // packages/core/src/auth.ts
155
+ import { readFile } from "fs/promises";
156
+ async function readAuth() {
157
+ try {
158
+ return JSON.parse(await readFile(authPath(), "utf8"));
159
+ } catch (err) {
160
+ throw new Error(`cannot read opencode auth store: ${err.message}`);
161
+ }
162
+ }
163
+ function authSecret(entry) {
164
+ return entry.key ?? entry.access;
165
+ }
166
+
167
+ // packages/core/src/quota/shared.ts
168
+ var BROWSER_UA = "Mozilla/5.0 (Macintosh; Intel Mac OS X 10_15_7) AppleWebKit/537.36";
169
+ async function getJson(url, headers) {
170
+ const res = await fetch(url, { headers: { "User-Agent": BROWSER_UA, ...headers }, signal: AbortSignal.timeout(15000) });
171
+ if (!res.ok)
172
+ throw new Error(`HTTP ${res.status} from ${url}`);
173
+ return res.json();
174
+ }
175
+
176
+ // packages/core/src/quota/zen.ts
177
+ async function fetchZen(key) {
178
+ try {
179
+ const data = await getJson("https://opencode.ai/zen/go/v1/usage", {
180
+ Authorization: `Bearer ${key}`
181
+ });
182
+ const u = data.usage;
183
+ const windows = [
184
+ { label: "5h", percentUsed: u.rolling.percent, resetsAt: u.rolling.resetsAt, status: u.rolling.status },
185
+ { label: "weekly", percentUsed: u.weekly.percent, resetsAt: u.weekly.resetsAt, status: u.weekly.status },
186
+ { label: "monthly", percentUsed: u.monthly.percent, resetsAt: u.monthly.resetsAt, status: u.monthly.status }
187
+ ];
188
+ const binding = windows.reduce((a, b) => b.percentUsed > a.percentUsed ? b : a);
189
+ return {
190
+ provider: "opencode-go",
191
+ ok: true,
192
+ windows,
193
+ budget: { percentUsed: binding.percentUsed, label: binding.label },
194
+ raw: data
195
+ };
196
+ } catch (err) {
197
+ return { provider: "opencode-go", ok: false, detail: err.message, windows: [] };
198
+ }
199
+ }
200
+
201
+ // packages/core/src/quota/openrouter.ts
202
+ async function fetchOpenRouter(key) {
203
+ try {
204
+ const credits = await getJson("https://openrouter.ai/api/v1/credits", {
205
+ Authorization: `Bearer ${key}`
206
+ });
207
+ let keyInfo;
208
+ try {
209
+ keyInfo = (await getJson("https://openrouter.ai/api/v1/key", { Authorization: `Bearer ${key}` })).data;
210
+ } catch {
211
+ keyInfo = undefined;
212
+ }
213
+ const totalCredits = credits.data.total_credits;
214
+ const totalUsage = credits.data.total_usage;
215
+ const balance = totalCredits - totalUsage;
216
+ const pct = totalCredits > 0 ? totalUsage / totalCredits * 100 : 0;
217
+ return {
218
+ provider: "openrouter",
219
+ ok: true,
220
+ detail: `balance $${balance.toFixed(2)} of $${totalCredits.toFixed(2)}`,
221
+ windows: keyInfo ? [
222
+ { label: "daily", percentUsed: 0, detail: `$${keyInfo.usage_daily.toFixed(4)}` },
223
+ { label: "weekly", percentUsed: 0, detail: `$${keyInfo.usage_weekly.toFixed(4)}` },
224
+ { label: "monthly", percentUsed: 0, detail: `$${keyInfo.usage_monthly.toFixed(4)}` }
225
+ ] : [],
226
+ budget: totalCredits > 0 ? { percentUsed: pct, label: "credits" } : undefined,
227
+ raw: { credits: credits.data, key: keyInfo }
228
+ };
229
+ } catch (err) {
230
+ return { provider: "openrouter", ok: false, detail: err.message, windows: [] };
231
+ }
232
+ }
233
+
234
+ // packages/core/src/quota/copilot.ts
235
+ function premiumBudget(s) {
236
+ if (s.unlimited && s.entitlement === 0)
237
+ return;
238
+ const remaining = s.remaining ?? s.quota_remaining;
239
+ if (remaining === undefined)
240
+ return;
241
+ const entitlement = s.entitlement;
242
+ if (entitlement <= 0)
243
+ return;
244
+ const used = (entitlement - remaining) / entitlement * 100;
245
+ return { percentUsed: used, label: "premium/mo" };
246
+ }
247
+ async function fetchCopilot(token) {
248
+ try {
249
+ const data = await getJson("https://api.github.com/copilot_internal/user", {
250
+ Authorization: `token ${token}`,
251
+ Accept: "application/json",
252
+ "Editor-Version": "vscode/1.96.2",
253
+ "Editor-Plugin-Version": "copilot-chat/0.26.7",
254
+ "X-Github-Api-Version": "2025-04-01"
255
+ });
256
+ const snaps = data.quota_snapshots ?? {};
257
+ const interesting = ["premium_interactions", "completions", "chat", "agent", "preview_features"];
258
+ const windows = Object.entries(snaps).filter(([id]) => interesting.includes(id)).map(([id, s]) => ({
259
+ label: id.replace(/_/g, " "),
260
+ percentUsed: s.unlimited && s.entitlement === 0 ? 0 : Math.round(100 - s.percent_remaining),
261
+ resetsAt: data.quota_reset_date,
262
+ detail: s.unlimited ? "unlimited" : `${s.quota_remaining} left of ${s.entitlement}`
263
+ }));
264
+ const budget = premiumBudget(snaps.premium_interactions) ?? premiumBudget(snaps.agent) ?? undefined;
265
+ return {
266
+ provider: "github-copilot",
267
+ ok: true,
268
+ detail: `plan ${data.copilot_plan}`,
269
+ windows,
270
+ budget,
271
+ raw: data
272
+ };
273
+ } catch (err) {
274
+ return { provider: "github-copilot", ok: false, detail: err.message, windows: [] };
275
+ }
276
+ }
277
+
278
+ // packages/core/src/quota/zai.ts
279
+ function parseZai(data) {
280
+ if (data.success === false || data.code !== undefined && data.code !== 0) {
281
+ return { provider: "zai", ok: false, detail: data.msg ?? "quota endpoint error", windows: [] };
282
+ }
283
+ const limits = data.data?.limits ?? [];
284
+ const budget = limits.length > 0 ? { percentUsed: limits[0].percentage ?? 0, label: limits[0].name ?? limits[0].limitType ?? "limit" } : undefined;
285
+ return {
286
+ provider: "zai",
287
+ ok: true,
288
+ detail: data.data?.planName,
289
+ windows: limits.map((l) => ({
290
+ label: l.name ?? l.limitType ?? "limit",
291
+ percentUsed: l.percentage ?? 0,
292
+ resetsAt: l.nextResetTime ? new Date(l.nextResetTime).toISOString() : undefined
293
+ })),
294
+ budget,
295
+ raw: data
296
+ };
297
+ }
298
+ async function fetchZai(key) {
299
+ try {
300
+ const data = await getJson("https://api.z.ai/api/monitor/usage/quota/limit", {
301
+ Authorization: `Bearer ${key}`,
302
+ Accept: "application/json"
303
+ });
304
+ return parseZai(data);
305
+ } catch (err) {
306
+ return { provider: "zai", ok: false, detail: err.message, windows: [] };
307
+ }
308
+ }
309
+
310
+ // packages/core/src/providers.ts
311
+ var TTL_MS = 15 * 60 * 1000;
312
+ async function readCache() {
313
+ try {
314
+ const raw = JSON.parse(await readFile2(path2.join(cacheDir(), "quota.json"), "utf8"));
315
+ if (Date.now() - raw.fetchedAt > TTL_MS)
316
+ return null;
317
+ return raw;
318
+ } catch {
319
+ return null;
320
+ }
321
+ }
322
+ async function writeCache(quotas) {
323
+ await mkdir(cacheDir(), { recursive: true });
324
+ const file = { fetchedAt: Date.now(), quotas };
325
+ await writeFile(path2.join(cacheDir(), "quota.json"), JSON.stringify(file));
326
+ }
327
+ var FETCHERS = {
328
+ "opencode-go": fetchZen,
329
+ openrouter: fetchOpenRouter,
330
+ "github-copilot": fetchCopilot,
331
+ zai: fetchZai
332
+ };
333
+ async function providerStatuses(opts) {
334
+ const auth = await readAuth();
335
+ const providers = Object.keys(auth).sort();
336
+ let cached = {};
337
+ let quotaSource = "none";
338
+ if (opts.noNet) {
339
+ const c = await readCache();
340
+ if (c) {
341
+ cached = c.quotas;
342
+ quotaSource = "cache";
343
+ }
344
+ } else {
345
+ const c = await readCache();
346
+ const fresh = {};
347
+ const live = await Promise.all(providers.map(async (p) => {
348
+ const fetcher = FETCHERS[p];
349
+ const secret = authSecret(auth[p]);
350
+ if (!fetcher || !secret)
351
+ return null;
352
+ return fetcher(secret);
353
+ }));
354
+ for (const q of live) {
355
+ if (q)
356
+ fresh[q.provider] = q;
357
+ }
358
+ if (Object.keys(fresh).length > 0) {
359
+ await writeCache(fresh);
360
+ cached = fresh;
361
+ quotaSource = "live";
362
+ } else if (c) {
363
+ cached = c.quotas;
364
+ quotaSource = "cache";
365
+ }
366
+ }
367
+ return providers.map((p) => ({
368
+ provider: p,
369
+ auth: true,
370
+ quota: cached[p] ?? null,
371
+ quotaSource: cached[p] ? quotaSource : "none"
372
+ }));
373
+ }
374
+ // packages/core/src/report.ts
375
+ function fmtTokens(n) {
376
+ if (n >= 1e9)
377
+ return `${(n / 1e9).toFixed(1)}B`;
378
+ if (n >= 1e6)
379
+ return `${(n / 1e6).toFixed(1)}M`;
380
+ if (n >= 1000)
381
+ return `${(n / 1000).toFixed(1)}K`;
382
+ return String(n);
383
+ }
384
+ function fmtCost(n) {
385
+ return `$${n.toFixed(2)}`;
386
+ }
387
+ function fmtAgo(ms, now = Date.now()) {
388
+ const diff = now - ms;
389
+ const h = Math.floor(diff / 3600000);
390
+ if (h < 1)
391
+ return `${Math.max(1, Math.floor(diff / 60000))}m ago`;
392
+ if (h < 24)
393
+ return `${h}h ago`;
394
+ return `${Math.floor(h / 24)}d ago`;
395
+ }
396
+ function pad(s, n) {
397
+ return s.length >= n ? s : s + " ".repeat(n - s.length);
398
+ }
399
+ function table(headers, rows) {
400
+ const widths = headers.map((h, i) => Math.max(h.length, ...rows.map((r) => r[i].length)));
401
+ const line = (cells) => cells.map((c, i) => pad(c, widths[i])).join(" ");
402
+ const head = line(headers);
403
+ const sep = widths.map((w) => "-".repeat(w)).join(" ");
404
+ const body = rows.map(line).join(`
405
+ `);
406
+ return [head, sep, body].join(`
407
+ `);
408
+ }
409
+ function usageTable(rows, totals, sinceLabel, groupBy, pct) {
410
+ const headers = [groupBy.toUpperCase(), "MSGS", "TOK IN", "TOK OUT", "EST COST"];
411
+ if (pct)
412
+ headers.push("% BUDGET");
413
+ const body = rows.slice(0, 30).map((r) => {
414
+ const row = [
415
+ r.group,
416
+ String(r.messages),
417
+ fmtTokens(r.tokensInput),
418
+ fmtTokens(r.tokensOutput),
419
+ fmtCost(r.cost)
420
+ ];
421
+ if (pct) {
422
+ const p = pct.get(r.provider);
423
+ if (p)
424
+ row.push(`${p.pct.toFixed(1)}%${p.label ? ` ${p.label}` : ""}`);
425
+ else
426
+ row.push("\u2014");
427
+ }
428
+ return row;
429
+ });
430
+ const t = table(headers, body);
431
+ const sourceNote = pct ? `
432
+ [sources: ${[...pct.values()].map((p) => `${p.source}${p.label ? `(${p.label})` : ""}`).filter((v, i, a) => a.indexOf(v) === i).join(", ")}]` : "";
433
+ return `${t}
434
+
435
+ TOTAL since ${sinceLabel}: ${totals.messages} msgs, ${fmtTokens(totals.tokensInput)} in / ${fmtTokens(totals.tokensOutput)} out, ${fmtCost(totals.cost)} est.${sourceNote}`;
436
+ }
437
+ function providersTable(statuses, local) {
438
+ const headers = ["PROVIDER", "QUOTA", "USAGE 7D (LOCAL)", "LAST USED"];
439
+ const rows = statuses.map((s) => {
440
+ const quota = s.quota ? s.quota.ok ? s.quota.windows.map((w) => w.detail ? `${w.label} ${w.detail}` : `${w.label} ${w.percentUsed}%`).join(", ") || s.quota.detail || "ok" : `unavailable (${s.quota.detail ?? "error"})` : "solo-local";
441
+ const loc = local.get(s.provider);
442
+ const usage = loc ? `${loc.messages} msgs, ${fmtCost(loc.cost)}` : "\u2014";
443
+ const last = loc ? fmtAgo(loc.lastUsedMs) : "never";
444
+ return [s.provider, quota, usage, last];
445
+ });
446
+ return table(headers, rows);
447
+ }
448
+ // packages/core/src/ranking/fetch.ts
449
+ import { mkdir as mkdir2, readFile as readFile3, writeFile as writeFile2 } from "fs/promises";
450
+ import path3 from "path";
451
+ var {$ } = globalThis.Bun;
452
+ var TTL_MS2 = 24 * 60 * 60 * 1000;
453
+ async function readCache2(file) {
454
+ try {
455
+ const raw = JSON.parse(await readFile3(path3.join(cacheDir(), file), "utf8"));
456
+ if (Date.now() - raw.fetchedAt > TTL_MS2)
457
+ return null;
458
+ return raw.data;
459
+ } catch {
460
+ return null;
461
+ }
462
+ }
463
+ async function writeCache2(file, data) {
464
+ await mkdir2(cacheDir(), { recursive: true });
465
+ await writeFile2(path3.join(cacheDir(), file), JSON.stringify({ fetchedAt: Date.now(), data }));
466
+ }
467
+ async function fetchModelsDev() {
468
+ const cached = await readCache2("models-dev.json");
469
+ if (cached)
470
+ return cached;
471
+ const res = await fetch("https://models.dev/api.json", { signal: AbortSignal.timeout(30000) });
472
+ if (!res.ok)
473
+ throw new Error(`models.dev fetch failed: HTTP ${res.status}`);
474
+ const data = await res.json();
475
+ const flat = {};
476
+ for (const provider of Object.values(data)) {
477
+ for (const [id, model] of Object.entries(provider.models)) {
478
+ flat[id] = model;
479
+ }
480
+ }
481
+ await writeCache2("models-dev.json", flat);
482
+ return flat;
483
+ }
484
+ async function aaApiKey() {
485
+ if (process.env.AA_API_KEY)
486
+ return process.env.AA_API_KEY;
487
+ const projectId = process.env.INFISICAL_PROJECT_ID;
488
+ if (!projectId)
489
+ return;
490
+ try {
491
+ const out = await $`infisical-secret get AA_API_KEY --projectId ${projectId} --env prod --plain --silent`.text();
492
+ return out.trim() || undefined;
493
+ } catch {
494
+ return;
495
+ }
496
+ }
497
+ async function fetchAA() {
498
+ const cached = await readCache2("aa.json");
499
+ if (cached)
500
+ return cached;
501
+ const key = await aaApiKey();
502
+ if (!key)
503
+ return [];
504
+ const res = await fetch("https://artificialanalysis.ai/api/v2/data/llms/models", {
505
+ headers: { "x-api-key": key },
506
+ signal: AbortSignal.timeout(30000)
507
+ });
508
+ if (!res.ok)
509
+ throw new Error(`AA fetch failed: HTTP ${res.status}`);
510
+ const data = await res.json();
511
+ await writeCache2("aa.json", data.data);
512
+ return data.data;
513
+ }
514
+
515
+ // packages/core/src/ranking/matcher.ts
516
+ var ALIASES = {
517
+ "gpt-5": "gpt-5",
518
+ "gpt-5-codex": "gpt-5-codex",
519
+ "claude-opus-4-5": "claude-opus-5",
520
+ "claude-sonnet-4-5": "claude-sonnet-5",
521
+ "claude-sonnet-4": "claude-sonnet-4",
522
+ "claude-3-5-sonnet": "claude-35-sonnet",
523
+ "gemini-2-5-pro": "gemini-2-5-pro",
524
+ "gemini-2-5-flash": "gemini-2-5-flash",
525
+ "deepseek-v3": "deepseek-v3",
526
+ "deepseek-r1": "deepseek-r1",
527
+ "qwen-2-5-coder": "qwen2-5-coder",
528
+ "qwen-3": "qwen3",
529
+ "llama-3-1-405b": "llama-3-1-405b",
530
+ "llama-3-1-70b": "llama-3-1-70b",
531
+ "llama-3-1-8b": "llama-3-1-8b",
532
+ "minimax-m2-1": "minimax-m2-1",
533
+ "command-r-plus": "command-r-plus",
534
+ "command-r": "command-r"
535
+ };
536
+ function normalize(name) {
537
+ return name.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-|-$/g, "").replace(/-(202[0-9]{5})$/, "");
538
+ }
539
+ function normalizeModelsDevId(id) {
540
+ const afterProvider = id.includes("/") ? id.split("/").pop() ?? id : id;
541
+ return normalize(afterProvider);
542
+ }
543
+ function buildMatcher(aaModels, modelsDev) {
544
+ const aaByNorm = new Map;
545
+ for (const m of aaModels) {
546
+ const key = normalize(m.slug ?? m.id ?? m.name);
547
+ if (!aaByNorm.has(key))
548
+ aaByNorm.set(key, m);
549
+ const alias = ALIASES[key];
550
+ if (alias && !aaByNorm.has(alias))
551
+ aaByNorm.set(alias, m);
552
+ }
553
+ for (const [alias, target] of Object.entries(ALIASES)) {
554
+ const m = aaModels.find((x) => normalize(x.slug ?? x.id ?? x.name) === target);
555
+ if (m)
556
+ aaByNorm.set(alias, m);
557
+ }
558
+ return (md) => {
559
+ const key = normalizeModelsDevId(md.id);
560
+ if (aaByNorm.has(key))
561
+ return aaByNorm.get(key);
562
+ const alias = ALIASES[key];
563
+ if (alias && aaByNorm.has(alias))
564
+ return aaByNorm.get(alias);
565
+ return;
566
+ };
567
+ }
568
+
569
+ // packages/core/src/ranking/metrics.ts
570
+ function getIntelligenceIndex(model) {
571
+ return model.evaluations?.artificial_analysis_intelligence_index ?? undefined;
572
+ }
573
+ function blendedPrice(cost) {
574
+ const input = cost.input ?? 0;
575
+ const output = cost.output ?? 0;
576
+ return (3 * input + 1 * output) / 4;
577
+ }
578
+ function valueScore(intelligenceIndex, cost) {
579
+ if (intelligenceIndex === undefined || intelligenceIndex <= 0)
580
+ return;
581
+ const bp = blendedPrice(cost);
582
+ if (bp <= 0)
583
+ return;
584
+ return intelligenceIndex / bp;
585
+ }
586
+
587
+ // packages/core/src/top.ts
588
+ async function buildTop(limit) {
589
+ const [modelsDev, aaModels] = await Promise.all([fetchModelsDev(), fetchAA()]);
590
+ const match = buildMatcher(aaModels, modelsDev);
591
+ const seen = new Set;
592
+ const rows = [];
593
+ for (const [id, md] of Object.entries(modelsDev)) {
594
+ if (!md.cost || md.cost.input === 0)
595
+ continue;
596
+ const modelId = id.includes("/") ? id.split("/").pop() : id;
597
+ const key = `${modelId}`;
598
+ if (seen.has(key))
599
+ continue;
600
+ const aa = match(md);
601
+ const ii = aa ? getIntelligenceIndex(aa) : undefined;
602
+ const coding = aa?.evaluations?.artificial_analysis_coding_index ?? undefined;
603
+ const bp = blendedPrice(md.cost);
604
+ const vs = valueScore(ii, md.cost);
605
+ seen.add(key);
606
+ rows.push({ model: modelId, name: md.name ?? modelId, intelligence: ii, codingIndex: coding, blended: bp, value: vs });
607
+ }
608
+ rows.sort((a, b) => (b.value ?? -1) - (a.value ?? -1));
609
+ return rows.slice(0, limit);
610
+ }
611
+ // packages/core/src/budget.ts
612
+ import { readFile as readFile4 } from "fs/promises";
613
+ async function readBudgets() {
614
+ try {
615
+ return JSON.parse(await readFile4(budgetsPath(), "utf8"));
616
+ } catch {
617
+ return {};
618
+ }
619
+ }
620
+ function pctFromLimit(provider, limit, usageTokens, usageRequests, isMonthly) {
621
+ if (limit.limit <= 0)
622
+ return;
623
+ const effectiveLimit = limit.metric.endsWith("/day") && limit.metric !== "neurons/day" ? limit.limit * 30 : limit.limit;
624
+ const metric = limit.metric.replace("/day", "").replace("/month", "");
625
+ switch (metric) {
626
+ case "tokens":
627
+ if (effectiveLimit > 0)
628
+ return usageTokens / effectiveLimit * 100;
629
+ return;
630
+ case "requests":
631
+ if (effectiveLimit > 0)
632
+ return usageRequests / effectiveLimit * 100;
633
+ return;
634
+ case "credits":
635
+ case "neurons":
636
+ return;
637
+ default:
638
+ return;
639
+ }
640
+ }
641
+ function resolvePct(provider, live, budgets, monthCost, usageTokens, usageRequests, limits) {
642
+ const liveQ = live.get(provider);
643
+ if (liveQ?.budget && liveQ.ok) {
644
+ return { pct: liveQ.budget.percentUsed, source: "live", label: liveQ.budget.label };
645
+ }
646
+ if (budgets[provider] !== undefined) {
647
+ const budget = budgets[provider];
648
+ if (budget <= 0)
649
+ return { pct: 0, source: "budgets" };
650
+ return { pct: monthCost / budget * 100, source: "budgets" };
651
+ }
652
+ const limit = limits.get(provider);
653
+ if (limit) {
654
+ const pct = pctFromLimit(provider, limit, usageTokens, usageRequests, true);
655
+ if (pct !== undefined) {
656
+ return { pct, source: "limits", label: `${limit.limit.toLocaleString()} ${limit.unit}/${limit.metric.split("/")[1]}` };
657
+ }
658
+ }
659
+ return { pct: 0, source: "none" };
660
+ }
661
+ // packages/core/src/limits.ts
662
+ var PROVIDER_LIMITS = [
663
+ {
664
+ provider: "groq",
665
+ metric: "tokens/day",
666
+ limit: 200000,
667
+ unit: "tokens",
668
+ tier: "free",
669
+ source: "https://console.groq.com/docs/rate-limits",
670
+ note: "Also 30k tokens/minute. Paid tiers higher."
671
+ },
672
+ {
673
+ provider: "google",
674
+ metric: "requests/day",
675
+ limit: 1500,
676
+ unit: "requests",
677
+ tier: "free",
678
+ source: "https://ai.google.dev/gemini-api/docs/rate-limits",
679
+ note: "Free tier: 1500 RPM, 1500 RPD. Paid tiers higher."
680
+ },
681
+ {
682
+ provider: "cloudflare-workers-ai",
683
+ metric: "neurons/day",
684
+ limit: 1e5,
685
+ unit: "neurons",
686
+ tier: "free",
687
+ source: "https://developers.cloudflare.com/workers-ai/platform/limits/",
688
+ note: "100k neurons/day free. Workers AI paid plans higher."
689
+ },
690
+ {
691
+ provider: "nvidia",
692
+ metric: "credits/month",
693
+ limit: 1000,
694
+ unit: "credits",
695
+ tier: "free",
696
+ source: "https://build.nvidia.com/",
697
+ note: "NIM free tier ~1000 credits/month. Varies by model."
698
+ },
699
+ {
700
+ provider: "digitalocean",
701
+ metric: "tokens/day",
702
+ limit: 5000000,
703
+ unit: "tokens",
704
+ tier: "paid",
705
+ source: "https://docs.digitalocean.com/products/genai/",
706
+ note: "GenAI platform paid plans. Exact limits per model."
707
+ },
708
+ {
709
+ provider: "snowflake-cortex",
710
+ metric: "credits/month",
711
+ limit: 100,
712
+ unit: "credits",
713
+ tier: "paid",
714
+ source: "https://docs.snowflake.com/en/user-guide/snowflake-cortex",
715
+ note: "Cortex functions consume credits. Budget per warehouse."
716
+ },
717
+ {
718
+ provider: "cerebras",
719
+ metric: "tokens/day",
720
+ limit: 1e6,
721
+ unit: "tokens",
722
+ tier: "free",
723
+ source: "https://cerebras.ai/",
724
+ note: "Free tier estimate. Paid tiers much higher."
725
+ },
726
+ {
727
+ provider: "orcarouter",
728
+ metric: "tokens/day",
729
+ limit: 0,
730
+ unit: "tokens",
731
+ tier: "unknown",
732
+ source: "unknown",
733
+ note: "Proxy service. No public limits documented."
734
+ }
735
+ ];
736
+ function limitsAsMap() {
737
+ const m = new Map;
738
+ for (const l of PROVIDER_LIMITS)
739
+ m.set(l.provider, l);
740
+ return m;
741
+ }
742
+ // packages/core/src/snapshot.ts
743
+ async function withConnectedProviders(rows) {
744
+ let connected;
745
+ try {
746
+ connected = Object.keys(await readAuth());
747
+ } catch {
748
+ return rows;
749
+ }
750
+ const seen = new Set(rows.map((r) => r.provider));
751
+ const idle = connected.filter((provider) => !seen.has(provider)).sort().map((provider) => ({
752
+ group: provider,
753
+ provider,
754
+ model: "",
755
+ day: "",
756
+ project: "",
757
+ agent: "",
758
+ messages: 0,
759
+ cost: 0,
760
+ tokensInput: 0,
761
+ tokensOutput: 0,
762
+ tokensReasoning: 0,
763
+ tokensCacheRead: 0,
764
+ tokensCacheWrite: 0
765
+ }));
766
+ return [...rows, ...idle].sort((a, b) => b.cost - a.cost || b.messages - a.messages || a.provider.localeCompare(b.provider));
767
+ }
768
+ async function getUsageSnapshot(sinceMs, groupBy, includePct) {
769
+ const db = openDb();
770
+ try {
771
+ const rows = groupBy === "provider" ? await withConnectedProviders(usageSince(db, sinceMs, groupBy)) : usageSince(db, sinceMs, groupBy);
772
+ const totals = usageTotals(db, sinceMs);
773
+ let pct;
774
+ if (includePct) {
775
+ const budgets = await readBudgets();
776
+ const live = await providerStatuses({ noNet: false });
777
+ const liveMap = new Map;
778
+ for (const s of live) {
779
+ if (s.quota)
780
+ liveMap.set(s.provider, s.quota);
781
+ }
782
+ const monthCosts = monthlyUsageByProvider(db, startOfMonthMs());
783
+ const limits = limitsAsMap();
784
+ const pctMap = new Map;
785
+ for (const r of rows) {
786
+ const p = resolvePct(r.provider, liveMap, budgets, monthCosts.get(r.provider) ?? 0, r.tokensInput + r.tokensOutput, r.messages, limits);
787
+ if (p.pct > 0 || p.source !== "none")
788
+ pctMap.set(r.provider, p);
789
+ }
790
+ pct = pctMap;
791
+ }
792
+ return { rows, totals, pct: pct ? Object.fromEntries(pct) : undefined };
793
+ } finally {
794
+ db.close();
795
+ }
796
+ }
797
+ // packages/cli/src/cli.ts
798
+ var USAGE = `opencode-usage \u2014 usage tracking and model ranking for opencode
799
+
800
+ USAGE
801
+ opencode-usage usage [--since 7d] [--by provider|model|day|project|agent] [--today] [--pct] [--json]
802
+ opencode-usage providers [--no-net] [--json]
803
+ opencode-usage top [--limit 20] [--json]
804
+
805
+ OPTIONS
806
+ --json machine-readable output
807
+ --no-net skip live quota fetches (cache only)
808
+ --today shortcut for --since with start of today
809
+ --since lookback window: Nh, Nd or Nw (default 7d)
810
+ --pct add % BUDGET column (requires --by provider)
811
+ `;
812
+ function fail(msg) {
813
+ console.error(`opencode-usage: ${msg}
814
+ `);
815
+ process.exit(1);
816
+ throw new Error("unreachable");
817
+ }
818
+ function parseArgs(argv) {
819
+ const cmd = argv[0];
820
+ if (!cmd || cmd.startsWith("-"))
821
+ fail(`missing command
822
+
823
+ ${USAGE}`);
824
+ const flags = new Map;
825
+ for (let i = 1;i < argv.length; i++) {
826
+ const a = argv[i];
827
+ if (!a.startsWith("--"))
828
+ fail(`unexpected argument: ${a}`);
829
+ const [key, val] = a.slice(2).split("=");
830
+ const name = `--${key}`;
831
+ if (val !== undefined)
832
+ flags.set(name, val);
833
+ else if (i + 1 < argv.length && !argv[i + 1].startsWith("--") && ["--since", "--by", "--limit"].includes(name)) {
834
+ flags.set(name, argv[++i]);
835
+ } else
836
+ flags.set(name, true);
837
+ }
838
+ return { cmd, flags };
839
+ }
840
+ async function cmdUsage(flags, json) {
841
+ const sinceMs = flags.has("--today") ? startOfDayMs() : flags.has("--since") ? (() => {
842
+ const v = flags.get("--since");
843
+ if (typeof v !== "string")
844
+ fail("--since needs a value like 7d");
845
+ return Date.now() - parseDuration(v);
846
+ })() : Date.now() - parseDuration("7d");
847
+ const groupBy = typeof flags.get("--by") === "string" ? flags.get("--by") : "provider";
848
+ if (!["provider", "model", "day", "project", "agent"].includes(groupBy))
849
+ fail(`invalid --by: ${groupBy}`);
850
+ const withPct = flags.has("--pct");
851
+ if (withPct && groupBy !== "provider")
852
+ fail("--pct only supported with --by provider");
853
+ const snapshot2 = await getUsageSnapshot(sinceMs, groupBy, withPct);
854
+ if (json) {
855
+ const out = {
856
+ since: new Date(sinceMs).toISOString(),
857
+ groupBy,
858
+ rows: snapshot2.rows,
859
+ totals: snapshot2.totals,
860
+ pct: snapshot2.pct
861
+ };
862
+ console.log(JSON.stringify(out, null, 2));
863
+ } else {
864
+ const label = flags.has("--today") ? "today" : flags.get("--since") ?? "7d";
865
+ console.log(usageTable(snapshot2.rows, snapshot2.totals, label, groupBy, snapshot2.pct ? new Map(Object.entries(snapshot2.pct)) : undefined));
866
+ }
867
+ }
868
+ async function cmdProviders(flags, json) {
869
+ const statuses = await providerStatuses({ noNet: flags.has("--no-net") });
870
+ const db2 = openDb();
871
+ try {
872
+ const local = localUsageByProvider(db2, Date.now() - parseDuration("7d"));
873
+ if (json) {
874
+ console.log(JSON.stringify({ statuses, local: Object.fromEntries(local) }, null, 2));
875
+ } else {
876
+ console.log(providersTable(statuses, local));
877
+ }
878
+ } finally {
879
+ db2.close();
880
+ }
881
+ }
882
+ async function cmdTop(flags, json) {
883
+ const limit = typeof flags.get("--limit") === "string" ? Number(flags.get("--limit")) : 20;
884
+ const rows = await buildTop(limit);
885
+ if (json) {
886
+ console.log(JSON.stringify(rows, null, 2));
887
+ return;
888
+ }
889
+ const head = ["MODEL", "NAME", "IQ", "CODING", "$/M", "IQ/$"];
890
+ const widths = [28, 26, 5, 7, 8, 6];
891
+ const line = (cells) => cells.map((c, i) => c.length >= widths[i] ? c.slice(0, widths[i]) : c + " ".repeat(widths[i] - c.length)).join(" ");
892
+ console.log(line(head));
893
+ console.log(widths.map((w) => "-".repeat(w)).join(" "));
894
+ for (const r of rows) {
895
+ console.log(line([
896
+ r.model,
897
+ r.name,
898
+ r.intelligence !== undefined ? r.intelligence.toFixed(0) : "\u2014",
899
+ r.codingIndex !== undefined ? r.codingIndex.toFixed(0) : "\u2014",
900
+ fmtCost(r.blended).replace(".00", ""),
901
+ r.value !== undefined ? r.value.toFixed(1) : "\u2014"
902
+ ]));
903
+ }
904
+ }
905
+ if (import.meta.main) {
906
+ const { cmd, flags } = parseArgs(process.argv.slice(2));
907
+ const json = flags.has("--json");
908
+ try {
909
+ if (cmd === "usage")
910
+ await cmdUsage(flags, json);
911
+ else if (cmd === "providers")
912
+ await cmdProviders(flags, json);
913
+ else if (cmd === "top")
914
+ await cmdTop(flags, json);
915
+ else
916
+ fail(`unknown command: ${cmd}
917
+
918
+ ${USAGE}`);
919
+ } catch (err) {
920
+ fail(err.message);
921
+ }
922
+ }