From 16515bed1f251a2546d518db6af12c472a0cb4de Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 8 Feb 2026 17:33:04 +0800 Subject: [PATCH 1/2] Fix header-style selection for gemini-cli requests --- src/plugin.ts | 13 ++++--- src/plugin/accounts.test.ts | 77 +++++++++++++++++++++++++++++++++++++ src/plugin/accounts.ts | 37 ++++++++++-------- 3 files changed, 106 insertions(+), 21 deletions(-) diff --git a/src/plugin.ts b/src/plugin.ts index 04ca3c06..64be9385 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1107,6 +1107,8 @@ export const createAntigravityPlugin = (providerId: string) => async ( while (true) { // Check for abort at the start of each iteration checkAborted(); + + const requestedHeaderStyle = getHeaderStyleFromUrl(urlString, family); const accountCount = accountManager.getAccountCount(); @@ -1123,7 +1125,7 @@ export const createAntigravityPlugin = (providerId: string) => async ( family, model, config.account_selection_strategy, - 'antigravity', + requestedHeaderStyle, config.pid_offset_enabled, config.soft_quota_threshold_percent, softQuotaCacheTtlMs, @@ -1160,7 +1162,7 @@ export const createAntigravityPlugin = (providerId: string) => async ( continue; } - const headerStyle = getHeaderStyleFromUrl(urlString, family); + const headerStyle = requestedHeaderStyle; const explicitQuota = isExplicitQuotaFromUrl(urlString); // All accounts are rate-limited - wait and retry const waitMs = accountManager.getMinWaitTimeForFamily( @@ -1411,9 +1413,10 @@ export const createAntigravityPlugin = (providerId: string) => async ( let shouldSwitchAccount = false; // Determine header style from model suffix: - // - Gemini models default to Antigravity - // - Claude models always use Antigravity - let headerStyle = getHeaderStyleFromUrl(urlString, family); + // - Models with :antigravity suffix -> use Antigravity quota + // - Models without suffix (default) -> use Gemini CLI quota + // - Claude models -> always use Antigravity + let headerStyle = requestedHeaderStyle; const explicitQuota = isExplicitQuotaFromUrl(urlString); const cliFirst = getCliFirst(config); pushDebug(`headerStyle=${headerStyle} explicit=${explicitQuota}`); diff --git a/src/plugin/accounts.test.ts b/src/plugin/accounts.test.ts index 9550cb0f..7cb841ac 100644 --- a/src/plugin/accounts.test.ts +++ b/src/plugin/accounts.test.ts @@ -1056,6 +1056,51 @@ describe("AccountManager", () => { }); }); + describe("Issue #400: hybrid strategy respects requested header style", () => { + it("skips account when requested antigravity style is rate-limited", () => { + const stored: AccountStorageV3 = { + version: 3, + accounts: [ + { refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 }, + { refreshToken: "r2", projectId: "p2", addedAt: 1, lastUsed: 0 }, + ], + activeIndex: 0, + activeIndexByFamily: { claude: 0, gemini: 0 }, + }; + + const manager = new AccountManager(undefined, stored); + const firstAccount = manager.getCurrentOrNextForFamily("gemini", null, "hybrid", "antigravity"); + + manager.markRateLimited(firstAccount!, 60000, "gemini", "antigravity"); + + expect(manager.isRateLimitedForHeaderStyle(firstAccount!, "gemini", "antigravity")).toBe(true); + expect(manager.isRateLimitedForHeaderStyle(firstAccount!, "gemini", "gemini-cli")).toBe(false); + + const nextAccount = manager.getCurrentOrNextForFamily("gemini", null, "hybrid", "antigravity"); + expect(nextAccount?.index).toBe(1); + }); + + it("keeps account for gemini-cli when only antigravity style is rate-limited", () => { + const stored: AccountStorageV3 = { + version: 3, + accounts: [ + { refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 }, + ], + activeIndex: 0, + activeIndexByFamily: { claude: 0, gemini: 0 }, + }; + + const manager = new AccountManager(undefined, stored); + const firstAccount = manager.getCurrentOrNextForFamily("gemini", null, "hybrid", "gemini-cli"); + + manager.markRateLimited(firstAccount!, 60000, "gemini", "antigravity"); + + const nextAccount = manager.getCurrentOrNextForFamily("gemini", null, "hybrid", "gemini-cli"); + expect(nextAccount).not.toBeNull(); + expect(nextAccount?.parts.refreshToken).toBe("r1"); + }); + }); + describe("Issue #174: saveToDisk throttling", () => { it("requestSaveToDisk coalesces multiple calls into one write", async () => { vi.useFakeTimers(); @@ -1608,6 +1653,38 @@ describe("AccountManager", () => { expect(account?.parts.refreshToken).toBe("r1"); }); + it("does not apply soft quota threshold to gemini-cli in sticky mode", () => { + const stored: AccountStorageV3 = { + version: 3, + accounts: [ + { refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 }, + ], + activeIndex: 0, + }; + + const manager = new AccountManager(undefined, stored); + manager.updateQuotaCache(0, { "gemini-pro": { remainingFraction: 0.01, modelCount: 1 } }); + + const account = manager.getCurrentOrNextForFamily("gemini", "gemini-2.5-pro", "sticky", "gemini-cli", false, 90); + expect(account?.parts.refreshToken).toBe("r1"); + }); + + it("does not apply soft quota threshold to gemini-cli in hybrid mode", () => { + const stored: AccountStorageV3 = { + version: 3, + accounts: [ + { refreshToken: "r1", projectId: "p1", addedAt: 1, lastUsed: 0 }, + ], + activeIndex: 0, + }; + + const manager = new AccountManager(undefined, stored); + manager.updateQuotaCache(0, { "gemini-pro": { remainingFraction: 0.01, modelCount: 1 } }); + + const account = manager.getCurrentOrNextForFamily("gemini", "gemini-2.5-pro", "hybrid", "gemini-cli", false, 90); + expect(account?.parts.refreshToken).toBe("r1"); + }); + it("returns null when all accounts over threshold", () => { const stored: AccountStorageV3 = { version: 3, diff --git a/src/plugin/accounts.ts b/src/plugin/accounts.ts index bcdd07f1..d71cafe2 100644 --- a/src/plugin/accounts.ts +++ b/src/plugin/accounts.ts @@ -516,20 +516,22 @@ export class AccountManager { if (strategy === 'hybrid') { const healthTracker = getHealthTracker(); const tokenTracker = getTokenTracker(); - - const accountsWithMetrics: AccountWithMetrics[] = this.accounts - .filter(acc => acc.enabled !== false) - .map(acc => { - clearExpiredRateLimits(acc); - return { - index: acc.index, - lastUsed: acc.lastUsed, - healthScore: healthTracker.getScore(acc.index), - isRateLimited: isRateLimitedForFamily(acc, family, model) || - isOverSoftQuotaThreshold(acc, family, softQuotaThresholdPercent, softQuotaCacheTtlMs, model), - isCoolingDown: this.isAccountCoolingDown(acc), - }; - }); + + const shouldApplySoftQuota = headerStyle !== "gemini-cli"; + const accountsWithMetrics: AccountWithMetrics[] = this.accounts.map(acc => { + clearExpiredRateLimits(acc); + const isOverThreshold = shouldApplySoftQuota && + isOverSoftQuotaThreshold(acc, family, softQuotaThresholdPercent, softQuotaCacheTtlMs, model); + return { + index: acc.index, + lastUsed: acc.lastUsed, + healthScore: healthTracker.getScore(acc.index), + isRateLimited: acc.enabled === false || + isRateLimitedForHeaderStyle(acc, family, headerStyle, model) || + isOverThreshold, + isCoolingDown: this.isAccountCoolingDown(acc), + }; + }); // Get current account index for stickiness const currentIndex = this.currentAccountIndexByFamily[family] ?? null; @@ -564,7 +566,8 @@ export class AccountManager { if (current) { clearExpiredRateLimits(current); const isLimitedForRequestedStyle = isRateLimitedForHeaderStyle(current, family, headerStyle, model); - const isOverThreshold = isOverSoftQuotaThreshold(current, family, softQuotaThresholdPercent, softQuotaCacheTtlMs, model); + const isOverThreshold = headerStyle !== "gemini-cli" && + isOverSoftQuotaThreshold(current, family, softQuotaThresholdPercent, softQuotaCacheTtlMs, model); if (!isLimitedForRequestedStyle && !isOverThreshold && !this.isAccountCoolingDown(current)) { this.markTouchedForQuota(current, quotaKey); return current; @@ -582,9 +585,11 @@ export class AccountManager { getNextForFamily(family: ModelFamily, model?: string | null, headerStyle: HeaderStyle = "antigravity", softQuotaThresholdPercent: number = 100, softQuotaCacheTtlMs: number = 10 * 60 * 1000): ManagedAccount | null { const available = this.accounts.filter((a) => { clearExpiredRateLimits(a); + const isOverThreshold = headerStyle !== "gemini-cli" && + isOverSoftQuotaThreshold(a, family, softQuotaThresholdPercent, softQuotaCacheTtlMs, model); return a.enabled !== false && !isRateLimitedForHeaderStyle(a, family, headerStyle, model) && - !isOverSoftQuotaThreshold(a, family, softQuotaThresholdPercent, softQuotaCacheTtlMs, model) && + !isOverThreshold && !this.isAccountCoolingDown(a); }); From 6328d5cdad4ec9b5cf1a0d2d3fa042440059ab13 Mon Sep 17 00:00:00 2001 From: ndycode Date: Sun, 8 Feb 2026 18:03:25 +0800 Subject: [PATCH 2/2] Clarify header-style comment for gemini fallback --- src/plugin.ts | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/src/plugin.ts b/src/plugin.ts index 64be9385..0a26a991 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1413,9 +1413,9 @@ export const createAntigravityPlugin = (providerId: string) => async ( let shouldSwitchAccount = false; // Determine header style from model suffix: - // - Models with :antigravity suffix -> use Antigravity quota - // - Models without suffix (default) -> use Gemini CLI quota // - Claude models -> always use Antigravity + // - Gemini defaults to Antigravity (including explicit gemini-cli suffix) + // - Gemini CLI is used as fallback when Antigravity is exhausted let headerStyle = requestedHeaderStyle; const explicitQuota = isExplicitQuotaFromUrl(urlString); const cliFirst = getCliFirst(config);