import { useEffect, useId, useState } from "react"; import { createPortal } from "react-dom"; import { invoke } from "@tauri-apps/api/core"; import { useTranslation } from "../i18n"; import type { TranslationKey } from "../i18n/zh"; import { getDefaultMaxContextSize } from "../lib/model-defaults"; import { capabilitiesFromRef, getModelRef } from "../lib/models-dev"; import { getIconMetadata } from "../icons/extracted/metadata"; import { AgentSettingsPanel } from "./AgentSettingsPanel"; import { ProviderIcon } from "./ProviderIcon"; import { IconPicker } from "./IconPicker"; import type { Agent, DiscoveredModel, Model, Provider, ProviderType } from "../types"; const KNOWN_CAPABILITIES = [ "thinking", "always_thinking", "image_in", "video_in", "tool_use", ] as const; const CAPABILITY_LABELS: Record< (typeof KNOWN_CAPABILITIES)[number], TranslationKey > = { thinking: "capThinking", always_thinking: "capAlwaysThinking", image_in: "capImageIn", video_in: "capVideoIn", tool_use: "capToolUse", }; const PROVIDER_TYPES: ProviderType[] = [ "openai", "openai_responses", "anthropic", "google-genai", "vertexai", "kimi", ]; function defaultBaseUrl(agent: Agent, type: ProviderType): string { if (agent === "kimi_code") { switch (type) { case "kimi": return "https://api.kimi.com/coding/v1"; case "openai": case "openai_responses": return "https://api.openai.com/v1"; case "google-genai": return "https://generativelanguage.googleapis.com"; case "anthropic": case "vertexai": return ""; } } // Pi defaults switch (type) { case "kimi": return "https://api.moonshot.ai/v1"; case "openai": case "openai_responses": return "https://api.openai.com/v1"; case "google-genai": return "https://generativelanguage.googleapis.com"; case "anthropic": case "vertexai": return ""; } } interface ProviderEditProps { agent: Agent; provider: Provider; models: Model[]; defaultModel: string | null; rawOther: unknown; onRawOtherChange: (nextRawOther: unknown) => void; onBack: () => void; onChange: (provider: Provider) => void; onDelete: () => void; onModelChange: (model: Model) => void; onModelDelete: (alias: string) => void; onModelAdd: () => void; onBulkAdd: (models: Model[]) => void; onSetDefault: (alias: string) => void; onApplyJson: (provider: Provider, models: Model[]) => void; onSave: () => void; } export function ProviderEdit({ agent, provider, models, defaultModel, rawOther, onRawOtherChange, onBack, onChange, onDelete, onModelChange, onModelDelete, onModelAdd, onBulkAdd, onSetDefault, onApplyJson, onSave, }: ProviderEditProps) { const { t } = useTranslation(); const nameId = useId(); const noteId = useId(); const officialUrlId = useId(); const apiKeyId = useId(); const baseUrlId = useId(); const [activeTab, setActiveTab] = useState<"basic" | "models" | "json">("basic"); const [showApiKey, setShowApiKey] = useState(false); const [showIconPicker, setShowIconPicker] = useState(false); useEffect(() => { const def = defaultBaseUrl(agent, provider.provider_type); if (!def) return; if (!provider.base_url) { onChange({ ...provider, base_url: def }); } }, [provider.provider_type, agent]); return (
{/* Header */}

{t("editProvider")}

{/* Tabs */}
{[ { key: "basic", label: t("basicInfo") }, { key: "models", label: t("modelMapping") }, { key: "json", label: t("configJson") }, ].map((tab) => ( ))}
{/* Content */}
{activeTab === "basic" && (
{/* Basic info */}

{t("basicInfo")}

{/* Icon selector */}
{provider.icon && ( )}
onChange({ ...provider, name: e.target.value })} />
onChange({ ...provider, note: e.target.value || null }) } />
onChange({ ...provider, official_url: e.target.value || null }) } />
onChange({ ...provider, managed: e.target.checked }) } className="w-4 h-4 rounded border-border bg-input text-blue-600 focus:ring-blue-500" />
{/* API settings */}

{t("apiSettings")}

{!provider.managed && (
onChange({ ...provider, api_key: e.target.value || null }) } />
)}
onChange({ ...provider, base_url: e.target.value || null }) } />
{t("baseUrlHint", { type: provider.provider_type })}
)} {activeTab === "models" && ( <> {agent === "kimi_code" && ( )} )} {activeTab === "json" && ( )}
{showIconPicker && createPortal(
setShowIconPicker(false)} role="dialog" aria-modal="true" >
e.stopPropagation()} >

