mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-08 00:02:20 +03:00
* feat(sse): deprecate the gemini-cli upstream provider with a real migration path
Stored `gemini-cli` connections were being kept alive for nothing. Measured before
touching anything:
routable? absent from PROVIDERS, from REGISTRY, from OAUTH_PROVIDERS, and no
executor references it → the connection can NEVER serve a request
refreshing? yes, and successfully — it redeemed against PROVIDERS.gemini's client
(681255809395-oo8ft2o…), the same public Gemini CLI / Code Assist OAuth
client
So the scheduler made periodic upstream calls to Google to keep a credential fresh
that had nowhere to go. That is the waste this removes.
This is a deprecation, not a deletion, and the difference is deliberate. The path was
not dead code: #8232 added it after a user report (the UI advertises automatic OAuth
rotation and these rows never rotated), and #8275 narrowed it to exactly the legacy
refresh. Simply dropping it from `supportsTokenRefresh` would have produced a SILENT
skip — `Skipping … (refresh unsupported)` — leaving the row at "active" forever, doing
nothing. Worse than before.
Instead:
DEPRECATED_PROVIDERS + isDeprecatedProvider/getDeprecationNotice in tokenRefresh
one place naming the provider and where to migrate. A test asserts the migration
target is itself routable, so the notice can never point somewhere useless.
_getAccessTokenInternal returns the ESTABLISHED unrecoverable envelope
{ error: "unrecoverable_refresh_error", code: "provider_deprecated", migrateTo }
Reusing `error` means isUnrecoverableRefreshError and the manual-refresh route
already stop retrying — no new contract for callers to learn. The distinct `code`
is what makes it legible. A bare `null` would read as transient and retry forever.
tokenHealthCheck marks the connection terminal with the reason
Placed after the existing terminal-status guard, which makes it idempotent for
free: once "expired", later sweeps skip the row, so it writes once instead of
rewriting the same reason every cycle.
the manual-refresh route stops lying
It said "Refresh token expired. Please re-authenticate this account." — false
here: the token is fine, the provider is gone. Re-authenticating would loop
against something that no longer exists. It now reports the deprecation and the
migration target.
`gemini` uses the same OAuth client, so re-adding the account there is a working path,
not advice to start over.
Deliberately NOT touched:
Category A — the gemini-cli CLIENT identity (#7034): clientIdentityProfiles.ts,
clientApi.ts, googApiKeyAuth.ts. Same string, opposite direction — requests
ARRIVING from the Gemini CLI, where OmniRoute is the server. Deleting these is the
failure this change must never cause, so a test now asserts the profile survives.
Audited: `git diff --name-only` touches none of those files.
errorClassifier.ts's isCloudCodeProvider list still names gemini-cli. It is a
defensive 403→PROJECT_ROUTE_ERROR list shared with cloudcode/cloud-code; the entry
is unreachable for a non-routable provider, and editing a shared classification
path for a dead string is risk without upside.
Tests — 42 across the six files that mention the identifier, all green:
gemini-cli-legacy-refresh.test.ts 5 (3 assertions REWRITTEN, see below)
gemini-cli-deprecation.test.ts 5 (new)
client-identity-profiles.test.ts 9 (category A, untouched)
service-token-refresh.test.ts 14
errorclassifier-antigravity-403.test.ts 4
gemini-cli-ansi-sanitization.test.ts 5 (category C, untouched)
The three rewritten assertions in the legacy file are alignment, not weakening, and the
gate is right to ask: each is now STRONGER. "refresh succeeds against Google's token
endpoint" became "zero upstream calls happen at all"; "a 400 surfaces invalid_grant"
became "the envelope is unchanged but the code says provider_deprecated" plus a control
asserting `gemini` still reports invalid_grant, proving the real path was not blunted.
The file's header keeps the whole #8232 → #8275 → deprecation arc, because each step is
why the next made sense. Count unchanged; no test deleted, so no allowlist entry needed.
* docs(changelog): fragment for #8980
---------
Co-authored-by: diegosouzapw <diegosouzapw@users.noreply.github.com>
112 lines
4.9 KiB
TypeScript
112 lines
4.9 KiB
TypeScript
/**
|
|
* Deprecation of the `gemini-cli` UPSTREAM provider (not the client identity).
|
|
*
|
|
* Why this is a deprecation and not a deletion — measured on 2026-07-30:
|
|
*
|
|
* - `gemini-cli` is NOT routable: absent from PROVIDERS (open-sse/config/constants),
|
|
* REGISTRY (providerRegistry), OAUTH_PROVIDERS, and no executor references it. A
|
|
* stored connection can therefore never serve a request, no matter how fresh its
|
|
* token is.
|
|
* - The legacy refresh path DID work: it redeemed the token with
|
|
* `PROVIDERS.gemini.clientId`, which is the same public Gemini CLI / Code Assist
|
|
* OAuth client. So refreshing kept a credential alive that had nowhere to go.
|
|
* - Removing it from `supportsTokenRefresh` alone would produce a SILENT skip
|
|
* (`Skipping … (refresh unsupported)` in tokenHealthCheck) — the connection would
|
|
* sit at `active` forever while doing nothing.
|
|
*
|
|
* So the deprecation has to be *legible*: the connection becomes terminal with a
|
|
* reason that names the migration. `gemini` uses the very same OAuth client, so
|
|
* re-adding the account there is a real, working path — not advice to nowhere.
|
|
*
|
|
* NOT touched, and asserted here so a future edit cannot conflate them: the
|
|
* `gemini-cli` CLIENT identity (issue #7034) — requests ARRIVING from the Gemini CLI
|
|
* or any @google/genai-based client, where OmniRoute is the server.
|
|
*/
|
|
import test from "node:test";
|
|
import assert from "node:assert/strict";
|
|
|
|
import { PROVIDERS } from "../../open-sse/config/constants.ts";
|
|
import { REGISTRY } from "../../open-sse/config/providerRegistry.ts";
|
|
import {
|
|
DEPRECATED_PROVIDERS,
|
|
getAccessToken,
|
|
getDeprecationNotice,
|
|
getRefreshLeadMs,
|
|
isDeprecatedProvider,
|
|
REFRESH_LEAD_MS,
|
|
supportsTokenRefresh,
|
|
TOKEN_EXPIRY_BUFFER_MS,
|
|
} from "../../open-sse/services/tokenRefresh.ts";
|
|
import { CLIENT_IDENTITY_PROFILES } from "../../src/shared/constants/clientIdentityProfiles.ts";
|
|
|
|
test("gemini-cli is registered as deprecated, with a migration target that is routable", () => {
|
|
assert.equal(isDeprecatedProvider("gemini-cli"), true);
|
|
assert.equal(isDeprecatedProvider("gemini"), false);
|
|
assert.equal(isDeprecatedProvider("antigravity"), false);
|
|
assert.equal(isDeprecatedProvider(""), false);
|
|
|
|
const notice = getDeprecationNotice("gemini-cli");
|
|
assert.ok(notice, "a deprecated provider must carry a notice");
|
|
assert.equal(notice.migrateTo, "gemini");
|
|
assert.match(notice.reason, /gemini/i);
|
|
|
|
// The migration target must actually be usable — otherwise the notice sends the
|
|
// operator nowhere. This is the assertion that makes the advice honest.
|
|
assert.ok(REGISTRY[notice.migrateTo], "the migration target must be a routable provider");
|
|
assert.ok(PROVIDERS[notice.migrateTo], "the migration target must have OAuth config");
|
|
});
|
|
|
|
test("a deprecated provider is no longer refresh-capable and carries no refresh lead", () => {
|
|
assert.equal(supportsTokenRefresh("gemini-cli"), false);
|
|
// The TTL entry existed only to pace a refresh that no longer happens. Dropping it
|
|
// means the generic fallback applies, which is the honest answer for a provider the
|
|
// scheduler no longer refreshes.
|
|
assert.equal(REFRESH_LEAD_MS["gemini-cli"], undefined);
|
|
assert.equal(getRefreshLeadMs("gemini-cli"), TOKEN_EXPIRY_BUFFER_MS);
|
|
});
|
|
|
|
test("refreshing a stored gemini-cli connection fails with a CLASSIFIED code, not silence", async () => {
|
|
const originalFetch = globalThis.fetch;
|
|
let upstreamCalls = 0;
|
|
globalThis.fetch = (async () => {
|
|
upstreamCalls++;
|
|
return new Response("{}", { status: 200 });
|
|
}) as typeof fetch;
|
|
|
|
try {
|
|
const result = await getAccessToken(
|
|
"gemini-cli",
|
|
{ refreshToken: "legacy-gemini-cli-refresh" },
|
|
{}
|
|
);
|
|
|
|
assert.equal(upstreamCalls, 0, "a deprecated provider must not touch the upstream at all");
|
|
assert.equal(
|
|
result.error,
|
|
"unrecoverable_refresh_error",
|
|
"reuse the established unrecoverable contract so every existing caller stops retrying"
|
|
);
|
|
assert.equal(result.code, "provider_deprecated", "…but with a code that says WHY");
|
|
assert.equal(result.migrateTo, "gemini", "and the migration target, for a legible message");
|
|
assert.equal(result.accessToken, undefined);
|
|
} finally {
|
|
globalThis.fetch = originalFetch;
|
|
}
|
|
});
|
|
|
|
test("the gemini-cli CLIENT identity is untouched (issue #7034)", () => {
|
|
// Category A. Requests ARRIVING from the Gemini CLI — OmniRoute is the server here.
|
|
// Deleting this is the failure mode the deprecation must never cause.
|
|
assert.ok(
|
|
CLIENT_IDENTITY_PROFILES["gemini-cli"],
|
|
"the gemini-cli client-identity profile must survive the provider deprecation"
|
|
);
|
|
assert.equal(CLIENT_IDENTITY_PROFILES["gemini-cli"].id, "gemini-cli");
|
|
});
|
|
|
|
test("deprecation does not resurrect the provider into any routable registry", () => {
|
|
assert.equal(REGISTRY["gemini-cli"], undefined);
|
|
assert.equal(PROVIDERS["gemini-cli"], undefined);
|
|
assert.ok(Object.prototype.hasOwnProperty.call(DEPRECATED_PROVIDERS, "gemini-cli"));
|
|
});
|