From 69a636921ef5ddbcc909dae9ffbc574b4b96302d Mon Sep 17 00:00:00 2001 From: linshen <32978552+linshenkx@users.noreply.github.com> Date: Thu, 13 Aug 2026 22:13:54 +0800 Subject: [PATCH] feat: refresh built-in provider models --- .../core/src/services/image-model/defaults.ts | 2 +- .../core/src/services/image-model/manager.ts | 35 +++- .../src/services/image/adapters/dashscope.ts | 48 ++++- .../src/services/image/adapters/gemini.ts | 42 +++- .../src/services/image/adapters/seedream.ts | 2 +- .../llm/adapters/anthropic-adapter.ts | 181 ++++++++++-------- .../llm/adapters/dashscope-adapter.ts | 20 ++ .../services/llm/adapters/gemini-adapter.ts | 95 ++++++--- .../src/services/llm/adapters/grok-adapter.ts | 26 +-- .../services/llm/adapters/minimax-adapter.ts | 4 +- .../services/llm/adapters/openai-adapter.ts | 58 +++++- .../services/llm/adapters/zhipu-adapter.ts | 10 + packages/core/src/services/model/manager.ts | 60 +++++- .../model/migration.integration.test.ts | 2 +- .../unit/image/dashscope-adapter.test.ts | 43 +++++ .../tests/unit/image/gemini-adapter.test.ts | 12 +- .../unit/image/image-default-models.test.ts | 2 +- .../unit/image/image-model-manager.test.ts | 35 ++++ .../tests/unit/image/seedream-adapter.test.ts | 6 +- .../tests/unit/llm/anthropic-adapter.test.ts | 32 ++++ .../tests/unit/llm/gemini-adapter.test.ts | 63 ++++-- .../core/tests/unit/llm/grok-adapter.test.ts | 12 +- .../tests/unit/llm/minimax-adapter.test.ts | 3 +- .../tests/unit/llm/openai-adapter.test.ts | 25 ++- .../core/tests/unit/model/defaults.test.ts | 6 +- .../core/tests/unit/model/manager.test.ts | 51 +++++ 26 files changed, 693 insertions(+), 182 deletions(-) diff --git a/packages/core/src/services/image-model/defaults.ts b/packages/core/src/services/image-model/defaults.ts index 751aa6f3..3bd46cdf 100644 --- a/packages/core/src/services/image-model/defaults.ts +++ b/packages/core/src/services/image-model/defaults.ts @@ -36,7 +36,7 @@ const IMAGE_BUILTIN_CONFIGS: readonly BuiltinImageConfigSpec[] = [ { providerId: 'openai', configId: 'image-openai-gpt' }, { providerId: 'siliconflow', configId: 'image-siliconflow-kolors' }, { providerId: 'seedream', configId: 'image-seedream', modelId: 'doubao-seedream-4-0-250828' }, - { providerId: 'seedream', configId: 'image-seedream-50-lite', modelId: 'doubao-seedream-5-0-260128', displayName: 'Doubao Seedream 5.0 Lite' }, + { providerId: 'seedream', configId: 'image-seedream-50-lite', modelId: 'doubao-seedream-5-0-lite-260128', displayName: 'Doubao Seedream 5.0 Lite' }, { providerId: 'dashscope', configId: 'image-dashscope' }, { providerId: 'modelscope', configId: 'image-modelscope' }, { providerId: 'ollama', configId: 'image-ollama' }, diff --git a/packages/core/src/services/image-model/manager.ts b/packages/core/src/services/image-model/manager.ts index e40dffdf..8fe33f1f 100644 --- a/packages/core/src/services/image-model/manager.ts +++ b/packages/core/src/services/image-model/manager.ts @@ -81,7 +81,14 @@ export class ImageModelManager implements IImageModelManager { data[key] = cfg changed = true } else { - const existingConfig = data[key] + let existingConfig = data[key] + const upgradedConfig = this.patchBuiltinModelUpgrade(key, existingConfig, cfg) + if (upgradedConfig !== existingConfig) { + existingConfig = upgradedConfig + data[key] = upgradedConfig + changed = true + console.log(`[ImageModelManager] Migrated legacy builtin model: ${key}`) + } const backfillableFields = this.getBackfillableBuiltinConnectionFields( key, existingConfig, @@ -129,6 +136,32 @@ export class ImageModelManager implements IImageModelManager { } } + private patchBuiltinModelUpgrade( + key: string, + config: ImageModelConfig, + defaultConfig: ImageModelConfig + ): ImageModelConfig { + const legacyDefaultIds: Record = { + 'image-gemini-nanobanana': ['gemini-2.5-flash-image', 'gemini-3.1-flash-image-preview'], + 'image-dashscope': ['qwen-image'], + 'image-seedream-50-lite': ['doubao-seedream-5-0-260128'] + } + const currentModelId = config.modelId || config.model?.id + if (!currentModelId || !legacyDefaultIds[key]?.includes(currentModelId)) { + return config + } + + return { + ...config, + modelId: defaultConfig.modelId, + model: defaultConfig.model, + paramOverrides: { + ...(defaultConfig.paramOverrides || {}), + ...(config.paramOverrides || {}) + } + } + } + // === 配置 CRUD 操作 === async addConfig(config: ImageModelConfigInput): Promise { diff --git a/packages/core/src/services/image/adapters/dashscope.ts b/packages/core/src/services/image/adapters/dashscope.ts index fbb413f6..f7343b67 100644 --- a/packages/core/src/services/image/adapters/dashscope.ts +++ b/packages/core/src/services/image/adapters/dashscope.ts @@ -54,6 +54,23 @@ export class DashScopeImageAdapter extends AbstractImageProviderAdapter { getModels(): ImageModel[] { return [ + { + id: 'qwen-image-2.0', + name: 'Qwen Image 2.0', + description: 'Unified Qwen image model for text-to-image generation and image editing', + providerId: 'dashscope', + capabilities: { + text2image: true, + image2image: true, + multiImage: true + }, + parameterDefinitions: this.getQwenImageParameterDefinitions(), + defaultParameterValues: { + size: '1328*1328', + prompt_extend: true, + watermark: false + } + }, // Qwen-Image 文生图模型 { id: 'qwen-image', @@ -181,6 +198,17 @@ export class DashScopeImageAdapter extends AbstractImageProviderAdapter { } } + if (testType === 'image2image') { + return { + prompt: 'make this image more colorful', + inputImage: { + b64: AbstractImageProviderAdapter.TEST_IMAGE_BASE64.split(',')[1], + mimeType: 'image/png' + }, + count: 1 + } + } + throw new ImageError(IMAGE_ERROR_CODES.UNSUPPORTED_TEST_TYPE, undefined, { testType }) } @@ -209,8 +237,13 @@ export class DashScopeImageAdapter extends AbstractImageProviderAdapter { return modelId.startsWith('qwen-image-edit') } + private isQwenImageUnifiedModel(modelId: string): boolean { + return modelId === 'qwen-image-2.0' + } + protected async doGenerate(request: ImageRequest, config: ImageModelConfig): Promise { - if (this.isQwenImageEditModel(config.modelId)) { + const hasInputImages = !!request.inputImage || !!request.inputImages?.length + if (this.isQwenImageEditModel(config.modelId) || (this.isQwenImageUnifiedModel(config.modelId) && hasInputImages)) { // Qwen-Image-Edit 图生图使用同步 API return await this.generateWithQwenImageEdit(request, config) } @@ -325,12 +358,18 @@ export class DashScopeImageAdapter extends AbstractImageProviderAdapter { // 构建 content 数组,包含输入图像和文本提示词 const content: Array<{ image?: string; text?: string }> = [] + const inputImages = request.inputImages?.length + ? request.inputImages + : request.inputImage + ? [request.inputImage] + : [] + // 添加输入图像 - if (request.inputImage) { + for (const inputImage of inputImages) { // DashScope 的 image 字段要求:公网 URL 或 data:{mime};base64,{data}。 // 本项目只支持本地 base64,因此这里统一拼成 data URL。 - const mimeType = request.inputImage.mimeType || 'image/png' - const b64 = request.inputImage.b64 || '' + const mimeType = inputImage.mimeType || 'image/png' + const b64 = inputImage.b64 || '' const dataUrl = b64.startsWith('data:') ? b64 : `data:${mimeType};base64,${b64}` content.push({ image: dataUrl }) } @@ -349,6 +388,7 @@ export class DashScopeImageAdapter extends AbstractImageProviderAdapter { ] }, parameters: { + ...(this.isQwenImageUnifiedModel(config.modelId) ? { size: merged.size || '1328*1328' } : {}), ...(merged.negative_prompt ? { negative_prompt: merged.negative_prompt } : {}), ...(merged.prompt_extend !== undefined ? { prompt_extend: merged.prompt_extend } : {}), ...(merged.watermark !== undefined ? { watermark: merged.watermark } : {}), diff --git a/packages/core/src/services/image/adapters/gemini.ts b/packages/core/src/services/image/adapters/gemini.ts index bc78aac7..41ede59b 100644 --- a/packages/core/src/services/image/adapters/gemini.ts +++ b/packages/core/src/services/image/adapters/gemini.ts @@ -36,9 +36,9 @@ export class GeminiImageAdapter extends AbstractImageProviderAdapter { getModels(): ImageModel[] { return [ { - id: 'gemini-2.5-flash-image', - name: 'Gemini 2.5 Flash Image', - description: 'Google Gemini 2.5 Flash image generation model (Nano Banana) with text-to-image, image editing, and multi-image input support', + id: 'gemini-3.1-flash-image', + name: 'Gemini 3.1 Flash Image', + description: 'Stable Gemini 3.1 Flash image model with text-to-image, editing, and multi-image input support', providerId: 'gemini', capabilities: { text2image: true, @@ -51,9 +51,39 @@ export class GeminiImageAdapter extends AbstractImageProviderAdapter { } }, { - id: 'gemini-3.1-flash-image-preview', - name: 'Gemini 3.1 Flash Image Preview', - description: 'Google Gemini 3.1 Flash image generation preview model with text-to-image, image editing, and multi-image input support', + id: 'gemini-3.1-flash-lite-image', + name: 'Gemini 3.1 Flash-Lite Image', + description: 'Efficient Gemini 3.1 image model for fast, cost-sensitive generation and editing', + providerId: 'gemini', + capabilities: { + text2image: true, + image2image: true, + multiImage: true + }, + parameterDefinitions: [], + defaultParameterValues: { + outputMimeType: 'image/png' + } + }, + { + id: 'gemini-3-pro-image', + name: 'Gemini 3 Pro Image', + description: 'High-capability Gemini image model for detailed generation and editing', + providerId: 'gemini', + capabilities: { + text2image: true, + image2image: true, + multiImage: true + }, + parameterDefinitions: [], + defaultParameterValues: { + outputMimeType: 'image/png' + } + }, + { + id: 'gemini-2.5-flash-image', + name: 'Gemini 2.5 Flash Image', + description: 'Legacy Gemini 2.5 Flash image model retained for compatibility', providerId: 'gemini', capabilities: { text2image: true, diff --git a/packages/core/src/services/image/adapters/seedream.ts b/packages/core/src/services/image/adapters/seedream.ts index ebaaff15..6cd7fc52 100644 --- a/packages/core/src/services/image/adapters/seedream.ts +++ b/packages/core/src/services/image/adapters/seedream.ts @@ -12,7 +12,7 @@ import { IMAGE_ERROR_CODES } from '../../../constants/error-codes' const SEEDREAM_40_MODEL_ID = 'doubao-seedream-4-0-250828' const SEEDREAM_45_MODEL_ID = 'doubao-seedream-4-5-251128' -const SEEDREAM_50_LITE_MODEL_ID = 'doubao-seedream-5-0-260128' +const SEEDREAM_50_LITE_MODEL_ID = 'doubao-seedream-5-0-lite-260128' const SEEDREAM_40_SIZE_VALUES = ['1K', '2K', '4K', '1024x1024', '512x512', '768x768', '1024x768', '768x1024'] as const const SEEDREAM_45_SIZE_VALUES = ['2K', '4K', '2048x2048'] as const diff --git a/packages/core/src/services/llm/adapters/anthropic-adapter.ts b/packages/core/src/services/llm/adapters/anthropic-adapter.ts index 31394280..2e07df01 100644 --- a/packages/core/src/services/llm/adapters/anthropic-adapter.ts +++ b/packages/core/src/services/llm/adapters/anthropic-adapter.ts @@ -64,32 +64,44 @@ export class AnthropicAdapter extends AbstractTextProviderAdapter { const providerId = 'anthropic' return [ - // Claude 4.0 系列 { - id: 'claude-opus-4-20250514', - name: 'Claude 4.0 Opus', - description: 'Most powerful Claude model for complex tasks', + id: 'claude-sonnet-5', + name: 'Claude Sonnet 5', + description: 'Balanced Claude 5 model for coding, agents, and general-purpose work', providerId, capabilities: { - supportsTools: true, - supportsReasoning: false, - maxContextLength: 200000 + supportsTools: true, + supportsReasoning: true, + maxContextLength: 1000000 }, - parameterDefinitions: this.getParameterDefinitions('claude-opus-4-20250514'), - defaultParameterValues: this.getDefaultParameterValues('claude-opus-4-20250514') + parameterDefinitions: this.getParameterDefinitions('claude-sonnet-5'), + defaultParameterValues: this.getDefaultParameterValues('claude-sonnet-5') }, { - id: 'claude-sonnet-4-20250514', - name: 'Claude 4.0 Sonnet', - description: 'Balanced Claude model for most tasks', + id: 'claude-opus-5', + name: 'Claude Opus 5', + description: 'Most capable Claude model for complex reasoning and agentic tasks', providerId, capabilities: { - supportsTools: true, - supportsReasoning: false, + supportsTools: true, + supportsReasoning: true, + maxContextLength: 1000000 + }, + parameterDefinitions: this.getParameterDefinitions('claude-opus-5'), + defaultParameterValues: this.getDefaultParameterValues('claude-opus-5') + }, + { + id: 'claude-haiku-4-5-20251001', + name: 'Claude Haiku 4.5', + description: 'Fast, efficient Claude model for high-throughput workloads', + providerId, + capabilities: { + supportsTools: true, + supportsReasoning: true, maxContextLength: 200000 }, - parameterDefinitions: this.getParameterDefinitions('claude-sonnet-4-20250514'), - defaultParameterValues: this.getDefaultParameterValues('claude-sonnet-4-20250514') + parameterDefinitions: this.getParameterDefinitions('claude-haiku-4-5-20251001'), + defaultParameterValues: this.getDefaultParameterValues('claude-haiku-4-5-20251001') } ] } @@ -150,8 +162,8 @@ export class AnthropicAdapter extends AbstractTextProviderAdapter { /** * 获取参数定义 */ - protected getParameterDefinitions(_modelId: string): readonly ParameterDefinition[] { - return [ + protected getParameterDefinitions(modelId: string): readonly ParameterDefinition[] { + const samplingDefinitions: ParameterDefinition[] = [ { name: 'temperature', labelKey: 'params.temperature.label', @@ -185,11 +197,13 @@ export class AnthropicAdapter extends AbstractTextProviderAdapter { labelKey: 'params.top_k.label', descriptionKey: 'params.top_k.description', description: 'Top-k sampling parameter', - type: 'integer', + type: 'integer' as const, minValue: 1, min: 1, step: 1 }, + ] + const sharedDefinitions: ParameterDefinition[] = [ { name: 'max_tokens', labelKey: 'params.max_tokens.label', @@ -203,28 +217,71 @@ export class AnthropicAdapter extends AbstractTextProviderAdapter { unitKey: 'params.tokens.unit', step: 1 }, - { + ...(this.isAdaptiveThinkingModel(modelId) ? [{ + name: 'effort', + labelKey: 'params.reasoning_effort.label', + descriptionKey: 'params.reasoning_effort.description', + description: 'Adaptive reasoning effort for Claude 5.', + type: 'string' as const, + defaultValue: 'high', + default: 'high', + allowedValues: ['low', 'medium', 'high', 'xhigh', 'max'] + }] : [{ name: 'thinking_budget_tokens', labelKey: 'params.thinkingBudget.label', descriptionKey: 'params.thinkingBudget.description', description: 'Extended thinking budget in tokens (requires ≥1024)', - type: 'integer', + type: 'integer' as const, minValue: 1024, min: 1024, unitKey: 'params.tokens.unit', step: 1, tags: ['advanced'] - } + }]) ] + + return this.isAdaptiveThinkingModel(modelId) + ? sharedDefinitions + : [...samplingDefinitions, ...sharedDefinitions] } /** * 获取默认参数值 * 返回空对象,让服务器使用官方默认值,避免客户端错误默认值影响效果 */ - protected getDefaultParameterValues(_modelId: string): Record { + protected getDefaultParameterValues(modelId: string): Record { return { max_tokens: DEFAULT_MAX_TOKENS, // 8192 - Anthropic API 强制要求 + ...(this.isAdaptiveThinkingModel(modelId) ? { effort: 'high' } : {}) + } + } + + private isAdaptiveThinkingModel(modelId: string): boolean { + return /^claude-(?:sonnet|opus)-5(?:-|$)/.test(modelId) + } + + private applyThinkingConfig( + requestParams: Record, + modelId: string, + thinkingBudgetTokens: unknown, + effort: unknown + ): void { + if (this.isAdaptiveThinkingModel(modelId)) { + requestParams.thinking = { type: 'adaptive' } + if (['low', 'medium', 'high', 'xhigh', 'max'].includes(String(effort))) { + requestParams.output_config = { + ...(requestParams.output_config || {}), + effort + } + } + return + } + + if (typeof thinkingBudgetTokens === 'number' && thinkingBudgetTokens >= 1024) { + requestParams.thinking = { + type: 'enabled', + budget_tokens: thinkingBudgetTokens + } } } @@ -247,6 +304,7 @@ export class AnthropicAdapter extends AbstractTextProviderAdapter { top_p, top_k, thinking_budget_tokens, + effort, ...otherParams // 其他参数(包括自定义参数) } = (config.paramOverrides || {}) as any @@ -257,13 +315,13 @@ export class AnthropicAdapter extends AbstractTextProviderAdapter { } // 只在用户明确设置时才添加参数,避免使用客户端默认值 - if (temperature !== undefined) { + if (!this.isAdaptiveThinkingModel(config.modelMeta.id) && temperature !== undefined) { requestParams.temperature = temperature } - if (top_p !== undefined) { + if (!this.isAdaptiveThinkingModel(config.modelMeta.id) && top_p !== undefined) { requestParams.top_p = top_p } - if (top_k !== undefined) { + if (!this.isAdaptiveThinkingModel(config.modelMeta.id) && top_k !== undefined) { requestParams.top_k = top_k } @@ -273,16 +331,9 @@ export class AnthropicAdapter extends AbstractTextProviderAdapter { requestParams.system = systemMessage } - // 添加 Extended Thinking 配置 - if (thinking_budget_tokens !== undefined && thinking_budget_tokens >= 1024) { - requestParams.thinking = { - type: 'enabled', - budget_tokens: thinking_budget_tokens - } - } - // 添加其他参数(包括自定义参数) Object.assign(requestParams, otherParams) + this.applyThinkingConfig(requestParams, config.modelMeta.id, thinking_budget_tokens, effort) const response = await client.messages.create(requestParams) @@ -321,6 +372,7 @@ export class AnthropicAdapter extends AbstractTextProviderAdapter { top_p, top_k, thinking_budget_tokens, + effort, responseMimeType: _responseMimeType, ...otherParams } = mergedParams as any @@ -349,26 +401,20 @@ export class AnthropicAdapter extends AbstractTextProviderAdapter { max_tokens: max_tokens ?? DEFAULT_MAX_TOKENS } - if (temperature !== undefined) { + if (!this.isAdaptiveThinkingModel(config.modelMeta.id) && temperature !== undefined) { requestParams.temperature = temperature } - if (top_p !== undefined) { + if (!this.isAdaptiveThinkingModel(config.modelMeta.id) && top_p !== undefined) { requestParams.top_p = top_p } - if (top_k !== undefined) { + if (!this.isAdaptiveThinkingModel(config.modelMeta.id) && top_k !== undefined) { requestParams.top_k = top_k } if (request.systemPrompt?.trim()) { requestParams.system = request.systemPrompt } - if (thinking_budget_tokens !== undefined && thinking_budget_tokens >= 1024) { - requestParams.thinking = { - type: 'enabled', - budget_tokens: thinking_budget_tokens - } - } - Object.assign(requestParams, otherParams) + this.applyThinkingConfig(requestParams, config.modelMeta.id, thinking_budget_tokens, effort) const response = await client.messages.create(requestParams) const reasoning = this.extractThinking(response) @@ -407,6 +453,7 @@ export class AnthropicAdapter extends AbstractTextProviderAdapter { top_p, top_k, thinking_budget_tokens, + effort, responseMimeType: _responseMimeType, ...otherParams } = mergedParams as any @@ -435,26 +482,20 @@ export class AnthropicAdapter extends AbstractTextProviderAdapter { max_tokens: max_tokens ?? DEFAULT_MAX_TOKENS } - if (temperature !== undefined) { + if (!this.isAdaptiveThinkingModel(config.modelMeta.id) && temperature !== undefined) { requestParams.temperature = temperature } - if (top_p !== undefined) { + if (!this.isAdaptiveThinkingModel(config.modelMeta.id) && top_p !== undefined) { requestParams.top_p = top_p } - if (top_k !== undefined) { + if (!this.isAdaptiveThinkingModel(config.modelMeta.id) && top_k !== undefined) { requestParams.top_k = top_k } if (request.systemPrompt?.trim()) { requestParams.system = request.systemPrompt } - if (thinking_budget_tokens !== undefined && thinking_budget_tokens >= 1024) { - requestParams.thinking = { - type: 'enabled', - budget_tokens: thinking_budget_tokens - } - } - Object.assign(requestParams, otherParams) + this.applyThinkingConfig(requestParams, config.modelMeta.id, thinking_budget_tokens, effort) const stream = await client.messages.stream(requestParams) @@ -513,6 +554,7 @@ export class AnthropicAdapter extends AbstractTextProviderAdapter { top_p, top_k, thinking_budget_tokens, + effort, ...otherParams // 其他参数(包括自定义参数) } = (config.paramOverrides || {}) as any @@ -523,13 +565,13 @@ export class AnthropicAdapter extends AbstractTextProviderAdapter { } // 只在用户明确设置时才添加参数,避免使用客户端默认值 - if (temperature !== undefined) { + if (!this.isAdaptiveThinkingModel(config.modelMeta.id) && temperature !== undefined) { requestParams.temperature = temperature } - if (top_p !== undefined) { + if (!this.isAdaptiveThinkingModel(config.modelMeta.id) && top_p !== undefined) { requestParams.top_p = top_p } - if (top_k !== undefined) { + if (!this.isAdaptiveThinkingModel(config.modelMeta.id) && top_k !== undefined) { requestParams.top_k = top_k } @@ -539,16 +581,9 @@ export class AnthropicAdapter extends AbstractTextProviderAdapter { requestParams.system = systemMessage } - // 添加 Extended Thinking 配置 - if (thinking_budget_tokens !== undefined && thinking_budget_tokens >= 1024) { - requestParams.thinking = { - type: 'enabled', - budget_tokens: thinking_budget_tokens - } - } - // 添加其他参数(包括自定义参数) Object.assign(requestParams, otherParams) + this.applyThinkingConfig(requestParams, config.modelMeta.id, thinking_budget_tokens, effort) const stream = await client.messages.stream(requestParams) @@ -614,6 +649,7 @@ export class AnthropicAdapter extends AbstractTextProviderAdapter { top_p, top_k, thinking_budget_tokens, + effort, ...otherParams // 其他参数(包括自定义参数) } = (config.paramOverrides || {}) as any @@ -625,13 +661,13 @@ export class AnthropicAdapter extends AbstractTextProviderAdapter { } // 只在用户明确设置时才添加参数,避免使用客户端默认值 - if (temperature !== undefined) { + if (!this.isAdaptiveThinkingModel(config.modelMeta.id) && temperature !== undefined) { requestParams.temperature = temperature } - if (top_p !== undefined) { + if (!this.isAdaptiveThinkingModel(config.modelMeta.id) && top_p !== undefined) { requestParams.top_p = top_p } - if (top_k !== undefined) { + if (!this.isAdaptiveThinkingModel(config.modelMeta.id) && top_k !== undefined) { requestParams.top_k = top_k } @@ -641,16 +677,9 @@ export class AnthropicAdapter extends AbstractTextProviderAdapter { requestParams.system = systemMessage } - // 添加 Extended Thinking 配置 - if (thinking_budget_tokens !== undefined && thinking_budget_tokens >= 1024) { - requestParams.thinking = { - type: 'enabled', - budget_tokens: thinking_budget_tokens - } - } - // 添加其他参数(包括自定义参数) Object.assign(requestParams, otherParams) + this.applyThinkingConfig(requestParams, config.modelMeta.id, thinking_budget_tokens, effort) const stream = await client.messages.stream(requestParams) diff --git a/packages/core/src/services/llm/adapters/dashscope-adapter.ts b/packages/core/src/services/llm/adapters/dashscope-adapter.ts index 79e39a33..aed5d514 100644 --- a/packages/core/src/services/llm/adapters/dashscope-adapter.ts +++ b/packages/core/src/services/llm/adapters/dashscope-adapter.ts @@ -13,6 +13,26 @@ interface ModelOverride { * 阿里百炼(DashScope)静态模型定义 */ const DASHSCOPE_STATIC_MODELS: ModelOverride[] = [ + { + id: 'qwen3.7-plus', + name: 'Qwen3.7 Plus', + description: 'Latest Qwen Plus model for complex reasoning and agentic workloads', + capabilities: { + supportsTools: true, + supportsReasoning: true, + maxContextLength: 1000000 + } + }, + { + id: 'qwen3.6-flash', + name: 'Qwen3.6 Flash', + description: 'Fast Qwen model for low-latency general-purpose workloads', + capabilities: { + supportsTools: true, + supportsReasoning: true, + maxContextLength: 1000000 + } + }, { id: 'qwen3.5-27b', name: 'Qwen3.5-27B', diff --git a/packages/core/src/services/llm/adapters/gemini-adapter.ts b/packages/core/src/services/llm/adapters/gemini-adapter.ts index 582c18b8..5570cc81 100644 --- a/packages/core/src/services/llm/adapters/gemini-adapter.ts +++ b/packages/core/src/services/llm/adapters/gemini-adapter.ts @@ -33,19 +33,9 @@ interface ModelOverride { */ const GEMINI_STATIC_MODELS: ModelOverride[] = [ { - id: 'gemini-2.5-flash', - name: 'Gemini 2.5 Flash', - description: 'Latest Gemini 2.5 Flash model, fast and efficient', - capabilities: { - supportsTools: true, - supportsReasoning: false, - maxContextLength: 1000000 - } - }, - { - id: 'gemini-2.5-pro', - name: 'Gemini 2.5 Pro', - description: 'Gemini 2.5 Pro model with enhanced reasoning capabilities', + id: 'gemini-3.6-flash', + name: 'Gemini 3.6 Flash', + description: 'Latest Gemini Flash model for fast multimodal reasoning and agentic workloads', capabilities: { supportsTools: true, supportsReasoning: true, @@ -53,9 +43,19 @@ const GEMINI_STATIC_MODELS: ModelOverride[] = [ } }, { - id: 'gemini-3-pro-preview', - name: 'Gemini 3 Pro Preview', - description: 'Preview version of Gemini 3 Pro with cutting-edge capabilities', + id: 'gemini-3.5-flash-lite', + name: 'Gemini 3.5 Flash-Lite', + description: 'Efficient Gemini model for high-throughput, cost-sensitive workloads', + capabilities: { + supportsTools: true, + supportsReasoning: true, + maxContextLength: 1000000 + } + }, + { + id: 'gemini-3.1-pro-preview', + name: 'Gemini 3.1 Pro Preview', + description: 'Advanced Gemini Pro preview model for complex reasoning and coding', capabilities: { supportsTools: true, supportsReasoning: true, @@ -188,8 +188,8 @@ export class GeminiAdapter extends AbstractTextProviderAdapter { /** * 获取参数定义 */ - protected getParameterDefinitions(_modelId: string): readonly ParameterDefinition[] { - return [ + protected getParameterDefinitions(modelId: string): readonly ParameterDefinition[] { + const definitions: ParameterDefinition[] = [ { name: 'temperature', labelKey: 'params.temperature.label', @@ -290,6 +290,31 @@ export class GeminiAdapter extends AbstractTextProviderAdapter { defaultValue: false } ] + + if (this.isGemini3Model(modelId)) { + const allowedValues = modelId.replace(/^models\//, '').startsWith('gemini-3.1-pro') + ? ['low', 'medium', 'high'] + : ['minimal', 'low', 'medium', 'high'] + return [ + ...definitions.filter((definition) => { + if (definition.name === 'thinkingBudget') return false + if (this.usesDeprecatedSamplingParameters(modelId)) { + return !['temperature', 'topP', 'topK', 'candidateCount'].includes(definition.name) + } + return true + }), + { + name: 'thinkingLevel', + labelKey: 'params.reasoning_effort.label', + descriptionKey: 'params.reasoning_effort.description', + description: 'Dynamic thinking level for Gemini 3 models.', + type: 'string', + allowedValues + } + ] + } + + return definitions } /** @@ -334,6 +359,7 @@ export class GeminiAdapter extends AbstractTextProviderAdapter { * @returns GenerateContentConfig */ private buildGenerationConfig( + modelId: string, params: Record = {}, systemInstruction?: string ): GenerateContentConfig { @@ -345,6 +371,7 @@ export class GeminiAdapter extends AbstractTextProviderAdapter { candidateCount, stopSequences, thinkingBudget, // 思考预算(token数) + thinkingLevel, // Gemini 3 思考等级 includeThoughts, // 是否包含思考过程 ...otherParams } = params @@ -357,19 +384,21 @@ export class GeminiAdapter extends AbstractTextProviderAdapter { } // 添加已知参数 - if (temperature !== undefined) { + const allowSamplingParameters = !this.usesDeprecatedSamplingParameters(modelId) + + if (allowSamplingParameters && temperature !== undefined) { config.temperature = temperature } if (maxOutputTokens !== undefined) { config.maxOutputTokens = maxOutputTokens } - if (topP !== undefined) { + if (allowSamplingParameters && topP !== undefined) { config.topP = topP } - if (topK !== undefined) { + if (allowSamplingParameters && topK !== undefined) { config.topK = topK } - if (candidateCount !== undefined) { + if (!this.usesDeprecatedSamplingParameters(modelId) && candidateCount !== undefined) { config.candidateCount = candidateCount } if (stopSequences !== undefined && Array.isArray(stopSequences)) { @@ -377,13 +406,17 @@ export class GeminiAdapter extends AbstractTextProviderAdapter { } // 添加思考配置(Gemini 2.5+ 支持) - if (thinkingBudget !== undefined || includeThoughts !== undefined) { + if (thinkingBudget !== undefined || thinkingLevel !== undefined || includeThoughts !== undefined) { ;(config as any).thinkingConfig = {} - if (thinkingBudget !== undefined) { + if (!this.isGemini3Model(modelId) && thinkingBudget !== undefined) { ;(config as any).thinkingConfig.thinkingBudget = thinkingBudget } + if (this.isGemini3Model(modelId) && thinkingLevel !== undefined) { + ;(config as any).thinkingConfig.thinkingLevel = thinkingLevel + } + if (includeThoughts !== undefined) { ;(config as any).thinkingConfig.includeThoughts = includeThoughts } @@ -399,6 +432,15 @@ export class GeminiAdapter extends AbstractTextProviderAdapter { return config } + private usesDeprecatedSamplingParameters(modelId: string): boolean { + const normalizedModelId = modelId.replace(/^models\//, '') + return normalizedModelId.startsWith('gemini-3.5-') || normalizedModelId.startsWith('gemini-3.6-') + } + + private isGemini3Model(modelId: string): boolean { + return modelId.replace(/^models\//, '').startsWith('gemini-3') + } + /** * 转换工具定义为 Gemini 格式 * 将标准的 ToolDefinition 转换为 Gemini SDK 所需的 Tool 格式 @@ -504,6 +546,7 @@ export class GeminiAdapter extends AbstractTextProviderAdapter { // 构建配置(包含系统指令) const generationConfig = this.buildGenerationConfig( + config.modelMeta.id, config.paramOverrides || {}, systemInstruction ) @@ -536,6 +579,7 @@ export class GeminiAdapter extends AbstractTextProviderAdapter { } as Record const generationConfig = this.buildGenerationConfig( + config.modelMeta.id, mergedParams, request.systemPrompt ) @@ -585,6 +629,7 @@ export class GeminiAdapter extends AbstractTextProviderAdapter { } as Record const generationConfig = this.buildGenerationConfig( + config.modelMeta.id, mergedParams, request.systemPrompt ) @@ -744,6 +789,7 @@ export class GeminiAdapter extends AbstractTextProviderAdapter { // 构建配置(包含系统指令) const generationConfig = this.buildGenerationConfig( + config.modelMeta.id, config.paramOverrides || {}, systemInstruction ) @@ -851,6 +897,7 @@ export class GeminiAdapter extends AbstractTextProviderAdapter { // 构建配置(包含系统指令和工具) const generationConfig = this.buildGenerationConfig( + config.modelMeta.id, config.paramOverrides || {}, systemInstruction ) diff --git a/packages/core/src/services/llm/adapters/grok-adapter.ts b/packages/core/src/services/llm/adapters/grok-adapter.ts index a39a2d8e..a39e4812 100644 --- a/packages/core/src/services/llm/adapters/grok-adapter.ts +++ b/packages/core/src/services/llm/adapters/grok-adapter.ts @@ -16,21 +16,21 @@ interface ModelOverride { const GROK_STATIC_MODELS: ModelOverride[] = [ { - id: 'grok-4.3', - name: 'Grok 4.3', - description: 'xAI Grok 4.3 model via OpenAI-compatible Chat Completions API', + id: 'grok-4.5', + name: 'Grok 4.5', + description: 'xAI Grok 4.5 reasoning model via OpenAI-compatible Chat Completions API', capabilities: { supportsTools: true, supportsReasoning: true, - maxContextLength: 1000000 + maxContextLength: 500000 }, defaultParameterValues: { - reasoning_effort: 'none' + reasoning_effort: 'high' } } ] -type GrokReasoningEffort = 'none' | 'low' | 'medium' | 'high' +type GrokReasoningEffort = 'low' | 'medium' | 'high' export class GrokAdapter extends OpenAIAdapter { public getProvider(): TextProvider { @@ -125,11 +125,11 @@ export class GrokAdapter extends OpenAIAdapter { name: 'reasoning_effort', labelKey: 'params.reasoning_effort.label', descriptionKey: 'params.reasoning_effort.description', - description: 'Reasoning effort for Grok. Use none to disable reasoning tokens.', + description: 'Reasoning effort for Grok 4.5.', type: 'string', - defaultValue: 'none', - default: 'none', - allowedValues: ['none', 'low', 'medium', 'high'] + defaultValue: 'high', + default: 'high', + allowedValues: ['low', 'medium', 'high'] }, { name: 'temperature', @@ -192,7 +192,7 @@ export class GrokAdapter extends OpenAIAdapter { protected getDefaultParameterValues(_modelId: string): Record { return { - reasoning_effort: 'none' + reasoning_effort: 'high' } } @@ -209,13 +209,13 @@ export class GrokAdapter extends OpenAIAdapter { ...model, defaultParameterValues: { ...(model.defaultParameterValues || {}), - reasoning_effort: effort ?? 'none' + reasoning_effort: effort ?? 'high' } } } private normalizeReasoningEffort(value: unknown): GrokReasoningEffort | undefined { - return value === 'none' || value === 'low' || value === 'medium' || value === 'high' + return value === 'low' || value === 'medium' || value === 'high' ? value : undefined } diff --git a/packages/core/src/services/llm/adapters/minimax-adapter.ts b/packages/core/src/services/llm/adapters/minimax-adapter.ts index e394006e..e50017c3 100644 --- a/packages/core/src/services/llm/adapters/minimax-adapter.ts +++ b/packages/core/src/services/llm/adapters/minimax-adapter.ts @@ -51,8 +51,8 @@ const MINIMAX_STATIC_MODELS: ModelOverride[] = [ description: 'High-speed version of M2.7 for low-latency scenarios', capabilities: { supportsTools: true, - supportsReasoning: false, - maxContextLength: 1000000 + supportsReasoning: true, + maxContextLength: 204800 } } ] diff --git a/packages/core/src/services/llm/adapters/openai-adapter.ts b/packages/core/src/services/llm/adapters/openai-adapter.ts index 5096fc87..273dc272 100644 --- a/packages/core/src/services/llm/adapters/openai-adapter.ts +++ b/packages/core/src/services/llm/adapters/openai-adapter.ts @@ -27,23 +27,42 @@ interface ModelOverride { */ const OPENAI_STATIC_MODELS: ModelOverride[] = [ { - id: 'gpt-5-mini', - name: 'GPT-5 Mini', - description: 'Fast, capable, and efficient small model with significant improvements in instruction-following and coding', + id: 'gpt-5.6-terra', + name: 'GPT-5.6 Terra', + description: 'Balanced GPT-5.6 model for general-purpose work with strong reasoning and low latency', capabilities: { supportsTools: true, - supportsReasoning: false, - maxContextLength: 1047576 + supportsReasoning: true, + maxContextLength: 1050000 + }, + defaultParameterValues: { + reasoning_effort: 'none' } }, { - id: 'gpt-5.1', - name: 'GPT-5.1', - description: 'Latest GPT-5.1 flagship model with enhanced capabilities', + id: 'gpt-5.6-sol', + name: 'GPT-5.6 Sol', + description: 'Most capable GPT-5.6 model for complex coding and agentic tasks', capabilities: { supportsTools: true, - supportsReasoning: false, - maxContextLength: 1047576 + supportsReasoning: true, + maxContextLength: 1050000 + }, + defaultParameterValues: { + reasoning_effort: 'none' + } + }, + { + id: 'gpt-5.6-luna', + name: 'GPT-5.6 Luna', + description: 'Fast GPT-5.6 model for cost-sensitive, high-throughput workloads', + capabilities: { + supportsTools: true, + supportsReasoning: true, + maxContextLength: 1050000 + }, + defaultParameterValues: { + reasoning_effort: 'none' } } ] @@ -177,6 +196,16 @@ export class OpenAIAdapter extends AbstractTextProviderAdapter { */ protected getParameterDefinitions(_modelId: string): readonly ParameterDefinition[] { return [ + { + name: 'reasoning_effort', + labelKey: 'params.reasoning_effort.label', + descriptionKey: 'params.reasoning_effort.description', + description: 'Reasoning effort for GPT-5.6 models.', + type: 'string', + defaultValue: 'none', + default: 'none', + allowedValues: ['none', 'low', 'medium', 'high', 'xhigh', 'max'] + }, { name: 'temperature', labelKey: 'params.temperature.label', @@ -411,6 +440,7 @@ export class OpenAIAdapter extends AbstractTextProviderAdapter { n: _n, seed: _seed, logprobs, + reasoning_effort, responseMimeType: _responseMimeType, ...restParams } = (paramOverrides || {}) as Record @@ -427,6 +457,14 @@ export class OpenAIAdapter extends AbstractTextProviderAdapter { normalizedParams.include = ['message.output_text.logprobs'] } + if (reasoning_effort !== undefined) { + const existingReasoning = normalizedParams.reasoning + normalizedParams.reasoning = { + ...(existingReasoning && typeof existingReasoning === 'object' ? existingReasoning : {}), + effort: reasoning_effort + } + } + return normalizedParams } diff --git a/packages/core/src/services/llm/adapters/zhipu-adapter.ts b/packages/core/src/services/llm/adapters/zhipu-adapter.ts index 5e88014f..4847c0e3 100644 --- a/packages/core/src/services/llm/adapters/zhipu-adapter.ts +++ b/packages/core/src/services/llm/adapters/zhipu-adapter.ts @@ -10,6 +10,16 @@ interface ModelOverride { } const ZHIPU_STATIC_MODELS: ModelOverride[] = [ + { + id: 'glm-5.2', + name: 'GLM-5.2', + description: 'Latest GLM flagship model for reasoning, coding, and agentic tasks', + capabilities: { + supportsTools: true, + supportsReasoning: true, + maxContextLength: 1000000 + } + }, { id: 'glm-4.7', name: 'GLM-4.7', diff --git a/packages/core/src/services/model/manager.ts b/packages/core/src/services/model/manager.ts index 9139718c..8847c495 100644 --- a/packages/core/src/services/model/manager.ts +++ b/packages/core/src/services/model/manager.ts @@ -115,9 +115,20 @@ export class ModelManager implements IModelManager { if (isTextModelConfig(existingModel)) { // 已经是新格式,保留用户配置,仅在缺失关键字段时补齐默认值 - let updatedModel = { ...existingModel } as TextModelConfig; + const copiedExistingModel = { ...existingModel } as TextModelConfig; + let updatedModel = this.patchBuiltinModelUpgrade( + key, + copiedExistingModel, + defaultConfig + ); let patched = false; + if (updatedModel !== copiedExistingModel) { + updatedModels[key] = updatedModel; + hasUpdates = true; + console.log(`[ModelManager] Migrated legacy builtin model: ${key}`); + } + if (!updatedModel.providerMeta && defaultConfig.providerMeta) { updatedModel.providerMeta = defaultConfig.providerMeta; patched = true; @@ -240,6 +251,53 @@ export class ModelManager implements IModelManager { } } + private patchBuiltinModelUpgrade( + key: string, + config: TextModelConfig, + defaultConfig: TextModelConfig + ): TextModelConfig { + const legacyDefaultIds: Record = { + openai: ['gpt-5-mini'], + gemini: ['gemini-2.5-flash'], + anthropic: ['claude-opus-4-20250514', 'claude-sonnet-4-20250514'], + zhipu: ['glm-4.7'], + dashscope: ['qwen3.5-27b'], + grok: ['grok-4.3'] + }; + const currentModelId = config.modelId || config.modelMeta?.id; + if (!currentModelId || !legacyDefaultIds[key]?.includes(currentModelId)) { + return config; + } + + const paramOverrides = { + ...(defaultConfig.paramOverrides || {}), + ...(config.paramOverrides || {}) + } as Record; + + if (key === 'anthropic') { + delete paramOverrides.thinking_budget_tokens; + delete paramOverrides.temperature; + delete paramOverrides.top_p; + delete paramOverrides.top_k; + paramOverrides.effort = paramOverrides.effort || 'high'; + } else if (key === 'gemini') { + delete paramOverrides.temperature; + delete paramOverrides.topP; + delete paramOverrides.topK; + delete paramOverrides.candidateCount; + delete paramOverrides.thinkingBudget; + } else if (key === 'grok' && paramOverrides.reasoning_effort === 'none') { + paramOverrides.reasoning_effort = 'high'; + } + + return { + ...config, + modelId: defaultConfig.modelId, + modelMeta: defaultConfig.modelMeta, + paramOverrides + }; + } + /** * 获取默认模型配置(返回TextModelConfig格式) * 注意:每次调用都会重新计算,确保环境变量变化能被感知 diff --git a/packages/core/tests/integration/model/migration.integration.test.ts b/packages/core/tests/integration/model/migration.integration.test.ts index ce2636d7..3673f3ce 100644 --- a/packages/core/tests/integration/model/migration.integration.test.ts +++ b/packages/core/tests/integration/model/migration.integration.test.ts @@ -292,7 +292,7 @@ describe('配置迁移集成测试', () => { describe('新格式配置处理', () => { it('应该直接识别并保留新格式配置', async () => { const adapter = registry.getAdapter('openai'); - const model = adapter.getModels().find(m => m.id === 'gpt-5-mini')!; + const model = adapter.getModels().find(m => m.id === 'gpt-5.6-sol')!; const newConfig: TextModelConfig = { id: 'openai', diff --git a/packages/core/tests/unit/image/dashscope-adapter.test.ts b/packages/core/tests/unit/image/dashscope-adapter.test.ts index 54026861..b90a0727 100644 --- a/packages/core/tests/unit/image/dashscope-adapter.test.ts +++ b/packages/core/tests/unit/image/dashscope-adapter.test.ts @@ -68,4 +68,47 @@ describe('DashScopeImageAdapter', () => { }) ) }) + + test('should route Qwen Image 2.0 with multiple inputs through the unified edit payload', async () => { + const model = adapter.getModels()[0] + const config: ImageModelConfig = { + id: 'test-qwen-image-2', + name: 'Qwen Image 2.0', + providerId: 'dashscope', + modelId: model.id, + enabled: true, + connectionConfig: { + apiKey: 'test-api-key', + baseURL: 'https://dashscope.aliyuncs.com' + }, + paramOverrides: {}, + provider: adapter.getProvider(), + model + } + global.fetch = vi.fn().mockResolvedValue({ + ok: true, + json: () => Promise.resolve({ + output: { choices: [{ message: { content: [{ image: 'https://example.com/image.png' }] } }] } + }) + }) + + await adapter.generate({ + prompt: 'combine these images', + configId: config.id, + count: 1, + inputImages: [ + { b64: 'aGVsbG8=', mimeType: 'image/png' }, + { b64: 'd29ybGQ=', mimeType: 'image/jpeg' } + ] + }, config) + + const [, init] = vi.mocked(fetch).mock.calls[0]! + const body = JSON.parse(String(init?.body)) + expect(body.model).toBe('qwen-image-2.0') + expect(body.input.messages[0].content).toEqual([ + { image: 'data:image/png;base64,aGVsbG8=' }, + { image: 'data:image/jpeg;base64,d29ybGQ=' }, + { text: 'combine these images' } + ]) + }) }) diff --git a/packages/core/tests/unit/image/gemini-adapter.test.ts b/packages/core/tests/unit/image/gemini-adapter.test.ts index 7c6d438a..bc48ef9a 100644 --- a/packages/core/tests/unit/image/gemini-adapter.test.ts +++ b/packages/core/tests/unit/image/gemini-adapter.test.ts @@ -47,8 +47,10 @@ describe('GeminiImageAdapter', () => { expect(Array.isArray(models)).toBe(true) expect(models.length).toBeGreaterThan(0) expect(models.map(model => model.id)).toEqual([ - 'gemini-2.5-flash-image', - 'gemini-3.1-flash-image-preview' + 'gemini-3.1-flash-image', + 'gemini-3.1-flash-lite-image', + 'gemini-3-pro-image', + 'gemini-2.5-flash-image' ]) const geminiModel = models.find(m => m.id.includes('gemini')) @@ -117,8 +119,10 @@ describe('GeminiImageAdapter', () => { const models = await adapter.getModelsAsync({ apiKey: 'test-api-key' }) expect(models.map(model => model.id)).toEqual([ - 'gemini-2.5-flash-image', - 'gemini-3.1-flash-image-preview' + 'gemini-3.1-flash-image', + 'gemini-3.1-flash-lite-image', + 'gemini-3-pro-image', + 'gemini-2.5-flash-image' ]) }) }) diff --git a/packages/core/tests/unit/image/image-default-models.test.ts b/packages/core/tests/unit/image/image-default-models.test.ts index 3b359e53..e94cf014 100644 --- a/packages/core/tests/unit/image/image-default-models.test.ts +++ b/packages/core/tests/unit/image/image-default-models.test.ts @@ -48,7 +48,7 @@ describe('default image models', () => { expect(models['image-seedream-50-lite']).toBeDefined() expect(models['image-seedream-50-lite'].providerId).toBe('seedream') - expect(models['image-seedream-50-lite'].modelId).toBe('doubao-seedream-5-0-260128') + expect(models['image-seedream-50-lite'].modelId).toBe('doubao-seedream-5-0-lite-260128') expect(models['image-seedream-50-lite'].enabled).toBe(true) }) diff --git a/packages/core/tests/unit/image/image-model-manager.test.ts b/packages/core/tests/unit/image/image-model-manager.test.ts index bb95ff5f..5eeae7b0 100644 --- a/packages/core/tests/unit/image/image-model-manager.test.ts +++ b/packages/core/tests/unit/image/image-model-manager.test.ts @@ -98,6 +98,41 @@ describe('ImageModelManager initialization behavior', () => { expect(reloaded?.model.capabilities.multiImage).toBe(true) }) + it('should migrate exact legacy builtin image model ids', async () => { + await modelManager.ensureInitialized() + const defaults = { + gemini: await modelManager.getConfig('image-gemini-nanobanana'), + dashscope: await modelManager.getConfig('image-dashscope'), + seedream: await modelManager.getConfig('image-seedream-50-lite') + } + expect(defaults.gemini && defaults.dashscope && defaults.seedream).toBeTruthy() + + await storageProvider.setItem(CORE_SERVICE_KEYS.IMAGE_MODELS, JSON.stringify({ + 'image-gemini-nanobanana': { + ...defaults.gemini, + modelId: 'gemini-2.5-flash-image', + model: { ...defaults.gemini!.model, id: 'gemini-2.5-flash-image' } + }, + 'image-dashscope': { + ...defaults.dashscope, + modelId: 'qwen-image', + model: { ...defaults.dashscope!.model, id: 'qwen-image' } + }, + 'image-seedream-50-lite': { + ...defaults.seedream, + modelId: 'doubao-seedream-5-0-260128', + model: { ...defaults.seedream!.model, id: 'doubao-seedream-5-0-260128' } + } + })) + + const reloadedManager = new ImageModelManager(storageProvider, new ImageAdapterRegistry()) + await reloadedManager.ensureInitialized() + + expect((await reloadedManager.getConfig('image-gemini-nanobanana'))?.modelId).toBe('gemini-3.1-flash-image') + expect((await reloadedManager.getConfig('image-dashscope'))?.modelId).toBe('qwen-image-2.0') + expect((await reloadedManager.getConfig('image-seedream-50-lite'))?.modelId).toBe('doubao-seedream-5-0-lite-260128') + }) + it('should refresh embedded provider and model metadata when providerId/modelId are updated directly', async () => { await modelManager.ensureInitialized() const existing = await modelManager.getConfig('image-seedream') diff --git a/packages/core/tests/unit/image/seedream-adapter.test.ts b/packages/core/tests/unit/image/seedream-adapter.test.ts index 2bb8e529..5f3af97d 100644 --- a/packages/core/tests/unit/image/seedream-adapter.test.ts +++ b/packages/core/tests/unit/image/seedream-adapter.test.ts @@ -62,7 +62,7 @@ describe('SeedreamImageAdapter', () => { expect(modelIds).toEqual(expect.arrayContaining([ 'doubao-seedream-4-0-250828', 'doubao-seedream-4-5-251128', - 'doubao-seedream-5-0-260128' + 'doubao-seedream-5-0-lite-260128' ])) }) @@ -343,7 +343,7 @@ describe('SeedreamImageAdapter', () => { id: 'seedream-50-config', name: 'Seedream 5.0 Config', providerId: 'seedream', - modelId: 'doubao-seedream-5-0-260128', + modelId: 'doubao-seedream-5-0-lite-260128', enabled: true, connectionConfig: { apiKey: 'test-api-key', @@ -421,7 +421,7 @@ describe('SeedreamImageAdapter', () => { }) test('should expose Seedream 5.0 lite specific parameters', () => { - const model = adapter.getModels().find(item => item.id === 'doubao-seedream-5-0-260128') + const model = adapter.getModels().find(item => item.id === 'doubao-seedream-5-0-lite-260128') const parameterNames = model?.parameterDefinitions.map(p => p.name) || [] expect(parameterNames).toEqual(expect.arrayContaining(['size', 'output_format', 'tools'])) diff --git a/packages/core/tests/unit/llm/anthropic-adapter.test.ts b/packages/core/tests/unit/llm/anthropic-adapter.test.ts index e1d4da65..51d86981 100644 --- a/packages/core/tests/unit/llm/anthropic-adapter.test.ts +++ b/packages/core/tests/unit/llm/anthropic-adapter.test.ts @@ -92,6 +92,11 @@ describe('AnthropicAdapter', () => { const claude4 = models.find(m => m.id.includes('claude-')); expect(claude4).toBeDefined(); expect(claude4?.providerId).toBe('anthropic'); + expect(models.map(model => model.id)).toEqual([ + 'claude-sonnet-5', + 'claude-opus-5', + 'claude-haiku-4-5-20251001' + ]); }); }); @@ -122,6 +127,33 @@ describe('AnthropicAdapter', () => { }); describe('sendMessage', () => { + it('should use adaptive thinking and effort for Claude 5', async () => { + const mockCreate = vi.fn().mockResolvedValue({ + content: [{ type: 'text', text: 'ok' }], + model: 'claude-sonnet-5', + stop_reason: 'end_turn', + usage: { input_tokens: 1, output_tokens: 1 } + }); + (Anthropic as any).prototype.messages = { create: mockCreate, stream: vi.fn() }; + + await adapter.sendMessage(mockMessages, { + ...mockConfig, + modelMeta: adapter.getModels()[0], + paramOverrides: { + max_tokens: 8192, + effort: 'medium', + temperature: 0.2 + } + }); + + expect(mockCreate).toHaveBeenCalledWith(expect.objectContaining({ + model: 'claude-sonnet-5', + thinking: { type: 'adaptive' }, + output_config: { effort: 'medium' } + })); + expect(mockCreate.mock.calls[0][0].temperature).toBeUndefined(); + }); + it('should return response content from Anthropic API', async () => { const anthropicResponse = { content: [ diff --git a/packages/core/tests/unit/llm/gemini-adapter.test.ts b/packages/core/tests/unit/llm/gemini-adapter.test.ts index 32e995f7..9aafc476 100644 --- a/packages/core/tests/unit/llm/gemini-adapter.test.ts +++ b/packages/core/tests/unit/llm/gemini-adapter.test.ts @@ -74,10 +74,12 @@ describe('GeminiAdapter', () => { expect(Array.isArray(models)).toBe(true); expect(models.length).toBeGreaterThan(0); - // 更新为新版本的模型 ID - const gemini25Flash = models.find(m => m.id === 'gemini-2.5-flash'); - expect(gemini25Flash).toBeDefined(); - expect(gemini25Flash?.providerId).toBe('gemini'); + expect(models.map(model => model.id)).toEqual([ + 'gemini-3.6-flash', + 'gemini-3.5-flash-lite', + 'gemini-3.1-pro-preview' + ]); + expect(models[0].providerId).toBe('gemini'); }); }); @@ -98,22 +100,19 @@ describe('GeminiAdapter', () => { const paramNames = model.parameterDefinitions.map(p => p.name); - // 验证基础参数存在 - expect(paramNames).toContain('temperature'); - expect(paramNames).toContain('topP'); + // Gemini 3.5/3.6 已弃用这些采样参数 + expect(paramNames).not.toContain('temperature'); + expect(paramNames).not.toContain('topP'); + expect(paramNames).not.toContain('topK'); expect(paramNames).toContain('maxOutputTokens'); - // 验证思考参数存在 - expect(paramNames).toContain('thinkingBudget'); + // Gemini 3 使用 thinkingLevel,不再使用 token budget + expect(paramNames).not.toContain('thinkingBudget'); + expect(paramNames).toContain('thinkingLevel'); expect(paramNames).toContain('includeThoughts'); - // 验证思考参数定义 - const thinkingBudget = model.parameterDefinitions.find(p => p.name === 'thinkingBudget'); - expect(thinkingBudget).toBeDefined(); - expect(thinkingBudget?.type).toBe('number'); - expect(thinkingBudget?.min).toBe(0); // 允许0来禁用思考功能 - expect(thinkingBudget?.max).toBe(8192); - expect(thinkingBudget?.description).toContain('Gemini 2.5+'); + const thinkingLevel = model.parameterDefinitions.find(p => p.name === 'thinkingLevel'); + expect(thinkingLevel?.allowedValues).toEqual(['minimal', 'low', 'medium', 'high']); const includeThoughts = model.parameterDefinitions.find(p => p.name === 'includeThoughts'); expect(includeThoughts).toBeDefined(); @@ -133,12 +132,42 @@ describe('GeminiAdapter', () => { // 验证参数定义中包含思考参数 const paramNames = model.parameterDefinitions.map(p => p.name); - expect(paramNames).toContain('thinkingBudget'); + expect(paramNames).toContain('thinkingLevel'); expect(paramNames).toContain('includeThoughts'); }); }); describe('error handling', () => { + it('filters deprecated sampling parameters for Gemini 3.6 requests', async () => { + const generateContent = vi.fn().mockResolvedValue({ + text: 'ok', + candidates: [{ content: { parts: [{ text: 'ok' }] } }] + }); + (adapter as any).createClient = () => ({ models: { generateContent } }); + + await adapter.sendMessage(mockMessages, { + ...mockConfig, + modelMeta: adapter.getModels()[0], + paramOverrides: { + temperature: 0.2, + topP: 0.8, + topK: 20, + candidateCount: 2, + thinkingBudget: 2000, + thinkingLevel: 'high', + maxOutputTokens: 2048 + } + }); + + const requestConfig = generateContent.mock.calls[0][0].config; + expect(requestConfig.temperature).toBeUndefined(); + expect(requestConfig.topP).toBeUndefined(); + expect(requestConfig.topK).toBeUndefined(); + expect(requestConfig.candidateCount).toBeUndefined(); + expect(requestConfig.maxOutputTokens).toBe(2048); + expect(requestConfig.thinkingConfig).toEqual({ thinkingLevel: 'high' }); + }); + it('should throw error when API key is missing', async () => { const configWithoutKey = { ...mockConfig, diff --git a/packages/core/tests/unit/llm/grok-adapter.test.ts b/packages/core/tests/unit/llm/grok-adapter.test.ts index 81f884b9..2413bf8a 100644 --- a/packages/core/tests/unit/llm/grok-adapter.test.ts +++ b/packages/core/tests/unit/llm/grok-adapter.test.ts @@ -67,17 +67,17 @@ describe('GrokAdapter', () => { expect(provider.connectionSchema?.required).toContain('apiKey') }) - it('should default Grok 4.3 with reasoning disabled', () => { + it('should default Grok 4.5 with high reasoning effort', () => { const models = adapter.getModels() - expect(models.map(model => model.id)).toEqual(['grok-4.3']) + expect(models.map(model => model.id)).toEqual(['grok-4.5']) expect(models[0].defaultParameterValues).toEqual({ - reasoning_effort: 'none' + reasoning_effort: 'high' }) expect(models[0].parameterDefinitions.map(definition => definition.name)).toContain('reasoning_effort') }) - it('should send reasoning_effort none through chat completions by default', async () => { + it('should send reasoning_effort high through chat completions by default', async () => { mockOpenAIInstance.chat.completions.create.mockResolvedValue({ choices: [ { @@ -93,9 +93,9 @@ describe('GrokAdapter', () => { expect(mockOpenAIInstance.chat.completions.create).toHaveBeenCalledWith( expect.objectContaining({ - model: 'grok-4.3', + model: 'grok-4.5', messages: [{ role: 'user', content: 'Hello' }], - reasoning_effort: 'none' + reasoning_effort: 'high' }) ) expect(mockOpenAIInstance.responses.create).not.toHaveBeenCalled() diff --git a/packages/core/tests/unit/llm/minimax-adapter.test.ts b/packages/core/tests/unit/llm/minimax-adapter.test.ts index 938ecef2..afe17b57 100644 --- a/packages/core/tests/unit/llm/minimax-adapter.test.ts +++ b/packages/core/tests/unit/llm/minimax-adapter.test.ts @@ -146,7 +146,8 @@ describe('MinimaxAdapter', () => { expect(m27?.capabilities.maxContextLength).toBe(204800); const m27Highspeed = models.find(m => m.id === 'MiniMax-M2.7-highspeed'); - expect(m27Highspeed?.capabilities.maxContextLength).toBe(1000000); + expect(m27Highspeed?.capabilities.maxContextLength).toBe(204800); + expect(m27Highspeed?.capabilities.supportsReasoning).toBe(true); }); it('should include all expected models', () => { diff --git a/packages/core/tests/unit/llm/openai-adapter.test.ts b/packages/core/tests/unit/llm/openai-adapter.test.ts index 0f95c336..bd079591 100644 --- a/packages/core/tests/unit/llm/openai-adapter.test.ts +++ b/packages/core/tests/unit/llm/openai-adapter.test.ts @@ -127,12 +127,17 @@ describe('OpenAIAdapter', () => { expect(Array.isArray(models)).toBe(true); expect(models.length).toBeGreaterThan(0); - // 验证至少包含 GPT-5 Mini - const gpt5Mini = models.find(m => m.id === 'gpt-5-mini'); - expect(gpt5Mini).toBeDefined(); - expect(gpt5Mini?.name).toBe('GPT-5 Mini'); - expect(gpt5Mini?.providerId).toBe('openai'); - expect(gpt5Mini?.capabilities.supportsTools).toBe(true); + expect(models.map(model => model.id)).toEqual([ + 'gpt-5.6-terra', + 'gpt-5.6-sol', + 'gpt-5.6-luna' + ]); + const terra = models[0]; + expect(terra.name).toBe('GPT-5.6 Terra'); + expect(terra.providerId).toBe('openai'); + expect(terra.capabilities.supportsTools).toBe(true); + expect(terra.capabilities.supportsReasoning).toBe(true); + expect(terra.capabilities.maxContextLength).toBe(1050000); }); it('should have capabilities for each model', () => { @@ -167,6 +172,8 @@ describe('OpenAIAdapter', () => { const tempParam = model.parameterDefinitions.find(p => p.name === 'temperature'); expect(tempParam).toBeDefined(); expect(tempParam?.type).toBe('number'); + expect(model.parameterDefinitions.find(p => p.name === 'reasoning_effort')?.allowedValues) + .toEqual(['none', 'low', 'medium', 'high', 'xhigh', 'max']); }); }); @@ -226,6 +233,9 @@ describe('OpenAIAdapter', () => { connectionConfig: { ...mockConfig.connectionConfig, requestStyle: 'responses' + }, + paramOverrides: { + reasoning_effort: 'high' } }; @@ -240,7 +250,8 @@ describe('OpenAIAdapter', () => { expect(mockOpenAIInstance.responses.create).toHaveBeenCalledWith( expect.objectContaining({ model: 'gpt-5-mini', - input: [{ role: 'user', content: 'Hello, world!' }] + input: [{ role: 'user', content: 'Hello, world!' }], + reasoning: { effort: 'high' } }) ); expect(mockOpenAIInstance.chat.completions.create).not.toHaveBeenCalled(); diff --git a/packages/core/tests/unit/model/defaults.test.ts b/packages/core/tests/unit/model/defaults.test.ts index 8ff072a7..a71a7f34 100644 --- a/packages/core/tests/unit/model/defaults.test.ts +++ b/packages/core/tests/unit/model/defaults.test.ts @@ -239,15 +239,15 @@ describe('model defaults provider env mapping', () => { }) }) - it('should include Grok with reasoning disabled by default', () => { + it('should include Grok 4.5 with high reasoning effort by default', () => { const models = getDefaultTextModels() expect(models.grok).toBeDefined() expect(models.grok.providerMeta.id).toBe('grok') - expect(models.grok.modelMeta.id).toBe('grok-4.3') + expect(models.grok.modelMeta.id).toBe('grok-4.5') expect(models.grok.enabled).toBe(false) expect(models.grok.paramOverrides).toEqual({ - reasoning_effort: 'none' + reasoning_effort: 'high' }) }) diff --git a/packages/core/tests/unit/model/manager.test.ts b/packages/core/tests/unit/model/manager.test.ts index 7520dc07..6bff4b3f 100644 --- a/packages/core/tests/unit/model/manager.test.ts +++ b/packages/core/tests/unit/model/manager.test.ts @@ -118,6 +118,57 @@ describe('ModelManager', () => { }); describe('initialization behavior', () => { + it('should migrate exact legacy builtin defaults while preserving connection settings', async () => { + const existing = await modelManager.getModel('openai') + expect(existing).toBeDefined() + + const legacyOpenAI: TextModelConfig = { + ...existing!, + modelId: 'gpt-5-mini', + modelMeta: { + ...existing!.modelMeta, + id: 'gpt-5-mini', + name: 'GPT-5 Mini' + }, + connectionConfig: { + ...existing!.connectionConfig, + baseURL: 'https://custom-openai.example/v1' + } + } + await storageProvider.setItem('models', JSON.stringify({ openai: legacyOpenAI })) + + const reloadedManager = new ModelManager(storageProvider, new TextAdapterRegistry()) + const reloaded = await reloadedManager.getModel('openai') + + expect(reloaded?.modelId).toBe('gpt-5.6-terra') + expect(reloaded?.modelMeta.id).toBe('gpt-5.6-terra') + expect(reloaded?.paramOverrides.reasoning_effort).toBe('none') + expect(reloaded?.connectionConfig.baseURL).toBe('https://custom-openai.example/v1') + }) + + it('should not migrate a legacy model id stored under a custom config key', async () => { + const existing = await modelManager.getModel('openai') + expect(existing).toBeDefined() + const custom = { + ...existing!, + id: 'custom-openai-legacy', + name: 'Custom Legacy OpenAI', + modelId: 'gpt-5-mini', + modelMeta: { + ...existing!.modelMeta, + id: 'gpt-5-mini', + name: 'GPT-5 Mini' + } + } + await modelManager.addModel(custom.id, custom) + + const reloadedManager = new ModelManager(storageProvider, new TextAdapterRegistry()) + const reloaded = await reloadedManager.getModel(custom.id) + + expect(reloaded?.modelId).toBe('gpt-5-mini') + expect(reloaded?.modelMeta.id).toBe('gpt-5-mini') + }) + it('should backfill missing builtin apiKey when env key becomes available for an enabled model', async () => { const originalGeminiKey = process.env.VITE_GEMINI_API_KEY process.env.VITE_GEMINI_API_KEY = 'env_gemini_key'