basant307/AI_Governance_Project
048
1/**2 * @license3 * Copyright 2025 Qwen Team4 * SPDX-License-Identifier: Apache-2.05 */6 7import {8 AuthType,9 MODEL_GENERATION_CONFIG_FIELDS,10 type ContentGeneratorConfig,11 type ContentGeneratorConfigSources,12 normalizeReasoningEffort,13 REASONING_EFFORT_TIERS,14 resolveModelConfig,15 resolveProviderProtocol,16 type ModelConfigSourcesInput,17 type ModelProvidersConfig,18 type ProviderModelConfig,19 type ProviderProtocolConfig,20 stripRuntimeSnapshotPrefix,21} from '@qwen-code/qwen-code-core';22import type { Settings } from '../config/settings.js';23import { sanitizeProviderBaseUrl } from './acpModelUtils.js';24 25/**26 * Env var names that hold model selections for each auth type.27 * Mirrors the model-var mappings in core's AUTH_ENV_MAPPINGS.28 */29const AUTH_ENV_MODEL_VARS: Record<AuthType, string[]> = {30 [AuthType.USE_OPENAI]: ['OPENAI_MODEL', 'QWEN_MODEL'],31 [AuthType.USE_GEMINI]: ['GEMINI_MODEL'],32 [AuthType.USE_VERTEX_AI]: ['GOOGLE_MODEL'],33 [AuthType.USE_ANTHROPIC]: ['ANTHROPIC_MODEL'],34 [AuthType.QWEN_OAUTH]: [],35};36 37/**38 * Collect every modelProviders entry whose provider id resolves (via39 * providerProtocol) to the given protocol, in declaration order. Mirrors40 * {@link ModelRegistry}: a built-in key resolves to itself, a custom id resolves41 * through providerProtocol. Lets credential/metadata lookups find a custom42 * provider's models under their resolved protocol instead of only the protocol43 * key. For built-in-only configs this equals `modelProviders[protocol]`.44 */45export function collectProviderModelsForProtocol(46 modelProviders: ModelProvidersConfig | undefined,47 providerProtocol: ProviderProtocolConfig | undefined,48 protocol: string,49): ProviderModelConfig[] {50 if (!modelProviders) {51 return [];52 }53 const out: ProviderModelConfig[] = [];54 for (const [providerId, models] of Object.entries(modelProviders)) {55 if (!Array.isArray(models)) {56 continue;57 }58 if (resolveProviderProtocol(providerId, providerProtocol) === protocol) {59 out.push(...models);60 }61 }62 return out;63}64 65function findProviderIdForModel(66 modelProviders: ModelProvidersConfig | undefined,67 providerProtocol: ProviderProtocolConfig | undefined,68 protocol: string,69 modelProvider: ProviderModelConfig | undefined,70): string | undefined {71 if (!modelProviders || !modelProvider) {72 return undefined;73 }74 for (const [providerId, models] of Object.entries(modelProviders)) {75 if (!Array.isArray(models)) {76 continue;77 }78 if (resolveProviderProtocol(providerId, providerProtocol) !== protocol) {79 continue;80 }81 if (models.includes(modelProvider)) {82 return providerId;83 }84 }85 return undefined;86}87 88/**89 * Build user-visible warnings for modelProviders entries that carry models but90 * are dropped at registration: an unknown provider id with no providerProtocol91 * mapping, or a mapping to an unknown protocol. Without this the models silently92 * vanish from selection (the registry only logs at debug level).93 */94function buildSkippedProviderWarnings(95 modelProviders: ModelProvidersConfig | undefined,96 providerProtocol: ProviderProtocolConfig | undefined,97): string[] {98 if (!modelProviders) {99 return [];100 }101 const warnings: string[] = [];102 const known = Object.values(AuthType).join(', ');103 for (const [providerId, models] of Object.entries(modelProviders)) {104 if (!Array.isArray(models) || models.length === 0) {105 continue;106 }107 const protocol = resolveProviderProtocol(providerId, providerProtocol);108 if (109 protocol === AuthType.QWEN_OAUTH &&110 providerId !== AuthType.QWEN_OAUTH111 ) {112 warnings.push(113 `Warning: modelProviders provider "${providerId}" maps to "qwen-oauth" via providerProtocol, but qwen-oauth uses hard-coded models only; its ${models.length} model(s) are ignored.`,114 );115 continue;116 }117 if (protocol !== undefined) {118 continue;119 }120 const mapped =121 providerProtocol && Object.hasOwn(providerProtocol, providerId)122 ? providerProtocol[providerId]123 : undefined;124 warnings.push(125 mapped !== undefined126 ? `Warning: providerProtocol["${providerId}"] = "${mapped}" is not a known protocol (expected one of: ${known}); its ${models.length} model(s) are ignored.`127 : `Warning: modelProviders provider "${providerId}" is not a built-in protocol (${known}) and has no providerProtocol mapping; its ${models.length} model(s) are ignored. Add providerProtocol["${providerId}"] (e.g. "openai") to enable them.`,128 );129 }130 return warnings;131}132 133function getIgnoredTopLevelGenerationConfigFields(134 settingsGenerationConfig: Partial<ContentGeneratorConfig> | undefined,135 modelProvider: ProviderModelConfig | undefined,136): string[] {137 if (!settingsGenerationConfig || !modelProvider) {138 return [];139 }140 141 const providerGenerationConfig = modelProvider.generationConfig ?? {};142 return MODEL_GENERATION_CONFIG_FIELDS.filter(143 (field) =>144 Object.hasOwn(settingsGenerationConfig, field) &&145 !Object.hasOwn(providerGenerationConfig, field),146 );147}148 149function buildIgnoredTopLevelGenerationConfigWarning(150 providerId: string,151 modelProvider: ProviderModelConfig,152 ignoredFields: string[],153): string | undefined {154 if (ignoredFields.length === 0) {155 return undefined;156 }157 158 const fieldList = ignoredFields159 .map((field) => `model.generationConfig.${field}`)160 .join(', ');161 const isSingular = ignoredFields.length === 1;162 const verb = isSingular ? 'is' : 'are';163 const fieldReference = isSingular ? 'this field' : 'these fields';164 const pronoun = isSingular ? 'it' : 'them';165 166 return `Warning: ${fieldList} ${verb} ignored for provider model "${modelProvider.id}" from modelProviders.${providerId}. Move ${fieldReference} to modelProviders.${providerId}[].generationConfig for that model if you want ${pronoun} to apply.`;167}168 169export interface CliGenerationConfigInputs {170 argv: {171 model?: string | undefined;172 openaiApiKey?: string | undefined;173 openaiBaseUrl?: string | undefined;174 openaiLogging?: boolean | undefined;175 openaiLoggingDir?: string | undefined;176 };177 settings: Settings;178 selectedAuthType: AuthType | undefined;179 /**180 * Injectable env for testability. Defaults to process.env at callsites.181 */182 env?: Record<string, string | undefined>;183}184 185export interface ResolvedCliGenerationConfig {186 /** The resolved model id (may be empty string if not resolvable at CLI layer) */187 model: string;188 /** API key for OpenAI-compatible auth */189 apiKey: string;190 /** Base URL for OpenAI-compatible auth */191 baseUrl: string;192 /** The full generation config to pass to core Config */193 generationConfig: Partial<ContentGeneratorConfig>;194 /** Source attribution for each resolved field */195 sources: ContentGeneratorConfigSources;196 /** Warnings generated during resolution */197 warnings: string[];198}199 200export function getAuthTypeFromEnv(201 env: Record<string, string | undefined> = process.env,202): AuthType | undefined {203 if (env['QWEN_OAUTH']) {204 return AuthType.QWEN_OAUTH;205 }206 207 if (208 env['OPENAI_API_KEY'] &&209 (env['OPENAI_MODEL'] || env['QWEN_MODEL']) &&210 env['OPENAI_BASE_URL']211 ) {212 return AuthType.USE_OPENAI;213 }214 215 if (env['GEMINI_API_KEY'] && env['GEMINI_MODEL']) {216 return AuthType.USE_GEMINI;217 }218 219 if (env['GOOGLE_API_KEY'] && env['GOOGLE_MODEL']) {220 return AuthType.USE_VERTEX_AI;221 }222 223 if (224 env['ANTHROPIC_API_KEY'] &&225 env['ANTHROPIC_MODEL'] &&226 env['ANTHROPIC_BASE_URL']227 ) {228 return AuthType.USE_ANTHROPIC;229 }230 231 return undefined;232}233 234/**235 * Unified resolver for CLI generation config.236 *237 * Model precedence (all auth types):238 * - argv.model > settings.model.name > auth-specific env model vars239 *240 * Env var mapping by auth type (mirrors core's AUTH_ENV_MAPPINGS):241 * - USE_OPENAI: OPENAI_MODEL, QWEN_MODEL242 * - USE_GEMINI: GEMINI_MODEL243 * - USE_VERTEX_AI: GOOGLE_MODEL244 * - USE_ANTHROPIC: ANTHROPIC_MODEL245 *246 * When model is resolved from argv or settings, all model env vars are stripped247 * from the env passed to core's resolveModelConfig to prevent incorrect overrides.248 * When model is resolved from an auth-specific env var, only that env var is249 * kept in the filtered env so core can access the provider metadata.250 */251export function resolveCliGenerationConfig(252 inputs: CliGenerationConfigInputs,253): ResolvedCliGenerationConfig {254 const { argv, settings, selectedAuthType } = inputs;255 const env = inputs.env ?? (process.env as Record<string, string | undefined>);256 257 const authType = selectedAuthType;258 259 // Resolve the target model based on strict precedence:260 // argv.model > settings.model.name > auth-specific env model vars261 // Env vars are ONLY considered when neither argv.model nor settings.model.name is set.262 let resolvedModel: string | undefined;263 let sourceEnvVar: string | undefined;264 // Whether the model came from settings.model.name (vs argv/env). The persisted265 // settings.model.baseUrl disambiguator only applies to this case.266 let resolvedFromSettings = false;267 if (argv.model) {268 resolvedModel = argv.model;269 } else if (settings.model?.name) {270 // Self-heal configs already corrupted by older builds.271 resolvedModel = stripRuntimeSnapshotPrefix(settings.model.name);272 resolvedFromSettings = true;273 } else if (authType && AUTH_ENV_MODEL_VARS[authType]) {274 // Only check env vars for the current auth type275 for (const envVar of AUTH_ENV_MODEL_VARS[authType]) {276 if (env[envVar]) {277 resolvedModel = env[envVar];278 sourceEnvVar = envVar;279 break;280 }281 }282 }283 284 // Find a matching provider for the resolved model (for metadata: generationConfig, envKey, etc.)285 // When resolvedModel is from settings and matches a provider, modelProvider.id == settings.model.name,286 // so the resolver correctly uses the settings-selected model (no override occurs).287 // The old candidate-loop code that fell through to OPENAI_MODEL is gone.288 let modelProvider: ProviderModelConfig | undefined;289 let disambiguationWarning: string | undefined;290 if (resolvedModel && authType && settings.modelProviders) {291 // Merge across all provider ids that route to this protocol (built-in key292 // or custom id via providerProtocol), so a custom provider's envKey/metadata293 // is honored, not just entries stored under the protocol key itself.294 const providers = collectProviderModelsForProtocol(295 settings.modelProviders,296 settings.providerProtocol,297 authType,298 );299 if (providers.length > 0) {300 // When multiple providers share the same id, disambiguate by the301 // persisted settings.model.baseUrl (written by the model picker). This302 // only applies when the model itself came from settings.model.name.303 // Fall back to the first id match if the paired provider was edited or304 // removed (and for the legacy id-only case where no baseUrl was saved),305 // mirroring auth.ts:findModelConfig.306 //307 // Note: `settings` is already merged across user/workspace/system scopes.308 // Every writer of model.name (the picker, /model, ACP, provider install)309 // also writes model.baseUrl in the SAME scope — a real URL, or an empty310 // string tombstone when there is none. The tombstone matters because an311 // omitted key cannot override a stale model.baseUrl in a lower-priority312 // scope on merge, but '' (a present value) can. Empty string is treated313 // as "no disambiguator" here. The only remaining desync is a hand-edited314 // config that sets model.name in a higher scope with no baseUrl key at315 // all; the id-only fallback bounds the blast radius to a same-id provider.316 const persistedBaseUrl = settings.model?.baseUrl;317 if (resolvedFromSettings && persistedBaseUrl) {318 const exactMatch = providers.find(319 (p) => p.id === resolvedModel && p.baseUrl === persistedBaseUrl,320 );321 modelProvider =322 exactMatch ?? providers.find((p) => p.id === resolvedModel);323 // Surface the silent fallback: the paired provider was removed or its324 // baseUrl changed, so traffic now routes to a different same-id provider.325 if (!exactMatch && modelProvider) {326 const fallbackBaseUrl =327 modelProvider.baseUrl === undefined328 ? '(default baseUrl)'329 : sanitizeProviderBaseUrl(modelProvider.baseUrl);330 disambiguationWarning =331 `Persisted model.baseUrl '${sanitizeProviderBaseUrl(persistedBaseUrl)}' no longer matches any provider ` +332 `for model '${resolvedModel}' (authType '${authType}'); using the first id match ` +333 `('${fallbackBaseUrl}'). Re-select the model to update it.`;334 }335 } else {336 modelProvider = providers.find((p) => p.id === resolvedModel);337 }338 }339 }340 341 // Filter env to prevent auth-specific model env vars from overriding higher-priority sources.342 // sourceEnvVar is only set when the model was actually resolved from an env var (lines 119-128),343 // so this is source-based filtering, not value-based. If model came from argv or settings,344 // sourceEnvVar is undefined and ALL model env vars are stripped.345 // Build a list of ALL model env vars across all auth types.346 const allModelEnvVars = Object.values(AUTH_ENV_MODEL_VARS).flat();347 const filteredEnv = { ...env };348 if (sourceEnvVar) {349 // Keep only the env var that was actually used350 for (const envVar of allModelEnvVars) {351 if (envVar !== sourceEnvVar) {352 delete filteredEnv[envVar];353 }354 }355 } else {356 // Model was not resolved from env - strip ALL model env vars357 for (const envVar of allModelEnvVars) {358 delete filteredEnv[envVar];359 }360 }361 362 const configSources: ModelConfigSourcesInput = {363 authType,364 cli: {365 model: argv.model,366 apiKey: argv.openaiApiKey,367 baseUrl: argv.openaiBaseUrl,368 },369 settings: {370 model: settings.model?.name371 ? stripRuntimeSnapshotPrefix(settings.model.name)372 : undefined,373 apiKey: settings.security?.auth?.apiKey,374 baseUrl: settings.security?.auth?.baseUrl,375 generationConfig: settings.model?.generationConfig as376 | Partial<ContentGeneratorConfig>377 | undefined,378 },379 modelProvider,380 env: filteredEnv,381 };382 383 const resolved = resolveModelConfig(configSources);384 385 // Provider-backed models are synced again during Config.refreshAuth(), which386 // reapplies provider defaults after the initial resolver fallback.387 const ignoredGenerationConfigWarning =388 authType && modelProvider389 ? buildIgnoredTopLevelGenerationConfigWarning(390 findProviderIdForModel(391 settings.modelProviders,392 settings.providerProtocol,393 authType,394 modelProvider,395 ) ?? authType,396 modelProvider,397 getIgnoredTopLevelGenerationConfigFields(398 settings.model?.generationConfig as399 | Partial<ContentGeneratorConfig>400 | undefined,401 modelProvider,402 ),403 )404 : undefined;405 406 // Resolve OpenAI logging config (CLI-specific, not part of core resolver)407 const enableOpenAILogging =408 (typeof argv.openaiLogging === 'undefined'409 ? settings.model?.enableOpenAILogging410 : argv.openaiLogging) ?? false;411 412 const openAILoggingDir =413 argv.openaiLoggingDir || settings.model?.openAILoggingDir;414 415 // Build the full generation config416 // Note: we merge the resolved config with logging settings417 const generationConfig: Partial<ContentGeneratorConfig> = {418 ...resolved.config,419 enableOpenAILogging,420 openAILoggingDir,421 };422 423 // Apply the global reasoning-effort preference (settings.model.reasoningEffort,424 // set via /effort) onto the unified reasoning config. Skip when thinking is425 // explicitly disabled (reasoning === false) so effort never silently426 // re-enables it; provider adapters clamp the tier to the active model.427 const rawReasoningEffort = settings.model?.reasoningEffort;428 const reasoningEffort = normalizeReasoningEffort(rawReasoningEffort);429 // A configured-but-unrecognized value (e.g. a "hihg" typo in settings.json)430 // normalizes to undefined and is silently skipped below. Surface it as a431 // warning so the user isn't left wondering why /effort had no effect.432 const invalidReasoningEffortWarning =433 rawReasoningEffort && !reasoningEffort434 ? `Ignoring invalid model.reasoningEffort "${rawReasoningEffort}"; expected one of: ${REASONING_EFFORT_TIERS.join(', ')}.`435 : undefined;436 if (reasoningEffort && generationConfig.reasoning !== false) {437 generationConfig.reasoning = {438 ...(generationConfig.reasoning ?? {}),439 effort: reasoningEffort,440 };441 }442 443 return {444 model: resolved.config.model || '',445 apiKey: resolved.config.apiKey || '',446 baseUrl: resolved.config.baseUrl || '',447 generationConfig,448 sources: resolved.sources,449 warnings: [450 ...resolved.warnings,451 ...(invalidReasoningEffortWarning ? [invalidReasoningEffortWarning] : []),452 ...(disambiguationWarning ? [disambiguationWarning] : []),453 ...(ignoredGenerationConfigWarning454 ? [ignoredGenerationConfigWarning]455 : []),456 ...buildSkippedProviderWarnings(457 settings.modelProviders,458 settings.providerProtocol,459 ),460 ],461 };462}463 