tokentracker-cli 0.90.2 → 0.91.3

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 (117) hide show
  1. package/README.de.md +7 -5
  2. package/README.ja.md +9 -5
  3. package/README.ko.md +9 -5
  4. package/README.md +7 -5
  5. package/README.zh-CN.md +6 -4
  6. package/dashboard/dist/assets/{AchievementBadge-BJl0ibLg.js → AchievementBadge-LHOuekQ3.js} +1 -1
  7. package/dashboard/dist/assets/{AchievementsPage-CV6aNe3g.js → AchievementsPage-DNzeYWBl.js} +2 -2
  8. package/dashboard/dist/assets/{AchievementsSection-DxLHu9EV.js → AchievementsSection-Ch5dImt8.js} +1 -1
  9. package/dashboard/dist/assets/{BadgeDetailModal-ConoHhIL.js → BadgeDetailModal-CuikWkUy.js} +1 -1
  10. package/dashboard/dist/assets/{Card-C_VQxG6G.js → Card-Chwjmxjz.js} +1 -1
  11. package/dashboard/dist/assets/{ClawdAnimated-_Mfn4MnU.js → ClawdAnimated-BdB7aycS.js} +1 -1
  12. package/dashboard/dist/assets/{CommandPalette-CCRCnYY9.js → CommandPalette-BMwuSEWM.js} +1 -1
  13. package/dashboard/dist/assets/{CommunityStatsModal-DuICwpdM.js → CommunityStatsModal-eEDF-xn8.js} +1 -1
  14. package/dashboard/dist/assets/{ConfirmModal-CI27robt.js → ConfirmModal-Be7f6XdL.js} +1 -1
  15. package/dashboard/dist/assets/{Controls-Cwu0gtgj.js → Controls-CtBFPCBY.js} +1 -1
  16. package/dashboard/dist/assets/{DashboardPage-GsFvPie1.js → DashboardPage-M0p-SU-f.js} +1 -1
  17. package/dashboard/dist/assets/{DevicePage-BSK76UQE.js → DevicePage-CAfcUond.js} +1 -1
  18. package/dashboard/dist/assets/{DialogClose-BXQlPs02.js → DialogClose-kd1wIljN.js} +1 -1
  19. package/dashboard/dist/assets/DialogDescription-CjJLpfCL.js +1 -0
  20. package/dashboard/dist/assets/{DialogTitle-DyvhHV1-.js → DialogTitle-DhB-tbnZ.js} +1 -1
  21. package/dashboard/dist/assets/{FadeIn-DaP6F4f8.js → FadeIn-B8eRWBdy.js} +1 -1
  22. package/dashboard/dist/assets/{HeaderGithubStar-CKB5puCd.js → HeaderGithubStar-DmWep8uP.js} +1 -1
  23. package/dashboard/dist/assets/{HoverTooltip-BG6k61dF.js → HoverTooltip-DDyTZli0.js} +1 -1
  24. package/dashboard/dist/assets/{IpCheckPage-Oqt1J9S2.js → IpCheckPage-CN8hYm4o.js} +1 -1
  25. package/dashboard/dist/assets/{LandingPage-45qoUn79.js → LandingPage-B3cHASqK.js} +2 -2
  26. package/dashboard/dist/assets/{LeaderboardAvatar-ZgXnaI_N.js → LeaderboardAvatar-CEcLXrJL.js} +1 -1
  27. package/dashboard/dist/assets/{LeaderboardPage-C7E_LrFk.js → LeaderboardPage-H6HW9hon.js} +2 -2
  28. package/dashboard/dist/assets/{LeaderboardProfileModal-D6PUcj3b.js → LeaderboardProfileModal-B_5PyjRA.js} +4 -4
  29. package/dashboard/dist/assets/{LeaderboardProfilePage-VcC9DK_B.js → LeaderboardProfilePage-DR9KWLWv.js} +3 -3
  30. package/dashboard/dist/assets/{LimitsPage-B9K0RFQD.js → LimitsPage-Da0hRxO_.js} +1 -1
  31. package/dashboard/dist/assets/{LocalOnlyNotice-BGsfKEM9.js → LocalOnlyNotice-DuTdva_n.js} +1 -1
  32. package/dashboard/dist/assets/LoginCard-DOL3osrj.js +1 -0
  33. package/dashboard/dist/assets/{LoginModal-N3pWGciC.js → LoginModal-DBmaQL2L.js} +1 -1
  34. package/dashboard/dist/assets/{LoginPage-TGgL0aXF.js → LoginPage-Dk08VrkC.js} +1 -1
  35. package/dashboard/dist/assets/{PetPage-DPi7OT_4.js → PetPage-BbCIdpzx.js} +1 -1
  36. package/dashboard/dist/assets/{PopoverPopup-BkptZaPd.js → PopoverPopup-C1fJZ4qt.js} +1 -1
  37. package/dashboard/dist/assets/ProviderIcon-FfLD-f73.js +1 -0
  38. package/dashboard/dist/assets/{ResetPasswordPage-DQ4qdjBD.js → ResetPasswordPage-Bg7eVc6L.js} +1 -1
  39. package/dashboard/dist/assets/{SegmentedControl-CuTrxHXS.js → SegmentedControl-CFNHcUz7.js} +1 -1
  40. package/dashboard/dist/assets/{Select-_Txi2j1D.js → Select-B0Q0eDcJ.js} +1 -1
  41. package/dashboard/dist/assets/{SelectItemText-DVKhj-YN.js → SelectItemText-BVbLYxSQ.js} +1 -1
  42. package/dashboard/dist/assets/{ServiceStatusPage-ChflNSK0.js → ServiceStatusPage-DCZH2kz5.js} +1 -1
  43. package/dashboard/dist/assets/{SessionsPage-CHv6xK2J.js → SessionsPage-DO5Et7ss.js} +2 -2
  44. package/dashboard/dist/assets/{SettingsPage-DkWAYALG.js → SettingsPage-afgxGfzT.js} +2 -2
  45. package/dashboard/dist/assets/{SkillsPage-DnTSlBl-.js → SkillsPage-DDKiHCF7.js} +1 -1
  46. package/dashboard/dist/assets/{TokenGalaxy-yAMCaU-k.js → TokenGalaxy-DV1Xoe5z.js} +1 -1
  47. package/dashboard/dist/assets/{ToolbarRootContext-BeDIqB0k.js → ToolbarRootContext-DNckd9Qz.js} +1 -1
  48. package/dashboard/dist/assets/TrendMonitor-CliMJxCC.js +19 -0
  49. package/dashboard/dist/assets/{WidgetsPage-y_qKZ9Gv.js → WidgetsPage-BrwG6YMM.js} +1 -1
  50. package/dashboard/dist/assets/{WrappedPage-BGUGI6vk.js → WrappedPage-Ft9YJd0D.js} +1 -1
  51. package/dashboard/dist/assets/{__vite-browser-external-CRvi_9hb.js → __vite-browser-external-C6_BtvDo.js} +4 -3
  52. package/dashboard/dist/assets/agent-logos-yYNu1ZUy.js +1 -0
  53. package/dashboard/dist/assets/{arrow-up-right-CjEesyJI.js → arrow-up-right-B_K57Vbg.js} +1 -1
  54. package/dashboard/dist/assets/{check-KDrH1gwE.js → check-xH63o2xJ.js} +1 -1
  55. package/dashboard/dist/assets/{chevron-right-DseqFm-D.js → chevron-right-D3F2rAfr.js} +1 -1
  56. package/dashboard/dist/assets/{copy-CFJ1QKAQ.js → copy-WPfJGGI0.js} +1 -1
  57. package/dashboard/dist/assets/{download-BQUNGUdh.js → download-BgtWtEXI.js} +1 -1
  58. package/dashboard/dist/assets/{external-link-BCEOLLH2.js → external-link-DSHGoZRM.js} +1 -1
  59. package/dashboard/dist/assets/{flame-BigtoGg2.js → flame-CNGl6Qac.js} +1 -1
  60. package/dashboard/dist/assets/{format-tokens-CS1GUpAV.js → format-tokens-jVm2UX7Y.js} +1 -1
  61. package/dashboard/dist/assets/{icons-DpZ1a8Ct.js → icons-B1JZg-Id.js} +1 -1
  62. package/dashboard/dist/assets/{index-DqkwD6eO.js → index-BTZibjLi.js} +1 -1
  63. package/dashboard/dist/assets/{index-lGAAw1Rv.js → index-BjD81Qce.js} +1 -1
  64. package/dashboard/dist/assets/{index-DFU_yVc5.js → index-CUFHknAy.js} +1 -1
  65. package/dashboard/dist/assets/{info-Br_jtGtO.js → info-CLM460KM.js} +1 -1
  66. package/dashboard/dist/assets/{limits-providers-CkqO1LMQ.js → limits-providers-CFO4O1qh.js} +1 -1
  67. package/dashboard/dist/assets/{link-2-D-nAhkk-.js → link-2-BHOsUnAI.js} +1 -1
  68. package/dashboard/dist/assets/{loader-circle-CuNPuDRR.js → loader-circle-CgcIQi92.js} +1 -1
  69. package/dashboard/dist/assets/{maximize-2-dlvwitfg.js → maximize-2-DvqfuJEA.js} +1 -1
  70. package/dashboard/dist/assets/{modifiers.esm-ORha_lPI.js → modifiers.esm-CNiwIkPh.js} +1 -1
  71. package/dashboard/dist/assets/module-CV1mYFsv.js +3 -0
  72. package/dashboard/dist/assets/{monitor-smartphone-CI2qvlMB.js → monitor-smartphone-BLutpxrG.js} +1 -1
  73. package/dashboard/dist/assets/provider-display-BxIgWUEa.js +42 -0
  74. package/dashboard/dist/assets/{react-Dy9iWvqT.js → react-CM-ozVnh.js} +1 -1
  75. package/dashboard/dist/assets/{search-Do7tzc3k.js → search-4T07INKt.js} +1 -1
  76. package/dashboard/dist/assets/{skills-api-CJ-8EFac.js → skills-api-EXMD3LD1.js} +1 -1
  77. package/dashboard/dist/assets/{sortable.esm-BKQg8NHC.js → sortable.esm-jC6mPJiL.js} +1 -1
  78. package/dashboard/dist/assets/{store-SUT-n3k3.js → store-CvSEdgf2.js} +1 -1
  79. package/dashboard/dist/assets/{terminal-BX4DOxd6.js → terminal-D9fyE2pP.js} +1 -1
  80. package/dashboard/dist/assets/{trash-2-VERlG247.js → trash-2-QLUsAY3l.js} +1 -1
  81. package/dashboard/dist/assets/{use-community-stats-BX6_XUdu.js → use-community-stats-Chrt3kpU.js} +1 -1
  82. package/dashboard/dist/assets/{use-limits-display-prefs-BpC3tYRw.js → use-limits-display-prefs-BJJd750e.js} +1 -1
  83. package/dashboard/dist/assets/{use-ordered-list-CX-LREPo.js → use-ordered-list-BBJp5lFv.js} +1 -1
  84. package/dashboard/dist/assets/use-reduced-motion-BvXZCGFC.js +1 -0
  85. package/dashboard/dist/assets/{use-session-efficiency-pref-B1G8hzVM.js → use-session-efficiency-pref-BXbvx2Ht.js} +1 -1
  86. package/dashboard/dist/assets/{use-transform-DNCqLbUD.js → use-transform-BVp_MUsU.js} +1 -1
  87. package/dashboard/dist/assets/{use-usage-limits-Cvbvf30m.js → use-usage-limits-Dk3EkeIs.js} +1 -1
  88. package/dashboard/dist/assets/{useCurrency-EciQmJkm.js → useCurrency-Bi88Sd__.js} +1 -1
  89. package/dashboard/dist/assets/{useScrollLock-sLkmam8C.js → useScrollLock-if34g2H-.js} +1 -1
  90. package/dashboard/dist/assets/{useTokenFormat-nb9na-2-.js → useTokenFormat-C2cz-5qJ.js} +1 -1
  91. package/dashboard/dist/assets/{zap-BQOWCMov.js → zap-D1yHmcQR.js} +1 -1
  92. package/dashboard/dist/index.html +28 -24
  93. package/dashboard/dist/ip-check.html +1 -1
  94. package/dashboard/dist/leaderboard.html +1 -1
  95. package/dashboard/dist/llms.txt +4 -2
  96. package/dashboard/dist/share.html +1 -1
  97. package/package.json +2 -2
  98. package/src/commands/init.js +12 -1
  99. package/src/commands/status.js +59 -0
  100. package/src/commands/sync.js +220 -4
  101. package/src/lib/account-usage-dedup.js +267 -0
  102. package/src/lib/local-api.js +4 -0
  103. package/src/lib/pricing/curated-overrides.json +15 -9
  104. package/src/lib/pricing/index.js +38 -2
  105. package/src/lib/pricing/matcher.js +1 -0
  106. package/src/lib/pricing/seed-snapshot.json +1 -1
  107. package/src/lib/rollout.js +853 -14
  108. package/src/lib/source-metadata.js +1 -1
  109. package/src/lib/trae-cn-config.js +539 -0
  110. package/dashboard/dist/assets/DialogDescription-M_lg2jhD.js +0 -1
  111. package/dashboard/dist/assets/LoginCard-j5HVFRdP.js +0 -1
  112. package/dashboard/dist/assets/ProviderIcon-zaz4nwxI.js +0 -1
  113. package/dashboard/dist/assets/TrendMonitor-BthLuYwy.js +0 -19
  114. package/dashboard/dist/assets/agent-logos-DwHwn_cb.js +0 -1
  115. package/dashboard/dist/assets/module-BRJxSJmz.js +0 -3
  116. package/dashboard/dist/assets/provider-display-BNCdZ83n.js +0 -42
  117. package/dashboard/dist/assets/use-reduced-motion-DbebVWBw.js +0 -1
