@hystudio/dsh-quota-pill 0.2.11 → 0.2.13
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/AGENTS.md +7 -7
- package/README.md +20 -8
- package/client.js +29 -5
- package/package.json +1 -1
- package/src/adapters-cliproxy.js +194 -51
package/AGENTS.md
CHANGED
|
@@ -16,10 +16,10 @@ For a fresh DSH Desktop:
|
|
|
16
16
|
|
|
17
17
|
1. If `dsh` is not on PATH: DSH Desktop → Settings → **Manage dsh Command**.
|
|
18
18
|
2. Install a **pinned** build, never the bare name:
|
|
19
|
-
- GUI: **Plugins → Install** → `@hystudio/dsh-quota-pill@0.2.
|
|
20
|
-
- CLI: `dsh plugin add @hystudio/dsh-quota-pill@0.2.
|
|
21
|
-
-
|
|
22
|
-
`github:598934321/dsh-quota-pill#
|
|
19
|
+
- GUI: **Plugins → Install** → `@hystudio/dsh-quota-pill@0.2.12`
|
|
20
|
+
- CLI: `dsh plugin add @hystudio/dsh-quota-pill@0.2.12 --profile desktop`
|
|
21
|
+
- If pnpm's minimumReleaseAge rejects the fresh 0.2.12 release, use the
|
|
22
|
+
branch until it ages: `github:598934321/dsh-quota-pill#master`
|
|
23
23
|
3. Confirm the dependency and the bundle are present:
|
|
24
24
|
|
|
25
25
|
```bash
|
|
@@ -34,7 +34,7 @@ For a fresh DSH Desktop:
|
|
|
34
34
|
|
|
35
35
|
**Why pinned:** pnpm's `minimumReleaseAge` can resolve an unpinned install to the
|
|
36
36
|
broken 0.2.2 (its client half fails to import and the web GUI shows "Failed to
|
|
37
|
-
load plugins"). 0.2.
|
|
37
|
+
load plugins"). 0.2.12 is the first version with all providers, Open Design
|
|
38
38
|
Control Keys, sidecar auto-start, and user-defined adapters.
|
|
39
39
|
|
|
40
40
|
The host half starts the sidecar when nothing is listening on :8319, so a normal
|
|
@@ -77,7 +77,7 @@ DSH account menu, then re-run step 5.
|
|
|
77
77
|
|
|
78
78
|
## 1. Verify the sidecar runs
|
|
79
79
|
|
|
80
|
-
The 0.2.
|
|
80
|
+
The 0.2.12 host plugin starts it automatically when :8319 is free. Give DSH a few
|
|
81
81
|
seconds, then check. If the host half could not start it (or you opted out with
|
|
82
82
|
`QUOTA_PILL_NO_AUTOSTART=1`), run it manually:
|
|
83
83
|
|
|
@@ -105,7 +105,7 @@ CLIProxyAPI". That is expected on a fresh machine — tell the human that
|
|
|
105
105
|
`kimi` / `opencode` / `commandcode` work without it, and ask whether they want
|
|
106
106
|
the OAuth three as well.
|
|
107
107
|
|
|
108
|
-
**Auto-start is the default from 0.2.
|
|
108
|
+
**Auto-start is the default from 0.2.12**; do not add a service unless the human
|
|
109
109
|
asks for one or DSH is headless. If you do add one, set
|
|
110
110
|
`QUOTA_PILL_NO_AUTOSTART=1` so the plugin and the service do not race, and use
|
|
111
111
|
the **absolute** Node path (launchd's PATH does not include nvm/homebrew):
|
package/README.md
CHANGED
|
@@ -71,25 +71,26 @@ in [`src/user-adapters.js`](src/user-adapters.js) and in AGENTS.md §2b.
|
|
|
71
71
|
|
|
72
72
|
**DSH Desktop (recommended).** Open **Plugins → Install**, paste a spec, enable
|
|
73
73
|
the bundle. **Pin the version**: an unpinned install can resolve 0.2.2, which
|
|
74
|
-
breaks web boot. 0.2.
|
|
74
|
+
breaks web boot. 0.2.12 is the first version with every provider, Open Design
|
|
75
75
|
Control Keys, sidecar auto-start, and **user-defined adapters** for providers the
|
|
76
76
|
package does not ship.
|
|
77
77
|
|
|
78
78
|
```text
|
|
79
|
-
@hystudio/dsh-quota-pill@0.2.
|
|
79
|
+
@hystudio/dsh-quota-pill@0.2.12
|
|
80
80
|
```
|
|
81
81
|
|
|
82
|
-
|
|
82
|
+
0.2.12 is published on npm. If pnpm's minimumReleaseAge rejects this fresh
|
|
83
|
+
release, install the branch until it ages:
|
|
83
84
|
|
|
84
85
|
```text
|
|
85
|
-
github:598934321/dsh-quota-pill#
|
|
86
|
+
github:598934321/dsh-quota-pill#master
|
|
86
87
|
```
|
|
87
88
|
|
|
88
89
|
**CLI.** If `dsh` is not on PATH, open DSH Desktop → Settings → **Manage dsh
|
|
89
90
|
Command** first. Then:
|
|
90
91
|
|
|
91
92
|
```bash
|
|
92
|
-
dsh plugin add @hystudio/dsh-quota-pill@0.2.
|
|
93
|
+
dsh plugin add @hystudio/dsh-quota-pill@0.2.12
|
|
93
94
|
```
|
|
94
95
|
|
|
95
96
|
> The npm package is `@hystudio/...` while the repo lives at `598934321/...` —
|
|
@@ -239,9 +240,15 @@ directly, current plans only reveal allowance in `x-ratelimit-*` response header
|
|
|
239
240
|
of a one-token completion, and unified-billing accounts have no ceiling at all
|
|
240
241
|
(reported as `payg`).
|
|
241
242
|
|
|
242
|
-
**Antigravity**
|
|
243
|
-
|
|
244
|
-
|
|
243
|
+
**Antigravity** has three independent quota families, and the pill shows one row
|
|
244
|
+
per family: Gemini models (5-hour + weekly), Gemini 4 Argon (its own weekly
|
|
245
|
+
bucket) and Claude / GPT (5-hour + weekly). The backend decides which catalogue
|
|
246
|
+
and which quota groups it reports from the client label in the `User-Agent`: a
|
|
247
|
+
generic UA gets a legacy answer where every Gemini model is a constant 100% and
|
|
248
|
+
`gemini-4-argon-*` does not exist at all. The adapter therefore sends the
|
|
249
|
+
Antigravity CLI's own UA (`antigravity/cli/<version> (aidev_client; …)`), reads
|
|
250
|
+
`retrieveUserQuotaSummary` and falls back to per-model `fetchAvailableModels`.
|
|
251
|
+
Set `QUOTA_PILL_AGY_UA` to override the UA if a future CLI changes it.
|
|
245
252
|
|
|
246
253
|
**Command Code** does not expose a monthly ceiling. The adapter derives it from
|
|
247
254
|
`monthlyCap = weekly.cap × 2`, an identity verified against two different plans
|
|
@@ -263,6 +270,11 @@ then writes `OPENDESIGN_CONTROL_KEY` and `OPENDESIGN_WORKSPACE_ID` into
|
|
|
263
270
|
`~/.dsh/.credentials.yaml`. The workspace id is optional when using a Control
|
|
264
271
|
Key — it is otherwise resolved from `/workspaces/current`.
|
|
265
272
|
|
|
273
|
+
Windows with a real reset period (5-hour / weekly / monthly) always render as a
|
|
274
|
+
percentage — including the 100% seen right after a reset — so a bucket never
|
|
275
|
+
switches units as it is spent. The credit counts stay in the row's tooltip;
|
|
276
|
+
only credit packs without a period render as credits.
|
|
277
|
+
|
|
266
278
|
The older console-session route (`__Secure-better-auth.session_token` cookie,
|
|
267
279
|
browser-session-only `/billing/coding-plan-usage`) is still supported as a
|
|
268
280
|
fallback; that cookie is HttpOnly, so copy it from devtools → Application →
|
package/client.js
CHANGED
|
@@ -172,7 +172,7 @@ window.__ModuleLoader__.load({
|
|
|
172
172
|
*/
|
|
173
173
|
function adaptationPrompt(model, provider, lang) {
|
|
174
174
|
const m = String(model || '(unknown)');
|
|
175
|
-
const p = String(provider && provider !== 'unknown' ? provider : '
|
|
175
|
+
const p = String(provider && provider !== 'unknown' ? provider : 'unknown');
|
|
176
176
|
if (lang === 'zh') {
|
|
177
177
|
return [
|
|
178
178
|
'你是 DSH quota-pill 的 provider 适配 agent。当前模型 ' + m + ' 的 provider(' + p + ')未被插件识别,读取不到余额。',
|
|
@@ -430,6 +430,12 @@ window.__ModuleLoader__.load({
|
|
|
430
430
|
'7d': ['每周', 'Weekly'],
|
|
431
431
|
monthly: ['每月', 'Monthly'],
|
|
432
432
|
'model-pool': ['模型池', 'Model pool'],
|
|
433
|
+
// Antigravity 的两个独立额度族(Gemini / Claude·Opus)。必须登记成已知
|
|
434
|
+
// 窗口:否则 providerEntries 会认为它们不是额度周期,再补一行汇总,
|
|
435
|
+
// 同一份额度显示两次。
|
|
436
|
+
'agy-gemini': ['Gemini', 'Gemini'],
|
|
437
|
+
'agy-argon': ['Gemini 4', 'Gemini 4'],
|
|
438
|
+
'agy-opus': ['Claude / Opus', 'Claude / Opus'],
|
|
433
439
|
// 积分类的固定三行。放进这里是为了让 isKnownWindow 认得它们——
|
|
434
440
|
// 否则 providerEntries 会再前置一条汇总行,变成四行。
|
|
435
441
|
'credits-all': ['全部积分', 'All credits'],
|
|
@@ -535,6 +541,21 @@ window.__ModuleLoader__.load({
|
|
|
535
541
|
return !!WINDOW_LABEL[String(key).replace(/^USAGE_PERIOD_TYPE_/i, '').toLowerCase()];
|
|
536
542
|
}
|
|
537
543
|
|
|
544
|
+
/**
|
|
545
|
+
* 「周期性额度窗口」= 有真实重置周期的桶(5 小时 / 每周 / 每月)。
|
|
546
|
+
*
|
|
547
|
+
* 这类窗口的百分比永远有信息量:刚重置就是 100%,用掉就是 94%,两种状态
|
|
548
|
+
* 都值得看。它们不能走 pctIsInformative 的「满格就显示积分」规则——那条
|
|
549
|
+
* 规则是为「没有周期的积分包」写的,用在 opendesign 的 5h 桶上会让它在
|
|
550
|
+
* 刚重置时显示「50k 积分」、用掉之后又变回百分比,同一个窗口两种单位。
|
|
551
|
+
*/
|
|
552
|
+
const PERIODIC_WINDOWS = new Set(['5h', 'fivehour', 'rolling', 'weekly', '7d', 'monthly']);
|
|
553
|
+
|
|
554
|
+
function isPeriodicWindow(key) {
|
|
555
|
+
const k = String(key || '').replace(/^USAGE_PERIOD_TYPE_/i, '').toLowerCase();
|
|
556
|
+
return PERIODIC_WINDOWS.has(k) || /^weekly-\d+$/.test(k);
|
|
557
|
+
}
|
|
558
|
+
|
|
538
559
|
/**
|
|
539
560
|
* 免费渠道池的两种汇总口径。它们不是「额度周期」,所以不加「额度剩余」后缀,
|
|
540
561
|
* 否则会读成「最紧窗口额度剩余」这种别扭的说法。
|
|
@@ -641,6 +662,8 @@ window.__ModuleLoader__.load({
|
|
|
641
662
|
pct: Number.isFinite(pct) ? pct : null,
|
|
642
663
|
reset: (w.credits && w.resetAt) ? fmtExpiry(w.resetAt, lang) : fmtResetAt(w.resetAt, lang),
|
|
643
664
|
credits: w.credits || null,
|
|
665
|
+
// 有重置周期的窗口一律优先显示百分比(见 isPeriodicWindow)。
|
|
666
|
+
periodic: isPeriodicWindow(w.key),
|
|
644
667
|
});
|
|
645
668
|
}
|
|
646
669
|
// 积分类用的是固定三行,三个键都算「已知窗口」,所以不会再前置汇总行——
|
|
@@ -1385,7 +1408,7 @@ window.__ModuleLoader__.load({
|
|
|
1385
1408
|
}
|
|
1386
1409
|
|
|
1387
1410
|
function WindowRow(props) {
|
|
1388
|
-
const { label, pct, reset, detail, credits, lang, stale, money: isMoney, compact } = props;
|
|
1411
|
+
const { label, pct, reset, detail, credits, lang, stale, money: isMoney, compact, periodic } = props;
|
|
1389
1412
|
// `detail` is a generic secondary line (a payg bonus, an aggregate hint);
|
|
1390
1413
|
// `reset` is the window-reset countdown. Rows in a normal group reserve
|
|
1391
1414
|
// the line even when empty so their numbers stay aligned inside the group;
|
|
@@ -1402,7 +1425,8 @@ window.__ModuleLoader__.load({
|
|
|
1402
1425
|
// · 百分比恒 100% → 显示积分(这种渠道百分比什么都说明不了)
|
|
1403
1426
|
// 判据见 pctIsInformative;两种方式都保留,只是不同渠道各取其一。
|
|
1404
1427
|
const hasCredits = credits && typeof credits.remain === 'number';
|
|
1405
|
-
|
|
1428
|
+
// 周期窗口:百分比永远是主读数;积分只在 tooltip 里作为补充。
|
|
1429
|
+
const showPct = isNum && (periodic || pctIsInformative(pct, credits));
|
|
1406
1430
|
const showCredits = hasCredits && !showPct;
|
|
1407
1431
|
// data-stale:这份读数是「该 provider 上次的值」,正在后台刷新。
|
|
1408
1432
|
// 视觉上降饱和 + 缓慢呼吸,数据到了再撤掉(见 CSS)。
|
|
@@ -1458,7 +1482,7 @@ window.__ModuleLoader__.load({
|
|
|
1458
1482
|
h('div', { className: 'quota-pill-group-head' }, name),
|
|
1459
1483
|
// 组内行 key 带组名,避免不同 provider 的同名窗口(都是 5h/weekly)
|
|
1460
1484
|
// 被 React 当成同一个组件实例,导致 tween 跨 provider 插值。
|
|
1461
|
-
entries.map((e) => h(WindowRow, { key: `${name}|${e.key}`, label: e.label, pct: e.pct, reset: e.reset, detail: e.detail, compact: e.compact, credits: e.credits, lang, stale, money: e.money })),
|
|
1485
|
+
entries.map((e) => h(WindowRow, { key: `${name}|${e.key}`, label: e.label, pct: e.pct, reset: e.reset, detail: e.detail, compact: e.compact, credits: e.credits, periodic: e.periodic, lang, stale, money: e.money })),
|
|
1462
1486
|
);
|
|
1463
1487
|
}
|
|
1464
1488
|
|
|
@@ -1533,7 +1557,7 @@ window.__ModuleLoader__.load({
|
|
|
1533
1557
|
// key 带 provider:换 provider 时强制重挂载,让 tween 从「空」开始,
|
|
1534
1558
|
// 而不是把 A 的 5h 平滑滑到 B 的 5h——那是两个不同账户的额度,
|
|
1535
1559
|
// 中间的数字不代表任何真实状态。
|
|
1536
|
-
entries.map((e) => h(WindowRow, { key: `${providerName}|${e.key}`, label: e.label, pct: e.pct, reset: e.reset, credits: e.credits, lang, stale, money: e.money })),
|
|
1560
|
+
entries.map((e) => h(WindowRow, { key: `${providerName}|${e.key}`, label: e.label, pct: e.pct, reset: e.reset, credits: e.credits, periodic: e.periodic, lang, stale, money: e.money })),
|
|
1537
1561
|
),
|
|
1538
1562
|
h('dl', { className: 'quota-pill-stats' },
|
|
1539
1563
|
data.account ? statRow(pick(lang, 'account'), String(data.account)) : null,
|
package/package.json
CHANGED
package/src/adapters-cliproxy.js
CHANGED
|
@@ -422,67 +422,210 @@ export async function codex() {
|
|
|
422
422
|
|
|
423
423
|
/* --------------------------------------------------------------------- agy */
|
|
424
424
|
/**
|
|
425
|
-
* Antigravity (Google Cloud Code)
|
|
425
|
+
* Antigravity (Google Cloud Code) reports THREE independent quota families,
|
|
426
|
+
* each with its own limits:
|
|
426
427
|
*
|
|
427
|
-
*
|
|
428
|
-
*
|
|
429
|
-
*
|
|
430
|
-
*
|
|
431
|
-
*
|
|
428
|
+
* - Gemini models - 5-hour + weekly buckets ("Gemini Models" group)
|
|
429
|
+
* - Gemini 4 Argon - its own weekly bucket ("Gemini 4 Argon" group)
|
|
430
|
+
* - Claude / GPT - 5-hour + weekly buckets ("Claude and GPT models")
|
|
431
|
+
*
|
|
432
|
+
* The backend decides which catalogue AND which quota groups it reports from
|
|
433
|
+
* the client label in the User-Agent. With a generic UA ("antigravity/1.0") it
|
|
434
|
+
* serves a legacy catalogue: no gemini-4-argon-*, and every Gemini model comes
|
|
435
|
+
* back with remainingFraction 1 and a resetTime that merely tracks "now + 5h"
|
|
436
|
+
* - a permanent 100% that hides real usage. The Antigravity CLI's own UA
|
|
437
|
+
* ("antigravity/cli/<version> (aidev_client; ...)") gets the real catalogue and
|
|
438
|
+
* the real groups. Verified against the CLI's captured traffic on 2026-10-26
|
|
439
|
+
* (same token: generic UA -> 23 models, no argon, no Gemini group; CLI UA ->
|
|
440
|
+
* 29 models, argon at ~91%, three groups).
|
|
441
|
+
*
|
|
442
|
+
* Preferred source is retrieveUserQuotaSummary: one request, one group per
|
|
443
|
+
* family, each with its buckets. Per-model fetchAvailableModels stays as a
|
|
444
|
+
* fallback if that RPC changes shape.
|
|
445
|
+
*/
|
|
446
|
+
const AGY_CLI_UA = process.env.QUOTA_PILL_AGY_UA
|
|
447
|
+
|| 'antigravity/cli/1.4.7 (aidev_client; os_type=darwin; arch=arm64; cl=1006008713; auth_method=consumer)';
|
|
448
|
+
const AGY_PROJECT = { project: 'aicode-consumers' };
|
|
449
|
+
|
|
450
|
+
/**
|
|
451
|
+
* One entry per quota family the pill shows. The key is the window key the
|
|
452
|
+
* client renders; keep it stable - the label table and the spend history are
|
|
453
|
+
* keyed by it.
|
|
454
|
+
*/
|
|
455
|
+
const AGY_FAMILIES = [
|
|
456
|
+
{ key: 'agy-gemini', label: 'Gemini' },
|
|
457
|
+
{ key: 'agy-argon', label: 'Gemini 4' },
|
|
458
|
+
{ key: 'agy-opus', label: 'Claude / Opus' },
|
|
459
|
+
];
|
|
460
|
+
|
|
461
|
+
/** Family a model id belongs to (decides which number is the headline one). */
|
|
462
|
+
function agyFamilyForModel(model) {
|
|
463
|
+
// Model ids arrive bare from the all page and as provider/model on /quota.
|
|
464
|
+
const id = String(model || '').split('/').pop();
|
|
465
|
+
if (/argon/i.test(id)) return 'agy-argon';
|
|
466
|
+
if (/^(claude|gpt)/i.test(id)) return 'agy-opus';
|
|
467
|
+
if (/gemini/i.test(id)) return 'agy-gemini';
|
|
468
|
+
return null;
|
|
469
|
+
}
|
|
470
|
+
|
|
471
|
+
/** Family a quota-summary group belongs to. */
|
|
472
|
+
function agyFamilyForGroup(name) {
|
|
473
|
+
const n = String(name || '');
|
|
474
|
+
if (/argon/i.test(n)) return 'agy-argon';
|
|
475
|
+
if (/claude|gpt/i.test(n)) return 'agy-opus';
|
|
476
|
+
if (/gemini/i.test(n)) return 'agy-gemini';
|
|
477
|
+
return null;
|
|
478
|
+
}
|
|
479
|
+
|
|
480
|
+
/**
|
|
481
|
+
* Collapse quota-summary groups into one reading per family. The tightest
|
|
482
|
+
* bucket decides the percentage (a 5-hour bucket at 0 blocks even when the
|
|
483
|
+
* weekly one still has 46%), and that bucket's reset is what the user can act
|
|
484
|
+
* on.
|
|
485
|
+
*/
|
|
486
|
+
function agyFamiliesFromSummary(data) {
|
|
487
|
+
const out = new Map();
|
|
488
|
+
for (const group of (data && data.groups) || []) {
|
|
489
|
+
const key = agyFamilyForGroup(group && group.displayName);
|
|
490
|
+
if (!key) continue;
|
|
491
|
+
const fam = AGY_FAMILIES.find((f) => f.key === key);
|
|
492
|
+
let frac = null;
|
|
493
|
+
let resetAt = null;
|
|
494
|
+
const buckets = [];
|
|
495
|
+
for (const b of (group && group.buckets) || []) {
|
|
496
|
+
const v = b && b.remainingFraction;
|
|
497
|
+
if (typeof v !== 'number') continue;
|
|
498
|
+
buckets.push({ id: b.bucketId || null, window: b.window || null, frac: v, resetAt: b.resetTime || null });
|
|
499
|
+
if (frac === null || v < frac) {
|
|
500
|
+
frac = v;
|
|
501
|
+
resetAt = b.resetTime || resetAt;
|
|
502
|
+
}
|
|
503
|
+
}
|
|
504
|
+
if (frac === null) continue;
|
|
505
|
+
out.set(key, { key, label: fam.label, frac, resetAt, buckets });
|
|
506
|
+
}
|
|
507
|
+
return out;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
/** Fallback: collapse per-model buckets into one reading per family. */
|
|
511
|
+
function agyFamiliesFromModels(models) {
|
|
512
|
+
const out = new Map();
|
|
513
|
+
for (const [mid, info] of Object.entries(models || {})) {
|
|
514
|
+
const key = agyFamilyForModel(mid);
|
|
515
|
+
if (!key) continue;
|
|
516
|
+
const q = (info && info.quotaInfo) || {};
|
|
517
|
+
const frac = q.remainingFraction;
|
|
518
|
+
if (typeof frac !== 'number') continue;
|
|
519
|
+
const fam = AGY_FAMILIES.find((f) => f.key === key);
|
|
520
|
+
const cur = out.get(key) || { key, label: fam.label, frac: 1, resetAt: null, buckets: [] };
|
|
521
|
+
cur.buckets.push({ id: mid, window: null, frac, resetAt: q.resetTime || null });
|
|
522
|
+
if (frac < cur.frac) {
|
|
523
|
+
cur.frac = frac;
|
|
524
|
+
cur.resetAt = q.resetTime || cur.resetAt;
|
|
525
|
+
} else if (!cur.resetAt && q.resetTime) {
|
|
526
|
+
cur.resetAt = q.resetTime;
|
|
527
|
+
}
|
|
528
|
+
out.set(key, cur);
|
|
529
|
+
}
|
|
530
|
+
return out;
|
|
531
|
+
}
|
|
532
|
+
|
|
533
|
+
/** Shape one adapter payload from the families that were actually reported. */
|
|
534
|
+
function agyPayload(cred, fams, model, source) {
|
|
535
|
+
const wanted = agyFamilyForModel(model);
|
|
536
|
+
// Headline: the family the current model uses. Without a model (the all
|
|
537
|
+
// page) Gemini leads, which is also the number quota failover tools compare
|
|
538
|
+
// against - do not change that silently.
|
|
539
|
+
const primary = (wanted && fams.get(wanted))
|
|
540
|
+
|| fams.get('agy-gemini')
|
|
541
|
+
|| fams.values().next().value;
|
|
542
|
+
const remaining = round2(primary.frac * 100);
|
|
543
|
+
const windows = AGY_FAMILIES
|
|
544
|
+
.filter((f) => fams.has(f.key))
|
|
545
|
+
.map((f) => {
|
|
546
|
+
const v = fams.get(f.key);
|
|
547
|
+
return { key: v.key, remainPct: round2(v.frac * 100), resetAt: v.resetAt || null };
|
|
548
|
+
});
|
|
549
|
+
return {
|
|
550
|
+
kind: 'subscription-quota',
|
|
551
|
+
provider: 'agy',
|
|
552
|
+
account: cred.account || cred.name,
|
|
553
|
+
remainingPct: remaining,
|
|
554
|
+
usedPct: round2(100 - remaining),
|
|
555
|
+
window: primary.key,
|
|
556
|
+
resetAt: primary.resetAt || null,
|
|
557
|
+
windows,
|
|
558
|
+
raw: {
|
|
559
|
+
source,
|
|
560
|
+
families: Object.fromEntries([...fams].map(([k, v]) => [k, {
|
|
561
|
+
remainPct: round2(v.frac * 100),
|
|
562
|
+
resetAt: v.resetAt || null,
|
|
563
|
+
buckets: v.buckets,
|
|
564
|
+
}])),
|
|
565
|
+
},
|
|
566
|
+
};
|
|
567
|
+
}
|
|
568
|
+
|
|
569
|
+
/**
|
|
570
|
+
* Read one Antigravity login. Both endpoints need the CLI User-Agent - see the
|
|
571
|
+
* comment above: without it the backend answers from a legacy catalogue whose
|
|
572
|
+
* Gemini quota is a permanent 100%.
|
|
432
573
|
*/
|
|
433
|
-
|
|
574
|
+
async function agyReading(cred, model, errors) {
|
|
575
|
+
const post = (url) => curlJson(url, {
|
|
576
|
+
method: 'POST',
|
|
577
|
+
// googleapis is unreachable directly on some networks; CLIProxyAPI users
|
|
578
|
+
// almost always have a local proxy configured.
|
|
579
|
+
proxy: process.env.QUOTA_PILL_PROXY || defaultProxy(),
|
|
580
|
+
token: cred.token,
|
|
581
|
+
body: AGY_PROJECT,
|
|
582
|
+
headers: { 'Content-Type': 'application/json', 'User-Agent': AGY_CLI_UA },
|
|
583
|
+
timeoutMs: 25000,
|
|
584
|
+
});
|
|
585
|
+
|
|
586
|
+
const summary = await post('https://cloudcode-pa.googleapis.com/v1internal:retrieveUserQuotaSummary');
|
|
587
|
+
if (summary && summary.ok) {
|
|
588
|
+
const fams = agyFamiliesFromSummary(summary.data);
|
|
589
|
+
if (fams.size) return agyPayload(cred, fams, model, 'v1internal:retrieveUserQuotaSummary');
|
|
590
|
+
errors.push(cred.name + ': retrieveUserQuotaSummary had no known groups');
|
|
591
|
+
} else {
|
|
592
|
+
errors.push(cred.name + ': retrieveUserQuotaSummary ' + ((summary && summary.error) || 'request failed'));
|
|
593
|
+
}
|
|
594
|
+
|
|
595
|
+
const available = await post('https://cloudcode-pa.googleapis.com/v1internal:fetchAvailableModels');
|
|
596
|
+
if (!available || !available.ok) {
|
|
597
|
+
errors.push(cred.name + ': ' + ((available && available.error) || 'request failed'));
|
|
598
|
+
return null;
|
|
599
|
+
}
|
|
600
|
+
const fams = agyFamiliesFromModels((available.data && available.data.models) || {});
|
|
601
|
+
if (!fams.size) {
|
|
602
|
+
errors.push(cred.name + ': fetchAvailableModels had no quotaInfo');
|
|
603
|
+
return null;
|
|
604
|
+
}
|
|
605
|
+
return agyPayload(cred, fams, model, 'v1internal:fetchAvailableModels');
|
|
606
|
+
}
|
|
607
|
+
|
|
608
|
+
export async function agy(model) {
|
|
434
609
|
const usable = usableFiles('antigravity');
|
|
435
610
|
if (!usable.length) return noCredential('agy', 'antigravity');
|
|
436
611
|
|
|
437
612
|
const errors = [];
|
|
438
613
|
let best = null;
|
|
439
|
-
|
|
440
614
|
for (const cred of usable) {
|
|
441
|
-
const
|
|
442
|
-
|
|
443
|
-
|
|
444
|
-
|
|
445
|
-
|
|
446
|
-
|
|
447
|
-
proxy: process.env.QUOTA_PILL_PROXY || defaultProxy(),
|
|
448
|
-
token: cred.token,
|
|
449
|
-
body: {},
|
|
450
|
-
headers: { 'Content-Type': 'application/json', 'User-Agent': 'antigravity/1.0' },
|
|
451
|
-
timeoutMs: 25000,
|
|
452
|
-
},
|
|
453
|
-
);
|
|
454
|
-
if (!res || !res.ok) {
|
|
455
|
-
errors.push(`${cred.name}: ${(res && res.error) || 'request failed'}`);
|
|
456
|
-
continue;
|
|
457
|
-
}
|
|
458
|
-
const models = (res.data && res.data.models) || {};
|
|
459
|
-
const fractions = [];
|
|
460
|
-
for (const [mid, info] of Object.entries(models)) {
|
|
461
|
-
const q = (info && info.quotaInfo) || {};
|
|
462
|
-
const frac = q.remainingFraction;
|
|
463
|
-
if (typeof frac === 'number') fractions.push([mid, frac, q.resetTime]);
|
|
464
|
-
}
|
|
465
|
-
if (!fractions.length) {
|
|
466
|
-
errors.push(cred.name + ': no quotaInfo');
|
|
467
|
-
continue;
|
|
468
|
-
}
|
|
469
|
-
fractions.sort((a, b) => a[1] - b[1]);
|
|
470
|
-
const [sampleModel, frac, reset] = fractions[0];
|
|
471
|
-
const remaining = round2(frac * 100);
|
|
472
|
-
const candidate = {
|
|
473
|
-
kind: 'subscription-quota',
|
|
474
|
-
provider: 'agy',
|
|
475
|
-
account: cred.account || cred.name,
|
|
476
|
-
remainingPct: remaining,
|
|
477
|
-
usedPct: round2(100 - remaining),
|
|
478
|
-
window: 'model-pool',
|
|
479
|
-
resetAt: reset || null,
|
|
480
|
-
raw: { accounts: usable.length, poolModels: fractions.length, sampleModel },
|
|
481
|
-
};
|
|
482
|
-
if (!best || candidate.remainingPct > best.remainingPct) best = candidate;
|
|
615
|
+
const reading = await agyReading(cred, model, errors);
|
|
616
|
+
if (!reading) continue;
|
|
617
|
+
// Several antigravity logins: report the one with the most headroom in its
|
|
618
|
+
// tightest family, so a half-spent account cannot mask a full one.
|
|
619
|
+
const tightest = Math.min(...reading.windows.map((w) => w.remainPct));
|
|
620
|
+
if (!best || tightest > best.tightest) best = { reading, tightest };
|
|
483
621
|
}
|
|
484
622
|
|
|
485
|
-
if (best)
|
|
623
|
+
if (best) {
|
|
624
|
+
if (usable.length > 1) {
|
|
625
|
+
best.reading.raw = { ...best.reading.raw, accounts: usable.length };
|
|
626
|
+
}
|
|
627
|
+
return best.reading;
|
|
628
|
+
}
|
|
486
629
|
return { kind: 'probe-failed', provider: 'agy', error: errors.join('; ') || 'no quota' };
|
|
487
630
|
}
|
|
488
631
|
|