Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -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)
Expand Down
5 changes: 3 additions & 2 deletions file-size-baseline.json
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down Expand Up @@ -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."
}
13 changes: 13 additions & 0 deletions src/app/api/oauth/[provider]/[action]/route.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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.",
});
Comment on lines +162 to +171

Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

Hardcoding the redirect URI to http://localhost:20128/callback in the error message can be misleading for operators running the application on a custom port, domain, or in a shared/VPS environment (as noted in the repository guide).

It is highly recommended to dynamically construct the redirect URI using the request's origin so that the instructions match the operator's actual deployment setup. Please ensure that the variable name request matches the parameter name of your GET route handler.

Suggested change
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.",
});
if (provider === "gitlab-duo" && !authData.authUrl) {
const origin = typeof request !== "undefined" ? new URL(request.url).origin : "http://localhost:20128";
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 " +
origin + "/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);
}

Expand Down
8 changes: 5 additions & 3 deletions src/lib/oauth/providers/gitlab-duo.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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({
Expand Down
22 changes: 22 additions & 0 deletions tests/unit/oauth-providers-config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down