@@ -0,0 +1,267 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Account-level cross-device truth - JS reference implementation.
5
+ *
6
+ * Account-level sources (ACCOUNT_LEVEL_SOURCES in src/lib/source-metadata.js)
7
+ * come from a per-ACCOUNT cloud API, so every device that syncs them stores
8
+ * its own copy of the SAME account data. How to dedup depends on the source:
9
+ *
10
+ * - 'cursor' has NO stable session identity: its rows are identical across
11
+ * devices, so the legacy whole-row MAX pick per (hour, source, model)
12
+ * dedups them (dedupeAccountLevelRows below).
13
+ * - 'trae-cn' is a CORRECTABLE snapshot: totals can be revised down, a
14
+ * session can move to another model or another half-hour. Hour-level
15
+ * dedup CANNOT express that (a fresh device's first data bucket cannot
16
+ * safely displace earlier hours - that would need an unproven "absent =
17
+ * deleted" contract - and a 10:00 -> 10:30 bucket migration strands the
18
+ * old hour). Canonical truth for trae-cn therefore lives at the SESSION
19
+ * level (tokentracker_account_session_states; migrations/
20
+ * 20260817120000_account-session-states.sql): identity is
21
+ * (user_id, source, session_id) - device_id is NOT identity (the usage
22
+ * API request carries no device discriminator). Session-id evidence
23
+ * (2026-08-17): repeated-fetch stability VERIFIED, cross-window
24
+ * stability VERIFIED; cross-device same-account stability NOT DIRECTLY
25
+ * VERIFIED (no second independent device/auth experiment). Every
26
+ * observation of one
27
+ * session whole-row-replaces the previous state under a strict LWW
28
+ * guard, so the three correction classes collapse into ONE operation:
29
+ *
30
+ * downward S tokens 100 -> 60 replaced, total 60
31
+ * model S model A -> B replaced, only B remains
32
+ * bucket S bucket 10:00 -> 10:30 replaced, only 10:30 remains
33
+ *
34
+ * ABSENCE is NOT PROVEN to mean deletion, so nothing ever deletes a session
35
+ * row: a session missing from a non-empty snapshot asserts nothing, and an
36
+ * empty snapshot asserts nothing at all.
37
+ *
38
+ * Freshness: the TRAE API exposes no provider-side ordering signal (probed
39
+ * 2026-08-17: headers carry only CDN trace ids, rows carry no revision), so
40
+ * snapshot_verified_at is the CLIENT logical fetch stamp (best-effort
41
+ * cross-device ordering under clock skew - documented residual risk, NOT
42
+ * strict correctness). It is stamped once per real fetch and replayed
43
+ * verbatim by the append-only queue; the LWW guard applies strictly newer
44
+ * (>) stamps only, so retries are idempotent and a re-delivered older
45
+ * observation cannot displace a newer one. Equal stamps keep the
46
+ * first-applied row (stable under retries).
47
+ *
48
+ * This module is the executable specification of that semantics; the
49
+ * deployed SQL (migrations/20260817120000_account-session-states.sql upsert,
50
+ * scripts/ops/account-usage-grouped-rpc.sql + leaderboard_hourly_dedup_v2
51
+ * aggregation branches) implements the same algorithm and MUST stay in sync
52
+ * - test/account-usage-dedup.test.js pins both.
53
+ */
54
+
55
+ // Queue record kind the TRAE CN parser emits for one changed session's
56
+ // canonical observation (src/lib/rollout.js appendTraeCnSessionStates).
57
+ const SESSION_STATE_KIND = "account_session_state";
58
+
59
+ function parseMs(value) {
60
+ const ms = Date.parse(value);
61
+ return Number.isFinite(ms) ? ms : null;
62
+ }
63
+
64
+ // Token columns that make up a canonical session state row (the SQL table's
65
+ // non-key, non-metadata columns).
66
+ const TOKEN_COLUMNS = [
67
+ "input_tokens",
68
+ "output_tokens",
69
+ "cached_input_tokens",
70
+ "cache_creation_input_tokens",
71
+ "reasoning_output_tokens",
72
+ "total_tokens",
73
+ ];
74
+
75
+ function normalizeStateRow(observation) {
76
+ const source = String(observation?.source ?? "").trim().toLowerCase();
77
+ const sessionId = String(observation?.session_id ?? "").trim();
78
+ if (!source || !sessionId) return null;
79
+ const verifiedMs = parseMs(observation?.snapshot_verified_at);
80
+ if (verifiedMs === null) return null;
81
+ const bucketStart = String(observation?.bucket_start ?? "");
82
+ if (parseMs(bucketStart) === null) return null;
83
+ const model = String(observation?.model ?? "");
84
+ if (!model) return null;
85
+ const row = {
86
+ source,
87
+ session_id: sessionId,
88
+ model,
89
+ bucket_start: bucketStart,
90
+ snapshot_verified_at: observation.snapshot_verified_at,
91
+ };
92
+ for (const column of TOKEN_COLUMNS) {
93
+ const value = Number(observation?.[column]);
94
+ row[column] = Number.isFinite(value) ? value : 0;
95
+ }
96
+ return row;
97
+ }
98
+
99
+ /**
100
+ * Whole-row LWW replace of canonical session states - mirrors
101
+ * tokentracker_upsert_account_session_states() (migrations/
102
+ * 20260817120000_account-session-states.sql): a row applies only when its
103
+ * snapshot_verified_at is STRICTLY newer (>) than the stored one. Replays of
104
+ * the same observation are no-ops; equal stamps keep the first-applied row.
105
+ *
106
+ * @param {Map<string, object>} states - canonical table keyed by
107
+ * `${source}|${session_id}` (mirrors the (user_id, source, session_id) PK
108
+ * for one account; mutated in place).
109
+ * @param {Array<object>} observations - ingest batch (already deduped to one
110
+ * row per session by the edge, like the SQL batch upsert requires).
111
+ * @returns {number} applied row count (mirrors the SQL ROW_COUNT return).
112
+ */
113
+ function upsertAccountSessionStates(states, observations) {
114
+ const table = states instanceof Map ? states : new Map();
115
+ const batch = Array.isArray(observations) ? observations : [];
116
+ let applied = 0;
117
+ for (const observation of batch) {
118
+ const row = normalizeStateRow(observation);
119
+ if (!row) continue;
120
+ const key = `${row.source}|${row.session_id}`;
121
+ const stored = table.get(key);
122
+ if (!stored) {
123
+ table.set(key, row);
124
+ applied += 1;
125
+ continue;
126
+ }
127
+ const storedMs = parseMs(stored.snapshot_verified_at) ?? -Infinity;
128
+ const rowMs = parseMs(row.snapshot_verified_at) ?? -Infinity;
129
+ if (rowMs > storedMs) {
130
+ table.set(key, row);
131
+ applied += 1;
132
+ }
133
+ }
134
+ return applied;
135
+ }
136
+
137
+ /**
138
+ * Aggregate canonical session states into hourly rows - mirrors the trae-cn
139
+ * branch of account_usage_grouped (scripts/ops/account-usage-grouped-rpc.sql)
140
+ * and leaderboard_hourly_dedup_v2: SUM per (bucket_start, source, model),
141
+ * conversations = session count.
142
+ *
143
+ * @param {Map<string, object>|Array<object>} states
144
+ * @param {{from?: string, to?: string}} [opts] - optional [from, to) window.
145
+ * @returns {Array<object>} rows shaped like tokentracker_hourly aggregates.
146
+ */
147
+ function aggregateSessionStates(states, opts = {}) {
148
+ const values = states instanceof Map ? [...states.values()] : Array.isArray(states) ? states : [];
149
+ const fromMs = parseMs(opts.from ?? "");
150
+ const toMs = parseMs(opts.to ?? "");
151
+ const grouped = new Map();
152
+ for (const state of values) {
153
+ const row = normalizeStateRow(state);
154
+ if (!row) continue;
155
+ const bucketMs = parseMs(row.bucket_start);
156
+ if (bucketMs === null) continue;
157
+ if (fromMs !== null && bucketMs < fromMs) continue;
158
+ if (toMs !== null && bucketMs >= toMs) continue;
159
+ const key = `${row.bucket_start}||${row.source}||${row.model}`;
160
+ const acc = grouped.get(key) || {
161
+ hour_start: row.bucket_start,
162
+ source: row.source,
163
+ model: row.model,
164
+ input_tokens: 0,
165
+ output_tokens: 0,
166
+ cached_input_tokens: 0,
167
+ cache_creation_input_tokens: 0,
168
+ reasoning_output_tokens: 0,
169
+ total_tokens: 0,
170
+ conversations: 0,
171
+ };
172
+ for (const column of TOKEN_COLUMNS) acc[column] += row[column];
173
+ acc.conversations += 1;
174
+ grouped.set(key, acc);
175
+ }
176
+ return [...grouped.values()].sort(
177
+ (a, b) =>
178
+ String(a.hour_start).localeCompare(String(b.hour_start)) ||
179
+ String(a.source).localeCompare(String(b.source)) ||
180
+ String(a.model).localeCompare(String(b.model)),
181
+ );
182
+ }
183
+
184
+ // Mirrors the legacy SQL DISTINCT ON (hour, source, model)
185
+ // ORDER BY total_tokens DESC, updated_at DESC (device_id asc as the final
186
+ // deterministic tiebreak). Used ONLY for account-level sources without a
187
+ // stable session identity ('cursor') and watermark-less history.
188
+ function pickLegacyRow(rows) {
189
+ let best = null;
190
+ for (const row of rows) {
191
+ if (!best) {
192
+ best = row;
193
+ continue;
194
+ }
195
+ const rowTotal = Number(row.total_tokens) || 0;
196
+ const bestTotal = Number(best.total_tokens) || 0;
197
+ if (rowTotal !== bestTotal) {
198
+ if (rowTotal > bestTotal) best = row;
199
+ continue;
200
+ }
201
+ const rowUpdated = parseMs(row.updated_at ?? "") ?? -Infinity;
202
+ const bestUpdated = parseMs(best.updated_at ?? "") ?? -Infinity;
203
+ if (rowUpdated !== bestUpdated) {
204
+ if (rowUpdated > bestUpdated) best = row;
205
+ continue;
206
+ }
207
+ if (String(row.device_id) < String(best.device_id)) best = row;
208
+ }
209
+ return best;
210
+ }
211
+
212
+ /**
213
+ * Legacy whole-row MAX dedup for account-level hourly rows WITHOUT a stable
214
+ * session identity ('cursor'): rows are identical across devices, so one
215
+ * canonical row per (hour, source, model) suffices. Sources with a session
216
+ * identity ('trae-cn') must instead aggregate from canonical session states
217
+ * (aggregateSessionStates) - never from per-device hourly rows.
218
+ *
219
+ * @param {Array<{device_id: string, hour_start: string, source: string,
220
+ * model: string, updated_at?: string, total_tokens: number}>} rows -
221
+ * every device's tokentracker_hourly-shaped rows.
222
+ * @returns {Array} canonical rows - one consistent row per (hour, model).
223
+ */
224
+ function dedupeAccountLevelRows(rows) {
225
+ if (!Array.isArray(rows)) return [];
226
+ // Group rows by (source, hour_start); keep original row objects untouched.
227
+ const byHour = new Map();
228
+ for (const row of rows) {
229
+ if (!row || typeof row.hour_start !== "string" || !row.hour_start) continue;
230
+ const key = `${row.source}||${row.hour_start}`;
231
+ const list = byHour.get(key) || [];
232
+ list.push(row);
233
+ byHour.set(key, list);
234
+ }
235
+
236
+ const out = [];
237
+ for (const [, hourRows] of byHour) {
238
+ const byModel = new Map();
239
+ for (const row of hourRows) {
240
+ const modelKey = row.model || "unknown";
241
+ const list = byModel.get(modelKey) || [];
242
+ list.push(row);
243
+ byModel.set(modelKey, list);
244
+ }
245
+ for (const list of byModel.values()) {
246
+ out.push(pickLegacyRow(list));
247
+ }
248
+ }
249
+ return out;
250
+ }
251
+
252
+ /** Total tokens of a deduped row set - convenience for tests/diagnostics. */
253
+ function sumTotalTokens(rows) {
254
+ return (Array.isArray(rows) ? rows : []).reduce(
255
+ (acc, row) => acc + (Number(row?.total_tokens) || 0),
256
+ 0,
257
+ );
258
+ }
259
+
260
+ module.exports = {
261
+ SESSION_STATE_KIND,
262
+ TOKEN_COLUMNS,
263
+ upsertAccountSessionStates,
264
+ aggregateSessionStates,
265
+ dedupeAccountLevelRows,
266
+ sumTotalTokens,
267
+ };
@@ -338,6 +338,10 @@ function readQueueData(queuePath) {
338
338
  if (!line.trim()) continue;
339
339
  try {
340
340
  const row = JSON.parse(line);
341
+ // Account session states (and the pre-release watermark records
342
+ // this branch may still hold in a dev queue) are cloud-side control
343
+ // records, not usage rows - never surface them locally.
344
+ if (row?.kind === "account_session_state" || row?.kind === "account_sync_watermark") continue;
341
345
  // Deduplicate: each sync appends cumulative totals per bucket, so for
342
346
  // each (source, model, hour_start) keep only the latest (last) entry.
343
347
  const key = `${row.source || ""}|${row.model || ""}|${row.hour_start || ""}`;
@@ -2,7 +2,7 @@
2
2
  "_meta": {
3
3
  "note": "Curated price overrides. Always wins over LiteLLM. Two reasons to live here: (1) self-defined alias names that LiteLLM will never carry (kiro-*, hy3-*, composer-*, kimi-for-coding, free-tier OpenRouter routes); (2) prices we want to pin even if LiteLLM has the model (e.g. cache_write fields LiteLLM often omits). Units: USD per million tokens. Edit this file to override pricing without redeploying.",
4
4
  "units": "usd_per_million_tokens",
5
- "deepseek_v4_pro_pricing_note": "The 2026-05-31 promotional-expiry warning is obsolete: api-docs.deepseek.com/quick_start/pricing verified on 2026-06-10 still lists input $0.435/M, output $0.87/M, cache_read $0.003625/M as the official v4-pro rates (the discount became the list price). Do NOT 'restore' the old 4x rates; re-verify against the official page before any change."
5
+ "deepseek_v4_pricing_note": "Official DeepSeek V4 pricing verified on 2026-08-21. Peak hours are 01:00-04:00 and 06:00-10:00 UTC; all other hours are off-peak at half price. Exact entries store peak rates. computeRowCost applies the time-of-use discount from each row's UTC hour_start."
6
6
  },
7
7
  "exact": {
8
8
  "claude-fable-5": {
@@ -133,16 +133,22 @@
133
133
  "cache_write": 0
134
134
  },
135
135
  "deepseek-v4-flash": {
136
- "input": 0.14,
137
- "output": 0.28,
138
- "cache_read": 0.0028,
139
- "cache_write": 0.14
136
+ "input": 0.44,
137
+ "output": 1.32,
138
+ "cache_read": 0.014,
139
+ "cache_write": 0.44
140
140
  },
141
141
  "deepseek-v4-pro": {
142
- "input": 0.435,
143
- "output": 0.87,
144
- "cache_read": 0.003625,
145
- "cache_write": 0.435
142
+ "input": 1.32,
143
+ "output": 3.96,
144
+ "cache_read": 0.044,
145
+ "cache_write": 1.32
146
+ },
147
+ "deepseek-v4-flash-vision-exp": {
148
+ "input": 0.44,
149
+ "output": 1.32,
150
+ "cache_read": 0.014,
151
+ "cache_write": 0.44
146
152
  },
147
153
  "deepseek-chat": {
148
154
  "input": 0.14,
@@ -15,8 +15,17 @@ const {
15
15
  const { loadLitellmData } = require("./litellm-fetcher");
16
16
 
17
17
  const ZERO_PRICING = { input: 0, output: 0, cache_read: 0, cache_write: 0 };
18
- const PI_SUBSCRIPTION_SOURCES = new Set(["pi-github-copilot", "pi-copilot"]);
18
+ const PI_SUBSCRIPTION_SOURCES = new Set([
19
+ "pi-github-copilot",
20
+ "pi-copilot",
21
+ "prime-agent-github-copilot",
22
+ "prime-agent-copilot",
23
+ ]);
19
24
  const SEED_SNAPSHOT_PATH = path.resolve(__dirname, "seed-snapshot.json");
25
+ const DEEPSEEK_TIME_PRICED_MODELS = [
26
+ "deepseek-v4-flash",
27
+ "deepseek-v4-pro",
28
+ ];
20
29
 
21
30
  // Sync seed load. Done at require-time so callers that haven't awaited
22
31
  // ensurePricingLoaded() (e.g. tests, vite mock startup, edge functions) still
@@ -112,6 +121,32 @@ function getModelPricing(model, opts = {}) {
112
121
  return ZERO_PRICING;
113
122
  }
114
123
 
124
+ function isDeepSeekTimePricedModel(model) {
125
+ const lower = String(model || "").toLowerCase();
126
+ return DEEPSEEK_TIME_PRICED_MODELS.some((name) => lower.includes(name));
127
+ }
128
+
129
+ function isDeepSeekOffPeak(row) {
130
+ if (row?.pricing_tier === "off_peak") return true;
131
+ if (row?.pricing_tier === "peak") return false;
132
+ const timestamp = Date.parse(String(row?.hour_start || ""));
133
+ if (!Number.isFinite(timestamp)) return false;
134
+ const hour = new Date(timestamp).getUTCHours();
135
+ return !((hour >= 1 && hour < 4) || (hour >= 6 && hour < 10));
136
+ }
137
+
138
+ function getRowPricing(row) {
139
+ const pricing = getModelPricing(row?.model, { source: row?.source });
140
+ if (!isDeepSeekTimePricedModel(row?.model) || !isDeepSeekOffPeak(row)) return pricing;
141
+ return {
142
+ ...pricing,
143
+ input: (pricing.input || 0) * 0.5,
144
+ output: (pricing.output || 0) * 0.5,
145
+ cache_read: (pricing.cache_read || 0) * 0.5,
146
+ cache_write: (pricing.cache_write || 0) * 0.5,
147
+ };
148
+ }
149
+
115
150
  // Same formula and Codex/every-code reasoning-folding rule as the previous
116
151
  // computeRowCost in src/lib/local-api.js. Moved here so vite mock + local
117
152
  // server share one source of truth.
@@ -120,7 +155,7 @@ function computeRowCost(row) {
120
155
  // usage record reports a zero marginal cost for those turns; do not
121
156
  // reinterpret the Claude model name as an Anthropic API bill.
122
157
  if (PI_SUBSCRIPTION_SOURCES.has(String(row?.source || "").toLowerCase())) return 0;
123
- const pricing = getModelPricing(row.model, { source: row.source });
158
+ const pricing = getRowPricing(row);
124
159
  const reasoningIncludedInOutput = row.source === "codex" || row.source === "every-code";
125
160
  const reasoningCost = reasoningIncludedInOutput
126
161
  ? 0
@@ -147,6 +182,7 @@ module.exports = {
147
182
  ensurePricingLoaded,
148
183
  getPricingRevision,
149
184
  getModelPricing,
185
+ getRowPricing,
150
186
  computeRowCost,
151
187
  resetPricingForTests,
152
188
  MODEL_PRICING,
@@ -172,6 +172,7 @@ const SOURCE_MODEL_NORMALIZERS = {
172
172
  claude: normalizeClaudeModel,
173
173
  cursor: normalizeCursorModel,
174
174
  "pi-anthropic": normalizeClaudeModel,
175
+ "prime-agent-anthropic": normalizeClaudeModel,
175
176
  zed: normalizeZedModel,
176
177
  workbuddy: normalizeWorkbuddyModel,
177
178
  };