{t("selectIcon")}

{ const meta = getIconMetadata(iconName); onChange({ ...provider, icon: iconName, icon_color: meta?.defaultColor ?? null, }); setShowIconPicker(false); }} />
, document.body )}
); } function ModelMapping({ agent, provider, models, defaultModel, onModelChange, onModelDelete, onModelAdd, onBulkAdd, onSetDefault, }: { agent: Agent; provider: Provider; models: Model[]; defaultModel: string | null; onModelChange: (model: Model) => void; onModelDelete: (alias: string) => void; onModelAdd: () => void; onBulkAdd: (models: Model[]) => void; onSetDefault: (alias: string) => void; }) { const { t } = useTranslation(); const [discovering, setDiscovering] = useState(false); const [discovered, setDiscovered] = useState(null); const [selected, setSelected] = useState>(new Set()); const [discoverError, setDiscoverError] = useState(null); const [fetchThinking, setFetchThinking] = useState(true); const handleDiscover = async () => { setDiscovering(true); setDiscoverError(null); setDiscovered(null); setSelected(new Set()); try { const result = await invoke("list_provider_models", { provider, }); setDiscovered(result); } catch (err) { setDiscoverError(err instanceof Error ? err.message : String(err)); } finally { setDiscovering(false); } }; const toggleSelect = (id: string) => { setSelected((prev) => { const next = new Set(prev); if (next.has(id)) next.delete(id); else next.add(id); return next; }); }; const handleAddSelected = () => { if (!discovered || selected.size === 0) return; const toAdd: Model[] = []; for (const dm of discovered) { if (!selected.has(dm.id)) continue; const safeProvider = provider.name.replace(/\//g, "-"); const safeModelId = dm.id.replace(/\//g, "-"); const alias = `${safeProvider}/${safeModelId}`; const ref = getModelRef(dm.id); // Priority: API-provided value > models.dev reference > regex fallback const max_context_size = dm.max_context_size ?? ref?.context ?? getDefaultMaxContextSize(dm.id); toAdd.push({ alias, provider: provider.name, model: dm.id, max_context_size, display_name: dm.display_name ?? ref?.name ?? null, supports_1m: max_context_size >= 1_000_000, // Reference data wins when available; otherwise keep the old // fetchThinking-checkbox behavior as the fallback. capabilities: ref ? capabilitiesFromRef(ref) : fetchThinking ? ["thinking"] : [], }); } onBulkAdd(toAdd); setDiscovered(null); setSelected(new Set()); }; return (

{t("modelMapping")}

{t("modelMappingDesc")}

{discoverError && (
{discoverError}
)} {discovered && (
{t("discoveredModels", { count: discovered.length })}
{discovered.map((m) => ( ))}
)}
{agent === "kimi_code" && ( )} {models.map((m) => ( {agent === "kimi_code" && ( )} ))}
{t("displayName")} {t("actualModel")} {t("contextSize")} {t("supports1M")}{t("capabilities")}{t("default")} {t("operation")}
{m.model ? `${m.model}[${m.provider}]` : m.alias} onModelChange({ ...m, model: e.target.value }) } /> { const value = parseInt(e.target.value, 10); onModelChange({ ...m, max_context_size: isNaN(value) ? 0 : value, }); }} /> onModelChange({ ...m, supports_1m: e.target.checked, }) } className="w-4 h-4 rounded border-border bg-input text-blue-600 focus:ring-blue-500" /> onModelChange({ ...m, capabilities: next }) } />
{models.length === 0 && (
{t("noModelMappings")}
)}
); } function JsonPreview({ provider, models, onApply, }: { provider: Provider; models: Model[]; onApply: (provider: Provider, models: Model[]) => void; }) { const { t } = useTranslation(); const [text, setText] = useState(() => JSON.stringify({ provider, models }, null, 2) ); const [error, setError] = useState(null); useEffect(() => { setText(JSON.stringify({ provider, models }, null, 2)); setError(null); }, [provider, models]); const handleApply = () => { try { const parsed = JSON.parse(text); if (!parsed.provider || typeof parsed.provider !== "object") { throw new Error("missing provider object"); } if (!Array.isArray(parsed.models)) { throw new Error("models must be an array"); } onApply(parsed.provider as Provider, parsed.models as Model[]); setError(null); } catch (err) { setError(err instanceof Error ? err.message : String(err)); } }; return (

{t("configJson")}

{t("readOnlyPreview")}
{error && (
{t("invalidJson", { message: error })}
)}