diff --git a/packages/cli/src/commands/auth/handler.ts b/packages/cli/src/commands/auth/handler.ts index 25a7d44fa..b5cf3da59 100644 --- a/packages/cli/src/commands/auth/handler.ts +++ b/packages/cli/src/commands/auth/handler.ts @@ -8,6 +8,7 @@ import { AuthType, getErrorMessage, type Config, + type ModelProvidersConfig, type ProviderModelConfig as ModelConfig, } from '@qwen-code/qwen-code-core'; import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js'; @@ -39,6 +40,8 @@ interface MergedSettingsWithCodingPlan { security?: { auth?: { selectedType?: string; + apiKey?: string; + baseUrl?: string; }; }; codingPlan?: CodingPlanSettings; @@ -446,54 +449,110 @@ export async function showAuthStatus(): Promise { 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 = + // Detect Coding Plan via region OR the presence of its env key + const hasCodingPlanKey = !!process.env[CODING_PLAN_ENV_KEY] || !!mergedSettings.env?.[CODING_PLAN_ENV_KEY]; - if (hasApiKey) { - writeStdoutLine( - t('✓ Authentication Method: Alibaba Cloud Coding Plan'), - ); + if (codingPlanRegion || hasCodingPlanKey) { + // --- Coding Plan path --- + const codingPlanVersion = mergedSettings.codingPlan?.version; - if (codingPlanRegion) { - const regionDisplay = - codingPlanRegion === CodingPlanRegion.CHINA - ? t('中国 (China) - 阿里云百炼') - : t('Global - Alibaba Cloud'); - writeStdoutLine(t(' Region: {{region}}', { region: regionDisplay })); - } - - if (modelName) { + if (hasCodingPlanKey) { writeStdoutLine( - t(' Current Model: {{model}}', { model: modelName }), + 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'), ); } - - 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')); + // --- Generic OpenAI-compatible provider path --- + const modelProviders = mergedSettings.modelProviders as + | ModelProvidersConfig + | undefined; + const models = modelProviders?.[AuthType.USE_OPENAI]; + const modelConfig = Array.isArray(models) + ? (models.find((m) => m.id === modelName) ?? models[0]) + : undefined; + + const hasApiKey = + !!( + modelConfig?.envKey && + (process.env[modelConfig.envKey] || + mergedSettings.env?.[modelConfig.envKey]) + ) || + !!process.env['OPENAI_API_KEY'] || + !!mergedSettings.env?.['OPENAI_API_KEY'] || + !!mergedSettings.security?.auth?.apiKey; + + if (hasApiKey) { + writeStdoutLine( + t('✓ Authentication Method: OpenAI-compatible Provider'), + ); + + if (modelName) { + writeStdoutLine( + t(' Current Model: {{model}}', { model: modelName }), + ); + } + + const baseUrl = + modelConfig?.baseUrl || mergedSettings.security?.auth?.baseUrl; + if (baseUrl) { + writeStdoutLine(t(' Base URL: {{baseUrl}}', { baseUrl })); + } + + writeStdoutLine(t(' Status: API key configured\n')); + } else { + writeStdoutLine( + t( + '⚠️ Authentication Method: OpenAI-compatible Provider (Incomplete)', + ), + ); + writeStdoutLine( + t(' Issue: API key not found in environment or settings\n'), + ); + writeStdoutLine(t(' Run `qwen auth` to re-configure.\n')); + } } } else { writeStdoutLine( diff --git a/packages/cli/src/commands/auth/status.test.ts b/packages/cli/src/commands/auth/status.test.ts index ca5a52718..bda010bb4 100644 --- a/packages/cli/src/commands/auth/status.test.ts +++ b/packages/cli/src/commands/auth/status.test.ts @@ -144,6 +144,39 @@ describe('showAuthStatus', () => { ); }); + it('should show Coding Plan when env key is set but codingPlan.region is missing', async () => { + process.env[CODING_PLAN_ENV_KEY] = 'test-api-key'; + + vi.mocked(loadSettings).mockReturnValue( + createMockSettings({ + security: { + auth: { + selectedType: AuthType.USE_OPENAI, + }, + }, + model: { + name: 'qwen3.5-plus', + }, + }), + ); + + await showAuthStatus(); + + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('Alibaba Cloud Coding Plan'), + ); + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('API key configured'), + ); + expect(writeStdoutLine).not.toHaveBeenCalledWith( + expect.stringContaining('OpenAI-compatible Provider'), + ); + expect(writeStdoutLine).not.toHaveBeenCalledWith( + expect.stringContaining('Region:'), + ); + expect(process.exit).toHaveBeenCalledWith(0); + }); + it('should show Coding Plan region for china', async () => { process.env[CODING_PLAN_ENV_KEY] = 'test-api-key'; @@ -262,4 +295,176 @@ describe('showAuthStatus', () => { ); expect(process.exit).toHaveBeenCalledWith(1); }); + + describe('OpenAI-compatible provider (no Coding Plan)', () => { + afterEach(() => { + delete process.env['OPENAI_API_KEY']; + delete process.env['CUSTOM_API_KEY']; + }); + + it('should show OpenAI-compatible status with OPENAI_API_KEY', async () => { + process.env['OPENAI_API_KEY'] = 'test-key'; + + vi.mocked(loadSettings).mockReturnValue( + createMockSettings({ + security: { + auth: { + selectedType: AuthType.USE_OPENAI, + }, + }, + model: { + name: 'gpt-4o', + }, + }), + ); + + await showAuthStatus(); + + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('OpenAI-compatible Provider'), + ); + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('gpt-4o'), + ); + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('API key configured'), + ); + expect(writeStdoutLine).not.toHaveBeenCalledWith( + expect.stringContaining('Alibaba Cloud Coding Plan'), + ); + expect(process.exit).toHaveBeenCalledWith(0); + }); + + it('should show OpenAI-compatible status with custom envKey from modelProviders', async () => { + process.env['CUSTOM_API_KEY'] = 'test-key'; + + vi.mocked(loadSettings).mockReturnValue( + createMockSettings({ + security: { + auth: { + selectedType: AuthType.USE_OPENAI, + }, + }, + model: { + name: 'custom-model', + }, + modelProviders: { + openai: [ + { + id: 'custom-model', + envKey: 'CUSTOM_API_KEY', + baseUrl: 'https://custom-api.example.com/v1', + }, + ], + }, + }), + ); + + await showAuthStatus(); + + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('OpenAI-compatible Provider'), + ); + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('custom-model'), + ); + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('https://custom-api.example.com/v1'), + ); + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('API key configured'), + ); + expect(process.exit).toHaveBeenCalledWith(0); + }); + + it('should show OpenAI-compatible status with settings.security.auth.apiKey', async () => { + vi.mocked(loadSettings).mockReturnValue( + createMockSettings({ + security: { + auth: { + selectedType: AuthType.USE_OPENAI, + apiKey: 'settings-api-key', + baseUrl: 'https://my-provider.example.com/v1', + }, + }, + model: { + name: 'my-model', + }, + }), + ); + + await showAuthStatus(); + + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('OpenAI-compatible Provider'), + ); + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('https://my-provider.example.com/v1'), + ); + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('API key configured'), + ); + expect(process.exit).toHaveBeenCalledWith(0); + }); + + it('should show incomplete when no API key is found for OpenAI-compatible provider', async () => { + vi.mocked(loadSettings).mockReturnValue( + createMockSettings({ + security: { + auth: { + selectedType: AuthType.USE_OPENAI, + }, + }, + }), + ); + + await showAuthStatus(); + + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('OpenAI-compatible Provider (Incomplete)'), + ); + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('API key not found'), + ); + expect(writeStdoutLine).not.toHaveBeenCalledWith( + expect.stringContaining('Alibaba Cloud Coding Plan'), + ); + }); + + it('should detect API key via default model when model.name is unset', async () => { + process.env['CUSTOM_API_KEY'] = 'test-key'; + + vi.mocked(loadSettings).mockReturnValue( + createMockSettings({ + security: { + auth: { + selectedType: AuthType.USE_OPENAI, + }, + }, + modelProviders: { + openai: [ + { + id: 'default-model', + envKey: 'CUSTOM_API_KEY', + baseUrl: 'https://default-api.example.com/v1', + }, + ], + }, + }), + ); + + await showAuthStatus(); + + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('OpenAI-compatible Provider'), + ); + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('https://default-api.example.com/v1'), + ); + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('API key configured'), + ); + expect(process.exit).toHaveBeenCalledWith(0); + }); + }); });