tokentracker-cli 0.94.1 → 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 (126) 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-DnmgEa4q.js → AchievementBadge-Pi3Gvbm8.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-B7YTbAuK.js → AchievementsPage-C4WLT8Rl.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-CwgT5sqH.js → AchievementsSection-ilvR2ssr.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-Dets6jzW.js → BadgeDetailModal-CjEeToxa.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-5KwUvC9F.js → BotAnimated-DDf9pBuK.js} +1 -1
  11. package/dashboard/dist/assets/{Card-jf9w-4-N.js → Card-DNous84-.js} +1 -1
  12. package/dashboard/dist/assets/{ClawdAnimated-CGxDVQ0c.js → ClawdAnimated-BLMIRntb.js} +2 -2
  13. package/dashboard/dist/assets/{CommandPalette-B1bUUf32.js → CommandPalette-vVekhoFO.js} +1 -1
  14. package/dashboard/dist/assets/CommunityStatsModal-C0Kp_J8B.js +1 -0
  15. package/dashboard/dist/assets/{ConfirmModal-BR-aIRH9.js → ConfirmModal-DFdpcx12.js} +1 -1
  16. package/dashboard/dist/assets/{Controls-CbHsJAqV.js → Controls-RNhqybmE.js} +1 -1
  17. package/dashboard/dist/assets/DashboardPage-BQjQh1IQ.js +1 -0
  18. package/dashboard/dist/assets/{DevicePage-DDlicLLn.js → DevicePage-DIhh-YAk.js} +1 -1
  19. package/dashboard/dist/assets/{DialogClose-B89uLXqp.js → DialogClose-ZqxYVIzl.js} +1 -1
  20. package/dashboard/dist/assets/DialogDescription-DDM4aanX.js +1 -0
  21. package/dashboard/dist/assets/{DialogTitle-jOyycYKF.js → DialogTitle-BiRcejlu.js} +1 -1
  22. package/dashboard/dist/assets/{FadeIn-BEND42ze.js → FadeIn-BDBQpeGe.js} +1 -1
  23. package/dashboard/dist/assets/{HeaderGithubStar-Dacl1WGS.js → HeaderGithubStar-CztIwtKT.js} +1 -1
  24. package/dashboard/dist/assets/{HoverTooltip-B2Mh2LTi.js → HoverTooltip-Cfso7LTC.js} +1 -1
  25. package/dashboard/dist/assets/{IpCheckPage-CPEuRhiU.js → IpCheckPage-CNE7dLs3.js} +1 -1
  26. package/dashboard/dist/assets/{LandingPage-BYWNR_dx.js → LandingPage-B2a3cweM.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardAvatar-B9_5LDVj.js → LeaderboardAvatar-CwmZnIji.js} +1 -1
  28. package/dashboard/dist/assets/{LeaderboardPage-_Hl1NaIa.js → LeaderboardPage-DkTHj3fB.js} +2 -2
  29. package/dashboard/dist/assets/{LeaderboardProfileModal-Cydp7CyA.js → LeaderboardProfileModal-CefTO3Zh.js} +3 -3
  30. package/dashboard/dist/assets/{LeaderboardProfilePage-Dz0ecujs.js → LeaderboardProfilePage-CnOXlQ3L.js} +1 -1
  31. package/dashboard/dist/assets/{LimitsPage-CmdT2qOZ.js → LimitsPage-BIZ1-6BB.js} +2 -2
  32. package/dashboard/dist/assets/{LocalOnlyNotice-DhuPGUkw.js → LocalOnlyNotice-CdHQiPhy.js} +1 -1
  33. package/dashboard/dist/assets/{LoginCard-DQtHaX_5.js → LoginCard-yXLA9geu.js} +1 -1
  34. package/dashboard/dist/assets/{LoginModal-DvpPRnYV.js → LoginModal-BF2Pfkqa.js} +1 -1
  35. package/dashboard/dist/assets/{LoginPage-DOjfmdwb.js → LoginPage-DuOypZap.js} +1 -1
  36. package/dashboard/dist/assets/{PetPage-Dd6U9fh2.js → PetPage-BH3xDVV9.js} +1 -1
  37. package/dashboard/dist/assets/{PopoverPopup-B1V01u10.js → PopoverPopup-CewF_cGz.js} +1 -1
  38. package/dashboard/dist/assets/{ProviderIcon-BLRe55c7.js → ProviderIcon-Dg0K0RPn.js} +1 -1
  39. package/dashboard/dist/assets/{ResetPasswordPage-BJ95vy1S.js → ResetPasswordPage-CYwt85O3.js} +1 -1
  40. package/dashboard/dist/assets/{SegmentedControl-DacS_m9M.js → SegmentedControl-BJkXXomQ.js} +1 -1
  41. package/dashboard/dist/assets/{Select-bQDO5BEA.js → Select-B3_WH0Qe.js} +1 -1
  42. package/dashboard/dist/assets/{SelectItemText-CUGrs9CY.js → SelectItemText-BUby_DUu.js} +1 -1
  43. package/dashboard/dist/assets/{ServiceStatusPage-CsERvmVC.js → ServiceStatusPage-BDlVZk4i.js} +1 -1
  44. package/dashboard/dist/assets/{SessionsPage-0VvqEfPx.js → SessionsPage-TaC9nahK.js} +1 -1
  45. package/dashboard/dist/assets/{SettingsPage-9EHh2HfD.js → SettingsPage-CudcQh6m.js} +2 -2
  46. package/dashboard/dist/assets/SkillsPage-iDLQ8lpZ.js +1 -0
  47. package/dashboard/dist/assets/{TokenGalaxy-DSKTHQSs.js → TokenGalaxy-DtYzUsXU.js} +1 -1
  48. package/dashboard/dist/assets/{ToolbarRootContext-DP4mQLbq.js → ToolbarRootContext-PC8Tg1Jz.js} +1 -1
  49. package/dashboard/dist/assets/TrendMonitor-r8n8YO_9.js +19 -0
  50. package/dashboard/dist/assets/{WidgetsPage-Cr6Wg9QX.js → WidgetsPage-vzfGccWb.js} +1 -1
  51. package/dashboard/dist/assets/{WrappedPage-DqQvRcnA.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-BjIh_mqf.js → agent-logos-B7nzn3n2.js} +1 -1
  55. package/dashboard/dist/assets/{arrow-up-right-BIhbN0NO.js → arrow-up-right-CsxyvCER.js} +1 -1
  56. package/dashboard/dist/assets/{check-U-wNzPvd.js → check-hYcC7MdH.js} +1 -1
  57. package/dashboard/dist/assets/{composite-BR91CxNl.js → composite-C79LNbex.js} +1 -1
  58. package/dashboard/dist/assets/{copy-Bw3hXBCu.js → copy-Cz4qzE62.js} +1 -1
  59. package/dashboard/dist/assets/{download-WOqcQzvK.js → download-CUaSNE2E.js} +1 -1
  60. package/dashboard/dist/assets/{external-link-BZtf6WfS.js → external-link-vq7kIyKK.js} +1 -1
  61. package/dashboard/dist/assets/{flame-B5kp2VI9.js → flame-CB6K4eR6.js} +1 -1
  62. package/dashboard/dist/assets/{format-tokens-BOCnuMuJ.js → format-tokens-KI-e_JQ-.js} +1 -1
  63. package/dashboard/dist/assets/{icons-CA3FiIa_.js → icons-3maItkrp.js} +1 -1
  64. package/dashboard/dist/assets/{index-DQaanK52.js → index-BRXoZ7Jj.js} +1 -1
  65. package/dashboard/dist/assets/{index-mEG0HZ-a.js → index-C5WrfmLB.js} +1 -1
  66. package/dashboard/dist/assets/{index-DjWC63Bs.js → index-CUbC50Zp.js} +1 -1
  67. package/dashboard/dist/assets/{info-Djer9i3-.js → info-r0yGihUe.js} +1 -1
  68. package/dashboard/dist/assets/{limits-providers-F-RnNFAb.js → limits-providers-BN_94oe9.js} +1 -1
  69. package/dashboard/dist/assets/{link-2-DV4ocD0l.js → link-2-J9t0VXWM.js} +1 -1
  70. package/dashboard/dist/assets/{loader-circle-DJK72c-o.js → loader-circle-DCvOsPze.js} +1 -1
  71. package/dashboard/dist/assets/{maximize-2-BwBA_lMm.js → maximize-2-CZ9JhGjf.js} +1 -1
  72. package/dashboard/dist/assets/{modifiers.esm-B3sl4qyZ.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-BUTpM4ng.js → monitor-smartphone-nqbWjJx_.js} +1 -1
  75. package/dashboard/dist/assets/{plus-BA9RP5L6.js → plus-Cg7wd2jK.js} +1 -1
  76. package/dashboard/dist/assets/{provider-display-DqsJzGSr.js → provider-display-B9L1nRY9.js} +2 -2
  77. package/dashboard/dist/assets/{react-BYw5AVSz.js → react-D2AD6Pk2.js} +1 -1
  78. package/dashboard/dist/assets/{search-BzcnFWVD.js → search-uNiWba1h.js} +1 -1
  79. package/dashboard/dist/assets/{skills-api-vAME1P7G.js → skills-api-BBYuAhiL.js} +1 -1
  80. package/dashboard/dist/assets/{sortable.esm-BpEiRqqI.js → sortable.esm-CzPxc949.js} +1 -1
  81. package/dashboard/dist/assets/{store-GeT5fhRX.js → store-D4A7EUwE.js} +1 -1
  82. package/dashboard/dist/assets/{terminal-CZCMYQ8K.js → terminal-Crmm_yKF.js} +1 -1
  83. package/dashboard/dist/assets/{trash-2-DzlWEwKO.js → trash-2-ia9VjRnU.js} +1 -1
  84. package/dashboard/dist/assets/{use-community-stats-DOvJfj-i.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-BmLs4HyN.js → use-limits-display-prefs-JSTYlbTD.js} +1 -1
  87. package/dashboard/dist/assets/{use-ordered-list-BgozDSSV.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-HP1ckVkS.js → use-session-efficiency-pref-CgyUz5Gc.js} +1 -1
  90. package/dashboard/dist/assets/{use-transform-B4VSogOx.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-CSuzCBAL.js → useCurrency-CK_gcb3J.js} +1 -1
  93. package/dashboard/dist/assets/{useTokenFormat-ClnL9OFe.js → useTokenFormat-C_a-UkCA.js} +1 -1
  94. package/dashboard/dist/assets/{zap-C_T9CCiS.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/local-api.js +99 -13
  111. package/src/lib/pricing/index.js +5 -0
  112. package/src/lib/pricing/matcher.js +15 -0
  113. package/src/lib/pricing/seed-snapshot.json +1 -1
  114. package/src/lib/rollout.js +1035 -28
  115. package/src/lib/usage-limits.js +507 -55
  116. package/src/lib/zcode-limits.js +65 -8
  117. package/dashboard/dist/assets/CommunityStatsModal-DRDpZycV.js +0 -1
  118. package/dashboard/dist/assets/DashboardPage-B5XMQoxy.js +0 -1
  119. package/dashboard/dist/assets/DialogDescription-DiVbwvTR.js +0 -1
  120. package/dashboard/dist/assets/SkillsPage-B1AcLX41.js +0 -1
  121. package/dashboard/dist/assets/TrendMonitor-rabtcE5w.js +0 -19
  122. package/dashboard/dist/assets/__vite-browser-external-C8KwSyTz.js +0 -45
  123. package/dashboard/dist/assets/chevron-right-Du-QVNj4.js +0 -1
  124. package/dashboard/dist/assets/module-D2D5r9gC.js +0 -3
  125. package/dashboard/dist/assets/use-reduced-motion-Bc3RFJdg.js +0 -1
  126. package/dashboard/dist/assets/use-usage-limits-DNNhD2H6.js +0 -1
@@ -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
 
@@ -21,6 +21,10 @@ 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"]);
24
28
  const SOURCES_WITH_AUTHORITATIVE_COST = new Set(["grok"]);
25
29
  const SEED_SNAPSHOT_PATH = path.resolve(__dirname, "seed-snapshot.json");
26
30
  const DEEPSEEK_TIME_PRICED_MODELS = [
@@ -169,6 +173,7 @@ function getRowPricing(row) {
169
173
  // computeRowCost in src/lib/local-api.js. Moved here so vite mock + local
170
174
  // server share one source of truth.
171
175
  function computeRowCost(row) {
176
+ if (LOCAL_INFERENCE_SOURCES.has(String(row?.source || "").toLowerCase())) return 0;
172
177
  // Pi can route a turn through a subscription-backed Copilot provider. Pi's
173
178
  // usage record reports a zero marginal cost for those turns; do not
174
179
  // reinterpret the Claude model name as an Anthropic API bill.
@@ -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