Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
19 changes: 18 additions & 1 deletion docs/cli/openai-auth.md
Original file line number Diff line number Diff line change
Expand Up @@ -62,7 +62,24 @@ You can use custom endpoints by setting the `OPENAI_BASE_URL` environment variab

- Using Azure OpenAI
- Using other OpenAI-compatible APIs
- Using local OpenAI-compatible servers
- Using local OpenAI-compatible servers (e.g., LM Studio, Ollama, llama.cpp)

### Local OpenAI-Compatible Servers

Run Qwen Code with local OpenAI-compatible servers for offline development:

```bash
# For local OpenAI-compatible servers
export OPENAI_API_KEY="any-value" # Required but can be any non-empty value
export OPENAI_BASE_URL="http://localhost:1234/v1" # Your local server URL
export OPENAI_MODEL="your-model-name" # Model name from your server
export OPENAI_STREAMING="false" # Optional: disable streaming for better reliability

qwen
```

**Notes:**
- Some local servers may have streaming reliability issues. Set `OPENAI_STREAMING=false` for improved stability

## Switching Authentication Methods

Expand Down
58 changes: 49 additions & 9 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import {
FileDiscoveryService,
TelemetryTarget,
FileFilteringOptions,
AuthType,
ShellTool,
EditTool,
WriteFileTool,
Expand Down Expand Up @@ -344,15 +345,8 @@ export async function loadCliConfig(
const activeExtensions = extensions.filter(
(_, i) => allExtensions[i].isActive,
);
// Handle OpenAI API key from command line
if (argv.openaiApiKey) {
process.env.OPENAI_API_KEY = argv.openaiApiKey;
}

// Handle OpenAI base URL from command line
if (argv.openaiBaseUrl) {
process.env.OPENAI_BASE_URL = argv.openaiBaseUrl;
}
// Set up OpenAI configuration from CLI arguments
setupOpenAIFromCliArgs(argv);

// Handle Tavily API key from command line
if (argv.tavilyApiKey) {
Expand Down Expand Up @@ -608,3 +602,49 @@ function mergeExcludeTools(
}
return [...allExcludeTools];
}

/**
* Apply OpenAI configuration from CLI arguments to environment.
* Following the existing pattern for environment setup.
* @param argv - Command line arguments
*/
function setupOpenAIFromCliArgs(argv: CliArgs): void {
// Following existing pattern for environment variable setup
if (argv.openaiApiKey) {
process.env.OPENAI_API_KEY = argv.openaiApiKey;
}

if (argv.openaiBaseUrl) {
process.env.OPENAI_BASE_URL = argv.openaiBaseUrl;
}

// Set model if using OpenAI
if (argv.model && process.env.OPENAI_API_KEY) {
process.env.OPENAI_MODEL = argv.model;
}
}

/**
* Get the effective auth type based on configuration hierarchy.
* Environment variables take precedence over settings.
* @param settings - Loaded settings
* @returns The auth type to use
*/
export function getEffectiveAuthType(settings: Settings): AuthType | undefined {
// Check environment variables first (highest precedence)
if (process.env.OPENAI_API_KEY) {
return AuthType.USE_OPENAI;
}
if (process.env.GEMINI_API_KEY) {
return AuthType.USE_GEMINI;
}
if (process.env.GOOGLE_GENAI_USE_VERTEXAI === 'true') {

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] This new shared auth resolver no longer considers QWEN_OAUTH_TOKEN, even though other code paths still treat that env var as a valid signal for Qwen auth. As soon as validateNonInteractiveAuth() switches to this helper, existing token-based Qwen setups stop resolving an auth type in non-interactive/shared flows. Please preserve the previous env-based auth matrix here before making this the source of truth.

— gpt-5.4 via Qwen Code /review

return AuthType.USE_VERTEX_AI;
}
if (process.env.GOOGLE_GENAI_USE_GCA === 'true') {
return AuthType.LOGIN_WITH_GOOGLE;
}

// Fall back to settings
return settings.selectedAuthType;
}
64 changes: 49 additions & 15 deletions packages/cli/src/gemini.tsx
Original file line number Diff line number Diff line change
Expand Up @@ -39,13 +39,50 @@ import {
IdeConnectionType,
} from '@qwen-code/qwen-code-core';
import { validateAuthMethod } from './config/auth.js';
import { getEffectiveAuthType } from './config/config.js';
import { setMaxSizedBoxDebugging } from './ui/components/shared/MaxSizedBox.js';
import { validateNonInteractiveAuth } from './validateNonInterActiveAuth.js';
import { checkForUpdates } from './ui/utils/updateCheck.js';
import { handleAutoUpdate } from './utils/handleAutoUpdate.js';
import { appEvents, AppEvent } from './utils/events.js';
import { SettingsContext } from './ui/contexts/SettingsContext.js';

/**
* Initialize authentication for the config based on settings and environment.
* Centralizes auth initialization to follow DRY principle.
* @param config - The Config instance to initialize auth for
* @param settings - The loaded settings containing selectedAuthType
* @returns Promise<void>
*/
async function initializeAuth(
config: Config,
settings: LoadedSettings,
): Promise<void> {
// Skip if using external auth
if (settings.merged.useExternalAuth) {
return;
}

// Get the effective auth type based on configuration hierarchy
const effectiveAuthType = getEffectiveAuthType(settings.merged);

if (!effectiveAuthType) {
return;
}

Copy link
Copy Markdown
Collaborator

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

[Critical] initializeAuth() now swallows every auth initialization failure here. That changes the sandbox startup path too, because the surrounding try/catch no longer sees the error and cannot fail fast before launching. Broken or expired credentials will now slip past startup and fail later in a much harder-to-diagnose place. Please rethrow after logging, or return an explicit failure result that callers must handle.

— gpt-5.4 via Qwen Code /review


try {
const err = validateAuthMethod(effectiveAuthType);
if (!err) {
await config.refreshAuth(effectiveAuthType);
}
} catch (err) {
// Log auth errors but don't exit - let the appropriate handler deal with auth errors
if (config.getDebugMode()) {
console.error('Auth initialization error:', err);
}
}
}

export function validateDnsResolutionOrder(
order: string | undefined,
): DnsResolutionOrder {
Expand Down Expand Up @@ -191,6 +228,12 @@ export async function main() {

await config.initialize();

// Initialize auth after config.initialize() to ensure correct backend is used
// In sandbox mode, auth is handled separately below to avoid OAuth redirect issues
if (!process.env.SANDBOX) {
await initializeAuth(config, settings);
}

if (config.getIdeMode() && config.getIdeModeFeature()) {
await config.getIdeClient().connect();
logIdeConnection(config, new IdeConnectionEvent(IdeConnectionType.START));
Expand All @@ -214,21 +257,12 @@ export async function main() {
: [];
const sandboxConfig = config.getSandbox();
if (sandboxConfig) {
if (
settings.merged.selectedAuthType &&
!settings.merged.useExternalAuth
) {
// Validate authentication here because the sandbox will interfere with the Oauth2 web redirect.
try {
const err = validateAuthMethod(settings.merged.selectedAuthType);
if (err) {
throw new Error(err);
}
await config.refreshAuth(settings.merged.selectedAuthType);
} catch (err) {
console.error('Error authenticating:', err);
process.exit(1);
}
// Initialize auth before entering sandbox to avoid OAuth redirect issues
try {
await initializeAuth(config, settings);
} catch (err) {
console.error('Error authenticating:', err);
process.exit(1);
}
await start_sandbox(sandboxConfig, memoryArgs, config);
process.exit(0);
Expand Down
4 changes: 4 additions & 0 deletions packages/cli/src/nonInteractiveCli.ts
Original file line number Diff line number Diff line change
Expand Up @@ -40,6 +40,10 @@ export async function runNonInteractive(
});

const geminiClient = config.getGeminiClient();
if (config.getDebugMode()) {
const contentGen = config.getContentGeneratorConfig();
console.debug(`NonInteractive using content generator: ${JSON.stringify(contentGen)}`);
}
const toolRegistry: ToolRegistry = await config.getToolRegistry();

const abortController = new AbortController();
Expand Down
45 changes: 25 additions & 20 deletions packages/cli/src/validateNonInterActiveAuth.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,32 +7,25 @@
import { AuthType, Config } from '@qwen-code/qwen-code-core';
import { USER_SETTINGS_PATH } from './config/settings.js';
import { validateAuthMethod } from './config/auth.js';

function getAuthTypeFromEnv(): AuthType | undefined {
if (process.env.GOOGLE_GENAI_USE_GCA === 'true') {
return AuthType.LOGIN_WITH_GOOGLE;
}
if (process.env.GOOGLE_GENAI_USE_VERTEXAI === 'true') {
return AuthType.USE_VERTEX_AI;
}
if (process.env.GEMINI_API_KEY) {
return AuthType.USE_GEMINI;
}
if (process.env.OPENAI_API_KEY) {
return AuthType.USE_OPENAI;
}
if (process.env.QWEN_OAUTH_TOKEN) {
return AuthType.QWEN_OAUTH;
}
return undefined;
}
import { getEffectiveAuthType } from './config/config.js';

export async function validateNonInteractiveAuth(
configuredAuthType: AuthType | undefined,
useExternalAuth: boolean | undefined,
nonInteractiveConfig: Config,
) {
const effectiveAuthType = configuredAuthType || getAuthTypeFromEnv();
const debug = nonInteractiveConfig.getDebugMode() || process.env.DEBUG === 'true' || process.env.DEBUG === '1';

// Use the configuration hierarchy from config module
// This ensures CLI args > env vars > settings.json precedence
const effectiveAuthType = getEffectiveAuthType({ selectedAuthType: configuredAuthType });

if (debug) {
console.debug('[DEBUG:validateNonInteractiveAuth] Called\n' +
` configuredAuthType: ${configuredAuthType}\n` +
` useExternalAuth: ${useExternalAuth}\n` +
` effectiveAuthType: ${effectiveAuthType}`);
}

if (!effectiveAuthType) {
console.error(
Expand All @@ -49,6 +42,18 @@ export async function validateNonInteractiveAuth(
}
}

if (debug) {
console.debug('[DEBUG:validateNonInteractiveAuth] Before refreshAuth:', effectiveAuthType);
}

await nonInteractiveConfig.refreshAuth(effectiveAuthType);

if (debug) {
const contentGen = nonInteractiveConfig.getContentGeneratorConfig();
console.debug('[DEBUG:validateNonInteractiveAuth] Auth refreshed\n' +
` effectiveAuthType: ${effectiveAuthType}\n` +
` contentGenerator: ${JSON.stringify(contentGen)}`);
}

return nonInteractiveConfig;
}
56 changes: 56 additions & 0 deletions packages/core/src/core/contentGenerator.ts
Original file line number Diff line number Diff line change
Expand Up @@ -50,6 +50,36 @@ export enum AuthType {
QWEN_OAUTH = 'qwen-oauth',
}

/**
* Get a human-readable backend name based on the auth type.
* @param authType - The authentication type being used
* @returns A human-readable backend name for error messages
*/
export function getBackendName(authType?: AuthType | string): string {
switch (authType) {
case AuthType.USE_OPENAI:
case 'openai':
return 'OpenAI API';
case AuthType.QWEN_OAUTH:
case 'qwen-oauth':
return 'Qwen API';
case AuthType.USE_VERTEX_AI:
case 'vertex-ai':
return 'Vertex AI';
case AuthType.USE_GEMINI:
case 'gemini-api-key':
return 'Gemini API';
case AuthType.CLOUD_SHELL:
case 'cloud-shell':
return 'Cloud Shell';
case AuthType.LOGIN_WITH_GOOGLE:
case 'oauth-personal':
return 'Google OAuth';
default:
return 'AI API';
}
}

export type ContentGeneratorConfig = {
model: string;
apiKey?: string;
Expand All @@ -76,6 +106,15 @@ export function createContentGeneratorConfig(
config: Config,
authType: AuthType | undefined,
): ContentGeneratorConfig {
const debug = config.getDebugMode() || process.env.DEBUG === 'true' || process.env.DEBUG === '1';
if (debug) {
console.debug(
'[DEBUG:createContentGeneratorConfig] Called\n' +
` authType: ${authType}\n` +
` Caller stack: ${new Error().stack?.split('\n').slice(2, 4).join('\n')}`
);
}

const geminiApiKey = process.env.GEMINI_API_KEY || undefined;
const googleApiKey = process.env.GOOGLE_API_KEY || undefined;
const googleCloudProject = process.env.GOOGLE_CLOUD_PROJECT || undefined;
Expand All @@ -84,6 +123,15 @@ export function createContentGeneratorConfig(

// Use runtime model from config if available; otherwise, fall back to parameter or default
const effectiveModel = config.getModel() || DEFAULT_GEMINI_MODEL;

if (debug) {
console.debug(
'[DEBUG:createContentGeneratorConfig] Environment:\n' +
` OPENAI_API_KEY: ${openaiApiKey ? 'SET' : 'NOT SET'}\n` +
` GEMINI_API_KEY: ${geminiApiKey ? 'SET' : 'NOT SET'}\n` +
` Effective model: ${effectiveModel}`
);
}

const contentGeneratorConfig: ContentGeneratorConfig = {
model: effectiveModel,
Expand Down Expand Up @@ -129,6 +177,14 @@ export function createContentGeneratorConfig(
contentGeneratorConfig.apiKey = openaiApiKey;
contentGeneratorConfig.model =
process.env.OPENAI_MODEL || DEFAULT_GEMINI_MODEL;

if (debug) {
console.debug(
'[DEBUG:createContentGeneratorConfig] Configured for OpenAI\n' +
` Final model: ${contentGeneratorConfig.model}\n` +
` Returning config with authType: ${contentGeneratorConfig.authType}`
);
}

return contentGeneratorConfig;
}
Expand Down
Loading