tokentracker-cli 0.97.1 → 0.98.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 (143) hide show
  1. package/README.de.md +3 -3
  2. package/README.ja.md +3 -3
  3. package/README.ko.md +3 -3
  4. package/README.md +7 -5
  5. package/README.zh-CN.md +3 -3
  6. package/dashboard/dist/assets/AchievementBadge-DlZklawB.js +1 -0
  7. package/dashboard/dist/assets/{AchievementsPage-CrqvRxnb.js → AchievementsPage-CprbOKnt.js} +2 -2
  8. package/dashboard/dist/assets/AchievementsSection-BKLJBRKw.js +1 -0
  9. package/dashboard/dist/assets/{BadgeDetailModal-CquST3dj.js → BadgeDetailModal-CfVXLh_t.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-EXhdUHSg.js → BotAnimated-wzqeGbEY.js} +1 -1
  11. package/dashboard/dist/assets/{Card-Il2WWgOG.js → Card-B-gcOAn7.js} +1 -1
  12. package/dashboard/dist/assets/{ClawdAnimated-DioF5wa-.js → ClawdAnimated-1c5ezBBN.js} +2 -2
  13. package/dashboard/dist/assets/CommandPalette-CD8ggEF6.js +1 -0
  14. package/dashboard/dist/assets/CommunityStatsModal-b2fPhi1E.js +1 -0
  15. package/dashboard/dist/assets/{ConfirmModal-BHWQLUCM.js → ConfirmModal-ARBkT8AC.js} +1 -1
  16. package/dashboard/dist/assets/{Controls--LFhExZX.js → Controls-WFXDjIYG.js} +1 -1
  17. package/dashboard/dist/assets/DashboardPage-XtiZa2JN.js +1 -0
  18. package/dashboard/dist/assets/{DevicePage-RKTirqz0.js → DevicePage-BCwSpNXa.js} +1 -1
  19. package/dashboard/dist/assets/{DialogClose-DPFsTKmJ.js → DialogClose-D2PE0TAc.js} +1 -1
  20. package/dashboard/dist/assets/DialogDescription-5LunWukT.js +1 -0
  21. package/dashboard/dist/assets/{DialogTitle-DolLpmFL.js → DialogTitle-Bby0zSCw.js} +1 -1
  22. package/dashboard/dist/assets/{FadeIn-92GGNCPe.js → FadeIn-Cx1hl4so.js} +1 -1
  23. package/dashboard/dist/assets/{HeaderGithubStar-GfXudotT.js → HeaderGithubStar-my4zR6Pp.js} +1 -1
  24. package/dashboard/dist/assets/{HoverTooltip-n7gbPEsq.js → HoverTooltip-DOiGAPLE.js} +1 -1
  25. package/dashboard/dist/assets/{IpCheckPage-BaQOTbXi.js → IpCheckPage-4SdHlFuA.js} +1 -1
  26. package/dashboard/dist/assets/{LandingPage-D3JcvxW_.js → LandingPage-bSWc9xiS.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardAvatar-tVaWJXZ3.js → LeaderboardAvatar-EN4117E5.js} +1 -1
  28. package/dashboard/dist/assets/LeaderboardPage-Bwjf-xZl.js +2 -0
  29. package/dashboard/dist/assets/{LeaderboardProfileModal-DmvcCVS7.js → LeaderboardProfileModal-B1mgVtfP.js} +4 -4
  30. package/dashboard/dist/assets/{LeaderboardProfilePage-VSv3IlzS.js → LeaderboardProfilePage-DVt5S3lY.js} +1 -1
  31. package/dashboard/dist/assets/{LimitsPage-CtxgT20z.js → LimitsPage-BCXjJE1B.js} +5 -5
  32. package/dashboard/dist/assets/{LocalOnlyNotice-O9o586G8.js → LocalOnlyNotice-CzeH4OoJ.js} +1 -1
  33. package/dashboard/dist/assets/LoginCard-aiqSQInP.js +1 -0
  34. package/dashboard/dist/assets/{LoginModal-CJ3g7tBc.js → LoginModal-DdGKIi8h.js} +1 -1
  35. package/dashboard/dist/assets/{LoginPage-DPWzPywx.js → LoginPage-D7xGSoMn.js} +1 -1
  36. package/dashboard/dist/assets/PetPage-Dlb6rNpF.js +1 -0
  37. package/dashboard/dist/assets/{PopoverPopup-nL3YOLPO.js → PopoverPopup-Xbruj7Zi.js} +1 -1
  38. package/dashboard/dist/assets/{ProviderIcon-C5RX_xPk.js → ProviderIcon-BPxacBzj.js} +1 -1
  39. package/dashboard/dist/assets/{ResetPasswordPage-DW_NCwA_.js → ResetPasswordPage-9wgPnLRj.js} +1 -1
  40. package/dashboard/dist/assets/{SegmentedControl-CS6wQtck.js → SegmentedControl-CYFcZFNE.js} +1 -1
  41. package/dashboard/dist/assets/{Select-YqlqvbT5.js → Select-zpFEMqpk.js} +1 -1
  42. package/dashboard/dist/assets/{SelectItemText-BahqsiIc.js → SelectItemText-Bldm5Y0C.js} +1 -1
  43. package/dashboard/dist/assets/{ServiceStatusPage-DyhutLUy.js → ServiceStatusPage-dgtwFazV.js} +1 -1
  44. package/dashboard/dist/assets/{SessionsPage-wbvi4EHi.js → SessionsPage-DHfpv53M.js} +2 -2
  45. package/dashboard/dist/assets/SettingsPage-DcGToOT7.js +2 -0
  46. package/dashboard/dist/assets/SkillsPage-DiQDLw2l.js +1 -0
  47. package/dashboard/dist/assets/TokenGalaxy-GDx7ftMU.js +4426 -0
  48. package/dashboard/dist/assets/ToolbarRootContext-PoCcP2AV.js +1 -0
  49. package/dashboard/dist/assets/{TrendMonitor-BtBcCjx-.js → TrendMonitor-CIfq_yk8.js} +1 -1
  50. package/dashboard/dist/assets/{WidgetsPage-BSNl2WIL.js → WidgetsPage-Dwy-E3Lf.js} +1 -1
  51. package/dashboard/dist/assets/{WrappedPage-d5oiNV4T.js → WrappedPage-Ce7NV6Ij.js} +1 -1
  52. package/dashboard/dist/assets/__vite-browser-external-D-WLjvZW.js +45 -0
  53. package/dashboard/dist/assets/{agent-logos-CCVzO3uU.js → agent-logos-BY8n7b9g.js} +1 -1
  54. package/dashboard/dist/assets/arrow-up-right-BDiV6Wlk.js +1 -0
  55. package/dashboard/dist/assets/check-Chyo6ujc.js +1 -0
  56. package/dashboard/dist/assets/{composite-CWz1s64R.js → composite-DgBqMs_Z.js} +1 -1
  57. package/dashboard/dist/assets/copy-ENRv4pAy.js +1 -0
  58. package/dashboard/dist/assets/download-D3y3gd-z.js +1 -0
  59. package/dashboard/dist/assets/external-link-VxSAxWnH.js +1 -0
  60. package/dashboard/dist/assets/flame-CPdTWeRo.js +1 -0
  61. package/dashboard/dist/assets/{format-tokens-Bs2YotMr.js → format-tokens-BF2G89Q7.js} +1 -1
  62. package/dashboard/dist/assets/{icons-OKyij2y_.js → icons-D6rfdVyV.js} +1 -1
  63. package/dashboard/dist/assets/{index-CN7xY7j_.js → index-4bHcp2Ep.js} +1 -1
  64. package/dashboard/dist/assets/{index-C8nY1OaT.js → index-CgmpxGym.js} +1 -1
  65. package/dashboard/dist/assets/{index-DOwPCQx2.js → index-mzMCUT_4.js} +1 -1
  66. package/dashboard/dist/assets/info-BMso9PcY.js +1 -0
  67. package/dashboard/dist/assets/link-2-Bg34XOcd.js +1 -0
  68. package/dashboard/dist/assets/loader-circle-C4Vul1Yx.js +1 -0
  69. package/dashboard/dist/assets/maximize-2-BBqcwFFo.js +1 -0
  70. package/dashboard/dist/assets/{modifiers.esm-D_aVVjx5.js → modifiers.esm-B2F4Iiey.js} +1 -1
  71. package/dashboard/dist/assets/module-B5tW98jG.js +3 -0
  72. package/dashboard/dist/assets/monitor-smartphone-k1Ja3ngF.js +1 -0
  73. package/dashboard/dist/assets/plus-BaCIejxc.js +1 -0
  74. package/dashboard/dist/assets/provider-display-BYxmO9Bu.js +42 -0
  75. package/dashboard/dist/assets/{react-jS0-5kwE.js → react-xGx5RIqz.js} +1 -1
  76. package/dashboard/dist/assets/search-LZQC7j6a.js +1 -0
  77. package/dashboard/dist/assets/{skills-api-PwUMTdmV.js → skills-api-yLfQkApo.js} +1 -1
  78. package/dashboard/dist/assets/{sortable.esm-BDdw7tkz.js → sortable.esm-B8XRHrmA.js} +1 -1
  79. package/dashboard/dist/assets/{store-CFdu9yv_.js → store-DfUIKvz3.js} +1 -1
  80. package/dashboard/dist/assets/terminal-CsFh6eZS.js +1 -0
  81. package/dashboard/dist/assets/trash-CVk10mDa.js +1 -0
  82. package/dashboard/dist/assets/{use-community-stats-D55oU9bu.js → use-community-stats-DP7lJyDV.js} +1 -1
  83. package/dashboard/dist/assets/{use-latest-request-guard-BO89yQ3n.js → use-latest-request-guard-B7uLArxV.js} +1 -1
  84. package/dashboard/dist/assets/{use-limits-display-prefs-C358N7B0.js → use-limits-display-prefs-DG97cWcv.js} +1 -1
  85. package/dashboard/dist/assets/{use-ordered-list-B9ZYDS1K.js → use-ordered-list-ChQc2v-Q.js} +1 -1
  86. package/dashboard/dist/assets/use-reduced-motion-BoFF1l9U.js +1 -0
  87. package/dashboard/dist/assets/use-session-efficiency-pref-CtF9JAlF.js +1 -0
  88. package/dashboard/dist/assets/{use-transform-9S4Ruq_s.js → use-transform-DVIBk7FO.js} +1 -1
  89. package/dashboard/dist/assets/{use-usage-limits-CpnM5V06.js → use-usage-limits-BYQuJ9Ud.js} +1 -1
  90. package/dashboard/dist/assets/{useCurrency-BKJGW_4l.js → useCurrency-CNkzJCP9.js} +1 -1
  91. package/dashboard/dist/assets/{useTokenFormat-CTB5svOB.js → useTokenFormat-CU3MtXtS.js} +1 -1
  92. package/dashboard/dist/assets/zap-D5k595AR.js +1 -0
  93. package/dashboard/dist/index.html +13 -11
  94. package/dashboard/dist/ip-check.html +1 -1
  95. package/dashboard/dist/leaderboard.html +1 -1
  96. package/dashboard/dist/llms.txt +3 -2
  97. package/dashboard/dist/share.html +1 -1
  98. package/package.json +2 -2
  99. package/src/commands/init.js +1 -0
  100. package/src/commands/status.js +13 -0
  101. package/src/commands/sync.js +61 -4
  102. package/src/lib/cloud-account.js +108 -0
  103. package/src/lib/grok-limits.js +37 -0
  104. package/src/lib/opencode-config.js +20 -1
  105. package/src/lib/pricing/curated-overrides.json +22 -0
  106. package/src/lib/pricing/index.js +9 -1
  107. package/src/lib/pricing/seed-snapshot.json +1 -1
  108. package/src/lib/rollout.js +1244 -168
  109. package/src/lib/usage-limits.js +7 -1
  110. package/dashboard/dist/assets/AchievementBadge-BdvqY4Qb.js +0 -1
  111. package/dashboard/dist/assets/AchievementsSection-wyLAM9N7.js +0 -1
  112. package/dashboard/dist/assets/CommandPalette-CaDx06tM.js +0 -1
  113. package/dashboard/dist/assets/CommunityStatsModal-BbYWzM07.js +0 -1
  114. package/dashboard/dist/assets/DashboardPage-DuqJOpHL.js +0 -1
  115. package/dashboard/dist/assets/DialogDescription-DgaV8Gtk.js +0 -1
  116. package/dashboard/dist/assets/LeaderboardPage-NqHcStnk.js +0 -2
  117. package/dashboard/dist/assets/LoginCard-JCejigVu.js +0 -1
  118. package/dashboard/dist/assets/PetPage-MSJ3jseK.js +0 -1
  119. package/dashboard/dist/assets/SettingsPage-HGSwpV1O.js +0 -2
  120. package/dashboard/dist/assets/SkillsPage-BbVT9oyh.js +0 -1
  121. package/dashboard/dist/assets/TokenGalaxy-Cej6MLu6.js +0 -4314
  122. package/dashboard/dist/assets/ToolbarRootContext-D9pPY73U.js +0 -1
  123. package/dashboard/dist/assets/__vite-browser-external-CjrP5KfC.js +0 -45
  124. package/dashboard/dist/assets/arrow-up-right-AKItXMyh.js +0 -1
  125. package/dashboard/dist/assets/check-BQw5n0V0.js +0 -1
  126. package/dashboard/dist/assets/copy-CF8XNmv3.js +0 -1
  127. package/dashboard/dist/assets/download-BZvLU_hD.js +0 -1
  128. package/dashboard/dist/assets/external-link-bZrcVkeE.js +0 -1
  129. package/dashboard/dist/assets/flame-CMHolW1z.js +0 -1
  130. package/dashboard/dist/assets/info-CU7bMBhU.js +0 -1
  131. package/dashboard/dist/assets/link-2-wwoUOujz.js +0 -1
  132. package/dashboard/dist/assets/loader-circle-BK3Ltc-j.js +0 -1
  133. package/dashboard/dist/assets/maximize-2-DIPnLqR8.js +0 -1
  134. package/dashboard/dist/assets/module-DUSXqJtA.js +0 -3
  135. package/dashboard/dist/assets/monitor-smartphone-4NOM9BHb.js +0 -1
  136. package/dashboard/dist/assets/plus-JqSTJ28Y.js +0 -1
  137. package/dashboard/dist/assets/provider-display-CEFlg9ho.js +0 -42
  138. package/dashboard/dist/assets/search-CbBetPN7.js +0 -1
  139. package/dashboard/dist/assets/terminal-Dh7Sh6IG.js +0 -1
  140. package/dashboard/dist/assets/trash-B6_OaEVL.js +0 -1
  141. package/dashboard/dist/assets/use-reduced-motion-DWHVI51t.js +0 -1
  142. package/dashboard/dist/assets/use-session-efficiency-pref-CwGRDyVq.js +0 -1
  143. package/dashboard/dist/assets/zap-DGdBuT6m.js +0 -1
