mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-28 03:30:40 +00:00
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>
This commit is contained in:
parent
83d1e6dcae
commit
bf49da22fc
2 changed files with 301 additions and 37 deletions
|
|
@ -8,6 +8,7 @@ import {
|
||||||
AuthType,
|
AuthType,
|
||||||
getErrorMessage,
|
getErrorMessage,
|
||||||
type Config,
|
type Config,
|
||||||
|
type ModelProvidersConfig,
|
||||||
type ProviderModelConfig as ModelConfig,
|
type ProviderModelConfig as ModelConfig,
|
||||||
} from '@qwen-code/qwen-code-core';
|
} from '@qwen-code/qwen-code-core';
|
||||||
import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js';
|
import { writeStdoutLine, writeStderrLine } from '../../utils/stdioHelpers.js';
|
||||||
|
|
@ -39,6 +40,8 @@ interface MergedSettingsWithCodingPlan {
|
||||||
security?: {
|
security?: {
|
||||||
auth?: {
|
auth?: {
|
||||||
selectedType?: string;
|
selectedType?: string;
|
||||||
|
apiKey?: string;
|
||||||
|
baseUrl?: string;
|
||||||
};
|
};
|
||||||
};
|
};
|
||||||
codingPlan?: CodingPlanSettings;
|
codingPlan?: CodingPlanSettings;
|
||||||
|
|
@ -446,54 +449,110 @@ export async function showAuthStatus(): Promise<void> {
|
||||||
t('\n ⚠ Run /auth to switch to Coding Plan or another provider.\n'),
|
t('\n ⚠ Run /auth to switch to Coding Plan or another provider.\n'),
|
||||||
);
|
);
|
||||||
} else if (selectedType === AuthType.USE_OPENAI) {
|
} else if (selectedType === AuthType.USE_OPENAI) {
|
||||||
// Check for Coding Plan configuration
|
|
||||||
const codingPlanRegion = mergedSettings.codingPlan?.region;
|
const codingPlanRegion = mergedSettings.codingPlan?.region;
|
||||||
const codingPlanVersion = mergedSettings.codingPlan?.version;
|
|
||||||
const modelName = mergedSettings.model?.name;
|
const modelName = mergedSettings.model?.name;
|
||||||
|
|
||||||
// Check if API key is set in environment
|
// Detect Coding Plan via region OR the presence of its env key
|
||||||
const hasApiKey =
|
const hasCodingPlanKey =
|
||||||
!!process.env[CODING_PLAN_ENV_KEY] ||
|
!!process.env[CODING_PLAN_ENV_KEY] ||
|
||||||
!!mergedSettings.env?.[CODING_PLAN_ENV_KEY];
|
!!mergedSettings.env?.[CODING_PLAN_ENV_KEY];
|
||||||
|
|
||||||
if (hasApiKey) {
|
if (codingPlanRegion || hasCodingPlanKey) {
|
||||||
writeStdoutLine(
|
// --- Coding Plan path ---
|
||||||
t('✓ Authentication Method: Alibaba Cloud Coding Plan'),
|
const codingPlanVersion = mergedSettings.codingPlan?.version;
|
||||||
);
|
|
||||||
|
|
||||||
if (codingPlanRegion) {
|
if (hasCodingPlanKey) {
|
||||||
const regionDisplay =
|
|
||||||
codingPlanRegion === CodingPlanRegion.CHINA
|
|
||||||
? t('中国 (China) - 阿里云百炼')
|
|
||||||
: t('Global - Alibaba Cloud');
|
|
||||||
writeStdoutLine(t(' Region: {{region}}', { region: regionDisplay }));
|
|
||||||
}
|
|
||||||
|
|
||||||
if (modelName) {
|
|
||||||
writeStdoutLine(
|
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 {
|
} else {
|
||||||
writeStdoutLine(
|
// --- Generic OpenAI-compatible provider path ---
|
||||||
t(
|
const modelProviders = mergedSettings.modelProviders as
|
||||||
'⚠️ Authentication Method: Alibaba Cloud Coding Plan (Incomplete)',
|
| ModelProvidersConfig
|
||||||
),
|
| undefined;
|
||||||
);
|
const models = modelProviders?.[AuthType.USE_OPENAI];
|
||||||
writeStdoutLine(
|
const modelConfig = Array.isArray(models)
|
||||||
t(' Issue: API key not found in environment or settings\n'),
|
? (models.find((m) => m.id === modelName) ?? models[0])
|
||||||
);
|
: undefined;
|
||||||
writeStdoutLine(t(' Run `qwen auth coding-plan` to re-configure.\n'));
|
|
||||||
|
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 {
|
} else {
|
||||||
writeStdoutLine(
|
writeStdoutLine(
|
||||||
|
|
|
||||||
|
|
@ -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 () => {
|
it('should show Coding Plan region for china', async () => {
|
||||||
process.env[CODING_PLAN_ENV_KEY] = 'test-api-key';
|
process.env[CODING_PLAN_ENV_KEY] = 'test-api-key';
|
||||||
|
|
||||||
|
|
@ -262,4 +295,176 @@ describe('showAuthStatus', () => {
|
||||||
);
|
);
|
||||||
expect(process.exit).toHaveBeenCalledWith(1);
|
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);
|
||||||
|
});
|
||||||
|
});
|
||||||
});
|
});
|
||||||
|
|
|
||||||
Loading…
Add table
Add a link
Reference in a new issue