import { useState } from 'react'; import { useAppStore } from '@/store/app-store'; import { Button } from '@/components/ui/button'; import { Input } from '@/components/ui/input'; import { Label } from '@/components/ui/label'; import { Select, SelectContent, SelectItem, SelectTrigger, SelectValue, } from '@/components/ui/select'; import { Switch } from '@/components/ui/switch'; import { Dialog, DialogContent, DialogDescription, DialogFooter, DialogHeader, DialogTitle, } from '@/components/ui/dialog'; import { cn } from '@/lib/utils'; import { ChevronDown, Eye, EyeOff, ExternalLink, MoreVertical, Pencil, Plus, Server, Settings2, Trash2, Zap, } from 'lucide-react'; import { DropdownMenu, DropdownMenuContent, DropdownMenuItem, DropdownMenuSeparator, DropdownMenuTrigger, } from '@/components/ui/dropdown-menu'; import type { ClaudeCompatibleProvider, ClaudeCompatibleProviderType, ApiKeySource, ProviderModel, ClaudeModelAlias, } from '@automaker/types'; import { CLAUDE_PROVIDER_TEMPLATES } from '@automaker/types'; import { Badge } from '@/components/ui/badge'; // Generate unique ID for providers function generateProviderId(): string { return `provider-${Date.now()}-${Math.random().toString(36).substr(2, 9)}`; } // Mask API key for display (show first 4 + last 4 chars) function maskApiKey(key?: string): string { if (!key || key.length <= 8) return '••••••••'; return `${key.substring(0, 4)}••••${key.substring(key.length - 4)}`; } // Provider type display names const PROVIDER_TYPE_LABELS: Record = { anthropic: 'Anthropic', glm: 'GLM', minimax: 'MiniMax', openrouter: 'OpenRouter', custom: 'Custom', }; // Provider type badge colors const PROVIDER_TYPE_COLORS: Record = { anthropic: 'bg-brand-500/20 text-brand-500', glm: 'bg-emerald-500/20 text-emerald-500', minimax: 'bg-purple-500/20 text-purple-500', openrouter: 'bg-amber-500/20 text-amber-500', custom: 'bg-zinc-500/20 text-zinc-400', }; // Claude model display names const CLAUDE_MODEL_LABELS: Record = { haiku: 'Claude Haiku', sonnet: 'Claude Sonnet', opus: 'Claude Opus', }; interface ModelFormEntry { id: string; displayName: string; mapsToClaudeModel: ClaudeModelAlias; } interface ProviderFormData { name: string; providerType: ClaudeCompatibleProviderType; baseUrl: string; apiKeySource: ApiKeySource; apiKey: string; useAuthToken: boolean; timeoutMs: string; // String for input, convert to number models: ModelFormEntry[]; disableNonessentialTraffic: boolean; } const emptyFormData: ProviderFormData = { name: '', providerType: 'custom', baseUrl: '', apiKeySource: 'inline', apiKey: '', useAuthToken: false, timeoutMs: '', models: [], disableNonessentialTraffic: false, }; // Provider types that have fixed settings (no need to show toggles) const FIXED_SETTINGS_PROVIDERS: ClaudeCompatibleProviderType[] = ['glm', 'minimax']; // Check if provider type has fixed settings function hasFixedSettings(providerType: ClaudeCompatibleProviderType): boolean { return FIXED_SETTINGS_PROVIDERS.includes(providerType); } export function ApiProfilesSection() { const { claudeCompatibleProviders, addClaudeCompatibleProvider, updateClaudeCompatibleProvider, deleteClaudeCompatibleProvider, toggleClaudeCompatibleProviderEnabled, } = useAppStore(); const [isDialogOpen, setIsDialogOpen] = useState(false); const [editingProviderId, setEditingProviderId] = useState(null); const [formData, setFormData] = useState(emptyFormData); const [showApiKey, setShowApiKey] = useState(false); const [deleteConfirmId, setDeleteConfirmId] = useState(null); const [currentTemplate, setCurrentTemplate] = useState< (typeof CLAUDE_PROVIDER_TEMPLATES)[0] | null >(null); const [showModelMappings, setShowModelMappings] = useState(false); const handleOpenAddDialog = (templateName?: string) => { const template = templateName ? CLAUDE_PROVIDER_TEMPLATES.find((t) => t.name === templateName) : undefined; if (template) { setFormData({ name: template.name, providerType: template.providerType, baseUrl: template.baseUrl, apiKeySource: template.defaultApiKeySource ?? 'inline', apiKey: '', useAuthToken: template.useAuthToken, timeoutMs: template.timeoutMs?.toString() ?? '', models: (template.defaultModels || []).map((m) => ({ id: m.id, displayName: m.displayName, mapsToClaudeModel: m.mapsToClaudeModel || 'sonnet', })), disableNonessentialTraffic: template.disableNonessentialTraffic ?? false, }); setCurrentTemplate(template); } else { setFormData(emptyFormData); setCurrentTemplate(null); } setEditingProviderId(null); setShowApiKey(false); // For fixed providers, hide model mappings by default (they have sensible defaults) setShowModelMappings(template ? !hasFixedSettings(template.providerType) : true); setIsDialogOpen(true); }; const handleOpenEditDialog = (provider: ClaudeCompatibleProvider) => { // Find matching template by provider type const template = CLAUDE_PROVIDER_TEMPLATES.find( (t) => t.providerType === provider.providerType ); setFormData({ name: provider.name, providerType: provider.providerType, baseUrl: provider.baseUrl, apiKeySource: provider.apiKeySource ?? 'inline', apiKey: provider.apiKey ?? '', useAuthToken: provider.useAuthToken ?? false, timeoutMs: provider.timeoutMs?.toString() ?? '', models: (provider.models || []).map((m) => ({ id: m.id, displayName: m.displayName, mapsToClaudeModel: m.mapsToClaudeModel || 'sonnet', })), disableNonessentialTraffic: provider.disableNonessentialTraffic ?? false, }); setEditingProviderId(provider.id); setCurrentTemplate(template ?? null); setShowApiKey(false); // For fixed providers, hide model mappings by default when editing setShowModelMappings(!hasFixedSettings(provider.providerType)); setIsDialogOpen(true); }; const handleSave = () => { // For GLM/MiniMax, enforce fixed settings const isFixedProvider = hasFixedSettings(formData.providerType); // Convert form models to ProviderModel format const models: ProviderModel[] = formData.models .filter((m) => m.id.trim()) // Only include models with IDs .map((m) => ({ id: m.id.trim(), displayName: m.displayName.trim() || m.id.trim(), mapsToClaudeModel: m.mapsToClaudeModel, })); const providerData: ClaudeCompatibleProvider = { id: editingProviderId ?? generateProviderId(), name: formData.name.trim(), providerType: formData.providerType, enabled: true, baseUrl: formData.baseUrl.trim(), // For fixed providers, always use inline apiKeySource: isFixedProvider ? 'inline' : formData.apiKeySource, // Only include apiKey when source is 'inline' apiKey: isFixedProvider || formData.apiKeySource === 'inline' ? formData.apiKey : undefined, // For fixed providers, always use auth token useAuthToken: isFixedProvider ? true : formData.useAuthToken, timeoutMs: (() => { const parsed = Number(formData.timeoutMs); return Number.isFinite(parsed) ? parsed : undefined; })(), models, // For fixed providers, always disable non-essential disableNonessentialTraffic: isFixedProvider ? true : formData.disableNonessentialTraffic || undefined, }; if (editingProviderId) { updateClaudeCompatibleProvider(editingProviderId, providerData); } else { addClaudeCompatibleProvider(providerData); } setIsDialogOpen(false); setFormData(emptyFormData); setEditingProviderId(null); }; const handleDelete = (id: string) => { deleteClaudeCompatibleProvider(id); setDeleteConfirmId(null); }; const handleAddModel = () => { setFormData({ ...formData, models: [...formData.models, { id: '', displayName: '', mapsToClaudeModel: 'sonnet' }], }); }; const handleUpdateModel = (index: number, updates: Partial) => { const newModels = [...formData.models]; newModels[index] = { ...newModels[index], ...updates }; setFormData({ ...formData, models: newModels }); }; const handleRemoveModel = (index: number) => { setFormData({ ...formData, models: formData.models.filter((_, i) => i !== index), }); }; // Check for duplicate provider name (case-insensitive, excluding current provider when editing) const isDuplicateName = claudeCompatibleProviders.some( (p) => p.name.toLowerCase() === formData.name.trim().toLowerCase() && p.id !== editingProviderId ); // For fixed providers, API key is always required (inline only) // For others, only required when source is 'inline' const isFixedProvider = hasFixedSettings(formData.providerType); const isFormValid = formData.name.trim().length > 0 && formData.baseUrl.trim().length > 0 && (isFixedProvider ? formData.apiKey.length > 0 : formData.apiKeySource !== 'inline' || formData.apiKey.length > 0) && !isDuplicateName; // Check model coverage const modelCoverage = { hasHaiku: formData.models.some((m) => m.mapsToClaudeModel === 'haiku'), hasSonnet: formData.models.some((m) => m.mapsToClaudeModel === 'sonnet'), hasOpus: formData.models.some((m) => m.mapsToClaudeModel === 'opus'), }; const hasAllMappings = modelCoverage.hasHaiku && modelCoverage.hasSonnet && modelCoverage.hasOpus; return (
{/* Header */}

