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.
- package/README.de.md +7 -5
- package/README.ja.md +9 -5
- package/README.ko.md +9 -5
- package/README.md +7 -5
- package/README.zh-CN.md +6 -4
- package/dashboard/dist/assets/{AchievementBadge-BJl0ibLg.js → AchievementBadge-LHOuekQ3.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-CV6aNe3g.js → AchievementsPage-DNzeYWBl.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-DxLHu9EV.js → AchievementsSection-Ch5dImt8.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-ConoHhIL.js → BadgeDetailModal-CuikWkUy.js} +1 -1
- package/dashboard/dist/assets/{Card-C_VQxG6G.js → Card-Chwjmxjz.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-_Mfn4MnU.js → ClawdAnimated-BdB7aycS.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-CCRCnYY9.js → CommandPalette-BMwuSEWM.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-DuICwpdM.js → CommunityStatsModal-eEDF-xn8.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-CI27robt.js → ConfirmModal-Be7f6XdL.js} +1 -1
- package/dashboard/dist/assets/{Controls-Cwu0gtgj.js → Controls-CtBFPCBY.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-GsFvPie1.js → DashboardPage-M0p-SU-f.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-BSK76UQE.js → DevicePage-CAfcUond.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-BXQlPs02.js → DialogClose-kd1wIljN.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-CjJLpfCL.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-DyvhHV1-.js → DialogTitle-DhB-tbnZ.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-DaP6F4f8.js → FadeIn-B8eRWBdy.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-CKB5puCd.js → HeaderGithubStar-DmWep8uP.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-BG6k61dF.js → HoverTooltip-DDyTZli0.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-Oqt1J9S2.js → IpCheckPage-CN8hYm4o.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-45qoUn79.js → LandingPage-B3cHASqK.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-ZgXnaI_N.js → LeaderboardAvatar-CEcLXrJL.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-C7E_LrFk.js → LeaderboardPage-H6HW9hon.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-D6PUcj3b.js → LeaderboardProfileModal-B_5PyjRA.js} +4 -4
- package/dashboard/dist/assets/{LeaderboardProfilePage-VcC9DK_B.js → LeaderboardProfilePage-DR9KWLWv.js} +3 -3
- package/dashboard/dist/assets/{LimitsPage-B9K0RFQD.js → LimitsPage-Da0hRxO_.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-BGsfKEM9.js → LocalOnlyNotice-DuTdva_n.js} +1 -1
- package/dashboard/dist/assets/LoginCard-DOL3osrj.js +1 -0
- package/dashboard/dist/assets/{LoginModal-N3pWGciC.js → LoginModal-DBmaQL2L.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-TGgL0aXF.js → LoginPage-Dk08VrkC.js} +1 -1
- package/dashboard/dist/assets/{PetPage-DPi7OT_4.js → PetPage-BbCIdpzx.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-BkptZaPd.js → PopoverPopup-C1fJZ4qt.js} +1 -1
- package/dashboard/dist/assets/ProviderIcon-FfLD-f73.js +1 -0
- package/dashboard/dist/assets/{ResetPasswordPage-DQ4qdjBD.js → ResetPasswordPage-Bg7eVc6L.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-CuTrxHXS.js → SegmentedControl-CFNHcUz7.js} +1 -1
- package/dashboard/dist/assets/{Select-_Txi2j1D.js → Select-B0Q0eDcJ.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-DVKhj-YN.js → SelectItemText-BVbLYxSQ.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-ChflNSK0.js → ServiceStatusPage-DCZH2kz5.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-CHv6xK2J.js → SessionsPage-DO5Et7ss.js} +2 -2
- package/dashboard/dist/assets/{SettingsPage-DkWAYALG.js → SettingsPage-afgxGfzT.js} +2 -2
- package/dashboard/dist/assets/{SkillsPage-DnTSlBl-.js → SkillsPage-DDKiHCF7.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-yAMCaU-k.js → TokenGalaxy-DV1Xoe5z.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-BeDIqB0k.js → ToolbarRootContext-DNckd9Qz.js} +1 -1
- package/dashboard/dist/assets/TrendMonitor-CliMJxCC.js +19 -0
- package/dashboard/dist/assets/{WidgetsPage-y_qKZ9Gv.js → WidgetsPage-BrwG6YMM.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-BGUGI6vk.js → WrappedPage-Ft9YJd0D.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-CRvi_9hb.js → __vite-browser-external-C6_BtvDo.js} +4 -3
- package/dashboard/dist/assets/agent-logos-yYNu1ZUy.js +1 -0
- package/dashboard/dist/assets/{arrow-up-right-CjEesyJI.js → arrow-up-right-B_K57Vbg.js} +1 -1
- package/dashboard/dist/assets/{check-KDrH1gwE.js → check-xH63o2xJ.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-DseqFm-D.js → chevron-right-D3F2rAfr.js} +1 -1
- package/dashboard/dist/assets/{copy-CFJ1QKAQ.js → copy-WPfJGGI0.js} +1 -1
- package/dashboard/dist/assets/{download-BQUNGUdh.js → download-BgtWtEXI.js} +1 -1
- package/dashboard/dist/assets/{external-link-BCEOLLH2.js → external-link-DSHGoZRM.js} +1 -1
- package/dashboard/dist/assets/{flame-BigtoGg2.js → flame-CNGl6Qac.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CS1GUpAV.js → format-tokens-jVm2UX7Y.js} +1 -1
- package/dashboard/dist/assets/{icons-DpZ1a8Ct.js → icons-B1JZg-Id.js} +1 -1
- package/dashboard/dist/assets/{index-DqkwD6eO.js → index-BTZibjLi.js} +1 -1
- package/dashboard/dist/assets/{index-lGAAw1Rv.js → index-BjD81Qce.js} +1 -1
- package/dashboard/dist/assets/{index-DFU_yVc5.js → index-CUFHknAy.js} +1 -1
- package/dashboard/dist/assets/{info-Br_jtGtO.js → info-CLM460KM.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-CkqO1LMQ.js → limits-providers-CFO4O1qh.js} +1 -1
- package/dashboard/dist/assets/{link-2-D-nAhkk-.js → link-2-BHOsUnAI.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-CuNPuDRR.js → loader-circle-CgcIQi92.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-dlvwitfg.js → maximize-2-DvqfuJEA.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-ORha_lPI.js → modifiers.esm-CNiwIkPh.js} +1 -1
- package/dashboard/dist/assets/module-CV1mYFsv.js +3 -0
- package/dashboard/dist/assets/{monitor-smartphone-CI2qvlMB.js → monitor-smartphone-BLutpxrG.js} +1 -1
- package/dashboard/dist/assets/provider-display-BxIgWUEa.js +42 -0
- package/dashboard/dist/assets/{react-Dy9iWvqT.js → react-CM-ozVnh.js} +1 -1
- package/dashboard/dist/assets/{search-Do7tzc3k.js → search-4T07INKt.js} +1 -1
- package/dashboard/dist/assets/{skills-api-CJ-8EFac.js → skills-api-EXMD3LD1.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-BKQg8NHC.js → sortable.esm-jC6mPJiL.js} +1 -1
- package/dashboard/dist/assets/{store-SUT-n3k3.js → store-CvSEdgf2.js} +1 -1
- package/dashboard/dist/assets/{terminal-BX4DOxd6.js → terminal-D9fyE2pP.js} +1 -1
- package/dashboard/dist/assets/{trash-2-VERlG247.js → trash-2-QLUsAY3l.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-BX6_XUdu.js → use-community-stats-Chrt3kpU.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-BpC3tYRw.js → use-limits-display-prefs-BJJd750e.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-CX-LREPo.js → use-ordered-list-BBJp5lFv.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-BvXZCGFC.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-B1G8hzVM.js → use-session-efficiency-pref-BXbvx2Ht.js} +1 -1
- package/dashboard/dist/assets/{use-transform-DNCqLbUD.js → use-transform-BVp_MUsU.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-Cvbvf30m.js → use-usage-limits-Dk3EkeIs.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-EciQmJkm.js → useCurrency-Bi88Sd__.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-sLkmam8C.js → useScrollLock-if34g2H-.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-nb9na-2-.js → useTokenFormat-C2cz-5qJ.js} +1 -1
- package/dashboard/dist/assets/{zap-BQOWCMov.js → zap-D1yHmcQR.js} +1 -1
- package/dashboard/dist/index.html +28 -24
- package/dashboard/dist/ip-check.html +1 -1
- package/dashboard/dist/leaderboard.html +1 -1
- package/dashboard/dist/llms.txt +4 -2
- package/dashboard/dist/share.html +1 -1
- package/package.json +2 -2
- package/src/commands/init.js +12 -1
- package/src/commands/status.js +59 -0
- package/src/commands/sync.js +220 -4
- package/src/lib/account-usage-dedup.js +267 -0
- package/src/lib/local-api.js +4 -0
- package/src/lib/pricing/curated-overrides.json +15 -9
- package/src/lib/pricing/index.js +38 -2
- package/src/lib/pricing/matcher.js +1 -0
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +853 -14
- package/src/lib/source-metadata.js +1 -1
- package/src/lib/trae-cn-config.js +539 -0
- package/dashboard/dist/assets/DialogDescription-M_lg2jhD.js +0 -1
- package/dashboard/dist/assets/LoginCard-j5HVFRdP.js +0 -1
- package/dashboard/dist/assets/ProviderIcon-zaz4nwxI.js +0 -1
- package/dashboard/dist/assets/TrendMonitor-BthLuYwy.js +0 -19
- package/dashboard/dist/assets/agent-logos-DwHwn_cb.js +0 -1
- package/dashboard/dist/assets/module-BRJxSJmz.js +0 -3
- package/dashboard/dist/assets/provider-display-BNCdZ83n.js +0 -42
- 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
|
+
};
|
package/src/lib/local-api.js
CHANGED
|
@@ -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
|
-
"
|
|
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.
|
|
137
|
-
"output":
|
|
138
|
-
"cache_read": 0.
|
|
139
|
-
"cache_write": 0.
|
|
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":
|
|
143
|
-
"output":
|
|
144
|
-
"cache_read": 0.
|
|
145
|
-
"cache_write":
|
|
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,
|
package/src/lib/pricing/index.js
CHANGED
|
@@ -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([
|
|
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 =
|
|
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
|
};
|