import { useEffect, useMemo, useState, useCallback, useRef } from "react"; import { useForm } from "react-hook-form"; import { zodResolver } from "@hookform/resolvers/zod"; import { useTranslation } from "react-i18next"; import { toast } from "sonner"; import { Button } from "@/components/ui/button"; import { Form, FormField, FormItem, FormMessage } from "@/components/ui/form"; import { Input } from "@/components/ui/input"; import { providerSchema, type ProviderFormData } from "@/lib/schemas/provider"; import { providersApi, type AppId } from "@/lib/api"; import type { ProviderCategory, ProviderMeta, ProviderTestConfig, ProviderProxyConfig, ClaudeApiFormat, OpenCodeModel, OpenCodeProviderConfig, } from "@/types"; import { providerPresets, type ProviderPreset, } from "@/config/claudeProviderPresets"; import { codexProviderPresets, type CodexProviderPreset, } from "@/config/codexProviderPresets"; import { geminiProviderPresets, type GeminiProviderPreset, } from "@/config/geminiProviderPresets"; import { opencodeProviderPresets, type OpenCodeProviderPreset, } from "@/config/opencodeProviderPresets"; import { OpenCodeFormFields } from "./OpenCodeFormFields"; import type { UniversalProviderPreset } from "@/config/universalProviderPresets"; import { applyTemplateValues } from "@/utils/providerConfigUtils"; import { mergeProviderMeta } from "@/utils/providerMetaUtils"; import { getCodexCustomTemplate } from "@/config/codexTemplates"; import CodexConfigEditor from "./CodexConfigEditor"; import { CommonConfigEditor } from "./CommonConfigEditor"; import GeminiConfigEditor from "./GeminiConfigEditor"; import JsonEditor from "@/components/JsonEditor"; import { Label } from "@/components/ui/label"; import { ProviderPresetSelector } from "./ProviderPresetSelector"; import { BasicFormFields } from "./BasicFormFields"; import { ClaudeFormFields } from "./ClaudeFormFields"; import { CodexFormFields } from "./CodexFormFields"; import { GeminiFormFields } from "./GeminiFormFields"; import { OmoFormFields } from "./OmoFormFields"; import { type OmoGlobalConfigFieldsRef } from "./OmoGlobalConfigFields"; import { OmoCommonConfigEditor } from "./OmoCommonConfigEditor"; import * as configApi from "@/lib/api/config"; import type { OmoGlobalConfig } from "@/types/omo"; import { mergeOmoConfigPreview } from "@/types/omo"; import { ProviderAdvancedConfig, type PricingModelSourceOption, } from "./ProviderAdvancedConfig"; import { useProviderCategory, useApiKeyState, useBaseUrlState, useModelState, useCodexConfigState, useApiKeyLink, useTemplateValues, useCommonConfigSnippet, useCodexCommonConfig, useSpeedTestEndpoints, useCodexTomlValidation, useGeminiConfigState, useGeminiCommonConfig, } from "./hooks"; import { useProvidersQuery } from "@/lib/query/queries"; import { useOmoGlobalConfig } from "@/lib/query/omo"; const CLAUDE_DEFAULT_CONFIG = JSON.stringify({ env: {} }, null, 2); const CODEX_DEFAULT_CONFIG = JSON.stringify({ auth: {}, config: "" }, null, 2); const GEMINI_DEFAULT_CONFIG = JSON.stringify( { env: { GOOGLE_GEMINI_BASE_URL: "", GEMINI_API_KEY: "", GEMINI_MODEL: "gemini-3-pro-preview", }, }, null, 2, ); const OPENCODE_DEFAULT_NPM = "@ai-sdk/openai-compatible"; const OPENCODE_DEFAULT_CONFIG = JSON.stringify( { npm: OPENCODE_DEFAULT_NPM, options: { baseURL: "", apiKey: "", }, models: {}, }, null, 2, ); const OPENCODE_KNOWN_OPTION_KEYS = ["baseURL", "apiKey", "headers"] as const; const isKnownOpencodeOptionKey = (key: string) => OPENCODE_KNOWN_OPTION_KEYS.includes( key as (typeof OPENCODE_KNOWN_OPTION_KEYS)[number], ); function parseOpencodeConfig( settingsConfig?: Record, ): OpenCodeProviderConfig { try { const parsed = JSON.parse( settingsConfig ? JSON.stringify(settingsConfig) : OPENCODE_DEFAULT_CONFIG, ) as Partial; return { npm: parsed.npm || OPENCODE_DEFAULT_NPM, options: parsed.options && typeof parsed.options === "object" ? (parsed.options as OpenCodeProviderConfig["options"]) : {}, models: parsed.models && typeof parsed.models === "object" ? (parsed.models as Record) : {}, }; } catch { return { npm: OPENCODE_DEFAULT_NPM, options: {}, models: {}, }; } } function toOpencodeExtraOptions( options: OpenCodeProviderConfig["options"], ): Record { const extra: Record = {}; for (const [k, v] of Object.entries(options || {})) { if (!isKnownOpencodeOptionKey(k)) { extra[k] = typeof v === "string" ? v : JSON.stringify(v); } } return extra; } type PresetEntry = { id: string; preset: | ProviderPreset | CodexProviderPreset | GeminiProviderPreset | OpenCodeProviderPreset; }; interface ProviderFormProps { appId: AppId; providerId?: string; submitLabel: string; onSubmit: (values: ProviderFormValues) => void; onCancel: () => void; onUniversalPresetSelect?: (preset: UniversalProviderPreset) => void; onManageUniversalProviders?: () => void; initialData?: { name?: string; websiteUrl?: string; notes?: string; settingsConfig?: Record; category?: ProviderCategory; meta?: ProviderMeta; icon?: string; iconColor?: string; }; showButtons?: boolean; } const normalizePricingSource = (value?: string): PricingModelSourceOption => value === "request" || value === "response" ? value : "inherit"; function buildOmoProfilePreview( agents: Record>, categories: Record>, otherFieldsStr: string, ): Record { const profileOnly: Record = {}; if (Object.keys(agents).length > 0) { profileOnly.agents = agents; } if (Object.keys(categories).length > 0) { profileOnly.categories = categories; } if (otherFieldsStr.trim()) { try { const other = JSON.parse(otherFieldsStr); Object.assign(profileOnly, other); } catch {} } return profileOnly; } const EMPTY_OMO_GLOBAL_CONFIG: OmoGlobalConfig = { id: "global", disabledAgents: [], disabledMcps: [], disabledHooks: [], disabledSkills: [], updatedAt: "", }; export function ProviderForm({ appId, providerId, submitLabel, onSubmit, onCancel, onUniversalPresetSelect, onManageUniversalProviders, initialData, showButtons = true, }: ProviderFormProps) { const { t } = useTranslation(); const isEditMode = Boolean(initialData); const [selectedPresetId, setSelectedPresetId] = useState( initialData ? null : "custom", ); const [activePreset, setActivePreset] = useState<{ id: string; category?: ProviderCategory; isPartner?: boolean; partnerPromotionKey?: string; } | null>(null); const [isEndpointModalOpen, setIsEndpointModalOpen] = useState(false); const [isCodexEndpointModalOpen, setIsCodexEndpointModalOpen] = useState(false); const [draftCustomEndpoints, setDraftCustomEndpoints] = useState( () => { if (initialData) return []; return []; }, ); const [endpointAutoSelect, setEndpointAutoSelect] = useState( () => initialData?.meta?.endpointAutoSelect ?? true, ); const [testConfig, setTestConfig] = useState( () => initialData?.meta?.testConfig ?? { enabled: false }, ); const [proxyConfig, setProxyConfig] = useState( () => initialData?.meta?.proxyConfig ?? { enabled: false }, ); const [pricingConfig, setPricingConfig] = useState<{ enabled: boolean; costMultiplier?: string; pricingModelSource: PricingModelSourceOption; }>(() => ({ enabled: initialData?.meta?.costMultiplier !== undefined || initialData?.meta?.pricingModelSource !== undefined, costMultiplier: initialData?.meta?.costMultiplier, pricingModelSource: normalizePricingSource( initialData?.meta?.pricingModelSource, ), })); const { category } = useProviderCategory({ appId, selectedPresetId, isEditMode, initialCategory: initialData?.category, }); const isOmoCategory = appId === "opencode" && category === "omo"; const { data: queriedOmoGlobalConfig } = useOmoGlobalConfig(isOmoCategory); useEffect(() => { setSelectedPresetId(initialData ? null : "custom"); setActivePreset(null); if (!initialData) { setDraftCustomEndpoints([]); } setEndpointAutoSelect(initialData?.meta?.endpointAutoSelect ?? true); setTestConfig(initialData?.meta?.testConfig ?? { enabled: false }); setProxyConfig(initialData?.meta?.proxyConfig ?? { enabled: false }); setPricingConfig({ enabled: initialData?.meta?.costMultiplier !== undefined || initialData?.meta?.pricingModelSource !== undefined, costMultiplier: initialData?.meta?.costMultiplier, pricingModelSource: normalizePricingSource( initialData?.meta?.pricingModelSource, ), }); }, [appId, initialData]); const defaultValues: ProviderFormData = useMemo( () => ({ name: initialData?.name ?? "", websiteUrl: initialData?.websiteUrl ?? "", notes: initialData?.notes ?? "", settingsConfig: initialData?.settingsConfig ? JSON.stringify(initialData.settingsConfig, null, 2) : appId === "codex" ? CODEX_DEFAULT_CONFIG : appId === "gemini" ? GEMINI_DEFAULT_CONFIG : appId === "opencode" ? OPENCODE_DEFAULT_CONFIG : CLAUDE_DEFAULT_CONFIG, icon: initialData?.icon ?? "", iconColor: initialData?.iconColor ?? "", }), [initialData, appId], ); const form = useForm({ resolver: zodResolver(providerSchema), defaultValues, mode: "onSubmit", }); const { apiKey, handleApiKeyChange, showApiKey: shouldShowApiKey, } = useApiKeyState({ initialConfig: form.getValues("settingsConfig"), onConfigChange: (config) => form.setValue("settingsConfig", config), selectedPresetId, category, appType: appId, }); const { baseUrl, handleClaudeBaseUrlChange } = useBaseUrlState({ appType: appId, category, settingsConfig: form.getValues("settingsConfig"), codexConfig: "", onSettingsConfigChange: (config) => form.setValue("settingsConfig", config), onCodexConfigChange: () => {}, }); const { claudeModel, reasoningModel, defaultHaikuModel, defaultSonnetModel, defaultOpusModel, handleModelChange, } = useModelState({ settingsConfig: form.getValues("settingsConfig"), onConfigChange: (config) => form.setValue("settingsConfig", config), }); const [localApiFormat, setLocalApiFormat] = useState(() => { if (appId !== "claude") return "anthropic"; return initialData?.meta?.apiFormat ?? "anthropic"; }); const handleApiFormatChange = useCallback((format: ClaudeApiFormat) => { setLocalApiFormat(format); }, []); const { codexAuth, codexConfig, codexApiKey, codexBaseUrl, codexModelName, codexAuthError, setCodexAuth, handleCodexApiKeyChange, handleCodexBaseUrlChange, handleCodexModelNameChange, handleCodexConfigChange: originalHandleCodexConfigChange, resetCodexConfig, } = useCodexConfigState({ initialData }); const { configError: codexConfigError, debouncedValidate } = useCodexTomlValidation(); const handleCodexConfigChange = useCallback( (value: string) => { originalHandleCodexConfigChange(value); debouncedValidate(value); }, [originalHandleCodexConfigChange, debouncedValidate], ); useEffect(() => { if (appId === "codex" && !initialData && selectedPresetId === "custom") { const template = getCodexCustomTemplate(); resetCodexConfig(template.auth, template.config); } }, [appId, initialData, selectedPresetId, resetCodexConfig]); useEffect(() => { form.reset(defaultValues); }, [defaultValues, form]); const presetCategoryLabels: Record = useMemo( () => ({ official: t("providerForm.categoryOfficial", { defaultValue: "官方", }), cn_official: t("providerForm.categoryCnOfficial", { defaultValue: "国内官方", }), aggregator: t("providerForm.categoryAggregation", { defaultValue: "聚合服务", }), third_party: t("providerForm.categoryThirdParty", { defaultValue: "第三方", }), omo: "OMO", }), [t], ); const presetEntries = useMemo(() => { if (appId === "codex") { return codexProviderPresets.map((preset, index) => ({ id: `codex-${index}`, preset, })); } else if (appId === "gemini") { return geminiProviderPresets.map((preset, index) => ({ id: `gemini-${index}`, preset, })); } else if (appId === "opencode") { return opencodeProviderPresets.map((preset, index) => ({ id: `opencode-${index}`, preset, })); } return providerPresets.map((preset, index) => ({ id: `claude-${index}`, preset, })); }, [appId]); const { templateValues, templateValueEntries, selectedPreset: templatePreset, handleTemplateValueChange, validateTemplateValues, } = useTemplateValues({ selectedPresetId: appId === "claude" ? selectedPresetId : null, presetEntries: appId === "claude" ? presetEntries : [], settingsConfig: form.getValues("settingsConfig"), onConfigChange: (config) => form.setValue("settingsConfig", config), }); const { useCommonConfig, commonConfigSnippet, commonConfigError, handleCommonConfigToggle, handleCommonConfigSnippetChange, isExtracting: isClaudeExtracting, handleExtract: handleClaudeExtract, } = useCommonConfigSnippet({ settingsConfig: form.getValues("settingsConfig"), onConfigChange: (config) => form.setValue("settingsConfig", config), initialData: appId === "claude" ? initialData : undefined, selectedPresetId: selectedPresetId ?? undefined, enabled: appId === "claude", }); const { useCommonConfig: useCodexCommonConfigFlag, commonConfigSnippet: codexCommonConfigSnippet, commonConfigError: codexCommonConfigError, handleCommonConfigToggle: handleCodexCommonConfigToggle, handleCommonConfigSnippetChange: handleCodexCommonConfigSnippetChange, isExtracting: isCodexExtracting, handleExtract: handleCodexExtract, } = useCodexCommonConfig({ codexConfig, onConfigChange: handleCodexConfigChange, initialData: appId === "codex" ? initialData : undefined, selectedPresetId: selectedPresetId ?? undefined, }); const { geminiEnv, geminiConfig, geminiApiKey, geminiBaseUrl, geminiModel, envError, configError: geminiConfigError, handleGeminiApiKeyChange: originalHandleGeminiApiKeyChange, handleGeminiBaseUrlChange: originalHandleGeminiBaseUrlChange, handleGeminiModelChange: originalHandleGeminiModelChange, handleGeminiEnvChange, handleGeminiConfigChange, resetGeminiConfig, envStringToObj, envObjToString, } = useGeminiConfigState({ initialData: appId === "gemini" ? initialData : undefined, }); const updateGeminiEnvField = useCallback( ( key: "GEMINI_API_KEY" | "GOOGLE_GEMINI_BASE_URL" | "GEMINI_MODEL", value: string, ) => { try { const config = JSON.parse(form.getValues("settingsConfig") || "{}") as { env?: Record; }; if (!config.env || typeof config.env !== "object") { config.env = {}; } config.env[key] = value; form.setValue("settingsConfig", JSON.stringify(config, null, 2)); } catch {} }, [form], ); const handleGeminiApiKeyChange = useCallback( (key: string) => { originalHandleGeminiApiKeyChange(key); updateGeminiEnvField("GEMINI_API_KEY", key.trim()); }, [originalHandleGeminiApiKeyChange, updateGeminiEnvField], ); const handleGeminiBaseUrlChange = useCallback( (url: string) => { originalHandleGeminiBaseUrlChange(url); updateGeminiEnvField( "GOOGLE_GEMINI_BASE_URL", url.trim().replace(/\/+$/, ""), ); }, [originalHandleGeminiBaseUrlChange, updateGeminiEnvField], ); const handleGeminiModelChange = useCallback( (model: string) => { originalHandleGeminiModelChange(model); updateGeminiEnvField("GEMINI_MODEL", model.trim()); }, [originalHandleGeminiModelChange, updateGeminiEnvField], ); const { useCommonConfig: useGeminiCommonConfigFlag, commonConfigSnippet: geminiCommonConfigSnippet, commonConfigError: geminiCommonConfigError, handleCommonConfigToggle: handleGeminiCommonConfigToggle, handleCommonConfigSnippetChange: handleGeminiCommonConfigSnippetChange, isExtracting: isGeminiExtracting, handleExtract: handleGeminiExtract, } = useGeminiCommonConfig({ envValue: geminiEnv, onEnvChange: handleGeminiEnvChange, envStringToObj, envObjToString, initialData: appId === "gemini" ? initialData : undefined, selectedPresetId: selectedPresetId ?? undefined, }); const { data: opencodeProvidersData } = useProvidersQuery("opencode"); const existingOpencodeKeys = useMemo(() => { if (!opencodeProvidersData?.providers) return []; return Object.keys(opencodeProvidersData.providers).filter( (k) => k !== providerId, ); }, [opencodeProvidersData?.providers, providerId]); const [enabledOpencodeProviderIds, setEnabledOpencodeProviderIds] = useState< string[] >([]); useEffect(() => { let active = true; if (!isOmoCategory) { setEnabledOpencodeProviderIds([]); return () => { active = false; }; } (async () => { try { const ids = await providersApi.getOpenCodeLiveProviderIds(); if (active) { setEnabledOpencodeProviderIds(ids); } } catch (error) { console.error("Failed to load OpenCode live provider ids:", error); if (active) { setEnabledOpencodeProviderIds([]); } } })(); return () => { active = false; }; }, [isOmoCategory]); const omoModelOptions = useMemo(() => { if (!isOmoCategory) return []; const allProviders = opencodeProvidersData?.providers; if (!allProviders) return []; const enabledSet = new Set(enabledOpencodeProviderIds); if (enabledSet.size === 0) return []; const dedupedOptions = new Map(); for (const [providerKey, provider] of Object.entries(allProviders)) { if (provider.category === "omo" || !enabledSet.has(providerKey)) { continue; } const parsedConfig = parseOpencodeConfig(provider.settingsConfig); for (const [modelId, model] of Object.entries( parsedConfig.models || {}, )) { const modelName = typeof model.name === "string" && model.name.trim() ? model.name : modelId; const providerDisplayName = typeof provider.name === "string" && provider.name.trim() ? provider.name : providerKey; const value = `${providerKey}/${modelId}`; const label = `${providerDisplayName} / ${modelName} (${modelId})`; if (!dedupedOptions.has(value)) { dedupedOptions.set(value, label); } } } return Array.from(dedupedOptions.entries()) .map(([value, label]) => ({ value, label })) .sort((a, b) => a.label.localeCompare(b.label, "zh-CN")); }, [ isOmoCategory, opencodeProvidersData?.providers, enabledOpencodeProviderIds, ]); const omoModelVariantsMap = useMemo(() => { const variantsMap: Record = {}; if (!isOmoCategory) { return variantsMap; } const allProviders = opencodeProvidersData?.providers; if (!allProviders) { return variantsMap; } const enabledSet = new Set(enabledOpencodeProviderIds); if (enabledSet.size === 0) { return variantsMap; } for (const [providerKey, provider] of Object.entries(allProviders)) { if (provider.category === "omo" || !enabledSet.has(providerKey)) { continue; } const parsedConfig = parseOpencodeConfig(provider.settingsConfig); for (const [modelId, model] of Object.entries( parsedConfig.models || {}, )) { const rawVariants = model.variants; if ( !rawVariants || typeof rawVariants !== "object" || Array.isArray(rawVariants) ) { continue; } const variantKeys = Object.keys(rawVariants).filter(Boolean); if (variantKeys.length === 0) { continue; } variantsMap[`${providerKey}/${modelId}`] = variantKeys; } } return variantsMap; }, [ isOmoCategory, opencodeProvidersData?.providers, enabledOpencodeProviderIds, ]); const initialOmoSettings = appId === "opencode" && initialData?.category === "omo" ? (initialData.settingsConfig as Record | undefined) : undefined; const initialOpencodeConfig = appId === "opencode" ? parseOpencodeConfig(initialData?.settingsConfig) : null; const initialOpencodeOptions = initialOpencodeConfig?.options || {}; const [opencodeProviderKey, setOpencodeProviderKey] = useState(() => { if (appId !== "opencode") return ""; return providerId || ""; }); const [opencodeNpm, setOpencodeNpm] = useState(() => { if (appId !== "opencode") return OPENCODE_DEFAULT_NPM; return initialOpencodeConfig?.npm || OPENCODE_DEFAULT_NPM; }); const [opencodeApiKey, setOpencodeApiKey] = useState(() => { if (appId !== "opencode") return ""; const value = initialOpencodeOptions.apiKey; return typeof value === "string" ? value : ""; }); const [opencodeBaseUrl, setOpencodeBaseUrl] = useState(() => { if (appId !== "opencode") return ""; const value = initialOpencodeOptions.baseURL; return typeof value === "string" ? value : ""; }); const [opencodeModels, setOpencodeModels] = useState< Record >(() => { if (appId !== "opencode") return {}; return initialOpencodeConfig?.models || {}; }); const [opencodeExtraOptions, setOpencodeExtraOptions] = useState< Record >(() => { if (appId !== "opencode") return {}; return toOpencodeExtraOptions(initialOpencodeOptions); }); const [omoAgents, setOmoAgents] = useState< Record> >( () => (initialOmoSettings?.agents as Record>) || {}, ); const [omoCategories, setOmoCategories] = useState< Record> >( () => (initialOmoSettings?.categories as Record< string, Record >) || {}, ); const [omoOtherFieldsStr, setOmoOtherFieldsStr] = useState(() => { const otherFields = initialOmoSettings?.otherFields; return otherFields ? JSON.stringify(otherFields, null, 2) : ""; }); const [omoGlobalState, setOmoGlobalState] = useState( null, ); const [isOmoConfigModalOpen, setIsOmoConfigModalOpen] = useState(false); const [useOmoCommonConfig, setUseOmoCommonConfig] = useState(() => { const raw = initialOmoSettings?.useCommonConfig; return typeof raw === "boolean" ? raw : true; }); const [isOmoSaving, setIsOmoSaving] = useState(false); const omoGlobalConfigRef = useRef(null); const [omoFieldsKey, setOmoFieldsKey] = useState(0); const effectiveOmoGlobalConfig = omoGlobalState ?? queriedOmoGlobalConfig ?? EMPTY_OMO_GLOBAL_CONFIG; const mergedOmoJsonPreview = useMemo(() => { if (useOmoCommonConfig) { const merged = mergeOmoConfigPreview( effectiveOmoGlobalConfig, omoAgents, omoCategories, omoOtherFieldsStr, ); return JSON.stringify(merged, null, 2); } else { return JSON.stringify( buildOmoProfilePreview(omoAgents, omoCategories, omoOtherFieldsStr), null, 2, ); } }, [ useOmoCommonConfig, effectiveOmoGlobalConfig, omoAgents, omoCategories, omoOtherFieldsStr, ]); useEffect(() => { if (appId !== "opencode" || category !== "omo" || isEditMode) return; let active = true; (async () => { let next = false; try { const raw = await configApi.getCommonConfigSnippet("omo"); if (raw) { const parsed = JSON.parse(raw) as Record; next = Object.keys(parsed).some( (k) => k !== "id" && k !== "updatedAt", ); } } catch {} if (active) setUseOmoCommonConfig(next); })(); return () => { active = false; }; }, [appId, category, isEditMode]); const handleOmoGlobalConfigSave = useCallback(async () => { if (!omoGlobalConfigRef.current) return; setIsOmoSaving(true); try { const config = omoGlobalConfigRef.current.buildCurrentConfigStrict(); await configApi.setCommonConfigSnippet("omo", JSON.stringify(config)); setIsOmoConfigModalOpen(false); toast.success( t("omo.globalConfigSaved", { defaultValue: "Global config saved" }), ); } catch (err) { toast.error(String(err)); } finally { setIsOmoSaving(false); } }, [t]); const handleOmoEditClick = useCallback(() => { setOmoFieldsKey((k) => k + 1); setIsOmoConfigModalOpen(true); }, []); const resetOmoDraftState = useCallback((useCommonConfig = true) => { setOmoAgents({}); setOmoCategories({}); setOmoOtherFieldsStr(""); setUseOmoCommonConfig(useCommonConfig); }, []); const updateOpencodeSettings = useCallback( (updater: (config: Record) => void) => { try { const config = JSON.parse( form.getValues("settingsConfig") || OPENCODE_DEFAULT_CONFIG, ) as Record; updater(config); form.setValue("settingsConfig", JSON.stringify(config, null, 2)); } catch {} }, [form], ); const handleOpencodeNpmChange = useCallback( (npm: string) => { setOpencodeNpm(npm); updateOpencodeSettings((config) => { config.npm = npm; }); }, [updateOpencodeSettings], ); const handleOpencodeApiKeyChange = useCallback( (apiKey: string) => { setOpencodeApiKey(apiKey); updateOpencodeSettings((config) => { if (!config.options) config.options = {}; config.options.apiKey = apiKey; }); }, [updateOpencodeSettings], ); const handleOpencodeBaseUrlChange = useCallback( (baseUrl: string) => { setOpencodeBaseUrl(baseUrl); updateOpencodeSettings((config) => { if (!config.options) config.options = {}; config.options.baseURL = baseUrl.trim().replace(/\/+$/, ""); }); }, [updateOpencodeSettings], ); const handleOpencodeModelsChange = useCallback( (models: Record) => { setOpencodeModels(models); updateOpencodeSettings((config) => { config.models = models; }); }, [updateOpencodeSettings], ); const handleOpencodeExtraOptionsChange = useCallback( (options: Record) => { setOpencodeExtraOptions(options); updateOpencodeSettings((config) => { if (!config.options) config.options = {}; for (const k of Object.keys(config.options)) { if (!isKnownOpencodeOptionKey(k)) { delete config.options[k]; } } for (const [k, v] of Object.entries(options)) { const trimmedKey = k.trim(); if (trimmedKey && !trimmedKey.startsWith("option-")) { try { config.options[trimmedKey] = JSON.parse(v); } catch { config.options[trimmedKey] = v; } } } }); }, [updateOpencodeSettings], ); const [isCommonConfigModalOpen, setIsCommonConfigModalOpen] = useState(false); const handleSubmit = (values: ProviderFormData) => { if (appId === "claude" && templateValueEntries.length > 0) { const validation = validateTemplateValues(); if (!validation.isValid && validation.missingField) { toast.error( t("providerForm.fillParameter", { label: validation.missingField.label, defaultValue: `请填写 ${validation.missingField.label}`, }), ); return; } } if (!values.name.trim()) { toast.error( t("providerForm.fillSupplierName", { defaultValue: "请填写供应商名称", }), ); return; } if (appId === "opencode" && category !== "omo") { const keyPattern = /^[a-z0-9]+(-[a-z0-9]+)*$/; if (!opencodeProviderKey.trim()) { toast.error(t("opencode.providerKeyRequired")); return; } if (!keyPattern.test(opencodeProviderKey)) { toast.error(t("opencode.providerKeyInvalid")); return; } if (!isEditMode && existingOpencodeKeys.includes(opencodeProviderKey)) { toast.error(t("opencode.providerKeyDuplicate")); return; } if (Object.keys(opencodeModels).length === 0) { toast.error(t("opencode.modelsRequired")); return; } } if (category !== "official") { if (appId === "claude") { if (!baseUrl.trim()) { toast.error( t("providerForm.endpointRequired", { defaultValue: "非官方供应商请填写 API 端点", }), ); return; } if (!apiKey.trim()) { toast.error( t("providerForm.apiKeyRequired", { defaultValue: "非官方供应商请填写 API Key", }), ); return; } } else if (appId === "codex") { if (!codexBaseUrl.trim()) { toast.error( t("providerForm.endpointRequired", { defaultValue: "非官方供应商请填写 API 端点", }), ); return; } if (!codexApiKey.trim()) { toast.error( t("providerForm.apiKeyRequired", { defaultValue: "非官方供应商请填写 API Key", }), ); return; } } else if (appId === "gemini") { if (!geminiBaseUrl.trim()) { toast.error( t("providerForm.endpointRequired", { defaultValue: "非官方供应商请填写 API 端点", }), ); return; } if (!geminiApiKey.trim()) { toast.error( t("providerForm.apiKeyRequired", { defaultValue: "非官方供应商请填写 API Key", }), ); return; } } } let settingsConfig: string; if (appId === "codex") { try { const authJson = JSON.parse(codexAuth); const configObj = { auth: authJson, config: codexConfig ?? "", }; settingsConfig = JSON.stringify(configObj); } catch (err) { settingsConfig = values.settingsConfig.trim(); } } else if (appId === "gemini") { try { const envObj = envStringToObj(geminiEnv); const configObj = geminiConfig.trim() ? JSON.parse(geminiConfig) : {}; const combined = { env: envObj, config: configObj, }; settingsConfig = JSON.stringify(combined); } catch (err) { settingsConfig = values.settingsConfig.trim(); } } else if (appId === "opencode" && category === "omo") { const omoConfig: Record = {}; omoConfig.useCommonConfig = useOmoCommonConfig; if (Object.keys(omoAgents).length > 0) { omoConfig.agents = omoAgents; } if (Object.keys(omoCategories).length > 0) { omoConfig.categories = omoCategories; } if (omoOtherFieldsStr.trim()) { try { omoConfig.otherFields = JSON.parse(omoOtherFieldsStr); } catch { toast.error( t("omo.invalidJson", { defaultValue: "Other Fields contains invalid JSON", }), ); return; } } settingsConfig = JSON.stringify(omoConfig); } else { settingsConfig = values.settingsConfig.trim(); } const payload: ProviderFormValues = { ...values, name: values.name.trim(), websiteUrl: values.websiteUrl?.trim() ?? "", settingsConfig, }; if (appId === "opencode") { if (category === "omo") { if (!isEditMode) { payload.providerKey = `omo-${crypto.randomUUID().slice(0, 8)}`; } } else { payload.providerKey = opencodeProviderKey; } } if (category === "omo" && !payload.presetCategory) { payload.presetCategory = "omo"; } if (activePreset) { payload.presetId = activePreset.id; if (activePreset.category) { payload.presetCategory = activePreset.category; } if (activePreset.isPartner) { payload.isPartner = activePreset.isPartner; } } if (!isEditMode && draftCustomEndpoints.length > 0) { const customEndpointsToSave: Record< string, import("@/types").CustomEndpoint > = draftCustomEndpoints.reduce( (acc, url) => { const now = Date.now(); acc[url] = { url, addedAt: now, lastUsed: undefined }; return acc; }, {} as Record, ); const hadEndpoints = initialData?.meta?.custom_endpoints && Object.keys(initialData.meta.custom_endpoints).length > 0; const needsClearEndpoints = hadEndpoints && draftCustomEndpoints.length === 0; let mergedMeta = needsClearEndpoints ? mergeProviderMeta(initialData?.meta, {}) : mergeProviderMeta(initialData?.meta, customEndpointsToSave); if (activePreset?.isPartner) { mergedMeta = { ...(mergedMeta ?? {}), isPartner: true, }; } if (activePreset?.partnerPromotionKey) { mergedMeta = { ...(mergedMeta ?? {}), partnerPromotionKey: activePreset.partnerPromotionKey, }; } if (mergedMeta !== undefined) { payload.meta = mergedMeta; } } const baseMeta: ProviderMeta | undefined = payload.meta ?? (initialData?.meta ? { ...initialData.meta } : undefined); payload.meta = { ...(baseMeta ?? {}), endpointAutoSelect, testConfig: testConfig.enabled ? testConfig : undefined, proxyConfig: proxyConfig.enabled ? proxyConfig : undefined, costMultiplier: pricingConfig.enabled ? pricingConfig.costMultiplier : undefined, pricingModelSource: pricingConfig.enabled && pricingConfig.pricingModelSource !== "inherit" ? pricingConfig.pricingModelSource : undefined, apiFormat: appId === "claude" && category !== "official" ? localApiFormat : undefined, }; onSubmit(payload); }; const groupedPresets = useMemo(() => { return presetEntries.reduce>((acc, entry) => { const category = entry.preset.category ?? "others"; if (!acc[category]) { acc[category] = []; } acc[category].push(entry); return acc; }, {}); }, [presetEntries]); const categoryKeys = useMemo(() => { return Object.keys(groupedPresets).filter( (key) => key !== "custom" && groupedPresets[key]?.length, ); }, [groupedPresets]); const shouldShowSpeedTest = category !== "official"; const { shouldShowApiKeyLink: shouldShowClaudeApiKeyLink, websiteUrl: claudeWebsiteUrl, isPartner: isClaudePartner, partnerPromotionKey: claudePartnerPromotionKey, } = useApiKeyLink({ appId: "claude", category, selectedPresetId, presetEntries, formWebsiteUrl: form.watch("websiteUrl") || "", }); const { shouldShowApiKeyLink: shouldShowCodexApiKeyLink, websiteUrl: codexWebsiteUrl, isPartner: isCodexPartner, partnerPromotionKey: codexPartnerPromotionKey, } = useApiKeyLink({ appId: "codex", category, selectedPresetId, presetEntries, formWebsiteUrl: form.watch("websiteUrl") || "", }); const { shouldShowApiKeyLink: shouldShowGeminiApiKeyLink, websiteUrl: geminiWebsiteUrl, isPartner: isGeminiPartner, partnerPromotionKey: geminiPartnerPromotionKey, } = useApiKeyLink({ appId: "gemini", category, selectedPresetId, presetEntries, formWebsiteUrl: form.watch("websiteUrl") || "", }); const { shouldShowApiKeyLink: shouldShowOpencodeApiKeyLink, websiteUrl: opencodeWebsiteUrl, isPartner: isOpencodePartner, partnerPromotionKey: opencodePartnerPromotionKey, } = useApiKeyLink({ appId: "opencode", category, selectedPresetId, presetEntries, formWebsiteUrl: form.watch("websiteUrl") || "", }); const speedTestEndpoints = useSpeedTestEndpoints({ appId, selectedPresetId, presetEntries, baseUrl, codexBaseUrl, initialData, }); const handlePresetChange = (value: string) => { setSelectedPresetId(value); if (value === "custom") { setActivePreset(null); form.reset(defaultValues); if (appId === "codex") { const template = getCodexCustomTemplate(); resetCodexConfig(template.auth, template.config); } if (appId === "gemini") { resetGeminiConfig({}, {}); } if (appId === "opencode") { setOpencodeProviderKey(""); setOpencodeNpm(OPENCODE_DEFAULT_NPM); setOpencodeBaseUrl(""); setOpencodeApiKey(""); setOpencodeModels({}); setOpencodeExtraOptions({}); resetOmoDraftState(); } return; } const entry = presetEntries.find((item) => item.id === value); if (!entry) { return; } setActivePreset({ id: value, category: entry.preset.category, isPartner: entry.preset.isPartner, partnerPromotionKey: entry.preset.partnerPromotionKey, }); if (appId === "codex") { const preset = entry.preset as CodexProviderPreset; const auth = preset.auth ?? {}; const config = preset.config ?? ""; resetCodexConfig(auth, config); form.reset({ name: preset.name, websiteUrl: preset.websiteUrl ?? "", settingsConfig: JSON.stringify({ auth, config }, null, 2), icon: preset.icon ?? "", iconColor: preset.iconColor ?? "", }); return; } if (appId === "gemini") { const preset = entry.preset as GeminiProviderPreset; const env = (preset.settingsConfig as any)?.env ?? {}; const config = (preset.settingsConfig as any)?.config ?? {}; resetGeminiConfig(env, config); form.reset({ name: preset.name, websiteUrl: preset.websiteUrl ?? "", settingsConfig: JSON.stringify(preset.settingsConfig, null, 2), icon: preset.icon ?? "", iconColor: preset.iconColor ?? "", }); return; } if (appId === "opencode") { const preset = entry.preset as OpenCodeProviderPreset; const config = preset.settingsConfig; if (preset.category === "omo") { resetOmoDraftState(); form.reset({ name: "OMO", websiteUrl: preset.websiteUrl ?? "", settingsConfig: JSON.stringify({}, null, 2), icon: preset.icon ?? "", iconColor: preset.iconColor ?? "", }); return; } setOpencodeProviderKey(""); setOpencodeNpm(config.npm || OPENCODE_DEFAULT_NPM); setOpencodeBaseUrl(config.options?.baseURL || ""); setOpencodeApiKey(config.options?.apiKey || ""); setOpencodeModels(config.models || {}); setOpencodeExtraOptions(toOpencodeExtraOptions(config.options || {})); form.reset({ name: preset.name, websiteUrl: preset.websiteUrl ?? "", settingsConfig: JSON.stringify(config, null, 2), icon: preset.icon ?? "", iconColor: preset.iconColor ?? "", }); return; } const preset = entry.preset as ProviderPreset; const config = applyTemplateValues( preset.settingsConfig, preset.templateValues, ); if (preset.apiFormat) { setLocalApiFormat(preset.apiFormat); } else { setLocalApiFormat("anthropic"); } form.reset({ name: preset.name, websiteUrl: preset.websiteUrl ?? "", settingsConfig: JSON.stringify(config, null, 2), icon: preset.icon ?? "", iconColor: preset.iconColor ?? "", }); }; const settingsConfigErrorField = ( ( )} /> ); return (
{!initialData && ( )} setOpencodeProviderKey( e.target.value.toLowerCase().replace(/[^a-z0-9-]/g, ""), ) } placeholder={t("opencode.providerKeyPlaceholder")} disabled={isEditMode} className={ (existingOpencodeKeys.includes(opencodeProviderKey) && !isEditMode) || (opencodeProviderKey.trim() !== "" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(opencodeProviderKey)) ? "border-destructive" : "" } /> {existingOpencodeKeys.includes(opencodeProviderKey) && !isEditMode && (

{t("opencode.providerKeyDuplicate")}

)} {opencodeProviderKey.trim() !== "" && !/^[a-z0-9]+(-[a-z0-9]+)*$/.test(opencodeProviderKey) && (

{t("opencode.providerKeyInvalid")}

)} {!( existingOpencodeKeys.includes(opencodeProviderKey) && !isEditMode ) && (opencodeProviderKey.trim() === "" || /^[a-z0-9]+(-[a-z0-9]+)*$/.test(opencodeProviderKey)) && (

{t("opencode.providerKeyHint")}

)} ) : undefined } /> {appId === "claude" && ( )} {appId === "codex" && ( )} {appId === "gemini" && ( )} {appId === "opencode" && category !== "omo" && ( )} {appId === "opencode" && category === "omo" && ( )} {appId === "codex" ? ( <> {settingsConfigErrorField} ) : appId === "gemini" ? ( <> {settingsConfigErrorField} ) : appId === "opencode" && category === "omo" ? ( setIsOmoConfigModalOpen(false)} onSave={handleOmoGlobalConfigSave} isSaving={isOmoSaving} onGlobalConfigStateChange={setOmoGlobalState} globalConfigRef={omoGlobalConfigRef} fieldsKey={omoFieldsKey} /> ) : appId === "opencode" && category !== "omo" ? ( <>
form.setValue("settingsConfig", config)} placeholder={`{ "npm": "@ai-sdk/openai-compatible", "options": { "baseURL": "https://your-api-endpoint.com", "apiKey": "your-api-key-here" }, "models": {} }`} rows={14} showValidation={true} language="json" />
{settingsConfigErrorField} ) : ( <> form.setValue("settingsConfig", value)} useCommonConfig={useCommonConfig} onCommonConfigToggle={handleCommonConfigToggle} commonConfigSnippet={commonConfigSnippet} onCommonConfigSnippetChange={handleCommonConfigSnippetChange} commonConfigError={commonConfigError} onEditClick={() => setIsCommonConfigModalOpen(true)} isModalOpen={isCommonConfigModalOpen} onModalClose={() => setIsCommonConfigModalOpen(false)} onExtract={handleClaudeExtract} isExtracting={isClaudeExtracting} /> {settingsConfigErrorField} )} {category !== "omo" && ( )} {showButtons && (
)} ); } export type ProviderFormValues = ProviderFormData & { presetId?: string; presetCategory?: ProviderCategory; isPartner?: boolean; meta?: ProviderMeta; providerKey?: string; };