From d751d9c12b4c06b6b6992e3191e7d20746c92e54 Mon Sep 17 00:00:00 2001 From: sor85 Date: Wed, 10 Jun 2026 00:36:03 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat(adapter-gemini):=20=E6=94=AF=E6=8C=81?= =?UTF-8?q?=E6=89=8B=E5=8A=A8=E9=85=8D=E7=BD=AE=E6=A8=A1=E5=9E=8B=E5=88=97?= =?UTF-8?q?=E8=A1=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- packages/adapter-gemini/src/client.ts | 60 ++++++++++++++----- packages/adapter-gemini/src/index.ts | 37 ++++++++++++ .../src/locales/en-US.schema.yml | 17 ++++++ .../src/locales/zh-CN.schema.yml | 17 ++++++ packages/adapter-gemini/src/utils.ts | 25 ++++++++ 5 files changed, 141 insertions(+), 15 deletions(-) diff --git a/packages/adapter-gemini/src/client.ts b/packages/adapter-gemini/src/client.ts index c75ec47a5..3d6039bed 100644 --- a/packages/adapter-gemini/src/client.ts +++ b/packages/adapter-gemini/src/client.ts @@ -25,6 +25,7 @@ import { GeminiModelInfo } from './types' import { createGeminiCapabilities, expandModelVariants, + isGeminiModelName, shouldFilterOutGeminiModel } from './utils' @@ -126,9 +127,11 @@ export class GeminiClient extends PlatformModelAndEmbeddingsClient let rawModels: GeminiModelInfo[] = [] try { - rawModels = await this._requester.getModels(config) + rawModels = this._config.pullModels + ? await this._requester.getModels(config) + : [] - if (rawModels.length === 0) { + if (this._config.pullModels && rawModels.length === 0) { throw new ChatLunaError( ChatLunaErrorCode.MODEL_INIT_ERROR, new Error('No model found') @@ -142,6 +145,44 @@ export class GeminiClient extends PlatformModelAndEmbeddingsClient // --- 将原始模型转换并展开为变体列表 --- const models: ModelInfo[] = [] + // 展开变体后按名字去重加入;同名模型不覆盖,使 additionalModels 优先于远程模型 + const pushModel = (baseInfo: ModelInfo) => { + const expanded: ModelInfo[] = [] + if ( + !expandModelVariants( + expanded, + baseInfo, + this._config.imageModelSearch + ) + ) { + expanded.push(baseInfo) + } + + for (const item of expanded) { + if (models.findIndex((info) => info.name === item.name) < 0) { + models.push(item) + } + } + } + + for (const model of this._config.additionalModels) { + const isEmbedding = model.modelType === 'Embeddings 嵌入模型' + + pushModel({ + name: model.model, + maxTokens: model.contextSize, + type: isEmbedding ? ModelType.embeddings : ModelType.llm, + capabilities: isEmbedding + ? [] + : isGeminiModelName(model.model) + ? createGeminiCapabilities( + model.model.toLowerCase(), + false + ) + : model.modelCapabilities + }) + } + for (const model of rawModels) { const modelNameLower = model.name.toLowerCase() @@ -151,7 +192,7 @@ export class GeminiClient extends PlatformModelAndEmbeddingsClient const isEmbedding = modelNameLower.includes('embedding') - const baseInfo: ModelInfo = { + pushModel({ name: model.name, maxTokens: model.inputTokenLimit, type: isEmbedding ? ModelType.embeddings : ModelType.llm, @@ -159,18 +200,7 @@ export class GeminiClient extends PlatformModelAndEmbeddingsClient modelNameLower, isEmbedding ) - } - - // 尝试展开特殊变体;未命中则直接加入 - if ( - !expandModelVariants( - models, - baseInfo, - this._config.imageModelSearch - ) - ) { - models.push(baseInfo) - } + }) } return models diff --git a/packages/adapter-gemini/src/index.ts b/packages/adapter-gemini/src/index.ts index da2f7d205..add3427a1 100644 --- a/packages/adapter-gemini/src/index.ts +++ b/packages/adapter-gemini/src/index.ts @@ -2,6 +2,7 @@ import { ChatLunaPlugin } from 'koishi-plugin-chatluna/services/chat' import { Context, Logger, Schema } from 'koishi' import { GeminiClient } from './client' import { createLogger } from 'koishi-plugin-chatluna/utils/logger' +import { ModelCapabilities } from 'koishi-plugin-chatluna/llm-core/platform/types' export let logger: Logger export const reusable = true @@ -38,6 +39,13 @@ export function apply(ctx: Context, config: Config) { export interface Config extends ChatLunaPlugin.Config { apiKeys: [string, string, boolean][] + pullModels: boolean + additionalModels: { + model: string + modelType: string + modelCapabilities: ModelCapabilities[] + contextSize: number + }[] maxContextRatio: number platform: string temperature: number @@ -58,6 +66,35 @@ export const Config: Schema = Schema.intersect([ ChatLunaPlugin.Config, Schema.object({ platform: Schema.string().default('gemini'), + pullModels: Schema.boolean().default(true), + additionalModels: Schema.array( + Schema.object({ + model: Schema.string(), + modelType: Schema.union([ + 'LLM 大语言模型', + 'Embeddings 嵌入模型' + ]).default('LLM 大语言模型'), + modelCapabilities: Schema.array( + Schema.union([ + ModelCapabilities.TextInput, + ModelCapabilities.ToolCall, + ModelCapabilities.ImageInput, + ModelCapabilities.ImageGeneration, + ModelCapabilities.AudioInput, + ModelCapabilities.VideoInput, + ModelCapabilities.FileInput + ]) + ) + .default([ + ModelCapabilities.TextInput, + ModelCapabilities.ToolCall + ]) + .role('checkbox'), + contextSize: Schema.number().default(128000) + }) + ) + .default([]) + .role('table'), apiKeys: Schema.array( Schema.tuple([ Schema.string().role('secret').default(''), diff --git a/packages/adapter-gemini/src/locales/en-US.schema.yml b/packages/adapter-gemini/src/locales/en-US.schema.yml index 1aff08c74..33ec53f29 100644 --- a/packages/adapter-gemini/src/locales/en-US.schema.yml +++ b/packages/adapter-gemini/src/locales/en-US.schema.yml @@ -2,6 +2,23 @@ $inner: - {} - $desc: 'API Configuration' platform: 'Adapter platform name. (Do not modify if you do not understand)' + pullModels: 'Auto-fetch model list. If disabled, use only specified models.' + additionalModels: + $desc: 'Additional model list.' + $inner: + model: 'Model name.' + modelType: 'Model type.' + contextSize: 'Context size.' + modelCapabilities: + $desc: 'Model capabilities.' + $inner: + - 'Text input' + - 'Tool calling' + - 'Visual image input' + - 'Image generation' + - 'Audio input' + - 'Video input' + - 'File input' apiKeys: $inner: - 'Gemini API Key' diff --git a/packages/adapter-gemini/src/locales/zh-CN.schema.yml b/packages/adapter-gemini/src/locales/zh-CN.schema.yml index 4175289b8..684b147aa 100644 --- a/packages/adapter-gemini/src/locales/zh-CN.schema.yml +++ b/packages/adapter-gemini/src/locales/zh-CN.schema.yml @@ -2,6 +2,23 @@ $inner: - {} - $desc: '请求选项' platform: '适配器的平台名。(不懂请不要修改)' + pullModels: '是否自动拉取模型列表。关闭后将仅使用下方指定的模型。' + additionalModels: + $desc: '额外模型列表。' + $inner: + model: '模型名称。' + modelType: '模型类型。' + contextSize: '模型上下文大小。' + modelCapabilities: + $desc: '模型支持的能力。' + $inner: + - '文本输入' + - '工具调用' + - '图片视觉输入' + - '图像生成' + - '音频输入' + - '视频输入' + - '文件输入' apiKeys: $inner: - 'Gemini API Key' diff --git a/packages/adapter-gemini/src/utils.ts b/packages/adapter-gemini/src/utils.ts index 3f8ce0e1d..1822640ef 100644 --- a/packages/adapter-gemini/src/utils.ts +++ b/packages/adapter-gemini/src/utils.ts @@ -547,6 +547,22 @@ export function prepareModelConfig( let imageSize: string | undefined let forceGoogleSearch = false + const extra = pluginConfig.additionalModels.find( + (item) => item.model === model && !isGeminiModelName(item.model) + ) + + if (extra) { + return { + model, + enabledThinking, + thinkingBudget: pluginConfig.thinkingBudget ?? -1, + imageGeneration: pluginConfig.imageGeneration ?? false, + thinkingLevel: undefined, + imageSize, + forceGoogleSearch + } + } + if (model.toLowerCase().endsWith('-search')) { forceGoogleSearch = true model = model.slice(0, -'-search'.length) @@ -746,6 +762,15 @@ export function isChatResponse(response: any): response is ChatResponse { // #region refreshModels helpers +/** + * 判断模型名是否为标准 Gemini 请求名。 + * 用于决定是否套用 Gemini 的后缀展开 / 能力推断逻辑: + * 命中则按 Gemini 处理,否则保留用户填写的原始模型名与能力。 + */ +export function isGeminiModelName(model: string): boolean { + return model.toLowerCase().includes('gemini') +} + export function createGeminiCapabilities( modelNameLower: string, isEmbedding: boolean From 13dcd28b79a4299aefc56cc80214a8dfa9d3012f Mon Sep 17 00:00:00 2001 From: dingyi Date: Wed, 10 Jun 2026 23:05:25 +0800 Subject: [PATCH 2/3] fix(adapter-gemini): simplify model variant expansion --- packages/adapter-gemini/src/client.ts | 128 +++++++++++------- packages/adapter-gemini/src/utils.ts | 179 ++------------------------ 2 files changed, 87 insertions(+), 220 deletions(-) diff --git a/packages/adapter-gemini/src/client.ts b/packages/adapter-gemini/src/client.ts index 3d6039bed..e8dfc9edf 100644 --- a/packages/adapter-gemini/src/client.ts +++ b/packages/adapter-gemini/src/client.ts @@ -22,12 +22,7 @@ import { GeminiRequester } from './requester' import { ChatLunaPlugin } from 'koishi-plugin-chatluna/services/chat' import { RunnableConfig } from '@langchain/core/runnables' import { GeminiModelInfo } from './types' -import { - createGeminiCapabilities, - expandModelVariants, - isGeminiModelName, - shouldFilterOutGeminiModel -} from './utils' +import { createGeminiCapabilities, shouldFilterOutGeminiModel } from './utils' // #region GeminiClient @@ -142,67 +137,102 @@ export class GeminiClient extends PlatformModelAndEmbeddingsClient throw new ChatLunaError(ChatLunaErrorCode.MODEL_INIT_ERROR, e) } - // --- 将原始模型转换并展开为变体列表 --- - const models: ModelInfo[] = [] - - // 展开变体后按名字去重加入;同名模型不覆盖,使 additionalModels 优先于远程模型 - const pushModel = (baseInfo: ModelInfo) => { - const expanded: ModelInfo[] = [] - if ( - !expandModelVariants( - expanded, - baseInfo, - this._config.imageModelSearch - ) - ) { - expanded.push(baseInfo) - } - - for (const item of expanded) { - if (models.findIndex((info) => info.name === item.name) < 0) { - models.push(item) - } - } - } + const items: ModelInfo[] = [] for (const model of this._config.additionalModels) { - const isEmbedding = model.modelType === 'Embeddings 嵌入模型' + const name = model.model.toLowerCase() + const type = + model.modelType === 'Embeddings 嵌入模型' + ? ModelType.embeddings + : ModelType.llm - pushModel({ + items.push({ name: model.model, maxTokens: model.contextSize, - type: isEmbedding ? ModelType.embeddings : ModelType.llm, - capabilities: isEmbedding - ? [] - : isGeminiModelName(model.model) - ? createGeminiCapabilities( - model.model.toLowerCase(), - false - ) - : model.modelCapabilities + type, + capabilities: + type === ModelType.embeddings + ? [] + : name.includes('gemini') + ? createGeminiCapabilities(name, false) + : model.modelCapabilities }) } - for (const model of rawModels) { - const modelNameLower = model.name.toLowerCase() + const name = model.name.toLowerCase() - if (shouldFilterOutGeminiModel(modelNameLower)) { - continue - } + if (shouldFilterOutGeminiModel(name)) continue - const isEmbedding = modelNameLower.includes('embedding') + const type = name.includes('embedding') + ? ModelType.embeddings + : ModelType.llm - pushModel({ + items.push({ name: model.name, maxTokens: model.inputTokenLimit, - type: isEmbedding ? ModelType.embeddings : ModelType.llm, + type, capabilities: createGeminiCapabilities( - modelNameLower, - isEmbedding + name, + type === ModelType.embeddings ) }) } + const models: ModelInfo[] = [] + const names = new Set() + + for (const model of items) { + const name = model.name.toLowerCase() + const suffixes: string[] = [] + + if (name.includes('gemini-3-pro-image')) { + suffixes.push('-2k', '-4k') + if (this._config.imageModelSearch) { + suffixes.push('-search', '-2k-search', '-4k-search') + } + } else if (name.includes('gemini-3.1-flash-image')) { + suffixes.push('-0.5k', '-2k', '-4k') + if (this._config.imageModelSearch) { + suffixes.push( + '-search', + '-0.5k-search', + '-2k-search', + '-4k-search' + ) + } + } else if ( + name.includes('gemini-2.5') && + !name.includes('image') && + !name.includes('-thinking') + ) { + suffixes.push('-non-thinking', '-thinking') + } else if ( + (name.includes('gemini-3-pro') || + name.includes('gemini-3-flash') || + name.includes('gemini-3.1-pro')) && + !name.includes('image') + ) { + suffixes.push('-low-thinking', '-high-thinking') + suffixes.push('-minimal-thinking') + if (!/gemini-3(\.1)?-pro/.test(name)) { + suffixes.push('-medium-thinking') + } + } + + for (const suffix of suffixes) { + const full = model.name + suffix + if (!names.has(full)) { + names.add(full) + models.push({ ...model, name: full }) + } + } + + if (names.has(model.name)) continue + + names.add(model.name) + models.push(model) + } + return models } diff --git a/packages/adapter-gemini/src/utils.ts b/packages/adapter-gemini/src/utils.ts index 1822640ef..ec30c9ceb 100644 --- a/packages/adapter-gemini/src/utils.ts +++ b/packages/adapter-gemini/src/utils.ts @@ -34,10 +34,7 @@ import { isZodSchemaV3 } from '@langchain/core/utils/types' import { generateSchema } from '@anatine/zod-openapi' import { deepAssign } from 'koishi-plugin-chatluna/utils/object' import { ClientConfig } from 'koishi-plugin-chatluna/llm-core/platform/config' -import { - ModelCapabilities, - ModelInfo -} from 'koishi-plugin-chatluna/llm-core/platform/types' +import { ModelCapabilities } from 'koishi-plugin-chatluna/llm-core/platform/types' export async function langchainMessageToGeminiMessage( messages: BaseMessage[], @@ -547,11 +544,13 @@ export function prepareModelConfig( let imageSize: string | undefined let forceGoogleSearch = false - const extra = pluginConfig.additionalModels.find( - (item) => item.model === model && !isGeminiModelName(item.model) - ) - - if (extra) { + if ( + pluginConfig.additionalModels.some( + (item) => + item.model === model && + !item.model.toLowerCase().includes('gemini') + ) + ) { return { model, enabledThinking, @@ -762,15 +761,6 @@ export function isChatResponse(response: any): response is ChatResponse { // #region refreshModels helpers -/** - * 判断模型名是否为标准 Gemini 请求名。 - * 用于决定是否套用 Gemini 的后缀展开 / 能力推断逻辑: - * 命中则按 Gemini 处理,否则保留用户填写的原始模型名与能力。 - */ -export function isGeminiModelName(model: string): boolean { - return model.toLowerCase().includes('gemini') -} - export function createGeminiCapabilities( modelNameLower: string, isEmbedding: boolean @@ -835,164 +825,11 @@ export function shouldFilterOutGeminiModel(modelNameLower: string): boolean { ) } -/** 支持 thinking 开关(-thinking / -non-thinking)的模型前缀 */ -const THINKING_MODELS = ['gemini-2.5-pro', 'gemini-2.5-flash'] as const - -/** 支持 thinking 等级(-low/medium/high/minimal-thinking)的模型前缀 */ -const THINKING_LEVEL_MODELS = [ - 'gemini-3-pro', - 'gemini-3-flash', - 'gemini-3.1-pro' -] as const - -/** - * 带分辨率 / 搜索后缀变体的图片生成模型配置。 - * resolutions: 该模型支持的分辨率后缀列表 - * supportSearch: 是否同时生成 -search 变体 - */ -export const IMAGE_VARIANT_MODELS = [ - { - name: 'gemini-3-pro-image', - resolutions: ['2k', '4k'], - supportSearch: true - }, - { - name: 'gemini-3.1-flash-image', - resolutions: ['0.5k', '2k', '4k'], - supportSearch: true - } -] as const - -/** 判断 haystack 中是否包含 needles 里的任意一项 */ -export function includesAny( - needles: readonly string[], - haystack: string -): boolean { - return needles.some((name) => haystack.includes(name)) -} - -/** - * 将 base 模型连同所有 suffixes 变体一起压入 out 数组。 - * 变体先入,base 最后入,保持列表顺序直观。 - */ -export function pushExpanded( - out: ModelInfo[], - base: ModelInfo, - suffixes: readonly string[] -): void { - for (const suffix of suffixes) { - out.push({ ...base, name: base.name + suffix }) - } - out.push(base) -} - -/** - * 查找模型名是否命中 IMAGE_VARIANT_MODELS 中的某一项。 - * 命中则返回该配置,否则返回 undefined。 - */ -export function getImageVariantConfig(modelName: string) { - return IMAGE_VARIANT_MODELS.find((item) => modelName.includes(item.name)) -} - -/** - * 为图片生成模型生成所有分辨率变体并压入 out。 - * 仅当 imageModelSearch 为 true 且该模型配置 supportSearch 时, - * 才额外生成 -search / --search 后缀变体。 - */ -export function pushImageVariants( - out: ModelInfo[], - base: ModelInfo, - resolutions: readonly string[], - supportSearch: boolean, - imageModelSearch: boolean -): void { - const resolutionSuffixes = resolutions.map((r) => `-${r}`) - const searchSuffixes = - supportSearch && imageModelSearch - ? ['-search', ...resolutions.map((r) => `-${r}-search`)] - : [] - - pushExpanded(out, base, [...resolutionSuffixes, ...searchSuffixes]) -} - /** 判断是否属于 gemini-3-pro / gemini-3.1-pro 系列(影响 thinking 等级列表) */ export function isGemini3ProFamily(modelName: string): boolean { return /gemini-3(\.1)?-pro/.test(modelName) } -/** - * 判断模型是否支持 thinking 开关(gemini-2.5 系列,且不是图片生成模型)。 - */ -export function isThinkingModel(modelNameLower: string): boolean { - return ( - includesAny(THINKING_MODELS, modelNameLower) && - !modelNameLower.includes('image') - ) -} - -/** - * 判断模型是否支持 thinking 等级(gemini-3 系列,且不是图片生成模型)。 - */ -export function isThinkingLevelModel(modelNameLower: string): boolean { - return ( - includesAny(THINKING_LEVEL_MODELS, modelNameLower) && - !modelNameLower.includes('image') - ) -} - -/** - * 根据模型类型,将模型展开为所有变体后压入 models 数组。 - * imageModelSearch 控制图片模型是否生成 -search 后缀变体。 - * 返回 true 表示已处理(调用方应 continue),false 表示未命中任何特殊类型。 - */ -export function expandModelVariants( - models: ModelInfo[], - base: ModelInfo, - imageModelSearch: boolean -): boolean { - const nameLower = base.name.toLowerCase() - - // 图片生成模型:展开分辨率变体,按配置决定是否附加搜索变体 - const imageVariantConfig = getImageVariantConfig(nameLower) - if (imageVariantConfig) { - pushImageVariants( - models, - base, - imageVariantConfig.resolutions, - imageVariantConfig.supportSearch, - imageModelSearch - ) - return true - } - - // gemini-2.5 系列:展开 -thinking / -non-thinking 变体 - if (isThinkingModel(nameLower)) { - if (nameLower.includes('-thinking')) { - // 已经是 thinking 变体,直接加入 - models.push(base) - } else { - pushExpanded(models, base, ['-non-thinking', '-thinking']) - } - return true - } - - // gemini-3 系列:展开 thinking 等级变体 - if (isThinkingLevelModel(nameLower)) { - const suffixes = isGemini3ProFamily(nameLower) - ? ['-low-thinking', '-high-thinking', '-minimal-thinking'] - : [ - '-low-thinking', - '-high-thinking', - '-minimal-thinking', - '-medium-thinking' - ] - pushExpanded(models, base, suffixes) - return true - } - - return false -} - // #endregion export function getModalityTokens( From 3f84e7546a493914480158148415c4229bd0f445 Mon Sep 17 00:00:00 2001 From: dingyi Date: Thu, 11 Jun 2026 00:14:46 +0800 Subject: [PATCH 3/3] fix(adapter-gemini): tighten additional model variants --- packages/adapter-gemini/src/client.ts | 121 ++++++++++---------------- packages/adapter-gemini/src/utils.ts | 49 ++++++++++- 2 files changed, 90 insertions(+), 80 deletions(-) diff --git a/packages/adapter-gemini/src/client.ts b/packages/adapter-gemini/src/client.ts index e8dfc9edf..64e9ff80e 100644 --- a/packages/adapter-gemini/src/client.ts +++ b/packages/adapter-gemini/src/client.ts @@ -22,7 +22,12 @@ import { GeminiRequester } from './requester' import { ChatLunaPlugin } from 'koishi-plugin-chatluna/services/chat' import { RunnableConfig } from '@langchain/core/runnables' import { GeminiModelInfo } from './types' -import { createGeminiCapabilities, shouldFilterOutGeminiModel } from './utils' +import { + createGeminiCapabilities, + getModelVariantSuffixes, + isGeminiModelName, + shouldFilterOutGeminiModel +} from './utils' // #region GeminiClient @@ -111,14 +116,10 @@ export class GeminiClient extends PlatformModelAndEmbeddingsClient // #region refreshModels /** - * 从 API 获取模型列表,并将每个模型展开为对应的所有变体: - * - 图片生成模型:分辨率 + 搜索后缀变体 - * - gemini-2.5 系列:-thinking / -non-thinking 变体 - * - gemini-3 系列:thinking 等级变体(low / medium / high / minimal) - * - 其他模型:直接加入,不展开 + * 从配置与 API 获取模型列表,并将特定系列展开为变体 + * (图片分辨率 / 搜索后缀、thinking 开关与等级)。 */ async refreshModels(config?: RunnableConfig): Promise { - // --- 获取原始模型列表 --- let rawModels: GeminiModelInfo[] = [] try { @@ -137,100 +138,66 @@ export class GeminiClient extends PlatformModelAndEmbeddingsClient throw new ChatLunaError(ChatLunaErrorCode.MODEL_INIT_ERROR, e) } - const items: ModelInfo[] = [] - - for (const model of this._config.additionalModels) { - const name = model.model.toLowerCase() - const type = - model.modelType === 'Embeddings 嵌入模型' - ? ModelType.embeddings - : ModelType.llm + const items: ModelInfo[] = this._config.additionalModels.map( + (model) => { + const name = model.model.toLowerCase() + const isEmbedding = model.modelType === 'Embeddings 嵌入模型' - items.push({ - name: model.model, - maxTokens: model.contextSize, - type, - capabilities: - type === ModelType.embeddings + return { + name: model.model, + maxTokens: model.contextSize, + type: isEmbedding ? ModelType.embeddings : ModelType.llm, + capabilities: isEmbedding ? [] - : name.includes('gemini') + : isGeminiModelName(name) ? createGeminiCapabilities(name, false) : model.modelCapabilities - }) - } + } + } + ) + for (const model of rawModels) { const name = model.name.toLowerCase() if (shouldFilterOutGeminiModel(name)) continue - const type = name.includes('embedding') - ? ModelType.embeddings - : ModelType.llm + const isEmbedding = name.includes('embedding') items.push({ name: model.name, maxTokens: model.inputTokenLimit, - type, - capabilities: createGeminiCapabilities( - name, - type === ModelType.embeddings - ) + type: isEmbedding ? ModelType.embeddings : ModelType.llm, + capabilities: createGeminiCapabilities(name, isEmbedding) }) } const models: ModelInfo[] = [] const names = new Set() + const addModel = (model: ModelInfo) => { + const key = model.name.toLowerCase() + if (!names.has(key)) { + names.add(key) + models.push(model) + } + } for (const model of items) { - const name = model.name.toLowerCase() - const suffixes: string[] = [] - - if (name.includes('gemini-3-pro-image')) { - suffixes.push('-2k', '-4k') - if (this._config.imageModelSearch) { - suffixes.push('-search', '-2k-search', '-4k-search') - } - } else if (name.includes('gemini-3.1-flash-image')) { - suffixes.push('-0.5k', '-2k', '-4k') - if (this._config.imageModelSearch) { - suffixes.push( - '-search', - '-0.5k-search', - '-2k-search', - '-4k-search' - ) - } - } else if ( - name.includes('gemini-2.5') && - !name.includes('image') && - !name.includes('-thinking') - ) { - suffixes.push('-non-thinking', '-thinking') - } else if ( - (name.includes('gemini-3-pro') || - name.includes('gemini-3-flash') || - name.includes('gemini-3.1-pro')) && - !name.includes('image') - ) { - suffixes.push('-low-thinking', '-high-thinking') - suffixes.push('-minimal-thinking') - if (!/gemini-3(\.1)?-pro/.test(name)) { - suffixes.push('-medium-thinking') - } - } + const suffixes = getModelVariantSuffixes( + model.name.toLowerCase(), + this._config.imageModelSearch + ) for (const suffix of suffixes) { - const full = model.name + suffix - if (!names.has(full)) { - names.add(full) - models.push({ ...model, name: full }) - } + addModel({ ...model, name: model.name + suffix }) } + addModel(model) + } - if (names.has(model.name)) continue - - names.add(model.name) - models.push(model) + if (models.length === 0) { + throw new ChatLunaError( + ChatLunaErrorCode.MODEL_INIT_ERROR, + new Error('No model configured') + ) } return models diff --git a/packages/adapter-gemini/src/utils.ts b/packages/adapter-gemini/src/utils.ts index ec30c9ceb..4fc40dbfb 100644 --- a/packages/adapter-gemini/src/utils.ts +++ b/packages/adapter-gemini/src/utils.ts @@ -546,9 +546,7 @@ export function prepareModelConfig( if ( pluginConfig.additionalModels.some( - (item) => - item.model === model && - !item.model.toLowerCase().includes('gemini') + (item) => item.model === model && !isGeminiModelName(item.model) ) ) { return { @@ -761,6 +759,11 @@ export function isChatResponse(response: any): response is ChatResponse { // #region refreshModels helpers +export function isGeminiModelName(model: string): boolean { + const name = model.toLowerCase().split('/').pop() ?? model.toLowerCase() + return /^gemini(?:-|$)/.test(name) +} + export function createGeminiCapabilities( modelNameLower: string, isEmbedding: boolean @@ -830,6 +833,46 @@ export function isGemini3ProFamily(modelName: string): boolean { return /gemini-3(\.1)?-pro/.test(modelName) } +/** 图片生成模型支持的分辨率变体 */ +const IMAGE_MODEL_RESOLUTIONS: [string, string[]][] = [ + ['gemini-3-pro-image', ['-2k', '-4k']], + ['gemini-3.1-flash-image', ['-0.5k', '-2k', '-4k']] +] + +/** 计算模型需要展开的变体后缀(图片分辨率 / 搜索、thinking 开关与等级) */ +export function getModelVariantSuffixes( + name: string, + imageModelSearch: boolean +): string[] { + const resolutions = IMAGE_MODEL_RESOLUTIONS.find(([model]) => + name.includes(model) + )?.[1] + + if (resolutions) { + if (!imageModelSearch) return resolutions + return [ + ...resolutions, + '-search', + ...resolutions.map((r) => `${r}-search`) + ] + } + + if (name.includes('image')) return [] + + if (name.includes('gemini-2.5')) { + return name.includes('-thinking') ? [] : ['-non-thinking', '-thinking'] + } + + if (!/gemini-3(-pro|-flash|\.5-flash|\.1-pro)/.test(name)) return [] + + // gemini-3-pro(不含 3.1)不提供 medium 等级 + return ( + /(^|\/)gemini-3-pro/.test(name) + ? ['low', 'high', 'minimal'] + : ['low', 'high', 'minimal', 'medium'] + ).map((level) => `-${level}-thinking`) +} + // #endregion export function getModalityTokens(