diff --git a/esbuild.config.js b/esbuild.config.js
index 7d38c2a7..9f24d0ba 100644
--- a/esbuild.config.js
+++ b/esbuild.config.js
@@ -7,7 +7,7 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';
-import { writeFileSync } from 'node:fs';
+import { writeFileSync, rmSync } from 'node:fs';
let esbuild;
try {
@@ -22,6 +22,9 @@ const __dirname = path.dirname(__filename);
const require = createRequire(import.meta.url);
const pkg = require(path.resolve(__dirname, 'package.json'));
+// Clean dist directory (cross-platform)
+rmSync(path.resolve(__dirname, 'dist'), { recursive: true, force: true });
+
const external = [
'@lydell/node-pty',
'node-pty',
diff --git a/package.json b/package.json
index dffb3d1d..3e4d1e02 100644
--- a/package.json
+++ b/package.json
@@ -28,7 +28,7 @@
"build:all": "npm run build && npm run build:sandbox && npm run build:vscode",
"build:packages": "npm run build --workspaces",
"build:sandbox": "node scripts/build_sandbox.js",
- "bundle": "rm -rf dist && npm run generate && node esbuild.config.js && node scripts/copy_bundle_assets.js",
+ "bundle": "npm run generate && node esbuild.config.js && node scripts/copy_bundle_assets.js",
"test": "npm run test --workspaces --if-present --parallel",
"test:ci": "npm run test:ci --workspaces --if-present --parallel && npm run test:scripts",
"test:scripts": "vitest run --config ./scripts/tests/vitest.config.ts",
diff --git a/packages/cli/src/config/auth.test.ts b/packages/cli/src/config/auth.test.ts
index b69c5fb0..e28184ac 100644
--- a/packages/cli/src/config/auth.test.ts
+++ b/packages/cli/src/config/auth.test.ts
@@ -18,60 +18,26 @@ vi.mock('./settings.js', () => ({
describe('validateAuthMethod', () => {
beforeEach(() => {
vi.resetModules();
- vi.stubEnv('GEMINI_API_KEY', undefined);
- vi.stubEnv('GOOGLE_CLOUD_PROJECT', undefined);
- vi.stubEnv('GOOGLE_CLOUD_LOCATION', undefined);
- vi.stubEnv('GOOGLE_API_KEY', undefined);
});
afterEach(() => {
vi.unstubAllEnvs();
});
- it('should return null for LOGIN_WITH_GOOGLE', () => {
- expect(validateAuthMethod(AuthType.LOGIN_WITH_GOOGLE)).toBeNull();
+ it('should return null for USE_OPENAI', () => {
+ process.env['OPENAI_API_KEY'] = 'fake-key';
+ expect(validateAuthMethod(AuthType.USE_OPENAI)).toBeNull();
});
- it('should return null for CLOUD_SHELL', () => {
- expect(validateAuthMethod(AuthType.CLOUD_SHELL)).toBeNull();
+ it('should return an error message for USE_OPENAI if OPENAI_API_KEY is not set', () => {
+ delete process.env['OPENAI_API_KEY'];
+ expect(validateAuthMethod(AuthType.USE_OPENAI)).toBe(
+ 'OPENAI_API_KEY environment variable not found. You can enter it interactively or add it to your .env file.',
+ );
});
- describe('USE_GEMINI', () => {
- it('should return null if GEMINI_API_KEY is set', () => {
- vi.stubEnv('GEMINI_API_KEY', 'test-key');
- expect(validateAuthMethod(AuthType.USE_GEMINI)).toBeNull();
- });
-
- it('should return an error message if GEMINI_API_KEY is not set', () => {
- vi.stubEnv('GEMINI_API_KEY', undefined);
- expect(validateAuthMethod(AuthType.USE_GEMINI)).toBe(
- 'GEMINI_API_KEY environment variable not found. Add that to your environment and try again (no reload needed if using .env)!',
- );
- });
- });
-
- describe('USE_VERTEX_AI', () => {
- it('should return null if GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION are set', () => {
- vi.stubEnv('GOOGLE_CLOUD_PROJECT', 'test-project');
- vi.stubEnv('GOOGLE_CLOUD_LOCATION', 'test-location');
- expect(validateAuthMethod(AuthType.USE_VERTEX_AI)).toBeNull();
- });
-
- it('should return null if GOOGLE_API_KEY is set', () => {
- vi.stubEnv('GOOGLE_API_KEY', 'test-api-key');
- expect(validateAuthMethod(AuthType.USE_VERTEX_AI)).toBeNull();
- });
-
- it('should return an error message if no required environment variables are set', () => {
- vi.stubEnv('GOOGLE_CLOUD_PROJECT', undefined);
- vi.stubEnv('GOOGLE_CLOUD_LOCATION', undefined);
- expect(validateAuthMethod(AuthType.USE_VERTEX_AI)).toBe(
- 'When using Vertex AI, you must specify either:\n' +
- '• GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION environment variables.\n' +
- '• GOOGLE_API_KEY environment variable (if using express mode).\n' +
- 'Update your environment and try again (no reload needed if using .env)!',
- );
- });
+ it('should return null for QWEN_OAUTH', () => {
+ expect(validateAuthMethod(AuthType.QWEN_OAUTH)).toBeNull();
});
it('should return an error message for an invalid auth method', () => {
diff --git a/packages/cli/src/config/auth.ts b/packages/cli/src/config/auth.ts
index dfc0d50b..83761e3e 100644
--- a/packages/cli/src/config/auth.ts
+++ b/packages/cli/src/config/auth.ts
@@ -8,39 +8,13 @@ import { AuthType } from '@qwen-code/qwen-code-core';
import { loadEnvironment, loadSettings } from './settings.js';
export function validateAuthMethod(authMethod: string): string | null {
- loadEnvironment(loadSettings().merged);
- if (
- authMethod === AuthType.LOGIN_WITH_GOOGLE ||
- authMethod === AuthType.CLOUD_SHELL
- ) {
- return null;
- }
-
- if (authMethod === AuthType.USE_GEMINI) {
- if (!process.env['GEMINI_API_KEY']) {
- return 'GEMINI_API_KEY environment variable not found. Add that to your environment and try again (no reload needed if using .env)!';
- }
- return null;
- }
-
- if (authMethod === AuthType.USE_VERTEX_AI) {
- const hasVertexProjectLocationConfig =
- !!process.env['GOOGLE_CLOUD_PROJECT'] &&
- !!process.env['GOOGLE_CLOUD_LOCATION'];
- const hasGoogleApiKey = !!process.env['GOOGLE_API_KEY'];
- if (!hasVertexProjectLocationConfig && !hasGoogleApiKey) {
- return (
- 'When using Vertex AI, you must specify either:\n' +
- '• GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION environment variables.\n' +
- '• GOOGLE_API_KEY environment variable (if using express mode).\n' +
- 'Update your environment and try again (no reload needed if using .env)!'
- );
- }
- return null;
- }
+ const settings = loadSettings();
+ loadEnvironment(settings.merged);
if (authMethod === AuthType.USE_OPENAI) {
- if (!process.env['OPENAI_API_KEY']) {
+ const hasApiKey =
+ process.env['OPENAI_API_KEY'] || settings.merged.security?.auth?.apiKey;
+ if (!hasApiKey) {
return 'OPENAI_API_KEY environment variable not found. You can enter it interactively or add it to your .env file.';
}
return null;
@@ -54,15 +28,3 @@ export function validateAuthMethod(authMethod: string): string | null {
return 'Invalid auth method selected.';
}
-
-export const setOpenAIApiKey = (apiKey: string): void => {
- process.env['OPENAI_API_KEY'] = apiKey;
-};
-
-export const setOpenAIBaseUrl = (baseUrl: string): void => {
- process.env['OPENAI_BASE_URL'] = baseUrl;
-};
-
-export const setOpenAIModel = (model: string): void => {
- process.env['OPENAI_MODEL'] = model;
-};
diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts
index 7296ff43..f7752df6 100755
--- a/packages/cli/src/config/config.ts
+++ b/packages/cli/src/config/config.ts
@@ -13,7 +13,6 @@ import { extensionsCommand } from '../commands/extensions.js';
import {
ApprovalMode,
Config,
- DEFAULT_QWEN_MODEL,
DEFAULT_QWEN_EMBEDDING_MODEL,
DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
EditTool,
@@ -669,13 +668,11 @@ export async function loadCliConfig(
);
}
- const defaultModel = DEFAULT_QWEN_MODEL;
- const resolvedModel: string =
+ const resolvedModel =
argv.model ||
process.env['OPENAI_MODEL'] ||
process.env['QWEN_MODEL'] ||
- settings.model?.name ||
- defaultModel;
+ settings.model?.name;
const sandboxConfig = await loadSandboxConfig(settings, argv);
const screenReader =
@@ -739,8 +736,14 @@ export async function loadCliConfig(
generationConfig: {
...(settings.model?.generationConfig || {}),
model: resolvedModel,
- apiKey: argv.openaiApiKey || process.env['OPENAI_API_KEY'],
- baseUrl: argv.openaiBaseUrl || process.env['OPENAI_BASE_URL'],
+ apiKey:
+ argv.openaiApiKey ||
+ process.env['OPENAI_API_KEY'] ||
+ settings.security?.auth?.apiKey,
+ baseUrl:
+ argv.openaiBaseUrl ||
+ process.env['OPENAI_BASE_URL'] ||
+ settings.security?.auth?.baseUrl,
enableOpenAILogging:
(typeof argv.openaiLogging === 'undefined'
? settings.model?.enableOpenAILogging
diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts
index 34ebe4b0..0fd492fa 100644
--- a/packages/cli/src/config/settingsSchema.ts
+++ b/packages/cli/src/config/settingsSchema.ts
@@ -991,6 +991,24 @@ const SETTINGS_SCHEMA = {
description: 'Whether to use an external authentication flow.',
showInDialog: false,
},
+ apiKey: {
+ type: 'string',
+ label: 'API Key',
+ category: 'Security',
+ requiresRestart: true,
+ default: undefined as string | undefined,
+ description: 'API key for OpenAI compatible authentication.',
+ showInDialog: false,
+ },
+ baseUrl: {
+ type: 'string',
+ label: 'Base URL',
+ category: 'Security',
+ requiresRestart: true,
+ default: undefined as string | undefined,
+ description: 'Base URL for OpenAI compatible API.',
+ showInDialog: false,
+ },
},
},
},
diff --git a/packages/cli/src/gemini.tsx b/packages/cli/src/gemini.tsx
index d5ffd023..46c1052f 100644
--- a/packages/cli/src/gemini.tsx
+++ b/packages/cli/src/gemini.tsx
@@ -17,11 +17,7 @@ import dns from 'node:dns';
import { randomUUID } from 'node:crypto';
import { start_sandbox } from './utils/sandbox.js';
import type { DnsResolutionOrder, LoadedSettings } from './config/settings.js';
-import {
- loadSettings,
- migrateDeprecatedSettings,
- SettingScope,
-} from './config/settings.js';
+import { loadSettings, migrateDeprecatedSettings } from './config/settings.js';
import { themeManager } from './ui/themes/theme-manager.js';
import { getStartupWarnings } from './utils/startupWarnings.js';
import { getUserStartupWarnings } from './utils/userStartupWarnings.js';
@@ -233,17 +229,6 @@ export async function main() {
validateDnsResolutionOrder(settings.merged.advanced?.dnsResolutionOrder),
);
- // Set a default auth type if one isn't set.
- if (!settings.merged.security?.auth?.selectedType) {
- if (process.env['CLOUD_SHELL'] === 'true') {
- settings.setValue(
- SettingScope.User,
- 'selectedAuthType',
- AuthType.CLOUD_SHELL,
- );
- }
- }
-
// Load custom themes from settings
themeManager.loadCustomThemes(settings.merged.ui?.customThemes);
diff --git a/packages/cli/src/ui/auth/AuthDialog.tsx b/packages/cli/src/ui/auth/AuthDialog.tsx
index f5b9f4be..4104a775 100644
--- a/packages/cli/src/ui/auth/AuthDialog.tsx
+++ b/packages/cli/src/ui/auth/AuthDialog.tsx
@@ -8,12 +8,7 @@ import type React from 'react';
import { useState } from 'react';
import { AuthType } from '@qwen-code/qwen-code-core';
import { Box, Text } from 'ink';
-import {
- setOpenAIApiKey,
- setOpenAIBaseUrl,
- setOpenAIModel,
- validateAuthMethod,
-} from '../../config/auth.js';
+import { validateAuthMethod } from '../../config/auth.js';
import { type LoadedSettings, SettingScope } from '../../config/settings.js';
import { Colors } from '../colors.js';
import { useKeypress } from '../hooks/useKeypress.js';
@@ -21,7 +16,15 @@ import { OpenAIKeyPrompt } from '../components/OpenAIKeyPrompt.js';
import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js';
interface AuthDialogProps {
- onSelect: (authMethod: AuthType | undefined, scope: SettingScope) => void;
+ onSelect: (
+ authMethod: AuthType | undefined,
+ scope: SettingScope,
+ credentials?: {
+ apiKey?: string;
+ baseUrl?: string;
+ model?: string;
+ },
+ ) => void;
settings: LoadedSettings;
initialErrorMessage?: string | null;
}
@@ -70,11 +73,7 @@ export function AuthDialog({
return item.value === defaultAuthType;
}
- if (process.env['GEMINI_API_KEY']) {
- return item.value === AuthType.USE_GEMINI;
- }
-
- return item.value === AuthType.LOGIN_WITH_GOOGLE;
+ return item.value === AuthType.QWEN_OAUTH;
}),
);
@@ -101,11 +100,12 @@ export function AuthDialog({
baseUrl: string,
model: string,
) => {
- setOpenAIApiKey(apiKey);
- setOpenAIBaseUrl(baseUrl);
- setOpenAIModel(model);
setShowOpenAIKeyPrompt(false);
- onSelect(AuthType.USE_OPENAI, SettingScope.User);
+ onSelect(AuthType.USE_OPENAI, SettingScope.User, {
+ apiKey,
+ baseUrl,
+ model,
+ });
};
const handleOpenAIKeyCancel = () => {
diff --git a/packages/cli/src/ui/auth/useAuth.ts b/packages/cli/src/ui/auth/useAuth.ts
index 5cbffff8..e761043d 100644
--- a/packages/cli/src/ui/auth/useAuth.ts
+++ b/packages/cli/src/ui/auth/useAuth.ts
@@ -6,12 +6,11 @@
import { useState, useCallback, useEffect } from 'react';
import type { LoadedSettings, SettingScope } from '../../config/settings.js';
-import { AuthType, type Config } from '@qwen-code/qwen-code-core';
+import type { AuthType, Config } from '@qwen-code/qwen-code-core';
import {
clearCachedCredentialFile,
getErrorMessage,
} from '@qwen-code/qwen-code-core';
-import { runExitCleanup } from '../../utils/cleanup.js';
import { AuthState } from '../types.js';
import { validateAuthMethod } from '../../config/auth.js';
@@ -47,6 +46,7 @@ export const useAuthCommand = (settings: LoadedSettings, config: Config) => {
setAuthError(error);
if (error) {
setAuthState(AuthState.Updating);
+ setIsAuthDialogOpen(true);
}
},
[setAuthError, setAuthState],
@@ -87,24 +87,49 @@ export const useAuthCommand = (settings: LoadedSettings, config: Config) => {
// Handle auth selection from dialog
const handleAuthSelect = useCallback(
- async (authType: AuthType | undefined, scope: SettingScope) => {
+ async (
+ authType: AuthType | undefined,
+ scope: SettingScope,
+ credentials?: {
+ apiKey?: string;
+ baseUrl?: string;
+ model?: string;
+ },
+ ) => {
if (authType) {
await clearCachedCredentialFile();
- settings.setValue(scope, 'security.auth.selectedType', authType);
+ // Save OpenAI credentials if provided
+ if (credentials) {
+ // Update Config's internal generationConfig before calling refreshAuth
+ // This ensures refreshAuth has access to the new credentials
+ config.updateCredentials({
+ apiKey: credentials.apiKey,
+ baseUrl: credentials.baseUrl,
+ model: credentials.model,
+ });
- if (
- authType === AuthType.LOGIN_WITH_GOOGLE &&
- config.isBrowserLaunchSuppressed()
- ) {
- await runExitCleanup();
- console.log(`
-----------------------------------------------------------------
-Logging in with Google... Please restart Gemini CLI to continue.
-----------------------------------------------------------------
- `);
- process.exit(0);
+ // Also set environment variables for compatibility with other parts of the code
+ if (credentials.apiKey) {
+ settings.setValue(
+ scope,
+ 'security.auth.apiKey',
+ credentials.apiKey,
+ );
+ }
+ if (credentials.baseUrl) {
+ settings.setValue(
+ scope,
+ 'security.auth.baseUrl',
+ credentials.baseUrl,
+ );
+ }
+ if (credentials.model) {
+ settings.setValue(scope, 'model.name', credentials.model);
+ }
}
+
+ settings.setValue(scope, 'security.auth.selectedType', authType);
}
setIsAuthDialogOpen(false);
diff --git a/packages/cli/src/ui/commands/bugCommand.test.ts b/packages/cli/src/ui/commands/bugCommand.test.ts
index 2a579b67..1cc5da45 100644
--- a/packages/cli/src/ui/commands/bugCommand.test.ts
+++ b/packages/cli/src/ui/commands/bugCommand.test.ts
@@ -11,6 +11,7 @@ import { createMockCommandContext } from '../../test-utils/mockCommandContext.js
import { getCliVersion } from '../../utils/version.js';
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
import { formatMemoryUsage } from '../utils/formatters.js';
+import { AuthType } from '@qwen-code/qwen-code-core';
// Mock dependencies
vi.mock('open');
@@ -59,6 +60,15 @@ describe('bugCommand', () => {
getBugCommand: () => undefined,
getIdeMode: () => true,
},
+ settings: {
+ merged: {
+ security: {
+ auth: {
+ selectedType: undefined,
+ },
+ },
+ },
+ },
},
});
@@ -71,6 +81,7 @@ describe('bugCommand', () => {
* **Session ID:** test-session-id
* **Operating System:** test-platform v20.0.0
* **Sandbox Environment:** test
+* **Auth Type:**
* **Model Version:** qwen3-coder-plus
* **Memory Usage:** 100 MB
* **IDE Client:** VSCode
@@ -92,6 +103,15 @@ describe('bugCommand', () => {
getBugCommand: () => ({ urlTemplate: customTemplate }),
getIdeMode: () => true,
},
+ settings: {
+ merged: {
+ security: {
+ auth: {
+ selectedType: undefined,
+ },
+ },
+ },
+ },
},
});
@@ -104,6 +124,7 @@ describe('bugCommand', () => {
* **Session ID:** test-session-id
* **Operating System:** test-platform v20.0.0
* **Sandbox Environment:** test
+* **Auth Type:**
* **Model Version:** qwen3-coder-plus
* **Memory Usage:** 100 MB
* **IDE Client:** VSCode
@@ -114,4 +135,49 @@ describe('bugCommand', () => {
expect(open).toHaveBeenCalledWith(expectedUrl);
});
+
+ it('should include Base URL when auth type is OpenAI', async () => {
+ const mockContext = createMockCommandContext({
+ services: {
+ config: {
+ getModel: () => 'qwen3-coder-plus',
+ getBugCommand: () => undefined,
+ getIdeMode: () => true,
+ getContentGeneratorConfig: () => ({
+ baseUrl: 'https://api.openai.com/v1',
+ }),
+ },
+ settings: {
+ merged: {
+ security: {
+ auth: {
+ selectedType: AuthType.USE_OPENAI,
+ },
+ },
+ },
+ },
+ },
+ });
+
+ if (!bugCommand.action) throw new Error('Action is not defined');
+ await bugCommand.action(mockContext, 'OpenAI bug');
+
+ const expectedInfo = `
+* **CLI Version:** 0.1.0
+* **Git Commit:** ${GIT_COMMIT_INFO}
+* **Session ID:** test-session-id
+* **Operating System:** test-platform v20.0.0
+* **Sandbox Environment:** test
+* **Auth Type:** ${AuthType.USE_OPENAI}
+* **Base URL:** https://api.openai.com/v1
+* **Model Version:** qwen3-coder-plus
+* **Memory Usage:** 100 MB
+* **IDE Client:** VSCode
+`;
+ const expectedUrl =
+ 'https://github.com/QwenLM/qwen-code/issues/new?template=bug_report.yml&title=OpenAI%20bug&info=' +
+ encodeURIComponent(expectedInfo);
+
+ expect(open).toHaveBeenCalledWith(expectedUrl);
+ });
});
diff --git a/packages/cli/src/ui/commands/bugCommand.ts b/packages/cli/src/ui/commands/bugCommand.ts
index 79c56744..4c16f4fb 100644
--- a/packages/cli/src/ui/commands/bugCommand.ts
+++ b/packages/cli/src/ui/commands/bugCommand.ts
@@ -15,7 +15,7 @@ import { MessageType } from '../types.js';
import { GIT_COMMIT_INFO } from '../../generated/git-commit.js';
import { formatMemoryUsage } from '../utils/formatters.js';
import { getCliVersion } from '../../utils/version.js';
-import { IdeClient, sessionId } from '@qwen-code/qwen-code-core';
+import { IdeClient, sessionId, AuthType } from '@qwen-code/qwen-code-core';
export const bugCommand: SlashCommand = {
name: 'bug',
@@ -38,6 +38,12 @@ export const bugCommand: SlashCommand = {
const cliVersion = await getCliVersion();
const memoryUsage = formatMemoryUsage(process.memoryUsage().rss);
const ideClient = await getIdeClientName(context);
+ const selectedAuthType =
+ context.services.settings.merged.security?.auth?.selectedType || '';
+ const baseUrl =
+ selectedAuthType === AuthType.USE_OPENAI
+ ? config?.getContentGeneratorConfig()?.baseUrl
+ : undefined;
let info = `
* **CLI Version:** ${cliVersion}
@@ -45,6 +51,11 @@ export const bugCommand: SlashCommand = {
* **Session ID:** ${sessionId}
* **Operating System:** ${osVersion}
* **Sandbox Environment:** ${sandboxEnv}
+* **Auth Type:** ${selectedAuthType}`;
+ if (baseUrl) {
+ info += `\n* **Base URL:** ${baseUrl}`;
+ }
+ info += `
* **Model Version:** ${modelVersion}
* **Memory Usage:** ${memoryUsage}
`;
diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx
index b649c366..423b6b28 100644
--- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx
+++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.test.tsx
@@ -12,6 +12,7 @@ import type {
Config,
} from '@qwen-code/qwen-code-core';
import { renderWithProviders } from '../../../test-utils/render.js';
+import type { LoadedSettings } from '../../../config/settings.js';
describe('ToolConfirmationMessage', () => {
const mockConfig = {
@@ -187,4 +188,63 @@ describe('ToolConfirmationMessage', () => {
});
});
});
+
+ describe('external editor option', () => {
+ const editConfirmationDetails: ToolCallConfirmationDetails = {
+ type: 'edit',
+ title: 'Confirm Edit',
+ fileName: 'test.txt',
+ filePath: '/test.txt',
+ fileDiff: '...diff...',
+ originalContent: 'a',
+ newContent: 'b',
+ onConfirm: vi.fn(),
+ };
+
+ it('should show "Modify with external editor" when preferredEditor is set', () => {
+ const mockConfig = {
+ isTrustedFolder: () => true,
+ getIdeMode: () => false,
+ } as unknown as Config;
+
+ const { lastFrame } = renderWithProviders(
+ ,
+ {
+ settings: {
+ merged: { general: { preferredEditor: 'vscode' } },
+ } as unknown as LoadedSettings,
+ },
+ );
+
+ expect(lastFrame()).toContain('Modify with external editor');
+ });
+
+ it('should NOT show "Modify with external editor" when preferredEditor is not set', () => {
+ const mockConfig = {
+ isTrustedFolder: () => true,
+ getIdeMode: () => false,
+ } as unknown as Config;
+
+ const { lastFrame } = renderWithProviders(
+ ,
+ {
+ settings: {
+ merged: { general: {} },
+ } as unknown as LoadedSettings,
+ },
+ );
+
+ expect(lastFrame()).not.toContain('Modify with external editor');
+ });
+ });
});
diff --git a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
index 6504dedc..6fea96cb 100644
--- a/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
+++ b/packages/cli/src/ui/components/messages/ToolConfirmationMessage.tsx
@@ -15,12 +15,14 @@ import type {
ToolExecuteConfirmationDetails,
ToolMcpConfirmationDetails,
Config,
+ EditorType,
} from '@qwen-code/qwen-code-core';
import { IdeClient, ToolConfirmationOutcome } from '@qwen-code/qwen-code-core';
import type { RadioSelectItem } from '../shared/RadioButtonSelect.js';
import { RadioButtonSelect } from '../shared/RadioButtonSelect.js';
import { MaxSizedBox } from '../shared/MaxSizedBox.js';
import { useKeypress } from '../../hooks/useKeypress.js';
+import { useSettings } from '../../contexts/SettingsContext.js';
import { theme } from '../../semantic-colors.js';
export interface ToolConfirmationMessageProps {
@@ -45,6 +47,11 @@ export const ToolConfirmationMessage: React.FC<
const { onConfirm } = confirmationDetails;
const childWidth = terminalWidth - 2; // 2 for padding
+ const settings = useSettings();
+ const preferredEditor = settings.merged.general?.preferredEditor as
+ | EditorType
+ | undefined;
+
const [ideClient, setIdeClient] = useState(null);
const [isDiffingEnabled, setIsDiffingEnabled] = useState(false);
@@ -199,7 +206,7 @@ export const ToolConfirmationMessage: React.FC<
key: 'Yes, allow always',
});
}
- if (!config.getIdeMode() || !isDiffingEnabled) {
+ if ((!config.getIdeMode() || !isDiffingEnabled) && preferredEditor) {
options.push({
label: 'Modify with external editor',
value: ToolConfirmationOutcome.ModifyWithEditor,
diff --git a/packages/cli/src/ui/hooks/useEditorSettings.test.ts b/packages/cli/src/ui/hooks/useEditorSettings.test.ts
index 6c4a5d74..fa3cf98b 100644
--- a/packages/cli/src/ui/hooks/useEditorSettings.test.ts
+++ b/packages/cli/src/ui/hooks/useEditorSettings.test.ts
@@ -109,7 +109,7 @@ describe('useEditorSettings', () => {
expect(mockLoadedSettings.setValue).toHaveBeenCalledWith(
scope,
- 'preferredEditor',
+ 'general.preferredEditor',
editorType,
);
@@ -139,7 +139,7 @@ describe('useEditorSettings', () => {
expect(mockLoadedSettings.setValue).toHaveBeenCalledWith(
scope,
- 'preferredEditor',
+ 'general.preferredEditor',
undefined,
);
@@ -170,7 +170,7 @@ describe('useEditorSettings', () => {
expect(mockLoadedSettings.setValue).toHaveBeenCalledWith(
scope,
- 'preferredEditor',
+ 'general.preferredEditor',
editorType,
);
@@ -199,7 +199,7 @@ describe('useEditorSettings', () => {
expect(mockLoadedSettings.setValue).toHaveBeenCalledWith(
scope,
- 'preferredEditor',
+ 'general.preferredEditor',
editorType,
);
diff --git a/packages/cli/src/ui/hooks/useEditorSettings.ts b/packages/cli/src/ui/hooks/useEditorSettings.ts
index 6d7a217c..5d6a5a37 100644
--- a/packages/cli/src/ui/hooks/useEditorSettings.ts
+++ b/packages/cli/src/ui/hooks/useEditorSettings.ts
@@ -45,7 +45,7 @@ export const useEditorSettings = (
}
try {
- loadedSettings.setValue(scope, 'preferredEditor', editorType);
+ loadedSettings.setValue(scope, 'general.preferredEditor', editorType);
addItem(
{
type: MessageType.INFO,
diff --git a/packages/cli/src/validateNonInterActiveAuth.test.ts b/packages/cli/src/validateNonInterActiveAuth.test.ts
index c76296a5..dba93e62 100644
--- a/packages/cli/src/validateNonInterActiveAuth.test.ts
+++ b/packages/cli/src/validateNonInterActiveAuth.test.ts
@@ -105,34 +105,6 @@ describe('validateNonInterActiveAuth', () => {
expect(processExitSpy).toHaveBeenCalledWith(1);
});
- it('uses LOGIN_WITH_GOOGLE if GOOGLE_GENAI_USE_GCA is set', async () => {
- process.env['GOOGLE_GENAI_USE_GCA'] = 'true';
- const nonInteractiveConfig = {
- refreshAuth: refreshAuthMock,
- } as unknown as Config;
- await validateNonInteractiveAuth(
- undefined,
- undefined,
- nonInteractiveConfig,
- mockSettings,
- );
- expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.LOGIN_WITH_GOOGLE);
- });
-
- it('uses USE_GEMINI if GEMINI_API_KEY is set', async () => {
- process.env['GEMINI_API_KEY'] = 'fake-key';
- const nonInteractiveConfig = {
- refreshAuth: refreshAuthMock,
- } as unknown as Config;
- await validateNonInteractiveAuth(
- undefined,
- undefined,
- nonInteractiveConfig,
- mockSettings,
- );
- expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
- });
-
it('uses USE_OPENAI if OPENAI_API_KEY is set', async () => {
process.env['OPENAI_API_KEY'] = 'fake-openai-key';
const nonInteractiveConfig = {
@@ -168,104 +140,6 @@ describe('validateNonInterActiveAuth', () => {
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.QWEN_OAUTH);
});
- it('uses USE_VERTEX_AI if GOOGLE_GENAI_USE_VERTEXAI is true (with GOOGLE_CLOUD_PROJECT and GOOGLE_CLOUD_LOCATION)', async () => {
- process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
- process.env['GOOGLE_CLOUD_PROJECT'] = 'test-project';
- process.env['GOOGLE_CLOUD_LOCATION'] = 'us-central1';
- const nonInteractiveConfig = {
- refreshAuth: refreshAuthMock,
- } as unknown as Config;
- await validateNonInteractiveAuth(
- undefined,
- undefined,
- nonInteractiveConfig,
- mockSettings,
- );
- expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_VERTEX_AI);
- });
-
- it('uses USE_VERTEX_AI if GOOGLE_GENAI_USE_VERTEXAI is true and GOOGLE_API_KEY is set', async () => {
- process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
- process.env['GOOGLE_API_KEY'] = 'vertex-api-key';
- const nonInteractiveConfig = {
- refreshAuth: refreshAuthMock,
- } as unknown as Config;
- await validateNonInteractiveAuth(
- undefined,
- undefined,
- nonInteractiveConfig,
- mockSettings,
- );
- expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_VERTEX_AI);
- });
-
- it('uses LOGIN_WITH_GOOGLE if GOOGLE_GENAI_USE_GCA is set, even with other env vars', async () => {
- process.env['GOOGLE_GENAI_USE_GCA'] = 'true';
- process.env['GEMINI_API_KEY'] = 'fake-key';
- process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
- process.env['GOOGLE_CLOUD_PROJECT'] = 'test-project';
- process.env['GOOGLE_CLOUD_LOCATION'] = 'us-central1';
- const nonInteractiveConfig = {
- refreshAuth: refreshAuthMock,
- } as unknown as Config;
- await validateNonInteractiveAuth(
- undefined,
- undefined,
- nonInteractiveConfig,
- mockSettings,
- );
- expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.LOGIN_WITH_GOOGLE);
- });
-
- it('uses USE_VERTEX_AI if both GEMINI_API_KEY and GOOGLE_GENAI_USE_VERTEXAI are set', async () => {
- process.env['GEMINI_API_KEY'] = 'fake-key';
- process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
- process.env['GOOGLE_CLOUD_PROJECT'] = 'test-project';
- process.env['GOOGLE_CLOUD_LOCATION'] = 'us-central1';
- const nonInteractiveConfig = {
- refreshAuth: refreshAuthMock,
- } as unknown as Config;
- await validateNonInteractiveAuth(
- undefined,
- undefined,
- nonInteractiveConfig,
- mockSettings,
- );
- expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_VERTEX_AI);
- });
-
- it('uses USE_GEMINI if GOOGLE_GENAI_USE_VERTEXAI is false, GEMINI_API_KEY is set, and project/location are available', async () => {
- process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'false';
- process.env['GEMINI_API_KEY'] = 'fake-key';
- process.env['GOOGLE_CLOUD_PROJECT'] = 'test-project';
- process.env['GOOGLE_CLOUD_LOCATION'] = 'us-central1';
- const nonInteractiveConfig = {
- refreshAuth: refreshAuthMock,
- } as unknown as Config;
- await validateNonInteractiveAuth(
- undefined,
- undefined,
- nonInteractiveConfig,
- mockSettings,
- );
- expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
- });
-
- it('uses configuredAuthType if provided', async () => {
- // Set required env var for USE_GEMINI
- process.env['GEMINI_API_KEY'] = 'fake-key';
- const nonInteractiveConfig = {
- refreshAuth: refreshAuthMock,
- } as unknown as Config;
- await validateNonInteractiveAuth(
- AuthType.USE_GEMINI,
- undefined,
- nonInteractiveConfig,
- mockSettings,
- );
- expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
- });
-
it('exits if validateAuthMethod returns error', async () => {
// Mock validateAuthMethod to return error
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
@@ -317,26 +191,25 @@ describe('validateNonInterActiveAuth', () => {
});
it('uses enforcedAuthType if provided', async () => {
- mockSettings.merged.security!.auth!.enforcedType = AuthType.USE_GEMINI;
- mockSettings.merged.security!.auth!.selectedType = AuthType.USE_GEMINI;
- // Set required env var for USE_GEMINI to ensure enforcedAuthType takes precedence
- process.env['GEMINI_API_KEY'] = 'fake-key';
+ mockSettings.merged.security!.auth!.enforcedType = AuthType.USE_OPENAI;
+ mockSettings.merged.security!.auth!.selectedType = AuthType.USE_OPENAI;
+ // Set required env var for USE_OPENAI to ensure enforcedAuthType takes precedence
+ process.env['OPENAI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
} as unknown as Config;
await validateNonInteractiveAuth(
- AuthType.USE_GEMINI,
+ AuthType.USE_OPENAI,
undefined,
nonInteractiveConfig,
mockSettings,
);
- expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
+ expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_OPENAI);
});
it('exits if currentAuthType does not match enforcedAuthType', async () => {
- mockSettings.merged.security!.auth!.enforcedType =
- AuthType.LOGIN_WITH_GOOGLE;
- process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
+ mockSettings.merged.security!.auth!.enforcedType = AuthType.QWEN_OAUTH;
+ process.env['OPENAI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
@@ -346,7 +219,7 @@ describe('validateNonInterActiveAuth', () => {
} as unknown as Config;
try {
await validateNonInteractiveAuth(
- AuthType.USE_GEMINI,
+ AuthType.USE_OPENAI,
undefined,
nonInteractiveConfig,
mockSettings,
@@ -356,7 +229,7 @@ describe('validateNonInterActiveAuth', () => {
expect((e as Error).message).toContain('process.exit(1) called');
}
expect(consoleErrorSpy).toHaveBeenCalledWith(
- 'The configured auth type is oauth-personal, but the current auth type is vertex-ai. Please re-authenticate with the correct type.',
+ 'The configured auth type is qwen-oauth, but the current auth type is openai. Please re-authenticate with the correct type.',
);
expect(processExitSpy).toHaveBeenCalledWith(1);
});
@@ -394,8 +267,8 @@ describe('validateNonInterActiveAuth', () => {
});
it('prints JSON error when enforced auth mismatches current auth and exits with code 1', async () => {
- mockSettings.merged.security!.auth!.enforcedType = AuthType.USE_GEMINI;
- process.env['GOOGLE_GENAI_USE_GCA'] = 'true';
+ mockSettings.merged.security!.auth!.enforcedType = AuthType.QWEN_OAUTH;
+ process.env['OPENAI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
@@ -424,14 +297,14 @@ describe('validateNonInterActiveAuth', () => {
expect(payload.error.type).toBe('Error');
expect(payload.error.code).toBe(1);
expect(payload.error.message).toContain(
- 'The configured auth type is gemini-api-key, but the current auth type is oauth-personal.',
+ 'The configured auth type is qwen-oauth, but the current auth type is openai.',
);
}
});
it('prints JSON error when validateAuthMethod fails and exits with code 1', async () => {
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
- process.env['GEMINI_API_KEY'] = 'fake-key';
+ process.env['OPENAI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
@@ -444,7 +317,7 @@ describe('validateNonInterActiveAuth', () => {
let thrown: Error | undefined;
try {
await validateNonInteractiveAuth(
- AuthType.USE_GEMINI,
+ AuthType.USE_OPENAI,
undefined,
nonInteractiveConfig,
mockSettings,
diff --git a/packages/cli/src/validateNonInterActiveAuth.ts b/packages/cli/src/validateNonInterActiveAuth.ts
index ab1675b9..e44cd0a4 100644
--- a/packages/cli/src/validateNonInterActiveAuth.ts
+++ b/packages/cli/src/validateNonInterActiveAuth.ts
@@ -12,21 +12,13 @@ import { type LoadedSettings } from './config/settings.js';
import { handleError } from './utils/errors.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']) {
return AuthType.QWEN_OAUTH;
}
+
return undefined;
}
diff --git a/packages/core/src/config/config.test.ts b/packages/core/src/config/config.test.ts
index 2f908f0c..42442632 100644
--- a/packages/core/src/config/config.test.ts
+++ b/packages/core/src/config/config.test.ts
@@ -16,6 +16,7 @@ import {
QwenLogger,
} from '../telemetry/index.js';
import type { ContentGeneratorConfig } from '../core/contentGenerator.js';
+import { DEFAULT_DASHSCOPE_BASE_URL } from '../core/openaiContentGenerator/constants.js';
import {
AuthType,
createContentGeneratorConfig,
@@ -250,6 +251,7 @@ describe('Server Config (config.ts)', () => {
authType,
{
model: MODEL,
+ baseUrl: DEFAULT_DASHSCOPE_BASE_URL,
},
);
// Verify that contentGeneratorConfig is updated
diff --git a/packages/core/src/config/config.ts b/packages/core/src/config/config.ts
index 6ac472f1..44878bad 100644
--- a/packages/core/src/config/config.ts
+++ b/packages/core/src/config/config.ts
@@ -88,8 +88,9 @@ import {
DEFAULT_FILE_FILTERING_OPTIONS,
DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
} from './constants.js';
-import { DEFAULT_QWEN_EMBEDDING_MODEL } from './models.js';
+import { DEFAULT_QWEN_EMBEDDING_MODEL, DEFAULT_QWEN_MODEL } from './models.js';
import { Storage } from './storage.js';
+import { DEFAULT_DASHSCOPE_BASE_URL } from '../core/openaiContentGenerator/constants.js';
// Re-export types
export type { AnyToolInvocation, FileFilteringOptions, MCPOAuthConfig };
@@ -243,7 +244,7 @@ export interface ConfigParameters {
fileDiscoveryService?: FileDiscoveryService;
includeDirectories?: string[];
bugCommand?: BugCommandSettings;
- model: string;
+ model?: string;
extensionContextFilePaths?: string[];
maxSessionTurns?: number;
sessionTokenLimit?: number;
@@ -289,7 +290,7 @@ export class Config {
private fileSystemService: FileSystemService;
private contentGeneratorConfig!: ContentGeneratorConfig;
private contentGenerator!: ContentGenerator;
- private readonly _generationConfig: ContentGeneratorConfig;
+ private _generationConfig: Partial;
private readonly embeddingModel: string;
private readonly sandbox: SandboxConfig | undefined;
private readonly targetDir: string;
@@ -440,8 +441,10 @@ export class Config {
this._generationConfig = {
model: params.model,
...(params.generationConfig || {}),
+ baseUrl: params.generationConfig?.baseUrl || DEFAULT_DASHSCOPE_BASE_URL,
};
- this.contentGeneratorConfig = this._generationConfig;
+ this.contentGeneratorConfig = this
+ ._generationConfig as ContentGeneratorConfig;
this.cliVersion = params.cliVersion;
this.loadMemoryFromIncludeDirectories =
@@ -520,6 +523,26 @@ export class Config {
return this.contentGenerator;
}
+ /**
+ * Updates the credentials in the generation config.
+ * This is needed when credentials are set after Config construction.
+ */
+ updateCredentials(credentials: {
+ apiKey?: string;
+ baseUrl?: string;
+ model?: string;
+ }): void {
+ if (credentials.apiKey) {
+ this._generationConfig.apiKey = credentials.apiKey;
+ }
+ if (credentials.baseUrl) {
+ this._generationConfig.baseUrl = credentials.baseUrl;
+ }
+ if (credentials.model) {
+ this._generationConfig.model = credentials.model;
+ }
+ }
+
async refreshAuth(authMethod: AuthType) {
// Vertex and Genai have incompatible encryption and sending history with
// throughtSignature from Genai to Vertex will fail, we need to strip them
@@ -587,7 +610,7 @@ export class Config {
}
getModel(): string {
- return this.contentGeneratorConfig.model;
+ return this.contentGeneratorConfig?.model || DEFAULT_QWEN_MODEL;
}
async setModel(
diff --git a/packages/core/src/core/contentGenerator.test.ts b/packages/core/src/core/contentGenerator.test.ts
index 98df2d79..729481c0 100644
--- a/packages/core/src/core/contentGenerator.test.ts
+++ b/packages/core/src/core/contentGenerator.test.ts
@@ -4,13 +4,9 @@
* SPDX-License-Identifier: Apache-2.0
*/
-import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
+import { describe, it, expect, vi } from 'vitest';
import type { ContentGenerator } from './contentGenerator.js';
-import {
- createContentGenerator,
- AuthType,
- createContentGeneratorConfig,
-} from './contentGenerator.js';
+import { createContentGenerator, AuthType } from './contentGenerator.js';
import { createCodeAssistContentGenerator } from '../code_assist/codeAssist.js';
import { GoogleGenAI } from '@google/genai';
import type { Config } from '../config/config.js';
@@ -110,83 +106,3 @@ describe('createContentGenerator', () => {
);
});
});
-
-describe('createContentGeneratorConfig', () => {
- const mockConfig = {
- getModel: vi.fn().mockReturnValue('gemini-pro'),
- setModel: vi.fn(),
- flashFallbackHandler: vi.fn(),
- getProxy: vi.fn(),
- getEnableOpenAILogging: vi.fn().mockReturnValue(false),
- getSamplingParams: vi.fn().mockReturnValue(undefined),
- getContentGeneratorTimeout: vi.fn().mockReturnValue(undefined),
- getContentGeneratorMaxRetries: vi.fn().mockReturnValue(undefined),
- getContentGeneratorDisableCacheControl: vi.fn().mockReturnValue(undefined),
- getContentGeneratorSamplingParams: vi.fn().mockReturnValue(undefined),
- getCliVersion: vi.fn().mockReturnValue('1.0.0'),
- } as unknown as Config;
-
- beforeEach(() => {
- // Reset modules to re-evaluate imports and environment variables
- vi.resetModules();
- vi.clearAllMocks();
- });
-
- afterEach(() => {
- vi.unstubAllEnvs();
- });
-
- it('should configure for Gemini using GEMINI_API_KEY when set', async () => {
- vi.stubEnv('GEMINI_API_KEY', 'env-gemini-key');
- const config = await createContentGeneratorConfig(
- mockConfig,
- AuthType.USE_GEMINI,
- );
- expect(config.apiKey).toBe('env-gemini-key');
- expect(config.vertexai).toBe(false);
- });
-
- it('should not configure for Gemini if GEMINI_API_KEY is empty', async () => {
- vi.stubEnv('GEMINI_API_KEY', '');
- const config = await createContentGeneratorConfig(
- mockConfig,
- AuthType.USE_GEMINI,
- );
- expect(config.apiKey).toBeUndefined();
- expect(config.vertexai).toBeUndefined();
- });
-
- it('should configure for Vertex AI using GOOGLE_API_KEY when set', async () => {
- vi.stubEnv('GOOGLE_API_KEY', 'env-google-key');
- const config = await createContentGeneratorConfig(
- mockConfig,
- AuthType.USE_VERTEX_AI,
- );
- expect(config.apiKey).toBe('env-google-key');
- expect(config.vertexai).toBe(true);
- });
-
- it('should configure for Vertex AI using GCP project and location when set', async () => {
- vi.stubEnv('GOOGLE_API_KEY', undefined);
- vi.stubEnv('GOOGLE_CLOUD_PROJECT', 'env-gcp-project');
- vi.stubEnv('GOOGLE_CLOUD_LOCATION', 'env-gcp-location');
- const config = await createContentGeneratorConfig(
- mockConfig,
- AuthType.USE_VERTEX_AI,
- );
- expect(config.vertexai).toBe(true);
- expect(config.apiKey).toBeUndefined();
- });
-
- it('should not configure for Vertex AI if required env vars are empty', async () => {
- vi.stubEnv('GOOGLE_API_KEY', '');
- vi.stubEnv('GOOGLE_CLOUD_PROJECT', '');
- vi.stubEnv('GOOGLE_CLOUD_LOCATION', '');
- const config = await createContentGeneratorConfig(
- mockConfig,
- AuthType.USE_VERTEX_AI,
- );
- expect(config.apiKey).toBeUndefined();
- expect(config.vertexai).toBeUndefined();
- });
-});
diff --git a/packages/core/src/core/contentGenerator.ts b/packages/core/src/core/contentGenerator.ts
index 2d3c1949..3258cd5c 100644
--- a/packages/core/src/core/contentGenerator.ts
+++ b/packages/core/src/core/contentGenerator.ts
@@ -14,8 +14,8 @@ import type {
} from '@google/genai';
import { GoogleGenAI } from '@google/genai';
import { createCodeAssistContentGenerator } from '../code_assist/codeAssist.js';
-import type { Config } from '../config/config.js';
import { DEFAULT_QWEN_MODEL } from '../config/models.js';
+import type { Config } from '../config/config.js';
import type { UserTierId } from '../code_assist/types.js';
import { InstallationManager } from '../utils/installationManager.js';
@@ -82,53 +82,37 @@ export function createContentGeneratorConfig(
authType: AuthType | undefined,
generationConfig?: Partial,
): ContentGeneratorConfig {
- const geminiApiKey = process.env['GEMINI_API_KEY'] || undefined;
- const googleApiKey = process.env['GOOGLE_API_KEY'] || undefined;
- const googleCloudProject = process.env['GOOGLE_CLOUD_PROJECT'] || undefined;
- const googleCloudLocation = process.env['GOOGLE_CLOUD_LOCATION'] || undefined;
-
- const newContentGeneratorConfig: ContentGeneratorConfig = {
+ const newContentGeneratorConfig: Partial = {
...(generationConfig || {}),
- model: generationConfig?.model || DEFAULT_QWEN_MODEL,
authType,
proxy: config?.getProxy(),
};
- // If we are using Google auth or we are in Cloud Shell, there is nothing else to validate for now
- if (
- authType === AuthType.LOGIN_WITH_GOOGLE ||
- authType === AuthType.CLOUD_SHELL
- ) {
- return newContentGeneratorConfig;
- }
-
- if (authType === AuthType.USE_GEMINI && geminiApiKey) {
- newContentGeneratorConfig.apiKey = geminiApiKey;
- newContentGeneratorConfig.vertexai = false;
-
- return newContentGeneratorConfig;
- }
-
- if (
- authType === AuthType.USE_VERTEX_AI &&
- (googleApiKey || (googleCloudProject && googleCloudLocation))
- ) {
- newContentGeneratorConfig.apiKey = googleApiKey;
- newContentGeneratorConfig.vertexai = true;
-
- return newContentGeneratorConfig;
- }
-
if (authType === AuthType.QWEN_OAUTH) {
// For Qwen OAuth, we'll handle the API key dynamically in createContentGenerator
// Set a special marker to indicate this is Qwen OAuth
- newContentGeneratorConfig.apiKey = 'QWEN_OAUTH_DYNAMIC_TOKEN';
- newContentGeneratorConfig.model = DEFAULT_QWEN_MODEL;
-
- return newContentGeneratorConfig;
+ return {
+ ...newContentGeneratorConfig,
+ model: DEFAULT_QWEN_MODEL,
+ apiKey: 'QWEN_OAUTH_DYNAMIC_TOKEN',
+ } as ContentGeneratorConfig;
}
- return newContentGeneratorConfig;
+ if (authType === AuthType.USE_OPENAI) {
+ if (!newContentGeneratorConfig.apiKey) {
+ throw new Error('OpenAI API key is required');
+ }
+
+ return {
+ ...newContentGeneratorConfig,
+ model: newContentGeneratorConfig?.model || 'qwen3-coder-plus',
+ } as ContentGeneratorConfig;
+ }
+
+ return {
+ ...newContentGeneratorConfig,
+ model: newContentGeneratorConfig?.model || DEFAULT_QWEN_MODEL,
+ } as ContentGeneratorConfig;
}
export async function createContentGenerator(
diff --git a/packages/core/src/core/openaiContentGenerator/constants.ts b/packages/core/src/core/openaiContentGenerator/constants.ts
index d2b5ce81..c213d643 100644
--- a/packages/core/src/core/openaiContentGenerator/constants.ts
+++ b/packages/core/src/core/openaiContentGenerator/constants.ts
@@ -1,2 +1,8 @@
export const DEFAULT_TIMEOUT = 120000;
export const DEFAULT_MAX_RETRIES = 3;
+
+export const DEFAULT_OPENAI_BASE_URL = 'https://api.openai.com/v1';
+export const DEFAULT_DASHSCOPE_BASE_URL =
+ 'https://dashscope.aliyuncs.com/compatible-mode/v1';
+export const DEFAULT_DEEPSEEK_BASE_URL = 'https://api.deepseek.com/v1';
+export const DEFAULT_OPEN_ROUTER_BASE_URL = 'https://openrouter.ai/api/v1';
diff --git a/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts b/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts
index 9130238f..2df72221 100644
--- a/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts
+++ b/packages/core/src/core/openaiContentGenerator/provider/dashscope.ts
@@ -2,7 +2,11 @@ import OpenAI from 'openai';
import type { Config } from '../../../config/config.js';
import type { ContentGeneratorConfig } from '../../contentGenerator.js';
import { AuthType } from '../../contentGenerator.js';
-import { DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES } from '../constants.js';
+import {
+ DEFAULT_TIMEOUT,
+ DEFAULT_MAX_RETRIES,
+ DEFAULT_DASHSCOPE_BASE_URL,
+} from '../constants.js';
import { tokenLimit } from '../../tokenLimits.js';
import type {
OpenAICompatibleProvider,
@@ -53,7 +57,7 @@ export class DashScopeOpenAICompatibleProvider
buildClient(): OpenAI {
const {
apiKey,
- baseUrl,
+ baseUrl = DEFAULT_DASHSCOPE_BASE_URL,
timeout = DEFAULT_TIMEOUT,
maxRetries = DEFAULT_MAX_RETRIES,
} = this.contentGeneratorConfig;
diff --git a/packages/core/src/qwen/qwenContentGenerator.ts b/packages/core/src/qwen/qwenContentGenerator.ts
index 0e3ca12e..0b0e249f 100644
--- a/packages/core/src/qwen/qwenContentGenerator.ts
+++ b/packages/core/src/qwen/qwenContentGenerator.ts
@@ -8,7 +8,7 @@ import { OpenAIContentGenerator } from '../core/openaiContentGenerator/index.js'
import { DashScopeOpenAICompatibleProvider } from '../core/openaiContentGenerator/provider/dashscope.js';
import type { IQwenOAuth2Client } from './qwenOAuth2.js';
import { SharedTokenManager } from './sharedTokenManager.js';
-import type { Config } from '../config/config.js';
+import { type Config } from '../config/config.js';
import type {
GenerateContentParameters,
GenerateContentResponse,
@@ -18,10 +18,7 @@ import type {
EmbedContentResponse,
} from '@google/genai';
import type { ContentGeneratorConfig } from '../core/contentGenerator.js';
-
-// Default fallback base URL if no endpoint is provided
-const DEFAULT_QWEN_BASE_URL =
- 'https://dashscope.aliyuncs.com/compatible-mode/v1';
+import { DEFAULT_DASHSCOPE_BASE_URL } from '../core/openaiContentGenerator/constants.js';
/**
* Qwen Content Generator that uses Qwen OAuth tokens with automatic refresh
@@ -58,7 +55,7 @@ export class QwenContentGenerator extends OpenAIContentGenerator {
* Get the current endpoint URL with proper protocol and /v1 suffix
*/
private getCurrentEndpoint(resourceUrl?: string): string {
- const baseEndpoint = resourceUrl || DEFAULT_QWEN_BASE_URL;
+ const baseEndpoint = resourceUrl || DEFAULT_DASHSCOPE_BASE_URL;
const suffix = '/v1';
// Normalize the URL: add protocol if missing, ensure /v1 suffix
diff --git a/packages/core/src/utils/editor.test.ts b/packages/core/src/utils/editor.test.ts
index db721240..acc9e1a1 100644
--- a/packages/core/src/utils/editor.test.ts
+++ b/packages/core/src/utils/editor.test.ts
@@ -339,6 +339,7 @@ describe('editor utils', () => {
diffCommand.args,
{
stdio: 'inherit',
+ shell: process.platform === 'win32',
},
);
expect(mockSpawnOn).toHaveBeenCalledWith('close', expect.any(Function));
diff --git a/packages/core/src/utils/editor.ts b/packages/core/src/utils/editor.ts
index 8b507926..1023abe4 100644
--- a/packages/core/src/utils/editor.ts
+++ b/packages/core/src/utils/editor.ts
@@ -195,6 +195,7 @@ export async function openDiff(
return new Promise((resolve, reject) => {
const childProcess = spawn(diffCommand.command, diffCommand.args, {
stdio: 'inherit',
+ shell: process.platform === 'win32',
});
childProcess.on('close', (code) => {