basant307/AI_Governance_Project
045
1/**2 * @license3 * Copyright 2025 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6 7/**8 * ModelConfigResolver - Unified resolver for model-related configuration.9 *10 * This module consolidates all model configuration resolution logic,11 * eliminating duplicate code between CLI and Core layers.12 *13 * Configuration priority (highest to lowest):14 * 1. modelProvider - Explicit selection from ModelProviders config15 * 2. CLI arguments - Command line flags (--model, --openaiApiKey, etc.)16 * 3. Environment variables - OPENAI_API_KEY, OPENAI_MODEL, etc.17 * 4. Settings - User/workspace settings file18 * 5. Defaults - Built-in default values19 */20 21import { AuthType } from '../core/contentGenerator.js';22import type { ContentGeneratorConfig } from '../core/contentGenerator.js';23import { DEFAULT_QWEN_MODEL } from '../config/models.js';24import { defaultModalities } from '../core/modalityDefaults.js';25import { knownTokenLimit } from '../core/tokenLimits.js';26import {27 resolveField,28 resolveOptionalField,29 layer,30 envLayer,31 cliSource,32 settingsSource,33 modelProvidersSource,34 defaultSource,35 computedSource,36 type ConfigSource,37 type ConfigSources,38 type ConfigLayer,39} from '../utils/configResolver.js';40import {41 AUTH_ENV_MAPPINGS,42 DEFAULT_MODELS,43 QWEN_OAUTH_ALLOWED_MODELS,44 MODEL_GENERATION_CONFIG_FIELDS,45} from './constants.js';46import type { ModelConfig as ModelProviderConfig } from './types.js';47export {48 validateModelConfig,49 type ModelConfigValidationResult,50} from '../core/contentGenerator.js';51 52/**53 * CLI-provided configuration values54 */55export interface ModelConfigCliInput {56 model?: string;57 apiKey?: string;58 baseUrl?: string;59}60 61/**62 * Settings-provided configuration values63 */64export interface ModelConfigSettingsInput {65 /** Model name from settings.model.name */66 model?: string;67 /** API key from settings.security.auth.apiKey */68 apiKey?: string;69 /** Base URL from settings.security.auth.baseUrl */70 baseUrl?: string;71 /** Generation config from settings.model.generationConfig */72 generationConfig?: Partial<ContentGeneratorConfig>;73}74 75/**76 * All input sources for model configuration resolution77 */78export interface ModelConfigSourcesInput {79 /** Authentication type */80 authType?: AuthType;81 82 /** CLI arguments (highest priority for user-provided values) */83 cli?: ModelConfigCliInput;84 85 /** Settings file configuration */86 settings?: ModelConfigSettingsInput;87 88 /** Environment variables (injected for testability) */89 env: Record<string, string | undefined>;90 91 /** Model from ModelProviders (explicit selection, highest priority) */92 modelProvider?: ModelProviderConfig;93 94 /** Proxy URL (computed from Config) */95 proxy?: string;96}97 98/**99 * Result of model configuration resolution100 */101export interface ModelConfigResolutionResult {102 /** The fully resolved configuration */103 config: ContentGeneratorConfig;104 /** Source attribution for each field */105 sources: ConfigSources;106 /** Warnings generated during resolution */107 warnings: string[];108}109 110/**111 * Applies QWEN_CODE_API_TIMEOUT_MS env override if modelProvider has not set a timeout.112 * Precedence: modelProvider > env > settings > default113 * Mutates generationConfig and sources in-place.114 */115function applyTimeoutEnvOverride(116 env: Record<string, string | undefined>,117 generationConfig: Partial<ContentGeneratorConfig>,118 sources: ConfigSources,119 modelProvider?: ModelProviderConfig,120): void {121 if (modelProvider?.generationConfig?.timeout !== undefined) return;122 123 const raw = env['QWEN_CODE_API_TIMEOUT_MS'];124 if (raw === undefined) return;125 126 const trimmed = raw.trim();127 // Accept a non-negative integer; `0` disables the request timeout downstream128 // (see resolveRequestTimeout). Malformed values are ignored (keep default).129 if (!/^\d+$/.test(trimmed)) return;130 const parsed = Number(trimmed);131 if (!Number.isSafeInteger(parsed)) return;132 generationConfig.timeout = parsed;133 sources['timeout'] = {134 kind: 'env',135 envKey: 'QWEN_CODE_API_TIMEOUT_MS',136 };137}138 139/**140 * Resolve model configuration from all input sources.141 *142 * This is the single entry point for model configuration resolution.143 * It replaces the duplicate logic in:144 * - packages/cli/src/utils/modelProviderUtils.ts (resolveCliGenerationConfig)145 * - packages/core/src/core/contentGenerator.ts (resolveContentGeneratorConfigWithSources)146 *147 * @param input - All configuration sources148 * @returns Resolved configuration with source tracking149 */150export function resolveModelConfig(151 input: ModelConfigSourcesInput,152): ModelConfigResolutionResult {153 const { authType, cli, settings, env, modelProvider, proxy } = input;154 const warnings: string[] = [];155 const sources: ConfigSources = {};156 157 // Special handling for Qwen OAuth158 if (authType === AuthType.QWEN_OAUTH) {159 return resolveQwenOAuthConfig(input, warnings);160 }161 162 // Get auth-specific env var mappings.163 // If authType is not provided, do not read any auth env vars.164 const envMapping = authType165 ? AUTH_ENV_MAPPINGS[authType]166 : { model: [], apiKey: [], baseUrl: [] };167 168 // Build layers for each field in priority order169 // Priority: modelProvider > cli > env > settings > default170 171 // ---- Model ----172 const modelLayers: Array<ConfigLayer<string>> = [];173 174 if (authType && modelProvider) {175 modelLayers.push(176 layer(177 modelProvider.id,178 modelProvidersSource(authType, modelProvider.id, 'model.id'),179 ),180 );181 }182 if (cli?.model) {183 modelLayers.push(layer(cli.model, cliSource('--model')));184 }185 for (const envKey of envMapping.model) {186 modelLayers.push(envLayer(env, envKey));187 }188 if (settings?.model) {189 modelLayers.push(layer(settings.model, settingsSource('model.name')));190 }191 192 const defaultModel = authType ? DEFAULT_MODELS[authType] : '';193 const modelResult = resolveField(194 modelLayers,195 defaultModel,196 defaultSource(defaultModel),197 );198 sources['model'] = modelResult.source;199 200 // ---- API Key ----201 const apiKeyLayers: Array<ConfigLayer<string>> = [];202 203 // For modelProvider, read from the specified envKey204 if (authType && modelProvider?.envKey) {205 const apiKeyFromEnv = env[modelProvider.envKey];206 if (apiKeyFromEnv) {207 apiKeyLayers.push(208 layer(apiKeyFromEnv, {209 kind: 'env',210 envKey: modelProvider.envKey,211 via: modelProvidersSource(authType, modelProvider.id, 'envKey'),212 }),213 );214 }215 }216 if (cli?.apiKey) {217 apiKeyLayers.push(layer(cli.apiKey, cliSource('--openaiApiKey')));218 }219 for (const envKey of envMapping.apiKey) {220 apiKeyLayers.push(envLayer(env, envKey));221 }222 if (settings?.apiKey) {223 apiKeyLayers.push(224 layer(settings.apiKey, settingsSource('security.auth.apiKey')),225 );226 }227 228 const apiKeyResult = resolveOptionalField(apiKeyLayers);229 if (apiKeyResult) {230 sources['apiKey'] = apiKeyResult.source;231 }232 233 // ---- Base URL ----234 const baseUrlLayers: Array<ConfigLayer<string>> = [];235 236 if (authType && modelProvider?.baseUrl) {237 baseUrlLayers.push(238 layer(239 modelProvider.baseUrl,240 modelProvidersSource(authType, modelProvider.id, 'baseUrl'),241 ),242 );243 }244 if (cli?.baseUrl) {245 baseUrlLayers.push(layer(cli.baseUrl, cliSource('--openaiBaseUrl')));246 }247 for (const envKey of envMapping.baseUrl) {248 baseUrlLayers.push(envLayer(env, envKey));249 }250 if (settings?.baseUrl) {251 baseUrlLayers.push(252 layer(settings.baseUrl, settingsSource('security.auth.baseUrl')),253 );254 }255 256 const baseUrlResult = resolveOptionalField(baseUrlLayers);257 if (baseUrlResult) {258 sources['baseUrl'] = baseUrlResult.source;259 }260 261 // ---- API Key Env Key (for error messages) ----262 let apiKeyEnvKey: string | undefined;263 if (authType && modelProvider?.envKey) {264 apiKeyEnvKey = modelProvider.envKey;265 sources['apiKeyEnvKey'] = {266 ...modelProvidersSource(authType, modelProvider.id, 'envKey'),267 envKey: modelProvider.envKey,268 };269 }270 271 // ---- Generation Config (from settings or modelProvider) ----272 const generationConfig = resolveGenerationConfig(273 settings?.generationConfig,274 modelProvider?.generationConfig,275 authType,276 modelProvider?.id ?? modelResult.value,277 sources,278 );279 280 // ---- Env override: QWEN_CODE_API_TIMEOUT_MS ----281 applyTimeoutEnvOverride(env, generationConfig, sources, modelProvider);282 283 // Build final config284 const config: ContentGeneratorConfig = {285 authType,286 model: modelResult.value || '',287 apiKey: apiKeyResult?.value,288 apiKeyEnvKey,289 baseUrl: baseUrlResult?.value,290 proxy,291 ...generationConfig,292 };293 294 // Add proxy source295 if (proxy) {296 sources['proxy'] = computedSource('Config.getProxy()');297 }298 299 // Add authType source300 sources['authType'] = computedSource('provided by caller');301 302 return { config, sources, warnings };303}304 305/**306 * Special resolver for Qwen OAuth authentication.307 * Qwen OAuth has fixed model options and uses dynamic tokens.308 */309function resolveQwenOAuthConfig(310 input: ModelConfigSourcesInput,311 warnings: string[],312): ModelConfigResolutionResult {313 const { cli, settings, proxy, modelProvider } = input;314 const sources: ConfigSources = {};315 316 // Qwen OAuth only allows specific models317 const allowedModels = new Set<string>(QWEN_OAUTH_ALLOWED_MODELS);318 319 // Determine requested model320 const requestedModel = cli?.model || settings?.model;321 let resolvedModel: string;322 let modelSource: ConfigSource;323 324 if (requestedModel && allowedModels.has(requestedModel)) {325 resolvedModel = requestedModel;326 modelSource = cli?.model327 ? cliSource('--model')328 : settingsSource('model.name');329 } else {330 if (requestedModel) {331 const isVisionModel =332 requestedModel.includes('vl') || requestedModel.includes('vision');333 const extraMessage = isVisionModel334 ? ` Note: vision-model has been removed since coder-model now supports vision capabilities.`335 : '';336 warnings.push(337 `Warning: Unsupported Qwen OAuth model '${requestedModel}', falling back to '${DEFAULT_QWEN_MODEL}'.${extraMessage}`,338 );339 }340 resolvedModel = DEFAULT_QWEN_MODEL;341 modelSource = defaultSource(`fallback to '${DEFAULT_QWEN_MODEL}'`);342 }343 344 sources['model'] = modelSource;345 sources['apiKey'] = computedSource('Qwen OAuth dynamic token');346 sources['authType'] = computedSource('provided by caller');347 348 if (proxy) {349 sources['proxy'] = computedSource('Config.getProxy()');350 }351 352 // Resolve generation config from settings and modelProvider353 const generationConfig = resolveGenerationConfig(354 settings?.generationConfig,355 modelProvider?.generationConfig,356 AuthType.QWEN_OAUTH,357 resolvedModel,358 sources,359 );360 361 // ---- Env override: QWEN_CODE_API_TIMEOUT_MS ----362 applyTimeoutEnvOverride(input.env, generationConfig, sources, modelProvider);363 364 const config: ContentGeneratorConfig = {365 authType: AuthType.QWEN_OAUTH,366 model: resolvedModel,367 apiKey: 'QWEN_OAUTH_DYNAMIC_TOKEN',368 proxy,369 ...generationConfig,370 };371 372 return { config, sources, warnings };373}374 375/**376 * Resolve generation config fields (samplingParams, timeout, etc.)377 */378function resolveGenerationConfig(379 settingsConfig: Partial<ContentGeneratorConfig> | undefined,380 modelProviderConfig: Partial<ContentGeneratorConfig> | undefined,381 authType: AuthType | undefined,382 modelId: string | undefined,383 sources: ConfigSources,384): Partial<ContentGeneratorConfig> {385 const result: Partial<ContentGeneratorConfig> = {};386 387 for (const field of MODEL_GENERATION_CONFIG_FIELDS) {388 // ModelProvider config takes priority over settings config389 if (authType && modelProviderConfig && field in modelProviderConfig) {390 // eslint-disable-next-line @typescript-eslint/no-explicit-any391 (result as any)[field] = modelProviderConfig[field];392 sources[field] = modelProvidersSource(393 authType,394 modelId || '',395 `generationConfig.${field}`,396 );397 } else if (settingsConfig && field in settingsConfig) {398 // eslint-disable-next-line @typescript-eslint/no-explicit-any399 (result as any)[field] = settingsConfig[field];400 sources[field] = settingsSource(`model.generationConfig.${field}`);401 }402 }403 404 // contextWindowSize fallback: auto-detect from model when neither405 // modelProvider nor settings supplied it. Only known models are stamped —406 // unknown models keep `undefined` so downstream `?? DEFAULT_TOKEN_LIMIT`407 // consumers apply the generic default without a misleading408 // 'auto-detected' source label.409 if (result.contextWindowSize === undefined && modelId) {410 const knownLimit = knownTokenLimit(modelId, 'input');411 if (knownLimit !== undefined) {412 result.contextWindowSize = knownLimit;413 sources['contextWindowSize'] = computedSource('auto-detected from model');414 }415 }416 417 // modalities fallback: auto-detect from model when neither modelProvider nor418 // settings supplied it. Mirrors modelRegistry.resolveModelConfig and419 // modelsConfig.applyResolvedModelDefaults so all paths agree on which models420 // are multimodal — without this, env-var-only setups silently drop @image421 // attachments for image-capable models (issue #4219).422 //423 // Invariant: defaultModalities() returns `{}` (text-only) for unknown424 // models, never `undefined`. After this fallback runs with a known modelId,425 // `result.modalities` is always defined. Downstream code must NOT branch426 // on `modalities === undefined` to mean "unresolved" — use the sources map427 // (kind === 'computed' vs 'modelProviders'/'settings') if that distinction428 // matters.429 if (result.modalities === undefined && modelId) {430 result.modalities = defaultModalities(modelId);431 sources['modalities'] = computedSource('auto-detected from model');432 }433 434 return result;435}436 