import React, { useState, useEffect, useCallback } from "react"; import Modal from "./Modal"; import { customModelsApi, CustomModel, CreateCustomModelRequest, TestCustomModelRequest, } from "../services/api"; interface ModelsModalProps { isOpen: boolean; onClose: () => void; onModelsChanged?: () => void; } type ProviderType = "anthropic" | "openai" | "openai-responses" | "gemini"; const DEFAULT_ENDPOINTS: Record = { anthropic: "https://api.anthropic.com/v1/messages", openai: "https://api.openai.com/v1", "openai-responses": "https://api.openai.com/v1", gemini: "https://generativelanguage.googleapis.com/v1beta", }; const PROVIDER_LABELS: Record = { anthropic: "Anthropic", openai: "OpenAI (Chat API)", "openai-responses": "OpenAI (Responses API)", gemini: "Google Gemini", }; const DEFAULT_MODELS: Record = { anthropic: [ { name: "Claude Sonnet 4.5", model_name: "claude-sonnet-4-5" }, { name: "Claude Opus 4.5", model_name: "claude-opus-4-5" }, { name: "Claude Haiku 4.5", model_name: "claude-haiku-4-5" }, ], openai: [{ name: "GPT-5.2", model_name: "gpt-5.2" }], "openai-responses": [{ name: "GPT-5.2 Codex", model_name: "gpt-5.2-codex" }], gemini: [ { name: "Gemini 3 Pro", model_name: "gemini-3-pro-preview" }, { name: "Gemini 3 Flash", model_name: "gemini-3-flash-preview" }, ], }; // Built-in model info from init data interface BuiltInModel { id: string; display_name?: string; source?: string; ready: boolean; } interface FormData { display_name: string; provider_type: ProviderType; endpoint: string; endpoint_custom: boolean; api_key: string; model_name: string; max_tokens: number; tags: string; // Comma-separated tags } const emptyForm: FormData = { display_name: "", provider_type: "anthropic", endpoint: DEFAULT_ENDPOINTS.anthropic, endpoint_custom: false, api_key: "", model_name: "", max_tokens: 200000, tags: "", }; function ModelsModal({ isOpen, onClose, onModelsChanged }: ModelsModalProps) { const [models, setModels] = useState([]); const [loading, setLoading] = useState(true); const [error, setError] = useState(null); const [builtInModels, setBuiltInModels] = useState([]); // Form state const [showForm, setShowForm] = useState(false); const [editingModelId, setEditingModelId] = useState(null); const [form, setForm] = useState(emptyForm); // Test state const [testing, setTesting] = useState(false); const [testResult, setTestResult] = useState<{ success: boolean; message: string } | null>(null); // Tooltip state const [showTagsTooltip, setShowTagsTooltip] = useState(false); const loadModels = useCallback(async () => { try { setLoading(true); setError(null); const result = await customModelsApi.getCustomModels(); setModels(result); } catch (err) { setError(err instanceof Error ? err.message : "Failed to load models"); } finally { setLoading(false); } }, []); useEffect(() => { if (isOpen) { loadModels(); // Get built-in models from init data (those with non-custom source) const initData = window.__SHELLEY_INIT__; if (initData?.models) { const builtIn = initData.models.filter( (m: BuiltInModel) => m.source && m.source !== "custom", ); setBuiltInModels(builtIn); } } }, [isOpen, loadModels]); const handleProviderChange = (provider: ProviderType) => { setForm((prev) => ({ ...prev, provider_type: provider, endpoint: prev.endpoint_custom ? prev.endpoint : DEFAULT_ENDPOINTS[provider], })); }; const handleEndpointModeChange = (custom: boolean) => { setForm((prev) => ({ ...prev, endpoint_custom: custom, endpoint: custom ? prev.endpoint : DEFAULT_ENDPOINTS[prev.provider_type], })); }; const handleSelectPresetModel = (preset: { name: string; model_name: string }) => { setForm((prev) => ({ ...prev, display_name: preset.name, model_name: preset.model_name, })); }; const handleTest = async () => { // Need model_name always, and either api_key or editing an existing model if (!form.model_name) { setTestResult({ success: false, message: "Model name is required" }); return; } if (!form.api_key && !editingModelId) { setTestResult({ success: false, message: "API key is required" }); return; } setTesting(true); setTestResult(null); try { const request: TestCustomModelRequest = { model_id: editingModelId || undefined, // Pass model_id to use stored key provider_type: form.provider_type, endpoint: form.endpoint, api_key: form.api_key, model_name: form.model_name, }; const result = await customModelsApi.testCustomModel(request); setTestResult(result); } catch (err) { setTestResult({ success: false, message: err instanceof Error ? err.message : "Test failed", }); } finally { setTesting(false); } }; const handleSave = async () => { if (!form.display_name || !form.api_key || !form.model_name) { setError("Display name, API key, and model name are required"); return; } try { setError(null); const request: CreateCustomModelRequest = { display_name: form.display_name, provider_type: form.provider_type, endpoint: form.endpoint, api_key: form.api_key, model_name: form.model_name, max_tokens: form.max_tokens, tags: form.tags, }; if (editingModelId) { await customModelsApi.updateCustomModel(editingModelId, request); } else { await customModelsApi.createCustomModel(request); } setShowForm(false); setEditingModelId(null); setForm(emptyForm); setTestResult(null); await loadModels(); onModelsChanged?.(); } catch (err) { setError(err instanceof Error ? err.message : "Failed to save model"); } }; const handleEdit = (model: CustomModel) => { setEditingModelId(model.model_id); setForm({ display_name: model.display_name, provider_type: model.provider_type, endpoint: model.endpoint, endpoint_custom: model.endpoint !== DEFAULT_ENDPOINTS[model.provider_type], api_key: model.api_key, model_name: model.model_name, max_tokens: model.max_tokens, tags: model.tags, }); setShowForm(true); setTestResult(null); }; const handleDuplicate = async (model: CustomModel) => { try { setError(null); await customModelsApi.duplicateCustomModel(model.model_id); await loadModels(); onModelsChanged?.(); } catch (err) { setError(err instanceof Error ? err.message : "Failed to duplicate model"); } }; const handleDelete = async (modelId: string) => { try { setError(null); await customModelsApi.deleteCustomModel(modelId); await loadModels(); onModelsChanged?.(); } catch (err) { setError(err instanceof Error ? err.message : "Failed to delete model"); } }; const handleCancel = () => { setShowForm(false); setEditingModelId(null); setForm(emptyForm); setTestResult(null); }; const handleAddNew = () => { setEditingModelId(null); setForm(emptyForm); setShowForm(true); setTestResult(null); }; const headerRight = !showForm ? ( ) : null; return (
{error && (
{error}
)} {loading ? (
Loading models...
) : showForm ? ( // Add/Edit form

{editingModelId ? "Edit Model" : "Add Model"}

{/* Provider Selection */}
{(["anthropic", "openai", "openai-responses", "gemini"] as ProviderType[]).map( (p) => ( ), )}
{/* Endpoint Selection */}
{form.endpoint_custom ? ( setForm((prev) => ({ ...prev, endpoint: e.target.value }))} placeholder="https://..." className="form-input" /> ) : (
{form.endpoint}
)}
{/* Model Name with Presets */}
{DEFAULT_MODELS[form.provider_type].map((preset) => ( ))}
setForm((prev) => ({ ...prev, model_name: e.target.value }))} placeholder="Model name (e.g., claude-sonnet-4-5)" className="form-input" />
{/* Display Name */}
setForm((prev) => ({ ...prev, display_name: e.target.value }))} placeholder="Name shown in model selector" className="form-input" />
{/* API Key */}
setForm((prev) => ({ ...prev, api_key: e.target.value }))} placeholder="Enter API key" className="form-input" autoComplete="off" />
{/* Max Tokens */}
setForm((prev) => ({ ...prev, max_tokens: parseInt(e.target.value) || 200000 })) } className="form-input" />
{/* Tags */}
setForm((prev) => ({ ...prev, tags: e.target.value }))} placeholder="comma-separated, e.g., slug, cheap" className="form-input" />
{/* Test Result */} {testResult && (
{testResult.success ? "✓" : "✗"} {testResult.message}
)} {/* Form Actions */}
) : ( // Model List <>
{/* Built-in models (from env vars or gateway) - read only */} {builtInModels .filter((m) => m.id !== "predictable") .map((model) => (
{model.display_name || model.id} {model.source}
{model.id}
))} {/* Custom models - editable */} {models.map((model) => (
{model.display_name} {PROVIDER_LABELS[model.provider_type]} {model.tags && ( {model.tags.split(",")[0]} )}
{model.model_name} {model.endpoint}
))} {/* Empty state when no models at all */} {builtInModels.length === 0 && models.length === 0 && (

No models configured.

Set environment variables like ANTHROPIC_API_KEY, or use the -gateway flag, or add a custom model below.

)}
)}
); } export default ModelsModal;