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.
This commit is contained in:
Andrew Munsell
2026-05-11 13:09:41 -07:00
parent 0594b1d2ea
commit 51c4e1cdc2
4 changed files with 56 additions and 7 deletions

View File

@@ -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,

View File

@@ -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 };

View File

@@ -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: {

View File

@@ -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",