dsh-agy-link 0.4.16 → 0.4.19
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/CHANGELOG.md +24 -0
- package/dist/client.js +3 -3
- package/dist/index.js +66 -17
- package/package.json +1 -1
package/CHANGELOG.md
CHANGED
|
@@ -1,5 +1,29 @@
|
|
|
1
1
|
# Changelog
|
|
2
2
|
|
|
3
|
+
## 0.4.19 (2026-08-26)
|
|
4
|
+
|
|
5
|
+
- **Fixed: Antigravity Models Missing From the Model Picker (登录成功、额度正常但模型列表为空 — issue #1).**
|
|
6
|
+
- **Root cause**: when `agy models` lists a bare Gemini base alongside its effort variants (the agy 1.1.13 output shape, e.g. `gemini-3.7-flash` + `gemini-3.7-flash-medium`), `foldEfforts` emitted the base id TWICE — once as the folded base entry and once as a verbatim row. DSH's `llm.listModels` contract throws `INVALID_CATALOG` on any duplicate model id, and the host's model-catalog builder then drops the ENTIRE Antigravity provider group from the picker — so login and quota panels looked perfectly healthy while no Antigravity model could be selected.
|
|
7
|
+
- **Fix (three layers)**: (1) `foldEfforts` now absorbs the bare base into its folded entry instead of duplicating it; (2) `parseModelsOutput` dedupes repeated raw slugs (first occurrence wins); (3) `AgyAdapter.listModels` dedupes ids as a final guard, so even a user-configured `fallbackModels` list containing repeats can never nuke the whole group.
|
|
8
|
+
- Regression tests pin both bare-base-plus-variants shapes and the duplicate-slug parse, plus an adapter-level uniqueness guard test.
|
|
9
|
+
- **Observability**: when the adapter-level guard drops duplicate ids it now logs which ids were removed (`model catalog contained duplicate ids [...]`) so field instances surface in DSH server logs instead of being silently masked. When reporting picker issues, attach the `/agy doctor` report (`~/.dsh/agy-link/diagnostics/doctor-*.md`) and the raw `agy models` output.
|
|
10
|
+
|
|
11
|
+
## 0.4.18 (2026-08-25)
|
|
12
|
+
|
|
13
|
+
- **Quota Fallback Never Clobbers Good Data (5h=100%/weekly-missing 根因).**
|
|
14
|
+
- **What happened**: `retrieveUserQuotaSummary` transiently failed (proxy blip) while `fetchAvailableModels` still answered; the per-model fallback then OVERWROTE the stored family entry with a single-window partial shape — weeklyFraction vanished and the 5h row received wrong-window numbers (observed 5h=100%, reset a week out, weekly `—`, while the live API actually reported 5h 84% / weekly 47%).
|
|
15
|
+
- **`mergeFallbackFamilyQuota`**: last-known-good complete family data now always wins over partial fallback; the fallback only fills families with no usable previous entry (first-ever refresh). Verified live: both endpoints answer correctly and a successful summary refresh fully restores the display.
|
|
16
|
+
- Added `scripts/diag-quota.mts` one-shot endpoint probe for future incidents.
|
|
17
|
+
|
|
18
|
+
## 0.4.17 (2026-08-25)
|
|
19
|
+
|
|
20
|
+
- **Ghost-Cooldown & Quota-Display Fix (额度显示 0% 根因).**
|
|
21
|
+
- **What happened**: the UI showed 5h quota as 0% while `agy` reported 98% — the parsed quota data was CORRECT all along, but (a) any active local cooldown forced the 5h bar to render 0%, and (b) the loose rate-limit classifier kept creating ghost cooldowns: it scanned the ENTIRE stdout (model prose mentioning "rate limit"/"quota", hash fragments containing "429") and matched bare keywords, so an unrelated tool/permission error froze a healthy account out of rotation with a 15-minute+ cooldown (captured real reason: `rate limit reached: declaring permissions: cortex tool write_to_file … invalid tool call error`).
|
|
22
|
+
- **Hard vs soft classification**: new `looksLikeHardRateLimit` (RESOURCE_EXHAUSTED / code·status·HTTP 429 / too many requests / individual quota reached / quota exceeded·reached·exhausted / rate limit exceeded·reached·hit) is the ONLY pattern allowed to put an account into cooldown; soft signals (model overloaded / high traffic) still shape the error message but never cool accounts.
|
|
23
|
+
- **Scan scope narrowed**: error classification reads stderr + the result envelope's error field only — stdout (event JSON + model prose) no longer participates.
|
|
24
|
+
- **Honest quota bars**: a local cooldown no longer overwrites the server-reported fraction with 0%; it now appends a `· 本地冷却中` note next to the reset time instead.
|
|
25
|
+
- Regression tests pin the exact incident text (cortex tool permission error) as a non-rate-limit fixture.
|
|
26
|
+
|
|
3
27
|
## 0.4.16 (2026-08-24)
|
|
4
28
|
|
|
5
29
|
- **External Re-Login Sync (换号自动/手动同步).**
|
package/dist/client.js
CHANGED
|
@@ -963,8 +963,8 @@ body.dark,
|
|
|
963
963
|
const info = acc.quotas[familyKey];
|
|
964
964
|
const cd = acc.cooldowns[familyKey];
|
|
965
965
|
const inCooldown = cd && cd.cooldownUntil > Date.now();
|
|
966
|
-
|
|
967
|
-
|
|
966
|
+
const pct5h = typeof info?.remainingFraction === "number" && Number.isFinite(info.remainingFraction) ? Math.max(0, Math.min(100, Math.round(info.remainingFraction * 100))) : -1;
|
|
967
|
+
const cdNote = inCooldown ? " · 本地冷却中" : "";
|
|
968
968
|
const w5h = formatQuotaWindow(info?.resetTime);
|
|
969
969
|
const pctWeekly = typeof info?.weeklyFraction === "number" && Number.isFinite(info.weeklyFraction) ? Math.max(0, Math.min(100, Math.round(info.weeklyFraction * 100))) : -1;
|
|
970
970
|
const wWeekly = formatQuotaWindow(info?.weeklyResetTime);
|
|
@@ -1052,7 +1052,7 @@ body.dark,
|
|
|
1052
1052
|
fontSize: "12.5px",
|
|
1053
1053
|
marginBottom: "5px",
|
|
1054
1054
|
color: "var(--agy-text-primary)"
|
|
1055
|
-
} }, brandIcon(FAMILY_BRAND[familyKey], 14), h("span", null, label)), renderLine("5h 额度", pct5h, c5h, w5h.resetText), renderLine("周额度", pctWeekly, cWeekly, wWeekly.resetText));
|
|
1055
|
+
} }, brandIcon(FAMILY_BRAND[familyKey], 14), h("span", null, label)), renderLine("5h 额度", pct5h, c5h, w5h.resetText ? w5h.resetText + cdNote : cdNote.replace(/^ · /, "")), renderLine("周额度", pctWeekly, cWeekly, wWeekly.resetText));
|
|
1056
1056
|
};
|
|
1057
1057
|
const renderedAccountCards = accounts.map((acc) => {
|
|
1058
1058
|
const isPrimary = acc.id === pool?.primaryAccountId;
|
package/dist/index.js
CHANGED
|
@@ -115,9 +115,25 @@ function extractAuthUrl(text) {
|
|
|
115
115
|
if (!m) return void 0;
|
|
116
116
|
return m[0].replace(/[)\]>.,;\x27\x22]+$/, "");
|
|
117
117
|
}
|
|
118
|
+
/**
|
|
119
|
+
* HARD, server-issued rate-limit signatures — the ONLY patterns allowed to
|
|
120
|
+
* put an account into cooldown. Deliberately narrow: bare `429` or
|
|
121
|
+
* `rate limit` matched incidental substrings in the wild (hash/UUID
|
|
122
|
+
* fragments, model prose mentioning quotas, unrelated tool/permission
|
|
123
|
+
* errors quoting such words) and produced ghost cooldowns that froze
|
|
124
|
+
* healthy accounts out of rotation.
|
|
125
|
+
*/
|
|
126
|
+
function looksLikeHardRateLimit(text) {
|
|
127
|
+
if (!text) return false;
|
|
128
|
+
return /RESOURCE_EXHAUSTED|code[ :]?429\b|status[ :]?429\b|HTTP[ :]?429\b|too many requests|individual quota reached|quota (?:exceeded|reached|exhausted)|rate[ -]?limit(?:ed)? (?:exceeded|reached|hit)|exceeded (?:your |the )?quota/i.test(text);
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Soft heuristic adds capacity signals (model overloaded / high traffic).
|
|
132
|
+
* Shapes the user-facing error message only — NEVER cools an account down.
|
|
133
|
+
*/
|
|
118
134
|
function looksLikeRateLimit(text) {
|
|
119
135
|
if (!text) return false;
|
|
120
|
-
return
|
|
136
|
+
return looksLikeHardRateLimit(text) || /model overloaded|experiencing high traffic/i.test(text);
|
|
121
137
|
}
|
|
122
138
|
/**
|
|
123
139
|
* Parse reset duration in milliseconds from rate-limit / quota-exhausted error strings.
|
|
@@ -1058,7 +1074,7 @@ function parseModelsOutput(stdout) {
|
|
|
1058
1074
|
if (text === "") return [];
|
|
1059
1075
|
try {
|
|
1060
1076
|
const list = extractModelList(JSON.parse(text));
|
|
1061
|
-
if (list) return list;
|
|
1077
|
+
if (list) return dedupeBySlug(list);
|
|
1062
1078
|
} catch {}
|
|
1063
1079
|
const out = [];
|
|
1064
1080
|
for (const line of text.split(/\n/)) {
|
|
@@ -1074,6 +1090,17 @@ function parseModelsOutput(stdout) {
|
|
|
1074
1090
|
label: t
|
|
1075
1091
|
});
|
|
1076
1092
|
}
|
|
1093
|
+
return dedupeBySlug(out);
|
|
1094
|
+
}
|
|
1095
|
+
/** First occurrence wins: duplicate raw slugs would become duplicate catalog ids. */
|
|
1096
|
+
function dedupeBySlug(raw) {
|
|
1097
|
+
const seen = /* @__PURE__ */ new Set();
|
|
1098
|
+
const out = [];
|
|
1099
|
+
for (const r of raw) {
|
|
1100
|
+
if (seen.has(r.slug)) continue;
|
|
1101
|
+
seen.add(r.slug);
|
|
1102
|
+
out.push(r);
|
|
1103
|
+
}
|
|
1077
1104
|
return out;
|
|
1078
1105
|
}
|
|
1079
1106
|
function extractModelList(parsed) {
|
|
@@ -1173,7 +1200,7 @@ function foldEfforts(raw) {
|
|
|
1173
1200
|
};
|
|
1174
1201
|
folded.sort((a, b) => rank(a) - rank(b));
|
|
1175
1202
|
verbatim.sort((a, b) => rank(a) - rank(b));
|
|
1176
|
-
return [...folded, ...verbatim];
|
|
1203
|
+
return [...folded, ...verbatim.filter((e) => !bases.has(e.id))];
|
|
1177
1204
|
}
|
|
1178
1205
|
function stripEffortLabel(label, eff) {
|
|
1179
1206
|
const re = new RegExp("\\s*\\(?" + eff + "\\)?\\s*$", "i");
|
|
@@ -2055,12 +2082,25 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
2055
2082
|
}
|
|
2056
2083
|
async listModels(_provider) {
|
|
2057
2084
|
this.deps.catalog.refreshIfNeeded();
|
|
2058
|
-
|
|
2059
|
-
|
|
2060
|
-
|
|
2061
|
-
|
|
2062
|
-
|
|
2063
|
-
|
|
2085
|
+
const cat = this.deps.catalog.get();
|
|
2086
|
+
const seen = /* @__PURE__ */ new Set();
|
|
2087
|
+
const models = [];
|
|
2088
|
+
const dropped = [];
|
|
2089
|
+
for (const m of cat.models) {
|
|
2090
|
+
if (seen.has(m.id)) {
|
|
2091
|
+
dropped.push(m.id);
|
|
2092
|
+
continue;
|
|
2093
|
+
}
|
|
2094
|
+
seen.add(m.id);
|
|
2095
|
+
models.push({
|
|
2096
|
+
provider: PROVIDER_ID,
|
|
2097
|
+
id: m.id,
|
|
2098
|
+
name: m.name,
|
|
2099
|
+
inputModalities: ["text", "image"]
|
|
2100
|
+
});
|
|
2101
|
+
}
|
|
2102
|
+
if (dropped.length > 0) this.warnOnce("catalog-dupes", "model catalog contained duplicate ids [" + dropped.join(", ") + "] — kept first occurrence so DSH does not drop the whole provider group (INVALID_CATALOG)");
|
|
2103
|
+
return models;
|
|
2064
2104
|
}
|
|
2065
2105
|
async resolveModel(_provider, model, _signal) {
|
|
2066
2106
|
const cfg = this.deps.getConfig();
|
|
@@ -2364,11 +2404,8 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
2364
2404
|
const conversationId = streamCid ?? diffed;
|
|
2365
2405
|
const r = rec.getResultEvent();
|
|
2366
2406
|
const consumable = r !== null && (r.ok || r.response !== "");
|
|
2367
|
-
const
|
|
2368
|
-
|
|
2369
|
-
outcome.stdout,
|
|
2370
|
-
parser.stats.lastResultError
|
|
2371
|
-
].filter(Boolean).join(" "));
|
|
2407
|
+
const rawErrText = [outcome.stderrTail, parser.stats.lastResultError].filter(Boolean).join(" ");
|
|
2408
|
+
const isRateLimit = looksLikeRateLimit(rawErrText);
|
|
2372
2409
|
let failure = null;
|
|
2373
2410
|
if (outcome.aborted) failure = {
|
|
2374
2411
|
kind: "aborted",
|
|
@@ -2423,7 +2460,7 @@ var AgyAdapter = class extends LlmAdapter {
|
|
|
2423
2460
|
}
|
|
2424
2461
|
} else {
|
|
2425
2462
|
const effectiveRateLimit = isRateLimit || looksLikeRateLimit(failure.message);
|
|
2426
|
-
if (account &&
|
|
2463
|
+
if (account && looksLikeHardRateLimit(rawErrText)) this.deps.pool?.recordFailure(account.id, family, failure.message);
|
|
2427
2464
|
if (account && (failure.code === Err.AUTH || /invalid_grant|not signed in|auth/i.test(failure.message))) this.deps.pool?.markAuthRequired(account.id, failure.message);
|
|
2428
2465
|
if (!isAux && sessionAccountKey !== "") {
|
|
2429
2466
|
if (failure.code === Err.AUTH || effectiveRateLimit || failure.message && /conversation.*(not found|invalid|not recognized|expired|does not exist)|session.*(expired|invalid)/i.test(failure.message)) this.deps.store.delete(sessionAccountKey);
|
|
@@ -27668,6 +27705,18 @@ var PoolAuthFlow = class {
|
|
|
27668
27705
|
};
|
|
27669
27706
|
//#endregion
|
|
27670
27707
|
//#region src/host/quota.ts
|
|
27708
|
+
/**
|
|
27709
|
+
* When the quota-summary endpoint transiently fails, per-model fallback
|
|
27710
|
+
* data carries a SINGLE window (sometimes the weekly one) and no weekly
|
|
27711
|
+
* fields. Overwriting a previously COMPLETE family entry with that partial
|
|
27712
|
+
* shape dropped weeklyFraction to none and put wrong-window numbers into
|
|
27713
|
+
* the 5h row (observed: 5h=100% / reset a week out / weekly missing).
|
|
27714
|
+
* Rule: last-known-good complete data always wins over partial fallback.
|
|
27715
|
+
*/
|
|
27716
|
+
function mergeFallbackFamilyQuota(prev, fallback) {
|
|
27717
|
+
if (prev && typeof prev.remainingFraction === "number") return prev;
|
|
27718
|
+
return fallback;
|
|
27719
|
+
}
|
|
27671
27720
|
function detectEmailFromAgyLogs(homeDir) {
|
|
27672
27721
|
const logDir = join(homeDir, ".gemini", "antigravity-cli", "log");
|
|
27673
27722
|
if (!existsSync(logDir)) return void 0;
|
|
@@ -27972,11 +28021,11 @@ var QuotaService = class {
|
|
|
27972
28021
|
remainingFraction: remaining,
|
|
27973
28022
|
resetTime
|
|
27974
28023
|
});
|
|
27975
|
-
if (!familyQuotas[fam]) familyQuotas[fam] = {
|
|
28024
|
+
if (!familyQuotas[fam]) familyQuotas[fam] = mergeFallbackFamilyQuota(account.quotas[fam], {
|
|
27976
28025
|
remainingFraction: remaining,
|
|
27977
28026
|
resetTime,
|
|
27978
28027
|
updatedAt: now
|
|
27979
|
-
};
|
|
28028
|
+
});
|
|
27980
28029
|
else if (familyQuotas[fam].remainingFraction === void 0) {
|
|
27981
28030
|
const curRemaining = familyQuotas[fam].remainingFraction ?? 1;
|
|
27982
28031
|
if (remaining < curRemaining) {
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "dsh-agy-link",
|
|
3
|
-
"version": "0.4.
|
|
3
|
+
"version": "0.4.19",
|
|
4
4
|
"description": "Google Antigravity (agy CLI) models for DeepSeek Harness — stream Gemini/Claude/GPT-OSS subscriptions into DSH with thinking, tool activity, token usage and in-GUI Google OAuth login.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|