diff --git a/packages/core/src/services/image-model/defaults.ts b/packages/core/src/services/image-model/defaults.ts index e4ab0e45..751aa6f3 100644 --- a/packages/core/src/services/image-model/defaults.ts +++ b/packages/core/src/services/image-model/defaults.ts @@ -14,6 +14,7 @@ const IMAGE_PROVIDER_ENV_KEYS = { seedream: ['VITE_SEEDREAM_API_KEY', 'VITE_ARK_API_KEY'], dashscope: ['VITE_DASHSCOPE_API_KEY'], modelscope: ['VITE_MODELSCOPE_API_KEY'], + ollama: [], cloudflare: ['VITE_CF_API_TOKEN'], grok: ['VITE_GROK_API_KEY', 'VITE_XAI_API_KEY'] } as const @@ -38,6 +39,7 @@ const IMAGE_BUILTIN_CONFIGS: readonly BuiltinImageConfigSpec[] = [ { providerId: 'seedream', configId: 'image-seedream-50-lite', modelId: 'doubao-seedream-5-0-260128', displayName: 'Doubao Seedream 5.0 Lite' }, { providerId: 'dashscope', configId: 'image-dashscope' }, { providerId: 'modelscope', configId: 'image-modelscope' }, + { providerId: 'ollama', configId: 'image-ollama' }, { providerId: 'cloudflare', configId: 'image-cloudflare-flux-klein' }, { providerId: 'grok', configId: 'image-grok-imagine' } ] as const @@ -63,6 +65,7 @@ const IMAGE_EXTRA_CONNECTION_ENV_KEYS: Record> * 某些 Provider 需要多个字段都存在时才视为可用 */ const IMAGE_REQUIRED_CONNECTION_FIELDS: Record = { + ollama: [], cloudflare: ['apiKey', 'accountId'] } @@ -74,6 +77,18 @@ function getFirstEnvValue(envKeys: readonly string[]): string { return '' } +function hasConnectionValue(value: unknown): boolean { + return typeof value === 'string' ? value.trim().length > 0 : !!value +} + +function shouldEnableFromRequiredFields( + connectionConfig: Record, + requiredConnectionFields: readonly string[] +): boolean { + return requiredConnectionFields.length > 0 + && requiredConnectionFields.every(field => hasConnectionValue(connectionConfig[field])) +} + /** * 图像模型默认配置生成器 * 返回完整的自包含配置对象,包含 provider 和 model 完整信息 @@ -123,10 +138,7 @@ export function getDefaultImageModels(registry?: IImageAdapterRegistry): Record< } const requiredConnectionFields = IMAGE_REQUIRED_CONNECTION_FIELDS[providerId] || ['apiKey'] - const enabled = requiredConnectionFields.every(field => { - const value = connectionConfig[field] - return typeof value === 'string' ? value.trim().length > 0 : !!value - }) + const enabled = shouldEnableFromRequiredFields(connectionConfig, requiredConnectionFields) result[builtinConfig.configId] = { id: builtinConfig.configId, diff --git a/packages/core/src/services/model/defaults.ts b/packages/core/src/services/model/defaults.ts index 2274dfee..36203512 100644 --- a/packages/core/src/services/model/defaults.ts +++ b/packages/core/src/services/model/defaults.ts @@ -19,6 +19,7 @@ const PROVIDER_ENV_KEYS = { dashscope: ['VITE_DASHSCOPE_API_KEY'], openrouter: ['VITE_OPENROUTER_API_KEY'], modelscope: ['VITE_MODELSCOPE_API_KEY'], + ollama: [], minimax: ['VITE_MINIMAX_API_KEY'], cloudflare: ['VITE_CF_API_TOKEN'], grok: ['VITE_GROK_API_KEY', 'VITE_XAI_API_KEY'] @@ -31,6 +32,7 @@ const PROVIDER_EXTRA_CONNECTION_ENV_KEYS: Record = { + ollama: [], cloudflare: ['apiKey', 'accountId'] }; @@ -42,6 +44,18 @@ function getFirstEnvValue(envKeys: readonly string[]): string { return ''; } +function hasConnectionValue(value: unknown): boolean { + return typeof value === 'string' ? value.trim().length > 0 : !!value; +} + +function shouldEnableFromRequiredFields( + connectionConfig: Record, + requiredConnectionFields: readonly string[] +): boolean { + return requiredConnectionFields.length > 0 + && requiredConnectionFields.every((field) => hasConnectionValue(connectionConfig[field])); +} + /** * 获取所有内置模型的 ID 列表 * 包括 PROVIDER_ENV_KEYS 中的所有 Provider 和 'custom' @@ -83,10 +97,7 @@ export function getDefaultTextModels(registry?: ITextAdapterRegistry): Record { - const value = connectionConfig[field]; - return typeof value === 'string' ? value.trim().length > 0 : !!value; - }); + const enabled = shouldEnableFromRequiredFields(connectionConfig, requiredConnectionFields); result[providerId] = { id: provider.id, @@ -103,9 +114,16 @@ export function getDefaultTextModels(registry?: ITextAdapterRegistry): Record value.trim().length > 0); let customHeaders: Record | undefined; if (rawCustomHeaders) { try { @@ -133,7 +151,7 @@ export function getDefaultTextModels(registry?: ITextAdapterRegistry): Record { expect(models['image-openrouter-nanobanana'].enabled).toBe(false) }) + it('includes Ollama but keeps it disabled without explicit user configuration', () => { + const models = getDefaultImageModels(registry) + const ollamaConfig = models['image-ollama'] + + expect(ollamaConfig).toBeDefined() + expect(ollamaConfig.providerId).toBe('ollama') + expect(ollamaConfig.provider.requiresApiKey).toBe(false) + expect(ollamaConfig.connectionConfig?.apiKey).toBe('') + expect(ollamaConfig.connectionConfig?.baseURL).toBe('http://localhost:11434/v1') + expect(ollamaConfig.enabled).toBe(false) + }) + it('OpenRouter model has correct provider and model information', () => { process.env.VITE_OPENROUTER_API_KEY = 'test-key' const models = getDefaultImageModels(registry) diff --git a/packages/core/tests/unit/model/defaults.test.ts b/packages/core/tests/unit/model/defaults.test.ts index 1bc33f85..767b6cbf 100644 --- a/packages/core/tests/unit/model/defaults.test.ts +++ b/packages/core/tests/unit/model/defaults.test.ts @@ -123,6 +123,19 @@ describe('model defaults provider env mapping', () => { expect(builtinModelIds).toContain('cloudflare') }) + it('should include ollama but keep it disabled without explicit user configuration', () => { + const builtinModelIds = getBuiltinModelIds() + const models = getDefaultTextModels() + + expect(builtinModelIds).toContain('ollama') + expect(models.ollama).toBeDefined() + expect(models.ollama.providerMeta.id).toBe('ollama') + expect(models.ollama.providerMeta.requiresApiKey).toBe(false) + expect(models.ollama.connectionConfig.apiKey).toBe('') + expect(models.ollama.connectionConfig.baseURL).toBe('http://localhost:11434/v1') + expect(models.ollama.enabled).toBe(false) + }) + it('should include cloudflare config and keep it disabled when credentials are empty', () => { const models = getDefaultTextModels() @@ -155,17 +168,26 @@ describe('model defaults provider env mapping', () => { expect(models.cloudflare.connectionConfig.accountId).toBe('') }) - it('should expose the custom preset as OpenAI-compatible with chat completions by default', () => { + it('should expose the custom preset as OpenAI-compatible with chat completions but keep it disabled by default', () => { const models = getDefaultTextModels() expect(models.custom).toBeDefined() expect(models.custom.providerMeta.id).toBe('openai-compatible') expect(models.custom.providerMeta.name).toBe('OpenAI Compatible (Custom)') - expect(models.custom.enabled).toBe(true) + expect(models.custom.enabled).toBe(false) expect(models.custom.connectionConfig.apiKey).toBe('') expect(models.custom.connectionConfig.requestStyle).toBe('chat_completions') }) + it('should enable the custom preset when explicit custom connection config is provided', () => { + process.env.VITE_CUSTOM_API_BASE_URL = 'http://localhost:11434/v1' + + const models = getDefaultTextModels() + + expect(models.custom.enabled).toBe(true) + expect(models.custom.connectionConfig.baseURL).toBe('http://localhost:11434/v1') + }) + it('should expose VITE_CUSTOM_API_HEADERS on the custom preset connection config', () => { process.env.VITE_CUSTOM_API_HEADERS = '{"x-auth-token":"gateway-token"}'