tokentracker-cli 1.5.3 → 1.5.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.
Files changed (131) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-uuDGO9Mq.js → AchievementBadge-CKNb_cwg.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage-BCTVT7Jo.js → AchievementsPage-u9P8DXJI.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-B6fJSJSr.js → AchievementsSection-BSCUJ4DR.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-FjUN__9G.js → BadgeDetailModal-Bg7cE3Zq.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-B-KqY_Wr.js → BotAnimated-C7D2WbQA.js} +1 -1
  6. package/dashboard/dist/assets/{Card-DS8T8piP.js → Card-B4IqAhai.js} +1 -1
  7. package/dashboard/dist/assets/{CloudCheckoutPage-_0UjsFPu.js → CloudCheckoutPage-P75pH9Ym.js} +1 -1
  8. package/dashboard/dist/assets/{CloudPage-BstgVBaV.js → CloudPage-DDEZfmZg.js} +2 -2
  9. package/dashboard/dist/assets/{CommandPalette-DtVsYZgc.js → CommandPalette-CER_AklA.js} +1 -1
  10. package/dashboard/dist/assets/{CommunityStatsModal-DTpEJqV3.js → CommunityStatsModal-Cbb74eib.js} +1 -1
  11. package/dashboard/dist/assets/{ConfirmModal-Lfdh2vr7.js → ConfirmModal-Q1ZQdZHt.js} +1 -1
  12. package/dashboard/dist/assets/{Controls-DpwQ7pkq.js → Controls-DaGOfcoz.js} +1 -1
  13. package/dashboard/dist/assets/DashboardPage-CXI7RmxO.js +1 -0
  14. package/dashboard/dist/assets/{DateRangePopover-CrWgrzT2.js → DateRangePopover-DSS1Ira_.js} +1 -1
  15. package/dashboard/dist/assets/{DevicePage-DbK5KWJS.js → DevicePage-6zPLYydh.js} +1 -1
  16. package/dashboard/dist/assets/{DialogClose-Dd2B_o03.js → DialogClose-DdCQ36LK.js} +1 -1
  17. package/dashboard/dist/assets/DialogDescription-DztHQvj-.js +1 -0
  18. package/dashboard/dist/assets/{DialogTitle-3JQBVha3.js → DialogTitle-BjP217Y1.js} +1 -1
  19. package/dashboard/dist/assets/{ElectricLogo-LYAqG8pG.js → ElectricLogo-Dhq8PM4h.js} +1 -1
  20. package/dashboard/dist/assets/{FadeIn-DhDvUor7.js → FadeIn-CGNbED4E.js} +1 -1
  21. package/dashboard/dist/assets/{HeaderGithubStar-9f1XmgYC.js → HeaderGithubStar-DwqEFzb6.js} +1 -1
  22. package/dashboard/dist/assets/{HoverTooltip-BXhHAkP7.js → HoverTooltip-CQZTgyQy.js} +1 -1
  23. package/dashboard/dist/assets/{IpCheckPage-ChPdDeMl.js → IpCheckPage-Bz09HXLn.js} +1 -1
  24. package/dashboard/dist/assets/{LandingPage-DnJ7Y9zw.js → LandingPage-DC-lZsYR.js} +2 -2
  25. package/dashboard/dist/assets/{LeaderboardPage-BF9isLzu.js → LeaderboardPage-D8MpLM_t.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardProBadge-BM4JKgkC.js → LeaderboardProBadge-pdXgokhE.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardProfileModal-DfZABOpN.js → LeaderboardProfileModal-nHZLiE0m.js} +3 -3
  28. package/dashboard/dist/assets/{LeaderboardProfilePage-BaZ38iT5.js → LeaderboardProfilePage-BKv3oIAW.js} +1 -1
  29. package/dashboard/dist/assets/LimitsPage-TmRTgSCb.js +6 -0
  30. package/dashboard/dist/assets/{LocalOnlyNotice-DvhlYoqQ.js → LocalOnlyNotice-aF01udyR.js} +1 -1
  31. package/dashboard/dist/assets/{LoginCard-g_Gbnzhs.js → LoginCard-BaroD0Ct.js} +1 -1
  32. package/dashboard/dist/assets/{LoginModal-DkJrI6b7.js → LoginModal-0jgyMGVM.js} +1 -1
  33. package/dashboard/dist/assets/{LoginPage-CqCKSo0n.js → LoginPage-BUx8U58i.js} +1 -1
  34. package/dashboard/dist/assets/{PetPage-DbF7ROsj.js → PetPage-CpOjtJqg.js} +2 -2
  35. package/dashboard/dist/assets/{PopoverPopup-4WuH41ZT.js → PopoverPopup-BYjfer-M.js} +1 -1
  36. package/dashboard/dist/assets/{ProviderIcon-6j0A2KD9.js → ProviderIcon-NtYa2h0u.js} +1 -1
  37. package/dashboard/dist/assets/{ResetPasswordPage-DBd-iwuM.js → ResetPasswordPage-CYUg7TR1.js} +1 -1
  38. package/dashboard/dist/assets/{Select-BBEpLeiG.js → Select-CYP95o5y.js} +1 -1
  39. package/dashboard/dist/assets/{SelectItemText-Cn_U_rrO.js → SelectItemText-BCDfusOM.js} +1 -1
  40. package/dashboard/dist/assets/{SelfHostPage-2WZc6Rml.js → SelfHostPage-BmVc2rIT.js} +1 -1
  41. package/dashboard/dist/assets/ServiceStatusPage-Dyp8NwIn.js +1 -0
  42. package/dashboard/dist/assets/SessionsPage-tGEj6Ztz.js +3 -0
  43. package/dashboard/dist/assets/{SettingsPage-rXNr96i-.js → SettingsPage-CCMbq5wc.js} +3 -3
  44. package/dashboard/dist/assets/{SkillsPage-CudTvbZL.js → SkillsPage-C4O4fbaQ.js} +1 -1
  45. package/dashboard/dist/assets/{TokenGalaxy-BRKrbTsJ.js → TokenGalaxy-D2d738xr.js} +1 -1
  46. package/dashboard/dist/assets/{TooltipPopup-CsLKXhWC.js → TooltipPopup-BJfFYesG.js} +1 -1
  47. package/dashboard/dist/assets/{TrendMonitor-8onUTbkk.js → TrendMonitor-CElakFSU.js} +1 -1
  48. package/dashboard/dist/assets/{WidgetsPage-CcwE0lz8.js → WidgetsPage-Cvd1LbJm.js} +1 -1
  49. package/dashboard/dist/assets/{WrappedPage-Cj9LLYqP.js → WrappedPage-wRC1tE_a.js} +1 -1
  50. package/dashboard/dist/assets/__vite-browser-external-CMBWGkOq.css +1 -0
  51. package/dashboard/dist/assets/__vite-browser-external-V4TPLCJG.js +45 -0
  52. package/dashboard/dist/assets/{agent-logos-ChBA1nAH.js → agent-logos-Ba57HF3w.js} +1 -1
  53. package/dashboard/dist/assets/{arrow-left-Bb5mfoPc.js → arrow-left-B1bKnxXL.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-right-D1Ti2XUI.js → arrow-right-DrOlKzX5.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-B_CjA_y8.js → arrow-up-right-hlYpo3yw.js} +1 -1
  56. package/dashboard/dist/assets/{calendar-days-DdfrAqie.js → calendar-days-CSY6SEsI.js} +1 -1
  57. package/dashboard/dist/assets/{check-29zGw1MK.js → check-xrP1gLw5.js} +1 -1
  58. package/dashboard/dist/assets/{chevron-down-DAzlFX7g.js → chevron-down-6-zT5n9d.js} +1 -1
  59. package/dashboard/dist/assets/{cloud-checkout-C2SeCjFM.js → cloud-checkout-BZymZD4K.js} +1 -1
  60. package/dashboard/dist/assets/{composite-DIf735Hq.js → composite-C9izWafa.js} +1 -1
  61. package/dashboard/dist/assets/{copy-BOuOM0HQ.js → copy-Dkas5wZX.js} +1 -1
  62. package/dashboard/dist/assets/{download-50tQRpGe.js → download-CagGRs0_.js} +1 -1
  63. package/dashboard/dist/assets/{external-link-BhlQClVe.js → external-link-DRl10hxd.js} +1 -1
  64. package/dashboard/dist/assets/{flame-CeGfA-0L.js → flame-CkvmsTFB.js} +1 -1
  65. package/dashboard/dist/assets/{format-tokens-Cq7YiAkJ.js → format-tokens-BtwhO3mV.js} +1 -1
  66. package/dashboard/dist/assets/{gift-Do-UFhiT.js → gift-YzjXkMQx.js} +1 -1
  67. package/dashboard/dist/assets/{icons-C6EZ_HmP.js → icons-CHhKYs0u.js} +1 -1
  68. package/dashboard/dist/assets/{index-BWhbBAd9.js → index-B0MQZTrM.js} +1 -1
  69. package/dashboard/dist/assets/{index-D9kwYWLX.js → index-C2DK8av-.js} +1 -1
  70. package/dashboard/dist/assets/{index-BFrbVqsm.js → index-CJknN9Jd.js} +1 -1
  71. package/dashboard/dist/assets/{index-c0crpe9Z.js → index-DojzoMqd.js} +1 -1
  72. package/dashboard/dist/assets/{info-B4MXmsNE.js → info-Torep-Ti.js} +1 -1
  73. package/dashboard/dist/assets/{link-2-DuWCslzl.js → link-2-DE5Yu-z6.js} +1 -1
  74. package/dashboard/dist/assets/{loader-circle-C2I3NZAO.js → loader-circle-BL8LxD9n.js} +1 -1
  75. package/dashboard/dist/assets/{local-usage-transfer-BQasvsUF.js → local-usage-transfer-8X1InWix.js} +1 -1
  76. package/dashboard/dist/assets/{modifiers.esm-DbeNS6FT.js → modifiers.esm-D60-6UQa.js} +1 -1
  77. package/dashboard/dist/assets/{monitor-smartphone-C6Y4ktsc.js → monitor-smartphone-C-IFPjqk.js} +1 -1
  78. package/dashboard/dist/assets/{play-CwgcDJC2.js → play-n6uWrdH9.js} +1 -1
  79. package/dashboard/dist/assets/{plus-B796wsUO.js → plus-B2f6TaNj.js} +1 -1
  80. package/dashboard/dist/assets/{provider-display-Cw26RFDm.js → provider-display-DWqCEcsP.js} +1 -1
  81. package/dashboard/dist/assets/{react-d7Bk1-_K.js → react-B-zfi_j6.js} +1 -1
  82. package/dashboard/dist/assets/{safePolygon-CDE0dNv_.js → safePolygon-C2PsvI-v.js} +1 -1
  83. package/dashboard/dist/assets/{search-DtKvsdRs.js → search-DZOaTLF9.js} +1 -1
  84. package/dashboard/dist/assets/{server-gAURHUyz.js → server-DIp4Qfsv.js} +1 -1
  85. package/dashboard/dist/assets/{shield-check-DFVdDTwh.js → shield-check-Cs2M8-iT.js} +1 -1
  86. package/dashboard/dist/assets/{skills-api-2rdPUrJI.js → skills-api-CoYy6Tov.js} +1 -1
  87. package/dashboard/dist/assets/{sortable.esm-DFlfwnJr.js → sortable.esm-B5_hjIfs.js} +1 -1
  88. package/dashboard/dist/assets/{store-BKPCk_de.js → store-BLXqtJzb.js} +1 -1
  89. package/dashboard/dist/assets/{terminal-BtWFakvS.js → terminal-CEMKX3ba.js} +1 -1
  90. package/dashboard/dist/assets/{trash-bpr9zTAM.js → trash-DOWYaXjK.js} +1 -1
  91. package/dashboard/dist/assets/{use-community-stats-DMnGgLne.js → use-community-stats-BjRI1Bzz.js} +1 -1
  92. package/dashboard/dist/assets/{use-latest-request-guard-BSBlJJHe.js → use-latest-request-guard-T7L5qBU6.js} +1 -1
  93. package/dashboard/dist/assets/use-limits-display-prefs-vNIrCPZA.js +1 -0
  94. package/dashboard/dist/assets/{use-native-settings-OthqiF6g.js → use-native-settings-Cg5Zb4q_.js} +1 -1
  95. package/dashboard/dist/assets/{use-ordered-list-DpAtr0R-.js → use-ordered-list-BF-wIvmG.js} +1 -1
  96. package/dashboard/dist/assets/use-reduced-motion-B9K5mIx4.js +1 -0
  97. package/dashboard/dist/assets/{use-transform-BRz9dz_2.js → use-transform-CbJ1IcNc.js} +1 -1
  98. package/dashboard/dist/assets/{use-usage-limits-CwmhudAX.js → use-usage-limits-Dgg3jpC4.js} +1 -1
  99. package/dashboard/dist/assets/{useCurrency-CAA-tQ27.js → useCurrency-3oBHWfSe.js} +1 -1
  100. package/dashboard/dist/assets/{usePositioner-Cj2DPmwM.js → usePositioner-D0YrIW0A.js} +1 -1
  101. package/dashboard/dist/assets/{useTokenFormat-Bf5-f7Hx.js → useTokenFormat-DZAHWE_R.js} +1 -1
  102. package/dashboard/dist/assets/{zap-B8Fg6PaC.js → zap-kLub6Jiv.js} +1 -1
  103. package/dashboard/dist/index.html +2 -2
  104. package/dashboard/dist/ip-check.html +2 -2
  105. package/dashboard/dist/leaderboard.html +2 -2
  106. package/dashboard/dist/share.html +2 -2
  107. package/package.json +1 -1
  108. package/src/commands/serve.js +3 -2
  109. package/src/lib/accept-encoding.js +59 -0
  110. package/src/lib/cursor-config.js +3 -1
  111. package/src/lib/deepseek-limits.js +280 -0
  112. package/src/lib/grok-limits.js +40 -1
  113. package/src/lib/local-api.js +229 -31
  114. package/src/lib/opencode-status.js +88 -0
  115. package/src/lib/pricing/index.js +182 -79
  116. package/src/lib/pricing/seed-snapshot.json +1 -1
  117. package/src/lib/provider-api-key-store.js +2 -0
  118. package/src/lib/provider-status.js +17 -5
  119. package/src/lib/session-analytics.js +194 -29
  120. package/src/lib/static-server.js +74 -2
  121. package/src/lib/usage-limits.js +535 -16
  122. package/src/lib/workbuddy-limits.js +106 -14
  123. package/dashboard/dist/assets/DashboardPage-DUFWejkE.js +0 -1
  124. package/dashboard/dist/assets/DialogDescription-CLy0arbt.js +0 -1
  125. package/dashboard/dist/assets/LimitsPage-C7LbG3LX.js +0 -6
  126. package/dashboard/dist/assets/ServiceStatusPage-B-g4MDl0.js +0 -1
  127. package/dashboard/dist/assets/SessionsPage-CWS0iQ1L.js +0 -3
  128. package/dashboard/dist/assets/__vite-browser-external-07djSGmy.js +0 -45
  129. package/dashboard/dist/assets/__vite-browser-external-BDldtsD2.css +0 -1
  130. package/dashboard/dist/assets/use-limits-display-prefs-eYlvnUKn.js +0 -1
  131. package/dashboard/dist/assets/use-reduced-motion-DF0grwHs.js +0 -1
