import { DEFAULT_MODEL_CONFIG_KEYS, type DEFAULT_MODEL_CONFIG_KEYS_LEGACY, INSIGHT_MODEL_CONFIG_KEYS, LOCATE_MODEL_CONFIG_KEYS, PLANNING_MODEL_CONFIG_KEYS, } from './constants'; import { type IModelConfig, MIDSCENE_MODEL_FAMILY, MIDSCENE_OPENAI_HTTP_PROXY, MIDSCENE_OPENAI_INIT_CONFIG_JSON, MIDSCENE_OPENAI_SOCKS_PROXY, MIDSCENE_USE_DOUBAO_VISION, MIDSCENE_USE_GEMINI, MIDSCENE_USE_QWEN3_VL, MIDSCENE_USE_QWEN_VL, MIDSCENE_USE_VLM_UI_TARS, MODEL_FAMILY_VALUES, OPENAI_API_KEY, OPENAI_BASE_URL, type TIntent, type TModelFamily, UITarsModelVersion, } from './types'; import { getDebug } from '../logger'; import { assert } from '../utils'; import { maskConfig, parseJson } from './helper'; import { initDebugConfig } from './init-debug'; declare const __VERSION__: string | undefined; const MODEL_CONFIG_DOC_URL = 'https://midscenejs.com/model-api-setup.html'; const getCurrentVersion = (): string => { if (typeof __VERSION__ !== 'undefined' && __VERSION__) { return __VERSION__; } return 'unknown'; }; const getInvalidModelFamilyMessage = (modelFamily: TModelFamily): string => `Invalid MIDSCENE_MODEL_FAMILY value: ${modelFamily}. Current version v${getCurrentVersion()} accepts the following model families: ${MODEL_FAMILY_VALUES.join(', ')}. You can also visit ${MODEL_CONFIG_DOC_URL} for the latest configuration information.`; type TModelConfigKeys = | typeof LOCATE_MODEL_CONFIG_KEYS | typeof INSIGHT_MODEL_CONFIG_KEYS | typeof PLANNING_MODEL_CONFIG_KEYS | typeof DEFAULT_MODEL_CONFIG_KEYS | typeof DEFAULT_MODEL_CONFIG_KEYS_LEGACY; const KEYS_MAP: Record = { locate: LOCATE_MODEL_CONFIG_KEYS, insight: INSIGHT_MODEL_CONFIG_KEYS, planning: PLANNING_MODEL_CONFIG_KEYS, default: DEFAULT_MODEL_CONFIG_KEYS, } as const; /** * Get UI-TARS model version from model family * @param modelFamily - The model family value * @returns UITarsModelVersion if the model family is a UI-TARS variant, undefined otherwise */ export const getUITarsModelVersion = ( modelFamily?: TModelFamily, ): UITarsModelVersion | undefined => { // UI-TARS variants with version handling if (modelFamily === 'vlm-ui-tars') { return UITarsModelVersion.V1_0; } if ( modelFamily === 'vlm-ui-tars-doubao' || modelFamily === 'vlm-ui-tars-doubao-1.5' ) { return UITarsModelVersion.DOUBAO_1_5_20B; } return undefined; }; /** * Validate model family value * @param modelFamily - The model family value to validate * @throws Error if the model family is invalid */ export const validateModelFamily = (modelFamily?: TModelFamily): void => { if (modelFamily && !MODEL_FAMILY_VALUES.includes(modelFamily as any)) { throw new Error(getInvalidModelFamilyMessage(modelFamily)); } }; /** * Convert legacy environment variables to model family * @param provider - Environment variable provider (e.g., process.env) * @returns The corresponding model family value, or undefined if no legacy config is found */ export const legacyConfigToModelFamily = ( provider: Record, ): TModelFamily | undefined => { const isDoubao = provider[MIDSCENE_USE_DOUBAO_VISION]; const isQwen = provider[MIDSCENE_USE_QWEN_VL]; const isQwen3 = provider[MIDSCENE_USE_QWEN3_VL]; const isUiTars = provider[MIDSCENE_USE_VLM_UI_TARS]; const isGemini = provider[MIDSCENE_USE_GEMINI]; const enabledModes = [ isDoubao && MIDSCENE_USE_DOUBAO_VISION, isQwen && MIDSCENE_USE_QWEN_VL, isQwen3 && MIDSCENE_USE_QWEN3_VL, isUiTars && MIDSCENE_USE_VLM_UI_TARS, isGemini && MIDSCENE_USE_GEMINI, ].filter(Boolean); if (enabledModes.length > 1) { throw new Error( `Only one vision mode can be enabled at a time. Currently enabled modes: ${enabledModes.join(', ')}. Please disable all but one mode.`, ); } // Simple modes that directly map to model family if (isQwen3) return 'qwen3-vl'; if (isQwen) return 'qwen2.5-vl'; if (isDoubao) return 'doubao-vision'; if (isGemini) return 'gemini'; // UI-TARS with version detection if (isUiTars) { if (isUiTars === '1') { return 'vlm-ui-tars'; } else if (isUiTars === 'DOUBAO' || isUiTars === 'DOUBAO-1.5') { return 'vlm-ui-tars-doubao-1.5'; } else { // Handle other UI-TARS versions return 'vlm-ui-tars-doubao'; } } return undefined; }; const getModelDescription = ( modelFamily: TModelFamily | undefined, uiTarsModelVersion: UITarsModelVersion | undefined, ) => { if (uiTarsModelVersion) { return `UI-TARS=${uiTarsModelVersion}`; } if (modelFamily) { return `${modelFamily} mode`; } return ''; }; const normalizeOpenaiExtraConfig = ( config: unknown, ): Record | undefined => { if (!config || typeof config !== 'object' || Array.isArray(config)) { return undefined; } const { defaultHeaders, extra_headers, extraHeaders, ...rest } = config as Record; // Priority: defaultHeaders > extra_headers > extraHeaders const headers = defaultHeaders ?? extra_headers ?? extraHeaders; if (headers !== undefined) { return { ...rest, defaultHeaders: headers }; } return rest; }; const parseTemperature = (rawValue: string | undefined): number | undefined => { if (rawValue === undefined || rawValue === '') { return undefined; } const temperature = Number(rawValue); return Number.isFinite(temperature) ? temperature : undefined; }; /** * Parse OpenAI SDK config */ export const parseOpenaiSdkConfig = ({ keys, provider, useLegacyLogic = false, }: { keys: TModelConfigKeys; provider: Record; useLegacyLogic?: boolean; }): IModelConfig => { initDebugConfig(); const debugLog = getDebug('ai:config'); debugLog('enter parseOpenaiSdkConfig with keys:', keys); const legacyAPIKey = useLegacyLogic ? provider[OPENAI_API_KEY] : undefined; const legacyBaseURL = useLegacyLogic ? provider[OPENAI_BASE_URL] : undefined; const legacySocksProxy = useLegacyLogic ? provider[MIDSCENE_OPENAI_SOCKS_PROXY] : undefined; const legacyHttpProxy = useLegacyLogic ? provider[MIDSCENE_OPENAI_HTTP_PROXY] : undefined; const legacyOpenaiExtraConfig = useLegacyLogic ? provider[MIDSCENE_OPENAI_INIT_CONFIG_JSON] : undefined; const legacyModelFamily = useLegacyLogic ? legacyConfigToModelFamily(provider) : undefined; const modelFamilyRaw = provider[keys.modelFamily] || legacyModelFamily; const openaiApiKey: string | undefined = provider[keys.openaiApiKey] || legacyAPIKey; const openaiBaseURL: string | undefined = provider[keys.openaiBaseURL] || legacyBaseURL; const socksProxy: string | undefined = provider[keys.socksProxy] || legacySocksProxy; const httpProxy: string | undefined = provider[keys.httpProxy] || legacyHttpProxy; const modelName: string | undefined = provider[keys.modelName]; const openaiExtraConfigStr: string | undefined = provider[keys.openaiExtraConfig]; const openaiExtraConfig = parseJson( keys.openaiExtraConfig, openaiExtraConfigStr || legacyOpenaiExtraConfig, ); const extraBodyStr: string | undefined = provider[keys.extraBody]; const extraBody = parseJson(keys.extraBody, extraBodyStr); const temperature = parseTemperature(provider[keys.temperature]); const modelFamily = modelFamilyRaw as unknown as TModelFamily; validateModelFamily(modelFamily); const uiTarsModelVersion = getUITarsModelVersion(modelFamily); const modelDescription = getModelDescription(modelFamily, uiTarsModelVersion); return { socksProxy, httpProxy, authMode: (() => { const value = provider[keys.authMode]?.trim() ?? 'api-key'; if (value === 'api-key' || value === 'siwc') { return value; } throw new Error( `Invalid ${keys.authMode}: "${value}". Expected "api-key" or "siwc".`, ); })(), openaiBaseURL, openaiApiKey, openaiExtraConfig: normalizeOpenaiExtraConfig(openaiExtraConfig), extraBody, protocol: (() => { const value = provider[keys.protocol]; if (value === undefined) { return undefined; } if (value === 'openai-chat' || value === 'openai-responses') { return value; } throw new Error( `${keys.protocol} must be one of: openai-chat, openai-responses. Got: ${value}`, ); })(), modelFamily, uiTarsModelVersion, modelName: modelName!, modelDescription, intent: '-' as any, slot: '-' as any, timeout: (() => { const value = provider[keys.timeout]; if (value === undefined) { return undefined; } const timeout = Number(value); if (value.trim() === '' || !Number.isFinite(timeout) || timeout < 0) { throw new Error( `${keys.timeout} must be a finite number greater than or equal to 0, got ${JSON.stringify(value)}`, ); } return timeout; })(), temperature, streamMode: (() => { const mode = provider[keys.streamMode]?.trim() ?? 'non-stream'; if (mode === 'stream' || mode === 'non-stream') { return mode; } throw new Error( `Invalid ${keys.streamMode}: "${mode}". Expected "stream" or "non-stream".`, ); })(), retryCount: (() => { if (!provider[keys.retryCount]) return 1; const val = Number(provider[keys.retryCount]); if (!Number.isFinite(val)) return 1; if (val < 0) throw new Error(`${keys.retryCount} must be non-negative, got ${val}`); return val; })(), retryInterval: (() => { if (!provider[keys.retryInterval]) return 2000; const val = Number(provider[keys.retryInterval]); if (!Number.isFinite(val)) return 2000; if (val < 0) throw new Error( `${keys.retryInterval} must be non-negative, got ${val}`, ); return val; })(), reasoningEffort: provider[keys.reasoningEffort]?.trim() || undefined, reasoningEnabled: (() => { const val = provider[keys.reasoningEnabled]?.trim()?.toLowerCase(); if (val === 'true' || val === '1') return true; if (val === 'false' || val === '0') return false; if (val === 'default') return 'default'; return undefined; })(), reasoningBudget: (() => { const val = provider[keys.reasoningBudget]?.trim(); if (!val) return undefined; const num = Number(val); return Number.isFinite(num) ? num : undefined; })(), responseFormat: (() => { const val = provider[keys.responseFormat]?.trim()?.toLowerCase(); if (!val || val === 'auto') return 'auto'; if (val === 'none') return 'none'; throw new Error( `${keys.responseFormat} must be one of: none, auto. Got: ${val}`, ); })(), }; }; export const decideModelConfigFromIntentConfig = ( intent: TIntent, configMap: Record, ): IModelConfig | undefined => { const debugLog = getDebug('ai:config'); debugLog( 'will decideModelConfig base on agent.modelConfig()', intent, maskConfig(configMap), ); const keysForFn = KEYS_MAP[intent]; const modelName = configMap[keysForFn.modelName]; if (!modelName) { debugLog('no modelName found for intent', intent); return undefined; } const finalResult = parseOpenaiSdkConfig({ keys: keysForFn, provider: configMap, useLegacyLogic: intent === 'default', }); finalResult.intent = intent; finalResult.slot = intent; debugLog( 'decideModelConfig result by agent.modelConfig() with intent', intent, maskConfig({ ...finalResult }), ); assert( finalResult.authMode === 'siwc' || finalResult.openaiBaseURL, `failed to get base URL of model (intent=${intent}). See https://midscenejs.com/model-api-setup`, ); if (!finalResult.modelName) { console.warn( `modelName is not set for intent ${intent}, this may cause unexpected behavior. See https://midscenejs.com/model-api-setup`, ); } return finalResult; };