diff --git a/.env.example b/.env.example index d2473c1163..252ee8d853 100644 --- a/.env.example +++ b/.env.example @@ -1347,6 +1347,12 @@ APP_LOG_TO_FILE=true # Disable the OAuth token healthcheck loop during tests (default: true). # OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK=true +# Exclude specific providers from the PROACTIVE token-refresh sweep (comma-separated, +# case-insensitive). Targeted alternative to OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK: keeps +# rotating-cascade providers (Codex/OpenAI share one Auth0 family) on the reactive 401 +# path only, while short-TTL providers like Kimi-coding keep being refreshed proactively. +# OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS=codex,openai + # Silence healthcheck noise in Playwright stdout (default: true). # OMNIROUTE_HIDE_HEALTHCHECK_LOGS=true diff --git a/docs/reference/ENVIRONMENT.md b/docs/reference/ENVIRONMENT.md index 92c02aa805..67e2b6156a 100644 --- a/docs/reference/ENVIRONMENT.md +++ b/docs/reference/ENVIRONMENT.md @@ -911,6 +911,7 @@ value below unset in production deployments. | `OMNIROUTE_E2E_PASSWORD` | falls back to `INITIAL_PASSWORD` | `scripts/dev/run-next-playwright.mjs` | Admin password injected into the Playwright environment. | | `OMNIROUTE_DISABLE_LOCAL_HEALTHCHECK` | `true` | `scripts/dev/run-next-playwright.mjs` | Disable the local healthcheck poll during Playwright runs. | | `OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK` | `true` | `scripts/dev/run-next-playwright.mjs` | Disable the OAuth token healthcheck loop during tests. | +| `OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS` | _(unset)_ | `src/lib/tokenHealthCheck.ts` | Comma-separated providers excluded from the proactive token-refresh sweep (e.g. `codex,openai`). Targeted alternative to fully disabling the healthcheck — short-TTL providers keep refreshing while cascade providers stay reactive-only. | | `OMNIROUTE_HIDE_HEALTHCHECK_LOGS` | `true` | `scripts/dev/run-next-playwright.mjs` | Silence healthcheck noise in Playwright stdout. | | `OMNIROUTE_PLAYWRIGHT_SKIP_BUILD` | `0` | `scripts/dev/run-next-playwright.mjs` | Skip the Next.js production build before Playwright starts (CI optimization). | | `OMNIROUTE_SKIP_UNINSTALL_HOOK` | `0` | `scripts/build/uninstall.mjs` | Skip the OmniRoute uninstall hook (used by CI to keep `node_modules` intact). | diff --git a/src/lib/tokenHealthCheck.ts b/src/lib/tokenHealthCheck.ts index d06aefcdae..1166238f04 100644 --- a/src/lib/tokenHealthCheck.ts +++ b/src/lib/tokenHealthCheck.ts @@ -109,6 +109,25 @@ function isHealthCheckDisabled(): boolean { ); } +/** + * Providers excluded from the PROACTIVE refresh sweep, comma-separated and + * case-insensitive (e.g. "codex,openai"). A targeted alternative to the blunt + * OMNIROUTE_DISABLE_TOKEN_HEALTHCHECK switch: it lets an operator keep the + * rotating-token cascade providers (Codex/OpenAI share one Auth0 family) off the + * proactive sweep — leaving their refresh to the reactive, serialized 401 path — + * WITHOUT also starving short-TTL providers like Kimi-coding, whose tokens expire + * while idle when the whole sweep is disabled. + */ +function getHealthCheckSkipProviders(): Set { + const raw = process.env.OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS || ""; + return new Set( + raw + .split(",") + .map((s) => s.trim().toLowerCase()) + .filter(Boolean) + ); +} + // ── Logging helper ─────────────────────────────────────────────────────────── let cachedHideLogs: boolean | null = null; let cacheTimestamp = 0; @@ -265,6 +284,13 @@ export async function checkConnection(conn) { const latestConnection = (await getProviderConnectionById(conn.id)) || conn; conn = latestConnection; + // Per-provider opt-out of proactive refresh (e.g. Codex/OpenAI cascade + // providers) — their token stays on the reactive, serialized 401 path while + // other providers keep being refreshed proactively. + if (getHealthCheckSkipProviders().has(String(conn.provider || "").toLowerCase())) { + return; + } + // Determine interval (0 = disabled) const intervalMin = conn.healthCheckInterval ?? DEFAULT_HEALTH_CHECK_INTERVAL_MIN; if (intervalMin <= 0) return; diff --git a/tests/unit/token-health-check.test.ts b/tests/unit/token-health-check.test.ts index 8f7cb92fcd..cb0b912d42 100644 --- a/tests/unit/token-health-check.test.ts +++ b/tests/unit/token-health-check.test.ts @@ -398,3 +398,74 @@ test("checkConnection skips interval refresh when token expiry is known and stil } ); }); + +test("checkConnection skips providers listed in OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS (#kimi-15)", async () => { + await resetStorage(); + + const providerId = "custom-oauth-skip-list"; + const refreshRequests: string[] = []; + const prevSkip = process.env.OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS; + + await withHttpServer( + (req, res) => { + let body = ""; + req.setEncoding("utf8"); + req.on("data", (chunk) => { + body += chunk; + }); + req.on("end", () => { + refreshRequests.push(body); + res.writeHead(200, { "Content-Type": "application/json" }); + res.end( + JSON.stringify({ + access_token: "should-not-be-fetched", + refresh_token: "should-not-be-fetched", + expires_in: 3600, + }) + ); + }); + }, + async (tokenServer) => { + await withPatchedProvider( + providerId, + { + tokenUrl: `${tokenServer.url}/token`, + clientId: "skip-client-id", + clientSecret: "skip-client-secret", + }, + async () => { + const connection = await providersDb.createProviderConnection({ + provider: providerId, + authType: "oauth", + name: "Skip-list Account", + email: "skip@example.com", + accessToken: "stale-access-token", + refreshToken: "refresh-token-skip", + isActive: true, + }); + + // The connection is due for refresh (no known expiry, never checked). + // With the provider listed, the proactive sweep must skip it entirely — + // NO refresh request is made. + process.env.OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS = `foo, ${providerId} ,bar`; + await tokenHealthCheck.checkConnection(connection); + assert.equal( + refreshRequests.length, + 0, + "listed provider must NOT trigger a proactive refresh" + ); + + // Control: with the provider no longer listed, the same due connection + // IS refreshed — proving the skip (not token freshness) gated it. + process.env.OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS = "some-other-provider"; + const stillStale = await providersDb.getProviderConnectionById((connection as any).id); + await tokenHealthCheck.checkConnection(stillStale); + assert.equal(refreshRequests.length, 1, "non-listed provider must refresh"); + } + ); + } + ); + + if (prevSkip === undefined) delete process.env.OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS; + else process.env.OMNIROUTE_HEALTHCHECK_SKIP_PROVIDERS = prevSkip; +});