feat: refresh built-in provider models
This commit is contained in:
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',
|
||||
|
||||
@@ -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'
|
||||
|
||||
Reference in new issue
Block a user