@@ -1,6 +1,8 @@
1
1
  const fs = require("node:fs");
2
2
  const os = require("node:os");
3
3
  const path = require("node:path");
4
+ const zlib = require("node:zlib");
5
+ const { promisify } = require("node:util");
4
6
  const { spawn } = require("node:child_process");
5
7
  const crypto = require("node:crypto");
6
8
  const { setImmediate: yieldToRequests } = require("node:timers/promises");
@@ -19,6 +21,7 @@ const { functionUrlFor, fetchFunctionResponse } = require("./function-url");
19
21
  const { createCloudDeviceTokenStore } = require("./cloud-device-token");
20
22
  const { parseBearerToken } = require("./bearer-token");
21
23
  const { writeJsonAtomicSync } = require("./atomic-json");
24
+ const { negotiateEncoding } = require("./accept-encoding");
22
25
  const { createLocalUsageTransfer, MAX_BYTES: LOCAL_USAGE_MAX_BYTES } = require("./local-usage-transfer");
23
26
  const { fetchProviderServiceStatus } = require("./provider-status");
24
27
 
@@ -54,6 +57,132 @@ const AVATAR_PROXY_MAX_BYTES = 512 * 1024; // 512 KiB per image
54
57
  const AVATAR_PROXY_MAX_ENTRIES = 64;
