tokentracker-cli 0.95.2 → 0.96.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dashboard/dist/assets/{AchievementBadge-DtB1MwFe.js → AchievementBadge-Bb2ljquW.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage--vddu0dC.js → AchievementsPage-ByUP2x8a.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-CDl9hsbj.js → AchievementsSection-_L6sCLTw.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-C7WsIRy9.js → BadgeDetailModal-B9n_-45B.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-CmXyzIhX.js → BotAnimated-lWmg-aZg.js} +1 -1
- package/dashboard/dist/assets/{Card-SUbC3VNS.js → Card-aSp259T0.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-BdEbs8Me.js → ClawdAnimated-Cy-XaTXW.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-D1RLLeHB.js → CommandPalette-CNwtKCUm.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-5ESGJgZQ.js → CommunityStatsModal-DJSezaPV.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-DsCd51b2.js → ConfirmModal-CVjmhZd6.js} +1 -1
- package/dashboard/dist/assets/{Controls-BVrLC_QD.js → Controls-BIj6yVxE.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-DtUOG6og.js → DashboardPage-BjYZLmqQ.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-zYjj-m5V.js → DevicePage-ptsI2fZc.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-D66yBqkd.js → DialogClose-D1pLoZ56.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-KY7X94CG.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-C4ia00lc.js → DialogTitle-DfSBUjWf.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-DF2c8XaR.js → FadeIn-DBn4L_BV.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-Cnr3aqcX.js → HeaderGithubStar-EJLBwEqp.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-DBOpxlH7.js → HoverTooltip-DaSrZhYd.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-CUY6KBaV.js → IpCheckPage-diQTVaYt.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-DKL8hZEq.js → LandingPage-BqCxjwar.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-DRYeMNlq.js → LeaderboardAvatar-CQImCsPN.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-DpD9Az3k.js → LeaderboardPage-D2ZDUi_O.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-Dfg0V13F.js → LeaderboardProfileModal-DHvADX1d.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-DH-jcVDu.js → LeaderboardProfilePage-CJsedB28.js} +1 -1
- package/dashboard/dist/assets/LimitsPage-CAwff24b.js +6 -0
- package/dashboard/dist/assets/{LocalOnlyNotice-Bj_m0hw7.js → LocalOnlyNotice-Cmnvu9tI.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-BVKvtx60.js → LoginCard-BQmKkD7X.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-mUGiSIJN.js → LoginModal-2oPROePZ.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-BORCuCUt.js → LoginPage-aqdUJoQK.js} +1 -1
- package/dashboard/dist/assets/{PetPage-D1JfEOVA.js → PetPage-Bn3p_o5r.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-DK3et-HG.js → PopoverPopup-DXD_YdiB.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-By8FAjxR.js → ProviderIcon-BZZ45-gY.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-DPa81jqa.js → ResetPasswordPage-Bn5ITiHQ.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-DLzS-2eZ.js → SegmentedControl-hGXboQZy.js} +1 -1
- package/dashboard/dist/assets/{Select-C05W6XEM.js → Select-BP8aVRZ1.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-CcfB3blP.js → SelectItemText-Odx47kND.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-ByqLbHv1.js → ServiceStatusPage-DUueam0N.js} +1 -1
- package/dashboard/dist/assets/SessionsPage-BA9IFqQ1.js +2 -0
- package/dashboard/dist/assets/{SettingsPage-DAcndJZQ.js → SettingsPage-DLOHHB4f.js} +2 -2
- package/dashboard/dist/assets/{SkillsPage-BuuqQuNU.js → SkillsPage-Ds2vH_PG.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-DzccxkXW.js → TokenGalaxy-CWBR4jnB.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-XUDMp5uy.js → ToolbarRootContext-Bmg56vYz.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-B-yYabbz.js → TrendMonitor-BQzcllvg.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-CwUDjEFo.js → WidgetsPage-Bu3tHDjO.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-Dr9ow3gH.js → WrappedPage-BrAMJhgy.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-DPOlUkcY.js → __vite-browser-external-4IGm4Djn.js} +12 -12
- package/dashboard/dist/assets/{agent-logos-Cqc_suYk.js → agent-logos-B8lT-HcI.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-DSsR3Ntr.js → arrow-up-right-rpBCOTg2.js} +1 -1
- package/dashboard/dist/assets/{check-Cy35lAKh.js → check-DIKw7TYH.js} +1 -1
- package/dashboard/dist/assets/{composite-BDwOlhhX.js → composite-_NeXO0lv.js} +1 -1
- package/dashboard/dist/assets/{copy-4szu5cYh.js → copy-CsfAtL7P.js} +1 -1
- package/dashboard/dist/assets/{download-DdGW6Krz.js → download-DM1W9pIW.js} +1 -1
- package/dashboard/dist/assets/{external-link-DwSt83cq.js → external-link-CwbuMbeQ.js} +1 -1
- package/dashboard/dist/assets/{flame-DubukWqK.js → flame-DcYbLdhE.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-B70EdQcN.js → format-tokens-CEvr3dNy.js} +1 -1
- package/dashboard/dist/assets/{icons-B86TLdAj.js → icons-DaGMVsJ5.js} +1 -1
- package/dashboard/dist/assets/{index-B_KMoBeK.js → index-BVVLPj3N.js} +1 -1
- package/dashboard/dist/assets/{index-DI-W-pun.js → index-ChZkJjpk.js} +1 -1
- package/dashboard/dist/assets/{index-DzEMcUwI.js → index-T7OjmEuX.js} +1 -1
- package/dashboard/dist/assets/{info-BzFpLbl6.js → info-CAjN3dX4.js} +1 -1
- package/dashboard/dist/assets/limits-providers-jn7Yln_V.js +1 -0
- package/dashboard/dist/assets/{link-2-BQp7-3T7.js → link-2-DaWH2Tyc.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-lrDmbIg5.js → loader-circle-CnF8qPi1.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-Cnd_Oth8.js → maximize-2-B-2Pnj6g.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-CPTpZdHU.js → modifiers.esm-BoDOvUKl.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-C7UY1zRx.js → monitor-smartphone-DDgwxG_E.js} +1 -1
- package/dashboard/dist/assets/{plus-Bglp9N_7.js → plus-DIc1YUWB.js} +1 -1
- package/dashboard/dist/assets/{provider-display-CGsFySs6.js → provider-display-yeNsVCO_.js} +1 -1
- package/dashboard/dist/assets/{react-DKXriBSF.js → react-DZqS6YTX.js} +1 -1
- package/dashboard/dist/assets/{search-D_LLDRdU.js → search-D6tEppYJ.js} +1 -1
- package/dashboard/dist/assets/{skills-api-CJWjKIco.js → skills-api-vsA1rc1Z.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-zlY09tye.js → sortable.esm-DDCnS1HI.js} +1 -1
- package/dashboard/dist/assets/{store-8e16Ac2h.js → store-Crne4DqF.js} +1 -1
- package/dashboard/dist/assets/{terminal-CR228ekj.js → terminal-Ca7dkLMF.js} +1 -1
- package/dashboard/dist/assets/{trash-2-BjLWewOp.js → trash-2-CjEAS5ic.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DhOJPaVg.js → use-community-stats-BHx2YxyK.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-lcGhb-NC.js → use-latest-request-guard-BLGbXQdf.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-DJIImtLN.js → use-limits-display-prefs-DBkAp6ob.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-BJZfZuoJ.js → use-ordered-list-BTfW5M9u.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-D_kUnDs4.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-C-l4b1cD.js → use-session-efficiency-pref-DBmd-MqH.js} +1 -1
- package/dashboard/dist/assets/{use-transform-UjIt477N.js → use-transform-Bp5XrwaQ.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-DqoTZrqT.js → use-usage-limits-BmqRcyhH.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-CnM8aB4v.js → useCurrency-B0YsRDj-.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-D4pAK2ld.js → useTokenFormat-PP3D0Ket.js} +1 -1
- package/dashboard/dist/assets/{zap-Du1IyEtj.js → zap-BVOm1aBH.js} +1 -1
- package/dashboard/dist/index.html +1 -1
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/share.html +1 -1
- package/package.json +1 -1
- package/src/lib/ark-agent-plan-limits.js +437 -0
- package/src/lib/ark-coding-plan-limits.js +104 -27
- package/src/lib/codex-model-attribution.js +128 -0
- package/src/lib/codex-rollout-parser.js +98 -4
- 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 +50 -9
- package/src/lib/session-analytics.js +374 -110
- package/src/lib/usage-limits.js +19 -5
- 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/limits-providers-DxZOjiXQ.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-DdpxZQoh.js +0 -1
|
@@ -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,
|
|
@@ -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",
|
package/src/lib/pricing/index.js
CHANGED
|
@@ -25,6 +25,11 @@ const PI_SUBSCRIPTION_SOURCES = new Set([
|
|
|
25
25
|
// server (including LM Link). Secure Cloud usage has a separate billing path
|
|
26
26
|
// and is not present in these logs.
|
|
27
27
|
const LOCAL_INFERENCE_SOURCES = new Set(["lmstudio"]);
|
|
28
|
+
// Forward-looking: unreachable for Codex today. Every observed token_count
|
|
29
|
+
// reports model_context_window = 258400, below this threshold, so no request
|
|
30
|
+
// can exceed it (max raw input seen locally: 238853). The long-context
|
|
31
|
+
// repricing stays in place for when OpenAI ships a larger Codex window.
|
|
32
|
+
const OPENAI_LONG_CONTEXT_INPUT_THRESHOLD = 272_000;
|
|
28
33
|
const SOURCES_WITH_AUTHORITATIVE_COST = new Set(["grok"]);
|
|
29
34
|
const SEED_SNAPSHOT_PATH = path.resolve(__dirname, "seed-snapshot.json");
|
|
30
35
|
const DEEPSEEK_TIME_PRICED_MODELS = [
|
|
@@ -192,7 +197,7 @@ function computeRowCost(row) {
|
|
|
192
197
|
const reasoningCost = reasoningIncludedInOutput
|
|
193
198
|
? 0
|
|
194
199
|
: (row.reasoning_output_tokens || 0) * (pricing.output || 0);
|
|
195
|
-
|
|
200
|
+
const baseCost = (
|
|
196
201
|
((row.input_tokens || 0) * (pricing.input || 0) +
|
|
197
202
|
(row.output_tokens || 0) * (pricing.output || 0) +
|
|
198
203
|
(row.cached_input_tokens || 0) * (pricing.cache_read || 0) +
|
|
@@ -200,6 +205,37 @@ function computeRowCost(row) {
|
|
|
200
205
|
reasoningCost) /
|
|
201
206
|
1_000_000
|
|
202
207
|
);
|
|
208
|
+
|
|
209
|
+
const model = String(row?.model || "").toLowerCase();
|
|
210
|
+
const usesGpt56SolLongContextTier =
|
|
211
|
+
model === "gpt-5.6" || model.includes("gpt-5.6-sol");
|
|
212
|
+
if (!usesGpt56SolLongContextTier) return baseCost;
|
|
213
|
+
|
|
214
|
+
const bounded = (value, total) => Math.min(
|
|
215
|
+
Math.max(0, Number(value) || 0),
|
|
216
|
+
Math.max(0, Number(total) || 0),
|
|
217
|
+
);
|
|
218
|
+
// The parser records only usage from requests whose cache-inclusive input
|
|
219
|
+
// exceeded 272K. OpenAI prices the whole such request at 2x input and 1.5x
|
|
220
|
+
// output, so add the premium over the standard-rate base exactly once.
|
|
221
|
+
const longInput = bounded(row.long_context_input_tokens, row.input_tokens);
|
|
222
|
+
const longCached = bounded(row.long_context_cached_input_tokens, row.cached_input_tokens);
|
|
223
|
+
const longCacheWrite = bounded(
|
|
224
|
+
row.long_context_cache_creation_input_tokens,
|
|
225
|
+
row.cache_creation_input_tokens,
|
|
226
|
+
);
|
|
227
|
+
const longOutput = bounded(row.long_context_output_tokens, row.output_tokens);
|
|
228
|
+
const longReasoning = reasoningIncludedInOutput
|
|
229
|
+
? 0
|
|
230
|
+
: bounded(row.long_context_reasoning_output_tokens, row.reasoning_output_tokens);
|
|
231
|
+
const longContextPremium = (
|
|
232
|
+
longInput * (pricing.input || 0) +
|
|
233
|
+
longCached * (pricing.cache_read || 0) +
|
|
234
|
+
longCacheWrite * (pricing.cache_write || 0) +
|
|
235
|
+
0.5 * longOutput * (pricing.output || 0) +
|
|
236
|
+
0.5 * longReasoning * (pricing.output || 0)
|
|
237
|
+
) / 1_000_000;
|
|
238
|
+
return baseCost + longContextPremium;
|
|
203
239
|
}
|
|
204
240
|
|
|
205
241
|
// Backwards-compatible MODEL_PRICING export. Test at
|
|
@@ -219,6 +255,7 @@ module.exports = {
|
|
|
219
255
|
resetPricingForTests,
|
|
220
256
|
MODEL_PRICING,
|
|
221
257
|
ZERO_PRICING,
|
|
258
|
+
OPENAI_LONG_CONTEXT_INPUT_THRESHOLD,
|
|
222
259
|
// Internal hooks for tests.
|
|
223
260
|
__getStateForTests: () => state,
|
|
224
261
|
};
|