Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot]
9cbd8bb4e5 chore(release): v0.1.1-preview.0 2025-10-29 00:14:43 +00:00
35 changed files with 466 additions and 369 deletions

View File

@@ -107,7 +107,7 @@ The `qwen-extension.json` file contains the configuration for the extension. The
- `mcpServers`: A map of MCP servers to configure. The key is the name of the server, and the value is the server configuration. These servers will be loaded on startup just like MCP servers configured in a [`settings.json` file](./cli/configuration.md). If both an extension and a `settings.json` file configure an MCP server with the same name, the server defined in the `settings.json` file takes precedence.
- Note that all MCP server configuration options are supported except for `trust`.
- `contextFileName`: The name of the file that contains the context for the extension. This will be used to load the context from the extension directory. If this property is not used but a `QWEN.md` file is present in your extension directory, then that file will be loaded.
- `excludeTools`: An array of tool names to exclude from the model. You can also specify command-specific restrictions for tools that support it, like the `run_shell_command` tool. For example, `"excludeTools": ["run_shell_command(rm -rf)"]` will block the `rm -rf` command. Note that this differs from the MCP server `excludeTools` functionality, which can be listed in the MCP server config. **Important:** Tools specified in `excludeTools` will be disabled for the entire conversation context and will affect all subsequent queries in the current session.
- `excludeTools`: An array of tool names to exclude from the model. You can also specify command-specific restrictions for tools that support it, like the `run_shell_command` tool. For example, `"excludeTools": ["run_shell_command(rm -rf)"]` will block the `rm -rf` command. Note that this differs from the MCP server `excludeTools` functionality, which can be listed in the MCP server config.
When Qwen Code starts, it loads all the extensions and merges their configurations. If there are any conflicts, the workspace configuration takes precedence.

View File

