diff --git a/src/plugin.ts b/src/plugin.ts index d447454..026bc0e 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1131,6 +1131,10 @@ export const createAntigravityPlugin = (providerId: string) => async ( checkAborted(); const accountCount = accountManager.getAccountCount(); + const cliFirst = getCliFirst(config); + const preferredHeaderStyle = getHeaderStyleFromUrl(urlString, family, cliFirst); + const explicitQuota = isExplicitQuotaFromUrl(urlString); + const allowQuotaFallback = config.quota_fallback && !explicitQuota && family === "gemini"; if (accountCount === 0) { throw new Error("No Antigravity accounts available. Run `opencode auth login`."); @@ -1141,15 +1145,34 @@ export const createAntigravityPlugin = (providerId: string) => async ( config.quota_refresh_interval_minutes, ); - const account = accountManager.getCurrentOrNextForFamily( + let account = accountManager.getCurrentOrNextForFamily( family, model, config.account_selection_strategy, - 'antigravity', + preferredHeaderStyle, config.pid_offset_enabled, config.soft_quota_threshold_percent, softQuotaCacheTtlMs, ); + + if (!account && allowQuotaFallback) { + const alternateHeaderStyle: HeaderStyle = + preferredHeaderStyle === "antigravity" ? "gemini-cli" : "antigravity"; + account = accountManager.getCurrentOrNextForFamily( + family, + model, + config.account_selection_strategy, + alternateHeaderStyle, + config.pid_offset_enabled, + config.soft_quota_threshold_percent, + softQuotaCacheTtlMs, + ); + if (account) { + pushDebug( + `selected-by-fallback idx=${account.index} preferred=${preferredHeaderStyle} alternate=${alternateHeaderStyle}`, + ); + } + } if (!account) { if (accountManager.areAllAccountsOverSoftQuota(family, config.soft_quota_threshold_percent, softQuotaCacheTtlMs, model)) { @@ -1182,14 +1205,13 @@ export const createAntigravityPlugin = (providerId: string) => async ( continue; } - const headerStyle = getHeaderStyleFromUrl(urlString, family); - const explicitQuota = isExplicitQuotaFromUrl(urlString); + const strictWait = explicitQuota || !allowQuotaFallback; // All accounts are rate-limited - wait and retry const waitMs = accountManager.getMinWaitTimeForFamily( family, model, - headerStyle, - explicitQuota, + preferredHeaderStyle, + strictWait, ) || 60_000; const waitSecValue = Math.max(1, Math.ceil(waitMs / 1000)); @@ -1434,11 +1456,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); - const explicitQuota = isExplicitQuotaFromUrl(urlString); - const cliFirst = getCliFirst(config); + // - Models with antigravity- prefix -> use Antigravity quota + // - Gemini models without explicit prefix -> follow cli_first + // - Claude models -> always use Antigravity + let headerStyle = preferredHeaderStyle; pushDebug(`headerStyle=${headerStyle} explicit=${explicitQuota}`); if (account.fingerprint) { pushDebug(`fingerprint: quotaUser=${account.fingerprint.quotaUser} deviceId=${account.fingerprint.deviceId.slice(0, 8)}...`); @@ -2802,16 +2823,20 @@ function getCliFirst(config: AntigravityConfig): boolean { return (config as AntigravityConfig & { cli_first?: boolean }).cli_first ?? false; } -function getHeaderStyleFromUrl(urlString: string, family: ModelFamily): HeaderStyle { +function getHeaderStyleFromUrl( + urlString: string, + family: ModelFamily, + cliFirst: boolean = false, +): HeaderStyle { if (family === "claude") { return "antigravity"; } const modelWithSuffix = extractModelFromUrlWithSuffix(urlString); if (!modelWithSuffix) { - return "antigravity"; + return cliFirst ? "gemini-cli" : "antigravity"; } - const { quotaPreference } = resolveModelWithTier(modelWithSuffix); - return quotaPreference === "gemini-cli" ? "antigravity" : (quotaPreference ?? "antigravity"); + const { quotaPreference } = resolveModelWithTier(modelWithSuffix, { cli_first: cliFirst }); + return quotaPreference ?? "antigravity"; } function isExplicitQuotaFromUrl(urlString: string): boolean { diff --git a/src/plugin/quota-fallback.test.ts b/src/plugin/quota-fallback.test.ts index 4139048..da5427a 100644 --- a/src/plugin/quota-fallback.test.ts +++ b/src/plugin/quota-fallback.test.ts @@ -10,7 +10,14 @@ type ResolveQuotaFallbackHeaderStyle = (input: { alternateStyle: HeaderStyle | null; }) => HeaderStyle | null; +type GetHeaderStyleFromUrl = ( + urlString: string, + family: ModelFamily, + cliFirst?: boolean, +) => HeaderStyle; + let resolveQuotaFallbackHeaderStyle: ResolveQuotaFallbackHeaderStyle | undefined; +let getHeaderStyleFromUrl: GetHeaderStyleFromUrl | undefined; beforeAll(async () => { vi.mock("@opencode-ai/plugin", () => ({ @@ -21,6 +28,9 @@ beforeAll(async () => { resolveQuotaFallbackHeaderStyle = (__testExports as { resolveQuotaFallbackHeaderStyle?: ResolveQuotaFallbackHeaderStyle; }).resolveQuotaFallbackHeaderStyle; + getHeaderStyleFromUrl = (__testExports as { + getHeaderStyleFromUrl?: GetHeaderStyleFromUrl; + }).getHeaderStyleFromUrl; }); describe("quota fallback direction", () => { @@ -63,3 +73,45 @@ describe("quota fallback direction", () => { expect(result).toBe("gemini-cli"); }); }); + +describe("header style resolution", () => { + it("uses gemini-cli for unsuffixed Gemini models when cli_first is enabled", () => { + const headerStyle = getHeaderStyleFromUrl?.( + "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash:streamGenerateContent", + "gemini", + true, + ); + + expect(headerStyle).toBe("gemini-cli"); + }); + + it("keeps antigravity for unsuffixed Gemini models when cli_first is disabled", () => { + const headerStyle = getHeaderStyleFromUrl?.( + "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash:streamGenerateContent", + "gemini", + false, + ); + + expect(headerStyle).toBe("antigravity"); + }); + + it("keeps antigravity for explicit antigravity prefix when cli_first is enabled", () => { + const headerStyle = getHeaderStyleFromUrl?.( + "https://generativelanguage.googleapis.com/v1beta/models/antigravity-gemini-3-flash:streamGenerateContent", + "gemini", + true, + ); + + expect(headerStyle).toBe("antigravity"); + }); + + it("keeps antigravity for Claude when cli_first is enabled", () => { + const headerStyle = getHeaderStyleFromUrl?.( + "https://generativelanguage.googleapis.com/v1beta/models/claude-sonnet-4-5-thinking:streamGenerateContent", + "claude", + true, + ); + + expect(headerStyle).toBe("antigravity"); + }); +});