dsh-cliproxyapi-pro 1.6.0 → 1.7.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/lib/quota.js ADDED
@@ -0,0 +1,1646 @@
1
+ // Host-side quota engine for dsh-cliproxyapi-pro.
2
+ //
3
+ // Reads a CLIProxyAPI (CPA) server's credentials through its management API
4
+ // (`/v0/management/auth-files`, `/auth-files/models`, `/api-call` with
5
+ // `$TOKEN$` substitution) and queries the upstream quota endpoints that the
6
+ // providers expose (Codex, Claude, Antigravity, Gemini CLI, Kimi, xAI, Devin,
7
+ // Meta), plus CPA's own normalized quota route for plugin providers. It
8
+ // produces a JSON-serializable snapshot without keys, tokens or raw upstream
9
+ // bodies, and maps DSH model ids to the credentials that serve them.
10
+ //
11
+ // Upstream protocol knowledge follows the MIT-licensed "CliProxyAPI quota"
12
+ // browser plugin, re-implemented here for the host.
13
+
14
+ export const QUOTA_TTL_MS = 5 * 60 * 1000;
15
+ export const DEFAULT_QUOTA_TIMEOUT_MS = 15_000;
16
+ export const DEFAULT_QUOTA_CONCURRENCY = 4;
17
+ export const MAX_QUOTA_RESPONSE_BYTES = 4 * 1024 * 1024;
18
+
19
+ const MANAGEMENT_KEY_REF = "CLI_PROXY_MANAGEMENT_KEY";
20
+ const STATUS_MESSAGE_MAX = 160;
21
+
22
+ const UPSTREAM = {
23
+ codexUsage: "https://chatgpt.com/backend-api/wham/usage",
24
+ claudeProfile: "https://api.anthropic.com/api/oauth/profile",
25
+ claudeUsage: "https://api.anthropic.com/api/oauth/usage",
26
+ googleLoad: "https://daily-cloudcode-pa.googleapis.com/v1internal:loadCodeAssist",
27
+ googleQuota: "https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuota",
28
+ // cloudcode-pa still serves the Gemini group; the daily/sandbox hosts may
29
+ // return only "Claude and GPT models". Summary hosts first, then the
30
+ // per-model fallbacks.
31
+ antigravityQuota: [
32
+ "https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary",
33
+ "https://daily-cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary",
34
+ "https://daily-cloudcode-pa.sandbox.googleapis.com/v1internal:retrieveUserQuotaSummary",
35
+ ],
36
+ antigravityModels: [
37
+ "https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels",
38
+ "https://daily-cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels",
39
+ ],
40
+ kimiUsage: "https://api.kimi.com/coding/v1/usages",
41
+ // Kimi International (kimi.ai) accounts use the same API on another host.
42
+ kimiAiUsage: "https://api.kimi.ai/coding/v1/usages",
43
+ // Meta Muse Code subscription usage (needs the credential's dca_token).
44
+ metaUsage: "https://api.meta.ai/muse-code/key",
45
+ xaiBillingWeekly: "https://cli-chat-proxy.grok.com/v1/billing?format=credits",
46
+ xaiBillingMonthly: "https://cli-chat-proxy.grok.com/v1/billing",
47
+ // Connect RPC (JSON); the same call CPA's management center makes for Devin.
48
+ devinStatus: "https://server.codeium.com/exa.seat_management_pb.SeatManagementService/GetUserStatus",
49
+ };
50
+
51
+ // Client identity sent by CPA's management center; `$TOKEN$` is substituted
52
+ // by CPA's api-call with the credential's own session token.
53
+ const DEVIN_METADATA = {
54
+ ideName: "chisel",
55
+ ideVersion: "3000.10.21",
56
+ apiKey: "$TOKEN$",
57
+ locale: "en",
58
+ os: "darwin",
59
+ extensionVersion: "3000.10.21",
60
+ clientName: "chisel",
61
+ };
62
+
63
+ const AG_META = { ideType: "ANTIGRAVITY", platform: "PLATFORM_UNSPECIFIED", pluginType: "GEMINI" };
64
+ const GEMINI_META = { ideType: "IDE_UNSPECIFIED", platform: "PLATFORM_UNSPECIFIED", pluginType: "GEMINI" };
65
+ const AG_SUMMARY_UA = "antigravity/cli/1.0.13 (aidev_client; os_type=darwin; arch=arm64)";
66
+ const AG_MODELS_UA = "antigravity/1.11.5 windows/amd64";
67
+ const CODEX_UA = "codex_cli_rs/0.76.0 (Debian 13.0.0; x86_64) WindowsTerminal";
68
+
69
+ const GROUP_CLAUDE_GPT = "Claude and GPT models";
70
+ const GROUP_GEMINI = "Gemini models";
71
+ const GROUP_OTHER = "Other models";
72
+
73
+ const W5H = 5 * 60 * 60;
74
+ const W1D = 24 * 60 * 60;
75
+ const W7D = 7 * W1D;
76
+ const KIND_RANK = { "5h": 0, "7d": 1, "30d": 2, "1d": 3, other: 4 };
77
+
78
+ const PLAN_LABELS = {
79
+ free: "Free",
80
+ "free-tier": "Free",
81
+ "plan-free": "Free",
82
+ basic: "Basic",
83
+ standard: "Standard",
84
+ team: "Team",
85
+ "plan-team": "Team",
86
+ pro: "Pro",
87
+ "plan-pro": "Pro",
88
+ "g1-pro-tier": "Pro",
89
+ max: "Max",
90
+ "plan-max": "Max",
91
+ plus: "Plus",
92
+ ultra: "Ultra",
93
+ "g1-ultra-tier": "Ultra",
94
+ "ultra-lite": "Ultra Lite",
95
+ "g1-ultra-lite-tier": "Ultra Lite",
96
+ paid: "Paid",
97
+ enterprise: "Enterprise",
98
+ };
99
+
100
+ //#region Helpers
101
+
102
+ class QuotaError extends Error {
103
+ constructor(code, status) {
104
+ super(`quota: ${code}${status === undefined ? "" : ` (${status})`}`);
105
+ this.name = "QuotaError";
106
+ this.code = code;
107
+ if (status !== undefined) this.status = status;
108
+ }
109
+ }
110
+
111
+ const isObj = (v) => v !== null && typeof v === "object" && !Array.isArray(v);
112
+ const text = (v) => (typeof v === "string" ? v.trim() : "");
113
+ const firstText = (...values) => {
114
+ for (const v of values) {
115
+ const t = typeof v === "number" && Number.isFinite(v) ? String(v) : text(v);
116
+ if (t !== "") return t;
117
+ }
118
+ return "";
119
+ };
120
+ const numberOrNull = (v) => {
121
+ if (typeof v === "number" && Number.isFinite(v)) return v;
122
+ if (typeof v === "string" && v.trim() !== "" && Number.isFinite(Number(v))) return Number(v);
123
+ return null;
124
+ };
125
+ const flagOf = (v) => {
126
+ if (v === true || v === 1) return true;
127
+ if (v === false || v === 0) return false;
128
+ if (typeof v === "string") {
129
+ const s = v.trim().toLowerCase();
130
+ if (["true", "1", "yes", "on"].includes(s)) return true;
131
+ if (["false", "0", "no", "off"].includes(s)) return false;
132
+ }
133
+ return undefined;
134
+ };
135
+ const clampPct = (v) => Math.max(0, Math.min(100, v));
136
+ const roundPct = (v) => (v === null ? null : Math.round(clampPct(v) * 100) / 100);
137
+ const getPath = (obj, path) => {
138
+ let cur = obj;
139
+ for (const key of path) {
140
+ if (cur === null || typeof cur !== "object") return undefined;
141
+ cur = cur[key];
142
+ }
143
+ return cur;
144
+ };
145
+
146
+ function fractionPercent(value) {
147
+ const raw = text(value);
148
+ if (raw.endsWith("%")) {
149
+ const n = Number(raw.slice(0, -1));
150
+ return Number.isFinite(n) ? clampPct(n) : null;
151
+ }
152
+ const n = numberOrNull(value);
153
+ if (n === null) return null;
154
+ return n > 1 && n <= 100 ? clampPct(n) : clampPct(n * 100);
155
+ }
156
+
157
+ function kindFromText(...values) {
158
+ const s = values.map((v) => text(v).toLowerCase()).filter(Boolean).join(" ");
159
+ if (s === "") return "";
160
+ if (/(?:^|[^0-9])5\s*[-_ ]?(?:h|hr|hour|hours)\b|five\s*[-_ ]?hour/.test(s)) return "5h";
161
+ if (/(?:^|[^0-9])7\s*[-_ ]?(?:d|day|days)\b|weekly|week/.test(s)) return "7d";
162
+ if (/(?:28|29|30|31)\s*[-_ ]?(?:d|day|days)\b|monthly|month/.test(s)) return "30d";
163
+ if (/daily|(?:^|[^0-9])1\s*[-_ ]?(?:d|day)\b|\bday\b/.test(s)) return "1d";
164
+ return "";
165
+ }
166
+
167
+ function kindFromSeconds(value) {
168
+ const seconds = numberOrNull(value);
169
+ if (seconds === null || seconds <= 0) return "";
170
+ if (seconds === W5H) return "5h";
171
+ if (seconds === W7D) return "7d";
172
+ if (seconds >= 28 * W1D && seconds <= 31 * W1D) return "30d";
173
+ if (seconds === W1D) return "1d";
174
+ return "";
175
+ }
176
+
177
+ function durationSeconds(value, unit) {
178
+ const n = numberOrNull(value);
179
+ if (n === null || n <= 0) return null;
180
+ const u = text(unit).toLowerCase().replace(/^time_unit_/, "");
181
+ if (u.startsWith("second")) return n;
182
+ if (u.startsWith("hour")) return n * 3600;
183
+ if (u.startsWith("day")) return n * W1D;
184
+ if (u.startsWith("week")) return n * W7D;
185
+ return n * 60;
186
+ }
187
+
188
+ /** Infers a window kind from the reset distance (Google per-model buckets carry no window metadata). */
189
+ function kindFromReset(resetAt, nowMs) {
190
+ if (resetAt === null) return "other";
191
+ const delta = Date.parse(resetAt) - nowMs;
192
+ if (!Number.isFinite(delta)) return "other";
193
+ return delta <= 8 * 3600 * 1000 ? "5h" : delta <= 8 * W1D * 1000 ? "7d" : "other";
194
+ }
195
+
196
+ function isoFromUnix(value) {
197
+ const n = numberOrNull(value);
198
+ if (n === null || n <= 0) return null;
199
+ const ms = n > 1e12 ? n : n * 1000;
200
+ const d = new Date(ms);
201
+ return Number.isNaN(d.getTime()) ? null : d.toISOString();
202
+ }
203
+
204
+ function isoFromString(value) {
205
+ const raw = text(value);
206
+ if (raw === "") return null;
207
+ if (/^\d+(?:\.\d+)?$/.test(raw)) return isoFromUnix(raw);
208
+ const ms = Date.parse(raw);
209
+ return Number.isNaN(ms) ? null : new Date(ms).toISOString();
210
+ }
211
+
212
+ function isoFromRelative(seconds, nowMs) {
213
+ const n = numberOrNull(seconds);
214
+ if (n === null || n < 0) return null;
215
+ return new Date(nowMs + n * 1000).toISOString();
216
+ }
217
+
218
+ function makeWindow({ id, kind, label, group, remainingPercent, resetAt, modelIds, modelPrefixes }) {
219
+ const row = {
220
+ id,
221
+ kind: KIND_RANK[kind] === undefined ? "other" : kind,
222
+ label: text(label),
223
+ remainingPercent: roundPct(remainingPercent ?? null),
224
+ resetAt: resetAt ?? null,
225
+ };
226
+ if (text(group) !== "") row.group = text(group);
227
+ if (Array.isArray(modelIds) && modelIds.length > 0) row.modelIds = [...new Set(modelIds)];
228
+ if (Array.isArray(modelPrefixes) && modelPrefixes.length > 0) row.modelPrefixes = [...modelPrefixes];
229
+ return row;
230
+ }
231
+
232
+ function sortWindows(rows) {
233
+ return rows
234
+ .map((row, index) => ({ row, index }))
235
+ .sort((a, b) => KIND_RANK[a.row.kind] - KIND_RANK[b.row.kind] || a.index - b.index)
236
+ .map(({ row }) => row);
237
+ }
238
+
239
+ /** Keeps the most conservative row for duplicate keys. */
240
+ function dedupeWindows(rows, keyOf) {
241
+ const map = new Map();
242
+ for (const row of rows) {
243
+ const key = keyOf(row);
244
+ const prev = map.get(key);
245
+ if (prev === undefined) {
246
+ map.set(key, row);
247
+ } else if (row.remainingPercent !== null && (prev.remainingPercent === null || row.remainingPercent < prev.remainingPercent)) {
248
+ map.set(key, { ...prev, remainingPercent: row.remainingPercent, resetAt: prev.resetAt ?? row.resetAt });
249
+ }
250
+ }
251
+ return [...map.values()];
252
+ }
253
+
254
+ function normalizeProvider(value) {
255
+ const p = text(value).toLowerCase();
256
+ if (p === "anthropic" || p === "claude-code") return "claude";
257
+ if (p === "grok" || p === "x-ai" || p === "x.ai") return "xai";
258
+ if (p === "zai" || p === "zhipu" || p === "chatglm") return "glm";
259
+ if (p === "moonshot" || p === "kimi-ai" || p === "kimi_ai") return "kimi";
260
+ return p;
261
+ }
262
+
263
+ function normalizePlanToken(value) {
264
+ const raw = text(value);
265
+ if (raw === "") return "";
266
+ const key = raw.toLowerCase().replace(/[\s_]+/g, "-");
267
+ if (PLAN_LABELS[key] !== undefined) return key;
268
+ if (/ultra-?lite/.test(key)) return "ultra-lite";
269
+ if (/ultra/.test(key)) return "ultra";
270
+ if (/(^|-)pro($|-)/.test(key)) return "pro";
271
+ if (/(^|-)free($|-)/.test(key)) return "free";
272
+ if (/plus/.test(key)) return "plus";
273
+ if (/(^|-)max($|-)/.test(key)) return "max";
274
+ if (/team/.test(key)) return "team";
275
+ if (/enterprise/.test(key)) return "enterprise";
276
+ return "";
277
+ }
278
+
279
+ const PLAN_FIELD = /plan|tier|subscription|membership/i;
280
+
281
+ function planValue(value, field) {
282
+ if (typeof value === "string") {
283
+ const raw = text(value);
284
+ if (raw === "") return "";
285
+ const token = normalizePlanToken(raw);
286
+ if (token !== "") return PLAN_LABELS[token];
287
+ return PLAN_FIELD.test(field) && raw.length <= 40 && !raw.includes("@") ? raw : "";
288
+ }
289
+ if (!isObj(value)) return "";
290
+ for (const key of ["plan", "plan_type", "planType", "tier_name", "tierName", "tier_id", "tierId"]) {
291
+ const resolved = planValue(value[key], key);
292
+ if (resolved !== "") return resolved;
293
+ }
294
+ if (PLAN_FIELD.test(field)) {
295
+ for (const key of ["name", "id"]) {
296
+ const resolved = planValue(value[key], `${field}.${key}`);
297
+ if (resolved !== "") return resolved;
298
+ }
299
+ }
300
+ return "";
301
+ }
302
+
303
+ const PLAN_PATHS = [
304
+ ["paidTier"], ["paid_tier"], ["subscription"], ["plan"], ["plan_type"], ["planType"],
305
+ ["chatgpt_plan_type"], ["https://api.openai.com/auth", "chatgpt_plan_type"],
306
+ ["tier"], ["tier_name"], ["tierName"], ["tier_id"], ["tierId"], ["currentTier"], ["current_tier"],
307
+ ["account", "subscription"], ["account", "plan"], ["account", "plan_type"], ["account", "tier"],
308
+ ["organization", "subscription"], ["organization", "plan"],
309
+ ["metadata", "subscription"], ["metadata", "plan"], ["metadata", "plan_type"], ["metadata", "tier"],
310
+ ["attributes", "subscription"], ["attributes", "plan"], ["attributes", "plan_type"], ["attributes", "tier"],
311
+ ["account_type"],
312
+ ];
313
+
314
+ /** Plan label from auth-file / upstream payloads; paid tiers are checked before current/default tiers. */
315
+ function extractPlan(...sources) {
316
+ for (const source of sources) {
317
+ if (!isObj(source)) continue;
318
+ for (const path of PLAN_PATHS) {
319
+ const resolved = planValue(getPath(source, path), path.join("."));
320
+ if (resolved !== "") return resolved;
321
+ }
322
+ }
323
+ return "";
324
+ }
325
+
326
+ function claimsOf(value) {
327
+ if (isObj(value)) return value;
328
+ const raw = text(value);
329
+ if (raw === "") return null;
330
+ try {
331
+ if (raw.startsWith("{")) return JSON.parse(raw);
332
+ const parts = raw.split(".");
333
+ if (parts.length < 2) return null;
334
+ const parsed = JSON.parse(Buffer.from(parts[1], "base64url").toString("utf8"));
335
+ return isObj(parsed) ? parsed : null;
336
+ } catch {
337
+ return null;
338
+ }
339
+ }
340
+
341
+ function entryClaims(entry) {
342
+ return [entry.id_token, getPath(entry, ["tokens", "id_token"]), getPath(entry, ["metadata", "id_token"]), getPath(entry, ["attributes", "id_token"])]
343
+ .map(claimsOf)
344
+ .filter((c) => c !== null);
345
+ }
346
+
347
+ function codexAccountId(entry) {
348
+ const direct = firstText(
349
+ entry.chatgpt_account_id,
350
+ entry.account_id,
351
+ getPath(entry, ["tokens", "account_id"]),
352
+ getPath(entry, ["tokens", "chatgpt_account_id"]),
353
+ getPath(entry, ["metadata", "account_id"]),
354
+ getPath(entry, ["attributes", "account_id"]),
355
+ );
356
+ if (direct !== "") return direct;
357
+ for (const claims of entryClaims(entry)) {
358
+ const id = firstText(claims.chatgpt_account_id, getPath(claims, ["https://api.openai.com/auth", "chatgpt_account_id"]));
359
+ if (id !== "") return id;
360
+ }
361
+ return "";
362
+ }
363
+
364
+ function projectIdOf(entry) {
365
+ return firstText(
366
+ entry.project_id,
367
+ entry.projectId,
368
+ getPath(entry, ["metadata", "project_id"]),
369
+ getPath(entry, ["metadata", "projectId"]),
370
+ getPath(entry, ["attributes", "project_id"]),
371
+ getPath(entry, ["attributes", "projectId"]),
372
+ getPath(entry, ["attributes", "gemini_virtual_project"]),
373
+ );
374
+ }
375
+
376
+ function shortMessage(value) {
377
+ const raw = typeof value === "string" ? value : "";
378
+ const cleaned = raw
379
+ .replace(/\s+/g, " ")
380
+ .replace(/\b(Bearer|Basic)\s+[^\s"',;]+/gi, "$1 [redacted]")
381
+ .replace(/\b(?:sk|rk|pk|xai|ya29)[-_.][A-Za-z0-9_\-.]{8,}/g, "[redacted]")
382
+ .trim();
383
+ return cleaned.length > STATUS_MESSAGE_MAX ? `${cleaned.slice(0, STATUS_MESSAGE_MAX - 1)}…` : cleaned;
384
+ }
385
+
386
+ function parseRecent(entry) {
387
+ let success = 0;
388
+ let failed = 0;
389
+ let seen = false;
390
+ const add = (item) => {
391
+ if (!isObj(item)) return;
392
+ const s = numberOrNull(item.success ?? item.success_count ?? item.successCount);
393
+ const f = numberOrNull(item.failed ?? item.failure ?? item.failed_count ?? item.failure_count ?? item.failedCount);
394
+ if (s === null && f === null) return;
395
+ seen = true;
396
+ success += Math.max(0, s ?? 0);
397
+ failed += Math.max(0, f ?? 0);
398
+ };
399
+ const raw = entry.recent_requests ?? entry.recentRequests;
400
+ if (Array.isArray(raw)) raw.forEach(add);
401
+ else add(raw);
402
+ if (!seen) add(entry);
403
+ return seen ? { success, failed } : undefined;
404
+ }
405
+
406
+ function credentialStatus(entry) {
407
+ if (flagOf(entry.disabled) === true) return "disabled";
408
+ const status = text(entry.status).toLowerCase();
409
+ if (status === "disabled") return "disabled";
410
+ if (flagOf(entry.unavailable) === true || status === "unavailable") return "unavailable";
411
+ if (status === "error" || status === "invalid" || status === "expired") return "error";
412
+ return "active";
413
+ }
414
+
415
+ function prefixOf(credential) {
416
+ return text(credential?.prefix).toLowerCase();
417
+ }
418
+
419
+ //#endregion
420
+
421
+ //#region Management Origin & Key Ref
422
+
423
+ /** Credential ref for an account's CPA management key. */
424
+ export function managementKeyRef(accountId) {
425
+ const id = text(accountId);
426
+ if (id === "" || id === "default") return MANAGEMENT_KEY_REF;
427
+ return `${MANAGEMENT_KEY_REF}_${id.toUpperCase().replace(/[^A-Z0-9]/g, "_")}`;
428
+ }
429
+
430
+ /** Management API base of an account endpoint: trailing slashes and a trailing `/v1` removed. */
431
+ export function managementOrigin(baseURL) {
432
+ let raw = text(baseURL);
433
+ if (raw === "") return undefined;
434
+ raw = raw.replace(/[?#].*$/, "").replace(/\/+$/, "").replace(/\/v1$/i, "").replace(/\/+$/, "");
435
+ let url;
436
+ try {
437
+ url = new URL(raw);
438
+ } catch {
439
+ return undefined;
440
+ }
441
+ if (url.protocol !== "http:" && url.protocol !== "https:") return undefined;
442
+ if (url.username !== "" || url.password !== "") return undefined;
443
+ return `${url.origin}${url.pathname.replace(/\/+$/, "")}`;
444
+ }
445
+
446
+ //#endregion
447
+
448
+ //#region Upstream Parsers
449
+
450
+ function parseCodexWindows(body, nowMs) {
451
+ const windows = [];
452
+ const addLimit = (info, idPrefix, group) => {
453
+ if (!isObj(info)) return;
454
+ const exhausted = flagOf(info.limit_reached ?? info.limitReached) === true || info.allowed === false;
455
+ const slots = [
456
+ ["primary", info.primary_window ?? info.primaryWindow, "5h"],
457
+ ["secondary", info.secondary_window ?? info.secondaryWindow, "7d"],
458
+ ];
459
+ for (const [slot, raw, fallbackKind] of slots) {
460
+ if (!isObj(raw)) continue;
461
+ const seconds = raw.limit_window_seconds ?? raw.limitWindowSeconds;
462
+ const kind = kindFromSeconds(seconds) || (numberOrNull(seconds) === null ? fallbackKind : "other");
463
+ const used = numberOrNull(raw.used_percent ?? raw.usedPercent);
464
+ const resetAt = isoFromUnix(raw.reset_at ?? raw.resetAt) ?? isoFromRelative(raw.reset_after_seconds ?? raw.resetAfterSeconds, nowMs);
465
+ if (used === null && resetAt === null) continue;
466
+ windows.push(makeWindow({
467
+ id: `${idPrefix}:${slot}`,
468
+ kind,
469
+ label: "",
470
+ group,
471
+ remainingPercent: used !== null ? 100 - clampPct(used) : exhausted ? 0 : null,
472
+ resetAt,
473
+ }));
474
+ }
475
+ };
476
+ addLimit(body.rate_limit ?? body.rateLimit, "codex", undefined);
477
+ addLimit(body.code_review_rate_limit ?? body.codeReviewRateLimit, "codex-review", "code_review");
478
+ const extras = body.additional_rate_limits ?? body.additionalRateLimits;
479
+ if (Array.isArray(extras)) {
480
+ extras.forEach((item, index) => {
481
+ if (!isObj(item)) return;
482
+ const name = firstText(item.limit_name, item.limitName, item.metered_feature, item.meteredFeature, `extra-${index + 1}`);
483
+ addLimit(item.rate_limit ?? item.rateLimit, `codex-extra:${name}`, name);
484
+ });
485
+ }
486
+ return windows;
487
+ }
488
+
489
+ const CLAUDE_WINDOWS = [
490
+ ["five_hour", "5h", undefined, undefined],
491
+ ["seven_day", "7d", undefined, undefined],
492
+ ["seven_day_oauth_apps", "7d", "oauth_apps", undefined],
493
+ ["seven_day_opus", "7d", "opus", ["claude-opus"]],
494
+ ["seven_day_sonnet", "7d", "sonnet", ["claude-sonnet"]],
495
+ ["seven_day_cowork", "7d", "cowork", undefined],
496
+ ["iguana_necktie", "7d", "fable", undefined],
497
+ ];
498
+
499
+ function parseClaudeWindows(body) {
500
+ const rows = [];
501
+ for (const [key, kind, group, prefixes] of CLAUDE_WINDOWS) {
502
+ const raw = body[key];
503
+ if (!isObj(raw)) continue;
504
+ // Anthropic reports utilization in percent (0..100).
505
+ const used = numberOrNull(raw.utilization);
506
+ const resetAt = isoFromString(firstText(raw.resets_at, raw.resetsAt, raw.reset_at, raw.resetAt));
507
+ if (used === null && resetAt === null) continue;
508
+ rows.push(makeWindow({ id: `claude:${key}`, kind, label: key, group, remainingPercent: used === null ? null : 100 - clampPct(used), resetAt, modelPrefixes: prefixes }));
509
+ }
510
+ if (Array.isArray(body.limits)) {
511
+ for (const limit of body.limits) {
512
+ if (!isObj(limit) || text(limit.kind).toLowerCase() !== "weekly_scoped") continue;
513
+ const model = text(getPath(limit, ["scope", "model", "display_name"]));
514
+ if (model === "") continue;
515
+ const used = numberOrNull(limit.percent);
516
+ const resetAt = isoFromString(firstText(limit.resets_at, limit.resetAt));
517
+ if (used === null && resetAt === null) continue;
518
+ const group = model.toLowerCase().replace(/\s+\d+(?:\.\d+)*$/, "").replace(/\s+/g, "_");
519
+ rows.push(makeWindow({ id: `claude:scoped:${group}`, kind: "7d", label: model, group, remainingPercent: used === null ? null : 100 - clampPct(used), resetAt }));
520
+ }
521
+ }
522
+ const extra = body.extra_usage;
523
+ if (isObj(extra)) {
524
+ const limit = numberOrNull(extra.monthly_limit);
525
+ const used = numberOrNull(extra.used_credits);
526
+ if (limit !== null && limit > 0 && used !== null) {
527
+ rows.push(makeWindow({ id: "claude:extra_usage", kind: "30d", label: "extra_usage", group: "extra_usage", remainingPercent: ((limit - used) / limit) * 100, resetAt: null }));
528
+ }
529
+ }
530
+ // The same scoped weekly limit can arrive as a named key and as limits[].
531
+ return dedupeWindows(rows, (row) => `${row.kind}:${row.group ?? ""}`);
532
+ }
533
+
534
+ function claudeProfilePlan(profile) {
535
+ const hasMax = flagOf(getPath(profile, ["account", "has_claude_max"]));
536
+ const hasPro = flagOf(getPath(profile, ["account", "has_claude_pro"]));
537
+ const orgType = text(getPath(profile, ["organization", "organization_type"])).toLowerCase();
538
+ const subStatus = text(getPath(profile, ["organization", "subscription_status"])).toLowerCase();
539
+ if (hasMax === true) return "Max";
540
+ if (hasPro === true) return "Pro";
541
+ if (orgType === "claude_team" && subStatus === "active") return "Team";
542
+ if (orgType === "claude_enterprise" && subStatus === "active") return "Enterprise";
543
+ if (hasMax === false && hasPro === false) return "Free";
544
+ return extractPlan(profile);
545
+ }
546
+
547
+ function groupPrefixes(groupText) {
548
+ const s = groupText.toLowerCase();
549
+ if (s.includes("claude") || s.includes("gpt")) return ["claude", "gpt"];
550
+ if (s.includes("gemini")) return ["gemini", "gemma"];
551
+ return undefined;
552
+ }
553
+
554
+ function groupForModel(modelId, label) {
555
+ const s = `${text(modelId)} ${text(label)}`.toLowerCase();
556
+ if (s.includes("claude") || s.includes("gpt")) return GROUP_CLAUDE_GPT;
557
+ if (s.includes("gemini") || s.includes("gemma")) return GROUP_GEMINI;
558
+ return GROUP_OTHER;
559
+ }
560
+
561
+ function antigravitySummaryWindows(body) {
562
+ return groupedQuotaWindows(body, "ag");
563
+ }
564
+
565
+ /**
566
+ * groups[].buckets[] with remainingFraction / resetTime: the Antigravity
567
+ * summary shape, which CPA also uses for its normalized plugin quota
568
+ * (pluginapi.QuotaFetchResponse).
569
+ */
570
+ function groupedQuotaWindows(body, idPrefix) {
571
+ const groups = Array.isArray(body.groups) ? body.groups
572
+ : Array.isArray(body.quota?.groups) ? body.quota.groups
573
+ : Array.isArray(body.data?.groups) ? body.data.groups : [];
574
+ const rows = [];
575
+ groups.forEach((group, groupIndex) => {
576
+ if (!isObj(group)) return;
577
+ const groupLabel = firstText(group.displayName, group.display_name, group.name, `Quota Group ${groupIndex + 1}`);
578
+ const groupId = text(group.id) || groupLabel.toLowerCase().replace(/[^a-z0-9]+/g, "-").replace(/^-+|-+$/g, "") || `group-${groupIndex + 1}`;
579
+ const prefixes = groupPrefixes(`${groupLabel} ${text(group.description)}`);
580
+ const buckets = Array.isArray(group.buckets) ? group.buckets : [];
581
+ buckets.forEach((bucket, bucketIndex) => {
582
+ if (!isObj(bucket)) return;
583
+ const remaining = fractionPercent(bucket.remainingFraction ?? bucket.remaining_fraction);
584
+ const resetAt = isoFromString(firstText(bucket.resetTime, bucket.reset_time));
585
+ if (remaining === null && resetAt === null) return;
586
+ const label = firstText(bucket.displayName, bucket.display_name, bucket.label, bucket.description, bucket.window);
587
+ const kind = kindFromText(bucket.window, bucket.windowType, bucket.window_type, label)
588
+ || kindFromSeconds(bucket.windowSeconds ?? bucket.window_seconds ?? bucket.limitWindowSeconds ?? bucket.limit_window_seconds)
589
+ || "other";
590
+ rows.push(makeWindow({
591
+ id: `${idPrefix}:${groupId}:${firstText(bucket.bucketId, bucket.bucket_id, `${kind}-${bucketIndex + 1}`)}`,
592
+ kind,
593
+ label,
594
+ group: groupLabel,
595
+ remainingPercent: remaining,
596
+ resetAt,
597
+ modelPrefixes: prefixes,
598
+ }));
599
+ });
600
+ });
601
+ return rows;
602
+ }
603
+
604
+ function antigravityModelWindows(body, nowMs) {
605
+ const models = body.models;
606
+ if (models === null || typeof models !== "object") return [];
607
+ const entries = Array.isArray(models) ? models.map((raw, i) => [String(i), raw]) : Object.entries(models);
608
+ const rows = [];
609
+ for (const [key, raw] of entries) {
610
+ if (!isObj(raw)) continue;
611
+ const modelId = firstText(raw.modelId, raw.model_id, raw.id, raw.model, Array.isArray(models) ? "" : key, raw.name);
612
+ if (modelId === "") continue;
613
+ const quota = isObj(raw.quotaInfo) ? raw.quotaInfo : isObj(raw.quota_info) ? raw.quota_info : raw;
614
+ let remaining = fractionPercent(quota.remainingFraction ?? quota.remaining_fraction ?? quota.remaining);
615
+ const amount = numberOrNull(quota.remainingAmount ?? quota.remaining_amount);
616
+ const resetAt = isoFromString(firstText(quota.resetTime, quota.reset_time, raw.resetTime, raw.reset_time));
617
+ if (remaining === null && amount !== null && amount <= 0) remaining = 0;
618
+ if (remaining === null && resetAt === null) continue;
619
+ const label = firstText(raw.displayName, raw.display_name, raw.name);
620
+ rows.push(makeWindow({
621
+ id: `ag-model:${modelId}`,
622
+ kind: kindFromReset(resetAt, nowMs),
623
+ label,
624
+ group: groupForModel(modelId, label),
625
+ remainingPercent: remaining,
626
+ resetAt,
627
+ modelIds: [modelId],
628
+ }));
629
+ }
630
+ return rows;
631
+ }
632
+
633
+ /** Google per-model quota buckets; identical buckets are merged into one window with all their model ids. */
634
+ function googleBucketWindows(body, nowMs, idPrefix) {
635
+ const buckets = Array.isArray(body.buckets) ? body.buckets : [];
636
+ const merged = new Map();
637
+ for (const bucket of buckets) {
638
+ if (!isObj(bucket)) continue;
639
+ const modelId = firstText(bucket.modelId, bucket.model_id).replace(/_vertex$/, "");
640
+ if (!/^(?:gemini|gemma)/i.test(modelId)) continue;
641
+ const remaining = fractionPercent(bucket.remainingFraction ?? bucket.remaining_fraction ?? bucket.remaining);
642
+ const resetAt = isoFromString(firstText(bucket.resetTime, bucket.reset_time));
643
+ if (remaining === null && resetAt === null) continue;
644
+ const label = firstText(bucket.displayName, bucket.display_name, bucket.label);
645
+ const kind = kindFromText(bucket.window, bucket.windowType, label) || kindFromReset(resetAt, nowMs);
646
+ const pct = roundPct(remaining);
647
+ const key = `${kind}|${pct}|${resetAt ?? ""}|${label}`;
648
+ const prev = merged.get(key);
649
+ if (prev !== undefined) {
650
+ if (!prev.modelIds.includes(modelId)) prev.modelIds.push(modelId);
651
+ continue;
652
+ }
653
+ merged.set(key, { kind, pct, resetAt, label, modelIds: [modelId] });
654
+ }
655
+ return [...merged.values()].map((w, i) => makeWindow({
656
+ id: `${idPrefix}:${w.kind}:${i + 1}`,
657
+ kind: w.kind,
658
+ label: w.label,
659
+ group: GROUP_GEMINI,
660
+ remainingPercent: w.pct,
661
+ resetAt: w.resetAt,
662
+ modelIds: w.modelIds,
663
+ }));
664
+ }
665
+
666
+ /** Hints in the auth-file listing that the credential is a kimi.ai account. */
667
+ function kimiPrefersAi(entry) {
668
+ const hint = (v) => {
669
+ const s = text(v).toLowerCase();
670
+ if (s === "") return null;
671
+ if (["ai", "kimi-ai", "kimi_ai", "kimi.ai"].includes(s) || /(?:^|[./])kimi\.ai(?:[/:]|$)/.test(s)) return true;
672
+ if (["com", "kimi", "kimi.com"].includes(s) || /(?:^|[./])kimi\.com(?:[/:]|$)/.test(s)) return false;
673
+ return null;
674
+ };
675
+ for (const v of [
676
+ entry.domain, getPath(entry, ["metadata", "domain"]), getPath(entry, ["attributes", "domain"]),
677
+ entry.base_url, getPath(entry, ["metadata", "base_url"]), getPath(entry, ["attributes", "base_url"]),
678
+ entry.provider, entry.type,
679
+ ]) {
680
+ const h = hint(v);
681
+ if (h !== null) return h;
682
+ }
683
+ return /kimi[-_.]ai/i.test(`${text(entry.id)} ${text(entry.name)}`);
684
+ }
685
+
686
+ function parseKimiWindows(body, nowMs) {
687
+ const rows = [];
688
+ const add = (data, index, duration, unit) => {
689
+ if (!isObj(data)) return;
690
+ const limit = numberOrNull(data.limit);
691
+ const used = numberOrNull(data.used);
692
+ const remainingAmount = numberOrNull(data.remaining);
693
+ let remaining = null;
694
+ if (limit !== null && limit > 0) {
695
+ if (remainingAmount !== null) remaining = (remainingAmount / limit) * 100;
696
+ else if (used !== null) remaining = 100 - (used / limit) * 100;
697
+ }
698
+ const resetAt = isoFromString(firstText(data.reset_at, data.resetAt, data.reset_time, data.resetTime))
699
+ ?? isoFromRelative(data.reset_in ?? data.resetIn ?? data.ttl, nowMs);
700
+ if (remaining === null && resetAt === null) return;
701
+ const label = firstText(data.name, data.title, data.scope);
702
+ const kind = kindFromText(label) || kindFromSeconds(durationSeconds(duration, unit)) || (index === "usage" ? "7d" : "other");
703
+ rows.push(makeWindow({ id: `kimi:${index}`, kind, label, remainingPercent: remaining, resetAt }));
704
+ };
705
+ (Array.isArray(body.limits) ? body.limits : []).forEach((item, index) => {
706
+ if (!isObj(item)) return;
707
+ const detail = isObj(item.detail) ? item.detail : item;
708
+ const window = isObj(item.window) ? item.window : {};
709
+ add(detail, String(index), window.duration ?? item.duration ?? detail.duration, window.timeUnit ?? item.timeUnit ?? detail.timeUnit);
710
+ });
711
+ if (isObj(body.usage)) add(body.usage, "usage", null, null);
712
+ return rows;
713
+ }
714
+
715
+ function parseXaiWindows(body, period) {
716
+ const config = isObj(body.config) ? body.config : body;
717
+ const rows = [];
718
+ const current = isObj(config.currentPeriod) ? config.currentPeriod : isObj(config.current_period) ? config.current_period : {};
719
+ const periodEnd = isoFromString(firstText(current.end, config.billingPeriodEnd, config.billing_period_end));
720
+ const creditUsage = numberOrNull(config.creditUsagePercent ?? config.credit_usage_percent);
721
+ if (creditUsage !== null && period !== "monthly") {
722
+ rows.push(makeWindow({ id: "xai:weekly", kind: "7d", label: "credits", remainingPercent: 100 - clampPct(creditUsage), resetAt: periodEnd }));
723
+ }
724
+ const valueOf = (v) => (isObj(v) ? numberOrNull(v.val) : numberOrNull(v));
725
+ const monthlyLimit = valueOf(config.monthlyLimit ?? config.monthly_limit);
726
+ const used = valueOf(config.used);
727
+ if (monthlyLimit !== null && monthlyLimit > 0 && used !== null) {
728
+ rows.push(makeWindow({ id: "xai:monthly", kind: "30d", label: "monthly credits", remainingPercent: ((monthlyLimit - used) / monthlyLimit) * 100, resetAt: period === "monthly" ? periodEnd : null }));
729
+ }
730
+ const products = config.productUsage ?? config.product_usage;
731
+ if (Array.isArray(products)) {
732
+ products.forEach((item, index) => {
733
+ if (!isObj(item)) return;
734
+ const usage = numberOrNull(item.usagePercent ?? item.usage_percent);
735
+ if (usage === null) return;
736
+ rows.push(makeWindow({ id: `xai:product:${index}`, kind: "7d", label: firstText(item.product), group: firstText(item.product), remainingPercent: 100 - clampPct(usage), resetAt: periodEnd }));
737
+ });
738
+ }
739
+ return rows;
740
+ }
741
+
742
+ /**
743
+ * Devin GetUserStatus: one account-wide daily and weekly quota shared by
744
+ * every Devin model (windows carry no model filter). Percentages are
745
+ * remaining 0–100; resets are Unix seconds. Mirrors CPA's management center.
746
+ */
747
+ function parseDevinStatus(body) {
748
+ const status = isObj(body.userStatus) && isObj(body.userStatus.planStatus) ? body.userStatus.planStatus : {};
749
+ const planInfo = isObj(status.planInfo) ? status.planInfo : isObj(body.planInfo) ? body.planInfo : {};
750
+ const percent = (v) => {
751
+ const n = numberOrNull(v);
752
+ return n !== null && n >= 0 && n <= 100 ? n : null;
753
+ };
754
+ const windows = [];
755
+ for (const [id, kind] of [["daily", "1d"], ["weekly", "7d"]]) {
756
+ const remainingPercent = percent(status[`${id}QuotaRemainingPercent`]);
757
+ const resetAt = isoFromUnix(status[`${id}QuotaResetAtUnix`]);
758
+ if (remainingPercent === null && resetAt === null) continue;
759
+ windows.push(makeWindow({ id: `devin:${id}`, kind, label: id, remainingPercent, resetAt }));
760
+ }
761
+ if (windows.length === 0) throw new QuotaError("invalid-response");
762
+ const name = text(planInfo.planName);
763
+ return { windows, plan: name === "" ? "" : PLAN_LABELS[name.toLowerCase()] ?? name };
764
+ }
765
+
766
+ /**
767
+ * Meta Muse Code `subs_usage`: a rolling window (window_duration_mins) and a
768
+ * weekly window, each with used_percent and resets_at (Unix seconds). Only
769
+ * these fields are read: the endpoint can echo api_key and PII. A valid body
770
+ * without subs_usage is a reachable account with no quota observed yet.
771
+ */
772
+ function parseMetaUsage(body) {
773
+ const usage = isObj(body.subs_usage) ? body.subs_usage : {};
774
+ const windows = [];
775
+ for (const [id, raw] of [["window", usage.window], ["weekly", usage.weekly]]) {
776
+ if (!isObj(raw)) continue;
777
+ const used = numberOrNull(raw.used_percent);
778
+ const resetAt = isoFromUnix(raw.resets_at);
779
+ if (used === null && resetAt === null) continue;
780
+ const minutes = numberOrNull(raw.window_duration_mins);
781
+ const kind = id === "weekly" ? "7d" : (minutes !== null && minutes > 0 ? kindFromSeconds(minutes * 60) : "") || "other";
782
+ windows.push(makeWindow({ id: `meta:${id}`, kind, label: id, remainingPercent: used === null ? null : 100 - clampPct(used), resetAt }));
783
+ }
784
+ return { windows, plan: firstText(body.subs_tier_name, usage.tier) };
785
+ }
786
+
787
+ /** CPA's normalized plugin quota (`/v0/management/quota/fetch`). */
788
+ function parsePluginQuota(body) {
789
+ const sub = isObj(body.subscription) ? body.subscription : {};
790
+ return { windows: groupedQuotaWindows(body, "plugin"), plan: firstText(sub.tierName, sub.plan, sub.tierId) };
791
+ }
792
+
793
+ //#endregion
794
+
795
+ //#region Model Matching
796
+
797
+ function modelFamily(modelId) {
798
+ const s = text(modelId).toLowerCase();
799
+ if (s === "") return "";
800
+ if (/grok|xai/.test(s)) return "xai";
801
+ if (/kimi|moonshot/.test(s)) return "kimi";
802
+ if (/qwen|qwq/.test(s)) return "qwen";
803
+ if (/iflow/.test(s)) return "iflow";
804
+ if (/glm|chatglm|zhipu/.test(s)) return "glm";
805
+ if (/deepseek/.test(s)) return "deepseek";
806
+ if (/gemini|gemma/.test(s)) return "gemini";
807
+ if (/claude|sonnet|opus|haiku/.test(s)) return "claude";
808
+ if (/gpt|openai|codex/.test(s) || /(?:^|[-_:/.])o[1-9](?:$|[-_:/.])/.test(s)) return "gpt";
809
+ return "";
810
+ }
811
+
812
+ /** CPA `owned_by` → family (or a provider type for families without a model-name pattern). */
813
+ function ownedByFamily(ownedBy) {
814
+ const o = text(ownedBy).toLowerCase();
815
+ if (o === "") return "";
816
+ if (/anthropic|claude/.test(o)) return "claude";
817
+ if (/openai|codex/.test(o)) return "gpt";
818
+ if (/google|gemini|antigravity/.test(o)) return "gemini";
819
+ if (/xai|grok/.test(o)) return "xai";
820
+ if (/moonshot|kimi/.test(o)) return "kimi";
821
+ return `provider:${normalizeProvider(o)}`;
822
+ }
823
+
824
+ function windowsMention(credential, word) {
825
+ const rows = Array.isArray(credential.windows) ? credential.windows : [];
826
+ return rows.some((w) =>
827
+ (Array.isArray(w.modelPrefixes) && w.modelPrefixes.some((p) => text(p).toLowerCase().startsWith(word)))
828
+ || (Array.isArray(w.modelIds) && w.modelIds.some((id) => text(id).toLowerCase().includes(word)))
829
+ || text(w.group).toLowerCase().includes(word));
830
+ }
831
+
832
+ function servesFamily(credential, family) {
833
+ if (family === "") return false;
834
+ const provider = text(credential.provider).toLowerCase();
835
+ if (family.startsWith("provider:")) return provider === family.slice("provider:".length);
836
+ switch (provider) {
837
+ case "claude": return family === "claude";
838
+ case "codex":
839
+ case "openai": return family === "gpt";
840
+ case "gemini":
841
+ case "gemini-cli":
842
+ case "gemini-api":
843
+ case "aistudio": return family === "gemini";
844
+ case "vertex": return family === "gemini" || family === "claude";
845
+ // Antigravity serves Gemini, and Claude/GPT only when its quota groups say so.
846
+ case "antigravity": return family === "gemini" || ((family === "claude" || family === "gpt") && windowsMention(credential, family));
847
+ case "kimi":
848
+ case "xai":
849
+ case "qwen":
850
+ case "iflow":
851
+ case "glm":
852
+ case "deepseek": return family === provider;
853
+ default: return false;
854
+ }
855
+ }
856
+
857
+ const listHas = (credential, idLower) => Array.isArray(credential.models) && credential.models.some((m) => text(m).toLowerCase() === idLower);
858
+
859
+ function finishMatch(list, matchedBy) {
860
+ const seen = new Set();
861
+ const unique = list.filter((c) => (seen.has(c.id) ? false : (seen.add(c.id), true)));
862
+ const rank = (c) => (c.status === "active" ? 0 : 1);
863
+ const credentialIds = unique
864
+ .map((c, index) => ({ c, index }))
865
+ .sort((a, b) => rank(a.c) - rank(b.c) || a.index - b.index)
866
+ .map(({ c }) => c.id);
867
+ return { credentialIds, matchedBy: credentialIds.length === 0 ? "none" : matchedBy };
868
+ }
869
+
870
+ /** Credentials of one snapshot account entry that serve `modelId`. */
871
+ export function matchCredentials(accountEntry, modelId) {
872
+ const credentials = (Array.isArray(accountEntry?.credentials) ? accountEntry.credentials : [])
873
+ .filter((c) => isObj(c) && typeof c.id === "string" && c.id !== "");
874
+ const id = text(modelId).toLowerCase();
875
+ if (id === "" || credentials.length === 0) return { credentialIds: [], matchedBy: "none" };
876
+
877
+ // Rule 1: "p/rest" routes only to credentials with prefix p.
878
+ const slash = id.indexOf("/");
879
+ if (slash > 0) {
880
+ const prefix = id.slice(0, slash);
881
+ const prefixed = credentials.filter((c) => prefixOf(c) === prefix);
882
+ if (prefixed.length > 0) {
883
+ const confirmed = prefixed.filter((c) => listHas(c, id));
884
+ const unknown = prefixed.filter((c) => !Array.isArray(c.models));
885
+ if (confirmed.length > 0) return finishMatch([...confirmed, ...unknown], "model-list");
886
+ return finishMatch(unknown.length > 0 ? unknown : prefixed, "prefix");
887
+ }
888
+ }
889
+
890
+ // Rule 2: exact model-list hits, un-prefixed credentials first.
891
+ const unprefixed = credentials.filter((c) => prefixOf(c) === "");
892
+ const prefixed = credentials.filter((c) => prefixOf(c) !== "");
893
+ const plainHits = unprefixed.filter((c) => listHas(c, id));
894
+ const prefixedHits = plainHits.length > 0 ? [] : prefixed.filter((c) => listHas(c, `${prefixOf(c)}/${id}`) || listHas(c, id));
895
+
896
+ // Rule 3: family heuristics for un-prefixed credentials whose model list is unknown.
897
+ let family = modelFamily(id);
898
+ if (family === "") {
899
+ const picker = Array.isArray(accountEntry?.pickerModels) ? accountEntry.pickerModels : [];
900
+ const hint = picker.find((m) => isObj(m) && text(m.id).toLowerCase() === id && text(m.ownedBy) !== "");
901
+ if (hint !== undefined) family = ownedByFamily(hint.ownedBy);
902
+ }
903
+ const familyHits = family === "" ? [] : unprefixed.filter((c) => !Array.isArray(c.models) && servesFamily(c, family));
904
+
905
+ const listHits = [...plainHits, ...prefixedHits];
906
+ if (listHits.length > 0) return finishMatch([...listHits, ...familyHits], "model-list");
907
+ if (familyHits.length > 0) return finishMatch(familyHits, "family");
908
+ return { credentialIds: [], matchedBy: "none" };
909
+ }
910
+
911
+ function buildModelIndex(entry) {
912
+ const ids = new Set();
913
+ for (const c of entry.credentials) {
914
+ if (!Array.isArray(c.models)) continue;
915
+ const prefix = prefixOf(c);
916
+ for (const m of c.models) {
917
+ ids.add(m);
918
+ if (prefix !== "" && m.toLowerCase().startsWith(`${prefix}/`)) ids.add(m.slice(prefix.length + 1));
919
+ }
920
+ }
921
+ for (const m of entry.pickerModels ?? []) if (text(m?.id) !== "") ids.add(text(m.id));
922
+ const modelIndex = {};
923
+ const modelMatch = {};
924
+ for (const id of ids) {
925
+ const match = matchCredentials(entry, id);
926
+ modelIndex[id] = match.credentialIds;
927
+ modelMatch[id] = match.matchedBy;
928
+ }
929
+ return { modelIndex, modelMatch };
930
+ }
931
+
932
+ //#endregion
933
+
934
+ //#region HTTP
935
+
936
+ async function readBounded(response, limit) {
937
+ const declared = Number(response.headers?.get?.("content-length"));
938
+ if (Number.isFinite(declared) && declared > limit) throw new QuotaError("invalid-response");
939
+ const body = response.body;
940
+ if (body !== null && body !== undefined && typeof body.getReader === "function") {
941
+ const reader = body.getReader();
942
+ const chunks = [];
943
+ let size = 0;
944
+ for (;;) {
945
+ const { done, value } = await reader.read();
946
+ if (done) break;
947
+ size += value.byteLength;
948
+ if (size > limit) {
949
+ reader.cancel().catch(() => {});
950
+ throw new QuotaError("invalid-response");
951
+ }
952
+ chunks.push(value);
953
+ }
954
+ const all = new Uint8Array(size);
955
+ let offset = 0;
956
+ for (const chunk of chunks) {
957
+ all.set(chunk, offset);
958
+ offset += chunk.byteLength;
959
+ }
960
+ return new TextDecoder().decode(all);
961
+ }
962
+ const raw = typeof response.text === "function" ? await response.text() : "";
963
+ if (Buffer.byteLength(raw, "utf8") > limit) throw new QuotaError("invalid-response");
964
+ return raw;
965
+ }
966
+
967
+ function parseJsonLoose(raw) {
968
+ if (typeof raw !== "string") return raw;
969
+ const s = raw.trim();
970
+ if (s === "") return null;
971
+ try {
972
+ return JSON.parse(s);
973
+ } catch {
974
+ return undefined;
975
+ }
976
+ }
977
+
978
+ /** Unwraps the occasional `{body|data|result}` wrapper or JSON-string nesting around an upstream payload. */
979
+ function unwrapNested(value) {
980
+ let current = value;
981
+ for (let depth = 0; depth < 4; depth += 1) {
982
+ if (typeof current === "string") {
983
+ const parsed = parseJsonLoose(current);
984
+ if (parsed === undefined || parsed === null) return current;
985
+ current = parsed;
986
+ }
987
+ if (!isObj(current)) return current;
988
+ if (["groups", "models", "currentTier", "paidTier", "cloudaicompanionProject", "config", "limits", "usage", "rate_limit", "rateLimit", "five_hour", "seven_day", "buckets", "account", "organization", "plan_type"].some((k) => Object.hasOwn(current, k))) return current;
989
+ const nested = current.body ?? current.data ?? current.result;
990
+ if (nested === undefined || nested === null || nested === current) return current;
991
+ current = nested;
992
+ }
993
+ return current;
994
+ }
995
+
996
+ function upstreamErrorCode(status) {
997
+ if (status === 401) return "upstream-unauthorized";
998
+ if (status === 403) return "upstream-forbidden";
999
+ if (status === 404) return "upstream-not-found";
1000
+ if (status === 429) return "upstream-rate-limited";
1001
+ return "upstream-http";
1002
+ }
1003
+
1004
+ function createLimiter(limit) {
1005
+ let active = 0;
1006
+ const queue = [];
1007
+ const next = () => {
1008
+ while (active < limit && queue.length > 0) {
1009
+ const job = queue.shift();
1010
+ active += 1;
1011
+ Promise.resolve()
1012
+ .then(job.fn)
1013
+ .then(job.resolve, job.reject)
1014
+ .finally(() => {
1015
+ active -= 1;
1016
+ next();
1017
+ });
1018
+ }
1019
+ };
1020
+ return (fn, signal) => new Promise((resolve, reject) => {
1021
+ const job = {
1022
+ fn: () => {
1023
+ if (signal?.aborted) throw new QuotaError("aborted");
1024
+ return fn();
1025
+ },
1026
+ resolve,
1027
+ reject,
1028
+ };
1029
+ queue.push(job);
1030
+ next();
1031
+ });
1032
+ }
1033
+
1034
+ //#endregion
1035
+
1036
+ //#region Quota Controller
1037
+
1038
+ /**
1039
+ * Owns the quota state of every plugin account: key lookups, management API
1040
+ * reads, upstream quota probes, TTL/lockout policy and the public snapshot.
1041
+ */
1042
+ export function createQuotaController(deps) {
1043
+ const fetchImpl = deps.fetch ?? globalThis.fetch;
1044
+ const now = deps.now ?? Date.now;
1045
+ const ttlMs = Number.isFinite(deps.ttlMs) && deps.ttlMs >= 0 ? deps.ttlMs : QUOTA_TTL_MS;
1046
+ const timeoutMs = Number.isFinite(deps.timeoutMs) && deps.timeoutMs > 0 ? deps.timeoutMs : DEFAULT_QUOTA_TIMEOUT_MS;
1047
+ const concurrency = Number.isInteger(deps.concurrency) && deps.concurrency > 0 ? deps.concurrency : DEFAULT_QUOTA_CONCURRENCY;
1048
+ const logger = deps.logger;
1049
+ const root = new AbortController();
1050
+ const pool = createLimiter(concurrency);
1051
+ const states = new Map();
1052
+ /** management origin → Antigravity endpoint that last returned usable rows. */
1053
+ const agPreferred = new Map();
1054
+ let disposed = false;
1055
+
1056
+ const stateFor = (id) => {
1057
+ let st = states.get(id);
1058
+ if (st === undefined) {
1059
+ st = { gen: 0, inflight: null, result: null, data: null, lockedOut: false, lastAttemptAt: undefined };
1060
+ states.set(id, st);
1061
+ }
1062
+ return st;
1063
+ };
1064
+
1065
+ const log = (level, message) => {
1066
+ try {
1067
+ logger?.[level]?.(message);
1068
+ } catch {
1069
+ /* logging must never break a refresh */
1070
+ }
1071
+ };
1072
+
1073
+ const listAccounts = () => {
1074
+ try {
1075
+ const list = deps.listAccounts();
1076
+ return Array.isArray(list) ? list.filter((a) => isObj(a) && text(a.id) !== "") : [];
1077
+ } catch {
1078
+ return [];
1079
+ }
1080
+ };
1081
+
1082
+ const findAccount = (accountId) => {
1083
+ const account = listAccounts().find((a) => a.id === accountId);
1084
+ if (account === undefined) throw new Error(`dsh-cliproxyapi: unknown account ${JSON.stringify(String(accountId))}`);
1085
+ return account;
1086
+ };
1087
+
1088
+ const keyConfigured = async (ref) => {
1089
+ try {
1090
+ return (await deps.describeKey(ref))?.configured === true;
1091
+ } catch {
1092
+ return false;
1093
+ }
1094
+ };
1095
+
1096
+ // --- HTTP with per-request timeout, linked to the cycle signal ---
1097
+ async function request(url, init, signal) {
1098
+ const controller = new AbortController();
1099
+ let timedOut = false;
1100
+ const timer = setTimeout(() => {
1101
+ timedOut = true;
1102
+ controller.abort();
1103
+ }, timeoutMs);
1104
+ const onAbort = () => controller.abort();
1105
+ if (signal.aborted) controller.abort();
1106
+ else signal.addEventListener("abort", onAbort, { once: true });
1107
+ // Settles even if the fetch implementation ignores the signal.
1108
+ const aborted = new Promise((_, reject) => {
1109
+ const fail = () => reject(new QuotaError(timedOut ? "timeout" : "aborted"));
1110
+ if (controller.signal.aborted) fail();
1111
+ else controller.signal.addEventListener("abort", fail, { once: true });
1112
+ });
1113
+ aborted.catch(() => {});
1114
+ try {
1115
+ const response = await Promise.race([fetchImpl(url, { ...init, signal: controller.signal, redirect: "error" }), aborted]);
1116
+ const body = await Promise.race([readBounded(response, MAX_QUOTA_RESPONSE_BYTES), aborted]);
1117
+ return { status: response.status, ok: response.ok, body };
1118
+ } catch (error) {
1119
+ if (timedOut) throw new QuotaError("timeout");
1120
+ if (signal.aborted) throw new QuotaError("aborted");
1121
+ if (error instanceof QuotaError) throw error;
1122
+ throw new QuotaError("network");
1123
+ } finally {
1124
+ clearTimeout(timer);
1125
+ signal.removeEventListener("abort", onAbort);
1126
+ }
1127
+ }
1128
+
1129
+ async function management(ctx, path, { method = "GET", body } = {}) {
1130
+ const headers = { Accept: "application/json", Authorization: `Bearer ${ctx.key}` };
1131
+ if (body !== undefined) headers["Content-Type"] = "application/json";
1132
+ const response = await request(`${ctx.origin}${path}`, { method, headers, ...(body === undefined ? {} : { body: JSON.stringify(body) }) }, ctx.signal);
1133
+ if (response.status === 401) throw new QuotaError("unauthorized", 401);
1134
+ if (response.status === 403) throw new QuotaError("forbidden", 403);
1135
+ if (!response.ok) throw new QuotaError("http", response.status);
1136
+ const json = parseJsonLoose(response.body);
1137
+ if (json === undefined) throw new QuotaError("invalid-response");
1138
+ return json;
1139
+ }
1140
+
1141
+ /** POST /v0/management/api-call; returns the upstream JSON object or throws an upstream code. */
1142
+ async function apiCall(ctx, payload) {
1143
+ const response = await management(ctx, "/v0/management/api-call", { method: "POST", body: payload });
1144
+ if (!isObj(response)) throw new QuotaError("invalid-response");
1145
+ const status = numberOrNull(response.status_code ?? response.statusCode ?? response.status);
1146
+ if (status === null) throw new QuotaError("invalid-response");
1147
+ if (status < 200 || status >= 300) throw new QuotaError(upstreamErrorCode(status), status);
1148
+ const body = unwrapNested(response.body ?? response.data);
1149
+ if (!isObj(body)) throw new QuotaError("invalid-response");
1150
+ return body;
1151
+ }
1152
+
1153
+ async function loadModels(ctx, name, prefix, status) {
1154
+ try {
1155
+ const payload = await management(ctx, `/v0/management/auth-files/models?name=${encodeURIComponent(name)}`);
1156
+ const rows = Array.isArray(payload) ? payload : Array.isArray(payload?.models) ? payload.models : null;
1157
+ if (rows === null) return null;
1158
+ const ids = [];
1159
+ for (const row of rows) {
1160
+ const id = typeof row === "string" ? text(row) : firstText(row?.id, row?.name);
1161
+ if (id === "") continue;
1162
+ const full = prefix !== "" && !id.toLowerCase().startsWith(`${prefix}/`) ? `${prefix}/${id}` : id;
1163
+ if (!ids.includes(full)) ids.push(full);
1164
+ }
1165
+ // CPA drops inactive credentials from its model registry; an empty list then means "unknown".
1166
+ if (ids.length === 0 && status !== "active") return null;
1167
+ return ids;
1168
+ } catch (error) {
1169
+ if (error?.code === "aborted") throw error;
1170
+ return null;
1171
+ }
1172
+ }
1173
+
1174
+ async function loadCodeAssist(ctx, authIndex, metadata) {
1175
+ const headers = metadata.ideType === "ANTIGRAVITY"
1176
+ ? { Authorization: "Bearer $TOKEN$", "Content-Type": "application/json", "User-Agent": AG_SUMMARY_UA }
1177
+ : googleHeaders(metadata);
1178
+ const body = await apiCall(ctx, { auth_index: authIndex, method: "POST", url: UPSTREAM.googleLoad, header: headers, data: JSON.stringify({ metadata }) });
1179
+ const project = body.cloudaicompanionProject ?? body.cloudaicompanion_project;
1180
+ return { body, projectID: isObj(project) ? firstText(project.id) : firstText(project) };
1181
+ }
1182
+
1183
+ function googleHeaders(metadata) {
1184
+ return {
1185
+ Authorization: "Bearer $TOKEN$",
1186
+ "Content-Type": "application/json",
1187
+ "User-Agent": "google-api-nodejs-client/9.15.1",
1188
+ "X-Goog-Api-Client": "google-cloud-sdk vscode_cloudshelleditor/0.1",
1189
+ "Client-Metadata": JSON.stringify(metadata),
1190
+ };
1191
+ }
1192
+
1193
+ // --- Provider quota probes: each returns { windows, plan? } or throws QuotaError ---
1194
+ const PROBES = {
1195
+ async codex(ctx, entry, authIndex) {
1196
+ const header = { Authorization: "Bearer $TOKEN$", "Content-Type": "application/json", "User-Agent": CODEX_UA };
1197
+ const accountId = codexAccountId(entry);
1198
+ if (accountId !== "") header["Chatgpt-Account-Id"] = accountId;
1199
+ const body = await apiCall(ctx, { auth_index: authIndex, method: "GET", url: UPSTREAM.codexUsage, header });
1200
+ return { windows: parseCodexWindows(body, now()), plan: extractPlan(body) };
1201
+ },
1202
+
1203
+ async claude(ctx, entry, authIndex) {
1204
+ const header = { Authorization: "Bearer $TOKEN$", "Content-Type": "application/json", "anthropic-beta": "oauth-2025-04-20" };
1205
+ const [usage, profile] = await Promise.allSettled([
1206
+ apiCall(ctx, { auth_index: authIndex, method: "GET", url: UPSTREAM.claudeUsage, header }),
1207
+ apiCall(ctx, { auth_index: authIndex, method: "GET", url: UPSTREAM.claudeProfile, header }),
1208
+ ]);
1209
+ if (usage.status === "rejected") throw usage.reason;
1210
+ return { windows: parseClaudeWindows(usage.value), plan: profile.status === "fulfilled" ? claudeProfilePlan(profile.value) : "" };
1211
+ },
1212
+
1213
+ async antigravity(ctx, entry, authIndex, currentPlan) {
1214
+ let projectID = projectIdOf(entry);
1215
+ let plan = "";
1216
+ let tier = null;
1217
+ if (projectID === "") {
1218
+ const loaded = await loadCodeAssist(ctx, authIndex, AG_META);
1219
+ projectID = loaded.projectID;
1220
+ plan = extractPlan(loaded.body);
1221
+ } else if (currentPlan === "") {
1222
+ tier = loadCodeAssist(ctx, authIndex, AG_META).then((l) => extractPlan(l.body), () => "");
1223
+ }
1224
+ if (projectID === "") throw new QuotaError("missing-project");
1225
+ const header = (url) => ({
1226
+ Authorization: "Bearer $TOKEN$",
1227
+ "Content-Type": "application/json",
1228
+ "User-Agent": url.includes("retrieveUserQuotaSummary") ? AG_SUMMARY_UA : AG_MODELS_UA,
1229
+ });
1230
+ const urls = [...UPSTREAM.antigravityQuota, ...UPSTREAM.antigravityModels];
1231
+ const preferred = agPreferred.get(ctx.origin);
1232
+ const ordered = preferred === undefined ? urls : [preferred, ...urls.filter((u) => u !== preferred)];
1233
+ let lastError = null;
1234
+ let partial = null;
1235
+ let windows = null;
1236
+ for (const url of ordered) {
1237
+ const summary = url.includes("retrieveUserQuotaSummary");
1238
+ if (partial !== null && !summary) continue;
1239
+ try {
1240
+ const body = await apiCall(ctx, { auth_index: authIndex, method: "POST", url, header: header(url), data: JSON.stringify({ project: projectID }) });
1241
+ plan = plan || extractPlan(body);
1242
+ const rows = summary ? antigravitySummaryWindows(body) : [];
1243
+ if (rows.length > 0) {
1244
+ if (rows.some((r) => r.modelPrefixes?.includes("gemini"))) {
1245
+ windows = rows;
1246
+ agPreferred.set(ctx.origin, url);
1247
+ break;
1248
+ }
1249
+ // A host without the Gemini group: keep looking at the other summary hosts.
1250
+ if (partial === null) partial = { rows, url };
1251
+ continue;
1252
+ }
1253
+ const modelRows = antigravityModelWindows(body, now());
1254
+ if (modelRows.length > 0) {
1255
+ windows = modelRows;
1256
+ agPreferred.set(ctx.origin, url);
1257
+ break;
1258
+ }
1259
+ } catch (error) {
1260
+ if (error?.code === "aborted") throw error;
1261
+ lastError = error;
1262
+ }
1263
+ }
1264
+ if (windows === null && partial !== null) {
1265
+ windows = partial.rows;
1266
+ agPreferred.set(ctx.origin, partial.url);
1267
+ try {
1268
+ const body = await apiCall(ctx, {
1269
+ auth_index: authIndex,
1270
+ method: "POST",
1271
+ url: UPSTREAM.googleQuota,
1272
+ header: { Authorization: "Bearer $TOKEN$", "Content-Type": "application/json", "User-Agent": AG_SUMMARY_UA },
1273
+ data: JSON.stringify({ project: projectID }),
1274
+ });
1275
+ windows = [...windows, ...googleBucketWindows(body, now(), "ag-gemini")];
1276
+ } catch (error) {
1277
+ if (error?.code === "aborted") throw error;
1278
+ /* the supplementary Gemini read is best-effort */
1279
+ }
1280
+ }
1281
+ if (tier !== null) plan = plan || (await tier);
1282
+ if (windows === null) throw lastError ?? new QuotaError("invalid-response");
1283
+ return { windows, plan };
1284
+ },
1285
+
1286
+ async "gemini-cli"(ctx, entry, authIndex) {
1287
+ let projectID = projectIdOf(entry);
1288
+ let plan = "";
1289
+ if (projectID === "") {
1290
+ const loaded = await loadCodeAssist(ctx, authIndex, GEMINI_META);
1291
+ projectID = loaded.projectID;
1292
+ plan = extractPlan(loaded.body);
1293
+ }
1294
+ if (projectID === "") throw new QuotaError("missing-project");
1295
+ const body = await apiCall(ctx, {
1296
+ auth_index: authIndex,
1297
+ method: "POST",
1298
+ url: UPSTREAM.googleQuota,
1299
+ header: googleHeaders({ ...GEMINI_META, duetProject: projectID }),
1300
+ data: JSON.stringify({ project: projectID }),
1301
+ });
1302
+ return { windows: googleBucketWindows(body, now(), "gemini"), plan };
1303
+ },
1304
+
1305
+ async kimi(ctx, entry, authIndex) {
1306
+ // kimi.com and Kimi International (kimi.ai) share the API on two hosts.
1307
+ // Prefer the host the credential hints at; on a rejected token try the
1308
+ // other one (both are Moonshot's own endpoints, never arbitrary hosts).
1309
+ const hosts = kimiPrefersAi(entry) ? [UPSTREAM.kimiAiUsage, UPSTREAM.kimiUsage] : [UPSTREAM.kimiUsage, UPSTREAM.kimiAiUsage];
1310
+ let lastError = null;
1311
+ for (const url of hosts) {
1312
+ try {
1313
+ const body = await apiCall(ctx, { auth_index: authIndex, method: "GET", url, header: { Authorization: "Bearer $TOKEN$" } });
1314
+ return { windows: parseKimiWindows(body, now()), plan: extractPlan(body) };
1315
+ } catch (error) {
1316
+ if (error?.code === "aborted") throw error;
1317
+ lastError = error;
1318
+ if (error?.code !== "upstream-unauthorized" && error?.code !== "upstream-forbidden") break;
1319
+ }
1320
+ }
1321
+ throw lastError;
1322
+ },
1323
+
1324
+ async meta(ctx, entry, authIndex) {
1325
+ // Meta's usage endpoint takes the credential's dca_token, not the LLM key
1326
+ // CPA would substitute for $TOKEN$. Read only that field from the auth
1327
+ // file for this one request; it is never stored, logged or returned.
1328
+ const name = firstText(entry.name, entry.id);
1329
+ if (name === "" || flagOf(entry.runtime_only ?? entry.runtimeOnly) === true) throw new QuotaError("missing-token");
1330
+ const file = await management(ctx, `/v0/management/auth-files/download?name=${encodeURIComponent(name)}`);
1331
+ const token = isObj(file) ? text(file.dca_token) : "";
1332
+ if (!/^dca:\S+$/.test(token)) throw new QuotaError("missing-token");
1333
+ const body = await apiCall(ctx, {
1334
+ auth_index: authIndex,
1335
+ method: "POST",
1336
+ url: UPSTREAM.metaUsage,
1337
+ header: { Accept: "application/json", "Content-Type": "application/json", Authorization: `Bearer ${token}`, "x-api-version": "1.0.0" },
1338
+ data: "{}",
1339
+ });
1340
+ return parseMetaUsage(body);
1341
+ },
1342
+
1343
+ async xai(ctx, entry, authIndex) {
1344
+ const header = {
1345
+ Authorization: "Bearer $TOKEN$",
1346
+ "x-xai-token-auth": "xai-grok-cli",
1347
+ "x-grok-client-version": "0.2.91",
1348
+ accept: "*/*",
1349
+ "user-agent": "grok-pager/0.2.91 grok-shell/0.2.91 (windows; amd64)",
1350
+ };
1351
+ const userId = firstText(entry.sub, entry.subject, entry.user_id, entry.userId, getPath(entry, ["metadata", "sub"]), getPath(entry, ["metadata", "user_id"]), getPath(entry, ["attributes", "sub"]));
1352
+ if (userId !== "") header["x-userid"] = userId;
1353
+ const results = await Promise.allSettled([
1354
+ apiCall(ctx, { auth_index: authIndex, method: "GET", url: UPSTREAM.xaiBillingWeekly, header }),
1355
+ apiCall(ctx, { auth_index: authIndex, method: "GET", url: UPSTREAM.xaiBillingMonthly, header }),
1356
+ ]);
1357
+ if (results.every((r) => r.status === "rejected")) throw results[0].reason;
1358
+ const rows = new Map();
1359
+ if (results[0].status === "fulfilled") for (const row of parseXaiWindows(results[0].value, "weekly")) rows.set(row.id, row);
1360
+ if (results[1].status === "fulfilled") for (const row of parseXaiWindows(results[1].value, "monthly")) rows.set(row.id, row);
1361
+ // Paid accounts expose no billing counters: reachable but window-less.
1362
+ return { windows: [...rows.values()], plan: rows.size === 0 ? "Paid" : "" };
1363
+ },
1364
+
1365
+ async devin(ctx, entry, authIndex) {
1366
+ const body = await apiCall(ctx, {
1367
+ auth_index: authIndex,
1368
+ method: "POST",
1369
+ url: UPSTREAM.devinStatus,
1370
+ header: { "Content-Type": "application/json", "Connect-Protocol-Version": "1" },
1371
+ data: JSON.stringify({ metadata: DEVIN_METADATA }),
1372
+ });
1373
+ return parseDevinStatus(body);
1374
+ },
1375
+ };
1376
+ PROBES.gemini = PROBES["gemini-cli"];
1377
+
1378
+ /** CPA plugin / declarative quota: POST /v0/management/quota/fetch. */
1379
+ async function pluginProbe(ctx, entry, authIndex) {
1380
+ const provider = text(firstText(entry.quota_provider, entry.quotaProvider, entry.provider, entry.type));
1381
+ const body = await management(ctx, "/v0/management/quota/fetch", {
1382
+ method: "POST",
1383
+ body: { auth_index: authIndex, ...(provider === "" ? {} : { provider }) },
1384
+ });
1385
+ if (!isObj(body)) throw new QuotaError("invalid-response");
1386
+ return parsePluginQuota(body);
1387
+ }
1388
+
1389
+ async function loadCredential(ctx, entry, previous) {
1390
+ const id = firstText(entry.name, entry.id);
1391
+ const provider = normalizeProvider(firstText(entry.provider, entry.type)) || "unknown";
1392
+ const prefix = text(firstText(entry.prefix, getPath(entry, ["attributes", "prefix"]), getPath(entry, ["metadata", "prefix"]))).replace(/^\/+|\/+$/g, "").toLowerCase();
1393
+ const status = credentialStatus(entry);
1394
+ const credential = {
1395
+ id,
1396
+ label: firstText(entry.label, entry.email, entry.account_email, typeof entry.account === "string" ? entry.account : ""),
1397
+ provider,
1398
+ };
1399
+ const plan = extractPlan(entry) || extractPlan(...entryClaims(entry)) || previous?.plan || "";
1400
+ credential.status = status;
1401
+ const statusMessage = shortMessage(firstText(entry.status_message, entry.statusMessage));
1402
+ if (statusMessage !== "") credential.statusMessage = statusMessage;
1403
+ if (prefix !== "") credential.prefix = prefix;
1404
+ const recent = parseRecent(entry);
1405
+
1406
+ const authIndex = firstText(entry.auth_index, entry.authIndex);
1407
+ // Built-in probes win; any other credential CPA marks `supports_quota`
1408
+ // (a CPA quota plugin or a declarative quota_probe) goes through CPA's
1409
+ // own normalized route.
1410
+ const probe = PROBES[provider] ?? (flagOf(entry.supports_quota ?? entry.supportsQuota) === true ? pluginProbe : undefined);
1411
+ const [models, quota] = await Promise.all([
1412
+ loadModels(ctx, id, prefix, status),
1413
+ (async () => {
1414
+ if (probe === undefined) return { quotaState: "unsupported", windows: [] };
1415
+ if (authIndex === "") return { quotaState: "error", quotaErrorCode: "missing-auth-index", windows: [] };
1416
+ try {
1417
+ const result = await probe(ctx, entry, authIndex, plan);
1418
+ return { quotaState: "ok", windows: sortWindows(result.windows), plan: result.plan };
1419
+ } catch (error) {
1420
+ if (error?.code === "aborted") throw error;
1421
+ return { quotaState: "error", quotaErrorCode: error instanceof QuotaError ? error.code : "invalid-response", windows: [] };
1422
+ }
1423
+ })(),
1424
+ ]);
1425
+ const finalPlan = quota.plan || plan;
1426
+ return {
1427
+ ...credential,
1428
+ ...(finalPlan === "" ? {} : { plan: finalPlan }),
1429
+ models,
1430
+ quotaState: quota.quotaState,
1431
+ ...(quota.quotaErrorCode === undefined ? {} : { quotaErrorCode: quota.quotaErrorCode }),
1432
+ windows: quota.windows,
1433
+ ...(recent === undefined ? {} : { recent }),
1434
+ };
1435
+ }
1436
+
1437
+ async function runCycle(account, signal) {
1438
+ let key;
1439
+ try {
1440
+ key = await deps.resolveKey(managementKeyRef(account.id));
1441
+ } catch {
1442
+ key = undefined;
1443
+ }
1444
+ if (typeof key !== "string" || key.trim() === "") return { kind: "nokey" };
1445
+ const origin = managementOrigin(account.endpoint) ?? managementOrigin(account.baseURL);
1446
+ if (origin === undefined) throw new QuotaError("no-endpoint");
1447
+ const ctx = { origin, key: key.trim(), signal };
1448
+ const files = await management(ctx, "/v0/management/auth-files");
1449
+ const entries = Array.isArray(files) ? files : Array.isArray(files?.files) ? files.files : null;
1450
+ if (entries === null) throw new QuotaError("invalid-response");
1451
+ const previous = new Map((stateFor(account.id).data?.credentials ?? []).map((c) => [c.id, c]));
1452
+ const seen = new Set();
1453
+ const unique = entries.filter((e) => {
1454
+ if (!isObj(e)) return false;
1455
+ const name = firstText(e.name, e.id);
1456
+ if (name === "" || seen.has(name)) return false;
1457
+ seen.add(name);
1458
+ return true;
1459
+ });
1460
+ const credentials = await Promise.all(unique.map((entry) => pool(() => loadCredential(ctx, entry, previous.get(firstText(entry.name, entry.id))), signal)));
1461
+ return { kind: "ok", credentials };
1462
+ }
1463
+
1464
+ function startRefresh(account) {
1465
+ const st = stateFor(account.id);
1466
+ if (st.inflight !== null) return st.inflight.promise;
1467
+ const controller = new AbortController();
1468
+ const onRootAbort = () => controller.abort();
1469
+ root.signal.addEventListener("abort", onRootAbort, { once: true });
1470
+ const gen = ++st.gen;
1471
+ const current = () => !disposed && st.gen === gen;
1472
+ const promise = (async () => {
1473
+ try {
1474
+ const result = await runCycle(account, controller.signal);
1475
+ if (!current()) return;
1476
+ if (result.kind === "nokey") {
1477
+ st.result = { state: "nokey" };
1478
+ } else {
1479
+ const fetchedAt = new Date(now()).toISOString();
1480
+ st.data = { credentials: result.credentials, fetchedAt };
1481
+ st.result = { state: "ok" };
1482
+ st.lockedOut = false;
1483
+ }
1484
+ } catch (error) {
1485
+ if (!current()) return;
1486
+ const code = error instanceof QuotaError && error.code !== "aborted" ? error.code : error?.code === "aborted" ? null : "network";
1487
+ if (code === null) return;
1488
+ st.result = { state: "error", errorCode: code, ...(error?.status === undefined ? {} : { errorStatus: error.status }) };
1489
+ if (code === "unauthorized" || code === "forbidden") st.lockedOut = true;
1490
+ log("warn", `dsh-cliproxyapi: quota refresh failed for account ${account.id}: ${code}${error?.status === undefined ? "" : ` (${error.status})`}`);
1491
+ } finally {
1492
+ root.signal.removeEventListener("abort", onRootAbort);
1493
+ if (st.gen === gen) {
1494
+ st.inflight = null;
1495
+ if (!disposed) st.lastAttemptAt = now();
1496
+ }
1497
+ }
1498
+ })();
1499
+ st.inflight = { gen, controller, promise };
1500
+ return promise;
1501
+ }
1502
+
1503
+ function cancelInflight(st) {
1504
+ if (st.inflight === null) return;
1505
+ st.inflight.controller.abort();
1506
+ st.inflight = null;
1507
+ st.gen += 1;
1508
+ }
1509
+
1510
+ async function buildSnapshot(prepared) {
1511
+ const accounts = prepared ?? (await prepare());
1512
+ const out = [];
1513
+ for (const { account, keyRef, keyConfigured: configured } of accounts) {
1514
+ const st = stateFor(account.id);
1515
+ const refreshing = st.inflight !== null;
1516
+ let state;
1517
+ if (!configured) state = "nokey";
1518
+ else if (st.result === null || (st.result.state === "nokey")) state = refreshing ? "loading" : "idle";
1519
+ else state = st.result.state;
1520
+ const pickerModels = (Array.isArray(account.pickerModels) ? account.pickerModels : [])
1521
+ .filter((m) => isObj(m) && text(m.id) !== "")
1522
+ .map((m) => ({ id: text(m.id), route: text(m.route), ...(text(m.ownedBy) === "" ? {} : { ownedBy: text(m.ownedBy) }) }));
1523
+ const entry = {
1524
+ accountId: account.id,
1525
+ displayName: text(account.displayName) || account.id,
1526
+ provider: text(account.provider),
1527
+ routes: Array.isArray(account.routes) ? account.routes.filter((r) => typeof r === "string") : [],
1528
+ enabled: account.enabled === true,
1529
+ keyRef,
1530
+ keyConfigured: configured,
1531
+ state,
1532
+ ...(state === "error" ? { errorCode: st.result.errorCode, ...(st.result.errorStatus === undefined ? {} : { errorStatus: st.result.errorStatus }) } : {}),
1533
+ ...(configured && st.data !== null ? { fetchedAt: st.data.fetchedAt } : {}),
1534
+ refreshing,
1535
+ credentials: configured && st.data !== null ? structuredClone(st.data.credentials) : [],
1536
+ pickerModels,
1537
+ };
1538
+ Object.assign(entry, buildModelIndex(entry));
1539
+ out.push(entry);
1540
+ }
1541
+ return {
1542
+ generatedAt: new Date(now()).toISOString(),
1543
+ refreshing: out.some((a) => a.refreshing),
1544
+ accounts: out,
1545
+ };
1546
+ }
1547
+
1548
+ async function prepare() {
1549
+ const accounts = listAccounts();
1550
+ const known = new Set(accounts.map((a) => a.id));
1551
+ for (const [id, st] of states) {
1552
+ if (!known.has(id)) {
1553
+ cancelInflight(st);
1554
+ states.delete(id);
1555
+ }
1556
+ }
1557
+ return Promise.all(accounts.map(async (account) => {
1558
+ const keyRef = managementKeyRef(account.id);
1559
+ return { account, keyRef, keyConfigured: await keyConfigured(keyRef) };
1560
+ }));
1561
+ }
1562
+
1563
+ return {
1564
+ async snapshot() {
1565
+ const prepared = await prepare();
1566
+ if (!disposed) {
1567
+ const at = now();
1568
+ for (const { account, keyConfigured: configured } of prepared) {
1569
+ if (account.enabled !== true || !configured) continue;
1570
+ const st = stateFor(account.id);
1571
+ if (st.inflight !== null || st.lockedOut) continue;
1572
+ if (st.lastAttemptAt !== undefined && at - st.lastAttemptAt < ttlMs) continue;
1573
+ startRefresh(account);
1574
+ }
1575
+ }
1576
+ return buildSnapshot(prepared);
1577
+ },
1578
+
1579
+ /** `options.wait === false` starts the refresh and returns at once (snapshot shows `refreshing`). */
1580
+ async refresh(accountId, options = {}) {
1581
+ const wait = options?.wait !== false;
1582
+ if (accountId !== undefined && accountId !== null && accountId !== "") {
1583
+ const account = findAccount(accountId);
1584
+ if (!disposed) {
1585
+ const st = stateFor(account.id);
1586
+ st.lockedOut = false;
1587
+ if (await keyConfigured(managementKeyRef(account.id))) {
1588
+ const running = startRefresh(account);
1589
+ if (wait) await running;
1590
+ }
1591
+ }
1592
+ return buildSnapshot();
1593
+ }
1594
+ const prepared = await prepare();
1595
+ if (!disposed) {
1596
+ // Refresh-all never retries a locked-out account; only refresh(id) or saveKey clears a lockout.
1597
+ await Promise.all(prepared
1598
+ .filter(({ account, keyConfigured: configured }) => account.enabled === true && configured && !stateFor(account.id).lockedOut)
1599
+ .map(({ account }) => (wait ? startRefresh(account) : (startRefresh(account), undefined))));
1600
+ }
1601
+ return buildSnapshot(prepared);
1602
+ },
1603
+
1604
+ async saveKey(accountId, input = {}, options = {}) {
1605
+ const wait = options?.wait !== false;
1606
+ const account = findAccount(accountId);
1607
+ const ref = managementKeyRef(account.id);
1608
+ const st = stateFor(account.id);
1609
+ if (input?.clear === true) {
1610
+ cancelInflight(st);
1611
+ await deps.unsetKey(ref);
1612
+ st.data = null;
1613
+ st.result = null;
1614
+ st.lockedOut = false;
1615
+ st.lastAttemptAt = undefined;
1616
+ return buildSnapshot();
1617
+ }
1618
+ const value = typeof input?.managementKey === "string" ? input.managementKey.trim() : "";
1619
+ if (value !== "") {
1620
+ if (/\s/.test(value)) throw new Error("dsh-cliproxyapi: management key must not contain whitespace");
1621
+ cancelInflight(st);
1622
+ await deps.setKey(ref, value);
1623
+ st.result = null;
1624
+ st.lockedOut = false;
1625
+ }
1626
+ if (!disposed && (await keyConfigured(ref))) {
1627
+ st.lockedOut = false;
1628
+ const running = startRefresh(account);
1629
+ if (wait) await running;
1630
+ }
1631
+ return buildSnapshot();
1632
+ },
1633
+
1634
+ dispose() {
1635
+ if (disposed) return;
1636
+ disposed = true;
1637
+ root.abort();
1638
+ for (const st of states.values()) {
1639
+ st.inflight?.controller.abort();
1640
+ st.inflight = null;
1641
+ }
1642
+ },
1643
+ };
1644
+ }
1645
+
1646
+ //#endregion