diff --git a/packages/twenty-front/src/generated-metadata/graphql.ts b/packages/twenty-front/src/generated-metadata/graphql.ts index 77623999a0..a13af5b05f 100644 --- a/packages/twenty-front/src/generated-metadata/graphql.ts +++ b/packages/twenty-front/src/generated-metadata/graphql.ts @@ -571,6 +571,7 @@ export type CheckUserExistOutput = { export type ClientAiModelConfig = { __typename?: 'ClientAIModelConfig'; + deprecated?: Maybe; inputCostPer1kTokensInCredits: Scalars['Float']; label: Scalars['String']; modelId: Scalars['String']; diff --git a/packages/twenty-front/src/generated/graphql.ts b/packages/twenty-front/src/generated/graphql.ts index 444684dea7..c1774b5967 100644 --- a/packages/twenty-front/src/generated/graphql.ts +++ b/packages/twenty-front/src/generated/graphql.ts @@ -571,6 +571,7 @@ export type CheckUserExistOutput = { export type ClientAiModelConfig = { __typename?: 'ClientAIModelConfig'; + deprecated?: Maybe; inputCostPer1kTokensInCredits: Scalars['Float']; label: Scalars['String']; modelId: Scalars['String']; diff --git a/packages/twenty-front/src/modules/ai/hooks/useAiModelOptions.ts b/packages/twenty-front/src/modules/ai/hooks/useAiModelOptions.ts index 46ca78ed45..14e67e676c 100644 --- a/packages/twenty-front/src/modules/ai/hooks/useAiModelOptions.ts +++ b/packages/twenty-front/src/modules/ai/hooks/useAiModelOptions.ts @@ -5,10 +5,13 @@ import { type SelectOption } from 'twenty-ui/input'; import { DEFAULT_FAST_MODEL } from '@/ai/constants/DefaultFastModel'; import { DEFAULT_SMART_MODEL } from '@/ai/constants/DefaultSmartModel'; -export const useAiModelOptions = (): SelectOption[] => { +export const useAiModelOptions = ( + includeDeprecated = false, +): SelectOption[] => { const aiModels = useRecoilValue(aiModelsState); return aiModels + .filter((model) => includeDeprecated || !model.deprecated) .map((model) => ({ value: model.modelId, label: @@ -19,3 +22,26 @@ export const useAiModelOptions = (): SelectOption[] => { })) .sort((a, b) => a.label.localeCompare(b.label)); }; + +export const useAiModelLabel = (modelId: string | undefined): string => { + const aiModels = useRecoilValue(aiModelsState); + + if (!modelId) { + return ''; + } + + const model = aiModels.find((m) => m.modelId === modelId); + + if (!model) { + return modelId; + } + + if ( + model.modelId === DEFAULT_FAST_MODEL || + model.modelId === DEFAULT_SMART_MODEL + ) { + return model.label; + } + + return `${model.label} (${model.provider})`; +}; diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAIRouterSettings.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAIRouterSettings.tsx index be4ae71547..213cead967 100644 --- a/packages/twenty-front/src/pages/settings/ai/components/SettingsAIRouterSettings.tsx +++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAIRouterSettings.tsx @@ -1,10 +1,14 @@ import styled from '@emotion/styled'; -import { useRecoilState } from 'recoil'; +import { useRecoilState, useRecoilValue } from 'recoil'; -import { useAiModelOptions } from '@/ai/hooks/useAiModelOptions'; import { DEFAULT_FAST_MODEL } from '@/ai/constants/DefaultFastModel'; import { DEFAULT_SMART_MODEL } from '@/ai/constants/DefaultSmartModel'; +import { + useAiModelLabel, + useAiModelOptions, +} from '@/ai/hooks/useAiModelOptions'; import { currentWorkspaceState } from '@/auth/states/currentWorkspaceState'; +import { aiModelsState } from '@/client-config/states/aiModelsState'; import { StyledSettingsOptionCardContent, StyledSettingsOptionCardDescription, @@ -38,8 +42,41 @@ export const SettingsAIRouterSettings = () => { ); const [updateWorkspace] = useUpdateWorkspaceMutation(); - const modelOptions = useAiModelOptions(); - const noModelsAvailable = modelOptions.length === 0; + const aiModels = useRecoilValue(aiModelsState); + const activeModelOptions = useAiModelOptions(); + const fastModelLabel = useAiModelLabel(currentWorkspace?.fastModel); + const smartModelLabel = useAiModelLabel(currentWorkspace?.smartModel); + + const currentFastModel = aiModels.find( + (m) => m.modelId === currentWorkspace?.fastModel, + ); + const currentSmartModel = aiModels.find( + (m) => m.modelId === currentWorkspace?.smartModel, + ); + + const fastModelOptions = + currentFastModel?.deprecated === true + ? [ + { + value: currentWorkspace?.fastModel ?? '', + label: `${fastModelLabel} (deprecated)`, + }, + ...activeModelOptions, + ] + : activeModelOptions; + + const smartModelOptions = + currentSmartModel?.deprecated === true + ? [ + { + value: currentWorkspace?.smartModel ?? '', + label: `${smartModelLabel} (deprecated)`, + }, + ...activeModelOptions, + ] + : activeModelOptions; + + const noModelsAvailable = activeModelOptions.length === 0; const handleFastModelChange = async (value: string) => { if (!currentWorkspace?.id) { @@ -149,7 +186,7 @@ export const SettingsAIRouterSettings = () => { dropdownId="fast-model-select" value={currentWorkspace?.fastModel || DEFAULT_FAST_MODEL} onChange={handleFastModelChange} - options={modelOptions} + options={fastModelOptions} selectSizeVariant="small" /> @@ -172,7 +209,7 @@ export const SettingsAIRouterSettings = () => { dropdownId="smart-model-select" value={currentWorkspace?.smartModel || DEFAULT_SMART_MODEL} onChange={handleSmartModelChange} - options={modelOptions} + options={smartModelOptions} selectSizeVariant="small" /> diff --git a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSettingsTab.tsx b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSettingsTab.tsx index b9e816ce75..c3950a53a7 100644 --- a/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSettingsTab.tsx +++ b/packages/twenty-front/src/pages/settings/ai/components/SettingsAgentSettingsTab.tsx @@ -1,12 +1,17 @@ import styled from '@emotion/styled'; import { useLingui } from '@lingui/react/macro'; -import { useAiModelOptions } from '@/ai/hooks/useAiModelOptions'; +import { + useAiModelLabel, + useAiModelOptions, +} from '@/ai/hooks/useAiModelOptions'; +import { aiModelsState } from '@/client-config/states/aiModelsState'; import { IconPicker } from '@/ui/input/components/IconPicker'; import { Select } from '@/ui/input/components/Select'; import { SettingsTextInput } from '@/ui/input/components/SettingsTextInput'; import { TextArea } from '@/ui/input/components/TextArea'; import { useModal } from '@/ui/layout/modal/hooks/useModal'; +import { useRecoilValue } from 'recoil'; import { isDefined } from 'twenty-shared/utils'; import { H2Title, IconTrash } from 'twenty-ui/display'; import { Button } from 'twenty-ui/input'; @@ -61,7 +66,22 @@ export const SettingsAgentSettingsTab = ({ const { t } = useLingui(); const { openModal } = useModal(); - const modelOptions = useAiModelOptions(); + const aiModels = useRecoilValue(aiModelsState); + const activeModelOptions = useAiModelOptions(); + const currentModelLabel = useAiModelLabel(formValues.modelId); + + const currentModel = aiModels.find((m) => m.modelId === formValues.modelId); + const isCurrentModelDeprecated = currentModel?.deprecated === true; + + const modelOptions = isCurrentModelDeprecated + ? [ + { + value: formValues.modelId, + label: `${currentModelLabel} (deprecated)`, + }, + ...activeModelOptions, + ] + : activeModelOptions; const noModelsAvailable = modelOptions.length === 0; diff --git a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts index d581d1d41f..c318257678 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/client-config.entity.ts @@ -47,6 +47,9 @@ export class ClientAIModelConfig { @Field(() => NativeModelCapabilities, { nullable: true }) nativeCapabilities?: NativeModelCapabilities; + + @Field(() => Boolean, { nullable: true }) + deprecated?: boolean; } @ObjectType() diff --git a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts b/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts index 61bff3c6c8..b0d9e583c1 100644 --- a/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts +++ b/packages/twenty-server/src/engine/core-modules/client-config/services/client-config.service.ts @@ -59,6 +59,7 @@ export class ClientConfigService { builtInModel.outputCostPer1kTokensInCents, ) : 0, + deprecated: builtInModel?.deprecated, }; }, ); diff --git a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts index e790bfb3ef..5acfe08741 100644 --- a/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts +++ b/packages/twenty-server/src/engine/core-modules/twenty-config/config-variables.ts @@ -1111,20 +1111,21 @@ export class ConfigVariables { @ConfigVariablesMetadata({ group: ConfigVariablesGroup.LLM, description: - 'Default AI model ID for speed-optimized operations (lightweight tasks, high throughput)', + 'Comma-separated list of AI model IDs for speed-optimized operations, in priority order. The first available model will be used.', type: ConfigVariableType.STRING, }) @IsOptional() - DEFAULT_AI_SPEED_MODEL_ID = 'gpt-4o-mini'; + DEFAULT_AI_SPEED_MODEL_ID = + 'gpt-4.1-mini,claude-haiku-4-5-20251001,grok-3-mini'; @ConfigVariablesMetadata({ group: ConfigVariablesGroup.LLM, description: - 'Default AI model ID for performance-optimized operations (complex reasoning, quality focus)', + 'Comma-separated list of AI model IDs for performance-optimized operations, in priority order. The first available model will be used.', type: ConfigVariableType.STRING, }) @IsOptional() - DEFAULT_AI_PERFORMANCE_MODEL_ID = 'gpt-4o'; + DEFAULT_AI_PERFORMANCE_MODEL_ID = 'gpt-4.1,claude-sonnet-4-5-20250929,grok-4'; @ConfigVariablesMetadata({ group: ConfigVariablesGroup.LLM, diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/constants/ai-models-types.const.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/constants/ai-models-types.const.ts new file mode 100644 index 0000000000..f6f5340cd9 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/constants/ai-models-types.const.ts @@ -0,0 +1,64 @@ +export enum ModelProvider { + NONE = 'none', + OPENAI = 'openai', + ANTHROPIC = 'anthropic', + OPENAI_COMPATIBLE = 'open_ai_compatible', + XAI = 'xai', +} + +export const DEFAULT_FAST_MODEL = 'default-fast-model' as const; +export const DEFAULT_SMART_MODEL = 'default-smart-model' as const; + +export type ModelId = + | typeof DEFAULT_FAST_MODEL + | typeof DEFAULT_SMART_MODEL + // OpenAI models + | 'gpt-4o' + | 'gpt-4o-mini' + | 'gpt-4-turbo' + | 'gpt-4.1' + | 'gpt-4.1-mini' + | 'o3' + | 'o4-mini' + // Anthropic models + | 'claude-opus-4-20250514' + | 'claude-sonnet-4-20250514' + | 'claude-3-5-haiku-20241022' + | 'claude-opus-4-5-20251101' + | 'claude-sonnet-4-5-20250929' + | 'claude-haiku-4-5-20251001' + // xAI models + | 'grok-3' + | 'grok-3-mini' + | 'grok-4' + | 'grok-4-1-fast-reasoning' + | string; // Allow custom model names + +export type SupportedFileType = + | 'image/png' + | 'image/jpeg' + | 'image/gif' + | 'image/webp' + | 'application/pdf' + | 'text/plain' + | 'text/html' + | 'text/csv' + | 'application/json'; + +export interface AIModelConfig { + modelId: ModelId; + label: string; + description: string; + provider: ModelProvider; + inputCostPer1kTokensInCents: number; + outputCostPer1kTokensInCents: number; + contextWindowTokens: number; + maxOutputTokens: number; + supportedFileTypes?: SupportedFileType[]; + doesSupportThinking?: boolean; + nativeCapabilities?: { + webSearch?: boolean; + twitterSearch?: boolean; + }; + deprecated?: boolean; +} diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/constants/ai-models.const.spec.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/constants/ai-models.const.spec.ts index 535b815eb8..daf1b407e7 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/constants/ai-models.const.spec.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/constants/ai-models.const.spec.ts @@ -10,19 +10,56 @@ import { } from './ai-models.const'; describe('AI_MODELS', () => { - it('should contain all expected models', () => { - expect(AI_MODELS).toHaveLength(9); - expect(AI_MODELS.map((model) => model.modelId)).toEqual([ - 'gpt-4o', - 'gpt-4o-mini', - 'gpt-4-turbo', - 'claude-opus-4-20250514', - 'claude-sonnet-4-20250514', - 'claude-3-5-haiku-20241022', - 'grok-3', - 'grok-3-mini', - 'grok-4', - ]); + it('should have at least one model per provider', () => { + const providers = [ + ModelProvider.OPENAI, + ModelProvider.ANTHROPIC, + ModelProvider.XAI, + ]; + + providers.forEach((provider) => { + const modelsForProvider = AI_MODELS.filter( + (model) => model.provider === provider, + ); + + expect(modelsForProvider.length).toBeGreaterThan(0); + }); + }); + + it('should have all required fields for each model', () => { + AI_MODELS.forEach((model) => { + expect(model.modelId).toBeDefined(); + expect(model.label).toBeDefined(); + expect(model.description).toBeDefined(); + expect(model.provider).toBeDefined(); + expect(model.inputCostPer1kTokensInCents).toBeDefined(); + expect(model.outputCostPer1kTokensInCents).toBeDefined(); + expect(model.contextWindowTokens).toBeGreaterThan(0); + expect(model.maxOutputTokens).toBeGreaterThan(0); + }); + }); + + it('should have unique model IDs', () => { + const modelIds = AI_MODELS.map((model) => model.modelId); + const uniqueModelIds = new Set(modelIds); + + expect(uniqueModelIds.size).toBe(modelIds.length); + }); + + it('should have at least one non-deprecated model per provider', () => { + const providers = [ + ModelProvider.OPENAI, + ModelProvider.ANTHROPIC, + ModelProvider.XAI, + ]; + + providers.forEach((provider) => { + const activeModelsForProvider = AI_MODELS.filter( + (model) => model.provider === provider && !model.deprecated, + ); + + expect(activeModelsForProvider.length).toBeGreaterThan(0); + }); }); }); @@ -140,4 +177,50 @@ describe('AiModelRegistryService', () => { 'Model with ID non-existent-model not found', ); }); + + it('should find first available model from comma-separated list', () => { + // First model not available, second model available + MOCK_CONFIG_SERVICE.get.mockReturnValue( + 'gpt-4.1-mini,claude-haiku-4-5-20251001,grok-3-mini', + ); + + const getModelSpy = jest + .spyOn(SERVICE, 'getModel') + .mockImplementation((modelId: string) => { + if (modelId === 'claude-haiku-4-5-20251001') { + return { + modelId: 'claude-haiku-4-5-20251001', + provider: ModelProvider.ANTHROPIC, + model: {} as any, + }; + } + + return undefined; + }); + + const result = SERVICE.getDefaultSpeedModel(); + + expect(result).toBeDefined(); + expect(result.modelId).toBe('claude-haiku-4-5-20251001'); + expect(getModelSpy).toHaveBeenCalledWith('gpt-4.1-mini'); + expect(getModelSpy).toHaveBeenCalledWith('claude-haiku-4-5-20251001'); + }); + + it('should fall back to any available model if none in list are available', () => { + MOCK_CONFIG_SERVICE.get.mockReturnValue('model-a,model-b,model-c'); + + jest.spyOn(SERVICE, 'getModel').mockReturnValue(undefined); + jest.spyOn(SERVICE, 'getAvailableModels').mockReturnValue([ + { + modelId: 'fallback-model', + provider: ModelProvider.OPENAI_COMPATIBLE, + model: {} as any, + }, + ]); + + const result = SERVICE.getDefaultSpeedModel(); + + expect(result).toBeDefined(); + expect(result.modelId).toBe('fallback-model'); + }); }); diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/constants/ai-models.const.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/constants/ai-models.const.ts index 37c8b4b7e5..d4966d128e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/constants/ai-models.const.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/constants/ai-models.const.ts @@ -1,223 +1,19 @@ -export enum ModelProvider { - NONE = 'none', - OPENAI = 'openai', - ANTHROPIC = 'anthropic', - OPENAI_COMPATIBLE = 'open_ai_compatible', - XAI = 'xai', -} +export { + DEFAULT_FAST_MODEL, + DEFAULT_SMART_MODEL, + ModelProvider, + type AIModelConfig, + type ModelId, + type SupportedFileType, +} from './ai-models-types.const'; -export const DEFAULT_FAST_MODEL = 'default-fast-model' as const; -export const DEFAULT_SMART_MODEL = 'default-smart-model' as const; - -export type ModelId = - | typeof DEFAULT_FAST_MODEL - | typeof DEFAULT_SMART_MODEL - | 'gpt-4o' - | 'gpt-4o-mini' - | 'gpt-4-turbo' - | 'claude-opus-4-20250514' - | 'claude-sonnet-4-20250514' - | 'claude-3-5-haiku-20241022' - | 'grok-3' - | 'grok-3-mini' - | 'grok-4' - | string; // Allow custom model names - -export type SupportedFileType = - | 'image/png' - | 'image/jpeg' - | 'image/gif' - | 'image/webp' - | 'application/pdf' - | 'text/plain' - | 'text/html' - | 'text/csv' - | 'application/json'; - -export interface AIModelConfig { - modelId: ModelId; - label: string; - description: string; - provider: ModelProvider; - inputCostPer1kTokensInCents: number; - outputCostPer1kTokensInCents: number; - contextWindowTokens: number; - maxOutputTokens: number; - supportedFileTypes?: SupportedFileType[]; - doesSupportThinking?: boolean; - nativeCapabilities?: { - webSearch?: boolean; - twitterSearch?: boolean; - }; -} +import { type AIModelConfig } from './ai-models-types.const'; +import { ANTHROPIC_MODELS } from './anthropic-models.const'; +import { OPENAI_MODELS } from './openai-models.const'; +import { XAI_MODELS } from './xai-models.const'; export const AI_MODELS: AIModelConfig[] = [ - { - modelId: 'gpt-4o', - label: 'GPT-4o', - description: - 'Most advanced multimodal model with strong reasoning, vision, and coding capabilities', - provider: ModelProvider.OPENAI, - inputCostPer1kTokensInCents: 0.25, - outputCostPer1kTokensInCents: 1.0, - contextWindowTokens: 128000, - maxOutputTokens: 16384, - supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'], - nativeCapabilities: { - webSearch: true, - }, - }, - { - modelId: 'gpt-4o-mini', - label: 'GPT-4o Mini', - description: - 'Fast and cost-efficient model for lightweight tasks and high-volume operations', - provider: ModelProvider.OPENAI, - inputCostPer1kTokensInCents: 0.015, - outputCostPer1kTokensInCents: 0.06, - contextWindowTokens: 128000, - maxOutputTokens: 16384, - supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'], - nativeCapabilities: { - webSearch: true, - }, - }, - { - modelId: 'gpt-4-turbo', - label: 'GPT-4 Turbo', - description: - 'Previous generation high-performance model with vision capabilities', - provider: ModelProvider.OPENAI, - inputCostPer1kTokensInCents: 1.0, - outputCostPer1kTokensInCents: 3.0, - contextWindowTokens: 128000, - maxOutputTokens: 4096, - supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'], - nativeCapabilities: { - webSearch: false, - }, - }, - { - modelId: 'claude-opus-4-20250514', - label: 'Claude Opus 4', - description: - 'Most powerful Claude model with extended thinking for complex reasoning tasks', - provider: ModelProvider.ANTHROPIC, - inputCostPer1kTokensInCents: 1.5, - outputCostPer1kTokensInCents: 7.5, - contextWindowTokens: 200000, - maxOutputTokens: 8192, - supportedFileTypes: [ - 'image/png', - 'image/jpeg', - 'image/gif', - 'image/webp', - 'application/pdf', - 'text/plain', - 'text/html', - 'text/csv', - ], - doesSupportThinking: true, - nativeCapabilities: { - webSearch: true, - }, - }, - { - modelId: 'claude-sonnet-4-20250514', - label: 'Claude Sonnet 4', - description: - 'Balanced model with strong performance and extended thinking capabilities', - provider: ModelProvider.ANTHROPIC, - inputCostPer1kTokensInCents: 0.3, - outputCostPer1kTokensInCents: 1.5, - contextWindowTokens: 200000, - maxOutputTokens: 8192, - supportedFileTypes: [ - 'image/png', - 'image/jpeg', - 'image/gif', - 'image/webp', - 'application/pdf', - 'text/plain', - 'text/html', - 'text/csv', - ], - doesSupportThinking: true, - nativeCapabilities: { - webSearch: true, - }, - }, - { - modelId: 'claude-3-5-haiku-20241022', - label: 'Claude Haiku 3.5', - description: - 'Fast and efficient model optimized for speed and cost-effectiveness', - provider: ModelProvider.ANTHROPIC, - inputCostPer1kTokensInCents: 0.08, - outputCostPer1kTokensInCents: 0.4, - contextWindowTokens: 200000, - maxOutputTokens: 8192, - supportedFileTypes: [ - 'image/png', - 'image/jpeg', - 'image/gif', - 'image/webp', - 'application/pdf', - 'text/plain', - 'text/html', - 'text/csv', - ], - doesSupportThinking: false, - nativeCapabilities: { - webSearch: true, - }, - }, - { - modelId: 'grok-3', - label: 'Grok-3', - description: - 'Advanced model with web and Twitter search, optimized for real-time information', - provider: ModelProvider.XAI, - inputCostPer1kTokensInCents: 0.3, - outputCostPer1kTokensInCents: 1.5, - contextWindowTokens: 131072, - maxOutputTokens: 8192, - supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'], - nativeCapabilities: { - webSearch: true, - twitterSearch: true, - }, - }, - { - modelId: 'grok-3-mini', - label: 'Grok-3 Mini', - description: - 'Lightweight model with web and Twitter search for fast, cost-effective operations', - provider: ModelProvider.XAI, - inputCostPer1kTokensInCents: 0.03, - outputCostPer1kTokensInCents: 0.05, - contextWindowTokens: 131072, - maxOutputTokens: 8192, - supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'], - nativeCapabilities: { - webSearch: true, - twitterSearch: true, - }, - }, - { - modelId: 'grok-4', - label: 'Grok-4', - description: - 'Most capable Grok model with enhanced reasoning, web and Twitter search', - provider: ModelProvider.XAI, - inputCostPer1kTokensInCents: 0.5, - outputCostPer1kTokensInCents: 2.5, - contextWindowTokens: 131072, - maxOutputTokens: 8192, - supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'], - nativeCapabilities: { - webSearch: true, - twitterSearch: true, - }, - }, + ...OPENAI_MODELS, + ...ANTHROPIC_MODELS, + ...XAI_MODELS, ]; diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/constants/anthropic-models.const.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/constants/anthropic-models.const.ts new file mode 100644 index 0000000000..f75203cb9f --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/constants/anthropic-models.const.ts @@ -0,0 +1,159 @@ +import { type AIModelConfig, ModelProvider } from './ai-models-types.const'; + +export const ANTHROPIC_MODELS: AIModelConfig[] = [ + // Active models + { + modelId: 'claude-opus-4-5-20251101', + label: 'Claude Opus 4.5', + description: + 'Most powerful Claude model excelling in complex reasoning, coding, and agentic tasks', + provider: ModelProvider.ANTHROPIC, + inputCostPer1kTokensInCents: 0.5, + outputCostPer1kTokensInCents: 2.5, + contextWindowTokens: 200000, + maxOutputTokens: 64000, + supportedFileTypes: [ + 'image/png', + 'image/jpeg', + 'image/gif', + 'image/webp', + 'application/pdf', + 'text/plain', + 'text/html', + 'text/csv', + ], + doesSupportThinking: true, + nativeCapabilities: { + webSearch: true, + }, + }, + { + modelId: 'claude-sonnet-4-5-20250929', + label: 'Claude Sonnet 4.5', + description: + 'Advanced model for coding tasks and complex agent-based workflows with 1M context', + provider: ModelProvider.ANTHROPIC, + inputCostPer1kTokensInCents: 0.3, + outputCostPer1kTokensInCents: 1.5, + contextWindowTokens: 1000000, + maxOutputTokens: 64000, + supportedFileTypes: [ + 'image/png', + 'image/jpeg', + 'image/gif', + 'image/webp', + 'application/pdf', + 'text/plain', + 'text/html', + 'text/csv', + ], + doesSupportThinking: true, + nativeCapabilities: { + webSearch: true, + }, + }, + { + modelId: 'claude-haiku-4-5-20251001', + label: 'Claude Haiku 4.5', + description: + 'Fast and cost-effective model optimized for high-speed processing', + provider: ModelProvider.ANTHROPIC, + inputCostPer1kTokensInCents: 0.1, + outputCostPer1kTokensInCents: 0.5, + contextWindowTokens: 200000, + maxOutputTokens: 64000, + supportedFileTypes: [ + 'image/png', + 'image/jpeg', + 'image/gif', + 'image/webp', + 'application/pdf', + 'text/plain', + 'text/html', + 'text/csv', + ], + doesSupportThinking: false, + nativeCapabilities: { + webSearch: true, + }, + }, + { + modelId: 'claude-3-5-haiku-20241022', + label: 'Claude Haiku 3.5', + description: + 'Fast and efficient model optimized for speed and cost-effectiveness', + provider: ModelProvider.ANTHROPIC, + inputCostPer1kTokensInCents: 0.08, + outputCostPer1kTokensInCents: 0.4, + contextWindowTokens: 200000, + maxOutputTokens: 8192, + supportedFileTypes: [ + 'image/png', + 'image/jpeg', + 'image/gif', + 'image/webp', + 'application/pdf', + 'text/plain', + 'text/html', + 'text/csv', + ], + doesSupportThinking: false, + nativeCapabilities: { + webSearch: true, + }, + }, + + // Deprecated models - kept for backward compatibility with existing agents + { + modelId: 'claude-opus-4-20250514', + label: 'Claude Opus 4', + description: + 'Most powerful Claude model with extended thinking for complex reasoning tasks', + provider: ModelProvider.ANTHROPIC, + inputCostPer1kTokensInCents: 1.5, + outputCostPer1kTokensInCents: 7.5, + contextWindowTokens: 200000, + maxOutputTokens: 8192, + supportedFileTypes: [ + 'image/png', + 'image/jpeg', + 'image/gif', + 'image/webp', + 'application/pdf', + 'text/plain', + 'text/html', + 'text/csv', + ], + doesSupportThinking: true, + nativeCapabilities: { + webSearch: true, + }, + deprecated: true, + }, + { + modelId: 'claude-sonnet-4-20250514', + label: 'Claude Sonnet 4', + description: + 'Balanced model with strong performance and extended thinking capabilities', + provider: ModelProvider.ANTHROPIC, + inputCostPer1kTokensInCents: 0.3, + outputCostPer1kTokensInCents: 1.5, + contextWindowTokens: 200000, + maxOutputTokens: 8192, + supportedFileTypes: [ + 'image/png', + 'image/jpeg', + 'image/gif', + 'image/webp', + 'application/pdf', + 'text/plain', + 'text/html', + 'text/csv', + ], + doesSupportThinking: true, + nativeCapabilities: { + webSearch: true, + }, + deprecated: true, + }, +]; diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/constants/openai-models.const.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/constants/openai-models.const.ts new file mode 100644 index 0000000000..1778a8374b --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/constants/openai-models.const.ts @@ -0,0 +1,117 @@ +import { type AIModelConfig, ModelProvider } from './ai-models-types.const'; + +export const OPENAI_MODELS: AIModelConfig[] = [ + // Active models + { + modelId: 'gpt-4.1', + label: 'GPT-4.1', + description: + 'Advanced model excelling in coding, instruction following, and long-context comprehension', + provider: ModelProvider.OPENAI, + inputCostPer1kTokensInCents: 0.2, + outputCostPer1kTokensInCents: 0.8, + contextWindowTokens: 1047576, + maxOutputTokens: 32768, + supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'], + nativeCapabilities: { + webSearch: true, + }, + }, + { + modelId: 'gpt-4.1-mini', + label: 'GPT-4.1 Mini', + description: + 'Fast and cost-efficient version of GPT-4.1 optimized for low latency', + provider: ModelProvider.OPENAI, + inputCostPer1kTokensInCents: 0.04, + outputCostPer1kTokensInCents: 0.16, + contextWindowTokens: 1047576, + maxOutputTokens: 32768, + supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'], + nativeCapabilities: { + webSearch: true, + }, + }, + { + modelId: 'o3', + label: 'o3', + description: + 'Powerful reasoning model excelling in complex queries, coding, math, and science', + provider: ModelProvider.OPENAI, + inputCostPer1kTokensInCents: 0.2, + outputCostPer1kTokensInCents: 0.8, + contextWindowTokens: 200000, + maxOutputTokens: 100000, + supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'], + doesSupportThinking: true, + nativeCapabilities: { + webSearch: true, + }, + }, + { + modelId: 'o4-mini', + label: 'o4-mini', + description: + 'Cost-effective reasoning model excelling in math, coding, and visual tasks', + provider: ModelProvider.OPENAI, + inputCostPer1kTokensInCents: 0.11, + outputCostPer1kTokensInCents: 0.44, + contextWindowTokens: 200000, + maxOutputTokens: 100000, + supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'], + doesSupportThinking: true, + nativeCapabilities: { + webSearch: true, + }, + }, + + // Deprecated models - kept for backward compatibility with existing agents + { + modelId: 'gpt-4o', + label: 'GPT-4o', + description: + 'Most advanced multimodal model with strong reasoning, vision, and coding capabilities', + provider: ModelProvider.OPENAI, + inputCostPer1kTokensInCents: 0.25, + outputCostPer1kTokensInCents: 1.0, + contextWindowTokens: 128000, + maxOutputTokens: 16384, + supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'], + nativeCapabilities: { + webSearch: true, + }, + deprecated: true, + }, + { + modelId: 'gpt-4o-mini', + label: 'GPT-4o Mini', + description: + 'Fast and cost-efficient model for lightweight tasks and high-volume operations', + provider: ModelProvider.OPENAI, + inputCostPer1kTokensInCents: 0.015, + outputCostPer1kTokensInCents: 0.06, + contextWindowTokens: 128000, + maxOutputTokens: 16384, + supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'], + nativeCapabilities: { + webSearch: true, + }, + deprecated: true, + }, + { + modelId: 'gpt-4-turbo', + label: 'GPT-4 Turbo', + description: + 'Previous generation high-performance model with vision capabilities', + provider: ModelProvider.OPENAI, + inputCostPer1kTokensInCents: 1.0, + outputCostPer1kTokensInCents: 3.0, + contextWindowTokens: 128000, + maxOutputTokens: 4096, + supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'], + nativeCapabilities: { + webSearch: false, + }, + deprecated: true, + }, +]; diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/constants/xai-models.const.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/constants/xai-models.const.ts new file mode 100644 index 0000000000..075130f335 --- /dev/null +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/constants/xai-models.const.ts @@ -0,0 +1,70 @@ +import { type AIModelConfig, ModelProvider } from './ai-models-types.const'; + +export const XAI_MODELS: AIModelConfig[] = [ + // Active models + { + modelId: 'grok-4-1-fast-reasoning', + label: 'Grok 4.1 Fast', + description: + 'Next-generation tool-calling agent with 2M context for advanced agentic workflows', + provider: ModelProvider.XAI, + inputCostPer1kTokensInCents: 0.02, + outputCostPer1kTokensInCents: 0.05, + contextWindowTokens: 2000000, + maxOutputTokens: 8192, + supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'], + doesSupportThinking: true, + nativeCapabilities: { + webSearch: true, + twitterSearch: true, + }, + }, + { + modelId: 'grok-4', + label: 'Grok-4', + description: + 'Most capable Grok model with enhanced reasoning, web and Twitter search', + provider: ModelProvider.XAI, + inputCostPer1kTokensInCents: 0.3, + outputCostPer1kTokensInCents: 1.5, + contextWindowTokens: 256000, + maxOutputTokens: 8192, + supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'], + nativeCapabilities: { + webSearch: true, + twitterSearch: true, + }, + }, + { + modelId: 'grok-3', + label: 'Grok-3', + description: + 'Advanced model with web and Twitter search, optimized for real-time information', + provider: ModelProvider.XAI, + inputCostPer1kTokensInCents: 0.3, + outputCostPer1kTokensInCents: 1.5, + contextWindowTokens: 131072, + maxOutputTokens: 8192, + supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'], + nativeCapabilities: { + webSearch: true, + twitterSearch: true, + }, + }, + { + modelId: 'grok-3-mini', + label: 'Grok-3 Mini', + description: + 'Lightweight model with web and Twitter search for fast, cost-effective operations', + provider: ModelProvider.XAI, + inputCostPer1kTokensInCents: 0.03, + outputCostPer1kTokensInCents: 0.05, + contextWindowTokens: 131072, + maxOutputTokens: 8192, + supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'], + nativeCapabilities: { + webSearch: true, + twitterSearch: true, + }, + }, +]; diff --git a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service.ts b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service.ts index a411d741f3..e851beab9e 100644 --- a/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service.ts +++ b/packages/twenty-server/src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service.ts @@ -13,6 +13,9 @@ import { ModelProvider, type AIModelConfig, } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const'; +import { ANTHROPIC_MODELS } from 'src/engine/metadata-modules/ai/ai-models/constants/anthropic-models.const'; +import { OPENAI_MODELS } from 'src/engine/metadata-modules/ai/ai-models/constants/openai-models.const'; +import { XAI_MODELS } from 'src/engine/metadata-modules/ai/ai-models/constants/xai-models.const'; export interface RegisteredAIModel { modelId: string; @@ -66,25 +69,18 @@ export class AiModelRegistryService { } private registerOpenAIModels(): void { - const openaiModels = AI_MODELS.filter( - (model) => model.provider === ModelProvider.OPENAI, - ); - - openaiModels.forEach((modelConfig) => { + OPENAI_MODELS.forEach((modelConfig) => { this.modelRegistry.set(modelConfig.modelId, { modelId: modelConfig.modelId, provider: ModelProvider.OPENAI, model: openai(modelConfig.modelId), + doesSupportThinking: modelConfig.doesSupportThinking, }); }); } private registerAnthropicModels(): void { - const anthropicModels = AI_MODELS.filter( - (model) => model.provider === ModelProvider.ANTHROPIC, - ); - - anthropicModels.forEach((modelConfig) => { + ANTHROPIC_MODELS.forEach((modelConfig) => { this.modelRegistry.set(modelConfig.modelId, { modelId: modelConfig.modelId, provider: ModelProvider.ANTHROPIC, @@ -95,15 +91,12 @@ export class AiModelRegistryService { } private registerXaiModels(): void { - const xaiModels = AI_MODELS.filter( - (model) => model.provider === ModelProvider.XAI, - ); - - xaiModels.forEach((modelConfig) => { + XAI_MODELS.forEach((modelConfig) => { this.modelRegistry.set(modelConfig.modelId, { modelId: modelConfig.modelId, provider: ModelProvider.XAI, model: xai(modelConfig.modelId), + doesSupportThinking: modelConfig.doesSupportThinking, }); }); } @@ -140,11 +133,30 @@ export class AiModelRegistryService { return Array.from(this.modelRegistry.values()); } + private getFirstAvailableModelFromList( + modelIdList: string, + ): RegisteredAIModel | undefined { + const modelIds = modelIdList + .split(',') + .map((id) => id.trim()) + .filter((id) => id.length > 0); + + for (const modelId of modelIds) { + const model = this.getModel(modelId); + + if (model) { + return model; + } + } + + return undefined; + } + getDefaultSpeedModel(): RegisteredAIModel { - const defaultModelId = this.twentyConfigService.get( + const defaultModelIds = this.twentyConfigService.get( 'DEFAULT_AI_SPEED_MODEL_ID', ); - let model = this.getModel(defaultModelId); + let model = this.getFirstAvailableModelFromList(defaultModelIds); if (!model) { const availableModels = this.getAvailableModels(); @@ -156,10 +168,10 @@ export class AiModelRegistryService { } getDefaultPerformanceModel(): RegisteredAIModel { - const defaultModelId = this.twentyConfigService.get( + const defaultModelIds = this.twentyConfigService.get( 'DEFAULT_AI_PERFORMANCE_MODEL_ID', ); - let model = this.getModel(defaultModelId); + let model = this.getFirstAvailableModelFromList(defaultModelIds); if (!model) { const availableModels = this.getAvailableModels();