@@ -68,6 +68,23 @@ function decodeJwtExpMs(token) {
68
68
  return 0;
69
69
  }
70
70
 
71
+ // The account id the access token was minted for. Used to key the payload cache
72
+ // below on something the backend does NOT rotate: the refresh token changes
73
+ // under us on rotation, which would throw away a still-warm cache entry on
74
+ // exactly the reads that repeat most.
75
+ function decodeJwtSub(token) {
76
+ try {
77
+ const part = String(token || "").split(".")[1];
78
+ if (!part) return "";
79
+ const json = Buffer.from(part.replace(/-/g, "+").replace(/_/g, "/"), "base64").toString("utf8");
80
+ const payload = JSON.parse(json);
81
+ if (payload && typeof payload.sub === "string") return payload.sub;
82
+ } catch {
83
+ /* ignore */
84
+ }
85
+ return "";
86
+ }
87
+
71
88
  // Module-level access-token cache, keyed by the cloud base URL and refresh token
72
89
  // that produced it.
73
90
  // The popover polls frequently, so caching avoids hammering /api/auth/refresh.
@@ -81,9 +98,78 @@ let tokenCache = { cacheKey: null, accessToken: null, expMs: 0 };
81
98
  // intermittent "Activity silently dropped to this-machine data" symptom.
