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.
- package/dashboard/dist/assets/{AchievementBadge-uuDGO9Mq.js → AchievementBadge-CKNb_cwg.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-BCTVT7Jo.js → AchievementsPage-u9P8DXJI.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-B6fJSJSr.js → AchievementsSection-BSCUJ4DR.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-FjUN__9G.js → BadgeDetailModal-Bg7cE3Zq.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-B-KqY_Wr.js → BotAnimated-C7D2WbQA.js} +1 -1
- package/dashboard/dist/assets/{Card-DS8T8piP.js → Card-B4IqAhai.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-_0UjsFPu.js → CloudCheckoutPage-P75pH9Ym.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-BstgVBaV.js → CloudPage-DDEZfmZg.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-DtVsYZgc.js → CommandPalette-CER_AklA.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-DTpEJqV3.js → CommunityStatsModal-Cbb74eib.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-Lfdh2vr7.js → ConfirmModal-Q1ZQdZHt.js} +1 -1
- package/dashboard/dist/assets/{Controls-DpwQ7pkq.js → Controls-DaGOfcoz.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-CXI7RmxO.js +1 -0
- package/dashboard/dist/assets/{DateRangePopover-CrWgrzT2.js → DateRangePopover-DSS1Ira_.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-DbK5KWJS.js → DevicePage-6zPLYydh.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-Dd2B_o03.js → DialogClose-DdCQ36LK.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-DztHQvj-.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-3JQBVha3.js → DialogTitle-BjP217Y1.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-LYAqG8pG.js → ElectricLogo-Dhq8PM4h.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-DhDvUor7.js → FadeIn-CGNbED4E.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-9f1XmgYC.js → HeaderGithubStar-DwqEFzb6.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-BXhHAkP7.js → HoverTooltip-CQZTgyQy.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-ChPdDeMl.js → IpCheckPage-Bz09HXLn.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-DnJ7Y9zw.js → LandingPage-DC-lZsYR.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-BF9isLzu.js → LeaderboardPage-D8MpLM_t.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-BM4JKgkC.js → LeaderboardProBadge-pdXgokhE.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-DfZABOpN.js → LeaderboardProfileModal-nHZLiE0m.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-BaZ38iT5.js → LeaderboardProfilePage-BKv3oIAW.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-TmRTgSCb.js +6 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-DvhlYoqQ.js → LocalOnlyNotice-aF01udyR.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-g_Gbnzhs.js → LoginCard-BaroD0Ct.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-DkJrI6b7.js → LoginModal-0jgyMGVM.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-CqCKSo0n.js → LoginPage-BUx8U58i.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DbF7ROsj.js → PetPage-CpOjtJqg.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-4WuH41ZT.js → PopoverPopup-BYjfer-M.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-6j0A2KD9.js → ProviderIcon-NtYa2h0u.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-DBd-iwuM.js → ResetPasswordPage-CYUg7TR1.js} +1 -1
- package/dashboard/dist/assets/{Select-BBEpLeiG.js → Select-CYP95o5y.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-Cn_U_rrO.js → SelectItemText-BCDfusOM.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-2WZc6Rml.js → SelfHostPage-BmVc2rIT.js} +1 -1
- package/dashboard/dist/assets/ServiceStatusPage-Dyp8NwIn.js +1 -0
- package/dashboard/dist/assets/SessionsPage-tGEj6Ztz.js +3 -0
- package/dashboard/dist/assets/{SettingsPage-rXNr96i-.js → SettingsPage-CCMbq5wc.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-CudTvbZL.js → SkillsPage-C4O4fbaQ.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-BRKrbTsJ.js → TokenGalaxy-D2d738xr.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-CsLKXhWC.js → TooltipPopup-BJfFYesG.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-8onUTbkk.js → TrendMonitor-CElakFSU.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-CcwE0lz8.js → WidgetsPage-Cvd1LbJm.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-Cj9LLYqP.js → WrappedPage-wRC1tE_a.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-CMBWGkOq.css +1 -0
- package/dashboard/dist/assets/__vite-browser-external-V4TPLCJG.js +45 -0
- package/dashboard/dist/assets/{agent-logos-ChBA1nAH.js → agent-logos-Ba57HF3w.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-Bb5mfoPc.js → arrow-left-B1bKnxXL.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-D1Ti2XUI.js → arrow-right-DrOlKzX5.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-B_CjA_y8.js → arrow-up-right-hlYpo3yw.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-DdfrAqie.js → calendar-days-CSY6SEsI.js} +1 -1
- package/dashboard/dist/assets/{check-29zGw1MK.js → check-xrP1gLw5.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-DAzlFX7g.js → chevron-down-6-zT5n9d.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-C2SeCjFM.js → cloud-checkout-BZymZD4K.js} +1 -1
- package/dashboard/dist/assets/{composite-DIf735Hq.js → composite-C9izWafa.js} +1 -1
- package/dashboard/dist/assets/{copy-BOuOM0HQ.js → copy-Dkas5wZX.js} +1 -1
- package/dashboard/dist/assets/{download-50tQRpGe.js → download-CagGRs0_.js} +1 -1
- package/dashboard/dist/assets/{external-link-BhlQClVe.js → external-link-DRl10hxd.js} +1 -1
- package/dashboard/dist/assets/{flame-CeGfA-0L.js → flame-CkvmsTFB.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-Cq7YiAkJ.js → format-tokens-BtwhO3mV.js} +1 -1
- package/dashboard/dist/assets/{gift-Do-UFhiT.js → gift-YzjXkMQx.js} +1 -1
- package/dashboard/dist/assets/{icons-C6EZ_HmP.js → icons-CHhKYs0u.js} +1 -1
- package/dashboard/dist/assets/{index-BWhbBAd9.js → index-B0MQZTrM.js} +1 -1
- package/dashboard/dist/assets/{index-D9kwYWLX.js → index-C2DK8av-.js} +1 -1
- package/dashboard/dist/assets/{index-BFrbVqsm.js → index-CJknN9Jd.js} +1 -1
- package/dashboard/dist/assets/{index-c0crpe9Z.js → index-DojzoMqd.js} +1 -1
- package/dashboard/dist/assets/{info-B4MXmsNE.js → info-Torep-Ti.js} +1 -1
- package/dashboard/dist/assets/{link-2-DuWCslzl.js → link-2-DE5Yu-z6.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-C2I3NZAO.js → loader-circle-BL8LxD9n.js} +1 -1
- package/dashboard/dist/assets/{local-usage-transfer-BQasvsUF.js → local-usage-transfer-8X1InWix.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-DbeNS6FT.js → modifiers.esm-D60-6UQa.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-C6Y4ktsc.js → monitor-smartphone-C-IFPjqk.js} +1 -1
- package/dashboard/dist/assets/{play-CwgcDJC2.js → play-n6uWrdH9.js} +1 -1
- package/dashboard/dist/assets/{plus-B796wsUO.js → plus-B2f6TaNj.js} +1 -1
- package/dashboard/dist/assets/{provider-display-Cw26RFDm.js → provider-display-DWqCEcsP.js} +1 -1
- package/dashboard/dist/assets/{react-d7Bk1-_K.js → react-B-zfi_j6.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-CDE0dNv_.js → safePolygon-C2PsvI-v.js} +1 -1
- package/dashboard/dist/assets/{search-DtKvsdRs.js → search-DZOaTLF9.js} +1 -1
- package/dashboard/dist/assets/{server-gAURHUyz.js → server-DIp4Qfsv.js} +1 -1
- package/dashboard/dist/assets/{shield-check-DFVdDTwh.js → shield-check-Cs2M8-iT.js} +1 -1
- package/dashboard/dist/assets/{skills-api-2rdPUrJI.js → skills-api-CoYy6Tov.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-DFlfwnJr.js → sortable.esm-B5_hjIfs.js} +1 -1
- package/dashboard/dist/assets/{store-BKPCk_de.js → store-BLXqtJzb.js} +1 -1
- package/dashboard/dist/assets/{terminal-BtWFakvS.js → terminal-CEMKX3ba.js} +1 -1
- package/dashboard/dist/assets/{trash-bpr9zTAM.js → trash-DOWYaXjK.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DMnGgLne.js → use-community-stats-BjRI1Bzz.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-BSBlJJHe.js → use-latest-request-guard-T7L5qBU6.js} +1 -1
- package/dashboard/dist/assets/use-limits-display-prefs-vNIrCPZA.js +1 -0
- package/dashboard/dist/assets/{use-native-settings-OthqiF6g.js → use-native-settings-Cg5Zb4q_.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-DpAtr0R-.js → use-ordered-list-BF-wIvmG.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-B9K5mIx4.js +1 -0
- package/dashboard/dist/assets/{use-transform-BRz9dz_2.js → use-transform-CbJ1IcNc.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-CwmhudAX.js → use-usage-limits-Dgg3jpC4.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-CAA-tQ27.js → useCurrency-3oBHWfSe.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-Cj2DPmwM.js → usePositioner-D0YrIW0A.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-Bf5-f7Hx.js → useTokenFormat-DZAHWE_R.js} +1 -1
- package/dashboard/dist/assets/{zap-B8Fg6PaC.js → zap-kLub6Jiv.js} +1 -1
- package/dashboard/dist/index.html +2 -2
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +1 -1
- package/src/commands/serve.js +3 -2
- package/src/lib/accept-encoding.js +59 -0
- package/src/lib/cursor-config.js +3 -1
- package/src/lib/deepseek-limits.js +280 -0
- package/src/lib/grok-limits.js +40 -1
- package/src/lib/local-api.js +229 -31
- package/src/lib/opencode-status.js +88 -0
- package/src/lib/pricing/index.js +182 -79
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/provider-api-key-store.js +2 -0
- package/src/lib/provider-status.js +17 -5
- package/src/lib/session-analytics.js +194 -29
- package/src/lib/static-server.js +74 -2
- package/src/lib/usage-limits.js +535 -16
- package/src/lib/workbuddy-limits.js +106 -14
- package/dashboard/dist/assets/DashboardPage-DUFWejkE.js +0 -1
- package/dashboard/dist/assets/DialogDescription-CLy0arbt.js +0 -1
- package/dashboard/dist/assets/LimitsPage-C7LbG3LX.js +0 -6
- package/dashboard/dist/assets/ServiceStatusPage-B-g4MDl0.js +0 -1
- package/dashboard/dist/assets/SessionsPage-CWS0iQ1L.js +0 -3
- package/dashboard/dist/assets/__vite-browser-external-07djSGmy.js +0 -45
- package/dashboard/dist/assets/__vite-browser-external-BDldtsD2.css +0 -1
- package/dashboard/dist/assets/use-limits-display-prefs-eYlvnUKn.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-DF0grwHs.js +0 -1
package/src/lib/local-api.js
CHANGED
|
@@ -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
|
-
|
|
1680
|
-
|
|
1681
|
-
|
|
1682
|
-
|
|
1683
|
-
|
|
1684
|
-
|
|
1685
|
-
|
|
1686
|
-
|
|
1687
|
-
|
|
1688
|
-
|
|
1689
|
-
|
|
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
|
-
|
|
1693
|
-
|
|
1694
|
-
|
|
1695
|
-
|
|
1696
|
-
|
|
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
|
-
|
|
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
|
|
2650
|
-
const
|
|
2651
|
-
|
|
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: {
|
|
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
|
+
};
|