fix(cli): recognize OpenAI-compatible providers in qwen auth status (#3623)

* fix(cli): recognize OpenAI-compatible providers in `qwen auth status`

Previously `qwen auth status` treated all `selectedType=openai` setups
as Coding Plan, checking only `BAILIAN_CODING_PLAN_API_KEY`. Users who
configured generic OpenAI-compatible providers (e.g. Xunfei, DeepSeek,
Ollama) via `modelProviders.openai` saw a misleading "Alibaba Cloud
Coding Plan (Incomplete)" even though their provider worked correctly.

Split the USE_OPENAI branch into two paths:
- Coding Plan: detected by `codingPlan.region` or `CODING_PLAN_ENV_KEY`
- Generic OpenAI-compatible: checks API key from modelProviders envKey,
  OPENAI_API_KEY, or settings.security.auth.apiKey; displays provider
  info including model name and base URL.

Closes #3612

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): improve Coding Plan detection and align API key check semantics

Address review feedback:

1. Detect Coding Plan via isCodingPlanConfig(baseUrl, envKey) on the
   active modelProviders entry instead of checking BAILIAN_CODING_PLAN_API_KEY
   env var presence. A stale env key from a previous setup no longer
   misclassifies a generic OpenAI-compatible provider as Coding Plan.

2. When modelProviders entry has an explicit envKey, only check that key
   without falling back to OPENAI_API_KEY or settings.security.auth.apiKey.
   This mirrors hasApiKeyForAuth() semantics in auth.ts, preventing
   status from reporting "configured" when the actual provider key is
   missing.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): fix TS2367 type error in displayRegion comparison

`detectedCodingPlanRegion` is `CodingPlanRegion | false`, so
`!== true` comparison is invalid. Simplify to truthiness check.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): refine model fallback and Coding Plan key detection

- Only fall back to models[0] when model.name is unset; when set but
  not matching any provider entry, treat as unmanaged to avoid binding
  status to an unrelated provider's envKey/baseUrl.
- Simplify hasCodingPlanKey to only check CODING_PLAN_ENV_KEY, not
  activeModelConfig.envKey, preventing a generic provider key from
  being mistaken as Coding Plan credentials when codingPlan.region
  is stale.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): prioritize active model config over stale codingPlan.region

When activeModelConfig exists, trust isCodingPlanConfig() result over
potentially stale codingPlan.region from a previous setup. This prevents
a user who switched from Coding Plan to a generic provider from still
seeing "Alibaba Cloud Coding Plan" in auth status.

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

* fix(cli): avoid stale Coding Plan fallback in auth status

Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>

---------

Co-authored-by: jinye.djy <jinye.djy@alibaba-inc.com>
Co-authored-by: Qwen-Coder <qwen-coder@alibabacloud.com>
This commit is contained in:
jinye 2026-04-28 09:06:09 +08:00 committed by GitHub
parent 1befabe586
commit 4ac9ec07c3
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
2 changed files with 475 additions and 30 deletions

View file

@ -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<void> {
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<void> {
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<void> {
);
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<void> {
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<void> {
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<void> {
);
}
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<void> {
} 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'));
}

View file

@ -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);
});
});
});