From fd7bafe392057c4e188e01b8d443d2a269ff252b Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Thu, 21 May 2026 23:58:02 -0300 Subject: [PATCH] fix(validation): strip trailing /models in Gemini validator to avoid /models/models 404 (#2545) --- src/lib/providers/validation.ts | 6 ++++- .../provider-validation-hardening.test.ts | 25 +++++++++++++++++++ 2 files changed, 30 insertions(+), 1 deletion(-) diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index b36c209e34..41b00aa5df 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -741,11 +741,15 @@ async function validateGeminiLikeProvider({ return { valid: false, error: "Missing base URL" }; } + // Strip a trailing /models before appending — the default Gemini registry baseUrl is + // `.../v1beta/models` (for the chat urlBuilder), so naively appending /models produced + // `.../v1beta/models/models` → upstream 404 on connection validation (#2545). + const baseForModels = baseUrl.replace(/\/models\/?$/, ""); const requestUrl = typeof providerSpecificData?.modelsUrl === "string" && providerSpecificData.modelsUrl.trim() !== "" ? providerSpecificData.modelsUrl.trim() - : `${baseUrl}/models`; + : `${baseForModels}/models`; const urlWithKey = authType === "query" ? `${requestUrl}?key=${encodeURIComponent(apiKey)}` : requestUrl; diff --git a/tests/unit/provider-validation-hardening.test.ts b/tests/unit/provider-validation-hardening.test.ts index 8858c87dbd..9bbeba99e9 100644 --- a/tests/unit/provider-validation-hardening.test.ts +++ b/tests/unit/provider-validation-hardening.test.ts @@ -163,3 +163,28 @@ test("#2463 openai-compatible validation does not throw on non-string modelsUrl" }); assert.equal(typeof result.valid, "boolean"); }); + +// Regression for #2545: the default Gemini (AI Studio) base URL ends in /v1beta/models, +// so the validator must not append a second /models (which produced /models/models → 404). +test("#2545 gemini validation does not produce /models/models", async () => { + const calls: string[] = []; + globalThis.fetch = async (url: any) => { + calls.push(String(url)); + return new Response(JSON.stringify({ models: [] }), { status: 200 }); + }; + const result = await validateProviderApiKey({ + provider: "gemini", + apiKey: "AIzaTestKey", + providerSpecificData: {}, + }); + assert.equal(typeof result.valid, "boolean"); + assert.ok(calls.length > 0, "validator must make a request"); + assert.ok( + !calls.some((u) => u.includes("/models/models")), + `outbound URL must not contain /models/models — got ${calls.join(", ")}` + ); + assert.ok( + calls.some((u) => /\/v1beta\/models(\?|$)/.test(u)), + `outbound URL must hit a single /models segment — got ${calls.join(", ")}` + ); +});