tokentracker-cli 0.94.1 → 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-DnmgEa4q.js → AchievementBadge-Pi3Gvbm8.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-B7YTbAuK.js → AchievementsPage-C4WLT8Rl.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-CwgT5sqH.js → AchievementsSection-ilvR2ssr.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-Dets6jzW.js → BadgeDetailModal-CjEeToxa.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-5KwUvC9F.js → BotAnimated-DDf9pBuK.js} +1 -1
- package/dashboard/dist/assets/{Card-jf9w-4-N.js → Card-DNous84-.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-CGxDVQ0c.js → ClawdAnimated-BLMIRntb.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-B1bUUf32.js → CommandPalette-vVekhoFO.js} +1 -1
- package/dashboard/dist/assets/CommunityStatsModal-C0Kp_J8B.js +1 -0
- package/dashboard/dist/assets/{ConfirmModal-BR-aIRH9.js → ConfirmModal-DFdpcx12.js} +1 -1
- package/dashboard/dist/assets/{Controls-CbHsJAqV.js → Controls-RNhqybmE.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-BQjQh1IQ.js +1 -0
- package/dashboard/dist/assets/{DevicePage-DDlicLLn.js → DevicePage-DIhh-YAk.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-B89uLXqp.js → DialogClose-ZqxYVIzl.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-DDM4aanX.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-jOyycYKF.js → DialogTitle-BiRcejlu.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-BEND42ze.js → FadeIn-BDBQpeGe.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-Dacl1WGS.js → HeaderGithubStar-CztIwtKT.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-B2Mh2LTi.js → HoverTooltip-Cfso7LTC.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-CPEuRhiU.js → IpCheckPage-CNE7dLs3.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-BYWNR_dx.js → LandingPage-B2a3cweM.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-B9_5LDVj.js → LeaderboardAvatar-CwmZnIji.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-_Hl1NaIa.js → LeaderboardPage-DkTHj3fB.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-Cydp7CyA.js → LeaderboardProfileModal-CefTO3Zh.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-Dz0ecujs.js → LeaderboardProfilePage-CnOXlQ3L.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-CmdT2qOZ.js → LimitsPage-BIZ1-6BB.js} +2 -2
- package/dashboard/dist/assets/{LocalOnlyNotice-DhuPGUkw.js → LocalOnlyNotice-CdHQiPhy.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-DQtHaX_5.js → LoginCard-yXLA9geu.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-DvpPRnYV.js → LoginModal-BF2Pfkqa.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-DOjfmdwb.js → LoginPage-DuOypZap.js} +1 -1
- package/dashboard/dist/assets/{PetPage-Dd6U9fh2.js → PetPage-BH3xDVV9.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-B1V01u10.js → PopoverPopup-CewF_cGz.js} +1 -1
- package/dashboard/dist/assets/{ProviderIcon-BLRe55c7.js → ProviderIcon-Dg0K0RPn.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-BJ95vy1S.js → ResetPasswordPage-CYwt85O3.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-DacS_m9M.js → SegmentedControl-BJkXXomQ.js} +1 -1
- package/dashboard/dist/assets/{Select-bQDO5BEA.js → Select-B3_WH0Qe.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-CUGrs9CY.js → SelectItemText-BUby_DUu.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-CsERvmVC.js → ServiceStatusPage-BDlVZk4i.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-0VvqEfPx.js → SessionsPage-TaC9nahK.js} +1 -1
- package/dashboard/dist/assets/{SettingsPage-9EHh2HfD.js → SettingsPage-CudcQh6m.js} +2 -2
- package/dashboard/dist/assets/SkillsPage-iDLQ8lpZ.js +1 -0
- package/dashboard/dist/assets/{TokenGalaxy-DSKTHQSs.js → TokenGalaxy-DtYzUsXU.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-DP4mQLbq.js → ToolbarRootContext-PC8Tg1Jz.js} +1 -1
- package/dashboard/dist/assets/TrendMonitor-r8n8YO_9.js +19 -0
- package/dashboard/dist/assets/{WidgetsPage-Cr6Wg9QX.js → WidgetsPage-vzfGccWb.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-DqQvRcnA.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-BjIh_mqf.js → agent-logos-B7nzn3n2.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-BIhbN0NO.js → arrow-up-right-CsxyvCER.js} +1 -1
- package/dashboard/dist/assets/{check-U-wNzPvd.js → check-hYcC7MdH.js} +1 -1
- package/dashboard/dist/assets/{composite-BR91CxNl.js → composite-C79LNbex.js} +1 -1
- package/dashboard/dist/assets/{copy-Bw3hXBCu.js → copy-Cz4qzE62.js} +1 -1
- package/dashboard/dist/assets/{download-WOqcQzvK.js → download-CUaSNE2E.js} +1 -1
- package/dashboard/dist/assets/{external-link-BZtf6WfS.js → external-link-vq7kIyKK.js} +1 -1
- package/dashboard/dist/assets/{flame-B5kp2VI9.js → flame-CB6K4eR6.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-BOCnuMuJ.js → format-tokens-KI-e_JQ-.js} +1 -1
- package/dashboard/dist/assets/{icons-CA3FiIa_.js → icons-3maItkrp.js} +1 -1
- package/dashboard/dist/assets/{index-DQaanK52.js → index-BRXoZ7Jj.js} +1 -1
- package/dashboard/dist/assets/{index-mEG0HZ-a.js → index-C5WrfmLB.js} +1 -1
- package/dashboard/dist/assets/{index-DjWC63Bs.js → index-CUbC50Zp.js} +1 -1
- package/dashboard/dist/assets/{info-Djer9i3-.js → info-r0yGihUe.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-F-RnNFAb.js → limits-providers-BN_94oe9.js} +1 -1
- package/dashboard/dist/assets/{link-2-DV4ocD0l.js → link-2-J9t0VXWM.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-DJK72c-o.js → loader-circle-DCvOsPze.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-BwBA_lMm.js → maximize-2-CZ9JhGjf.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-B3sl4qyZ.js → modifiers.esm-DJ0_WKg0.js} +1 -1
- package/dashboard/dist/assets/module-xpSFyNBK.js +3 -0
- package/dashboard/dist/assets/{monitor-smartphone-BUTpM4ng.js → monitor-smartphone-nqbWjJx_.js} +1 -1
- package/dashboard/dist/assets/{plus-BA9RP5L6.js → plus-Cg7wd2jK.js} +1 -1
- package/dashboard/dist/assets/{provider-display-DqsJzGSr.js → provider-display-B9L1nRY9.js} +2 -2
- package/dashboard/dist/assets/{react-BYw5AVSz.js → react-D2AD6Pk2.js} +1 -1
- package/dashboard/dist/assets/{search-BzcnFWVD.js → search-uNiWba1h.js} +1 -1
- package/dashboard/dist/assets/{skills-api-vAME1P7G.js → skills-api-BBYuAhiL.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-BpEiRqqI.js → sortable.esm-CzPxc949.js} +1 -1
- package/dashboard/dist/assets/{store-GeT5fhRX.js → store-D4A7EUwE.js} +1 -1
- package/dashboard/dist/assets/{terminal-CZCMYQ8K.js → terminal-Crmm_yKF.js} +1 -1
- package/dashboard/dist/assets/{trash-2-DzlWEwKO.js → trash-2-ia9VjRnU.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DOvJfj-i.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-BmLs4HyN.js → use-limits-display-prefs-JSTYlbTD.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-BgozDSSV.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-HP1ckVkS.js → use-session-efficiency-pref-CgyUz5Gc.js} +1 -1
- package/dashboard/dist/assets/{use-transform-B4VSogOx.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-CSuzCBAL.js → useCurrency-CK_gcb3J.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-ClnL9OFe.js → useTokenFormat-C_a-UkCA.js} +1 -1
- package/dashboard/dist/assets/{zap-C_T9CCiS.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/local-api.js +99 -13
- package/src/lib/pricing/index.js +5 -0
- package/src/lib/pricing/matcher.js +15 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +1035 -28
- package/src/lib/usage-limits.js +507 -55
- package/src/lib/zcode-limits.js +65 -8
- package/dashboard/dist/assets/CommunityStatsModal-DRDpZycV.js +0 -1
- package/dashboard/dist/assets/DashboardPage-B5XMQoxy.js +0 -1
- package/dashboard/dist/assets/DialogDescription-DiVbwvTR.js +0 -1
- package/dashboard/dist/assets/SkillsPage-B1AcLX41.js +0 -1
- package/dashboard/dist/assets/TrendMonitor-rabtcE5w.js +0 -19
- package/dashboard/dist/assets/__vite-browser-external-C8KwSyTz.js +0 -45
- package/dashboard/dist/assets/chevron-right-Du-QVNj4.js +0 -1
- package/dashboard/dist/assets/module-D2D5r9gC.js +0 -3
- package/dashboard/dist/assets/use-reduced-motion-Bc3RFJdg.js +0 -1
- package/dashboard/dist/assets/use-usage-limits-DNNhD2H6.js +0 -1
package/src/lib/rollout.js
CHANGED
|
@@ -4900,6 +4900,42 @@ function isZcodeNativeMessage(data) {
|
|
|
4900
4900
|
);
|
|
4901
4901
|
}
|
|
4902
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
|
+
|
|
4903
4939
|
const ZCODE_NATIVE_USAGE_COLUMNS = new Set([
|
|
4904
4940
|
"id",
|
|
4905
4941
|
"logical_request_id",
|
|
@@ -5045,18 +5081,6 @@ function readZcodeNativeUsageMessages(dbPath, sqliteOptions = {}) {
|
|
|
5045
5081
|
const startedAt = coerceEpochMs(row?.started_at);
|
|
5046
5082
|
if (!providerID || !modelID || !sessionID || !id || !startedAt) continue;
|
|
5047
5083
|
|
|
5048
|
-
// ZCode's persisted input/output columns are inclusive parents:
|
|
5049
|
-
// cache read/write are subsets of input, and reasoning is a subset of
|
|
5050
|
-
// output. TokenTracker stores those five columns disjointly, so split the
|
|
5051
|
-
// subsets here before entering the shared OpenCode parser. This preserves
|
|
5052
|
-
// sum(input + cache read + cache write + output + reasoning) without
|
|
5053
|
-
// billing either subset twice.
|
|
5054
|
-
const rawInput = toNonNegativeInt(row?.input_tokens);
|
|
5055
|
-
const rawOutput = toNonNegativeInt(row?.output_tokens);
|
|
5056
|
-
const cacheRead = toNonNegativeInt(row?.cache_read_input_tokens);
|
|
5057
|
-
const cacheWrite = toNonNegativeInt(row?.cache_creation_input_tokens);
|
|
5058
|
-
const reasoning = toNonNegativeInt(row?.reasoning_tokens);
|
|
5059
|
-
|
|
5060
5084
|
const data = {
|
|
5061
5085
|
id,
|
|
5062
5086
|
sessionID,
|
|
@@ -5064,15 +5088,15 @@ function readZcodeNativeUsageMessages(dbPath, sqliteOptions = {}) {
|
|
|
5064
5088
|
providerID,
|
|
5065
5089
|
modelID,
|
|
5066
5090
|
time: { created: startedAt, completed: startedAt },
|
|
5067
|
-
tokens: {
|
|
5068
|
-
input:
|
|
5069
|
-
output:
|
|
5070
|
-
reasoning,
|
|
5091
|
+
tokens: normalizeZcodeInclusiveTokens({
|
|
5092
|
+
input: row?.input_tokens,
|
|
5093
|
+
output: row?.output_tokens,
|
|
5094
|
+
reasoning: row?.reasoning_tokens,
|
|
5071
5095
|
cache: {
|
|
5072
|
-
read:
|
|
5073
|
-
write:
|
|
5096
|
+
read: row?.cache_read_input_tokens,
|
|
5097
|
+
write: row?.cache_creation_input_tokens,
|
|
5074
5098
|
},
|
|
5075
|
-
},
|
|
5099
|
+
}),
|
|
5076
5100
|
};
|
|
5077
5101
|
if (typeof row?.directory === "string" && row.directory.trim()) {
|
|
5078
5102
|
data.path = { cwd: row.directory.trim() };
|
|
@@ -5085,12 +5109,31 @@ function readZcodeNativeUsageMessages(dbPath, sqliteOptions = {}) {
|
|
|
5085
5109
|
|
|
5086
5110
|
// Read only genuine ZCode assistant messages (its own GLM models via Z.ai /
|
|
5087
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.
|
|
5088
5118
|
function readZcodeDbMessages(dbPath, sqliteOptions = {}) {
|
|
5089
5119
|
if (!dbPath || !fssync.existsSync(dbPath)) return [];
|
|
5090
5120
|
const nativeMessages = readZcodeNativeUsageMessages(dbPath, sqliteOptions);
|
|
5091
|
-
|
|
5092
|
-
|
|
5093
|
-
|
|
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];
|
|
5094
5137
|
}
|
|
5095
5138
|
|
|
5096
5139
|
async function parseOpencodeDbIncremental({
|
|
@@ -11160,6 +11203,955 @@ async function parseZedIncremental({
|
|
|
11160
11203
|
return { recordsProcessed, eventsAggregated, bucketsQueued };
|
|
11161
11204
|
}
|
|
11162
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
|
+
|
|
11163
12155
|
// ─────────────────────────────────────────────────────────────────────────────
|
|
11164
12156
|
// AnythingLLM Desktop (Mintplex Labs)
|
|
11165
12157
|
//
|
|
@@ -14251,6 +15243,13 @@ async function migrateCopilotChatLogRecordDedup({
|
|
|
14251
15243
|
touchedBuckets,
|
|
14252
15244
|
seenIds,
|
|
14253
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
|
+
}
|
|
14254
15253
|
const trackedFiles = Object.keys(fileOffsets || {}).filter(
|
|
14255
15254
|
(filePath) => Number(fileOffsets[filePath]?.size) > 0,
|
|
14256
15255
|
);
|
|
@@ -14258,11 +15257,6 @@ async function migrateCopilotChatLogRecordDedup({
|
|
|
14258
15257
|
return { applied: true, changed: false };
|
|
14259
15258
|
}
|
|
14260
15259
|
|
|
14261
|
-
const availableFiles = new Set(Array.isArray(files) ? files : []);
|
|
14262
|
-
if (trackedFiles.some((filePath) => !availableFiles.has(filePath))) {
|
|
14263
|
-
return { applied: false, reason: "a previously parsed OTEL file is unavailable" };
|
|
14264
|
-
}
|
|
14265
|
-
|
|
14266
15260
|
const oldSeen = new Set();
|
|
14267
15261
|
const newSeen = new Set();
|
|
14268
15262
|
const oldTotals = new Map();
|
|
@@ -14342,7 +15336,10 @@ async function migrateCopilotChatLogRecordDedup({
|
|
|
14342
15336
|
}
|
|
14343
15337
|
|
|
14344
15338
|
const keys = new Set([...oldTotals.keys(), ...newTotals.keys()]);
|
|
14345
|
-
|
|
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()) {
|
|
14346
15343
|
const [model, bucketStart] = JSON.parse(key);
|
|
14347
15344
|
const oldUsage = oldTotals.get(key) || initTotals();
|
|
14348
15345
|
if (!copilotTotalsCover(
|
|
@@ -17417,6 +18414,7 @@ async function parseAntigravityFile({
|
|
|
17417
18414
|
// Match the mainstream convention (Codebuddy / Kilocode / OMP / Hermes):
|
|
17418
18415
|
// total_tokens = sum of every token column. No cache columns here.
|
|
17419
18416
|
delta.total_tokens = inputDelta + outputTokens + reasoningTokens;
|
|
18417
|
+
delta.billable_total_tokens = delta.total_tokens;
|
|
17420
18418
|
delta.conversation_count = 1;
|
|
17421
18419
|
billedPlanner = delta.total_tokens > 0;
|
|
17422
18420
|
}
|
|
@@ -18955,6 +19953,15 @@ module.exports = {
|
|
|
18955
19953
|
sumZedRequestUsage,
|
|
18956
19954
|
readZedUsage,
|
|
18957
19955
|
parseZedIncremental,
|
|
19956
|
+
resolveLmstudioHome,
|
|
19957
|
+
resolveLmstudioLogFiles,
|
|
19958
|
+
normalizeLocalStudioTokens,
|
|
19959
|
+
readLmstudioFileRecords,
|
|
19960
|
+
parseLmstudioIncremental,
|
|
19961
|
+
resolveUnslothDbPath,
|
|
19962
|
+
readUnslothUsageRows,
|
|
19963
|
+
normalizeUnslothUsageRow,
|
|
19964
|
+
parseUnslothIncremental,
|
|
18958
19965
|
resolveAnythingllmDbPath,
|
|
18959
19966
|
parseAnythingllmTimestamp,
|
|
18960
19967
|
readAnythingllmUsageRows,
|