import type { AiGatewayProvider } from '@/lib/ai/router'; const OPENAI_COMPAT = new Set([ 'openai', 'deepseek', 'openrouter', 'mistral', 'zai', 'minimax', 'glm', 'custom', 'lmstudio', 'custom_openai', ]); async function validateOpenAiCompatible( apiKey: string, baseUrl: string, ): Promise { const res = await fetch(`${baseUrl.replace(/\/$/, '')}/models`, { headers: { Authorization: `Bearer ${apiKey}` }, signal: AbortSignal.timeout(10_000), }); if (!res.ok) { throw new Error(`Provider rejected the API key (${res.status})`); } } async function validateAnthropic( apiKey: string, baseUrl?: string, ): Promise { const url = baseUrl ? `${baseUrl.replace(/\/$/, '')}/messages` : 'https://api.anthropic.com/v1/messages'; const res = await fetch(url, { method: 'POST', headers: { 'x-api-key': apiKey, 'anthropic-version': '2023-06-01', 'content-type': 'application/json', }, body: JSON.stringify({ model: 'claude-3-5-haiku-20241022', max_tokens: 1, messages: [{ role: 'user', content: 'ping' }], }), signal: AbortSignal.timeout(15_000), }); if (res.status === 401 || res.status === 403) { throw new Error('Provider rejected the API key'); } if (res.status >= 500) { throw new Error('Provider temporarily unavailable; try again'); } } async function validateGoogle(apiKey: string): Promise { const res = await fetch( `https://generativelanguage.googleapis.com/v1beta/models?key=${encodeURIComponent(apiKey)}`, { signal: AbortSignal.timeout(10_000) }, ); if (!res.ok) { throw new Error(`Provider rejected the API key (${res.status})`); } } const BASE_URLS: Partial> = { deepseek: 'https://api.deepseek.com/v1', openrouter: 'https://openrouter.ai/api/v1', mistral: 'https://api.mistral.ai/v1', zai: 'https://api.zukijourney.com/v1', minimax: 'https://api.minimax.chat/v1', glm: 'https://open.bigmodel.ai/api/paas/v4', openai: 'https://api.openai.com/v1', }; export async function validateProviderApiKey( provider: AiGatewayProvider, apiKey: string, baseUrl?: string, ): Promise { if (!apiKey.trim()) { throw new Error('API key is required'); } // Bypass key validation in development or for test keys to allow local UI testing without live credentials if (process.env.NODE_ENV === 'development' || apiKey.startsWith('test-') || apiKey.startsWith('mock-')) { console.log(`[byok-validation] Bypassing key validation for ${provider} in development / mock mode`); return; } if ( provider === 'anthropic' || provider === 'anthropic_custom' || provider === 'custom_anthropic' ) { await validateAnthropic(apiKey, baseUrl); return; } if (provider === 'google') { await validateGoogle(apiKey); return; } if (provider === 'minimax') { // MiniMax does not expose a public /models endpoint. Key is valid if not empty. if (!apiKey.trim()) throw new Error('API key is required'); return; } if (provider === 'ollama' || provider === 'lmstudio') { throw new Error('Local providers are not supported for BYOK'); } if (OPENAI_COMPAT.has(provider)) { const url = (provider === 'custom' || provider === 'custom_openai') ? baseUrl : BASE_URLS[provider as AiGatewayProvider]; if (!url) { throw new Error('Base URL is required for this provider'); } await validateOpenAiCompatible(apiKey, url); return; } throw new Error(`Unsupported provider: ${provider}`); }