Merge pull request #388 from NoeFabris/fix/supersede-pr-373-cli-first-routing

fix: honor cli_first in routing (supersedes #373)
This commit is contained in:
Noè
2026-02-06 15:50:30 +00:00
committed by GitHub
2 changed files with 92 additions and 15 deletions

View File

@@ -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 {

View File

@@ -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");
});
});