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:
@@ -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);
|
||||||
|
|||||||
@@ -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;
|
||||||
|
}
|
||||||
|
|||||||
@@ -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) {
|
||||||
|
|||||||
@@ -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');
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|||||||
Reference in New Issue
Block a user