CoolFace
Datasetpublic

basant307/AI_Governance_Project

sourceHugging Faceapache-2.0updated 2mo agoView on Hugging Face
0likes45downloads
modelConfigResolver.ts436 linesDownload Raw Back to models
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 
basant307/AI_Governance_Project · CoolFace