55
58
  const avatarProxyCache = new Map();
56
59
 
60
+ // The sessions endpoint serializes ~9MB of row data. Building it twice in a
61
+ // row is pure duplicate work when the sidecar and pricing revision have not
62
+ // changed; keep the exact JSON bytes keyed by both. A short TTL also covers
63
+ // new source session files without requiring a sidecar rebuild; sync's
64
+ // writeAtomic() replaces the sidecar and invalidates immediately. Force-refresh
65
+ // requests bypass reads but still refresh the cache.
66
+ // Keyed by sidecar signature + pricing revision + query, so one entry per
67
+ // (include_buckets, range) variant the dashboard has open. Keep a few variants
68
+ // but bound the total bytes: the bucket-less summary is ~2.9MB raw while the
69
+ // full legacy variant is ~9MB.
70
+ const SESSIONS_RESPONSE_CACHE_MAX = 8;
71
+ const SESSIONS_RESPONSE_CACHE_MAX_BYTES = 32 * 1024 * 1024;
72
+ const SESSIONS_RESPONSE_CACHE_TTL_MS = 60 * 1000;
73
+ const sessionsResponseCache = new Map();
74
+
75
+ function sessionsResponseCacheKey({ sidecarPath, from, to, limit, includeBuckets, includeModelDetail, timeZone, offsetMinutes }) {
76
+ try {
77
+ const stat = fs.statSync(sidecarPath);
78
+ return [
79
+ sidecarPath,
80
+ stat.dev,
81
+ stat.ino,
82
+ stat.size,
83
+ stat.mtimeMs,
84
+ stat.ctimeMs,
85
+ getPricingRevision(),
86
+ from,
87
+ to,
88
+ limit,
89
+ includeBuckets ? 1 : 0,
90
+ includeModelDetail ? 1 : 0,
91
+ timeZone || "",
92
+ Number.isFinite(offsetMinutes) ? offsetMinutes : "",
93
+ ].join("\0");
94
+ } catch {
95
+ return null;
96
+ }
97
+ }
98
+
99
+ function sessionsResponseCacheBytes() {
100
+ let total = 0;
101
+ for (const entry of sessionsResponseCache.values()) {
102
+ total += entry.raw.length + (entry.gzip?.length || 0) + (entry.br?.length || 0);
103
+ }
104
+ return total;
105
+ }
106
+
107
+ function rememberSessionsResponse(key, body) {
108
+ const entry = { raw: body, gzip: null, br: null, expiresAt: Date.now() + SESSIONS_RESPONSE_CACHE_TTL_MS };
109
+ if (!key) return entry;
110
+ // Re-inserting moves the entry to the end, so eviction stays least-recently-used.
111
+ sessionsResponseCache.delete(key);
112
+ sessionsResponseCache.set(key, entry);
113
+ while (sessionsResponseCache.size > SESSIONS_RESPONSE_CACHE_MAX
114
+ || (sessionsResponseCache.size > 1 && sessionsResponseCacheBytes() > SESSIONS_RESPONSE_CACHE_MAX_BYTES)) {
115
+ sessionsResponseCache.delete(sessionsResponseCache.keys().next().value);
116
+ }
117
+ return entry;
118
+ }
119
+
120
+ function getCachedSessionsResponse(key) {
121
+ const entry = sessionsResponseCache.get(key);
122
+ if (!entry) return null;
123
+ if (entry.expiresAt <= Date.now()) {
124
+ sessionsResponseCache.delete(key);
125
+ return null;
126
+ }
127
+ return entry;
128
+ }
129
+
130
+ const gzipAsync = promisify(zlib.gzip);
131
+ const brotliCompressAsync = promisify(zlib.brotliCompress);
132
+
133
+ async function sendSessionsResponse(req, res, entry) {
134
+ const { encoding: accepted, identityAllowed } = negotiateEncoding(req.headers["accept-encoding"]);
135
+ const notAcceptable = () => {
136
+ const message = "Not Acceptable";
137
+ res.writeHead(406, {
138
+ "Content-Type": "text/plain; charset=utf-8",
139
+ "Content-Length": Buffer.byteLength(message),
140
+ "Cache-Control": "no-store",
141
+ "Vary": "Accept-Encoding",
142
+ });
143
+ res.end(message);
144
+ };
145
+ if (!accepted && !identityAllowed) {
146
+ notAcceptable();
147
+ return;
148
+ }
149
+ let body = entry.raw;
150
+ let encoding = null;
151
+ if (entry.raw.length > 32 * 1024 && accepted === "br") {
152
+ try {
153
+ entry.br ||= brotliCompressAsync(entry.raw, { params: { [zlib.constants.BROTLI_PARAM_QUALITY]: 5 } });
154
+ body = await entry.br;
155
+ encoding = "br";
156
+ } catch (error) {
157
+ entry.br = null;
158
+ console.warn("[local-api] sessions brotli failed, serving identity:", error?.message || error);
159
+ }
160
+ } else if (entry.raw.length > 32 * 1024 && accepted === "gzip") {
161
+ try {
162
+ entry.gzip ||= gzipAsync(entry.raw);
163
+ body = await entry.gzip;
164
+ encoding = "gzip";
165
+ } catch (error) {
166
+ entry.gzip = null;
167
+ console.warn("[local-api] sessions gzip failed, serving identity:", error?.message || error);
168
+ }
169
+ }
170
+ if (!encoding && !identityAllowed) {
171
+ notAcceptable();
172
+ return;
173
+ }
174
+ const headers = {
175
+ "Content-Type": "application/json",
176
+ "Content-Length": body.length,
177
+ "Cache-Control": "no-store",
178
+ "X-Content-Type-Options": "nosniff",
179
+ "Vary": "Accept-Encoding",
180
+ };
181
+ if (encoding) headers["Content-Encoding"] = encoding;
182
+ res.writeHead(200, headers);
183
+ res.end(body);
184
+ }
185
+
57
186
  // ---------------------------------------------------------------------------
