tokentracker-cli 1.1.3 → 1.1.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (115) hide show
  1. package/README.de.md +5 -4
  2. package/README.ja.md +5 -4
  3. package/README.ko.md +5 -4
  4. package/README.md +5 -4
  5. package/README.zh-CN.md +5 -4
  6. package/dashboard/dist/assets/{AchievementBadge-dt_UDU_h.js → AchievementBadge-BGA-HT_B.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-B5DJ3S9l.js → AchievementsPage-CtRh5AjH.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-CDxcyig2.js → AchievementsSection-BarRcKaN.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-Cp98zNn2.js → BadgeDetailModal-Bg0BDAN2.js} +1 -1
  10. package/dashboard/dist/assets/{BotAnimated-CR7MiUpo.js → BotAnimated-By9J3-dn.js} +1 -1
  11. package/dashboard/dist/assets/{Card-C8IdDvlg.js → Card-CIoJYi2j.js} +1 -1
  12. package/dashboard/dist/assets/{ClawdAnimated-COeTlW8z.js → ClawdAnimated-Dkvc2xe8.js} +2 -2
  13. package/dashboard/dist/assets/{CommandPalette-Co9k2CCc.js → CommandPalette-BssDtHRl.js} +1 -1
  14. package/dashboard/dist/assets/{CommunityStatsModal-C6XQeRgw.js → CommunityStatsModal-CQrgLlDb.js} +1 -1
  15. package/dashboard/dist/assets/{ConfirmModal-Ai4182O-.js → ConfirmModal-C3hBPgYc.js} +1 -1
  16. package/dashboard/dist/assets/{Controls-BbKzN-Pe.js → Controls-BSW6a1HS.js} +1 -1
  17. package/dashboard/dist/assets/DashboardPage-Dhv7fejy.js +1 -0
  18. package/dashboard/dist/assets/{DevicePage-CZbrtJmb.js → DevicePage-D5eZn0wo.js} +1 -1
  19. package/dashboard/dist/assets/{DialogClose-DMl8tAoX.js → DialogClose-BltQpaJo.js} +1 -1
  20. package/dashboard/dist/assets/DialogDescription-Cof52Icj.js +1 -0
  21. package/dashboard/dist/assets/{DialogTitle-BJxpp2t-.js → DialogTitle-CVddcRPZ.js} +1 -1
  22. package/dashboard/dist/assets/{FadeIn-hU_MXl6R.js → FadeIn-goPv73MT.js} +1 -1
  23. package/dashboard/dist/assets/{HeaderGithubStar-CM5RSxxN.js → HeaderGithubStar-BDBrrkzt.js} +1 -1
  24. package/dashboard/dist/assets/{HoverTooltip-BZKfQyZQ.js → HoverTooltip-qyMezXOs.js} +1 -1
  25. package/dashboard/dist/assets/{IpCheckPage-BjDx-zja.js → IpCheckPage-Ces8JIPb.js} +1 -1
  26. package/dashboard/dist/assets/{LandingPage-CAt8BCDv.js → LandingPage-B2TmLGno.js} +2 -2
  27. package/dashboard/dist/assets/{LeaderboardAvatar-DiCrakjI.js → LeaderboardAvatar-CVVRoNua.js} +1 -1
  28. package/dashboard/dist/assets/{LeaderboardPage-Dj3pHv6U.js → LeaderboardPage-xtt-bOY8.js} +2 -2
  29. package/dashboard/dist/assets/{LeaderboardProfileModal-DEWbcP9p.js → LeaderboardProfileModal-BM4d_I_r.js} +3 -3
  30. package/dashboard/dist/assets/{LeaderboardProfilePage-CTbWHQkY.js → LeaderboardProfilePage-BWqhFLQX.js} +1 -1
  31. package/dashboard/dist/assets/{LimitsPage-V-98OUhF.js → LimitsPage-BZL4Pcl8.js} +1 -1
  32. package/dashboard/dist/assets/{LocalOnlyNotice-a4PqtUZY.js → LocalOnlyNotice-D4gnOEJj.js} +1 -1
  33. package/dashboard/dist/assets/{LoginCard-oD1A2Ru8.js → LoginCard-CB17FQ5w.js} +1 -1
  34. package/dashboard/dist/assets/{LoginModal-D8aSmxok.js → LoginModal-BI-O1rH4.js} +1 -1
  35. package/dashboard/dist/assets/{LoginPage-DIjhBv2X.js → LoginPage-DnGzsvJ4.js} +1 -1
  36. package/dashboard/dist/assets/{PetPage-D7601BTX.js → PetPage-NRnIKMaJ.js} +1 -1
  37. package/dashboard/dist/assets/{PopoverPopup-aPnmKopO.js → PopoverPopup-DHoK6UJZ.js} +1 -1
  38. package/dashboard/dist/assets/{ProviderIcon-rwyfcNlT.js → ProviderIcon-BTkdx9o2.js} +1 -1
  39. package/dashboard/dist/assets/{ResetPasswordPage-AF3SBgW2.js → ResetPasswordPage-C6K0w318.js} +1 -1
  40. package/dashboard/dist/assets/{SegmentedControl-fnlScNMT.js → SegmentedControl-gEoWioBE.js} +1 -1
  41. package/dashboard/dist/assets/{Select-DDjC3RjB.js → Select-CI-qtEYu.js} +1 -1
  42. package/dashboard/dist/assets/{SelectItemText-VhBWE2O8.js → SelectItemText-q-yhzzjw.js} +1 -1
  43. package/dashboard/dist/assets/{ServiceStatusPage-BL3qqEsp.js → ServiceStatusPage-C4xD1CEe.js} +1 -1
  44. package/dashboard/dist/assets/{SessionsPage-DXYiuP2x.js → SessionsPage-CS0Yun2z.js} +1 -1
  45. package/dashboard/dist/assets/{SettingsPage-DTeA_ahr.js → SettingsPage-GVY_ktz4.js} +2 -2
  46. package/dashboard/dist/assets/{SkillsPage-CtTRwVQH.js → SkillsPage-COPRL95O.js} +1 -1
  47. package/dashboard/dist/assets/{TokenGalaxy-CifK91V7.js → TokenGalaxy-Czi0Ic48.js} +1 -1
  48. package/dashboard/dist/assets/{ToolbarRootContext-ClGAIYoa.js → ToolbarRootContext-BZkTiBJB.js} +1 -1
  49. package/dashboard/dist/assets/{TrendMonitor-D1evFHRM.js → TrendMonitor-CnIsuxV-.js} +1 -1
  50. package/dashboard/dist/assets/{WidgetsPage-Cq4RYJDE.js → WidgetsPage-OdX_Fsul.js} +1 -1
  51. package/dashboard/dist/assets/{WrappedPage-CoC5RJH7.js → WrappedPage-h9N63-F8.js} +1 -1
  52. package/dashboard/dist/assets/__vite-browser-external-BFNWaGsk.js +45 -0
  53. package/dashboard/dist/assets/{agent-logos-QLdA9OeA.js → agent-logos-BDiPxrST.js} +1 -1
  54. package/dashboard/dist/assets/{arrow-up-right-BYLgB3bH.js → arrow-up-right-DtFQUeJG.js} +1 -1
  55. package/dashboard/dist/assets/{check-DfnIvqZD.js → check-C35onaNL.js} +1 -1
  56. package/dashboard/dist/assets/{composite--JllTpIX.js → composite-qxBzts0W.js} +1 -1
  57. package/dashboard/dist/assets/{copy-ClKVBIgD.js → copy-D_D71TLc.js} +1 -1
  58. package/dashboard/dist/assets/{download-Di1-UFJX.js → download-DMQI2hQe.js} +1 -1
  59. package/dashboard/dist/assets/{external-link-D7zPVFLO.js → external-link-CemM4dWf.js} +1 -1
  60. package/dashboard/dist/assets/{flame-BMJTxjVg.js → flame-CIvk0SP5.js} +1 -1
  61. package/dashboard/dist/assets/{format-tokens-DCZM5Nfn.js → format-tokens-D3JSvcFl.js} +1 -1
  62. package/dashboard/dist/assets/{icons-I615W2IU.js → icons-Ccf5W0AL.js} +1 -1
  63. package/dashboard/dist/assets/{index-BejnmdPe.js → index-Brd2evyz.js} +1 -1
  64. package/dashboard/dist/assets/{index-BX3CXVdD.js → index-C8Wri_HI.js} +1 -1
  65. package/dashboard/dist/assets/{index-CwczM3BZ.js → index-CFXmD_KX.js} +1 -1
  66. package/dashboard/dist/assets/{info-CFJPny5Q.js → info-CafrKEG8.js} +1 -1
  67. package/dashboard/dist/assets/{link-2-D1ydWh_w.js → link-2-Ds-9Nzlp.js} +1 -1
  68. package/dashboard/dist/assets/{loader-circle-Cq-J-OFg.js → loader-circle-DA4I55-V.js} +1 -1
  69. package/dashboard/dist/assets/{maximize-2-kIoSq57I.js → maximize-2-CjMPt5kU.js} +1 -1
  70. package/dashboard/dist/assets/{modifiers.esm-CLojthuF.js → modifiers.esm-DIxTS09h.js} +1 -1
  71. package/dashboard/dist/assets/module-WSx8viXK.js +3 -0
  72. package/dashboard/dist/assets/{monitor-smartphone-DX2qWHYV.js → monitor-smartphone-IZpIOxn3.js} +1 -1
  73. package/dashboard/dist/assets/{plus-B-JEpo3V.js → plus-vbLFNs70.js} +1 -1
  74. package/dashboard/dist/assets/{provider-display-DJDdrTAS.js → provider-display-BAu_UIfV.js} +2 -2
  75. package/dashboard/dist/assets/{react-Ctoe8iLl.js → react-Djb4VGKb.js} +1 -1
  76. package/dashboard/dist/assets/{search-BTSQrlW-.js → search-Ct6LY9Ke.js} +1 -1
  77. package/dashboard/dist/assets/{skills-api-DKQyM8h7.js → skills-api-BnGI2gEp.js} +1 -1
  78. package/dashboard/dist/assets/{sortable.esm-pOEL13xU.js → sortable.esm-CjCGd6g5.js} +1 -1
  79. package/dashboard/dist/assets/{store-B2RplzvW.js → store-CBxkOqRe.js} +1 -1
  80. package/dashboard/dist/assets/{terminal-BH_6_EW_.js → terminal-Dlpdp5Az.js} +1 -1
  81. package/dashboard/dist/assets/{trash-CC444Mz6.js → trash-pxUSKv66.js} +1 -1
  82. package/dashboard/dist/assets/{use-community-stats-CBXLdD_j.js → use-community-stats-tHaixRIw.js} +1 -1
  83. package/dashboard/dist/assets/{use-latest-request-guard-DMuPfZQI.js → use-latest-request-guard-Cs44WPfL.js} +1 -1
  84. package/dashboard/dist/assets/{use-limits-display-prefs-CmsGTBA4.js → use-limits-display-prefs-Dw56PXWy.js} +1 -1
  85. package/dashboard/dist/assets/{use-ordered-list-BNJNpXjB.js → use-ordered-list-DpLx8oLV.js} +1 -1
  86. package/dashboard/dist/assets/use-reduced-motion-DrlJ9X6p.js +1 -0
  87. package/dashboard/dist/assets/{use-session-efficiency-pref-g_0ISxe1.js → use-session-efficiency-pref-BN3FtBvr.js} +1 -1
  88. package/dashboard/dist/assets/{use-transform-D36BHcXk.js → use-transform-C4nTGgzb.js} +1 -1
  89. package/dashboard/dist/assets/{use-usage-limits-Dxw3vzsD.js → use-usage-limits-Dmwdc1yo.js} +1 -1
  90. package/dashboard/dist/assets/{useCurrency-V2FE4TMo.js → useCurrency-DSc3A_Ih.js} +1 -1
  91. package/dashboard/dist/assets/{useTokenFormat-CEOwvzMt.js → useTokenFormat-BHjcMgm8.js} +1 -1
  92. package/dashboard/dist/assets/{zap-BS7ZU76I.js → zap-BE8i_wu2.js} +1 -1
  93. package/dashboard/dist/brand-logos/trae.svg +1 -0
  94. package/dashboard/dist/index.html +13 -11
  95. package/dashboard/dist/ip-check.html +1 -1
  96. package/dashboard/dist/leaderboard.html +1 -1
  97. package/dashboard/dist/llms.txt +3 -2
  98. package/dashboard/dist/share.html +1 -1
  99. package/package.json +6 -4
  100. package/src/commands/init.js +9 -7
  101. package/src/commands/status.js +9 -9
  102. package/src/commands/sync.js +25 -0
  103. package/src/lib/pricing/curated-overrides.json +7 -0
  104. package/src/lib/pricing/matcher.js +5 -0
  105. package/src/lib/pricing/seed-snapshot.json +1 -1
  106. package/src/lib/rollout.js +197 -18
  107. package/src/lib/trae-db.js +127 -0
  108. package/src/lib/trae-snapshot.js +230 -0
  109. package/src/lib/trae-sqlite.js +143 -0
  110. package/src/lib/trae-usage.js +121 -0
  111. package/dashboard/dist/assets/DashboardPage-DTcVGG4e.js +0 -1
  112. package/dashboard/dist/assets/DialogDescription-haPz7CX9.js +0 -1
  113. package/dashboard/dist/assets/__vite-browser-external-BDugA6I0.js +0 -45
  114. package/dashboard/dist/assets/module-DtWfRKM4.js +0 -3
  115. package/dashboard/dist/assets/use-reduced-motion-CNpG-Yoi.js +0 -1
