fix(cli): honor --openai-api-key in non-interactive auth validation (#3187)

validateAuthMethod's pre-flight check only inspected OPENAI_API_KEY (and
settings.security.auth.apiKey), so credentials supplied via --openai-api-key
were rejected even though refreshAuth would have accepted them. macOS users
were unaffected because OPENAI_API_KEY is commonly exported in their shell
profile; on Linux without that env var, the CLI failed to start.

hasApiKeyForAuth now prefers the API key already resolved into
generationConfig.apiKey when a Config is provided. The unified resolver
folds CLI flags, env vars, settings, and modelProvider envKey lookups into
this single value, so validation matches runtime behavior.

Fixes #3171
This commit is contained in:
tanzhenxin 2026-04-13 18:06:27 +08:00 committed by GitHub
parent 9cdf7bd7c8
commit 62867702f7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
3 changed files with 77 additions and 0 deletions

View file

@ -186,6 +186,7 @@ describe('validateAuthMethod', () => {
const mockConfig = {
getModelsConfig: vi.fn().mockReturnValue({
getModel: vi.fn().mockReturnValue('cli-model'),
getGenerationConfig: vi.fn().mockReturnValue({}),
}),
} as unknown as import('@qwen-code/qwen-code-core').Config;
@ -219,6 +220,7 @@ describe('validateAuthMethod', () => {
const mockConfig = {
getModelsConfig: vi.fn().mockReturnValue({
getModel: vi.fn().mockReturnValue('cli-model'),
getGenerationConfig: vi.fn().mockReturnValue({}),
}),
} as unknown as import('@qwen-code/qwen-code-core').Config;
@ -227,4 +229,54 @@ describe('validateAuthMethod', () => {
expect(result).not.toBeNull();
expect(result).toContain('CLI_API_KEY');
});
// Regression test for #3171: validation must accept the API key resolved
// into generationConfig.apiKey (e.g. from --openai-api-key) instead of
// requiring an OPENAI_API_KEY env var.
it('should accept API key resolved into generationConfig from CLI flag', () => {
delete process.env['OPENAI_API_KEY'];
vi.mocked(settings.loadSettings).mockReturnValue({
merged: {},
} as unknown as ReturnType<typeof settings.loadSettings>);
const mockConfig = {
getModelsConfig: vi.fn().mockReturnValue({
getModel: vi.fn().mockReturnValue('gpt-4'),
getGenerationConfig: vi
.fn()
.mockReturnValue({ apiKey: 'cli-provided-key' }),
}),
} as unknown as import('@qwen-code/qwen-code-core').Config;
const result = validateAuthMethod(AuthType.USE_OPENAI, mockConfig);
expect(result).toBeNull();
});
// Regression test for #3171: when a modelProvider has a custom envKey but
// the user passes --openai-api-key on the CLI, the resolver picks the CLI
// value. Validation should match the resolver and accept it instead of
// demanding the env var.
it('should accept CLI-resolved key even when modelProvider declares a custom envKey', () => {
delete process.env['CUSTOM_API_KEY'];
vi.mocked(settings.loadSettings).mockReturnValue({
merged: {
model: { name: 'custom-model' },
modelProviders: {
openai: [{ id: 'custom-model', envKey: 'CUSTOM_API_KEY' }],
},
},
} as unknown as ReturnType<typeof settings.loadSettings>);
const mockConfig = {
getModelsConfig: vi.fn().mockReturnValue({
getModel: vi.fn().mockReturnValue('custom-model'),
getGenerationConfig: vi
.fn()
.mockReturnValue({ apiKey: 'cli-provided-key' }),
}),
} as unknown as import('@qwen-code/qwen-code-core').Config;
const result = validateAuthMethod(AuthType.USE_OPENAI, mockConfig);
expect(result).toBeNull();
});
});

View file

@ -66,6 +66,26 @@ function hasApiKeyForAuth(
// Try to find model-specific envKey from modelProviders
const modelConfig = findModelConfig(modelProviders, authType, modelId);
// If a Config is available, prefer the API key already resolved into the
// generation config. The unified resolver folds CLI flags (e.g.
// --openai-api-key), env vars, settings.security.auth.apiKey, and
// modelProvider envKey lookups into this single value, so it is the same
// key that refreshAuth will actually use at runtime. Validating against it
// keeps pre-flight checks consistent with runtime behavior — without this,
// CLI-provided credentials are silently ignored when no env var is set
// (issue #3171).
const resolvedApiKey = config
?.getModelsConfig()
.getGenerationConfig()?.apiKey;
if (resolvedApiKey) {
return {
hasKey: true,
checkedEnvKey: modelConfig?.envKey ?? DEFAULT_ENV_KEYS[authType],
isExplicitEnvKey: !!modelConfig?.envKey,
};
}
if (modelConfig?.envKey) {
// Explicit envKey configured - only check this env var, no apiKey fallback
const hasKey = !!process.env[modelConfig.envKey];