58
187
  // Per-model pricing — delegated to src/lib/pricing/
59
188
  // - CURATED overrides (kiro-*, hy3-*, composer-*, kimi-for-coding, etc.)
@@ -65,6 +194,7 @@ const {
65
194
  MODEL_PRICING,
66
195
  getModelPricing,
67
196
  computeRowCost,
197
+ computeRowTierCosts,
68
198
  ensurePricingLoaded,
69
199
  getPricingRevision,
70
200
  getModelPricingInfo,
@@ -100,6 +230,19 @@ const CLAUDE_MEM_OBSERVER_PROJECT_KEY = deriveProjectKeyFromRef(
100
230
  CLAUDE_MEM_OBSERVER_PROJECT_REF,
101
231
  );
102
232
  const PROJECT_USAGE_MAX_ENTRIES = 10;
233
+
234
+ // Speed-tier subset columns surfaced by /usage-model-breakdown (issue 886):
235
+ // the Codex parser persists these on queue rows as annotations of the base
236
+ // columns ("how much of this row came from Fast/Ultrafast requests"), so the
237
+ // model breakdown can sum them per model without re-adding volume.
238
+ const SPEED_TIER_SUBSET_PREFIXES = ["priority_", "ultrafast_"];
239
+ const SPEED_TIER_SUBSET_TOKEN_FIELDS = [
240
+ "input_tokens",
241
+ "cached_input_tokens",
242
+ "cache_creation_input_tokens",
243
+ "output_tokens",
244
+ "reasoning_output_tokens",
245
+ ];
103
246
  const MAX_TIME_ZONE_CACHE_ENTRIES = 16;
104
247
  const AGGREGATION_BATCH_ROWS = 256;
105
248
 
@@ -1676,34 +1819,38 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
1676
1819
  return hasAllowedLoopbackOrigin(req?.headers || {});
1677
1820
  }