Model Providers

Configure providers whose models appear in all model selectors

handleOpenAddDialog()}> Custom Provider {CLAUDE_PROVIDER_TEMPLATES.filter((t) => t.providerType !== 'anthropic').map( (template) => ( handleOpenAddDialog(template.name)} > {template.name} ) )}
{/* Content */}
{/* Info Banner */}
Models from enabled providers appear in all model dropdowns throughout the app. You can select different models from different providers for each phase.
{/* Provider List */} {claudeCompatibleProviders.length === 0 ? (

No model providers configured

Add a provider to use alternative Claude-compatible models

) : (
{claudeCompatibleProviders.map((provider) => ( handleOpenEditDialog(provider)} onDelete={() => setDeleteConfirmId(provider.id)} onToggleEnabled={() => toggleClaudeCompatibleProviderEnabled(provider.id)} /> ))}
)}
{/* Add/Edit Dialog */} {editingProviderId ? 'Edit Model Provider' : 'Add Model Provider'} {isFixedProvider ? `Configure ${PROVIDER_TYPE_LABELS[formData.providerType]} endpoint with model mappings to Claude.` : 'Configure a Claude-compatible API endpoint. Models from this provider will appear in all model selectors.'}
{/* Name */}
setFormData({ ...formData, name: e.target.value })} placeholder="e.g., GLM (Work)" className={isDuplicateName ? 'border-destructive' : ''} /> {isDuplicateName && (

A provider with this name already exists

)}
{/* Provider Type - only for custom providers */} {!isFixedProvider && (
)} {/* API Key - always shown first for fixed providers */}
setFormData({ ...formData, apiKey: e.target.value })} placeholder="Enter API key" className="pr-10" />
{currentTemplate?.apiKeyUrl && ( Get API Key from {currentTemplate.name} )}
{/* Base URL - hidden for fixed providers since it's pre-configured */} {!isFixedProvider && (
setFormData({ ...formData, baseUrl: e.target.value })} placeholder="https://api.example.com/v1" />
)} {/* Advanced options for non-fixed providers only */} {!isFixedProvider && ( <> {/* API Key Source */}
{/* Use Auth Token */}

Use ANTHROPIC_AUTH_TOKEN instead of ANTHROPIC_API_KEY

setFormData({ ...formData, useAuthToken: checked }) } />
{/* Disable Non-essential Traffic */}

Sets CLAUDE_CODE_DISABLE_NONESSENTIAL_TRAFFIC=1

setFormData({ ...formData, disableNonessentialTraffic: checked }) } />
)} {/* Timeout */}
setFormData({ ...formData, timeoutMs: e.target.value })} placeholder="Optional, e.g., 3000000" />
{/* Models */}
{/* For fixed providers, show collapsible section */} {isFixedProvider ? ( <>

{formData.models.length} mappings configured (Haiku, Sonnet, Opus)

{/* Expanded model mappings for fixed providers */} {showModelMappings && (
{formData.models.map((model, index) => (
handleUpdateModel(index, { id: e.target.value })} placeholder="e.g., GLM-4.7" className="text-xs h-8" />
handleUpdateModel(index, { displayName: e.target.value }) } placeholder="e.g., GLM 4.7" className="text-xs h-8" />
))}
)} ) : ( <> {/* Non-fixed providers: always show full editing UI */}

Map provider models to Claude equivalents (Haiku, Sonnet, Opus)

{/* Coverage warning - only for non-fixed providers */} {formData.models.length > 0 && !hasAllMappings && (
Missing mappings:{' '} {[ !modelCoverage.hasHaiku && 'Haiku', !modelCoverage.hasSonnet && 'Sonnet', !modelCoverage.hasOpus && 'Opus', ] .filter(Boolean) .join(', ')}
)} {formData.models.length === 0 ? (
No models configured. Add models to use with this provider.
) : (
{formData.models.map((model, index) => (
handleUpdateModel(index, { id: e.target.value })} placeholder="e.g., GLM-4.7" className="text-xs h-8" />
handleUpdateModel(index, { displayName: e.target.value }) } placeholder="e.g., GLM 4.7" className="text-xs h-8" />
))}
)} )}
{/* Delete Confirmation Dialog */} !open && setDeleteConfirmId(null)}> Delete Provider? This will permanently delete the provider and its models. Any phase model configurations using these models will need to be updated.
); } interface ProviderCardProps { provider: ClaudeCompatibleProvider; onEdit: () => void; onDelete: () => void; onToggleEnabled: () => void; } function ProviderCard({ provider, onEdit, onDelete, onToggleEnabled }: ProviderCardProps) { const isEnabled = provider.enabled !== false; return (

{provider.name}

{PROVIDER_TYPE_LABELS[provider.providerType]} {!isEnabled && ( Disabled )}

{provider.baseUrl}

Key: {maskApiKey(provider.apiKey)} {provider.models?.length || 0} model(s)
{/* Show models with their Claude mapping */} {provider.models && provider.models.length > 0 && (
{provider.models.map((model) => ( {model.displayName || model.id} {model.mapsToClaudeModel && ( → {CLAUDE_MODEL_LABELS[model.mapsToClaudeModel]} )} ))}
)}
Edit Delete
); }