fix(providers): limit Gemini CLI to legacy OAuth refresh (#8275)

#8232 correctly targeted OAuth auto-refresh for legacy stored Gemini CLI connections, but exceeded that compatibility goal by recreating a complete routable and UI-visible provider with an Antigravity model catalog.

Preserve legacy refresh by mapping gemini-cli rows to the existing Gemini OAuth credentials. Remove the public provider registry, OAuth preset, model routing snapshot, and canonical-provider tests while keeping Gemini API and Antigravity unchanged.
This commit is contained in:
backryun
2026-07-23 21:50:49 +09:00
committed by GitHub
parent 1110f9ca5f
commit 2f65339378
8 changed files with 118 additions and 242 deletions

View File

@@ -1971,32 +1971,6 @@
"stream": "https://generativelanguage.googleapis.com/v1beta/models/test-model:streamGenerateContent?alt=sse"
}
},
"gemini-cli": {
"format": "antigravity",
"headers": {
"apiKey": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json",
"User-Agent": "antigravity/ide/2.1.1 <OS>/<ARCH>"
},
"nonStream": {
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json",
"User-Agent": "antigravity/ide/2.1.1 <OS>/<ARCH>"
},
"oauth": {
"Accept": "text/event-stream",
"Authorization": "Bearer <TOK>",
"Content-Type": "application/json",
"User-Agent": "antigravity/ide/2.1.1 <OS>/<ARCH>"
}
},
"url": {
"nonStream": "https://daily-cloudcode-pa.googleapis.com/v1internal:generateContent",
"stream": "https://daily-cloudcode-pa.googleapis.com/v1internal:streamGenerateContent?alt=sse"
}
},
"gemini-web": {
"format": "openai",
"headers": {

View File

@@ -0,0 +1,103 @@
import test from "node:test";
import assert from "node:assert/strict";
import { readFileSync } from "node:fs";
import { OAUTH_ENDPOINTS, PROVIDERS as LEGACY_PROVIDERS } from "../../open-sse/config/constants.ts";
import { REGISTRY } from "../../open-sse/config/providerRegistry.ts";
import {
getAccessToken,
REFRESH_LEAD_MS,
supportsTokenRefresh,
} from "../../open-sse/services/tokenRefresh.ts";
import { OAUTH_PROVIDERS } from "../../src/shared/constants/providers.ts";
// #8232 set out to repair OAuth refresh for legacy stored connections, but
// exceeded that compatibility goal by restoring a complete routable and
// UI-visible Gemini CLI provider. Preserve only the legacy refresh path while
// keeping the discontinued provider out of public registries and routing.
test("Gemini CLI stays out of the chat and OAuth provider registries", () => {
assert.equal(REGISTRY["gemini-cli"], undefined);
assert.equal(LEGACY_PROVIDERS["gemini-cli"], undefined);
assert.equal((OAUTH_PROVIDERS as Record<string, unknown>)["gemini-cli"], undefined);
assert.ok(REGISTRY.gemini);
assert.ok(REGISTRY.antigravity);
});
test("legacy Gemini CLI connections retain proactive token refresh", () => {
assert.equal(REFRESH_LEAD_MS["gemini-cli"], REFRESH_LEAD_MS.antigravity);
assert.equal(supportsTokenRefresh("gemini-cli"), true);
});
test("Gemini CLI stays out of the provider translation snapshot", () => {
const snapshotPath = new URL("../snapshots/provider/translate-path.json", import.meta.url);
const snapshot = JSON.parse(readFileSync(snapshotPath, "utf8")) as Record<string, unknown>;
assert.equal(snapshot["gemini-cli"], undefined);
});
test("legacy Gemini CLI refresh reuses Gemini OAuth credentials without a provider entry", async () => {
const originalFetch = globalThis.fetch;
const calls: Array<{ url: string; options: RequestInit }> = [];
globalThis.fetch = (async (url, options: RequestInit = {}) => {
calls.push({ url: String(url), options });
return new Response(
JSON.stringify({
access_token: "legacy-gemini-cli-access-new",
expires_in: 3600,
}),
{
status: 200,
headers: { "content-type": "application/json" },
}
);
}) as typeof fetch;
try {
const result = await getAccessToken(
"gemini-cli",
{ refreshToken: "legacy-gemini-cli-refresh-old" },
{}
);
assert.equal(calls.length, 1);
assert.equal(calls[0].url, OAUTH_ENDPOINTS.google.token);
const body = new URLSearchParams(String(calls[0].options.body));
assert.equal(body.get("grant_type"), "refresh_token");
assert.equal(body.get("refresh_token"), "legacy-gemini-cli-refresh-old");
assert.equal(body.get("client_id"), LEGACY_PROVIDERS.gemini.clientId);
assert.equal(body.get("client_secret"), LEGACY_PROVIDERS.gemini.clientSecret);
assert.deepEqual(result, {
accessToken: "legacy-gemini-cli-access-new",
refreshToken: "legacy-gemini-cli-refresh-old",
expiresIn: 3600,
});
} finally {
globalThis.fetch = originalFetch;
}
});
test("legacy Gemini CLI refresh surfaces revoked tokens as unrecoverable", async () => {
const originalFetch = globalThis.fetch;
globalThis.fetch = (async () =>
new Response(JSON.stringify({ error: "invalid_grant" }), {
status: 400,
headers: { "content-type": "application/json" },
})) as typeof fetch;
try {
const result = await getAccessToken(
"gemini-cli",
{ refreshToken: "legacy-gemini-cli-refresh-revoked" },
{}
);
assert.deepEqual(result, {
error: "unrecoverable_refresh_error",
code: "invalid_grant",
});
} finally {
globalThis.fetch = originalFetch;
}
});

View File

@@ -1,146 +0,0 @@
import test from "node:test";
import assert from "node:assert/strict";
const tokenRefresh = await import("../../open-sse/services/tokenRefresh.ts");
const { PROVIDERS: LEGACY_PROVIDERS, OAUTH_ENDPOINTS } =
await import("../../open-sse/config/constants.ts");
const { REGISTRY } = await import("../../open-sse/config/providerRegistry.ts");
const { supportsTokenRefresh, REFRESH_LEAD_MS, getAccessToken } = tokenRefresh;
type TestFetch = typeof fetch;
type FetchCall = { url: string; options: RequestInit };
function jsonResponse(body: unknown, status = 200) {
return new Response(JSON.stringify(body), {
status,
headers: { "content-type": "application/json" },
});
}
function bodyToString(body: BodyInit | null | undefined) {
if (typeof body === "string") return body;
if (body instanceof URLSearchParams) return body.toString();
return String(body ?? "");
}
async function withMockedFetch<TResult>(fetchImpl: TestFetch, fn: () => Promise<TResult>) {
const originalFetch = globalThis.fetch;
globalThis.fetch = fetchImpl;
try {
return await fn();
} finally {
globalThis.fetch = originalFetch;
}
}
function createLog() {
return {
debug: () => {},
info: () => {},
warn: () => {},
error: () => {},
};
}
// ─── Regression tests for the two-gap gemini-cli refresh bug ────────────────
//
// Root cause (both gaps had to be fixed together):
// 1. supportsTokenRefresh() gated on a hardcoded id allow-set that had
// "gemini" but not "gemini-cli" (the id actually stored on gemini-cli
// OAuth connections), and its PROVIDERS[e].tokenUrl fallback also
// failed because...
// 2. ...the OAuth PROVIDERS registry had NO entry at all for "gemini-cli"
// — no clientId/clientSecret/tokenUrl/refreshUrl — so even a provider
// that reached the generic refresh path had nothing to refresh with.
//
// Together this meant the proactive health-check scheduler permanently
// skipped gemini-cli connections ("[HealthCheck] Skipping gemini-cli/...
// (refresh unsupported)") until the access token expired and the account
// went dark, forcing a full re-authentication even though the stored
// refresh_token was perfectly valid.
test("gemini-cli is registered in the OAuth PROVIDERS registry with Google refresh credentials", () => {
const entry = REGISTRY["gemini-cli"];
assert.ok(entry, "REGISTRY['gemini-cli'] must exist");
assert.equal(entry.authType, "oauth");
const legacy = LEGACY_PROVIDERS["gemini-cli"];
assert.ok(legacy, "LEGACY_PROVIDERS['gemini-cli'] must exist");
assert.ok(legacy.clientId, "gemini-cli must have a clientId");
assert.ok(legacy.clientSecret, "gemini-cli must have a clientSecret");
assert.equal(legacy.tokenUrl, "https://oauth2.googleapis.com/token");
assert.equal(legacy.refreshUrl, "https://oauth2.googleapis.com/token");
// Same well-known public Gemini CLI OAuth client as the Gemini Studio
// provider's own (previously unused-for-refresh) oauth block — not a new
// embedded secret.
assert.equal(legacy.clientId, LEGACY_PROVIDERS.gemini.clientId);
assert.equal(legacy.clientSecret, LEGACY_PROVIDERS.gemini.clientSecret);
assert.equal(
legacy.clientId,
"681255809395-oo8ft2oprdrnp9e3aqf6av3hmdib135j.apps.googleusercontent.com"
);
});
test("supportsTokenRefresh('gemini-cli') is true (both the explicit allow-set and the PROVIDERS fallback)", () => {
assert.equal(supportsTokenRefresh("gemini-cli"), true);
// Sanity: still false for a random unregistered id.
assert.equal(supportsTokenRefresh("gemini-cli-typo"), false);
});
test("gemini-cli gets the same 15-minute proactive refresh lead as antigravity/agy (non-rotating Google tokens)", () => {
assert.equal(REFRESH_LEAD_MS["gemini-cli"], REFRESH_LEAD_MS.antigravity);
assert.equal(REFRESH_LEAD_MS["gemini-cli"], REFRESH_LEAD_MS.agy);
});
test("getAccessToken('gemini-cli', ...) performs a real grant_type=refresh_token exchange against Google's token endpoint", async () => {
const log = createLog();
const calls: FetchCall[] = [];
const result = await withMockedFetch(
async (url, options: RequestInit = {}) => {
calls.push({ url: String(url), options });
return jsonResponse({
access_token: "gemini-cli-access-new",
refresh_token: "gemini-cli-refresh-new",
expires_in: 3600,
});
},
async () => getAccessToken("gemini-cli", { refreshToken: "gemini-cli-refresh-old" }, log)
);
assert.equal(calls.length, 1, "must hit the network exactly once");
assert.equal(calls[0].url, OAUTH_ENDPOINTS.google.token);
assert.equal(calls[0].url, "https://oauth2.googleapis.com/token");
const sentBody = bodyToString(calls[0].options.body);
assert.match(sentBody, /grant_type=refresh_token/);
assert.match(sentBody, /refresh_token=gemini-cli-refresh-old/);
assert.ok(
sentBody.includes(`client_id=${encodeURIComponent(LEGACY_PROVIDERS["gemini-cli"].clientId)}`),
"must send the gemini-cli client_id"
);
assert.ok(sentBody.includes("client_secret="), "must send client_secret");
assert.deepEqual(result, {
accessToken: "gemini-cli-access-new",
refreshToken: "gemini-cli-refresh-new",
expiresIn: 3600,
});
});
test("getAccessToken('gemini-cli', ...) surfaces an unrecoverable error on invalid_grant (revoked/expired refresh token) instead of looping forever", async () => {
const log = createLog();
const result = await withMockedFetch(
async () =>
new Response(JSON.stringify({ error: "invalid_grant" }), {
status: 400,
headers: { "content-type": "application/json" },
}),
async () => getAccessToken("gemini-cli", { refreshToken: "revoked-refresh-token" }, log)
);
assert.deepEqual(result, { error: "unrecoverable_refresh_error", code: "invalid_grant" });
});