import type { GlobalConfigManager } from './global-config-manager'; import { decideModelConfigFromIntentConfig } from './parse-model-config'; import type { CreateOpenAIClientFn, IModelConfig, TIntent, TModelConfig, } from './types'; export class ModelConfigManager { private modelConfigMap: Record | undefined = undefined; private isInitialized = false; // once modelConfig is set, isolatedMode will be true // modelConfigMap will only depend on provided config and not be affected by process.env private isolatedMode = false; private globalConfigManager: GlobalConfigManager | undefined = undefined; private modelConfig?: TModelConfig; private createOpenAIClientFn?: CreateOpenAIClientFn; constructor( modelConfig?: TModelConfig, createOpenAIClientFn?: CreateOpenAIClientFn, ) { this.modelConfig = modelConfig; this.createOpenAIClientFn = createOpenAIClientFn; } private initialize() { if (this.isInitialized) { return; } let configMap: Record; if (this.modelConfig) { this.isolatedMode = true; configMap = this.normalizeModelConfig(this.modelConfig); } else { configMap = this.globalConfigManager?.getAllEnvConfig() || {}; } const defaultConfig = decideModelConfigFromIntentConfig( 'default', configMap, ); if (!defaultConfig) { throw new Error( 'Model configuration is incomplete: model name (MIDSCENE_MODEL_NAME) is required. See https://midscenejs.com/model-api-setup', ); } if (defaultConfig.modelFamily === 'jev') { throw new Error( 'Jev cannot be the default model. Configure a general-purpose MIDSCENE_MODEL_FAMILY and use Jev in Locate, Planning, or Insight.', ); } const insightConfig = decideModelConfigFromIntentConfig( 'insight', configMap, ); const planningConfig = decideModelConfigFromIntentConfig( 'planning', configMap, ); const locateConfig = decideModelConfigFromIntentConfig('locate', configMap); // Each intent uses its own timeout from parsed config (MIDSCENE_MODEL_TIMEOUT, // MIDSCENE_INSIGHT_MODEL_TIMEOUT, MIDSCENE_PLANNING_MODEL_TIMEOUT, MIDSCENE_LOCATE_MODEL_TIMEOUT). this.modelConfigMap = { default: { ...defaultConfig, intent: 'default', slot: 'default', createOpenAIClient: this.createOpenAIClientFn, }, locate: { ...(locateConfig || defaultConfig), intent: 'locate', slot: locateConfig ? 'locate' : 'default', createOpenAIClient: this.createOpenAIClientFn, }, insight: { ...(insightConfig || defaultConfig), intent: 'insight', slot: insightConfig ? 'insight' : 'default', createOpenAIClient: this.createOpenAIClientFn, }, planning: { ...(planningConfig || defaultConfig), intent: 'planning', slot: planningConfig ? 'planning' : 'default', createOpenAIClient: this.createOpenAIClientFn, }, }; this.isInitialized = true; } private normalizeModelConfig( config: TModelConfig, ): Record { return Object.entries(config).reduce>( (acc, [key, value]) => { if (value === undefined || value === null) { return acc; } acc[key] = String(value); return acc; }, Object.create(null), ); } /** * should only be called by GlobalConfigManager */ clearModelConfigMap() { if (this.isolatedMode) { throw new Error( 'ModelConfigManager work in isolated mode, so clearModelConfigMap should not be called', ); } this.isInitialized = false; } /** * if isolatedMode is true, modelConfigMap was initialized in constructor and can't be changed * if isolatedMode is false, modelConfigMap can be changed by process.env so we need to recalculate it when it's undefined */ getModelConfig(intent: TIntent): IModelConfig { // check if initialized if (!this.isInitialized) { this.initialize(); } if (!this.modelConfigMap) { throw new Error( 'modelConfigMap is not initialized, which should not happen', ); } return this.modelConfigMap[intent]; } getUploadTestServerUrl(): string | undefined { const { openaiExtraConfig } = this.getModelConfig('default'); const serverUrl = openaiExtraConfig?.REPORT_SERVER_URL as string; return serverUrl; } registerGlobalConfigManager(globalConfigManager: GlobalConfigManager) { this.globalConfigManager = globalConfigManager; } throwErrorIfNonVLModel() { const modelConfig = this.getModelConfig('locate'); if (!modelConfig.modelFamily) { throw new Error( `${modelConfig.slot === 'locate' ? 'MIDSCENE_LOCATE_MODEL_FAMILY' : 'MIDSCENE_MODEL_FAMILY'} is not set to a multimodal model with UI localization, so element localization cannot be achieved. Check your model configuration. See https://midscenejs.com/model-api-setup`, ); } } }