82
99
  const mintInflight = new Map();
83
100
 
101
+ // Response cache for the account reads, mirroring the 30-second window the edge
102
+ // functions already keep.
103
+ //
104
+ // Each `tokentracker-account-*` function caches the RPC snapshot it renders
105
+ // from for 30 seconds (`COMPACT_TTL_MS` in dashboard/edge-patches/*), so two
106
+ // reads inside one window render the same numbers from the same snapshot. That
107
+ // cache only saves the edge-to-PostgREST hop, though: the full body still goes
108
+ // out to every caller. And the callers repeat a lot — the Windows tray poller
109
+ // re-reads the 52-week heatmap every ~49 seconds, and one popover refresh fans
110
+ // out six reads at once. Holding the payload here for the same 30 seconds
111
+ // returns exactly what the cloud would have returned, without paying for the
112
+ // transfer again.
113
+ //
114
+ // Bodies are stored serialized and re-parsed on a hit so a caller mutating the
115
+ // object it got back cannot corrupt what the next caller reads.
116
+ const PAYLOAD_TTL_MS = 30_000;
117
+ const PAYLOAD_CACHE_MAX = 64;
118
+ const payloadCache = new Map();
119
+
120
+ // Mirrors the params `fetchAccountFunction` actually puts on the wire, so two
121
+ // requests share an entry only when they would have produced the same URL.
122
+ function payloadCacheKey({ root, sub, slug, searchParams }) {
123
+ const pairs = [];
124
+ if (searchParams && typeof searchParams.entries === "function") {
125
+ for (const [key, value] of searchParams.entries()) {
126
+ if (key === "account" || key === "scope") continue;
127
+ if (value == null || value === "") continue;
128
+ pairs.push(`${key}=${String(value)}`);
129
+ }
130
+ }
131
+ pairs.sort();
132
+ return `${root}\0${sub}\0${slug}\0${pairs.join("&")}`;
133
+ }
134
+
135
+ function payloadCacheGet(key, now) {
136
+ const hit = payloadCache.get(key);
137
+ if (!hit) return null;
138
+ if (hit.expMs <= now()) {
139
+ payloadCache.delete(key);
140
+ return null;
141
+ }
142
+ // Re-insert so the eviction below drops the coldest entry, not this one.
143
+ payloadCache.delete(key);
144
+ payloadCache.set(key, hit);
145
+ try {
146
+ return JSON.parse(hit.body);
147
+ } catch {
148
+ payloadCache.delete(key);
149
+ return null;
150
+ }
151
+ }
152
+
153
+ function payloadCacheSet(key, data, now) {
154
+ let body;
155
+ try {
156
+ body = JSON.stringify(data);
157
+ } catch {
158
+ return; // Not serializable: skip the cache rather than fail the read.
159
+ }
160
+ if (body === undefined) return;
161
+ payloadCache.set(key, { expMs: now() + PAYLOAD_TTL_MS, body });
162
+ while (payloadCache.size > PAYLOAD_CACHE_MAX) {
163
+ const oldest = payloadCache.keys().next();
164
+ if (oldest.done) break;
165
+ payloadCache.delete(oldest.value);
166
+ }
167
+ }
168
+
84
169
  function __resetCloudAccountCacheForTests() {
85
170
  tokenCache = { cacheKey: null, accessToken: null, expMs: 0 };
86
171
  mintInflight.clear();
172
+ payloadCache.clear();
87
173
  }
