remove ai-model-preferences var env and config (#20859)

Split the single AI_MODEL_PREFERENCES JSON config into 4 array configs
and migrates existing workspace data.
This commit is contained in:
Etienne
2026-05-27 08:47:47 +02:00
committed by GitHub
parent 9d6c5b7d58
commit 4a82cddad6
13 changed files with 449 additions and 74 deletions
@@ -1,59 +1,26 @@
import { Injectable, Logger, type OnModuleInit } from '@nestjs/common';
import { Injectable } from '@nestjs/common';
import { FileStorageDriverFactory } from 'src/engine/core-modules/file-storage/file-storage-driver.factory';
import { ConfigSource } from 'src/engine/core-modules/twenty-config/enums/config-source.enum';
import { TwentyConfigService } from 'src/engine/core-modules/twenty-config/twenty-config.service';
import { aiModelPreferencesSchema } from 'src/engine/metadata-modules/ai/ai-models/types/ai-model-preferences.schema';
import { type AiModelPreferences } from 'src/engine/metadata-modules/ai/ai-models/types/ai-model-preferences.type';
import { AiModelRole } from 'src/engine/metadata-modules/ai/ai-models/types/ai-model-role.enum';
import { streamToBuffer } from 'src/utils/stream-to-buffer';
@Injectable()
export class AiModelPreferencesService implements OnModuleInit {
private readonly logger = new Logger(AiModelPreferencesService.name);
private filePreferences: AiModelPreferences | null = null;
constructor(
private readonly twentyConfigService: TwentyConfigService,
private readonly fileStorageDriverFactory: FileStorageDriverFactory,
) {}
async onModuleInit(): Promise<void> {
const storagePath = this.twentyConfigService.get(
'AI_MODEL_PREFERENCES_STORAGE_PATH',
);
if (!storagePath) {
return;
}
try {
this.filePreferences = await this.fetchPreferences(storagePath);
this.logger.log(
`AI_MODEL_PREF - Loaded AI model preferences from storage: ${storagePath}`,
);
} catch (error: unknown) {
const message = error instanceof Error ? error.message : String(error);
this.logger.error(
`AI_MODEL_PREF - Failed to load AI model preferences from storage: ${message}`,
);
}
}
export class AiModelPreferencesService {
constructor(private readonly twentyConfigService: TwentyConfigService) {}
getPreferences(): AiModelPreferences {
const { source } =
this.twentyConfigService.getVariableWithMetadata(
'AI_MODEL_PREFERENCES',
) ?? {};
if (source !== ConfigSource.DEFAULT) {
return this.twentyConfigService.get('AI_MODEL_PREFERENCES');
}
return (
this.filePreferences ??
this.twentyConfigService.get('AI_MODEL_PREFERENCES')
);
return {
defaultFastModels: this.twentyConfigService.get('AI_MODELS_DEFAULT_FAST'),
defaultSmartModels: this.twentyConfigService.get(
'AI_MODELS_DEFAULT_SMART',
),
recommendedModels: this.twentyConfigService.get(
'AI_MODELS_DEFAULT_RECOMMENDED',
),
disabledModels: this.twentyConfigService.get(
'AI_MODELS_DEFAULT_DISABLED',
),
};
}
getRecommendedModelIds(): Set<string> {
@@ -130,16 +97,23 @@ export class AiModelPreferencesService implements OnModuleInit {
}
private async persistPreferences(prefs: AiModelPreferences): Promise<void> {
await this.twentyConfigService.set('AI_MODEL_PREFERENCES', prefs);
}
private async fetchPreferences(
filePath: string,
): Promise<AiModelPreferences> {
const driver = this.fileStorageDriverFactory.getCurrentDriver();
const stream = await driver.readFile({ filePath });
const body = (await streamToBuffer(stream)).toString('utf-8');
return aiModelPreferencesSchema.parse(JSON.parse(body));
await Promise.all([
this.twentyConfigService.set(
'AI_MODELS_DEFAULT_FAST',
prefs.defaultFastModels ?? [],
),
this.twentyConfigService.set(
'AI_MODELS_DEFAULT_SMART',
prefs.defaultSmartModels ?? [],
),
this.twentyConfigService.set(
'AI_MODELS_DEFAULT_RECOMMENDED',
prefs.recommendedModels ?? [],
),
this.twentyConfigService.set(
'AI_MODELS_DEFAULT_DISABLED',
prefs.disabledModels ?? [],
),
]);
}
}
@@ -2,7 +2,7 @@
// instead of hardcoding model IDs that become stale as models evolve
import { type AiModelPreferences } from 'src/engine/metadata-modules/ai/ai-models/types/ai-model-preferences.type';
const DEFAULT_FAST_MODELS = [
export const DEFAULT_FAST_MODELS = [
'openai/gpt-5-mini',
'anthropic/claude-haiku-4-5-20251001',
'google/gemini-3-flash-preview',
@@ -10,7 +10,7 @@ const DEFAULT_FAST_MODELS = [
'mistral/mistral-large-latest',
];
const DEFAULT_SMART_MODELS = [
export const DEFAULT_SMART_MODELS = [
'openai/gpt-5.2',
'anthropic/claude-sonnet-4-6',
'google/gemini-3.1-pro-preview',
@@ -18,7 +18,7 @@ const DEFAULT_SMART_MODELS = [
'mistral/mistral-large-latest',
];
const DEFAULT_RECOMMENDED_MODELS = [
export const DEFAULT_RECOMMENDED_MODELS = [
'openai/gpt-5.2',
'openai/gpt-4.1',
'anthropic/claude-opus-4-6',
@@ -27,6 +27,8 @@ const DEFAULT_RECOMMENDED_MODELS = [
'xai/grok-4',
];
export const DEFAULT_DISABLED_MODELS: string[] = [];
export const DEFAULT_MODEL_PREFERENCES: AiModelPreferences = {
disabledModels: [],
recommendedModels: DEFAULT_RECOMMENDED_MODELS,