Compare commits

..

1 Commits

Author SHA1 Message Date
mingholy.lmh
30919b90e6 fix: arrow keys on windows 2025-09-19 19:38:30 +08:00
8 changed files with 22 additions and 175 deletions

12
package-lock.json generated
View File

@@ -1,12 +1,12 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.0.12",
"version": "0.0.11",
"lockfileVersion": 3,
"requires": true,
"packages": {
"": {
"name": "@qwen-code/qwen-code",
"version": "0.0.12",
"version": "0.0.11",
"workspaces": [
"packages/*"
],
@@ -13454,7 +13454,7 @@
},
"packages/cli": {
"name": "@qwen-code/qwen-code",
"version": "0.0.12",
"version": "0.0.11",
"dependencies": {
"@google/genai": "1.9.0",
"@iarna/toml": "^2.2.5",
@@ -13662,7 +13662,7 @@
},
"packages/core": {
"name": "@qwen-code/qwen-code-core",
"version": "0.0.12",
"version": "0.0.11",
"dependencies": {
"@google/genai": "1.13.0",
"@lvce-editor/ripgrep": "^1.6.0",
@@ -13788,7 +13788,7 @@
},
"packages/test-utils": {
"name": "@qwen-code/qwen-code-test-utils",
"version": "0.0.12",
"version": "0.0.11",
"dev": true,
"license": "Apache-2.0",
"devDependencies": {
@@ -13800,7 +13800,7 @@
},
"packages/vscode-ide-companion": {
"name": "qwen-code-vscode-ide-companion",
"version": "0.0.12",
"version": "0.0.11",
"license": "LICENSE",
"dependencies": {
"@modelcontextprotocol/sdk": "^1.15.1",

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code",
"version": "0.0.12",
"version": "0.0.11",
"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.0.12"
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.0.11"
},
"scripts": {
"start": "node scripts/start.js",

View File

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

View File

@@ -1,6 +1,6 @@
{
"name": "@qwen-code/qwen-code-core",
"version": "0.0.12",
"version": "0.0.11",
"description": "Qwen Code Core",
"repository": {
"type": "git",

View File

@@ -1105,164 +1105,5 @@ describe('ContentGenerationPipeline', () => {
expect.any(Array),
);
});
it('should collect all OpenAI chunks for logging even when Gemini responses are filtered', async () => {
// Create chunks that would produce empty Gemini responses (partial tool calls)
const partialToolCallChunk1: OpenAI.Chat.ChatCompletionChunk = {
id: 'chunk-1',
object: 'chat.completion.chunk',
created: Date.now(),
model: 'test-model',
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
id: 'call_123',
type: 'function',
function: { name: 'test_function', arguments: '{"par' },
},
],
},
finish_reason: null,
},
],
};
const partialToolCallChunk2: OpenAI.Chat.ChatCompletionChunk = {
id: 'chunk-2',
object: 'chat.completion.chunk',
created: Date.now(),
model: 'test-model',
choices: [
{
index: 0,
delta: {
tool_calls: [
{
index: 0,
function: { arguments: 'am": "value"}' },
},
],
},
finish_reason: null,
},
],
};
const finishChunk: OpenAI.Chat.ChatCompletionChunk = {
id: 'chunk-3',
object: 'chat.completion.chunk',
created: Date.now(),
model: 'test-model',
choices: [
{
index: 0,
delta: {},
finish_reason: 'tool_calls',
},
],
};
// Mock empty Gemini responses for partial chunks (they get filtered)
const emptyGeminiResponse1 = new GenerateContentResponse();
emptyGeminiResponse1.candidates = [
{
content: { parts: [], role: 'model' },
index: 0,
safetyRatings: [],
},
];
const emptyGeminiResponse2 = new GenerateContentResponse();
emptyGeminiResponse2.candidates = [
{
content: { parts: [], role: 'model' },
index: 0,
safetyRatings: [],
},
];
// Mock final Gemini response with tool call
const finalGeminiResponse = new GenerateContentResponse();
finalGeminiResponse.candidates = [
{
content: {
parts: [
{
functionCall: {
id: 'call_123',
name: 'test_function',
args: { param: 'value' },
},
},
],
role: 'model',
},
finishReason: FinishReason.STOP,
index: 0,
safetyRatings: [],
},
];
// Setup converter mocks
(mockConverter.convertGeminiRequestToOpenAI as Mock).mockReturnValue([
{ role: 'user', content: 'test' },
]);
(mockConverter.convertOpenAIChunkToGemini as Mock)
.mockReturnValueOnce(emptyGeminiResponse1) // First partial chunk -> empty response
.mockReturnValueOnce(emptyGeminiResponse2) // Second partial chunk -> empty response
.mockReturnValueOnce(finalGeminiResponse); // Finish chunk -> complete response
// Mock stream
const mockStream = {
async *[Symbol.asyncIterator]() {
yield partialToolCallChunk1;
yield partialToolCallChunk2;
yield finishChunk;
},
};
(mockClient.chat.completions.create as Mock).mockResolvedValue(
mockStream,
);
const request: GenerateContentParameters = {
model: 'test-model',
contents: [{ role: 'user', parts: [{ text: 'test' }] }],
};
// Collect responses
const responses: GenerateContentResponse[] = [];
const resultGenerator = await pipeline.executeStream(
request,
'test-prompt-id',
);
for await (const response of resultGenerator) {
responses.push(response);
}
// Should only yield the final response (empty ones are filtered)
expect(responses).toHaveLength(1);
expect(responses[0]).toBe(finalGeminiResponse);
// Verify telemetry was called with ALL OpenAI chunks, including the filtered ones
expect(mockTelemetryService.logStreamingSuccess).toHaveBeenCalledWith(
expect.objectContaining({
model: 'test-model',
duration: expect.any(Number),
userPromptId: 'test-prompt-id',
authType: 'openai',
}),
[finalGeminiResponse], // Only the non-empty Gemini response
expect.objectContaining({
model: 'test-model',
messages: [{ role: 'user', content: 'test' }],
}),
[partialToolCallChunk1, partialToolCallChunk2, finishChunk], // ALL OpenAI chunks
);
});
});
});

