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:
Félix Malfait
2026-02-24 10:14:24 +01:00
committed by GitHub
parent c369baf63a
commit 9a3852bf04
41 changed files with 1320 additions and 101 deletions
@@ -4,7 +4,12 @@ import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAd
import { SETTINGS_ADMIN_TABS_ID } from '@/settings/admin-panel/constants/SettingsAdminTabsId';
import { TabList } from '@/ui/layout/tab-list/components/TabList';
import { t } from '@lingui/core/macro';
import { IconHeart, IconSettings2, IconVariable } from 'twenty-ui/display';
import {
IconHeart,
IconSettings2,
IconSparkles,
IconVariable,
} from 'twenty-ui/display';
import { useRecoilValueV2 } from '@/ui/utilities/state/jotai/hooks/useRecoilValueV2';
export const SettingsAdminContent = () => {
@@ -19,15 +24,21 @@ export const SettingsAdminContent = () => {
Icon: IconSettings2,
disabled: !canAccessFullAdminPanel && !canImpersonate,
},
{
id: SETTINGS_ADMIN_TABS.AI,
title: t`AI`,
Icon: IconSparkles,
disabled: !canAccessFullAdminPanel,
},
{
id: SETTINGS_ADMIN_TABS.CONFIG_VARIABLES,
title: t`Config Variables`,
title: t`Config`,
Icon: IconVariable,
disabled: !canAccessFullAdminPanel,
},
{
id: SETTINGS_ADMIN_TABS.HEALTH_STATUS,
title: t`Health Status`,
title: t`Health`,
Icon: IconHeart,
disabled: !canAccessFullAdminPanel,
},
@@ -1,3 +1,4 @@
import { SettingsAdminAI } from '@/settings/admin-panel/ai/components/SettingsAdminAI';
import { SettingsAdminGeneral } from '@/settings/admin-panel/components/SettingsAdminGeneral';
import { SettingsAdminConfigVariables } from '@/settings/admin-panel/config-variables/components/SettingsAdminConfigVariables';
import { SETTINGS_ADMIN_TABS } from '@/settings/admin-panel/constants/SettingsAdminTabs';
@@ -15,6 +16,8 @@ export const SettingsAdminTabContent = () => {
switch (activeTabId) {
case SETTINGS_ADMIN_TABS.GENERAL:
return <SettingsAdminGeneral />;
case SETTINGS_ADMIN_TABS.AI:
return <SettingsAdminAI />;
case SETTINGS_ADMIN_TABS.CONFIG_VARIABLES:
return <SettingsAdminConfigVariables />;
case SETTINGS_ADMIN_TABS.HEALTH_STATUS: