From 7d0621dcc24c7c0101d622390190a66bea6ee1e2 Mon Sep 17 00:00:00 2001 From: Haze <709547807@qq.com> Date: Sun, 1 Mar 2026 17:40:07 +0800 Subject: [PATCH] fix(provider): ollama provider fix (#246) Co-authored-by: Cursor Agent Co-authored-by: Haze --- src/components/settings/ProvidersSettings.tsx | 10 +++++++- src/lib/providers.ts | 13 +++++++++- src/pages/Setup/index.tsx | 6 +++-- tests/unit/providers.test.ts | 24 ++++++++++++++++++- 4 files changed, 48 insertions(+), 5 deletions(-) diff --git a/src/components/settings/ProvidersSettings.tsx b/src/components/settings/ProvidersSettings.tsx index 745f0fd2e..4e5a5216c 100644 --- a/src/components/settings/ProvidersSettings.tsx +++ b/src/components/settings/ProvidersSettings.tsx @@ -29,6 +29,7 @@ import { PROVIDER_TYPE_INFO, type ProviderType, getProviderIconUrl, + resolveProviderApiKeyForSave, shouldInvertInDark, } from '@/lib/providers'; import { cn } from '@/lib/utils'; @@ -66,6 +67,7 @@ export function ProvidersSettings() { // Only custom supports multiple instances. // Built-in providers remain singleton by type. const id = type === 'custom' ? `custom-${crypto.randomUUID()}` : type; + const effectiveApiKey = resolveProviderApiKeyForSave(type, apiKey); try { await addProvider( { @@ -76,7 +78,7 @@ export function ProvidersSettings() { model: options?.model, enabled: true, }, - apiKey.trim() || undefined + effectiveApiKey ); // Auto-set as default if no default is currently configured @@ -261,6 +263,12 @@ function ProviderCard({ } } + // Keep Ollama key optional in UI, but persist a placeholder when + // editing legacy configs that have no stored key. + if (provider.type === 'ollama' && !provider.hasKey && !payload.newApiKey) { + payload.newApiKey = resolveProviderApiKeyForSave(provider.type, '') as string; + } + if (!payload.newApiKey && !payload.updates) { onCancelEdit(); setSaving(false); diff --git a/src/lib/providers.ts b/src/lib/providers.ts index 990f7d0cf..d5189ee37 100644 --- a/src/lib/providers.ts +++ b/src/lib/providers.ts @@ -21,6 +21,8 @@ export const PROVIDER_TYPES = [ ] as const; export type ProviderType = (typeof PROVIDER_TYPES)[number]; +export const OLLAMA_PLACEHOLDER_API_KEY = 'ollama-local'; + export interface ProviderConfig { id: string; name: string; @@ -77,7 +79,7 @@ export const PROVIDER_TYPE_INFO: ProviderTypeInfo[] = [ { id: 'minimax-portal', name: 'MiniMax (Global)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.5', apiKeyUrl: 'https://intl.minimaxi.com/' }, { id: 'minimax-portal-cn', name: 'MiniMax (CN)', icon: '☁️', placeholder: 'sk-...', model: 'MiniMax', requiresApiKey: false, isOAuth: true, supportsApiKey: true, defaultModelId: 'MiniMax-M2.5', apiKeyUrl: 'https://platform.minimaxi.com/' }, { id: 'qwen-portal', name: 'Qwen', icon: '☁️', placeholder: 'sk-...', model: 'Qwen', requiresApiKey: false, isOAuth: true, defaultModelId: 'coder-model' }, - { id: 'ollama', name: 'Ollama', icon: '🦙', placeholder: 'Not required', requiresApiKey: false, defaultBaseUrl: 'http://localhost:11434', showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'qwen3:latest' }, + { id: 'ollama', name: 'Ollama', icon: '🦙', placeholder: 'Not required', requiresApiKey: false, defaultBaseUrl: 'http://localhost:11434/v1', showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'qwen3:latest' }, { id: 'custom', name: 'Custom', icon: '⚙️', placeholder: 'API key...', requiresApiKey: true, showBaseUrl: true, showModelId: true, modelIdPlaceholder: 'your-provider/model-id' }, ]; @@ -98,3 +100,12 @@ export const SETUP_PROVIDERS = PROVIDER_TYPE_INFO; export function getProviderTypeInfo(type: ProviderType): ProviderTypeInfo | undefined { return PROVIDER_TYPE_INFO.find((t) => t.id === type); } + +/** Normalize provider API key before saving; Ollama uses a local placeholder when blank. */ +export function resolveProviderApiKeyForSave(type: ProviderType | string, apiKey: string): string | undefined { + const trimmed = apiKey.trim(); + if (type === 'ollama') { + return trimmed || OLLAMA_PLACEHOLDER_API_KEY; + } + return trimmed || undefined; +} diff --git a/src/pages/Setup/index.tsx b/src/pages/Setup/index.tsx index 63a635a2d..730df904b 100644 --- a/src/pages/Setup/index.tsx +++ b/src/pages/Setup/index.tsx @@ -103,7 +103,7 @@ const defaultSkills: DefaultSkill[] = [ { id: 'terminal', name: 'Terminal', description: 'Shell command execution' }, ]; -import { SETUP_PROVIDERS, type ProviderTypeInfo, getProviderIconUrl, shouldInvertInDark } from '@/lib/providers'; +import { SETUP_PROVIDERS, type ProviderTypeInfo, getProviderIconUrl, resolveProviderApiKeyForSave, shouldInvertInDark } from '@/lib/providers'; import clawxIcon from '@/assets/logo.svg'; // Use the shared provider registry for setup providers @@ -970,6 +970,8 @@ function ProviderContent({ : `custom-${crypto.randomUUID()}`) : selectedProvider; + const effectiveApiKey = resolveProviderApiKeyForSave(selectedProvider, apiKey); + // Save provider config + API key, then set as default const saveResult = await window.electron.ipcRenderer.invoke( 'provider:save', @@ -983,7 +985,7 @@ function ProviderContent({ createdAt: new Date().toISOString(), updatedAt: new Date().toISOString(), }, - apiKey || undefined + effectiveApiKey ) as { success: boolean; error?: string }; if (!saveResult.success) { diff --git a/tests/unit/providers.test.ts b/tests/unit/providers.test.ts index dc85e3781..81ffb87e7 100644 --- a/tests/unit/providers.test.ts +++ b/tests/unit/providers.test.ts @@ -1,5 +1,5 @@ import { describe, expect, it } from 'vitest'; -import { PROVIDER_TYPES, PROVIDER_TYPE_INFO } from '@/lib/providers'; +import { PROVIDER_TYPES, PROVIDER_TYPE_INFO, resolveProviderApiKeyForSave } from '@/lib/providers'; import { BUILTIN_PROVIDER_TYPES, getProviderConfig, @@ -39,4 +39,26 @@ describe('provider metadata', () => { expect.arrayContaining(['anthropic', 'openai', 'google', 'openrouter', 'ark', 'moonshot', 'siliconflow', 'minimax-portal', 'minimax-portal-cn', 'qwen-portal', 'ollama']) ); }); + + it('uses OpenAI-compatible Ollama default base URL', () => { + expect(PROVIDER_TYPE_INFO).toEqual( + expect.arrayContaining([ + expect.objectContaining({ + id: 'ollama', + defaultBaseUrl: 'http://localhost:11434/v1', + requiresApiKey: false, + showBaseUrl: true, + showModelId: true, + }), + ]) + ); + }); + + it('normalizes provider API keys for save flow', () => { + expect(resolveProviderApiKeyForSave('ollama', '')).toBe('ollama-local'); + expect(resolveProviderApiKeyForSave('ollama', ' ')).toBe('ollama-local'); + expect(resolveProviderApiKeyForSave('ollama', 'real-key')).toBe('real-key'); + expect(resolveProviderApiKeyForSave('openai', '')).toBeUndefined(); + expect(resolveProviderApiKeyForSave('openai', ' sk-test ')).toBe('sk-test'); + }); });