tokentracker-cli 0.94.0 → 0.95.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/README.de.md +9 -7
- package/README.ja.md +8 -6
- package/README.ko.md +8 -6
- package/README.md +23 -10
- package/README.zh-CN.md +8 -6
- package/dashboard/dist/assets/{AchievementBadge-DPqwk2zI.js → AchievementBadge-Pi3Gvbm8.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-CELHoMQt.js → AchievementsPage-C4WLT8Rl.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DdAC0VCb.js → AchievementsSection-ilvR2ssr.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-Dd4I1SpM.js → BadgeDetailModal-CjEeToxa.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-DL2-4mnm.js → BotAnimated-DDf9pBuK.js} +1 -1
- package/dashboard/dist/assets/{Card-B_MpSRse.js → Card-DNous84-.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-BLZZ4rG6.js → ClawdAnimated-BLMIRntb.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-B-vfdIbH.js → CommandPalette-vVekhoFO.js} +1 -1
- package/dashboard/dist/assets/CommunityStatsModal-C0Kp_J8B.js +1 -0
- package/dashboard/dist/assets/{ConfirmModal-DLnRwUXC.js → ConfirmModal-DFdpcx12.js} +1 -1
- package/dashboard/dist/assets/{Controls-FZ8wEN5w.js → Controls-RNhqybmE.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-BQjQh1IQ.js +1 -0
- package/dashboard/dist/assets/{DevicePage-D2IzPc5o.js → DevicePage-DIhh-YAk.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-BaqQaUUk.js → DialogClose-ZqxYVIzl.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-DDM4aanX.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-DrbKjtuS.js → DialogTitle-BiRcejlu.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-GddKDE8i.js → FadeIn-BDBQpeGe.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-DUu-Z5mE.js → HeaderGithubStar-CztIwtKT.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-BmDkY35A.js → HoverTooltip-Cfso7LTC.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-CGJyqWk2.js → IpCheckPage-CNE7dLs3.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-Bd91ALUj.js → LandingPage-B2a3cweM.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-BGGUwXFd.js → LeaderboardAvatar-CwmZnIji.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-BMEqHwif.js → LeaderboardPage-DkTHj3fB.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-BSweJqm2.js → LeaderboardProfileModal-CefTO3Zh.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-CRrI3RFz.js → LeaderboardProfilePage-CnOXlQ3L.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-XIRDv46K.js → LimitsPage-BIZ1-6BB.js} +2 -2
- package/dashboard/dist/assets/{LocalOnlyNotice-CCD-lGWG.js → LocalOnlyNotice-CdHQiPhy.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-lDuqSv4f.js → LoginCard-yXLA9geu.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-DStt5VXN.js → LoginModal-BF2Pfkqa.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-dmVPSL72.js → LoginPage-DuOypZap.js} +1 -1
- package/dashboard/dist/assets/{PetPage-ybcZRctF.js → PetPage-BH3xDVV9.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-Dar_UjxF.js → PopoverPopup-CewF_cGz.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-Ct9tILpz.js → ProviderIcon-Dg0K0RPn.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-B4yLh5zx.js → ResetPasswordPage-CYwt85O3.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-BOE1CEzq.js → SegmentedControl-BJkXXomQ.js} +1 -1
- package/dashboard/dist/assets/{Select-D-bSh7Zz.js → Select-B3_WH0Qe.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-DMYjXXq4.js → SelectItemText-BUby_DUu.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-CaleS-6_.js → ServiceStatusPage-BDlVZk4i.js} +1 -1
- package/dashboard/dist/assets/SessionsPage-TaC9nahK.js +2 -0
- package/dashboard/dist/assets/{SettingsPage-iJH0Yyah.js → SettingsPage-CudcQh6m.js} +2 -2
- package/dashboard/dist/assets/SkillsPage-iDLQ8lpZ.js +1 -0
- package/dashboard/dist/assets/{TokenGalaxy-CuXKoMPg.js → TokenGalaxy-DtYzUsXU.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-Dx5GB7F5.js → ToolbarRootContext-PC8Tg1Jz.js} +1 -1
- package/dashboard/dist/assets/TrendMonitor-r8n8YO_9.js +19 -0
- package/dashboard/dist/assets/{WidgetsPage-DUaIyGtu.js → WidgetsPage-vzfGccWb.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-BzqlcAUC.js → WrappedPage-DaKuIkYE.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-BHz2N7Ih.css → __vite-browser-external-CCMdjCX2.css} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-Dnxl50k1.js +45 -0
- package/dashboard/dist/assets/{agent-logos-BdWWxza6.js → agent-logos-B7nzn3n2.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-CANSzmm-.js → arrow-up-right-CsxyvCER.js} +1 -1
- package/dashboard/dist/assets/{check-BnJpzXRZ.js → check-hYcC7MdH.js} +1 -1
- package/dashboard/dist/assets/{composite-Ccj6KQmh.js → composite-C79LNbex.js} +1 -1
- package/dashboard/dist/assets/{copy-C2Yc8Evu.js → copy-Cz4qzE62.js} +1 -1
- package/dashboard/dist/assets/{download-B52JTk6Y.js → download-CUaSNE2E.js} +1 -1
- package/dashboard/dist/assets/{external-link-DayTwZx-.js → external-link-vq7kIyKK.js} +1 -1
- package/dashboard/dist/assets/{flame-B8GEX2yT.js → flame-CB6K4eR6.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-mhkbohkb.js → format-tokens-KI-e_JQ-.js} +1 -1
- package/dashboard/dist/assets/{icons-o8bPXqRP.js → icons-3maItkrp.js} +1 -1
- package/dashboard/dist/assets/{index-MBVWUKpA.js → index-BRXoZ7Jj.js} +1 -1
- package/dashboard/dist/assets/{index-CK_CduLm.js → index-C5WrfmLB.js} +1 -1
- package/dashboard/dist/assets/{index-Bp0ZHDtz.js → index-CUbC50Zp.js} +1 -1
- package/dashboard/dist/assets/{info-D94QM_vX.js → info-r0yGihUe.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-BLzMyCiy.js → limits-providers-BN_94oe9.js} +1 -1
- package/dashboard/dist/assets/{link-2-CqBmFaDI.js → link-2-J9t0VXWM.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-SaxUpDR6.js → loader-circle-DCvOsPze.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-BoXqARks.js → maximize-2-CZ9JhGjf.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-Conh_-BD.js → modifiers.esm-DJ0_WKg0.js} +1 -1
- package/dashboard/dist/assets/module-xpSFyNBK.js +3 -0
- package/dashboard/dist/assets/{monitor-smartphone-D6qLR9qd.js → monitor-smartphone-nqbWjJx_.js} +1 -1
- package/dashboard/dist/assets/{plus-D51mjIc2.js → plus-Cg7wd2jK.js} +1 -1
- package/dashboard/dist/assets/{provider-display-DpHPjd_s.js → provider-display-B9L1nRY9.js} +2 -2
- package/dashboard/dist/assets/{react-D_AcI466.js → react-D2AD6Pk2.js} +1 -1
- package/dashboard/dist/assets/{search-CwLcNdvZ.js → search-uNiWba1h.js} +1 -1
- package/dashboard/dist/assets/{skills-api-Dqi7Mjgo.js → skills-api-BBYuAhiL.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-BPDlUxPb.js → sortable.esm-CzPxc949.js} +1 -1
- package/dashboard/dist/assets/{store-ikeRIRGd.js → store-D4A7EUwE.js} +1 -1
- package/dashboard/dist/assets/{terminal-BfeX3L5T.js → terminal-Crmm_yKF.js} +1 -1
- package/dashboard/dist/assets/{trash-2-5_Qlp1k7.js → trash-2-ia9VjRnU.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-D90Fy2_j.js → use-community-stats-KaGCAxaF.js} +1 -1
- package/dashboard/dist/assets/use-latest-request-guard-Dy8gSDkk.js +1 -0
- package/dashboard/dist/assets/{use-limits-display-prefs-D4BYFaa-.js → use-limits-display-prefs-JSTYlbTD.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-DJxLNdXY.js → use-ordered-list-C4JnHdE0.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-AQE0ummK.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-CAX_GqXc.js → use-session-efficiency-pref-CgyUz5Gc.js} +1 -1
- package/dashboard/dist/assets/{use-transform-CeW6Z5R0.js → use-transform-C7-jSJ9k.js} +1 -1
- package/dashboard/dist/assets/use-usage-limits-B3YYqn-v.js +1 -0
- package/dashboard/dist/assets/{useCurrency-BarGeFyK.js → useCurrency-CK_gcb3J.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-DUJ9iT9H.js → useTokenFormat-C_a-UkCA.js} +1 -1
- package/dashboard/dist/assets/{zap--m0IWoI3.js → zap-BIJNn-Ic.js} +1 -1
- package/dashboard/dist/brand-logos/lmstudio.svg +12 -0
- package/dashboard/dist/brand-logos/unsloth.svg +1 -0
- package/dashboard/dist/index.html +16 -12
- package/dashboard/dist/ip-check.html +2 -2
- package/dashboard/dist/leaderboard.html +2 -2
- package/dashboard/dist/llms.txt +4 -2
- package/dashboard/dist/share.html +2 -2
- package/package.json +2 -2
- package/src/commands/device-login.js +5 -10
- package/src/commands/init.js +28 -1
- package/src/commands/serve.js +6 -0
- package/src/commands/status.js +24 -0
- package/src/commands/sync.js +114 -18
- package/src/lib/cloud-account.js +102 -29
- package/src/lib/fs.js +30 -0
- package/src/lib/grok-context-breakdown.js +24 -33
- package/src/lib/grok-usage.js +95 -0
- package/src/lib/local-api.js +101 -13
- package/src/lib/pricing/index.js +15 -0
- package/src/lib/pricing/matcher.js +15 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +1094 -72
- package/src/lib/session-analytics.js +100 -40
- package/src/lib/usage-limits.js +507 -55
- package/src/lib/zcode-limits.js +65 -8
- package/dashboard/dist/assets/CommunityStatsModal-aAico565.js +0 -1
- package/dashboard/dist/assets/DashboardPage-BByRBsbt.js +0 -1
- package/dashboard/dist/assets/DialogDescription-BUrh0Jod.js +0 -1
- package/dashboard/dist/assets/SessionsPage-DOaY6_E9.js +0 -2
- package/dashboard/dist/assets/SkillsPage-BkzJOBLk.js +0 -1
- package/dashboard/dist/assets/TrendMonitor-BuKmsznP.js +0 -19
- package/dashboard/dist/assets/__vite-browser-external-CVIGknXl.js +0 -45
- package/dashboard/dist/assets/chevron-right-DJm0mbG0.js +0 -1
- package/dashboard/dist/assets/module-D2D5r9gC.js +0 -3
- package/dashboard/dist/assets/use-reduced-motion-yE3T3cat.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-5HP8k9a3.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -15,6 +15,7 @@ const {
|
|
|
15
15
|
createUsageDeltaState,
|
|
16
16
|
snapshotUsageBaselines,
|
|
17
17
|
} = require("./codex-token-usage");
|
|
18
|
+
const { USD_TICKS_PER_USD, normalizeGrokUsage } = require("./grok-usage");
|
|
18
19
|
|
|
19
20
|
const DEFAULT_SOURCE = "codex";
|
|
20
21
|
const DEFAULT_MODEL = "unknown";
|
|
@@ -2887,6 +2888,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
|
2887
2888
|
reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
|
|
2888
2889
|
total_tokens: zeroTotals.total_tokens,
|
|
2889
2890
|
billable_total_tokens: zeroTotals.billable_total_tokens,
|
|
2891
|
+
total_cost_usd: zeroTotals.total_cost_usd,
|
|
2890
2892
|
conversation_count: zeroTotals.conversation_count,
|
|
2891
2893
|
}),
|
|
2892
2894
|
);
|
|
@@ -2905,10 +2907,12 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
|
2905
2907
|
hour_start: group.hourStart,
|
|
2906
2908
|
input_tokens: zeroTotals.input_tokens,
|
|
2907
2909
|
cached_input_tokens: zeroTotals.cached_input_tokens,
|
|
2910
|
+
cache_creation_input_tokens: zeroTotals.cache_creation_input_tokens,
|
|
2908
2911
|
output_tokens: zeroTotals.output_tokens,
|
|
2909
2912
|
reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
|
|
2910
2913
|
total_tokens: zeroTotals.total_tokens,
|
|
2911
2914
|
billable_total_tokens: zeroTotals.billable_total_tokens,
|
|
2915
|
+
total_cost_usd: zeroTotals.total_cost_usd,
|
|
2912
2916
|
conversation_count: zeroTotals.conversation_count,
|
|
2913
2917
|
}),
|
|
2914
2918
|
);
|
|
@@ -2923,7 +2927,8 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
|
2923
2927
|
totals = cloneTotals(bucket.totals);
|
|
2924
2928
|
addTotals(totals, unknownBucket.totals);
|
|
2925
2929
|
}
|
|
2926
|
-
const
|
|
2930
|
+
const usagePrecision = bucket.usage_precision || null;
|
|
2931
|
+
const key = usagePrecision ? `${totalsKey(totals)}|${usagePrecision}` : totalsKey(totals);
|
|
2927
2932
|
if (bucket.queuedKey === key) continue;
|
|
2928
2933
|
toAppend.push(
|
|
2929
2934
|
JSON.stringify({
|
|
@@ -2937,6 +2942,8 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
|
2937
2942
|
reasoning_output_tokens: totals.reasoning_output_tokens,
|
|
2938
2943
|
total_tokens: totals.total_tokens,
|
|
2939
2944
|
billable_total_tokens: totals.billable_total_tokens ?? totals.total_tokens,
|
|
2945
|
+
total_cost_usd: totals.total_cost_usd || 0,
|
|
2946
|
+
usage_precision: usagePrecision || undefined,
|
|
2940
2947
|
conversation_count: totals.conversation_count,
|
|
2941
2948
|
}),
|
|
2942
2949
|
);
|
|
@@ -2960,10 +2967,12 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
|
2960
2967
|
hour_start: group.hourStart,
|
|
2961
2968
|
input_tokens: zeroTotals.input_tokens,
|
|
2962
2969
|
cached_input_tokens: zeroTotals.cached_input_tokens,
|
|
2970
|
+
cache_creation_input_tokens: zeroTotals.cache_creation_input_tokens,
|
|
2963
2971
|
output_tokens: zeroTotals.output_tokens,
|
|
2964
2972
|
reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
|
|
2965
2973
|
total_tokens: zeroTotals.total_tokens,
|
|
2966
2974
|
billable_total_tokens: zeroTotals.billable_total_tokens,
|
|
2975
|
+
total_cost_usd: zeroTotals.total_cost_usd,
|
|
2967
2976
|
conversation_count: zeroTotals.conversation_count,
|
|
2968
2977
|
}),
|
|
2969
2978
|
);
|
|
@@ -2987,6 +2996,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
|
2987
2996
|
reasoning_output_tokens: zeroTotals.reasoning_output_tokens,
|
|
2988
2997
|
total_tokens: zeroTotals.total_tokens,
|
|
2989
2998
|
billable_total_tokens: zeroTotals.billable_total_tokens,
|
|
2999
|
+
total_cost_usd: zeroTotals.total_cost_usd,
|
|
2990
3000
|
conversation_count: zeroTotals.conversation_count,
|
|
2991
3001
|
}),
|
|
2992
3002
|
);
|
|
@@ -2994,7 +3004,10 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
|
2994
3004
|
}
|
|
2995
3005
|
}
|
|
2996
3006
|
if (unknownBucket) unknownBucket.alignedModel = nextAligned;
|
|
2997
|
-
const
|
|
3007
|
+
const usagePrecision = unknownBucket.usage_precision || null;
|
|
3008
|
+
const key = usagePrecision
|
|
3009
|
+
? `${totalsKey(unknownBucket.totals)}|${usagePrecision}`
|
|
3010
|
+
: totalsKey(unknownBucket.totals);
|
|
2998
3011
|
const outputKey = outputModel === DEFAULT_MODEL ? key : `${key}|${outputModel}`;
|
|
2999
3012
|
if (unknownBucket.queuedKey === outputKey) continue;
|
|
3000
3013
|
toAppend.push(
|
|
@@ -3009,6 +3022,8 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
|
3009
3022
|
reasoning_output_tokens: unknownBucket.totals.reasoning_output_tokens,
|
|
3010
3023
|
total_tokens: unknownBucket.totals.total_tokens,
|
|
3011
3024
|
billable_total_tokens: unknownBucket.totals.billable_total_tokens ?? unknownBucket.totals.total_tokens,
|
|
3025
|
+
total_cost_usd: unknownBucket.totals.total_cost_usd || 0,
|
|
3026
|
+
usage_precision: usagePrecision || undefined,
|
|
3012
3027
|
conversation_count: unknownBucket.totals.conversation_count,
|
|
3013
3028
|
}),
|
|
3014
3029
|
);
|
|
@@ -3056,6 +3071,7 @@ async function enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets })
|
|
|
3056
3071
|
reasoning_output_tokens: group.totals.reasoning_output_tokens,
|
|
3057
3072
|
total_tokens: group.totals.total_tokens,
|
|
3058
3073
|
billable_total_tokens: group.totals.billable_total_tokens ?? group.totals.total_tokens,
|
|
3074
|
+
total_cost_usd: group.totals.total_cost_usd || 0,
|
|
3059
3075
|
conversation_count: group.totals.conversation_count,
|
|
3060
3076
|
}),
|
|
3061
3077
|
);
|
|
@@ -3111,6 +3127,7 @@ async function enqueueTouchedProjectBuckets({
|
|
|
3111
3127
|
reasoning_output_tokens: totals.reasoning_output_tokens,
|
|
3112
3128
|
total_tokens: totals.total_tokens,
|
|
3113
3129
|
billable_total_tokens: totals.billable_total_tokens ?? totals.total_tokens,
|
|
3130
|
+
total_cost_usd: totals.total_cost_usd || 0,
|
|
3114
3131
|
conversation_count: totals.conversation_count,
|
|
3115
3132
|
}),
|
|
3116
3133
|
);
|
|
@@ -3635,6 +3652,7 @@ function initTotals() {
|
|
|
3635
3652
|
reasoning_output_tokens: 0,
|
|
3636
3653
|
total_tokens: 0,
|
|
3637
3654
|
billable_total_tokens: 0,
|
|
3655
|
+
total_cost_usd: 0,
|
|
3638
3656
|
conversation_count: 0,
|
|
3639
3657
|
};
|
|
3640
3658
|
}
|
|
@@ -3647,6 +3665,9 @@ function addTotals(target, delta) {
|
|
|
3647
3665
|
target.reasoning_output_tokens += delta.reasoning_output_tokens || 0;
|
|
3648
3666
|
target.total_tokens += delta.total_tokens || 0;
|
|
3649
3667
|
target.billable_total_tokens += delta.billable_total_tokens ?? delta.total_tokens ?? 0;
|
|
3668
|
+
target.total_cost_usd = Math.round(
|
|
3669
|
+
((target.total_cost_usd || 0) + (delta.total_cost_usd || 0)) * USD_TICKS_PER_USD,
|
|
3670
|
+
) / USD_TICKS_PER_USD;
|
|
3650
3671
|
target.conversation_count += delta.conversation_count || 0;
|
|
3651
3672
|
}
|
|
3652
3673
|
|
|
@@ -3671,6 +3692,12 @@ function subtractTotals(target, totals) {
|
|
|
3671
3692
|
target.billable_total_tokens -
|
|
3672
3693
|
(totals.billable_total_tokens ?? totals.total_tokens ?? 0),
|
|
3673
3694
|
);
|
|
3695
|
+
target.total_cost_usd = Math.max(
|
|
3696
|
+
0,
|
|
3697
|
+
Math.round(
|
|
3698
|
+
((target.total_cost_usd || 0) - (totals.total_cost_usd || 0)) * USD_TICKS_PER_USD,
|
|
3699
|
+
) / USD_TICKS_PER_USD,
|
|
3700
|
+
);
|
|
3674
3701
|
target.conversation_count = Math.max(
|
|
3675
3702
|
0,
|
|
3676
3703
|
target.conversation_count - (totals.conversation_count || 0),
|
|
@@ -3686,6 +3713,7 @@ function totalsKey(totals) {
|
|
|
3686
3713
|
totals.reasoning_output_tokens || 0,
|
|
3687
3714
|
totals.total_tokens || 0,
|
|
3688
3715
|
totals.billable_total_tokens ?? totals.total_tokens ?? 0,
|
|
3716
|
+
totals.total_cost_usd || 0,
|
|
3689
3717
|
totals.conversation_count || 0,
|
|
3690
3718
|
].join("|");
|
|
3691
3719
|
}
|
|
@@ -4872,6 +4900,42 @@ function isZcodeNativeMessage(data) {
|
|
|
4872
4900
|
);
|
|
4873
4901
|
}
|
|
4874
4902
|
|
|
4903
|
+
// ZCode persists inclusive parent counters in both its legacy OpenCode tables
|
|
4904
|
+
// and the newer model_usage table: cache read/write are already included in
|
|
4905
|
+
// input, and reasoning is already included in output. The shared OpenCode
|
|
4906
|
+
// parser expects disjoint columns, so split the subsets before it computes
|
|
4907
|
+
// queue totals and cost (issue #554).
|
|
4908
|
+
function normalizeZcodeInclusiveTokens(tokens) {
|
|
4909
|
+
if (!tokens || typeof tokens !== "object") return tokens;
|
|
4910
|
+
const rawInput = toNonNegativeInt(tokens.input);
|
|
4911
|
+
const rawOutput = toNonNegativeInt(tokens.output);
|
|
4912
|
+
const cacheRead = toNonNegativeInt(tokens.cache?.read);
|
|
4913
|
+
const cacheWrite = toNonNegativeInt(tokens.cache?.write);
|
|
4914
|
+
const reasoning = toNonNegativeInt(tokens.reasoning);
|
|
4915
|
+
return {
|
|
4916
|
+
...tokens,
|
|
4917
|
+
input: Math.max(0, rawInput - cacheRead - cacheWrite),
|
|
4918
|
+
output: Math.max(0, rawOutput - reasoning),
|
|
4919
|
+
reasoning,
|
|
4920
|
+
cache: {
|
|
4921
|
+
...(tokens.cache && typeof tokens.cache === "object" ? tokens.cache : {}),
|
|
4922
|
+
read: cacheRead,
|
|
4923
|
+
write: cacheWrite,
|
|
4924
|
+
},
|
|
4925
|
+
};
|
|
4926
|
+
}
|
|
4927
|
+
|
|
4928
|
+
function normalizeZcodeLegacyMessage(message) {
|
|
4929
|
+
if (!message?.data?.tokens) return message;
|
|
4930
|
+
return {
|
|
4931
|
+
...message,
|
|
4932
|
+
data: {
|
|
4933
|
+
...message.data,
|
|
4934
|
+
tokens: normalizeZcodeInclusiveTokens(message.data.tokens),
|
|
4935
|
+
},
|
|
4936
|
+
};
|
|
4937
|
+
}
|
|
4938
|
+
|
|
4875
4939
|
const ZCODE_NATIVE_USAGE_COLUMNS = new Set([
|
|
4876
4940
|
"id",
|
|
4877
4941
|
"logical_request_id",
|
|
@@ -5017,18 +5081,6 @@ function readZcodeNativeUsageMessages(dbPath, sqliteOptions = {}) {
|
|
|
5017
5081
|
const startedAt = coerceEpochMs(row?.started_at);
|
|
5018
5082
|
if (!providerID || !modelID || !sessionID || !id || !startedAt) continue;
|
|
5019
5083
|
|
|
5020
|
-
// ZCode's persisted input/output columns are inclusive parents:
|
|
5021
|
-
// cache read/write are subsets of input, and reasoning is a subset of
|
|
5022
|
-
// output. TokenTracker stores those five columns disjointly, so split the
|
|
5023
|
-
// subsets here before entering the shared OpenCode parser. This preserves
|
|
5024
|
-
// sum(input + cache read + cache write + output + reasoning) without
|
|
5025
|
-
// billing either subset twice.
|
|
5026
|
-
const rawInput = toNonNegativeInt(row?.input_tokens);
|
|
5027
|
-
const rawOutput = toNonNegativeInt(row?.output_tokens);
|
|
5028
|
-
const cacheRead = toNonNegativeInt(row?.cache_read_input_tokens);
|
|
5029
|
-
const cacheWrite = toNonNegativeInt(row?.cache_creation_input_tokens);
|
|
5030
|
-
const reasoning = toNonNegativeInt(row?.reasoning_tokens);
|
|
5031
|
-
|
|
5032
5084
|
const data = {
|
|
5033
5085
|
id,
|
|
5034
5086
|
sessionID,
|
|
@@ -5036,15 +5088,15 @@ function readZcodeNativeUsageMessages(dbPath, sqliteOptions = {}) {
|
|
|
5036
5088
|
providerID,
|
|
5037
5089
|
modelID,
|
|
5038
5090
|
time: { created: startedAt, completed: startedAt },
|
|
5039
|
-
tokens: {
|
|
5040
|
-
input:
|
|
5041
|
-
output:
|
|
5042
|
-
reasoning,
|
|
5091
|
+
tokens: normalizeZcodeInclusiveTokens({
|
|
5092
|
+
input: row?.input_tokens,
|
|
5093
|
+
output: row?.output_tokens,
|
|
5094
|
+
reasoning: row?.reasoning_tokens,
|
|
5043
5095
|
cache: {
|
|
5044
|
-
read:
|
|
5045
|
-
write:
|
|
5096
|
+
read: row?.cache_read_input_tokens,
|
|
5097
|
+
write: row?.cache_creation_input_tokens,
|
|
5046
5098
|
},
|
|
5047
|
-
},
|
|
5099
|
+
}),
|
|
5048
5100
|
};
|
|
5049
5101
|
if (typeof row?.directory === "string" && row.directory.trim()) {
|
|
5050
5102
|
data.path = { cwd: row.directory.trim() };
|
|
@@ -5057,12 +5109,31 @@ function readZcodeNativeUsageMessages(dbPath, sqliteOptions = {}) {
|
|
|
5057
5109
|
|
|
5058
5110
|
// Read only genuine ZCode assistant messages (its own GLM models via Z.ai /
|
|
5059
5111
|
// BigModel), dropping any bundled sub-agent turns. See isZcodeNativeMessage.
|
|
5112
|
+
//
|
|
5113
|
+
// ZCode started writing model_usage after many installations had already
|
|
5114
|
+
// accumulated months of history in the OpenCode message tables. The native
|
|
5115
|
+
// table is authoritative from its first completed row onward, but it is not a
|
|
5116
|
+
// historical backfill. Keep legacy rows before that boundary so merely adding
|
|
5117
|
+
// the new table cannot make older usage disappear from TokenTracker.
|
|
5060
5118
|
function readZcodeDbMessages(dbPath, sqliteOptions = {}) {
|
|
5061
5119
|
if (!dbPath || !fssync.existsSync(dbPath)) return [];
|
|
5062
5120
|
const nativeMessages = readZcodeNativeUsageMessages(dbPath, sqliteOptions);
|
|
5063
|
-
|
|
5064
|
-
|
|
5065
|
-
|
|
5121
|
+
const legacyMessages = readOpencodeDbMessages(dbPath, sqliteOptions)
|
|
5122
|
+
.filter((message) => isZcodeNativeMessage(message.data))
|
|
5123
|
+
.map(normalizeZcodeLegacyMessage);
|
|
5124
|
+
if (nativeMessages === null) return legacyMessages;
|
|
5125
|
+
|
|
5126
|
+
const nativeStartMs = nativeMessages.reduce((earliest, message) => {
|
|
5127
|
+
const timestampMs = coerceEpochMs(message?.timeUpdated);
|
|
5128
|
+
return timestampMs > 0 ? Math.min(earliest, timestampMs) : earliest;
|
|
5129
|
+
}, Number.POSITIVE_INFINITY);
|
|
5130
|
+
if (!Number.isFinite(nativeStartMs)) return legacyMessages;
|
|
5131
|
+
|
|
5132
|
+
const historicalMessages = legacyMessages.filter((message) => {
|
|
5133
|
+
const timestampMs = coerceEpochMs(message?.timeUpdated);
|
|
5134
|
+
return timestampMs > 0 && timestampMs < nativeStartMs;
|
|
5135
|
+
});
|
|
5136
|
+
return [...historicalMessages, ...nativeMessages];
|
|
5066
5137
|
}
|
|
5067
5138
|
|
|
5068
5139
|
async function parseOpencodeDbIncremental({
|
|
@@ -11132,6 +11203,955 @@ async function parseZedIncremental({
|
|
|
11132
11203
|
return { recordsProcessed, eventsAggregated, bucketsQueued };
|
|
11133
11204
|
}
|
|
11134
11205
|
|
|
11206
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11207
|
+
// LM Studio
|
|
11208
|
+
//
|
|
11209
|
+
// Data: pretty-printed OpenAI-compatible final responses beneath
|
|
11210
|
+
// `~/.lmstudio/server-logs/`. The reader recognizes both Chat Completions and
|
|
11211
|
+
// Responses API usage shapes. It scans only response identity, model, timestamp,
|
|
11212
|
+
// and the balanced `usage` object; prompt and response bodies are never parsed
|
|
11213
|
+
// or persisted.
|
|
11214
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11215
|
+
|
|
11216
|
+
const LMSTUDIO_SOURCE = "lmstudio";
|
|
11217
|
+
const LMSTUDIO_WINDOW_BYTES = 8 * 1024 * 1024;
|
|
11218
|
+
const LMSTUDIO_CHUNK_BYTES = 64 * 1024;
|
|
11219
|
+
const LMSTUDIO_IDENTITY_OVERLAP_BYTES = 512;
|
|
11220
|
+
const LMSTUDIO_MESSAGE_LIMIT = 10_000;
|
|
11221
|
+
const LMSTUDIO_USAGE_MARKER = Buffer.from('"usage"');
|
|
11222
|
+
|
|
11223
|
+
function resolveLmstudioHome(env = process.env) {
|
|
11224
|
+
const override = typeof env.TOKENTRACKER_LMSTUDIO_HOME === "string"
|
|
11225
|
+
? env.TOKENTRACKER_LMSTUDIO_HOME.trim()
|
|
11226
|
+
: "";
|
|
11227
|
+
if (override) return path.resolve(override);
|
|
11228
|
+
const nativeHome = typeof env.LM_STUDIO_HOME === "string"
|
|
11229
|
+
? env.LM_STUDIO_HOME.trim()
|
|
11230
|
+
: "";
|
|
11231
|
+
if (nativeHome) return path.resolve(nativeHome);
|
|
11232
|
+
return path.join(env.HOME || env.USERPROFILE || os.homedir(), ".lmstudio");
|
|
11233
|
+
}
|
|
11234
|
+
|
|
11235
|
+
async function resolveLmstudioLogFiles(env = process.env) {
|
|
11236
|
+
const root = path.join(resolveLmstudioHome(env), "server-logs");
|
|
11237
|
+
const files = [];
|
|
11238
|
+
const pending = [root];
|
|
11239
|
+
while (pending.length > 0) {
|
|
11240
|
+
const dir = pending.pop();
|
|
11241
|
+
let entries;
|
|
11242
|
+
try {
|
|
11243
|
+
entries = await fs.readdir(dir, { withFileTypes: true });
|
|
11244
|
+
} catch (_e) {
|
|
11245
|
+
continue;
|
|
11246
|
+
}
|
|
11247
|
+
for (const entry of entries) {
|
|
11248
|
+
const fullPath = path.join(dir, entry.name);
|
|
11249
|
+
if (entry.isDirectory()) pending.push(fullPath);
|
|
11250
|
+
else if (entry.isFile() && entry.name.toLowerCase().endsWith(".log")) files.push(fullPath);
|
|
11251
|
+
}
|
|
11252
|
+
}
|
|
11253
|
+
return files.sort((a, b) => a.localeCompare(b));
|
|
11254
|
+
}
|
|
11255
|
+
|
|
11256
|
+
function skipLmstudioWhitespace(buffer, index) {
|
|
11257
|
+
while (index < buffer.length && /\s/.test(String.fromCharCode(buffer[index]))) index += 1;
|
|
11258
|
+
return index;
|
|
11259
|
+
}
|
|
11260
|
+
|
|
11261
|
+
function scanLmstudioUsageObjectStart(buffer, from = 0) {
|
|
11262
|
+
let cursor = Math.max(0, from);
|
|
11263
|
+
while (cursor < buffer.length) {
|
|
11264
|
+
const marker = buffer.indexOf(LMSTUDIO_USAGE_MARKER, cursor);
|
|
11265
|
+
if (marker < 0) break;
|
|
11266
|
+
cursor = marker + LMSTUDIO_USAGE_MARKER.length;
|
|
11267
|
+
if (marker > 0 && buffer[marker - 1] === 0x5c) continue;
|
|
11268
|
+
const colon = skipLmstudioWhitespace(buffer, cursor);
|
|
11269
|
+
if (colon >= buffer.length) return { found: null, certainTo: marker };
|
|
11270
|
+
if (buffer[colon] !== 0x3a) continue;
|
|
11271
|
+
const brace = skipLmstudioWhitespace(buffer, colon + 1);
|
|
11272
|
+
if (brace >= buffer.length) return { found: null, certainTo: marker };
|
|
11273
|
+
if (buffer[brace] === 0x7b) return { found: { marker, objectStart: brace }, certainTo: marker };
|
|
11274
|
+
}
|
|
11275
|
+
return {
|
|
11276
|
+
found: null,
|
|
11277
|
+
certainTo: Math.max(0, buffer.length - Math.max(0, LMSTUDIO_USAGE_MARKER.length - 1)),
|
|
11278
|
+
};
|
|
11279
|
+
}
|
|
11280
|
+
|
|
11281
|
+
function lmstudioBalancedObjectEnd(buffer, start) {
|
|
11282
|
+
if (buffer[start] !== 0x7b) return -1;
|
|
11283
|
+
let depth = 0;
|
|
11284
|
+
let inString = false;
|
|
11285
|
+
let escaped = false;
|
|
11286
|
+
for (let index = start; index < buffer.length; index++) {
|
|
11287
|
+
const byte = buffer[index];
|
|
11288
|
+
if (inString) {
|
|
11289
|
+
if (escaped) escaped = false;
|
|
11290
|
+
else if (byte === 0x5c) escaped = true;
|
|
11291
|
+
else if (byte === 0x22) inString = false;
|
|
11292
|
+
continue;
|
|
11293
|
+
}
|
|
11294
|
+
if (byte === 0x22) inString = true;
|
|
11295
|
+
else if (byte === 0x7b) depth += 1;
|
|
11296
|
+
else if (byte === 0x7d) {
|
|
11297
|
+
depth -= 1;
|
|
11298
|
+
if (depth === 0) return index + 1;
|
|
11299
|
+
}
|
|
11300
|
+
}
|
|
11301
|
+
return -1;
|
|
11302
|
+
}
|
|
11303
|
+
|
|
11304
|
+
function lmstudioJsonStringEnd(buffer, start) {
|
|
11305
|
+
if (buffer[start] !== 0x22) return -1;
|
|
11306
|
+
let escaped = false;
|
|
11307
|
+
for (let index = start + 1; index < buffer.length; index++) {
|
|
11308
|
+
const byte = buffer[index];
|
|
11309
|
+
if (escaped) escaped = false;
|
|
11310
|
+
else if (byte === 0x5c) escaped = true;
|
|
11311
|
+
else if (byte === 0x22) return index + 1;
|
|
11312
|
+
}
|
|
11313
|
+
return -1;
|
|
11314
|
+
}
|
|
11315
|
+
|
|
11316
|
+
function lastLmstudioJsonStringField(buffer, field) {
|
|
11317
|
+
const markerBuffer = Buffer.from(`"${field}"`);
|
|
11318
|
+
let cursor = 0;
|
|
11319
|
+
let found = null;
|
|
11320
|
+
while (cursor < buffer.length) {
|
|
11321
|
+
const index = buffer.indexOf(markerBuffer, cursor);
|
|
11322
|
+
if (index < 0) break;
|
|
11323
|
+
cursor = index + markerBuffer.length;
|
|
11324
|
+
if (index > 0 && buffer[index - 1] === 0x5c) continue;
|
|
11325
|
+
let valueStart = skipLmstudioWhitespace(buffer, cursor);
|
|
11326
|
+
if (buffer[valueStart] !== 0x3a) continue;
|
|
11327
|
+
valueStart = skipLmstudioWhitespace(buffer, valueStart + 1);
|
|
11328
|
+
const valueEnd = lmstudioJsonStringEnd(buffer, valueStart);
|
|
11329
|
+
if (valueEnd < 0) continue;
|
|
11330
|
+
try {
|
|
11331
|
+
const value = JSON.parse(buffer.subarray(valueStart, valueEnd).toString("utf8"));
|
|
11332
|
+
if (typeof value === "string") found = value;
|
|
11333
|
+
} catch (_e) { }
|
|
11334
|
+
}
|
|
11335
|
+
return found;
|
|
11336
|
+
}
|
|
11337
|
+
|
|
11338
|
+
function lastLmstudioTimestamp(buffer) {
|
|
11339
|
+
const text = buffer.toString("utf8");
|
|
11340
|
+
let timestamp = null;
|
|
11341
|
+
for (const rawLine of text.split("\n")) {
|
|
11342
|
+
const line = rawLine.startsWith("\r") ? rawLine.slice(1) : rawLine;
|
|
11343
|
+
const match = /^\[(\d{4})-(\d{2})-(\d{2}) (\d{2}):(\d{2}):(\d{2})\]\[/.exec(line);
|
|
11344
|
+
if (!match) continue;
|
|
11345
|
+
const date = new Date(
|
|
11346
|
+
Number(match[1]),
|
|
11347
|
+
Number(match[2]) - 1,
|
|
11348
|
+
Number(match[3]),
|
|
11349
|
+
Number(match[4]),
|
|
11350
|
+
Number(match[5]),
|
|
11351
|
+
Number(match[6]),
|
|
11352
|
+
);
|
|
11353
|
+
if (!Number.isNaN(date.getTime())) timestamp = date.getTime();
|
|
11354
|
+
}
|
|
11355
|
+
return timestamp;
|
|
11356
|
+
}
|
|
11357
|
+
|
|
11358
|
+
function normalizeLocalStudioTokens(usage) {
|
|
11359
|
+
if (!usage || typeof usage !== "object") return null;
|
|
11360
|
+
const prompt = toNonNegativeInt(
|
|
11361
|
+
usage.prompt_tokens ?? usage.promptTokens ?? usage.input_tokens ?? usage.inputTokens,
|
|
11362
|
+
);
|
|
11363
|
+
const completion = toNonNegativeInt(
|
|
11364
|
+
usage.completion_tokens ?? usage.completionTokens ?? usage.output_tokens ?? usage.outputTokens,
|
|
11365
|
+
);
|
|
11366
|
+
const total = Math.max(
|
|
11367
|
+
toNonNegativeInt(usage.total_tokens ?? usage.totalTokens),
|
|
11368
|
+
prompt + completion,
|
|
11369
|
+
);
|
|
11370
|
+
if (total <= 0) return null;
|
|
11371
|
+
const promptDetails = usage.prompt_tokens_details
|
|
11372
|
+
|| usage.input_tokens_details
|
|
11373
|
+
|| usage.inputTokensDetails
|
|
11374
|
+
|| {};
|
|
11375
|
+
const outputDetails = usage.completion_tokens_details
|
|
11376
|
+
|| usage.output_tokens_details
|
|
11377
|
+
|| usage.completionTokensDetails
|
|
11378
|
+
|| usage.outputTokensDetails
|
|
11379
|
+
|| {};
|
|
11380
|
+
const cacheRead = Math.min(
|
|
11381
|
+
prompt,
|
|
11382
|
+
Math.max(
|
|
11383
|
+
toNonNegativeInt(promptDetails.cached_tokens ?? promptDetails.cache_read_tokens),
|
|
11384
|
+
toNonNegativeInt(usage.cached_tokens),
|
|
11385
|
+
),
|
|
11386
|
+
);
|
|
11387
|
+
const cacheWrite = Math.min(
|
|
11388
|
+
Math.max(0, prompt - cacheRead),
|
|
11389
|
+
Math.max(
|
|
11390
|
+
toNonNegativeInt(
|
|
11391
|
+
promptDetails.cache_creation_input_tokens ?? promptDetails.cache_write_tokens,
|
|
11392
|
+
),
|
|
11393
|
+
toNonNegativeInt(usage.cache_creation_input_tokens),
|
|
11394
|
+
),
|
|
11395
|
+
);
|
|
11396
|
+
const reasoning = Math.min(
|
|
11397
|
+
completion,
|
|
11398
|
+
Math.max(
|
|
11399
|
+
toNonNegativeInt(outputDetails.reasoning_tokens),
|
|
11400
|
+
toNonNegativeInt(usage.reasoning_tokens),
|
|
11401
|
+
),
|
|
11402
|
+
);
|
|
11403
|
+
return {
|
|
11404
|
+
input_tokens: Math.max(0, total - completion - cacheRead - cacheWrite),
|
|
11405
|
+
cached_input_tokens: cacheRead,
|
|
11406
|
+
cache_creation_input_tokens: cacheWrite,
|
|
11407
|
+
output_tokens: Math.max(0, completion - reasoning),
|
|
11408
|
+
reasoning_output_tokens: reasoning,
|
|
11409
|
+
total_tokens: total,
|
|
11410
|
+
billable_total_tokens: total,
|
|
11411
|
+
total_cost_usd: 0,
|
|
11412
|
+
conversation_count: 1,
|
|
11413
|
+
};
|
|
11414
|
+
}
|
|
11415
|
+
|
|
11416
|
+
function lmstudioResponseId(buffer) {
|
|
11417
|
+
const id = lastLmstudioJsonStringField(buffer, "id");
|
|
11418
|
+
return typeof id === "string" && ["chatcmpl-", "cmpl-", "resp_"].some((prefix) => id.startsWith(prefix))
|
|
11419
|
+
? id
|
|
11420
|
+
: null;
|
|
11421
|
+
}
|
|
11422
|
+
|
|
11423
|
+
function absorbLmstudioIdentity(identity, buffer) {
|
|
11424
|
+
const responseId = lmstudioResponseId(buffer);
|
|
11425
|
+
const model = lastLmstudioJsonStringField(buffer, "model");
|
|
11426
|
+
const timestamp = lastLmstudioTimestamp(buffer);
|
|
11427
|
+
if (responseId) identity.responseId = responseId;
|
|
11428
|
+
if (model && model.trim()) identity.model = model.trim();
|
|
11429
|
+
if (timestamp) identity.timestamp = timestamp;
|
|
11430
|
+
}
|
|
11431
|
+
|
|
11432
|
+
function normalizeLmstudioResumeIdentity(value) {
|
|
11433
|
+
const identity = {};
|
|
11434
|
+
const responseId = typeof value?.responseId === "string" ? value.responseId : "";
|
|
11435
|
+
if (["chatcmpl-", "cmpl-", "resp_"].some((prefix) => responseId.startsWith(prefix))) {
|
|
11436
|
+
identity.responseId = responseId;
|
|
11437
|
+
}
|
|
11438
|
+
const model = typeof value?.model === "string" ? value.model.trim() : "";
|
|
11439
|
+
if (model) identity.model = model;
|
|
11440
|
+
const timestamp = Number(value?.timestamp);
|
|
11441
|
+
if (Number.isFinite(timestamp) && timestamp > 0) identity.timestamp = timestamp;
|
|
11442
|
+
return identity;
|
|
11443
|
+
}
|
|
11444
|
+
|
|
11445
|
+
function lmstudioRecordFromSlices({ usageBuffer, metadataBuffer, carried, filePath, marker, fallbackTimestamp }) {
|
|
11446
|
+
let usage;
|
|
11447
|
+
try {
|
|
11448
|
+
usage = JSON.parse(usageBuffer.toString("utf8"));
|
|
11449
|
+
} catch (_e) {
|
|
11450
|
+
return null;
|
|
11451
|
+
}
|
|
11452
|
+
const totals = normalizeLocalStudioTokens(usage);
|
|
11453
|
+
if (!totals) return null;
|
|
11454
|
+
const responseId = lmstudioResponseId(metadataBuffer) || carried.responseId || null;
|
|
11455
|
+
const model = normalizeModelInput(
|
|
11456
|
+
lastLmstudioJsonStringField(metadataBuffer, "model") || carried.model,
|
|
11457
|
+
) || DEFAULT_MODEL;
|
|
11458
|
+
const timestampMs = lastLmstudioTimestamp(metadataBuffer) || carried.timestamp || fallbackTimestamp;
|
|
11459
|
+
const bucketStart = timestampMs
|
|
11460
|
+
? toUtcHalfHourStart(new Date(timestampMs).toISOString())
|
|
11461
|
+
: null;
|
|
11462
|
+
if (!bucketStart) return null;
|
|
11463
|
+
const fallback = crypto.createHash("sha256")
|
|
11464
|
+
.update(filePath)
|
|
11465
|
+
.update("\0")
|
|
11466
|
+
.update(String(marker))
|
|
11467
|
+
.update("\0")
|
|
11468
|
+
.update(model)
|
|
11469
|
+
.update("\0")
|
|
11470
|
+
.update(totalsKey(totals))
|
|
11471
|
+
.digest("base64url");
|
|
11472
|
+
return {
|
|
11473
|
+
key: responseId ? `lmstudio:${responseId}` : `lmstudio:${fallback}`,
|
|
11474
|
+
model,
|
|
11475
|
+
bucketStart,
|
|
11476
|
+
totals,
|
|
11477
|
+
marker,
|
|
11478
|
+
};
|
|
11479
|
+
}
|
|
11480
|
+
|
|
11481
|
+
async function readLmstudioFileRecords(
|
|
11482
|
+
filePath,
|
|
11483
|
+
{
|
|
11484
|
+
windowBytes = LMSTUDIO_WINDOW_BYTES,
|
|
11485
|
+
chunkBytes = LMSTUDIO_CHUNK_BYTES,
|
|
11486
|
+
startOffset = 0,
|
|
11487
|
+
resumeIdentity,
|
|
11488
|
+
} = {},
|
|
11489
|
+
) {
|
|
11490
|
+
const handle = await fs.open(filePath, "r");
|
|
11491
|
+
try {
|
|
11492
|
+
const initialStat = await handle.stat();
|
|
11493
|
+
const fallbackTimestamp = initialStat.mtimeMs;
|
|
11494
|
+
const records = [];
|
|
11495
|
+
const initialOffset = Math.min(
|
|
11496
|
+
initialStat.size,
|
|
11497
|
+
Math.max(0, Number.isFinite(startOffset) ? Math.floor(startOffset) : 0),
|
|
11498
|
+
);
|
|
11499
|
+
let window = Buffer.alloc(0);
|
|
11500
|
+
let windowStart = initialOffset;
|
|
11501
|
+
let metadataStart = initialOffset;
|
|
11502
|
+
let scannedTo = initialOffset;
|
|
11503
|
+
let position = initialOffset;
|
|
11504
|
+
let carried = normalizeLmstudioResumeIdentity(resumeIdentity);
|
|
11505
|
+
const chunk = Buffer.alloc(Math.max(1, chunkBytes));
|
|
11506
|
+
|
|
11507
|
+
while (true) {
|
|
11508
|
+
const { bytesRead } = await handle.read(chunk, 0, chunk.length, position);
|
|
11509
|
+
if (bytesRead > 0) {
|
|
11510
|
+
window = Buffer.concat([window, chunk.subarray(0, bytesRead)]);
|
|
11511
|
+
position += bytesRead;
|
|
11512
|
+
}
|
|
11513
|
+
const atEof = bytesRead === 0;
|
|
11514
|
+
|
|
11515
|
+
while (true) {
|
|
11516
|
+
const scanFrom = Math.min(window.length, Math.max(0, scannedTo - windowStart));
|
|
11517
|
+
const scan = scanLmstudioUsageObjectStart(window, scanFrom);
|
|
11518
|
+
if (!scan.found) {
|
|
11519
|
+
scannedTo = windowStart + scan.certainTo;
|
|
11520
|
+
break;
|
|
11521
|
+
}
|
|
11522
|
+
const objectEnd = lmstudioBalancedObjectEnd(window, scan.found.objectStart);
|
|
11523
|
+
if (objectEnd < 0) {
|
|
11524
|
+
scannedTo = atEof
|
|
11525
|
+
? windowStart + window.length
|
|
11526
|
+
: windowStart + scan.found.marker;
|
|
11527
|
+
break;
|
|
11528
|
+
}
|
|
11529
|
+
scannedTo = windowStart + objectEnd;
|
|
11530
|
+
const absoluteMarker = windowStart + scan.found.marker;
|
|
11531
|
+
const absoluteEnd = windowStart + objectEnd;
|
|
11532
|
+
const metadataFrom = Math.min(
|
|
11533
|
+
scan.found.marker,
|
|
11534
|
+
Math.max(0, metadataStart - windowStart),
|
|
11535
|
+
);
|
|
11536
|
+
const record = lmstudioRecordFromSlices({
|
|
11537
|
+
usageBuffer: window.subarray(scan.found.objectStart, objectEnd),
|
|
11538
|
+
metadataBuffer: window.subarray(metadataFrom, scan.found.marker),
|
|
11539
|
+
carried,
|
|
11540
|
+
filePath,
|
|
11541
|
+
marker: absoluteMarker,
|
|
11542
|
+
fallbackTimestamp,
|
|
11543
|
+
});
|
|
11544
|
+
if (record) records.push(record);
|
|
11545
|
+
metadataStart = absoluteEnd;
|
|
11546
|
+
carried = {};
|
|
11547
|
+
}
|
|
11548
|
+
|
|
11549
|
+
const keepFrom = Math.min(window.length, Math.max(0, metadataStart - windowStart));
|
|
11550
|
+
if (keepFrom > 0) {
|
|
11551
|
+
window = window.subarray(keepFrom);
|
|
11552
|
+
windowStart += keepFrom;
|
|
11553
|
+
}
|
|
11554
|
+
|
|
11555
|
+
if (window.length > windowBytes) {
|
|
11556
|
+
const overflow = window.length - windowBytes;
|
|
11557
|
+
const absorbTo = Math.min(
|
|
11558
|
+
window.length,
|
|
11559
|
+
overflow + LMSTUDIO_IDENTITY_OVERLAP_BYTES,
|
|
11560
|
+
);
|
|
11561
|
+
absorbLmstudioIdentity(carried, window.subarray(0, absorbTo));
|
|
11562
|
+
window = window.subarray(overflow);
|
|
11563
|
+
windowStart += overflow;
|
|
11564
|
+
metadataStart = Math.max(metadataStart, windowStart);
|
|
11565
|
+
scannedTo = Math.max(scannedTo, windowStart);
|
|
11566
|
+
}
|
|
11567
|
+
|
|
11568
|
+
if (atEof) {
|
|
11569
|
+
const finalStat = await handle.stat().catch(() => initialStat);
|
|
11570
|
+
const nextIdentity = normalizeLmstudioResumeIdentity(carried);
|
|
11571
|
+
return {
|
|
11572
|
+
records,
|
|
11573
|
+
stat: {
|
|
11574
|
+
dev: finalStat.dev,
|
|
11575
|
+
ino: finalStat.ino,
|
|
11576
|
+
size: finalStat.size,
|
|
11577
|
+
mtimeMs: finalStat.size === position ? finalStat.mtimeMs : -1,
|
|
11578
|
+
resumeOffset: Math.max(metadataStart, position - windowBytes),
|
|
11579
|
+
...(Object.keys(nextIdentity).length > 0
|
|
11580
|
+
? { resumeIdentity: nextIdentity }
|
|
11581
|
+
: {}),
|
|
11582
|
+
},
|
|
11583
|
+
};
|
|
11584
|
+
}
|
|
11585
|
+
}
|
|
11586
|
+
} finally {
|
|
11587
|
+
await handle.close().catch(() => {});
|
|
11588
|
+
}
|
|
11589
|
+
}
|
|
11590
|
+
|
|
11591
|
+
function retainNewestPassiveMessages(messages, maxEntries) {
|
|
11592
|
+
const limit = Number.isInteger(maxEntries) && maxEntries > 0
|
|
11593
|
+
? maxEntries
|
|
11594
|
+
: LMSTUDIO_MESSAGE_LIMIT;
|
|
11595
|
+
const entries = Object.entries(messages);
|
|
11596
|
+
if (entries.length <= limit) return messages;
|
|
11597
|
+
entries.sort((left, right) => {
|
|
11598
|
+
const leftTime = Date.parse(left[1]?.updatedAt || left[1]?.bucketStart || "") || 0;
|
|
11599
|
+
const rightTime = Date.parse(right[1]?.updatedAt || right[1]?.bucketStart || "") || 0;
|
|
11600
|
+
if (leftTime !== rightTime) return rightTime - leftTime;
|
|
11601
|
+
return right[0].localeCompare(left[0]);
|
|
11602
|
+
});
|
|
11603
|
+
return Object.fromEntries(entries.slice(0, limit));
|
|
11604
|
+
}
|
|
11605
|
+
|
|
11606
|
+
function reconcilePassiveUsageEvent({ event, source, messages, hourlyState, touchedBuckets }) {
|
|
11607
|
+
const previous = messages[event.key];
|
|
11608
|
+
const unchanged = previous
|
|
11609
|
+
&& previous.model === event.model
|
|
11610
|
+
&& previous.bucketStart === event.bucketStart
|
|
11611
|
+
&& totalsKey(previous.totals) === totalsKey(event.totals);
|
|
11612
|
+
if (unchanged) return false;
|
|
11613
|
+
|
|
11614
|
+
if (previous?.model && previous?.bucketStart && previous?.totals) {
|
|
11615
|
+
const oldBucket = getHourlyBucket(hourlyState, source, previous.model, previous.bucketStart);
|
|
11616
|
+
subtractTotals(oldBucket.totals, previous.totals);
|
|
11617
|
+
touchedBuckets.add(bucketKey(source, previous.model, previous.bucketStart));
|
|
11618
|
+
}
|
|
11619
|
+
const bucket = getHourlyBucket(hourlyState, source, event.model, event.bucketStart);
|
|
11620
|
+
addTotals(bucket.totals, event.totals);
|
|
11621
|
+
touchedBuckets.add(bucketKey(source, event.model, event.bucketStart));
|
|
11622
|
+
messages[event.key] = {
|
|
11623
|
+
model: event.model,
|
|
11624
|
+
bucketStart: event.bucketStart,
|
|
11625
|
+
totals: event.totals,
|
|
11626
|
+
updatedAt: new Date().toISOString(),
|
|
11627
|
+
};
|
|
11628
|
+
return true;
|
|
11629
|
+
}
|
|
11630
|
+
|
|
11631
|
+
async function parseLmstudioIncremental({
|
|
11632
|
+
logFiles,
|
|
11633
|
+
cursors,
|
|
11634
|
+
queuePath,
|
|
11635
|
+
onProgress,
|
|
11636
|
+
messageLimit = LMSTUDIO_MESSAGE_LIMIT,
|
|
11637
|
+
readerOptions,
|
|
11638
|
+
} = {}) {
|
|
11639
|
+
await ensureDir(path.dirname(queuePath));
|
|
11640
|
+
const files = Array.isArray(logFiles) ? [...logFiles].sort((a, b) => a.localeCompare(b)) : [];
|
|
11641
|
+
const priorState = cursors.lmstudio && typeof cursors.lmstudio === "object"
|
|
11642
|
+
? cursors.lmstudio
|
|
11643
|
+
: {};
|
|
11644
|
+
let fileState = priorState.files && typeof priorState.files === "object"
|
|
11645
|
+
? { ...priorState.files }
|
|
11646
|
+
: {};
|
|
11647
|
+
let messages = priorState.messages && typeof priorState.messages === "object"
|
|
11648
|
+
? priorState.messages
|
|
11649
|
+
: {};
|
|
11650
|
+
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
11651
|
+
const touchedBuckets = new Set();
|
|
11652
|
+
const presentFiles = new Set(files);
|
|
11653
|
+
const cb = typeof onProgress === "function" ? onProgress : null;
|
|
11654
|
+
const effectiveMessageLimit = Number.isInteger(messageLimit) && messageLimit > 0
|
|
11655
|
+
? messageLimit
|
|
11656
|
+
: LMSTUDIO_MESSAGE_LIMIT;
|
|
11657
|
+
let recordsProcessed = 0;
|
|
11658
|
+
let eventsAggregated = 0;
|
|
11659
|
+
let stateChanged = false;
|
|
11660
|
+
const readOptions = readerOptions && typeof readerOptions === "object" ? readerOptions : {};
|
|
11661
|
+
const filePlans = [];
|
|
11662
|
+
let allFilesReadable = true;
|
|
11663
|
+
|
|
11664
|
+
for (const filePath of files) {
|
|
11665
|
+
const previousFile = fileState[filePath];
|
|
11666
|
+
let stat;
|
|
11667
|
+
try {
|
|
11668
|
+
stat = await fs.stat(filePath);
|
|
11669
|
+
} catch (_e) {
|
|
11670
|
+
allFilesReadable = false;
|
|
11671
|
+
continue;
|
|
11672
|
+
}
|
|
11673
|
+
const unchanged = Boolean(previousFile
|
|
11674
|
+
&& previousFile.dev === stat.dev
|
|
11675
|
+
&& previousFile.ino === stat.ino
|
|
11676
|
+
&& previousFile.size === stat.size
|
|
11677
|
+
&& previousFile.mtimeMs === stat.mtimeMs
|
|
11678
|
+
&& Number.isFinite(previousFile.resumeOffset));
|
|
11679
|
+
const canResume = Boolean(previousFile
|
|
11680
|
+
&& previousFile.dev === stat.dev
|
|
11681
|
+
&& previousFile.ino === stat.ino
|
|
11682
|
+
&& (stat.size > previousFile.size || previousFile.mtimeMs === -1)
|
|
11683
|
+
&& Number.isFinite(previousFile.resumeOffset)
|
|
11684
|
+
&& previousFile.resumeOffset >= 0
|
|
11685
|
+
&& previousFile.resumeOffset <= stat.size);
|
|
11686
|
+
filePlans.push({ filePath, previousFile, unchanged, canResume });
|
|
11687
|
+
}
|
|
11688
|
+
|
|
11689
|
+
const requiresRebuild = filePlans.some(
|
|
11690
|
+
({ previousFile, unchanged, canResume }) => previousFile && !unchanged && !canResume,
|
|
11691
|
+
);
|
|
11692
|
+
if (requiresRebuild) {
|
|
11693
|
+
// Bounded message retention cannot safely deduplicate a replay from byte
|
|
11694
|
+
// zero. Rebuild every current LM Studio log in isolation, then replace only
|
|
11695
|
+
// this source's buckets while preserving their queue fingerprints.
|
|
11696
|
+
if (!allFilesReadable || filePlans.length !== files.length) {
|
|
11697
|
+
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
11698
|
+
}
|
|
11699
|
+
const rebuiltHourly = normalizeHourlyState(null);
|
|
11700
|
+
const rebuiltMessages = {};
|
|
11701
|
+
const rebuiltFiles = {};
|
|
11702
|
+
const rebuiltTouched = new Set();
|
|
11703
|
+
let rebuiltRecords = 0;
|
|
11704
|
+
let rebuiltEvents = 0;
|
|
11705
|
+
try {
|
|
11706
|
+
for (let index = 0; index < filePlans.length; index++) {
|
|
11707
|
+
const { filePath } = filePlans[index];
|
|
11708
|
+
const parsed = await readLmstudioFileRecords(filePath, {
|
|
11709
|
+
...readOptions,
|
|
11710
|
+
startOffset: 0,
|
|
11711
|
+
resumeIdentity: undefined,
|
|
11712
|
+
});
|
|
11713
|
+
for (const event of parsed.records) {
|
|
11714
|
+
if (reconcilePassiveUsageEvent({
|
|
11715
|
+
event,
|
|
11716
|
+
source: LMSTUDIO_SOURCE,
|
|
11717
|
+
messages: rebuiltMessages,
|
|
11718
|
+
hourlyState: rebuiltHourly,
|
|
11719
|
+
touchedBuckets: rebuiltTouched,
|
|
11720
|
+
})) rebuiltEvents += 1;
|
|
11721
|
+
}
|
|
11722
|
+
rebuiltFiles[filePath] = { ...parsed.stat, updatedAt: new Date().toISOString() };
|
|
11723
|
+
rebuiltRecords += 1;
|
|
11724
|
+
if (cb) {
|
|
11725
|
+
cb({
|
|
11726
|
+
index: index + 1,
|
|
11727
|
+
total: files.length,
|
|
11728
|
+
recordsProcessed: rebuiltRecords,
|
|
11729
|
+
eventsAggregated: rebuiltEvents,
|
|
11730
|
+
bucketsQueued: rebuiltTouched.size,
|
|
11731
|
+
});
|
|
11732
|
+
}
|
|
11733
|
+
}
|
|
11734
|
+
} catch (error) {
|
|
11735
|
+
if (process.env.TOKENTRACKER_DEBUG) {
|
|
11736
|
+
process.stderr.write(`[lmstudio] rebuild deferred: ${error?.message || error}\n`);
|
|
11737
|
+
}
|
|
11738
|
+
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
11739
|
+
}
|
|
11740
|
+
|
|
11741
|
+
for (const [key, bucket] of Object.entries(hourlyState.buckets || {})) {
|
|
11742
|
+
if (parseBucketKey(key).source !== LMSTUDIO_SOURCE || !bucket?.totals) continue;
|
|
11743
|
+
bucket.totals = initTotals();
|
|
11744
|
+
touchedBuckets.add(key);
|
|
11745
|
+
}
|
|
11746
|
+
for (const [key, bucket] of Object.entries(rebuiltHourly.buckets || {})) {
|
|
11747
|
+
const current = hourlyState.buckets[key];
|
|
11748
|
+
if (current && typeof current === "object") current.totals = cloneTotals(bucket.totals);
|
|
11749
|
+
else hourlyState.buckets[key] = bucket;
|
|
11750
|
+
touchedBuckets.add(key);
|
|
11751
|
+
}
|
|
11752
|
+
fileState = rebuiltFiles;
|
|
11753
|
+
messages = rebuiltMessages;
|
|
11754
|
+
recordsProcessed = rebuiltRecords;
|
|
11755
|
+
eventsAggregated = rebuiltEvents;
|
|
11756
|
+
stateChanged = true;
|
|
11757
|
+
} else {
|
|
11758
|
+
for (let index = 0; index < filePlans.length; index++) {
|
|
11759
|
+
const { filePath, previousFile, unchanged, canResume } = filePlans[index];
|
|
11760
|
+
if (!unchanged) {
|
|
11761
|
+
try {
|
|
11762
|
+
const parsed = await readLmstudioFileRecords(filePath, {
|
|
11763
|
+
...readOptions,
|
|
11764
|
+
startOffset: canResume ? previousFile.resumeOffset : 0,
|
|
11765
|
+
resumeIdentity: canResume ? previousFile.resumeIdentity : undefined,
|
|
11766
|
+
});
|
|
11767
|
+
for (const event of parsed.records) {
|
|
11768
|
+
if (reconcilePassiveUsageEvent({
|
|
11769
|
+
event,
|
|
11770
|
+
source: LMSTUDIO_SOURCE,
|
|
11771
|
+
messages,
|
|
11772
|
+
hourlyState,
|
|
11773
|
+
touchedBuckets,
|
|
11774
|
+
})) eventsAggregated += 1;
|
|
11775
|
+
}
|
|
11776
|
+
fileState[filePath] = { ...parsed.stat, updatedAt: new Date().toISOString() };
|
|
11777
|
+
recordsProcessed += 1;
|
|
11778
|
+
stateChanged = true;
|
|
11779
|
+
} catch (error) {
|
|
11780
|
+
if (process.env.TOKENTRACKER_DEBUG) {
|
|
11781
|
+
process.stderr.write(`[lmstudio] skipped ${filePath}: ${error?.message || error}\n`);
|
|
11782
|
+
}
|
|
11783
|
+
}
|
|
11784
|
+
}
|
|
11785
|
+
if (cb) {
|
|
11786
|
+
cb({
|
|
11787
|
+
index: index + 1,
|
|
11788
|
+
total: files.length,
|
|
11789
|
+
recordsProcessed,
|
|
11790
|
+
eventsAggregated,
|
|
11791
|
+
bucketsQueued: touchedBuckets.size,
|
|
11792
|
+
});
|
|
11793
|
+
}
|
|
11794
|
+
}
|
|
11795
|
+
}
|
|
11796
|
+
|
|
11797
|
+
for (const filePath of Object.keys(fileState)) {
|
|
11798
|
+
if (!presentFiles.has(filePath)) {
|
|
11799
|
+
delete fileState[filePath];
|
|
11800
|
+
stateChanged = true;
|
|
11801
|
+
}
|
|
11802
|
+
}
|
|
11803
|
+
if (
|
|
11804
|
+
!stateChanged
|
|
11805
|
+
&& touchedBuckets.size === 0
|
|
11806
|
+
&& Object.keys(messages).length <= effectiveMessageLimit
|
|
11807
|
+
) {
|
|
11808
|
+
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
11809
|
+
}
|
|
11810
|
+
const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
|
|
11811
|
+
const updatedAt = new Date().toISOString();
|
|
11812
|
+
hourlyState.updatedAt = updatedAt;
|
|
11813
|
+
cursors.hourly = hourlyState;
|
|
11814
|
+
cursors.lmstudio = {
|
|
11815
|
+
files: fileState,
|
|
11816
|
+
messages: retainNewestPassiveMessages(messages, effectiveMessageLimit),
|
|
11817
|
+
updatedAt,
|
|
11818
|
+
};
|
|
11819
|
+
return { recordsProcessed, eventsAggregated, bucketsQueued };
|
|
11820
|
+
}
|
|
11821
|
+
|
|
11822
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11823
|
+
// Unsloth Studio
|
|
11824
|
+
//
|
|
11825
|
+
// Durable inference usage lives in `studio.db`. The SQL projections below
|
|
11826
|
+
// extract only stable IDs, timestamps, model identifiers, and scalar counters.
|
|
11827
|
+
// Message content, attachments, API subjects, credentials, and training data
|
|
11828
|
+
// never leave SQLite.
|
|
11829
|
+
// ─────────────────────────────────────────────────────────────────────────────
|
|
11830
|
+
|
|
11831
|
+
const UNSLOTH_SOURCE = "unsloth";
|
|
11832
|
+
const UNSLOTH_SQL_PAGE_SIZE = 500;
|
|
11833
|
+
const UNSLOTH_CURSOR_OVERLAP_ROWS = 256;
|
|
11834
|
+
const UNSLOTH_METERED_PROVIDER_TYPES = new Set([
|
|
11835
|
+
"anthropic",
|
|
11836
|
+
"deepseek",
|
|
11837
|
+
"gemini",
|
|
11838
|
+
"huggingface",
|
|
11839
|
+
"kimi",
|
|
11840
|
+
"mistral",
|
|
11841
|
+
"openai",
|
|
11842
|
+
"openrouter",
|
|
11843
|
+
"qwen",
|
|
11844
|
+
]);
|
|
11845
|
+
|
|
11846
|
+
function resolveUnslothDbPath(env = process.env) {
|
|
11847
|
+
const override = typeof env.TOKENTRACKER_UNSLOTH_DB === "string"
|
|
11848
|
+
? env.TOKENTRACKER_UNSLOTH_DB.trim()
|
|
11849
|
+
: "";
|
|
11850
|
+
if (override) return path.resolve(override);
|
|
11851
|
+
const studioHome = typeof env.UNSLOTH_STUDIO_HOME === "string"
|
|
11852
|
+
? env.UNSLOTH_STUDIO_HOME.trim()
|
|
11853
|
+
: "";
|
|
11854
|
+
if (studioHome) return path.join(path.resolve(studioHome), "studio.db");
|
|
11855
|
+
return path.join(env.HOME || env.USERPROFILE || os.homedir(), ".unsloth", "studio", "studio.db");
|
|
11856
|
+
}
|
|
11857
|
+
|
|
11858
|
+
function unslothSqliteFingerprint(dbPath) {
|
|
11859
|
+
const fingerprint = sqliteSidecarFingerprint(dbPath);
|
|
11860
|
+
delete fingerprint["-shm"];
|
|
11861
|
+
return fingerprint;
|
|
11862
|
+
}
|
|
11863
|
+
|
|
11864
|
+
function unslothRowPosition(row) {
|
|
11865
|
+
const createdAt = row?.created_at == null ? "" : String(row.created_at);
|
|
11866
|
+
const id = row?.id == null ? "" : String(row.id);
|
|
11867
|
+
return createdAt && id ? { createdAt, id } : null;
|
|
11868
|
+
}
|
|
11869
|
+
|
|
11870
|
+
function unslothPositionClause(rowAlias, position, inclusive) {
|
|
11871
|
+
if (!position?.createdAt || !position?.id) return null;
|
|
11872
|
+
const createdAt = sqliteStringLiteral(position.createdAt);
|
|
11873
|
+
const id = sqliteStringLiteral(position.id);
|
|
11874
|
+
const idOperator = inclusive ? ">=" : ">";
|
|
11875
|
+
return `(${rowAlias}.created_at > ${createdAt} OR ` +
|
|
11876
|
+
`(${rowAlias}.created_at = ${createdAt} AND ${rowAlias}.id ${idOperator} ${id}))`;
|
|
11877
|
+
}
|
|
11878
|
+
|
|
11879
|
+
function readUnslothRowsPaged({
|
|
11880
|
+
dbPath,
|
|
11881
|
+
select,
|
|
11882
|
+
from,
|
|
11883
|
+
where,
|
|
11884
|
+
rowAlias,
|
|
11885
|
+
lowerBound,
|
|
11886
|
+
options,
|
|
11887
|
+
}) {
|
|
11888
|
+
const requestedPageSize = Number(options?.pageSize);
|
|
11889
|
+
const pageSize = Number.isInteger(requestedPageSize) && requestedPageSize > 0
|
|
11890
|
+
? Math.min(requestedPageSize, UNSLOTH_SQL_PAGE_SIZE)
|
|
11891
|
+
: UNSLOTH_SQL_PAGE_SIZE;
|
|
11892
|
+
const rows = [];
|
|
11893
|
+
let after = null;
|
|
11894
|
+
|
|
11895
|
+
while (true) {
|
|
11896
|
+
const clauses = [where, `${rowAlias}.created_at IS NOT NULL`, `${rowAlias}.id IS NOT NULL`];
|
|
11897
|
+
const positionClause = unslothPositionClause(rowAlias, after || lowerBound, !after);
|
|
11898
|
+
if (positionClause) clauses.push(positionClause);
|
|
11899
|
+
const page = readSqliteJsonRows(dbPath, `
|
|
11900
|
+
SELECT
|
|
11901
|
+
${select}
|
|
11902
|
+
FROM ${from}
|
|
11903
|
+
WHERE ${clauses.filter(Boolean).join(" AND ")}
|
|
11904
|
+
ORDER BY ${rowAlias}.created_at, ${rowAlias}.id
|
|
11905
|
+
LIMIT ${pageSize}
|
|
11906
|
+
`.trim(), options);
|
|
11907
|
+
rows.push(...page);
|
|
11908
|
+
if (page.length < pageSize) break;
|
|
11909
|
+
|
|
11910
|
+
const next = unslothRowPosition(page[page.length - 1]);
|
|
11911
|
+
if (!next || (
|
|
11912
|
+
after && next.createdAt === after.createdAt && next.id === after.id
|
|
11913
|
+
)) {
|
|
11914
|
+
throw new Error("Unsloth Studio pagination did not advance");
|
|
11915
|
+
}
|
|
11916
|
+
after = next;
|
|
11917
|
+
}
|
|
11918
|
+
|
|
11919
|
+
return rows;
|
|
11920
|
+
}
|
|
11921
|
+
|
|
11922
|
+
function readUnslothUsageRows(dbPath, sqliteOptions = {}, scanState = {}) {
|
|
11923
|
+
if (!dbPath || !fssync.existsSync(dbPath)) return [];
|
|
11924
|
+
const options = {
|
|
11925
|
+
label: "Unsloth Studio",
|
|
11926
|
+
maxBuffer: 32 * 1024 * 1024,
|
|
11927
|
+
timeout: 30_000,
|
|
11928
|
+
readOnly: true,
|
|
11929
|
+
throwOnReadFailure: true,
|
|
11930
|
+
...sqliteOptions,
|
|
11931
|
+
};
|
|
11932
|
+
const tables = new Set(
|
|
11933
|
+
readSqliteJsonRows(
|
|
11934
|
+
dbPath,
|
|
11935
|
+
"SELECT name FROM sqlite_master WHERE type='table' AND name IN ('chat_messages','chat_threads','api_usage_events')",
|
|
11936
|
+
options,
|
|
11937
|
+
).map((row) => row?.name).filter(Boolean),
|
|
11938
|
+
);
|
|
11939
|
+
const rows = [];
|
|
11940
|
+
|
|
11941
|
+
if (tables.has("chat_messages")) {
|
|
11942
|
+
const joinThread = tables.has("chat_threads")
|
|
11943
|
+
? "LEFT JOIN chat_threads t ON t.id = m.thread_id"
|
|
11944
|
+
: "";
|
|
11945
|
+
const threadModel = tables.has("chat_threads") ? "t.model_id" : "NULL";
|
|
11946
|
+
const field = (jsonPath, alias) =>
|
|
11947
|
+
`CASE WHEN json_valid(m.metadata_json) THEN json_extract(m.metadata_json, '${jsonPath}') ELSE NULL END AS ${alias}`;
|
|
11948
|
+
// Re-read a bounded tail because Studio finalizes recent rows in place and
|
|
11949
|
+
// does not expose an updated_at column. Older immutable history stays behind
|
|
11950
|
+
// the persisted timestamp/ID overlap cursor.
|
|
11951
|
+
const chatRows = readUnslothRowsPaged({
|
|
11952
|
+
dbPath,
|
|
11953
|
+
select: `
|
|
11954
|
+
'chat' AS usage_kind,
|
|
11955
|
+
m.id,
|
|
11956
|
+
m.created_at,
|
|
11957
|
+
${field("$.responseDetails.responseModelId", "response_model")},
|
|
11958
|
+
${field("$.contextUsage.modelId", "requested_model")},
|
|
11959
|
+
${field("$.responseDetails.providerType", "provider_type")},
|
|
11960
|
+
${threadModel} AS fallback_model,
|
|
11961
|
+
${field("$.contextUsage.promptTokens", "prompt_tokens")},
|
|
11962
|
+
${field("$.contextUsage.completionTokens", "completion_tokens")},
|
|
11963
|
+
${field("$.contextUsage.totalTokens", "total_tokens")},
|
|
11964
|
+
${field("$.contextUsage.cachedTokens", "cached_tokens")},
|
|
11965
|
+
${field("$.contextUsage.cacheWriteTokens", "cache_write_tokens")},
|
|
11966
|
+
${field("$.contextUsage.reasoningTokens", "reasoning_tokens")}
|
|
11967
|
+
`.trim(),
|
|
11968
|
+
from: `chat_messages m ${joinThread}`,
|
|
11969
|
+
where: "m.role = 'assistant'",
|
|
11970
|
+
rowAlias: "m",
|
|
11971
|
+
lowerBound: scanState?.chat?.overlap,
|
|
11972
|
+
options,
|
|
11973
|
+
});
|
|
11974
|
+
rows.push(...chatRows);
|
|
11975
|
+
}
|
|
11976
|
+
|
|
11977
|
+
if (tables.has("api_usage_events")) {
|
|
11978
|
+
const columns = new Set(
|
|
11979
|
+
readSqliteJsonRows(dbPath, "PRAGMA table_info(api_usage_events)", options)
|
|
11980
|
+
.map((row) => row?.name)
|
|
11981
|
+
.filter(Boolean),
|
|
11982
|
+
);
|
|
11983
|
+
const required = ["id", "model", "prompt_tokens", "completion_tokens", "total_tokens", "created_at"];
|
|
11984
|
+
if (required.every((column) => columns.has(column))) {
|
|
11985
|
+
rows.push(...readUnslothRowsPaged({
|
|
11986
|
+
dbPath,
|
|
11987
|
+
select: `
|
|
11988
|
+
'api' AS usage_kind,
|
|
11989
|
+
e.id,
|
|
11990
|
+
e.created_at,
|
|
11991
|
+
e.model AS response_model,
|
|
11992
|
+
e.model AS requested_model,
|
|
11993
|
+
'local' AS provider_type,
|
|
11994
|
+
NULL AS fallback_model,
|
|
11995
|
+
e.prompt_tokens,
|
|
11996
|
+
e.completion_tokens,
|
|
11997
|
+
e.total_tokens,
|
|
11998
|
+
0 AS cached_tokens,
|
|
11999
|
+
0 AS cache_write_tokens,
|
|
12000
|
+
0 AS reasoning_tokens
|
|
12001
|
+
`.trim(),
|
|
12002
|
+
from: "api_usage_events e",
|
|
12003
|
+
where: "",
|
|
12004
|
+
rowAlias: "e",
|
|
12005
|
+
lowerBound: scanState?.api?.overlap,
|
|
12006
|
+
options,
|
|
12007
|
+
}));
|
|
12008
|
+
}
|
|
12009
|
+
}
|
|
12010
|
+
return rows;
|
|
12011
|
+
}
|
|
12012
|
+
|
|
12013
|
+
function qualifyUnslothModel(row) {
|
|
12014
|
+
const rawModel = normalizeModelInput(row?.response_model)
|
|
12015
|
+
|| normalizeModelInput(row?.requested_model)
|
|
12016
|
+
|| normalizeModelInput(row?.fallback_model)
|
|
12017
|
+
|| DEFAULT_MODEL;
|
|
12018
|
+
const providerType = normalizeMessageKeyPart(row?.provider_type).toLowerCase();
|
|
12019
|
+
if (row?.usage_kind === "api" || providerType === "local") {
|
|
12020
|
+
return `local/${rawModel}`;
|
|
12021
|
+
}
|
|
12022
|
+
if (!UNSLOTH_METERED_PROVIDER_TYPES.has(providerType)) {
|
|
12023
|
+
return `unpriced/${providerType || "unknown"}/${rawModel}`;
|
|
12024
|
+
}
|
|
12025
|
+
const lowerModel = rawModel.toLowerCase();
|
|
12026
|
+
return lowerModel.startsWith(`${providerType}/`)
|
|
12027
|
+
? rawModel
|
|
12028
|
+
: `${providerType}/${rawModel}`;
|
|
12029
|
+
}
|
|
12030
|
+
|
|
12031
|
+
function normalizeUnslothUsageRow(row) {
|
|
12032
|
+
const totals = normalizeLocalStudioTokens({
|
|
12033
|
+
prompt_tokens: row?.prompt_tokens,
|
|
12034
|
+
completion_tokens: row?.completion_tokens,
|
|
12035
|
+
total_tokens: row?.total_tokens,
|
|
12036
|
+
cached_tokens: row?.cached_tokens,
|
|
12037
|
+
cache_creation_input_tokens: row?.cache_write_tokens,
|
|
12038
|
+
reasoning_tokens: row?.reasoning_tokens,
|
|
12039
|
+
});
|
|
12040
|
+
const id = normalizeMessageKeyPart(row?.id == null ? "" : String(row.id));
|
|
12041
|
+
const timestampMs = coerceEpochMs(row?.created_at) || parseIsoTimestampMs(row?.created_at);
|
|
12042
|
+
const bucketStart = timestampMs
|
|
12043
|
+
? toUtcHalfHourStart(new Date(timestampMs).toISOString())
|
|
12044
|
+
: null;
|
|
12045
|
+
if (!id || !totals || !bucketStart) return null;
|
|
12046
|
+
const kind = row?.usage_kind === "api" ? "api" : "chat";
|
|
12047
|
+
return {
|
|
12048
|
+
key: `unsloth:${kind}:${id}`,
|
|
12049
|
+
model: qualifyUnslothModel(row),
|
|
12050
|
+
bucketStart,
|
|
12051
|
+
totals,
|
|
12052
|
+
};
|
|
12053
|
+
}
|
|
12054
|
+
|
|
12055
|
+
async function parseUnslothIncremental({
|
|
12056
|
+
dbPath,
|
|
12057
|
+
cursors,
|
|
12058
|
+
queuePath,
|
|
12059
|
+
onProgress,
|
|
12060
|
+
env,
|
|
12061
|
+
sqliteOptions,
|
|
12062
|
+
overlapRows = UNSLOTH_CURSOR_OVERLAP_ROWS,
|
|
12063
|
+
} = {}) {
|
|
12064
|
+
await ensureDir(path.dirname(queuePath));
|
|
12065
|
+
const resolvedDb = dbPath || resolveUnslothDbPath(env || process.env);
|
|
12066
|
+
const priorState = cursors.unsloth && typeof cursors.unsloth === "object"
|
|
12067
|
+
? cursors.unsloth
|
|
12068
|
+
: {};
|
|
12069
|
+
const messages = priorState.messages && typeof priorState.messages === "object"
|
|
12070
|
+
? priorState.messages
|
|
12071
|
+
: {};
|
|
12072
|
+
if (!resolvedDb || !fssync.existsSync(resolvedDb)) {
|
|
12073
|
+
cursors.unsloth = { ...priorState, messages, updatedAt: new Date().toISOString() };
|
|
12074
|
+
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
12075
|
+
}
|
|
12076
|
+
|
|
12077
|
+
const initialFingerprint = unslothSqliteFingerprint(resolvedDb);
|
|
12078
|
+
const hasBoundedScan = priorState.scan
|
|
12079
|
+
&& typeof priorState.scan === "object"
|
|
12080
|
+
&& Object.keys(messages).length <= UNSLOTH_CURSOR_OVERLAP_ROWS * 2;
|
|
12081
|
+
if (hasBoundedScan && sameSqliteFingerprint(initialFingerprint, priorState.fingerprint)) {
|
|
12082
|
+
return { recordsProcessed: 0, eventsAggregated: 0, bucketsQueued: 0 };
|
|
12083
|
+
}
|
|
12084
|
+
|
|
12085
|
+
const rows = readUnslothUsageRows(resolvedDb, sqliteOptions, priorState.scan);
|
|
12086
|
+
const hourlyState = normalizeHourlyState(cursors?.hourly);
|
|
12087
|
+
const touchedBuckets = new Set();
|
|
12088
|
+
const cb = typeof onProgress === "function" ? onProgress : null;
|
|
12089
|
+
let recordsProcessed = 0;
|
|
12090
|
+
let eventsAggregated = 0;
|
|
12091
|
+
for (let index = 0; index < rows.length; index++) {
|
|
12092
|
+
const event = normalizeUnslothUsageRow(rows[index]);
|
|
12093
|
+
recordsProcessed += 1;
|
|
12094
|
+
if (event && reconcilePassiveUsageEvent({
|
|
12095
|
+
event,
|
|
12096
|
+
source: UNSLOTH_SOURCE,
|
|
12097
|
+
messages,
|
|
12098
|
+
hourlyState,
|
|
12099
|
+
touchedBuckets,
|
|
12100
|
+
})) eventsAggregated += 1;
|
|
12101
|
+
if (cb) {
|
|
12102
|
+
cb({
|
|
12103
|
+
index: index + 1,
|
|
12104
|
+
total: rows.length,
|
|
12105
|
+
recordsProcessed,
|
|
12106
|
+
eventsAggregated,
|
|
12107
|
+
bucketsQueued: touchedBuckets.size,
|
|
12108
|
+
});
|
|
12109
|
+
}
|
|
12110
|
+
}
|
|
12111
|
+
|
|
12112
|
+
const retention = Number.isInteger(overlapRows) && overlapRows > 0
|
|
12113
|
+
? Math.min(overlapRows, UNSLOTH_CURSOR_OVERLAP_ROWS)
|
|
12114
|
+
: UNSLOTH_CURSOR_OVERLAP_ROWS;
|
|
12115
|
+
const nextScan = {};
|
|
12116
|
+
const retainedMessageKeys = new Set();
|
|
12117
|
+
for (const kind of ["chat", "api"]) {
|
|
12118
|
+
const kindRows = rows.filter((row) => (row?.usage_kind === "api" ? "api" : "chat") === kind);
|
|
12119
|
+
if (kindRows.length === 0) {
|
|
12120
|
+
if (priorState.scan?.[kind]) nextScan[kind] = priorState.scan[kind];
|
|
12121
|
+
for (const key of Object.keys(messages)) {
|
|
12122
|
+
if (key.startsWith(`unsloth:${kind}:`)) retainedMessageKeys.add(key);
|
|
12123
|
+
}
|
|
12124
|
+
continue;
|
|
12125
|
+
}
|
|
12126
|
+
const tail = kindRows.slice(-retention);
|
|
12127
|
+
const overlap = unslothRowPosition(tail[0]);
|
|
12128
|
+
const latest = unslothRowPosition(kindRows[kindRows.length - 1]);
|
|
12129
|
+
if (overlap && latest) nextScan[kind] = { overlap, latest };
|
|
12130
|
+
for (const row of tail) {
|
|
12131
|
+
const event = normalizeUnslothUsageRow(row);
|
|
12132
|
+
if (event) retainedMessageKeys.add(event.key);
|
|
12133
|
+
}
|
|
12134
|
+
}
|
|
12135
|
+
for (const key of Object.keys(messages)) {
|
|
12136
|
+
if (key.startsWith("unsloth:") && !retainedMessageKeys.has(key)) delete messages[key];
|
|
12137
|
+
}
|
|
12138
|
+
|
|
12139
|
+
const bucketsQueued = await enqueueTouchedBuckets({ queuePath, hourlyState, touchedBuckets });
|
|
12140
|
+
const finalFingerprint = unslothSqliteFingerprint(resolvedDb);
|
|
12141
|
+
const updatedAt = new Date().toISOString();
|
|
12142
|
+
hourlyState.updatedAt = updatedAt;
|
|
12143
|
+
cursors.hourly = hourlyState;
|
|
12144
|
+
cursors.unsloth = {
|
|
12145
|
+
messages,
|
|
12146
|
+
scan: nextScan,
|
|
12147
|
+
fingerprint: sameSqliteFingerprint(initialFingerprint, finalFingerprint)
|
|
12148
|
+
? finalFingerprint
|
|
12149
|
+
: initialFingerprint,
|
|
12150
|
+
updatedAt,
|
|
12151
|
+
};
|
|
12152
|
+
return { recordsProcessed, eventsAggregated, bucketsQueued };
|
|
12153
|
+
}
|
|
12154
|
+
|
|
11135
12155
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
11136
12156
|
// AnythingLLM Desktop (Mintplex Labs)
|
|
11137
12157
|
//
|
|
@@ -14223,6 +15243,13 @@ async function migrateCopilotChatLogRecordDedup({
|
|
|
14223
15243
|
touchedBuckets,
|
|
14224
15244
|
seenIds,
|
|
14225
15245
|
} = {}) {
|
|
15246
|
+
// A v2 cursor can retain an offset for a rotated or deleted OTEL file that
|
|
15247
|
+
// is no longer returned by discovery. Its historical contribution cannot be
|
|
15248
|
+
// reconciled, but the stale offset must not block migration of new files.
|
|
15249
|
+
const availableFiles = new Set(Array.isArray(files) ? files : []);
|
|
15250
|
+
for (const filePath of Object.keys(fileOffsets || {})) {
|
|
15251
|
+
if (!availableFiles.has(filePath)) delete fileOffsets[filePath];
|
|
15252
|
+
}
|
|
14226
15253
|
const trackedFiles = Object.keys(fileOffsets || {}).filter(
|
|
14227
15254
|
(filePath) => Number(fileOffsets[filePath]?.size) > 0,
|
|
14228
15255
|
);
|
|
@@ -14230,11 +15257,6 @@ async function migrateCopilotChatLogRecordDedup({
|
|
|
14230
15257
|
return { applied: true, changed: false };
|
|
14231
15258
|
}
|
|
14232
15259
|
|
|
14233
|
-
const availableFiles = new Set(Array.isArray(files) ? files : []);
|
|
14234
|
-
if (trackedFiles.some((filePath) => !availableFiles.has(filePath))) {
|
|
14235
|
-
return { applied: false, reason: "a previously parsed OTEL file is unavailable" };
|
|
14236
|
-
}
|
|
14237
|
-
|
|
14238
15260
|
const oldSeen = new Set();
|
|
14239
15261
|
const newSeen = new Set();
|
|
14240
15262
|
const oldTotals = new Map();
|
|
@@ -14314,7 +15336,10 @@ async function migrateCopilotChatLogRecordDedup({
|
|
|
14314
15336
|
}
|
|
14315
15337
|
|
|
14316
15338
|
const keys = new Set([...oldTotals.keys(), ...newTotals.keys()]);
|
|
14317
|
-
|
|
15339
|
+
// Only keys that existed in the v2 contribution need coverage validation.
|
|
15340
|
+
// A key that exists only in newTotals is a newly recovered Chat request; its
|
|
15341
|
+
// old contribution is zero, so an absent old bucket is expected.
|
|
15342
|
+
for (const key of oldTotals.keys()) {
|
|
14318
15343
|
const [model, bucketStart] = JSON.parse(key);
|
|
14319
15344
|
const oldUsage = oldTotals.get(key) || initTotals();
|
|
14320
15345
|
if (!copilotTotalsCover(
|
|
@@ -16096,14 +17121,14 @@ async function parseCopilotAppDbIncremental({
|
|
|
16096
17121
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
16097
17122
|
// Grok Build (xAI) — passive reader for ~/.grok/sessions/**/updates.jsonl + signals.json
|
|
16098
17123
|
// Triggered either by full scan in sync or by the SessionEnd hook writing a signal.
|
|
16099
|
-
//
|
|
16100
|
-
//
|
|
16101
|
-
//
|
|
17124
|
+
// turn_completed.usage exposes the reported input/output/cache/reasoning split
|
|
17125
|
+
// and, on current Grok builds, exact server cost ticks. Older/partial sessions
|
|
17126
|
+
// without that event retain the explicitly isolated context-watermark fallback.
|
|
16102
17127
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
16103
17128
|
|
|
16104
17129
|
const GROK_ESTIMATED_INPUT_RATIO = 0.8;
|
|
16105
|
-
//
|
|
16106
|
-
const GROK_CURSOR_VERSION =
|
|
17130
|
+
// v5: split cache creation + reasoning correctly and retain reported cost.
|
|
17131
|
+
const GROK_CURSOR_VERSION = 5;
|
|
16107
17132
|
|
|
16108
17133
|
function resolveGrokBuildHome(env = process.env) {
|
|
16109
17134
|
if (env.TOKENTRACKER_GROK_HOME) return env.TOKENTRACKER_GROK_HOME;
|
|
@@ -16346,37 +17371,13 @@ function canonicalizeGrokUsageModel(model) {
|
|
|
16346
17371
|
}
|
|
16347
17372
|
|
|
16348
17373
|
function normalizeGrokTurnUsage(usage, model, timestamp, eventId) {
|
|
16349
|
-
|
|
16350
|
-
|
|
16351
|
-
usage.inputTokens ?? usage.input_tokens,
|
|
16352
|
-
);
|
|
16353
|
-
const output = normalizeNonNegativeNumber(
|
|
16354
|
-
usage.outputTokens ?? usage.output_tokens,
|
|
16355
|
-
);
|
|
16356
|
-
const cached = normalizeNonNegativeNumber(
|
|
16357
|
-
usage.cachedReadTokens ??
|
|
16358
|
-
usage.cache_read_input_tokens ??
|
|
16359
|
-
usage.cached_input_tokens,
|
|
16360
|
-
);
|
|
16361
|
-
const reasoning = normalizeNonNegativeNumber(
|
|
16362
|
-
usage.reasoningTokens ?? usage.reasoning_output_tokens,
|
|
16363
|
-
);
|
|
16364
|
-
// Grok reports inputTokens as the full prompt (including cache hits). Split
|
|
16365
|
-
// so pricing can apply cache_read rates correctly.
|
|
16366
|
-
const nonCachedInput = Math.max(0, inputRaw - cached);
|
|
16367
|
-
let total = normalizeNonNegativeNumber(usage.totalTokens ?? usage.total_tokens);
|
|
16368
|
-
if (total <= 0) {
|
|
16369
|
-
total = inputRaw + output + reasoning;
|
|
16370
|
-
}
|
|
16371
|
-
if (total <= 0 && nonCachedInput <= 0 && cached <= 0 && output <= 0) return null;
|
|
17374
|
+
const normalized = normalizeGrokUsage(usage);
|
|
17375
|
+
if (!normalized) return null;
|
|
16372
17376
|
return {
|
|
16373
|
-
|
|
16374
|
-
|
|
16375
|
-
|
|
16376
|
-
|
|
16377
|
-
reasoning_output_tokens: reasoning,
|
|
16378
|
-
total_tokens: total > 0 ? total : nonCachedInput + cached + output + reasoning,
|
|
16379
|
-
billable_total_tokens: total > 0 ? total : nonCachedInput + cached + output + reasoning,
|
|
17377
|
+
...normalized,
|
|
17378
|
+
// A missing/partial cost must fall back to model pricing, not turn into a
|
|
17379
|
+
// falsely precise $0 row in the hourly queue.
|
|
17380
|
+
total_cost_usd: normalized.total_cost_usd ?? 0,
|
|
16380
17381
|
conversation_count: 1,
|
|
16381
17382
|
model: canonicalizeGrokUsageModel(model),
|
|
16382
17383
|
timestamp,
|
|
@@ -16540,6 +17541,12 @@ function estimateGrokTokenDelta(totalTokens, conversationCount, options = {}) {
|
|
|
16540
17541
|
};
|
|
16541
17542
|
}
|
|
16542
17543
|
|
|
17544
|
+
function mergeGrokUsagePrecision(current, next) {
|
|
17545
|
+
if (!current) return next;
|
|
17546
|
+
if (!next || current === next) return current;
|
|
17547
|
+
return "mixed";
|
|
17548
|
+
}
|
|
17549
|
+
|
|
16543
17550
|
function clearGrokHourlyBuckets(hourlyState) {
|
|
16544
17551
|
if (!hourlyState || typeof hourlyState !== "object") return;
|
|
16545
17552
|
const buckets = hourlyState.buckets && typeof hourlyState.buckets === "object" ? hourlyState.buckets : null;
|
|
@@ -16668,9 +17675,9 @@ async function parseGrokBuildIncremental({
|
|
|
16668
17675
|
const prevVersion = Number(grokState.version) || 0;
|
|
16669
17676
|
const needsTurnUsageMigration = prevVersion < GROK_CURSOR_VERSION;
|
|
16670
17677
|
|
|
16671
|
-
// v3 and earlier treated context-window totalTokens as cumulative spend
|
|
16672
|
-
//
|
|
16673
|
-
// turn_completed.usage
|
|
17678
|
+
// v3 and earlier treated context-window totalTokens as cumulative spend;
|
|
17679
|
+
// v4 still overlapped output/reasoning, discarded cache creation, and ignored
|
|
17680
|
+
// provider-reported cost. Rebuild from turn_completed.usage for v5.
|
|
16674
17681
|
//
|
|
16675
17682
|
// Drop prior watermark totals / updateOffsets so files are re-read from byte 0,
|
|
16676
17683
|
// but keep legacySeen markers from seenSessions so the one-shot baseline
|
|
@@ -16794,9 +17801,10 @@ async function parseGrokBuildIncremental({
|
|
|
16794
17801
|
reasoning_output_tokens: event.reasoning_output_tokens,
|
|
16795
17802
|
total_tokens: event.total_tokens,
|
|
16796
17803
|
billable_total_tokens: event.billable_total_tokens,
|
|
17804
|
+
total_cost_usd: event.total_cost_usd,
|
|
16797
17805
|
conversation_count: event.conversation_count || 1,
|
|
16798
17806
|
};
|
|
16799
|
-
pendingBucketDeltas.push({ model: eventModel, hourStartStr, delta });
|
|
17807
|
+
pendingBucketDeltas.push({ model: eventModel, hourStartStr, delta, usagePrecision: "reported" });
|
|
16800
17808
|
cumulativeTotal += event.total_tokens;
|
|
16801
17809
|
tokenDeltaForSession += event.total_tokens;
|
|
16802
17810
|
finalTouchedHourStart = hourStartStr;
|
|
@@ -16823,7 +17831,7 @@ async function parseGrokBuildIncremental({
|
|
|
16823
17831
|
toUtcHalfHourStart(Date.now());
|
|
16824
17832
|
if (!hourStartStr) continue;
|
|
16825
17833
|
const delta = estimateGrokTokenDelta(deltaTokens, 0, { allowZeroConversationCount: true });
|
|
16826
|
-
pendingBucketDeltas.push({ model, hourStartStr, delta });
|
|
17834
|
+
pendingBucketDeltas.push({ model, hourStartStr, delta, usagePrecision: "estimated" });
|
|
16827
17835
|
tokenDeltaForSession += deltaTokens;
|
|
16828
17836
|
finalTouchedHourStart = hourStartStr;
|
|
16829
17837
|
source = "updates";
|
|
@@ -16836,7 +17844,7 @@ async function parseGrokBuildIncremental({
|
|
|
16836
17844
|
const hourStartStr = toUtcHalfHourStart(lastActive) || toUtcHalfHourStart(Date.now());
|
|
16837
17845
|
if (hourStartStr) {
|
|
16838
17846
|
const delta = estimateGrokTokenDelta(deltaTokens, 0, { allowZeroConversationCount: true });
|
|
16839
|
-
pendingBucketDeltas.push({ model, hourStartStr, delta });
|
|
17847
|
+
pendingBucketDeltas.push({ model, hourStartStr, delta, usagePrecision: "estimated" });
|
|
16840
17848
|
tokenDeltaForSession += deltaTokens;
|
|
16841
17849
|
finalTouchedHourStart = hourStartStr;
|
|
16842
17850
|
source = "signals";
|
|
@@ -16854,6 +17862,10 @@ async function parseGrokBuildIncremental({
|
|
|
16854
17862
|
for (const pending of pendingBucketDeltas) {
|
|
16855
17863
|
const bucket = getHourlyBucket(hourlyState, "grok", pending.model, pending.hourStartStr);
|
|
16856
17864
|
addTotals(bucket.totals, pending.delta);
|
|
17865
|
+
bucket.usage_precision = mergeGrokUsagePrecision(
|
|
17866
|
+
bucket.usage_precision,
|
|
17867
|
+
pending.usagePrecision,
|
|
17868
|
+
);
|
|
16857
17869
|
touchedBuckets.add(bucketKey("grok", pending.model, pending.hourStartStr));
|
|
16858
17870
|
eventsAggregated++;
|
|
16859
17871
|
}
|
|
@@ -17092,7 +18104,7 @@ async function parseGrokBuildIncremental({
|
|
|
17092
18104
|
? { ...grokState.migrations }
|
|
17093
18105
|
: {};
|
|
17094
18106
|
if (needsTurnUsageMigration) {
|
|
17095
|
-
migrations.
|
|
18107
|
+
migrations.turnUsageV5 = {
|
|
17096
18108
|
appliedAt: new Date().toISOString(),
|
|
17097
18109
|
fromVersion: prevVersion,
|
|
17098
18110
|
toVersion: GROK_CURSOR_VERSION,
|
|
@@ -17402,6 +18414,7 @@ async function parseAntigravityFile({
|
|
|
17402
18414
|
// Match the mainstream convention (Codebuddy / Kilocode / OMP / Hermes):
|
|
17403
18415
|
// total_tokens = sum of every token column. No cache columns here.
|
|
17404
18416
|
delta.total_tokens = inputDelta + outputTokens + reasoningTokens;
|
|
18417
|
+
delta.billable_total_tokens = delta.total_tokens;
|
|
17405
18418
|
delta.conversation_count = 1;
|
|
17406
18419
|
billedPlanner = delta.total_tokens > 0;
|
|
17407
18420
|
}
|
|
@@ -18940,6 +19953,15 @@ module.exports = {
|
|
|
18940
19953
|
sumZedRequestUsage,
|
|
18941
19954
|
readZedUsage,
|
|
18942
19955
|
parseZedIncremental,
|
|
19956
|
+
resolveLmstudioHome,
|
|
19957
|
+
resolveLmstudioLogFiles,
|
|
19958
|
+
normalizeLocalStudioTokens,
|
|
19959
|
+
readLmstudioFileRecords,
|
|
19960
|
+
parseLmstudioIncremental,
|
|
19961
|
+
resolveUnslothDbPath,
|
|
19962
|
+
readUnslothUsageRows,
|
|
19963
|
+
normalizeUnslothUsageRow,
|
|
19964
|
+
parseUnslothIncremental,
|
|
18943
19965
|
resolveAnythingllmDbPath,
|
|
18944
19966
|
parseAnythingllmTimestamp,
|
|
18945
19967
|
readAnythingllmUsageRows,
|