fix(provider): ollama provider fix (#246)

Co-authored-by: Cursor Agent <cursoragent@cursor.com>
Co-authored-by: Haze <hazeone@users.noreply.github.com>
This commit is contained in:
Haze
2026-03-01 17:40:07 +08:00
committed by GitHub
Unverified
parent a8f61d5a61
commit 7d0621dcc2
4 changed files with 48 additions and 5 deletions

View File

@@ -29,6 +29,7 @@ import {
PROVIDER_TYPE_INFO, PROVIDER_TYPE_INFO,
type ProviderType, type ProviderType,
getProviderIconUrl, getProviderIconUrl,
resolveProviderApiKeyForSave,
shouldInvertInDark, shouldInvertInDark,
} from '@/lib/providers'; } from '@/lib/providers';
import { cn } from '@/lib/utils'; import { cn } from '@/lib/utils';
@@ -66,6 +67,7 @@ export function ProvidersSettings() {
// Only custom supports multiple instances. // Only custom supports multiple instances.
// Built-in providers remain singleton by type. // Built-in providers remain singleton by type.
const id = type === 'custom' ? `custom-${crypto.randomUUID()}` : type; const id = type === 'custom' ? `custom-${crypto.randomUUID()}` : type;
const effectiveApiKey = resolveProviderApiKeyForSave(type, apiKey);
try { try {
await addProvider( await addProvider(
{ {
@@ -76,7 +78,7 @@ export function ProvidersSettings() {
model: options?.model, model: options?.model,
enabled: true, enabled: true,
}, },
apiKey.trim() || undefined effectiveApiKey
); );
// Auto-set as default if no default is currently configured // 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) { if (!payload.newApiKey && !payload.updates) {
onCancelEdit(); onCancelEdit();
setSaving(false); setSaving(false);

View File

@@ -21,6 +21,8 @@ export const PROVIDER_TYPES = [
] as const; ] as const;
export type ProviderType = (typeof PROVIDER_TYPES)[number]; export type ProviderType = (typeof PROVIDER_TYPES)[number];
export const OLLAMA_PLACEHOLDER_API_KEY = 'ollama-local';
export interface ProviderConfig { export interface ProviderConfig {
id: string; id: string;
name: 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', 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: '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: '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' }, { 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 { export function getProviderTypeInfo(type: ProviderType): ProviderTypeInfo | undefined {
return PROVIDER_TYPE_INFO.find((t) => t.id === type); 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;
}

View File

@@ -103,7 +103,7 @@ const defaultSkills: DefaultSkill[] = [
{ id: 'terminal', name: 'Terminal', description: 'Shell command execution' }, { 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'; import clawxIcon from '@/assets/logo.svg';
// Use the shared provider registry for setup providers // Use the shared provider registry for setup providers
@@ -970,6 +970,8 @@ function ProviderContent({
: `custom-${crypto.randomUUID()}`) : `custom-${crypto.randomUUID()}`)
: selectedProvider; : selectedProvider;
const effectiveApiKey = resolveProviderApiKeyForSave(selectedProvider, apiKey);
// Save provider config + API key, then set as default // Save provider config + API key, then set as default
const saveResult = await window.electron.ipcRenderer.invoke( const saveResult = await window.electron.ipcRenderer.invoke(
'provider:save', 'provider:save',
@@ -983,7 +985,7 @@ function ProviderContent({
createdAt: new Date().toISOString(), createdAt: new Date().toISOString(),
updatedAt: new Date().toISOString(), updatedAt: new Date().toISOString(),
}, },
apiKey || undefined effectiveApiKey
) as { success: boolean; error?: string }; ) as { success: boolean; error?: string };
if (!saveResult.success) { if (!saveResult.success) {

View File

@@ -1,5 +1,5 @@
import { describe, expect, it } from 'vitest'; 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 { import {
BUILTIN_PROVIDER_TYPES, BUILTIN_PROVIDER_TYPES,
getProviderConfig, 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']) 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');
});
}); });