From 51c4e1cdc224667dc63864e562e443103fc3d4fc Mon Sep 17 00:00:00 2001 From: Andrew Munsell Date: Mon, 11 May 2026 13:09:41 -0700 Subject: [PATCH] fix(providers): allow optional-key providers to pass connection test Centralize the requiresApiKey guard into providerAllowsOptionalApiKey() in src/shared/constants/providers.ts so testApiKeyConnection and validateProviderApiKey share the same logic. The test route was missing openai-compatible-* and anthropic-compatible-* checks, causing connection test failures for those provider types when no API key was provided. Test file now imports providerAllowsOptionalApiKey and SELF_HOSTED_CHAT_PROVIDER_IDS directly from the source instead of duplicating them. --- src/app/api/providers/[id]/test/route.ts | 4 ++- src/lib/providers/validation.ts | 8 ++--- src/shared/constants/providers.ts | 10 ++++++ tests/unit/proxy-connection-test.test.ts | 41 ++++++++++++++++++++++++ 4 files changed, 56 insertions(+), 7 deletions(-) diff --git a/src/app/api/providers/[id]/test/route.ts b/src/app/api/providers/[id]/test/route.ts index d1e7ab0b98..5d7c0956b4 100644 --- a/src/app/api/providers/[id]/test/route.ts +++ b/src/app/api/providers/[id]/test/route.ts @@ -21,6 +21,7 @@ import { isGitLabDirectAccessDisabled, resolveGitLabOAuthBaseUrl, } from "@/lib/oauth/gitlab"; +import { providerAllowsOptionalApiKey } from "@/shared/constants/providers"; // OAuth provider test endpoints const OAUTH_TEST_CONFIG = { @@ -522,7 +523,8 @@ async function testOAuthConnection(connection: any) { * Test API key connection */ async function testApiKeyConnection(connection: any) { - if (!connection.apiKey) { + const requiresApiKey = !providerAllowsOptionalApiKey(connection.provider); + if (requiresApiKey && !connection.apiKey) { const error = "Missing API key"; return { valid: false, diff --git a/src/lib/providers/validation.ts b/src/lib/providers/validation.ts index c5e63c264c..57a95eee6d 100644 --- a/src/lib/providers/validation.ts +++ b/src/lib/providers/validation.ts @@ -17,6 +17,7 @@ import { isAnthropicCompatibleProvider, isOpenAICompatibleProvider, isSelfHostedChatProvider, + providerAllowsOptionalApiKey, } from "@/shared/constants/providers"; import { SAFE_OUTBOUND_FETCH_PRESETS, @@ -2967,12 +2968,7 @@ async function validateMuseSparkWebProvider({ apiKey, providerSpecificData = {} } export async function validateProviderApiKey({ provider, apiKey, providerSpecificData = {} }: any) { - const requiresApiKey = - provider !== "searxng-search" && - provider !== "petals" && - !isSelfHostedChatProvider(provider) && - !isOpenAICompatibleProvider(provider) && - !isAnthropicCompatibleProvider(provider); + const requiresApiKey = !providerAllowsOptionalApiKey(provider); if (!provider || (requiresApiKey && !apiKey)) { return { valid: false, error: "Provider and API key required", unsupported: false }; diff --git a/src/shared/constants/providers.ts b/src/shared/constants/providers.ts index f30f276ef0..aed3227f26 100644 --- a/src/shared/constants/providers.ts +++ b/src/shared/constants/providers.ts @@ -1914,6 +1914,16 @@ export function isSelfHostedChatProvider(providerId: unknown): boolean { return typeof providerId === "string" && SELF_HOSTED_CHAT_PROVIDER_IDS.has(providerId); } +export function providerAllowsOptionalApiKey(providerId: unknown): boolean { + return ( + providerId === "searxng-search" || + providerId === "petals" || + isSelfHostedChatProvider(providerId) || + isOpenAICompatibleProvider(providerId) || + isAnthropicCompatibleProvider(providerId) + ); +} + // ── System Providers (virtual, not user-connectable) ────────────────────────── export const SYSTEM_PROVIDERS = { auto: { diff --git a/tests/unit/proxy-connection-test.test.ts b/tests/unit/proxy-connection-test.test.ts index 3d07ad1c99..358c715847 100644 --- a/tests/unit/proxy-connection-test.test.ts +++ b/tests/unit/proxy-connection-test.test.ts @@ -1,5 +1,6 @@ import test from "node:test"; import assert from "node:assert/strict"; +import { providerAllowsOptionalApiKey, SELF_HOSTED_CHAT_PROVIDER_IDS } from "@/shared/constants/providers"; // ── Import test targets from connection test route ────────────────────────── @@ -322,6 +323,46 @@ test("OAuth test config covers all expected providers", () => { } }); +// ── testApiKeyConnection requiresApiKey Check ────────────────────────────── +// Uses the centralized providerAllowsOptionalApiKey from providers.ts + +test("testApiKeyConnection: searxng-search with empty API key does NOT require API key", () => { + assert.equal(providerAllowsOptionalApiKey("searxng-search"), true); +}); + +test("testApiKeyConnection: petals with empty API key does NOT require API key", () => { + assert.equal(providerAllowsOptionalApiKey("petals"), true); +}); + +test("testApiKeyConnection: self-hosted chat providers with empty API key do NOT require API key", () => { + for (const provider of SELF_HOSTED_CHAT_PROVIDER_IDS) { + assert.equal( + providerAllowsOptionalApiKey(provider), + true, + `Expected ${provider} to not require API key` + ); + } +}); + +test("testApiKeyConnection: openai-compatible providers with empty API key do NOT require API key", () => { + assert.equal(providerAllowsOptionalApiKey("openai-compatible-chat-test"), true); +}); + +test("testApiKeyConnection: anthropic-compatible providers with empty API key do NOT require API key", () => { + assert.equal(providerAllowsOptionalApiKey("anthropic-compatible-chat-test"), true); +}); + +test("testApiKeyConnection: providers requiring an API key are correctly identified", () => { + const providersThatRequireKeys = ["openai", "groq", "gemini", "unknown-provider"]; + for (const provider of providersThatRequireKeys) { + assert.equal( + providerAllowsOptionalApiKey(provider), + false, + `Expected ${provider} to require an API key` + ); + } +}); + test("Refreshable OAuth providers are correctly identified", () => { const refreshable = [ "codex",