feat: refresh built-in model catalog

This commit is contained in:
linshen committed 2026-09-10 16:42:28 +08:00
1 parent e4a281f42d
commit bfd1543110
23 files changed
+195 -117

No files matched your search

@@ -142,9 +142,12 @@ export class ImageModelManager implements IImageModelManager {
defaultConfig: ImageModelConfig
): ImageModelConfig {
const legacyDefaultIds: Record<string, readonly string[]> = {
'image-openrouter-nanobanana': ['google/gemini-2.5-flash-image'],
'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']
'image-openai-gpt': ['gpt-image-2'],
'image-dashscope': ['qwen-image', 'qwen-image-2.0'],
'image-seedream-50-lite': ['doubao-seedream-5-0-260128'],
'image-grok-imagine': ['grok-imagine-image-quality']
}
const currentModelId = config.modelId || config.model?.id
if (!currentModelId || !legacyDefaultIds[key]?.includes(currentModelId)) {
@@ -55,9 +55,9 @@ 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',
id: 'qwen-image-3.0-pro',
name: 'Qwen Image 3.0 Pro',
description: 'Flagship unified Qwen image model for text-to-image generation and image editing',
providerId: 'dashscope',
capabilities: {
text2image: true,
@@ -71,16 +71,15 @@ export class DashScopeImageAdapter extends AbstractImageProviderAdapter {
watermark: false
}
},
// Qwen-Image 文生图模型
{
id: 'qwen-image',
name: 'Qwen Image',
description: 'Qwen text-to-image model with strong text rendering, multi-line layout, and paragraph-level text generation support',
id: 'qwen-image-3.0',
name: 'Qwen Image 3.0',
description: 'Fast unified Qwen image model for text-to-image generation and image editing',
providerId: 'dashscope',
capabilities: {
text2image: true,
image2image: false,
multiImage: false
image2image: true,
multiImage: true
},
parameterDefinitions: this.getQwenImageParameterDefinitions(),
defaultParameterValues: {
@@ -238,7 +237,7 @@ export class DashScopeImageAdapter extends AbstractImageProviderAdapter {
}
private isQwenImageUnifiedModel(modelId: string): boolean {
return modelId === 'qwen-image-2.0'
return modelId === 'qwen-image-2.0' || modelId.startsWith('qwen-image-3.0')
}
protected async doGenerate(request: ImageRequest, config: ImageModelConfig): Promise<ImageResult> {
@@ -13,7 +13,7 @@ import { IMAGE_ERROR_CODES } from '../../../constants/error-codes'
export class GrokImageAdapter extends AbstractImageProviderAdapter {
private static readonly FORCED_SINGLE_OUTPUT_PARAM_KEYS = ['n', 'batch_size', 'count']
private static readonly MAX_INPUT_IMAGES = 3
private static readonly MAX_INPUT_IMAGES = 5
protected normalizeBaseUrl(base: string): string {
const trimmed = base.replace(/\/$/, '')
@@ -43,17 +43,17 @@ export class GrokImageAdapter extends AbstractImageProviderAdapter {
getModels(): ImageModel[] {
return [
{
id: 'grok-imagine-image-quality',
name: 'Grok Imagine Image Quality',
description: 'xAI Grok Imagine image generation and JSON-based image editing model',
id: 'grok-imagine-image-2.0',
name: 'Grok Imagine Image 2.0',
description: 'xAI Grok Imagine 2.0 image generation and JSON-based image editing model',
providerId: 'grok',
capabilities: {
text2image: true,
image2image: true,
multiImage: true
},
parameterDefinitions: this.getParameterDefinitions('grok-imagine-image-quality'),
defaultParameterValues: this.getDefaultParameterValues('grok-imagine-image-quality')
parameterDefinitions: this.getParameterDefinitions('grok-imagine-image-2.0'),
defaultParameterValues: this.getDefaultParameterValues('grok-imagine-image-2.0')
}
]
}
@@ -138,7 +138,7 @@ export class GrokImageAdapter extends AbstractImageProviderAdapter {
descriptionKey: 'image.params.aspect_ratio.description',
type: 'string',
defaultValue: 'auto',
allowedValues: ['auto', '1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3', '2:1', '1:2', '19.5:9', '9:19.5', '20:9', '9:20']
allowedValues: ['auto', '1:1', '16:9', '9:16', '4:3', '3:4', '3:2', '2:3', '2:1', '1:2', '19.5:9', '9:19.5', '20:9', '9:20', '21:9', '5:2']
},
{
name: 'resolution',
@@ -147,6 +147,14 @@ export class GrokImageAdapter extends AbstractImageProviderAdapter {
type: 'string',
defaultValue: '1k',
allowedValues: ['1k', '2k']
},
{
name: 'quality',
labelKey: 'image.params.quality.label',
descriptionKey: 'image.params.quality.description',
type: 'string',
defaultValue: 'auto',
allowedValues: ['auto', 'low', 'medium']
}
]
}
@@ -155,6 +163,7 @@ export class GrokImageAdapter extends AbstractImageProviderAdapter {
return {
aspect_ratio: 'auto',
resolution: '1k',
quality: 'auto',
response_format: 'b64_json'
}
}
@@ -10,7 +10,7 @@ import type {
import { ImageError } from '../errors'
import { IMAGE_ERROR_CODES } from '../../../constants/error-codes'
const GPT_IMAGE_2_SIZE_VALUES = [
const GPT_IMAGE_SIZE_VALUES = [
'1024x1024',
'1536x1024',
'1024x1536',
@@ -21,7 +21,7 @@ const GPT_IMAGE_2_SIZE_VALUES = [
'auto'
] as const
const GPT_IMAGE_2_BACKGROUND_VALUES = ['auto', 'opaque'] as const
const GPT_IMAGE_BACKGROUND_VALUES = ['auto', 'opaque'] as const
export class OpenAIImageAdapter extends AbstractImageProviderAdapter {
private static readonly FORCED_SINGLE_OUTPUT_PARAM_KEYS = ['n', 'batch_size', 'outputMimeType', 'response_format']
@@ -53,9 +53,9 @@ export class OpenAIImageAdapter extends AbstractImageProviderAdapter {
getModels(): ImageModel[] {
return [
{
id: 'gpt-image-2',
name: 'GPT Image 2',
description: 'OpenAI GPT Image 2 image generation model with text-to-image and image editing support',
id: 'gpt-image-2.5-flare',
name: 'GPT Image 2.5 Flare',
description: 'OpenAI fast, high-quality image generation and editing model for everyday use',
providerId: 'openai',
capabilities: {
text2image: true,
@@ -69,7 +69,7 @@ export class OpenAIImageAdapter extends AbstractImageProviderAdapter {
descriptionKey: 'image.params.size.description',
type: 'string',
defaultValue: '1024x1024',
allowedValues: [...GPT_IMAGE_2_SIZE_VALUES]
allowedValues: [...GPT_IMAGE_SIZE_VALUES]
},
{
name: 'quality',
@@ -77,7 +77,7 @@ export class OpenAIImageAdapter extends AbstractImageProviderAdapter {
descriptionKey: 'image.params.quality.description',
type: 'string',
defaultValue: 'auto',
allowedValues: ['auto', 'high', 'medium', 'low']
allowedValues: ['auto', 'max', 'xhigh', 'high', 'medium', 'low']
},
{
name: 'background',
@@ -85,7 +85,7 @@ export class OpenAIImageAdapter extends AbstractImageProviderAdapter {
descriptionKey: 'image.params.background.description',
type: 'string',
defaultValue: 'auto',
allowedValues: [...GPT_IMAGE_2_BACKGROUND_VALUES]
allowedValues: [...GPT_IMAGE_BACKGROUND_VALUES]
}
],
defaultParameterValues: {
@@ -158,7 +158,7 @@ export class OpenAIImageAdapter extends AbstractImageProviderAdapter {
}
protected getParameterDefinitions(_modelId: string): readonly ImageParameterDefinition[] {
// GPT Image 2 使用统一的参数定义,n参数固定为1不暴露给用户
// GPT Image 2.x 使用统一的参数定义,n参数固定为1不暴露给用户
return [
{
name: 'size',
@@ -166,7 +166,7 @@ export class OpenAIImageAdapter extends AbstractImageProviderAdapter {
descriptionKey: 'image.params.size.description',
type: 'string',
defaultValue: '1024x1024',
allowedValues: [...GPT_IMAGE_2_SIZE_VALUES]
allowedValues: [...GPT_IMAGE_SIZE_VALUES]
},
{
name: 'quality',
@@ -174,7 +174,7 @@ export class OpenAIImageAdapter extends AbstractImageProviderAdapter {
descriptionKey: 'image.params.quality.description',
type: 'string',
defaultValue: 'auto',
allowedValues: ['auto', 'high', 'medium', 'low']
allowedValues: ['auto', 'max', 'xhigh', 'high', 'medium', 'low']
},
{
name: 'background',
@@ -182,7 +182,7 @@ export class OpenAIImageAdapter extends AbstractImageProviderAdapter {
descriptionKey: 'image.params.background.description',
type: 'string',
defaultValue: 'auto',
allowedValues: [...GPT_IMAGE_2_BACKGROUND_VALUES]
allowedValues: [...GPT_IMAGE_BACKGROUND_VALUES]
}
]
}
@@ -41,9 +41,9 @@ export class OpenRouterImageAdapter extends AbstractImageProviderAdapter {
getModels(): ImageModel[] {
return [
{
id: 'google/gemini-2.5-flash-image',
name: 'Gemini 2.5 Flash Image (Nano Banana)',
description: 'Google Gemini 2.5 Flash image model via OpenRouter with text-to-image, image editing, and multi-turn editing support',
id: 'google/gemini-3.1-flash-image',
name: 'Gemini 3.1 Flash Image (Nano Banana 2)',
description: 'Google Gemini 3.1 Flash image model via OpenRouter with text-to-image, image editing, and multi-turn editing support',
providerId: 'openrouter',
capabilities: {
text2image: true,
@@ -24,8 +24,8 @@ const DASHSCOPE_STATIC_MODELS: ModelOverride[] = [
}
},
{
id: 'qwen3.6-flash',
name: 'Qwen3.6 Flash',
id: 'qwen3.8-flash',
name: 'Qwen3.8 Flash',
description: 'Fast Qwen model for low-latency general-purpose workloads',
capabilities: {
supportsTools: true,
@@ -34,13 +34,13 @@ const DASHSCOPE_STATIC_MODELS: ModelOverride[] = [
}
},
{
id: 'qwen3.5-27b',
name: 'Qwen3.5-27B',
description: 'Qwen3.5 27B model for DashScope text and vision understanding scenarios',
id: 'qwen3.8-max',
name: 'Qwen3.8 Max',
description: 'Latest Qwen flagship model for complex reasoning and agentic workloads',
capabilities: {
supportsTools: true,
supportsReasoning: true,
maxContextLength: 131072
maxContextLength: 1000000
}
},
{
@@ -33,8 +33,8 @@ interface ModelOverride {
*/
const GEMINI_STATIC_MODELS: ModelOverride[] = [
{
id: 'gemini-3.6-flash',
name: 'Gemini 3.6 Flash',
id: 'gemini-3.8-flash',
name: 'Gemini 3.8 Flash',
description: 'Latest Gemini Flash model for fast multimodal reasoning and agentic workloads',
capabilities: {
supportsTools: true,
@@ -292,7 +292,8 @@ export class GeminiAdapter extends AbstractTextProviderAdapter {
]
if (this.isGemini3Model(modelId)) {
const allowedValues = modelId.replace(/^models\//, '').startsWith('gemini-3.1-pro')
const normalizedModelId = modelId.replace(/^models\//, '')
const allowedValues = normalizedModelId.startsWith('gemini-3.1-pro') || normalizedModelId.startsWith('gemini-3.8-')
? ['low', 'medium', 'high']
: ['minimal', 'low', 'medium', 'high']
return [
@@ -434,7 +435,7 @@ export class GeminiAdapter extends AbstractTextProviderAdapter {
private usesDeprecatedSamplingParameters(modelId: string): boolean {
const normalizedModelId = modelId.replace(/^models\//, '')
return normalizedModelId.startsWith('gemini-3.5-') || normalizedModelId.startsWith('gemini-3.6-')
return normalizedModelId.startsWith('gemini-3.5-') || normalizedModelId.startsWith('gemini-3.6-') || normalizedModelId.startsWith('gemini-3.8-')
}
private isGemini3Model(modelId: string): boolean {
@@ -16,9 +16,9 @@ interface ModelOverride {
const GROK_STATIC_MODELS: ModelOverride[] = [
{
id: 'grok-4.5',
name: 'Grok 4.5',
description: 'xAI Grok 4.5 reasoning model via OpenAI-compatible Chat Completions API',
id: 'grok-4.6',
name: 'Grok 4.6',
description: 'xAI Grok 4.6 reasoning model via OpenAI-compatible Chat Completions API',
capabilities: {
supportsTools: true,
supportsReasoning: true,
@@ -30,7 +30,7 @@ const GROK_STATIC_MODELS: ModelOverride[] = [
}
]
type GrokReasoningEffort = 'low' | 'medium' | 'high'
type GrokReasoningEffort = 'low' | 'medium' | 'high' | 'xhigh'
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 4.5.',
description: 'Reasoning effort for Grok 4.6.',
type: 'string',
defaultValue: 'high',
default: 'high',
allowedValues: ['low', 'medium', 'high']
allowedValues: ['low', 'medium', 'high', 'xhigh']
},
{
name: 'temperature',
@@ -215,7 +215,7 @@ export class GrokAdapter extends OpenAIAdapter {
}
private normalizeReasoningEffort(value: unknown): GrokReasoningEffort | undefined {
return value === 'low' || value === 'medium' || value === 'high'
return value === 'low' || value === 'medium' || value === 'high' || value === 'xhigh'
? value
: undefined
}
@@ -40,16 +40,16 @@ const OPENAI_STATIC_MODELS: ModelOverride[] = [
}
},
{
id: 'gpt-5.6-sol',
name: 'GPT-5.6 Sol',
description: 'Most capable GPT-5.6 model for complex coding and agentic tasks',
id: 'gpt-6-astra',
name: 'GPT-6 Astra',
description: 'Most capable OpenAI model for the hardest end-to-end work',
capabilities: {
supportsTools: true,
supportsReasoning: true,
maxContextLength: 1050000
},
defaultParameterValues: {
reasoning_effort: 'none'
reasoning_effort: 'low'
}
},
{
@@ -194,17 +194,20 @@ export class OpenAIAdapter extends AbstractTextProviderAdapter {
* 获取参数定义
* 基于 OpenAI 官方文档: https://platform.openai.com/docs/api-reference/chat/create
*/
protected getParameterDefinitions(_modelId: string): readonly ParameterDefinition[] {
protected getParameterDefinitions(modelId: string): readonly ParameterDefinition[] {
const isAstra = modelId === 'gpt-6-astra'
return [
{
name: 'reasoning_effort',
labelKey: 'params.reasoning_effort.label',
descriptionKey: 'params.reasoning_effort.description',
description: 'Reasoning effort for GPT-5.6 models.',
description: 'Reasoning effort for current OpenAI models.',
type: 'string',
defaultValue: 'none',
default: 'none',
allowedValues: ['none', 'low', 'medium', 'high', 'xhigh', 'max']
defaultValue: isAstra ? 'low' : 'none',
default: isAstra ? 'low' : 'none',
allowedValues: isAstra
? ['low', 'medium', 'high', 'xhigh', 'max']
: ['none', 'low', 'medium', 'high', 'xhigh', 'max']
},
{
name: 'temperature',
@@ -11,13 +11,13 @@ interface ModelOverride {
const OPENROUTER_STATIC_MODELS: ModelOverride[] = [
{
id: 'google/gemma-3-27b-it:free',
name: 'Gemma 3 27B IT (Free)',
description: 'Free Google Gemma 3 27B model served through OpenRouter',
id: 'openrouter/free',
name: 'OpenRouter Free Models Router',
description: 'OpenRouter router that selects a compatible free model for each request',
capabilities: {
supportsTools: true,
supportsReasoning: false,
maxContextLength: 96000
maxContextLength: 200000
}
}
]
@@ -1,4 +1,4 @@
import type { TextModel, TextProvider } from '../types'
import type { ParameterDefinition, TextModel, TextProvider } from '../types'
import { OpenAIAdapter } from './openai-adapter'
interface ModelOverride {
@@ -11,9 +11,9 @@ 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',
id: 'glm-5.3-flash',
name: 'GLM-5.3-Flash',
description: 'Latest efficient multimodal GLM model for reasoning, coding, and agentic tasks',
capabilities: {
supportsTools: true,
supportsReasoning: true,
@@ -21,23 +21,23 @@ const ZHIPU_STATIC_MODELS: ModelOverride[] = [
}
},
{
id: 'glm-4.7',
name: 'GLM-4.7',
description: 'GLM-4.7 是最新的旗舰模型系列,专为智能体应用打造的基础模型',
id: 'glm-5.3',
name: 'GLM-5.3',
description: 'Latest GLM flagship model for complex reasoning, coding, and agentic tasks',
capabilities: {
supportsTools: true,
supportsReasoning: true,
maxContextLength: 128000
maxContextLength: 1000000
}
},
{
id: 'glm-4.6',
name: 'GLM-4.6',
description: 'GLM-4.6 是最新的旗舰模型系列,专为智能体应用打造的基础模型',
id: 'glm-5.2',
name: 'GLM-5.2',
description: 'Previous GLM flagship model retained for compatibility',
capabilities: {
supportsTools: true,
supportsReasoning: true,
maxContextLength: 128000
maxContextLength: 1000000
}
}
]
@@ -84,4 +84,21 @@ export class ZhipuAdapter extends OpenAIAdapter {
}
})
}
protected getParameterDefinitions(modelId: string): readonly ParameterDefinition[] {
return super.getParameterDefinitions(modelId).map((definition) => {
if (definition.name !== 'reasoning_effort' || !/^glm-5\.(?:2|3)/.test(modelId)) {
return definition
}
const allowedValues = modelId === 'glm-5.2' ? ['high', 'max'] : ['low', 'high', 'max']
return {
...definition,
description: 'Reasoning effort for current GLM models.',
defaultValue: 'max',
default: 'max',
allowedValues
}
})
}
}
+3 -3
View File
@@ -258,11 +258,11 @@ export class ModelManager implements IModelManager {
): TextModelConfig {
const legacyDefaultIds: Record<string, readonly string[]> = {
openai: ['gpt-5-mini'],
gemini: ['gemini-2.5-flash'],
gemini: ['gemini-2.5-flash', 'gemini-3.6-flash'],
anthropic: ['claude-opus-4-20250514', 'claude-sonnet-4-20250514'],
zhipu: ['glm-4.7'],
zhipu: ['glm-4.7', 'glm-5.2'],
dashscope: ['qwen3.5-27b'],
grok: ['grok-4.3']
grok: ['grok-4.3', 'grok-4.5']
};
const currentModelId = config.modelId || config.modelMeta?.id;
if (!currentModelId || !legacyDefaultIds[key]?.includes(currentModelId)) {
@@ -28,7 +28,7 @@ function pickRealVisionProvider(): RealVisionProvider | null {
if (dashscopeApiKey) {
const adapter = new DashScopeAdapter()
const modelId = process.env.REAL_QWEN_VISION_MODEL || 'qwen3.5-27b'
const modelId = process.env.REAL_QWEN_VISION_MODEL || 'qwen3.8-flash'
return {
label: `dashscope/${modelId}`,
@@ -104,7 +104,7 @@ describe('DashScopeImageAdapter', () => {
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.model).toBe('qwen-image-3.0-pro')
expect(body.input.messages[0].content).toEqual([
{ image: 'data:image/png;base64,aGVsbG8=' },
{ image: 'data:image/jpeg;base64,d29ybGQ=' },
@@ -38,7 +38,9 @@ describe('GrokImageAdapter', () => {
it('should expose current non-retired Grok Imagine model', () => {
const models = adapter.getModels()
expect(models.map(model => model.id)).toEqual(['grok-imagine-image-quality'])
expect(models.map(model => model.id)).toEqual(['grok-imagine-image-2.0'])
expect(models[0].parameterDefinitions.find(parameter => parameter.name === 'quality')?.allowedValues)
.toEqual(['auto', 'low', 'medium'])
expect(models[0].providerId).toBe('grok')
expect(models[0].capabilities).toEqual({
text2image: true,
@@ -83,7 +85,7 @@ describe('GrokImageAdapter', () => {
const payload = JSON.parse(options.body)
expect(payload).toEqual(
expect.objectContaining({
model: 'grok-imagine-image-quality',
model: 'grok-imagine-image-2.0',
prompt: 'a city at dawn',
response_format: 'b64_json',
n: 1
@@ -118,7 +120,7 @@ describe('GrokImageAdapter', () => {
const payload = JSON.parse(options.body)
expect(payload).toEqual(
expect.objectContaining({
model: 'grok-imagine-image-quality',
model: 'grok-imagine-image-2.0',
prompt: 'make it cinematic',
response_format: 'b64_json',
n: 1,
@@ -130,11 +132,11 @@ describe('GrokImageAdapter', () => {
)
})
it('should reject more than three input images before calling xAI', async () => {
it('should reject more than five input images before calling xAI', async () => {
const request: ImageRequest = {
prompt: 'combine references',
configId: config.id,
inputImages: Array.from({ length: 4 }, () => ({
inputImages: Array.from({ length: 6 }, () => ({
b64: 'cmVmZXJlbmNl',
mimeType: 'image/png'
})),
@@ -146,8 +148,8 @@ describe('GrokImageAdapter', () => {
await expect(adapter.generate(request, config)).rejects.toMatchObject({
code: IMAGE_ERROR_CODES.INPUT_IMAGE_TOO_MANY,
params: {
maxCount: 3,
actualCount: 4
maxCount: 5,
actualCount: 6
}
})
expect(global.fetch).not.toHaveBeenCalled()
@@ -71,8 +71,8 @@ describe('default image models', () => {
expect(openaiConfig).toBeDefined()
expect(openaiConfig.providerId).toBe('openai')
expect(openaiConfig.modelId).toBe('gpt-image-2')
expect(openaiConfig.model.id).toBe('gpt-image-2')
expect(openaiConfig.modelId).toBe('gpt-image-2.5-flare')
expect(openaiConfig.model.id).toBe('gpt-image-2.5-flare')
expect(openaiConfig.model.capabilities.multiImage).toBe(true)
expect(openaiConfig.connectionConfig?.apiKey).toBe('openai-key')
expect(openaiConfig.enabled).toBe(true)
@@ -158,7 +158,7 @@ describe('default image models', () => {
expect(grokConfig).toBeDefined()
expect(grokConfig.providerId).toBe('grok')
expect(grokConfig.modelId).toBe('grok-imagine-image-quality')
expect(grokConfig.modelId).toBe('grok-imagine-image-2.0')
expect(grokConfig.model.capabilities.text2image).toBe(true)
expect(grokConfig.model.capabilities.image2image).toBe(true)
expect(grokConfig.model.capabilities.multiImage).toBe(true)
@@ -101,36 +101,64 @@ describe('ImageModelManager initialization behavior', () => {
it('should migrate exact legacy builtin image model ids', async () => {
await modelManager.ensureInitialized()
const defaults = {
openrouter: await modelManager.getConfig('image-openrouter-nanobanana'),
gemini: await modelManager.getConfig('image-gemini-nanobanana'),
openai: await modelManager.getConfig('image-openai-gpt'),
dashscope: await modelManager.getConfig('image-dashscope'),
seedream: await modelManager.getConfig('image-seedream-50-lite')
seedream: await modelManager.getConfig('image-seedream-50-lite'),
grok: await modelManager.getConfig('image-grok-imagine')
}
expect(defaults.gemini && defaults.dashscope && defaults.seedream).toBeTruthy()
expect(
defaults.openrouter
&& defaults.gemini
&& defaults.openai
&& defaults.dashscope
&& defaults.seedream
&& defaults.grok
).toBeTruthy()
await storageProvider.setItem(CORE_SERVICE_KEYS.IMAGE_MODELS, JSON.stringify({
'image-openrouter-nanobanana': {
...defaults.openrouter,
modelId: 'google/gemini-2.5-flash-image',
model: { ...defaults.openrouter!.model, id: 'google/gemini-2.5-flash-image' }
},
'image-gemini-nanobanana': {
...defaults.gemini,
modelId: 'gemini-2.5-flash-image',
model: { ...defaults.gemini!.model, id: 'gemini-2.5-flash-image' }
},
'image-openai-gpt': {
...defaults.openai,
modelId: 'gpt-image-2',
model: { ...defaults.openai!.model, id: 'gpt-image-2' }
},
'image-dashscope': {
...defaults.dashscope,
modelId: 'qwen-image',
model: { ...defaults.dashscope!.model, id: 'qwen-image' }
modelId: 'qwen-image-2.0',
model: { ...defaults.dashscope!.model, id: 'qwen-image-2.0' }
},
'image-seedream-50-lite': {
...defaults.seedream,
modelId: 'doubao-seedream-5-0-260128',
model: { ...defaults.seedream!.model, id: 'doubao-seedream-5-0-260128' }
},
'image-grok-imagine': {
...defaults.grok,
modelId: 'grok-imagine-image-quality',
model: { ...defaults.grok!.model, id: 'grok-imagine-image-quality' }
}
}))
const reloadedManager = new ImageModelManager(storageProvider, new ImageAdapterRegistry())
await reloadedManager.ensureInitialized()
expect((await reloadedManager.getConfig('image-openrouter-nanobanana'))?.modelId).toBe('google/gemini-3.1-flash-image')
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-openai-gpt'))?.modelId).toBe('gpt-image-2.5-flare')
expect((await reloadedManager.getConfig('image-dashscope'))?.modelId).toBe('qwen-image-3.0-pro')
expect((await reloadedManager.getConfig('image-seedream-50-lite'))?.modelId).toBe('doubao-seedream-5-0-lite-260128')
expect((await reloadedManager.getConfig('image-grok-imagine'))?.modelId).toBe('grok-imagine-image-2.0')
})
it('should refresh embedded provider and model metadata when providerId/modelId are updated directly', async () => {
@@ -34,15 +34,15 @@ describe('OpenAIImageAdapter', () => {
})
describe('Static Models', () => {
test('should return GPT Image 2 as the static default model', () => {
test('should return GPT Image 2.5 Flare as the static default model', () => {
const models = adapter.getModels()
expect(Array.isArray(models)).toBe(true)
expect(models.map(model => model.id)).toEqual(['gpt-image-2'])
expect(models.map(model => model.id)).toEqual(['gpt-image-2.5-flare'])
const imageModel = models[0]
expect(imageModel).toMatchObject({
id: 'gpt-image-2',
id: 'gpt-image-2.5-flare',
name: expect.any(String),
providerId: 'openai',
capabilities: {
@@ -54,9 +54,9 @@ describe('OpenAIImageAdapter', () => {
})
})
test('should expose supported GPT Image 2 parameters', () => {
test('should expose supported GPT Image 2.5 parameters', () => {
const models = adapter.getModels()
const model = models.find(m => m.id === 'gpt-image-2')
const model = models.find(m => m.id === 'gpt-image-2.5-flare')
expect(model?.parameterDefinitions).toBeDefined()
const qualityParam = model?.parameterDefinitions?.find(p => p.name === 'quality')
@@ -65,7 +65,7 @@ describe('OpenAIImageAdapter', () => {
expect(qualityParam).toBeDefined()
expect(qualityParam?.type).toBe('string')
expect(qualityParam?.allowedValues).toEqual(expect.arrayContaining(['auto', 'high', 'medium', 'low']))
expect(qualityParam?.allowedValues).toEqual(['auto', 'max', 'xhigh', 'high', 'medium', 'low'])
expect(sizeParam).toBeDefined()
expect(sizeParam?.allowedValues).toEqual([
@@ -91,7 +91,7 @@ describe('OpenAIImageAdapter', () => {
data: [
{ id: 'gpt-5.1' },
{ id: 'third-party-text-model', name: 'Text Model' },
{ id: 'gpt-image-2', name: 'GPT Image 2' },
{ id: 'gpt-image-2.5-flare', name: 'GPT Image 2.5 Flare' },
{ id: 'vendor/custom-image-fast', name: 'Custom Image Fast' }
]
})
@@ -103,7 +103,7 @@ describe('OpenAIImageAdapter', () => {
})
expect(models.map(model => model.id)).toEqual([
'gpt-image-2',
'gpt-image-2.5-flare',
'vendor/custom-image-fast',
'gpt-5.1',
'third-party-text-model'
@@ -120,7 +120,7 @@ describe('OpenAIImageAdapter', () => {
)
})
test('should fall back to static GPT Image 2 model when model fetch fails', async () => {
test('should fall back to static GPT Image 2.5 Flare model when model fetch fails', async () => {
global.fetch = vi.fn().mockResolvedValue({
ok: false,
status: 503,
@@ -129,7 +129,7 @@ describe('OpenAIImageAdapter', () => {
const models = await adapter.getModelsAsync({ apiKey: 'test-api-key' })
expect(models.map(model => model.id)).toEqual(['gpt-image-2'])
expect(models.map(model => model.id)).toEqual(['gpt-image-2.5-flare'])
})
})
@@ -41,8 +41,8 @@ describe('OpenRouterImageAdapter', () => {
const models = adapter.getModels()
expect(models.length).toBeGreaterThan(0)
expect(models[0].id).toBe('google/gemini-2.5-flash-image')
expect(models[0].name).toBe('Gemini 2.5 Flash Image (Nano Banana)')
expect(models[0].id).toBe('google/gemini-3.1-flash-image')
expect(models[0].name).toBe('Gemini 3.1 Flash Image (Nano Banana 2)')
expect(models[0].providerId).toBe('openrouter')
})
@@ -75,7 +75,7 @@ describe('GeminiAdapter', () => {
expect(models.length).toBeGreaterThan(0);
expect(models.map(model => model.id)).toEqual([
'gemini-3.6-flash',
'gemini-3.8-flash',
'gemini-3.5-flash-lite',
'gemini-3.1-pro-preview'
]);
@@ -100,7 +100,7 @@ describe('GeminiAdapter', () => {
const paramNames = model.parameterDefinitions.map(p => p.name);
// Gemini 3.5/3.6 已弃用这些采样参数
// Gemini 3.5/3.8 已弃用这些采样参数
expect(paramNames).not.toContain('temperature');
expect(paramNames).not.toContain('topP');
expect(paramNames).not.toContain('topK');
@@ -112,7 +112,7 @@ describe('GeminiAdapter', () => {
expect(paramNames).toContain('includeThoughts');
const thinkingLevel = model.parameterDefinitions.find(p => p.name === 'thinkingLevel');
expect(thinkingLevel?.allowedValues).toEqual(['minimal', 'low', 'medium', 'high']);
expect(thinkingLevel?.allowedValues).toEqual(['low', 'medium', 'high']);
const includeThoughts = model.parameterDefinitions.find(p => p.name === 'includeThoughts');
expect(includeThoughts).toBeDefined();
@@ -67,14 +67,16 @@ describe('GrokAdapter', () => {
expect(provider.connectionSchema?.required).toContain('apiKey')
})
it('should default Grok 4.5 with high reasoning effort', () => {
it('should default Grok 4.6 with high reasoning effort', () => {
const models = adapter.getModels()
expect(models.map(model => model.id)).toEqual(['grok-4.5'])
expect(models.map(model => model.id)).toEqual(['grok-4.6'])
expect(models[0].defaultParameterValues).toEqual({
reasoning_effort: 'high'
})
expect(models[0].parameterDefinitions.map(definition => definition.name)).toContain('reasoning_effort')
expect(models[0].parameterDefinitions.find(definition => definition.name === 'reasoning_effort')?.allowedValues)
.toEqual(['low', 'medium', 'high', 'xhigh'])
})
it('should send reasoning_effort high through chat completions by default', async () => {
@@ -93,7 +95,7 @@ describe('GrokAdapter', () => {
expect(mockOpenAIInstance.chat.completions.create).toHaveBeenCalledWith(
expect.objectContaining({
model: 'grok-4.5',
model: 'grok-4.6',
messages: [{ role: 'user', content: 'Hello' }],
reasoning_effort: 'high'
})
@@ -129,7 +129,7 @@ describe('OpenAIAdapter', () => {
expect(models.map(model => model.id)).toEqual([
'gpt-5.6-terra',
'gpt-5.6-sol',
'gpt-6-astra',
'gpt-5.6-luna'
]);
const terra = models[0];
@@ -138,6 +138,12 @@ describe('OpenAIAdapter', () => {
expect(terra.capabilities.supportsTools).toBe(true);
expect(terra.capabilities.supportsReasoning).toBe(true);
expect(terra.capabilities.maxContextLength).toBe(1050000);
const astra = models[1];
expect(astra.name).toBe('GPT-6 Astra');
expect(astra.defaultParameterValues).toMatchObject({ reasoning_effort: 'low' });
expect(astra.parameterDefinitions.find(p => p.name === 'reasoning_effort')?.allowedValues)
.toEqual(['low', 'medium', 'high', 'xhigh', 'max']);
});
it('should have capabilities for each model', () => {
@@ -239,18 +239,26 @@ describe('model defaults provider env mapping', () => {
})
})
it('should include Grok 4.5 with high reasoning effort by default', () => {
it('should include Grok 4.6 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.5')
expect(models.grok.modelMeta.id).toBe('grok-4.6')
expect(models.grok.enabled).toBe(false)
expect(models.grok.paramOverrides).toEqual({
reasoning_effort: 'high'
})
})
it('should use GLM-5.3-Flash as the Zhipu default', () => {
const models = getDefaultTextModels()
expect(models.zhipu.modelMeta.id).toBe('glm-5.3-flash')
expect(models.zhipu.modelMeta.parameterDefinitions.find((definition) => definition.name === 'reasoning_effort')?.allowedValues)
.toEqual(['low', 'high', 'max'])
})
it('should enable Grok when VITE_XAI_API_KEY is provided', () => {
process.env.VITE_XAI_API_KEY = 'test-xai-key'