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:
@@ -42,9 +42,13 @@ import {
|
||||
ConfigVariableException,
|
||||
ConfigVariableExceptionCode,
|
||||
} from 'src/engine/core-modules/twenty-config/twenty-config.exception';
|
||||
import { type AiModelPreferences } from 'src/engine/metadata-modules/ai/ai-models/types/ai-model-preferences.type';
|
||||
import { type AiProvidersConfig } from 'src/engine/metadata-modules/ai/ai-models/types/ai-providers-config.type';
|
||||
import { DEFAULT_MODEL_PREFERENCES } from 'src/engine/metadata-modules/ai/ai-models/utils/load-default-model-preferences.util';
|
||||
import {
|
||||
DEFAULT_DISABLED_MODELS,
|
||||
DEFAULT_FAST_MODELS,
|
||||
DEFAULT_RECOMMENDED_MODELS,
|
||||
DEFAULT_SMART_MODELS,
|
||||
} from 'src/engine/metadata-modules/ai/ai-models/utils/load-default-model-preferences.util';
|
||||
|
||||
export class ConfigVariables {
|
||||
@ConfigVariablesMetadata({
|
||||
@@ -1435,20 +1439,38 @@ export class ConfigVariables {
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.LLM,
|
||||
description:
|
||||
'AI model admin preferences: disabled models, recommended models, and default fast/smart model lists. Managed via admin panel or env.',
|
||||
type: ConfigVariableType.JSON,
|
||||
'Ordered list of fast model IDs to use as defaults. Managed via admin panel or env.',
|
||||
type: ConfigVariableType.ARRAY,
|
||||
})
|
||||
@IsOptional()
|
||||
AI_MODEL_PREFERENCES: AiModelPreferences = DEFAULT_MODEL_PREFERENCES;
|
||||
AI_MODELS_DEFAULT_FAST: string[] = DEFAULT_FAST_MODELS;
|
||||
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.LLM,
|
||||
description:
|
||||
'Storage path for AI model preferences fallback (e.g. config/ai-model-preferences.json). Loaded at startup and used only when no value is set via AI_MODEL_PREFERENCES env var or the database.',
|
||||
type: ConfigVariableType.STRING,
|
||||
'Ordered list of smart model IDs to use as defaults. Managed via admin panel or env.',
|
||||
type: ConfigVariableType.ARRAY,
|
||||
})
|
||||
@IsOptional()
|
||||
AI_MODEL_PREFERENCES_STORAGE_PATH?: string;
|
||||
AI_MODELS_DEFAULT_SMART: string[] = DEFAULT_SMART_MODELS;
|
||||
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.LLM,
|
||||
description:
|
||||
'List of recommended model IDs shown to workspaces using curated model selection. Managed via admin panel or env.',
|
||||
type: ConfigVariableType.ARRAY,
|
||||
})
|
||||
@IsOptional()
|
||||
AI_MODELS_DEFAULT_RECOMMENDED: string[] = DEFAULT_RECOMMENDED_MODELS;
|
||||
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.LLM,
|
||||
description:
|
||||
'List of model IDs disabled by default. Disabled models cannot be used by any workspace. Managed via admin panel or env.',
|
||||
type: ConfigVariableType.ARRAY,
|
||||
})
|
||||
@IsOptional()
|
||||
AI_MODELS_DEFAULT_DISABLED: string[] = DEFAULT_DISABLED_MODELS;
|
||||
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.SERVER_CONFIG,
|
||||
|
||||
+2
-1
@@ -1,3 +1,4 @@
|
||||
import { isNonEmptyString } from '@sniptt/guards';
|
||||
import { Transform } from 'class-transformer';
|
||||
|
||||
import { MeterDriver } from 'src/engine/core-modules/metrics/types/meter-driver.type';
|
||||
@@ -6,7 +7,7 @@ export const CastToMeterDriverArray = () =>
|
||||
Transform(({ value }: { value: string }) => toMeterDriverArray(value));
|
||||
|
||||
const toMeterDriverArray = (value: string | undefined) => {
|
||||
if (typeof value === 'string') {
|
||||
if (isNonEmptyString(value)) {
|
||||
const rawMeterDrivers = value.split(',').map((driver) => driver.trim());
|
||||
const isInvalid = rawMeterDrivers.some(
|
||||
(driver) => !Object.values(MeterDriver).includes(driver as MeterDriver),
|
||||
|
||||
+74
@@ -1,4 +1,9 @@
|
||||
import { plainToInstance } from 'class-transformer';
|
||||
import { validateSync } from 'class-validator';
|
||||
|
||||
import { ConfigVariablesMetadata } from 'src/engine/core-modules/twenty-config/decorators/config-variables-metadata.decorator';
|
||||
import { ConfigVariableType } from 'src/engine/core-modules/twenty-config/enums/config-variable-type.enum';
|
||||
import { ConfigVariablesGroup } from 'src/engine/core-modules/twenty-config/enums/config-variables-group.enum';
|
||||
import { typeTransformers } from 'src/engine/core-modules/twenty-config/utils/type-transformers.registry';
|
||||
|
||||
describe('Type Transformers Registry', () => {
|
||||
@@ -141,6 +146,75 @@ describe('Type Transformers Registry', () => {
|
||||
}).toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('class-transformer transformer (env-loading path)', () => {
|
||||
class TestArrayConfig {
|
||||
@ConfigVariablesMetadata({
|
||||
group: ConfigVariablesGroup.ADVANCED_SETTINGS,
|
||||
description: 'Test array',
|
||||
type: ConfigVariableType.ARRAY,
|
||||
})
|
||||
ARRAY_VALUE: string[] = [];
|
||||
}
|
||||
|
||||
const transformAndValidate = (raw: unknown) => {
|
||||
const instance = plainToInstance(TestArrayConfig, {
|
||||
ARRAY_VALUE: raw,
|
||||
});
|
||||
const errors = validateSync(instance, { strictGroups: true });
|
||||
|
||||
return { instance, errors };
|
||||
};
|
||||
|
||||
it('should parse comma-separated string into array', () => {
|
||||
const { instance, errors } = transformAndValidate('a,b,c');
|
||||
|
||||
expect(instance.ARRAY_VALUE).toEqual(['a', 'b', 'c']);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should parse JSON-encoded array string', () => {
|
||||
const { instance, errors } = transformAndValidate('["a","b","c"]');
|
||||
|
||||
expect(instance.ARRAY_VALUE).toEqual(['a', 'b', 'c']);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should parse empty JSON array string', () => {
|
||||
const { instance, errors } = transformAndValidate('[]');
|
||||
|
||||
expect(instance.ARRAY_VALUE).toEqual([]);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should wrap a single value in an array', () => {
|
||||
const { instance, errors } = transformAndValidate('openai/gpt-4.1');
|
||||
|
||||
expect(instance.ARRAY_VALUE).toEqual(['openai/gpt-4.1']);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should trim whitespace around comma-separated items', () => {
|
||||
const { instance, errors } = transformAndValidate(' a , b , c ');
|
||||
|
||||
expect(instance.ARRAY_VALUE).toEqual(['a', 'b', 'c']);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should drop empty entries from comma-separated input', () => {
|
||||
const { instance, errors } = transformAndValidate('a,,b,');
|
||||
|
||||
expect(instance.ARRAY_VALUE).toEqual(['a', 'b']);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
|
||||
it('should keep already-array values unchanged', () => {
|
||||
const { instance, errors } = transformAndValidate(['a', 'b']);
|
||||
|
||||
expect(instance.ARRAY_VALUE).toEqual(['a', 'b']);
|
||||
expect(errors).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('Enum Transformer', () => {
|
||||
|
||||
+18
-1
@@ -2,9 +2,11 @@ import { Transform } from 'class-transformer';
|
||||
import {
|
||||
IsArray,
|
||||
IsBoolean,
|
||||
isDefined,
|
||||
IsEnum,
|
||||
IsNumber,
|
||||
IsObject,
|
||||
isString,
|
||||
IsString,
|
||||
} from 'class-validator';
|
||||
|
||||
@@ -15,6 +17,7 @@ import {
|
||||
} from 'src/engine/core-modules/twenty-config/twenty-config.exception';
|
||||
import { type ConfigVariableOptions } from 'src/engine/core-modules/twenty-config/types/config-variable-options.type';
|
||||
import { configTransformers } from 'src/engine/core-modules/twenty-config/utils/config-transformers.util';
|
||||
import { tryParseJsonArray } from 'src/utils/try-parse-json-array';
|
||||
|
||||
export interface TypeTransformer<T> {
|
||||
toApp: (value: unknown, options?: ConfigVariableOptions) => T | undefined;
|
||||
@@ -193,7 +196,21 @@ export const typeTransformers: Record<
|
||||
|
||||
getValidators: (): PropertyDecorator[] => [IsArray()],
|
||||
|
||||
getTransformers: (): PropertyDecorator[] => [],
|
||||
getTransformers: (): PropertyDecorator[] => [
|
||||
Transform(({ value }) => {
|
||||
if (Array.isArray(value)) return value;
|
||||
if (!isString(value)) return value;
|
||||
|
||||
const fromJson = tryParseJsonArray(value);
|
||||
|
||||
if (isDefined(fromJson)) return fromJson;
|
||||
|
||||
return value
|
||||
.split(',')
|
||||
.map((item) => item.trim())
|
||||
.filter((item) => item.length > 0);
|
||||
}),
|
||||
],
|
||||
},
|
||||
|
||||
[ConfigVariableType.ENUM]: {
|
||||
|
||||
Reference in New Issue
Block a user