-
-
Notifications
You must be signed in to change notification settings - Fork 53
feat(adapter-gemini): 支持手动模型列表 #910
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -121,60 +121,95 @@ | |
| * - gemini-3 系列:thinking 等级变体(low / medium / high / minimal) | ||
| * - 其他模型:直接加入,不展开 | ||
| */ | ||
| async refreshModels(config?: RunnableConfig): Promise<ModelInfo[]> { | ||
| // --- 获取原始模型列表 --- | ||
| 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') | ||
| ) | ||
| } | ||
| } catch (e) { | ||
| if (e instanceof ChatLunaError) throw e | ||
| throw new ChatLunaError(ChatLunaErrorCode.MODEL_INIT_ERROR, e) | ||
| } | ||
|
|
||
| // --- 将原始模型转换并展开为变体列表 --- | ||
| const models: ModelInfo[] = [] | ||
|
|
||
| for (const model of this._config.additionalModels) { | ||
| const modelNameLower = model.model.toLowerCase() | ||
| const isEmbedding = model.modelType === 'Embeddings 嵌入模型' | ||
|
|
||
| const baseInfo: ModelInfo = { | ||
| name: model.model, | ||
| maxTokens: model.contextSize, | ||
| type: isEmbedding ? ModelType.embeddings : ModelType.llm, | ||
| capabilities: isEmbedding | ||
| ? [] | ||
| : modelNameLower.includes('gemini') | ||
| ? createGeminiCapabilities(modelNameLower, false) | ||
| : model.modelCapabilities | ||
| } | ||
|
|
||
| if ( | ||
| !expandModelVariants( | ||
| models, | ||
| baseInfo, | ||
| this._config.imageModelSearch | ||
| ) | ||
| ) { | ||
| models.push(baseInfo) | ||
| } | ||
| } | ||
|
Comment on lines
+147
to
+171
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 补充模型分支缺少按名称去重,可能返回重复模型 Line 147-171 当前把 建议修复(在 additionalModels 分支也走同样的去重路径)- for (const model of this._config.additionalModels) {
+ for (const model of this._config.additionalModels) {
const modelNameLower = model.model.toLowerCase()
const isEmbedding = model.modelType === 'Embeddings 嵌入模型'
const baseInfo: ModelInfo = {
name: model.model,
maxTokens: model.contextSize,
type: isEmbedding ? ModelType.embeddings : ModelType.llm,
capabilities: isEmbedding
? []
: modelNameLower.includes('gemini')
? createGeminiCapabilities(modelNameLower, false)
: model.modelCapabilities
}
- if (
- !expandModelVariants(
- models,
- baseInfo,
- this._config.imageModelSearch
- )
- ) {
- models.push(baseInfo)
- }
+ const items: ModelInfo[] = []
+ if (
+ !expandModelVariants(
+ items,
+ baseInfo,
+ this._config.imageModelSearch
+ )
+ ) {
+ items.push(baseInfo)
+ }
+
+ for (const item of items) {
+ if (models.findIndex((info) => info.name === item.name) < 0) {
+ models.push(item)
+ }
+ }
}🤖 Prompt for AI Agents |
||
|
|
||
| for (const model of rawModels) { | ||
| const modelNameLower = model.name.toLowerCase() | ||
|
|
||
| if (shouldFilterOutGeminiModel(modelNameLower)) { | ||
| continue | ||
| } | ||
|
|
||
| const isEmbedding = modelNameLower.includes('embedding') | ||
|
|
||
| const baseInfo: ModelInfo = { | ||
| name: model.name, | ||
| maxTokens: model.inputTokenLimit, | ||
| type: isEmbedding ? ModelType.embeddings : ModelType.llm, | ||
| capabilities: createGeminiCapabilities( | ||
| modelNameLower, | ||
| isEmbedding | ||
| ) | ||
| } | ||
|
|
||
| // 尝试展开特殊变体;未命中则直接加入 | ||
| const items: ModelInfo[] = [] | ||
| if ( | ||
| !expandModelVariants( | ||
| models, | ||
| items, | ||
| baseInfo, | ||
| this._config.imageModelSearch | ||
| ) | ||
| ) { | ||
| models.push(baseInfo) | ||
| items.push(baseInfo) | ||
| } | ||
|
|
||
| for (const item of items) { | ||
| if (models.findIndex((info) => info.name === item.name) < 0) { | ||
| models.push(item) | ||
| } | ||
| } | ||
| } | ||
|
|
||
| return models | ||
| } | ||
|
|
||
| // #endregion | ||
|
|
||
|
|
||
| Original file line number | Diff line number | Diff line change | ||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -547,6 +547,23 @@ export function prepareModelConfig( | |||||||||||||||||
| let imageSize: string | undefined | ||||||||||||||||||
| let forceGoogleSearch = false | ||||||||||||||||||
|
|
||||||||||||||||||
| const extra = pluginConfig.additionalModels.find( | ||||||||||||||||||
| (item) => | ||||||||||||||||||
| item.model === model && !item.model.toLowerCase().includes('gemini') | ||||||||||||||||||
| ) | ||||||||||||||||||
|
Comment on lines
+550
to
+553
Contributor
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. 同理,在 建议使用可选链(optional chaining)进行安全调用。
Suggested change
|
||||||||||||||||||
|
|
||||||||||||||||||
| 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) | ||||||||||||||||||
|
|
||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,119 @@ | ||
| /// <reference types="mocha" /> | ||
|
|
||
| import { assert } from 'chai' | ||
| import { | ||
| ModelCapabilities, | ||
| ModelType | ||
| } from 'koishi-plugin-chatluna/llm-core/platform/types' | ||
| import { GeminiClient } from '../src/client' | ||
| import { prepareModelConfig } from '../src/utils' | ||
|
|
||
| function client(cfg, raw = []) { | ||
| let calls = 0 | ||
| const item = Object.create(GeminiClient.prototype) | ||
|
|
||
| Object.assign(item, { | ||
| _config: cfg, | ||
| _requester: { | ||
| getModels: async () => { | ||
| calls++ | ||
| return raw | ||
| } | ||
| } | ||
| }) | ||
|
|
||
| return { | ||
| item, | ||
| get calls() { | ||
| return calls | ||
| } | ||
| } | ||
| } | ||
|
|
||
| it('GeminiClient expands additional Gemini models without pulling remote models', async () => { | ||
| const cfg = { | ||
| pullModels: false, | ||
| imageModelSearch: false, | ||
| additionalModels: [ | ||
| { | ||
| model: 'gemini-2.5-pro', | ||
| modelType: 'LLM 大语言模型', | ||
| modelCapabilities: [ModelCapabilities.ToolCall], | ||
| contextSize: 8192 | ||
| } | ||
| ] | ||
| } | ||
| const mock = client(cfg) | ||
|
|
||
| const models = await mock.item.refreshModels() | ||
|
|
||
| assert.equal(mock.calls, 0) | ||
| assert.deepEqual( | ||
| models.map((model) => model.name), | ||
| [ | ||
| 'gemini-2.5-pro-non-thinking', | ||
| 'gemini-2.5-pro-thinking', | ||
| 'gemini-2.5-pro' | ||
| ] | ||
| ) | ||
| assert.equal(models[0].type, ModelType.llm) | ||
| assert.equal(models[0].maxTokens, 8192) | ||
| }) | ||
|
|
||
| it('GeminiClient keeps additional non-Gemini model names unchanged', async () => { | ||
| const cfg = { | ||
| pullModels: false, | ||
| imageModelSearch: true, | ||
| additionalModels: [ | ||
| { | ||
| model: 'chatluna-chat-search', | ||
| modelType: 'LLM 大语言模型', | ||
| modelCapabilities: [ | ||
| ModelCapabilities.TextInput, | ||
| ModelCapabilities.ToolCall | ||
| ], | ||
| contextSize: 4096 | ||
| } | ||
| ] | ||
| } | ||
| const mock = client(cfg) | ||
|
|
||
| const models = await mock.item.refreshModels() | ||
|
|
||
| assert.deepEqual(models, [ | ||
| { | ||
| name: 'chatluna-chat-search', | ||
| type: ModelType.llm, | ||
| capabilities: [ | ||
| ModelCapabilities.TextInput, | ||
| ModelCapabilities.ToolCall | ||
| ], | ||
| maxTokens: 4096 | ||
| } | ||
| ]) | ||
| }) | ||
|
|
||
| it('prepareModelConfig preserves configured non-Gemini request model names', () => { | ||
| const cfg = { | ||
| thinkingBudget: 77, | ||
| imageGeneration: true, | ||
| additionalModels: [ | ||
| { | ||
| model: 'chatluna-chat-search', | ||
| modelType: 'LLM 大语言模型', | ||
| modelCapabilities: [ | ||
| ModelCapabilities.TextInput, | ||
| ModelCapabilities.ToolCall | ||
| ], | ||
| contextSize: 4096 | ||
| } | ||
| ] | ||
| } | ||
|
|
||
| const model = prepareModelConfig({ model: 'chatluna-chat-search' }, cfg) | ||
|
|
||
| assert.equal(model.model, 'chatluna-chat-search') | ||
| assert.equal(model.forceGoogleSearch, false) | ||
| assert.equal(model.imageGeneration, true) | ||
| assert.equal(model.thinkingBudget, 77) | ||
| }) |
| Original file line number | Diff line number | Diff line change |
|---|---|---|
| @@ -0,0 +1,9 @@ | ||
| { | ||
| "extends": "../../../tsconfig.json", | ||
| "compilerOptions": { | ||
| "module": "commonjs", | ||
| "moduleResolution": "node", | ||
| "types": ["node", "mocha", "chai"] | ||
| }, | ||
| "include": ["./**/*.ts", "../src/**/*.ts"] | ||
| } |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
在 Koishi 插件中,当用户更新插件但尚未在控制台重新保存配置时,新引入的配置项
additionalModels可能会是undefined。直接对其进行for...of遍历会导致运行时报错TypeError: ... is not iterable。建议添加空值保护,例如使用空数组作为回退值。