diff --git a/packages/core/src/middlewares/chat/chat_time_limit_check.ts b/packages/core/src/middlewares/chat/chat_time_limit_check.ts index 8d5b59378..58edea8d4 100644 --- a/packages/core/src/middlewares/chat/chat_time_limit_check.ts +++ b/packages/core/src/middlewares/chat/chat_time_limit_check.ts @@ -124,7 +124,11 @@ export function apply(ctx: Context, config: Config, chain: ChatChain) { } return { - model: resolved.effectiveModel ?? conversation.model, + model: + ctx.chatluna.conversation.pickModel( + resolved.constraint, + conversation + ) ?? conversation.model, conversationId: conversation.id } } diff --git a/packages/core/src/middlewares/chat/read_chat_message.ts b/packages/core/src/middlewares/chat/read_chat_message.ts index c03288d83..7c97418a0 100644 --- a/packages/core/src/middlewares/chat/read_chat_message.ts +++ b/packages/core/src/middlewares/chat/read_chat_message.ts @@ -141,7 +141,10 @@ export function apply(ctx: Context, config: Config, chain: ChatChain) { await ctx.chatluna.messageTransformer.transform( session, message, - resolved.effectiveModel ?? '', + ctx.chatluna.conversation.pickModel( + resolved.constraint, + resolved.conversation + ) ?? '', undefined, { quote: false, diff --git a/packages/core/src/middlewares/chat/rollback_chat.ts b/packages/core/src/middlewares/chat/rollback_chat.ts index 3998f7328..3497a63fe 100644 --- a/packages/core/src/middlewares/chat/rollback_chat.ts +++ b/packages/core/src/middlewares/chat/rollback_chat.ts @@ -219,7 +219,8 @@ async function rollbackConversation( inputMessage = await ctx.chatluna.messageTransformer.transform( session, transformMessageContentToElements(humanContent), - resolved.effectiveModel ?? current.model, + ctx.chatluna.conversation.pickModel(resolved.constraint, current) ?? + current.model, undefined, { quote: false, diff --git a/packages/core/src/middlewares/model/resolve_model.ts b/packages/core/src/middlewares/model/resolve_model.ts index 94861bf9f..37c6f97fc 100644 --- a/packages/core/src/middlewares/model/resolve_model.ts +++ b/packages/core/src/middlewares/model/resolve_model.ts @@ -20,10 +20,10 @@ export function apply(ctx: Context, config: Config, chain: ChatChain) { try { const modelName = - resolved.effectiveModel ?? - resolved.conversation?.model ?? - config.defaultModel ?? - 'empty' + ctx.chatluna.conversation.pickModel( + resolved.constraint, + resolved.conversation + ) ?? 'empty' const presetName = resolved.effectivePreset ?? resolved.conversation?.preset ?? diff --git a/packages/core/src/middlewares/system/conversation_manage.ts b/packages/core/src/middlewares/system/conversation_manage.ts index 3c9d32629..d9df2faba 100644 --- a/packages/core/src/middlewares/system/conversation_manage.ts +++ b/packages/core/src/middlewares/system/conversation_manage.ts @@ -128,7 +128,10 @@ export function apply(ctx: Context, config: Config, chain: ChatChain) { session.text('chatluna.conversation.default_title'), model: createModel ?? - resolved.effectiveModel ?? + ctx.chatluna.conversation.pickModel( + resolved.constraint, + resolved.conversation + ) ?? config.defaultModel, preset: createPreset ?? @@ -223,6 +226,7 @@ export function apply(ctx: Context, config: Config, chain: ChatChain) { const pagination = new Pagination({ formatItem: (item) => formatConversationLine( + ctx, session, item.conversation, resolved, @@ -265,7 +269,12 @@ export function apply(ctx: Context, config: Config, chain: ChatChain) { context.message = [ session.text('chatluna.conversation.messages.current_header'), - formatConversationLine(session, resolved.conversation, resolved) + formatConversationLine( + ctx, + session, + resolved.conversation, + resolved + ) ].join('\n') return ChainMiddlewareRunStatus.STOP }) @@ -998,6 +1007,7 @@ function formatConversationError( } function formatConversationLine( + ctx: Context, session: Session, conversation: ConversationRecord, resolved: ResolvedConversationContext, @@ -1010,10 +1020,10 @@ function formatConversationLine( resolved.binding?.activeConversationId ) const effectiveModel = - resolved.constraint.fixedModel ?? - conversation.model ?? - resolved.constraint.defaultModel ?? - '-' + ctx.chatluna.conversation.pickModel( + resolved.constraint, + conversation + ) ?? '-' const effectivePreset = resolved.constraint.fixedPreset ?? conversation.preset ?? diff --git a/packages/core/src/services/conversation.ts b/packages/core/src/services/conversation.ts index 9e29e3899..3e0724e87 100644 --- a/packages/core/src/services/conversation.ts +++ b/packages/core/src/services/conversation.ts @@ -2,6 +2,8 @@ import { createHash, randomUUID } from 'crypto' import fs from 'fs/promises' import path from 'path' import type { Context, Session } from 'koishi' +import { ModelType } from 'koishi-plugin-chatluna/llm-core/platform/types' +import { parseRawModelName } from 'koishi-plugin-chatluna/llm-core/utils/count_tokens' import type { Config } from '../config' import { deserializeConversation, @@ -281,17 +283,17 @@ export class ConversationService { )) ? conversation : null + const effectiveModel = this.pickModel(constraint, allowedConversation) return { bindingKey, presetLane: getPresetLane(bindingKey), binding: binding ?? null, - conversation: allowedConversation, - effectiveModel: - constraint.fixedModel ?? - allowedConversation?.model ?? - constraint.defaultModel ?? - this.config.defaultModel, + conversation: + allowedConversation != null && effectiveModel != null + ? { ...allowedConversation, model: effectiveModel } + : allowedConversation, + effectiveModel, effectivePreset: constraint.fixedPreset ?? allowedConversation?.preset ?? @@ -325,7 +327,7 @@ export class ConversationService { return { ...target, mode, - conversation, + conversation: target.conversation ?? conversation, conversationId: conversation.id } } @@ -361,11 +363,18 @@ export class ConversationService { current.bindingKey )) ) { + const effectiveModel = this.pickModel( + current.constraint, + conversation + ) + current = { ...current, - conversation, - effectiveModel: - current.constraint.fixedModel ?? conversation.model, + conversation: + effectiveModel != null + ? { ...conversation, model: effectiveModel } + : conversation, + effectiveModel, effectivePreset: current.constraint.fixedPreset ?? conversation.preset, @@ -392,13 +401,20 @@ export class ConversationService { conversationId: current.conversation.id } ) + const effectiveModel = this.pickModel( + current.constraint, + conversation + ) return { ...current, mode, - conversation, + conversation: + effectiveModel != null + ? { ...conversation, model: effectiveModel } + : conversation, conversationId: conversation.id, - effectiveModel: conversation.model, + effectiveModel, effectivePreset: conversation.preset, effectiveChatMode: conversation.chatMode } @@ -1895,6 +1911,48 @@ export class ConversationService { await fs.mkdir(target, { recursive: true }) return target } + + pickModel( + constraint: ResolvedConstraint, + conversation?: ConversationRecord | null + ) { + for (const model of [ + constraint.fixedModel, + conversation?.model, + constraint.defaultModel, + this.config.defaultModel + ]) { + if ( + model == null || + model.trim().length < 1 || + model === '无' || + model === 'empty' + ) { + continue + } + + const [platform, name] = parseRawModelName(model) + + if (platform == null || name == null) { + continue + } + + const platformModels = + this.ctx.chatluna.platform.listPlatformModels( + platform, + ModelType.llm + ).value + + if ( + platformModels.length > 0 && + platformModels.some((m) => m.name === name) + ) { + return model + } + } + + return null + } } function isConstraintMatched(constraint: ConstraintRecord, session: Session) { diff --git a/packages/core/tests/conversation-service.spec.ts b/packages/core/tests/conversation-service.spec.ts index afb357b9c..6595c7e7c 100644 --- a/packages/core/tests/conversation-service.spec.ts +++ b/packages/core/tests/conversation-service.spec.ts @@ -123,6 +123,45 @@ it('ConversationService defaults resolveConversation mode to context and sets nu assert.equal(resolved.conversationId, null) }) +it('ConversationService skips unavailable models before using config default', async () => { + const conversation = createConversation({ + model: 'missing-platform/old-model' + }) + const { service } = await createService({ + tables: { + chatluna_conversation: [conversation as unknown as TableRow], + chatluna_binding: [ + { + bindingKey: conversation.bindingKey, + activeConversationId: conversation.id, + lastConversationId: null, + updatedAt: new Date() + } + ], + chatluna_constraint: [ + { + id: 1, + name: 'unavailable-models', + enabled: true, + priority: 10, + createdBy: 'admin', + createdAt: new Date(), + updatedAt: new Date(), + users: null, + excludeUsers: null, + fixedModel: 'missing-platform/fixed-model', + defaultModel: 'missing-platform/default-model' + } as unknown as TableRow + ] + } + }) + + const resolved = await service.ensureActiveConversation(createSession()) + + assert.equal(resolved.effectiveModel, 'test-platform/test-model') + assert.equal(resolved.conversation.model, 'test-platform/test-model') +}) + it('ConversationService resolveConversation uses explicit binding key constraints', async () => { const remote = createConversation({ id: 'conversation-remote-binding', diff --git a/packages/core/tests/helpers.ts b/packages/core/tests/helpers.ts index 92e5008df..148434cc9 100644 --- a/packages/core/tests/helpers.ts +++ b/packages/core/tests/helpers.ts @@ -12,6 +12,7 @@ import { } from '../src/services/conversation_types' import { ChatLunaService } from '../src/services/chat' import { ConversationService } from '../src/services/conversation' +import { ModelType } from '../src/llm-core/platform/types' export async function expectRejected( promise: Promise, @@ -312,6 +313,24 @@ export async function createService( } }, chatluna: { + platform: { + chatChains: { + value: [{ name: 'plugin' }] + }, + listPlatformModels: (platform: string) => ({ + value: + platform === 'test-platform' + ? [ + { + name: 'test-model', + type: ModelType.llm, + maxTokens: 4096, + capabilities: [] + } + ] + : [] + }) + }, conversation: { getArchive: async (id: string) => database.tables.chatluna_archive.find( @@ -364,6 +383,18 @@ export async function createMemoryService( app.plugin(memory) app.plugin(ChatLunaService, createConfig(options.config)) await app.start() + ;( + app.chatluna.platform as unknown as { + _models: Record + } + )._models['test-platform'] = [ + { + name: 'test-model', + type: ModelType.llm, + maxTokens: 4096, + capabilities: [] + } + ] for (const [table, rows] of Object.entries(options.tables ?? {})) { for (const row of rows ?? []) { diff --git a/packages/extension-agent/src/trigger/executor.ts b/packages/extension-agent/src/trigger/executor.ts index b6cedbdc4..c1ebda702 100644 --- a/packages/extension-agent/src/trigger/executor.ts +++ b/packages/extension-agent/src/trigger/executor.ts @@ -208,10 +208,12 @@ export class ChatLunaAgentTriggerExecutor { constraint.fixedPreset ?? constraint.defaultPreset ?? this.ctx.chatluna.config.defaultPreset - const model = - constraint.fixedModel ?? - constraint.defaultModel ?? - this.ctx.chatluna.config.defaultModel + const model = this.ctx.chatluna.conversation.pickModel(constraint) + + if (model == null) { + throw new Error('No available model found.') + } + const chatMode = constraint.fixedChatMode ?? constraint.defaultChatMode ??