diff --git a/packages/cli/src/commands/auth/handler.ts b/packages/cli/src/commands/auth/handler.ts index 6793ade1b..31c3aace6 100644 --- a/packages/cli/src/commands/auth/handler.ts +++ b/packages/cli/src/commands/auth/handler.ts @@ -55,6 +55,8 @@ interface MergedSettingsWithCodingPlan { security?: { auth?: { selectedType?: string; + apiKey?: string; + baseUrl?: string; }; }; codingPlan?: CodingPlanSettings; @@ -832,9 +834,15 @@ export async function showAuthStatus(): Promise { const isActiveOpenRouter = activeConfig ? isOpenRouterConfig(activeConfig) : false; - const isActiveCodingPlan = - activeConfig && - isCodingPlanConfig(activeConfig.baseUrl, activeConfig.envKey) !== false; + const providerCodingPlanRegion = isCodingPlanConfig( + activeConfig?.baseUrl, + activeConfig?.envKey, + ); + const detectedCodingPlanRegion = activeConfig + ? providerCodingPlanRegion + : !modelName + ? codingPlanRegion + : false; const isActiveStandard = activeConfig && activeConfig.envKey === DASHSCOPE_STANDARD_API_KEY_ENV_KEY && @@ -849,11 +857,13 @@ export async function showAuthStatus(): Promise { if (isActiveOpenRouter) { if (hasOpenRouterApiKey) { writeStdoutLine(t('✓ Authentication Method: OpenRouter')); + if (modelName) { writeStdoutLine( t(' Current Model: {{model}}', { model: modelName }), ); } + writeStdoutLine(t(' Status: API key configured\n')); } else { writeStdoutLine( @@ -864,7 +874,7 @@ export async function showAuthStatus(): Promise { ); writeStdoutLine(t(' Run `qwen auth openrouter` to re-configure.\n')); } - } else if (isActiveCodingPlan) { + } else if (detectedCodingPlanRegion) { const hasCodingPlanKey = !!process.env[CODING_PLAN_ENV_KEY] || !!mergedSettings.env?.[CODING_PLAN_ENV_KEY]; @@ -874,9 +884,10 @@ export async function showAuthStatus(): Promise { t('✓ Authentication Method: Alibaba Cloud Coding Plan'), ); - if (codingPlanRegion) { + const displayRegion = codingPlanRegion || detectedCodingPlanRegion; + if (displayRegion) { const regionDisplay = - codingPlanRegion === CodingPlanRegion.CHINA + displayRegion === CodingPlanRegion.CHINA ? t('中国 (China) - 阿里云百炼') : t('Global - Alibaba Cloud'); writeStdoutLine( @@ -943,13 +954,21 @@ export async function showAuthStatus(): Promise { writeStdoutLine(t(' Run `qwen auth api-key` to re-configure.\n')); } } else if (activeConfig) { - const envKey = activeConfig.envKey; - const hasKey = - envKey && (!!process.env[envKey] || !!mergedSettings.env?.[envKey]); + let hasApiKey: boolean; + if (activeConfig.envKey) { + hasApiKey = + !!process.env[activeConfig.envKey] || + !!mergedSettings.env?.[activeConfig.envKey]; + } else { + hasApiKey = + !!process.env['OPENAI_API_KEY'] || + !!mergedSettings.env?.['OPENAI_API_KEY'] || + !!mergedSettings.security?.auth?.apiKey; + } - if (hasKey || !envKey) { + if (hasApiKey) { writeStdoutLine( - t('✓ Authentication Method: OpenAI-compatible API Key'), + t('✓ Authentication Method: OpenAI-compatible Provider'), ); if (modelName) { @@ -958,34 +977,53 @@ export async function showAuthStatus(): Promise { ); } - writeStdoutLine(t(' Status: Configured\n')); + const baseUrl = + activeConfig.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 API Key (Incomplete)', + '⚠️ Authentication Method: OpenAI-compatible Provider (Incomplete)', ), ); writeStdoutLine( - t( - ' Issue: API key {{envKey}} not found in environment or settings\n', - { - envKey, - }, - ), - ); - writeStdoutLine( - t( - ' Configure it in settings.json or set the environment variable.\n', - ), + t(' Issue: API key not found in environment or settings\n'), ); + writeStdoutLine(t(' Run `qwen auth` to re-configure.\n')); } } else { const hasCodingPlanKey = !!process.env[CODING_PLAN_ENV_KEY] || !!mergedSettings.env?.[CODING_PLAN_ENV_KEY]; - const hasCodingPlanMetadata = !!codingPlanRegion || !!codingPlanVersion; + const hasGenericApiKey = + !!process.env['OPENAI_API_KEY'] || + !!mergedSettings.env?.['OPENAI_API_KEY'] || + !!mergedSettings.security?.auth?.apiKey; + const hasCodingPlanMetadata = + !modelName && (!!codingPlanRegion || !!codingPlanVersion); - if (hasCodingPlanKey) { + if (hasGenericApiKey) { + writeStdoutLine( + t('✓ Authentication Method: OpenAI-compatible Provider'), + ); + + if (modelName) { + writeStdoutLine( + t(' Current Model: {{model}}', { model: modelName }), + ); + } + + const baseUrl = mergedSettings.security?.auth?.baseUrl; + if (baseUrl) { + writeStdoutLine(t(' Base URL: {{baseUrl}}', { baseUrl })); + } + + writeStdoutLine(t(' Status: API key configured\n')); + } else if (hasCodingPlanKey) { writeStdoutLine( t('✓ Authentication Method: Alibaba Cloud Coding Plan'), ); @@ -1030,13 +1068,11 @@ export async function showAuthStatus(): Promise { } else { writeStdoutLine( t( - '⚠️ Authentication Method: OpenAI-compatible API Key (Incomplete)', + '⚠️ Authentication Method: OpenAI-compatible Provider (Incomplete)', ), ); writeStdoutLine( - t( - ' Issue: No model provider configuration found for the selected model.\n', - ), + t(' Issue: API key not found in environment or settings\n'), ); writeStdoutLine(t(' Run `qwen auth` to re-configure.\n')); } diff --git a/packages/cli/src/commands/auth/status.test.ts b/packages/cli/src/commands/auth/status.test.ts index b7b649a82..e49a4e553 100644 --- a/packages/cli/src/commands/auth/status.test.ts +++ b/packages/cli/src/commands/auth/status.test.ts @@ -27,12 +27,14 @@ describe('showAuthStatus', () => { vi.clearAllMocks(); vi.spyOn(process, 'exit').mockImplementation((() => undefined) as never); delete process.env[CODING_PLAN_ENV_KEY]; + delete process.env['OPENAI_API_KEY']; delete process.env['OPENROUTER_API_KEY']; }); afterEach(() => { vi.restoreAllMocks(); delete process.env[CODING_PLAN_ENV_KEY]; + delete process.env['OPENAI_API_KEY']; delete process.env['OPENROUTER_API_KEY']; }); @@ -221,6 +223,78 @@ describe('showAuthStatus', () => { ); }); + it('should show Coding Plan when detected via modelProviders entry (no codingPlan.region)', 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', + }, + modelProviders: { + openai: [ + { + id: 'qwen3.5-plus', + envKey: 'BAILIAN_CODING_PLAN_API_KEY', + baseUrl: 'https://coding.dashscope.aliyuncs.com/v1', + }, + ], + }, + }), + ); + + await showAuthStatus(); + + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('Alibaba Cloud Coding Plan'), + ); + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('API key configured'), + ); + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('中国 (China)'), + ); + expect(writeStdoutLine).not.toHaveBeenCalledWith( + expect.stringContaining('OpenAI-compatible Provider'), + ); + expect(process.exit).toHaveBeenCalledWith(0); + }); + + it('should not fall back to stale Coding Plan metadata when model selection is unmatched', async () => { + process.env['OPENAI_API_KEY'] = 'test-openai-key'; + + vi.mocked(loadSettings).mockReturnValue( + createMockSettings({ + security: { + auth: { + selectedType: AuthType.USE_OPENAI, + }, + }, + codingPlan: { + region: 'global', + version: 'abc123def456', + }, + model: { + name: 'manual-provider-model', + }, + }), + ); + + await showAuthStatus(); + + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('OpenAI-compatible Provider'), + ); + expect(writeStdoutLine).not.toHaveBeenCalledWith( + expect.stringContaining('Alibaba Cloud Coding Plan'), + ); + }); + it('should show Coding Plan region for china', async () => { process.env[CODING_PLAN_ENV_KEY] = 'test-api-key'; @@ -339,4 +413,339 @@ 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']; + delete process.env['XUNFEI_API_KEY']; + delete process.env[CODING_PLAN_ENV_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); + }); + + it('should show Incomplete when explicit envKey is missing even if OPENAI_API_KEY is set', async () => { + process.env['OPENAI_API_KEY'] = 'fallback-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 (Incomplete)'), + ); + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('API key not found'), + ); + }); + + it('should not bind to unrelated provider entry when model.name does not match', async () => { + process.env['OPENAI_API_KEY'] = 'test-key'; + + vi.mocked(loadSettings).mockReturnValue( + createMockSettings({ + security: { + auth: { + selectedType: AuthType.USE_OPENAI, + }, + }, + model: { + name: 'manual-model-not-in-providers', + }, + modelProviders: { + openai: [ + { + id: 'other-model', + envKey: 'OTHER_API_KEY', + baseUrl: 'https://other-api.example.com/v1', + }, + ], + }, + }), + ); + + await showAuthStatus(); + + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('OpenAI-compatible Provider'), + ); + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('API key configured'), + ); + // Should NOT show the unrelated provider's base URL + expect(writeStdoutLine).not.toHaveBeenCalledWith( + expect.stringContaining('https://other-api.example.com/v1'), + ); + expect(process.exit).toHaveBeenCalledWith(0); + }); + + it('should show OpenAI-compatible when stale codingPlan.region exists but active model is generic', async () => { + process.env['XUNFEI_API_KEY'] = 'active-key'; + + vi.mocked(loadSettings).mockReturnValue( + createMockSettings({ + security: { + auth: { + selectedType: AuthType.USE_OPENAI, + }, + }, + codingPlan: { + region: 'china', + version: 'stale-version', + }, + model: { + name: 'spark-v4', + }, + modelProviders: { + openai: [ + { + id: 'spark-v4', + envKey: 'XUNFEI_API_KEY', + baseUrl: 'https://spark-api-open.xf-yun.com/v1', + }, + ], + }, + }), + ); + + await showAuthStatus(); + + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('OpenAI-compatible Provider'), + ); + 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 when stale Coding Plan key exists but active model is generic', async () => { + process.env[CODING_PLAN_ENV_KEY] = 'stale-coding-plan-key'; + process.env['XUNFEI_API_KEY'] = 'active-key'; + + vi.mocked(loadSettings).mockReturnValue( + createMockSettings({ + security: { + auth: { + selectedType: AuthType.USE_OPENAI, + }, + }, + model: { + name: 'spark-v4', + }, + modelProviders: { + openai: [ + { + id: 'spark-v4', + envKey: 'XUNFEI_API_KEY', + baseUrl: 'https://spark-api-open.xf-yun.com/v1', + }, + ], + }, + }), + ); + + await showAuthStatus(); + + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('OpenAI-compatible Provider'), + ); + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('spark-v4'), + ); + expect(writeStdoutLine).toHaveBeenCalledWith( + expect.stringContaining('API key configured'), + ); + expect(writeStdoutLine).not.toHaveBeenCalledWith( + expect.stringContaining('Alibaba Cloud Coding Plan'), + ); + expect(process.exit).toHaveBeenCalledWith(0); + }); + }); });