feat(ai): refresh AI models with deprecation support and multi-provider defaults (BREAKING: deploy server before frontend please) (#16503)

## Summary

- Add latest AI models from OpenAI (GPT-4.1, o3, o4-mini), Anthropic
(Claude 4.5 Opus/Sonnet/Haiku), and xAI (Grok 4.1)
- Mark deprecated models (GPT-4o, GPT-4o-mini, GPT-4-turbo, Claude Opus
4, Claude Sonnet 4) with a `deprecated` flag
- Split AI models into separate files per provider for better
maintainability
- Support comma-separated default model lists for automatic fallback
across providers (works out of the box for self-hosters regardless of
which provider they configure)
- Filter deprecated models from dropdown selection while keeping them
functional for existing agents

## Changes

### New Models Added
| Provider | Models |
|----------|--------|
| OpenAI | gpt-4.1, gpt-4.1-mini, o3, o4-mini |
| Anthropic | claude-opus-4-5, claude-sonnet-4-5, claude-haiku-4-5 |
| xAI | grok-4-1-fast-reasoning |

### Deprecated Models
- gpt-4o, gpt-4o-mini, gpt-4-turbo (OpenAI)
- claude-opus-4-20250514, claude-sonnet-4-20250514 (Anthropic)

### Config Changes
Default model configs now support comma-separated fallback lists:
-
`DEFAULT_AI_SPEED_MODEL_ID=gpt-4.1-mini,claude-haiku-4-5-20251001,grok-3-mini`
-
`DEFAULT_AI_PERFORMANCE_MODEL_ID=gpt-4.1,claude-sonnet-4-5-20250929,grok-4`

## Test plan

- [x] Unit tests pass
- [x] Typecheck passes
- [x] Lint passes
- [ ] Verify deprecated models don't appear in model dropdowns
- [ ] Verify agents with deprecated models still work correctly
- [ ] Verify default model fallback works when only one provider is
configured
This commit is contained in:
Félix Malfait
2025-12-11 21:47:38 +01:00
committed by GitHub
parent 999bc84b17
commit a13727335b
15 changed files with 655 additions and 264 deletions
@@ -571,6 +571,7 @@ export type CheckUserExistOutput = {
export type ClientAiModelConfig = {
__typename?: 'ClientAIModelConfig';
deprecated?: Maybe<Scalars['Boolean']>;
inputCostPer1kTokensInCredits: Scalars['Float'];
label: Scalars['String'];
modelId: Scalars['String'];
@@ -571,6 +571,7 @@ export type CheckUserExistOutput = {
export type ClientAiModelConfig = {
__typename?: 'ClientAIModelConfig';
deprecated?: Maybe<Scalars['Boolean']>;
inputCostPer1kTokensInCredits: Scalars['Float'];
label: Scalars['String'];
modelId: Scalars['String'];
@@ -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<string>[] => {
export const useAiModelOptions = (
includeDeprecated = false,
): SelectOption<string>[] => {
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<string>[] => {
}))
.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})`;
};
@@ -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"
/>
</StyledSelectContainer>
@@ -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"
/>
</StyledSelectContainer>
@@ -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;
@@ -47,6 +47,9 @@ export class ClientAIModelConfig {
@Field(() => NativeModelCapabilities, { nullable: true })
nativeCapabilities?: NativeModelCapabilities;
@Field(() => Boolean, { nullable: true })
deprecated?: boolean;
}
@ObjectType()
@@ -59,6 +59,7 @@ export class ClientConfigService {
builtInModel.outputCostPer1kTokensInCents,
)
: 0,
deprecated: builtInModel?.deprecated,
};
},
);
@@ -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,
@@ -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;
}
@@ -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');
});
});
@@ -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,
];
@@ -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,
},
];
@@ -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,
},
];
@@ -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,
},
},
];
@@ -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();