|
@@ -1,16 +1,16 @@
|
|
|
-import { Injectable, NotFoundException, ForbiddenException, BadRequestException, forwardRef, Inject } from '@nestjs/common';
|
|
|
|
|
|
|
+import { Injectable, NotFoundException, ForbiddenException, BadRequestException, forwardRef, Inject, OnModuleInit, Logger } from '@nestjs/common';
|
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
import { InjectRepository } from '@nestjs/typeorm';
|
|
|
import { Repository } from 'typeorm';
|
|
import { Repository } from 'typeorm';
|
|
|
import { ModelConfig } from './model-config.entity';
|
|
import { ModelConfig } from './model-config.entity';
|
|
|
import { CreateModelConfigDto } from './dto/create-model-config.dto';
|
|
import { CreateModelConfigDto } from './dto/create-model-config.dto';
|
|
|
import { UpdateModelConfigDto } from './dto/update-model-config.dto';
|
|
import { UpdateModelConfigDto } from './dto/update-model-config.dto';
|
|
|
-import { GLOBAL_TENANT_ID } from '../common/constants';
|
|
|
|
|
import { TenantService } from '../tenant/tenant.service';
|
|
import { TenantService } from '../tenant/tenant.service';
|
|
|
import { ModelType } from '../types';
|
|
import { ModelType } from '../types';
|
|
|
import { I18nService } from '../i18n/i18n.service';
|
|
import { I18nService } from '../i18n/i18n.service';
|
|
|
|
|
|
|
|
@Injectable()
|
|
@Injectable()
|
|
|
-export class ModelConfigService {
|
|
|
|
|
|
|
+export class ModelConfigService implements OnModuleInit {
|
|
|
|
|
+ private readonly logger = new Logger(ModelConfigService.name);
|
|
|
constructor(
|
|
constructor(
|
|
|
@InjectRepository(ModelConfig)
|
|
@InjectRepository(ModelConfig)
|
|
|
private modelConfigRepository: Repository<ModelConfig>,
|
|
private modelConfigRepository: Repository<ModelConfig>,
|
|
@@ -19,62 +19,73 @@ export class ModelConfigService {
|
|
|
private i18nService: I18nService,
|
|
private i18nService: I18nService,
|
|
|
) { }
|
|
) { }
|
|
|
|
|
|
|
|
|
|
+ async onModuleInit() {
|
|
|
|
|
+ try {
|
|
|
|
|
+ await this.sanitizeExistingModelIds();
|
|
|
|
|
+ } catch (err) {
|
|
|
|
|
+ this.logger.error(`Failed to sanitize existing model IDs: ${err.message}`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private async sanitizeExistingModelIds() {
|
|
|
|
|
+ const models = await this.modelConfigRepository.find();
|
|
|
|
|
+ let sanitizedCount = 0;
|
|
|
|
|
+ for (const model of models) {
|
|
|
|
|
+ const sanitizedId = model.modelId.trim().replace(/\s+/g, '');
|
|
|
|
|
+ if (sanitizedId !== model.modelId) {
|
|
|
|
|
+ this.logger.log(`Sanitizing malformed model ID for "${model.name}": "${model.modelId}" -> "${sanitizedId}"`);
|
|
|
|
|
+ model.modelId = sanitizedId;
|
|
|
|
|
+ await this.modelConfigRepository.save(model);
|
|
|
|
|
+ sanitizedCount++;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ if (sanitizedCount > 0) {
|
|
|
|
|
+ this.logger.log(`Successfully sanitized ${sanitizedCount} malformed model IDs.`);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
async create(
|
|
async create(
|
|
|
- userId: string,
|
|
|
|
|
- tenantId: string,
|
|
|
|
|
createModelConfigDto: CreateModelConfigDto,
|
|
createModelConfigDto: CreateModelConfigDto,
|
|
|
): Promise<ModelConfig> {
|
|
): Promise<ModelConfig> {
|
|
|
- const modelConfig = this.modelConfigRepository.create({
|
|
|
|
|
- ...createModelConfigDto,
|
|
|
|
|
- userId,
|
|
|
|
|
- tenantId,
|
|
|
|
|
- });
|
|
|
|
|
|
|
+ // Sanitize modelId (remove whitespace)
|
|
|
|
|
+ if (createModelConfigDto.modelId) {
|
|
|
|
|
+ createModelConfigDto.modelId = createModelConfigDto.modelId.trim().replace(/\s+/g, '');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ const modelConfig = this.modelConfigRepository.create(createModelConfigDto);
|
|
|
return this.modelConfigRepository.save(modelConfig);
|
|
return this.modelConfigRepository.save(modelConfig);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- async findAll(userId: string, tenantId: string): Promise<ModelConfig[]> {
|
|
|
|
|
- return this.modelConfigRepository.createQueryBuilder('model')
|
|
|
|
|
- .where('model.tenantId = :tenantId OR model.tenantId IS NULL OR model.tenantId = :globalTenantId', {
|
|
|
|
|
- tenantId,
|
|
|
|
|
- globalTenantId: GLOBAL_TENANT_ID
|
|
|
|
|
- })
|
|
|
|
|
- .getMany();
|
|
|
|
|
|
|
+ async findAll(): Promise<ModelConfig[]> {
|
|
|
|
|
+ return this.modelConfigRepository.find();
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- async findOne(id: string, userId: string, tenantId: string): Promise<ModelConfig> {
|
|
|
|
|
- const modelConfig = await this.modelConfigRepository.createQueryBuilder('model')
|
|
|
|
|
- .where('model.id = :id', { id })
|
|
|
|
|
- .andWhere('(model.tenantId = :tenantId OR model.tenantId IS NULL OR model.tenantId = :globalTenantId)', {
|
|
|
|
|
- tenantId,
|
|
|
|
|
- globalTenantId: GLOBAL_TENANT_ID
|
|
|
|
|
- })
|
|
|
|
|
- .getOne();
|
|
|
|
|
|
|
+ async findOne(id: string): Promise<ModelConfig> {
|
|
|
|
|
+ const modelConfig = await this.modelConfigRepository.findOne({ where: { id } });
|
|
|
|
|
|
|
|
if (!modelConfig) {
|
|
if (!modelConfig) {
|
|
|
throw new NotFoundException(
|
|
throw new NotFoundException(
|
|
|
this.i18nService.formatMessage('modelConfigNotFound', { id }),
|
|
this.i18nService.formatMessage('modelConfigNotFound', { id }),
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
+
|
|
|
|
|
+ // Defensive sanitization in case DB hasn't been updated yet
|
|
|
|
|
+ if (modelConfig.modelId) {
|
|
|
|
|
+ modelConfig.modelId = modelConfig.modelId.trim().replace(/\s+/g, '');
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
return modelConfig;
|
|
return modelConfig;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- async findByType(userId: string, tenantId: string, type: string): Promise<ModelConfig[]> {
|
|
|
|
|
- return this.modelConfigRepository.createQueryBuilder('model')
|
|
|
|
|
- .where('model.type = :type', { type })
|
|
|
|
|
- .andWhere('(model.tenantId = :tenantId OR model.tenantId IS NULL OR model.tenantId = :globalTenantId)', {
|
|
|
|
|
- tenantId,
|
|
|
|
|
- globalTenantId: GLOBAL_TENANT_ID
|
|
|
|
|
- })
|
|
|
|
|
- .getMany();
|
|
|
|
|
|
|
+ async findByType(type: string): Promise<ModelConfig[]> {
|
|
|
|
|
+ return this.modelConfigRepository.find({ where: { type } });
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
async update(
|
|
async update(
|
|
|
- userId: string,
|
|
|
|
|
- tenantId: string,
|
|
|
|
|
id: string,
|
|
id: string,
|
|
|
updateModelConfigDto: UpdateModelConfigDto,
|
|
updateModelConfigDto: UpdateModelConfigDto,
|
|
|
): Promise<ModelConfig> {
|
|
): Promise<ModelConfig> {
|
|
|
- const modelConfig = await this.findOne(id, userId, tenantId);
|
|
|
|
|
|
|
+ const modelConfig = await this.findOne(id);
|
|
|
|
|
|
|
|
if (!modelConfig) {
|
|
if (!modelConfig) {
|
|
|
throw new NotFoundException(
|
|
throw new NotFoundException(
|
|
@@ -82,9 +93,11 @@ export class ModelConfigService {
|
|
|
);
|
|
);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- // Only allow updating if it belongs to the tenant, or if it's a global admin (not fully implemented, so we check tenantId)
|
|
|
|
|
- if (modelConfig.tenantId && modelConfig.tenantId !== tenantId) {
|
|
|
|
|
- throw new ForbiddenException(this.i18nService.getMessage('cannotUpdateOtherTenantModel'));
|
|
|
|
|
|
|
+ // Models are now global, no tenant check needed.
|
|
|
|
|
+
|
|
|
|
|
+ // Sanitize modelId (remove whitespace)
|
|
|
|
|
+ if (updateModelConfigDto.modelId) {
|
|
|
|
|
+ updateModelConfigDto.modelId = updateModelConfigDto.modelId.trim().replace(/\s+/g, '');
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
// Update the model
|
|
// Update the model
|
|
@@ -95,12 +108,9 @@ export class ModelConfigService {
|
|
|
return this.modelConfigRepository.save(updated);
|
|
return this.modelConfigRepository.save(updated);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
- async remove(userId: string, tenantId: string, id: string): Promise<void> {
|
|
|
|
|
- // Only allow removing if it exists and accessible in current tenant context
|
|
|
|
|
- const model = await this.findOne(id, userId, tenantId);
|
|
|
|
|
- if (model.tenantId && model.tenantId !== tenantId) {
|
|
|
|
|
- throw new ForbiddenException(this.i18nService.getMessage('cannotDeleteOtherTenantModel'));
|
|
|
|
|
- }
|
|
|
|
|
|
|
+ async remove(id: string): Promise<void> {
|
|
|
|
|
+ // Only allow removing if it exists
|
|
|
|
|
+ await this.findOne(id);
|
|
|
const result = await this.modelConfigRepository.delete({ id });
|
|
const result = await this.modelConfigRepository.delete({ id });
|
|
|
if (result.affected === 0) {
|
|
if (result.affected === 0) {
|
|
|
throw new NotFoundException(this.i18nService.formatMessage('modelConfigNotFound', { id }));
|
|
throw new NotFoundException(this.i18nService.formatMessage('modelConfigNotFound', { id }));
|
|
@@ -110,19 +120,15 @@ export class ModelConfigService {
|
|
|
/**
|
|
/**
|
|
|
* Set the specified model as default
|
|
* Set the specified model as default
|
|
|
*/
|
|
*/
|
|
|
- async setDefault(userId: string, tenantId: string, id: string): Promise<ModelConfig> {
|
|
|
|
|
- const modelConfig = await this.findOne(id, userId, tenantId);
|
|
|
|
|
|
|
+ async setDefault(id: string): Promise<ModelConfig> {
|
|
|
|
|
+ const modelConfig = await this.findOne(id);
|
|
|
|
|
|
|
|
- // Clear default flag for other models of the same type (within current tenant or global)
|
|
|
|
|
|
|
+ // Clear default flag for other models of the same type (globally)
|
|
|
await this.modelConfigRepository
|
|
await this.modelConfigRepository
|
|
|
.createQueryBuilder()
|
|
.createQueryBuilder()
|
|
|
.update(ModelConfig)
|
|
.update(ModelConfig)
|
|
|
.set({ isDefault: false })
|
|
.set({ isDefault: false })
|
|
|
.where('type = :type', { type: modelConfig.type })
|
|
.where('type = :type', { type: modelConfig.type })
|
|
|
- .andWhere('(tenantId = :tenantId OR tenantId IS NULL OR tenantId = :globalTenantId)', {
|
|
|
|
|
- tenantId,
|
|
|
|
|
- globalTenantId: GLOBAL_TENANT_ID
|
|
|
|
|
- })
|
|
|
|
|
.execute();
|
|
.execute();
|
|
|
|
|
|
|
|
modelConfig.isDefault = true;
|
|
modelConfig.isDefault = true;
|
|
@@ -133,31 +139,94 @@ export class ModelConfigService {
|
|
|
* Get default model for specified type
|
|
* Get default model for specified type
|
|
|
* Strict rule: Only return models specified in Index Chat Config, throw error if not found
|
|
* Strict rule: Only return models specified in Index Chat Config, throw error if not found
|
|
|
*/
|
|
*/
|
|
|
- async findDefaultByType(tenantId: string, type: ModelType): Promise<ModelConfig> {
|
|
|
|
|
- const settings = await this.tenantService.getSettings(tenantId);
|
|
|
|
|
- if (!settings) {
|
|
|
|
|
- throw new BadRequestException(`Organization settings not found for tenant: ${tenantId}`);
|
|
|
|
|
|
|
+ async findDefaultByType(tenantId: string, type: ModelType, strict: boolean = false): Promise<ModelConfig> {
|
|
|
|
|
+ const systemId = await this.tenantService.getSystemTenantId();
|
|
|
|
|
+
|
|
|
|
|
+ // 1. Resolve effective tenant ID
|
|
|
|
|
+ let effectiveTenantId = tenantId;
|
|
|
|
|
+ if (!tenantId || tenantId === 'default') {
|
|
|
|
|
+ effectiveTenantId = systemId;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 2. Try to get settings for the target tenant
|
|
|
|
|
+ const settings = await this.tenantService.getSettings(effectiveTenantId);
|
|
|
|
|
+
|
|
|
|
|
+ // 3. Extract model ID from settings
|
|
|
let modelId: string | undefined;
|
|
let modelId: string | undefined;
|
|
|
- if (type === ModelType.LLM) {
|
|
|
|
|
- modelId = settings.selectedLLMId;
|
|
|
|
|
- } else if (type === ModelType.EMBEDDING) {
|
|
|
|
|
- modelId = settings.selectedEmbeddingId;
|
|
|
|
|
- } else if (type === ModelType.RERANK) {
|
|
|
|
|
- modelId = settings.selectedRerankId;
|
|
|
|
|
|
|
+
|
|
|
|
|
+ const extractModelId = (s: any) => {
|
|
|
|
|
+ if (type === ModelType.LLM) return s.selectedLLMId;
|
|
|
|
|
+ if (type === ModelType.EMBEDDING) return s.selectedEmbeddingId;
|
|
|
|
|
+ if (type === ModelType.RERANK) return s.selectedRerankId;
|
|
|
|
|
+ if (type === ModelType.VISION) return s.selectedVisionId;
|
|
|
|
|
+ return undefined;
|
|
|
|
|
+ };
|
|
|
|
|
+
|
|
|
|
|
+ if (settings) {
|
|
|
|
|
+ modelId = extractModelId(settings);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
|
|
+ // 4. Fallbacks (Disabled in strict mode)
|
|
|
|
|
+ if (!strict) {
|
|
|
|
|
+ // a. Try system tenant settings if not already at system level
|
|
|
|
|
+ if (!modelId && effectiveTenantId !== systemId) {
|
|
|
|
|
+ const systemSettings = await this.tenantService.getSettings(systemId);
|
|
|
|
|
+ if (systemSettings) {
|
|
|
|
|
+ modelId = extractModelId(systemSettings);
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // b. Try global default flag in ModelConfig table
|
|
|
|
|
+ if (!modelId) {
|
|
|
|
|
+ const globalDefault = await this.modelConfigRepository.findOne({
|
|
|
|
|
+ where: { type, isDefault: true, isEnabled: true },
|
|
|
|
|
+ order: { updatedAt: 'DESC' },
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (globalDefault) {
|
|
|
|
|
+ this.logger.log(`Using global default model for type "${type}": ${globalDefault.name}`);
|
|
|
|
|
+ return globalDefault;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // 5. Final validation
|
|
|
if (!modelId) {
|
|
if (!modelId) {
|
|
|
- throw new BadRequestException(`Model of type "${type}" is not configured in Index Chat Config for this organization.`);
|
|
|
|
|
|
|
+ const errorMsg = strict
|
|
|
|
|
+ ? `Model of type "${type}" is not configured for organization "${tenantId || 'system'}". Please select a model in "Index Chat Config".`
|
|
|
|
|
+ : `Model of type "${type}" is not configured in Index Chat Config for organization "${tenantId || 'system'}". Please set the default model in Admin Settings.`;
|
|
|
|
|
+
|
|
|
|
|
+ throw new BadRequestException(errorMsg);
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
const model = await this.modelConfigRepository.findOne({
|
|
const model = await this.modelConfigRepository.findOne({
|
|
|
- where: { id: modelId, isEnabled: true }
|
|
|
|
|
|
|
+ where: { id: modelId, isEnabled: true },
|
|
|
});
|
|
});
|
|
|
|
|
|
|
|
if (!model) {
|
|
if (!model) {
|
|
|
- throw new BadRequestException(`The configured model for "${type}" (ID: ${modelId}) is either missing or disabled in model management.`);
|
|
|
|
|
|
|
+ // If strict mode, don't allow fallback to ANY model
|
|
|
|
|
+ if (strict) {
|
|
|
|
|
+ throw new BadRequestException(
|
|
|
|
|
+ `The configured model (ID: ${modelId}) for type "${type}" for organization "${tenantId || 'system'}" is missing or disabled.`,
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ // If the specific configured model is missing, try to find ANY enabled model of that type
|
|
|
|
|
+ const fallbackModel = await this.modelConfigRepository.findOne({
|
|
|
|
|
+ where: { type, isEnabled: true },
|
|
|
|
|
+ order: { isDefault: 'DESC', updatedAt: 'DESC' },
|
|
|
|
|
+ });
|
|
|
|
|
+
|
|
|
|
|
+ if (!fallbackModel) {
|
|
|
|
|
+ throw new BadRequestException(
|
|
|
|
|
+ `No enabled model of type "${type}" found in the system.`,
|
|
|
|
|
+ );
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ this.logger.warn(
|
|
|
|
|
+ `Configured model (ID: ${modelId}) for type "${type}" is missing or disabled. Falling back to: ${fallbackModel.name}`,
|
|
|
|
|
+ );
|
|
|
|
|
+ return fallbackModel;
|
|
|
}
|
|
}
|
|
|
|
|
|
|
|
return model;
|
|
return model;
|