tokentracker-cli 0.94.0 → 0.95.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 (130) hide show
  1. package/README.de.md +9 -7
  2. package/README.ja.md +8 -6
  3. package/README.ko.md +8 -6
  4. package/README.md +23 -10
  5. package/README.zh-CN.md +8 -6
  6. package/dashboard/dist/assets/{AchievementBadge-DPqwk2zI.js → AchievementBadge-Pi3Gvbm8.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-CELHoMQt.js → AchievementsPage-C4WLT8Rl.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-DdAC0VCb.js → AchievementsSection-ilvR2ssr.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-Dd4I1SpM.js → BadgeDetailModal-CjEeToxa.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-DL2-4mnm.js → BotAnimated-DDf9pBuK.js} +1 -1
  11. package/dashboard/dist/assets/{Card-B_MpSRse.js → Card-DNous84-.js} +1 -1
  12. package/dashboard/dist/assets/{ClawdAnimated-BLZZ4rG6.js → ClawdAnimated-BLMIRntb.js} +2 -2
  13. package/dashboard/dist/assets/{CommandPalette-B-vfdIbH.js → CommandPalette-vVekhoFO.js} +1 -1
  14. package/dashboard/dist/assets/CommunityStatsModal-C0Kp_J8B.js +1 -0
  15. package/dashboard/dist/assets/{ConfirmModal-DLnRwUXC.js → ConfirmModal-DFdpcx12.js} +1 -1
  16. package/dashboard/dist/assets/{Controls-FZ8wEN5w.js → Controls-RNhqybmE.js} +1 -1
  17. package/dashboard/dist/assets/DashboardPage-BQjQh1IQ.js +1 -0
  18. package/dashboard/dist/assets/{DevicePage-D2IzPc5o.js → DevicePage-DIhh-YAk.js} +1 -1
  19. package/dashboard/dist/assets/{DialogClose-BaqQaUUk.js → DialogClose-ZqxYVIzl.js} +1 -1
  20. package/dashboard/dist/assets/DialogDescription-DDM4aanX.js +1 -0
  21. package/dashboard/dist/assets/{DialogTitle-DrbKjtuS.js → DialogTitle-BiRcejlu.js} +1 -1
  22. package/dashboard/dist/assets/{FadeIn-GddKDE8i.js → FadeIn-BDBQpeGe.js} +1 -1
  23. package/dashboard/dist/assets/{HeaderGithubStar-DUu-Z5mE.js → HeaderGithubStar-CztIwtKT.js} +1 -1
  24. package/dashboard/dist/assets/{HoverTooltip-BmDkY35A.js → HoverTooltip-Cfso7LTC.js} +1 -1
  25. package/dashboard/dist/assets/{IpCheckPage-CGJyqWk2.js → IpCheckPage-CNE7dLs3.js} +1 -1
  26. package/dashboard/dist/assets/{LandingPage-Bd91ALUj.js → LandingPage-B2a3cweM.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardAvatar-BGGUwXFd.js → LeaderboardAvatar-CwmZnIji.js} +1 -1
  28. package/dashboard/dist/assets/{LeaderboardPage-BMEqHwif.js → LeaderboardPage-DkTHj3fB.js} +2 -2
  29. package/dashboard/dist/assets/{LeaderboardProfileModal-BSweJqm2.js → LeaderboardProfileModal-CefTO3Zh.js} +3 -3
  30. package/dashboard/dist/assets/{LeaderboardProfilePage-CRrI3RFz.js → LeaderboardProfilePage-CnOXlQ3L.js} +1 -1
  31. package/dashboard/dist/assets/{LimitsPage-XIRDv46K.js → LimitsPage-BIZ1-6BB.js} +2 -2
  32. package/dashboard/dist/assets/{LocalOnlyNotice-CCD-lGWG.js → LocalOnlyNotice-CdHQiPhy.js} +1 -1
  33. package/dashboard/dist/assets/{LoginCard-lDuqSv4f.js → LoginCard-yXLA9geu.js} +1 -1
  34. package/dashboard/dist/assets/{LoginModal-DStt5VXN.js → LoginModal-BF2Pfkqa.js} +1 -1
  35. package/dashboard/dist/assets/{LoginPage-dmVPSL72.js → LoginPage-DuOypZap.js} +1 -1
  36. package/dashboard/dist/assets/{PetPage-ybcZRctF.js → PetPage-BH3xDVV9.js} +1 -1
  37. package/dashboard/dist/assets/{PopoverPopup-Dar_UjxF.js → PopoverPopup-CewF_cGz.js} +1 -1
  38. package/dashboard/dist/assets/{ProviderIcon-Ct9tILpz.js → ProviderIcon-Dg0K0RPn.js} +1 -1
  39. package/dashboard/dist/assets/{ResetPasswordPage-B4yLh5zx.js → ResetPasswordPage-CYwt85O3.js} +1 -1
  40. package/dashboard/dist/assets/{SegmentedControl-BOE1CEzq.js → SegmentedControl-BJkXXomQ.js} +1 -1
  41. package/dashboard/dist/assets/{Select-D-bSh7Zz.js → Select-B3_WH0Qe.js} +1 -1
  42. package/dashboard/dist/assets/{SelectItemText-DMYjXXq4.js → SelectItemText-BUby_DUu.js} +1 -1
  43. package/dashboard/dist/assets/{ServiceStatusPage-CaleS-6_.js → ServiceStatusPage-BDlVZk4i.js} +1 -1
  44. package/dashboard/dist/assets/SessionsPage-TaC9nahK.js +2 -0
  45. package/dashboard/dist/assets/{SettingsPage-iJH0Yyah.js → SettingsPage-CudcQh6m.js} +2 -2
  46. package/dashboard/dist/assets/SkillsPage-iDLQ8lpZ.js +1 -0
  47. package/dashboard/dist/assets/{TokenGalaxy-CuXKoMPg.js → TokenGalaxy-DtYzUsXU.js} +1 -1
  48. package/dashboard/dist/assets/{ToolbarRootContext-Dx5GB7F5.js → ToolbarRootContext-PC8Tg1Jz.js} +1 -1
  49. package/dashboard/dist/assets/TrendMonitor-r8n8YO_9.js +19 -0
  50. package/dashboard/dist/assets/{WidgetsPage-DUaIyGtu.js → WidgetsPage-vzfGccWb.js} +1 -1
  51. package/dashboard/dist/assets/{WrappedPage-BzqlcAUC.js → WrappedPage-DaKuIkYE.js} +1 -1
  52. package/dashboard/dist/assets/{__vite-browser-external-BHz2N7Ih.css → __vite-browser-external-CCMdjCX2.css} +1 -1
  53. package/dashboard/dist/assets/__vite-browser-external-Dnxl50k1.js +45 -0
  54. package/dashboard/dist/assets/{agent-logos-BdWWxza6.js → agent-logos-B7nzn3n2.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-CANSzmm-.js → arrow-up-right-CsxyvCER.js} +1 -1
  56. package/dashboard/dist/assets/{check-BnJpzXRZ.js → check-hYcC7MdH.js} +1 -1
  57. package/dashboard/dist/assets/{composite-Ccj6KQmh.js → composite-C79LNbex.js} +1 -1
  58. package/dashboard/dist/assets/{copy-C2Yc8Evu.js → copy-Cz4qzE62.js} +1 -1
  59. package/dashboard/dist/assets/{download-B52JTk6Y.js → download-CUaSNE2E.js} +1 -1
  60. package/dashboard/dist/assets/{external-link-DayTwZx-.js → external-link-vq7kIyKK.js} +1 -1
  61. package/dashboard/dist/assets/{flame-B8GEX2yT.js → flame-CB6K4eR6.js} +1 -1
  62. package/dashboard/dist/assets/{format-tokens-mhkbohkb.js → format-tokens-KI-e_JQ-.js} +1 -1
  63. package/dashboard/dist/assets/{icons-o8bPXqRP.js → icons-3maItkrp.js} +1 -1
  64. package/dashboard/dist/assets/{index-MBVWUKpA.js → index-BRXoZ7Jj.js} +1 -1
  65. package/dashboard/dist/assets/{index-CK_CduLm.js → index-C5WrfmLB.js} +1 -1
  66. package/dashboard/dist/assets/{index-Bp0ZHDtz.js → index-CUbC50Zp.js} +1 -1
  67. package/dashboard/dist/assets/{info-D94QM_vX.js → info-r0yGihUe.js} +1 -1
  68. package/dashboard/dist/assets/{limits-providers-BLzMyCiy.js → limits-providers-BN_94oe9.js} +1 -1
  69. package/dashboard/dist/assets/{link-2-CqBmFaDI.js → link-2-J9t0VXWM.js} +1 -1
  70. package/dashboard/dist/assets/{loader-circle-SaxUpDR6.js → loader-circle-DCvOsPze.js} +1 -1
  71. package/dashboard/dist/assets/{maximize-2-BoXqARks.js → maximize-2-CZ9JhGjf.js} +1 -1
  72. package/dashboard/dist/assets/{modifiers.esm-Conh_-BD.js → modifiers.esm-DJ0_WKg0.js} +1 -1
  73. package/dashboard/dist/assets/module-xpSFyNBK.js +3 -0
  74. package/dashboard/dist/assets/{monitor-smartphone-D6qLR9qd.js → monitor-smartphone-nqbWjJx_.js} +1 -1
  75. package/dashboard/dist/assets/{plus-D51mjIc2.js → plus-Cg7wd2jK.js} +1 -1
  76. package/dashboard/dist/assets/{provider-display-DpHPjd_s.js → provider-display-B9L1nRY9.js} +2 -2
  77. package/dashboard/dist/assets/{react-D_AcI466.js → react-D2AD6Pk2.js} +1 -1
  78. package/dashboard/dist/assets/{search-CwLcNdvZ.js → search-uNiWba1h.js} +1 -1
  79. package/dashboard/dist/assets/{skills-api-Dqi7Mjgo.js → skills-api-BBYuAhiL.js} +1 -1
  80. package/dashboard/dist/assets/{sortable.esm-BPDlUxPb.js → sortable.esm-CzPxc949.js} +1 -1
  81. package/dashboard/dist/assets/{store-ikeRIRGd.js → store-D4A7EUwE.js} +1 -1
  82. package/dashboard/dist/assets/{terminal-BfeX3L5T.js → terminal-Crmm_yKF.js} +1 -1
  83. package/dashboard/dist/assets/{trash-2-5_Qlp1k7.js → trash-2-ia9VjRnU.js} +1 -1
  84. package/dashboard/dist/assets/{use-community-stats-D90Fy2_j.js → use-community-stats-KaGCAxaF.js} +1 -1
  85. package/dashboard/dist/assets/use-latest-request-guard-Dy8gSDkk.js +1 -0
  86. package/dashboard/dist/assets/{use-limits-display-prefs-D4BYFaa-.js → use-limits-display-prefs-JSTYlbTD.js} +1 -1
  87. package/dashboard/dist/assets/{use-ordered-list-DJxLNdXY.js → use-ordered-list-C4JnHdE0.js} +1 -1
  88. package/dashboard/dist/assets/use-reduced-motion-AQE0ummK.js +1 -0
  89. package/dashboard/dist/assets/{use-session-efficiency-pref-CAX_GqXc.js → use-session-efficiency-pref-CgyUz5Gc.js} +1 -1
  90. package/dashboard/dist/assets/{use-transform-CeW6Z5R0.js → use-transform-C7-jSJ9k.js} +1 -1
  91. package/dashboard/dist/assets/use-usage-limits-B3YYqn-v.js +1 -0
  92. package/dashboard/dist/assets/{useCurrency-BarGeFyK.js → useCurrency-CK_gcb3J.js} +1 -1
  93. package/dashboard/dist/assets/{useTokenFormat-DUJ9iT9H.js → useTokenFormat-C_a-UkCA.js} +1 -1
  94. package/dashboard/dist/assets/{zap--m0IWoI3.js → zap-BIJNn-Ic.js} +1 -1
  95. package/dashboard/dist/brand-logos/lmstudio.svg +12 -0
  96. package/dashboard/dist/brand-logos/unsloth.svg +1 -0
  97. package/dashboard/dist/index.html +16 -12
  98. package/dashboard/dist/ip-check.html +2 -2
  99. package/dashboard/dist/leaderboard.html +2 -2
  100. package/dashboard/dist/llms.txt +4 -2
  101. package/dashboard/dist/share.html +2 -2
  102. package/package.json +2 -2
  103. package/src/commands/device-login.js +5 -10
  104. package/src/commands/init.js +28 -1
  105. package/src/commands/serve.js +6 -0
  106. package/src/commands/status.js +24 -0
  107. package/src/commands/sync.js +114 -18
  108. package/src/lib/cloud-account.js +102 -29
  109. package/src/lib/fs.js +30 -0
  110. package/src/lib/grok-context-breakdown.js +24 -33
  111. package/src/lib/grok-usage.js +95 -0
  112. package/src/lib/local-api.js +101 -13
  113. package/src/lib/pricing/index.js +15 -0
  114. package/src/lib/pricing/matcher.js +15 -0
  115. package/src/lib/pricing/seed-snapshot.json +1 -1
  116. package/src/lib/rollout.js +1094 -72
  117. package/src/lib/session-analytics.js +100 -40
  118. package/src/lib/usage-limits.js +507 -55
  119. package/src/lib/zcode-limits.js +65 -8
  120. package/dashboard/dist/assets/CommunityStatsModal-aAico565.js +0 -1
  121. package/dashboard/dist/assets/DashboardPage-BByRBsbt.js +0 -1
  122. package/dashboard/dist/assets/DialogDescription-BUrh0Jod.js +0 -1
  123. package/dashboard/dist/assets/SessionsPage-DOaY6_E9.js +0 -2
  124. package/dashboard/dist/assets/SkillsPage-BkzJOBLk.js +0 -1
  125. package/dashboard/dist/assets/TrendMonitor-BuKmsznP.js +0 -19
  126. package/dashboard/dist/assets/__vite-browser-external-CVIGknXl.js +0 -45
  127. package/dashboard/dist/assets/chevron-right-DJm0mbG0.js +0 -1
  128. package/dashboard/dist/assets/module-D2D5r9gC.js +0 -3
  129. package/dashboard/dist/assets/use-reduced-motion-yE3T3cat.js +0 -1
  130. package/dashboard/dist/assets/use-usage-limits-5HP8k9a3.js +0 -1
