feat: refresh built-in provider models

This commit is contained in:
linshen committed 2026-08-13 22:13:54 +08:00
1 parent 4c393babac
commit 69a636921e
26 files changed
+693 -182

No files matched your search

@@ -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' },
@@ -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<string, readonly string[]> = {
'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<void> {
@@ -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<ImageResult> {
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 } : {}),
@@ -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,
@@ -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
@@ -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<string, unknown> {
protected getDefaultParameterValues(modelId: string): Record<string, unknown> {
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<string, any>,
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)
@@ -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',
@@ -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<string, any> = {},
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<string, unknown>
const generationConfig = this.buildGenerationConfig(
config.modelMeta.id,
mergedParams,
request.systemPrompt
)
@@ -585,6 +629,7 @@ export class GeminiAdapter extends AbstractTextProviderAdapter {
} as Record<string, unknown>
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
)
@@ -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<string, unknown> {
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
}
@@ -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
}
}
]
@@ -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<string, unknown>
@@ -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
}
@@ -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',
+59 -1
View File
@@ -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<string, readonly string[]> = {
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<string, unknown>;
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格式)
* 注意:每次调用都会重新计算,确保环境变量变化能被感知
@@ -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',
@@ -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' }
])
})
})
@@ -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'
])
})
})
@@ -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)
})
@@ -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')
@@ -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']))
@@ -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: [
@@ -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,
@@ -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()
@@ -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', () => {
@@ -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();
@@ -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'
})
})
@@ -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'