@@ -0,0 +1,143 @@
1
+ const fs = require("node:fs");
2
+
3
+ let enginePromise;
4
+ let queryQueue = Promise.resolve();
5
+
6
+ async function createEngine() {
7
+ const [{ default: initialize }, sqlite, { FacadeVFS }] = await Promise.all([
8
+ import("@journeyapps/wa-sqlite/dist/wa-sqlite.mjs"),
9
+ import("@journeyapps/wa-sqlite"),
10
+ import("@journeyapps/wa-sqlite/src/FacadeVFS.js"),
11
+ ]);
12
+ const module = await initialize({
13
+ wasmBinary: fs.readFileSync(require.resolve("@journeyapps/wa-sqlite/dist/wa-sqlite.wasm")),
14
+ });
15
+ const api = sqlite.Factory(module);
16
+ const snapshots = new Map();
17
+ const handles = new Map();
18
+
19
+ // Register one VFS for the lifetime of the engine. Each query gets a distinct
20
+ // virtual filename, without accumulating WASM callbacks or touching disk.
21
+ class TraeVFS extends FacadeVFS {
22
+ constructor() {
23
+ super("tokentracker-trae", module);
24
+ }
25
+
26
+ jOpen(name, fileId, flags, outputFlags) {
27
+ const state = snapshots.get(name);
28
+ if (!state || !(flags & sqlite.SQLITE_OPEN_MAIN_DB)) return sqlite.SQLITE_CANTOPEN;
29
+ handles.set(fileId, state);
30
+ outputFlags.setInt32(0, sqlite.SQLITE_OPEN_READONLY, true);
31
+ return sqlite.SQLITE_OK;
32
+ }
33
+
34
+ jRead(fileId, output, offset) {
35
+ const state = handles.get(fileId);
36
+ if (!state) return sqlite.SQLITE_IOERR_READ;
37
+ // FacadeVFS passes a proxy over WASM memory, not a native Uint8Array.
38
+ // Keep the temporary buffer small and erase it after copying its page.
39
+ const buffer = Buffer.alloc(output.byteLength);
40
+ try {
41
+ const count = state.snapshot.read(buffer, offset);
42
+ if (!Number.isSafeInteger(count) || count < 0 || count > buffer.length) {
43
+ throw new Error("Invalid TRAE snapshot read length.");
44
+ }
45
+ buffer.fill(0, count);
46
+ output.set(buffer);
47
+ return count === buffer.length ? sqlite.SQLITE_OK : sqlite.SQLITE_IOERR_SHORT_READ;
48
+ } catch (err) {
49
+ state.error ||= err;
50
+ output.fill(0);
51
+ return sqlite.SQLITE_IOERR_READ;
52
+ } finally {
53
+ buffer.fill(0);
54
+ }
55
+ }
56
+
57
+ jFileSize(fileId, output) {
58
+ const state = handles.get(fileId);
59
+ if (!state) return sqlite.SQLITE_IOERR_FSTAT;
60
+ output.setBigInt64(0, BigInt(state.snapshot.byteLength), true);
61
+ return sqlite.SQLITE_OK;
62
+ }
63
+
64
+ jClose(fileId) {
65
+ handles.delete(fileId);
66
+ return sqlite.SQLITE_OK;
67
+ }
68
+
69
+ jAccess(name, flags, output) {
70
+ output.setInt32(0, snapshots.has(name) ? 1 : 0, true);
71
+ return sqlite.SQLITE_OK;
72
+ }
73
+
74
+ jWrite() { return sqlite.SQLITE_READONLY; }
75
+ jTruncate() { return sqlite.SQLITE_READONLY; }
76
+ jDelete() { return sqlite.SQLITE_READONLY; }
77
+ }
78
+
79
+ const vfs = new TraeVFS();
80
+ api.vfs_register(vfs, false);
81
+ return { api, sqlite, snapshots, handles, vfs, nextId: 0 };
82
+ }
83
+
84
+ /**
85
+ * Query a read-only, random-access snapshot without materializing its database.
86
+ * The caller owns snapshot cleanup; only the projected SQL rows leave this VFS.
87
+ */
88
+ async function querySnapshot(snapshot, callback) {
89
+ if (!Number.isSafeInteger(snapshot.byteLength) || snapshot.byteLength < 0) {
90
+ throw new Error("Invalid TRAE snapshot size.");
91
+ }
92
+ enginePromise ||= createEngine().catch((err) => {
93
+ enginePromise = undefined;
94
+ throw err;
95
+ });
96
+ const engine = await enginePromise;
97
+ const { api, sqlite, snapshots, handles, vfs } = engine;
98
+ const name = `trae-${++engine.nextId}.db`;
99
+ const state = { snapshot, error: null };
100
+ snapshots.set(name, state);
101
+ let db;
102
+ let failure;
103
+ try {
104
+ db = await api.open_v2(name, sqlite.SQLITE_OPEN_READONLY, vfs.name);
105
+ const query = async (sql) => {
106
+ const rows = [];
107
+ try {
108
+ await api.exec(db, sql, (values, columns) => {
109
+ rows.push(Object.fromEntries(columns.map((column, index) => [column, values[index]])));
110
+ });
111
+ } catch (err) {
112
+ throw state.error || err;
113
+ }
114
+ return rows;
115
+ };
116
+ await query("PRAGMA query_only=ON; PRAGMA cache_size=-4096; PRAGMA temp_store=MEMORY;");
117
+ return await callback(query);
118
+ } catch (err) {
119
+ failure = state.error || err;
120
+ throw failure;
121
+ } finally {
122
+ try {
123
+ if (db) await api.close(db);
124
+ } catch (err) {
125
+ if (!failure) throw state.error || err;
126
+ } finally {
127
+ snapshots.delete(name);
128
+ for (const [fileId, opened] of handles) {
129
+ if (opened === state) handles.delete(fileId);
130
+ }
131
+ }
132
+ }
133
+ }
134
+
135
+ function withTraeSqlite(snapshot, callback) {
136
+ // The package's async convenience API shares temporary WASM allocations.
137
+ // Serialize access to the single engine even when callers read in parallel.
138
+ const operation = queryQueue.then(() => querySnapshot(snapshot, callback));
139
+ queryQueue = operation.catch(() => {});
140
+ return operation;
141
+ }
142
+
143
+ module.exports = { withTraeSqlite };
@@ -0,0 +1,121 @@
1
+ "use strict";
2
+
3
+ // TRAE stores the last request's usage alongside optional whole-turn prompt
4
+ // and completion counters. Reconcile the request first: its total cannot
5
+ // validate a whole-turn count, and its cache count is not a whole-turn count.
6
+ function normalizeTraeUsage(raw, { model } = {}) {
7
+ let usage = raw;
8
+ if (typeof usage === "string") {
9
+ try { usage = JSON.parse(usage); } catch { return null; }
10
+ }
11
+ if (!usage || typeof usage !== "object" || Array.isArray(usage)) return null;
12
+ const fields = [
13
+ "prompt_tokens", "prompt_tokens_total", "input_tokens",
14
+ "completion_tokens", "completion_tokens_total", "output_tokens",
15
+ "cache_read_input_tokens", "cache_creation_input_tokens",
16
+ "reasoning_tokens", "reasoning_output_tokens", "total_tokens",
17
+ ];
18
+ for (const field of fields) {
19
+ if (usage[field] != null && (!Number.isSafeInteger(usage[field]) || usage[field] < 0)) {
20
+ return null;
21
+ }
22
+ }
23
+ for (const [left, right] of [["prompt_tokens", "input_tokens"],
24
+ ["completion_tokens", "output_tokens"], ["reasoning_tokens", "reasoning_output_tokens"]]) {
25
+ if (usage[left] != null && usage[right] != null && usage[left] !== usage[right]) return null;
26
+ }
27
+ if (!["prompt_tokens", "prompt_tokens_total", "input_tokens", "completion_tokens",
28
+ "completion_tokens_total", "output_tokens"].some((field) => usage[field] != null)) {
29
+ return null;
30
+ }
31
+ const input = usage.prompt_tokens ?? usage.input_tokens ?? usage.prompt_tokens_total ?? 0;
32
+ const output = usage.completion_tokens ?? usage.output_tokens ?? usage.completion_tokens_total ?? 0;
33
+ const aggregateInput = usage.prompt_tokens_total > 0 ? usage.prompt_tokens_total : input;
34
+ const aggregateOutput = usage.completion_tokens_total > 0 ? usage.completion_tokens_total : output;
35
+ if (aggregateInput < input || aggregateOutput < output) return null;
36
+ const cached = usage.cache_read_input_tokens ?? 0;
37
+ let written = usage.cache_creation_input_tokens ?? 0;
38
+ let reasoning = usage.reasoning_tokens ?? usage.reasoning_output_tokens ?? 0;
39
+ let estimated = false;
40
+ const isGemini = /^gemini(?:-|$)/i.test(normalizeTraeModel(model ?? usage.name));
41
+ // Some TRAE Gemini rows copy the cache-read counter into the write field,
42
+ // even when their sum exceeds the entire prompt. Gemini's GenerateContent
43
+ // metadata reports cachedContentTokenCount, not an additional write count.
44
+ // Keep the reported read and mark this narrow compatibility repair estimated.
45
+ if (isGemini && cached > 0 && written === cached && cached + written > input) {
46
+ written = 0;
47
+ estimated = true;
48
+ }
49
+ // Older TRAE Gemini rows omit thoughts while retaining Gemini's total
50
+ // (prompt + candidates + thoughts). Infer only that documented residual,
51
+ // never an arbitrary provider's unexplained total.
52
+ if (isGemini && usage.reasoning_tokens == null && usage.reasoning_output_tokens == null &&
53
+ usage.total_tokens > input + output && written === 0 && cached <= input) {
54
+ reasoning = usage.total_tokens - input - output;
55
+ estimated = true;
56
+ }
57
+ // A total is necessary to distinguish inclusive/exclusive counters when
58
+ // cache or reasoning is present. Missing metadata must not imply $0 usage
59
+ // or guessed input. A zero *_total field means unset, not an empty turn.
60
+ if (usage.total_tokens == null && (cached || written || reasoning)) return null;
61
+ const total = usage.total_tokens ?? (input + output);
62
+ if (!Number.isSafeInteger(total)) return null;
63
+ const candidates = new Map();
64
+ for (const includedCache of new Set([cached + written, cached, 0])) {
65
+ for (const includedReasoning of new Set([reasoning, 0])) {
66
+ const noncached = input - includedCache;
67
+ const nonreasoning = output - includedReasoning;
68
+ if (noncached < 0 || nonreasoning < 0) continue;
69
+ if (noncached + cached + written + nonreasoning + reasoning !== total) continue;
70
+ const totals = {
71
+ input_tokens: noncached,
72
+ cached_input_tokens: cached,
73
+ cache_creation_input_tokens: written,
74
+ output_tokens: nonreasoning,
75
+ reasoning_output_tokens: reasoning,
76
+ total_tokens: total,
77
+ };
78
+ candidates.set(JSON.stringify(totals), totals);
79
+ }
80
+ }
81
+ if (candidates.size !== 1) return null;
82
+ const totals = candidates.values().next().value;
83
+ if (aggregateInput !== input || aggregateOutput !== output) {
84
+ // The *_total counters cover the whole turn; the cache and reasoning fields
85
+ // describe its final request. The difference in prompt tokens has no cache
86
+ // split, so it counts toward total_tokens but no priced column. Completion
87
+ // tokens are priced the same with or without caching and stay in output.
88
+ const unpriced = aggregateInput - input;
89
+ totals.output_tokens += aggregateOutput - output;
90
+ totals.total_tokens += unpriced + aggregateOutput - output;
91
+ if (unpriced > 0) totals.unpriced_input_tokens = unpriced;
92
+ }
93
+ if (Object.values(totals).some((value) => !Number.isSafeInteger(value))) return null;
94
+ if (estimated) totals.usage_precision = "estimated";
95
+ return totals;
96
+ }
97
+
98
+ function normalizeTraeModel(value) {
99
+ if (typeof value !== "string") return "trae-unknown";
100
+ const model = value.trim().split("__")[0].toLowerCase().replace(/\s+/g, "-");
101
+ // TRAE's display alias otherwise fuzzy-matches a regional Sonnet SKU and
102
+ // loses cache pricing. Sonnet 4 has the fixed 2025-05-14 API model id.
103
+ if (model === "claude-4-sonnet") return "claude-sonnet-4-20250514";
104
+ return model && model !== "-" && !model.includes("|") ? model : "trae-unknown";
105
+ }
106
+
107
+ function traeTimestamp(value) {
108
+ if (value == null || value === "") return null;
109
+ let millis;
110
+ if (typeof value === "number" || (typeof value === "string" && /^\d+(\.\d+)?$/.test(value))) {
111
+ const numeric = Number(value);
112
+ millis = numeric < 100_000_000_000 ? numeric * 1000 : numeric;
113
+ } else if (typeof value === "string") {
114
+ millis = Date.parse(value);
115
+ }
116
+ if (!Number.isFinite(millis) || millis <= 0) return null;
117
+ const date = new Date(millis);
118
+ return Number.isFinite(date.getTime()) ? date.toISOString() : null;
119
+ }
120
+
121
+ module.exports = { normalizeTraeUsage, normalizeTraeModel, traeTimestamp };