Compare commits

..

1 Commits

Author SHA1 Message Date
github-actions[bot]
42dcc79877 chore(release): v0.8.0-preview.2 2026-01-23 01:56:40 +00:00
14 changed files with 50 additions and 417 deletions

12
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.8.0",
"version": "0.8.0-preview.2",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@qwen-code/qwen-code",
"version": "0.8.0",
"version": "0.8.0-preview.2",
"workspaces": [
"packages/*"
],
@@ -17343,7 +17343,7 @@
},
"packages/cli": {
"name": "@qwen-code/qwen-code",
"version": "0.8.0",
"version": "0.8.0-preview.2",
"dependencies": {
"@google/genai": "1.30.0",
"@iarna/toml": "^2.2.5",
@@ -17977,7 +17977,7 @@
},
"packages/core": {
"name": "@qwen-code/qwen-code-core",
"version": "0.8.0",
"version": "0.8.0-preview.2",
"hasInstallScript": true,
"dependencies": {
"@anthropic-ai/sdk": "^0.36.1",
@@ -21442,7 +21442,7 @@
},
"packages/test-utils": {
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.8.0",
"version": "0.8.0-preview.2",
"dev": true,
"license": "Apache-2.0",
"devDependencies": {
@@ -21454,7 +21454,7 @@
},
"packages/vscode-ide-companion": {
"name": "qwen-code-vscode-ide-companion",
"version": "0.8.0",
"version": "0.8.0-preview.2",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.25.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.8.0",
"version": "0.8.0-preview.2",
"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.8.0"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.8.0-preview.2"
},
"scripts": {
"start": "cross-env node scripts/start.js",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.8.0",
"version": "0.8.0-preview.2",
"description": "Qwen Code",
"repository": {
"type": "git",
@@ -33,7 +33,7 @@
"dist"
],
"config": {
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.8.0"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.8.0-preview.2"
},
"dependencies": {
"@google/genai": "1.30.0",

View File

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

View File

@@ -28,7 +28,6 @@ type RawMessageStreamEvent = Anthropic.RawMessageStreamEvent;
import { RequestTokenEstimator } from '../../utils/request-tokenizer/index.js';
import { safeJsonParse } from '../../utils/safeJsonParse.js';
import { AnthropicContentConverter } from './converter.js';
import { buildRuntimeFetchOptions } from '../../utils/runtimeFetchOptions.js';
type StreamingBlockState = {
type: string;
@@ -55,9 +54,6 @@ export class AnthropicContentGenerator implements ContentGenerator {
) {
const defaultHeaders = this.buildHeaders();
const baseURL = contentGeneratorConfig.baseUrl;
// Configure runtime options to ensure user-configured timeout works as expected
// bodyTimeout is always disabled (0) to let Anthropic SDK timeout control the request
const runtimeOptions = buildRuntimeFetchOptions('anthropic');
this.client = new Anthropic({
apiKey: contentGeneratorConfig.apiKey,
@@ -65,7 +61,6 @@ export class AnthropicContentGenerator implements ContentGenerator {
timeout: contentGeneratorConfig.timeout,
maxRetries: contentGeneratorConfig.maxRetries,
defaultHeaders,
...runtimeOptions,
});
this.converter = new AnthropicContentConverter(

View File

@@ -19,8 +19,6 @@ import type { ContentGeneratorConfig } from '../../contentGenerator.js';
import { AuthType } from '../../contentGenerator.js';
import type { ChatCompletionToolWithCache } from './types.js';
import { DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES } from '../constants.js';
import { buildRuntimeFetchOptions } from '../../../utils/runtimeFetchOptions.js';
import type { OpenAIRuntimeFetchOptions } from '../../../utils/runtimeFetchOptions.js';
// Mock OpenAI
vi.mock('openai', () => ({
@@ -34,10 +32,6 @@ vi.mock('openai', () => ({
})),
}));
vi.mock('../../../utils/runtimeFetchOptions.js', () => ({
buildRuntimeFetchOptions: vi.fn(),
}));
describe('DashScopeOpenAICompatibleProvider', () => {
let provider: DashScopeOpenAICompatibleProvider;
let mockContentGeneratorConfig: ContentGeneratorConfig;
@@ -45,11 +39,6 @@ describe('DashScopeOpenAICompatibleProvider', () => {
beforeEach(() => {
vi.clearAllMocks();
const mockedBuildRuntimeFetchOptions =
buildRuntimeFetchOptions as unknown as MockedFunction<
(sdkType: 'openai') => OpenAIRuntimeFetchOptions
>;
mockedBuildRuntimeFetchOptions.mockReturnValue(undefined);
// Mock ContentGeneratorConfig
mockContentGeneratorConfig = {
@@ -196,20 +185,18 @@ describe('DashScopeOpenAICompatibleProvider', () => {
it('should create OpenAI client with DashScope configuration', () => {
const client = provider.buildClient();
expect(OpenAI).toHaveBeenCalledWith(
expect.objectContaining({
apiKey: 'test-api-key',
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
timeout: 60000,
maxRetries: 2,
defaultHeaders: {
'User-Agent': `QwenCode/1.0.0 (${process.platform}; ${process.arch})`,
'X-DashScope-CacheControl': 'enable',
'X-DashScope-UserAgent': `QwenCode/1.0.0 (${process.platform}; ${process.arch})`,
'X-DashScope-AuthType': AuthType.QWEN_OAUTH,
},
}),
);
expect(OpenAI).toHaveBeenCalledWith({
apiKey: 'test-api-key',
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
timeout: 60000,
maxRetries: 2,
defaultHeaders: {
'User-Agent': `QwenCode/1.0.0 (${process.platform}; ${process.arch})`,
'X-DashScope-CacheControl': 'enable',
'X-DashScope-UserAgent': `QwenCode/1.0.0 (${process.platform}; ${process.arch})`,
'X-DashScope-AuthType': AuthType.QWEN_OAUTH,
},
});
expect(client).toBeDefined();
});
@@ -220,15 +207,13 @@ describe('DashScopeOpenAICompatibleProvider', () => {
provider.buildClient();
expect(OpenAI).toHaveBeenCalledWith(
expect.objectContaining({
apiKey: 'test-api-key',
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
timeout: DEFAULT_TIMEOUT,
maxRetries: DEFAULT_MAX_RETRIES,
defaultHeaders: expect.any(Object),
}),
);
expect(OpenAI).toHaveBeenCalledWith({
apiKey: 'test-api-key',
baseURL: 'https://dashscope.aliyuncs.com/compatible-mode/v1',
timeout: DEFAULT_TIMEOUT,
maxRetries: DEFAULT_MAX_RETRIES,
defaultHeaders: expect.any(Object),
});
});
});

View File

@@ -16,7 +16,6 @@ import type {
ChatCompletionContentPartWithCache,
ChatCompletionToolWithCache,
} from './types.js';
import { buildRuntimeFetchOptions } from '../../../utils/runtimeFetchOptions.js';
export class DashScopeOpenAICompatibleProvider
implements OpenAICompatibleProvider
@@ -69,16 +68,12 @@ export class DashScopeOpenAICompatibleProvider
maxRetries = DEFAULT_MAX_RETRIES,
} = this.contentGeneratorConfig;
const defaultHeaders = this.buildHeaders();
// Configure fetch options to ensure user-configured timeout works as expected
// bodyTimeout is always disabled (0) to let OpenAI SDK timeout control the request
const fetchOptions = buildRuntimeFetchOptions('openai');
return new OpenAI({
apiKey,
baseURL: baseUrl,
timeout,
maxRetries,
defaultHeaders,
...(fetchOptions ? { fetchOptions } : {}),
});
}

View File

@@ -17,8 +17,6 @@ import { DefaultOpenAICompatibleProvider } from './default.js';
import type { Config } from '../../../config/config.js';
import type { ContentGeneratorConfig } from '../../contentGenerator.js';
import { DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES } from '../constants.js';
import { buildRuntimeFetchOptions } from '../../../utils/runtimeFetchOptions.js';
import type { OpenAIRuntimeFetchOptions } from '../../../utils/runtimeFetchOptions.js';
// Mock OpenAI
vi.mock('openai', () => ({
@@ -32,10 +30,6 @@ vi.mock('openai', () => ({
})),
}));
vi.mock('../../../utils/runtimeFetchOptions.js', () => ({
buildRuntimeFetchOptions: vi.fn(),
}));
describe('DefaultOpenAICompatibleProvider', () => {
let provider: DefaultOpenAICompatibleProvider;
let mockContentGeneratorConfig: ContentGeneratorConfig;
@@ -43,11 +37,6 @@ describe('DefaultOpenAICompatibleProvider', () => {
beforeEach(() => {
vi.clearAllMocks();
const mockedBuildRuntimeFetchOptions =
buildRuntimeFetchOptions as unknown as MockedFunction<
(sdkType: 'openai') => OpenAIRuntimeFetchOptions
>;
mockedBuildRuntimeFetchOptions.mockReturnValue(undefined);
// Mock ContentGeneratorConfig
mockContentGeneratorConfig = {
@@ -123,17 +112,15 @@ describe('DefaultOpenAICompatibleProvider', () => {
it('should create OpenAI client with correct configuration', () => {
const client = provider.buildClient();
expect(OpenAI).toHaveBeenCalledWith(
expect.objectContaining({
apiKey: 'test-api-key',
baseURL: 'https://api.openai.com/v1',
timeout: 60000,
maxRetries: 2,
defaultHeaders: {
'User-Agent': `QwenCode/1.0.0 (${process.platform}; ${process.arch})`,
},
}),
);
expect(OpenAI).toHaveBeenCalledWith({
apiKey: 'test-api-key',
baseURL: 'https://api.openai.com/v1',
timeout: 60000,
maxRetries: 2,
defaultHeaders: {
'User-Agent': `QwenCode/1.0.0 (${process.platform}; ${process.arch})`,
},
});
expect(client).toBeDefined();
});
@@ -144,17 +131,15 @@ describe('DefaultOpenAICompatibleProvider', () => {
provider.buildClient();
expect(OpenAI).toHaveBeenCalledWith(
expect.objectContaining({
apiKey: 'test-api-key',
baseURL: 'https://api.openai.com/v1',
timeout: DEFAULT_TIMEOUT,
maxRetries: DEFAULT_MAX_RETRIES,
defaultHeaders: {
'User-Agent': `QwenCode/1.0.0 (${process.platform}; ${process.arch})`,
},
}),
);
expect(OpenAI).toHaveBeenCalledWith({
apiKey: 'test-api-key',
baseURL: 'https://api.openai.com/v1',
timeout: DEFAULT_TIMEOUT,
maxRetries: DEFAULT_MAX_RETRIES,
defaultHeaders: {
'User-Agent': `QwenCode/1.0.0 (${process.platform}; ${process.arch})`,
},
});
});
it('should include custom headers from buildHeaders', () => {

View File

@@ -4,7 +4,6 @@ import type { Config } from '../../../config/config.js';
import type { ContentGeneratorConfig } from '../../contentGenerator.js';
import { DEFAULT_TIMEOUT, DEFAULT_MAX_RETRIES } from '../constants.js';
import type { OpenAICompatibleProvider } from './types.js';
import { buildRuntimeFetchOptions } from '../../../utils/runtimeFetchOptions.js';
/**
* Default provider for standard OpenAI-compatible APIs
@@ -44,16 +43,12 @@ export class DefaultOpenAICompatibleProvider
maxRetries = DEFAULT_MAX_RETRIES,
} = this.contentGeneratorConfig;
const defaultHeaders = this.buildHeaders();
// Configure fetch options to ensure user-configured timeout works as expected
// bodyTimeout is always disabled (0) to let OpenAI SDK timeout control the request
const fetchOptions = buildRuntimeFetchOptions('openai');
return new OpenAI({
apiKey,
baseURL: baseUrl,
timeout,
maxRetries,
defaultHeaders,
...(fetchOptions ? { fetchOptions } : {}),
});
}

View File

@@ -1,167 +0,0 @@
/**
* @license
* Copyright 2025 Qwen Team
* SPDX-License-Identifier: Apache-2.0
*/
import { EnvHttpProxyAgent } from 'undici';
/**
* JavaScript runtime type
*/
export type Runtime = 'node' | 'bun' | 'unknown';
/**
* Detect the current JavaScript runtime
*/
export function detectRuntime(): Runtime {
if (typeof process !== 'undefined' && process.versions?.['bun']) {
return 'bun';
}
if (typeof process !== 'undefined' && process.versions?.node) {
return 'node';
}
return 'unknown';
}
/**
* Runtime fetch options for OpenAI SDK
*/
export type OpenAIRuntimeFetchOptions =
| {
dispatcher?: EnvHttpProxyAgent;
timeout?: false;
}
| undefined;
/**
* Runtime fetch options for Anthropic SDK
*/
export type AnthropicRuntimeFetchOptions = {
// eslint-disable-next-line @typescript-eslint/no-explicit-any
httpAgent?: any;
// eslint-disable-next-line @typescript-eslint/no-explicit-any
fetch?: any;
};
/**
* SDK type identifier
*/
export type SDKType = 'openai' | 'anthropic';
/**
* Build runtime-specific fetch options for OpenAI SDK
*/
export function buildRuntimeFetchOptions(
sdkType: 'openai',
): OpenAIRuntimeFetchOptions;
/**
* Build runtime-specific fetch options for Anthropic SDK
*/
export function buildRuntimeFetchOptions(
sdkType: 'anthropic',
): AnthropicRuntimeFetchOptions;
/**
* Build runtime-specific fetch options based on the detected runtime and SDK type
* This function applies runtime-specific configurations to handle timeout differences
* across Node.js and Bun, ensuring user-configured timeout works as expected.
*
* @param sdkType - The SDK type ('openai' or 'anthropic') to determine return type
* @returns Runtime-specific options compatible with the specified SDK
*/
export function buildRuntimeFetchOptions(
sdkType: SDKType,
): OpenAIRuntimeFetchOptions | AnthropicRuntimeFetchOptions {
const runtime = detectRuntime();
// Always disable bodyTimeout (set to 0) to let SDK's timeout parameter
// control the total request time. bodyTimeout only monitors intervals between
// data chunks, not the total request time, so we disable it to ensure user-configured
// timeout works as expected for both streaming and non-streaming requests.
switch (runtime) {
case 'bun': {
if (sdkType === 'openai') {
// Bun: Disable built-in 300s timeout to let OpenAI SDK timeout control
// This ensures user-configured timeout works as expected without interference
return {
timeout: false,
};
} else {
// Bun: Use custom fetch to disable built-in 300s timeout
// This allows Anthropic SDK timeout to control the request
// Note: Bun's fetch automatically uses proxy settings from environment variables
// (HTTP_PROXY, HTTPS_PROXY, NO_PROXY), so proxy behavior is preserved
const bunFetch: typeof fetch = async (
input: RequestInfo | URL,
init?: RequestInit,
) => {
const bunFetchOptions: RequestInit = {
...init,
// @ts-expect-error - Bun-specific timeout option
timeout: false,
};
return fetch(input, bunFetchOptions);
};
return {
fetch: bunFetch,
};
}
}
case 'node': {
// Node.js: Use EnvHttpProxyAgent to configure proxy and disable bodyTimeout
// EnvHttpProxyAgent automatically reads proxy settings from environment variables
// (HTTP_PROXY, HTTPS_PROXY, NO_PROXY, etc.) to preserve proxy functionality
// bodyTimeout is always 0 (disabled) to let SDK timeout control the request
try {
const agent = new EnvHttpProxyAgent({
bodyTimeout: 0, // Disable to let SDK timeout control total request time
});
if (sdkType === 'openai') {
return {
dispatcher: agent,
};
} else {
return {
httpAgent: agent,
};
}
} catch {
// If undici is not available, return appropriate default
if (sdkType === 'openai') {
return undefined;
} else {
return {};
}
}
}
default: {
// Unknown runtime: Try to use EnvHttpProxyAgent if available
// EnvHttpProxyAgent automatically reads proxy settings from environment variables
try {
const agent = new EnvHttpProxyAgent({
bodyTimeout: 0, // Disable to let SDK timeout control total request time
});
if (sdkType === 'openai') {
return {
dispatcher: agent,
};
} else {
return {
httpAgent: agent,
};
}
} catch {
if (sdkType === 'openai') {
return undefined;
} else {
return {};
}
}
}
}
}

View File

@@ -53,106 +53,4 @@ describe('evaluateShellCommandReadOnly', () => {
const result = isShellCommandReadOnly('FOO=bar ls');
expect(result).toBe(true);
});
describe('awk command security', () => {
it('allows safe awk commands', () => {
expect(isShellCommandReadOnly("awk '{print $1}' file.txt")).toBe(true);
expect(isShellCommandReadOnly('awk \'BEGIN {print "hello"}\'')).toBe(
true,
);
expect(isShellCommandReadOnly("awk '/pattern/ {print}' file.txt")).toBe(
true,
);
});
it('rejects awk with system() calls', () => {
expect(isShellCommandReadOnly('awk \'BEGIN {system("rm -rf /")}\'')).toBe(
false,
);
expect(
isShellCommandReadOnly('awk \'{system("touch file")}\' input.txt'),
).toBe(false);
expect(isShellCommandReadOnly('awk \'BEGIN { system ( "ls" ) }\'')).toBe(
false,
);
});
it('rejects awk with file output redirection', () => {
expect(
isShellCommandReadOnly('awk \'{print > "output.txt"}\' input.txt'),
).toBe(false);
expect(
isShellCommandReadOnly('awk \'{printf "%s\\n", $0 > "file.txt"}\''),
).toBe(false);
expect(
isShellCommandReadOnly('awk \'{print >> "append.txt"}\' input.txt'),
).toBe(false);
expect(
isShellCommandReadOnly('awk \'{printf "%s" >> "file.txt"}\''),
).toBe(false);
});
it('rejects awk with command pipes', () => {
expect(isShellCommandReadOnly('awk \'{print | "sort"}\' input.txt')).toBe(
false,
);
expect(
isShellCommandReadOnly('awk \'{printf "%s\\n", $0 | "wc -l"}\''),
).toBe(false);
});
it('rejects awk with getline from commands', () => {
expect(isShellCommandReadOnly('awk \'BEGIN {getline < "date"}\'')).toBe(
false,
);
expect(isShellCommandReadOnly('awk \'BEGIN {"date" | getline}\'')).toBe(
false,
);
});
it('rejects awk with close() calls', () => {
expect(isShellCommandReadOnly('awk \'BEGIN {close("file")}\'')).toBe(
false,
);
expect(isShellCommandReadOnly("awk '{close(cmd)}' input.txt")).toBe(
false,
);
});
});
describe('sed command security', () => {
it('allows safe sed commands', () => {
expect(isShellCommandReadOnly("sed 's/foo/bar/' file.txt")).toBe(true);
expect(isShellCommandReadOnly("sed -n '1,5p' file.txt")).toBe(true);
expect(isShellCommandReadOnly("sed '/pattern/d' file.txt")).toBe(true);
});
it('rejects sed with execute command', () => {
expect(isShellCommandReadOnly("sed 's/foo/bar/e' file.txt")).toBe(false);
expect(isShellCommandReadOnly("sed 'e date' file.txt")).toBe(false);
});
it('rejects sed with write command', () => {
expect(
isShellCommandReadOnly("sed 's/foo/bar/w output.txt' file.txt"),
).toBe(false);
expect(isShellCommandReadOnly("sed 'w backup.txt' file.txt")).toBe(false);
});
it('rejects sed with read command', () => {
expect(
isShellCommandReadOnly("sed 's/foo/bar/r input.txt' file.txt"),
).toBe(false);
expect(isShellCommandReadOnly("sed 'r header.txt' file.txt")).toBe(false);
});
it('still rejects sed in-place editing', () => {
expect(isShellCommandReadOnly("sed -i 's/foo/bar/' file.txt")).toBe(
false,
);
expect(
isShellCommandReadOnly("sed --in-place 's/foo/bar/' file.txt"),
).toBe(false);
});
});
});

View File

@@ -92,30 +92,6 @@ const BLOCKED_GIT_BRANCH_FLAGS = new Set([
const BLOCKED_SED_PREFIXES = ['-i'];
// AWK side-effect patterns that can execute commands or write files
const AWK_SIDE_EFFECT_PATTERNS = [
/system\s*\(/, // system() function calls
/print\s+[^>|]*>\s*"[^"]*"/, // print > "file"
/printf\s+[^>|]*>\s*"[^"]*"/, // printf > "file"
/print\s+[^>|]*>>\s*"[^"]*"/, // print >> "file"
/printf\s+[^>|]*>>\s*"[^"]*"/, // printf >> "file"
/print\s+[^|]*\|\s*"[^"]*"/, // print | "command"
/printf\s+[^|]*\|\s*"[^"]*"/, // printf | "command"
/getline\s*<\s*"[^"]*"/, // getline < "command"
/"[^"]*"\s*\|\s*getline/, // "command" | getline
/close\s*\(/, // close() can trigger command execution
];
// SED side-effect patterns
const SED_SIDE_EFFECT_PATTERNS = [
/[^\\]e\s/, // e command (execute)
/^e\s/, // e command at start
/[^\\]w\s/, // w command (write)
/^w\s/, // w command at start
/[^\\]r\s/, // r command (read file)
/^r\s/, // r command at start
];
const ENV_ASSIGNMENT_REGEX = /^[A-Za-z_][A-Za-z0-9_]*=/;
function containsWriteRedirection(command: string): boolean {
@@ -206,31 +182,6 @@ function evaluateSedCommand(tokens: string[]): boolean {
return false;
}
}
// Check for side-effect patterns in sed script
const scriptContent = rest.join(' ');
for (const pattern of SED_SIDE_EFFECT_PATTERNS) {
if (pattern.test(scriptContent)) {
return false;
}
}
return true;
}
function evaluateAwkCommand(tokens: string[]): boolean {
const [, ...rest] = tokens;
// Join all arguments to check for awk script content
const scriptContent = rest.join(' ');
// Check for dangerous side-effect patterns
for (const pattern of AWK_SIDE_EFFECT_PATTERNS) {
if (pattern.test(scriptContent)) {
return false;
}
}
return true;
}
@@ -325,10 +276,6 @@ function evaluateShellSegment(segment: string): boolean {
return evaluateSedCommand([normalizedRoot, ...args]);
}
if (normalizedRoot === 'awk') {
return evaluateAwkCommand([normalizedRoot, ...args]);
}
if (normalizedRoot === 'git') {
return evaluateGitCommand([normalizedRoot, ...args]);
}

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.8.0",
"version": "0.8.0-preview.2",
"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.8.0",
"version": "0.8.0-preview.2",
"publisher": "qwenlm",
"icon": "assets/icon.png",
"repository": {