From 9613025219a7a5db1dce140080dc423085fccfcd Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Sun, 14 Jun 2026 21:40:06 -0300 Subject: [PATCH] fix(oauth): clear GitLab Duo setup message instead of 500 (#3861) (#3868) --- CHANGELOG.md | 1 + file-size-baseline.json | 5 +++-- .../api/oauth/[provider]/[action]/route.ts | 13 +++++++++++ src/lib/oauth/providers/gitlab-duo.ts | 8 ++++--- tests/unit/oauth-providers-config.test.ts | 22 +++++++++++++++++++ 5 files changed, 44 insertions(+), 5 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 71f8d9264c..74b68b2ffc 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,7 @@ ### 🐛 Fixed +- **fix(oauth): clear setup message for GitLab Duo instead of "Internal server error"** — adding a GitLab Duo connection without a registered OAuth client returned an opaque `Internal server error` at the Add Connection step. `buildAuthUrl` **threw** when `GITLAB_DUO_OAUTH_CLIENT_ID` was missing, and the route swallowed it into a generic 500. It now returns `null` (mirroring the Qoder provider) and the authorize route surfaces an actionable message: register an OAuth app at `https://gitlab.com/-/profile/applications` with redirect URI `http://localhost:20128/callback` and scopes `ai_features read_user`, then set `GITLAB_DUO_OAUTH_CLIENT_ID`. ([#3861](https://github.com/diegosouzapw/OmniRoute/issues/3861) — thanks @sidinsearch) - **fix(db): persist the "Keep latest backups" retention setting** — changing the backup-retention count in Settings → Database backup retention had no effect: it always snapped back to 20 on refresh (and editing `.env` post-start was ignored too, since `process.env` isn't reloaded). `getDbBackupMaxFiles()` only read the `DB_BACKUP_MAX_FILES` env var — there was no setter and no persisted value. The value now round-trips through a dedicated `key_value` store (`getDbBackupMaxFiles` precedence: env override → persisted UI value → default 20), and the "Clean old backups" action persists the chosen count. Existing installs keep the historical default of 20 until explicitly changed. ([#3834](https://github.com/diegosouzapw/OmniRoute/issues/3834) — thanks @netstratego) - **fix(sse): clamp Gemini thinking budget to the model's real cap (`reasoning_effort`/`effort=high` 400)** — translating OpenAI `reasoning_effort=high` (and Claude-Code `output_config.effort=high`) to a Gemini target sent a hardcoded `thinkingBudget: 32768`, which exceeds Flash-tier Gemini's real max of 24576 → upstream HTTP 400 (the `thinkingLevel=high` path already used 24576 and worked on the same model). `gemini-2.5-flash` now declares its real `thinkingBudgetCap` (24576) so the existing `capThinkingBudget()` chokepoint actually clamps, and the Claude→Gemini `output_config.effort` path — which previously sent the raw value with no cap at all — now routes through the same clamp (pro-tier, real cap 32768, is left untouched). ([#3842](https://github.com/diegosouzapw/OmniRoute/issues/3842) — thanks @andrea-kingautomation) - **fix(intelligence): run pricing + models.dev sync from the live startup path** — like the Arena ELO sync (v3.8.24), the external **pricing sync** (`PRICING_SYNC_ENABLED`) and the **models.dev capability sync** (Settings → AI toggle) were only initialized from `server-init.ts`, which the Next standalone runtime never executes — and models.dev had no caller at all. Their toggles were inert in production. Both are now initialized from `instrumentation-node.ts` (self-gated, opt-in preserved, non-blocking, never fatal). (thanks @diegosouzapw) diff --git a/file-size-baseline.json b/file-size-baseline.json index 373ce79811..588031bc46 100644 --- a/file-size-baseline.json +++ b/file-size-baseline.json @@ -72,7 +72,7 @@ "src/app/(dashboard)/dashboard/usage/components/BudgetTab.tsx": 1016, "src/app/(dashboard)/dashboard/usage/components/EvalsTab.tsx": 2148, "src/app/(dashboard)/dashboard/usage/components/ProviderLimits/index.tsx": 1069, - "src/app/api/oauth/[provider]/[action]/route.ts": 903, + "src/app/api/oauth/[provider]/[action]/route.ts": 916, "src/app/api/providers/[id]/models/route.ts": 2489, "src/app/api/providers/[id]/test/route.ts": 842, "src/app/api/usage/analytics/route.ts": 941, @@ -131,5 +131,6 @@ "_rebaseline_2026_06_14_r3_3839_veo_video": "PR #3839 own growth: schemas.ts 2522→2523 (+1 = Veo video model (predictLongRunning) validation for Gemini/Vertex dynamic discovery).", "_rebaseline_2026_06_14_r3_3836_kiro_discovery": "PR #3836 own growth: models/route.ts 2426→2487 (+61 = kiro live per-account discovery branch wiring fetchKiroAvailableModels into the existing cache/auto-fetch/fallback discovery flow). Structural shrink of this route tracked in #3789.", "_rebaseline_2026_06_14_2997_disable_cooling": "Re-baseline #2997 (per-connection disable-cooling): EditConnectionModal.tsx 1171→1174 (+toggle UI) + auth.ts 2207→2216 (honor de disableCooling no markAccountUnavailable, pós-prettier). Lógica coesa; não-extraível. (combo.ts/RequestLoggerV2 drift já documentado em _r3_3835.)", - "_rebaseline_2026_06_14_r3_3848_compression": "PR #3848 own growth: chatCore.ts 5808→5811 (+3 = compression engine pipeline hooks). Also carries inherited release/v3.8.25 drift not touched by this PR: models/route.ts 2487→2489 (+2, post-#3836/prettier). Updating the frozen values restores Fast Quality Gates on the current base." + "_rebaseline_2026_06_14_r3_3848_compression": "PR #3848 own growth: chatCore.ts 5808→5811 (+3 = compression engine pipeline hooks). Also carries inherited release/v3.8.25 drift not touched by this PR: models/route.ts 2487→2489 (+2, post-#3836/prettier). Updating the frozen values restores Fast Quality Gates on the current base.", + "_rebaseline_2026_06_14_3861_gitlab_duo": "PR #3861 own growth: oauth/[provider]/[action]/route.ts 903→916 (+13 = gitlab-duo authorize guard mirroring the existing qoder guard — returns a clear 'register an OAuth app + set GITLAB_DUO_OAUTH_CLIENT_ID' message instead of letting buildAuthUrl's throw become an opaque 500). Cohesive with the qoder branch right above it; not separately extractable." } diff --git a/src/app/api/oauth/[provider]/[action]/route.ts b/src/app/api/oauth/[provider]/[action]/route.ts index 1ebd127c23..053db9a580 100755 --- a/src/app/api/oauth/[provider]/[action]/route.ts +++ b/src/app/api/oauth/[provider]/[action]/route.ts @@ -157,6 +157,19 @@ export async function GET( "Qoder browser OAuth is experimental and disabled by default. Configure QODER_OAUTH_* environment variables or use a Personal Access Token.", }); } + // #3861: GitLab Duo needs a self-registered OAuth app. Without a client_id, + // buildAuthUrl returns null — surface a clear setup message instead of a 500. + if (provider === "gitlab-duo" && !authData.authUrl) { + return NextResponse.json({ + ...authData, + supported: false, + error: + "GitLab Duo OAuth is not configured. Register an OAuth application at " + + "https://gitlab.com/-/profile/applications with redirect URI " + + "http://localhost:20128/callback and scopes \"ai_features read_user\", then set " + + "GITLAB_DUO_OAUTH_CLIENT_ID (and optionally GITLAB_DUO_OAUTH_CLIENT_SECRET) and restart.", + }); + } return NextResponse.json(authData); } diff --git a/src/lib/oauth/providers/gitlab-duo.ts b/src/lib/oauth/providers/gitlab-duo.ts index 7f911dcecc..f20f622916 100644 --- a/src/lib/oauth/providers/gitlab-duo.ts +++ b/src/lib/oauth/providers/gitlab-duo.ts @@ -25,10 +25,12 @@ export const gitlabDuo = { config: GITLAB_DUO_CONFIG, flowType: "authorization_code_pkce", buildAuthUrl: (config, redirectUri, state, codeChallenge) => { + // #3861: GitLab Duo needs an operator-registered OAuth client_id. When it is + // missing, return null (mirroring the Qoder provider) so the route can surface a + // clear "configure it" message instead of letting the throw bubble up as an + // opaque "Internal server error" 500 at the Add Connection step. if (!config.clientId) { - throw new Error( - "GitLab Duo OAuth requires GITLAB_DUO_OAUTH_CLIENT_ID (or GITLAB_OAUTH_CLIENT_ID) to be configured." - ); + return null; } const params = new URLSearchParams({ diff --git a/tests/unit/oauth-providers-config.test.ts b/tests/unit/oauth-providers-config.test.ts index be6fb012c0..20afd19c86 100644 --- a/tests/unit/oauth-providers-config.test.ts +++ b/tests/unit/oauth-providers-config.test.ts @@ -322,6 +322,28 @@ test("browser-based providers expose buildAuthUrl and return provider-specific a assert.equal(clineUrl.origin, "https://api.cline.bot"); }); +// Regression for #3861: GitLab Duo needs an operator-registered OAuth client_id. +// When it's missing, buildAuthUrl must return null (like Qoder) so the authorize route +// can surface a clear "configure it" message — it previously THREW, which the route +// swallowed into an opaque "Internal server error" 500 at the Add Connection step. +test("gitlab-duo buildAuthUrl returns null (not throw) when client_id is unconfigured (#3861)", () => { + const redirectUri = "http://localhost:20128/callback"; + const unconfigured = PROVIDERS["gitlab-duo"].buildAuthUrl( + { ...GITLAB_DUO_CONFIG, clientId: "" }, + redirectUri, + "state-x", + "challenge-y" + ); + assert.equal(unconfigured, null); + + // Configured: returns a real authorize URL carrying the client_id + PKCE challenge. + const configured = new URL( + PROVIDERS["gitlab-duo"].buildAuthUrl(GITLAB_DUO_CONFIG, redirectUri, "state-x", "challenge-y") + ); + assert.equal(configured.searchParams.get("client_id"), GITLAB_DUO_CONFIG.clientId); + assert.equal(configured.searchParams.get("code_challenge"), "challenge-y"); +}); + test("custom Google OAuth credentials switch Antigravity remote callbacks to NEXT_PUBLIC_BASE_URL", () => { const redirectUri = resolveBrowserOAuthRedirectUri( "antigravity",