mirror of
https://github.com/NoeFabris/opencode-antigravity-auth.git
synced 2026-05-13 23:53:18 +00:00
Merge pull request #413 from NoeFabris/fix/supersede-pr-404-cli-fallback
fix: simplify Gemini routing and ignore legacy quota_fallback
This commit is contained in:
@@ -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).
|
||||
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
|
||||
|
||||
@@ -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.
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -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,
|
||||
};
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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,24 +56,8 @@ 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();
|
||||
});
|
||||
|
||||
it("falls back from antigravity to gemini-cli when cli_first is disabled", () => {
|
||||
const result = resolveQuotaFallbackHeaderStyle?.({
|
||||
quotaFallback: true,
|
||||
cliFirst: false,
|
||||
explicitQuota: false,
|
||||
family: "gemini",
|
||||
headerStyle: "antigravity",
|
||||
alternateStyle: "gemini-cli",
|
||||
@@ -72,6 +65,16 @@ describe("quota fallback direction", () => {
|
||||
|
||||
expect(result).toBe("gemini-cli");
|
||||
});
|
||||
|
||||
it("returns null when no alternate quota is available", () => {
|
||||
const result = resolveQuotaFallbackHeaderStyle?.({
|
||||
family: "gemini",
|
||||
headerStyle: "antigravity",
|
||||
alternateStyle: null,
|
||||
});
|
||||
|
||||
expect(result).toBeNull();
|
||||
});
|
||||
});
|
||||
|
||||
describe("header style resolution", () => {
|
||||
@@ -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,
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user