Refresh AI model setup (#13171)

Instead of initializing model at start time we do it at run time to be
able to swap model provider more easily.

Also introduce a third driver for openai-compatible providers, which
among other allows for local models with Ollama
This commit is contained in:
Félix Malfait
2025-07-11 13:09:54 +02:00
committed by GitHub
parent fd13bb0258
commit 0a93468b95
32 changed files with 566 additions and 417 deletions
@@ -3,19 +3,13 @@ import { Test, TestingModule } from '@nestjs/testing';
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
import { CaptchaDriverType } from 'src/engine/core-modules/captcha/interfaces';
import { ClientConfigService } from 'src/engine/core-modules/client-config/services/client-config.service';
import { DomainManagerService } from 'src/engine/core-modules/domain-manager/services/domain-manager.service';
import { PUBLIC_FEATURE_FLAGS } from 'src/engine/core-modules/feature-flag/constants/public-feature-flag.const';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
jest.mock(
'src/engine/core-modules/ai/utils/get-ai-models-with-auto.util',
() => ({
getAIModelsWithAuto: jest.fn(() => []),
}),
);
describe('ClientConfigService', () => {
let service: ClientConfigService;
let twentyConfigService: TwentyConfigService;
@@ -37,6 +31,12 @@ describe('ClientConfigService', () => {
getFrontUrl: jest.fn(),
},
},
{
provide: AiModelRegistryService,
useValue: {
getAvailableModels: jest.fn().mockReturnValue([]),
},
},
],
}).compile();
@@ -3,9 +3,12 @@ import { Injectable } from '@nestjs/common';
import { NodeEnvironment } from 'src/engine/core-modules/twenty-config/interfaces/node-environment.interface';
import { SupportDriver } from 'src/engine/core-modules/twenty-config/interfaces/support.interface';
import { ModelProvider } from 'src/engine/core-modules/ai/constants/ai-models.const';
import {
AI_MODELS,
ModelProvider,
} from 'src/engine/core-modules/ai/constants/ai-models.const';
import { AiModelRegistryService } from 'src/engine/core-modules/ai/services/ai-model-registry.service';
import { convertCentsToBillingCredits } from 'src/engine/core-modules/ai/utils/convert-cents-to-billing-credits.util';
import { getAIModelsWithAuto } from 'src/engine/core-modules/ai/utils/get-ai-models-with-auto.util';
import {
ClientAIModelConfig,
ClientConfig,
@@ -19,41 +22,49 @@ export class ClientConfigService {
constructor(
private twentyConfigService: TwentyConfigService,
private domainManagerService: DomainManagerService,
private aiModelRegistryService: AiModelRegistryService,
) {}
async getClientConfig(): Promise<ClientConfig> {
const captchaProvider = this.twentyConfigService.get('CAPTCHA_DRIVER');
const supportDriver = this.twentyConfigService.get('SUPPORT_DRIVER');
const openaiApiKey = this.twentyConfigService.get('OPENAI_API_KEY');
const anthropicApiKey = this.twentyConfigService.get('ANTHROPIC_API_KEY');
const aiModels = getAIModelsWithAuto().reduce<ClientAIModelConfig[]>(
(acc, model) => {
const isAvailable =
(model.provider === ModelProvider.OPENAI && openaiApiKey) ||
(model.provider === ModelProvider.ANTHROPIC && anthropicApiKey);
const availableModels = this.aiModelRegistryService.getAvailableModels();
if (!isAvailable) {
return acc;
}
const aiModels: ClientAIModelConfig[] = availableModels.map(
(registeredModel) => {
const builtInModel = AI_MODELS.find(
(m) => m.modelId === registeredModel.modelId,
);
acc.push({
modelId: model.modelId,
label: model.label,
provider: model.provider,
inputCostPer1kTokensInCredits: convertCentsToBillingCredits(
model.inputCostPer1kTokensInCents,
),
outputCostPer1kTokensInCredits: convertCentsToBillingCredits(
model.outputCostPer1kTokensInCents,
),
});
return acc;
return {
modelId: registeredModel.modelId,
label: builtInModel?.label || registeredModel.modelId,
provider: registeredModel.provider,
inputCostPer1kTokensInCredits: builtInModel
? convertCentsToBillingCredits(
builtInModel.inputCostPer1kTokensInCents,
)
: 0,
outputCostPer1kTokensInCredits: builtInModel
? convertCentsToBillingCredits(
builtInModel.outputCostPer1kTokensInCents,
)
: 0,
};
},
[],
);
if (aiModels.length > 0) {
aiModels.unshift({
modelId: 'auto',
label: 'Auto',
provider: ModelProvider.NONE,
inputCostPer1kTokensInCredits: 0,
outputCostPer1kTokensInCredits: 0,
});
}
const clientConfig: ClientConfig = {
billing: {
isBillingEnabled: this.twentyConfigService.get('IS_BILLING_ENABLED'),