Просмотр исходного кода

模型属于admin全局,删除租户id和用户id

anhuiqiang 10 часов назад
Родитель
Сommit
2502215500

+ 4 - 2
server/src/admin/admin.service.ts

@@ -95,11 +95,13 @@ export class AdminService {
     }
 
     async getTenantSettings(tenantId: string) {
-        return this.tenantService.getSettings(tenantId);
+        const targetTenantId = tenantId || await this.tenantService.getSystemTenantId();
+        return this.tenantService.getSettings(targetTenantId);
     }
 
     async updateTenantSettings(tenantId: string, data: any) {
-        return this.tenantService.updateSettings(tenantId, data);
+        const targetTenantId = tenantId || await this.tenantService.getSystemTenantId();
+        return this.tenantService.updateSettings(targetTenantId, data);
     }
 
     // Notebook sharing approval and model assignments would go here

+ 1 - 1
server/src/api/api-v1.controller.ts

@@ -63,7 +63,7 @@ export class ApiV1Controller {
         // Get organization settings and model configuration
         const tenantSettings = await this.tenantService.getSettings(user.tenantId);
         const userSetting = await this.userSettingService.getByUser(user.id);
-        const models = await this.modelConfigService.findAll(user.id, user.tenantId);
+        const models = await this.modelConfigService.findAll();
         const llmModel = models.find((m) => m.id === tenantSettings?.selectedLLMId) ?? models.find((m) => m.type === 'llm' && m.isDefault);
 
         if (!llmModel) {

+ 1 - 1
server/src/api/api.controller.ts

@@ -41,7 +41,7 @@ export class ApiController {
 
     try {
       // ユーザーの LLM モデル設定を取得
-      const models = await this.modelConfigService.findAll(req.user.id, req.user.tenantId);
+      const models = await this.modelConfigService.findAll();
       const llmModel = models.find((m) => m.type === 'llm');
       if (!llmModel) {
         throw new Error(this.i18nService.getMessage('addLLMConfig'));

+ 2 - 2
server/src/chat/chat.controller.ts

@@ -74,7 +74,7 @@ export class ChatController {
       const tenantId = req.user.tenantId;
 
       // 获取用户的LLM模型配置
-      let models = await this.modelConfigService.findAll(userId, tenantId);
+      let models = await this.modelConfigService.findAll();
 
       if (role !== 'SUPER_ADMIN') {
         const tenantSettings = await this.tenantService.getSettings(tenantId);
@@ -86,7 +86,7 @@ export class ChatController {
       let llmModel;
       if (selectedLLMId) {
         // Find specifically selected model
-        llmModel = await this.modelConfigService.findOne(selectedLLMId, userId, tenantId);
+        llmModel = await this.modelConfigService.findOne(selectedLLMId);
         console.log('使用选中的LLM模型:', llmModel.name);
       } else {
         // Use organization's default LLM from Index Chat Config (strict)

+ 1 - 1
server/src/chat/chat.service.ts

@@ -115,7 +115,7 @@ export class ChatService {
 
       if (selectedEmbeddingId) {
         // Find specifically selected model
-        embeddingModel = await this.modelConfigService.findOne(selectedEmbeddingId, userId, tenantId || 'default');
+        embeddingModel = await this.modelConfigService.findOne(selectedEmbeddingId);
       } else {
         // Use organization's default from Index Chat Config (strict)
         embeddingModel = await this.modelConfigService.findDefaultByType(tenantId || 'default', ModelType.EMBEDDING);

+ 1 - 7
server/src/i18n/messages.ts

@@ -73,8 +73,6 @@ export const errorMessages = {
     sourcePathNotFound: '源路径不存在: {path}',
     targetGroupRequired: '未指定目标分组',
     modelConfigNotFound: '找不到模型配置: {id}',
-    cannotUpdateOtherTenantModel: '无法更新其他租户的模型',
-    cannotDeleteOtherTenantModel: '无法删除其他租户的模型',
     elasticsearchHostRequired: 'ELASTICSEARCH_HOST 环境变量未设置',
   },
   ja: {
@@ -151,8 +149,6 @@ export const errorMessages = {
     sourcePathNotFound: 'ソースパスが見つかりません: {path}',
     targetGroupRequired: 'ターゲットグループが指定されていません',
     modelConfigNotFound: 'モデル設定が見つかりません: {id}',
-    cannotUpdateOtherTenantModel: '他のテナントのモデルは更新できません',
-    cannotDeleteOtherTenantModel: '他のテナントのモデルは削除できません',
     elasticsearchHostRequired: 'ELASTICSEARCH_HOST 環境変数が設定されていません',
   },
   en: {
@@ -228,9 +224,7 @@ export const errorMessages = {
     sourcePathNotFound: 'Source path not found: {path}',
     targetGroupRequired: 'Target group not specified',
     modelConfigNotFound: 'Model config not found: {id}',
-    cannotUpdateOtherTenantModel: 'Cannot update models from another tenant',
-    cannotDeleteOtherTenantModel: 'Cannot delete models from another tenant',
-    elasticsearchHostRequired: 'ELASTICSEARCH_HOST environment variable is not set',
+    elasticsearchHostRequired: 'Elasticsearch host environment variable is not set',
     libreofficeUrlRequired: 'LIBREOFFICE_URL environment variable is required but not set',
     pdfToImageConversionFailed: 'PDF to image conversion failed. No images were generated.',
     pdfPageCountError: 'Could not get PDF page count',

+ 8 - 17
server/src/knowledge-base/chunk-config.service.ts

@@ -65,14 +65,14 @@ export class ChunkConfigService {
   /**
    * Get model limit settings (read from database)
    */
-  async getModelLimits(modelId: string, userId: string, tenantId?: string): Promise<{
+  async getModelLimits(modelId: string): Promise<{
     maxInputTokens: number;
     maxBatchSize: number;
     expectedDimensions: number;
     providerName: string;
     isVectorModel: boolean;
   }> {
-    const modelConfig = await this.modelConfigService.findOne(modelId, userId, tenantId || '');
+    const modelConfig = await this.modelConfigService.findOne(modelId);
 
     if (!modelConfig || modelConfig.type !== 'embedding') {
       throw new BadRequestException(this.i18nService.formatMessage('embeddingModelNotFound', { id: modelId }));
@@ -111,8 +111,6 @@ export class ChunkConfigService {
     chunkSize: number,
     chunkOverlap: number,
     modelId: string,
-    userId: string,
-    tenantId?: string,
   ): Promise<{
     chunkSize: number;
     chunkOverlap: number;
@@ -121,7 +119,7 @@ export class ChunkConfigService {
     effectiveMaxOverlapSize: number;
   }> {
     const warnings: string[] = [];
-    const limits = await this.getModelLimits(modelId, userId, tenantId);
+    const limits = await this.getModelLimits(modelId);
 
     // 1. Calculate final limits (choose smaller of env var and model limit)
     const effectiveMaxChunkSize = Math.min(
@@ -238,11 +236,9 @@ export class ChunkConfigService {
    */
   async getRecommendedBatchSize(
     modelId: string,
-    userId: string,
-    tenantId?: string,
     currentBatchSize: number = 100,
   ): Promise<number> {
-    const limits = await this.getModelLimits(modelId, userId, tenantId);
+    const limits = await this.getModelLimits(modelId);
 
     // Choose smaller of configured value and model limit
     const recommended = Math.min(
@@ -277,11 +273,9 @@ export class ChunkConfigService {
    */
   async validateDimensions(
     modelId: string,
-    userId: string,
     actualDimensions: number,
-    tenantId?: string,
   ): Promise<boolean> {
-    const limits = await this.getModelLimits(modelId, userId, tenantId);
+    const limits = await this.getModelLimits(modelId);
 
     if (actualDimensions !== limits.expectedDimensions) {
       this.logger.warn(
@@ -304,10 +298,8 @@ export class ChunkConfigService {
     chunkSize: number,
     chunkOverlap: number,
     modelId: string,
-    userId: string,
-    tenantId?: string,
   ): Promise<string> {
-    const limits = await this.getModelLimits(modelId, userId, tenantId);
+    const limits = await this.getModelLimits(modelId);
 
     return [
       `Model: ${modelId}`,
@@ -324,7 +316,6 @@ export class ChunkConfigService {
    */
   async getFrontendLimits(
     modelId: string,
-    userId: string,
     tenantId?: string,
   ): Promise<{
     maxChunkSize: number;
@@ -339,7 +330,7 @@ export class ChunkConfigService {
       expectedDimensions: number;
     };
   }> {
-    const limits = await this.getModelLimits(modelId, userId, tenantId);
+    const limits = await this.getModelLimits(modelId);
 
     // Calculate final limits (choose smaller of env var and model limit)
     const maxChunkSize = Math.min(this.envMaxChunkSize, limits.maxInputTokens);
@@ -349,7 +340,7 @@ export class ChunkConfigService {
     );
 
     // Get model config name
-    const modelConfig = await this.modelConfigService.findOne(modelId, userId, tenantId || '');
+    const modelConfig = await this.modelConfigService.findOne(modelId);
     const modelName = modelConfig?.name || 'Unknown';
 
     // Get defaults from tenant or user settings

+ 0 - 2
server/src/knowledge-base/embedding.service.ts

@@ -41,8 +41,6 @@ export class EmbeddingService {
 
     const modelConfig = await this.modelConfigService.findOne(
       embeddingModelConfigId,
-      userId,
-      tenantId || 'default',
     );
     if (!modelConfig || modelConfig.type !== 'embedding') {
       throw new Error(this.i18nService.formatMessage('embeddingModelNotFound', { id: embeddingModelConfigId }));

+ 0 - 1
server/src/knowledge-base/knowledge-base.controller.ts

@@ -129,7 +129,6 @@ export class KnowledgeBaseController {
 
     return await this.chunkConfigService.getFrontendLimits(
       embeddingModelId,
-      req.user.id,
       req.user.tenantId,
     );
   }

+ 12 - 12
server/src/knowledge-base/knowledge-base.service.ts

@@ -23,6 +23,7 @@ import { Pdf2ImageService } from '../pdf2image/pdf2image.service';
 import { DOC_EXTENSIONS, IMAGE_EXTENSIONS } from '../common/file-support.constants';
 import { ChatService } from '../chat/chat.service';
 import { UserSettingService } from '../user/user-setting.service';
+import { ModelType } from '../types';
 
 @Injectable()
 export class KnowledgeBaseService {
@@ -64,6 +65,15 @@ export class KnowledgeBaseService {
     const mode = config?.mode || 'fast';
     const processingMode = mode === 'precise' ? ProcessingMode.PRECISE : ProcessingMode.FAST;
 
+    // Strict validation: Ensure tenant has configured an embedding model in "Index Chat Config"
+    // If config.embeddingModelId is specifically passed (e.g. from a task), we use it.
+    // Otherwise, we lookup the tenant's default embedding model in strict mode.
+    let embeddingModelId = config?.embeddingModelId;
+    if (!embeddingModelId) {
+      const defaultEmbedding = await this.modelConfigService.findDefaultByType(tenantId, ModelType.EMBEDDING, true);
+      embeddingModelId = defaultEmbedding.id;
+    }
+
     const kb = this.kbRepository.create({
       originalName: fileInfo.originalname,
       storagePath: fileInfo.path,
@@ -74,7 +84,7 @@ export class KnowledgeBaseService {
       tenantId: tenantId,
       chunkSize: config?.chunkSize || 200,
       chunkOverlap: config?.chunkOverlap || 40,
-      embeddingModelId: config?.embeddingModelId || null,
+      embeddingModelId: embeddingModelId,
       processingMode: processingMode,
     });
 
@@ -381,8 +391,6 @@ export class KnowledgeBaseService {
       if (visionModelId) {
         const visionModel = await this.modelConfigService.findOne(
           visionModelId,
-          userId,
-          tenantId,
         );
         if (visionModel && visionModel.type === 'vision' && visionModel.isEnabled !== false) {
           text = await this.visionService.extractImageContent(kb.storagePath, {
@@ -451,8 +459,6 @@ export class KnowledgeBaseService {
 
     const visionModel = await this.modelConfigService.findOne(
       visionModelId,
-      userId,
-      tenantId,
     );
     if (!visionModel || visionModel.type !== 'vision' || visionModel.isEnabled === false) {
       this.logger.warn(
@@ -644,7 +650,6 @@ export class KnowledgeBaseService {
         kb.chunkSize,
         kb.chunkOverlap,
         kb.embeddingModelId,
-        userId,
       );
       this.logger.debug(`File ${kbId}: Chunk config validated.`);
 
@@ -670,7 +675,6 @@ export class KnowledgeBaseService {
         validatedConfig.chunkSize,
         validatedConfig.chunkOverlap,
         kb.embeddingModelId,
-        userId,
       );
       this.logger.log(`Chunk config: ${configSummary}`);
       this.logger.log(`Config limits: chunk=${validatedConfig.effectiveMaxChunkSize}, overlap=${validatedConfig.effectiveMaxOverlapSize}`);
@@ -703,8 +707,6 @@ export class KnowledgeBaseService {
       // 4. Get recommended batch size (based on model limits)
       const recommendedBatchSize = await this.chunkConfigService.getRecommendedBatchSize(
         kb.embeddingModelId,
-        userId,
-        tenantId,
         parseInt(process.env.CHUNK_BATCH_SIZE || '100'),
       );
 
@@ -1364,8 +1366,6 @@ export class KnowledgeBaseService {
       // 1. Prioritize getting from model config
       const modelConfig = await this.modelConfigService.findOne(
         embeddingModelId,
-        userId,
-        tenantId,
       );
 
       if (modelConfig && modelConfig.dimensions) {
@@ -1388,7 +1388,7 @@ export class KnowledgeBaseService {
         // Update model config for next use
         if (modelConfig) {
           try {
-            await this.modelConfigService.update(userId, tenantId, modelConfig.id, {
+            await this.modelConfigService.update(modelConfig.id, {
               dimensions: actualDimensions,
             });
             this.logger.log(`Updated model ${modelConfig.name} dimension config to ${actualDimensions}`);

+ 0 - 1
server/src/model-config/dto/model-config-response.dto.ts

@@ -15,7 +15,6 @@ export class ModelConfigResponseDto {
   type: string;
   isEnabled?: boolean;
   isDefault?: boolean;
-  userId: string;
   createdAt: Date;
   updatedAt: Date;
 

+ 6 - 14
server/src/model-config/model-config.controller.ts

@@ -36,25 +36,22 @@ export class ModelConfigController {
     @Body() createModelConfigDto: CreateModelConfigDto,
   ): Promise<ModelConfigResponseDto> {
     const modelConfig = await this.modelConfigService.create(
-      req.user.id,
-      req.user.tenantId,
       createModelConfigDto,
     );
     return plainToClass(ModelConfigResponseDto, modelConfig);
   }
 
   @Get()
-  async findAll(@Req() req): Promise<ModelConfigResponseDto[]> {
-    const modelConfigs = await this.modelConfigService.findAll(req.user.id, req.user.tenantId);
+  async findAll(): Promise<ModelConfigResponseDto[]> {
+    const modelConfigs = await this.modelConfigService.findAll();
     return modelConfigs.map((mc) => plainToClass(ModelConfigResponseDto, mc));
   }
 
   @Get(':id')
   async findOne(
-    @Req() req,
     @Param('id') id: string,
   ): Promise<ModelConfigResponseDto> {
-    const modelConfig = await this.modelConfigService.findOne(id, req.user.id, req.user.tenantId);
+    const modelConfig = await this.modelConfigService.findOne(id);
     return plainToClass(ModelConfigResponseDto, modelConfig);
   }
 
@@ -66,8 +63,6 @@ export class ModelConfigController {
     @Body() updateModelConfigDto: UpdateModelConfigDto,
   ): Promise<ModelConfigResponseDto> {
     const modelConfig = await this.modelConfigService.update(
-      req.user.id,
-      req.user.tenantId,
       id,
       updateModelConfigDto,
     );
@@ -77,19 +72,16 @@ export class ModelConfigController {
   @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
   @Delete(':id')
   @HttpCode(HttpStatus.NO_CONTENT)
-  async remove(@Req() req, @Param('id') id: string): Promise<void> {
-    await this.modelConfigService.remove(req.user.id, req.user.tenantId, id);
+  async remove(@Param('id') id: string): Promise<void> {
+    await this.modelConfigService.remove(id);
   }
 
   @Roles(UserRole.TENANT_ADMIN, UserRole.SUPER_ADMIN)
   @Patch(':id/set-default')
   async setDefault(
-    @Req() req,
     @Param('id') id: string,
   ): Promise<ModelConfigResponseDto> {
-    const userId = req.user.id;
-    const tenantId = req.user.tenantId;
-    const modelConfig = await this.modelConfigService.setDefault(userId, tenantId, id);
+    const modelConfig = await this.modelConfigService.setDefault(id);
     return plainToClass(ModelConfigResponseDto, modelConfig);
   }
 }

+ 0 - 20
server/src/model-config/model-config.entity.ts

@@ -3,12 +3,9 @@ import {
   Column,
   CreateDateColumn,
   Entity,
-  JoinColumn,
-  ManyToOne,
   PrimaryGeneratedColumn,
   UpdateDateColumn,
 } from 'typeorm';
-import { User } from '../user/user.entity';
 
 @Entity('model_configs')
 export class ModelConfig {
@@ -77,23 +74,6 @@ export class ModelConfig {
   @Column({ type: 'text', nullable: true })
   providerName?: string;
 
-  // ==================== Existing Fields ====================
-
-  @Column({ type: 'text', nullable: true })
-  userId: string;
-
-  // null = global/system model (visible to all tenants)
-  // set = private to this tenant
-  @Column({ type: 'text', nullable: true, name: 'tenant_id' })
-  tenantId: string;
-
-  @ManyToOne(() => User, (user) => user.modelConfigs, {
-    onDelete: 'CASCADE',
-    nullable: true,
-  })
-  @JoinColumn({ name: 'userId' })
-  user: User | null;
-
   @CreateDateColumn({ name: 'created_at' })
   createdAt: Date;
 

+ 134 - 65
server/src/model-config/model-config.service.ts

@@ -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 { Repository } from 'typeorm';
 import { ModelConfig } from './model-config.entity';
 import { CreateModelConfigDto } from './dto/create-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 { ModelType } from '../types';
 import { I18nService } from '../i18n/i18n.service';
 
 @Injectable()
-export class ModelConfigService {
+export class ModelConfigService implements OnModuleInit {
+  private readonly logger = new Logger(ModelConfigService.name);
   constructor(
     @InjectRepository(ModelConfig)
     private modelConfigRepository: Repository<ModelConfig>,
@@ -19,62 +19,73 @@ export class ModelConfigService {
     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(
-    userId: string,
-    tenantId: string,
     createModelConfigDto: CreateModelConfigDto,
   ): 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);
   }
 
-  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) {
       throw new NotFoundException(
         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;
   }
 
-  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(
-    userId: string,
-    tenantId: string,
     id: string,
     updateModelConfigDto: UpdateModelConfigDto,
   ): Promise<ModelConfig> {
-    const modelConfig = await this.findOne(id, userId, tenantId);
+    const modelConfig = await this.findOne(id);
 
     if (!modelConfig) {
       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
@@ -95,12 +108,9 @@ export class ModelConfigService {
     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 });
     if (result.affected === 0) {
       throw new NotFoundException(this.i18nService.formatMessage('modelConfigNotFound', { id }));
@@ -110,19 +120,15 @@ export class ModelConfigService {
   /**
    * 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
       .createQueryBuilder()
       .update(ModelConfig)
       .set({ isDefault: false })
       .where('type = :type', { type: modelConfig.type })
-      .andWhere('(tenantId = :tenantId OR tenantId IS NULL OR tenantId = :globalTenantId)', {
-        tenantId,
-        globalTenantId: GLOBAL_TENANT_ID
-      })
       .execute();
 
     modelConfig.isDefault = true;
@@ -133,31 +139,94 @@ export class ModelConfigService {
    * Get default model for specified type
    * 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;
-    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) {
-      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({
-      where: { id: modelId, isEnabled: true }
+      where: { id: modelId, isEnabled: true },
     });
 
     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;

+ 1 - 1
server/src/rag/rag.service.ts

@@ -345,7 +345,7 @@ ${answerHeader}`;
    */
   private async getInternalLlm(userId: string, tenantId: string): Promise<ChatOpenAI | null> {
     try {
-      const models = await this.modelConfigService.findAll(userId, tenantId || 'default');
+      const models = await this.modelConfigService.findAll();
       const defaultLlm = models.find(m => m.type === 'llm' && m.isDefault && m.isEnabled !== false);
 
       if (!defaultLlm) {

+ 1 - 1
server/src/rag/rerank.service.ts

@@ -42,7 +42,7 @@ export class RerankService {
         let modelConfig;
         try {
             // 1. Get model config
-            modelConfig = await this.modelConfigService.findOne(rerankModelId, userId, tenantId || 'default');
+            modelConfig = await this.modelConfigService.findOne(rerankModelId);
 
             if (!modelConfig || modelConfig.type !== ModelType.RERANK) {
                 this.logger.warn(`Invalid rerank model config: ${rerankModelId}`);

+ 8 - 0
server/src/tenant/tenant.service.ts

@@ -158,4 +158,12 @@ export class TenantService {
         });
         return members.map(m => m.userId);
     }
+
+    /**
+     * Get the ID of the system/default tenant
+     */
+    async getSystemTenantId(): Promise<string> {
+        const defaultTenant = await this.ensureDefaultTenant();
+        return defaultTenant.id;
+    }
 }

+ 0 - 4
server/src/user/user.entity.ts

@@ -11,7 +11,6 @@ import {
   UpdateDateColumn,
 } from 'typeorm';
 import * as bcrypt from 'bcrypt';
-import { ModelConfig } from '../model-config/model-config.entity';
 import { Tenant } from '../tenant/tenant.entity';
 import { TenantMember } from '../tenant/tenant-member.entity';
 import { ApiKey } from '../auth/entities/api-key.entity';
@@ -66,9 +65,6 @@ export class User {
   @UpdateDateColumn({ name: 'updated_at' })
   updatedAt: Date;
 
-  @OneToMany(() => ModelConfig, (modelConfig) => modelConfig.user)
-  modelConfigs: ModelConfig[];
-
   @OneToOne(() => UserSetting, (setting) => setting.user)
   userSetting: UserSetting;
 

+ 1 - 1
server/src/vision-pipeline/vision-pipeline-cost-aware.service.ts

@@ -187,7 +187,7 @@ export class VisionPipelineCostAwareService {
    * Get Vision model configuration
    */
   private async getVisionModelConfig(userId: string, modelId: string, tenantId?: string): Promise<VisionModelConfig> {
-    const config = await this.modelConfigService.findOne(modelId, userId, tenantId || 'default');
+    const config = await this.modelConfigService.findOne(modelId);
 
     if (!config) {
       throw new Error(`Model config not found: ${modelId}`);

+ 1 - 1
server/src/vision-pipeline/vision-pipeline.service.ts

@@ -209,7 +209,7 @@ export class VisionPipelineService {
     modelId: string,
     tenantId?: string,
   ): Promise<VisionModelConfig> {
-    const config = await this.modelConfigService.findOne(modelId, userId, tenantId || 'default');
+    const config = await this.modelConfigService.findOne(modelId);
 
     if (!config) {
       throw new Error(`Model configuration not found: ${modelId}`);