tokentracker-cli 0.91.0 → 0.92.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.de.md +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-iyRg0rYF.js → AchievementBadge-o8hl3my2.js} +1 -1
- package/dashboard/dist/assets/{AchievementsPage-CkBm3bN3.js → AchievementsPage-CjDEyZfD.js} +2 -2
- package/dashboard/dist/assets/{AchievementsSection-a4Z7VtlP.js → AchievementsSection-Cwv1Zmyf.js} +1 -1
- package/dashboard/dist/assets/{BadgeDetailModal-DHTN0Ky6.js → BadgeDetailModal-BDxTgOvT.js} +1 -1
- package/dashboard/dist/assets/{Card-CdygGEqz.js → Card-Cp5v7Y19.js} +1 -1
- package/dashboard/dist/assets/{ClawdAnimated-D-Z823Nc.js → ClawdAnimated-6Ibb4qCZ.js} +1 -1
- package/dashboard/dist/assets/{CommandPalette-VGkj8w2X.js → CommandPalette-CIf9oAqq.js} +1 -1
- package/dashboard/dist/assets/{CommunityStatsModal-BDT4xqfF.js → CommunityStatsModal-UwiOPFu0.js} +1 -1
- package/dashboard/dist/assets/{ConfirmModal-BIKv4AlL.js → ConfirmModal-xkYsrh0v.js} +1 -1
- package/dashboard/dist/assets/{Controls-BkZVm9t1.js → Controls-C2Z2X0ZQ.js} +1 -1
- package/dashboard/dist/assets/{DashboardPage-BFkdjUdd.js → DashboardPage-Do_NMUFK.js} +1 -1
- package/dashboard/dist/assets/{DevicePage-BGw861bu.js → DevicePage-DL5OjugJ.js} +1 -1
- package/dashboard/dist/assets/{DialogClose-CSEl6ru8.js → DialogClose-D0w08oR2.js} +1 -1
- package/dashboard/dist/assets/DialogDescription-rhGVm7N2.js +1 -0
- package/dashboard/dist/assets/{DialogTitle-CWPdu5V-.js → DialogTitle-IKAPm5Pk.js} +1 -1
- package/dashboard/dist/assets/{FadeIn-CdCANtvs.js → FadeIn-Dve77aEj.js} +1 -1
- package/dashboard/dist/assets/{HeaderGithubStar-dQQnH85f.js → HeaderGithubStar-BVYbLWDi.js} +1 -1
- package/dashboard/dist/assets/{HoverTooltip-CqJUI-WG.js → HoverTooltip-BrF9-z7f.js} +1 -1
- package/dashboard/dist/assets/{IpCheckPage-BRqTXlSu.js → IpCheckPage-BZ1VyVsm.js} +1 -1
- package/dashboard/dist/assets/{LandingPage-DhMTgDng.js → LandingPage-jkjtQl58.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardAvatar-BrGcByG1.js → LeaderboardAvatar-D9kUudjQ.js} +1 -1
- package/dashboard/dist/assets/{LeaderboardPage-BmdopPjo.js → LeaderboardPage-DXg7Qv8T.js} +2 -2
- package/dashboard/dist/assets/{LeaderboardProfileModal-DRZqPo-A.js → LeaderboardProfileModal-jTlaOi-j.js} +4 -4
- package/dashboard/dist/assets/{LeaderboardProfilePage-CTbC39KX.js → LeaderboardProfilePage-DsKQ_Of8.js} +3 -3
- package/dashboard/dist/assets/{LimitsPage-B7AuLq1S.js → LimitsPage-CMqZAQCa.js} +1 -1
- package/dashboard/dist/assets/{LocalOnlyNotice-DyIAspoq.js → LocalOnlyNotice-CkLs3CF_.js} +1 -1
- package/dashboard/dist/assets/LoginCard-SmOXM_PX.js +1 -0
- package/dashboard/dist/assets/{LoginModal-D3bmr_jI.js → LoginModal-CvtEBkrO.js} +1 -1
- package/dashboard/dist/assets/{LoginPage-D7SgmEhZ.js → LoginPage-Zm-dRt_Y.js} +1 -1
- package/dashboard/dist/assets/{PetPage-5A3Q0zlw.js → PetPage-Bcj7hzNz.js} +1 -1
- package/dashboard/dist/assets/{PopoverPopup-ajEhdI8m.js → PopoverPopup-CnteSROW.js} +1 -1
- package/dashboard/dist/assets/ProviderIcon-DcQmGbfz.js +1 -0
- package/dashboard/dist/assets/{ResetPasswordPage-Bwbt8tH2.js → ResetPasswordPage-zMb_m3Cx.js} +1 -1
- package/dashboard/dist/assets/{SegmentedControl-BSa5hMnN.js → SegmentedControl-D3KkbQ7T.js} +1 -1
- package/dashboard/dist/assets/{Select-kPKDGb7M.js → Select-0RiFEISo.js} +1 -1
- package/dashboard/dist/assets/{SelectItemText-DNgLclTV.js → SelectItemText-BRV3DFEB.js} +1 -1
- package/dashboard/dist/assets/{ServiceStatusPage-C1j7rPDp.js → ServiceStatusPage-CWF_vKOC.js} +1 -1
- package/dashboard/dist/assets/{SessionsPage-BgoKs_Yq.js → SessionsPage-DIqoT7MU.js} +2 -2
- package/dashboard/dist/assets/{SettingsPage-BUaPftV0.js → SettingsPage-DQZIqiE5.js} +2 -2
- package/dashboard/dist/assets/{SkillsPage-C5GkzfWW.js → SkillsPage-BIIOUHnq.js} +1 -1
- package/dashboard/dist/assets/{TokenGalaxy-DZffAa_0.js → TokenGalaxy-Bulaqjbm.js} +1 -1
- package/dashboard/dist/assets/{ToolbarRootContext-CuLP92P_.js → ToolbarRootContext-Dg-zL8fE.js} +1 -1
- package/dashboard/dist/assets/TrendMonitor-DfafDX6b.js +19 -0
- package/dashboard/dist/assets/{WidgetsPage-BUhiCpcq.js → WidgetsPage-F_2yLE7n.js} +1 -1
- package/dashboard/dist/assets/{WrappedPage-sNFISXVG.js → WrappedPage-BFxYKGQs.js} +1 -1
- package/dashboard/dist/assets/{__vite-browser-external-Cyw7ZNtj.js → __vite-browser-external-CZMdPW0g.js} +4 -3
- package/dashboard/dist/assets/agent-logos-DjyCpabA.js +1 -0
- package/dashboard/dist/assets/{arrow-up-right-BcRkt7hr.js → arrow-up-right-CIAd6sXA.js} +1 -1
- package/dashboard/dist/assets/{check-qhr4U2S5.js → check-Bt3Yu07t.js} +1 -1
- package/dashboard/dist/assets/{chevron-right-iLGEAP7_.js → chevron-right-BiXULxma.js} +1 -1
- package/dashboard/dist/assets/{copy-BhEPCq-q.js → copy-D3k1Xbfl.js} +1 -1
- package/dashboard/dist/assets/{download-D7O1ZzsX.js → download-co2WHJvo.js} +1 -1
- package/dashboard/dist/assets/{external-link-CY2kK2hC.js → external-link-D5az2QwL.js} +1 -1
- package/dashboard/dist/assets/{flame-D4FPuBNn.js → flame-CSCUyn3Z.js} +1 -1
- package/dashboard/dist/assets/{format-tokens-CVHJ7Hdh.js → format-tokens-BebRLYzr.js} +1 -1
- package/dashboard/dist/assets/{icons-B_-51UXa.js → icons-C_y5qr6T.js} +1 -1
- package/dashboard/dist/assets/{index-ClVWat-1.js → index-CXOOOeSl.js} +1 -1
- package/dashboard/dist/assets/{index-cwdr25Zm.js → index-DP3XlgDm.js} +1 -1
- package/dashboard/dist/assets/{index-Djv-aJy0.js → index-R488h3mD.js} +1 -1
- package/dashboard/dist/assets/{info-DOab0eZz.js → info-CD_G_DZp.js} +1 -1
- package/dashboard/dist/assets/{limits-providers-CESm2d6B.js → limits-providers-BlAlATd7.js} +1 -1
- package/dashboard/dist/assets/{link-2-B4LMsC4m.js → link-2-DIn-EC5g.js} +1 -1
- package/dashboard/dist/assets/{loader-circle-CPx6wpSn.js → loader-circle-B5Az_VQe.js} +1 -1
- package/dashboard/dist/assets/{maximize-2-C9z8mbdn.js → maximize-2-ptnkCbEN.js} +1 -1
- package/dashboard/dist/assets/{modifiers.esm-5xRgAl_R.js → modifiers.esm-DEN3VPKe.js} +1 -1
- package/dashboard/dist/assets/module-CV1mYFsv.js +3 -0
- package/dashboard/dist/assets/{monitor-smartphone-B9HAspdR.js → monitor-smartphone-Dqr_CnOQ.js} +1 -1
- package/dashboard/dist/assets/provider-display-BOAoggwg.js +42 -0
- package/dashboard/dist/assets/{react-C45OGvDd.js → react-B3eL33ha.js} +1 -1
- package/dashboard/dist/assets/{search-Bg1qyEdz.js → search-CNNSHW9P.js} +1 -1
- package/dashboard/dist/assets/{skills-api-a8pi7UTd.js → skills-api-BShLlV2m.js} +1 -1
- package/dashboard/dist/assets/{sortable.esm-CtPp8cFA.js → sortable.esm-C6kDS9DZ.js} +1 -1
- package/dashboard/dist/assets/{store-b8zXM1yr.js → store-B4atnDzu.js} +1 -1
- package/dashboard/dist/assets/{terminal-CPf0K0Fn.js → terminal-CXMttuiJ.js} +1 -1
- package/dashboard/dist/assets/{trash-2-uorzBdA0.js → trash-2-aXKBjf5M.js} +1 -1
- package/dashboard/dist/assets/{use-community-stats-DaVwywsM.js → use-community-stats-rGR2J-Vi.js} +1 -1
- package/dashboard/dist/assets/{use-limits-display-prefs-UIzz7PJ3.js → use-limits-display-prefs-CazeqdNI.js} +1 -1
- package/dashboard/dist/assets/{use-ordered-list-C2XOfMH3.js → use-ordered-list-Bx-enbl8.js} +1 -1
- package/dashboard/dist/assets/use-reduced-motion-2NcjtyzY.js +1 -0
- package/dashboard/dist/assets/{use-session-efficiency-pref-D5KHakYg.js → use-session-efficiency-pref-B7RHUtup.js} +1 -1
- package/dashboard/dist/assets/{use-transform-Cfo_2_id.js → use-transform-CWVOY76a.js} +1 -1
- package/dashboard/dist/assets/{use-usage-limits-CANTpxrA.js → use-usage-limits-C9XRiYNi.js} +1 -1
- package/dashboard/dist/assets/{useCurrency-CjvWekKH.js → useCurrency-D7ICju5j.js} +1 -1
- package/dashboard/dist/assets/{useScrollLock-ChQdbuJg.js → useScrollLock-DXWPQF_D.js} +1 -1
- package/dashboard/dist/assets/{useTokenFormat-4MY04JPl.js → useTokenFormat-wDhHrsAL.js} +1 -1
- package/dashboard/dist/assets/{zap-BSXHFUBb.js → zap-BZDqgZR1.js} +1 -1
- package/dashboard/dist/brand-logos/dots.png +0 -0
- package/dashboard/dist/brand-logos/pi.svg +8 -15
- package/dashboard/dist/index.html +31 -26
- 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 +3 -1
- package/src/commands/status.js +46 -0
- package/src/commands/sync.js +188 -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 +32 -1
- package/src/lib/pricing/seed-snapshot.json +1 -1
- package/src/lib/rollout.js +730 -1
- package/src/lib/source-metadata.js +1 -1
- package/src/lib/trae-cn-config.js +539 -0
- package/dashboard/dist/assets/DialogDescription-DfNXnqay.js +0 -1
- package/dashboard/dist/assets/LoginCard-LcOaKaCK.js +0 -1
- package/dashboard/dist/assets/ProviderIcon-C4yuu9FG.js +0 -1
- package/dashboard/dist/assets/TrendMonitor-BKLAKJmT.js +0 -19
- package/dashboard/dist/assets/agent-logos-Bwf2YFHS.js +0 -1
- package/dashboard/dist/assets/module-BRJxSJmz.js +0 -3
- package/dashboard/dist/assets/provider-display-C0NS0EvN.js +0 -42
- package/dashboard/dist/assets/use-reduced-motion-CNyMWBog.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
|
@@ -22,6 +22,10 @@ const PI_SUBSCRIPTION_SOURCES = new Set([
|
|
|
22
22
|
"prime-agent-copilot",
|
|
23
23
|
]);
|
|
24
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
|
+
];
|
|
25
29
|
|
|
26
30
|
// Sync seed load. Done at require-time so callers that haven't awaited
|
|
27
31
|
// ensurePricingLoaded() (e.g. tests, vite mock startup, edge functions) still
|
|
@@ -117,6 +121,32 @@ function getModelPricing(model, opts = {}) {
|
|
|
117
121
|
return ZERO_PRICING;
|
|
118
122
|
}
|
|
119
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
|
+
|
|
120
150
|
// Same formula and Codex/every-code reasoning-folding rule as the previous
|
|
121
151
|
// computeRowCost in src/lib/local-api.js. Moved here so vite mock + local
|
|
122
152
|
// server share one source of truth.
|
|
@@ -125,7 +155,7 @@ function computeRowCost(row) {
|
|
|
125
155
|
// usage record reports a zero marginal cost for those turns; do not
|
|
126
156
|
// reinterpret the Claude model name as an Anthropic API bill.
|
|
127
157
|
if (PI_SUBSCRIPTION_SOURCES.has(String(row?.source || "").toLowerCase())) return 0;
|
|
128
|
-
const pricing =
|
|
158
|
+
const pricing = getRowPricing(row);
|
|
129
159
|
const reasoningIncludedInOutput = row.source === "codex" || row.source === "every-code";
|
|
130
160
|
const reasoningCost = reasoningIncludedInOutput
|
|
131
161
|
? 0
|
|
@@ -152,6 +182,7 @@ module.exports = {
|
|
|
152
182
|
ensurePricingLoaded,
|
|
153
183
|
getPricingRevision,
|
|
154
184
|
getModelPricing,
|
|
185
|
+
getRowPricing,
|
|
155
186
|
computeRowCost,
|
|
156
187
|
resetPricingForTests,
|
|
157
188
|
MODEL_PRICING,
|