1678
1821
 
1679
- return async function handleLocalApi(req, res, url) {
1680
- const p = url.pathname;
1681
- // status.deepseek.com has no public JSON API or CORS, so the dashboard
1682
- // cannot probe it like the other providers. Relay it through the local app:
1683
- // fetchProviderServiceStatus("deepseek") fetches + parses the server-rendered
1684
- // page (with a 5-minute cache and 6s timeout in provider-status.js) and we
1685
- // return a Statuspage.io-shaped payload the existing dashboard parser reads.
1686
- if (p === "/api/deepseek-status") {
1687
- const method = String(req.method || "GET").toUpperCase();
1688
- if (method !== "GET" && method !== "HEAD") {
1689
- json(res, { error: "Method Not Allowed" }, 405);
1822
+ // status.deepseek.com (no public JSON API) and status.opencode.ai (official
1823
+ // API but no CORS headers) are both unreadable from the browser, so the
1824
+ // dashboard probes them through these same-origin relays instead. The
1825
+ // server-side fetch/parse/cache logic lives in provider-status.js (6s
1826
+ // timeout, 5-minute cache, last-good fallback); a failed probe surfaces as
1827
+ // 502 and the card renders Unreachable.
1828
+ const relayProviderStatus = (providerId, label) => async (req, res) => {
1829
+ const method = String(req.method || "GET").toUpperCase();
1830
+ if (method !== "GET" && method !== "HEAD") {
1831
+ json(res, { error: "Method Not Allowed" }, 405);
1832
+ return true;
1833
+ }
1834
+ try {
1835
+ const reading = await fetchProviderServiceStatus(providerId);
1836
+ if (!reading) {
1837
+ json(res, { error: `${label} status unavailable` }, 502);
1690
1838
  return true;
1691
1839
  }
1692
- try {
1693
- const reading = await fetchProviderServiceStatus("deepseek");
1694
- if (!reading) {
1695
- json(res, { error: "DeepSeek status unavailable" }, 502);
1696
- return true;
1697
- }
1698
- json(res, {
1699
- page: { updated_at: reading.updated_at },
1700
- status: { indicator: reading.indicator, description: reading.description || "" },
1701
- });
1702
- } catch (error) {
1703
- json(res, { error: `DeepSeek status probe error: ${error?.message || error}` }, 502);
1704
- }
1705
- return true;
1840
+ json(res, {
1841
+ page: { updated_at: reading.updated_at },
1842
+ status: { indicator: reading.indicator, description: reading.description || "" },
1843
+ });
1844
+ } catch (error) {
1845
+ json(res, { error: `${label} status probe error: ${error?.message || error}` }, 502);
1706
1846
  }
1847
+ return true;
1848
+ };
1849
+
1850
+ return async function handleLocalApi(req, res, url) {
1851
+ const p = url.pathname;
1852
+ if (p === "/api/deepseek-status") return relayProviderStatus("deepseek", "DeepSeek")(req, res);
1853
+ if (p === "/api/opencode-status") return relayProviderStatus("opencode", "OpenCode")(req, res);
1707
1854
  if (p === "/api/local-usage") {
1708
1855
  if (!isAuthorizedLocalMutation(req)) { json(res, { code: "unauthorized" }, 401); return true; }
1709
1856
  try {
@@ -2609,7 +2756,9 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
2609
2756
  const refresh = ["1", "true"].includes(url.searchParams.get("refresh"));
2610
2757
  try {
2611
2758
  const { buildSessionAnalytics, summarizeSessions, sessionsToCsv } = require("./session-analytics");
2612
- const sessions = await buildSessionAnalytics({ force: refresh });
2759
+ // Refresh means "re-walk the session files", not "re-parse everything":
2760
+ // recheck skips the TTL but keeps per-file stat reuse.
2761
+ const sessions = await buildSessionAnalytics({ recheck: refresh });
2613
2762
  const wantsCsv = url.searchParams.get("format") === "csv";
2614
2763
  const includeSessions = wantsCsv || ["1", "true"].includes(url.searchParams.get("include_sessions"));
2615
2764
  const result = summarizeSessions(sessions, { from, to, includeSessions, ...getTimeZoneContext(url) });
@@ -2639,16 +2788,48 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
2639
2788
  const refresh = ["1", "true"].includes(url.searchParams.get("refresh"));
2640
2789
  const limitParam = parseInt(url.searchParams.get("limit") || "0", 10);
2641
2790
  const limit = Number.isFinite(limitParam) && limitParam > 0 ? Math.min(limitParam, 2000) : 0;
2791
+ const includeBuckets = !["0", "false"].includes(String(url.searchParams.get("include_buckets") || "").toLowerCase());
2792
+ // The list page renders one model name plus a token/cost/performance
2793
+ // triple per model; the remaining per-model counters (~39 of 44 fields)
2794
+ // are not read by the dashboard, so the browser can opt out of them.
2795
+ const includeModelDetail = !["0", "false"].includes(String(url.searchParams.get("include_model_detail") || "").toLowerCase());
2642
2796
  // This payload contains local paths and resumable session identifiers.
2643
2797
  // The server is loopback-only; additionally prevent browser/proxy caches
2644
2798
  // from retaining the response after the page is closed.
2645
2799
  res.setHeader("Cache-Control", "no-store");
2646
2800
  res.setHeader("X-Content-Type-Options", "nosniff");
2647
2801
  try {
2648
- const { buildSessionAnalytics, listSessionsForBrowser } = require("./session-analytics");
2649
- const sessions = await buildSessionAnalytics({ force: refresh });
2650
- const result = listSessionsForBrowser(sessions, { from, to, limit, ...getTimeZoneContext(url) });
2651
- json(res, { from, to, ...result });
2802
+ const { buildSessionAnalytics, listSessionsForBrowser, resolveSessionSidecarPath } = require("./session-analytics");
2803
+ const timeZoneContext = getTimeZoneContext(url);
2804
+ const cacheParams = {
2805
+ sidecarPath: resolveSessionSidecarPath(os.homedir()),
2806
+ from,
2807
+ to,
2808
+ limit,
2809
+ includeBuckets,
2810
+ includeModelDetail,
2811
+ ...timeZoneContext,
2812
+ };
2813
+ const beforeCacheKey = sessionsResponseCacheKey(cacheParams);
2814
+ const cacheKey = refresh ? null : beforeCacheKey;
2815
+ const cached = cacheKey ? getCachedSessionsResponse(cacheKey) : null;
2816
+ if (cached) {
2817
+ await sendSessionsResponse(req, res, cached);
2818
+ return true;
2819
+ }
2820
+ const sessions = await buildSessionAnalytics({ recheck: refresh });
2821
+ const result = listSessionsForBrowser(sessions, { from, to, limit, includeBuckets, includeModelDetail, ...timeZoneContext });
2822
+ const afterBuildCacheKey = sessionsResponseCacheKey(cacheParams);
2823
+ const body = JSON.stringify({ from, to, ...result });
2824
+ const afterSerializeCacheKey = sessionsResponseCacheKey(cacheParams);
2825
+ const incomplete = Boolean(sessions.sidecarReadFailed) ||
2826
+ (Array.isArray(sessions.incompleteDirs) && sessions.incompleteDirs.length > 0) ||
2827
+ result.skipped_files !== 0;
2828
+ const cacheable = Boolean(afterBuildCacheKey) &&
2829
+ afterBuildCacheKey === afterSerializeCacheKey &&
2830
+ !incomplete;
2831
+ const entry = rememberSessionsResponse(cacheable ? afterBuildCacheKey : null, Buffer.from(body));
2832
+ await sendSessionsResponse(req, res, entry);
2652
2833
  } catch (error) {
2653
2834
  // Node fs errors embed the absolute path ("EACCES ... open '/Users/…'").
2654
2835
  // Keep that out of the HTTP body and log it locally instead.
@@ -2779,6 +2960,18 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
2779
2960
  // row is intact. Summing the base columns first and pricing once silently
2780
2961
  // dropped every premium — same tokens, fewer dollars than the hero.
2781
2962
  ma.totals.total_cost_usd = Number(ma.totals.total_cost_usd || 0) + computeRowCost(row);
2963
+ // Surface the Fast/Ultrafast split (issue 886): per-tier cost allocation
2964
+ // plus the observed subset token columns. Tier costs and the implicit
2965
+ // standard tier (total minus both) always add up to total_cost_usd.
2966
+ const tierCosts = computeRowTierCosts(row);
2967
+ ma.totals.priority_cost_usd = Number(ma.totals.priority_cost_usd || 0) + tierCosts.priority;
2968
+ ma.totals.ultrafast_cost_usd = Number(ma.totals.ultrafast_cost_usd || 0) + tierCosts.ultrafast;
2969
+ for (const prefix of SPEED_TIER_SUBSET_PREFIXES) {
2970
+ for (const field of SPEED_TIER_SUBSET_TOKEN_FIELDS) {
2971
+ const key = `${prefix}${field}`;
2972
+ ma.totals[key] = (ma.totals[key] || 0) + (Number(row[key]) || 0);
2973
+ }
2974
+ }
2782
2975
  }
2783
2976
  if (pricingRevision !== getPricingRevision()) return handleLocalApi(req, res, url);
2784
2977
 
@@ -2786,7 +2979,12 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
2786
2979
  s.models = Array.from(s.models.values())
2787
2980
  .map((m) => ({
2788
2981
  ...m,
2789
- totals: { ...m.totals, total_cost_usd: Number(m.totals.total_cost_usd || 0).toFixed(6) },
2982
+ totals: {
2983
+ ...m.totals,
2984
+ total_cost_usd: Number(m.totals.total_cost_usd || 0).toFixed(6),
2985
+ priority_cost_usd: Number(m.totals.priority_cost_usd || 0).toFixed(6),
2986
+ ultrafast_cost_usd: Number(m.totals.ultrafast_cost_usd || 0).toFixed(6),
2987
+ },
2790
2988
  }))
2791
2989
  .sort((a, b) => b.totals.total_tokens - a.totals.total_tokens);
2792
2990
  const sourceCost = s.models.reduce((sum, m) => sum + Number(m.totals.total_cost_usd), 0);
@@ -0,0 +1,88 @@
1
+ // OpenCode service-status probe.
2
+ //
3
+ // status.opencode.ai/api/status is OpenCode's official status feed — a custom
4
+ // JSON schema served without CORS headers, so the browser cannot read it and
5
+ // the local app relays it. Shape (verified against the live feed 2026-11-01):
6
+ // { updatedAt: <ms epoch>, since, stale, status: "operational",
7
+ // components: [{ service, status, reason, models, breakdown, speed }, ...] }
8
+ //
9
+ // Mapping to the Statuspage.io contract:
10
+ // operational -> none
11
+ // degraded / maintenance -> minor
12
+ // partial_outage -> major
13
+ // major / full outage -> critical
14
+ // Unknown non-operational statuses degrade to minor: report "something is
15
+ // wrong" rather than a false green. The reading is the worst of the top-level
16
+ // status and every component; anything we cannot parse throws, so callers fall
17
+ // back to last-good/unknown instead of inventing a false "operational".
18
+
19
+ const OPENCODE_STATUS_TO_INDICATOR = {
20
+ operational: "none",
21
+ degraded: "minor",
22
+ degraded_performance: "minor",
23
+ maintenance: "minor",
24
+ under_maintenance: "minor",
25
+ partial_outage: "major",
26
+ major_outage: "critical",
27
+ full_outage: "critical",
28
+ outage: "critical",
29
+ };
30
+
31
+ const INDICATOR_RANK = { none: 0, minor: 1, major: 2, critical: 3 };
32
+
33
+ function statusToIndicator(status) {
34
+ return OPENCODE_STATUS_TO_INDICATOR[String(status || "").toLowerCase()] || "minor";
35
+ }
36
+
37
+ /**
38
+ * Parse the api/status payload into the Statuspage.io shape:
39
+ * { status: { indicator, description }, page: { updated_at } }
40
+ * Throws when the payload is not understood.
41
+ */
42
+ function parseOpenCodeStatusPayload(payload) {
43
+ if (!payload || typeof payload !== "object" || Array.isArray(payload)) {
44
+ throw new Error("OpenCode status: expected a JSON object");
45
+ }
46
+ if (typeof payload.status !== "string" || payload.status.length === 0) {
47
+ throw new Error("OpenCode status: missing top-level status string");
48
+ }
49
+ const components = payload.components === undefined ? [] : payload.components;
50
+ if (!Array.isArray(components)) {
51
+ throw new Error("OpenCode status: components is not an array");
52
+ }
53
+
54
+ let worstIndicator = statusToIndicator(payload.status);
55
+ let worstRank = INDICATOR_RANK[worstIndicator];
56
+ const affected = [];
57
+ for (const component of components) {
58
+ // A null/primitive entry is not skippable: it is malformed, and swallowing
59
+ // it would let an otherwise-operational payload read green (the header
60
+ // contract throws rather than inventing "operational").
61
+ if (!component || typeof component !== "object" ||
62
+ typeof component.service !== "string" || typeof component.status !== "string") {
63
+ throw new Error("OpenCode status: malformed component entry");
64
+ }
65
+ const indicator = statusToIndicator(component.status);
66
+ if (INDICATOR_RANK[indicator] > worstRank) {
67
+ worstRank = INDICATOR_RANK[indicator];
68
+ worstIndicator = indicator;
69
+ }
70
+ if (indicator !== "none" && component.service) affected.push(component.service);
71
+ }
72
+
73
+ const updatedAt = typeof payload.updatedAt === "number" && Number.isFinite(payload.updatedAt) && payload.updatedAt > 0
74
+ ? new Date(payload.updatedAt).toISOString()
75
+ : undefined;
76
+
77
+ return {
78
+ status: {
79
+ indicator: worstIndicator,
80
+ description: affected.join("; "),
81
+ },
82
+ page: { updated_at: updatedAt },
83
+ };
84
+ }
85
+
86
+ module.exports = {
87
+ parseOpenCodeStatusPayload,
88
+ };