/** * @license * Copyright 2025 Google LLC * SPDX-License-Identifier: Apache-2.0 */ import { AuthType, getErrorMessage, type Config, type ProviderModelConfig as ModelConfig, } from '@qwen-code/qwen-code-core'; import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js'; import { t } from '../../i18n/index.js'; import { getCodingPlanConfig, isCodingPlanConfig, CodingPlanRegion, CODING_PLAN_ENV_KEY, } from '@qwen-code/qwen-code-core'; import { getPersistScopeForModelSelection } from '../../config/modelProvidersScope.js'; import { backupSettingsFile } from '../../utils/settingsUtils.js'; import { loadSettings, type LoadedSettings } from '../../config/settings.js'; import { loadCliConfig } from '../../config/config.js'; import type { CliArgs } from '../../config/config.js'; import { InteractiveSelector } from './interactiveSelector.js'; interface QwenAuthOptions { region?: string; key?: string; } interface CodingPlanSettings { region?: CodingPlanRegion; version?: string; } interface MergedSettingsWithCodingPlan { security?: { auth?: { selectedType?: string; }; }; codingPlan?: CodingPlanSettings; model?: { name?: string; }; modelProviders?: Record; env?: Record; } /** * Handles the authentication process based on the specified command and options */ export async function handleQwenAuth( command: 'qwen-oauth' | 'coding-plan', options: QwenAuthOptions, ) { try { const settings = loadSettings(); // Create a minimal argv for config loading const minimalArgv: CliArgs = { query: undefined, model: undefined, sandbox: undefined, sandboxImage: undefined, debug: undefined, prompt: undefined, promptInteractive: undefined, yolo: undefined, bare: undefined, approvalMode: undefined, telemetry: undefined, checkpointing: undefined, telemetryTarget: undefined, telemetryOtlpEndpoint: undefined, telemetryOtlpProtocol: undefined, telemetryLogPrompts: undefined, telemetryOutfile: undefined, allowedMcpServerNames: undefined, allowedTools: undefined, acp: undefined, experimentalAcp: undefined, experimentalLsp: undefined, extensions: [], listExtensions: undefined, openaiLogging: undefined, openaiApiKey: undefined, openaiBaseUrl: undefined, openaiLoggingDir: undefined, proxy: undefined, includeDirectories: undefined, screenReader: undefined, inputFormat: undefined, outputFormat: undefined, includePartialMessages: undefined, chatRecording: undefined, continue: undefined, resume: undefined, sessionId: undefined, maxSessionTurns: undefined, coreTools: undefined, excludeTools: undefined, disabledSlashCommands: undefined, authType: undefined, channel: undefined, systemPrompt: undefined, appendSystemPrompt: undefined, }; // Create a minimal config to access settings and storage const config = await loadCliConfig( settings.merged, minimalArgv, process.cwd(), [], // No extensions for auth command // Pass separated hooks for proper source attribution { userHooks: settings.getUserHooks(), projectHooks: settings.getProjectHooks(), }, ); if (command === 'qwen-oauth') { await handleQwenOAuth(config, settings); } else if (command === 'coding-plan') { await handleCodePlanAuth(config, settings, options); } // Exit after authentication is complete writeStdoutLine(t('Authentication completed successfully.')); process.exit(0); } catch (error) { writeStderrLine(getErrorMessage(error)); process.exit(1); } } /** * Handles Qwen OAuth authentication */ async function handleQwenOAuth( config: Config, settings: LoadedSettings, ): Promise { writeStdoutLine(t('Starting Qwen OAuth authentication...')); try { await config.refreshAuth(AuthType.QWEN_OAUTH); // Persist the auth type const authTypeScope = getPersistScopeForModelSelection(settings); settings.setValue( authTypeScope, 'security.auth.selectedType', AuthType.QWEN_OAUTH, ); writeStdoutLine(t('Successfully authenticated with Qwen OAuth.')); process.exit(0); } catch (error) { writeStderrLine( t('Failed to authenticate with Qwen OAuth: {{error}}', { error: getErrorMessage(error), }), ); process.exit(1); } } /** * Handles Alibaba Cloud Coding Plan authentication */ async function handleCodePlanAuth( config: Config, settings: LoadedSettings, options: QwenAuthOptions, ): Promise { const { region, key } = options; let selectedRegion: CodingPlanRegion; let selectedKey: string; // If region and key are provided as options, use them if (region && key) { selectedRegion = region.toLowerCase() === 'global' ? CodingPlanRegion.GLOBAL : CodingPlanRegion.CHINA; selectedKey = key; } else { // Otherwise, prompt interactively selectedRegion = await promptForRegion(); selectedKey = await promptForKey(); } writeStdoutLine(t('Processing Alibaba Cloud Coding Plan authentication...')); try { // Get configuration based on region const { template, version } = getCodingPlanConfig(selectedRegion); // Get persist scope const authTypeScope = getPersistScopeForModelSelection(settings); // Backup settings file before modification const settingsFile = settings.forScope(authTypeScope); backupSettingsFile(settingsFile.path); // Store api-key in settings.env (unified env key) settings.setValue(authTypeScope, `env.${CODING_PLAN_ENV_KEY}`, selectedKey); // Sync to process.env immediately so refreshAuth can read the apiKey process.env[CODING_PLAN_ENV_KEY] = selectedKey; // Generate model configs from template const newConfigs = template.map((templateConfig) => ({ ...templateConfig, envKey: CODING_PLAN_ENV_KEY, })); // Get existing configs const existingConfigs = (settings.merged.modelProviders as Record)?.[ AuthType.USE_OPENAI ] || []; // Filter out all existing Coding Plan configs (mutually exclusive) const nonCodingPlanConfigs = existingConfigs.filter( (existing) => !isCodingPlanConfig(existing.baseUrl, existing.envKey), ); // Add new Coding Plan configs at the beginning const updatedConfigs = [...newConfigs, ...nonCodingPlanConfigs]; // Persist to modelProviders settings.setValue( authTypeScope, `modelProviders.${AuthType.USE_OPENAI}`, updatedConfigs, ); // Also persist authType settings.setValue( authTypeScope, 'security.auth.selectedType', AuthType.USE_OPENAI, ); // Persist coding plan region settings.setValue(authTypeScope, 'codingPlan.region', selectedRegion); // Persist coding plan version (single field for backward compatibility) settings.setValue(authTypeScope, 'codingPlan.version', version); // If there are configs, use the first one as the model if (updatedConfigs.length > 0 && updatedConfigs[0]?.id) { settings.setValue( authTypeScope, 'model.name', (updatedConfigs[0] as ModelConfig).id, ); } // Refresh auth with the new configuration await config.refreshAuth(AuthType.USE_OPENAI); writeStdoutLine( t('Successfully authenticated with Alibaba Cloud Coding Plan.'), ); } catch (error) { writeStderrLine( t('Failed to authenticate with Coding Plan: {{error}}', { error: getErrorMessage(error), }), ); process.exit(1); } } /** * Prompts the user to select a region using an interactive selector */ async function promptForRegion(): Promise { const selector = new InteractiveSelector( [ { value: CodingPlanRegion.CHINA, label: t('中国 (China)'), description: t('阿里云百炼 (aliyun.com)'), }, { value: CodingPlanRegion.GLOBAL, label: t('Global'), description: t('Alibaba Cloud (alibabacloud.com)'), }, ], t('Select region for Coding Plan:'), ); return await selector.select(); } /** * Prompts the user to enter an API key */ async function promptForKey(): Promise { // Create a simple password-style input (without echoing characters) const stdin = process.stdin; const stdout = process.stdout; stdout.write(t('Enter your Coding Plan API key: ')); // Set raw mode to capture keystrokes const wasRaw = stdin.isRaw; if (stdin.setRawMode) { stdin.setRawMode(true); } stdin.resume(); return new Promise((resolve, reject) => { let input = ''; const onData = (chunk: string) => { for (const char of chunk) { switch (char) { case '\r': // Enter case '\n': stdin.removeListener('data', onData); if (stdin.setRawMode) { stdin.setRawMode(wasRaw); } stdout.write('\n'); // New line after input resolve(input); return; case '\x03': // Ctrl+C stdin.removeListener('data', onData); if (stdin.setRawMode) { stdin.setRawMode(wasRaw); } stdout.write('^C\n'); reject(new Error('Interrupted')); return; case '\x08': // Backspace case '\x7F': // Delete if (input.length > 0) { input = input.slice(0, -1); // Move cursor back, print space, move back again stdout.write('\x1B[D \x1B[D'); } break; default: // Add character to input input += char; // Print asterisk instead of the actual character for security stdout.write('*'); break; } } }; stdin.on('data', onData); }); } /** * Runs the interactive authentication flow */ export async function runInteractiveAuth() { const selector = new InteractiveSelector( [ { value: 'coding-plan' as const, label: t('Alibaba Cloud Coding Plan'), description: t( 'Paid · Up to 6,000 requests/5 hrs · All Alibaba Cloud Coding Plan Models', ), }, { value: 'qwen-oauth' as const, label: t('Qwen OAuth'), description: t('Discontinued — switch to Coding Plan or API Key'), }, ], t('Select authentication method:'), ); let choice = await selector.select(); // If user selects discontinued Qwen OAuth, warn and re-prompt while (choice === 'qwen-oauth') { writeStdoutLine( t( '\n⚠ Qwen OAuth free tier was discontinued on 2026-04-15. Please select another option.\n', ), ); choice = await selector.select(); } if (choice === 'coding-plan') { await handleQwenAuth('coding-plan', {}); } } /** * Shows the current authentication status */ export async function showAuthStatus(): Promise { try { const settings = loadSettings(); const mergedSettings = settings.merged as MergedSettingsWithCodingPlan; writeStdoutLine(t('\n=== Authentication Status ===\n')); // Check for selected auth type const selectedType = mergedSettings.security?.auth?.selectedType; if (!selectedType) { writeStdoutLine(t('⚠️ No authentication method configured.\n')); writeStdoutLine(t('Run one of the following commands to get started:\n')); writeStdoutLine( t( ' qwen auth qwen-oauth - Authenticate with Qwen OAuth (free tier)', ), ); writeStdoutLine( t( ' qwen auth coding-plan - Authenticate with Alibaba Cloud Coding Plan\n', ), ); writeStdoutLine(t('Or simply run:')); writeStdoutLine( t(' qwen auth - Interactive authentication setup\n'), ); process.exit(0); } // Display status based on auth type if (selectedType === AuthType.QWEN_OAUTH) { writeStdoutLine(t('✓ Authentication Method: Qwen OAuth')); writeStdoutLine(t(' Type: Free tier (discontinued 2026-04-15)')); writeStdoutLine(t(' Limit: No longer available')); writeStdoutLine(t(' Models: Qwen latest models')); writeStdoutLine( t('\n ⚠ Run /auth to switch to Coding Plan or another provider.\n'), ); } else if (selectedType === AuthType.USE_OPENAI) { // Check for Coding Plan configuration const codingPlanRegion = mergedSettings.codingPlan?.region; const codingPlanVersion = mergedSettings.codingPlan?.version; const modelName = mergedSettings.model?.name; // Check if API key is set in environment const hasApiKey = !!process.env[CODING_PLAN_ENV_KEY] || !!mergedSettings.env?.[CODING_PLAN_ENV_KEY]; if (hasApiKey) { writeStdoutLine( t('✓ Authentication Method: Alibaba Cloud Coding Plan'), ); if (codingPlanRegion) { const regionDisplay = codingPlanRegion === CodingPlanRegion.CHINA ? t('中国 (China) - 阿里云百炼') : t('Global - Alibaba Cloud'); writeStdoutLine(t(' Region: {{region}}', { region: regionDisplay })); } if (modelName) { writeStdoutLine( t(' Current Model: {{model}}', { model: modelName }), ); } if (codingPlanVersion) { writeStdoutLine( t(' Config Version: {{version}}', { version: codingPlanVersion.substring(0, 8) + '...', }), ); } writeStdoutLine(t(' Status: API key configured\n')); } else { writeStdoutLine( t( '⚠️ Authentication Method: Alibaba Cloud Coding Plan (Incomplete)', ), ); writeStdoutLine( t(' Issue: API key not found in environment or settings\n'), ); writeStdoutLine(t(' Run `qwen auth coding-plan` to re-configure.\n')); } } else { writeStdoutLine( t('✓ Authentication Method: {{type}}', { type: selectedType }), ); writeStdoutLine(t(' Status: Configured\n')); } process.exit(0); } catch (error) { writeStderrLine( t('Failed to check authentication status: {{error}}', { error: getErrorMessage(error), }), ); process.exit(1); } }