From 2b7d18c4c6631cb949c82a3eecc83beb0c885319 Mon Sep 17 00:00:00 2001 From: kaitranntt Date: Thu, 22 Jan 2026 14:09:59 -0500 Subject: [PATCH] feat(ui): add Ollama provider presets to dashboard - Add ollama (local) and ollama-cloud presets to UI provider-presets.ts - Update profile-create-dialog to handle requiresApiKey: false - Show "No API Key Required" info box for local Ollama - Add --api-key ignored warning to CLI for local Ollama preset - Add ollama-cloud to CLI help text preset list Fixes edge cases identified in PR review: - UI dashboard now has parity with CLI Ollama support - Conditional API key validation in profile creation form --- src/commands/api-command.ts | 5 +- .../profiles/profile-create-dialog.tsx | 84 ++++++++++++------- ui/src/lib/provider-presets.ts | 28 +++++++ 3 files changed, 84 insertions(+), 33 deletions(-) diff --git a/src/commands/api-command.ts b/src/commands/api-command.ts index 0327978d..41706a30 100644 --- a/src/commands/api-command.ts +++ b/src/commands/api-command.ts @@ -186,6 +186,9 @@ async function handleCreate(args: string[]): Promise { if (preset?.noApiKey) { // Preset doesn't require API key (e.g., local Ollama) + if (parsedArgs.apiKey) { + console.log(dim('Note: API key ignored for local Ollama (not required)')); + } console.log(info('No API key required for local Ollama')); apiKey = ''; // Empty string for local providers } else if (!apiKey) { @@ -431,7 +434,7 @@ async function showHelp(): Promise { console.log(''); console.log(subheader('Options')); console.log( - ` ${color('--preset ', 'command')} Use provider preset (openrouter, ollama, glm, glmt, kimi, foundry, mm, deepseek, qwen)` + ` ${color('--preset ', 'command')} Use provider preset (openrouter, ollama, ollama-cloud, glm, glmt, kimi, foundry, mm, deepseek, qwen)` ); console.log(` ${color('--base-url ', 'command')} API base URL (create)`); console.log(` ${color('--api-key ', 'command')} API key (create)`); diff --git a/ui/src/components/profiles/profile-create-dialog.tsx b/ui/src/components/profiles/profile-create-dialog.tsx index 37f92035..0d7fff12 100644 --- a/ui/src/components/profiles/profile-create-dialog.tsx +++ b/ui/src/components/profiles/profile-create-dialog.tsx @@ -47,7 +47,7 @@ const schema = z.object({ .min(1, 'Name is required') .regex(/^[a-zA-Z][a-zA-Z0-9._-]*$/, 'Must start with letter, only letters/numbers/.-_'), baseUrl: z.string().url('Invalid URL format'), - apiKey: z.string().min(1, 'API key is required'), + apiKey: z.string(), // Validation handled conditionally in onSubmit model: z.string().optional(), opusModel: z.string().optional(), sonnetModel: z.string().optional(), @@ -207,9 +207,16 @@ export function ProfileCreateDialog({ }, [baseUrlValue, selectedPreset]); const onSubmit = async (data: FormData) => { + // Validate API key - required unless preset has requiresApiKey: false + if (currentPreset?.requiresApiKey !== false && !data.apiKey) { + toast.error('API key is required'); + return; + } // Use user-provided baseUrl (allows customization of preset URLs) const finalData = { ...data, + // For presets that don't require API key, use empty string + apiKey: currentPreset?.requiresApiKey === false ? '' : data.apiKey, }; try { await createMutation.mutateAsync(finalData); @@ -367,38 +374,51 @@ export function ProfileCreateDialog({ )} - {/* API Key */} -
- -
- - + {/* API Key - hidden for presets that don't require it */} + {currentPreset?.requiresApiKey !== false ? ( +
+ +
+ + +
+ {errors.apiKey ? ( +

{errors.apiKey.message}

+ ) : ( + currentPreset?.apiKeyHint && ( +

{currentPreset.apiKeyHint}

+ ) + )}
- {errors.apiKey ? ( -

{errors.apiKey.message}

- ) : ( - currentPreset?.apiKeyHint && ( -

{currentPreset.apiKeyHint}

- ) - )} -
+ ) : ( +
+ +
+

No API Key Required

+

+ {currentPreset?.apiKeyHint || + 'This provider runs locally and does not require authentication.'} +

+
+
+ )} diff --git a/ui/src/lib/provider-presets.ts b/ui/src/lib/provider-presets.ts index 591260cb..2cd68f18 100644 --- a/ui/src/lib/provider-presets.ts +++ b/ui/src/lib/provider-presets.ts @@ -44,6 +44,21 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ apiKeyHint: 'Get your API key at openrouter.ai/keys', category: 'recommended', }, + // Recommended - Ollama (Local) + { + id: 'ollama', + name: 'Ollama (Local)', + description: 'Local open-source models via Ollama (32K+ context)', + baseUrl: 'http://localhost:11434', + defaultProfileName: 'ollama', + badge: 'Local', + featured: true, + defaultModel: 'qwen3-coder', + requiresApiKey: false, + apiKeyPlaceholder: '', + apiKeyHint: 'No API key required for local Ollama', + category: 'recommended', + }, // Alternative providers - GLM/GLMT/Kimi { id: 'glm', @@ -136,6 +151,19 @@ export const PROVIDER_PRESETS: ProviderPreset[] = [ apiKeyHint: 'Get your API key from Alibaba Cloud Model Studio', category: 'alternative', }, + { + id: 'ollama-cloud', + name: 'Ollama Cloud', + description: 'Ollama.com cloud models (glm-4.7:cloud, qwen3-coder:480b)', + baseUrl: 'https://ollama.com', + defaultProfileName: 'ollama-cloud', + badge: 'Cloud', + defaultModel: 'glm-4.7:cloud', + requiresApiKey: true, + apiKeyPlaceholder: 'YOUR_OLLAMA_CLOUD_API_KEY', + apiKeyHint: 'Get your API key at ollama.com', + category: 'alternative', + }, ]; /** Get presets by category */