@cad0p/pi-usage 0.11.3 → 0.11.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.
- package/CHANGELOG.md +28 -0
- package/README.md +274 -12
- package/bin/piquota.js +49 -4
- package/extensions/quota-panel.ts +188 -31
- package/omarchy/cad0p.pi-usage/Agent.qml +33 -0
- package/omarchy/cad0p.pi-usage/Main.qml +333 -0
- package/omarchy/cad0p.pi-usage/Panel.qml +136 -0
- package/omarchy/cad0p.pi-usage/README.md +40 -0
- package/omarchy/cad0p.pi-usage/UsageContent.qml +1001 -0
- package/omarchy/cad0p.pi-usage/UsageLogic.js +603 -0
- package/omarchy/cad0p.pi-usage/assets/antigravity.svg +3 -0
- package/omarchy/cad0p.pi-usage/assets/claude.svg +1 -0
- package/omarchy/cad0p.pi-usage/assets/codex-light.svg +1 -0
- package/omarchy/cad0p.pi-usage/assets/codex.svg +1 -0
- package/omarchy/cad0p.pi-usage/assets/fireworks.svg +7 -0
- package/omarchy/cad0p.pi-usage/assets/github-actions.svg +3 -0
- package/omarchy/cad0p.pi-usage/assets/opencode-go.svg +3 -0
- package/omarchy/cad0p.pi-usage/manifest.json +56 -0
- package/package.json +2 -1
- package/src/auth/pi-auth.js +3 -0
- package/src/cli/args.js +5 -0
- package/src/cli/commands/stats.js +76 -0
- package/src/engine.js +44 -0
- package/src/local/history.js +1160 -0
- package/src/local/pi-sessions.js +1511 -0
- package/src/omarchy/publish.js +1 -65
- package/src/omarchy/record.js +82 -7
|
@@ -0,0 +1,603 @@
|
|
|
1
|
+
// Pure logic for the AI Usage panel.
|
|
2
|
+
//
|
|
3
|
+
// No QML imports and no wall clock: Panel.qml and UsageContent.qml load this
|
|
4
|
+
// file as a JS module (import "UsageLogic.js" as Logic), and the node tests
|
|
5
|
+
// evaluate it in a sandbox. Every function that needs "now" takes it as an
|
|
6
|
+
// argument.
|
|
7
|
+
//
|
|
8
|
+
// The window, model-name and token formatting helpers are carried over from
|
|
9
|
+
// Omarchy's built-in Agents plugin (plugins/agents, MIT, by Omarchy) so the
|
|
10
|
+
// two panels read a record the same way.
|
|
11
|
+
|
|
12
|
+
function clamp(v, lo, hi) { return Math.max(lo, Math.min(hi, v)) }
|
|
13
|
+
|
|
14
|
+
// A record's lists arrive as JS arrays from JSON.parse, but once they pass
|
|
15
|
+
// through a Repeater's modelData they are QML sequences, which
|
|
16
|
+
// Array.isArray rejects. Anything with a numeric length is a list here.
|
|
17
|
+
function isList(value) {
|
|
18
|
+
return !!value && typeof value === "object" && typeof value.length === "number"
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
function toArray(value) {
|
|
22
|
+
if (!isList(value)) return []
|
|
23
|
+
var out = []
|
|
24
|
+
for (var i = 0; i < value.length; i++) out.push(value[i])
|
|
25
|
+
return out
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
function numberValue(value) {
|
|
29
|
+
var n = Number(value || 0)
|
|
30
|
+
return isFinite(n) ? Math.round(n) : 0
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
// ------------------------------------------------------------------ windows
|
|
34
|
+
|
|
35
|
+
// Claude spells its windows out ("Session (5-hour)"), Codex abbreviates them
|
|
36
|
+
// ("5h window", "30m window"). Both have to land on the same record.
|
|
37
|
+
function windowIsLong(text) {
|
|
38
|
+
return text.indexOf("week") >= 0 || text.indexOf("7-day") >= 0 || text.indexOf("seven") >= 0
|
|
39
|
+
|| text.indexOf("month") >= 0 || text.indexOf("30-day") >= 0
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function windowSpanMs(label) {
|
|
43
|
+
var text = String(label || "").toLowerCase()
|
|
44
|
+
if (text.indexOf("month") >= 0 || text.indexOf("30-day") >= 0) return 30 * 24 * 3600 * 1000
|
|
45
|
+
if (windowIsLong(text)) return 7 * 24 * 3600 * 1000
|
|
46
|
+
var hours = text.match(/(\d+)\s*-?\s*h(?:our)?\b/)
|
|
47
|
+
if (hours) return Number(hours[1]) * 3600 * 1000
|
|
48
|
+
var minutes = text.match(/(\d+)\s*-?\s*m(?:in(?:ute)?s?)?\b/)
|
|
49
|
+
if (minutes) return Number(minutes[1]) * 60 * 1000
|
|
50
|
+
return 0
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
function windowTitle(label) {
|
|
54
|
+
var text = String(label || "").toLowerCase()
|
|
55
|
+
if (text.indexOf("month") >= 0) return "Monthly"
|
|
56
|
+
if (windowIsLong(text)) return "Weekly"
|
|
57
|
+
if (text.indexOf("session") >= 0 || windowSpanMs(label) > 0) return "Session"
|
|
58
|
+
var plain = String(label || "").replace(/\s*\(.*\)\s*/, "").trim()
|
|
59
|
+
return plain === "" ? "Limit" : plain
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
// A collector that already knows which window a limit belongs to says so in
|
|
63
|
+
// `title`, and that beats reading it back out of the label.
|
|
64
|
+
function limitWindow(label, percent, resetAt, title) {
|
|
65
|
+
return {
|
|
66
|
+
title: String(title || "") !== "" ? String(title) : windowTitle(label),
|
|
67
|
+
percent: Number(percent),
|
|
68
|
+
resetAt: String(resetAt || "")
|
|
69
|
+
}
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
// Negative percentages are placeholders (an error record keeps its tab with
|
|
73
|
+
// one), never drawn.
|
|
74
|
+
function limitWindows(list) {
|
|
75
|
+
var out = []
|
|
76
|
+
list = toArray(list)
|
|
77
|
+
for (var i = 0; i < list.length; i++) {
|
|
78
|
+
var entry = list[i] || {}
|
|
79
|
+
var percent = Number(entry.percent)
|
|
80
|
+
if (entry.percent !== null && entry.percent !== undefined && entry.percent !== "" && percent >= 0) {
|
|
81
|
+
percent = clamp(percent, 0, 1)
|
|
82
|
+
out.push(limitWindow(entry.label, percent, entry.resetsAt, entry.title))
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
return out
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
function resetOrder(w) {
|
|
89
|
+
if (!w || w.resetAt === "") return Infinity
|
|
90
|
+
var ms = new Date(w.resetAt).getTime()
|
|
91
|
+
return isFinite(ms) ? ms : Infinity
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
// The window that decides how much room is left: the fullest one, and on a
|
|
95
|
+
// tie the one that resets first.
|
|
96
|
+
function bindingWindow(windows) {
|
|
97
|
+
var best = null
|
|
98
|
+
for (var i = 0; i < (windows || []).length; i++) {
|
|
99
|
+
var w = windows[i]
|
|
100
|
+
if (!best || w.percent > best.percent
|
|
101
|
+
|| (w.percent === best.percent && resetOrder(w) < resetOrder(best))) best = w
|
|
102
|
+
}
|
|
103
|
+
return best
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// The short words /usage uses for an account's windows.
|
|
107
|
+
function windowTag(title) {
|
|
108
|
+
var text = String(title || "")
|
|
109
|
+
if (text === "Session") return "session"
|
|
110
|
+
if (text === "Weekly") return "week"
|
|
111
|
+
if (text === "Monthly") return "month"
|
|
112
|
+
return text.toLowerCase()
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function resetMs(w, nowMs) {
|
|
116
|
+
if (!w || !w.resetAt) return -1
|
|
117
|
+
var ms = new Date(w.resetAt).getTime()
|
|
118
|
+
return isFinite(ms) ? ms - nowMs : -1
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// "Resets in 2d 4h": the built-in panel's countdown.
|
|
122
|
+
function formatDuration(ms) {
|
|
123
|
+
if (!(ms > 0)) return "now"
|
|
124
|
+
var minutes = Math.floor(ms / 60000)
|
|
125
|
+
var hours = Math.floor(minutes / 60)
|
|
126
|
+
var days = Math.floor(hours / 24)
|
|
127
|
+
if (days > 0) return days + "d " + (hours % 24) + "h"
|
|
128
|
+
if (hours > 0) return hours + "h " + (minutes % 60) + "m"
|
|
129
|
+
return Math.max(1, minutes) + "m"
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// One unit, for the dense rows: 10m, 4h, 19d — the same reading as /usage.
|
|
133
|
+
function compactDuration(ms) {
|
|
134
|
+
if (!(ms > 0)) return "now"
|
|
135
|
+
var minutes = Math.floor(ms / 60000)
|
|
136
|
+
if (minutes < 60) return Math.max(1, minutes) + "m"
|
|
137
|
+
var hours = Math.floor(minutes / 60)
|
|
138
|
+
if (hours < 24) return hours + "h"
|
|
139
|
+
return Math.floor(hours / 24) + "d"
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
// ----------------------------------------------------------------- accounts
|
|
143
|
+
|
|
144
|
+
// One row per account of a multi-account record, every window kept.
|
|
145
|
+
function accountRows(p) {
|
|
146
|
+
var list = p && isList(p.accounts) ? p.accounts : []
|
|
147
|
+
var rows = []
|
|
148
|
+
for (var i = 0; i < list.length; i++) {
|
|
149
|
+
var entry = list[i] || {}
|
|
150
|
+
var windows = limitWindows(entry.limits)
|
|
151
|
+
rows.push({
|
|
152
|
+
id: String(entry.id || ("#" + (i + 1))),
|
|
153
|
+
ok: entry.ok === true,
|
|
154
|
+
status: String(entry.status || ""),
|
|
155
|
+
windows: windows,
|
|
156
|
+
binding: bindingWindow(windows)
|
|
157
|
+
})
|
|
158
|
+
}
|
|
159
|
+
return rows
|
|
160
|
+
}
|
|
161
|
+
|
|
162
|
+
function failedAccountCount(p) {
|
|
163
|
+
var rows = accountRows(p)
|
|
164
|
+
var failed = 0
|
|
165
|
+
for (var i = 0; i < rows.length; i++) if (!rows[i].ok) failed++
|
|
166
|
+
return failed
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
// What the section header and the bar alarm read.
|
|
170
|
+
//
|
|
171
|
+
// A single account is bound by its fullest window. Several accounts of one
|
|
172
|
+
// family are used one after another (pi rotates to the next slot when one is
|
|
173
|
+
// exhausted), so what stops the next prompt is the account with the most
|
|
174
|
+
// room left: the healthy account whose own fullest window is the emptiest.
|
|
175
|
+
// That is the "lowest" figure /usage prints for the family.
|
|
176
|
+
function headline(p) {
|
|
177
|
+
if (!p) return null
|
|
178
|
+
var rows = accountRows(p)
|
|
179
|
+
if (rows.length > 0) {
|
|
180
|
+
var best = null
|
|
181
|
+
for (var i = 0; i < rows.length; i++) {
|
|
182
|
+
var row = rows[i]
|
|
183
|
+
if (!row.ok || !row.binding) continue
|
|
184
|
+
if (!best || row.binding.percent < best.binding.percent
|
|
185
|
+
|| (row.binding.percent === best.binding.percent && resetOrder(row.binding) < resetOrder(best.binding)))
|
|
186
|
+
best = row
|
|
187
|
+
}
|
|
188
|
+
if (best) return {
|
|
189
|
+
title: best.id + " · " + best.binding.title,
|
|
190
|
+
percent: best.binding.percent,
|
|
191
|
+
resetAt: best.binding.resetAt,
|
|
192
|
+
accountId: best.id
|
|
193
|
+
}
|
|
194
|
+
// Every account failed: fall through to the flattened limits, which then
|
|
195
|
+
// only hold the placeholder and yield nothing.
|
|
196
|
+
}
|
|
197
|
+
return bindingWindow(limitWindows(p.limits))
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
function urgency(p) {
|
|
201
|
+
var h = headline(p)
|
|
202
|
+
if (h) return h.percent
|
|
203
|
+
if (p && p.balance && p.balance.funded > 0)
|
|
204
|
+
return 1 - clamp(p.balance.remaining / p.balance.funded, 0, 1)
|
|
205
|
+
return -1
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
// ---------------------------------------------------------------- expansion
|
|
209
|
+
|
|
210
|
+
// With nothing chosen yet, the section that is closest to stopping you opens
|
|
211
|
+
// and the rest stay folded: their headers already carry the headline, so the
|
|
212
|
+
// panel answers "am I about to run out?" without a keystroke. Ties go to the
|
|
213
|
+
// window that resets first; with no limits anywhere the first section opens.
|
|
214
|
+
function defaultExpandedId(providers) {
|
|
215
|
+
var best = null
|
|
216
|
+
var bestUrgency = -Infinity
|
|
217
|
+
var bestReset = Infinity
|
|
218
|
+
for (var i = 0; i < (providers || []).length; i++) {
|
|
219
|
+
var p = providers[i]
|
|
220
|
+
var u = urgency(p)
|
|
221
|
+
var h = headline(p)
|
|
222
|
+
var r = h ? resetOrder(h) : Infinity
|
|
223
|
+
if (best === null || u > bestUrgency || (u === bestUrgency && r < bestReset)) {
|
|
224
|
+
best = p
|
|
225
|
+
bestUrgency = u
|
|
226
|
+
bestReset = r
|
|
227
|
+
}
|
|
228
|
+
}
|
|
229
|
+
return best ? String(best.providerId) : ""
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// An explicit choice sticks; an untouched section follows the default.
|
|
233
|
+
function isExpanded(id, overrides, defaultId) {
|
|
234
|
+
if (overrides && Object.prototype.hasOwnProperty.call(overrides, id)) return overrides[id] === true
|
|
235
|
+
return id === defaultId
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
function withExpanded(overrides, id, expanded) {
|
|
239
|
+
var next = {}
|
|
240
|
+
for (var key in (overrides || {})) next[key] = overrides[key]
|
|
241
|
+
next[id] = expanded === true
|
|
242
|
+
return next
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
function parseUiState(text) {
|
|
246
|
+
try {
|
|
247
|
+
var parsed = JSON.parse(String(text || ""))
|
|
248
|
+
var expanded = parsed && parsed.expanded && typeof parsed.expanded === "object" ? parsed.expanded : {}
|
|
249
|
+
var clean = {}
|
|
250
|
+
for (var key in expanded) if (typeof expanded[key] === "boolean") clean[key] = expanded[key]
|
|
251
|
+
return { expanded: clean }
|
|
252
|
+
} catch (e) {
|
|
253
|
+
return { expanded: {} }
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
function serializeUiState(state) {
|
|
258
|
+
return JSON.stringify({ schemaVersion: 1, expanded: (state && state.expanded) || {} }, null, 2) + "\n"
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
// ---------------------------------------------------------------- providers
|
|
262
|
+
|
|
263
|
+
function balanceValue(raw) {
|
|
264
|
+
if (!raw || typeof raw !== "object") return null
|
|
265
|
+
var remaining = Number(raw.remaining)
|
|
266
|
+
var funded = Number(raw.funded)
|
|
267
|
+
if (!isFinite(remaining) || remaining < 0) return null
|
|
268
|
+
return {
|
|
269
|
+
remaining: remaining,
|
|
270
|
+
funded: isFinite(funded) && funded > 0 ? funded : 0,
|
|
271
|
+
spent: Math.max(0, Number(raw.spent) || 0),
|
|
272
|
+
currency: String(raw.currency || "USD"),
|
|
273
|
+
estimated: raw.estimated === true
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
// The record, normalized the way the built-in Main.qml does (minus the
|
|
278
|
+
// cross-device merge), plus the accounts list.
|
|
279
|
+
function displayProvider(record) {
|
|
280
|
+
record = record || {}
|
|
281
|
+
return {
|
|
282
|
+
providerId: String(record.id),
|
|
283
|
+
providerName: String(record.name || record.id),
|
|
284
|
+
ready: record.ready === true,
|
|
285
|
+
usageStatusText: String(record.usageStatusText || ""),
|
|
286
|
+
authHelpText: String(record.authHelpText || ""),
|
|
287
|
+
limits: isList(record.limits) ? record.limits : [],
|
|
288
|
+
accounts: isList(record.accounts) ? record.accounts : [],
|
|
289
|
+
tierLabel: String(record.tierLabel || ""),
|
|
290
|
+
balance: balanceValue(record.balance),
|
|
291
|
+
todayPrompts: numberValue(record.todayPrompts),
|
|
292
|
+
todaySessions: numberValue(record.todaySessions),
|
|
293
|
+
todayTotalTokens: numberValue(record.todayTotalTokens),
|
|
294
|
+
recentDays: isList(record.recentDays) ? record.recentDays : [],
|
|
295
|
+
totalPrompts: numberValue(record.totalPrompts),
|
|
296
|
+
totalSessions: numberValue(record.totalSessions),
|
|
297
|
+
activeDays: numberValue(record.activeDays),
|
|
298
|
+
modelUsage: record.modelUsage && typeof record.modelUsage === "object" ? record.modelUsage : {},
|
|
299
|
+
hasLocalStats: record.hasLocalStats !== false,
|
|
300
|
+
hasPromptStats: record.hasPromptStats !== false
|
|
301
|
+
}
|
|
302
|
+
}
|
|
303
|
+
|
|
304
|
+
// The built-in rule: a provider earns a section by having produced numbers.
|
|
305
|
+
function providerHasData(p) {
|
|
306
|
+
return numberValue(p.totalPrompts) > 0 || numberValue(p.totalSessions) > 0
|
|
307
|
+
|| numberValue(p.activeDays) > 0 || numberValue(p.todayPrompts) > 0
|
|
308
|
+
|| numberValue(p.todaySessions) > 0 || (p.limits && p.limits.length > 0)
|
|
309
|
+
|| (p.accounts && p.accounts.length > 0) || !!p.balance
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
function providerEnabled(settings, id) {
|
|
313
|
+
if (!settings || !settings.providers || !settings.providers[id]) return true
|
|
314
|
+
return settings.providers[id].enabled !== false
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
function pluginVisible(providers) {
|
|
318
|
+
return isList(providers) && providers.length > 0
|
|
319
|
+
}
|
|
320
|
+
|
|
321
|
+
function uiStateDirectory(stateHome, override) {
|
|
322
|
+
var path = String(override || "")
|
|
323
|
+
return path !== "" ? path : String(stateHome || "") + "/omarchy/settings"
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
function queueUpdateKind(pending, requested) {
|
|
327
|
+
if (requested === "force" || pending === "") return requested
|
|
328
|
+
return pending
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
// Whether this plugin may run Omarchy's collectors at all. "On" always, "Auto"
|
|
332
|
+
// only while the built-in Agents widget is off the bar (so keeping both never
|
|
333
|
+
// doubles the work), "Off" never. Anything unrecognized reads as Auto, the
|
|
334
|
+
// manifest default, so a missing or mistyped setting cannot switch the
|
|
335
|
+
// collectors on.
|
|
336
|
+
function collectorsWanted(setting, builtinOnBar) {
|
|
337
|
+
var mode = String(setting === null || setting === undefined ? "Auto" : setting).trim().toLowerCase()
|
|
338
|
+
if (mode === "off") return false
|
|
339
|
+
if (mode === "on") return true
|
|
340
|
+
return builtinOnBar !== true
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// A collector that could not reach its limits endpoint writes retryAdvised
|
|
344
|
+
// into its record. A retry is a collector run like any other, so it inherits
|
|
345
|
+
// the same switch: with the setting Off, or Auto while the built-in widget is
|
|
346
|
+
// on the bar, no id comes back and nothing runs.
|
|
347
|
+
function advisedRetryIds(records, settings, collectors) {
|
|
348
|
+
if (!collectors) return []
|
|
349
|
+
var ids = []
|
|
350
|
+
records = toArray(records)
|
|
351
|
+
for (var i = 0; i < records.length; i++) {
|
|
352
|
+
var record = records[i]
|
|
353
|
+
var id = record && String(record.id || "")
|
|
354
|
+
if (id && record.retryAdvised === true && providerEnabled(settings, id)) ids.push(id)
|
|
355
|
+
}
|
|
356
|
+
return ids
|
|
357
|
+
}
|
|
358
|
+
|
|
359
|
+
function recordTime(record) {
|
|
360
|
+
var ms = Date.parse(String(record && record.updatedAt || ""))
|
|
361
|
+
return isFinite(ms) ? ms : -Infinity
|
|
362
|
+
}
|
|
363
|
+
|
|
364
|
+
// One id is one section. A stale file, or a fixture dropped next to a live
|
|
365
|
+
// record, can claim an id that already exists; the newest updatedAt wins, so
|
|
366
|
+
// the panel never draws a provider twice and the second copy cannot share the
|
|
367
|
+
// first one's expansion state.
|
|
368
|
+
function newestRecords(records) {
|
|
369
|
+
var chosen = {}
|
|
370
|
+
var order = []
|
|
371
|
+
for (var i = 0; i < (records || []).length; i++) {
|
|
372
|
+
var record = records[i]
|
|
373
|
+
if (!record || !record.id) continue
|
|
374
|
+
var id = String(record.id)
|
|
375
|
+
if (!Object.prototype.hasOwnProperty.call(chosen, id)) {
|
|
376
|
+
chosen[id] = record
|
|
377
|
+
order.push(id)
|
|
378
|
+
continue
|
|
379
|
+
}
|
|
380
|
+
if (recordTime(record) >= recordTime(chosen[id])) chosen[id] = record
|
|
381
|
+
}
|
|
382
|
+
var out = []
|
|
383
|
+
for (var j = 0; j < order.length; j++) out.push(chosen[order[j]])
|
|
384
|
+
return out
|
|
385
|
+
}
|
|
386
|
+
|
|
387
|
+
// Records sorted by id, the built-in's order, enabled and non-empty only.
|
|
388
|
+
function visibleProviders(records, settings) {
|
|
389
|
+
var list = []
|
|
390
|
+
var unique = newestRecords(records)
|
|
391
|
+
for (var i = 0; i < unique.length; i++) {
|
|
392
|
+
var record = unique[i]
|
|
393
|
+
if (!providerEnabled(settings, String(record.id))) continue
|
|
394
|
+
var p = displayProvider(record)
|
|
395
|
+
if (providerHasData(p)) list.push(p)
|
|
396
|
+
}
|
|
397
|
+
list.sort(function(a, b) { return a.providerId < b.providerId ? -1 : (a.providerId > b.providerId ? 1 : 0) })
|
|
398
|
+
return list
|
|
399
|
+
}
|
|
400
|
+
|
|
401
|
+
// The plan line under the name. A provider that cannot report says why
|
|
402
|
+
// instead, the same substitution the built-in hero makes.
|
|
403
|
+
function headerMeta(p) {
|
|
404
|
+
if (!p) return ""
|
|
405
|
+
if (!p.ready && p.usageStatusText !== "") return p.usageStatusText
|
|
406
|
+
var tier = String(p.tierLabel || "")
|
|
407
|
+
if (tier === "") return "Subscription"
|
|
408
|
+
return tier.charAt(0).toUpperCase() + tier.slice(1)
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
// A short chip on the header when something needs attention.
|
|
412
|
+
function statusChip(p) {
|
|
413
|
+
if (!p) return ""
|
|
414
|
+
if (!p.ready) return p.usageStatusText !== "" ? "Unavailable" : ""
|
|
415
|
+
var failed = failedAccountCount(p)
|
|
416
|
+
if (failed > 0) return failed + " down"
|
|
417
|
+
return ""
|
|
418
|
+
}
|
|
419
|
+
|
|
420
|
+
// Marks resolve by convention: assets/<family>.svg for a pi- record
|
|
421
|
+
// (pi-claude -> claude.svg), then assets/<id>.svg, each with a -light twin
|
|
422
|
+
// tried first on light surfaces.
|
|
423
|
+
// Marks that need a dark twin on light surfaces; every other shipped mark
|
|
424
|
+
// reads on both. Listing them keeps the lookup from logging a failed load
|
|
425
|
+
// for a -light file that was never shipped.
|
|
426
|
+
var LIGHT_TWINS = { codex: true }
|
|
427
|
+
|
|
428
|
+
function iconCandidates(providerId, lightSurface) {
|
|
429
|
+
var id = String(providerId || "")
|
|
430
|
+
// The family first: this plugin ships family marks, so a pi- record finds
|
|
431
|
+
// its mark on the first try instead of logging a failed load.
|
|
432
|
+
var names = id.indexOf("pi-") === 0 && id.length > 3 ? [id.slice(3), id] : [id]
|
|
433
|
+
var out = []
|
|
434
|
+
for (var i = 0; i < names.length; i++) {
|
|
435
|
+
if (lightSurface && LIGHT_TWINS[names[i]] === true) out.push("assets/" + names[i] + "-light.svg")
|
|
436
|
+
out.push("assets/" + names[i] + ".svg")
|
|
437
|
+
}
|
|
438
|
+
return out
|
|
439
|
+
}
|
|
440
|
+
|
|
441
|
+
// -------------------------------------------------------------- local stats
|
|
442
|
+
|
|
443
|
+
// A corrupt or hand-written record can carry a count no provider will ever
|
|
444
|
+
// report (1e299, Infinity). Those must not reach the panel as "1e+299B" or
|
|
445
|
+
// "InfinityB": a non-finite count renders as an em dash and the unit is capped
|
|
446
|
+
// at billions.
|
|
447
|
+
var MAX_TOKEN_COUNT = 999.9e9
|
|
448
|
+
|
|
449
|
+
function formatTokenCount(n) {
|
|
450
|
+
if (n === undefined || n === null) return "0"
|
|
451
|
+
var value = Number(n)
|
|
452
|
+
if (!isFinite(value)) return "—"
|
|
453
|
+
n = Math.min(value, MAX_TOKEN_COUNT)
|
|
454
|
+
if (n >= 1e9) return (n / 1e9).toFixed(1) + "B"
|
|
455
|
+
if (n >= 1e6) return (n / 1e6).toFixed(1) + "M"
|
|
456
|
+
if (n >= 1e3) return (n / 1e3).toFixed(1) + "K"
|
|
457
|
+
return String(n)
|
|
458
|
+
}
|
|
459
|
+
|
|
460
|
+
function modelWordCase(word) {
|
|
461
|
+
if (word === "gpt") return "GPT"
|
|
462
|
+
if (word === "deepseek") return "DeepSeek"
|
|
463
|
+
return word.charAt(0).toUpperCase() + word.slice(1)
|
|
464
|
+
}
|
|
465
|
+
|
|
466
|
+
// `claude-opus-4-8` -> "Opus 4.8", `gpt-5.6-sol` -> "GPT 5.6 Sol".
|
|
467
|
+
function friendlyModelName(id) {
|
|
468
|
+
if (!id) return "Unknown"
|
|
469
|
+
var name = String(id).replace(/^claude-/, "").replace(/-\d{8}$/, "")
|
|
470
|
+
var parts = name.split("-")
|
|
471
|
+
var words = []
|
|
472
|
+
var version = []
|
|
473
|
+
for (var i = 0; i < parts.length; i++) {
|
|
474
|
+
var part = parts[i]
|
|
475
|
+
if (part === "") continue
|
|
476
|
+
if (/^\d/.test(part)) {
|
|
477
|
+
version.push(part)
|
|
478
|
+
continue
|
|
479
|
+
}
|
|
480
|
+
if (version.length > 0) {
|
|
481
|
+
words.push(version.join("."))
|
|
482
|
+
version = []
|
|
483
|
+
}
|
|
484
|
+
words.push(modelWordCase(part))
|
|
485
|
+
}
|
|
486
|
+
if (version.length > 0) words.push(version.join("."))
|
|
487
|
+
return words.length > 0 ? words.join(" ") : "Unknown"
|
|
488
|
+
}
|
|
489
|
+
|
|
490
|
+
function recentDayRows(p) {
|
|
491
|
+
var days = p && isList(p.recentDays) ? p.recentDays : []
|
|
492
|
+
var rows = []
|
|
493
|
+
for (var i = 0; i < days.length; i++) {
|
|
494
|
+
var day = days[i]
|
|
495
|
+
if (day && typeof day === "object") rows.push(day)
|
|
496
|
+
}
|
|
497
|
+
return rows
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
function hasDays(p) {
|
|
501
|
+
return !!p && p.hasLocalStats !== false && recentDayRows(p).length > 0
|
|
502
|
+
}
|
|
503
|
+
|
|
504
|
+
function weekPeak(p) {
|
|
505
|
+
var days = p ? (p.recentDays || []) : []
|
|
506
|
+
var peak = 0
|
|
507
|
+
for (var i = 0; i < days.length; i++) {
|
|
508
|
+
var day = days[i]
|
|
509
|
+
peak = Math.max(peak, Number(day && day.messageCount || 0))
|
|
510
|
+
}
|
|
511
|
+
return peak
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
// The heaviest four models, as the built-in shows them.
|
|
515
|
+
function modelRows(p) {
|
|
516
|
+
var usageByModel = p && p.hasLocalStats !== false ? (p.modelUsage || {}) : {}
|
|
517
|
+
var rows = []
|
|
518
|
+
for (var id in usageByModel) {
|
|
519
|
+
var bucket = usageByModel[id] || {}
|
|
520
|
+
var input = Number(bucket.inputTokens || 0)
|
|
521
|
+
var output = Number(bucket.outputTokens || 0)
|
|
522
|
+
var cacheRead = Number(bucket.cacheReadInputTokens || 0)
|
|
523
|
+
var cacheWrite = Number(bucket.cacheCreationInputTokens || 0)
|
|
524
|
+
rows.push({
|
|
525
|
+
name: friendlyModelName(id),
|
|
526
|
+
total: input + output + cacheRead + cacheWrite,
|
|
527
|
+
input: input,
|
|
528
|
+
output: output,
|
|
529
|
+
cacheRead: cacheRead,
|
|
530
|
+
cacheWrite: cacheWrite
|
|
531
|
+
})
|
|
532
|
+
}
|
|
533
|
+
rows.sort(function(a, b) { return b.total - a.total })
|
|
534
|
+
return rows.slice(0, 4)
|
|
535
|
+
}
|
|
536
|
+
|
|
537
|
+
function modelTooltip(row) {
|
|
538
|
+
if (!row) return ""
|
|
539
|
+
return "In " + formatTokenCount(row.input)
|
|
540
|
+
+ " · out " + formatTokenCount(row.output)
|
|
541
|
+
+ " · cache read " + formatTokenCount(row.cacheRead)
|
|
542
|
+
+ " · cache write " + formatTokenCount(row.cacheWrite)
|
|
543
|
+
}
|
|
544
|
+
|
|
545
|
+
function todayDate(nowMs) {
|
|
546
|
+
var now = new Date(nowMs)
|
|
547
|
+
return now.getFullYear()
|
|
548
|
+
+ "-" + String(now.getMonth() + 1).padStart(2, "0")
|
|
549
|
+
+ "-" + String(now.getDate()).padStart(2, "0")
|
|
550
|
+
}
|
|
551
|
+
|
|
552
|
+
function dayName(date) {
|
|
553
|
+
var parsed = new Date(String(date || "") + "T00:00:00")
|
|
554
|
+
if (isNaN(parsed.getTime())) return String(date || "")
|
|
555
|
+
return ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"][parsed.getDay()]
|
|
556
|
+
}
|
|
557
|
+
|
|
558
|
+
function dayLabel(date, today) {
|
|
559
|
+
return today ? "Today" : dayName(date)
|
|
560
|
+
}
|
|
561
|
+
|
|
562
|
+
function dayTooltip(day, today, p) {
|
|
563
|
+
if (!day) return ""
|
|
564
|
+
var parsed = new Date(String(day.date) + "T00:00:00")
|
|
565
|
+
var label = isNaN(parsed.getTime())
|
|
566
|
+
? String(day.date)
|
|
567
|
+
: dayName(day.date) + " " + (parsed.getMonth() + 1) + "/" + parsed.getDate()
|
|
568
|
+
var text = label + " · " + formatTokenCount(Number(day.messageCount || 0)) + " tokens"
|
|
569
|
+
if (today && p && p.hasPromptStats !== false)
|
|
570
|
+
text += " · " + Number(p.todayPrompts || 0) + " prompts · " + Number(p.todaySessions || 0) + " sessions"
|
|
571
|
+
return text
|
|
572
|
+
}
|
|
573
|
+
|
|
574
|
+
// ------------------------------------------------------------------ balance
|
|
575
|
+
|
|
576
|
+
function currencyPrefix(currency) {
|
|
577
|
+
var code = String(currency || "USD").toUpperCase()
|
|
578
|
+
if (code === "USD") return "$"
|
|
579
|
+
if (code === "EUR") return "€"
|
|
580
|
+
if (code === "GBP") return "£"
|
|
581
|
+
return code + " "
|
|
582
|
+
}
|
|
583
|
+
|
|
584
|
+
var MAX_MONEY_AMOUNT = 999999.99
|
|
585
|
+
|
|
586
|
+
function formatMoney(value, currency) {
|
|
587
|
+
var amount = Number(value)
|
|
588
|
+
// A non-finite amount is unknown, not zero: "$0.00" would read as an empty balance.
|
|
589
|
+
if (!isFinite(amount)) return "—"
|
|
590
|
+
amount = Math.max(-MAX_MONEY_AMOUNT, Math.min(amount, MAX_MONEY_AMOUNT))
|
|
591
|
+
return currencyPrefix(currency) + amount.toFixed(2)
|
|
592
|
+
}
|
|
593
|
+
|
|
594
|
+
function balanceDetailText(b) {
|
|
595
|
+
if (!b || !(b.funded > 0)) return ""
|
|
596
|
+
var text = formatMoney(b.spent, b.currency) + " spent of " + formatMoney(b.funded, b.currency) + " funded"
|
|
597
|
+
if (b.estimated) text += " · estimated"
|
|
598
|
+
return text
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
function balanceAlarming(b) {
|
|
602
|
+
return !!b && b.funded > 0 && b.remaining / b.funded <= 0.1
|
|
603
|
+
}
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" width="256" height="257" preserveAspectRatio="xMidYMid" viewBox="0 0 256 257"><path fill="#D97757" d="m50.228 170.321 50.357-28.257.843-2.463-.843-1.361h-2.462l-8.426-.518-28.775-.778-24.952-1.037-24.175-1.296-6.092-1.297L0 125.796l.583-3.759 5.12-3.434 7.324.648 16.202 1.101 24.304 1.685 17.629 1.037 26.118 2.722h4.148l.583-1.685-1.426-1.037-1.101-1.037-25.147-17.045-27.22-18.017-14.258-10.37-7.713-5.25-3.888-4.925-1.685-10.758 7-7.713 9.397.649 2.398.648 9.527 7.323 20.35 15.75L94.817 91.9l3.889 3.24 1.555-1.102.195-.777-1.75-2.917-14.453-26.118-15.425-26.572-6.87-11.018-1.814-6.61c-.648-2.723-1.102-4.991-1.102-7.778l7.972-10.823L71.42 0 82.05 1.426l4.472 3.888 6.61 15.101 10.694 23.786 16.591 32.34 4.861 9.592 2.592 8.879.973 2.722h1.685v-1.556l1.36-18.211 2.528-22.36 2.463-28.776.843-8.1 4.018-9.722 7.971-5.25 6.222 2.981 5.12 7.324-.713 4.73-3.046 19.768-5.962 30.98-3.889 20.739h2.268l2.593-2.593 10.499-13.934 17.628-22.036 7.778-8.749 9.073-9.657 5.833-4.601h11.018l8.1 12.055-3.628 12.443-11.342 14.388-9.398 12.184-13.48 18.147-8.426 14.518.778 1.166 2.01-.194 30.46-6.481 16.462-2.982 19.637-3.37 8.88 4.148.971 4.213-3.5 8.62-20.998 5.184-24.628 4.926-36.682 8.685-.454.324.519.648 16.526 1.555 7.065.389h17.304l32.21 2.398 8.426 5.574 5.055 6.805-.843 5.184-12.962 6.611-17.498-4.148-40.83-9.721-14-3.5h-1.944v1.167l11.666 11.406 21.387 19.314 26.767 24.887 1.36 6.157-3.434 4.86-3.63-.518-23.526-17.693-9.073-7.972-20.545-17.304h-1.36v1.814l4.73 6.935 25.017 37.59 1.296 11.536-1.814 3.76-6.481 2.268-7.13-1.297-14.647-20.544-15.1-23.138-12.185-20.739-1.49.843-7.194 77.448-3.37 3.953-7.778 2.981-6.48-4.925-3.436-7.972 3.435-15.749 4.148-20.544 3.37-16.333 3.046-20.285 1.815-6.74-.13-.454-1.49.194-15.295 20.999-23.267 31.433-18.406 19.702-4.407 1.75-7.648-3.954.713-7.064 4.277-6.286 25.47-32.405 15.36-20.092 9.917-11.6-.065-1.686h-.583L44.07 198.125l-12.055 1.555-5.185-4.86.648-7.972 2.463-2.593 20.35-13.999-.064.065Z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="#111" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Codex</title><path clip-rule="evenodd" d="M8.086.457a6.105 6.105 0 013.046-.415c1.333.153 2.521.72 3.564 1.7a.117.117 0 00.107.029c1.408-.346 2.762-.224 4.061.366l.063.03.154.076c1.357.703 2.33 1.77 2.918 3.198.278.679.418 1.388.421 2.126a5.655 5.655 0 01-.18 1.631.167.167 0 00.04.155 5.982 5.982 0 011.578 2.891c.385 1.901-.01 3.615-1.183 5.14l-.182.22a6.063 6.063 0 01-2.934 1.851.162.162 0 00-.108.102c-.255.736-.511 1.364-.987 1.992-1.199 1.582-2.962 2.462-4.948 2.451-1.583-.008-2.986-.587-4.21-1.736a.145.145 0 00-.14-.032c-.518.167-1.04.191-1.604.185a5.924 5.924 0 01-2.595-.622 6.058 6.058 0 01-2.146-1.781c-.203-.269-.404-.522-.551-.821a7.74 7.74 0 01-.495-1.283 6.11 6.11 0 01-.017-3.064.166.166 0 00.008-.074.115.115 0 00-.037-.064 5.958 5.958 0 01-1.38-2.202 5.196 5.196 0 01-.333-1.589 6.915 6.915 0 01.188-2.132c.45-1.484 1.309-2.648 2.577-3.493.282-.188.55-.334.802-.438.286-.12.573-.22.861-.304a.129.129 0 00.087-.087A6.016 6.016 0 015.635 2.31C6.315 1.464 7.132.846 8.086.457zm-.804 7.85a.848.848 0 00-1.473.842l1.694 2.965-1.688 2.848a.849.849 0 001.46.864l1.94-3.272a.849.849 0 00.007-.854l-1.94-3.393zm5.446 6.24a.849.849 0 000 1.695h4.848a.849.849 0 000-1.696h-4.848z"/></svg>
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
<svg fill="#fff" fill-rule="evenodd" height="1em" style="flex:none;line-height:1" viewBox="0 0 24 24" width="1em" xmlns="http://www.w3.org/2000/svg"><title>Codex</title><path clip-rule="evenodd" d="M8.086.457a6.105 6.105 0 013.046-.415c1.333.153 2.521.72 3.564 1.7a.117.117 0 00.107.029c1.408-.346 2.762-.224 4.061.366l.063.03.154.076c1.357.703 2.33 1.77 2.918 3.198.278.679.418 1.388.421 2.126a5.655 5.655 0 01-.18 1.631.167.167 0 00.04.155 5.982 5.982 0 011.578 2.891c.385 1.901-.01 3.615-1.183 5.14l-.182.22a6.063 6.063 0 01-2.934 1.851.162.162 0 00-.108.102c-.255.736-.511 1.364-.987 1.992-1.199 1.582-2.962 2.462-4.948 2.451-1.583-.008-2.986-.587-4.21-1.736a.145.145 0 00-.14-.032c-.518.167-1.04.191-1.604.185a5.924 5.924 0 01-2.595-.622 6.058 6.058 0 01-2.146-1.781c-.203-.269-.404-.522-.551-.821a7.74 7.74 0 01-.495-1.283 6.11 6.11 0 01-.017-3.064.166.166 0 00.008-.074.115.115 0 00-.037-.064 5.958 5.958 0 01-1.38-2.202 5.196 5.196 0 01-.333-1.589 6.915 6.915 0 01.188-2.132c.45-1.484 1.309-2.648 2.577-3.493.282-.188.55-.334.802-.438.286-.12.573-.22.861-.304a.129.129 0 00.087-.087A6.016 6.016 0 015.635 2.31C6.315 1.464 7.132.846 8.086.457zm-.804 7.85a.848.848 0 00-1.473.842l1.694 2.965-1.688 2.848a.849.849 0 001.46.864l1.94-3.272a.849.849 0 00.007-.854l-1.94-3.393zm5.446 6.24a.849.849 0 000 1.695h4.848a.849.849 0 000-1.696h-4.848z"/></svg>
|
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 64 64">
|
|
2
|
+
<g fill="none" stroke="#ff6b22" stroke-linecap="round" stroke-width="5">
|
|
3
|
+
<path d="M32 5v12M32 47v12M5 32h12M47 32h12"/>
|
|
4
|
+
<path d="m13 13 8.5 8.5M42.5 42.5 51 51M51 13l-8.5 8.5M21.5 42.5 13 51"/>
|
|
5
|
+
</g>
|
|
6
|
+
<circle cx="32" cy="32" r="4" fill="#ff6b22"/>
|
|
7
|
+
</svg>
|
|
@@ -0,0 +1,3 @@
|
|
|
1
|
+
<svg viewBox="0 0 98 96" width="24" height="24" xmlns="http://www.w3.org/2000/svg">
|
|
2
|
+
<path fill-rule="evenodd" clip-rule="evenodd" d="M48.854 0C21.839 0 0 22 0 49.217c0 21.756 13.993 40.172 33.405 46.69 2.427.49 3.316-1.059 3.316-2.362 0-1.141-.08-5.052-.08-9.127-13.59 2.934-16.42-5.867-16.42-5.867-2.184-5.704-5.42-7.17-5.42-7.17-4.448-3.015.324-3.015.324-3.015 4.934.326 7.523 5.052 7.523 5.052 4.367 7.496 11.404 5.378 14.235 4.074.404-3.178 1.699-5.378 3.074-6.6-10.839-1.141-22.243-5.378-22.243-24.283 0-5.378 1.94-9.778 5.014-13.2-.485-1.222-2.184-6.275.486-13.038 0 0 4.125-1.304 13.426 5.052a46.97 46.97 0 0 1 12.214-1.63c4.125 0 8.33.571 12.213 1.63 9.302-6.356 13.427-5.052 13.427-5.052 2.67 6.763.97 11.816.485 13.038 3.155 3.422 5.015 7.822 5.015 13.2 0 18.905-11.404 23.06-22.324 24.283 1.78 1.548 3.316 4.481 3.316 9.126 0 6.6-.08 11.897-.08 13.526 0 1.304.89 2.853 3.316 2.364 19.412-6.52 33.405-24.935 33.405-46.691C97.707 22 75.788 0 48.854 0z" fill="#2088FF"/>
|
|
3
|
+
</svg>
|