tokentracker-cli 0.95.2 → 0.96.1
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 +2 -0
- package/README.zh-CN.md +1 -1
- package/dashboard/dist/assets/{AchievementBadge-DtB1MwFe.js → AchievementBadge-DKyElm0T.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage--vddu0dC.js → AchievementsPage-BvCx3wND.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-CDl9hsbj.js → AchievementsSection-DN69LzXX.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-C7WsIRy9.js → BadgeDetailModal-DGiKdZfz.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-CmXyzIhX.js → BotAnimated-zN8rH7_K.js} +1 -1
- package/dashboard/dist/assets/{Card-SUbC3VNS.js → Card-COWo-ClV.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-BdEbs8Me.js → ClawdAnimated-BWsB7Eat.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-D1RLLeHB.js → CommandPalette-BfvIwuoJ.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-5ESGJgZQ.js → CommunityStatsModal-BQWg7R_y.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-DsCd51b2.js → ConfirmModal-C7ABSI-O.js} +1 -1
- package/dashboard/dist/assets/{Controls-BVrLC_QD.js → Controls-vMzEZzej.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-QBhbCMZF.js +1 -0
- package/dashboard/dist/assets/{DevicePage-zYjj-m5V.js → DevicePage-3ejUBLWM.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-D66yBqkd.js → DialogClose-D_dvtfGV.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-ryGYdQjX.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-C4ia00lc.js → DialogTitle-_ahf0z8b.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-DF2c8XaR.js → FadeIn-D7Jcfz1N.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-Cnr3aqcX.js → HeaderGithubStar-D--Rx5BS.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-DBOpxlH7.js → HoverTooltip-n1W0z3Wr.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-CUY6KBaV.js → IpCheckPage-D202tl9B.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-DKL8hZEq.js → LandingPage-D6yL2EZo.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-DRYeMNlq.js → LeaderboardAvatar-ChFg2mtA.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-DpD9Az3k.js → LeaderboardPage-BXHwGs8r.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-Dfg0V13F.js → LeaderboardProfileModal-B5LoOpCk.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-DH-jcVDu.js → LeaderboardProfilePage-jsukhRz8.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-PvqkOOwp.js +6 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-Bj_m0hw7.js → LocalOnlyNotice-Du0lz657.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-BVKvtx60.js → LoginCard-BFEX9x7-.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-mUGiSIJN.js → LoginModal-DqC7_d35.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-BORCuCUt.js → LoginPage-Dzx8N7HE.js} +1 -1
- package/dashboard/dist/assets/{PetPage-D1JfEOVA.js → PetPage-CNlWHF4z.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-DK3et-HG.js → PopoverPopup-D9jadD3j.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-By8FAjxR.js → ProviderIcon-Caz5PuAm.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-DPa81jqa.js → ResetPasswordPage-x_GR79sR.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-DLzS-2eZ.js → SegmentedControl-BxDiDV4R.js} +1 -1
- package/dashboard/dist/assets/{Select-C05W6XEM.js → Select-B5o-1sEl.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-CcfB3blP.js → SelectItemText-CiA6L7Wk.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-ByqLbHv1.js → ServiceStatusPage-BVQuVdjg.js} +1 -1
- package/dashboard/dist/assets/SessionsPage-DE39wNIO.js +2 -0
- package/dashboard/dist/assets/SettingsPage-DJrb_fsK.js +2 -0
- package/dashboard/dist/assets/{SkillsPage-BuuqQuNU.js → SkillsPage-COk-YQQ5.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-DzccxkXW.js → TokenGalaxy-BPM779Wb.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-XUDMp5uy.js → ToolbarRootContext-CozJT6Vs.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-B-yYabbz.js → TrendMonitor-BkNy-o1D.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-CwUDjEFo.js → WidgetsPage-Bpg6H-ZB.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-Dr9ow3gH.js → WrappedPage-C6-NM-RO.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-BGnq8Ebm.js +45 -0
- package/dashboard/dist/assets/__vite-browser-external-CvMMIjhi.css +1 -0
- package/dashboard/dist/assets/{agent-logos-Cqc_suYk.js → agent-logos-DpbkRqXT.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-DSsR3Ntr.js → arrow-up-right-BjOtWUVs.js} +1 -1
- package/dashboard/dist/assets/{check-Cy35lAKh.js → check-BlxnX5jZ.js} +1 -1
- package/dashboard/dist/assets/{composite-BDwOlhhX.js → composite-Dxmivlk4.js} +1 -1
- package/dashboard/dist/assets/{copy-4szu5cYh.js → copy-CIsWjLY5.js} +1 -1
- package/dashboard/dist/assets/{download-DdGW6Krz.js → download-QDaQbQdd.js} +1 -1
- package/dashboard/dist/assets/{external-link-DwSt83cq.js → external-link-DPV4VU3m.js} +1 -1
- package/dashboard/dist/assets/{flame-DubukWqK.js → flame-B_X-7VNp.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-B70EdQcN.js → format-tokens-DEx2TJj8.js} +1 -1
- package/dashboard/dist/assets/{icons-B86TLdAj.js → icons-983RyGSl.js} +1 -1
- package/dashboard/dist/assets/{index-B_KMoBeK.js → index-BONYA8HZ.js} +1 -1
- package/dashboard/dist/assets/{index-DI-W-pun.js → index-Cjs5stC0.js} +1 -1
- package/dashboard/dist/assets/{index-DzEMcUwI.js → index-qtonw_x4.js} +1 -1
- package/dashboard/dist/assets/{info-BzFpLbl6.js → info-DpVOhXZI.js} +1 -1
- package/dashboard/dist/assets/limits-providers-BUoquSA-.js +1 -0
- package/dashboard/dist/assets/{link-2-BQp7-3T7.js → link-2-CBro-upz.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-lrDmbIg5.js → loader-circle-wYFdSyOd.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-Cnd_Oth8.js → maximize-2-DhTyQ4ly.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-CPTpZdHU.js → modifiers.esm-CPljwVhN.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-C7UY1zRx.js → monitor-smartphone-D_wr-HOR.js} +1 -1
- package/dashboard/dist/assets/{plus-Bglp9N_7.js → plus-BNjfkBEF.js} +1 -1
- package/dashboard/dist/assets/{provider-display-CGsFySs6.js → provider-display-BOBtUyFi.js} +1 -1
- package/dashboard/dist/assets/{react-DKXriBSF.js → react-yHv9MTUA.js} +1 -1
- package/dashboard/dist/assets/{search-D_LLDRdU.js → search-Dfak92ZA.js} +1 -1
- package/dashboard/dist/assets/{skills-api-CJWjKIco.js → skills-api-By2PdNa1.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-zlY09tye.js → sortable.esm-B9xy66_v.js} +1 -1
- package/dashboard/dist/assets/{store-8e16Ac2h.js → store-oQZR4d9M.js} +1 -1
- package/dashboard/dist/assets/{terminal-CR228ekj.js → terminal-pbXWBmZM.js} +1 -1
- package/dashboard/dist/assets/{trash-2-BjLWewOp.js → trash-2-CboDPwbW.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DhOJPaVg.js → use-community-stats-D6HTdnHq.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-lcGhb-NC.js → use-latest-request-guard-hwSr9Cam.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-DJIImtLN.js → use-limits-display-prefs-B-pbwUS8.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-BJZfZuoJ.js → use-ordered-list-OtfOqrNb.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-DGXcZBX3.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-C-l4b1cD.js → use-session-efficiency-pref-BScvtS8F.js} +1 -1
- package/dashboard/dist/assets/{use-transform-UjIt477N.js → use-transform-Cy1ykjEs.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-DqoTZrqT.js → use-usage-limits-CSMb2zYW.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-CnM8aB4v.js → useCurrency-VqF3cy1C.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-D4pAK2ld.js → useTokenFormat-Cgub4MXu.js} +1 -1
- package/dashboard/dist/assets/{zap-Du1IyEtj.js → zap-DStzvQJI.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/status.js +4 -4
- package/src/commands/sync.js +5 -3
- package/src/lib/ark-agent-plan-limits.js +438 -0
- package/src/lib/ark-coding-plan-limits.js +103 -28
- package/src/lib/codex-model-attribution.js +128 -0
- package/src/lib/codex-rollout-parser.js +98 -4
- package/src/lib/command-runner.js +13 -0
- package/src/lib/local-api.js +3 -0
- package/src/lib/pricing/curated-overrides.json +6 -6
- package/src/lib/pricing/index.js +38 -1
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +67 -14
- package/src/lib/session-analytics.js +374 -110
- package/src/lib/usage-limits.js +130 -18
- package/dashboard/dist/assets/DashboardPage-DtUOG6og.js +0 -1
- package/dashboard/dist/assets/DialogDescription-BVlaoajd.js +0 -1
- package/dashboard/dist/assets/LimitsPage-CVCKPScf.js +0 -5
- package/dashboard/dist/assets/SessionsPage-CEDv1thj.js +0 -2
- package/dashboard/dist/assets/SettingsPage-DAcndJZQ.js +0 -2
- package/dashboard/dist/assets/__vite-browser-external-CCMdjCX2.css +0 -1
- package/dashboard/dist/assets/__vite-browser-external-DPOlUkcY.js +0 -45
- package/dashboard/dist/assets/limits-providers-DxZOjiXQ.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-DdpxZQoh.js +0 -1
|
@@ -2,13 +2,17 @@
|
|
|
2
2
|
|
|
3
3
|
// Ark Coding Plan (火山方舟 Coding Plan) quota monitoring.
|
|
4
4
|
//
|
|
5
|
-
// Coding Plan
|
|
6
|
-
//
|
|
7
|
-
//
|
|
8
|
-
//
|
|
9
|
-
//
|
|
10
|
-
//
|
|
11
|
-
//
|
|
5
|
+
// Coding Plan (Lite/Pro) and Agent Plan (Small/Medium/Large/Max) are
|
|
6
|
+
// two parallel subscription products that each refresh on three windows —
|
|
7
|
+
// 5-hour (session for Coding Plan, 5h for Agent Plan), weekly and monthly —
|
|
8
|
+
// and are shared by compatible coding tools (Claude Code, Codex CLI,
|
|
9
|
+
// OpenCode, TRAE, ...). Official docs present them as parallel, not
|
|
10
|
+
// successor/replacement. TokenTracker already counts those tools' token
|
|
11
|
+
// consumption from their local files, so this module deliberately adds
|
|
12
|
+
// NO consumption source. It only surfaces the subscription quota
|
|
13
|
+
// percentage, which is otherwise only visible in the Volcano console web
|
|
14
|
+
// page. Agent Plan is handled by a separate provider module
|
|
15
|
+
// `ark-agent-plan-limits.js` (dual Provider, issue #555).
|
|
12
16
|
//
|
|
13
17
|
// The quota is read through the user's own `arkcli` binary
|
|
14
18
|
// (`arkcli usage plan --format json`), which is already installed and logged
|
|
@@ -21,7 +25,7 @@ const fs = require("node:fs");
|
|
|
21
25
|
const os = require("node:os");
|
|
22
26
|
const path = require("node:path");
|
|
23
27
|
|
|
24
|
-
const { runCommand, resolveBinaryPath, statBinaryInDirs, commonGlobalBinDirectories } = require("./command-runner");
|
|
28
|
+
const { runCommand, resolveBinaryPath, statBinaryInDirs, commonGlobalBinDirectories, resolvedCliEnvironment } = require("./command-runner");
|
|
25
29
|
|
|
26
30
|
const ARK_LIMITS_CACHE_FILE = "ark-coding-plan-limits-cache.json";
|
|
27
31
|
const ARK_LIMITS_CACHE_UNKNOWN_RESET_TTL_MS = 12 * 60 * 60 * 1000;
|
|
@@ -43,14 +47,23 @@ const ARK_PROVIDER_TIMEOUT_MS = 15_000;
|
|
|
43
47
|
// Same shape as codexResetCreditListTimeoutMs's guard in usage-limits.js.
|
|
44
48
|
const ARK_PROVIDER_BUDGET_GUARD_MS = 1_500;
|
|
45
49
|
|
|
46
|
-
// arkcli period label -> canonical window slot.
|
|
47
|
-
//
|
|
50
|
+
// arkcli period label -> canonical window slot.
|
|
51
|
+
// Coding Plan uses `session` for the 5-hour rolling window; Agent Plan
|
|
52
|
+
// uses `5h`. `weekly` and `monthly` are shared. `daily` (Agent Plan
|
|
53
|
+
// visual quota) is intentionally not surfaced — it is a separate
|
|
54
|
+
// per-day visual bucket that does not fit the three-window panel.
|
|
48
55
|
const ARK_PERIOD_WINDOW = {
|
|
49
56
|
session: "primary_window",
|
|
57
|
+
"5h": "primary_window",
|
|
50
58
|
weekly: "secondary_window",
|
|
51
59
|
monthly: "tertiary_window",
|
|
52
60
|
};
|
|
53
61
|
|
|
62
|
+
// Coding Plan personal product. Team products (coding-plan-team) are
|
|
63
|
+
// seat-scoped and not surfaced in this personal-quota panel.
|
|
64
|
+
const ARK_PERSONAL_PRODUCTS = ["coding-plan"];
|
|
65
|
+
const ARK_PRODUCT_PRIORITY = ["coding-plan"];
|
|
66
|
+
|
|
54
67
|
function clampPercent(value) {
|
|
55
68
|
return Math.max(0, Math.min(100, value));
|
|
56
69
|
}
|
|
@@ -73,22 +86,33 @@ function normalizeResetAt(value) {
|
|
|
73
86
|
// "Ark Coding Plan Coding Plan Lite".
|
|
74
87
|
function planLabelForTier(tier) {
|
|
75
88
|
const normalized = String(tier || "").trim().toLowerCase();
|
|
76
|
-
|
|
77
|
-
|
|
89
|
+
const mapping = {
|
|
90
|
+
pro: "Pro",
|
|
91
|
+
lite: "Lite",
|
|
92
|
+
small: "Small",
|
|
93
|
+
medium: "Medium",
|
|
94
|
+
large: "Large",
|
|
95
|
+
max: "Max",
|
|
96
|
+
};
|
|
97
|
+
if (mapping[normalized]) return mapping[normalized];
|
|
78
98
|
return tier && String(tier).trim() ? String(tier).trim() : null;
|
|
79
99
|
}
|
|
80
100
|
|
|
81
101
|
/**
|
|
82
102
|
* Normalize the JSON payload returned by `arkcli plans get --format json`.
|
|
83
|
-
* The tier ("lite" / "pro") lives on the plans payload,
|
|
84
|
-
* payload, so it is resolved here and merged into the
|
|
103
|
+
* The tier ("lite" / "pro" for Coding Plan) lives on the plans payload,
|
|
104
|
+
* not on the usage payload, so it is resolved here and merged into the
|
|
105
|
+
* plan label.
|
|
85
106
|
* Returns null when there is no Coding Plan entry.
|
|
86
107
|
*/
|
|
87
108
|
function normalizeArkPlansResponse(body) {
|
|
88
109
|
if (!body || typeof body !== "object") return null;
|
|
89
110
|
const plans = Array.isArray(body.plans) ? body.plans : [];
|
|
90
|
-
|
|
91
|
-
|
|
111
|
+
for (const key of ARK_PRODUCT_PRIORITY) {
|
|
112
|
+
const plan = plans.find((entry) => entry?.key === key);
|
|
113
|
+
if (plan?.tier) return String(plan.tier);
|
|
114
|
+
}
|
|
115
|
+
return null;
|
|
92
116
|
}
|
|
93
117
|
|
|
94
118
|
function arkProfileIdentity(body) {
|
|
@@ -114,26 +138,56 @@ function arkProfileIdentity(body) {
|
|
|
114
138
|
return identity || null;
|
|
115
139
|
}
|
|
116
140
|
|
|
141
|
+
function resolveArkProductItem(items) {
|
|
142
|
+
for (const product of ARK_PRODUCT_PRIORITY) {
|
|
143
|
+
const item = items.find((entry) => entry?.product === product && entry?.subscribed === true);
|
|
144
|
+
if (item) return item;
|
|
145
|
+
}
|
|
146
|
+
return null;
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
function percentFromPeriod(period) {
|
|
150
|
+
// null / undefined / blank percent must fall through to the used/total
|
|
151
|
+
// fallback: Number(null) and Number("") both coerce to 0, which would
|
|
152
|
+
// otherwise report a fake fresh 0% window instead of deriving real usage
|
|
153
|
+
// (Agent Plan's 5h period can omit percent entirely, #555).
|
|
154
|
+
const rawPercent = period?.percent;
|
|
155
|
+
if (rawPercent !== null && rawPercent !== undefined && String(rawPercent).trim() !== "") {
|
|
156
|
+
const direct = Number(rawPercent);
|
|
157
|
+
if (Number.isFinite(direct)) return direct;
|
|
158
|
+
}
|
|
159
|
+
const used = Number(period?.used);
|
|
160
|
+
const total = Number(period?.total);
|
|
161
|
+
if (Number.isFinite(used) && Number.isFinite(total) && total > 0) {
|
|
162
|
+
return (used / total) * 100;
|
|
163
|
+
}
|
|
164
|
+
return NaN;
|
|
165
|
+
}
|
|
166
|
+
|
|
117
167
|
/**
|
|
118
168
|
* Normalize the JSON payload returned by `arkcli usage plan --format json`.
|
|
119
169
|
* Returns `null` when the account has no active Coding Plan subscription
|
|
120
170
|
* (caller reports `configured: false`). Throws when the payload shape is
|
|
121
171
|
* unusable so the caller can fall back to the disk cache.
|
|
172
|
+
* Only `coding-plan` (Lite/Pro, session label) is handled here;
|
|
173
|
+
* Agent Plan is handled by `ark-agent-plan-limits.js`.
|
|
122
174
|
*/
|
|
123
175
|
function normalizeArkCodingPlanResponse(body) {
|
|
124
176
|
if (!body || typeof body !== "object") {
|
|
125
177
|
throw new Error("Ark Coding Plan response is not an object.");
|
|
126
178
|
}
|
|
127
179
|
const items = Array.isArray(body.items) ? body.items : [];
|
|
128
|
-
const item = items
|
|
129
|
-
if (!item
|
|
180
|
+
const item = resolveArkProductItem(items);
|
|
181
|
+
if (!item) return null;
|
|
130
182
|
|
|
131
183
|
const windows = {};
|
|
132
184
|
const periods = Array.isArray(item.periods) ? item.periods : [];
|
|
133
185
|
for (const period of periods) {
|
|
186
|
+
// Agent Plan `daily` is a visual-only quota not shown in the 3-window panel.
|
|
187
|
+
if (period?.label === "daily") continue;
|
|
134
188
|
const slot = ARK_PERIOD_WINDOW[period?.label];
|
|
135
189
|
if (!slot) continue;
|
|
136
|
-
const percent =
|
|
190
|
+
const percent = percentFromPeriod(period);
|
|
137
191
|
if (!Number.isFinite(percent)) continue;
|
|
138
192
|
windows[slot] = {
|
|
139
193
|
used_percent: clampPercent(percent),
|
|
@@ -149,6 +203,7 @@ function normalizeArkCodingPlanResponse(body) {
|
|
|
149
203
|
configured: true,
|
|
150
204
|
error: null,
|
|
151
205
|
plan_label: planLabelForTier(item.tier),
|
|
206
|
+
product: item.product || null,
|
|
152
207
|
primary_window: windows.primary_window || null,
|
|
153
208
|
secondary_window: windows.secondary_window || null,
|
|
154
209
|
tertiary_window: windows.tertiary_window || null,
|
|
@@ -194,6 +249,7 @@ function readArkCodingPlanLimitsCache({ home = os.homedir(), nowMs = Date.now(),
|
|
|
194
249
|
configured: true,
|
|
195
250
|
error: null,
|
|
196
251
|
plan_label: typeof parsed?.plan_label === "string" ? parsed.plan_label : null,
|
|
252
|
+
product: typeof parsed?.product === "string" ? parsed.product : null,
|
|
197
253
|
primary_window: bounded[0],
|
|
198
254
|
secondary_window: bounded[1],
|
|
199
255
|
tertiary_window: bounded[2],
|
|
@@ -212,6 +268,7 @@ function writeArkCodingPlanLimitsCache(limits, { home = os.homedir(), nowMs = Da
|
|
|
212
268
|
const cachePath = arkCodingPlanCachePath({ home });
|
|
213
269
|
const payload = {
|
|
214
270
|
plan_label: limits.plan_label || null,
|
|
271
|
+
product: limits.product || null,
|
|
215
272
|
profile_identity: limits.profile_identity || null,
|
|
216
273
|
primary_window: limits.primary_window || null,
|
|
217
274
|
secondary_window: limits.secondary_window || null,
|
|
@@ -325,6 +382,7 @@ async function fetchArkCodingPlanLimits({
|
|
|
325
382
|
if (!arkcliPath) return { configured: false };
|
|
326
383
|
|
|
327
384
|
const commandOptions = {
|
|
385
|
+
env: resolvedCliEnvironment(arkcliPath, { platform }),
|
|
328
386
|
signal,
|
|
329
387
|
killProcessGroup: true,
|
|
330
388
|
platform,
|
|
@@ -384,12 +442,19 @@ async function fetchArkCodingPlanLimits({
|
|
|
384
442
|
};
|
|
385
443
|
|
|
386
444
|
if (result?.error || result?.status !== 0) {
|
|
387
|
-
const
|
|
445
|
+
const isExit127 = result?.status === 127;
|
|
446
|
+
const baseDetail = result?.error?.message
|
|
388
447
|
|| (result?.status !== 0 && result?.status !== null
|
|
389
448
|
? `arkcli exited with code ${result.status}`
|
|
390
449
|
: "arkcli usage plan failed");
|
|
450
|
+
// Exit 127 can mean a missing interpreter as well as an outdated CLI.
|
|
451
|
+
// Preserve stderr so users can distinguish runtime and command failures.
|
|
452
|
+
const detail = isExit127
|
|
453
|
+
? "arkcli exited with code 127 — check its runtime/PATH or update arkcli: npm i -g @volcengine/ark-cli"
|
|
454
|
+
: baseDetail;
|
|
391
455
|
const stderr = trimStderr(result?.stderr);
|
|
392
|
-
|
|
456
|
+
const message = stderr ? `${detail}: ${stderr}` : detail;
|
|
457
|
+
return failWithCache(message);
|
|
393
458
|
}
|
|
394
459
|
|
|
395
460
|
let body;
|
|
@@ -406,17 +471,22 @@ async function fetchArkCodingPlanLimits({
|
|
|
406
471
|
return failWithCache(error?.message || "Ark Coding Plan response could not be parsed.");
|
|
407
472
|
}
|
|
408
473
|
if (!limits) {
|
|
409
|
-
// Only a response that explicitly carries
|
|
474
|
+
// Only a response that explicitly carries an Ark product entry with
|
|
410
475
|
// `subscribed: false` confirms the plan was retired — drop the cache
|
|
411
476
|
// then, or a later transient CLI failure would resurrect the retired
|
|
412
|
-
// plan's numbers through failWithCache. A payload with *no*
|
|
413
|
-
//
|
|
414
|
-
//
|
|
415
|
-
//
|
|
477
|
+
// plan's numbers through failWithCache. A payload with *no* Ark entry
|
|
478
|
+
// at all (`{}`, `{"items":[]}` — not logged in, backend degraded,
|
|
479
|
+
// product key renamed) is ambiguous and must NOT destroy the cache:
|
|
480
|
+
// transient signals never drive persistent state.
|
|
481
|
+
// Only coding-plan is considered here; agent-plan is handled by the
|
|
482
|
+
// parallel provider `ark-agent-plan-limits.js`.
|
|
416
483
|
const entry = Array.isArray(body?.items)
|
|
417
|
-
? body.items.find(
|
|
484
|
+
? body.items.find(
|
|
485
|
+
(candidate) =>
|
|
486
|
+
ARK_PERSONAL_PRODUCTS.includes(candidate?.product) && candidate?.subscribed === false,
|
|
487
|
+
)
|
|
418
488
|
: null;
|
|
419
|
-
if (entry
|
|
489
|
+
if (entry) {
|
|
420
490
|
try {
|
|
421
491
|
fs.unlinkSync(arkCodingPlanCachePath({ home }));
|
|
422
492
|
} catch (_error) {}
|
|
@@ -452,6 +522,8 @@ async function fetchArkCodingPlanLimits({
|
|
|
452
522
|
|
|
453
523
|
module.exports = {
|
|
454
524
|
ARK_PERIOD_WINDOW,
|
|
525
|
+
ARK_PERSONAL_PRODUCTS,
|
|
526
|
+
ARK_PRODUCT_PRIORITY,
|
|
455
527
|
normalizeArkPlansResponse,
|
|
456
528
|
arkProfileIdentity,
|
|
457
529
|
normalizeArkCodingPlanResponse,
|
|
@@ -459,4 +531,7 @@ module.exports = {
|
|
|
459
531
|
writeArkCodingPlanLimitsCache,
|
|
460
532
|
hasArkCliInstallEvidence,
|
|
461
533
|
fetchArkCodingPlanLimits,
|
|
534
|
+
// exposed for testing
|
|
535
|
+
resolveArkProductItem,
|
|
536
|
+
percentFromPeriod,
|
|
462
537
|
};
|
|
@@ -0,0 +1,128 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Codex token_count events do not carry a model id. Every usage event is
|
|
4
|
+
// therefore attributed to the model named by the most recent turn_context,
|
|
5
|
+
// which Codex emits at the start of each turn and which tracks mid-session
|
|
6
|
+
// model switches exactly. That per-turn path is what actually runs today.
|
|
7
|
+
//
|
|
8
|
+
// The model/rerouted upgrade below is forward-looking: it promotes usage to
|
|
9
|
+
// the effective model when the app server reports a server-side reroute.
|
|
10
|
+
//
|
|
11
|
+
// Deliberately NOT used for attribution: event_msg/thread_settings_applied.
|
|
12
|
+
// It carries thread_settings.model, but it fires when the user picks a model
|
|
13
|
+
// in the UI - seconds to minutes before the switch takes effect - so usage
|
|
14
|
+
// still streaming from the in-flight turn belongs to the previous model.
|
|
15
|
+
// turn_context is the only signal that marks where a turn actually begins.
|
|
16
|
+
//
|
|
17
|
+
// session_meta is a defensive fallback, not an observed source. Codex has
|
|
18
|
+
// never written a model into it - 0 of 10310 session_meta rows across 5849
|
|
19
|
+
// local rollouts carry the field (codex-cli 0.151.0); the payload records
|
|
20
|
+
// model_provider ("openai"), which is provenance rather than a model. The
|
|
21
|
+
// fallback only fills selectedModel when nothing else has, so a future format
|
|
22
|
+
// that does carry one attributes instead of falling through to "unknown". It
|
|
23
|
+
// never overrides a turn_context model.
|
|
24
|
+
|
|
25
|
+
function cleanString(value) {
|
|
26
|
+
return typeof value === "string" && value.trim() ? value.trim() : null;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function normalizeEventName(value) {
|
|
30
|
+
return cleanString(value)?.toLowerCase().replaceAll("_", "/") || "";
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function eventEnvelope(obj) {
|
|
34
|
+
const candidates = [
|
|
35
|
+
{ name: obj?.method, data: obj?.params },
|
|
36
|
+
{ name: obj?.type, data: obj?.payload },
|
|
37
|
+
{ name: obj?.payload?.method, data: obj?.payload?.params },
|
|
38
|
+
{ name: obj?.payload?.type, data: obj?.payload },
|
|
39
|
+
{ name: obj?.payload?.msg?.method, data: obj?.payload?.msg?.params },
|
|
40
|
+
{ name: obj?.payload?.msg?.type, data: obj?.payload?.msg },
|
|
41
|
+
{ name: obj?.msg?.method, data: obj?.msg?.params },
|
|
42
|
+
{ name: obj?.msg?.type, data: obj?.msg },
|
|
43
|
+
];
|
|
44
|
+
return candidates.find(({ name }) => normalizeEventName(name) === "model/rerouted") || null;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
// Forward-looking: no model/rerouted event has been observed in any real
|
|
48
|
+
// rollout (verified against 5832 local files, codex-cli 0.151.0), so the
|
|
49
|
+
// envelope shapes and the fromModel/toModel field names below are inferred
|
|
50
|
+
// from the app-server protocol rather than confirmed against logged data.
|
|
51
|
+
// Re-verify against a real sample before relying on rerouted attribution.
|
|
52
|
+
function extractModelReroute(obj) {
|
|
53
|
+
const envelope = eventEnvelope(obj);
|
|
54
|
+
if (!envelope) return null;
|
|
55
|
+
const data = envelope.data && typeof envelope.data === "object" ? envelope.data : {};
|
|
56
|
+
const fromModel = cleanString(data.fromModel ?? data.from_model);
|
|
57
|
+
const toModel = cleanString(data.toModel ?? data.to_model);
|
|
58
|
+
if (!toModel) return null;
|
|
59
|
+
return {
|
|
60
|
+
fromModel,
|
|
61
|
+
toModel,
|
|
62
|
+
reason: cleanString(data.reason),
|
|
63
|
+
threadId: cleanString(data.threadId ?? data.thread_id),
|
|
64
|
+
turnId: cleanString(data.turnId ?? data.turn_id),
|
|
65
|
+
};
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function createCodexModelAttributionState(value = {}) {
|
|
69
|
+
const selectedModel = cleanString(value.selectedModel ?? value.selected_model ?? value.model);
|
|
70
|
+
const effectiveModel = cleanString(value.effectiveModel ?? value.effective_model) || selectedModel;
|
|
71
|
+
return {
|
|
72
|
+
selectedModel,
|
|
73
|
+
effectiveModel,
|
|
74
|
+
turnId: cleanString(value.turnId ?? value.turn_id),
|
|
75
|
+
rerouted: Boolean(value.rerouted),
|
|
76
|
+
rerouteReason: cleanString(value.rerouteReason ?? value.reroute_reason),
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
function applyCodexModelEvent(state, obj) {
|
|
81
|
+
if (!state || typeof state !== "object") return null;
|
|
82
|
+
if (obj?.type === "turn_context" && obj.payload && typeof obj.payload === "object") {
|
|
83
|
+
const selectedModel = cleanString(obj.payload.model);
|
|
84
|
+
if (selectedModel) {
|
|
85
|
+
state.selectedModel = selectedModel;
|
|
86
|
+
}
|
|
87
|
+
state.effectiveModel = state.selectedModel || state.effectiveModel;
|
|
88
|
+
state.turnId = cleanString(obj.payload.turn_id ?? obj.payload.turnId) || state.turnId;
|
|
89
|
+
state.rerouted = false;
|
|
90
|
+
state.rerouteReason = null;
|
|
91
|
+
return { type: "turn_context", selectedModel };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// Strictly weaker than turn_context, and never allowed to overwrite it: a
|
|
95
|
+
// forked rollout replays the parent's session_meta rows after its own.
|
|
96
|
+
if (obj?.type === "session_meta" && obj.payload && typeof obj.payload === "object") {
|
|
97
|
+
const metaModel = cleanString(obj.payload.model);
|
|
98
|
+
if (!metaModel || state.selectedModel) return null;
|
|
99
|
+
state.selectedModel = metaModel;
|
|
100
|
+
state.effectiveModel = state.effectiveModel || metaModel;
|
|
101
|
+
return { type: "session_meta", selectedModel: metaModel };
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const reroute = extractModelReroute(obj);
|
|
105
|
+
if (!reroute) return null;
|
|
106
|
+
state.selectedModel = reroute.fromModel || state.selectedModel || state.effectiveModel;
|
|
107
|
+
state.effectiveModel = reroute.toModel;
|
|
108
|
+
state.turnId = reroute.turnId || state.turnId;
|
|
109
|
+
state.rerouted = true;
|
|
110
|
+
state.rerouteReason = reroute.reason;
|
|
111
|
+
return { type: "model_rerouted", ...reroute };
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
function currentCodexModel(state) {
|
|
115
|
+
return cleanString(state?.effectiveModel) || cleanString(state?.selectedModel);
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
function snapshotCodexModelAttributionState(state) {
|
|
119
|
+
return createCodexModelAttributionState(state);
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
module.exports = {
|
|
123
|
+
applyCodexModelEvent,
|
|
124
|
+
createCodexModelAttributionState,
|
|
125
|
+
currentCodexModel,
|
|
126
|
+
extractModelReroute,
|
|
127
|
+
snapshotCodexModelAttributionState,
|
|
128
|
+
};
|
|
@@ -24,12 +24,21 @@ const {
|
|
|
24
24
|
getExecutableName,
|
|
25
25
|
buildExecStatsEntry,
|
|
26
26
|
} = require("./categorizer-utils");
|
|
27
|
+
const {
|
|
28
|
+
applyCodexModelEvent,
|
|
29
|
+
createCodexModelAttributionState,
|
|
30
|
+
currentCodexModel,
|
|
31
|
+
snapshotCodexModelAttributionState,
|
|
32
|
+
} = require("./codex-model-attribution");
|
|
27
33
|
|
|
28
34
|
const DISCOVERY_FULL_AUDIT_INTERVAL_MS = 60 * 1000;
|
|
29
35
|
const MAX_DISCOVERY_INVENTORIES = 32;
|
|
30
36
|
const DISCOVERY_INVENTORIES = new Map();
|
|
31
37
|
const ZONED_DAY_FORMATTERS = new Map();
|
|
32
38
|
const CODEX_RESUME_INVALIDATED = "TOKENTRACKER_CODEX_RESUME_INVALIDATED";
|
|
39
|
+
// See src/lib/pricing/index.js: no observed Codex request reaches this
|
|
40
|
+
// threshold yet (context window 258400), so long_context_* stays at zero.
|
|
41
|
+
const OPENAI_LONG_CONTEXT_INPUT_THRESHOLD = 272_000;
|
|
33
42
|
|
|
34
43
|
// ---------------------------------------------------------------------------
|
|
35
44
|
// Timezone helpers
|
|
@@ -520,6 +529,17 @@ function finalizeExecRows(map) {
|
|
|
520
529
|
.sort((a, b) => (b.totals?.total_tokens || 0) - (a.totals?.total_tokens || 0));
|
|
521
530
|
}
|
|
522
531
|
|
|
532
|
+
function finalizeModelUsageRows(map) {
|
|
533
|
+
return Array.from(map.values())
|
|
534
|
+
.map((row) => ({
|
|
535
|
+
...row,
|
|
536
|
+
selected_models: Array.from(row.selected_models).sort(),
|
|
537
|
+
reroute_reasons: Array.from(row.reroute_reasons).sort(),
|
|
538
|
+
model_attribution: row.rerouted_usage_events > 0 ? "effective" : "selected",
|
|
539
|
+
}))
|
|
540
|
+
.sort((a, b) => b.total_tokens - a.total_tokens || a.model.localeCompare(b.model));
|
|
541
|
+
}
|
|
542
|
+
|
|
523
543
|
// ---------------------------------------------------------------------------
|
|
524
544
|
// Main parser
|
|
525
545
|
// ---------------------------------------------------------------------------
|
|
@@ -537,6 +557,9 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
537
557
|
captureContentHash = false,
|
|
538
558
|
contentHashState = null,
|
|
539
559
|
sourceHandle = null,
|
|
560
|
+
collectBreakdowns = true,
|
|
561
|
+
collectModelUsage = false,
|
|
562
|
+
onObject = null,
|
|
540
563
|
} = {}) {
|
|
541
564
|
const filePaths = (Array.isArray(filePath) ? filePath : [filePath]).filter(Boolean);
|
|
542
565
|
const primaryFilePath = filePaths[0] || String(filePath || "");
|
|
@@ -566,6 +589,9 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
566
589
|
let sessionId = isResuming ? resumeState.sessionId || null : null;
|
|
567
590
|
let cwd = isResuming ? resumeState.cwd || null : null;
|
|
568
591
|
let model = isResuming ? resumeState.model || null : null;
|
|
592
|
+
const modelAttributionState = createCodexModelAttributionState(
|
|
593
|
+
isResuming ? resumeState.modelAttributionState || { model } : {},
|
|
594
|
+
);
|
|
569
595
|
let provider = isResuming ? resumeState.provider || null : null;
|
|
570
596
|
let cliVersion = isResuming ? resumeState.cliVersion || null : null;
|
|
571
597
|
let forkedFromId = isResuming ? resumeState.forkedFromId || null : null;
|
|
@@ -607,9 +633,55 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
607
633
|
const byExecCommand = new Map(); // sanitized executable + subcommand -> stats
|
|
608
634
|
const byExecDuration = new Map(); // duration bucket -> stats
|
|
609
635
|
const byExecOutput = new Map(); // output size bucket -> stats
|
|
636
|
+
const byModel = new Map(); // effective model -> observed token usage
|
|
610
637
|
|
|
611
638
|
let turnCount = 0;
|
|
612
639
|
|
|
640
|
+
function recordModelUsage(delta, rawRequestUsage) {
|
|
641
|
+
if (!collectModelUsage || !delta || delta.total_tokens <= 0) return;
|
|
642
|
+
const effectiveModel = currentCodexModel(modelAttributionState) || "unknown";
|
|
643
|
+
let row = byModel.get(effectiveModel);
|
|
644
|
+
if (!row) {
|
|
645
|
+
row = {
|
|
646
|
+
model: effectiveModel,
|
|
647
|
+
...emptyTotals(),
|
|
648
|
+
long_context_input_tokens: 0,
|
|
649
|
+
long_context_cached_input_tokens: 0,
|
|
650
|
+
long_context_cache_creation_input_tokens: 0,
|
|
651
|
+
long_context_output_tokens: 0,
|
|
652
|
+
long_context_reasoning_output_tokens: 0,
|
|
653
|
+
usage_events: 0,
|
|
654
|
+
rerouted_usage_events: 0,
|
|
655
|
+
long_context_usage_events: 0,
|
|
656
|
+
selected_models: new Set(),
|
|
657
|
+
reroute_reasons: new Set(),
|
|
658
|
+
};
|
|
659
|
+
byModel.set(effectiveModel, row);
|
|
660
|
+
}
|
|
661
|
+
addInto(row, delta);
|
|
662
|
+
row.usage_events += 1;
|
|
663
|
+
if (modelAttributionState.selectedModel) {
|
|
664
|
+
row.selected_models.add(modelAttributionState.selectedModel);
|
|
665
|
+
}
|
|
666
|
+
if (modelAttributionState.rerouted) {
|
|
667
|
+
row.rerouted_usage_events += 1;
|
|
668
|
+
if (modelAttributionState.rerouteReason) {
|
|
669
|
+
row.reroute_reasons.add(modelAttributionState.rerouteReason);
|
|
670
|
+
}
|
|
671
|
+
}
|
|
672
|
+
// OpenAI applies the long-context tier to the whole request when its raw
|
|
673
|
+
// (cache-inclusive) input exceeds 272K tokens. Preserve the exact subset
|
|
674
|
+
// so pricing can be recomputed later without rescanning private logs.
|
|
675
|
+
if (Number(rawRequestUsage?.input_tokens || 0) > OPENAI_LONG_CONTEXT_INPUT_THRESHOLD) {
|
|
676
|
+
row.long_context_usage_events += 1;
|
|
677
|
+
row.long_context_input_tokens += delta.input_tokens;
|
|
678
|
+
row.long_context_cached_input_tokens += delta.cached_input_tokens;
|
|
679
|
+
row.long_context_cache_creation_input_tokens += delta.cache_creation_input_tokens;
|
|
680
|
+
row.long_context_output_tokens += delta.output_tokens;
|
|
681
|
+
row.long_context_reasoning_output_tokens += delta.reasoning_output_tokens;
|
|
682
|
+
}
|
|
683
|
+
}
|
|
684
|
+
|
|
613
685
|
function ensureTool(name) {
|
|
614
686
|
if (!byTool.has(name)) {
|
|
615
687
|
byTool.set(name, { name, ...buildToolStatsEntry() });
|
|
@@ -686,6 +758,14 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
686
758
|
}
|
|
687
759
|
turnCount += 1;
|
|
688
760
|
|
|
761
|
+
if (!collectBreakdowns) {
|
|
762
|
+
addInto(totals, delta);
|
|
763
|
+
pendingToolNames = [];
|
|
764
|
+
pendingSkills = [];
|
|
765
|
+
pendingExecDetails = [];
|
|
766
|
+
return;
|
|
767
|
+
}
|
|
768
|
+
|
|
689
769
|
const unique = [...new Set(pendingToolNames.filter(Boolean))];
|
|
690
770
|
const tools = unique.length > 0 ? unique : ["text_response"];
|
|
691
771
|
const share = 1 / tools.length;
|
|
@@ -822,6 +902,16 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
822
902
|
contentHasher,
|
|
823
903
|
sourceHandle,
|
|
824
904
|
})) {
|
|
905
|
+
const modelEvent = applyCodexModelEvent(modelAttributionState, obj);
|
|
906
|
+
const attributedModel = currentCodexModel(modelAttributionState);
|
|
907
|
+
model = attributedModel || model;
|
|
908
|
+
// onObject runs AFTER the attribution state machine, and is handed the
|
|
909
|
+
// model it just decided. A consumer sharing this pass (the delivery-signal
|
|
910
|
+
// collector, which attributes edit turns) therefore sees exactly the model
|
|
911
|
+
// recordModelUsage() bills the turn's tokens to. Letting it re-read
|
|
912
|
+
// turn_context.payload.model itself would make codex-model-attribution.js
|
|
913
|
+
// one of two authorities on the same question.
|
|
914
|
+
if (typeof onObject === "function") onObject(obj, attributedModel);
|
|
825
915
|
const ts = typeof obj?.timestamp === "string" ? obj.timestamp : null;
|
|
826
916
|
if (!ts) continue;
|
|
827
917
|
const inRequestedRange = isTimestampInRequestedDayRange(ts, { from, to, timeZoneContext });
|
|
@@ -849,18 +939,19 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
849
939
|
if (obj.type === "turn_context") {
|
|
850
940
|
const p = obj.payload || {};
|
|
851
941
|
if (typeof p.cwd === "string") cwd = p.cwd;
|
|
852
|
-
if (typeof p.model === "string") model = p.model;
|
|
853
942
|
continue;
|
|
854
943
|
}
|
|
855
944
|
|
|
856
|
-
if (
|
|
945
|
+
if (modelEvent?.type === "model_rerouted") continue;
|
|
946
|
+
|
|
947
|
+
if (collectBreakdowns && obj.type === "response_item" && obj.payload?.type === "function_call") {
|
|
857
948
|
pendingToolNames.push(normalizeToolName(obj.payload));
|
|
858
949
|
const skill = extractSkillNameFromFunctionCall(obj.payload, diagnostics);
|
|
859
950
|
if (skill) pendingSkills.push(skill);
|
|
860
951
|
continue;
|
|
861
952
|
}
|
|
862
953
|
|
|
863
|
-
if (obj.type === "event_msg" && obj.payload?.type === "exec_command_end") {
|
|
954
|
+
if (collectBreakdowns && obj.type === "event_msg" && obj.payload?.type === "exec_command_end") {
|
|
864
955
|
const details = getExecKeys(obj.payload);
|
|
865
956
|
if (details) pendingExecDetails.push(details);
|
|
866
957
|
continue;
|
|
@@ -881,6 +972,7 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
881
972
|
attributeTurn(null);
|
|
882
973
|
} else {
|
|
883
974
|
if (seenTokenEvents && delta?.total_tokens > 0) seenTokenEvents.add(eventKey);
|
|
975
|
+
recordModelUsage(delta, lastUsage || rawDelta);
|
|
884
976
|
attributeTurn(delta);
|
|
885
977
|
}
|
|
886
978
|
} else {
|
|
@@ -895,7 +987,7 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
895
987
|
const result = {
|
|
896
988
|
sessionId: sessionId || rolloutSessionIdFromPath(primaryFilePath) || primaryFilePath,
|
|
897
989
|
cwd,
|
|
898
|
-
model: model || provider,
|
|
990
|
+
model: currentCodexModel(modelAttributionState) || model || provider,
|
|
899
991
|
provider,
|
|
900
992
|
version: cliVersion,
|
|
901
993
|
forkedFromId,
|
|
@@ -906,6 +998,7 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
906
998
|
filePath: primaryFilePath,
|
|
907
999
|
turnCount,
|
|
908
1000
|
totals,
|
|
1001
|
+
modelUsage: finalizeModelUsageRows(byModel),
|
|
909
1002
|
toolBreakdown: {
|
|
910
1003
|
tool_rows: finalizeToolRows(byTool),
|
|
911
1004
|
},
|
|
@@ -926,6 +1019,7 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
926
1019
|
sessionId,
|
|
927
1020
|
cwd,
|
|
928
1021
|
model,
|
|
1022
|
+
modelAttributionState: snapshotCodexModelAttributionState(modelAttributionState),
|
|
929
1023
|
provider,
|
|
930
1024
|
cliVersion,
|
|
931
1025
|
forkedFromId,
|
|
@@ -226,6 +226,8 @@ function commonGlobalBinDirectories({ home = os.homedir(), platform = process.pl
|
|
|
226
226
|
return [
|
|
227
227
|
"/opt/homebrew/bin",
|
|
228
228
|
"/usr/local/bin",
|
|
229
|
+
path.join(home, ".local", "bin"),
|
|
230
|
+
path.join(home, ".bun", "bin"),
|
|
229
231
|
path.join(home, ".npm-global", "bin"),
|
|
230
232
|
// volta keeps shims for every global install here
|
|
231
233
|
path.join(home, ".volta", "bin"),
|
|
@@ -276,6 +278,16 @@ async function resolveBinaryPath(binary, { commandRunner, home, platform = proce
|
|
|
276
278
|
return statBinaryInDirs(binary, searchDirs, platform);
|
|
277
279
|
}
|
|
278
280
|
|
|
281
|
+
// Resolving an npm CLI's absolute path does not resolve its /usr/bin/env node
|
|
282
|
+
// shebang. Finder-launched apps have a minimal PATH even when the CLI lives
|
|
283
|
+
// beside a working nvm/fnm/Homebrew Node. Scope this environment to the child;
|
|
284
|
+
// preserve the install's own runtime and use our running Node as a fallback.
|
|
285
|
+
function resolvedCliEnvironment(binaryPath, { env = process.env, platform = process.platform } = {}) {
|
|
286
|
+
if (platform === "win32") return { ...env };
|
|
287
|
+
const dirs = [path.dirname(binaryPath), ...(env.PATH || "/usr/bin:/bin").split(path.delimiter), path.dirname(process.execPath)];
|
|
288
|
+
return { ...env, PATH: [...new Set(dirs.filter(Boolean))].join(path.delimiter) };
|
|
289
|
+
}
|
|
290
|
+
|
|
279
291
|
module.exports = {
|
|
280
292
|
runCommand,
|
|
281
293
|
whichBinary,
|
|
@@ -283,4 +295,5 @@ module.exports = {
|
|
|
283
295
|
commonGlobalBinDirectories,
|
|
284
296
|
statBinaryInDirs,
|
|
285
297
|
resolveBinaryPath,
|
|
298
|
+
resolvedCliEnvironment,
|
|
286
299
|
};
|
package/src/lib/local-api.js
CHANGED
|
@@ -2019,6 +2019,9 @@ function createLocalApiHandler({ queuePath }) {
|
|
|
2019
2019
|
const publishAccount =
|
|
2020
2020
|
background && body.publishAccount === true && getCloudSyncPref();
|
|
2021
2021
|
const allLocalSources = background && body.allLocalSources === true;
|
|
2022
|
+
if (background && body.nativeOnlyWsl === true) {
|
|
2023
|
+
extraEnv.TOKENTRACKER_WSL_MODE = "native-only";
|
|
2024
|
+
}
|
|
2022
2025
|
if (typeof body.deviceToken === "string" && body.deviceToken.trim()) {
|
|
2023
2026
|
extraEnv.TOKENTRACKER_DEVICE_TOKEN = body.deviceToken.trim();
|
|
2024
2027
|
}
|
|
@@ -48,11 +48,11 @@
|
|
|
48
48
|
"note": "Canonical undated alias emitted by Cursor. Pin to the same rates as claude-haiku-4-5-20251001 so local cost does not depend on which dated LiteLLM aliases are present in the current cache."
|
|
49
49
|
},
|
|
50
50
|
"gpt-5.6-sol": {
|
|
51
|
-
"input":
|
|
52
|
-
"output":
|
|
53
|
-
"cache_read": 0.
|
|
54
|
-
"cache_write":
|
|
55
|
-
"note": "GPT-5.6
|
|
51
|
+
"input": 4,
|
|
52
|
+
"output": 20,
|
|
53
|
+
"cache_read": 0.4,
|
|
54
|
+
"cache_write": 5,
|
|
55
|
+
"note": "Official GPT-5.6 Sol standard pricing: $4 input, $0.40 cached input, $5 cache write, and $20 output per MTok. Requests above 272K input are repriced from their preserved per-request long-context subset. Codex emits gpt-5.6-sol plus reasoning-effort variants caught by fuzzy matching."
|
|
56
56
|
},
|
|
57
57
|
"gpt-5.6-terra": {
|
|
58
58
|
"input": 2,
|
|
@@ -428,7 +428,7 @@
|
|
|
428
428
|
},
|
|
429
429
|
{
|
|
430
430
|
"match": "gpt-5.6",
|
|
431
|
-
"ref": "gpt-5.6-
|
|
431
|
+
"ref": "gpt-5.6-sol"
|
|
432
432
|
},
|
|
433
433
|
{
|
|
434
434
|
"match": "kiro",
|