@cad0p/pi-usage 0.0.0-stage → 0.11.2
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/LICENSE +21 -0
- package/README.md +509 -2
- package/bin/piquota.js +199 -0
- package/extensions/quota-panel.ts +963 -0
- package/package.json +59 -4
- package/src/auth/claude-code-auth.js +490 -0
- package/src/auth/jwt.js +84 -0
- package/src/auth/pi-auth.js +424 -0
- package/src/browser/cookies.js +557 -0
- package/src/browser/history.js +173 -0
- package/src/cache.js +224 -0
- package/src/cli/args.js +154 -0
- package/src/cli/commands/auth.js +216 -0
- package/src/cli/commands/gentle-pi.js +44 -0
- package/src/cli/commands/omarchy.js +75 -0
- package/src/cli/explain.js +99 -0
- package/src/cli/output.js +34 -0
- package/src/engine.js +404 -0
- package/src/exec.js +31 -0
- package/src/gentle-pi/rail-patch.js +335 -0
- package/src/http.js +182 -0
- package/src/model.js +332 -0
- package/src/omarchy/assets/antigravity.svg +3 -0
- package/src/omarchy/assets/claude.svg +1 -0
- package/src/omarchy/assets/codex.svg +1 -0
- package/src/omarchy/assets/github-actions.svg +3 -0
- package/src/omarchy/assets/opencode-go.svg +3 -0
- package/src/omarchy/publish.js +152 -0
- package/src/omarchy/record.js +462 -0
- package/src/opencode/dashboard.js +337 -0
- package/src/opencode/session.js +485 -0
- package/src/providers/antigravity-oauth.js +173 -0
- package/src/providers/antigravity.js +225 -0
- package/src/providers/backoff.js +146 -0
- package/src/providers/claude.js +194 -0
- package/src/providers/codex.js +151 -0
- package/src/providers/error-kind.js +128 -0
- package/src/providers/github-actions.js +379 -0
- package/src/providers/opencode-go.js +274 -0
- package/src/refresh.js +292 -0
- package/src/render/panel.js +411 -0
- package/src/render/theme.js +102 -0
- package/src/sticky.js +319 -0
|
@@ -0,0 +1,462 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Map piQuota's normalized report onto Omarchy's agent usage records.
|
|
3
|
+
*
|
|
4
|
+
* Omarchy's "Agents" bar panel draws whatever `*.json` record it finds in its
|
|
5
|
+
* usage directory. This module is pure: it turns a `PiQuotaReport` into the
|
|
6
|
+
* minimal valid record per provider and never touches the disk, the network or
|
|
7
|
+
* credentials. Only plan names, window labels, percentages and reset times can
|
|
8
|
+
* reach a record; accounts and e-mail addresses are never copied, and every free
|
|
9
|
+
* text field goes through `redact()` first.
|
|
10
|
+
*
|
|
11
|
+
* Record contract (see Omarchy's plugins/agents/Main.qml):
|
|
12
|
+
* id, name, ready, updatedAt, schemaVersion, tierLabel, usageStatusText,
|
|
13
|
+
* authHelpText and `limits[{label, title, percent, resetsAt}]`. The local
|
|
14
|
+
* token statistics are optional and are deliberately left out; the panel
|
|
15
|
+
* treats their absence as "no local stats".
|
|
16
|
+
*
|
|
17
|
+
* `percent` is a FRACTION in 0..1 (the panel multiplies it by 100), so 40.3% used
|
|
18
|
+
* is written as 0.403.
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
import { redact } from "../http.js";
|
|
22
|
+
import { errorKind } from "../providers/error-kind.js";
|
|
23
|
+
|
|
24
|
+
export const RECORD_PREFIX = "pi-";
|
|
25
|
+
export const SCHEMA_VERSION = 1;
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Pi's own provider name per family, for the "sign in again" hint.
|
|
29
|
+
*/
|
|
30
|
+
const LOGIN_HINT = {
|
|
31
|
+
claude: "Run `claude` once, or `/login anthropic` in Pi, to restore usage.",
|
|
32
|
+
codex: "Run `/login openai-codex` in Pi to restore usage.",
|
|
33
|
+
antigravity: "Run `/login antigravity` in Pi to restore usage.",
|
|
34
|
+
"opencode-go": "Run `piquota auth opencode` to restore usage.",
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const HELP_MAX = 200;
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* The panel hides a provider that has no limits and no local stats. An error
|
|
41
|
+
* record has neither, so it carries one placeholder window with a negative
|
|
42
|
+
* percent: the panel's own filter (`percent >= 0`) never draws it, but its
|
|
43
|
+
* presence keeps the provider's tab, and the status card, on screen.
|
|
44
|
+
*/
|
|
45
|
+
const PLACEHOLDER_LIMIT = Object.freeze({ label: "Unavailable", title: "Unavailable", percent: -1, resetsAt: "" });
|
|
46
|
+
|
|
47
|
+
/**
|
|
48
|
+
* @param {string} family
|
|
49
|
+
* @returns {string}
|
|
50
|
+
*/
|
|
51
|
+
export function recordId(family) {
|
|
52
|
+
return `${RECORD_PREFIX}${family}`;
|
|
53
|
+
}
|
|
54
|
+
|
|
55
|
+
/**
|
|
56
|
+
* @param {string | null | undefined} id
|
|
57
|
+
* @returns {boolean}
|
|
58
|
+
*/
|
|
59
|
+
export function isPiRecordId(id) {
|
|
60
|
+
return typeof id === "string" && /^pi-[a-z0-9][a-z0-9-]*$/.test(id);
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
/**
|
|
64
|
+
* @param {import("../model.js").QuotaWindow} window
|
|
65
|
+
* @param {import("../model.js").QuotaResult} [provider]
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
function windowTitle(window, provider) {
|
|
69
|
+
if (provider?.family === "github-actions") {
|
|
70
|
+
const match = typeof window.note === "string"
|
|
71
|
+
? window.note.match(/(\d+(?:\.\d+)?)\s+of\s+(\d+(?:\.\d+)?)\s+min/i)
|
|
72
|
+
: null;
|
|
73
|
+
const minutesLabel = match ? `${match[1]} / ${match[2]} min` : null;
|
|
74
|
+
|
|
75
|
+
let org = "";
|
|
76
|
+
if (window.label && window.label !== "Monthly window" && window.label !== "monthly") {
|
|
77
|
+
org = window.label.replace(/\s+(?:monthly.*|\(monthly.*\))$/i, "").trim();
|
|
78
|
+
}
|
|
79
|
+
if (!org && provider.account) {
|
|
80
|
+
org = provider.account;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
if (minutesLabel) {
|
|
84
|
+
return org ? `${org} (${minutesLabel})` : `Monthly (${minutesLabel})`;
|
|
85
|
+
}
|
|
86
|
+
if (org && org !== "GitHub Actions") {
|
|
87
|
+
return org;
|
|
88
|
+
}
|
|
89
|
+
return "Monthly";
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
switch (window.id) {
|
|
93
|
+
case "5h":
|
|
94
|
+
return "Session";
|
|
95
|
+
case "weekly":
|
|
96
|
+
return "Weekly";
|
|
97
|
+
case "monthly":
|
|
98
|
+
return "Monthly";
|
|
99
|
+
default:
|
|
100
|
+
return window.label || window.id || "Limit";
|
|
101
|
+
}
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* @param {import("../model.js").QuotaWindow[]} windows
|
|
106
|
+
* @param {import("../model.js").QuotaResult} [provider]
|
|
107
|
+
* @param {string} [prefix] Account prefix for a multi-account family, e.g. "#2 · ".
|
|
108
|
+
* @returns {Array<{ label: string, title: string, percent: number, resetsAt: string }>}
|
|
109
|
+
*/
|
|
110
|
+
function mapLimits(windows, provider, prefix = "") {
|
|
111
|
+
const limits = [];
|
|
112
|
+
for (const window of windows ?? []) {
|
|
113
|
+
// A window without a percentage would otherwise read as "0% used".
|
|
114
|
+
if (typeof window.usedPercent !== "number" || !Number.isFinite(window.usedPercent)) continue;
|
|
115
|
+
limits.push({
|
|
116
|
+
label: redact(`${prefix}${window.label || window.id || "Limit"}`),
|
|
117
|
+
title: redact(`${prefix}${windowTitle(window, provider)}`),
|
|
118
|
+
percent: Math.min(1, Math.max(0, window.usedPercent / 100)),
|
|
119
|
+
resetsAt: typeof window.resetsAt === "string" ? window.resetsAt : "",
|
|
120
|
+
});
|
|
121
|
+
}
|
|
122
|
+
return limits;
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} family
|
|
127
|
+
* @param {string | null | undefined} error
|
|
128
|
+
* @returns {{ usageStatusText: string, authHelpText: string }}
|
|
129
|
+
*/
|
|
130
|
+
export function describeError(family, error) {
|
|
131
|
+
const kind = errorKind(error);
|
|
132
|
+
const login = LOGIN_HINT[family] ?? "Sign in again to restore usage.";
|
|
133
|
+
switch (kind) {
|
|
134
|
+
case "expired":
|
|
135
|
+
return { usageStatusText: "Sign-in expired", authHelpText: login };
|
|
136
|
+
case "auth":
|
|
137
|
+
return { usageStatusText: "Credential rejected", authHelpText: login };
|
|
138
|
+
case "throttle":
|
|
139
|
+
return { usageStatusText: "Rate limited", authHelpText: "The provider throttled the usage endpoint; piQuota retries on its next run." };
|
|
140
|
+
case "transient":
|
|
141
|
+
return { usageStatusText: "Temporarily unavailable", authHelpText: "The provider did not answer; piQuota retries on its next run." };
|
|
142
|
+
case "missing":
|
|
143
|
+
return { usageStatusText: "Not configured", authHelpText: login };
|
|
144
|
+
default:
|
|
145
|
+
return { usageStatusText: "Quota unavailable", authHelpText: redact(error ?? "no data").slice(0, HELP_MAX) };
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
const SHORT_NAMES = {
|
|
150
|
+
"github-actions": "GH Actions",
|
|
151
|
+
antigravity: "Agy",
|
|
152
|
+
"opencode-go": "OP-Go",
|
|
153
|
+
};
|
|
154
|
+
|
|
155
|
+
/**
|
|
156
|
+
* @param {import("../model.js").QuotaResult} provider
|
|
157
|
+
* @returns {string}
|
|
158
|
+
*/
|
|
159
|
+
function recordName(provider) {
|
|
160
|
+
if (SHORT_NAMES[provider.family]) {
|
|
161
|
+
return SHORT_NAMES[provider.family];
|
|
162
|
+
}
|
|
163
|
+
return redact(String(provider.label || provider.family).replace(/\s*\(Pi\)\s*$/i, ""));
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/**
|
|
167
|
+
* @param {import("../model.js").QuotaResult} provider
|
|
168
|
+
* @returns {string}
|
|
169
|
+
*/
|
|
170
|
+
function recordTierLabel(provider) {
|
|
171
|
+
if (provider.family === "github-actions") {
|
|
172
|
+
const account = provider.account ? redact(provider.account) : "";
|
|
173
|
+
const plan = provider.plan ? redact(provider.plan) : "";
|
|
174
|
+
if (account && plan) return `${account} · ${plan}`;
|
|
175
|
+
return account || plan || "";
|
|
176
|
+
}
|
|
177
|
+
return provider.plan ? redact(provider.plan) : "";
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Slot label of one account: "#1" for the base entry (or the first result when
|
|
182
|
+
* the provider does not carry slots at all), "#<slot>" for a sibling.
|
|
183
|
+
*
|
|
184
|
+
* @param {import("../model.js").QuotaResult} provider
|
|
185
|
+
* @param {number} index
|
|
186
|
+
* @returns {string}
|
|
187
|
+
*/
|
|
188
|
+
function accountLabel(provider, index) {
|
|
189
|
+
if (typeof provider.slot === "string" && provider.slot !== "") return `#${provider.slot}`;
|
|
190
|
+
if (typeof provider.account === "string" && /^#\d/.test(provider.account)) return provider.account;
|
|
191
|
+
return `#${index + 1}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* The window an account is actually constrained by: the most consumed one, and
|
|
196
|
+
* on a tie the one that resets first. Unknown reset times sort last.
|
|
197
|
+
*
|
|
198
|
+
* @param {import("../model.js").QuotaWindow[]} windows
|
|
199
|
+
* @returns {import("../model.js").QuotaWindow | null}
|
|
200
|
+
*/
|
|
201
|
+
function bindingWindow(windows) {
|
|
202
|
+
/** @type {import("../model.js").QuotaWindow | null} */
|
|
203
|
+
let best = null;
|
|
204
|
+
for (const window of windows ?? []) {
|
|
205
|
+
if (typeof window.usedPercent !== "number" || !Number.isFinite(window.usedPercent)) continue;
|
|
206
|
+
if (!best) {
|
|
207
|
+
best = window;
|
|
208
|
+
continue;
|
|
209
|
+
}
|
|
210
|
+
const used = window.usedPercent;
|
|
211
|
+
const bestUsed = /** @type {number} */ (best.usedPercent);
|
|
212
|
+
if (used > bestUsed) {
|
|
213
|
+
best = window;
|
|
214
|
+
continue;
|
|
215
|
+
}
|
|
216
|
+
if (used === bestUsed && resetOrder(window.resetsAt) < resetOrder(best.resetsAt)) best = window;
|
|
217
|
+
}
|
|
218
|
+
return best;
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* @param {string | null | undefined} resetsAt
|
|
223
|
+
* @returns {number} Epoch ms, or Infinity when unknown, so unknown resets sort last.
|
|
224
|
+
*/
|
|
225
|
+
function resetOrder(resetsAt) {
|
|
226
|
+
if (typeof resetsAt !== "string" || resetsAt === "") return Number.POSITIVE_INFINITY;
|
|
227
|
+
const parsed = Date.parse(resetsAt);
|
|
228
|
+
return Number.isNaN(parsed) ? Number.POSITIVE_INFINITY : parsed;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
/**
|
|
232
|
+
* A short, user-facing reason for one failed account.
|
|
233
|
+
*
|
|
234
|
+
* @param {string | null | undefined} error
|
|
235
|
+
* @returns {string}
|
|
236
|
+
*/
|
|
237
|
+
function failureReason(error) {
|
|
238
|
+
const text = typeof error === "string" ? error : "";
|
|
239
|
+
if (/key rejected|key was rejected/i.test(text)) return "key rejected";
|
|
240
|
+
if (/Go plan not active|Workspace access denied/i.test(text)) return "no Go plan";
|
|
241
|
+
switch (errorKind(error)) {
|
|
242
|
+
case "expired":
|
|
243
|
+
return "sign-in expired";
|
|
244
|
+
case "auth":
|
|
245
|
+
return "credential rejected";
|
|
246
|
+
case "throttle":
|
|
247
|
+
return "rate limited";
|
|
248
|
+
case "transient":
|
|
249
|
+
return "temporarily unavailable";
|
|
250
|
+
case "missing":
|
|
251
|
+
return "not configured";
|
|
252
|
+
default:
|
|
253
|
+
return "unavailable";
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
|
|
257
|
+
/**
|
|
258
|
+
* Collapse consecutive numeric account labels into ranges: #8,#9,#10 -> "#8–#10".
|
|
259
|
+
*
|
|
260
|
+
* @param {string[]} labels
|
|
261
|
+
* @returns {string}
|
|
262
|
+
*/
|
|
263
|
+
function compactLabels(labels) {
|
|
264
|
+
/** @type {string[]} */
|
|
265
|
+
const parts = [];
|
|
266
|
+
/** @type {number | null} */
|
|
267
|
+
let runStart = null;
|
|
268
|
+
/** @type {number | null} */
|
|
269
|
+
let runEnd = null;
|
|
270
|
+
const flush = () => {
|
|
271
|
+
if (runStart === null || runEnd === null) return;
|
|
272
|
+
parts.push(runStart === runEnd ? `#${runStart}` : `#${runStart}–#${runEnd}`);
|
|
273
|
+
runStart = null;
|
|
274
|
+
runEnd = null;
|
|
275
|
+
};
|
|
276
|
+
for (const label of labels) {
|
|
277
|
+
const match = /^#(\d+)$/.exec(label);
|
|
278
|
+
if (!match) {
|
|
279
|
+
flush();
|
|
280
|
+
parts.push(label);
|
|
281
|
+
continue;
|
|
282
|
+
}
|
|
283
|
+
const number = Number(match[1]);
|
|
284
|
+
if (runStart === null || runEnd === null) {
|
|
285
|
+
runStart = number;
|
|
286
|
+
runEnd = number;
|
|
287
|
+
} else if (number === runEnd + 1) {
|
|
288
|
+
runEnd = number;
|
|
289
|
+
} else {
|
|
290
|
+
flush();
|
|
291
|
+
runStart = number;
|
|
292
|
+
runEnd = number;
|
|
293
|
+
}
|
|
294
|
+
}
|
|
295
|
+
flush();
|
|
296
|
+
return parts.join(", ");
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
/**
|
|
300
|
+
* Explain failures compactly, grouped by reason in slot order, e.g.
|
|
301
|
+
* "#8–#10 key rejected · #11–#14 no Go plan".
|
|
302
|
+
*
|
|
303
|
+
* @param {Array<{ label: string, reason: string }>} failures
|
|
304
|
+
* @returns {string}
|
|
305
|
+
*/
|
|
306
|
+
function compactFailureList(failures) {
|
|
307
|
+
/** @type {string[]} */
|
|
308
|
+
const groups = [];
|
|
309
|
+
let index = 0;
|
|
310
|
+
while (index < failures.length) {
|
|
311
|
+
const reason = failures[index].reason;
|
|
312
|
+
/** @type {string[]} */
|
|
313
|
+
const labels = [];
|
|
314
|
+
while (index < failures.length && failures[index].reason === reason) {
|
|
315
|
+
labels.push(failures[index].label);
|
|
316
|
+
index += 1;
|
|
317
|
+
}
|
|
318
|
+
groups.push(`${compactLabels(labels)} ${reason}`);
|
|
319
|
+
}
|
|
320
|
+
const text = redact(groups.join(" · "));
|
|
321
|
+
return text.length <= HELP_MAX ? text : `${text.slice(0, HELP_MAX - 1).trimEnd()}…`;
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
/**
|
|
325
|
+
* One record for a family with several accounts.
|
|
326
|
+
*
|
|
327
|
+
* Each healthy account contributes only the window that constrains it (the most
|
|
328
|
+
* consumed one), so a fourteen-account family stays a readable tab instead of a
|
|
329
|
+
* wall of forty rows. Failed accounts contribute no rows at all: they are
|
|
330
|
+
* summarised in the status and the compact help text, which is what makes a
|
|
331
|
+
* revoked key distinguishable from a workspace without a plan.
|
|
332
|
+
*
|
|
333
|
+
* @param {string} family
|
|
334
|
+
* @param {import("../model.js").QuotaResult[]} providers
|
|
335
|
+
* @param {{ now?: number }} [options]
|
|
336
|
+
* @returns {{ id: string, record: Record<string, unknown> } | null}
|
|
337
|
+
*/
|
|
338
|
+
function buildFamilyRecord(family, providers, options = {}) {
|
|
339
|
+
const now = options.now ?? Date.now();
|
|
340
|
+
const first = providers[0];
|
|
341
|
+
if (!first) return null;
|
|
342
|
+
if (providers.every((provider) => !provider.ok && errorKind(provider.error) === "missing")) return null;
|
|
343
|
+
|
|
344
|
+
/** @type {Array<{ label: string, title: string, percent: number, resetsAt: string }>} */
|
|
345
|
+
const limits = [];
|
|
346
|
+
/** @type {Array<{ label: string, reason: string }>} */
|
|
347
|
+
const failures = [];
|
|
348
|
+
let okCount = 0;
|
|
349
|
+
|
|
350
|
+
providers.forEach((provider, index) => {
|
|
351
|
+
const label = accountLabel(provider, index);
|
|
352
|
+
if (provider.ok) {
|
|
353
|
+
okCount += 1;
|
|
354
|
+
const window = bindingWindow(provider.windows);
|
|
355
|
+
const mapped = window ? mapLimits([window], provider, `${label} · `)[0] : null;
|
|
356
|
+
if (mapped) limits.push(mapped);
|
|
357
|
+
return;
|
|
358
|
+
}
|
|
359
|
+
failures.push({ label, reason: failureReason(provider.error) });
|
|
360
|
+
});
|
|
361
|
+
|
|
362
|
+
const failedCount = providers.length - okCount;
|
|
363
|
+
const allFailed = okCount === 0;
|
|
364
|
+
const status = allFailed
|
|
365
|
+
? describeError(family, first.error ?? "no usage windows")
|
|
366
|
+
: {
|
|
367
|
+
usageStatusText: failedCount > 0 ? `${failedCount} of ${providers.length} accounts unavailable` : "",
|
|
368
|
+
authHelpText: "",
|
|
369
|
+
};
|
|
370
|
+
|
|
371
|
+
const id = recordId(family);
|
|
372
|
+
return {
|
|
373
|
+
id,
|
|
374
|
+
record: {
|
|
375
|
+
schemaVersion: SCHEMA_VERSION,
|
|
376
|
+
id,
|
|
377
|
+
name: recordName(first),
|
|
378
|
+
ready: okCount > 0,
|
|
379
|
+
tierLabel: `${okCount} of ${providers.length} accounts`,
|
|
380
|
+
limits: limits.length > 0 ? limits : [{ ...PLACEHOLDER_LIMIT }],
|
|
381
|
+
usageStatusText: status.usageStatusText,
|
|
382
|
+
authHelpText: failures.length > 0 ? compactFailureList(failures) : status.authHelpText,
|
|
383
|
+
hasLocalStats: false,
|
|
384
|
+
hasPromptStats: false,
|
|
385
|
+
updatedAt: new Date(now).toISOString(),
|
|
386
|
+
},
|
|
387
|
+
};
|
|
388
|
+
}
|
|
389
|
+
|
|
390
|
+
/**
|
|
391
|
+
* Build the record for one provider, or `null` when the provider is simply not
|
|
392
|
+
* configured: a tab that only says "not configured" is noise, so it is omitted
|
|
393
|
+
* (and any record from an earlier run is removed by the publisher).
|
|
394
|
+
*
|
|
395
|
+
* @param {import("../model.js").QuotaResult} provider
|
|
396
|
+
* @param {{ now?: number }} [options]
|
|
397
|
+
* @returns {{ id: string, record: Record<string, unknown> } | null}
|
|
398
|
+
*/
|
|
399
|
+
export function buildRecord(provider, options = {}) {
|
|
400
|
+
const now = options.now ?? Date.now();
|
|
401
|
+
const limits = mapLimits(provider.windows, provider);
|
|
402
|
+
const failed = !provider.ok || limits.length === 0;
|
|
403
|
+
|
|
404
|
+
if (failed && errorKind(provider.error) === "missing") return null;
|
|
405
|
+
|
|
406
|
+
const status = failed
|
|
407
|
+
? describeError(provider.family, provider.error ?? "no usage windows")
|
|
408
|
+
: { usageStatusText: "", authHelpText: "" };
|
|
409
|
+
|
|
410
|
+
const id = recordId(provider.family);
|
|
411
|
+
return {
|
|
412
|
+
id,
|
|
413
|
+
record: {
|
|
414
|
+
schemaVersion: SCHEMA_VERSION,
|
|
415
|
+
id,
|
|
416
|
+
name: recordName(provider),
|
|
417
|
+
ready: !failed,
|
|
418
|
+
tierLabel: recordTierLabel(provider),
|
|
419
|
+
limits: failed && limits.length === 0 ? [{ ...PLACEHOLDER_LIMIT }] : limits,
|
|
420
|
+
usageStatusText: status.usageStatusText,
|
|
421
|
+
authHelpText: status.authHelpText,
|
|
422
|
+
hasLocalStats: false,
|
|
423
|
+
hasPromptStats: false,
|
|
424
|
+
updatedAt: new Date(now).toISOString(),
|
|
425
|
+
},
|
|
426
|
+
};
|
|
427
|
+
}
|
|
428
|
+
|
|
429
|
+
/**
|
|
430
|
+
* One record per family. A family with a single result produces exactly the same
|
|
431
|
+
* record as before; several accounts collapse into one record with one binding
|
|
432
|
+
* window per healthy account.
|
|
433
|
+
*
|
|
434
|
+
* Never throws: a malformed provider entry is skipped.
|
|
435
|
+
*
|
|
436
|
+
* @param {import("../engine.js").PiQuotaReport} report
|
|
437
|
+
* @param {{ now?: number }} [options]
|
|
438
|
+
* @returns {Array<{ id: string, record: Record<string, unknown> }>}
|
|
439
|
+
*/
|
|
440
|
+
export function buildRecords(report, options = {}) {
|
|
441
|
+
/** @type {Map<string, import("../model.js").QuotaResult[]>} */
|
|
442
|
+
const groups = new Map();
|
|
443
|
+
for (const provider of report?.providers ?? []) {
|
|
444
|
+
if (!provider || typeof provider.family !== "string") continue;
|
|
445
|
+
const list = groups.get(provider.family) ?? [];
|
|
446
|
+
list.push(provider);
|
|
447
|
+
groups.set(provider.family, list);
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
const records = [];
|
|
451
|
+
for (const [family, providers] of groups) {
|
|
452
|
+
try {
|
|
453
|
+
const built = providers.length > 1
|
|
454
|
+
? buildFamilyRecord(family, providers, options)
|
|
455
|
+
: buildRecord(providers[0], options);
|
|
456
|
+
if (built) records.push(built);
|
|
457
|
+
} catch {
|
|
458
|
+
// A broken entry must not take the other providers' records down with it.
|
|
459
|
+
}
|
|
460
|
+
}
|
|
461
|
+
return records;
|
|
462
|
+
}
|