@@ -7,7 +7,7 @@
import path from 'node:path';
import { fileURLToPath } from 'node:url';
import { createRequire } from 'node:module';
import { writeFileSync, rmSync } from 'node:fs';
import { writeFileSync } from 'node:fs';
let esbuild;
try {
@@ -22,9 +22,6 @@ 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',

View File

@@ -36,10 +36,10 @@ describe('JSON output', () => {
});
it('should return a JSON error for enforced auth mismatch before running', async () => {
process.env['OPENAI_API_KEY'] = 'test-key';
process.env['GOOGLE_GENAI_USE_GCA'] = 'true';
await rig.setup('json-output-auth-mismatch', {
settings: {
security: { auth: { enforcedType: 'qwen-oauth' } },
security: { auth: { enforcedType: 'gemini-api-key' } },
},
});
@@ -50,7 +50,7 @@ describe('JSON output', () => {
} catch (e) {
thrown = e as Error;
} finally {
delete process.env['OPENAI_API_KEY'];
delete process.env['GOOGLE_GENAI_USE_GCA'];
}
expect(thrown).toBeDefined();
@@ -80,8 +80,10 @@ describe('JSON output', () => {
expect(payload.error.type).toBe('Error');
expect(payload.error.code).toBe(1);
expect(payload.error.message).toContain(
'configured auth type is qwen-oauth',
'configured auth type is gemini-api-key',
);
expect(payload.error.message).toContain(
'current auth type is oauth-personal',
);
expect(payload.error.message).toContain('current auth type is openai');
});
});

View File

@@ -9,6 +9,7 @@ import { mkdirSync, writeFileSync, readFileSync } from 'node:fs';
import { join, dirname } from 'node:path';
import { fileURLToPath } from 'node:url';
import { env } from 'node:process';
import { DEFAULT_QWEN_MODEL } from '../packages/core/src/config/models.js';
import fs from 'node:fs';
import { EOL } from 'node:os';
import * as pty from '@lydell/node-pty';
@@ -181,6 +182,7 @@ export class TestRig {
otlpEndpoint: '',
outfile: telemetryPath,
},
model: DEFAULT_QWEN_MODEL,
sandbox: env.GEMINI_SANDBOX !== 'false' ? env.GEMINI_SANDBOX : false,
...options.settings, // Allow tests to override/add settings
};

12
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.1.2",
"version": "0.1.1-preview.0",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@qwen-code/qwen-code",
"version": "0.1.2",
"version": "0.1.1-preview.0",
"workspaces": [
"packages/*"
],
@@ -16024,7 +16024,7 @@
},
"packages/cli": {
"name": "@qwen-code/qwen-code",
"version": "0.1.2",
"version": "0.1.1-preview.0",
"dependencies": {
"@google/genai": "1.16.0",
"@iarna/toml": "^2.2.5",
@@ -16139,7 +16139,7 @@
},
"packages/core": {
"name": "@qwen-code/qwen-code-core",
"version": "0.1.2",
"version": "0.1.1-preview.0",
"hasInstallScript": true,
"dependencies": {
"@google/genai": "1.16.0",
@@ -16278,7 +16278,7 @@
},
"packages/test-utils": {
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.1.2",
"version": "0.1.1-preview.0",
"dev": true,
"license": "Apache-2.0",
"devDependencies": {
@@ -16290,7 +16290,7 @@
},
"packages/vscode-ide-companion": {
"name": "qwen-code-vscode-ide-companion",
"version": "0.1.2",
"version": "0.1.1-preview.0",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.1.2",
"version": "0.1.1-preview.0",
"engines": {
"node": ">=20.0.0"
},
@@ -13,7 +13,7 @@
"url": "git+https://github.com/QwenLM/qwen-code.git"
},
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.1.2"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.1.1-preview.0"
},
"scripts": {
"start": "cross-env node scripts/start.js",
@@ -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": "npm run generate && node esbuild.config.js && node scripts/copy_bundle_assets.js",
"bundle": "rm -rf dist && 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",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.1.2",
"version": "0.1.1-preview.0",
"description": "Qwen Code",
"repository": {
"type": "git",
@@ -25,7 +25,7 @@
"dist"
],
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.1.2"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.1.1-preview.0"
},
"dependencies": {
"@google/genai": "1.16.0",

View File

@@ -18,26 +18,60 @@ 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 USE_OPENAI', () => {
process.env['OPENAI_API_KEY'] = 'fake-key';
expect(validateAuthMethod(AuthType.USE_OPENAI)).toBeNull();
it('should return null for LOGIN_WITH_GOOGLE', () => {
expect(validateAuthMethod(AuthType.LOGIN_WITH_GOOGLE)).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.',
);
it('should return null for CLOUD_SHELL', () => {
expect(validateAuthMethod(AuthType.CLOUD_SHELL)).toBeNull();
});
it('should return null for QWEN_OAUTH', () => {
expect(validateAuthMethod(AuthType.QWEN_OAUTH)).toBeNull();
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 an error message for an invalid auth method', () => {

View File

@@ -8,13 +8,39 @@ import { AuthType } from '@qwen-code/qwen-code-core';
import { loadEnvironment, loadSettings } from './settings.js';
export function validateAuthMethod(authMethod: string): string | null {
const settings = loadSettings();
loadEnvironment(settings.merged);
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;
}
if (authMethod === AuthType.USE_OPENAI) {
const hasApiKey =
process.env['OPENAI_API_KEY'] || settings.merged.security?.auth?.apiKey;
if (!hasApiKey) {
if (!process.env['OPENAI_API_KEY']) {
return 'OPENAI_API_KEY environment variable not found. You can enter it interactively or add it to your .env file.';
}
return null;
@@ -28,3 +54,15 @@ 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;
};

View File

@@ -13,6 +13,7 @@ import { extensionsCommand } from '../commands/extensions.js';
import {
ApprovalMode,
Config,
DEFAULT_QWEN_MODEL,
DEFAULT_QWEN_EMBEDDING_MODEL,
DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
EditTool,
@@ -194,7 +195,7 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
.option('proxy', {
type: 'string',
description:
'Proxy for Qwen Code, like schema://user:password@host:port',
'Proxy for gemini client, like schema://user:password@host:port',
})
.deprecateOption(
'proxy',
@@ -668,11 +669,13 @@ export async function loadCliConfig(
);
}
const resolvedModel =
const defaultModel = DEFAULT_QWEN_MODEL;
const resolvedModel: string =
argv.model ||
process.env['OPENAI_MODEL'] ||
process.env['QWEN_MODEL'] ||
settings.model?.name;
settings.model?.name ||
defaultModel;
const sandboxConfig = await loadSandboxConfig(settings, argv);
const screenReader =
@@ -736,14 +739,8 @@ export async function loadCliConfig(
generationConfig: {
...(settings.model?.generationConfig || {}),
model: resolvedModel,
apiKey:
argv.openaiApiKey ||
process.env['OPENAI_API_KEY'] ||
settings.security?.auth?.apiKey,
baseUrl:
argv.openaiBaseUrl ||
process.env['OPENAI_BASE_URL'] ||
settings.security?.auth?.baseUrl,
apiKey: argv.openaiApiKey || process.env['OPENAI_API_KEY'],
baseUrl: argv.openaiBaseUrl || process.env['OPENAI_BASE_URL'],
enableOpenAILogging:
(typeof argv.openaiLogging === 'undefined'
? settings.model?.enableOpenAILogging

View File

@@ -991,24 +991,6 @@ 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,
},
},
},
},

View File

@@ -17,7 +17,11 @@ 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 } from './config/settings.js';
import {
loadSettings,
migrateDeprecatedSettings,
SettingScope,
} from './config/settings.js';
import { themeManager } from './ui/themes/theme-manager.js';
import { getStartupWarnings } from './utils/startupWarnings.js';
import { getUserStartupWarnings } from './utils/userStartupWarnings.js';
@@ -229,6 +233,17 @@ 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);

View File

@@ -8,7 +8,12 @@ import type React from 'react';
import { useState } from 'react';
import { AuthType } from '@qwen-code/qwen-code-core';
import { Box, Text } from 'ink';
import { validateAuthMethod } from '../../config/auth.js';
import {
setOpenAIApiKey,
setOpenAIBaseUrl,
setOpenAIModel,
validateAuthMethod,
} from '../../config/auth.js';
import { type LoadedSettings, SettingScope } from '../../config/settings.js';
import { Colors } from '../colors.js';
import { useKeypress } from '../hooks/useKeypress.js';
@@ -16,15 +21,7 @@ import { OpenAIKeyPrompt } from '../components/OpenAIKeyPrompt.js';
import { RadioButtonSelect } from '../components/shared/RadioButtonSelect.js';
interface AuthDialogProps {
onSelect: (
authMethod: AuthType | undefined,
scope: SettingScope,
credentials?: {
apiKey?: string;
baseUrl?: string;
model?: string;
},
) => void;
onSelect: (authMethod: AuthType | undefined, scope: SettingScope) => void;
settings: LoadedSettings;
initialErrorMessage?: string | null;
}
@@ -73,7 +70,11 @@ export function AuthDialog({
return item.value === defaultAuthType;
}
return item.value === AuthType.QWEN_OAUTH;
if (process.env['GEMINI_API_KEY']) {
return item.value === AuthType.USE_GEMINI;
}
return item.value === AuthType.LOGIN_WITH_GOOGLE;
}),
);
@@ -100,12 +101,11 @@ export function AuthDialog({
baseUrl: string,
model: string,
) => {
setOpenAIApiKey(apiKey);
setOpenAIBaseUrl(baseUrl);
setOpenAIModel(model);
setShowOpenAIKeyPrompt(false);
onSelect(AuthType.USE_OPENAI, SettingScope.User, {
apiKey,
baseUrl,
model,
});
onSelect(AuthType.USE_OPENAI, SettingScope.User);
};
const handleOpenAIKeyCancel = () => {

View File

@@ -6,11 +6,12 @@
import { useState, useCallback, useEffect } from 'react';
import type { LoadedSettings, SettingScope } from '../../config/settings.js';
import type { AuthType, Config } from '@qwen-code/qwen-code-core';
import { AuthType, type 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';
@@ -46,7 +47,6 @@ export const useAuthCommand = (settings: LoadedSettings, config: Config) => {
setAuthError(error);
if (error) {
setAuthState(AuthState.Updating);
setIsAuthDialogOpen(true);
}
},
[setAuthError, setAuthState],
@@ -87,49 +87,24 @@ export const useAuthCommand = (settings: LoadedSettings, config: Config) => {
// Handle auth selection from dialog
const handleAuthSelect = useCallback(
async (
authType: AuthType | undefined,
scope: SettingScope,
credentials?: {
apiKey?: string;
baseUrl?: string;
model?: string;
},
) => {
async (authType: AuthType | undefined, scope: SettingScope) => {
if (authType) {
await clearCachedCredentialFile();
// 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,
});
// 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);
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);
}
}
setIsAuthDialogOpen(false);

View File

@@ -11,7 +11,6 @@ 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');
@@ -27,6 +26,7 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
getDetectedIdeDisplayName: vi.fn().mockReturnValue('VSCode'),
}),
},
sessionId: 'test-session-id',
};
});
vi.mock('node:process', () => ({
@@ -58,16 +58,6 @@ describe('bugCommand', () => {
getModel: () => 'qwen3-coder-plus',
getBugCommand: () => undefined,
getIdeMode: () => true,
getSessionId: () => 'test-session-id',
},
settings: {
merged: {
security: {
auth: {
selectedType: undefined,
},
},
},
},
},
});
@@ -81,7 +71,6 @@ 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
@@ -102,16 +91,6 @@ describe('bugCommand', () => {
getModel: () => 'qwen3-coder-plus',
getBugCommand: () => ({ urlTemplate: customTemplate }),
getIdeMode: () => true,
getSessionId: () => 'test-session-id',
},
settings: {
merged: {
security: {
auth: {
selectedType: undefined,
},
},
},
},
},
});
@@ -125,7 +104,6 @@ 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
@@ -136,50 +114,4 @@ 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,
getSessionId: () => 'test-session-id',
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);
});
});

