From 5159cc0b6a981aa7eafd3e65fabddc58c248f86f Mon Sep 17 00:00:00 2001 From: Alex Date: Mon, 18 May 2026 18:16:43 +0800 Subject: [PATCH 1/2] feat: add Novita AI provider --- .../components/settings/panels/AIPanel.tsx | 37 ++++++--- .../panels/__tests__/AIPanel.test.tsx | 82 +++++++++++++++++++ 2 files changed, 109 insertions(+), 10 deletions(-) diff --git a/app/src/components/settings/panels/AIPanel.tsx b/app/src/components/settings/panels/AIPanel.tsx index 0ab696d18a..4de18ee0f0 100644 --- a/app/src/components/settings/panels/AIPanel.tsx +++ b/app/src/components/settings/panels/AIPanel.tsx @@ -103,12 +103,22 @@ const BUILTIN_PROVIDER_META: Record = { label: 'OpenRouter', tone: 'bg-slate-100 dark:bg-slate-500/15 ring-slate-300 text-slate-900 dark:text-slate-100', }, + novita: { + label: 'Novita AI', + tone: 'bg-cyan-50 dark:bg-cyan-500/10 ring-cyan-200 text-cyan-900 dark:text-cyan-100', + }, custom: { label: 'Custom', tone: 'bg-stone-100 dark:bg-neutral-800 ring-stone-300 text-stone-900 dark:text-neutral-100', }, }; +const BUILTIN_CLOUD_SLUGS = ['openai', 'anthropic', 'openrouter', 'novita', 'custom'] as const; + +const BUILTIN_PROVIDER_DEFAULT_MODELS: Record = { + novita: 'deepseek/deepseek-v4-pro', +}; + const WORKLOADS: Workload[] = [ { id: 'reasoning', @@ -451,7 +461,9 @@ const ProviderKeyDialog = ({ ? 'sk-ant-...' : slug === 'openrouter' ? 'sk-or-...' - : 'your-api-key'; + : slug === 'novita' + ? 'Novita API key' + : 'your-api-key'; const handleSave = async () => { const trimmed = apiKey.trim(); @@ -1515,7 +1527,7 @@ const CustomRoutingDialog = ({ if (initial.kind === 'cloud' || initial.kind === 'local') return initial.model; if (initialSource?.kind === 'cloud') { const p = customCloud.find(c => c.slug === initialSource.providerSlug); - return p ? '' : ''; + return p ? (BUILTIN_PROVIDER_DEFAULT_MODELS[p.slug] ?? '') : ''; } return localModels[0]?.id ?? ''; }); @@ -1591,7 +1603,7 @@ const CustomRoutingDialog = ({ setModel(localModels[0]?.id ?? ''); } else if (kind === 'cloud') { setSource({ kind: 'cloud', providerSlug: slug }); - setModel(''); + setModel(BUILTIN_PROVIDER_DEFAULT_MODELS[slug] ?? ''); } }} className="rounded-lg border border-stone-300 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-2 text-sm text-stone-900 dark:text-neutral-100 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500"> @@ -1624,7 +1636,12 @@ const CustomRoutingDialog = ({ type="text" value={model} onChange={e => setModel(e.target.value)} - placeholder={selectedCloud ? `${selectedCloud.slug} model id` : 'model-id'} + placeholder={ + selectedCloud + ? (BUILTIN_PROVIDER_DEFAULT_MODELS[selectedCloud.slug] ?? + `${selectedCloud.slug} model id`) + : 'model-id' + } className="rounded-lg border border-stone-300 dark:border-neutral-700 bg-white dark:bg-neutral-900 px-3 py-2 text-sm font-mono text-stone-900 dark:text-neutral-100 placeholder-stone-400 dark:placeholder-neutral-500 focus:border-primary-500 focus:outline-none focus:ring-1 focus:ring-primary-500" /> )} @@ -1797,8 +1814,8 @@ const AIPanel = ({ embedded = false }: AIPanelProps = {}) => { )}
- {/* Built-in cloud providers — openai/anthropic/openrouter/custom */} - {(['openai', 'anthropic', 'openrouter', 'custom'] as const).map(slug => { + {/* Built-in cloud providers — openai/anthropic/openrouter/novita/custom */} + {BUILTIN_CLOUD_SLUGS.map(slug => { const meta = BUILTIN_PROVIDER_META[slug]; const label = meta?.label ?? slug; const existing = draft.cloudProviders.find(cp => cp.slug === slug); @@ -2112,9 +2129,7 @@ const CloudProviderEditor = ({ const { t } = useT(); const defaultSlug: string = initial?.slug ?? - (['openai', 'anthropic', 'openrouter', 'custom'] as const).find( - s => !existingSlugs.includes(s) - ) ?? + BUILTIN_CLOUD_SLUGS.find(s => !existingSlugs.includes(s)) ?? 'custom'; const [slug, setSlug] = useState(defaultSlug); const [label, setLabel] = useState( @@ -2157,7 +2172,7 @@ const CloudProviderEditor = ({ }} disabled={!!initial} className="mt-1 w-full rounded-lg border border-stone-200 dark:border-neutral-800 bg-white dark:bg-neutral-900 px-3 py-2 text-sm text-stone-900 dark:text-neutral-100 disabled:opacity-60 focus:border-primary-400 focus:outline-none focus:ring-1 focus:ring-primary-200"> - {(['openai', 'anthropic', 'openrouter', 'custom'] as const) + {BUILTIN_CLOUD_SLUGS .filter(s => s === slug || !existingSlugs.includes(s)) .map(s => (