tokentracker-cli 0.99.0 → 1.0.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.de.md +6 -5
- package/README.ja.md +6 -5
- package/README.ko.md +6 -5
- package/README.md +7 -5
- package/README.zh-CN.md +6 -5
- package/dashboard/dist/assets/{AchievementBadge-kRsjlB3p.js → AchievementBadge-pib0F8p8.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-C3xybY0k.js → AchievementsPage-BMnub6CL.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-y5mok2w0.js → AchievementsSection-DG_-yxAt.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-IFLXYuKp.js → BadgeDetailModal-Sk2dHIkJ.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-D6x0PDt2.js → BotAnimated-VwwvOniA.js} +1 -1
- package/dashboard/dist/assets/{Card-CiKwEyVu.js → Card-COfjRX96.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-BQFcAIOy.js → ClawdAnimated-C3sF5c29.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-BvpX8PM_.js → CommandPalette-DvJFPsR2.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-D5rNlwfO.js → CommunityStatsModal-DfYHIeKr.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-CR0Evu8-.js → ConfirmModal-DtoHm0RD.js} +1 -1
- package/dashboard/dist/assets/{Controls-lyLUkbJW.js → Controls-DHHn10dy.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-X8CLr_au.js → DashboardPage-DJ_nDPjA.js} +1 -1
- package/dashboard/dist/assets/DevicePage-DlXWUpau.js +1 -0
- package/dashboard/dist/assets/{DialogClose-Bplj_718.js → DialogClose-BLegTe7F.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-Dl_wqXjc.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-Ckzf1tdi.js → DialogTitle-BZb-SEg0.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-C2Jqe95f.js → FadeIn-Dsk0V4DK.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-DgE7Cd5u.js → HeaderGithubStar-B20nLMWQ.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-CEmo0N-P.js → HoverTooltip-tGFmzZiJ.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-BwkpsLaF.js → IpCheckPage-BK7XI_aT.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-CA2klwbE.js → LandingPage-BYX6NXbl.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-B_PUenGZ.js → LeaderboardAvatar-CIDqKSNn.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-D6HGW3jX.js → LeaderboardPage-BxIpRUwn.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-Bfo1dSsS.js → LeaderboardProfileModal-DJlNN9YG.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-2OqwrqOs.js → LeaderboardProfilePage-D4HSgrjG.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-BHrkLXHZ.js → LimitsPage-BKTMBbKS.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-C45MmWYt.js → LocalOnlyNotice-DpZiPX4A.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-BikBibax.js → LoginCard-Cd03cg54.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-D0Aq85Zg.js → LoginModal-yPZi5JsE.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-B-kQaDQ_.js → LoginPage-BTL9oeVV.js} +1 -1
- package/dashboard/dist/assets/{PetPage-ZgPREiwD.js → PetPage-BWMSKd_3.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-eQgg3h1i.js → PopoverPopup-Dba9PYte.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-nDBPxn0N.js → ProviderIcon-DXI7Ztov.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-CjsOtiGr.js → ResetPasswordPage-wKDU07tl.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-6NcttkEH.js → SegmentedControl-DZh5rJHn.js} +1 -1
- package/dashboard/dist/assets/{Select-CzEhPICe.js → Select-CiJIsDf1.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-BwvzfXwj.js → SelectItemText-B8YkLJtE.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-C35wpxrT.js → ServiceStatusPage-CF_bMZMz.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-a30adkhf.js → SessionsPage-B0PnK1TY.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-C_7ZPBUF.js → SettingsPage-onYZIk-h.js} +2 -2
- package/dashboard/dist/assets/{SkillsPage-DsxkFnrV.js → SkillsPage-CSyoblke.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-D_bXRotA.js → TokenGalaxy-kK6wMGuy.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-CL7ma7ba.js → ToolbarRootContext-CnffyHPW.js} +1 -1
- package/dashboard/dist/assets/{TrendMonitor-QZBF2IEY.js → TrendMonitor-CpxDm5q_.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-CWlxR0H-.js → WidgetsPage-vBedlR_d.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-D8XQ72nF.js → WrappedPage-D25YzXGY.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-CTGqjrCe.js → __vite-browser-external-OxXsgcds.js} +3 -3
- package/dashboard/dist/assets/{agent-logos-BscA25fS.js → agent-logos-frktji23.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-DTxCEGbR.js → arrow-up-right-BtBcf39e.js} +1 -1
- package/dashboard/dist/assets/{check-CCEoh2wj.js → check-PVwhLtMf.js} +1 -1
- package/dashboard/dist/assets/{composite-B2s880f5.js → composite-BxlyIOoF.js} +1 -1
- package/dashboard/dist/assets/{copy-CJVQglwG.js → copy-s5UqKoXC.js} +1 -1
- package/dashboard/dist/assets/{download-Cn-OpYm6.js → download-Ns8TNfey.js} +1 -1
- package/dashboard/dist/assets/{external-link-BnYlUn56.js → external-link-zpvfw1sA.js} +1 -1
- package/dashboard/dist/assets/{flame-D4yFLV1L.js → flame-DhPzeD5Z.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CBUNbBvp.js → format-tokens-C4eAHkPw.js} +1 -1
- package/dashboard/dist/assets/{icons-CcgNgAvb.js → icons-CNeKwVFm.js} +1 -1
- package/dashboard/dist/assets/{index-CTjeDkbu.js → index-9qLVgAvh.js} +1 -1
- package/dashboard/dist/assets/{index-CkELwUyg.js → index-B5UHZH4-.js} +1 -1
- package/dashboard/dist/assets/{index-ZWeEKbo-.js → index-D8ciCYQM.js} +1 -1
- package/dashboard/dist/assets/{info-DoehRSQU.js → info-p2pKHzPt.js} +1 -1
- package/dashboard/dist/assets/{link-2-D8xSsPqy.js → link-2-BQhZyxUQ.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-DMu0-X5r.js → loader-circle-BnFE5J64.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-DLyYsZvE.js → maximize-2-Zt1L2xuV.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-Dm5Xy2jA.js → modifiers.esm-Sbhv1CrS.js} +1 -1
- package/dashboard/dist/assets/{monitor-smartphone-5uZGQNGJ.js → monitor-smartphone-Pmy9ktPf.js} +1 -1
- package/dashboard/dist/assets/{plus-8fnU0dkt.js → plus-FPP4P4tW.js} +1 -1
- package/dashboard/dist/assets/{provider-display-B3ed-pSc.js → provider-display-zpzUqFZ8.js} +2 -2
- package/dashboard/dist/assets/{react-Gft01KYk.js → react-C_5Bvle-.js} +1 -1
- package/dashboard/dist/assets/{search-jqzJg2kG.js → search-7f0zYUoC.js} +1 -1
- package/dashboard/dist/assets/{skills-api-Bwn_xcHV.js → skills-api-CdEcHyY0.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-CYjzLCp7.js → sortable.esm-DwOWVPK8.js} +1 -1
- package/dashboard/dist/assets/{store-CgJHe9jU.js → store-D3n2aJ7w.js} +1 -1
- package/dashboard/dist/assets/{terminal-CZ5j8yEQ.js → terminal-C6zDcArO.js} +1 -1
- package/dashboard/dist/assets/{trash-DtMLjI_S.js → trash-C_bcv-F0.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-Px3dHGlR.js → use-community-stats-fCkrn5Kd.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-B7Vw4Wsd.js → use-latest-request-guard-DD2Nt1qS.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-DWWrcMPi.js → use-limits-display-prefs-a9HtQcOE.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-CevbB79U.js → use-ordered-list-B6-Nb6rL.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-DyXrwS-h.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-DoIaI0vm.js → use-session-efficiency-pref-gD9i2h4k.js} +1 -1
- package/dashboard/dist/assets/{use-transform-qRwUetOk.js → use-transform-DP6mCHUn.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-DlP7yg78.js → use-usage-limits-TdwK-gf2.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-DRUe8YoY.js → useCurrency-Bbxwhe-A.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-CmLbznUC.js → useTokenFormat-CzeTrne_.js} +1 -1
- package/dashboard/dist/assets/{zap-CS80c_uN.js → zap-CucEQWyU.js} +1 -1
- package/dashboard/dist/index.html +13 -11
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/llms.txt +3 -2
- package/dashboard/dist/share.html +1 -1
- package/package.json +2 -2
- package/src/commands/init.js +10 -0
- package/src/commands/status.js +16 -0
- package/src/commands/sync.js +43 -1
- package/src/lib/codex-rollout-parser.js +86 -8
- package/src/lib/codex-usage-record.js +195 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +261 -11
- package/src/lib/skills-manager.js +2 -2
- package/dashboard/dist/assets/DevicePage-RCIANm45.js +0 -1
- package/dashboard/dist/assets/DialogDescription-C2EzE7DU.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-CCuEY-79.js +0 -1
package/src/commands/sync.js
CHANGED
|
@@ -18,6 +18,7 @@ const {
|
|
|
18
18
|
updateJsonLocked,
|
|
19
19
|
} = require("../lib/fs");
|
|
20
20
|
const { physicalJsonlRecords } = require("../lib/jsonl-lines");
|
|
21
|
+
const { countRecordOnlyFiles, formatRecordOnlyWarning } = require("../lib/codex-usage-record");
|
|
21
22
|
const {
|
|
22
23
|
listRolloutFiles,
|
|
23
24
|
listRolloutFilesDeep,
|
|
@@ -91,6 +92,8 @@ const {
|
|
|
91
92
|
piAgentDirCollidesWithOmp,
|
|
92
93
|
resolvePrimeAgentSessionFiles,
|
|
93
94
|
parsePrimeAgentIncremental,
|
|
95
|
+
resolveMinimaxCodeSessionFiles,
|
|
96
|
+
parseMinimaxCodeIncremental,
|
|
94
97
|
resolveCraftSessionFiles,
|
|
95
98
|
parseCraftIncremental,
|
|
96
99
|
resolveReasonixTelemetryFiles,
|
|
@@ -312,6 +315,7 @@ const AUTO_SYNC_SOURCES = new Set([
|
|
|
312
315
|
"kimi-code",
|
|
313
316
|
"lmstudio",
|
|
314
317
|
"mimo",
|
|
318
|
+
"minimax-code",
|
|
315
319
|
"omo",
|
|
316
320
|
"omp",
|
|
317
321
|
"opencode",
|
|
@@ -2514,6 +2518,34 @@ async function cmdSync(argv, context = {}) {
|
|
|
2514
2518
|
}
|
|
2515
2519
|
}
|
|
2516
2520
|
|
|
2521
|
+
// ── MiniMax Code — passive ~/.minimax/v2/sessions/**/messages.jsonl usage reader ──
|
|
2522
|
+
let minimaxCodeResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
2523
|
+
const minimaxCodeFiles = sourceAllowed("minimax-code")
|
|
2524
|
+
? mergeBothFileSources({ resolveFiles: resolveMinimaxCodeSessionFiles, env: process.env })
|
|
2525
|
+
: [];
|
|
2526
|
+
if (minimaxCodeFiles.length > 0) {
|
|
2527
|
+
if (progress?.enabled) {
|
|
2528
|
+
progress.start(`Parsing MiniMax Code ${renderBar(0)} | buckets 0`);
|
|
2529
|
+
}
|
|
2530
|
+
try {
|
|
2531
|
+
minimaxCodeResult = await parseMinimaxCodeIncremental({
|
|
2532
|
+
sessionFiles: minimaxCodeFiles,
|
|
2533
|
+
cursors,
|
|
2534
|
+
queuePath,
|
|
2535
|
+
env: process.env,
|
|
2536
|
+
onProgress: (p) => {
|
|
2537
|
+
if (!progress?.enabled) return;
|
|
2538
|
+
const pct = p.total > 0 ? p.index / p.total : 1;
|
|
2539
|
+
progress.update(
|
|
2540
|
+
`Parsing MiniMax Code ${renderBar(pct)} ${formatNumber(p.index)}/${formatNumber(p.total)} files | buckets ${formatNumber(p.bucketsQueued)}`,
|
|
2541
|
+
);
|
|
2542
|
+
},
|
|
2543
|
+
});
|
|
2544
|
+
} catch (err) {
|
|
2545
|
+
warnProviderParseFailure("MiniMax Code", err, opts);
|
|
2546
|
+
}
|
|
2547
|
+
}
|
|
2548
|
+
|
|
2517
2549
|
// ── Craft Agents (passive ~/.craft-agent + workspaces session.jsonl reader) ──
|
|
2518
2550
|
let craftResult = { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
2519
2551
|
const craftFiles = sourceAllowed("craft")
|
|
@@ -3014,6 +3046,7 @@ async function cmdSync(argv, context = {}) {
|
|
|
3014
3046
|
omoResult.recordsProcessed +
|
|
3015
3047
|
piResult.recordsProcessed +
|
|
3016
3048
|
primeAgentResult.recordsProcessed +
|
|
3049
|
+
minimaxCodeResult.recordsProcessed +
|
|
3017
3050
|
craftResult.recordsProcessed +
|
|
3018
3051
|
reasonixResult.recordsProcessed +
|
|
3019
3052
|
grokResult.recordsProcessed +
|
|
@@ -3054,6 +3087,7 @@ async function cmdSync(argv, context = {}) {
|
|
|
3054
3087
|
omoResult.bucketsQueued +
|
|
3055
3088
|
piResult.bucketsQueued +
|
|
3056
3089
|
primeAgentResult.bucketsQueued +
|
|
3090
|
+
minimaxCodeResult.bucketsQueued +
|
|
3057
3091
|
craftResult.bucketsQueued +
|
|
3058
3092
|
reasonixResult.bucketsQueued +
|
|
3059
3093
|
grokResult.bucketsQueued +
|
|
@@ -3272,6 +3306,7 @@ async function cmdSync(argv, context = {}) {
|
|
|
3272
3306
|
}
|
|
3273
3307
|
|
|
3274
3308
|
if (!opts.auto) {
|
|
3309
|
+
const codexRecordOnlyWarning = formatRecordOnlyWarning(countRecordOnlyFiles(cursors));
|
|
3275
3310
|
process.stdout.write(
|
|
3276
3311
|
[
|
|
3277
3312
|
"Sync finished:",
|
|
@@ -3283,6 +3318,7 @@ async function cmdSync(argv, context = {}) {
|
|
|
3283
3318
|
runtime.deviceToken && pendingBytes > 0 && !opts.drain
|
|
3284
3319
|
? `- Remaining: ${formatBytes(pendingBytes)} pending (run sync again, or use --drain)`
|
|
3285
3320
|
: null,
|
|
3321
|
+
codexRecordOnlyWarning ? `- Warning: ${codexRecordOnlyWarning}` : null,
|
|
3286
3322
|
"",
|
|
3287
3323
|
]
|
|
3288
3324
|
.filter(Boolean)
|
|
@@ -4779,8 +4815,10 @@ async function migrateRolloutCumulativeDeltaBuckets({ cursors, queuePath, rollou
|
|
|
4779
4815
|
// The migration clears Codex buckets and reparses the discovered corpus from
|
|
4780
4816
|
// byte zero. Persisted event keys belong to the cleared buckets, so retaining
|
|
4781
4817
|
// them can suppress the rebuild when a moved session still has an old path
|
|
4782
|
-
// cursor. Rebuild the hash inventory together with the buckets.
|
|
4818
|
+
// cursor. Rebuild the hash inventory together with the buckets. Counted
|
|
4819
|
+
// compaction ids (#652) belong to the cleared buckets the same way.
|
|
4783
4820
|
cursors.codexHashes = [];
|
|
4821
|
+
cursors.codexCompactionResponseIds = [];
|
|
4784
4822
|
|
|
4785
4823
|
const buckets = cursors.hourly?.buckets;
|
|
4786
4824
|
const retractions = [];
|
|
@@ -5332,6 +5370,9 @@ async function repairCodexRescanInflation({
|
|
|
5332
5370
|
buckets: tmpCursors.hourly.buckets || {},
|
|
5333
5371
|
groupQueued: tmpCursors.hourly.groupQueued || {},
|
|
5334
5372
|
codexHashes: Array.isArray(tmpCursors.codexHashes) ? tmpCursors.codexHashes : [],
|
|
5373
|
+
codexCompactionResponseIds: Array.isArray(tmpCursors.codexCompactionResponseIds)
|
|
5374
|
+
? tmpCursors.codexCompactionResponseIds
|
|
5375
|
+
: [],
|
|
5335
5376
|
files: tmpCursors.files || {},
|
|
5336
5377
|
queueRows: tmpRaw.split("\n").filter((l) => l.trim()),
|
|
5337
5378
|
projectHourly: tmpCursors.projectHourly || null,
|
|
@@ -5486,6 +5527,7 @@ async function repairCodexRescanInflation({
|
|
|
5486
5527
|
cursors.files[fp] = v;
|
|
5487
5528
|
}
|
|
5488
5529
|
cursors.codexHashes = rebuilt.codexHashes;
|
|
5530
|
+
cursors.codexCompactionResponseIds = rebuilt.codexCompactionResponseIds;
|
|
5489
5531
|
|
|
5490
5532
|
// 3. Project usage mirrors the main Codex repair: drop inflated Codex project
|
|
5491
5533
|
// rows, append the rebuilt rows, and swap only Codex project buckets. Project
|
|
@@ -31,6 +31,15 @@ const {
|
|
|
31
31
|
snapshotCodexModelAttributionState,
|
|
32
32
|
} = require("./codex-model-attribution");
|
|
33
33
|
const { readCodexServiceTier, isPriorityServiceTier } = require("./codex-service-tier");
|
|
34
|
+
const {
|
|
35
|
+
createUsageRecordState,
|
|
36
|
+
extractTokenUsageRecord,
|
|
37
|
+
noteLineAfterUsageRecord,
|
|
38
|
+
noteTokenCount,
|
|
39
|
+
noteUsageRecord,
|
|
40
|
+
snapshotUsageRecordState,
|
|
41
|
+
takeCompactionOnTokenCount,
|
|
42
|
+
} = require("./codex-usage-record");
|
|
34
43
|
|
|
35
44
|
const DISCOVERY_FULL_AUDIT_INTERVAL_MS = 60 * 1000;
|
|
36
45
|
const MAX_DISCOVERY_INVENTORIES = 32;
|
|
@@ -643,9 +652,37 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
643
652
|
|
|
644
653
|
let turnCount = 0;
|
|
645
654
|
|
|
646
|
-
|
|
655
|
+
// Compaction records (issue #652, rules in codex-usage-record.js). A resume
|
|
656
|
+
// state from before #652 with prevTotals has seen a token_count.
|
|
657
|
+
const usageRecordState = createUsageRecordState(
|
|
658
|
+
isResuming
|
|
659
|
+
? resumeState.usageRecordState || (resumeState.prevTotals ? { sawTokenCount: true } : null)
|
|
660
|
+
: null,
|
|
661
|
+
);
|
|
662
|
+
// Compactions counted in this session, by response_id: the files merged for
|
|
663
|
+
// a session can repeat a record (an archived copy, a split fragment), and a
|
|
664
|
+
// resumed parse must not count one from an earlier chunk again.
|
|
665
|
+
const compactionResponseIds = new Set(
|
|
666
|
+
isResuming && Array.isArray(resumeState.compactionResponseIds)
|
|
667
|
+
? resumeState.compactionResponseIds
|
|
668
|
+
: [],
|
|
669
|
+
);
|
|
670
|
+
|
|
671
|
+
// Attribution as of now. A compaction record is counted later than it is
|
|
672
|
+
// read, so it carries the attribution captured when it was read.
|
|
673
|
+
function currentUsageAttribution() {
|
|
674
|
+
return {
|
|
675
|
+
model: currentCodexModel(modelAttributionState) || "unknown",
|
|
676
|
+
selectedModel: modelAttributionState.selectedModel || null,
|
|
677
|
+
rerouted: Boolean(modelAttributionState.rerouted),
|
|
678
|
+
rerouteReason: modelAttributionState.rerouteReason || null,
|
|
679
|
+
serviceTier,
|
|
680
|
+
};
|
|
681
|
+
}
|
|
682
|
+
|
|
683
|
+
function recordModelUsage(delta, rawRequestUsage, attribution = currentUsageAttribution()) {
|
|
647
684
|
if (!collectModelUsage || !delta || delta.total_tokens <= 0) return;
|
|
648
|
-
const effectiveModel =
|
|
685
|
+
const effectiveModel = attribution.model;
|
|
649
686
|
let row = byModel.get(effectiveModel);
|
|
650
687
|
if (!row) {
|
|
651
688
|
row = {
|
|
@@ -677,13 +714,13 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
677
714
|
}
|
|
678
715
|
addInto(row, delta);
|
|
679
716
|
row.usage_events += 1;
|
|
680
|
-
if (
|
|
681
|
-
row.selected_models.add(
|
|
717
|
+
if (attribution.selectedModel) {
|
|
718
|
+
row.selected_models.add(attribution.selectedModel);
|
|
682
719
|
}
|
|
683
|
-
if (
|
|
720
|
+
if (attribution.rerouted) {
|
|
684
721
|
row.rerouted_usage_events += 1;
|
|
685
|
-
if (
|
|
686
|
-
row.reroute_reasons.add(
|
|
722
|
+
if (attribution.rerouteReason) {
|
|
723
|
+
row.reroute_reasons.add(attribution.rerouteReason);
|
|
687
724
|
}
|
|
688
725
|
}
|
|
689
726
|
// OpenAI applies the long-context tier to the whole request when its raw
|
|
@@ -704,7 +741,7 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
704
741
|
// instead of replacing it. The two subsets alone cannot say how much usage
|
|
705
742
|
// was on BOTH, so record the intersection explicitly — otherwise a long
|
|
706
743
|
// Fast request prices at 3x Standard input where it should be 4x.
|
|
707
|
-
if (isPriorityServiceTier(serviceTier)) {
|
|
744
|
+
if (isPriorityServiceTier(attribution.serviceTier)) {
|
|
708
745
|
row.priority_usage_events += 1;
|
|
709
746
|
row.priority_input_tokens += delta.input_tokens;
|
|
710
747
|
row.priority_cached_input_tokens += delta.cached_input_tokens;
|
|
@@ -885,6 +922,14 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
885
922
|
pendingExecDetails = [];
|
|
886
923
|
}
|
|
887
924
|
|
|
925
|
+
function countCompactionRecord(record) {
|
|
926
|
+
if (compactionResponseIds.has(record.responseId)) return;
|
|
927
|
+
compactionResponseIds.add(record.responseId);
|
|
928
|
+
if (!record.inRequestedRange) return;
|
|
929
|
+
recordModelUsage(record.delta, record.rawUsage, record.attribution);
|
|
930
|
+
attributeTurn(record.delta);
|
|
931
|
+
}
|
|
932
|
+
|
|
888
933
|
function usageEventSignature(lastUsage, totalUsage) {
|
|
889
934
|
return JSON.stringify({ lastUsage, totalUsage });
|
|
890
935
|
}
|
|
@@ -952,6 +997,8 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
952
997
|
// turn_context.payload.model itself would make codex-model-attribution.js
|
|
953
998
|
// one of two authorities on the same question.
|
|
954
999
|
if (typeof onObject === "function") onObject(obj, attributedModel);
|
|
1000
|
+
const usageRecord = extractTokenUsageRecord(obj);
|
|
1001
|
+
if (!usageRecord) noteLineAfterUsageRecord(usageRecordState, obj?.type === "compacted");
|
|
955
1002
|
const ts = typeof obj?.timestamp === "string" ? obj.timestamp : null;
|
|
956
1003
|
if (!ts) continue;
|
|
957
1004
|
const inRequestedRange = isTimestampInRequestedDayRange(ts, { from, to, timeZoneContext });
|
|
@@ -1003,13 +1050,42 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
1003
1050
|
continue;
|
|
1004
1051
|
}
|
|
1005
1052
|
|
|
1053
|
+
if (usageRecord) {
|
|
1054
|
+
const delta = normalizeUsage(usageRecord.usage);
|
|
1055
|
+
noteUsageRecord(
|
|
1056
|
+
usageRecordState,
|
|
1057
|
+
// Without a response_id a compaction cannot be deduplicated.
|
|
1058
|
+
delta.total_tokens > 0 && usageRecord.responseId
|
|
1059
|
+
? {
|
|
1060
|
+
responseId: usageRecord.responseId,
|
|
1061
|
+
timestamp: usageRecord.timestamp,
|
|
1062
|
+
inRequestedRange,
|
|
1063
|
+
rawUsage: usageRecord.usage,
|
|
1064
|
+
delta,
|
|
1065
|
+
attribution: currentUsageAttribution(),
|
|
1066
|
+
}
|
|
1067
|
+
: null,
|
|
1068
|
+
);
|
|
1069
|
+
continue;
|
|
1070
|
+
}
|
|
1071
|
+
|
|
1006
1072
|
const tokenCount = extractTokenCount(obj);
|
|
1007
1073
|
if (tokenCount) {
|
|
1008
1074
|
const info = tokenCount.info;
|
|
1009
1075
|
const lastUsage = info?.last_token_usage;
|
|
1010
1076
|
const totalUsage = info?.total_token_usage;
|
|
1077
|
+
let compaction = null;
|
|
1078
|
+
if (info && typeof info === "object") {
|
|
1079
|
+
noteTokenCount(usageRecordState);
|
|
1080
|
+
compaction = takeCompactionOnTokenCount(
|
|
1081
|
+
usageRecordState,
|
|
1082
|
+
usageDeltaState.lastTotal,
|
|
1083
|
+
totalUsage,
|
|
1084
|
+
);
|
|
1085
|
+
}
|
|
1011
1086
|
const rawDelta = consumeUsageDelta(usageDeltaState, lastUsage, totalUsage);
|
|
1012
1087
|
const delta = rawDelta ? normalizeUsage(rawDelta) : null;
|
|
1088
|
+
if (compaction) countCompactionRecord(compaction);
|
|
1013
1089
|
const isNewResumeEvent = noteTokenEvent(ts, lastUsage, totalUsage);
|
|
1014
1090
|
if (inRequestedRange) {
|
|
1015
1091
|
const eventSessionId = sessionId || rolloutSessionIdFromPath(primaryFilePath) || primaryFilePath;
|
|
@@ -1082,6 +1158,8 @@ async function parseCodexRolloutFile(filePath, {
|
|
|
1082
1158
|
pendingExecDetails,
|
|
1083
1159
|
lastTokenTimestamp,
|
|
1084
1160
|
lastTimestampEventSignatures: Array.from(materializeLastTimestampSignatures()),
|
|
1161
|
+
usageRecordState: snapshotUsageRecordState(usageRecordState),
|
|
1162
|
+
compactionResponseIds: Array.from(compactionResponseIds),
|
|
1085
1163
|
};
|
|
1086
1164
|
result.endOffset = readProgress.endOffset;
|
|
1087
1165
|
result.appendable = Boolean(contentHasher) && (
|
|
@@ -0,0 +1,195 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
// Codex rollouts written since CLI 0.151 carry a top-level
|
|
4
|
+
// {"type":"token_usage_record","payload":{usage,turn_token_usage,thread_token_usage,...}}
|
|
5
|
+
// next to the event_msg token_count that has always carried usage (issue
|
|
6
|
+
// #652). While both are written they describe the same model response, so
|
|
7
|
+
// token_count stays the only source of normal usage and a record is counted
|
|
8
|
+
// in exactly one case:
|
|
9
|
+
//
|
|
10
|
+
// Compaction. Codex bills the compaction model call in a record that is
|
|
11
|
+
// immediately followed by a top-level `compacted` line, and the next
|
|
12
|
+
// token_count repeats the previous cumulative total (its last_token_usage is
|
|
13
|
+
// the post-compaction context size, not a response). The cumulative delta is
|
|
14
|
+
// zero, so without the record the call is lost. Inside a file that has
|
|
15
|
+
// already shown a token_count, such a record is held as `compaction` and
|
|
16
|
+
// counted when the next token_count arrives with a total identical to the
|
|
17
|
+
// previous one. If that total advanced, the token_count already covers the
|
|
18
|
+
// call and the record is dropped. Each compaction is counted once per
|
|
19
|
+
// payload.response_id: a fork replays its parent's records and a copied
|
|
20
|
+
// rollout repeats them, both with the same id. A record without one is never
|
|
21
|
+
// counted.
|
|
22
|
+
//
|
|
23
|
+
// A file whose records are never matched by a token_count is not counted from
|
|
24
|
+
// its records. It is flagged `recordOnly` instead, and sync surfaces a
|
|
25
|
+
// warning. The proof is the same ordering Codex keeps for every response: its
|
|
26
|
+
// token_count is written after its record and before the next record or the
|
|
27
|
+
// turn end. So a record followed by another record or a turn end with no
|
|
28
|
+
// token_count in between flags the file; a sync that stops between a record
|
|
29
|
+
// and its token_count does not.
|
|
30
|
+
//
|
|
31
|
+
// payload.usage is the per-response delta; turn_token_usage and
|
|
32
|
+
// thread_token_usage are cumulative and are never read. All state below is
|
|
33
|
+
// persisted in the sync cursor and the session parser resume state, so the
|
|
34
|
+
// record, the `compacted` line and the token_count may land in different
|
|
35
|
+
// reads.
|
|
36
|
+
|
|
37
|
+
const { canonicalUsage, sameUsage } = require("./codex-token-usage");
|
|
38
|
+
|
|
39
|
+
function extractTokenUsageRecord(obj) {
|
|
40
|
+
if (obj?.type !== "token_usage_record") return null;
|
|
41
|
+
const usage = obj.payload?.usage;
|
|
42
|
+
if (!usage || typeof usage !== "object" || Array.isArray(usage)) return null;
|
|
43
|
+
const timestamp = typeof obj.timestamp === "string" ? obj.timestamp : null;
|
|
44
|
+
if (!timestamp) return null;
|
|
45
|
+
// canonicalUsage folds cache_write_input_tokens into
|
|
46
|
+
// cache_creation_input_tokens exactly as the token_count path does.
|
|
47
|
+
const canonical = canonicalUsage(usage);
|
|
48
|
+
if (!canonical) return null;
|
|
49
|
+
const responseId = typeof obj.payload.response_id === "string" && obj.payload.response_id
|
|
50
|
+
? obj.payload.response_id
|
|
51
|
+
: null;
|
|
52
|
+
return { usage: canonical, timestamp, responseId };
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
// Compaction calls already counted by sync, by response_id, kept in core
|
|
56
|
+
// cursor state as cursors.codexCompactionResponseIds (oldest first). The id
|
|
57
|
+
// is unique per API response and survives fork replays and file copies, so
|
|
58
|
+
// it is the only dedup key a compaction needs. Compactions are rare (19 on
|
|
59
|
+
// the maintainer's corpus), so the cap is far above any real history.
|
|
60
|
+
const MAX_COMPACTION_RESPONSE_IDS = 20000;
|
|
61
|
+
|
|
62
|
+
function createCompactionResponseIds(cursors) {
|
|
63
|
+
const stored = Array.isArray(cursors.codexCompactionResponseIds)
|
|
64
|
+
? cursors.codexCompactionResponseIds
|
|
65
|
+
: [];
|
|
66
|
+
const ids = new Set(stored);
|
|
67
|
+
const added = [];
|
|
68
|
+
return {
|
|
69
|
+
has: (id) => ids.has(id),
|
|
70
|
+
add(id) {
|
|
71
|
+
if (ids.has(id)) return;
|
|
72
|
+
ids.add(id);
|
|
73
|
+
added.push(id);
|
|
74
|
+
},
|
|
75
|
+
persist() {
|
|
76
|
+
if (added.length === 0) return;
|
|
77
|
+
cursors.codexCompactionResponseIds = [...stored, ...added].slice(-MAX_COMPACTION_RESPONSE_IDS);
|
|
78
|
+
},
|
|
79
|
+
};
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
function isCodexTurnEndEvent(obj) {
|
|
83
|
+
if (obj?.type !== "event_msg") return false;
|
|
84
|
+
const type = obj.payload?.type;
|
|
85
|
+
return type === "task_complete" || type === "turn_aborted";
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function isPlainObject(value) {
|
|
89
|
+
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function createUsageRecordState(snapshot) {
|
|
93
|
+
const compaction = snapshot?.compaction;
|
|
94
|
+
return {
|
|
95
|
+
sawTokenCount: Boolean(snapshot?.sawTokenCount),
|
|
96
|
+
unmatchedRecord: Boolean(snapshot?.unmatchedRecord),
|
|
97
|
+
recordOnly: Boolean(snapshot?.recordOnly),
|
|
98
|
+
compaction: isPlainObject(compaction) && isPlainObject(compaction.candidate)
|
|
99
|
+
? { candidate: compaction.candidate, compacted: Boolean(compaction.compacted) }
|
|
100
|
+
: null,
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
// Called for every token_count carrying an info object.
|
|
105
|
+
function noteTokenCount(state) {
|
|
106
|
+
state.sawTokenCount = true;
|
|
107
|
+
state.unmatchedRecord = false;
|
|
108
|
+
state.recordOnly = false;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
// Called for every record. `candidate` is what the caller would count if the
|
|
112
|
+
// record turns out to be a compaction call (null when it cannot be counted).
|
|
113
|
+
function noteUsageRecord(state, candidate) {
|
|
114
|
+
if (state.sawTokenCount) {
|
|
115
|
+
state.compaction = candidate ? { candidate, compacted: false } : null;
|
|
116
|
+
return;
|
|
117
|
+
}
|
|
118
|
+
if (state.unmatchedRecord) state.recordOnly = true;
|
|
119
|
+
state.unmatchedRecord = true;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
// Turn end: a record still waiting for its token_count will not get one.
|
|
123
|
+
function noteTurnEnd(state) {
|
|
124
|
+
if (state.sawTokenCount) return;
|
|
125
|
+
if (state.unmatchedRecord) state.recordOnly = true;
|
|
126
|
+
state.unmatchedRecord = false;
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
// True while the line right after a record decides whether it was a
|
|
130
|
+
// compaction call.
|
|
131
|
+
function awaitsCompactedLine(state) {
|
|
132
|
+
return Boolean(state?.compaction && !state.compaction.compacted);
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
// Called for each complete line after a record, other than a record. Only a
|
|
136
|
+
// `compacted` line directly after the record keeps it as a candidate.
|
|
137
|
+
function noteLineAfterUsageRecord(state, isCompactedLine) {
|
|
138
|
+
if (!awaitsCompactedLine(state)) return;
|
|
139
|
+
if (isCompactedLine) state.compaction.compacted = true;
|
|
140
|
+
else state.compaction = null;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
// Called for every token_count carrying an info object, with the cumulative
|
|
144
|
+
// total that was active before it. Returns the compaction candidate to count,
|
|
145
|
+
// or null.
|
|
146
|
+
function takeCompactionOnTokenCount(state, previousTotal, totalUsage) {
|
|
147
|
+
const compaction = state.compaction;
|
|
148
|
+
state.compaction = null;
|
|
149
|
+
if (!compaction?.compacted) return null;
|
|
150
|
+
if (!canonicalUsage(previousTotal) || !sameUsage(previousTotal, totalUsage)) return null;
|
|
151
|
+
return compaction.candidate;
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
function isRecordOnly(state) {
|
|
155
|
+
return Boolean(state?.recordOnly && !state.sawTokenCount);
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
function snapshotUsageRecordState(state) {
|
|
159
|
+
if (!state || (!state.sawTokenCount && !state.unmatchedRecord && !state.recordOnly)) return null;
|
|
160
|
+
const out = { sawTokenCount: state.sawTokenCount };
|
|
161
|
+
if (state.unmatchedRecord) out.unmatchedRecord = true;
|
|
162
|
+
if (state.recordOnly) out.recordOnly = true;
|
|
163
|
+
if (state.compaction) out.compaction = state.compaction;
|
|
164
|
+
return out;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
// Sync keeps the flagged rollouts in core cursor state:
|
|
168
|
+
// cursors.codexUsageRecordOnlyFiles = { [rolloutPath]: true }.
|
|
169
|
+
function countRecordOnlyFiles(cursors) {
|
|
170
|
+
const flagged = cursors?.codexUsageRecordOnlyFiles;
|
|
171
|
+
return isPlainObject(flagged) ? Object.keys(flagged).length : 0;
|
|
172
|
+
}
|
|
173
|
+
|
|
174
|
+
function formatRecordOnlyWarning(count) {
|
|
175
|
+
if (!count) return null;
|
|
176
|
+
return `${count} Codex session(s) report usage only via token_usage_record; not counted yet (see #652)`;
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
module.exports = {
|
|
180
|
+
MAX_COMPACTION_RESPONSE_IDS,
|
|
181
|
+
createCompactionResponseIds,
|
|
182
|
+
countRecordOnlyFiles,
|
|
183
|
+
formatRecordOnlyWarning,
|
|
184
|
+
awaitsCompactedLine,
|
|
185
|
+
createUsageRecordState,
|
|
186
|
+
extractTokenUsageRecord,
|
|
187
|
+
isCodexTurnEndEvent,
|
|
188
|
+
isRecordOnly,
|
|
189
|
+
noteLineAfterUsageRecord,
|
|
190
|
+
noteTokenCount,
|
|
191
|
+
noteTurnEnd,
|
|
192
|
+
noteUsageRecord,
|
|
193
|
+
snapshotUsageRecordState,
|
|
194
|
+
takeCompactionOnTokenCount,
|
|
195
|
+
};
|