diff --git a/README.md b/README.md index f383d0f..3c77728 100644 --- a/README.md +++ b/README.md @@ -136,7 +136,7 @@ opencode run "Hello" --model=google/antigravity-claude-sonnet-4-5-thinking --var > **Routing Behavior:** > - **Antigravity-first (default):** Gemini models use Antigravity quota across accounts. > - **CLI-first (`cli_first: true`):** Gemini models use Gemini CLI quota first. -> - With `quota_fallback` enabled, the plugin can spill to the other quota when all accounts are exhausted. +> - When a Gemini quota pool is exhausted, the plugin automatically falls back to the other pool. > - Claude and image models always use Antigravity. > Model names are automatically transformed for the target API (e.g., `antigravity-gemini-3-flash` → `gemini-3-flash-preview` for CLI). diff --git a/assets/antigravity.schema.json b/assets/antigravity.schema.json index 1a48106..ec3c047 100644 --- a/assets/antigravity.schema.json +++ b/assets/antigravity.schema.json @@ -129,7 +129,8 @@ }, "quota_fallback": { "default": false, - "type": "boolean" + "type": "boolean", + "description": "Deprecated: accepted for backward compatibility but ignored at runtime. Gemini fallback between Antigravity and Gemini CLI is always enabled." }, "cli_first": { "default": false, diff --git a/docs/CONFIGURATION.md b/docs/CONFIGURATION.md index 08a043a..fc428b3 100644 --- a/docs/CONFIGURATION.md +++ b/docs/CONFIGURATION.md @@ -62,7 +62,7 @@ Settings for managing multiple Google accounts. | `account_selection_strategy` | `"hybrid"` | How to select accounts | | `switch_on_first_rate_limit` | `true` | Switch account immediately on first 429 | | `pid_offset_enabled` | `false` | Distribute sessions across accounts (for parallel agents) | -| `quota_fallback` | `false` | **Gemini only.** When Antigravity exhausted on ALL accounts, fall back to Gemini CLI quota | +| `quota_fallback` | `false` | Deprecated (ignored). Kept for backward compatibility; Gemini fallback is automatic | ### Strategy Guide diff --git a/docs/MULTI-ACCOUNT.md b/docs/MULTI-ACCOUNT.md index 53e5f13..8fac82d 100644 --- a/docs/MULTI-ACCOUNT.md +++ b/docs/MULTI-ACCOUNT.md @@ -25,9 +25,9 @@ For Gemini models, the plugin accesses **two independent quota pools** per accou | Quota Pool | When Used | |------------|-----------| | **Antigravity** | Default for all requests | -| **Gemini CLI** | Automatic fallback when Antigravity exhausted on ALL accounts | +| **Gemini CLI** | Automatic fallback between Antigravity and Gemini CLI in both directions | -This effectively **doubles your Gemini quota** when you have `quota_fallback` enabled. +This effectively **doubles your Gemini quota** through automatic fallback between Antigravity and Gemini CLI pools. ### How Quota Fallback Works @@ -37,13 +37,7 @@ This effectively **doubles your Gemini quota** when you have `quota_fallback` en 4. If no (all accounts exhausted) → fall back to Gemini CLI quota on current account 5. Model names are automatically transformed (e.g., `gemini-3-flash` → `gemini-3-flash-preview`) -To enable automatic fallback between pools, set in `antigravity.json`: - -```json -{ - "quota_fallback": true -} -``` +Automatic fallback between pools is always enabled for Gemini requests. --- diff --git a/src/plugin.ts b/src/plugin.ts index 6c7e57f..80becc6 100644 --- a/src/plugin.ts +++ b/src/plugin.ts @@ -1135,10 +1135,13 @@ 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"; + const routingDecision = resolveHeaderRoutingDecision(urlString, family, config); + const { + cliFirst, + preferredHeaderStyle, + explicitQuota, + allowQuotaFallback, + } = routingDecision; if (accountCount === 0) { throw new Error("No Antigravity accounts available. Run `opencode auth login`."); @@ -1209,7 +1212,7 @@ export const createAntigravityPlugin = (providerId: string) => async ( continue; } - const strictWait = explicitQuota || !allowQuotaFallback; + const strictWait = !allowQuotaFallback; // All accounts are rate-limited - wait and retry const waitMs = accountManager.getMinWaitTimeForFamily( family, @@ -1472,7 +1475,7 @@ export const createAntigravityPlugin = (providerId: string) => async ( // Check if this header style is rate-limited for this account if (accountManager.isRateLimitedForHeaderStyle(account, family, headerStyle, model)) { // Antigravity-first fallback: exhaust antigravity across ALL accounts before gemini-cli - if (config.quota_fallback && !explicitQuota && family === "gemini" && headerStyle === "antigravity" && !cliFirst) { + if (allowQuotaFallback && family === "gemini" && headerStyle === "antigravity") { // Check if ANY other account has antigravity available if (accountManager.hasOtherAccountWithAntigravityAvailable(account.index, family, model)) { // Switch to another account with antigravity (preserve antigravity priority) @@ -1482,9 +1485,6 @@ export const createAntigravityPlugin = (providerId: string) => async ( // All accounts exhausted antigravity - fall back to gemini-cli on this account const alternateStyle = accountManager.getAvailableHeaderStyle(account, family, model); const fallbackStyle = resolveQuotaFallbackHeaderStyle({ - quotaFallback: config.quota_fallback, - cliFirst, - explicitQuota, family, headerStyle, alternateStyle, @@ -1500,13 +1500,10 @@ export const createAntigravityPlugin = (providerId: string) => async ( shouldSwitchAccount = true; } } - } else if (config.quota_fallback && !explicitQuota && family === "gemini") { + } else if (allowQuotaFallback && family === "gemini") { // gemini-cli rate-limited - try alternate style (antigravity) on same account const alternateStyle = accountManager.getAvailableHeaderStyle(account, family, model); const fallbackStyle = resolveQuotaFallbackHeaderStyle({ - quotaFallback: config.quota_fallback, - cliFirst, - explicitQuota, family, headerStyle, alternateStyle, @@ -1742,7 +1739,7 @@ export const createAntigravityPlugin = (providerId: string) => async ( // For Gemini, preserve preferred quota across accounts before fallback if (family === "gemini") { - if (headerStyle === "antigravity" && !cliFirst) { + if (headerStyle === "antigravity") { // Check if any other account has Antigravity quota for this model if (hasOtherAccountWithAntigravity(account)) { pushDebug(`antigravity exhausted on account ${account.index}, but available on others. Switching account.`); @@ -1754,12 +1751,9 @@ export const createAntigravityPlugin = (providerId: string) => async ( // All accounts exhausted for Antigravity on THIS model. // Before falling back to gemini-cli, check if it's the last option (automatic fallback) - if (config.quota_fallback && !explicitQuota) { + if (allowQuotaFallback) { const alternateStyle = accountManager.getAvailableHeaderStyle(account, family, model); const fallbackStyle = resolveQuotaFallbackHeaderStyle({ - quotaFallback: config.quota_fallback, - cliFirst, - explicitQuota, family, headerStyle, alternateStyle, @@ -1775,13 +1769,10 @@ export const createAntigravityPlugin = (providerId: string) => async ( continue; } } - } else if (headerStyle === "gemini-cli" && cliFirst) { - if (config.quota_fallback && !explicitQuota) { + } else if (headerStyle === "gemini-cli") { + if (allowQuotaFallback) { const alternateStyle = accountManager.getAvailableHeaderStyle(account, family, model); const fallbackStyle = resolveQuotaFallbackHeaderStyle({ - quotaFallback: config.quota_fallback, - cliFirst, - explicitQuota, family, headerStyle, alternateStyle, @@ -2808,25 +2799,42 @@ function getModelFamilyFromUrl(urlString: string): ModelFamily { } function resolveQuotaFallbackHeaderStyle(input: { - quotaFallback: boolean; - cliFirst: boolean; - explicitQuota: boolean; family: ModelFamily; headerStyle: HeaderStyle; alternateStyle: HeaderStyle | null; }): HeaderStyle | null { - if (!input.quotaFallback || input.explicitQuota || input.family !== "gemini") { + if (input.family !== "gemini") { return null; } if (!input.alternateStyle || input.alternateStyle === input.headerStyle) { return null; } - if (input.cliFirst && input.headerStyle !== "gemini-cli") { - return null; - } return input.alternateStyle; } +type HeaderRoutingDecision = { + cliFirst: boolean; + preferredHeaderStyle: HeaderStyle; + explicitQuota: boolean; + allowQuotaFallback: boolean; +}; + +function resolveHeaderRoutingDecision( + urlString: string, + family: ModelFamily, + config: AntigravityConfig, +): HeaderRoutingDecision { + const cliFirst = getCliFirst(config); + const preferredHeaderStyle = getHeaderStyleFromUrl(urlString, family, cliFirst); + const explicitQuota = isExplicitQuotaFromUrl(urlString); + return { + cliFirst, + preferredHeaderStyle, + explicitQuota, + allowQuotaFallback: family === "gemini", + }; +} + function getCliFirst(config: AntigravityConfig): boolean { return (config as AntigravityConfig & { cli_first?: boolean }).cli_first ?? false; } @@ -2858,5 +2866,6 @@ function isExplicitQuotaFromUrl(urlString: string): boolean { export const __testExports = { getHeaderStyleFromUrl, + resolveHeaderRoutingDecision, resolveQuotaFallbackHeaderStyle, }; diff --git a/src/plugin/config/schema.ts b/src/plugin/config/schema.ts index e1a3b5a..8909a78 100644 --- a/src/plugin/config/schema.ts +++ b/src/plugin/config/schema.ts @@ -254,14 +254,10 @@ export const AntigravityConfigSchema = z.object({ max_rate_limit_wait_seconds: z.number().min(0).max(3600).default(300), /** - * Enable quota fallback for Gemini models. - * When the preferred quota (gemini-cli or antigravity) is exhausted, - * try the alternate quota on the same account before switching accounts. - * - * Only applies when model is requested without explicit quota suffix. - * Explicit suffixes like `:antigravity` or `:gemini-cli` always use - * that specific quota and switch accounts if exhausted. - * + * @deprecated Kept only for backward compatibility. + * This flag is ignored at runtime. + * Gemini requests always fall back between Antigravity and Gemini CLI quotas. + * * @default false */ quota_fallback: z.boolean().default(false), diff --git a/src/plugin/quota-fallback.test.ts b/src/plugin/quota-fallback.test.ts index da5427a..2a56fb1 100644 --- a/src/plugin/quota-fallback.test.ts +++ b/src/plugin/quota-fallback.test.ts @@ -2,9 +2,6 @@ import { beforeAll, describe, expect, it, vi } from "vitest"; import type { HeaderStyle, ModelFamily } from "./accounts"; type ResolveQuotaFallbackHeaderStyle = (input: { - quotaFallback: boolean; - cliFirst: boolean; - explicitQuota: boolean; family: ModelFamily; headerStyle: HeaderStyle; alternateStyle: HeaderStyle | null; @@ -16,8 +13,20 @@ type GetHeaderStyleFromUrl = ( cliFirst?: boolean, ) => HeaderStyle; +type ResolveHeaderRoutingDecision = ( + urlString: string, + family: ModelFamily, + config: unknown, +) => { + cliFirst: boolean; + preferredHeaderStyle: HeaderStyle; + explicitQuota: boolean; + allowQuotaFallback: boolean; +}; + let resolveQuotaFallbackHeaderStyle: ResolveQuotaFallbackHeaderStyle | undefined; let getHeaderStyleFromUrl: GetHeaderStyleFromUrl | undefined; +let resolveHeaderRoutingDecision: ResolveHeaderRoutingDecision | undefined; beforeAll(async () => { vi.mock("@opencode-ai/plugin", () => ({ @@ -31,14 +40,14 @@ beforeAll(async () => { getHeaderStyleFromUrl = (__testExports as { getHeaderStyleFromUrl?: GetHeaderStyleFromUrl; }).getHeaderStyleFromUrl; + resolveHeaderRoutingDecision = (__testExports as { + resolveHeaderRoutingDecision?: ResolveHeaderRoutingDecision; + }).resolveHeaderRoutingDecision; }); describe("quota fallback direction", () => { - it("falls back from gemini-cli to antigravity when cli_first is enabled", () => { + it("falls back from gemini-cli to antigravity when alternate quota is available", () => { const result = resolveQuotaFallbackHeaderStyle?.({ - quotaFallback: true, - cliFirst: true, - explicitQuota: false, family: "gemini", headerStyle: "gemini-cli", alternateStyle: "antigravity", @@ -47,30 +56,24 @@ describe("quota fallback direction", () => { expect(result).toBe("antigravity"); }); - it("does not fall back from antigravity when cli_first is enabled", () => { + it("falls back from antigravity to gemini-cli when alternate quota is available", () => { const result = resolveQuotaFallbackHeaderStyle?.({ - quotaFallback: true, - cliFirst: true, - explicitQuota: false, family: "gemini", headerStyle: "antigravity", alternateStyle: "gemini-cli", }); - expect(result).toBeNull(); + expect(result).toBe("gemini-cli"); }); - it("falls back from antigravity to gemini-cli when cli_first is disabled", () => { + it("returns null when no alternate quota is available", () => { const result = resolveQuotaFallbackHeaderStyle?.({ - quotaFallback: true, - cliFirst: false, - explicitQuota: false, family: "gemini", headerStyle: "antigravity", - alternateStyle: "gemini-cli", + alternateStyle: null, }); - expect(result).toBe("gemini-cli"); + expect(result).toBeNull(); }); }); @@ -115,3 +118,74 @@ describe("header style resolution", () => { expect(headerStyle).toBe("antigravity"); }); }); + +describe("header routing decision", () => { + it("defaults to antigravity-first for unsuffixed Gemini when cli_first is disabled", () => { + const decision = resolveHeaderRoutingDecision?.( + "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash:streamGenerateContent", + "gemini", + { + cli_first: false, + }, + ); + + expect(decision).toMatchObject({ + cliFirst: false, + preferredHeaderStyle: "antigravity", + explicitQuota: false, + allowQuotaFallback: true, + }); + }); + + it("uses gemini-cli-first for unsuffixed Gemini when cli_first is enabled", () => { + const decision = resolveHeaderRoutingDecision?.( + "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash:streamGenerateContent", + "gemini", + { + cli_first: true, + }, + ); + + expect(decision).toMatchObject({ + cliFirst: true, + preferredHeaderStyle: "gemini-cli", + explicitQuota: false, + allowQuotaFallback: true, + }); + }); + + it("keeps explicit antigravity prefix as primary route while fallback remains available", () => { + const decision = resolveHeaderRoutingDecision?.( + "https://generativelanguage.googleapis.com/v1beta/models/antigravity-gemini-3-flash:streamGenerateContent", + "gemini", + { + cli_first: true, + }, + ); + + expect(decision).toMatchObject({ + cliFirst: true, + preferredHeaderStyle: "antigravity", + explicitQuota: true, + allowQuotaFallback: true, + }); + }); + + it("ignores legacy quota_fallback when deciding Gemini fallback availability", () => { + const decision = resolveHeaderRoutingDecision?.( + "https://generativelanguage.googleapis.com/v1beta/models/gemini-3-flash:streamGenerateContent", + "gemini", + { + cli_first: false, + quota_fallback: false, + }, + ); + + expect(decision).toMatchObject({ + cliFirst: false, + preferredHeaderStyle: "antigravity", + explicitQuota: false, + allowQuotaFallback: true, + }); + }); +});