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.
- package/README.de.md +9 -7
- package/README.ja.md +8 -6
- package/README.ko.md +8 -6
- package/README.md +23 -10
- package/README.zh-CN.md +8 -6
- package/dashboard/dist/assets/{AchievementBadge-DPqwk2zI.js → AchievementBadge-Pi3Gvbm8.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-CELHoMQt.js → AchievementsPage-C4WLT8Rl.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DdAC0VCb.js → AchievementsSection-ilvR2ssr.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-Dd4I1SpM.js → BadgeDetailModal-CjEeToxa.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-DL2-4mnm.js → BotAnimated-DDf9pBuK.js} +1 -1
- package/dashboard/dist/assets/{Card-B_MpSRse.js → Card-DNous84-.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-BLZZ4rG6.js → ClawdAnimated-BLMIRntb.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-B-vfdIbH.js → CommandPalette-vVekhoFO.js} +1 -1
- package/dashboard/dist/assets/CommunityStatsModal-C0Kp_J8B.js +1 -0
- package/dashboard/dist/assets/{ConfirmModal-DLnRwUXC.js → ConfirmModal-DFdpcx12.js} +1 -1
- package/dashboard/dist/assets/{Controls-FZ8wEN5w.js → Controls-RNhqybmE.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-BQjQh1IQ.js +1 -0
- package/dashboard/dist/assets/{DevicePage-D2IzPc5o.js → DevicePage-DIhh-YAk.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-BaqQaUUk.js → DialogClose-ZqxYVIzl.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-DDM4aanX.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-DrbKjtuS.js → DialogTitle-BiRcejlu.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-GddKDE8i.js → FadeIn-BDBQpeGe.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-DUu-Z5mE.js → HeaderGithubStar-CztIwtKT.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-BmDkY35A.js → HoverTooltip-Cfso7LTC.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-CGJyqWk2.js → IpCheckPage-CNE7dLs3.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-Bd91ALUj.js → LandingPage-B2a3cweM.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-BGGUwXFd.js → LeaderboardAvatar-CwmZnIji.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-BMEqHwif.js → LeaderboardPage-DkTHj3fB.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-BSweJqm2.js → LeaderboardProfileModal-CefTO3Zh.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CRrI3RFz.js → LeaderboardProfilePage-CnOXlQ3L.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-XIRDv46K.js → LimitsPage-BIZ1-6BB.js} +2 -2
- package/dashboard/dist/assets/{LocalOnlyNotice-CCD-lGWG.js → LocalOnlyNotice-CdHQiPhy.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-lDuqSv4f.js → LoginCard-yXLA9geu.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-DStt5VXN.js → LoginModal-BF2Pfkqa.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-dmVPSL72.js → LoginPage-DuOypZap.js} +1 -1
- package/dashboard/dist/assets/{PetPage-ybcZRctF.js → PetPage-BH3xDVV9.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-Dar_UjxF.js → PopoverPopup-CewF_cGz.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-Ct9tILpz.js → ProviderIcon-Dg0K0RPn.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-B4yLh5zx.js → ResetPasswordPage-CYwt85O3.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-BOE1CEzq.js → SegmentedControl-BJkXXomQ.js} +1 -1
- package/dashboard/dist/assets/{Select-D-bSh7Zz.js → Select-B3_WH0Qe.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-DMYjXXq4.js → SelectItemText-BUby_DUu.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-CaleS-6_.js → ServiceStatusPage-BDlVZk4i.js} +1 -1
- package/dashboard/dist/assets/SessionsPage-TaC9nahK.js +2 -0
- package/dashboard/dist/assets/{SettingsPage-iJH0Yyah.js → SettingsPage-CudcQh6m.js} +2 -2
- package/dashboard/dist/assets/SkillsPage-iDLQ8lpZ.js +1 -0
- package/dashboard/dist/assets/{TokenGalaxy-CuXKoMPg.js → TokenGalaxy-DtYzUsXU.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-Dx5GB7F5.js → ToolbarRootContext-PC8Tg1Jz.js} +1 -1
- package/dashboard/dist/assets/TrendMonitor-r8n8YO_9.js +19 -0
- package/dashboard/dist/assets/{WidgetsPage-DUaIyGtu.js → WidgetsPage-vzfGccWb.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-BzqlcAUC.js → WrappedPage-DaKuIkYE.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-BHz2N7Ih.css → __vite-browser-external-CCMdjCX2.css} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-Dnxl50k1.js +45 -0
- package/dashboard/dist/assets/{agent-logos-BdWWxza6.js → agent-logos-B7nzn3n2.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-CANSzmm-.js → arrow-up-right-CsxyvCER.js} +1 -1
- package/dashboard/dist/assets/{check-BnJpzXRZ.js → check-hYcC7MdH.js} +1 -1
- package/dashboard/dist/assets/{composite-Ccj6KQmh.js → composite-C79LNbex.js} +1 -1
- package/dashboard/dist/assets/{copy-C2Yc8Evu.js → copy-Cz4qzE62.js} +1 -1
- package/dashboard/dist/assets/{download-B52JTk6Y.js → download-CUaSNE2E.js} +1 -1
- package/dashboard/dist/assets/{external-link-DayTwZx-.js → external-link-vq7kIyKK.js} +1 -1
- package/dashboard/dist/assets/{flame-B8GEX2yT.js → flame-CB6K4eR6.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-mhkbohkb.js → format-tokens-KI-e_JQ-.js} +1 -1
- package/dashboard/dist/assets/{icons-o8bPXqRP.js → icons-3maItkrp.js} +1 -1
- package/dashboard/dist/assets/{index-MBVWUKpA.js → index-BRXoZ7Jj.js} +1 -1
- package/dashboard/dist/assets/{index-CK_CduLm.js → index-C5WrfmLB.js} +1 -1
- package/dashboard/dist/assets/{index-Bp0ZHDtz.js → index-CUbC50Zp.js} +1 -1
- package/dashboard/dist/assets/{info-D94QM_vX.js → info-r0yGihUe.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-BLzMyCiy.js → limits-providers-BN_94oe9.js} +1 -1
- package/dashboard/dist/assets/{link-2-CqBmFaDI.js → link-2-J9t0VXWM.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-SaxUpDR6.js → loader-circle-DCvOsPze.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-BoXqARks.js → maximize-2-CZ9JhGjf.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-Conh_-BD.js → modifiers.esm-DJ0_WKg0.js} +1 -1
- package/dashboard/dist/assets/module-xpSFyNBK.js +3 -0
- package/dashboard/dist/assets/{monitor-smartphone-D6qLR9qd.js → monitor-smartphone-nqbWjJx_.js} +1 -1
- package/dashboard/dist/assets/{plus-D51mjIc2.js → plus-Cg7wd2jK.js} +1 -1
- package/dashboard/dist/assets/{provider-display-DpHPjd_s.js → provider-display-B9L1nRY9.js} +2 -2
- package/dashboard/dist/assets/{react-D_AcI466.js → react-D2AD6Pk2.js} +1 -1
- package/dashboard/dist/assets/{search-CwLcNdvZ.js → search-uNiWba1h.js} +1 -1
- package/dashboard/dist/assets/{skills-api-Dqi7Mjgo.js → skills-api-BBYuAhiL.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-BPDlUxPb.js → sortable.esm-CzPxc949.js} +1 -1
- package/dashboard/dist/assets/{store-ikeRIRGd.js → store-D4A7EUwE.js} +1 -1
- package/dashboard/dist/assets/{terminal-BfeX3L5T.js → terminal-Crmm_yKF.js} +1 -1
- package/dashboard/dist/assets/{trash-2-5_Qlp1k7.js → trash-2-ia9VjRnU.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-D90Fy2_j.js → use-community-stats-KaGCAxaF.js} +1 -1
- package/dashboard/dist/assets/use-latest-request-guard-Dy8gSDkk.js +1 -0
- package/dashboard/dist/assets/{use-limits-display-prefs-D4BYFaa-.js → use-limits-display-prefs-JSTYlbTD.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-DJxLNdXY.js → use-ordered-list-C4JnHdE0.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-AQE0ummK.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-CAX_GqXc.js → use-session-efficiency-pref-CgyUz5Gc.js} +1 -1
- package/dashboard/dist/assets/{use-transform-CeW6Z5R0.js → use-transform-C7-jSJ9k.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-B3YYqn-v.js +1 -0
- package/dashboard/dist/assets/{useCurrency-BarGeFyK.js → useCurrency-CK_gcb3J.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-DUJ9iT9H.js → useTokenFormat-C_a-UkCA.js} +1 -1
- package/dashboard/dist/assets/{zap--m0IWoI3.js → zap-BIJNn-Ic.js} +1 -1
- package/dashboard/dist/brand-logos/lmstudio.svg +12 -0
- package/dashboard/dist/brand-logos/unsloth.svg +1 -0
- package/dashboard/dist/index.html +16 -12
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/llms.txt +4 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +2 -2
- package/src/commands/device-login.js +5 -10
- package/src/commands/init.js +28 -1
- package/src/commands/serve.js +6 -0
- package/src/commands/status.js +24 -0
- package/src/commands/sync.js +114 -18
- package/src/lib/cloud-account.js +102 -29
- package/src/lib/fs.js +30 -0
- package/src/lib/grok-context-breakdown.js +24 -33
- package/src/lib/grok-usage.js +95 -0
- package/src/lib/local-api.js +101 -13
- package/src/lib/pricing/index.js +15 -0
- package/src/lib/pricing/matcher.js +15 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +1094 -72
- package/src/lib/session-analytics.js +100 -40
- package/src/lib/usage-limits.js +507 -55
- package/src/lib/zcode-limits.js +65 -8
- package/dashboard/dist/assets/CommunityStatsModal-aAico565.js +0 -1
- package/dashboard/dist/assets/DashboardPage-BByRBsbt.js +0 -1
- package/dashboard/dist/assets/DialogDescription-BUrh0Jod.js +0 -1
- package/dashboard/dist/assets/SessionsPage-DOaY6_E9.js +0 -2
- package/dashboard/dist/assets/SkillsPage-BkzJOBLk.js +0 -1
- package/dashboard/dist/assets/TrendMonitor-BuKmsznP.js +0 -19
- package/dashboard/dist/assets/__vite-browser-external-CVIGknXl.js +0 -45
- package/dashboard/dist/assets/chevron-right-DJm0mbG0.js +0 -1
- package/dashboard/dist/assets/module-D2D5r9gC.js +0 -3
- package/dashboard/dist/assets/use-reduced-motion-yE3T3cat.js +0 -1
- 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
|
+
};
|
package/src/lib/local-api.js
CHANGED
|
@@ -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
|
-
//
|
|
1403
|
-
//
|
|
1404
|
-
//
|
|
1405
|
-
//
|
|
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
|
|
1465
|
+
if (!getCloudSyncPref()) return ACCOUNT_FALLBACK_CLOUD_SYNC_OFF;
|
|
1408
1466
|
const refreshToken = getRefreshTokenForCloud();
|
|
1409
|
-
if (!refreshToken) return
|
|
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
|
-
|
|
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).
|
|
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
|
-
|
|
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
|
|
2010
|
-
//
|
|
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) => {
|
package/src/lib/pricing/index.js
CHANGED
|
@@ -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
|
|