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:
@@ -58,6 +58,45 @@ export class ClientAIModelConfig {
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
deprecated?: boolean;
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
isRecommended?: boolean;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class AdminAIModelConfig {
|
||||
@Field(() => String)
|
||||
modelId: string;
|
||||
|
||||
@Field(() => String)
|
||||
label: string;
|
||||
|
||||
@Field(() => ModelFamily, { nullable: true })
|
||||
modelFamily?: ModelFamily;
|
||||
|
||||
@Field(() => InferenceProvider)
|
||||
inferenceProvider: InferenceProvider;
|
||||
|
||||
@Field(() => Boolean)
|
||||
isAvailable: boolean;
|
||||
|
||||
@Field(() => Boolean)
|
||||
isAdminEnabled: boolean;
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
deprecated?: boolean;
|
||||
|
||||
@Field(() => Boolean, { nullable: true })
|
||||
isRecommended?: boolean;
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
export class AdminAIModelsOutput {
|
||||
@Field(() => Boolean)
|
||||
autoEnableNewModels: boolean;
|
||||
|
||||
@Field(() => [AdminAIModelConfig])
|
||||
models: AdminAIModelConfig[];
|
||||
}
|
||||
|
||||
@ObjectType()
|
||||
|
||||
+5
-3
@@ -43,7 +43,8 @@ export class ClientConfigService {
|
||||
'CALENDAR_BOOKING_PAGE_ID',
|
||||
);
|
||||
|
||||
const availableModels = this.aiModelRegistryService.getAvailableModels();
|
||||
const availableModels =
|
||||
this.aiModelRegistryService.getAdminFilteredModels();
|
||||
|
||||
const aiModels: ClientAIModelConfig[] = availableModels.map(
|
||||
(registeredModel) => {
|
||||
@@ -68,6 +69,7 @@ export class ClientConfigService {
|
||||
)
|
||||
: 0,
|
||||
deprecated: builtInModel?.deprecated,
|
||||
isRecommended: builtInModel?.isRecommended,
|
||||
};
|
||||
},
|
||||
);
|
||||
@@ -96,14 +98,14 @@ export class ClientConfigService {
|
||||
aiModels.unshift(
|
||||
{
|
||||
modelId: DEFAULT_SMART_MODEL,
|
||||
label: `Smart (${defaultPerformanceModelLabel})`,
|
||||
label: `Best (${defaultPerformanceModelLabel})`,
|
||||
inferenceProvider: InferenceProvider.NONE,
|
||||
inputCostPerMillionTokensInCredits: 0,
|
||||
outputCostPerMillionTokensInCredits: 0,
|
||||
},
|
||||
{
|
||||
modelId: DEFAULT_FAST_MODEL,
|
||||
label: `Fast (${defaultSpeedModelLabel})`,
|
||||
label: `Best (${defaultSpeedModelLabel})`,
|
||||
inferenceProvider: InferenceProvider.NONE,
|
||||
inputCostPerMillionTokensInCredits: 0,
|
||||
outputCostPerMillionTokensInCredits: 0,
|
||||
|
||||
Reference in New Issue
Block a user