mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-01-04 16:09:15 +00:00
Compare commits
10 Commits
v0.6.0-nig
...
v0.6.0-pre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
53604963e8 | ||
|
|
105ad743fa | ||
|
|
ac3f7cb8c8 | ||
|
|
e27e9a5f18 | ||
|
|
2578d8c151 | ||
|
|
a877fedc52 | ||
|
|
d2bc46cbb4 | ||
|
|
84eb5c562f | ||
|
|
7b01b26ff5 | ||
|
|
0f3e97ea1c |
@@ -191,6 +191,7 @@ See [settings](https://qwenlm.github.io/qwen-code-docs/en/users/configuration/se
|
||||
|
||||
Looking for a graphical interface?
|
||||
|
||||
- [**AionUi**](https://github.com/iOfficeAI/AionUi) A modern GUI for command-line AI tools including Qwen Code
|
||||
- [**Gemini CLI Desktop**](https://github.com/Piebald-AI/gemini-cli-desktop) A cross-platform desktop/web/mobile UI for Qwen Code
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.0-preview.2",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.0-preview.2",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -17316,7 +17316,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.0-preview.2",
|
||||
"dependencies": {
|
||||
"@google/genai": "1.30.0",
|
||||
"@iarna/toml": "^2.2.5",
|
||||
@@ -17953,7 +17953,7 @@
|
||||
},
|
||||
"packages/core": {
|
||||
"name": "@qwen-code/qwen-code-core",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.0-preview.2",
|
||||
"hasInstallScript": true,
|
||||
"dependencies": {
|
||||
"@anthropic-ai/sdk": "^0.36.1",
|
||||
@@ -21413,7 +21413,7 @@
|
||||
},
|
||||
"packages/test-utils": {
|
||||
"name": "@qwen-code/qwen-code-test-utils",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.0-preview.2",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
@@ -21425,7 +21425,7 @@
|
||||
},
|
||||
"packages/vscode-ide-companion": {
|
||||
"name": "qwen-code-vscode-ide-companion",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.0-preview.2",
|
||||
"license": "LICENSE",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.25.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.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.6.0"
|
||||
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.6.0-preview.2"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env node scripts/start.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.0-preview.2",
|
||||
"description": "Qwen Code",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -33,7 +33,7 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.6.0"
|
||||
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.6.0-preview.2"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "1.30.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code-core",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.0-preview.2",
|
||||
"description": "Qwen Code Core",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -542,4 +542,206 @@ describe('OpenAIContentConverter', () => {
|
||||
expect(original).toEqual(originalCopy);
|
||||
});
|
||||
});
|
||||
|
||||
describe('mergeConsecutiveAssistantMessages', () => {
|
||||
it('should merge two consecutive assistant messages with string content', () => {
|
||||
const request: GenerateContentParameters = {
|
||||
model: 'models/test',
|
||||
contents: [
|
||||
{
|
||||
role: 'model',
|
||||
parts: [{ text: 'First part' }],
|
||||
},
|
||||
{
|
||||
role: 'model',
|
||||
parts: [{ text: 'Second part' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const messages = converter.convertGeminiRequestToOpenAI(request);
|
||||
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0].role).toBe('assistant');
|
||||
const content = messages[0]
|
||||
.content as OpenAI.Chat.ChatCompletionContentPart[];
|
||||
expect(content).toHaveLength(2);
|
||||
expect(content[0]).toEqual({ type: 'text', text: 'First part' });
|
||||
expect(content[1]).toEqual({ type: 'text', text: 'Second part' });
|
||||
});
|
||||
|
||||
it('should merge multiple consecutive assistant messages', () => {
|
||||
const request: GenerateContentParameters = {
|
||||
model: 'models/test',
|
||||
contents: [
|
||||
{
|
||||
role: 'model',
|
||||
parts: [{ text: 'Part 1' }],
|
||||
},
|
||||
{
|
||||
role: 'model',
|
||||
parts: [{ text: 'Part 2' }],
|
||||
},
|
||||
{
|
||||
role: 'model',
|
||||
parts: [{ text: 'Part 3' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const messages = converter.convertGeminiRequestToOpenAI(request);
|
||||
|
||||
expect(messages).toHaveLength(1);
|
||||
expect(messages[0].role).toBe('assistant');
|
||||
const content = messages[0]
|
||||
.content as OpenAI.Chat.ChatCompletionContentPart[];
|
||||
expect(content).toHaveLength(3);
|
||||
});
|
||||
|
||||
it('should merge tool_calls from consecutive assistant messages', () => {
|
||||
const request: GenerateContentParameters = {
|
||||
model: 'models/test',
|
||||
contents: [
|
||||
{
|
||||
role: 'model',
|
||||
parts: [
|
||||
{
|
||||
functionCall: {
|
||||
id: 'call_1',
|
||||
name: 'tool_1',
|
||||
args: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
parts: [
|
||||
{
|
||||
functionResponse: {
|
||||
id: 'call_1',
|
||||
name: 'tool_1',
|
||||
response: { output: 'result_1' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'model',
|
||||
parts: [
|
||||
{
|
||||
functionCall: {
|
||||
id: 'call_2',
|
||||
name: 'tool_2',
|
||||
args: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
parts: [
|
||||
{
|
||||
functionResponse: {
|
||||
id: 'call_2',
|
||||
name: 'tool_2',
|
||||
response: { output: 'result_2' },
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const messages = converter.convertGeminiRequestToOpenAI(request);
|
||||
|
||||
// Should have: assistant (tool_call_1), tool (result_1), assistant (tool_call_2), tool (result_2)
|
||||
expect(messages).toHaveLength(4);
|
||||
expect(messages[0].role).toBe('assistant');
|
||||
expect(messages[1].role).toBe('tool');
|
||||
expect(messages[2].role).toBe('assistant');
|
||||
expect(messages[3].role).toBe('tool');
|
||||
});
|
||||
|
||||
it('should not merge assistant messages separated by user messages', () => {
|
||||
const request: GenerateContentParameters = {
|
||||
model: 'models/test',
|
||||
contents: [
|
||||
{
|
||||
role: 'model',
|
||||
parts: [{ text: 'First assistant' }],
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
parts: [{ text: 'User message' }],
|
||||
},
|
||||
{
|
||||
role: 'model',
|
||||
parts: [{ text: 'Second assistant' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const messages = converter.convertGeminiRequestToOpenAI(request);
|
||||
|
||||
expect(messages).toHaveLength(3);
|
||||
expect(messages[0].role).toBe('assistant');
|
||||
expect(messages[1].role).toBe('user');
|
||||
expect(messages[2].role).toBe('assistant');
|
||||
});
|
||||
|
||||
it('should handle merging when one message has array content and another has string', () => {
|
||||
const request: GenerateContentParameters = {
|
||||
model: 'models/test',
|
||||
contents: [
|
||||
{
|
||||
role: 'model',
|
||||
parts: [{ text: 'Text part' }],
|
||||
},
|
||||
{
|
||||
role: 'model',
|
||||
parts: [{ text: 'Another text' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const messages = converter.convertGeminiRequestToOpenAI(request);
|
||||
|
||||
expect(messages).toHaveLength(1);
|
||||
const content = messages[0]
|
||||
.content as OpenAI.Chat.ChatCompletionContentPart[];
|
||||
expect(Array.isArray(content)).toBe(true);
|
||||
expect(content).toHaveLength(2);
|
||||
});
|
||||
|
||||
it('should merge empty content correctly', () => {
|
||||
const request: GenerateContentParameters = {
|
||||
model: 'models/test',
|
||||
contents: [
|
||||
{
|
||||
role: 'model',
|
||||
parts: [{ text: 'First' }],
|
||||
},
|
||||
{
|
||||
role: 'model',
|
||||
parts: [],
|
||||
},
|
||||
{
|
||||
role: 'model',
|
||||
parts: [{ text: 'Second' }],
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const messages = converter.convertGeminiRequestToOpenAI(request);
|
||||
|
||||
// Empty messages should be filtered out
|
||||
expect(messages).toHaveLength(1);
|
||||
const content = messages[0]
|
||||
.content as OpenAI.Chat.ChatCompletionContentPart[];
|
||||
expect(content).toHaveLength(2);
|
||||
expect(content[0]).toEqual({ type: 'text', text: 'First' });
|
||||
expect(content[1]).toEqual({ type: 'text', text: 'Second' });
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1120,12 +1120,44 @@ export class OpenAIContentConverter {
|
||||
// If the last message is also an assistant message, merge them
|
||||
if (lastMessage.role === 'assistant') {
|
||||
// Combine content
|
||||
const combinedContent = [
|
||||
typeof lastMessage.content === 'string' ? lastMessage.content : '',
|
||||
typeof message.content === 'string' ? message.content : '',
|
||||
]
|
||||
.filter(Boolean)
|
||||
.join('');
|
||||
const lastContent = lastMessage.content;
|
||||
const currentContent = message.content;
|
||||
|
||||
// Determine if we should use array format (if either content is an array)
|
||||
const useArrayFormat =
|
||||
Array.isArray(lastContent) || Array.isArray(currentContent);
|
||||
|
||||
let combinedContent:
|
||||
| string
|
||||
| OpenAI.Chat.ChatCompletionContentPart[]
|
||||
| null;
|
||||
|
||||
if (useArrayFormat) {
|
||||
// Convert both to array format and merge
|
||||
const lastParts = Array.isArray(lastContent)
|
||||
? lastContent
|
||||
: typeof lastContent === 'string' && lastContent
|
||||
? [{ type: 'text' as const, text: lastContent }]
|
||||
: [];
|
||||
|
||||
const currentParts = Array.isArray(currentContent)
|
||||
? currentContent
|
||||
: typeof currentContent === 'string' && currentContent
|
||||
? [{ type: 'text' as const, text: currentContent }]
|
||||
: [];
|
||||
|
||||
combinedContent = [
|
||||
...lastParts,
|
||||
...currentParts,
|
||||
] as OpenAI.Chat.ChatCompletionContentPart[];
|
||||
} else {
|
||||
// Both are strings or null, merge as strings
|
||||
const lastText = typeof lastContent === 'string' ? lastContent : '';
|
||||
const currentText =
|
||||
typeof currentContent === 'string' ? currentContent : '';
|
||||
const mergedText = [lastText, currentText].filter(Boolean).join('');
|
||||
combinedContent = mergedText || null;
|
||||
}
|
||||
|
||||
// Combine tool calls
|
||||
const lastToolCalls =
|
||||
@@ -1137,14 +1169,17 @@ export class OpenAIContentConverter {
|
||||
// Update the last message with combined data
|
||||
(
|
||||
lastMessage as OpenAI.Chat.ChatCompletionMessageParam & {
|
||||
content: string | null;
|
||||
content: string | OpenAI.Chat.ChatCompletionContentPart[] | null;
|
||||
tool_calls?: OpenAI.Chat.ChatCompletionMessageToolCall[];
|
||||
}
|
||||
).content = combinedContent || null;
|
||||
if (combinedToolCalls.length > 0) {
|
||||
(
|
||||
lastMessage as OpenAI.Chat.ChatCompletionMessageParam & {
|
||||
content: string | null;
|
||||
content:
|
||||
| string
|
||||
| OpenAI.Chat.ChatCompletionContentPart[]
|
||||
| null;
|
||||
tool_calls?: OpenAI.Chat.ChatCompletionMessageToolCall[];
|
||||
}
|
||||
).tool_calls = combinedToolCalls;
|
||||
|
||||
@@ -169,6 +169,44 @@ describe('ShellTool', () => {
|
||||
});
|
||||
expect(invocation.getDescription()).not.toContain('[background]');
|
||||
});
|
||||
|
||||
describe('is_background parameter coercion', () => {
|
||||
it('should accept string "true" as boolean true', () => {
|
||||
const invocation = shellTool.build({
|
||||
command: 'npm run dev',
|
||||
is_background: 'true' as unknown as boolean,
|
||||
});
|
||||
expect(invocation).toBeDefined();
|
||||
expect(invocation.getDescription()).toContain('[background]');
|
||||
});
|
||||
|
||||
it('should accept string "false" as boolean false', () => {
|
||||
const invocation = shellTool.build({
|
||||
command: 'npm run build',
|
||||
is_background: 'false' as unknown as boolean,
|
||||
});
|
||||
expect(invocation).toBeDefined();
|
||||
expect(invocation.getDescription()).not.toContain('[background]');
|
||||
});
|
||||
|
||||
it('should accept string "True" as boolean true', () => {
|
||||
const invocation = shellTool.build({
|
||||
command: 'npm run dev',
|
||||
is_background: 'True' as unknown as boolean,
|
||||
});
|
||||
expect(invocation).toBeDefined();
|
||||
expect(invocation.getDescription()).toContain('[background]');
|
||||
});
|
||||
|
||||
it('should accept string "False" as boolean false', () => {
|
||||
const invocation = shellTool.build({
|
||||
command: 'npm run build',
|
||||
is_background: 'False' as unknown as boolean,
|
||||
});
|
||||
expect(invocation).toBeDefined();
|
||||
expect(invocation.getDescription()).not.toContain('[background]');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
describe('execute', () => {
|
||||
|
||||
@@ -122,4 +122,91 @@ describe('SchemaValidator', () => {
|
||||
};
|
||||
expect(SchemaValidator.validate(schema, params)).not.toBeNull();
|
||||
});
|
||||
|
||||
describe('boolean string coercion', () => {
|
||||
const booleanSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
is_background: {
|
||||
type: 'boolean',
|
||||
},
|
||||
},
|
||||
required: ['is_background'],
|
||||
};
|
||||
|
||||
it('should coerce string "true" to boolean true', () => {
|
||||
const params = { is_background: 'true' };
|
||||
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
|
||||
expect(params.is_background).toBe(true);
|
||||
});
|
||||
|
||||
it('should coerce string "True" to boolean true', () => {
|
||||
const params = { is_background: 'True' };
|
||||
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
|
||||
expect(params.is_background).toBe(true);
|
||||
});
|
||||
|
||||
it('should coerce string "TRUE" to boolean true', () => {
|
||||
const params = { is_background: 'TRUE' };
|
||||
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
|
||||
expect(params.is_background).toBe(true);
|
||||
});
|
||||
|
||||
it('should coerce string "false" to boolean false', () => {
|
||||
const params = { is_background: 'false' };
|
||||
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
|
||||
expect(params.is_background).toBe(false);
|
||||
});
|
||||
|
||||
it('should coerce string "False" to boolean false', () => {
|
||||
const params = { is_background: 'False' };
|
||||
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
|
||||
expect(params.is_background).toBe(false);
|
||||
});
|
||||
|
||||
it('should coerce string "FALSE" to boolean false', () => {
|
||||
const params = { is_background: 'FALSE' };
|
||||
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
|
||||
expect(params.is_background).toBe(false);
|
||||
});
|
||||
|
||||
it('should handle nested objects with string booleans', () => {
|
||||
const nestedSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
options: {
|
||||
type: 'object',
|
||||
properties: {
|
||||
enabled: { type: 'boolean' },
|
||||
},
|
||||
},
|
||||
},
|
||||
};
|
||||
const params = { options: { enabled: 'true' } };
|
||||
expect(SchemaValidator.validate(nestedSchema, params)).toBeNull();
|
||||
expect((params.options as unknown as { enabled: boolean }).enabled).toBe(
|
||||
true,
|
||||
);
|
||||
});
|
||||
|
||||
it('should not affect non-boolean strings', () => {
|
||||
const mixedSchema = {
|
||||
type: 'object',
|
||||
properties: {
|
||||
name: { type: 'string' },
|
||||
is_active: { type: 'boolean' },
|
||||
},
|
||||
};
|
||||
const params = { name: 'trueman', is_active: 'true' };
|
||||
expect(SchemaValidator.validate(mixedSchema, params)).toBeNull();
|
||||
expect(params.name).toBe('trueman');
|
||||
expect(params.is_active).toBe(true);
|
||||
});
|
||||
|
||||
it('should pass through actual boolean values unchanged', () => {
|
||||
const params = { is_background: true };
|
||||
expect(SchemaValidator.validate(booleanSchema, params)).toBeNull();
|
||||
expect(params.is_background).toBe(true);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -41,14 +41,12 @@ export class SchemaValidator {
|
||||
return 'Value of params must be an object';
|
||||
}
|
||||
const validate = ajValidator.compile(schema);
|
||||
const valid = validate(data);
|
||||
let valid = validate(data);
|
||||
if (!valid && validate.errors) {
|
||||
// Find any True or False values and lowercase them
|
||||
fixBooleanCasing(data as Record<string, unknown>);
|
||||
|
||||
const validate = ajValidator.compile(schema);
|
||||
const valid = validate(data);
|
||||
// Coerce string boolean values ("true"/"false") to actual booleans
|
||||
fixBooleanValues(data as Record<string, unknown>);
|
||||
|
||||
valid = validate(data);
|
||||
if (!valid && validate.errors) {
|
||||
return ajValidator.errorsText(validate.errors, { dataVar: 'params' });
|
||||
}
|
||||
@@ -57,13 +55,29 @@ export class SchemaValidator {
|
||||
}
|
||||
}
|
||||
|
||||
function fixBooleanCasing(data: Record<string, unknown>) {
|
||||
/**
|
||||
* Coerces string boolean values to actual booleans.
|
||||
* This handles cases where LLMs return "true"/"false" strings instead of boolean values,
|
||||
* which is common with self-hosted LLMs.
|
||||
*
|
||||
* Converts:
|
||||
* - "true", "True", "TRUE" -> true
|
||||
* - "false", "False", "FALSE" -> false
|
||||
*/
|
||||
function fixBooleanValues(data: Record<string, unknown>) {
|
||||
for (const key of Object.keys(data)) {
|
||||
if (!(key in data)) continue;
|
||||
const value = data[key];
|
||||
|
||||
if (typeof data[key] === 'object') {
|
||||
fixBooleanCasing(data[key] as Record<string, unknown>);
|
||||
} else if (data[key] === 'True') data[key] = 'true';
|
||||
else if (data[key] === 'False') data[key] = 'false';
|
||||
if (typeof value === 'object' && value !== null) {
|
||||
fixBooleanValues(value as Record<string, unknown>);
|
||||
} else if (typeof value === 'string') {
|
||||
const lower = value.toLowerCase();
|
||||
if (lower === 'true') {
|
||||
data[key] = true;
|
||||
} else if (lower === 'false') {
|
||||
data[key] = false;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code-test-utils",
|
||||
"version": "0.6.0",
|
||||
"version": "0.6.0-preview.2",
|
||||
"private": true,
|
||||
"main": "src/index.ts",
|
||||
"license": "Apache-2.0",
|
||||
|
||||
@@ -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.6.0",
|
||||
"version": "0.6.0-preview.2",
|
||||
"publisher": "qwenlm",
|
||||
"icon": "assets/icon.png",
|
||||
"repository": {
|
||||
|
||||
Reference in New Issue
Block a user