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:
@@ -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})`;
|
||||
};
|
||||
|
||||
+43
-6
@@ -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>
|
||||
|
||||
+22
-2
@@ -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()
|
||||
|
||||
+1
@@ -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,
|
||||
|
||||
+64
@@ -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;
|
||||
}
|
||||
+96
-13
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
+15
-219
@@ -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,
|
||||
];
|
||||
|
||||
+159
@@ -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,
|
||||
},
|
||||
];
|
||||
+117
@@ -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,
|
||||
},
|
||||
];
|
||||
+70
@@ -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,
|
||||
},
|
||||
},
|
||||
];
|
||||
+31
-19
@@ -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();
|
||||
|
||||
Reference in New Issue
Block a user