tokentracker-cli 1.3.16 → 1.4.2
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 +5 -4
- package/README.ja.md +5 -4
- package/README.ko.md +5 -4
- package/README.md +5 -4
- package/README.zh-CN.md +5 -4
- package/dashboard/dist/assets/{AchievementBadge-D5sRTMmp.js → AchievementBadge-WGbdoboF.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-CaiCfchd.js → AchievementsPage-BvybbsXv.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-D6dTxdUd.js → AchievementsSection-ChNzTFSR.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-C0UrWb9m.js → BadgeDetailModal-DhI9Ejz3.js} +1 -1
- package/dashboard/dist/assets/{BotAnimated-CgpBB5FZ.js → BotAnimated-Bch0Yxej.js} +1 -1
- package/dashboard/dist/assets/{Card-CuwBiIYz.js → Card-DWdL7d1T.js} +1 -1
- package/dashboard/dist/assets/{CloudCheckoutPage-BNCnBR0h.js → CloudCheckoutPage-BjGoOz6P.js} +1 -1
- package/dashboard/dist/assets/{CloudPage-CuYXUQnv.js → CloudPage-Btb6a97x.js} +2 -2
- package/dashboard/dist/assets/{CommandPalette-CV6a_7yK.js → CommandPalette-DwP3k7LE.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-Dw-64Tig.js → CommunityStatsModal-mzqLxpwE.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-CoFO1Nas.js → ConfirmModal-Dm76q4EX.js} +1 -1
- package/dashboard/dist/assets/{Controls-DB9eQ_rg.js → Controls-ClZjOBoD.js} +1 -1
- package/dashboard/dist/assets/DashboardPage-Doc8f4NL.js +1 -0
- package/dashboard/dist/assets/{DateRangePopover-DgZEnn4X.js → DateRangePopover-vfTUePPg.js} +1 -1
- package/dashboard/dist/assets/DevicePage-Jjg5q0C2.js +1 -0
- package/dashboard/dist/assets/{DialogClose-BR9QrB34.js → DialogClose-Ci5mPZ6X.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-Di5ev5K6.js +1 -0
- package/dashboard/dist/assets/DialogTitle-DI21oZxH.js +1 -0
- package/dashboard/dist/assets/{ElectricLogo-nrlBy-l9.js → ElectricLogo-DfeZBwPZ.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-CyJJ3l_P.js → FadeIn-C7SIILTc.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-C7BX7loS.js → HeaderGithubStar-RStLbadn.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-KGluh6sO.js → HoverTooltip-DI7uAp1_.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-Ci2ybTDg.js → IpCheckPage-DTZNTJVh.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-C9N17IpV.js → LandingPage-Df5scNlw.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardPage-Cco7iFen.js → LeaderboardPage-Ck3SqtnI.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProBadge-tmm5NN8p.js → LeaderboardProBadge-CGx6m7R6.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardProfileModal-WhjsvZ9Q.js → LeaderboardProfileModal-DuP7rgfv.js} +3 -3
- package/dashboard/dist/assets/{LeaderboardProfilePage-Ba3oVu5z.js → LeaderboardProfilePage-B5qUJkZX.js} +1 -1
- package/dashboard/dist/assets/{LimitsPage-CUa46XJy.js → LimitsPage-Bv_Lcvcz.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-BSewrWlY.js → LocalOnlyNotice-Cg4y02JM.js} +1 -1
- package/dashboard/dist/assets/{LoginCard-CUWn0Tz4.js → LoginCard-Wz0uJKXx.js} +1 -1
- package/dashboard/dist/assets/{LoginModal-UKk3xCsV.js → LoginModal-C13v_Gs0.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-BhxiWgc5.js → LoginPage-COH9xXtd.js} +1 -1
- package/dashboard/dist/assets/{PetPage-BRyxC-Iu.js → PetPage-BWCnURub.js} +2 -2
- package/dashboard/dist/assets/PopoverPopup-Dw811N-_.js +1 -0
- package/dashboard/dist/assets/{ProviderIcon-htNu1RTN.js → ProviderIcon-CHxhYzK-.js} +1 -1
- package/dashboard/dist/assets/{ResetPasswordPage-DawMNiM1.js → ResetPasswordPage-Ll6jshqg.js} +1 -1
- package/dashboard/dist/assets/Select-BbooQ3sa.js +1 -0
- package/dashboard/dist/assets/SelectItemText-DBiW70wi.js +1 -0
- package/dashboard/dist/assets/{SelfHostPage-BMCMrt5I.js → SelfHostPage-BMHJxz5q.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-e_7xC0RY.js → ServiceStatusPage-D6LH0ac-.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-Xr2FJa6E.js → SessionsPage-Mrer6v17.js} +2 -2
- package/dashboard/dist/assets/SettingsPage-D_7P_y_l.js +5 -0
- package/dashboard/dist/assets/SkillsPage-Ka7xm-1v.js +1 -0
- package/dashboard/dist/assets/{TokenGalaxy--6ijNK6P.js → TokenGalaxy-BEURDPFq.js} +1 -1
- package/dashboard/dist/assets/TooltipPopup-ZiuFFARC.js +1 -0
- package/dashboard/dist/assets/{TrendMonitor-DjIZyzxp.js → TrendMonitor-H73lyGEf.js} +1 -1
- package/dashboard/dist/assets/{WidgetsPage-CzCqGbb4.js → WidgetsPage-_7iCvyTk.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-B0RMtpvr.js → WrappedPage-DEXNb6-k.js} +1 -1
- package/dashboard/dist/assets/__vite-browser-external-BkNP5Ecj.js +45 -0
- package/dashboard/dist/assets/{agent-logos-CSlyeabY.js → agent-logos-OVBc_0cF.js} +1 -1
- package/dashboard/dist/assets/{arrow-left-DaqMiXTO.js → arrow-left-DDphsclm.js} +1 -1
- package/dashboard/dist/assets/{arrow-right-B1p9R8vH.js → arrow-right-B1aNq0Ki.js} +1 -1
- package/dashboard/dist/assets/{arrow-up-right-CK_9HuEh.js → arrow-up-right-yTM9azz_.js} +1 -1
- package/dashboard/dist/assets/{calendar-days-BVyCx4dJ.js → calendar-days-D22ithNF.js} +1 -1
- package/dashboard/dist/assets/{check-DIfCVsPJ.js → check-Dk7tknvE.js} +1 -1
- package/dashboard/dist/assets/{chevron-down-DrWxRBXX.js → chevron-down-DdyEzQ4f.js} +1 -1
- package/dashboard/dist/assets/{cloud-checkout-DOxBG7oS.js → cloud-checkout-BJjbipnH.js} +1 -1
- package/dashboard/dist/assets/composite-DEqP00XV.js +1 -0
- package/dashboard/dist/assets/{download-CEXAmejX.js → download-C9ixNyzn.js} +1 -1
- package/dashboard/dist/assets/{external-link-tDBZy6QP.js → external-link-DL4HSsLw.js} +1 -1
- package/dashboard/dist/assets/{flame-86y9Xf0g.js → flame-DqNH65nB.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-BNN9k5mV.js → format-tokens-CyVjKz4I.js} +1 -1
- package/dashboard/dist/assets/{gift-BEOvq2zc.js → gift-ChJQQe_G.js} +1 -1
- package/dashboard/dist/assets/{host-mode-UjbNKxFB.js → host-mode-DpUDpoOz.js} +1 -1
- package/dashboard/dist/assets/{icons-DJmZD0si.js → icons-CJiHeOaU.js} +1 -1
- package/dashboard/dist/assets/{index-BwUwnqE0.js → index-72LQsAaf.js} +1 -1
- package/dashboard/dist/assets/{index-B8kizz-s.js → index-BucEg3fu.js} +1 -1
- package/dashboard/dist/assets/{index-Ci3CZ-2g.js → index-CrFlTSra.js} +1 -1
- package/dashboard/dist/assets/{info-ynVg8RzK.js → info-DReYqxC7.js} +1 -1
- package/dashboard/dist/assets/{link-2-B9SLPcBg.js → link-2-kIZbhK2S.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-FTE9k7Sk.js → loader-circle-1An-X85y.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-B4babCm_.js → modifiers.esm-FoVheEKT.js} +1 -1
- package/dashboard/dist/assets/module-DOCYJaxl.js +3 -0
- package/dashboard/dist/assets/{monitor-smartphone-CNzgbo-M.js → monitor-smartphone-Ch6lr_P2.js} +1 -1
- package/dashboard/dist/assets/{play-9W4Ua21o.js → play-CbJ6ZF1K.js} +1 -1
- package/dashboard/dist/assets/{plus-BA85lApI.js → plus-C-5ID3a9.js} +1 -1
- package/dashboard/dist/assets/{provider-display-D1ETarsz.js → provider-display-BFzRtoL2.js} +2 -2
- package/dashboard/dist/assets/{react-VsHxeqq7.js → react-D4CR_fLQ.js} +1 -1
- package/dashboard/dist/assets/safePolygon-DP21Ibqj.js +1 -0
- package/dashboard/dist/assets/{search-Svi--7CN.js → search-CaznkY7U.js} +1 -1
- package/dashboard/dist/assets/{server-CgHdX6TE.js → server-CUOmC6xZ.js} +1 -1
- package/dashboard/dist/assets/{shield-check-Cjv-T4aT.js → shield-check-Dr2KBMmA.js} +1 -1
- package/dashboard/dist/assets/{skills-api-Cn8hEYL4.js → skills-api-BAz_fblS.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-g3UcmvFU.js → sortable.esm-knEfm5hE.js} +1 -1
- package/dashboard/dist/assets/store-CL97jcij.js +1 -0
- package/dashboard/dist/assets/{terminal-o0jvDfI0.js → terminal-EjFyreWj.js} +1 -1
- package/dashboard/dist/assets/{trash-0cqpTyN6.js → trash-ByqaZXkQ.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-BFgORaJS.js → use-community-stats-Bs4RDJZE.js} +1 -1
- package/dashboard/dist/assets/{use-latest-request-guard-9eZP_jA5.js → use-latest-request-guard-CG3Ir1cP.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-BjO3hsmw.js → use-limits-display-prefs-tACgCxeR.js} +1 -1
- package/dashboard/dist/assets/{use-native-settings-YKn-htVo.js → use-native-settings-CWjXyJXi.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-D5h4UwhF.js → use-ordered-list-DsuC9JQa.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-CwFguzkm.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-BShLuIbk.js → use-session-efficiency-pref-NeoJE8BL.js} +1 -1
- package/dashboard/dist/assets/{use-transform-DVZHJPVD.js → use-transform-BWGKOsya.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-BhJbW76K.js → use-usage-limits-CFWFNwHS.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-BmEbDVaa.js → useCurrency-B_C4_P5x.js} +1 -1
- package/dashboard/dist/assets/usePositioner-B0L5_o38.js +1 -0
- package/dashboard/dist/assets/{useTokenFormat-CBlBP2aA.js → useTokenFormat-DH1GWHSU.js} +1 -1
- package/dashboard/dist/assets/{zap-cML2-Nx4.js → zap-Chv1p7pS.js} +1 -1
- package/dashboard/dist/index.html +13 -11
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/llms.txt +3 -2
- package/dashboard/dist/share.html +1 -1
- package/package.json +3 -3
- package/src/commands/doctor.js +15 -1
- package/src/commands/init.js +9 -0
- package/src/commands/status.js +21 -3
- package/src/commands/sync.js +59 -4
- package/src/lib/cloud-device-token.js +77 -10
- package/src/lib/cloud-usage-reset.js +115 -0
- package/src/lib/diagnostics.js +5 -2
- package/src/lib/kimi-model-repair.js +173 -0
- package/src/lib/kimi-session.js +216 -0
- package/src/lib/local-api.js +46 -1
- package/src/lib/pricing/index.js +2 -1
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/qoder-cn-cli-limits.js +9 -7
- package/src/lib/qoder-limits.js +45 -15
- package/src/lib/rollout.js +408 -4
- package/src/lib/session-analytics.js +47 -9
- package/dashboard/dist/assets/DashboardPage-sZKijU6h.js +0 -1
- package/dashboard/dist/assets/DevicePage-YCnJnsmz.js +0 -1
- package/dashboard/dist/assets/DialogDescription-Cx1R3WT7.js +0 -1
- package/dashboard/dist/assets/DialogTitle-ck8f1qeP.js +0 -1
- package/dashboard/dist/assets/PopoverPopup-D-Tpl294.js +0 -1
- package/dashboard/dist/assets/Select-6WChWtqK.js +0 -1
- package/dashboard/dist/assets/SelectItemText-DRmxMwo5.js +0 -1
- package/dashboard/dist/assets/SettingsPage-Bxq2Mhxz.js +0 -5
- package/dashboard/dist/assets/SkillsPage-CEIBW63c.js +0 -1
- package/dashboard/dist/assets/TooltipPopup--TLYUg0c.js +0 -1
- package/dashboard/dist/assets/__vite-browser-external-BsOjTB8Q.js +0 -45
- package/dashboard/dist/assets/composite-W1R1XwoE.js +0 -1
- package/dashboard/dist/assets/module-BNnaKM__.js +0 -3
- package/dashboard/dist/assets/safePolygon-CCDOsx-z.js +0 -1
- package/dashboard/dist/assets/store-D2gn9bSV.js +0 -1
- package/dashboard/dist/assets/use-reduced-motion-C83gmg0F.js +0 -1
- package/dashboard/dist/assets/usePositioner-nY0YNa-6.js +0 -1
|
@@ -0,0 +1,173 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const fs = require("node:fs/promises");
|
|
4
|
+
const path = require("node:path");
|
|
5
|
+
const crypto = require("node:crypto");
|
|
6
|
+
|
|
7
|
+
const KIMI_MODEL_REPAIR_KEY = "kimiModelAttribution_2026_10";
|
|
8
|
+
const TOKEN_FIELDS = ["input_tokens", "cached_input_tokens", "cache_creation_input_tokens", "output_tokens", "reasoning_output_tokens", "total_tokens"];
|
|
9
|
+
const keyOf = (row) => row?.source === "kimi" && typeof row.model === "string" && typeof row.hour_start === "string"
|
|
10
|
+
? `kimi|${row.model}|${row.hour_start}` : null;
|
|
11
|
+
const digest = (bytes) => crypto.createHash("sha256").update(bytes).digest("hex");
|
|
12
|
+
|
|
13
|
+
async function readBytes(file) {
|
|
14
|
+
try { return await fs.readFile(file); } catch (error) { if (error.code === "ENOENT") return Buffer.alloc(0); throw error; }
|
|
15
|
+
}
|
|
16
|
+
async function readJson(file) {
|
|
17
|
+
const bytes = await readBytes(file);
|
|
18
|
+
return bytes.length ? JSON.parse(bytes.toString("utf8")) : null;
|
|
19
|
+
}
|
|
20
|
+
function latestRows(bytes) {
|
|
21
|
+
const rows = new Map();
|
|
22
|
+
for (const line of bytes.toString("utf8").split("\n")) {
|
|
23
|
+
let row;
|
|
24
|
+
try { row = JSON.parse(line); } catch { continue; }
|
|
25
|
+
const key = keyOf(row);
|
|
26
|
+
if (key) rows.set(key, row);
|
|
27
|
+
}
|
|
28
|
+
return rows;
|
|
29
|
+
}
|
|
30
|
+
function sumsByHour(rows) {
|
|
31
|
+
const totals = new Map();
|
|
32
|
+
for (const row of rows.values()) {
|
|
33
|
+
const total = totals.get(row.hour_start) || Object.fromEntries(TOKEN_FIELDS.map((field) => [field, 0]));
|
|
34
|
+
for (const field of TOKEN_FIELDS) {
|
|
35
|
+
const count = Number(row[field] || 0);
|
|
36
|
+
if (!Number.isSafeInteger(count) || count < 0) throw new Error("Kimi repair encountered invalid token totals");
|
|
37
|
+
total[field] += count;
|
|
38
|
+
}
|
|
39
|
+
if (TOKEN_FIELDS.some((field) => total[field] !== 0)) totals.set(row.hour_start, total);
|
|
40
|
+
}
|
|
41
|
+
return totals;
|
|
42
|
+
}
|
|
43
|
+
function sameTotals(before, after) {
|
|
44
|
+
const left = sumsByHour(before), right = sumsByHour(after);
|
|
45
|
+
return left.size === right.size && [...left].every(([hour, total]) => right.has(hour)
|
|
46
|
+
&& TOKEN_FIELDS.every((field) => total[field] === right.get(hour)[field]));
|
|
47
|
+
}
|
|
48
|
+
function sameSnapshot(left, right) {
|
|
49
|
+
return left && right && [...TOKEN_FIELDS, "conversation_count", "billable_total_tokens"]
|
|
50
|
+
.every((field) => Number(left[field] || 0) === Number(right[field] || 0));
|
|
51
|
+
}
|
|
52
|
+
async function fileSignature(file) {
|
|
53
|
+
const handle = await fs.open(file, "r");
|
|
54
|
+
try {
|
|
55
|
+
const stat = await handle.stat();
|
|
56
|
+
if (!stat.isFile()) throw new Error("Kimi repair source is not a file");
|
|
57
|
+
return { size: stat.size, ino: stat.ino, mtimeMs: stat.mtimeMs };
|
|
58
|
+
} finally { await handle.close(); }
|
|
59
|
+
}
|
|
60
|
+
async function writeJournal(file, journal) {
|
|
61
|
+
const temp = `${file}.${crypto.randomUUID()}.tmp`;
|
|
62
|
+
try {
|
|
63
|
+
await fs.writeFile(temp, JSON.stringify(journal), { mode: 0o600, flag: "wx" });
|
|
64
|
+
await fs.rename(temp, file);
|
|
65
|
+
} finally { await fs.rm(temp, { force: true }); }
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
// Caller holds sync.lock and commits cursors after this returns, before upload.
|
|
69
|
+
// Only append to queue.jsonl; the caller compacts its pending suffix with
|
|
70
|
+
// minPendingBytes:0 after cursor commit, preserving uploaded bytes and offset.
|
|
71
|
+
// Parsers are injected to keep rollout.js independent of its migration helper.
|
|
72
|
+
async function repairKimiModelAttribution({ cursors, queuePath, queueStatePath, legacyFiles = [], officialFiles = [], parseLegacy, parseOfficial, env = process.env }) {
|
|
73
|
+
const journalPath = `${queuePath}.kimi-model-repair.json`;
|
|
74
|
+
const migrations = cursors.migrations || {};
|
|
75
|
+
const applied = migrations[KIMI_MODEL_REPAIR_KEY];
|
|
76
|
+
if (applied?.status === "applied" || applied?.status === "noop") {
|
|
77
|
+
await fs.rm(journalPath, { force: true });
|
|
78
|
+
return { repaired: false, pendingCompaction: applied.pendingCompaction === true };
|
|
79
|
+
}
|
|
80
|
+
let journal = await readJson(journalPath);
|
|
81
|
+
if (!journal) {
|
|
82
|
+
const rawQueue = await readBytes(queuePath);
|
|
83
|
+
const live = latestRows(rawQueue);
|
|
84
|
+
for (const [key, bucket] of Object.entries(cursors.hourly?.buckets || {})) {
|
|
85
|
+
if (!key.startsWith("kimi|") || live.has(key)) continue;
|
|
86
|
+
const [, model, hour_start] = key.split("|");
|
|
87
|
+
live.set(key, { source: "kimi", model, hour_start, ...bucket.totals });
|
|
88
|
+
}
|
|
89
|
+
if (![...live.values()].some((row) => Number(row.total_tokens) > 0)) {
|
|
90
|
+
(cursors.migrations ||= {})[KIMI_MODEL_REPAIR_KEY] = { status: "noop", appliedAt: new Date().toISOString() };
|
|
91
|
+
return { repaired: false, pendingCompaction: false };
|
|
92
|
+
}
|
|
93
|
+
const files = [...new Set([...legacyFiles, ...officialFiles])];
|
|
94
|
+
const discovered = new Set(files);
|
|
95
|
+
const previousFiles = Object.entries(cursors.kimi?.fileOffsets || {}).concat(Object.entries(cursors.kimiCode?.fileOffsets || {}));
|
|
96
|
+
if (!officialFiles.length || previousFiles.some(([file]) => !discovered.has(file))) {
|
|
97
|
+
return { repaired: false, pendingCompaction: false, deferred: "missing-source" };
|
|
98
|
+
}
|
|
99
|
+
const signatures = new Map();
|
|
100
|
+
try {
|
|
101
|
+
for (const file of files) signatures.set(file, await fileSignature(file));
|
|
102
|
+
if (previousFiles.some(([file, prior]) => signatures.get(file).size < Number(prior.size || 0))) {
|
|
103
|
+
return { repaired: false, pendingCompaction: false, deferred: "truncated-source" };
|
|
104
|
+
}
|
|
105
|
+
} catch {
|
|
106
|
+
return { repaired: false, pendingCompaction: false, deferred: "unreadable-source" };
|
|
107
|
+
}
|
|
108
|
+
const scratch = await fs.mkdtemp(path.join(path.dirname(queuePath), ".kimi-model-repair-"));
|
|
109
|
+
const rebuilt = { version: 1, hourly: { version: 3, buckets: {}, groupQueued: {} } };
|
|
110
|
+
let corrected;
|
|
111
|
+
try {
|
|
112
|
+
const scratchQueue = path.join(scratch, "queue.jsonl");
|
|
113
|
+
await parseLegacy({ wireFiles: legacyFiles, cursors: rebuilt, queuePath: scratchQueue, env });
|
|
114
|
+
await parseOfficial({ wireFiles: officialFiles, cursors: rebuilt, queuePath: scratchQueue, env });
|
|
115
|
+
corrected = latestRows(await readBytes(scratchQueue));
|
|
116
|
+
for (const file of files) {
|
|
117
|
+
if (JSON.stringify(await fileSignature(file)) !== JSON.stringify(signatures.get(file))) {
|
|
118
|
+
return { repaired: false, pendingCompaction: false, deferred: "source-changed" };
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
} finally { await fs.rm(scratch, { recursive: true, force: true }); }
|
|
122
|
+
// Missing/corrupt history or newly appended usage must first settle through
|
|
123
|
+
// ordinary sync. A model-only repair may never change per-hour token totals.
|
|
124
|
+
if (!sameTotals(live, corrected)) return { repaired: false, pendingCompaction: false, deferred: "totals-not-conserved" };
|
|
125
|
+
const currentQueue = await readBytes(queuePath);
|
|
126
|
+
if (!currentQueue.equals(rawQueue)) return { repaired: false, pendingCompaction: false, deferred: "queue-changed" };
|
|
127
|
+
const state = await readJson(queueStatePath) || {};
|
|
128
|
+
const offset = Number(state.offset || 0);
|
|
129
|
+
if (!Number.isSafeInteger(offset) || offset < 0 || offset > rawQueue.length || (offset > 0 && rawQueue[offset - 1] !== 10)) {
|
|
130
|
+
return { repaired: false, pendingCompaction: false, deferred: "invalid-upload-offset" };
|
|
131
|
+
}
|
|
132
|
+
const corrections = [];
|
|
133
|
+
for (const [key, old] of live) {
|
|
134
|
+
if (!corrected.has(key)) corrections.push({ source: "kimi", model: old.model, hour_start: old.hour_start,
|
|
135
|
+
...Object.fromEntries(TOKEN_FIELDS.map((field) => [field, 0])), billable_total_tokens: 0, conversation_count: 0 });
|
|
136
|
+
}
|
|
137
|
+
for (const [key, row] of corrected) if (!sameSnapshot(live.get(key), row)) corrections.push(row);
|
|
138
|
+
journal = { version: 1, uploadedOffset: offset, uploadedPrefixSha256: digest(rawQueue.subarray(0, offset)),
|
|
139
|
+
rows: corrections, kimi: rebuilt.kimi || {}, kimiCode: rebuilt.kimiCode || {}, hourly: rebuilt.hourly,
|
|
140
|
+
preparedAt: new Date().toISOString() };
|
|
141
|
+
await writeJournal(journalPath, journal);
|
|
142
|
+
}
|
|
143
|
+
const rawQueue = await readBytes(queuePath);
|
|
144
|
+
const state = await readJson(queueStatePath) || {};
|
|
145
|
+
if (journal.version !== 1 || !Number.isSafeInteger(journal.uploadedOffset) || journal.uploadedOffset < 0
|
|
146
|
+
|| rawQueue.length < journal.uploadedOffset || Number(state.offset || 0) < journal.uploadedOffset
|
|
147
|
+
|| digest(rawQueue.subarray(0, journal.uploadedOffset)) !== journal.uploadedPrefixSha256) {
|
|
148
|
+
return { repaired: false, pendingCompaction: false, deferred: "uploaded-prefix-changed" };
|
|
149
|
+
}
|
|
150
|
+
if (journal.rows.length) {
|
|
151
|
+
// A prior append can have stopped halfway through a JSON line. Preserve
|
|
152
|
+
// those bytes, terminate that incomplete record, then append complete LWW
|
|
153
|
+
// snapshots again. The journal bypasses conservation checks on partial work.
|
|
154
|
+
const separator = rawQueue.length && rawQueue.at(-1) !== 10 ? "\n" : "";
|
|
155
|
+
await fs.appendFile(queuePath, separator + journal.rows.map(JSON.stringify).join("\n") + "\n");
|
|
156
|
+
}
|
|
157
|
+
const hourly = (cursors.hourly ||= { version: 3, buckets: {}, groupQueued: {} });
|
|
158
|
+
for (const field of ["buckets", "groupQueued"]) {
|
|
159
|
+
hourly[field] ||= {};
|
|
160
|
+
for (const key of Object.keys(hourly[field])) if (key.startsWith("kimi|")) delete hourly[field][key];
|
|
161
|
+
for (const [key, value] of Object.entries(journal.hourly?.[field] || {})) if (key.startsWith("kimi|")) hourly[field][key] = value;
|
|
162
|
+
}
|
|
163
|
+
cursors.kimi = journal.kimi;
|
|
164
|
+
cursors.kimiCode = journal.kimiCode;
|
|
165
|
+
(cursors.migrations ||= {})[KIMI_MODEL_REPAIR_KEY] = {
|
|
166
|
+
status: "applied", appliedAt: new Date().toISOString(), correctedBuckets: journal.rows.length,
|
|
167
|
+
pendingCompaction: journal.rows.length > 0,
|
|
168
|
+
};
|
|
169
|
+
// Leave the journal until the next call observes the persisted marker.
|
|
170
|
+
return { repaired: true, pendingCompaction: journal.rows.length > 0 };
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
module.exports = { KIMI_MODEL_REPAIR_KEY, repairKimiModelAttribution };
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const crypto = require("node:crypto");
|
|
4
|
+
const fs = require("node:fs");
|
|
5
|
+
const path = require("node:path");
|
|
6
|
+
const { physicalJsonlRecords } = require("./jsonl-lines");
|
|
7
|
+
const { resolveKimiDefaultModel } = require("./rollout");
|
|
8
|
+
const { mergePerformance } = require("./session-performance");
|
|
9
|
+
|
|
10
|
+
function readObject(file) {
|
|
11
|
+
try { return JSON.parse(fs.readFileSync(file, "utf8")); } catch { return {}; }
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function kimiSessionLocation(file) {
|
|
15
|
+
const official = path.basename(path.dirname(path.dirname(file))) === "agents";
|
|
16
|
+
const directory = official ? path.dirname(path.dirname(path.dirname(file))) : path.dirname(file);
|
|
17
|
+
const id = path.basename(directory);
|
|
18
|
+
const imported = official ? readObject(path.join(directory, "state.json"))?.custom : null;
|
|
19
|
+
// The official migrator keeps the legacy UUID under ses_<UUID>. Group its
|
|
20
|
+
// message-only mirror with the original wire, retaining any new usage tail.
|
|
21
|
+
const identity = imported?.imported_from_kimi_cli && typeof imported.kimi_cli_session_id === "string"
|
|
22
|
+
? imported.kimi_cli_session_id : id.replace(/^(?:ses_|session_)(?=[0-9a-f-]{36}$)/i, "");
|
|
23
|
+
return { directory, id, identity, official, main: !official || path.basename(path.dirname(file)) === "main" };
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
function kimiSessionDependencies(files) {
|
|
27
|
+
return [...new Set(files.flatMap((file) => {
|
|
28
|
+
const location = kimiSessionLocation(file);
|
|
29
|
+
const root = path.dirname(path.dirname(path.dirname(location.directory)));
|
|
30
|
+
return [path.join(location.directory, "state.json"), path.join(root, "config.toml"), path.join(root, "kimi.json")];
|
|
31
|
+
}))];
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function discoverKimiWireFiles(roots, incomplete) {
|
|
35
|
+
const files = new Set();
|
|
36
|
+
function walk(directory, depth = 0) {
|
|
37
|
+
if (depth > 5) return;
|
|
38
|
+
let entries;
|
|
39
|
+
try { entries = fs.readdirSync(directory, { withFileTypes: true }); } catch (error) {
|
|
40
|
+
if (error.code !== "ENOENT") incomplete.push({ path: directory, error: error.code || "unreadable" });
|
|
41
|
+
return;
|
|
42
|
+
}
|
|
43
|
+
for (const entry of entries) {
|
|
44
|
+
if (entry.name.startsWith(".")) continue;
|
|
45
|
+
const file = path.join(directory, entry.name);
|
|
46
|
+
if (entry.isDirectory()) walk(file, depth + 1);
|
|
47
|
+
else if (entry.isFile() && entry.name === "wire.jsonl") files.add(file);
|
|
48
|
+
}
|
|
49
|
+
}
|
|
50
|
+
for (const root of roots) walk(path.join(root, "sessions"));
|
|
51
|
+
return [...files].sort();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
const count = (value) => Number.isFinite(Number(value)) ? Math.max(0, Math.floor(Number(value))) : 0;
|
|
55
|
+
function kimiUsage(usage, legacy) {
|
|
56
|
+
if (!usage || typeof usage !== "object") return null;
|
|
57
|
+
let input, cached, created, output;
|
|
58
|
+
// Same column semantics as parseKimiIncremental / parseKimiCodeIncremental.
|
|
59
|
+
if (legacy || usage.inputOther != null) {
|
|
60
|
+
input = count(legacy ? usage.input_other : usage.inputOther);
|
|
61
|
+
cached = count(legacy ? usage.input_cache_read : usage.inputCacheRead);
|
|
62
|
+
created = count(legacy ? usage.input_cache_creation : usage.inputCacheCreation);
|
|
63
|
+
output = count(usage.output);
|
|
64
|
+
} else {
|
|
65
|
+
created = count(usage.cache_creation_input_tokens);
|
|
66
|
+
cached = count(usage.cache_read_input_tokens ?? usage.input_tokens_details?.cached_tokens);
|
|
67
|
+
input = Math.max(0, count(usage.input_tokens) - (usage.cache_read_input_tokens != null ? 0 : cached));
|
|
68
|
+
output = count(usage.output_tokens);
|
|
69
|
+
}
|
|
70
|
+
return { input_tokens: input, cached_input_tokens: cached, cache_creation_input_tokens: created,
|
|
71
|
+
output_tokens: output, reasoning_output_tokens: 0, total_tokens: input + cached + created + output };
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const modelAlias = (value) => typeof value === "string" && value.trim() ? value.trim().split("/").pop() : null;
|
|
75
|
+
const EDIT_TOOLS = new Set(["edit", "write", "multiedit", "notebookedit", "strreplacefile", "writefile", "apply_patch"]);
|
|
76
|
+
const AGENT_TOOLS = new Set(["agent", "agentswarm", "task"]);
|
|
77
|
+
const hash = (value) => crypto.createHash("sha256").update(value).digest("hex");
|
|
78
|
+
|
|
79
|
+
// Reads both wire protocols into metadata only. A full inventory shares its
|
|
80
|
+
// usage-ID set, as sync does, so copied/forked step records count only once.
|
|
81
|
+
async function readKimiSessions(files) {
|
|
82
|
+
const groups = new Map();
|
|
83
|
+
for (const file of [...new Set(files)]) {
|
|
84
|
+
const location = kimiSessionLocation(file);
|
|
85
|
+
const group = groups.get(location.identity) || { identity: location.identity, files: [], createdAt: Infinity };
|
|
86
|
+
const state = location.official ? readObject(path.join(location.directory, "state.json")) : {};
|
|
87
|
+
group.createdAt = Math.min(group.createdAt, Number(state.createdAt) || Infinity);
|
|
88
|
+
group.files.push({ file, ...location, state });
|
|
89
|
+
groups.set(location.identity, group);
|
|
90
|
+
}
|
|
91
|
+
const seenUsage = new Set();
|
|
92
|
+
const seenSignals = new Set();
|
|
93
|
+
const rows = [];
|
|
94
|
+
for (const group of [...groups.values()].sort((a, b) => a.createdAt - b.createdAt || a.identity.localeCompare(b.identity))) {
|
|
95
|
+
const tokens = kimiUsage({}, true);
|
|
96
|
+
const modelUsage = new Map();
|
|
97
|
+
const buckets = new Map();
|
|
98
|
+
const times = new Set();
|
|
99
|
+
let turns = 0, edits = 0, retries = 0, agents = 0, errors = 0;
|
|
100
|
+
let lastPrompt = null;
|
|
101
|
+
let cwd = null;
|
|
102
|
+
let resumeId = null;
|
|
103
|
+
const modelsForEdits = new Map();
|
|
104
|
+
// Legacy first preserves migrated user turns; context.append_message in
|
|
105
|
+
// the imported wire is history, never another prompt or usage event.
|
|
106
|
+
group.files.sort((a, b) => Number(a.official) - Number(b.official) || Number(b.main) - Number(a.main) || a.file.localeCompare(b.file));
|
|
107
|
+
for (const location of group.files) {
|
|
108
|
+
const root = path.dirname(path.dirname(path.dirname(location.directory)));
|
|
109
|
+
let model = resolveKimiDefaultModel({ KIMI_HOME: root });
|
|
110
|
+
if (location.official && location.main) {
|
|
111
|
+
resumeId = location.id;
|
|
112
|
+
if (typeof location.state.workDir === "string") cwd = location.state.workDir;
|
|
113
|
+
}
|
|
114
|
+
if (!location.official && !cwd) {
|
|
115
|
+
const workspaceHash = path.basename(path.dirname(location.directory));
|
|
116
|
+
const workDirs = readObject(path.join(root, "kimi.json"))?.work_dirs;
|
|
117
|
+
const workspace = Array.isArray(workDirs) && workDirs.find((row) => typeof row?.path === "string"
|
|
118
|
+
&& crypto.createHash("md5").update(row.path).digest("hex") === workspaceHash);
|
|
119
|
+
if (workspace) cwd = workspace.path;
|
|
120
|
+
}
|
|
121
|
+
let turnOpen = false, hadEdit = false, turnModel = model;
|
|
122
|
+
const closeTurn = () => {
|
|
123
|
+
if (hadEdit) { edits++; modelsForEdits.set(turnModel, (modelsForEdits.get(turnModel) || 0) + 1); }
|
|
124
|
+
hadEdit = false; turnOpen = false;
|
|
125
|
+
};
|
|
126
|
+
const input = fs.createReadStream(location.file);
|
|
127
|
+
for await (const { line, terminated } of physicalJsonlRecords(input)) {
|
|
128
|
+
if (!terminated || !line) continue;
|
|
129
|
+
let entry;
|
|
130
|
+
try { entry = JSON.parse(line); } catch { continue; }
|
|
131
|
+
if (!entry || typeof entry !== "object") continue;
|
|
132
|
+
const message = entry.message && typeof entry.message === "object" ? entry.message : null;
|
|
133
|
+
const event = entry.type === "context.append_loop_event" ? entry.event : entry;
|
|
134
|
+
const timestampMs = Number(location.official ? entry.time ?? event?.time : entry.timestamp ?? message?.payload?.timestamp) * (location.official ? 1 : 1000);
|
|
135
|
+
const timestamp = Number.isFinite(timestampMs) && timestampMs > 0 && timestampMs <= 8640000000000000
|
|
136
|
+
? new Date(timestampMs).toISOString() : null;
|
|
137
|
+
if (timestamp) times.add(timestampMs);
|
|
138
|
+
if (["config.update", "profile.bind", "llm.request"].includes(entry.type)) {
|
|
139
|
+
model = modelAlias(entry.modelAlias || entry.model) || model;
|
|
140
|
+
if (location.main && typeof (entry.cwd || entry.environmentDisclosure?.cwd) === "string") cwd = entry.cwd || entry.environmentDisclosure.cwd;
|
|
141
|
+
}
|
|
142
|
+
const isLegacyUsage = message?.type === "StatusUpdate";
|
|
143
|
+
const usage = isLegacyUsage ? message.payload?.token_usage : event?.type === "step.end" ? event.usage : null;
|
|
144
|
+
const usageId = isLegacyUsage ? message.payload?.message_id : event?.uuid;
|
|
145
|
+
if (usage && typeof usageId === "string" && timestamp) {
|
|
146
|
+
const ids = [isLegacyUsage ? `message:${usageId}` : `step:${usageId}`];
|
|
147
|
+
if (!isLegacyUsage && typeof event.messageId === "string") ids.push(`message:${event.messageId}`);
|
|
148
|
+
const delta = kimiUsage(usage, isLegacyUsage);
|
|
149
|
+
if (delta.total_tokens > 0 && !ids.some((id) => seenUsage.has(id))) {
|
|
150
|
+
for (const id of ids) seenUsage.add(id);
|
|
151
|
+
const modelRow = modelUsage.get(model) || { model, ...kimiUsage({}, true), usage_events: 0 };
|
|
152
|
+
const bucketTime = new Date(Math.floor(timestampMs / 60000) * 60000).toISOString();
|
|
153
|
+
const key = `${model}\0${bucketTime}`;
|
|
154
|
+
const bucket = buckets.get(key) || { model, timestamp: bucketTime, ...kimiUsage({}, true), usage_events: 0 };
|
|
155
|
+
for (const field of Object.keys(delta)) { tokens[field] += delta[field]; modelRow[field] += delta[field]; bucket[field] += delta[field]; }
|
|
156
|
+
modelRow.usage_events++; bucket.usage_events++;
|
|
157
|
+
const streamMs = count(event.llmStreamDurationMs);
|
|
158
|
+
const firstMs = Number(event.llmFirstTokenLatencyMs);
|
|
159
|
+
const performance = {
|
|
160
|
+
...(delta.output_tokens >= 200 && streamMs >= 1000 && streamMs <= 3600000
|
|
161
|
+
? { estimated_output_tokens: delta.output_tokens, estimated_duration_ms: streamMs, estimated_request_count: 1 } : {}),
|
|
162
|
+
...(event.llmFirstTokenLatencyMs != null && Number.isFinite(firstMs) && firstMs >= 0
|
|
163
|
+
? { first_response_total_ms: firstMs, first_response_sample_count: 1 } : {}),
|
|
164
|
+
};
|
|
165
|
+
modelRow.performance = mergePerformance(modelRow.performance, performance);
|
|
166
|
+
modelUsage.set(model, modelRow); buckets.set(key, bucket);
|
|
167
|
+
turnModel = model;
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
if (!location.main) continue;
|
|
171
|
+
const signalId = hash(line);
|
|
172
|
+
if (seenSignals.has(signalId)) continue;
|
|
173
|
+
seenSignals.add(signalId);
|
|
174
|
+
const prompt = message?.type === "TurnBegin" ? message.payload?.user_input
|
|
175
|
+
: entry.type === "turn.prompt" && (!entry.origin?.kind || entry.origin.kind === "user") ? entry.input : null;
|
|
176
|
+
if (prompt != null) {
|
|
177
|
+
closeTurn(); turns++; turnOpen = true; turnModel = model;
|
|
178
|
+
// Fingerprints live only for this scan; no text or hash is persisted.
|
|
179
|
+
const fingerprint = hash(JSON.stringify(prompt));
|
|
180
|
+
if (lastPrompt && fingerprint === lastPrompt) retries++;
|
|
181
|
+
lastPrompt = fingerprint;
|
|
182
|
+
}
|
|
183
|
+
const tool = message?.type === "ToolCall" ? message.payload?.function?.name : event?.type === "tool.call" ? event.name : null;
|
|
184
|
+
if (typeof tool === "string") {
|
|
185
|
+
const name = tool.toLowerCase();
|
|
186
|
+
if (turnOpen && EDIT_TOOLS.has(name)) hadEdit = true;
|
|
187
|
+
if (AGENT_TOOLS.has(name)) agents++;
|
|
188
|
+
}
|
|
189
|
+
if (entry.type === "turn.step.retrying" || message?.type === "StepInterrupted") errors++;
|
|
190
|
+
if (message?.type === "TurnEnd" || entry.type === "turn.ended") closeTurn();
|
|
191
|
+
}
|
|
192
|
+
closeTurn();
|
|
193
|
+
}
|
|
194
|
+
const orderedTimes = [...times].sort((a, b) => a - b);
|
|
195
|
+
let activeMs = 0;
|
|
196
|
+
for (let i = 1; i < orderedTimes.length; i++) {
|
|
197
|
+
const gap = orderedTimes[i] - orderedTimes[i - 1];
|
|
198
|
+
if (gap <= 30 * 60 * 1000) activeMs += gap;
|
|
199
|
+
}
|
|
200
|
+
const models = [...modelUsage.values()].map((row) => ({ ...row, edit_turns: modelsForEdits.get(row.model) || 0 }));
|
|
201
|
+
if (!tokens.total_tokens && !turns) continue;
|
|
202
|
+
rows.push({ identity: group.identity, files: group.files.map((row) => row.file), session_id: resumeId,
|
|
203
|
+
title: null, project_ref: cwd, model: models.length === 1 ? models[0].model : "unknown", model_usage: models,
|
|
204
|
+
usage_buckets: [...buckets.values()], tokens, turns, edit_turns: edits, retry_turns: retries,
|
|
205
|
+
subagent_calls: agents, subagent_types: {}, error_count: errors,
|
|
206
|
+
usage_events: models.reduce((sum, row) => sum + row.usage_events, 0),
|
|
207
|
+
started_at: orderedTimes.length ? new Date(orderedTimes[0]).toISOString() : null,
|
|
208
|
+
ended_at: orderedTimes.length ? new Date(orderedTimes.at(-1)).toISOString() : null, active_ms: activeMs,
|
|
209
|
+
performance: mergePerformance(...models.map((row) => row.performance)),
|
|
210
|
+
provenance: { source: "local-session-log", confidence: "observed", retry_confidence: "inferred", content_retained: false, usage: "kimi-wire-metadata" },
|
|
211
|
+
});
|
|
212
|
+
}
|
|
213
|
+
return rows;
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
module.exports = { discoverKimiWireFiles, kimiSessionDependencies, readKimiSessions };
|
package/src/lib/local-api.js
CHANGED
|
@@ -13,6 +13,7 @@ const {
|
|
|
13
13
|
normalizeUsageScope,
|
|
14
14
|
} = require("./source-metadata");
|
|
15
15
|
const { accountSlugFor, fetchAccountUsage, mintAccessToken, invalidateCloudAccountPayloadCache, getCloudAccountSessionGeneration } = require("./cloud-account");
|
|
16
|
+
const { resetCloudDeviceUsage, readCloudUsageReset } = require("./cloud-usage-reset");
|
|
16
17
|
const { getOrCreateMachineId, computeStableMachineId } = require("./machine-id");
|
|
17
18
|
const { functionUrlFor, fetchFunctionResponse } = require("./function-url");
|
|
18
19
|
const { createCloudDeviceTokenStore } = require("./cloud-device-token");
|
|
@@ -2162,6 +2163,39 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2162
2163
|
return true;
|
|
2163
2164
|
}
|
|
2164
2165
|
|
|
2166
|
+
if (p === "/functions/tokentracker-cloud-usage-reset") {
|
|
2167
|
+
if (!isAuthorizedLocalMutation(req) || req.headers["sec-fetch-site"] === "cross-site") { json(res, { error: "Unauthorized" }, 401); return true; }
|
|
2168
|
+
try {
|
|
2169
|
+
if (String(req.method).toUpperCase() === "GET") {
|
|
2170
|
+
json(res, { pending: Boolean(readCloudUsageReset(trackerDataDir)) }); return true;
|
|
2171
|
+
}
|
|
2172
|
+
if (String(req.method).toUpperCase() !== "POST") { json(res, { error: "Method Not Allowed" }, 405); return true; }
|
|
2173
|
+
const body = await readJsonBody(req);
|
|
2174
|
+
const session = localCloudSession;
|
|
2175
|
+
if (!getCloudSyncPref() || !session || body.cloudSessionId !== session.id || body.expectedOwnerId !== session.ownerId) {
|
|
2176
|
+
json(res, { code: "auth_session_changed" }, 409); return true;
|
|
2177
|
+
}
|
|
2178
|
+
const metadata = localSyncDeviceMetadata.get(session.token);
|
|
2179
|
+
const accessToken = String(req.headers.authorization || "").replace(/^Bearer /i, "").trim();
|
|
2180
|
+
const runtime = assertInsforgeRuntime(getRuntime());
|
|
2181
|
+
let claims;
|
|
2182
|
+
try { claims = JSON.parse(Buffer.from(accessToken.split(".")[1], "base64url").toString("utf8")); } catch { /* rejected below */ }
|
|
2183
|
+
if (claims?.sub !== session.ownerId || !Number.isFinite(claims?.exp) || claims.exp * 1000 <= Date.now()) {
|
|
2184
|
+
json(res, { code: "auth_required" }, 401); return true;
|
|
2185
|
+
}
|
|
2186
|
+
const isCurrent = () => getCloudSyncPref() && localCloudSession === session && session.generation === localCloudGeneration &&
|
|
2187
|
+
claims.exp * 1000 > Date.now() && getRuntime().baseUrl === session.baseUrl;
|
|
2188
|
+
const result = await resetCloudDeviceUsage({ trackerDir: trackerDataDir, ownerId: session.ownerId,
|
|
2189
|
+
deviceId: metadata?.deviceId, baseUrl: session.baseUrl, anonKey: runtime.anonKey, accessToken, isCurrent });
|
|
2190
|
+
if (!isCurrent()) { json(res, { code: "auth_session_changed" }, 409); return true; }
|
|
2191
|
+
clearLocalSyncDeviceTokenCache();
|
|
2192
|
+
invalidateAfterAccountUpload();
|
|
2193
|
+
invalidateCloudAccountPayloadCache();
|
|
2194
|
+
json(res, { ...result, pending: true });
|
|
2195
|
+
} catch (error) { json(res, { error: error.code || "cloud_usage_reset_failed", code: error.code || null }, error.status || 502); }
|
|
2196
|
+
return true;
|
|
2197
|
+
}
|
|
2198
|
+
|
|
2165
2199
|
// --- local-sync (POST) ---
|
|
2166
2200
|
if (p === "/functions/tokentracker-local-sync") {
|
|
2167
2201
|
if (String(req.method || "GET").toUpperCase() !== "POST") {
|
|
@@ -2263,10 +2297,20 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2263
2297
|
if (extraEnv.TOKENTRACKER_DEVICE_TOKEN && uploadGeneration !== localCloudGeneration) {
|
|
2264
2298
|
json(res, { ok: false, code: "auth_session_changed" }, 409); return true;
|
|
2265
2299
|
}
|
|
2300
|
+
if (extraEnv.TOKENTRACKER_DEVICE_TOKEN) {
|
|
2301
|
+
const metadata = localSyncDeviceMetadata.get(extraEnv.TOKENTRACKER_DEVICE_TOKEN);
|
|
2302
|
+
if (metadata?.deviceId) extraEnv.TOKENTRACKER_CLOUD_DEVICE_ID = metadata.deviceId;
|
|
2303
|
+
}
|
|
2266
2304
|
await Promise.all([...activeCloudSyncs].filter(controller => controller.signal.aborted).map(controller => controller.done).filter(Boolean));
|
|
2267
2305
|
if (extraEnv.TOKENTRACKER_DEVICE_TOKEN && uploadGeneration !== localCloudGeneration) throw Object.assign(new Error("Account session changed"), { code: "auth_session_changed", status: 409 });
|
|
2268
2306
|
if (extraEnv.TOKENTRACKER_DEVICE_TOKEN && localSyncDeviceTokenUserId) {
|
|
2269
|
-
const
|
|
2307
|
+
const uploadBaseUrl = localSyncBaseUrl || getRuntime().baseUrl;
|
|
2308
|
+
const reset = readCloudUsageReset(trackerDataDir);
|
|
2309
|
+
if (reset && (reset.ownerId !== localSyncDeviceTokenUserId || reset.baseUrl.replace(/\/$/, "") !== uploadBaseUrl.replace(/\/$/, "") ||
|
|
2310
|
+
(extraEnv.TOKENTRACKER_CLOUD_DEVICE_ID && reset.deviceId !== extraEnv.TOKENTRACKER_CLOUD_DEVICE_ID))) {
|
|
2311
|
+
json(res, { ok: false, code: "cloud_usage_reset_owner_changed" }, 409); return true;
|
|
2312
|
+
}
|
|
2313
|
+
const binding = JSON.stringify({ ownerId: localSyncDeviceTokenUserId, baseUrl: uploadBaseUrl });
|
|
2270
2314
|
const ownerPath = path.join(path.dirname(qp), "cloud-upload-owner.json");
|
|
2271
2315
|
let previous = "";
|
|
2272
2316
|
try { previous = fs.readFileSync(ownerPath, "utf8"); } catch { /* an unbound legacy cursor cannot prove its previous owner */ }
|
|
@@ -2279,6 +2323,7 @@ function createLocalApiHandler({ queuePath, serverVersion = null, trackerDataDir
|
|
|
2279
2323
|
const controller = new AbortController();
|
|
2280
2324
|
if (extraEnv.TOKENTRACKER_DEVICE_TOKEN) activeCloudSyncs.add(controller);
|
|
2281
2325
|
const syncStartedAt = Date.now();
|
|
2326
|
+
if (localSyncDeviceTokenUserId) extraEnv.TOKENTRACKER_CLOUD_OWNER_ID = localSyncDeviceTokenUserId;
|
|
2282
2327
|
extraEnv.TOKENTRACKER_LOCAL_SYNC_ATTEMPT_ID = crypto.randomUUID();
|
|
2283
2328
|
const syncPromise = runSyncCommand(extraEnv, {
|
|
2284
2329
|
signal: controller.signal,
|
package/src/lib/pricing/index.js
CHANGED
|
@@ -250,7 +250,8 @@ function computeRowCost(row) {
|
|
|
250
250
|
row.source === "acode" ||
|
|
251
251
|
row.source === "every-code" ||
|
|
252
252
|
row.source === "omo" ||
|
|
253
|
-
row.source === "cline"
|
|
253
|
+
row.source === "cline" ||
|
|
254
|
+
row.source === "muse";
|
|
254
255
|
const reasoningCost = reasoningIncludedInOutput
|
|
255
256
|
? 0
|
|
256
257
|
: (row.reasoning_output_tokens || 0) * (pricing.output || 0);
|