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) => {