View File

@@ -118,9 +118,6 @@ export class ContentGenerationPipeline {
try {
// Stage 2a: Convert and yield each chunk while preserving original
for await (const chunk of stream) {
// Always collect OpenAI chunks for logging, regardless of Gemini conversion result
collectedOpenAIChunks.push(chunk);
const response = this.converter.convertOpenAIChunkToGemini(chunk);
// Stage 2b: Filter empty responses to avoid downstream issues
@@ -135,7 +132,9 @@ export class ContentGenerationPipeline {
// Stage 2c: Handle chunk merging for providers that send finishReason and usageMetadata separately
const shouldYield = this.handleChunkMerging(
response,
chunk,
collectedGeminiResponses,
collectedOpenAIChunks,
(mergedResponse) => {
pendingFinishResponse = mergedResponse;
},
@@ -183,13 +182,17 @@ export class ContentGenerationPipeline {
* finishReason and the most up-to-date usage information from any provider pattern.
*
* @param response Current Gemini response
* @param chunk Current OpenAI chunk
* @param collectedGeminiResponses Array to collect responses for logging
* @param collectedOpenAIChunks Array to collect chunks for logging
* @param setPendingFinish Callback to set pending finish response
* @returns true if the response should be yielded, false if it should be held for merging
*/
private handleChunkMerging(
response: GenerateContentResponse,
chunk: OpenAI.Chat.ChatCompletionChunk,
collectedGeminiResponses: GenerateContentResponse[],
collectedOpenAIChunks: OpenAI.Chat.ChatCompletionChunk[],
setPendingFinish: (response: GenerateContentResponse) => void,
): boolean {
const isFinishChunk = response.candidates?.[0]?.finishReason;
@@ -203,6 +206,7 @@ export class ContentGenerationPipeline {
if (isFinishChunk) {
// This is a finish reason chunk
collectedGeminiResponses.push(response);
collectedOpenAIChunks.push(chunk);
setPendingFinish(response);
return false; // Don't yield yet, wait for potential subsequent chunks to merge
} else if (hasPendingFinish) {
@@ -224,6 +228,7 @@ export class ContentGenerationPipeline {
// Update the collected responses with the merged response
collectedGeminiResponses[collectedGeminiResponses.length - 1] =
mergedResponse;
collectedOpenAIChunks.push(chunk);
setPendingFinish(mergedResponse);
return true; // Yield the merged response
@@ -231,6 +236,7 @@ export class ContentGenerationPipeline {
// Normal chunk - collect and yield
collectedGeminiResponses.push(response);
collectedOpenAIChunks.push(chunk);
return true;
}

View File

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