@@ -0,0 +1,95 @@
1
+ "use strict";
2
+
3
+ // Grok's ACP turn_completed.usage uses camelCase fields. In that shape,
4
+ // inputTokens is the whole prompt (cache reads/writes included) and
5
+ // outputTokens includes reasoningTokens. TokenTracker stores mutually
6
+ // exclusive columns, so both reported totals need to be split before pricing.
7
+ //
8
+ // Grok's headless/CLI summaries may use snake_case fields whose input_tokens is
9
+ // already the non-cached portion. Keep the two shapes distinct instead of
10
+ // subtracting cache tokens twice.
11
+
12
+ const USD_TICKS_PER_USD = 10_000_000_000;
13
+
14
+ function nonNegative(value) {
15
+ const n = Number(value);
16
+ return Number.isFinite(n) && n > 0 ? n : 0;
17
+ }
18
+
19
+ function firstBoolean(...values) {
20
+ for (const value of values) {
21
+ if (typeof value === "boolean") return value;
22
+ }
23
+ return false;
24
+ }
25
+
26
+ function reportedCostUsd(usage, { costIsPartial, usageIsIncomplete }) {
27
+ if (costIsPartial || usageIsIncomplete) return null;
28
+
29
+ const ticks = Number(
30
+ usage.costUsdTicks ??
31
+ usage.totalCostUsdTicks ??
32
+ usage.cost_usd_ticks ??
33
+ usage.total_cost_usd_ticks,
34
+ );
35
+ if (Number.isFinite(ticks) && ticks >= 0) return ticks / USD_TICKS_PER_USD;
36
+
37
+ const usd = Number(usage.costUsd ?? usage.totalCostUsd ?? usage.cost_usd ?? usage.total_cost_usd);
38
+ return Number.isFinite(usd) && usd >= 0 ? usd : null;
39
+ }
40
+
41
+ function normalizeGrokUsage(usage) {
42
+ if (!usage || typeof usage !== "object") return null;
43
+
44
+ const camelCaseInput = usage.inputTokens != null;
45
+ const reportedInput = nonNegative(usage.inputTokens ?? usage.input_tokens);
46
+ const cachedInput = nonNegative(
47
+ usage.cachedReadTokens ?? usage.cacheReadInputTokens ?? usage.cache_read_input_tokens ?? usage.cached_input_tokens,
48
+ );
49
+ const cacheCreationInput = nonNegative(
50
+ usage.cacheCreationTokens ??
51
+ usage.cachedWriteTokens ??
52
+ usage.cacheWriteInputTokens ??
53
+ usage.cache_creation_input_tokens,
54
+ );
55
+ const rawOutput = nonNegative(usage.outputTokens ?? usage.output_tokens);
56
+ const reasoningOutput = nonNegative(usage.reasoningTokens ?? usage.reasoning_output_tokens);
57
+
58
+ const inputTokens = camelCaseInput
59
+ ? Math.max(0, reportedInput - cachedInput - cacheCreationInput)
60
+ : reportedInput;
61
+ const outputTokens = Math.max(0, rawOutput - reasoningOutput);
62
+
63
+ let totalTokens = nonNegative(usage.totalTokens ?? usage.total_tokens);
64
+ if (totalTokens <= 0) {
65
+ totalTokens = inputTokens
66
+ + cachedInput
67
+ + cacheCreationInput
68
+ + outputTokens
69
+ + reasoningOutput;
70
+ }
71
+ if (totalTokens <= 0) return null;
72
+
73
+ const costIsPartial = firstBoolean(usage.costIsPartial, usage.cost_is_partial);
74
+ const usageIsIncomplete = firstBoolean(usage.usageIsIncomplete, usage.usage_is_incomplete);
75
+
76
+ return {
77
+ input_tokens: inputTokens,
78
+ cached_input_tokens: cachedInput,
79
+ cache_creation_input_tokens: cacheCreationInput,
80
+ output_tokens: outputTokens,
81
+ reasoning_output_tokens: reasoningOutput,
82
+ total_tokens: totalTokens,
83
+ billable_total_tokens: totalTokens,
84
+ total_cost_usd: reportedCostUsd(usage, { costIsPartial, usageIsIncomplete }),
85
+ cost_is_partial: costIsPartial,
86
+ usage_is_incomplete: usageIsIncomplete,
87
+ model_calls: nonNegative(usage.modelCalls ?? usage.model_calls),
88
+ api_duration_ms: nonNegative(usage.apiDurationMs ?? usage.api_duration_ms),
89
+ };
90
+ }
91
+
92
+ module.exports = {
93
+ USD_TICKS_PER_USD,
94
+ normalizeGrokUsage,
95
+ };
@@ -14,6 +14,11 @@ const { accountSlugFor, fetchAccountUsage, mintAccessToken } = require("./cloud-
14
14
  const { getOrCreateMachineId, computeStableMachineId } = require("./machine-id");
15
15
 
16
16
  const SYNC_TIMEOUT_MS = 120_000;
17
+ // A failed account-view request should not stall every dashboard refresh
18
+ // while an expired session or unreachable backend keeps timing out. The first
19
+ // request still falls back normally; subsequent fan-out requests reuse that
20
+ // decision for a short cooldown and return local data immediately.
21
+ const ACCOUNT_VIEW_FAILURE_BACKOFF_MS = 2 * 60_000;
17
22
  const TRACKER_BIN = path.resolve(__dirname, "../../bin/tracker.js");
18
23
  const MAX_DEVICE_NAME_LENGTH = 128;
19
24
 
@@ -219,8 +224,12 @@ function normalizeQueueRow(row) {
219
224
  // non-zero billable usage (GitHub issue #106). Treat billing and usage as
220
225
  // orthogonal: bump billable up to total_tokens at read time so historical
221
226
  // queue.jsonl entries render correctly without requiring a file rewrite.
227
+ // Antigravity had the same symptom from e69e2746a (billable deleted during
228
+ // the O(N) refactoring, leaving 167 local rows with billable=0 until the
229
+ // parser fix). Apply the same read-time healing so upgraded installs show
230
+ // correct heatmap / rolling numbers without a queue rewrite.
222
231
  const sourceName = String(normalized.source || "").toLowerCase();
223
- if (sourceName === "cursor") {
232
+ if (sourceName === "cursor" || sourceName === "antigravity") {
224
233
  const totalTokens = Number(normalized.total_tokens || 0);
225
234
  const billable = Number(normalized.billable_total_tokens || 0);
226
235
  if (totalTokens > 0 && billable < totalTokens) {
@@ -1135,6 +1144,30 @@ function createLocalApiHandler({ queuePath }) {
1135
1144
  const cookiePath = path.join(trackerDataDir, "relay-cookies.json");
1136
1145
  const localSyncDeviceTokenCache = new Map();
1137
1146
  const localSyncDeviceTokenInflight = new Map();
1147
+ // A dashboard refresh fans out to several account-view endpoints. Keep the
1148
+ // circuit-breaker state per session instead of one global slot (a successful
1149
+ // request for one account must not clear another account's failure), and
1150
+ // suppress the duplicate fan-out while the first cloud probe is pending.
1151
+ const accountViewFailures = new Map();
1152
+ const accountViewInFlight = new Set();
1153
+
1154
+ function accountViewFailureUntil(failureKey) {
1155
+ const until = accountViewFailures.get(failureKey) || 0;
1156
+ if (until > Date.now()) return until;
1157
+ if (until) accountViewFailures.delete(failureKey);
1158
+ return 0;
1159
+ }
1160
+
1161
+ function rememberAccountViewFailure(failureKey) {
1162
+ accountViewFailures.set(failureKey, Date.now() + ACCOUNT_VIEW_FAILURE_BACKOFF_MS);
1163
+ // Refresh-token rotation or account switching can produce many keys over a
1164
+ // long-lived tray session. Bound the map without affecting the active key.
1165
+ while (accountViewFailures.size > 16) {
1166
+ const oldest = accountViewFailures.keys().next().value;
1167
+ if (!oldest || oldest === failureKey) break;
1168
+ accountViewFailures.delete(oldest);
1169
+ }
1170
+ }
1138
1171
 
1139
1172
  // Load persisted cookies on startup
1140
1173
  try {
@@ -1399,15 +1432,52 @@ function createLocalApiHandler({ queuePath }) {
1399
1432
  }
1400
1433
  }
1401
1434
 
1402
- // Returns "served" when the cross-device aggregate was written to `res`, or
1403
- // "fallthrough" when the caller should serve the local single-machine data.
1404
- // Any failure (not signed in, cloud sync off, network/auth error) falls
1405
- // through so the popover always renders something.
1435
+ // Why a *classified* fallback: the native popover keeps the last successful
1436
+ // account (cross-device) snapshot on screen when a cloud read fails
1437
+ // transiently, but must switch to this-machine data the moment the user signs
1438
+ // out or turns cloud sync off. Both used to look identical from the outside
1439
+ // (HTTP 200 + local payload + `X-TokenTracker-Account-View: 0`), so a single
1440
+ // timed-out read silently shrank Activity to one device until the next
1441
+ // manual sync. See ACCOUNT_FALLBACK_* below for the vocabulary.
1442
+ const ACCOUNT_FALLBACK_CLOUD_SYNC_OFF = "cloud-sync-off";
1443
+ const ACCOUNT_FALLBACK_SIGNED_OUT = "signed-out";
1444
+
1445
+ // Every transient reason is prefixed "transient-"; clients only need the
1446
+ // prefix, so new reasons can be added without a client change.
1447
+ function classifyAccountFallback(err) {
1448
+ if (!err) return "transient-error";
1449
+ if (err.name === "AbortError" || err.name === "TimeoutError" || err.code === "auth_timeout") {
1450
+ return "transient-timeout";
1451
+ }
1452
+ if (err.code === "auth_rejected" || err.code === "auth_invalid" || err.status === 401 || err.status === 403) {
1453
+ return "transient-auth";
1454
+ }
1455
+ if (err.code === "auth_network") return "transient-network";
1456
+ if (Number.isFinite(err.status) && err.status > 0) return "transient-upstream";
1457
+ return "transient-network";
1458
+ }
1459
+
1460
+ // Returns "served" when the cross-device aggregate was written to `res`,
1461
+ // otherwise a fallback reason (see above) telling the caller — and, through
1462
+ // the `X-TokenTracker-Account-Fallback` header, the popover — why the local
1463
+ // single-machine data is being served instead.
1406
1464
  async function tryServeAccountView(usageSlug, url, res) {
1407
- if (!getCloudSyncPref()) return "fallthrough";
1465
+ if (!getCloudSyncPref()) return ACCOUNT_FALLBACK_CLOUD_SYNC_OFF;
1408
1466
  const refreshToken = getRefreshTokenForCloud();
1409
- if (!refreshToken) return "fallthrough";
1467
+ if (!refreshToken) return ACCOUNT_FALLBACK_SIGNED_OUT;
1410
1468
  const runtime = resolveRuntimeConfig();
1469
+ const failureKey = `${runtime.baseUrl}\0${refreshToken}`;
1470
+ const requestKey = `${failureKey}\0${usageSlug}\0${url.searchParams.toString()}`;
1471
+ if (accountViewFailureUntil(failureKey) > Date.now()) {
1472
+ return "transient-backoff";
1473
+ }
1474
+ // Six native dashboard requests normally arrive together. If the cloud is
1475
+ // already being probed for this session, let the remaining requests use
1476
+ // their local fallback instead of opening more timeout-bound sockets.
1477
+ if (accountViewInFlight.has(requestKey)) {
1478
+ return "transient-inflight";
1479
+ }
1480
+ accountViewInFlight.add(requestKey);
1411
1481
  const envTimeout = process.env.TOKENTRACKER_HTTP_TIMEOUT_MS
1412
1482
  ? parseInt(process.env.TOKENTRACKER_HTTP_TIMEOUT_MS, 10)
1413
1483
  : 0;
@@ -1421,9 +1491,16 @@ function createLocalApiHandler({ queuePath }) {
1421
1491
  refreshToken,
1422
1492
  timeoutMs,
1423
1493
  });
1424
- if (!out || out.data == null) return "fallthrough";
1494
+ // `null` here means the slug has no cloud equivalent, or the session
1495
+ // vanished mid-request — a routing/session fact, not an outage.
1496
+ if (!out) return ACCOUNT_FALLBACK_SIGNED_OUT;
1497
+ if (out.data == null) {
1498
+ rememberAccountViewFailure(failureKey);
1499
+ return "transient-upstream";
1500
+ }
1425
1501
  if (out.rotatedRefreshToken) setRelayRefreshToken(out.rotatedRefreshToken);
1426
1502
  if (out.rotatedCsrfToken) setRelayCsrfToken(out.rotatedCsrfToken);
1503
+ accountViewFailures.delete(failureKey);
1427
1504
  res.writeHead(200, {
1428
1505
  "Content-Type": "application/json",
1429
1506
  "Cache-Control": "no-store",
@@ -1433,11 +1510,17 @@ function createLocalApiHandler({ queuePath }) {
1433
1510
  return "served";
1434
1511
  } catch (e) {
1435
1512
  // Signed in + cloud sync on, but the cloud read failed (offline, token
1436
- // rejected, edge error, or timeout). Fall back to local data rather than erroring.
1513
+ // rejected, edge error, or timeout). Serve local data rather than
1514
+ // erroring, but say so: this is a *temporary* downgrade and the client
1515
+ // must not treat it as the user's real data scope.
1516
+ const reason = classifyAccountFallback(e);
1437
1517
  if (resolveRuntimeConfig().debug) {
1438
- console.warn(`[LocalAPI] account view fallback for ${usageSlug}:`, e?.message || e);
1518
+ console.warn(`[LocalAPI] account view fallback (${reason}) for ${usageSlug}:`, e?.message || e);
1439
1519
  }
1440
- return "fallthrough";
1520
+ rememberAccountViewFailure(failureKey);
1521
+ return reason;
1522
+ } finally {
1523
+ accountViewInFlight.delete(requestKey);
1441
1524
  }
1442
1525
  }
1443
1526
 
@@ -2006,14 +2089,17 @@ function createLocalApiHandler({ queuePath }) {
2006
2089
  // --- account (cross-device) view proxy for the native popover ---
2007
2090
  // When ?account=1 and the user is signed in with cloud sync on, serve the
2008
2091
  // same cross-device aggregate the dashboard shows; otherwise tag the
2009
- // response (X-TokenTracker-Account-View: 0) so the popover knows it got
2010
- // local single-machine data, and fall through to the local handler below.
2092
+ // response (X-TokenTracker-Account-View: 0 plus an
2093
+ // X-TokenTracker-Account-Fallback reason) so the popover knows it got local
2094
+ // single-machine data and whether that is permanent (signed out / cloud
2095
+ // sync off) or transient, and fall through to the local handler below.
2011
2096
  if (url.searchParams.get("account") === "1") {
2012
2097
  const usageSlug = p.startsWith("/functions/") ? p.slice("/functions/".length) : "";
2013
2098
  if (accountSlugFor(usageSlug)) {
2014
2099
  const result = await tryServeAccountView(usageSlug, url, res);
2015
2100
  if (result === "served") return true;
2016
2101
  res.setHeader("X-TokenTracker-Account-View", "0");
2102
+ res.setHeader("X-TokenTracker-Account-Fallback", result);
2017
2103
  }
2018
2104
  }
2019
2105
 
@@ -2291,6 +2377,8 @@ function createLocalApiHandler({ queuePath }) {
2291
2377
  ma.totals.cached_input_tokens += row.cached_input_tokens || 0;
2292
2378
  ma.totals.cache_creation_input_tokens += row.cache_creation_input_tokens || 0;
2293
2379
  ma.totals.reasoning_output_tokens += row.reasoning_output_tokens || 0;
2380
+ ma.totals.total_cost_usd = Number(ma.totals.total_cost_usd || 0)
2381
+ + (Number(row.total_cost_usd) || 0);
2294
2382
  }
2295
2383
 
2296
2384
  const sources = Array.from(bySource.values()).map((s) => {
@@ -21,6 +21,11 @@ const PI_SUBSCRIPTION_SOURCES = new Set([
21
21
  "prime-agent-github-copilot",
22
22
  "prime-agent-copilot",
23
23
  ]);
24
+ // LM Studio's server logs describe inference served by the local developer
25
+ // server (including LM Link). Secure Cloud usage has a separate billing path
26
+ // and is not present in these logs.
27
+ const LOCAL_INFERENCE_SOURCES = new Set(["lmstudio"]);
28
+ const SOURCES_WITH_AUTHORITATIVE_COST = new Set(["grok"]);
24
29
  const SEED_SNAPSHOT_PATH = path.resolve(__dirname, "seed-snapshot.json");
25
30
  const DEEPSEEK_TIME_PRICED_MODELS = [
26
31
  "deepseek-v4-flash",
@@ -168,10 +173,20 @@ function getRowPricing(row) {
168
173
  // computeRowCost in src/lib/local-api.js. Moved here so vite mock + local
169
174
  // server share one source of truth.
170
175
  function computeRowCost(row) {
176
+ if (LOCAL_INFERENCE_SOURCES.has(String(row?.source || "").toLowerCase())) return 0;
171
177
  // Pi can route a turn through a subscription-backed Copilot provider. Pi's
172
178
  // usage record reports a zero marginal cost for those turns; do not
173
179
  // reinterpret the Claude model name as an Anthropic API bill.
174
180
  if (PI_SUBSCRIPTION_SOURCES.has(String(row?.source || "").toLowerCase())) return 0;
181
+ // Some providers (currently Grok) persist an exact server-reported cost on
182
+ // the usage bucket. Prefer it when positive; zero remains the legacy
183
+ // "unreported" sentinel and falls through to model pricing.
184
+ const reportedCost = Number(row?.total_cost_usd);
185
+ if (
186
+ SOURCES_WITH_AUTHORITATIVE_COST.has(row?.source) &&
187
+ Number.isFinite(reportedCost) &&
188
+ reportedCost > 0
189
+ ) return reportedCost;
175
190
  const pricing = getRowPricing(row);
176
191
  const reasoningIncludedInOutput = row.source === "codex" || row.source === "every-code";
177
192
  const reasoningCost = reasoningIncludedInOutput
@@ -164,6 +164,20 @@ function normalizeWorkbuddyModel(model) {
164
164
  return model;
165
165
  }
166
166
 
167
+ // Unsloth Studio can mix local engines and paid API providers in one database.
168
+ // The parser qualifies metered models with their provider and marks local,
169
+ // subscription-backed, or ambiguous custom routes as unpriced. Returning a
170
+ // sentinel here prevents the generic reverse-substring matcher from mistaking
171
+ // a locally hosted model name for the similarly named cloud SKU.
172
+ function normalizeUnslothModel(model) {
173
+ if (typeof model !== "string") return model;
174
+ const lower = model.trim().toLowerCase();
175
+ if (lower.startsWith("local/") || lower.startsWith("unpriced/")) {
176
+ return "__tokentracker_unpriced_unsloth_model__";
177
+ }
178
+ return model;
179
+ }
180
+
167
181
  // Per-source model-name normalizers, applied at pricing-lookup time only (the
168
182
  // raw model name is preserved for storage/display). Add a source here when its
169
183
  // model strings don't match the LiteLLM/curated keys verbatim.
@@ -174,6 +188,7 @@ const SOURCE_MODEL_NORMALIZERS = {
174
188
  "pi-anthropic": normalizeClaudeModel,
175
189
  "prime-agent-anthropic": normalizeClaudeModel,
176
190
  zed: normalizeZedModel,
191
+ unsloth: normalizeUnslothModel,
177
192
  workbuddy: normalizeWorkbuddyModel,
178
193
  };
179
194