tokentracker-cli 1.5.2 → 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-B5kmt0VD.js → AchievementBadge-CKNb_cwg.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-C0FlkOo0.js → AchievementsPage-u9P8DXJI.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-CRcp80Pp.js → AchievementsSection-BSCUJ4DR.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-DAFjGbKW.js → BadgeDetailModal-Bg7cE3Zq.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-CO0bbimB.js → BotAnimated-C7D2WbQA.js} +1 -1
- package/dashboard/dist/assets/{Card-Xhr5exz8.js → Card-B4IqAhai.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-DsQ7FmsI.js → CloudCheckoutPage-P75pH9Ym.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-DkSexu5V.js → CloudPage-DDEZfmZg.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-Bm0huQdj.js → CommandPalette-CER_AklA.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-BsL8GN8Q.js → CommunityStatsModal-Cbb74eib.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-_WPp292g.js → ConfirmModal-Q1ZQdZHt.js} +1 -1
- package/dashboard/dist/assets/{Controls-ktzn68KM.js → Controls-DaGOfcoz.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-CXI7RmxO.js +1 -0
- package/dashboard/dist/assets/{DateRangePopover-DWr4zIzF.js → DateRangePopover-DSS1Ira_.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-CBa8Mk47.js → DevicePage-6zPLYydh.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-CZ5jzgtb.js → DialogClose-DdCQ36LK.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-DztHQvj-.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-Dr0N6q15.js → DialogTitle-BjP217Y1.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-sYqgrO6-.js → ElectricLogo-Dhq8PM4h.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-Qq-hcrZW.js → FadeIn-CGNbED4E.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-B77zSv7i.js → HeaderGithubStar-DwqEFzb6.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-lRDaUmoW.js → HoverTooltip-CQZTgyQy.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-B6AvHZjA.js → IpCheckPage-Bz09HXLn.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-DgzOjf3x.js → LandingPage-DC-lZsYR.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-BtjjtDMC.js → LeaderboardPage-D8MpLM_t.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-B2wNTUTU.js → LeaderboardProBadge-pdXgokhE.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-DJJckwHh.js → LeaderboardProfileModal-nHZLiE0m.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-DJdxByl2.js → LeaderboardProfilePage-BKv3oIAW.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-TmRTgSCb.js +6 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-B2lVqf_H.js → LocalOnlyNotice-aF01udyR.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-i0gg1yby.js → LoginCard-BaroD0Ct.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-B-loLNZM.js → LoginModal-0jgyMGVM.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-D_swI75j.js → LoginPage-BUx8U58i.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DDpKvpjG.js → PetPage-CpOjtJqg.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-Gja4MyEK.js → PopoverPopup-BYjfer-M.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-DOPj7tAZ.js → ProviderIcon-NtYa2h0u.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-BVADLk89.js → ResetPasswordPage-CYUg7TR1.js} +1 -1
- package/dashboard/dist/assets/{Select-D-38tN-E.js → Select-CYP95o5y.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-jN61ukxk.js → SelectItemText-BCDfusOM.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-k7AkGpl1.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-DfnMCAuA.js → SettingsPage-CCMbq5wc.js} +3 -3
- package/dashboard/dist/assets/{SkillsPage-YyIHIaHN.js → SkillsPage-C4O4fbaQ.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-BLcvZuQ9.js → TokenGalaxy-D2d738xr.js} +1 -1
- package/dashboard/dist/assets/{TooltipPopup-3E8kyVzz.js → TooltipPopup-BJfFYesG.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-ByKAdXSF.js → TrendMonitor-CElakFSU.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-BgEVIIzL.js → WidgetsPage-Cvd1LbJm.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-HjbX8QaA.js → WrappedPage-wRC1tE_a.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-Br-KAXh6.css → __vite-browser-external-CMBWGkOq.css} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-V4TPLCJG.js +45 -0
- package/dashboard/dist/assets/{agent-logos-BVF2siPQ.js → agent-logos-Ba57HF3w.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-CYO24PGP.js → arrow-left-B1bKnxXL.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-Xg9PZYhw.js → arrow-right-DrOlKzX5.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-B7ah6Qv-.js → arrow-up-right-hlYpo3yw.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-BV_iG938.js → calendar-days-CSY6SEsI.js} +1 -1
- package/dashboard/dist/assets/{check--EZH8RTp.js → check-xrP1gLw5.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-BzGTlk0-.js → chevron-down-6-zT5n9d.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-BgyxPHEh.js → cloud-checkout-BZymZD4K.js} +1 -1
- package/dashboard/dist/assets/{composite-C0KrmLEK.js → composite-C9izWafa.js} +1 -1
- package/dashboard/dist/assets/{copy-BtucSK2U.js → copy-Dkas5wZX.js} +1 -1
- package/dashboard/dist/assets/{download-CWHq03Y2.js → download-CagGRs0_.js} +1 -1
- package/dashboard/dist/assets/{external-link-DCTXF5VZ.js → external-link-DRl10hxd.js} +1 -1
- package/dashboard/dist/assets/{flame-BE5TsqVF.js → flame-CkvmsTFB.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-ThNIMWnZ.js → format-tokens-BtwhO3mV.js} +1 -1
- package/dashboard/dist/assets/{gift-DgaN9s4-.js → gift-YzjXkMQx.js} +1 -1
- package/dashboard/dist/assets/{icons-B4r5EiDC.js → icons-CHhKYs0u.js} +1 -1
- package/dashboard/dist/assets/{index-DqCtgU0G.js → index-B0MQZTrM.js} +1 -1
- package/dashboard/dist/assets/{index-fNKH4U7j.js → index-C2DK8av-.js} +1 -1
- package/dashboard/dist/assets/{index-CKhcCLNC.js → index-CJknN9Jd.js} +1 -1
- package/dashboard/dist/assets/{index-DFF9xo6f.js → index-DojzoMqd.js} +1 -1
- package/dashboard/dist/assets/{info-B4YzRR4y.js → info-Torep-Ti.js} +1 -1
- package/dashboard/dist/assets/{link-2-DzbUbaBw.js → link-2-DE5Yu-z6.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-B9c7D-7v.js → loader-circle-BL8LxD9n.js} +1 -1
- package/dashboard/dist/assets/{local-usage-transfer-btuk_b0Y.js → local-usage-transfer-8X1InWix.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-BjrU-Xz7.js → modifiers.esm-D60-6UQa.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-Bwn4tb4q.js → monitor-smartphone-C-IFPjqk.js} +1 -1
- package/dashboard/dist/assets/{play-DIFOwGIQ.js → play-n6uWrdH9.js} +1 -1
- package/dashboard/dist/assets/{plus-BpBqnwVg.js → plus-B2f6TaNj.js} +1 -1
- package/dashboard/dist/assets/{provider-display-DSrs4u7T.js → provider-display-DWqCEcsP.js} +1 -1
- package/dashboard/dist/assets/{react-BgG7_MY0.js → react-B-zfi_j6.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-BUGWIDOM.js → safePolygon-C2PsvI-v.js} +1 -1
- package/dashboard/dist/assets/{search-aHrmBmtZ.js → search-DZOaTLF9.js} +1 -1
- package/dashboard/dist/assets/{server-DOEvwlEU.js → server-DIp4Qfsv.js} +1 -1
- package/dashboard/dist/assets/{shield-check-BPTnLhsT.js → shield-check-Cs2M8-iT.js} +1 -1
- package/dashboard/dist/assets/{skills-api-C052Mwez.js → skills-api-CoYy6Tov.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-DaZKhjhu.js → sortable.esm-B5_hjIfs.js} +1 -1
- package/dashboard/dist/assets/{store-Cq30Pofw.js → store-BLXqtJzb.js} +1 -1
- package/dashboard/dist/assets/{terminal-CoSDSkE7.js → terminal-CEMKX3ba.js} +1 -1
- package/dashboard/dist/assets/{trash-BAmt-kPj.js → trash-DOWYaXjK.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-Bxq0kBhm.js → use-community-stats-BjRI1Bzz.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-DmIQDlvk.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-s_CIC8Bb.js → use-native-settings-Cg5Zb4q_.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-BHhykAbt.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-CVeMFlS6.js → use-transform-CbJ1IcNc.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-Dgg3jpC4.js +1 -0
- package/dashboard/dist/assets/{useCurrency-DilqL2Iv.js → useCurrency-3oBHWfSe.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-BcnySw8a.js → usePositioner-D0YrIW0A.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-CvKW46WP.js → useTokenFormat-DZAHWE_R.js} +1 -1
- package/dashboard/dist/assets/{zap-D7VRNTwk.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 +25 -15
- 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/deepseek-status.js +157 -0
- package/src/lib/grok-limits.js +40 -1
- package/src/lib/local-api.js +248 -10
- 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 +27 -2
- package/src/lib/qoder-limits.js +17 -2
- package/src/lib/session-analytics.js +194 -29
- package/src/lib/static-server.js +74 -2
- package/src/lib/usage-limits.js +714 -53
- package/src/lib/workbuddy-limits.js +291 -26
- package/src/lib/zcode-limits.js +143 -35
- package/dashboard/dist/assets/DashboardPage-CdNXi3Im.js +0 -1
- package/dashboard/dist/assets/DialogDescription-kCZ4gN09.js +0 -1
- package/dashboard/dist/assets/LimitsPage-gSBHSBi_.js +0 -6
- package/dashboard/dist/assets/ServiceStatusPage-D_Fa_pYz.js +0 -1
- package/dashboard/dist/assets/SessionsPage-BymccEEz.js +0 -3
- package/dashboard/dist/assets/__vite-browser-external-D_cMeuqI.js +0 -45
- package/dashboard/dist/assets/use-limits-display-prefs-CyAn1fme.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-BmFyFE3S.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-DRZMI20u.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,7 +21,9 @@ 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");
|
|
26
|
+
const { fetchProviderServiceStatus } = require("./provider-status");
|
|
23
27
|
|
|
24
28
|
const SYNC_TIMEOUT_MS = 120_000;
|
|
25
29
|
// A failed account-view request should not stall every dashboard refresh
|
|
@@ -53,6 +57,132 @@ const AVATAR_PROXY_MAX_BYTES = 512 * 1024; // 512 KiB per image
|
|
|
53
57
|
const AVATAR_PROXY_MAX_ENTRIES = 64;
|
|
54
58
|
const avatarProxyCache = new Map();
|
|
55
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
|
+
|
|
56
186
|
// ---------------------------------------------------------------------------
|
|
57
187
|
// Per-model pricing — delegated to src/lib/pricing/
|
|
58
188
|
// - CURATED overrides (kiro-*, hy3-*, composer-*, kimi-for-coding, etc.)
|
|
@@ -64,6 +194,7 @@ const {
|
|
|
64
194
|
MODEL_PRICING,
|
|
65
195
|
getModelPricing,
|
|
66
196
|
computeRowCost,
|
|
197
|
+
computeRowTierCosts,
|
|
67
198
|
ensurePricingLoaded,
|
|
68
199
|
getPricingRevision,
|
|
69
200
|
getModelPricingInfo,
|
|
@@ -99,6 +230,19 @@ const CLAUDE_MEM_OBSERVER_PROJECT_KEY = deriveProjectKeyFromRef(
|
|
|
99
230
|
CLAUDE_MEM_OBSERVER_PROJECT_REF,
|
|
100
231
|
);
|
|
101
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
|
+
];
|
|
102
246
|
const MAX_TIME_ZONE_CACHE_ENTRIES = 16;
|
|
103
247
|
const AGGREGATION_BATCH_ROWS = 256;
|
|
104
248
|
|
|
@@ -1675,8 +1819,38 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
1675
1819
|
return hasAllowedLoopbackOrigin(req?.headers || {});
|
|
1676
1820
|
}
|
|
1677
1821
|
|
|
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);
|
|
1838
|
+
return true;
|
|
1839
|
+
}
|
|
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);
|
|
1846
|
+
}
|
|
1847
|
+
return true;
|
|
1848
|
+
};
|
|
1849
|
+
|
|
1678
1850
|
return async function handleLocalApi(req, res, url) {
|
|
1679
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);
|
|
1680
1854
|
if (p === "/api/local-usage") {
|
|
1681
1855
|
if (!isAuthorizedLocalMutation(req)) { json(res, { code: "unauthorized" }, 401); return true; }
|
|
1682
1856
|
try {
|
|
@@ -2582,7 +2756,9 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2582
2756
|
const refresh = ["1", "true"].includes(url.searchParams.get("refresh"));
|
|
2583
2757
|
try {
|
|
2584
2758
|
const { buildSessionAnalytics, summarizeSessions, sessionsToCsv } = require("./session-analytics");
|
|
2585
|
-
|
|
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 });
|
|
2586
2762
|
const wantsCsv = url.searchParams.get("format") === "csv";
|
|
2587
2763
|
const includeSessions = wantsCsv || ["1", "true"].includes(url.searchParams.get("include_sessions"));
|
|
2588
2764
|
const result = summarizeSessions(sessions, { from, to, includeSessions, ...getTimeZoneContext(url) });
|
|
@@ -2612,16 +2788,48 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2612
2788
|
const refresh = ["1", "true"].includes(url.searchParams.get("refresh"));
|
|
2613
2789
|
const limitParam = parseInt(url.searchParams.get("limit") || "0", 10);
|
|
2614
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());
|
|
2615
2796
|
// This payload contains local paths and resumable session identifiers.
|
|
2616
2797
|
// The server is loopback-only; additionally prevent browser/proxy caches
|
|
2617
2798
|
// from retaining the response after the page is closed.
|
|
2618
2799
|
res.setHeader("Cache-Control", "no-store");
|
|
2619
2800
|
res.setHeader("X-Content-Type-Options", "nosniff");
|
|
2620
2801
|
try {
|
|
2621
|
-
const { buildSessionAnalytics, listSessionsForBrowser } = require("./session-analytics");
|
|
2622
|
-
const
|
|
2623
|
-
const
|
|
2624
|
-
|
|
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);
|
|
2625
2833
|
} catch (error) {
|
|
2626
2834
|
// Node fs errors embed the absolute path ("EACCES ... open '/Users/…'").
|
|
2627
2835
|
// Keep that out of the HTTP body and log it locally instead.
|
|
@@ -2718,7 +2926,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2718
2926
|
const src = row.source || "unknown";
|
|
2719
2927
|
const mdl = row.model || "unknown";
|
|
2720
2928
|
if (!bySource.has(src))
|
|
2721
|
-
bySource.set(src, { source: src, source_scope: getSourceScope(src), totals: { total_tokens: 0, billable_total_tokens: 0, input_tokens: 0, output_tokens: 0, cached_input_tokens: 0, cache_creation_input_tokens: 0, reasoning_output_tokens: 0, total_cost_usd: "0" }, models: new Map() });
|
|
2929
|
+
bySource.set(src, { source: src, source_scope: getSourceScope(src), totals: { total_tokens: 0, billable_total_tokens: 0, input_tokens: 0, output_tokens: 0, cached_input_tokens: 0, cache_creation_input_tokens: 0, reasoning_output_tokens: 0, conversation_count: 0, total_cost_usd: "0" }, models: new Map() });
|
|
2722
2930
|
const sa = bySource.get(src);
|
|
2723
2931
|
sa.totals.total_tokens += row.total_tokens || 0;
|
|
2724
2932
|
sa.totals.billable_total_tokens += row.billable_total_tokens ?? row.total_tokens ?? 0;
|
|
@@ -2727,8 +2935,12 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2727
2935
|
sa.totals.cached_input_tokens += row.cached_input_tokens || 0;
|
|
2728
2936
|
sa.totals.cache_creation_input_tokens += row.cache_creation_input_tokens || 0;
|
|
2729
2937
|
sa.totals.reasoning_output_tokens += row.reasoning_output_tokens || 0;
|
|
2938
|
+
// Request-only sources (#948: credit-billed transcripts that report no
|
|
2939
|
+
// tokens) carry their activity solely as conversation_count — thread it
|
|
2940
|
+
// through so the dashboard can surface "0 tokens / N requests".
|
|
2941
|
+
sa.totals.conversation_count += row.conversation_count || 0;
|
|
2730
2942
|
if (!sa.models.has(mdl))
|
|
2731
|
-
sa.models.set(mdl, { model: mdl, model_id: mdl, pricing: getModelPricingInfo(mdl, { source: src }), totals: { total_tokens: 0, billable_total_tokens: 0, input_tokens: 0, output_tokens: 0, cached_input_tokens: 0, cache_creation_input_tokens: 0, reasoning_output_tokens: 0, total_cost_usd: "0" } });
|
|
2943
|
+
sa.models.set(mdl, { model: mdl, model_id: mdl, pricing: getModelPricingInfo(mdl, { source: src }), totals: { total_tokens: 0, billable_total_tokens: 0, input_tokens: 0, output_tokens: 0, cached_input_tokens: 0, cache_creation_input_tokens: 0, reasoning_output_tokens: 0, conversation_count: 0, total_cost_usd: "0" } });
|
|
2732
2944
|
const ma = sa.models.get(mdl);
|
|
2733
2945
|
const reportedCost = ["grok", "cline"].includes(src) && Number(row.total_cost_usd) > 0;
|
|
2734
2946
|
const costSource = reportedCost ? "provider_reported" : "model_pricing";
|
|
@@ -2740,6 +2952,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2740
2952
|
ma.totals.cached_input_tokens += row.cached_input_tokens || 0;
|
|
2741
2953
|
ma.totals.cache_creation_input_tokens += row.cache_creation_input_tokens || 0;
|
|
2742
2954
|
ma.totals.reasoning_output_tokens += row.reasoning_output_tokens || 0;
|
|
2955
|
+
ma.totals.conversation_count += row.conversation_count || 0;
|
|
2743
2956
|
// Price each row, exactly as aggregateByDay does for the hero (#653).
|
|
2744
2957
|
// computeRowCost is not linear for models with a Fast tier or
|
|
2745
2958
|
// long-context rates: priority_* / long_context_* mark a subset of the
|
|
@@ -2747,6 +2960,18 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2747
2960
|
// row is intact. Summing the base columns first and pricing once silently
|
|
2748
2961
|
// dropped every premium — same tokens, fewer dollars than the hero.
|
|
2749
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
|
+
}
|
|
2750
2975
|
}
|
|
2751
2976
|
if (pricingRevision !== getPricingRevision()) return handleLocalApi(req, res, url);
|
|
2752
2977
|
|
|
@@ -2754,7 +2979,12 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2754
2979
|
s.models = Array.from(s.models.values())
|
|
2755
2980
|
.map((m) => ({
|
|
2756
2981
|
...m,
|
|
2757
|
-
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
|
+
},
|
|
2758
2988
|
}))
|
|
2759
2989
|
.sort((a, b) => b.totals.total_tokens - a.totals.total_tokens);
|
|
2760
2990
|
const sourceCost = s.models.reduce((sum, m) => sum + Number(m.totals.total_cost_usd), 0);
|
|
@@ -3672,7 +3902,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
3672
3902
|
|
|
3673
3903
|
// --- usage-limits ---
|
|
3674
3904
|
if (p === "/functions/tokentracker-usage-limits") {
|
|
3675
|
-
const { getUsageLimits, resetUsageLimitsCache } = require("./usage-limits");
|
|
3905
|
+
const { getUsageLimits, resetUsageLimitsCache, normalizeHiddenProviders } = require("./usage-limits");
|
|
3676
3906
|
try {
|
|
3677
3907
|
// Devin quota is opt-in (Settings > Usage & Limits > Providers). An
|
|
3678
3908
|
// explicit devin=1 without local authentication is rejected before any
|
|
@@ -3683,7 +3913,14 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
3683
3913
|
const localAuthorized = isAuthorizedLocalMutation(req);
|
|
3684
3914
|
const devinParam = url.searchParams.get("devin");
|
|
3685
3915
|
const devinEnabled = devinParam === "1" || devinParam === "true";
|
|
3686
|
-
|
|
3916
|
+
// Provider visibility gate (#956): hidden=<csv> names provider rows to
|
|
3917
|
+
// skip entirely — no upstream call, no credential read. Like devin=1,
|
|
3918
|
+
// an explicit list is honored only together with the loopback auth
|
|
3919
|
+
// header (letting an unauthenticated caller scope the fetch would let
|
|
3920
|
+
// it pick exactly which credentials to leave alone). Unknown ids are
|
|
3921
|
+
// dropped by the normalizer, so the gate only ever narrows.
|
|
3922
|
+
const hiddenProviders = normalizeHiddenProviders(url.searchParams.get("hidden"));
|
|
3923
|
+
if ((devinEnabled || hiddenProviders) && !localAuthorized) {
|
|
3687
3924
|
json(res, { error: "Unauthorized" }, 401);
|
|
3688
3925
|
return true;
|
|
3689
3926
|
}
|
|
@@ -3701,6 +3938,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
3701
3938
|
// cooldown) — an explicit user refresh should hit upstream.
|
|
3702
3939
|
forceRefresh,
|
|
3703
3940
|
devinEnabled,
|
|
3941
|
+
hiddenProviders,
|
|
3704
3942
|
});
|
|
3705
3943
|
json(res, data);
|
|
3706
3944
|
} catch (e) {
|
|
@@ -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
|
+
};
|