feat: add two-layer AI model availability filtering (#18170)
## Summary - **Admin-level filtering**: New AI tab in admin panel with server-wide model availability controls (whitelist/blacklist via `AI_AUTO_ENABLE_NEW_MODELS`, `AI_DISABLED_MODEL_IDS`, `AI_ENABLED_MODEL_IDS` config variables). Dedicated `setAdminAiModelEnabled` mutation replaces frontend config-variable manipulation. Filter dropdown to show/hide unconfigured and deprecated models. - **Workspace-level filtering**: Per-workspace controls with "Use best models only" mode (curated list backed by `isRecommended` flag), or custom whitelist/blacklist. Separate Smart/Fast model selectors with "Best (...)" virtual options. - **Security enforcement**: Both layers enforced at every backend execution point — workspace update, agent create/update, chat execution. Model ID validated against known models before config mutation. All admin endpoints protected by `AdminPanelGuard`. ## Changes ### Backend (`twenty-server`) - New config variables for admin-level model filtering - `AiModelRegistryService`: `getAllModelsWithStatus()`, `setModelAdminEnabled()` with model ID validation, `isModelAdminAllowed()` - `AdminPanelResolver`: `getAdminAiModels` query, `setAdminAiModelEnabled` mutation - `WorkspaceEntity`: new fields (`autoEnableNewAiModels`, `disabledAiModelIds`, `enabledAiModelIds`, `useRecommendedModels`) - `WorkspaceService`: model validation on `smartModel`/`fastModel` updates - `AgentResolver`: model availability checks on create/update - `isModelAllowedByWorkspace` centralized utility - `isRecommended` flag on model definitions - Two TypeORM migrations ### Frontend (`twenty-front`) - New `SettingsAdminAI` component with search, filter dropdown (unconfigured/deprecated), and model toggle cards - AI tab added to admin panel navigation - `useWorkspaceAiModelAvailability` hook for workspace-level filtering - `SettingsAIModelsTab` redesigned: merged sections, "Use best models only" toggle, conditional available models list - `getModelIcon`/`getModelProviderLabel` shared utilities with GraphQL enum casing normalization - Updated generated GraphQL types and mock data ## Test plan - [ ] Toggle models on/off in admin panel AI tab and verify they appear/disappear in workspace settings - [ ] Enable "Use best models only" in workspace settings and verify only recommended models are selectable - [ ] Disable recommended mode and verify whitelist/blacklist toggles work correctly - [ ] Verify deprecated models hidden by default, shown greyed out when filter enabled - [ ] Verify unconfigured models hidden by default, shown disabled when filter enabled - [ ] Try setting a disabled model as Smart/Fast model — should be rejected - [ ] Try creating an agent with a disabled model — should be rejected - [ ] Verify admin panel AI tab requires admin access Made with [Cursor](https://cursor.com) --------- Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -7,6 +7,7 @@ import { PermissionFlagType } from 'twenty-shared/constants';
|
||||
import { AdminPanelHealthService } from 'src/engine/core-modules/admin-panel/admin-panel-health.service';
|
||||
import { AdminPanelQueueService } from 'src/engine/core-modules/admin-panel/admin-panel-queue.service';
|
||||
import { AdminPanelService } from 'src/engine/core-modules/admin-panel/admin-panel.service';
|
||||
import { AdminAIModelsOutput } from 'src/engine/core-modules/client-config/client-config.entity';
|
||||
import { ConfigVariableDTO } from 'src/engine/core-modules/admin-panel/dtos/config-variable.dto';
|
||||
import { ConfigVariablesOutput } from 'src/engine/core-modules/admin-panel/dtos/config-variables.output';
|
||||
import { DeleteJobsResponseDTO } from 'src/engine/core-modules/admin-panel/dtos/delete-jobs-response.dto';
|
||||
@@ -30,6 +31,7 @@ import { type MessageQueue } from 'src/engine/core-modules/message-queue/message
|
||||
import { type ConfigVariables } from 'src/engine/core-modules/twenty-config/config-variables';
|
||||
import { ConfigVariableGraphqlApiExceptionFilter } from 'src/engine/core-modules/twenty-config/filters/config-variable-graphql-api-exception.filter';
|
||||
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
|
||||
import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
|
||||
import { MetadataResolver } from 'src/engine/api/graphql/graphql-config/decorators/metadata-resolver.decorator';
|
||||
import { AdminPanelGuard } from 'src/engine/guards/admin-panel-guard';
|
||||
import { ServerLevelImpersonateGuard } from 'src/engine/guards/server-level-impersonate.guard';
|
||||
@@ -59,6 +61,7 @@ export class AdminPanelResolver {
|
||||
private adminPanelQueueService: AdminPanelQueueService,
|
||||
private featureFlagService: FeatureFlagService,
|
||||
private readonly twentyConfigService: TwentyConfigService,
|
||||
private readonly aiModelRegistryService: AiModelRegistryService,
|
||||
) {}
|
||||
|
||||
@UseGuards(ServerLevelImpersonateGuard)
|
||||
@@ -138,6 +141,41 @@ export class AdminPanelResolver {
|
||||
return this.adminService.getVersionInfo();
|
||||
}
|
||||
|
||||
@UseGuards(AdminPanelGuard)
|
||||
@Query(() => AdminAIModelsOutput)
|
||||
async getAdminAiModels(): Promise<AdminAIModelsOutput> {
|
||||
const models = this.aiModelRegistryService
|
||||
.getAllModelsWithStatus()
|
||||
.map(({ modelConfig, isAvailable, isAdminEnabled }) => ({
|
||||
modelId: modelConfig.modelId,
|
||||
label: modelConfig.label,
|
||||
modelFamily: modelConfig.modelFamily,
|
||||
inferenceProvider: modelConfig.inferenceProvider,
|
||||
isAvailable,
|
||||
isAdminEnabled,
|
||||
deprecated: modelConfig.deprecated,
|
||||
isRecommended: modelConfig.isRecommended,
|
||||
}));
|
||||
|
||||
return {
|
||||
autoEnableNewModels: this.twentyConfigService.get(
|
||||
'AI_AUTO_ENABLE_NEW_MODELS',
|
||||
),
|
||||
models,
|
||||
};
|
||||
}
|
||||
|
||||
@UseGuards(AdminPanelGuard)
|
||||
@Mutation(() => Boolean)
|
||||
async setAdminAiModelEnabled(
|
||||
@Args('modelId', { type: () => String }) modelId: string,
|
||||
@Args('enabled', { type: () => Boolean }) enabled: boolean,
|
||||
): Promise<boolean> {
|
||||
await this.aiModelRegistryService.setModelAdminEnabled(modelId, enabled);
|
||||
|
||||
return true;
|
||||
}
|
||||
|
||||
@UseGuards(AdminPanelGuard)
|
||||
@Query(() => ConfigVariableDTO)
|
||||
async getDatabaseConfigVariable(
|
||||
|
||||
Reference in New Issue
Block a user