// Works out which provider an API key belongs to, so users never have to know // what a Base URL is. // // The old flow asked for three things — Base URL, model name and key — on the // first screen a non-technical user ever saw. Every key in practice announces // its own provider through its prefix, so ask for the key and derive the rest. // Order matters: the longest, most specific prefixes are checked first, because // several providers issue keys that also start with "sk-". const PROVIDERS = [ { id: 'anthropic', label: 'Anthropic Claude', prefixes: ['sk-ant-'], baseUrl: 'https://api.anthropic.com/v1', model: 'claude-sonnet-4-20250514', models: ['claude-sonnet-4-20250514', 'claude-opus-4-20250514', 'claude-haiku-4-5-20251001'], }, { id: 'openrouter', label: 'OpenRouter', prefixes: ['sk-or-v1-', 'sk-or-'], baseUrl: 'https://openrouter.ai/api/v1', model: 'anthropic/claude-sonnet-4', models: ['anthropic/claude-sonnet-4', 'openai/gpt-4o', 'google/gemini-2.5-flash'], }, { id: 'google', label: 'Google Gemini', prefixes: ['AIza'], baseUrl: 'https://generativelanguage.googleapis.com/v1beta/openai', model: 'gemini-2.5-flash', models: ['gemini-2.5-flash', 'gemini-2.5-pro'], }, { id: 'groq', label: 'Groq', prefixes: ['gsk_'], baseUrl: 'https://api.groq.com/openai/v1', model: 'llama-3.3-70b-versatile', models: ['llama-3.3-70b-versatile', 'llama-3.1-8b-instant'], }, { id: 'sealion', label: 'SEA-LION', prefixes: ['sk-sealion-'], baseUrl: 'https://api.sea-lion.ai/v1', model: 'aisingapore/Qwen-SEA-LION-v4.5-27B-IT', models: ['aisingapore/Qwen-SEA-LION-v4.5-27B-IT', 'aisingapore/Llama-SEA-LION-v3.5-70B-R'], }, { id: 'openai', label: 'OpenAI', prefixes: ['sk-proj-', 'sk-svcacct-', 'sk-'], baseUrl: 'https://api.openai.com/v1', model: 'gpt-4o', models: ['gpt-4o', 'gpt-4o-mini', 'o3-mini'], }, ]; export function detectProvider(rawKey) { const key = String(rawKey ?? '').trim(); if (!key) return null; for (const provider of PROVIDERS) { if (provider.prefixes.some((prefix) => key.startsWith(prefix))) { return { id: provider.id, label: provider.label, baseUrl: provider.baseUrl, model: provider.model, models: provider.models }; } } return null; } // Maps a failed request to a message key the UI can translate. Every branch has // to name something the user can actually do next — "401 Unauthorized" on its // own tells a non-technical user nothing. export function classifyFailure({ status, code } = {}) { if (code === 'ENOTFOUND' || code === 'EAI_AGAIN' || code === 'ECONNREFUSED') return 'key.err_offline'; if (code === 'ETIMEDOUT' || code === 'UND_ERR_CONNECT_TIMEOUT' || code === 'ABORT_ERR') return 'key.err_timeout'; if (status === 401 || status === 403) return 'key.err_rejected'; if (status === 429) return 'key.err_quota'; if (status === 404) return 'key.err_model'; if (typeof status === 'number' && status >= 500) return 'key.err_provider_down'; return 'key.err_unknown'; } export function listProviders() { return PROVIDERS.map(({ id, label, baseUrl, model, models }) => ({ id, label, baseUrl, model, models })); }