Add AI model pricing, Bedrock provider, and InferenceProvider/ModelFamily split (#18155)

## Summary

- **Model pricing overhaul**: All model constants updated with accurate
pricing in dollars per 1M tokens, including cached input rates, cache
creation rates, and tiered >200k context pricing
- **New providers**: Added Google (Gemini 3.x), Mistral, and AWS Bedrock
as inference providers. Bedrock serves Claude Opus 4.6 and Sonnet 4.6
via AWS, with proper credential handling following the existing S3/SES
pattern
- **InferenceProvider/ModelFamily split**: Refactored `ModelProvider`
into two orthogonal enums — `InferenceProvider` (who serves the model:
auth, SDK, metadata format) and `ModelFamily` (who created it: token
counting semantics). This eliminates growing `||` chains for token
normalization checks like `excludesCachedTokens`
- **Billing improvements**: Reasoning tokens charged at output rate,
cache token discounts applied accurately, real errors thrown to Sentry
on billing failures

## Test plan

- [x] All existing unit tests updated and passing (23 tests across 3
test files)
- [x] Lint passes for both twenty-server and twenty-front
- [ ] CI checks pass


Made with [Cursor](https://cursor.com)

---------

Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
Félix Malfait
2026-02-22 14:39:04 +01:00
committed by GitHub
parent a900b4a4b4
commit 41f09c5a4c
37 changed files with 1462 additions and 383 deletions
@@ -1,12 +1,23 @@
export enum ModelProvider {
export enum InferenceProvider {
NONE = 'none',
OPENAI = 'openai',
ANTHROPIC = 'anthropic',
BEDROCK = 'bedrock',
GOOGLE = 'google',
MISTRAL = 'mistral',
OPENAI_COMPATIBLE = 'open_ai_compatible',
XAI = 'xai',
GROQ = 'groq',
}
export enum ModelFamily {
OPENAI = 'openai',
ANTHROPIC = 'anthropic',
GOOGLE = 'google',
MISTRAL = 'mistral',
XAI = 'xai',
}
export const DEFAULT_FAST_MODEL = 'default-fast-model' as const;
export const DEFAULT_SMART_MODEL = 'default-smart-model' as const;
@@ -14,27 +25,41 @@ export type ModelId =
| typeof DEFAULT_FAST_MODEL
| typeof DEFAULT_SMART_MODEL
// OpenAI models
| 'gpt-5.2'
| 'gpt-5-mini'
| 'gpt-4.1'
| 'gpt-4.1-mini'
| 'gpt-4o'
| 'gpt-4o-mini'
| 'gpt-4-turbo'
| 'gpt-4.1'
| 'gpt-4.1-mini'
| 'o3'
| 'o4-mini'
// Anthropic models
| 'claude-opus-4-6'
| 'claude-sonnet-4-6'
| 'claude-sonnet-4-5-20250929'
| 'claude-haiku-4-5-20251001'
| 'claude-opus-4-5-20251101'
| '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'
| 'grok-3'
| 'grok-3-mini'
// Google models
| 'gemini-3.1-pro-preview'
| 'gemini-3-flash-preview'
| 'gemini-2.5-pro'
| 'gemini-2.5-flash'
// Bedrock models (Anthropic via AWS)
| 'anthropic.claude-opus-4-6-v1'
| 'anthropic.claude-sonnet-4-6'
// Groq models
| 'openai/gpt-oss-120b'
// Mistral models
| 'mistral-large-latest'
| string; // Allow custom model names
export type SupportedFileType =
@@ -48,13 +73,25 @@ export type SupportedFileType =
| 'text/csv'
| 'application/json';
export type LongContextCost = {
inputCostPerMillionTokens: number;
outputCostPerMillionTokens: number;
cachedInputCostPerMillionTokens?: number;
cacheCreationCostPerMillionTokens?: number;
thresholdTokens: number;
};
export interface AIModelConfig {
modelId: ModelId;
label: string;
description: string;
provider: ModelProvider;
inputCostPer1kTokensInCents: number;
outputCostPer1kTokensInCents: number;
modelFamily: ModelFamily;
inferenceProvider: InferenceProvider;
inputCostPerMillionTokens: number;
outputCostPerMillionTokens: number;
cachedInputCostPerMillionTokens?: number;
cacheCreationCostPerMillionTokens?: number;
longContextCost?: LongContextCost;
contextWindowTokens: number;
maxOutputTokens: number;
supportedFileTypes?: SupportedFileType[];
@@ -6,21 +6,24 @@ import { AiModelRegistryService } from 'src/engine/metadata-modules/ai/ai-models
import {
AI_MODELS,
DEFAULT_SMART_MODEL,
ModelProvider,
InferenceProvider,
} from './ai-models.const';
describe('AI_MODELS', () => {
it('should have at least one model per provider', () => {
const providers = [
ModelProvider.OPENAI,
ModelProvider.ANTHROPIC,
ModelProvider.XAI,
ModelProvider.GROQ,
it('should have at least one model per inference provider', () => {
const inferenceProviders = [
InferenceProvider.OPENAI,
InferenceProvider.ANTHROPIC,
InferenceProvider.BEDROCK,
InferenceProvider.GOOGLE,
InferenceProvider.XAI,
InferenceProvider.GROQ,
InferenceProvider.MISTRAL,
];
providers.forEach((provider) => {
inferenceProviders.forEach((inferenceProvider) => {
const modelsForProvider = AI_MODELS.filter(
(model) => model.provider === provider,
(model) => model.inferenceProvider === inferenceProvider,
);
expect(modelsForProvider.length).toBeGreaterThan(0);
@@ -32,9 +35,10 @@ describe('AI_MODELS', () => {
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.modelFamily).toBeDefined();
expect(model.inferenceProvider).toBeDefined();
expect(model.inputCostPerMillionTokens).toBeDefined();
expect(model.outputCostPerMillionTokens).toBeDefined();
expect(model.contextWindowTokens).toBeGreaterThan(0);
expect(model.maxOutputTokens).toBeGreaterThan(0);
});
@@ -47,17 +51,21 @@ describe('AI_MODELS', () => {
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,
ModelProvider.GROQ,
it('should have at least one non-deprecated model per inference provider', () => {
const inferenceProviders = [
InferenceProvider.OPENAI,
InferenceProvider.ANTHROPIC,
InferenceProvider.BEDROCK,
InferenceProvider.GOOGLE,
InferenceProvider.XAI,
InferenceProvider.GROQ,
InferenceProvider.MISTRAL,
];
providers.forEach((provider) => {
inferenceProviders.forEach((inferenceProvider) => {
const activeModelsForProvider = AI_MODELS.filter(
(model) => model.provider === provider && !model.deprecated,
(model) =>
model.inferenceProvider === inferenceProvider && !model.deprecated,
);
expect(activeModelsForProvider.length).toBeGreaterThan(0);
@@ -88,35 +96,35 @@ describe('AiModelRegistryService', () => {
});
it('should return effective model config for DEFAULT_SMART_MODEL', () => {
MOCK_CONFIG_SERVICE.get.mockReturnValue('gpt-4o');
MOCK_CONFIG_SERVICE.get.mockReturnValue('gpt-5.2');
expect(() => SERVICE.getEffectiveModelConfig(DEFAULT_SMART_MODEL)).toThrow(
'No AI models are available. Please configure at least one AI provider API key (OPENAI_API_KEY, ANTHROPIC_API_KEY, XAI_API_KEY, or GROQ_API_KEY).',
'No AI models are available. Please configure at least one AI provider (OPENAI_API_KEY, ANTHROPIC_API_KEY, AWS_BEDROCK_REGION, GOOGLE_API_KEY, XAI_API_KEY, GROQ_API_KEY, or MISTRAL_API_KEY).',
);
});
it('should return effective model config for DEFAULT_SMART_MODEL when models are available', () => {
MOCK_CONFIG_SERVICE.get.mockReturnValue('gpt-4o');
MOCK_CONFIG_SERVICE.get.mockReturnValue('gpt-5.2');
jest.spyOn(SERVICE, 'getAvailableModels').mockReturnValue([
{
modelId: 'gpt-4o',
provider: ModelProvider.OPENAI,
modelId: 'gpt-5.2',
inferenceProvider: InferenceProvider.OPENAI,
model: {} as any,
},
]);
jest.spyOn(SERVICE, 'getModel').mockReturnValue({
modelId: 'gpt-4o',
provider: ModelProvider.OPENAI,
modelId: 'gpt-5.2',
inferenceProvider: InferenceProvider.OPENAI,
model: {} as any,
});
const RESULT = SERVICE.getEffectiveModelConfig(DEFAULT_SMART_MODEL);
expect(RESULT).toBeDefined();
expect(RESULT.modelId).toBe('gpt-4o');
expect(RESULT.provider).toBe(ModelProvider.OPENAI);
expect(RESULT.modelId).toBe('gpt-5.2');
expect(RESULT.inferenceProvider).toBe(InferenceProvider.OPENAI);
});
it('should return effective model config for DEFAULT_SMART_MODEL with custom model', () => {
@@ -125,14 +133,14 @@ describe('AiModelRegistryService', () => {
jest.spyOn(SERVICE, 'getAvailableModels').mockReturnValue([
{
modelId: 'mistral',
provider: ModelProvider.OPENAI_COMPATIBLE,
inferenceProvider: InferenceProvider.OPENAI_COMPATIBLE,
model: {} as any,
},
]);
jest.spyOn(SERVICE, 'getModel').mockReturnValue({
modelId: 'mistral',
provider: ModelProvider.OPENAI_COMPATIBLE,
inferenceProvider: InferenceProvider.OPENAI_COMPATIBLE,
model: {} as any,
});
@@ -140,25 +148,24 @@ describe('AiModelRegistryService', () => {
expect(RESULT).toBeDefined();
expect(RESULT.modelId).toBe('mistral');
expect(RESULT.provider).toBe(ModelProvider.OPENAI_COMPATIBLE);
expect(RESULT.inferenceProvider).toBe(InferenceProvider.OPENAI_COMPATIBLE);
expect(RESULT.label).toBe('mistral');
expect(RESULT.inputCostPer1kTokensInCents).toBe(0);
expect(RESULT.outputCostPer1kTokensInCents).toBe(0);
expect(RESULT.inputCostPerMillionTokens).toBe(0);
expect(RESULT.outputCostPerMillionTokens).toBe(0);
});
it('should return effective model config for specific model', () => {
const RESULT = SERVICE.getEffectiveModelConfig('gpt-4o-mini');
const RESULT = SERVICE.getEffectiveModelConfig('gpt-5.2');
expect(RESULT).toBeDefined();
expect(RESULT.modelId).toBe('gpt-4o-mini');
expect(RESULT.provider).toBe(ModelProvider.OPENAI);
expect(RESULT.modelId).toBe('gpt-5.2');
expect(RESULT.inferenceProvider).toBe(InferenceProvider.OPENAI);
});
it('should return effective model config for custom model', () => {
// Mock that the custom model exists in registry
jest.spyOn(SERVICE, 'getModel').mockReturnValue({
modelId: 'mistral',
provider: ModelProvider.OPENAI_COMPATIBLE,
inferenceProvider: InferenceProvider.OPENAI_COMPATIBLE,
model: {} as any,
});
@@ -166,10 +173,10 @@ describe('AiModelRegistryService', () => {
expect(RESULT).toBeDefined();
expect(RESULT.modelId).toBe('mistral');
expect(RESULT.provider).toBe(ModelProvider.OPENAI_COMPATIBLE);
expect(RESULT.inferenceProvider).toBe(InferenceProvider.OPENAI_COMPATIBLE);
expect(RESULT.label).toBe('mistral');
expect(RESULT.inputCostPer1kTokensInCents).toBe(0);
expect(RESULT.outputCostPer1kTokensInCents).toBe(0);
expect(RESULT.inputCostPerMillionTokens).toBe(0);
expect(RESULT.outputCostPerMillionTokens).toBe(0);
});
it('should throw error for non-existent model', () => {
@@ -181,9 +188,8 @@ describe('AiModelRegistryService', () => {
});
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',
'gpt-5-mini,claude-haiku-4-5-20251001,gemini-3-flash-preview',
);
const getModelSpy = jest
@@ -192,7 +198,7 @@ describe('AiModelRegistryService', () => {
if (modelId === 'claude-haiku-4-5-20251001') {
return {
modelId: 'claude-haiku-4-5-20251001',
provider: ModelProvider.ANTHROPIC,
inferenceProvider: InferenceProvider.ANTHROPIC,
model: {} as any,
};
}
@@ -204,7 +210,7 @@ describe('AiModelRegistryService', () => {
expect(result).toBeDefined();
expect(result.modelId).toBe('claude-haiku-4-5-20251001');
expect(getModelSpy).toHaveBeenCalledWith('gpt-4.1-mini');
expect(getModelSpy).toHaveBeenCalledWith('gpt-5-mini');
expect(getModelSpy).toHaveBeenCalledWith('claude-haiku-4-5-20251001');
});
@@ -215,7 +221,7 @@ describe('AiModelRegistryService', () => {
jest.spyOn(SERVICE, 'getAvailableModels').mockReturnValue([
{
modelId: 'fallback-model',
provider: ModelProvider.OPENAI_COMPATIBLE,
inferenceProvider: InferenceProvider.OPENAI_COMPATIBLE,
model: {} as any,
},
]);
@@ -1,7 +1,8 @@
export {
DEFAULT_FAST_MODEL,
DEFAULT_SMART_MODEL,
ModelProvider,
InferenceProvider,
ModelFamily,
type AIModelConfig,
type ModelId,
type SupportedFileType,
@@ -9,13 +10,19 @@ export {
import { type AIModelConfig } from './ai-models-types.const';
import { ANTHROPIC_MODELS } from './anthropic-models.const';
import { BEDROCK_MODELS } from './bedrock-models.const';
import { GOOGLE_MODELS } from './google-models.const';
import { GROQ_MODELS } from './groq-models.const';
import { MISTRAL_MODELS } from './mistral-models.const';
import { OPENAI_MODELS } from './openai-models.const';
import { XAI_MODELS } from './xai-models.const';
export const AI_MODELS: AIModelConfig[] = [
...OPENAI_MODELS,
...ANTHROPIC_MODELS,
...BEDROCK_MODELS,
...GOOGLE_MODELS,
...XAI_MODELS,
...GROQ_MODELS,
...MISTRAL_MODELS,
];
@@ -1,15 +1,64 @@
import { type AIModelConfig, ModelProvider } from './ai-models-types.const';
import {
type AIModelConfig,
InferenceProvider,
ModelFamily,
} from './ai-models-types.const';
export const ANTHROPIC_MODELS: AIModelConfig[] = [
// Active models
{
modelId: 'claude-opus-4-5-20251101',
label: 'Claude Opus 4.5',
modelId: 'claude-opus-4-6',
label: 'Claude Opus 4.6',
description:
'Most powerful Claude model excelling in complex reasoning, coding, and agentic tasks',
provider: ModelProvider.ANTHROPIC,
inputCostPer1kTokensInCents: 0.5,
outputCostPer1kTokensInCents: 2.5,
'Flagship Claude model for software engineering, scientific reasoning, and agent teams',
modelFamily: ModelFamily.ANTHROPIC,
inferenceProvider: InferenceProvider.ANTHROPIC,
inputCostPerMillionTokens: 5.0,
outputCostPerMillionTokens: 25.0,
cachedInputCostPerMillionTokens: 0.5,
cacheCreationCostPerMillionTokens: 6.25,
longContextCost: {
inputCostPerMillionTokens: 10.0,
outputCostPerMillionTokens: 37.5,
cachedInputCostPerMillionTokens: 1.0,
cacheCreationCostPerMillionTokens: 12.5,
thresholdTokens: 200_000,
},
contextWindowTokens: 1000000,
maxOutputTokens: 128000,
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-6',
label: 'Claude Sonnet 4.6',
description:
'Most capable Sonnet model with strong coding, computer use, and agent planning',
modelFamily: ModelFamily.ANTHROPIC,
inferenceProvider: InferenceProvider.ANTHROPIC,
inputCostPerMillionTokens: 3.0,
outputCostPerMillionTokens: 15.0,
cachedInputCostPerMillionTokens: 0.3,
cacheCreationCostPerMillionTokens: 3.75,
longContextCost: {
inputCostPerMillionTokens: 6.0,
outputCostPerMillionTokens: 22.5,
cachedInputCostPerMillionTokens: 0.6,
cacheCreationCostPerMillionTokens: 7.5,
thresholdTokens: 200_000,
},
contextWindowTokens: 200000,
maxOutputTokens: 64000,
supportedFileTypes: [
@@ -31,10 +80,20 @@ export const ANTHROPIC_MODELS: AIModelConfig[] = [
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,
'Previous gen Sonnet with 1M native context for long-context workflows',
modelFamily: ModelFamily.ANTHROPIC,
inferenceProvider: InferenceProvider.ANTHROPIC,
inputCostPerMillionTokens: 3.0,
outputCostPerMillionTokens: 15.0,
cachedInputCostPerMillionTokens: 0.3,
cacheCreationCostPerMillionTokens: 3.75,
longContextCost: {
inputCostPerMillionTokens: 6.0,
outputCostPerMillionTokens: 22.5,
cachedInputCostPerMillionTokens: 0.6,
cacheCreationCostPerMillionTokens: 7.5,
thresholdTokens: 200_000,
},
contextWindowTokens: 1000000,
maxOutputTokens: 64000,
supportedFileTypes: [
@@ -55,11 +114,13 @@ export const ANTHROPIC_MODELS: AIModelConfig[] = [
{
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,
description: 'Fast and cost-effective model for high-speed processing',
modelFamily: ModelFamily.ANTHROPIC,
inferenceProvider: InferenceProvider.ANTHROPIC,
inputCostPerMillionTokens: 1.0,
outputCostPerMillionTokens: 5.0,
cachedInputCostPerMillionTokens: 0.1,
cacheCreationCostPerMillionTokens: 1.25,
contextWindowTokens: 200000,
maxOutputTokens: 64000,
supportedFileTypes: [
@@ -77,43 +138,27 @@ export const ANTHROPIC_MODELS: AIModelConfig[] = [
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,
modelId: 'claude-opus-4-5-20251101',
label: 'Claude Opus 4.5',
description: 'Previous flagship model superseded by Opus 4.6',
modelFamily: ModelFamily.ANTHROPIC,
inferenceProvider: InferenceProvider.ANTHROPIC,
inputCostPerMillionTokens: 5.0,
outputCostPerMillionTokens: 25.0,
cachedInputCostPerMillionTokens: 0.5,
cacheCreationCostPerMillionTokens: 6.25,
longContextCost: {
inputCostPerMillionTokens: 10.0,
outputCostPerMillionTokens: 37.5,
cachedInputCostPerMillionTokens: 1.0,
cacheCreationCostPerMillionTokens: 12.5,
thresholdTokens: 200_000,
},
contextWindowTokens: 200000,
maxOutputTokens: 8192,
maxOutputTokens: 64000,
supportedFileTypes: [
'image/png',
'image/jpeg',
@@ -133,11 +178,20 @@ export const ANTHROPIC_MODELS: AIModelConfig[] = [
{
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,
description: 'Previous gen Sonnet superseded by Sonnet 4.6',
modelFamily: ModelFamily.ANTHROPIC,
inferenceProvider: InferenceProvider.ANTHROPIC,
inputCostPerMillionTokens: 3.0,
outputCostPerMillionTokens: 15.0,
cachedInputCostPerMillionTokens: 0.3,
cacheCreationCostPerMillionTokens: 3.75,
longContextCost: {
inputCostPerMillionTokens: 6.0,
outputCostPerMillionTokens: 22.5,
cachedInputCostPerMillionTokens: 0.6,
cacheCreationCostPerMillionTokens: 7.5,
thresholdTokens: 200_000,
},
contextWindowTokens: 200000,
maxOutputTokens: 8192,
supportedFileTypes: [
@@ -156,4 +210,60 @@ export const ANTHROPIC_MODELS: AIModelConfig[] = [
},
deprecated: true,
},
{
modelId: 'claude-opus-4-20250514',
label: 'Claude Opus 4',
description: 'Legacy Opus model with extended thinking',
modelFamily: ModelFamily.ANTHROPIC,
inferenceProvider: InferenceProvider.ANTHROPIC,
inputCostPerMillionTokens: 15.0,
outputCostPerMillionTokens: 75.0,
cachedInputCostPerMillionTokens: 1.5,
cacheCreationCostPerMillionTokens: 18.75,
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-3-5-haiku-20241022',
label: 'Claude Haiku 3.5',
description: 'Legacy fast model superseded by Haiku 4.5',
modelFamily: ModelFamily.ANTHROPIC,
inferenceProvider: InferenceProvider.ANTHROPIC,
inputCostPerMillionTokens: 0.8,
outputCostPerMillionTokens: 4.0,
cachedInputCostPerMillionTokens: 0.08,
cacheCreationCostPerMillionTokens: 1.0,
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: true,
},
];
@@ -0,0 +1,78 @@
import {
type AIModelConfig,
InferenceProvider,
ModelFamily,
} from './ai-models-types.const';
export const BEDROCK_MODELS: AIModelConfig[] = [
{
modelId: 'anthropic.claude-opus-4-6-v1',
label: 'Claude Opus 4.6 (Bedrock)',
description:
'Flagship Claude model via AWS Bedrock for enterprise deployments',
modelFamily: ModelFamily.ANTHROPIC,
inferenceProvider: InferenceProvider.BEDROCK,
inputCostPerMillionTokens: 5.0,
outputCostPerMillionTokens: 25.0,
cachedInputCostPerMillionTokens: 0.5,
cacheCreationCostPerMillionTokens: 6.25,
longContextCost: {
inputCostPerMillionTokens: 10.0,
outputCostPerMillionTokens: 37.5,
cachedInputCostPerMillionTokens: 1.0,
cacheCreationCostPerMillionTokens: 12.5,
thresholdTokens: 200_000,
},
contextWindowTokens: 1000000,
maxOutputTokens: 128000,
supportedFileTypes: [
'image/png',
'image/jpeg',
'image/gif',
'image/webp',
'application/pdf',
'text/plain',
'text/html',
'text/csv',
],
doesSupportThinking: true,
nativeCapabilities: {
webSearch: true,
},
},
{
modelId: 'anthropic.claude-sonnet-4-6',
label: 'Claude Sonnet 4.6 (Bedrock)',
description:
'Balanced Claude model via AWS Bedrock with strong coding and agent planning',
modelFamily: ModelFamily.ANTHROPIC,
inferenceProvider: InferenceProvider.BEDROCK,
inputCostPerMillionTokens: 3.0,
outputCostPerMillionTokens: 15.0,
cachedInputCostPerMillionTokens: 0.3,
cacheCreationCostPerMillionTokens: 3.75,
longContextCost: {
inputCostPerMillionTokens: 6.0,
outputCostPerMillionTokens: 22.5,
cachedInputCostPerMillionTokens: 0.6,
cacheCreationCostPerMillionTokens: 7.5,
thresholdTokens: 200_000,
},
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,
},
},
];
@@ -0,0 +1,81 @@
import {
type AIModelConfig,
InferenceProvider,
ModelFamily,
} from './ai-models-types.const';
export const GOOGLE_MODELS: AIModelConfig[] = [
// Active models
{
modelId: 'gemini-3.1-pro-preview',
label: 'Gemini 3.1 Pro',
description:
'Most advanced Gemini model for reasoning, coding, and agentic workflows',
modelFamily: ModelFamily.GOOGLE,
inferenceProvider: InferenceProvider.GOOGLE,
inputCostPerMillionTokens: 2.0,
outputCostPerMillionTokens: 12.0,
cachedInputCostPerMillionTokens: 0.2,
longContextCost: {
inputCostPerMillionTokens: 4.0,
outputCostPerMillionTokens: 18.0,
cachedInputCostPerMillionTokens: 0.4,
thresholdTokens: 200_000,
},
contextWindowTokens: 1048576,
maxOutputTokens: 65536,
supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
doesSupportThinking: true,
},
{
modelId: 'gemini-3-flash-preview',
label: 'Gemini 3 Flash',
description: 'Fast frontier-class Gemini model at low cost with 1M context',
modelFamily: ModelFamily.GOOGLE,
inferenceProvider: InferenceProvider.GOOGLE,
inputCostPerMillionTokens: 0.5,
outputCostPerMillionTokens: 3.0,
cachedInputCostPerMillionTokens: 0.05,
contextWindowTokens: 1048576,
maxOutputTokens: 65536,
supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
doesSupportThinking: true,
},
// Deprecated models - kept for backward compatibility with existing agents
{
modelId: 'gemini-2.5-pro',
label: 'Gemini 2.5 Pro',
description: 'Previous gen Gemini Pro superseded by 3.1 Pro',
modelFamily: ModelFamily.GOOGLE,
inferenceProvider: InferenceProvider.GOOGLE,
inputCostPerMillionTokens: 1.25,
outputCostPerMillionTokens: 10.0,
cachedInputCostPerMillionTokens: 0.315,
longContextCost: {
inputCostPerMillionTokens: 2.5,
outputCostPerMillionTokens: 15.0,
thresholdTokens: 200_000,
},
contextWindowTokens: 1048576,
maxOutputTokens: 65536,
supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
doesSupportThinking: true,
deprecated: true,
},
{
modelId: 'gemini-2.5-flash',
label: 'Gemini 2.5 Flash',
description: 'Previous gen Flash superseded by Gemini 3 Flash',
modelFamily: ModelFamily.GOOGLE,
inferenceProvider: InferenceProvider.GOOGLE,
inputCostPerMillionTokens: 0.3,
outputCostPerMillionTokens: 2.5,
cachedInputCostPerMillionTokens: 0.075,
contextWindowTokens: 1048576,
maxOutputTokens: 65536,
supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
doesSupportThinking: true,
deprecated: true,
},
];
@@ -1,14 +1,20 @@
import { type AIModelConfig, ModelProvider } from './ai-models-types.const';
import {
type AIModelConfig,
InferenceProvider,
ModelFamily,
} from './ai-models-types.const';
export const GROQ_MODELS: AIModelConfig[] = [
{
modelId: 'openai/gpt-oss-120b',
label: 'GPT-OSS 120B (Groq)',
description:
'Large-scale open-source model with browser search, served via Groq inference',
provider: ModelProvider.GROQ,
inputCostPer1kTokensInCents: 0.059,
outputCostPer1kTokensInCents: 0.079,
'Large-scale open-source model with ultra-fast inference via Groq',
modelFamily: ModelFamily.OPENAI,
inferenceProvider: InferenceProvider.GROQ,
inputCostPerMillionTokens: 0.15,
outputCostPerMillionTokens: 0.6,
cachedInputCostPerMillionTokens: 0.075,
contextWindowTokens: 128000,
maxOutputTokens: 16384,
},
@@ -0,0 +1,20 @@
import {
type AIModelConfig,
InferenceProvider,
ModelFamily,
} from './ai-models-types.const';
export const MISTRAL_MODELS: AIModelConfig[] = [
{
modelId: 'mistral-large-latest',
label: 'Mistral Large',
description:
'Flagship Mistral model with strong reasoning and 256K context',
modelFamily: ModelFamily.MISTRAL,
inferenceProvider: InferenceProvider.MISTRAL,
inputCostPerMillionTokens: 0.5,
outputCostPerMillionTokens: 1.5,
contextWindowTokens: 256000,
maxOutputTokens: 8192,
},
];
@@ -1,15 +1,54 @@
import { type AIModelConfig, ModelProvider } from './ai-models-types.const';
import {
type AIModelConfig,
InferenceProvider,
ModelFamily,
} from './ai-models-types.const';
export const OPENAI_MODELS: AIModelConfig[] = [
// Active models
{
modelId: 'gpt-5.2',
label: 'GPT-5.2',
description:
'Most advanced OpenAI model for coding, agentic tasks, and complex reasoning',
modelFamily: ModelFamily.OPENAI,
inferenceProvider: InferenceProvider.OPENAI,
inputCostPerMillionTokens: 1.75,
outputCostPerMillionTokens: 14.0,
cachedInputCostPerMillionTokens: 0.175,
contextWindowTokens: 400000,
maxOutputTokens: 128000,
supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
doesSupportThinking: true,
nativeCapabilities: {
webSearch: true,
},
},
{
modelId: 'gpt-5-mini',
label: 'GPT-5 Mini',
description: 'Fast and cost-efficient GPT-5 variant for well-defined tasks',
modelFamily: ModelFamily.OPENAI,
inferenceProvider: InferenceProvider.OPENAI,
inputCostPerMillionTokens: 0.25,
outputCostPerMillionTokens: 2.0,
cachedInputCostPerMillionTokens: 0.025,
contextWindowTokens: 128000,
maxOutputTokens: 32768,
supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
nativeCapabilities: {
webSearch: true,
},
},
{
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,
description: 'Strong model with 1M context, cost-effective output pricing',
modelFamily: ModelFamily.OPENAI,
inferenceProvider: InferenceProvider.OPENAI,
inputCostPerMillionTokens: 2.0,
outputCostPerMillionTokens: 8.0,
cachedInputCostPerMillionTokens: 0.5,
contextWindowTokens: 1047576,
maxOutputTokens: 32768,
supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
@@ -20,11 +59,12 @@ export const OPENAI_MODELS: AIModelConfig[] = [
{
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,
description: 'Budget-friendly model with 1M context for lightweight tasks',
modelFamily: ModelFamily.OPENAI,
inferenceProvider: InferenceProvider.OPENAI,
inputCostPerMillionTokens: 0.4,
outputCostPerMillionTokens: 1.6,
cachedInputCostPerMillionTokens: 0.1,
contextWindowTokens: 1047576,
maxOutputTokens: 32768,
supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
@@ -32,14 +72,18 @@ export const OPENAI_MODELS: AIModelConfig[] = [
webSearch: true,
},
},
// Deprecated models - kept for backward compatibility with existing agents
{
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,
'Reasoning model for complex queries, coding, math, and science',
modelFamily: ModelFamily.OPENAI,
inferenceProvider: InferenceProvider.OPENAI,
inputCostPerMillionTokens: 2.0,
outputCostPerMillionTokens: 8.0,
cachedInputCostPerMillionTokens: 0.5,
contextWindowTokens: 200000,
maxOutputTokens: 100000,
supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
@@ -47,15 +91,18 @@ export const OPENAI_MODELS: AIModelConfig[] = [
nativeCapabilities: {
webSearch: true,
},
deprecated: 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,
'Cost-effective reasoning model for math, coding, and visual tasks',
modelFamily: ModelFamily.OPENAI,
inferenceProvider: InferenceProvider.OPENAI,
inputCostPerMillionTokens: 1.1,
outputCostPerMillionTokens: 4.4,
cachedInputCostPerMillionTokens: 0.275,
contextWindowTokens: 200000,
maxOutputTokens: 100000,
supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
@@ -63,17 +110,18 @@ export const OPENAI_MODELS: AIModelConfig[] = [
nativeCapabilities: {
webSearch: true,
},
deprecated: 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,
'Previous generation multimodal model with strong reasoning and vision',
modelFamily: ModelFamily.OPENAI,
inferenceProvider: InferenceProvider.OPENAI,
inputCostPerMillionTokens: 2.5,
outputCostPerMillionTokens: 10.0,
cachedInputCostPerMillionTokens: 1.25,
contextWindowTokens: 128000,
maxOutputTokens: 16384,
supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
@@ -85,11 +133,12 @@ export const OPENAI_MODELS: AIModelConfig[] = [
{
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,
description: 'Previous generation fast model for lightweight tasks',
modelFamily: ModelFamily.OPENAI,
inferenceProvider: InferenceProvider.OPENAI,
inputCostPerMillionTokens: 0.15,
outputCostPerMillionTokens: 0.6,
cachedInputCostPerMillionTokens: 0.075,
contextWindowTokens: 128000,
maxOutputTokens: 16384,
supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
@@ -101,17 +150,14 @@ export const OPENAI_MODELS: AIModelConfig[] = [
{
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,
description: 'Legacy high-performance model with vision capabilities',
modelFamily: ModelFamily.OPENAI,
inferenceProvider: InferenceProvider.OPENAI,
inputCostPerMillionTokens: 10.0,
outputCostPerMillionTokens: 30.0,
contextWindowTokens: 128000,
maxOutputTokens: 4096,
supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
nativeCapabilities: {
webSearch: false,
},
deprecated: true,
},
];
@@ -1,32 +1,21 @@
import { type AIModelConfig, ModelProvider } from './ai-models-types.const';
import {
type AIModelConfig,
InferenceProvider,
ModelFamily,
} 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,
modelFamily: ModelFamily.XAI,
inferenceProvider: InferenceProvider.XAI,
inputCostPerMillionTokens: 3.0,
outputCostPerMillionTokens: 15.0,
cachedInputCostPerMillionTokens: 0.75,
contextWindowTokens: 256000,
maxOutputTokens: 8192,
supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
@@ -35,14 +24,37 @@ export const XAI_MODELS: AIModelConfig[] = [
twitterSearch: true,
},
},
{
modelId: 'grok-4-1-fast-reasoning',
label: 'Grok 4.1 Fast',
description:
'Next-generation tool-calling agent with 2M context for advanced agentic workflows',
modelFamily: ModelFamily.XAI,
inferenceProvider: InferenceProvider.XAI,
inputCostPerMillionTokens: 0.2,
outputCostPerMillionTokens: 0.5,
cachedInputCostPerMillionTokens: 0.05,
contextWindowTokens: 2000000,
maxOutputTokens: 8192,
supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
doesSupportThinking: true,
nativeCapabilities: {
webSearch: true,
twitterSearch: true,
},
},
// Deprecated models - kept for backward compatibility with existing agents
{
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,
modelFamily: ModelFamily.XAI,
inferenceProvider: InferenceProvider.XAI,
inputCostPerMillionTokens: 3.0,
outputCostPerMillionTokens: 15.0,
cachedInputCostPerMillionTokens: 0.75,
contextWindowTokens: 131072,
maxOutputTokens: 8192,
supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
@@ -50,15 +62,18 @@ export const XAI_MODELS: AIModelConfig[] = [
webSearch: true,
twitterSearch: true,
},
deprecated: 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,
modelFamily: ModelFamily.XAI,
inferenceProvider: InferenceProvider.XAI,
inputCostPerMillionTokens: 0.3,
outputCostPerMillionTokens: 0.5,
cachedInputCostPerMillionTokens: 0.07,
contextWindowTokens: 131072,
maxOutputTokens: 8192,
supportedFileTypes: ['image/png', 'image/jpeg', 'image/gif', 'image/webp'],
@@ -66,5 +81,6 @@ export const XAI_MODELS: AIModelConfig[] = [
webSearch: true,
twitterSearch: true,
},
deprecated: true,
},
];
@@ -6,23 +6,30 @@ import { ProviderOptions } from '@ai-sdk/provider-utils';
import { ToolSet } from 'ai';
import { AGENT_CONFIG } from 'src/engine/metadata-modules/ai/ai-agent/constants/agent-config.const';
import { ModelProvider } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import { RegisteredAIModel } from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
import { InferenceProvider } from 'src/engine/metadata-modules/ai/ai-models/constants/ai-models.const';
import {
AiModelRegistryService,
RegisteredAIModel,
} from 'src/engine/metadata-modules/ai/ai-models/services/ai-model-registry.service';
import { FlatAgentWithRoleId } from 'src/engine/metadata-modules/flat-agent/types/flat-agent.type';
@Injectable()
export class AgentModelConfigService {
constructor() {}
constructor(
private readonly aiModelRegistryService: AiModelRegistryService,
) {}
getProviderOptions(
model: RegisteredAIModel,
agent: FlatAgentWithRoleId,
): ProviderOptions {
switch (model.provider) {
case ModelProvider.XAI:
switch (model.inferenceProvider) {
case InferenceProvider.XAI:
return this.getXaiProviderOptions(agent);
case ModelProvider.ANTHROPIC:
case InferenceProvider.ANTHROPIC:
return this.getAnthropicProviderOptions(model);
case InferenceProvider.BEDROCK:
return this.getBedrockProviderOptions(model);
default:
return {};
}
@@ -38,13 +45,25 @@ export class AgentModelConfigService {
return tools;
}
switch (model.provider) {
case ModelProvider.ANTHROPIC:
switch (model.inferenceProvider) {
case InferenceProvider.ANTHROPIC:
if (agent.modelConfiguration.webSearch?.enabled) {
tools.web_search = anthropic.tools.webSearch_20250305();
}
break;
case ModelProvider.OPENAI:
case InferenceProvider.BEDROCK: {
if (agent.modelConfiguration.webSearch?.enabled) {
const bedrockProvider =
this.aiModelRegistryService.getBedrockProvider();
if (bedrockProvider) {
tools.web_search =
bedrockProvider.tools.webSearch_20250305() as ToolSet[string];
}
}
break;
}
case InferenceProvider.OPENAI:
if (agent.modelConfiguration.webSearch?.enabled) {
tools.web_search = openai.tools.webSearch();
}
@@ -99,4 +118,19 @@ export class AgentModelConfigService {
},
};
}
private getBedrockProviderOptions(model: RegisteredAIModel): ProviderOptions {
if (!model.doesSupportThinking) {
return {};
}
return {
bedrock: {
thinking: {
type: 'enabled',
budgetTokens: AGENT_CONFIG.REASONING_BUDGET_TOKENS,
},
},
};
}
}
@@ -1,7 +1,13 @@
import { Injectable } from '@nestjs/common';
import {
createAmazonBedrock,
type AmazonBedrockProvider,
} from '@ai-sdk/amazon-bedrock';
import { anthropic } from '@ai-sdk/anthropic';
import { google } from '@ai-sdk/google';
import { groq } from '@ai-sdk/groq';
import { mistral } from '@ai-sdk/mistral';
import { createOpenAI, openai } from '@ai-sdk/openai';
import { xai } from '@ai-sdk/xai';
import { type LanguageModel } from 'ai';
@@ -15,17 +21,21 @@ import {
AI_MODELS,
DEFAULT_FAST_MODEL,
DEFAULT_SMART_MODEL,
ModelProvider,
InferenceProvider,
ModelFamily,
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 { BEDROCK_MODELS } from 'src/engine/metadata-modules/ai/ai-models/constants/bedrock-models.const';
import { GOOGLE_MODELS } from 'src/engine/metadata-modules/ai/ai-models/constants/google-models.const';
import { GROQ_MODELS } from 'src/engine/metadata-modules/ai/ai-models/constants/groq-models.const';
import { MISTRAL_MODELS } from 'src/engine/metadata-modules/ai/ai-models/constants/mistral-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;
provider: ModelProvider;
inferenceProvider: InferenceProvider;
model: LanguageModel;
doesSupportThinking?: boolean;
}
@@ -33,13 +43,19 @@ export interface RegisteredAIModel {
@Injectable()
export class AiModelRegistryService {
private modelRegistry: Map<string, RegisteredAIModel> = new Map();
private bedrockProvider: AmazonBedrockProvider | null = null;
constructor(private twentyConfigService: TwentyConfigService) {
this.buildModelRegistry();
}
getBedrockProvider(): AmazonBedrockProvider | null {
return this.bedrockProvider;
}
private buildModelRegistry(): void {
this.modelRegistry.clear();
this.bedrockProvider = null;
const openaiApiKey = this.twentyConfigService.get('OPENAI_API_KEY');
@@ -65,6 +81,24 @@ export class AiModelRegistryService {
this.registerGroqModels();
}
const googleApiKey = this.twentyConfigService.get('GOOGLE_API_KEY');
if (googleApiKey) {
this.registerGoogleModels();
}
const mistralApiKey = this.twentyConfigService.get('MISTRAL_API_KEY');
if (mistralApiKey) {
this.registerMistralModels();
}
const bedrockRegion = this.twentyConfigService.get('AWS_BEDROCK_REGION');
if (bedrockRegion) {
this.registerBedrockModels(bedrockRegion);
}
const openaiCompatibleBaseUrl = this.twentyConfigService.get(
'OPENAI_COMPATIBLE_BASE_URL',
);
@@ -84,7 +118,7 @@ export class AiModelRegistryService {
OPENAI_MODELS.forEach((modelConfig) => {
this.modelRegistry.set(modelConfig.modelId, {
modelId: modelConfig.modelId,
provider: ModelProvider.OPENAI,
inferenceProvider: InferenceProvider.OPENAI,
model: openai(modelConfig.modelId),
doesSupportThinking: modelConfig.doesSupportThinking,
});
@@ -95,7 +129,7 @@ export class AiModelRegistryService {
ANTHROPIC_MODELS.forEach((modelConfig) => {
this.modelRegistry.set(modelConfig.modelId, {
modelId: modelConfig.modelId,
provider: ModelProvider.ANTHROPIC,
inferenceProvider: InferenceProvider.ANTHROPIC,
model: anthropic(modelConfig.modelId),
doesSupportThinking: modelConfig.doesSupportThinking,
});
@@ -106,7 +140,7 @@ export class AiModelRegistryService {
XAI_MODELS.forEach((modelConfig) => {
this.modelRegistry.set(modelConfig.modelId, {
modelId: modelConfig.modelId,
provider: ModelProvider.XAI,
inferenceProvider: InferenceProvider.XAI,
model: xai(modelConfig.modelId),
doesSupportThinking: modelConfig.doesSupportThinking,
});
@@ -117,13 +151,63 @@ export class AiModelRegistryService {
GROQ_MODELS.forEach((modelConfig) => {
this.modelRegistry.set(modelConfig.modelId, {
modelId: modelConfig.modelId,
provider: ModelProvider.GROQ,
inferenceProvider: InferenceProvider.GROQ,
model: groq(modelConfig.modelId),
doesSupportThinking: modelConfig.doesSupportThinking,
});
});
}
private registerGoogleModels(): void {
GOOGLE_MODELS.forEach((modelConfig) => {
this.modelRegistry.set(modelConfig.modelId, {
modelId: modelConfig.modelId,
inferenceProvider: InferenceProvider.GOOGLE,
model: google(modelConfig.modelId),
doesSupportThinking: modelConfig.doesSupportThinking,
});
});
}
private registerMistralModels(): void {
MISTRAL_MODELS.forEach((modelConfig) => {
this.modelRegistry.set(modelConfig.modelId, {
modelId: modelConfig.modelId,
inferenceProvider: InferenceProvider.MISTRAL,
model: mistral(modelConfig.modelId),
doesSupportThinking: modelConfig.doesSupportThinking,
});
});
}
private registerBedrockModels(region: string): void {
const accessKeyId = this.twentyConfigService.get(
'AWS_BEDROCK_ACCESS_KEY_ID',
);
const secretAccessKey = this.twentyConfigService.get(
'AWS_BEDROCK_SECRET_ACCESS_KEY',
);
const sessionToken = this.twentyConfigService.get(
'AWS_BEDROCK_SESSION_TOKEN',
);
this.bedrockProvider = createAmazonBedrock({
region,
...(accessKeyId && secretAccessKey
? { accessKeyId, secretAccessKey, sessionToken }
: {}),
});
BEDROCK_MODELS.forEach((modelConfig) => {
this.modelRegistry.set(modelConfig.modelId, {
modelId: modelConfig.modelId,
inferenceProvider: InferenceProvider.BEDROCK,
model: this.bedrockProvider!(modelConfig.modelId),
doesSupportThinking: modelConfig.doesSupportThinking,
});
});
}
private registerOpenAICompatibleModels(
baseUrl: string,
modelNamesString: string,
@@ -142,7 +226,7 @@ export class AiModelRegistryService {
modelNames.forEach((modelId) => {
this.modelRegistry.set(modelId, {
modelId,
provider: ModelProvider.OPENAI_COMPATIBLE,
inferenceProvider: InferenceProvider.OPENAI_COMPATIBLE,
model: provider(modelId),
});
});
@@ -189,7 +273,7 @@ export class AiModelRegistryService {
if (!model) {
throw new AgentException(
'No AI models are available. Please configure at least one AI provider API key (OPENAI_API_KEY, ANTHROPIC_API_KEY, XAI_API_KEY, or GROQ_API_KEY).',
'No AI models are available. Please configure at least one AI provider (OPENAI_API_KEY, ANTHROPIC_API_KEY, AWS_BEDROCK_REGION, GOOGLE_API_KEY, XAI_API_KEY, GROQ_API_KEY, or MISTRAL_API_KEY).',
AgentExceptionCode.API_KEY_NOT_CONFIGURED,
);
}
@@ -211,7 +295,7 @@ export class AiModelRegistryService {
if (!model) {
throw new AgentException(
'No AI models are available. Please configure at least one AI provider API key (OPENAI_API_KEY, ANTHROPIC_API_KEY, XAI_API_KEY, or GROQ_API_KEY).',
'No AI models are available. Please configure at least one AI provider (OPENAI_API_KEY, ANTHROPIC_API_KEY, AWS_BEDROCK_REGION, GOOGLE_API_KEY, XAI_API_KEY, GROQ_API_KEY, or MISTRAL_API_KEY).',
AgentExceptionCode.API_KEY_NOT_CONFIGURED,
);
}
@@ -221,7 +305,6 @@ export class AiModelRegistryService {
getEffectiveModelConfig(modelId: string): AIModelConfig {
if (modelId === DEFAULT_FAST_MODEL || modelId === DEFAULT_SMART_MODEL) {
// getDefaultSpeedModel/getDefaultPerformanceModel will throw AgentException if no models available
const defaultModel =
modelId === DEFAULT_FAST_MODEL
? this.getDefaultSpeedModel()
@@ -265,15 +348,29 @@ export class AiModelRegistryService {
modelId: registeredModel.modelId,
label: registeredModel.modelId,
description: `Custom model: ${registeredModel.modelId}`,
provider: registeredModel.provider,
inputCostPer1kTokensInCents: 0,
outputCostPer1kTokensInCents: 0,
modelFamily: this.inferModelFamily(registeredModel.inferenceProvider),
inferenceProvider: registeredModel.inferenceProvider,
inputCostPerMillionTokens: 0,
outputCostPerMillionTokens: 0,
contextWindowTokens: 128000,
maxOutputTokens: 4096,
};
}
// Force refresh the registry (useful if config changes)
private inferModelFamily(inferenceProvider: InferenceProvider): ModelFamily {
const providerToFamily: Partial<Record<InferenceProvider, ModelFamily>> = {
[InferenceProvider.OPENAI]: ModelFamily.OPENAI,
[InferenceProvider.ANTHROPIC]: ModelFamily.ANTHROPIC,
[InferenceProvider.BEDROCK]: ModelFamily.ANTHROPIC,
[InferenceProvider.GOOGLE]: ModelFamily.GOOGLE,
[InferenceProvider.MISTRAL]: ModelFamily.MISTRAL,
[InferenceProvider.XAI]: ModelFamily.XAI,
[InferenceProvider.GROQ]: ModelFamily.OPENAI,
};
return providerToFamily[inferenceProvider] ?? ModelFamily.OPENAI;
}
refreshRegistry(): void {
this.buildModelRegistry();
}
@@ -283,7 +380,7 @@ export class AiModelRegistryService {
agent?.modelId ?? DEFAULT_SMART_MODEL,
);
await this.validateApiKey(aiModel.provider);
await this.validateApiKey(aiModel.inferenceProvider);
const registeredModel = this.getModel(aiModel.modelId);
if (!registeredModel) {
@@ -296,23 +393,32 @@ export class AiModelRegistryService {
return registeredModel;
}
async validateApiKey(provider: ModelProvider): Promise<void> {
async validateApiKey(inferenceProvider: InferenceProvider): Promise<void> {
let apiKey: string | undefined;
switch (provider) {
case ModelProvider.OPENAI:
switch (inferenceProvider) {
case InferenceProvider.OPENAI:
apiKey = this.twentyConfigService.get('OPENAI_API_KEY');
break;
case ModelProvider.ANTHROPIC:
case InferenceProvider.ANTHROPIC:
apiKey = this.twentyConfigService.get('ANTHROPIC_API_KEY');
break;
case ModelProvider.XAI:
case InferenceProvider.XAI:
apiKey = this.twentyConfigService.get('XAI_API_KEY');
break;
case ModelProvider.GROQ:
case InferenceProvider.GROQ:
apiKey = this.twentyConfigService.get('GROQ_API_KEY');
break;
case ModelProvider.OPENAI_COMPATIBLE:
case InferenceProvider.GOOGLE:
apiKey = this.twentyConfigService.get('GOOGLE_API_KEY');
break;
case InferenceProvider.MISTRAL:
apiKey = this.twentyConfigService.get('MISTRAL_API_KEY');
break;
case InferenceProvider.BEDROCK:
apiKey = this.twentyConfigService.get('AWS_BEDROCK_REGION');
break;
case InferenceProvider.OPENAI_COMPATIBLE:
apiKey = this.twentyConfigService.get('OPENAI_COMPATIBLE_API_KEY');
break;
default:
@@ -321,7 +427,7 @@ export class AiModelRegistryService {
if (!apiKey) {
throw new AgentException(
`${provider.toUpperCase()} API key not configured. Please set the appropriate environment variable.`,
`${inferenceProvider.toUpperCase()} API key not configured. Please set the appropriate environment variable.`,
AgentExceptionCode.API_KEY_NOT_CONFIGURED,
);
}