import { useState } from 'react'; import { useLLMCatalog, useSaveApiKey, useProviderApiKey, type LLMProvider, } from '../../hooks/useLLM'; import { Button } from '../../ui/button'; import { Input } from '../../ui/input'; import { Label } from '../../ui/label'; import { Alert, AlertDescription } from '../../ui/alert'; import { Check, Eye, EyeOff, ExternalLink, Loader2 } from 'lucide-react'; // Provider info with display names and key URLs const PROVIDER_INFO: Record< string, { displayName: string; keyUrl?: string; description?: string } > = { openai: { displayName: 'OpenAI', keyUrl: 'https://platform.openai.com/api-keys', description: 'GPT models', }, anthropic: { displayName: 'Anthropic', keyUrl: 'https://console.anthropic.com/settings/keys', description: 'Claude models', }, google: { displayName: 'Google AI', keyUrl: 'https://aistudio.google.com/apikey', description: 'Gemini models (Free tier available)', }, groq: { displayName: 'Groq', keyUrl: 'https://console.groq.com/keys', description: 'Fast inference', }, xai: { displayName: 'xAI', keyUrl: 'https://console.x.ai/team/default/api-keys', description: 'Grok models', }, cohere: { displayName: 'Cohere', keyUrl: 'https://dashboard.cohere.com/api-keys', description: 'Command models', }, openrouter: { displayName: 'OpenRouter', keyUrl: 'https://openrouter.ai/keys', description: 'Multi-provider gateway', }, glama: { displayName: 'Glama', keyUrl: 'https://glama.ai/settings/api-keys', description: 'OpenAI-compatible', }, ollama: { displayName: 'Ollama', description: 'Local models (no key needed)', }, local: { displayName: 'Local', description: 'GGUF models (no key needed)', }, }; // Providers that don't need API keys or need special configuration // These are handled by the ModelPicker's custom model form instead const EXCLUDED_PROVIDERS = [ 'ollama', // Local, no key needed 'local', // Local GGUF, no key needed 'openai-compatible', // Needs baseURL + model name (use ModelPicker) 'litellm', // Needs baseURL (use ModelPicker) 'bedrock', // Uses AWS credentials, not API key 'vertex', // Uses Google Cloud ADC, not API key ]; type ProviderRowProps = { provider: LLMProvider; hasKey: boolean; envVar: string; onSave: (key: string) => Promise; }; function ProviderRow({ provider, hasKey, envVar, onSave }: ProviderRowProps) { const [isEditing, setIsEditing] = useState(false); const [apiKey, setApiKey] = useState(''); const [showKey, setShowKey] = useState(false); const [error, setError] = useState(null); const [isSaving, setIsSaving] = useState(false); const [saveSuccess, setSaveSuccess] = useState(false); const info = PROVIDER_INFO[provider] || { displayName: provider }; // Query for masked key value when has key const { data: keyData } = useProviderApiKey(hasKey ? provider : null); const handleSave = async () => { if (!apiKey.trim()) { setError('API key is required'); return; } setError(null); setIsSaving(true); try { await onSave(apiKey); setApiKey(''); setIsEditing(false); setSaveSuccess(true); setTimeout(() => setSaveSuccess(false), 2000); } catch (err) { setError(err instanceof Error ? err.message : 'Failed to save'); } finally { setIsSaving(false); } }; const handleCancel = () => { setIsEditing(false); setApiKey(''); setError(null); }; return (
{info.displayName} {hasKey && !isEditing && ( Configured )} {saveSuccess && ( Saved! )}
{info.description}
{hasKey && keyData?.keyValue && !isEditing && (
{keyData.keyValue}
)}
{info.keyUrl && ( )} {!isEditing ? ( ) : null}
{isEditing && (
setApiKey(e.target.value)} placeholder={`Enter ${info.displayName} API key`} className="pr-10" />
{error && ( {error} )}
)}
); } export function ApiKeysSection() { const { data: catalog, isLoading, error } = useLLMCatalog({ mode: 'grouped' }); const { mutateAsync: saveApiKey } = useSaveApiKey(); if (isLoading) { return (
); } if (error) { return ( Failed to load providers: {error.message} ); } if (!catalog || !('providers' in catalog)) { return ( No providers available ); } const providers = Object.entries(catalog.providers) as [ LLMProvider, { hasApiKey: boolean; primaryEnvVar: string }, ][]; // Filter out providers handled elsewhere (openai-compatible is in Default Model) const regularProviders = providers.filter(([id]) => !EXCLUDED_PROVIDERS.includes(id)); // Sort: configured first, then by display name const sortedProviders = regularProviders.sort((a, b) => { const aHasKey = a[1].hasApiKey; const bHasKey = b[1].hasApiKey; if (aHasKey !== bHasKey) return bHasKey ? 1 : -1; const aName = PROVIDER_INFO[a[0]]?.displayName || a[0]; const bName = PROVIDER_INFO[b[0]]?.displayName || b[0]; return aName.localeCompare(bName); }); const handleSave = async (provider: LLMProvider, apiKey: string) => { await saveApiKey({ provider, apiKey }); }; return (

API keys are stored securely in your local .env file and are never shared with third parties.

{sortedProviders.map(([provider, info]) => ( handleSave(provider, key)} /> ))}
); }