View File

@@ -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, AuthType } from '@qwen-code/qwen-code-core';
import { IdeClient, sessionId } from '@qwen-code/qwen-code-core';
export const bugCommand: SlashCommand = {
name: 'bug',
@@ -38,24 +38,13 @@ 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}
* **Git Commit:** ${GIT_COMMIT_INFO}
* **Session ID:** ${config?.getSessionId() || 'unknown'}
* **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}
`;

View File

@@ -130,7 +130,7 @@ export function OpenAIKeyPrompt({
}
// Handle regular character input
if (key.sequence && !key.ctrl && !key.meta) {
if (key.sequence && !key.ctrl && !key.meta && !key.name) {
// Filter control characters
const cleanInput = key.sequence
.split('')

View File

@@ -12,7 +12,6 @@ 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 = {
@@ -188,63 +187,4 @@ 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(
<ToolConfirmationMessage
confirmationDetails={editConfirmationDetails}
config={mockConfig}
availableTerminalHeight={30}
terminalWidth={80}
/>,
{
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(
<ToolConfirmationMessage
confirmationDetails={editConfirmationDetails}
config={mockConfig}
availableTerminalHeight={30}
terminalWidth={80}
/>,
{
settings: {
merged: { general: {} },
} as unknown as LoadedSettings,
},
);
expect(lastFrame()).not.toContain('Modify with external editor');
});
});
});

View File

@@ -15,14 +15,12 @@ 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 {
@@ -47,11 +45,6 @@ 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<IdeClient | null>(null);
const [isDiffingEnabled, setIsDiffingEnabled] = useState(false);
@@ -206,7 +199,7 @@ export const ToolConfirmationMessage: React.FC<
key: 'Yes, allow always',
});
}
if ((!config.getIdeMode() || !isDiffingEnabled) && preferredEditor) {
if (!config.getIdeMode() || !isDiffingEnabled) {
options.push({
label: 'Modify with external editor',
value: ToolConfirmationOutcome.ModifyWithEditor,

View File

@@ -109,7 +109,7 @@ describe('useEditorSettings', () => {
expect(mockLoadedSettings.setValue).toHaveBeenCalledWith(
scope,
'general.preferredEditor',
'preferredEditor',
editorType,
);
@@ -139,7 +139,7 @@ describe('useEditorSettings', () => {
expect(mockLoadedSettings.setValue).toHaveBeenCalledWith(
scope,
'general.preferredEditor',
'preferredEditor',
undefined,
);
@@ -170,7 +170,7 @@ describe('useEditorSettings', () => {
expect(mockLoadedSettings.setValue).toHaveBeenCalledWith(
scope,
'general.preferredEditor',
'preferredEditor',
editorType,
);
@@ -199,7 +199,7 @@ describe('useEditorSettings', () => {
expect(mockLoadedSettings.setValue).toHaveBeenCalledWith(
scope,
'general.preferredEditor',
'preferredEditor',
editorType,
);

View File

@@ -45,7 +45,7 @@ export const useEditorSettings = (
}
try {
loadedSettings.setValue(scope, 'general.preferredEditor', editorType);
loadedSettings.setValue(scope, 'preferredEditor', editorType);
addItem(
{
type: MessageType.INFO,

View File

@@ -105,6 +105,34 @@ 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 = {
@@ -140,6 +168,104 @@ 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!');
@@ -191,25 +317,26 @@ describe('validateNonInterActiveAuth', () => {
});
it('uses enforcedAuthType if provided', async () => {
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';
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';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
} as unknown as Config;
await validateNonInteractiveAuth(
AuthType.USE_OPENAI,
AuthType.USE_GEMINI,
undefined,
nonInteractiveConfig,
mockSettings,
);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_OPENAI);
expect(refreshAuthMock).toHaveBeenCalledWith(AuthType.USE_GEMINI);
});
it('exits if currentAuthType does not match enforcedAuthType', async () => {
mockSettings.merged.security!.auth!.enforcedType = AuthType.QWEN_OAUTH;
process.env['OPENAI_API_KEY'] = 'fake-key';
mockSettings.merged.security!.auth!.enforcedType =
AuthType.LOGIN_WITH_GOOGLE;
process.env['GOOGLE_GENAI_USE_VERTEXAI'] = 'true';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
getOutputFormat: vi.fn().mockReturnValue(OutputFormat.TEXT),
@@ -219,7 +346,7 @@ describe('validateNonInterActiveAuth', () => {
} as unknown as Config;
try {
await validateNonInteractiveAuth(
AuthType.USE_OPENAI,
AuthType.USE_GEMINI,
undefined,
nonInteractiveConfig,
mockSettings,
@@ -229,7 +356,7 @@ describe('validateNonInterActiveAuth', () => {
expect((e as Error).message).toContain('process.exit(1) called');
}
expect(consoleErrorSpy).toHaveBeenCalledWith(
'The configured auth type is qwen-oauth, but the current auth type is openai. Please re-authenticate with the correct type.',
'The configured auth type is oauth-personal, but the current auth type is vertex-ai. Please re-authenticate with the correct type.',
);
expect(processExitSpy).toHaveBeenCalledWith(1);
});
@@ -267,8 +394,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.QWEN_OAUTH;
process.env['OPENAI_API_KEY'] = 'fake-key';
mockSettings.merged.security!.auth!.enforcedType = AuthType.USE_GEMINI;
process.env['GOOGLE_GENAI_USE_GCA'] = 'true';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
@@ -297,14 +424,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 qwen-oauth, but the current auth type is openai.',
'The configured auth type is gemini-api-key, but the current auth type is oauth-personal.',
);
}
});
it('prints JSON error when validateAuthMethod fails and exits with code 1', async () => {
vi.spyOn(auth, 'validateAuthMethod').mockReturnValue('Auth error!');
process.env['OPENAI_API_KEY'] = 'fake-key';
process.env['GEMINI_API_KEY'] = 'fake-key';
const nonInteractiveConfig = {
refreshAuth: refreshAuthMock,
@@ -317,7 +444,7 @@ describe('validateNonInterActiveAuth', () => {
let thrown: Error | undefined;
try {
await validateNonInteractiveAuth(
AuthType.USE_OPENAI,
AuthType.USE_GEMINI,
undefined,
nonInteractiveConfig,
mockSettings,

View File

@@ -12,13 +12,21 @@ 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;
}

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-core",
"version": "0.1.2",
"version": "0.1.1-preview.0",
"description": "Qwen Code Core",
"repository": {
"type": "git",

View File

@@ -16,7 +16,6 @@ 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,
@@ -251,7 +250,6 @@ describe('Server Config (config.ts)', () => {
authType,
{
model: MODEL,
baseUrl: DEFAULT_DASHSCOPE_BASE_URL,
},
);
// Verify that contentGeneratorConfig is updated

View File

@@ -88,9 +88,8 @@ import {
DEFAULT_FILE_FILTERING_OPTIONS,
DEFAULT_MEMORY_FILE_FILTERING_OPTIONS,
} from './constants.js';
import { DEFAULT_QWEN_EMBEDDING_MODEL, DEFAULT_QWEN_MODEL } from './models.js';
import { DEFAULT_QWEN_EMBEDDING_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 };
@@ -244,7 +243,7 @@ export interface ConfigParameters {
fileDiscoveryService?: FileDiscoveryService;
includeDirectories?: string[];
bugCommand?: BugCommandSettings;
model?: string;
model: string;
extensionContextFilePaths?: string[];
maxSessionTurns?: number;
sessionTokenLimit?: number;
@@ -290,7 +289,7 @@ export class Config {
private fileSystemService: FileSystemService;
private contentGeneratorConfig!: ContentGeneratorConfig;
private contentGenerator!: ContentGenerator;
private _generationConfig: Partial<ContentGeneratorConfig>;
private readonly _generationConfig: ContentGeneratorConfig;
private readonly embeddingModel: string;
private readonly sandbox: SandboxConfig | undefined;
private readonly targetDir: string;
@@ -441,10 +440,8 @@ export class Config {
this._generationConfig = {
model: params.model,
...(params.generationConfig || {}),
baseUrl: params.generationConfig?.baseUrl || DEFAULT_DASHSCOPE_BASE_URL,
};
this.contentGeneratorConfig = this
._generationConfig as ContentGeneratorConfig;
this.contentGeneratorConfig = this._generationConfig;
this.cliVersion = params.cliVersion;
this.loadMemoryFromIncludeDirectories =
@@ -523,26 +520,6 @@ 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
@@ -610,7 +587,7 @@ export class Config {
}
getModel(): string {
return this.contentGeneratorConfig?.model || DEFAULT_QWEN_MODEL;
return this.contentGeneratorConfig.model;
}
async setModel(

View File

@@ -4,9 +4,13 @@
* SPDX-License-Identifier: Apache-2.0
*/
import { describe, it, expect, vi } from 'vitest';
import { describe, it, expect, vi, beforeEach, afterEach } from 'vitest';
import type { ContentGenerator } from './contentGenerator.js';
import { createContentGenerator, AuthType } from './contentGenerator.js';
import {
createContentGenerator,
AuthType,
createContentGeneratorConfig,
} from './contentGenerator.js';
import { createCodeAssistContentGenerator } from '../code_assist/codeAssist.js';
import { GoogleGenAI } from '@google/genai';
import type { Config } from '../config/config.js';
@@ -106,3 +110,83 @@ 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();
});
});

View File

@@ -14,8 +14,8 @@ import type {
} from '@google/genai';
import { GoogleGenAI } from '@google/genai';
import { createCodeAssistContentGenerator } from '../code_assist/codeAssist.js';
import { DEFAULT_QWEN_MODEL } from '../config/models.js';
import type { Config } from '../config/config.js';
import { DEFAULT_QWEN_MODEL } from '../config/models.js';
import type { UserTierId } from '../code_assist/types.js';
import { InstallationManager } from '../utils/installationManager.js';
@@ -82,37 +82,53 @@ export function createContentGeneratorConfig(
authType: AuthType | undefined,
generationConfig?: Partial<ContentGeneratorConfig>,
): ContentGeneratorConfig {
const newContentGeneratorConfig: 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 = {
...(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
return {
...newContentGeneratorConfig,
model: DEFAULT_QWEN_MODEL,
apiKey: 'QWEN_OAUTH_DYNAMIC_TOKEN',
} as ContentGeneratorConfig;
newContentGeneratorConfig.apiKey = 'QWEN_OAUTH_DYNAMIC_TOKEN';
newContentGeneratorConfig.model = DEFAULT_QWEN_MODEL;
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;
return newContentGeneratorConfig;
}
export async function createContentGenerator(

View File

@@ -1,8 +1,2 @@
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';

View File

@@ -2,11 +2,7 @@ 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,
DEFAULT_DASHSCOPE_BASE_URL,
} from '../constants.js';
import { DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES } from '../constants.js';
import { tokenLimit } from '../../tokenLimits.js';
import type {
OpenAICompatibleProvider,
@@ -57,7 +53,7 @@ export class DashScopeOpenAICompatibleProvider
buildClient(): OpenAI {
const {
apiKey,
baseUrl = DEFAULT_DASHSCOPE_BASE_URL,
baseUrl,
timeout = DEFAULT_TIMEOUT,
maxRetries = DEFAULT_MAX_RETRIES,
} = this.contentGeneratorConfig;

View File

@@ -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,7 +18,10 @@ import type {
EmbedContentResponse,
} from '@google/genai';
import type { ContentGeneratorConfig } from '../core/contentGenerator.js';
import { DEFAULT_DASHSCOPE_BASE_URL } from '../core/openaiContentGenerator/constants.js';
// Default fallback base URL if no endpoint is provided
const DEFAULT_QWEN_BASE_URL =
'https://dashscope.aliyuncs.com/compatible-mode/v1';
/**
* Qwen Content Generator that uses Qwen OAuth tokens with automatic refresh
@@ -55,7 +58,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_DASHSCOPE_BASE_URL;
const baseEndpoint = resourceUrl || DEFAULT_QWEN_BASE_URL;
const suffix = '/v1';
// Normalize the URL: add protocol if missing, ensure /v1 suffix

View File

@@ -339,7 +339,6 @@ describe('editor utils', () => {
diffCommand.args,
{
stdio: 'inherit',
shell: process.platform === 'win32',
},
);
expect(mockSpawnOn).toHaveBeenCalledWith('close', expect.any(Function));

View File

@@ -195,7 +195,6 @@ export async function openDiff(
return new Promise<void>((resolve, reject) => {
const childProcess = spawn(diffCommand.command, diffCommand.args, {
stdio: 'inherit',
shell: process.platform === 'win32',
});
childProcess.on('close', (code) => {

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.1.2",
"version": "0.1.1-preview.0",
"private": true,
"main": "src/index.ts",
"license": "Apache-2.0",

View File

@@ -2,7 +2,7 @@
"name": "qwen-code-vscode-ide-companion",
"displayName": "Qwen Code Companion",
"description": "Enable Qwen Code with direct access to your VS Code workspace.",
"version": "0.1.2",
"version": "0.1.1-preview.0",
"publisher": "qwenlm",
"icon": "assets/icon.png",
"repository": {