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.
Files changed (107) hide show
  1. package/dashboard/dist/assets/{AchievementBadge-DtB1MwFe.js → AchievementBadge-Bb2ljquW.js} +1 -1
  2. package/dashboard/dist/assets/{AchievementsPage--vddu0dC.js → AchievementsPage-ByUP2x8a.js} +2 -2
  3. package/dashboard/dist/assets/{AchievementsSection-CDl9hsbj.js → AchievementsSection-_L6sCLTw.js} +1 -1
  4. package/dashboard/dist/assets/{BadgeDetailModal-C7WsIRy9.js → BadgeDetailModal-B9n_-45B.js} +1 -1
  5. package/dashboard/dist/assets/{BotAnimated-CmXyzIhX.js → BotAnimated-lWmg-aZg.js} +1 -1
  6. package/dashboard/dist/assets/{Card-SUbC3VNS.js → Card-aSp259T0.js} +1 -1
  7. package/dashboard/dist/assets/{ClawdAnimated-BdEbs8Me.js → ClawdAnimated-Cy-XaTXW.js} +2 -2
  8. package/dashboard/dist/assets/{CommandPalette-D1RLLeHB.js → CommandPalette-CNwtKCUm.js} +1 -1
  9. package/dashboard/dist/assets/{CommunityStatsModal-5ESGJgZQ.js → CommunityStatsModal-DJSezaPV.js} +1 -1
  10. package/dashboard/dist/assets/{ConfirmModal-DsCd51b2.js → ConfirmModal-CVjmhZd6.js} +1 -1
  11. package/dashboard/dist/assets/{Controls-BVrLC_QD.js → Controls-BIj6yVxE.js} +1 -1
  12. package/dashboard/dist/assets/{DashboardPage-DtUOG6og.js → DashboardPage-BjYZLmqQ.js} +1 -1
  13. package/dashboard/dist/assets/{DevicePage-zYjj-m5V.js → DevicePage-ptsI2fZc.js} +1 -1
  14. package/dashboard/dist/assets/{DialogClose-D66yBqkd.js → DialogClose-D1pLoZ56.js} +1 -1
  15. package/dashboard/dist/assets/DialogDescription-KY7X94CG.js +1 -0
  16. package/dashboard/dist/assets/{DialogTitle-C4ia00lc.js → DialogTitle-DfSBUjWf.js} +1 -1
  17. package/dashboard/dist/assets/{FadeIn-DF2c8XaR.js → FadeIn-DBn4L_BV.js} +1 -1
  18. package/dashboard/dist/assets/{HeaderGithubStar-Cnr3aqcX.js → HeaderGithubStar-EJLBwEqp.js} +1 -1
  19. package/dashboard/dist/assets/{HoverTooltip-DBOpxlH7.js → HoverTooltip-DaSrZhYd.js} +1 -1
  20. package/dashboard/dist/assets/{IpCheckPage-CUY6KBaV.js → IpCheckPage-diQTVaYt.js} +1 -1
  21. package/dashboard/dist/assets/{LandingPage-DKL8hZEq.js → LandingPage-BqCxjwar.js} +2 -2
  22. package/dashboard/dist/assets/{LeaderboardAvatar-DRYeMNlq.js → LeaderboardAvatar-CQImCsPN.js} +1 -1
  23. package/dashboard/dist/assets/{LeaderboardPage-DpD9Az3k.js → LeaderboardPage-D2ZDUi_O.js} +2 -2
  24. package/dashboard/dist/assets/{LeaderboardProfileModal-Dfg0V13F.js → LeaderboardProfileModal-DHvADX1d.js} +3 -3
  25. package/dashboard/dist/assets/{LeaderboardProfilePage-DH-jcVDu.js → LeaderboardProfilePage-CJsedB28.js} +1 -1
  26. package/dashboard/dist/assets/LimitsPage-CAwff24b.js +6 -0
  27. package/dashboard/dist/assets/{LocalOnlyNotice-Bj_m0hw7.js → LocalOnlyNotice-Cmnvu9tI.js} +1 -1
  28. package/dashboard/dist/assets/{LoginCard-BVKvtx60.js → LoginCard-BQmKkD7X.js} +1 -1
  29. package/dashboard/dist/assets/{LoginModal-mUGiSIJN.js → LoginModal-2oPROePZ.js} +1 -1
  30. package/dashboard/dist/assets/{LoginPage-BORCuCUt.js → LoginPage-aqdUJoQK.js} +1 -1
  31. package/dashboard/dist/assets/{PetPage-D1JfEOVA.js → PetPage-Bn3p_o5r.js} +1 -1
  32. package/dashboard/dist/assets/{PopoverPopup-DK3et-HG.js → PopoverPopup-DXD_YdiB.js} +1 -1
  33. package/dashboard/dist/assets/{ProviderIcon-By8FAjxR.js → ProviderIcon-BZZ45-gY.js} +1 -1
  34. package/dashboard/dist/assets/{ResetPasswordPage-DPa81jqa.js → ResetPasswordPage-Bn5ITiHQ.js} +1 -1
  35. package/dashboard/dist/assets/{SegmentedControl-DLzS-2eZ.js → SegmentedControl-hGXboQZy.js} +1 -1
  36. package/dashboard/dist/assets/{Select-C05W6XEM.js → Select-BP8aVRZ1.js} +1 -1
  37. package/dashboard/dist/assets/{SelectItemText-CcfB3blP.js → SelectItemText-Odx47kND.js} +1 -1
  38. package/dashboard/dist/assets/{ServiceStatusPage-ByqLbHv1.js → ServiceStatusPage-DUueam0N.js} +1 -1
  39. package/dashboard/dist/assets/SessionsPage-BA9IFqQ1.js +2 -0
  40. package/dashboard/dist/assets/{SettingsPage-DAcndJZQ.js → SettingsPage-DLOHHB4f.js} +2 -2
  41. package/dashboard/dist/assets/{SkillsPage-BuuqQuNU.js → SkillsPage-Ds2vH_PG.js} +1 -1
  42. package/dashboard/dist/assets/{TokenGalaxy-DzccxkXW.js → TokenGalaxy-CWBR4jnB.js} +1 -1
  43. package/dashboard/dist/assets/{ToolbarRootContext-XUDMp5uy.js → ToolbarRootContext-Bmg56vYz.js} +1 -1
  44. package/dashboard/dist/assets/{TrendMonitor-B-yYabbz.js → TrendMonitor-BQzcllvg.js} +1 -1
  45. package/dashboard/dist/assets/{WidgetsPage-CwUDjEFo.js → WidgetsPage-Bu3tHDjO.js} +1 -1
  46. package/dashboard/dist/assets/{WrappedPage-Dr9ow3gH.js → WrappedPage-BrAMJhgy.js} +1 -1
  47. package/dashboard/dist/assets/{__vite-browser-external-DPOlUkcY.js → __vite-browser-external-4IGm4Djn.js} +12 -12
  48. package/dashboard/dist/assets/{agent-logos-Cqc_suYk.js → agent-logos-B8lT-HcI.js} +1 -1
  49. package/dashboard/dist/assets/{arrow-up-right-DSsR3Ntr.js → arrow-up-right-rpBCOTg2.js} +1 -1
  50. package/dashboard/dist/assets/{check-Cy35lAKh.js → check-DIKw7TYH.js} +1 -1
  51. package/dashboard/dist/assets/{composite-BDwOlhhX.js → composite-_NeXO0lv.js} +1 -1
  52. package/dashboard/dist/assets/{copy-4szu5cYh.js → copy-CsfAtL7P.js} +1 -1
  53. package/dashboard/dist/assets/{download-DdGW6Krz.js → download-DM1W9pIW.js} +1 -1
  54. package/dashboard/dist/assets/{external-link-DwSt83cq.js → external-link-CwbuMbeQ.js} +1 -1
  55. package/dashboard/dist/assets/{flame-DubukWqK.js → flame-DcYbLdhE.js} +1 -1
  56. package/dashboard/dist/assets/{format-tokens-B70EdQcN.js → format-tokens-CEvr3dNy.js} +1 -1
  57. package/dashboard/dist/assets/{icons-B86TLdAj.js → icons-DaGMVsJ5.js} +1 -1
  58. package/dashboard/dist/assets/{index-B_KMoBeK.js → index-BVVLPj3N.js} +1 -1
  59. package/dashboard/dist/assets/{index-DI-W-pun.js → index-ChZkJjpk.js} +1 -1
  60. package/dashboard/dist/assets/{index-DzEMcUwI.js → index-T7OjmEuX.js} +1 -1
  61. package/dashboard/dist/assets/{info-BzFpLbl6.js → info-CAjN3dX4.js} +1 -1
  62. package/dashboard/dist/assets/limits-providers-jn7Yln_V.js +1 -0
  63. package/dashboard/dist/assets/{link-2-BQp7-3T7.js → link-2-DaWH2Tyc.js} +1 -1
  64. package/dashboard/dist/assets/{loader-circle-lrDmbIg5.js → loader-circle-CnF8qPi1.js} +1 -1
  65. package/dashboard/dist/assets/{maximize-2-Cnd_Oth8.js → maximize-2-B-2Pnj6g.js} +1 -1
  66. package/dashboard/dist/assets/{modifiers.esm-CPTpZdHU.js → modifiers.esm-BoDOvUKl.js} +1 -1
  67. package/dashboard/dist/assets/{monitor-smartphone-C7UY1zRx.js → monitor-smartphone-DDgwxG_E.js} +1 -1
  68. package/dashboard/dist/assets/{plus-Bglp9N_7.js → plus-DIc1YUWB.js} +1 -1
  69. package/dashboard/dist/assets/{provider-display-CGsFySs6.js → provider-display-yeNsVCO_.js} +1 -1
  70. package/dashboard/dist/assets/{react-DKXriBSF.js → react-DZqS6YTX.js} +1 -1
  71. package/dashboard/dist/assets/{search-D_LLDRdU.js → search-D6tEppYJ.js} +1 -1
  72. package/dashboard/dist/assets/{skills-api-CJWjKIco.js → skills-api-vsA1rc1Z.js} +1 -1
  73. package/dashboard/dist/assets/{sortable.esm-zlY09tye.js → sortable.esm-DDCnS1HI.js} +1 -1
  74. package/dashboard/dist/assets/{store-8e16Ac2h.js → store-Crne4DqF.js} +1 -1
  75. package/dashboard/dist/assets/{terminal-CR228ekj.js → terminal-Ca7dkLMF.js} +1 -1
  76. package/dashboard/dist/assets/{trash-2-BjLWewOp.js → trash-2-CjEAS5ic.js} +1 -1
  77. package/dashboard/dist/assets/{use-community-stats-DhOJPaVg.js → use-community-stats-BHx2YxyK.js} +1 -1
  78. package/dashboard/dist/assets/{use-latest-request-guard-lcGhb-NC.js → use-latest-request-guard-BLGbXQdf.js} +1 -1
  79. package/dashboard/dist/assets/{use-limits-display-prefs-DJIImtLN.js → use-limits-display-prefs-DBkAp6ob.js} +1 -1
  80. package/dashboard/dist/assets/{use-ordered-list-BJZfZuoJ.js → use-ordered-list-BTfW5M9u.js} +1 -1
  81. package/dashboard/dist/assets/use-reduced-motion-D_kUnDs4.js +1 -0
  82. package/dashboard/dist/assets/{use-session-efficiency-pref-C-l4b1cD.js → use-session-efficiency-pref-DBmd-MqH.js} +1 -1
  83. package/dashboard/dist/assets/{use-transform-UjIt477N.js → use-transform-Bp5XrwaQ.js} +1 -1
  84. package/dashboard/dist/assets/{use-usage-limits-DqoTZrqT.js → use-usage-limits-BmqRcyhH.js} +1 -1
  85. package/dashboard/dist/assets/{useCurrency-CnM8aB4v.js → useCurrency-B0YsRDj-.js} +1 -1
  86. package/dashboard/dist/assets/{useTokenFormat-D4pAK2ld.js → useTokenFormat-PP3D0Ket.js} +1 -1
  87. package/dashboard/dist/assets/{zap-Du1IyEtj.js → zap-BVOm1aBH.js} +1 -1
  88. package/dashboard/dist/index.html +1 -1
  89. package/dashboard/dist/ip-check.html +1 -1
  90. package/dashboard/dist/leaderboard.html +1 -1
  91. package/dashboard/dist/share.html +1 -1
  92. package/package.json +1 -1
  93. package/src/lib/ark-agent-plan-limits.js +437 -0
  94. package/src/lib/ark-coding-plan-limits.js +104 -27
  95. package/src/lib/codex-model-attribution.js +128 -0
  96. package/src/lib/codex-rollout-parser.js +98 -4
  97. package/src/lib/pricing/curated-overrides.json +6 -6
  98. package/src/lib/pricing/index.js +38 -1
  99. package/src/lib/pricing/seed-snapshot.json +1 -1
  100. package/src/lib/rollout.js +50 -9
  101. package/src/lib/session-analytics.js +374 -110
  102. package/src/lib/usage-limits.js +19 -5
  103. package/dashboard/dist/assets/DialogDescription-BVlaoajd.js +0 -1
  104. package/dashboard/dist/assets/LimitsPage-CVCKPScf.js +0 -5
  105. package/dashboard/dist/assets/SessionsPage-CEDv1thj.js +0 -2
  106. package/dashboard/dist/assets/limits-providers-DxZOjiXQ.js +0 -1
  107. 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 (obj.type === "response_item" && obj.payload?.type === "function_call") {
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": 5,
52
- "output": 30,
53
- "cache_read": 0.5,
54
- "cache_write": 6.25,
55
- "note": "GPT-5.6 family (public 2026-07-09). Flagship tier. Not yet in LiteLLM. developers.openai.com/api/docs/pricing short-context: $5/$30 per MTok, cached input 0.1x, cache write 1.25x. Codex emits gpt-5.6-sol (+ reasoning-effort variants like gpt-5.6-solhigh, caught by fuzzy). Remove once LiteLLM carries it."
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-terra"
431
+ "ref": "gpt-5.6-sol"
432
432
  },
433
433
  {
434
434
  "match": "kiro",
@@ -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
- return (
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
  };