diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index 083f41c078..5825d71308 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -445,16 +445,22 @@ async function validateAnthropicCompatibleProvider({ apiKey, providerSpecificDat async function validateSearchProvider( url: string, init: RequestInit -): Promise<{ valid: boolean; error: string | null }> { +): Promise<{ valid: boolean; error: string | null; unsupported: false }> { try { const response = await fetch(url, init); - if (response.ok) return { valid: true, error: null }; + if (response.ok) return { valid: true, error: null, unsupported: false }; if (response.status === 401 || response.status === 403) { - return { valid: false, error: "Invalid API key" }; + return { valid: false, error: "Invalid API key", unsupported: false }; } - return { valid: false, error: `Validation failed: ${response.status}` }; + // For provider setup we only need to confirm authentication passed. + // Search providers may return non-auth statuses for exhausted credits, + // rate limiting, or request-shape quirks while still accepting the key. + if (response.status < 500) { + return { valid: true, error: null, unsupported: false }; + } + return { valid: false, error: `Validation failed: ${response.status}`, unsupported: false }; } catch (error: any) { - return { valid: false, error: error.message || "Validation failed" }; + return { valid: false, error: error.message || "Validation failed", unsupported: false }; } } diff --git a/tests/unit/search-provider-validation.test.mjs b/tests/unit/search-provider-validation.test.mjs new file mode 100644 index 0000000000..f05aa51864 --- /dev/null +++ b/tests/unit/search-provider-validation.test.mjs @@ -0,0 +1,50 @@ +import test from "node:test"; +import assert from "node:assert/strict"; + +const { validateProviderApiKey } = await import("../../src/lib/providers/validation.ts"); + +test("serper validation accepts authenticated non-auth upstream errors", async () => { + const originalFetch = globalThis.fetch; + + globalThis.fetch = async () => + new Response(JSON.stringify({ error: "credits_exhausted" }), { + status: 402, + headers: { "content-type": "application/json" }, + }); + + try { + const result = await validateProviderApiKey({ + provider: "serper-search", + apiKey: "valid-serper-key", + }); + + assert.equal(result.valid, true); + assert.equal(result.error, null); + assert.equal(result.unsupported, false); + } finally { + globalThis.fetch = originalFetch; + } +}); + +test("serper validation still rejects unauthorized keys", async () => { + const originalFetch = globalThis.fetch; + + globalThis.fetch = async () => + new Response(JSON.stringify({ error: "Unauthorized" }), { + status: 403, + headers: { "content-type": "application/json" }, + }); + + try { + const result = await validateProviderApiKey({ + provider: "serper-search", + apiKey: "bad-serper-key", + }); + + assert.equal(result.valid, false); + assert.equal(result.error, "Invalid API key"); + assert.equal(result.unsupported, false); + } finally { + globalThis.fetch = originalFetch; + } +});