tokentracker-cli 1.4.6 → 1.4.9
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.md +9 -1
- package/README.zh-CN.md +1 -1
- package/bin/tracker.js +6 -0
- package/dashboard/dist/assets/{AchievementBadge-BmWRsZMx.js → AchievementBadge-_dRLgxnp.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-CyIP2nck.js → AchievementsPage-DtvlZLlv.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-BeNyJ0LW.js → AchievementsSection-OgzKkbes.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-DIFnFzkv.js → BadgeDetailModal-D4Rlm_um.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-CKVeJHit.js → BotAnimated-errJ5IvB.js} +1 -1
- package/dashboard/dist/assets/{Card-lQmyKbh4.js → Card-esELVQcD.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-B_8y6dyX.js → CloudCheckoutPage-3sH4Dbx9.js} +1 -1
- package/dashboard/dist/assets/CloudPage-Bi_vgiqE.js +2 -0
- package/dashboard/dist/assets/{CommandPalette-BV9LqokI.js → CommandPalette-I0aze2EJ.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-WELxKEFc.js → CommunityStatsModal-CEn2g4J9.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-B8H5o8c_.js → ConfirmModal-D7SfFCaf.js} +1 -1
- package/dashboard/dist/assets/{Controls-9ynPx9rF.js → Controls-Dyb5Vmk_.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-CMHX8zDq.js +1 -0
- package/dashboard/dist/assets/{DateRangePopover-DEqWsV_8.js → DateRangePopover-CgmxsHVR.js} +1 -1
- package/dashboard/dist/assets/DevicePage-9R2MfK2Z.js +1 -0
- package/dashboard/dist/assets/{DialogClose-Dlg52bL6.js → DialogClose-DzR109OA.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-CdNjUDLZ.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-CduwXjqH.js → DialogTitle-BLH1MgOX.js} +1 -1
- package/dashboard/dist/assets/{ElectricLogo-DFt7_F2f.js → ElectricLogo-DcpBcJIy.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-DlyusE6n.js → FadeIn-CBAXe_zb.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-tUtJoFHh.js → HeaderGithubStar-BZjQ3uJo.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-v-zkCqsX.js → HoverTooltip-Bq4HuWOZ.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-C5Y1uVEr.js → IpCheckPage-Oiah1-YB.js} +1 -1
- package/dashboard/dist/assets/LandingPage-Cc1FfkWg.js +2 -0
- package/dashboard/dist/assets/{LeaderboardPage-BBZ_6tCw.js → LeaderboardPage-Bc2j_DQY.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-DakoXIQn.js → LeaderboardProBadge-w1olVHVp.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-CINpES_x.js → LeaderboardProfileModal-mgG7mdl8.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-B6ISf-1l.js → LeaderboardProfilePage-DqxyooT5.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-BIvK0GFj.js +6 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-BJSsmXF1.js → LocalOnlyNotice-DZLgI75v.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-Dggv2aGv.js → LoginCard-DVNU_QtS.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-B8qDd2kY.js → LoginModal-yxi5pCwC.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-Dv66U8yJ.js → LoginPage-CW9pu4mf.js} +1 -1
- package/dashboard/dist/assets/{PetPage-CNwrQdKM.js → PetPage-DwZsq4nC.js} +2 -2
- package/dashboard/dist/assets/{PopoverPopup-CqmvpYOi.js → PopoverPopup-DYGmmoD8.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-BF2Ch7wi.js → ProviderIcon-BKfj89GN.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-B0R8VUU3.js → ResetPasswordPage-gQdgCp9s.js} +1 -1
- package/dashboard/dist/assets/{Select-idAiBnJG.js → Select-DvSR56Lj.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-DPmmPY_D.js → SelectItemText-DXo30s3O.js} +1 -1
- package/dashboard/dist/assets/{SelfHostPage-oR9cPK02.js → SelfHostPage-jA51Nckw.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-CdV9gfGF.js → ServiceStatusPage-DEkRRPx8.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-BkD34VlL.js → SessionsPage-J7hvGPCj.js} +3 -3
- package/dashboard/dist/assets/SettingsPage-ewH1Mzsm.js +5 -0
- package/dashboard/dist/assets/{SkillsPage-CzMk9s3x.js → SkillsPage-BaeBHhU7.js} +1 -1
- package/dashboard/dist/assets/TokenGalaxy-D52pHpzj.js +4426 -0
- package/dashboard/dist/assets/TooltipPopup-mLpESQcv.js +1 -0
- package/dashboard/dist/assets/TrendMonitor-ErowR5GE.js +19 -0
- package/dashboard/dist/assets/{WidgetsPage-Cc16WM0Y.js → WidgetsPage-Bf1uuKE7.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-pMwbM4R7.js → WrappedPage-C8qacvpR.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-CIpYjfAT.js +45 -0
- package/dashboard/dist/assets/__vite-browser-external-CnVnsbqV.css +1 -0
- package/dashboard/dist/assets/{agent-logos-Zw7LNkMO.js → agent-logos-tmdKvCcK.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-DycYjE-k.js → arrow-left-CRKi420c.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-uNxLHoJ0.js → arrow-right-C_o_iQbK.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-CmdJ7gd7.js → arrow-up-right-N5YqKKnI.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-CJ33_t5O.js → calendar-days-BJh4FDj9.js} +1 -1
- package/dashboard/dist/assets/{check-Dbubk4Il.js → check-BfHGb92g.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-BwvBVAOs.js → chevron-down-D1pO8Xgc.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-IG-IOpeP.js → cloud-checkout-Ds13TmYz.js} +1 -1
- package/dashboard/dist/assets/{composite-IIgJPPIw.js → composite-DDEsQKPD.js} +1 -1
- package/dashboard/dist/assets/copy-CJTosYLe.js +1 -0
- package/dashboard/dist/assets/{download-DxtH7w6a.js → download-Bkqx3c3y.js} +1 -1
- package/dashboard/dist/assets/{external-link-evKf3mnD.js → external-link-IYUo5LcK.js} +1 -1
- package/dashboard/dist/assets/{flame-D2qOE_W4.js → flame-BD_hiMfM.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CqDmm-sK.js → format-tokens-DxwbhPag.js} +1 -1
- package/dashboard/dist/assets/{gift-Ge11HK1x.js → gift-DLzFdL-1.js} +1 -1
- package/dashboard/dist/assets/host-mode-Bc_iTzHk.js +1 -0
- package/dashboard/dist/assets/{icons-OmfWvE5K.js → icons-CbrWeFPS.js} +1 -1
- package/dashboard/dist/assets/{index-BsYtyo4U.js → index-3xGpnZnP.js} +1 -1
- package/dashboard/dist/assets/{index-tbe1aERA.js → index-AVEWo-aY.js} +1 -1
- package/dashboard/dist/assets/{index-BYuEzP9p.js → index-DfzCE-FL.js} +1 -1
- package/dashboard/dist/assets/index-Dj1QnoPF.js +502 -0
- package/dashboard/dist/assets/{info-xhrso9aO.js → info-odlIlCnm.js} +1 -1
- package/dashboard/dist/assets/{link-2-BgNpTaO4.js → link-2-Bt28Ey9r.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-DRooZ0e5.js → loader-circle-lGnIhIFS.js} +1 -1
- package/dashboard/dist/assets/local-usage-transfer-DSS0WaL-.js +1 -0
- package/dashboard/dist/assets/{modifiers.esm-D-QAEpOZ.js → modifiers.esm-CitRa_Z1.js} +1 -1
- package/dashboard/dist/assets/module-C2SOSUuR.js +3 -0
- package/dashboard/dist/assets/{monitor-smartphone-Bwu7__2B.js → monitor-smartphone-1T0GFcOz.js} +1 -1
- package/dashboard/dist/assets/{play-CBPvTiWM.js → play-d6YYOmED.js} +1 -1
- package/dashboard/dist/assets/{plus-B4lFIyP8.js → plus-GrGV9Fkm.js} +1 -1
- package/dashboard/dist/assets/{provider-display-CQlWusXH.js → provider-display-D1v3cgDm.js} +1 -1
- package/dashboard/dist/assets/{react-DDOtS4Bd.js → react-DYCAyQbo.js} +1 -1
- package/dashboard/dist/assets/{safePolygon-BGfBVKoy.js → safePolygon-iPu060Qe.js} +1 -1
- package/dashboard/dist/assets/{search-DVyNq72J.js → search-AQhOTwzm.js} +1 -1
- package/dashboard/dist/assets/{server-EpUTEsOf.js → server-uYTbWPMd.js} +1 -1
- package/dashboard/dist/assets/{shield-check-DRg8cPKV.js → shield-check-DB1NVCDf.js} +1 -1
- package/dashboard/dist/assets/{skills-api-y42dEF96.js → skills-api-D7X10n0t.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-D2S1gXCK.js → sortable.esm-B13KhrCd.js} +1 -1
- package/dashboard/dist/assets/{store-DfGPS2IR.js → store-D6XHiq8o.js} +1 -1
- package/dashboard/dist/assets/{terminal-hXIrrL2b.js → terminal-t37F8qyF.js} +1 -1
- package/dashboard/dist/assets/{trash-cjaASRQl.js → trash-CDHRwGsy.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-B-nipjWt.js → use-community-stats--kaGg0EW.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-DIEYrSzn.js → use-latest-request-guard-ChyODWU3.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-BdHDwBBC.js → use-limits-display-prefs-katFAxOA.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-7w71_h44.js → use-native-settings-i3O4GTY1.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-CSMpK4ar.js → use-ordered-list-BtewEEX_.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-DPacKl9n.js +1 -0
- package/dashboard/dist/assets/{use-transform-sjGGXCNn.js → use-transform-NBAyuphu.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-Nml-wUB-.js → use-usage-limits-hsv-93TF.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-DAKIY1nR.js → useCurrency-BdsxkAID.js} +1 -1
- package/dashboard/dist/assets/{usePositioner-B7vVQ7th.js → usePositioner-BAN79miB.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-gtgw8k2Z.js → useTokenFormat-CGEO_mX1.js} +1 -1
- package/dashboard/dist/assets/{zap-DFWOiTJ1.js → zap-BDHWp-gL.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/privacy.html +8 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +3 -2
- package/src/cli.js +10 -0
- package/src/commands/sync.js +18 -3
- package/src/lib/codex-rollout-parser.js +29 -2
- package/src/lib/codex-service-tier.js +13 -0
- package/src/lib/commandcode-limits.js +27 -11
- package/src/lib/local-api.js +122 -11
- package/src/lib/local-usage-transfer.js +271 -0
- package/src/lib/opencode-go-limits.js +17 -6
- package/src/lib/pricing/curated-overrides.json +24 -0
- package/src/lib/pricing/index.js +50 -39
- package/src/lib/pricing/matcher.js +9 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/provider-api-key-store.js +110 -0
- package/src/lib/rollout.js +27 -3
- package/src/lib/sentry.js +231 -0
- package/src/lib/session-analytics.js +15 -2
- package/src/lib/static-server.js +15 -2
- package/src/lib/usage-limits.js +54 -16
- package/dashboard/dist/assets/CloudPage-BOQjUGAt.js +0 -2
- package/dashboard/dist/assets/DashboardPage-B8z-U63p.js +0 -1
- package/dashboard/dist/assets/DevicePage-CUOlUySM.js +0 -1
- package/dashboard/dist/assets/DialogDescription-vuQFBlM_.js +0 -1
- package/dashboard/dist/assets/LandingPage-DzOJOAj7.js +0 -2
- package/dashboard/dist/assets/LimitsPage-UVX730CQ.js +0 -9
- package/dashboard/dist/assets/SettingsPage-DsthWCyM.js +0 -5
- package/dashboard/dist/assets/TokenGalaxy-DQ6o8YBp.js +0 -4426
- package/dashboard/dist/assets/TooltipPopup-CMQjA-HE.js +0 -1
- package/dashboard/dist/assets/TrendMonitor-DrV1m8E7.js +0 -19
- package/dashboard/dist/assets/__vite-browser-external-B0m8a6Te.css +0 -1
- package/dashboard/dist/assets/__vite-browser-external-DCy_YLfe.js +0 -45
- package/dashboard/dist/assets/host-mode-8_TNh7yE.js +0 -1
- package/dashboard/dist/assets/module-DOCYJaxl.js +0 -3
- package/dashboard/dist/assets/use-reduced-motion-HUGf223R.js +0 -1
- package/dashboard/dist/assets/use-session-efficiency-pref-BagBVWW6.js +0 -1
|
@@ -0,0 +1,110 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Provider API keys saved from the dashboard's Limits panel.
|
|
4
|
+
//
|
|
5
|
+
// The launch environment is a shared, machine-wide namespace: a
|
|
6
|
+
// `launchctl setenv OPENCODE_GO_API_KEY` reaches every launchd app on the
|
|
7
|
+
// machine (that is how a global key silently unlocked another app's field in
|
|
8
|
+
// #837). Keys entered in the panel are therefore persisted here instead, in a
|
|
9
|
+
// 0600 JSON file under the tracker data dir next to cloud-device-token.json.
|
|
10
|
+
//
|
|
11
|
+
// The environment variable still wins when set, so headless installs and
|
|
12
|
+
// wrapper-launched apps keep working; the store is the durable default for
|
|
13
|
+
// users who never touch a terminal.
|
|
14
|
+
|
|
15
|
+
const fssync = require("node:fs");
|
|
16
|
+
const path = require("node:path");
|
|
17
|
+
const { ensureDir, writeFileAtomic, chmod600IfPossible } = require("./fs");
|
|
18
|
+
|
|
19
|
+
const PROVIDER_KEY_FILES = {
|
|
20
|
+
"opencode-go": "opencode-go-api-key.json",
|
|
21
|
+
"command-code": "command-code-api-key.json",
|
|
22
|
+
};
|
|
23
|
+
|
|
24
|
+
const PROVIDER_ENV_NAMES = {
|
|
25
|
+
"opencode-go": "OPENCODE_GO_API_KEY",
|
|
26
|
+
"command-code": "COMMAND_CODE_API_KEY",
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
const MIN_API_KEY_LENGTH = 8;
|
|
30
|
+
const MAX_API_KEY_LENGTH = 4096;
|
|
31
|
+
|
|
32
|
+
function isProviderId(provider) {
|
|
33
|
+
return typeof provider === "string" && Object.prototype.hasOwnProperty.call(PROVIDER_KEY_FILES, provider);
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
function envNameForProvider(provider) {
|
|
37
|
+
return isProviderId(provider) ? PROVIDER_ENV_NAMES[provider] : null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function resolveProviderKeyDir({ trackerDir, home } = {}) {
|
|
41
|
+
if (typeof trackerDir === "string" && trackerDir.trim()) return trackerDir.trim();
|
|
42
|
+
if (typeof home === "string" && home.trim()) {
|
|
43
|
+
return path.join(home.trim(), ".tokentracker", "tracker");
|
|
44
|
+
}
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
function resolveProviderKeyPath({ provider, trackerDir, home } = {}) {
|
|
49
|
+
if (!isProviderId(provider)) return null;
|
|
50
|
+
const dir = resolveProviderKeyDir({ trackerDir, home });
|
|
51
|
+
if (!dir) return null;
|
|
52
|
+
return path.join(dir, PROVIDER_KEY_FILES[provider]);
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Keys are opaque bearer tokens. Reject anything that cannot be pasted into an
|
|
56
|
+
// Authorization header verbatim (internal whitespace/control characters) and
|
|
57
|
+
// keep an upper bound so a mis-pasted blob cannot bloat the auth file.
|
|
58
|
+
function normalizeApiKey(value) {
|
|
59
|
+
if (typeof value !== "string") return "";
|
|
60
|
+
const trimmed = value.trim();
|
|
61
|
+
if (trimmed.length < MIN_API_KEY_LENGTH || trimmed.length > MAX_API_KEY_LENGTH) return "";
|
|
62
|
+
if (/[\s\u0000-\u001f\u007f]/.test(trimmed)) return "";
|
|
63
|
+
return trimmed;
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
// Never throws: a missing, unreadable, or malformed store must degrade to
|
|
67
|
+
// "no stored key" so a limits fetch can still fall back to env/cookie/CLI.
|
|
68
|
+
function readStoredApiKey({ provider, trackerDir, home } = {}) {
|
|
69
|
+
const filePath = resolveProviderKeyPath({ provider, trackerDir, home });
|
|
70
|
+
if (!filePath) return "";
|
|
71
|
+
let raw;
|
|
72
|
+
try {
|
|
73
|
+
raw = fssync.readFileSync(filePath, "utf8");
|
|
74
|
+
} catch {
|
|
75
|
+
return "";
|
|
76
|
+
}
|
|
77
|
+
let parsed;
|
|
78
|
+
try {
|
|
79
|
+
parsed = JSON.parse(raw);
|
|
80
|
+
} catch {
|
|
81
|
+
return "";
|
|
82
|
+
}
|
|
83
|
+
return normalizeApiKey(parsed?.apiKey);
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
async function writeStoredApiKey({ provider, trackerDir, home, apiKey } = {}) {
|
|
87
|
+
const filePath = resolveProviderKeyPath({ provider, trackerDir, home });
|
|
88
|
+
if (!filePath) throw new Error("No tracker data directory available for the API key.");
|
|
89
|
+
const normalized = normalizeApiKey(apiKey);
|
|
90
|
+
if (!normalized) throw new Error("Invalid API key.");
|
|
91
|
+
await ensureDir(path.dirname(filePath));
|
|
92
|
+
const contents = `${JSON.stringify({ apiKey: normalized, updatedAt: new Date().toISOString() }, null, 2)}\n`;
|
|
93
|
+
await writeFileAtomic(filePath, contents, { mode: 0o600 });
|
|
94
|
+
// writeFileAtomic only applies `mode` to newly created files; an existing
|
|
95
|
+
// store with looser permissions must still end up private.
|
|
96
|
+
await chmod600IfPossible(filePath);
|
|
97
|
+
return filePath;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
module.exports = {
|
|
101
|
+
PROVIDER_KEY_FILES,
|
|
102
|
+
PROVIDER_ENV_NAMES,
|
|
103
|
+
isProviderId,
|
|
104
|
+
envNameForProvider,
|
|
105
|
+
resolveProviderKeyDir,
|
|
106
|
+
resolveProviderKeyPath,
|
|
107
|
+
normalizeApiKey,
|
|
108
|
+
readStoredApiKey,
|
|
109
|
+
writeStoredApiKey,
|
|
110
|
+
};
|
package/src/lib/rollout.js
CHANGED
|
@@ -28,6 +28,7 @@ const {
|
|
|
28
28
|
CODEX_SERVICE_TIER_MARKER,
|
|
29
29
|
readCodexServiceTier,
|
|
30
30
|
isPriorityServiceTier,
|
|
31
|
+
isUltrafastServiceTier,
|
|
31
32
|
} = require("./codex-service-tier");
|
|
32
33
|
const {
|
|
33
34
|
awaitsCompactedLine,
|
|
@@ -2649,6 +2650,7 @@ async function parseRolloutRecords({
|
|
|
2649
2650
|
const bucket = getHourlyBucket(hourlyState, source, event.model, event.bucketStart);
|
|
2650
2651
|
addTotals(bucket.totals, event.delta);
|
|
2651
2652
|
if (isPriorityServiceTier(event.serviceTier)) addPriorityUsage(bucket.totals, event.delta);
|
|
2653
|
+
if (isUltrafastServiceTier(event.serviceTier)) addUltrafastUsage(bucket.totals, event.delta);
|
|
2652
2654
|
touchedBuckets.add(bucketKey(source, event.model, event.bucketStart));
|
|
2653
2655
|
if (event.projectKey && projectState && projectTouchedBuckets) {
|
|
2654
2656
|
const projectBucket = getProjectBucket(
|
|
@@ -2903,6 +2905,7 @@ async function parseRolloutRecords({
|
|
|
2903
2905
|
// Only the model bucket gets the subset: the project bucket below carries
|
|
2904
2906
|
// no model, so no per-model rate can be applied to it anyway.
|
|
2905
2907
|
if (isPriorityServiceTier(serviceTier)) addPriorityUsage(bucket.totals, delta);
|
|
2908
|
+
if (isUltrafastServiceTier(serviceTier)) addUltrafastUsage(bucket.totals, delta);
|
|
2906
2909
|
touchedBuckets.add(bucketKey(source, model, bucketStart));
|
|
2907
2910
|
if (currentProjectKey && projectState && projectTouchedBuckets) {
|
|
2908
2911
|
const projectBucket = getProjectBucket(
|
|
@@ -4422,6 +4425,16 @@ const PRIORITY_SUBSET_FIELDS = [
|
|
|
4422
4425
|
["priority_reasoning_output_tokens", "reasoning_output_tokens"],
|
|
4423
4426
|
];
|
|
4424
4427
|
|
|
4428
|
+
const ULTRAFAST_SUBSET_FIELDS = [
|
|
4429
|
+
["ultrafast_input_tokens", "input_tokens"],
|
|
4430
|
+
["ultrafast_cached_input_tokens", "cached_input_tokens"],
|
|
4431
|
+
["ultrafast_cache_creation_input_tokens", "cache_creation_input_tokens"],
|
|
4432
|
+
["ultrafast_output_tokens", "output_tokens"],
|
|
4433
|
+
["ultrafast_reasoning_output_tokens", "reasoning_output_tokens"],
|
|
4434
|
+
];
|
|
4435
|
+
|
|
4436
|
+
const SPEED_TIER_SUBSET_FIELDS = [...PRIORITY_SUBSET_FIELDS, ...ULTRAFAST_SUBSET_FIELDS];
|
|
4437
|
+
|
|
4425
4438
|
// The whole delta belongs to a priority request, so every base column it
|
|
4426
4439
|
// carries is priority usage.
|
|
4427
4440
|
function addPriorityUsage(target, delta) {
|
|
@@ -4431,9 +4444,16 @@ function addPriorityUsage(target, delta) {
|
|
|
4431
4444
|
}
|
|
4432
4445
|
}
|
|
4433
4446
|
|
|
4447
|
+
function addUltrafastUsage(target, delta) {
|
|
4448
|
+
for (const [field, base] of ULTRAFAST_SUBSET_FIELDS) {
|
|
4449
|
+
const value = Number(delta?.[base] || 0);
|
|
4450
|
+
if (value > 0) target[field] = (Number(target[field]) || 0) + value;
|
|
4451
|
+
}
|
|
4452
|
+
}
|
|
4453
|
+
|
|
4434
4454
|
function prioritySubsetOf(totals) {
|
|
4435
4455
|
const subset = {};
|
|
4436
|
-
for (const [field] of
|
|
4456
|
+
for (const [field] of SPEED_TIER_SUBSET_FIELDS) {
|
|
4437
4457
|
const value = Number(totals?.[field]) || 0;
|
|
4438
4458
|
if (value > 0) subset[field] = value;
|
|
4439
4459
|
}
|
|
@@ -4441,7 +4461,7 @@ function prioritySubsetOf(totals) {
|
|
|
4441
4461
|
}
|
|
4442
4462
|
|
|
4443
4463
|
function addTotals(target, delta) {
|
|
4444
|
-
for (const [field] of
|
|
4464
|
+
for (const [field] of SPEED_TIER_SUBSET_FIELDS) {
|
|
4445
4465
|
const value = Number(delta?.[field]) || 0;
|
|
4446
4466
|
if (value > 0) target[field] = (Number(target[field]) || 0) + value;
|
|
4447
4467
|
}
|
|
@@ -4511,7 +4531,11 @@ function totalsKey(totals) {
|
|
|
4511
4531
|
// Appended only when a priority subset exists, so buckets that never see the
|
|
4512
4532
|
// tier keep their previously persisted queuedKey and are not re-enqueued.
|
|
4513
4533
|
const priority = PRIORITY_SUBSET_FIELDS.map(([field]) => Number(totals[field]) || 0);
|
|
4514
|
-
|
|
4534
|
+
const ultrafast = ULTRAFAST_SUBSET_FIELDS.map(([field]) => Number(totals[field]) || 0);
|
|
4535
|
+
let key = base;
|
|
4536
|
+
if (priority.some((value) => value > 0)) key += `|p:${priority.join(",")}`;
|
|
4537
|
+
if (ultrafast.some((value) => value > 0)) key += `|u:${ultrafast.join(",")}`;
|
|
4538
|
+
return key;
|
|
4515
4539
|
}
|
|
4516
4540
|
|
|
4517
4541
|
function toUtcHalfHourStart(ts) {
|
|
@@ -0,0 +1,231 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Optional Sentry error reporting for the Node CLI / EmbeddedServer runtime.
|
|
5
|
+
*
|
|
6
|
+
* Privacy contract (keep in sync with docs/PRIVACY.md and dashboard/src/lib/sentry.js):
|
|
7
|
+
* - Off unless TOKENTRACKER_SENTRY_DSN (or SENTRY_DSN) is set.
|
|
8
|
+
* - Honors the same telemetry opt-out as the daily heartbeat
|
|
9
|
+
* (TOKENTRACKER_NO_TELEMETRY / DO_NOT_TRACK / config.json `"telemetry": false`).
|
|
10
|
+
* - Fail-closed in the node test runner.
|
|
11
|
+
* - Never intended to carry prompts, code, or provider secrets; beforeSend
|
|
12
|
+
* scrubs common secret patterns from event payloads.
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const path = require("node:path");
|
|
16
|
+
const pkg = require("../../package.json");
|
|
17
|
+
const { readJson } = require("./fs");
|
|
18
|
+
const { isTelemetryDisabled } = require("./telemetry");
|
|
19
|
+
|
|
20
|
+
const SECRET_PATTERN =
|
|
21
|
+
/(?:api[_-]?key|access[_-]?token|refresh[_-]?token|cookie|secret|password|dsn)[=:\s]+["']?[^\s"'\\]{8,}/gi;
|
|
22
|
+
const BEARER_PATTERN = /\bBearer\s+[A-Za-z0-9._~+/=-]{8,}/gi;
|
|
23
|
+
const JWT_PATTERN = /\beyJ[A-Za-z0-9_-]{10,}\.[A-Za-z0-9._-]{10,}\.[A-Za-z0-9._-]{10,}\b/g;
|
|
24
|
+
|
|
25
|
+
let initialized = false;
|
|
26
|
+
|
|
27
|
+
function resolveDsn(env = process.env) {
|
|
28
|
+
const raw = env?.TOKENTRACKER_SENTRY_DSN || env?.SENTRY_DSN || "";
|
|
29
|
+
return typeof raw === "string" ? raw.trim() : "";
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
function resolveEnvironment(env = process.env) {
|
|
33
|
+
const raw = typeof env?.TOKENTRACKER_SENTRY_ENVIRONMENT === "string"
|
|
34
|
+
? env.TOKENTRACKER_SENTRY_ENVIRONMENT.trim()
|
|
35
|
+
: "";
|
|
36
|
+
if (raw) return raw;
|
|
37
|
+
if (env?.NODE_ENV === "production") return "production";
|
|
38
|
+
return "development";
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
function resolveShell(env = process.env) {
|
|
42
|
+
const raw = typeof env?.TOKENTRACKER_APP_SHELL === "string"
|
|
43
|
+
? env.TOKENTRACKER_APP_SHELL.trim().toLowerCase()
|
|
44
|
+
: "";
|
|
45
|
+
if (raw === "macos" || raw === "windows" || raw === "linux" || raw === "cli") return raw;
|
|
46
|
+
return "cli";
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
function scrubString(value) {
|
|
50
|
+
if (typeof value !== "string" || !value) return value;
|
|
51
|
+
return value
|
|
52
|
+
.replace(BEARER_PATTERN, "Bearer [Filtered]")
|
|
53
|
+
.replace(JWT_PATTERN, "[Filtered]")
|
|
54
|
+
.replace(SECRET_PATTERN, "[Filtered]");
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
function scrubValue(value, depth = 0) {
|
|
58
|
+
if (depth > 6 || value == null) return value;
|
|
59
|
+
if (typeof value === "string") return scrubString(value);
|
|
60
|
+
if (Array.isArray(value)) return value.map((item) => scrubValue(item, depth + 1));
|
|
61
|
+
if (typeof value === "object") {
|
|
62
|
+
const out = {};
|
|
63
|
+
for (const [key, nested] of Object.entries(value)) {
|
|
64
|
+
const lower = key.toLowerCase();
|
|
65
|
+
if (
|
|
66
|
+
lower.includes("authorization") ||
|
|
67
|
+
lower.includes("cookie") ||
|
|
68
|
+
lower.includes("password") ||
|
|
69
|
+
lower.includes("secret") ||
|
|
70
|
+
lower.includes("token") ||
|
|
71
|
+
lower.includes("api_key") ||
|
|
72
|
+
lower.includes("apikey")
|
|
73
|
+
) {
|
|
74
|
+
out[key] = "[Filtered]";
|
|
75
|
+
} else {
|
|
76
|
+
out[key] = scrubValue(nested, depth + 1);
|
|
77
|
+
}
|
|
78
|
+
}
|
|
79
|
+
return out;
|
|
80
|
+
}
|
|
81
|
+
return value;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function truncateMaybe(value, max = 240) {
|
|
85
|
+
if (typeof value !== "string") return value;
|
|
86
|
+
if (value.length <= max) return value;
|
|
87
|
+
return `${value.slice(0, max)}…[truncated]`;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function scrubEvent(event) {
|
|
91
|
+
if (!event || typeof event !== "object") return event;
|
|
92
|
+
const next = { ...event };
|
|
93
|
+
if (next.message) next.message = truncateMaybe(scrubString(next.message));
|
|
94
|
+
if (next.extra) next.extra = scrubValue(next.extra);
|
|
95
|
+
if (next.contexts) next.contexts = scrubValue(next.contexts);
|
|
96
|
+
if (next.request) next.request = scrubValue(next.request);
|
|
97
|
+
// Console breadcrumbs can carry parser / JSON.parse excerpts of local logs
|
|
98
|
+
// (prompt text). Drop them entirely; keep only non-console crumbs and scrub.
|
|
99
|
+
if (Array.isArray(next.breadcrumbs)) {
|
|
100
|
+
next.breadcrumbs = next.breadcrumbs
|
|
101
|
+
.filter((crumb) => crumb && crumb.category !== "console")
|
|
102
|
+
.map((crumb) => scrubValue(crumb));
|
|
103
|
+
}
|
|
104
|
+
if (next.exception?.values) {
|
|
105
|
+
next.exception = {
|
|
106
|
+
...next.exception,
|
|
107
|
+
values: next.exception.values.map((item) => ({
|
|
108
|
+
...item,
|
|
109
|
+
value: truncateMaybe(scrubString(item?.value)),
|
|
110
|
+
})),
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
// Never attach IP / user PII by default.
|
|
114
|
+
if (next.user) {
|
|
115
|
+
next.user = { id: typeof next.user.id === "string" ? next.user.id : undefined };
|
|
116
|
+
}
|
|
117
|
+
return next;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Initialise Sentry once for this process. Never throws.
|
|
122
|
+
* @returns {Promise<{ enabled: boolean, reason?: string }>}
|
|
123
|
+
*/
|
|
124
|
+
async function initSentry({
|
|
125
|
+
env = process.env,
|
|
126
|
+
trackerDir,
|
|
127
|
+
version = pkg.version,
|
|
128
|
+
} = {}) {
|
|
129
|
+
try {
|
|
130
|
+
if (initialized) return { enabled: true, reason: "already-initialized" };
|
|
131
|
+
if (env?.NODE_TEST_CONTEXT !== undefined) {
|
|
132
|
+
return { enabled: false, reason: "test-env" };
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const dsn = resolveDsn(env);
|
|
136
|
+
if (!dsn) return { enabled: false, reason: "no-dsn" };
|
|
137
|
+
|
|
138
|
+
let config = {};
|
|
139
|
+
if (trackerDir) {
|
|
140
|
+
config = (await readJson(path.join(trackerDir, "config.json"))) || {};
|
|
141
|
+
}
|
|
142
|
+
if (isTelemetryDisabled({ env, config })) {
|
|
143
|
+
return { enabled: false, reason: "opt-out" };
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
// Honor the same outbound proxy / fail-closed policy as the rest of the CLI.
|
|
147
|
+
// Sentry's Node transport uses node:https (not undici), so pass proxy explicitly.
|
|
148
|
+
const {
|
|
149
|
+
readPersistedProxyConfig,
|
|
150
|
+
resolveEffectiveProxySource,
|
|
151
|
+
} = require("./proxy-env");
|
|
152
|
+
const proxyConfig = readPersistedProxyConfig();
|
|
153
|
+
const proxyResolved = resolveEffectiveProxySource({ env, proxyConfig });
|
|
154
|
+
if (proxyResolved?.source === "blocked") {
|
|
155
|
+
return { enabled: false, reason: "proxy-blocked" };
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Lazy require keeps unit tests that never call init from paying the
|
|
159
|
+
// module graph cost at import time of unrelated modules.
|
|
160
|
+
const Sentry = require("@sentry/node");
|
|
161
|
+
Sentry.init({
|
|
162
|
+
dsn,
|
|
163
|
+
environment: resolveEnvironment(env),
|
|
164
|
+
release: `tokentracker@${String(version || "").trim() || "0.0.0"}`,
|
|
165
|
+
sendDefaultPii: false,
|
|
166
|
+
tracesSampleRate: 0,
|
|
167
|
+
// Errors only: drop process-session + console; replace Http so request
|
|
168
|
+
// sessions stay off (Http defaults sessions:true and bypasses beforeSend).
|
|
169
|
+
integrations(defaults) {
|
|
170
|
+
const filtered = defaults.filter((integration) => {
|
|
171
|
+
const name = integration?.name || "";
|
|
172
|
+
return name !== "ProcessSession" && name !== "Console" && name !== "Http";
|
|
173
|
+
});
|
|
174
|
+
filtered.push(Sentry.httpIntegration({ sessions: false }));
|
|
175
|
+
return filtered;
|
|
176
|
+
},
|
|
177
|
+
transportOptions: proxyResolved?.proxyUrl
|
|
178
|
+
? { proxy: proxyResolved.proxyUrl }
|
|
179
|
+
: undefined,
|
|
180
|
+
beforeSend(event) {
|
|
181
|
+
return scrubEvent(event);
|
|
182
|
+
},
|
|
183
|
+
initialScope: {
|
|
184
|
+
tags: {
|
|
185
|
+
shell: resolveShell(env),
|
|
186
|
+
runtime: "node",
|
|
187
|
+
},
|
|
188
|
+
},
|
|
189
|
+
});
|
|
190
|
+
initialized = true;
|
|
191
|
+
return { enabled: true };
|
|
192
|
+
} catch {
|
|
193
|
+
return { enabled: false, reason: "init-failed" };
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
function captureException(error, context) {
|
|
198
|
+
if (!initialized) return;
|
|
199
|
+
try {
|
|
200
|
+
const Sentry = require("@sentry/node");
|
|
201
|
+
if (context) {
|
|
202
|
+
Sentry.withScope((scope) => {
|
|
203
|
+
if (context.tags) {
|
|
204
|
+
for (const [key, value] of Object.entries(context.tags)) {
|
|
205
|
+
scope.setTag(key, String(value));
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
if (context.extra) scope.setExtras(scrubValue(context.extra));
|
|
209
|
+
Sentry.captureException(error);
|
|
210
|
+
});
|
|
211
|
+
return;
|
|
212
|
+
}
|
|
213
|
+
Sentry.captureException(error);
|
|
214
|
+
} catch {
|
|
215
|
+
/* never break the CLI */
|
|
216
|
+
}
|
|
217
|
+
}
|
|
218
|
+
|
|
219
|
+
module.exports = {
|
|
220
|
+
initSentry,
|
|
221
|
+
captureException,
|
|
222
|
+
resolveDsn,
|
|
223
|
+
resolveEnvironment,
|
|
224
|
+
resolveShell,
|
|
225
|
+
scrubEvent,
|
|
226
|
+
scrubString,
|
|
227
|
+
// test helpers
|
|
228
|
+
_resetForTests() {
|
|
229
|
+
initialized = false;
|
|
230
|
+
},
|
|
231
|
+
};
|
|
@@ -87,7 +87,9 @@ const {
|
|
|
87
87
|
// v23 excludes database updates and user records from request durations.
|
|
88
88
|
// v24 keeps OpenCode usage dates at event time and fork history on its ancestor.
|
|
89
89
|
// v25 adds Kimi wire sessions, including official/legacy migration dedup.
|
|
90
|
-
|
|
90
|
+
// v26 retains Codex Ultrafast (service_tier ultrafast) subset columns so
|
|
91
|
+
// date-filtered session views and cached sidecars keep the 6x premium.
|
|
92
|
+
const SIDECAR_VERSION = 26;
|
|
91
93
|
const EDIT_TOOLS = new Set([
|
|
92
94
|
"apply_patch",
|
|
93
95
|
"edit",
|
|
@@ -141,7 +143,7 @@ function addTotals(target, delta) {
|
|
|
141
143
|
target[key] = finite(target[key]) + finite(delta?.[key]);
|
|
142
144
|
}
|
|
143
145
|
for (const key of MODEL_USAGE_SUM_FIELDS) {
|
|
144
|
-
if (!/^(?:long_context_|priority_)|^(?:usage_events|rerouted_usage_events)$/.test(key)) continue;
|
|
146
|
+
if (!/^(?:long_context_|priority_|ultrafast_)|^(?:usage_events|rerouted_usage_events|long_context_usage_events|priority_usage_events|ultrafast_usage_events)$/.test(key)) continue;
|
|
145
147
|
if (delta?.[key]) target[key] = finite(target[key]) + finite(delta[key]);
|
|
146
148
|
}
|
|
147
149
|
}
|
|
@@ -179,10 +181,21 @@ const MODEL_USAGE_SUM_FIELDS = [
|
|
|
179
181
|
"priority_long_context_cache_creation_input_tokens",
|
|
180
182
|
"priority_long_context_output_tokens",
|
|
181
183
|
"priority_long_context_reasoning_output_tokens",
|
|
184
|
+
"ultrafast_input_tokens",
|
|
185
|
+
"ultrafast_cached_input_tokens",
|
|
186
|
+
"ultrafast_cache_creation_input_tokens",
|
|
187
|
+
"ultrafast_output_tokens",
|
|
188
|
+
"ultrafast_reasoning_output_tokens",
|
|
189
|
+
"ultrafast_long_context_input_tokens",
|
|
190
|
+
"ultrafast_long_context_cached_input_tokens",
|
|
191
|
+
"ultrafast_long_context_cache_creation_input_tokens",
|
|
192
|
+
"ultrafast_long_context_output_tokens",
|
|
193
|
+
"ultrafast_long_context_reasoning_output_tokens",
|
|
182
194
|
"usage_events",
|
|
183
195
|
"rerouted_usage_events",
|
|
184
196
|
"long_context_usage_events",
|
|
185
197
|
"priority_usage_events",
|
|
198
|
+
"ultrafast_usage_events",
|
|
186
199
|
// Not a token counter: an edit turn is a turn, and turn_context both marks
|
|
187
200
|
// where a turn begins and names its model, so an edit turn has exactly one
|
|
188
201
|
// owner. Splitting it here keeps by_model.tokens_per_edit a ratio over one
|
package/src/lib/static-server.js
CHANGED
|
@@ -33,11 +33,22 @@ async function serveStaticFile(baseDir, pathname, res, { localRuntimeConfig = fa
|
|
|
33
33
|
// prevent directory traversal
|
|
34
34
|
if (!filePath.startsWith(baseDir)) return false;
|
|
35
35
|
|
|
36
|
+
// Watch for the client going away from the start: if the browser cancels the
|
|
37
|
+
// request during the awaits below, "close" fires before the stream exists and
|
|
38
|
+
// a listener added later never runs, leaking the FileHandle (fatal on Node 25+).
|
|
39
|
+
let clientGone = res.destroyed;
|
|
40
|
+
let stream = null;
|
|
41
|
+
res.once("close", () => {
|
|
42
|
+
clientGone = true;
|
|
43
|
+
if (stream) stream.destroy();
|
|
44
|
+
});
|
|
45
|
+
|
|
36
46
|
let handle;
|
|
37
47
|
try {
|
|
38
48
|
handle = await fsPromises.open(filePath, "r");
|
|
39
49
|
const stat = await handle.stat();
|
|
40
50
|
if (!stat.isFile()) return false;
|
|
51
|
+
if (clientGone) return true; // finally closes the handle
|
|
41
52
|
|
|
42
53
|
const ext = path.extname(filePath).toLowerCase();
|
|
43
54
|
const contentType = MIME_TYPES[ext] || "application/octet-stream";
|
|
@@ -47,21 +58,23 @@ async function serveStaticFile(baseDir, pathname, res, { localRuntimeConfig = fa
|
|
|
47
58
|
const source = await handle.readFile("utf8");
|
|
48
59
|
const content = source.replace(/<head(?:\s[^>]*)?>/i,
|
|
49
60
|
"$&\n<script>window.__TOKENTRACKER_RUNTIME_CONFIG__={configurationError:\"runtime_config_unavailable\"};</script>\n<script src=\"/api/runtime-config.js\"></script>");
|
|
61
|
+
if (clientGone) return true;
|
|
50
62
|
res.writeHead(200, { "Content-Type": contentType, "Content-Length": Buffer.byteLength(content), "Cache-Control": "no-store" });
|
|
51
63
|
res.end(content);
|
|
52
64
|
return true;
|
|
53
65
|
}
|
|
54
66
|
|
|
67
|
+
if (clientGone) return true;
|
|
68
|
+
|
|
55
69
|
res.writeHead(200, {
|
|
56
70
|
"Content-Type": contentType,
|
|
57
71
|
"Content-Length": stat.size,
|
|
58
72
|
"Cache-Control": isHtml ? "no-cache" : "public, max-age=31536000, immutable",
|
|
59
73
|
});
|
|
60
74
|
|
|
61
|
-
|
|
75
|
+
stream = handle.createReadStream();
|
|
62
76
|
handle = null; // The stream owns and closes this descriptor.
|
|
63
77
|
stream.on("error", (error) => res.destroy(error));
|
|
64
|
-
res.on("close", () => stream.destroy());
|
|
65
78
|
stream.pipe(res);
|
|
66
79
|
return true;
|
|
67
80
|
} catch (_e) {
|
package/src/lib/usage-limits.js
CHANGED
|
@@ -56,6 +56,10 @@ const execFileAsync = promisify(cp.execFile);
|
|
|
56
56
|
const USAGE_LIMITS_SELECTION_KEYS = ["off-on", "off-off", "on-on", "on-off"];
|
|
57
57
|
const cacheBySelection = {};
|
|
58
58
|
for (const key of USAGE_LIMITS_SELECTION_KEYS) cacheBySelection[key] = { data: null, expiresAtMs: 0 };
|
|
59
|
+
// Credential replacements (provider-key save) bump this generation so a fetch
|
|
60
|
+
// that started under the old credential cannot repopulate the memory or disk
|
|
61
|
+
// caches after invalidation.
|
|
62
|
+
let usageLimitsCacheGeneration = 0;
|
|
59
63
|
function usageLimitsSelectionKey(options) {
|
|
60
64
|
const devin = options?.devinEnabled === true ? "on" : "off";
|
|
61
65
|
const antigravity = isAntigravityQuotaDisabled() ? "off" : "on";
|
|
@@ -2603,8 +2607,12 @@ function writeCodexLimitsCache(limits, { home, nowMs = Date.now() } = {}) {
|
|
|
2603
2607
|
} catch (_error) {}
|
|
2604
2608
|
}
|
|
2605
2609
|
|
|
2606
|
-
function resolveOpencodeGoLimitsCachePath({ home } = {}) {
|
|
2607
|
-
|
|
2610
|
+
function resolveOpencodeGoLimitsCachePath({ trackerDir, home } = {}) {
|
|
2611
|
+
const dir =
|
|
2612
|
+
typeof trackerDir === "string" && trackerDir.trim()
|
|
2613
|
+
? trackerDir.trim()
|
|
2614
|
+
: path.join(home || os.homedir(), ".tokentracker", "tracker");
|
|
2615
|
+
return path.join(dir, OPENCODE_GO_LIMITS_CACHE_FILE);
|
|
2608
2616
|
}
|
|
2609
2617
|
|
|
2610
2618
|
function hasOpencodeGoWindow(limits) {
|
|
@@ -2632,10 +2640,15 @@ function normalizeOpencodeGoCachedLimits(
|
|
|
2632
2640
|
if (cachedAtMs > nowMs + 60_000) return null;
|
|
2633
2641
|
if (nowMs - cachedAtMs > effectiveMaxAge) return null;
|
|
2634
2642
|
|
|
2643
|
+
const credentialSource =
|
|
2644
|
+
raw?.credential_source === "env" || raw?.credential_source === "stored"
|
|
2645
|
+
? raw.credential_source
|
|
2646
|
+
: null;
|
|
2635
2647
|
const cached = {
|
|
2636
2648
|
configured: true,
|
|
2637
2649
|
error: null,
|
|
2638
2650
|
source: typeof raw?.source === "string" ? raw.source : "api",
|
|
2651
|
+
credential_source: credentialSource,
|
|
2639
2652
|
subscription_status: typeof raw?.subscription_status === "string" ? raw.subscription_status : null,
|
|
2640
2653
|
plan_label: typeof raw?.plan_label === "string" ? raw.plan_label : null,
|
|
2641
2654
|
primary_window: isOpencodeGoCacheWindowUsable(raw?.primary_window, { nowMs }) ? raw.primary_window : null,
|
|
@@ -2647,8 +2660,8 @@ function normalizeOpencodeGoCachedLimits(
|
|
|
2647
2660
|
return hasOpencodeGoWindow(cached) ? cached : null;
|
|
2648
2661
|
}
|
|
2649
2662
|
|
|
2650
|
-
function readOpencodeGoLimitsCache({ home, nowMs = Date.now() } = {}) {
|
|
2651
|
-
const cachePath = resolveOpencodeGoLimitsCachePath({ home });
|
|
2663
|
+
function readOpencodeGoLimitsCache({ trackerDir, home, nowMs = Date.now() } = {}) {
|
|
2664
|
+
const cachePath = resolveOpencodeGoLimitsCachePath({ trackerDir, home });
|
|
2652
2665
|
try {
|
|
2653
2666
|
const parsed = JSON.parse(fs.readFileSync(cachePath, "utf8"));
|
|
2654
2667
|
return normalizeOpencodeGoCachedLimits(parsed?.opencodeGo, { nowMs });
|
|
@@ -2657,12 +2670,17 @@ function readOpencodeGoLimitsCache({ home, nowMs = Date.now() } = {}) {
|
|
|
2657
2670
|
}
|
|
2658
2671
|
}
|
|
2659
2672
|
|
|
2660
|
-
function writeOpencodeGoLimitsCache(limits, { home, nowMs = Date.now() } = {}) {
|
|
2673
|
+
function writeOpencodeGoLimitsCache(limits, { trackerDir, home, nowMs = Date.now() } = {}) {
|
|
2661
2674
|
if (!limits?.configured || limits.error || !hasOpencodeGoWindow(limits)) return;
|
|
2662
|
-
const cachePath = resolveOpencodeGoLimitsCachePath({ home });
|
|
2675
|
+
const cachePath = resolveOpencodeGoLimitsCachePath({ trackerDir, home });
|
|
2676
|
+
const credentialSource =
|
|
2677
|
+
limits.credential_source === "env" || limits.credential_source === "stored"
|
|
2678
|
+
? limits.credential_source
|
|
2679
|
+
: null;
|
|
2663
2680
|
const payload = {
|
|
2664
2681
|
opencodeGo: {
|
|
2665
2682
|
source: limits.source || "api",
|
|
2683
|
+
credential_source: credentialSource,
|
|
2666
2684
|
subscription_status: limits.subscription_status || null,
|
|
2667
2685
|
plan_label: limits.plan_label || null,
|
|
2668
2686
|
primary_window: limits.primary_window || null,
|
|
@@ -3989,7 +4007,12 @@ async function getUsageLimits(options = {}) {
|
|
|
3989
4007
|
if (inFlightBySelection[selection]) {
|
|
3990
4008
|
return inFlightBySelection[selection];
|
|
3991
4009
|
}
|
|
3992
|
-
const
|
|
4010
|
+
const cacheGeneration = usageLimitsCacheGeneration;
|
|
4011
|
+
const promise = fetchUsageLimitsUncached({
|
|
4012
|
+
...options,
|
|
4013
|
+
selectionKey: selection,
|
|
4014
|
+
cacheGeneration,
|
|
4015
|
+
}).finally(() => {
|
|
3993
4016
|
if (inFlightBySelection[selection] === promise) inFlightBySelection[selection] = null;
|
|
3994
4017
|
});
|
|
3995
4018
|
inFlightBySelection[selection] = promise;
|
|
@@ -4002,6 +4025,7 @@ async function getUsageLimits(options = {}) {
|
|
|
4002
4025
|
*/
|
|
4003
4026
|
async function fetchUsageLimitsUncached({
|
|
4004
4027
|
home,
|
|
4028
|
+
trackerDir,
|
|
4005
4029
|
env,
|
|
4006
4030
|
platform,
|
|
4007
4031
|
securityRunner,
|
|
@@ -4013,6 +4037,10 @@ async function fetchUsageLimitsUncached({
|
|
|
4013
4037
|
providerTimeoutMs = DEFAULT_PROVIDER_TIMEOUT_MS,
|
|
4014
4038
|
forceRefresh = false,
|
|
4015
4039
|
devinEnabled = false,
|
|
4040
|
+
// Generation captured by getUsageLimits before the fetch began. A provider
|
|
4041
|
+
// key save bumps it, and this fetch must then stop at returning its own
|
|
4042
|
+
// result: publishing it would restore pre-save data for every later caller.
|
|
4043
|
+
cacheGeneration = usageLimitsCacheGeneration,
|
|
4016
4044
|
// Selection key fixed by getUsageLimits before the fetch began. Re-deriving it
|
|
4017
4045
|
// here would let an opt-out flipped mid-fetch store a pre-opt-out result in the
|
|
4018
4046
|
// post-opt-out slot.
|
|
@@ -4131,7 +4159,7 @@ async function fetchUsageLimitsUncached({
|
|
|
4131
4159
|
// OpenCode Go: authoritative subscription windows come from the dashboard
|
|
4132
4160
|
// scrape; local opencode.db cost is available only as an explicit estimate.
|
|
4133
4161
|
// See src/lib/opencode-go-limits.js.
|
|
4134
|
-
withProviderTimeout(fetchOpencodeGoLimits({ home, env, fetchImpl: providerFetch }), "OpenCode Go", providerTimeoutMs)
|
|
4162
|
+
withProviderTimeout(fetchOpencodeGoLimits({ home, trackerDir, env, fetchImpl: providerFetch }), "OpenCode Go", providerTimeoutMs)
|
|
4135
4163
|
.catch((reason) => ({ configured: true, error: reason?.message || "Unknown error" })),
|
|
4136
4164
|
withProviderTimeout(
|
|
4137
4165
|
fetchQoderLimits({
|
|
@@ -4189,7 +4217,7 @@ async function fetchUsageLimitsUncached({
|
|
|
4189
4217
|
// outer race bounds the whole slot: without it three sequential per-request
|
|
4190
4218
|
// timeouts (whoami, then credits+subscriptions) could hold the aggregate
|
|
4191
4219
|
// ~2x longer than any sibling provider.
|
|
4192
|
-
withProviderTimeout(fetchCommandcodeLimits({ home, env, fetchImpl: providerFetch }), "CommandCode", providerTimeoutMs)
|
|
4220
|
+
withProviderTimeout(fetchCommandcodeLimits({ home, trackerDir, env, fetchImpl: providerFetch }), "CommandCode", providerTimeoutMs)
|
|
4193
4221
|
.then(
|
|
4194
4222
|
(value) => ({ status: "fulfilled", value }),
|
|
4195
4223
|
(reason) => ({ status: "rejected", reason }),
|
|
@@ -4382,9 +4410,11 @@ async function fetchUsageLimitsUncached({
|
|
|
4382
4410
|
stale: false,
|
|
4383
4411
|
cached_at: new Date(nowMs).toISOString(),
|
|
4384
4412
|
};
|
|
4385
|
-
|
|
4413
|
+
if (cacheGeneration === usageLimitsCacheGeneration) {
|
|
4414
|
+
writeOpencodeGoLimitsCache(opencodeGo, { trackerDir, home, nowMs });
|
|
4415
|
+
}
|
|
4386
4416
|
} else {
|
|
4387
|
-
const cached = readOpencodeGoLimitsCache({ home, nowMs });
|
|
4417
|
+
const cached = readOpencodeGoLimitsCache({ trackerDir, home, nowMs });
|
|
4388
4418
|
if (cached) {
|
|
4389
4419
|
opencodeGo = cached;
|
|
4390
4420
|
} else {
|
|
@@ -4491,16 +4521,22 @@ async function fetchUsageLimitsUncached({
|
|
|
4491
4521
|
};
|
|
4492
4522
|
}
|
|
4493
4523
|
|
|
4494
|
-
|
|
4495
|
-
|
|
4496
|
-
|
|
4497
|
-
|
|
4524
|
+
if (cacheGeneration === usageLimitsCacheGeneration) {
|
|
4525
|
+
cacheBySelection[selectionKey || usageLimitsSelectionKey({ devinEnabled })] = {
|
|
4526
|
+
data,
|
|
4527
|
+
expiresAtMs: cacheExpiresAtMs(data, nowMs),
|
|
4528
|
+
};
|
|
4529
|
+
}
|
|
4498
4530
|
return data;
|
|
4499
4531
|
}
|
|
4500
4532
|
|
|
4501
|
-
function resetUsageLimitsCache() {
|
|
4533
|
+
function resetUsageLimitsCache({ invalidateInFlight = false } = {}) {
|
|
4534
|
+
if (invalidateInFlight) usageLimitsCacheGeneration += 1;
|
|
4502
4535
|
for (const key of USAGE_LIMITS_SELECTION_KEYS) {
|
|
4503
4536
|
cacheBySelection[key] = { data: null, expiresAtMs: 0 };
|
|
4537
|
+
// Ordinary resets still let a running fetch serve refresh=1 callers; a
|
|
4538
|
+
// credential replacement must not, so drop the old-key promises too.
|
|
4539
|
+
if (invalidateInFlight) inFlightBySelection[key] = null;
|
|
4504
4540
|
}
|
|
4505
4541
|
}
|
|
4506
4542
|
|
|
@@ -4508,6 +4544,8 @@ module.exports = {
|
|
|
4508
4544
|
getUsageLimits,
|
|
4509
4545
|
normalizePlanLabel,
|
|
4510
4546
|
resetUsageLimitsCache,
|
|
4547
|
+
OPENCODE_GO_LIMITS_CACHE_FILE,
|
|
4548
|
+
resolveOpencodeGoLimitsCachePath,
|
|
4511
4549
|
cacheExpiresAtMs,
|
|
4512
4550
|
runCommand,
|
|
4513
4551
|
extractGeminiOauthClientCredentials,
|