88
174
 
89
175
  // Failure classes surfaced to local-api so it can tell an intentional local
@@ -328,6 +414,25 @@ async function fetchAccountUsage({
328
414
  });
329
415
  if (!minted) return null;
330
416
 
417
+ // Keyed after the mint so rotation cannot invalidate the entry, and so a
418
+ // revoked session fails here rather than being served from cache.
419
+ const sub = decodeJwtSub(minted.accessToken);
420
+ const cacheKey = sub
421
+ ? payloadCacheKey({ root: String(baseUrl || DEFAULT_BASE_URL).replace(/\/$/, ""), sub, slug, searchParams })
422
+ : null;
423
+ if (cacheKey) {
424
+ const cached = payloadCacheGet(cacheKey, now);
425
+ if (cached !== null) {
426
+ // Still surface a rotation from this mint: dropping it would leave the
427
+ // caller relaying a refresh token the backend has already consumed.
428
+ return {
429
+ data: cached,
430
+ rotatedRefreshToken: minted.refreshToken,
431
+ rotatedCsrfToken: minted.csrfToken,
432
+ };
433
+ }
434
+ }
435
+
331
436
  const data = await fetchAccountFunction({
332
437
  baseUrl,
333
438
  anonKey,
@@ -337,16 +442,19 @@ async function fetchAccountUsage({
337
442
  fetchImpl,
338
443
  timeoutMs: getRemainingTimeout(),
339
444
  });
445
+ if (cacheKey && data != null) payloadCacheSet(cacheKey, data, now);
340
446
  return { data, rotatedRefreshToken: minted.refreshToken, rotatedCsrfToken: minted.csrfToken };
341
447
  }
342
448
 
343
449
  module.exports = {
344
450
  AccountAuthError,
345
451
  USAGE_TO_ACCOUNT_SLUG,
452
+ PAYLOAD_TTL_MS,
346
453
  accountSlugFor,
347
454
  accessTokenFromRefreshPayload,
348
455
  refreshTokenFromRefreshPayload,
349
456
  decodeJwtExpMs,
457
+ decodeJwtSub,
350
458
  mintAccessToken,
351
459
  fetchAccountFunction,
352
460
  fetchAccountUsage,
@@ -495,6 +495,40 @@ async function resolveGrokAccessToken({
495
495
  };
496
496
  }
497
497
 
498
+ // xAI reports the subscription as a human-readable product name on the billing
499
+ // payload's top level (`subscriptionTier`, a sibling of `config`, not a member
500
+ // of it) -- a Free account reads exactly "Free".
501
+ const GROK_PLAN_TIERS = new Map([
502
+ ["supergrokheavy", "SuperGrok Heavy"],
503
+ ["supergrokplus", "SuperGrok Plus"],
504
+ ["supergroklite", "SuperGrok Lite"],
505
+ ["supergrok", "SuperGrok"],
506
+ ["xpremiumplus", "X Premium+"],
507
+ ["premiumplus", "X Premium+"],
508
+ ["xpremium", "X Premium+"],
509
+ ["apikey", "API Key"],
510
+ ["free", "Free"],
511
+ ]);
512
+
513
+ /**
514
+ * Map xAI's subscription tier to the name its own UI shows.
515
+ *
516
+ * Separators are folded away before matching so the same table covers whichever
517
+ * casing the API returns ("SuperGrok Heavy", "SUPER_GROK_HEAVY", "superGrokHeavy").
518
+ * Unknown values return null rather than being passed through: the Limits panel
519
+ * falls back to the bare brand name, which is better than rendering a raw enum
520
+ * (issue #130 shipped "Kimi Type_event" that way).
521
+ */
522
+ function deriveGrokPlanLabel(rawTier) {
523
+ if (rawTier == null) return null;
524
+ const key = String(rawTier).toLowerCase().replace(/[^a-z0-9]+/g, "");
525
+ if (!key) return null;
526
+ // Exact match only. Prefix matching would read an unseen tier such as
527
+ // "SuperGrok Business" as plain "SuperGrok", and a confidently wrong plan on
528
+ // the card is worse than no plan at all.
529
+ return GROK_PLAN_TIERS.get(key) ?? null;
530
+ }
531
+
498
532
  /**
499
533
  * Parse either:
500
534
  * - Unified billing (`?format=credits`): weekly/monthly period + creditUsagePercent
@@ -567,6 +601,8 @@ function normalizeGrokBillingResponse(body) {
567
601
  }
568
602
 
569
603
  return {
604
+ // Read from the response root, not from `config`.
605
+ plan_label: deriveGrokPlanLabel(body?.subscriptionTier),
570
606
  period_type: periodType,
571
607
  monthly_credits_limit: monthlyLimit,
572
608
  monthly_credits_used: used,
@@ -721,6 +757,7 @@ module.exports = {
721
757
  normalizeGrokPeriodType,
722
758
  inferGrokPeriodTypeFromDates,
723
759
  sumProductUsagePercent,
760
+ deriveGrokPlanLabel,
724
761
  normalizeGrokBillingResponse,
725
762
  fetchGrokBilling,
726
763
  fetchGrokLimits,
@@ -26,7 +26,7 @@ function buildOpencodePlugin({ notifyPath }) {
26
26
  return (
27
27
  `// ${PLUGIN_MARKER}\n` +
28
28
  `const notifyPath = ${JSON.stringify(safeNotifyPath)};\n` +
29
- `export const TokenTrackerPlugin = async ({ $ }) => {\n` +
29
+ `const server = async ({ $ }) => {\n` +
30
30
  ` return {\n` +
31
31
  ` event: async ({ event }) => {\n` +
32
32
  ` if (!event || event.type !== ${JSON.stringify(DEFAULT_EVENT)}) return;\n` +
@@ -40,6 +40,25 @@ function buildOpencodePlugin({ notifyPath }) {
40
40
  ` } catch (_) {}\n` +
41
41
  ` }\n` +
42
42
  ` };\n` +
43
+ `};\n` +
44
+ // Three loaders read this one file (issue #646):
45
+ // * older opencode v1 takes the named export;
46
+ // * current v1 (verified on 1.18.30) requires a default export carrying
47
+ // `id` and `server()`, and rejects the file outright without them;
48
+ // * v2 requires a default export carrying `id` and `setup()`/`effect()`,
49
+ // and ignores `server()`.
50
+ // Emitting all three keeps a single generated file valid everywhere. v2's
51
+ // plugin context exposes no event stream and no shell (`@opencode-ai/plugin`
52
+ // v2 PluginContext is agent/aisdk/catalog/command/integration/reference/skill
53
+ // only), so there is nothing for `setup` to subscribe to -- it exists to make
54
+ // the plugin load instead of erroring on every startup. Usage itself does not
55
+ // depend on this hook: opencode rows are parsed from the on-disk storage and
56
+ // opencode.db, and the hook only makes a sync happen sooner.
57
+ `export const TokenTrackerPlugin = server;\n` +
58
+ `export default {\n` +
59
+ ` id: "tokentracker",\n` +
60
+ ` server,\n` +
61
+ ` setup: async () => {},\n` +
43
62
  `};\n`
44
63
  );
45
64
  }
@@ -159,6 +159,20 @@
159
159
  "cache_read": 0.014,
160
160
  "cache_write": 0.44
161
161
  },
162
+ "deepseek-v4.1-flash": {
163
+ "input": 0.3,
164
+ "output": 1.2,
165
+ "cache_read": 0.006,
166
+ "cache_write": 0.3,
167
+ "note": "DeepSeek V4.1 Flash (official API id deepseek-flash, released 2026-09-10). deepseek-v4.1-flash is the id reported by OpenRouter, Command Code and WorkBuddy. Peak rates per https://api-docs.deepseek.com/quick_start/pricing/ ($0.30 / $1.20 / $0.006 cache read per MTok); computeRowCost halves them off-peak like the other DeepSeek V4 entries."
168
+ },
169
+ "deepseek-flash": {
170
+ "input": 0.3,
171
+ "output": 1.2,
172
+ "cache_read": 0.006,
173
+ "cache_write": 0.3,
174
+ "note": "Official DeepSeek API id for DeepSeek-V4.1-Flash. Same rates as deepseek-v4.1-flash."
175
+ },
162
176
  "deepseek-chat": {
163
177
  "input": 0.14,
164
178
  "output": 0.28,
@@ -544,6 +558,14 @@
544
558
  "match": "minimax-m2.7",
545
559
  "ref": "MiniMax-M2.7"
546
560
  },
561
+ {
562
+ "match": "deepseek-v4.1-flash",
563
+ "ref": "deepseek-v4.1-flash"
564
+ },
565
+ {
566
+ "match": "deepseek-flash",
567
+ "ref": "deepseek-flash"
568
+ },
547
569
  {
548
570
  "match": "deepseek-v4-flash",
549
571
  "ref": "deepseek-v4-flash"
@@ -33,6 +33,8 @@ const SOURCES_WITH_AUTHORITATIVE_COST = new Set(["grok"]);
33
33
  const SEED_SNAPSHOT_PATH = path.resolve(__dirname, "seed-snapshot.json");
34
34
  const DEEPSEEK_TIME_PRICED_MODELS = [
35
35
  "deepseek-v4-flash",
36
+ "deepseek-v4.1-flash",
37
+ "deepseek-flash",
36
38
  "deepseek-v4-pro",
37
39
  ];
38
40
  // Sync seed load. Done at require-time so callers that haven't awaited
@@ -194,8 +196,14 @@ function computeRowCost(row) {
194
196
  reportedCost > 0
195
197
  ) return reportedCost;
196
198
  const pricing = getRowPricing(row);
199
+ // OmO, like Codex, reports reasoning as a subset of `output` (its own
200
+ // usage.cost bills no separate reasoning component), so charging it again
201
+ // here would double-bill every reasoning token.
197
202
  const reasoningIncludedInOutput =
198
- row.source === "codex" || row.source === "acode" || row.source === "every-code";
203
+ row.source === "codex" ||
204
+ row.source === "acode" ||
205
+ row.source === "every-code" ||
206
+ row.source === "omo";
199
207
  const reasoningCost = reasoningIncludedInOutput
200
208
  ? 0
201
209
  : (row.reasoning_output_tokens || 0) * (pricing.output || 0);