mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-25 11:09:13 +00:00
Compare commits
16 Commits
web-search
...
v0.1.4
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
553a36302a | ||
|
|
498d7a083a | ||
|
|
3a69931791 | ||
|
|
d4ab328671 | ||
|
|
90500ea67b | ||
|
|
335e765df0 | ||
|
|
448e30bf88 | ||
|
|
26215b6d0a | ||
|
|
f6f76a17e6 | ||
|
|
55a3b69a8e | ||
|
|
22bd108775 | ||
|
|
7e827833bf | ||
|
|
45f1000dea | ||
|
|
04f0996327 | ||
|
|
d8cc0a1f04 | ||
|
|
50d5cc2f6a |
@@ -541,6 +541,9 @@ Arguments passed directly when running the CLI can override other configurations
|
||||
- Displays the version of the CLI.
|
||||
- **`--openai-logging`**:
|
||||
- Enables logging of OpenAI API calls for debugging and analysis. This flag overrides the `enableOpenAILogging` setting in `settings.json`.
|
||||
- **`--openai-logging-dir <directory>`**:
|
||||
- Sets a custom directory path for OpenAI API logs. This flag overrides the `openAILoggingDir` setting in `settings.json`. Supports absolute paths, relative paths, and `~` expansion.
|
||||
- **Example:** `qwen --openai-logging-dir "~/qwen-logs" --openai-logging`
|
||||
- **`--tavily-api-key <api_key>`**:
|
||||
- Sets the Tavily API key for web search functionality for this session.
|
||||
- Example: `qwen --tavily-api-key tvly-your-api-key-here`
|
||||
|
||||
@@ -171,6 +171,18 @@ Settings are organized into categories. All settings should be placed within the
|
||||
- **Description:** Disables loop detection checks. Loop detection prevents infinite loops in AI responses but can generate false positives that interrupt legitimate workflows. Enable this option if you experience frequent false positive loop detection interruptions.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`model.enableOpenAILogging`** (boolean):
|
||||
- **Description:** Enables logging of OpenAI API calls for debugging and analysis. When enabled, API requests and responses are logged to JSON files.
|
||||
- **Default:** `false`
|
||||
|
||||
- **`model.openAILoggingDir`** (string):
|
||||
- **Description:** Custom directory path for OpenAI API logs. If not specified, defaults to `logs/openai` in the current working directory. Supports absolute paths, relative paths (resolved from current working directory), and `~` expansion (home directory).
|
||||
- **Default:** `undefined`
|
||||
- **Examples:**
|
||||
- `"~/qwen-logs"` - Logs to `~/qwen-logs` directory
|
||||
- `"./custom-logs"` - Logs to `./custom-logs` relative to current directory
|
||||
- `"/tmp/openai-logs"` - Logs to absolute path `/tmp/openai-logs`
|
||||
|
||||
#### `context`
|
||||
|
||||
- **`context.fileName`** (string or array of strings):
|
||||
@@ -387,6 +399,8 @@ Here is an example of a `settings.json` file with the nested structure, new as o
|
||||
"model": {
|
||||
"name": "qwen3-coder-plus",
|
||||
"maxSessionTurns": 10,
|
||||
"enableOpenAILogging": false,
|
||||
"openAILoggingDir": "~/qwen-logs",
|
||||
"summarizeToolOutput": {
|
||||
"run_shell_command": {
|
||||
"tokenBudget": 100
|
||||
@@ -557,6 +571,9 @@ Arguments passed directly when running the CLI can override other configurations
|
||||
- Displays the version of the CLI.
|
||||
- **`--openai-logging`**:
|
||||
- Enables logging of OpenAI API calls for debugging and analysis. This flag overrides the `enableOpenAILogging` setting in `settings.json`.
|
||||
- **`--openai-logging-dir <directory>`**:
|
||||
- Sets a custom directory path for OpenAI API logs. This flag overrides the `openAILoggingDir` setting in `settings.json`. Supports absolute paths, relative paths, and `~` expansion.
|
||||
- **Example:** `qwen --openai-logging-dir "~/qwen-logs" --openai-logging`
|
||||
- **`--tavily-api-key <api_key>`**:
|
||||
- Sets the Tavily API key for web search functionality for this session.
|
||||
- Example: `qwen --tavily-api-key tvly-your-api-key-here`
|
||||
|
||||
12
package-lock.json
generated
12
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -16024,7 +16024,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"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.3",
|
||||
"version": "0.1.4",
|
||||
"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.3",
|
||||
"version": "0.1.4",
|
||||
"dev": true,
|
||||
"license": "Apache-2.0",
|
||||
"devDependencies": {
|
||||
@@ -16290,7 +16290,7 @@
|
||||
},
|
||||
"packages/vscode-ide-companion": {
|
||||
"name": "qwen-code-vscode-ide-companion",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"license": "LICENSE",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"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.3"
|
||||
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.1.4"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "cross-env node scripts/start.js",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"description": "Qwen Code",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
@@ -25,7 +25,7 @@
|
||||
"dist"
|
||||
],
|
||||
"config": {
|
||||
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.1.3"
|
||||
"sandboxImageUri": "ghcr.io/qwenlm/qwen-code:0.1.4"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "1.16.0",
|
||||
|
||||
@@ -114,6 +114,7 @@ export interface CliArgs {
|
||||
openaiLogging: boolean | undefined;
|
||||
openaiApiKey: string | undefined;
|
||||
openaiBaseUrl: string | undefined;
|
||||
openaiLoggingDir: string | undefined;
|
||||
proxy: string | undefined;
|
||||
includeDirectories: string[] | undefined;
|
||||
tavilyApiKey: string | undefined;
|
||||
@@ -317,6 +318,11 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
||||
description:
|
||||
'Enable logging of OpenAI API calls for debugging and analysis',
|
||||
})
|
||||
.option('openai-logging-dir', {
|
||||
type: 'string',
|
||||
description:
|
||||
'Custom directory path for OpenAI API logs. Overrides settings files.',
|
||||
})
|
||||
.option('openai-api-key', {
|
||||
type: 'string',
|
||||
description: 'OpenAI API key to use for authentication',
|
||||
@@ -764,6 +770,8 @@ export async function loadCliConfig(
|
||||
(typeof argv.openaiLogging === 'undefined'
|
||||
? settings.model?.enableOpenAILogging
|
||||
: argv.openaiLogging) ?? false,
|
||||
openAILoggingDir:
|
||||
argv.openaiLoggingDir || settings.model?.openAILoggingDir,
|
||||
},
|
||||
cliVersion: await getCliVersion(),
|
||||
webSearch: buildWebSearchConfig(
|
||||
|
||||
@@ -558,6 +558,16 @@ const SETTINGS_SCHEMA = {
|
||||
description: 'Enable OpenAI logging.',
|
||||
showInDialog: true,
|
||||
},
|
||||
openAILoggingDir: {
|
||||
type: 'string',
|
||||
label: 'OpenAI Logging Directory',
|
||||
category: 'Model',
|
||||
requiresRestart: false,
|
||||
default: undefined as string | undefined,
|
||||
description:
|
||||
'Custom directory path for OpenAI API logs. If not specified, defaults to logs/openai in the current working directory.',
|
||||
showInDialog: true,
|
||||
},
|
||||
generationConfig: {
|
||||
type: 'object',
|
||||
label: 'Generation Configuration',
|
||||
|
||||
@@ -327,6 +327,7 @@ describe('gemini.tsx main function kitty protocol', () => {
|
||||
openaiLogging: undefined,
|
||||
openaiApiKey: undefined,
|
||||
openaiBaseUrl: undefined,
|
||||
openaiLoggingDir: undefined,
|
||||
proxy: undefined,
|
||||
includeDirectories: undefined,
|
||||
tavilyApiKey: undefined,
|
||||
|
||||
@@ -1227,4 +1227,28 @@ describe('FileCommandLoader', () => {
|
||||
expect(commands).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
|
||||
describe('AbortError handling', () => {
|
||||
it('should silently ignore AbortError when operation is cancelled', async () => {
|
||||
const userCommandsDir = Storage.getUserCommandsDir();
|
||||
mock({
|
||||
[userCommandsDir]: {
|
||||
'test1.toml': 'prompt = "Prompt 1"',
|
||||
'test2.toml': 'prompt = "Prompt 2"',
|
||||
},
|
||||
});
|
||||
|
||||
const loader = new FileCommandLoader(null);
|
||||
const controller = new AbortController();
|
||||
const signal = controller.signal;
|
||||
|
||||
// Start loading and immediately abort
|
||||
const loadPromise = loader.loadCommands(signal);
|
||||
controller.abort();
|
||||
|
||||
// Should not throw or print errors
|
||||
const commands = await loadPromise;
|
||||
expect(commands).toHaveLength(0);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -120,7 +120,11 @@ export class FileCommandLoader implements ICommandLoader {
|
||||
// Add all commands without deduplication
|
||||
allCommands.push(...commands);
|
||||
} catch (error) {
|
||||
if ((error as NodeJS.ErrnoException).code !== 'ENOENT') {
|
||||
// Ignore ENOENT (directory doesn't exist) and AbortError (operation was cancelled)
|
||||
const isEnoent = (error as NodeJS.ErrnoException).code === 'ENOENT';
|
||||
const isAbortError =
|
||||
error instanceof Error && error.name === 'AbortError';
|
||||
if (!isEnoent && !isAbortError) {
|
||||
console.error(
|
||||
`[FileCommandLoader] Error loading commands from ${dirInfo.path}:`,
|
||||
error,
|
||||
|
||||
@@ -916,17 +916,9 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
(result: IdeIntegrationNudgeResult) => {
|
||||
if (result.userSelection === 'yes') {
|
||||
handleSlashCommand('/ide install');
|
||||
settings.setValue(
|
||||
SettingScope.User,
|
||||
'hasSeenIdeIntegrationNudge',
|
||||
true,
|
||||
);
|
||||
settings.setValue(SettingScope.User, 'ide.hasSeenNudge', true);
|
||||
} else if (result.userSelection === 'dismiss') {
|
||||
settings.setValue(
|
||||
SettingScope.User,
|
||||
'hasSeenIdeIntegrationNudge',
|
||||
true,
|
||||
);
|
||||
settings.setValue(SettingScope.User, 'ide.hasSeenNudge', true);
|
||||
}
|
||||
setIdePromptAnswered(true);
|
||||
},
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code-core",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"description": "Qwen Code Core",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -21,6 +21,9 @@ vi.mock('../../telemetry/loggers.js', () => ({
|
||||
}));
|
||||
|
||||
vi.mock('../../utils/openaiLogger.js', () => ({
|
||||
OpenAILogger: vi.fn().mockImplementation(() => ({
|
||||
logInteraction: vi.fn(),
|
||||
})),
|
||||
openaiLogger: {
|
||||
logInteraction: vi.fn(),
|
||||
},
|
||||
|
||||
@@ -58,6 +58,7 @@ export type ContentGeneratorConfig = {
|
||||
vertexai?: boolean;
|
||||
authType?: AuthType | undefined;
|
||||
enableOpenAILogging?: boolean;
|
||||
openAILoggingDir?: string;
|
||||
// Timeout configuration in milliseconds
|
||||
timeout?: number;
|
||||
// Maximum retries for failed requests
|
||||
|
||||
@@ -32,6 +32,7 @@ export class OpenAIContentGenerator implements ContentGenerator {
|
||||
telemetryService: new DefaultTelemetryService(
|
||||
cliConfig,
|
||||
contentGeneratorConfig.enableOpenAILogging,
|
||||
contentGeneratorConfig.openAILoggingDir,
|
||||
),
|
||||
errorHandler: new EnhancedErrorHandler(
|
||||
(error: unknown, request: GenerateContentParameters) =>
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
import type { Config } from '../../config/config.js';
|
||||
import { logApiError, logApiResponse } from '../../telemetry/loggers.js';
|
||||
import { ApiErrorEvent, ApiResponseEvent } from '../../telemetry/types.js';
|
||||
import { openaiLogger } from '../../utils/openaiLogger.js';
|
||||
import { OpenAILogger } from '../../utils/openaiLogger.js';
|
||||
import type { GenerateContentResponse } from '@google/genai';
|
||||
import type OpenAI from 'openai';
|
||||
|
||||
@@ -43,10 +43,17 @@ export interface TelemetryService {
|
||||
}
|
||||
|
||||
export class DefaultTelemetryService implements TelemetryService {
|
||||
private logger: OpenAILogger;
|
||||
|
||||
constructor(
|
||||
private config: Config,
|
||||
private enableOpenAILogging: boolean = false,
|
||||
) {}
|
||||
openAILoggingDir?: string,
|
||||
) {
|
||||
// Always create a new logger instance to ensure correct working directory
|
||||
// If no custom directory is provided, undefined will use the default path
|
||||
this.logger = new OpenAILogger(openAILoggingDir);
|
||||
}
|
||||
|
||||
async logSuccess(
|
||||
context: RequestContext,
|
||||
@@ -68,7 +75,7 @@ export class DefaultTelemetryService implements TelemetryService {
|
||||
|
||||
// Log interaction if enabled
|
||||
if (this.enableOpenAILogging && openaiRequest && openaiResponse) {
|
||||
await openaiLogger.logInteraction(openaiRequest, openaiResponse);
|
||||
await this.logger.logInteraction(openaiRequest, openaiResponse);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -97,7 +104,7 @@ export class DefaultTelemetryService implements TelemetryService {
|
||||
|
||||
// Log error interaction if enabled
|
||||
if (this.enableOpenAILogging && openaiRequest) {
|
||||
await openaiLogger.logInteraction(
|
||||
await this.logger.logInteraction(
|
||||
openaiRequest,
|
||||
undefined,
|
||||
error as Error,
|
||||
@@ -137,7 +144,7 @@ export class DefaultTelemetryService implements TelemetryService {
|
||||
openaiChunks.length > 0
|
||||
) {
|
||||
const combinedResponse = this.combineOpenAIChunksForLogging(openaiChunks);
|
||||
await openaiLogger.logInteraction(openaiRequest, combinedResponse);
|
||||
await this.logger.logInteraction(openaiRequest, combinedResponse);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -64,6 +64,12 @@ describe('normalize', () => {
|
||||
expect(normalize('qwen-vl-max-latest')).toBe('qwen-vl-max-latest');
|
||||
});
|
||||
|
||||
it('should preserve date suffixes for Kimi K2 models', () => {
|
||||
expect(normalize('kimi-k2-0905-preview')).toBe('kimi-k2-0905');
|
||||
expect(normalize('kimi-k2-0711-preview')).toBe('kimi-k2-0711');
|
||||
expect(normalize('kimi-k2-turbo-preview')).toBe('kimi-k2-turbo');
|
||||
});
|
||||
|
||||
it('should remove date like suffixes', () => {
|
||||
expect(normalize('deepseek-r1-0528')).toBe('deepseek-r1');
|
||||
});
|
||||
@@ -213,7 +219,7 @@ describe('tokenLimit', () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe('Other models', () => {
|
||||
describe('DeepSeek', () => {
|
||||
it('should return the correct limit for deepseek-r1', () => {
|
||||
expect(tokenLimit('deepseek-r1')).toBe(131072);
|
||||
});
|
||||
@@ -226,9 +232,27 @@ describe('tokenLimit', () => {
|
||||
it('should return the correct limit for deepseek-v3.2', () => {
|
||||
expect(tokenLimit('deepseek-v3.2-exp')).toBe(131072);
|
||||
});
|
||||
it('should return the correct limit for kimi-k2-instruct', () => {
|
||||
expect(tokenLimit('kimi-k2-instruct')).toBe(131072);
|
||||
});
|
||||
|
||||
describe('Moonshot Kimi', () => {
|
||||
it('should return the correct limit for kimi-k2-0905-preview', () => {
|
||||
expect(tokenLimit('kimi-k2-0905-preview')).toBe(262144); // 256K
|
||||
expect(tokenLimit('kimi-k2-0905')).toBe(262144);
|
||||
});
|
||||
it('should return the correct limit for kimi-k2-turbo-preview', () => {
|
||||
expect(tokenLimit('kimi-k2-turbo-preview')).toBe(262144); // 256K
|
||||
expect(tokenLimit('kimi-k2-turbo')).toBe(262144);
|
||||
});
|
||||
it('should return the correct limit for kimi-k2-0711-preview', () => {
|
||||
expect(tokenLimit('kimi-k2-0711-preview')).toBe(131072); // 128K
|
||||
expect(tokenLimit('kimi-k2-0711')).toBe(131072);
|
||||
});
|
||||
it('should return the correct limit for kimi-k2-instruct', () => {
|
||||
expect(tokenLimit('kimi-k2-instruct')).toBe(131072); // 128K
|
||||
});
|
||||
});
|
||||
|
||||
describe('Other models', () => {
|
||||
it('should return the correct limit for gpt-oss', () => {
|
||||
expect(tokenLimit('gpt-oss')).toBe(131072);
|
||||
});
|
||||
|
||||
@@ -47,8 +47,13 @@ export function normalize(model: string): string {
|
||||
// remove trailing build / date / revision suffixes:
|
||||
// - dates (e.g., -20250219), -v1, version numbers, 'latest', 'preview' etc.
|
||||
s = s.replace(/-preview/g, '');
|
||||
// Special handling for Qwen model names that include "-latest" as part of the model name
|
||||
if (!s.match(/^qwen-(?:plus|flash|vl-max)-latest$/)) {
|
||||
// Special handling for model names that include date/version as part of the model identifier
|
||||
// - Qwen models: qwen-plus-latest, qwen-flash-latest, qwen-vl-max-latest
|
||||
// - Kimi models: kimi-k2-0905, kimi-k2-0711, etc. (keep date for version distinction)
|
||||
if (
|
||||
!s.match(/^qwen-(?:plus|flash|vl-max)-latest$/) &&
|
||||
!s.match(/^kimi-k2-\d{4}$/)
|
||||
) {
|
||||
// Regex breakdown:
|
||||
// -(?:...)$ - Non-capturing group for suffixes at the end of the string
|
||||
// The following patterns are matched within the group:
|
||||
@@ -165,9 +170,16 @@ const PATTERNS: Array<[RegExp, TokenCount]> = [
|
||||
[/^deepseek-v3(?:\.\d+)?(?:-.*)?$/, LIMITS['128k']],
|
||||
|
||||
// -------------------
|
||||
// GPT-OSS / Kimi / Llama & Mistral examples
|
||||
// Moonshot / Kimi
|
||||
// -------------------
|
||||
[/^kimi-k2-0905$/, LIMITS['256k']], // Kimi-k2-0905-preview: 256K context
|
||||
[/^kimi-k2-turbo.*$/, LIMITS['256k']], // Kimi-k2-turbo-preview: 256K context
|
||||
[/^kimi-k2-0711$/, LIMITS['128k']], // Kimi-k2-0711-preview: 128K context
|
||||
[/^kimi-k2-instruct.*$/, LIMITS['128k']], // Kimi-k2-instruct: 128K context
|
||||
|
||||
// -------------------
|
||||
// GPT-OSS / Llama & Mistral examples
|
||||
// -------------------
|
||||
[/^kimi-k2-instruct.*$/, LIMITS['128k']],
|
||||
[/^gpt-oss.*$/, LIMITS['128k']],
|
||||
[/^llama-4-scout.*$/, LIMITS['10m']],
|
||||
[/^mistral-large-2.*$/, LIMITS['128k']],
|
||||
|
||||
@@ -113,7 +113,7 @@ describe('IdeClient', () => {
|
||||
'utf8',
|
||||
);
|
||||
expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(
|
||||
new URL('http://localhost:8080/mcp'),
|
||||
new URL('http://127.0.0.1:8080/mcp'),
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(mockClient.connect).toHaveBeenCalledWith(mockHttpTransport);
|
||||
@@ -181,7 +181,7 @@ describe('IdeClient', () => {
|
||||
await ideClient.connect();
|
||||
|
||||
expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(
|
||||
new URL('http://localhost:9090/mcp'),
|
||||
new URL('http://127.0.0.1:9090/mcp'),
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(mockClient.connect).toHaveBeenCalledWith(mockHttpTransport);
|
||||
@@ -230,7 +230,7 @@ describe('IdeClient', () => {
|
||||
await ideClient.connect();
|
||||
|
||||
expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(
|
||||
new URL('http://localhost:8080/mcp'),
|
||||
new URL('http://127.0.0.1:8080/mcp'),
|
||||
expect.any(Object),
|
||||
);
|
||||
expect(ideClient.getConnectionStatus().status).toBe(
|
||||
@@ -665,7 +665,7 @@ describe('IdeClient', () => {
|
||||
await ideClient.connect();
|
||||
|
||||
expect(StreamableHTTPClientTransport).toHaveBeenCalledWith(
|
||||
new URL('http://localhost:8080/mcp'),
|
||||
new URL('http://127.0.0.1:8080/mcp'),
|
||||
expect.objectContaining({
|
||||
requestInit: {
|
||||
headers: {
|
||||
|
||||
@@ -667,10 +667,10 @@ export class IdeClient {
|
||||
}
|
||||
|
||||
private createProxyAwareFetch() {
|
||||
// ignore proxy for 'localhost' by deafult to allow connecting to the ide mcp server
|
||||
// ignore proxy for '127.0.0.1' by deafult to allow connecting to the ide mcp server
|
||||
const existingNoProxy = process.env['NO_PROXY'] || '';
|
||||
const agent = new EnvHttpProxyAgent({
|
||||
noProxy: [existingNoProxy, 'localhost'].filter(Boolean).join(','),
|
||||
noProxy: [existingNoProxy, '127.0.0.1'].filter(Boolean).join(','),
|
||||
});
|
||||
const undiciPromise = import('undici');
|
||||
return async (url: string | URL, init?: RequestInit): Promise<Response> => {
|
||||
@@ -851,5 +851,5 @@ export class IdeClient {
|
||||
function getIdeServerHost() {
|
||||
const isInContainer =
|
||||
fs.existsSync('/.dockerenv') || fs.existsSync('/run/.containerenv');
|
||||
return isInContainer ? 'host.docker.internal' : 'localhost';
|
||||
return isInContainer ? 'host.docker.internal' : '127.0.0.1';
|
||||
}
|
||||
|
||||
@@ -112,14 +112,19 @@ describe('ide-installer', () => {
|
||||
platform: 'linux',
|
||||
});
|
||||
await installer.install();
|
||||
|
||||
// Note: The implementation uses process.platform, not the mocked platform
|
||||
const isActuallyWindows = process.platform === 'win32';
|
||||
const expectedCommand = isActuallyWindows ? '"code"' : 'code';
|
||||
|
||||
expect(child_process.spawnSync).toHaveBeenCalledWith(
|
||||
'code',
|
||||
expectedCommand,
|
||||
[
|
||||
'--install-extension',
|
||||
'qwenlm.qwen-code-vscode-ide-companion',
|
||||
'--force',
|
||||
],
|
||||
{ stdio: 'pipe' },
|
||||
{ stdio: 'pipe', shell: isActuallyWindows },
|
||||
);
|
||||
});
|
||||
|
||||
|
||||
@@ -117,15 +117,16 @@ class VsCodeInstaller implements IdeInstaller {
|
||||
};
|
||||
}
|
||||
|
||||
const isWindows = process.platform === 'win32';
|
||||
try {
|
||||
const result = child_process.spawnSync(
|
||||
commandPath,
|
||||
isWindows ? `"${commandPath}"` : commandPath,
|
||||
[
|
||||
'--install-extension',
|
||||
'qwenlm.qwen-code-vscode-ide-companion',
|
||||
'--force',
|
||||
],
|
||||
{ stdio: 'pipe' },
|
||||
{ stdio: 'pipe', shell: isWindows },
|
||||
);
|
||||
|
||||
if (result.status !== 0) {
|
||||
|
||||
381
packages/core/src/utils/openaiLogger.test.ts
Normal file
381
packages/core/src/utils/openaiLogger.test.ts
Normal file
@@ -0,0 +1,381 @@
|
||||
/**
|
||||
* @license
|
||||
* Copyright 2025 Qwen
|
||||
* SPDX-License-Identifier: Apache-2.0
|
||||
*/
|
||||
|
||||
import { describe, it, expect, beforeEach, afterEach } from 'vitest';
|
||||
import * as path from 'node:path';
|
||||
import * as os from 'os';
|
||||
import { promises as fs } from 'node:fs';
|
||||
import { OpenAILogger } from './openaiLogger.js';
|
||||
|
||||
describe('OpenAILogger', () => {
|
||||
let originalCwd: string;
|
||||
let testTempDir: string;
|
||||
const createdDirs: string[] = [];
|
||||
|
||||
beforeEach(() => {
|
||||
originalCwd = process.cwd();
|
||||
testTempDir = path.join(os.tmpdir(), `openai-logger-test-${Date.now()}`);
|
||||
createdDirs.length = 0; // Clear array
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
// Clean up all created directories
|
||||
const cleanupPromises = [
|
||||
testTempDir,
|
||||
...createdDirs,
|
||||
path.resolve(process.cwd(), 'relative-logs'),
|
||||
path.resolve(process.cwd(), 'custom-logs'),
|
||||
path.resolve(process.cwd(), 'test-relative-logs'),
|
||||
path.join(os.homedir(), 'custom-logs'),
|
||||
path.join(os.homedir(), 'test-openai-logs'),
|
||||
].map(async (dir) => {
|
||||
try {
|
||||
await fs.rm(dir, { recursive: true, force: true });
|
||||
} catch {
|
||||
// Ignore cleanup errors
|
||||
}
|
||||
});
|
||||
|
||||
await Promise.all(cleanupPromises);
|
||||
process.chdir(originalCwd);
|
||||
});
|
||||
|
||||
describe('constructor', () => {
|
||||
it('should use default directory when no custom directory is provided', () => {
|
||||
const logger = new OpenAILogger();
|
||||
// We can't directly access private logDir, but we can verify behavior
|
||||
expect(logger).toBeInstanceOf(OpenAILogger);
|
||||
});
|
||||
|
||||
it('should accept absolute path as custom directory', () => {
|
||||
const customDir = '/absolute/path/to/logs';
|
||||
const logger = new OpenAILogger(customDir);
|
||||
expect(logger).toBeInstanceOf(OpenAILogger);
|
||||
});
|
||||
|
||||
it('should resolve relative path to absolute path', async () => {
|
||||
const relativeDir = 'custom-logs';
|
||||
const logger = new OpenAILogger(relativeDir);
|
||||
const expectedDir = path.resolve(process.cwd(), relativeDir);
|
||||
createdDirs.push(expectedDir);
|
||||
expect(logger).toBeInstanceOf(OpenAILogger);
|
||||
});
|
||||
|
||||
it('should expand ~ to home directory', () => {
|
||||
const customDir = '~/custom-logs';
|
||||
const logger = new OpenAILogger(customDir);
|
||||
expect(logger).toBeInstanceOf(OpenAILogger);
|
||||
});
|
||||
|
||||
it('should expand ~/ to home directory', () => {
|
||||
const customDir = '~/custom-logs';
|
||||
const logger = new OpenAILogger(customDir);
|
||||
expect(logger).toBeInstanceOf(OpenAILogger);
|
||||
});
|
||||
|
||||
it('should handle just ~ as home directory', () => {
|
||||
const customDir = '~';
|
||||
const logger = new OpenAILogger(customDir);
|
||||
expect(logger).toBeInstanceOf(OpenAILogger);
|
||||
});
|
||||
});
|
||||
|
||||
describe('initialize', () => {
|
||||
it('should create directory if it does not exist', async () => {
|
||||
const logger = new OpenAILogger(testTempDir);
|
||||
await logger.initialize();
|
||||
|
||||
const dirExists = await fs
|
||||
.access(testTempDir)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
expect(dirExists).toBe(true);
|
||||
});
|
||||
|
||||
it('should create nested directories recursively', async () => {
|
||||
const nestedDir = path.join(testTempDir, 'nested', 'deep', 'path');
|
||||
const logger = new OpenAILogger(nestedDir);
|
||||
await logger.initialize();
|
||||
|
||||
const dirExists = await fs
|
||||
.access(nestedDir)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
expect(dirExists).toBe(true);
|
||||
});
|
||||
|
||||
it('should not throw if directory already exists', async () => {
|
||||
await fs.mkdir(testTempDir, { recursive: true });
|
||||
const logger = new OpenAILogger(testTempDir);
|
||||
await expect(logger.initialize()).resolves.not.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('logInteraction', () => {
|
||||
it('should create log file with correct format', async () => {
|
||||
const logger = new OpenAILogger(testTempDir);
|
||||
await logger.initialize();
|
||||
|
||||
const request = {
|
||||
model: 'gpt-4',
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
};
|
||||
const response = { id: 'test-id', choices: [] };
|
||||
|
||||
const logPath = await logger.logInteraction(request, response);
|
||||
|
||||
expect(logPath).toContain(testTempDir);
|
||||
expect(logPath).toMatch(
|
||||
/openai-\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}\.\d{3}Z-[a-f0-9]{8}\.json/,
|
||||
);
|
||||
|
||||
const fileExists = await fs
|
||||
.access(logPath)
|
||||
.then(() => true)
|
||||
.catch(() => false);
|
||||
expect(fileExists).toBe(true);
|
||||
});
|
||||
|
||||
it('should write correct log data structure', async () => {
|
||||
const logger = new OpenAILogger(testTempDir);
|
||||
await logger.initialize();
|
||||
|
||||
const request = {
|
||||
model: 'gpt-4',
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
};
|
||||
const response = { id: 'test-id', choices: [] };
|
||||
|
||||
const logPath = await logger.logInteraction(request, response);
|
||||
const logContent = JSON.parse(await fs.readFile(logPath, 'utf-8'));
|
||||
|
||||
expect(logContent).toHaveProperty('timestamp');
|
||||
expect(logContent).toHaveProperty('request', request);
|
||||
expect(logContent).toHaveProperty('response', response);
|
||||
expect(logContent).toHaveProperty('error', null);
|
||||
expect(logContent).toHaveProperty('system');
|
||||
expect(logContent.system).toHaveProperty('hostname');
|
||||
expect(logContent.system).toHaveProperty('platform');
|
||||
expect(logContent.system).toHaveProperty('release');
|
||||
expect(logContent.system).toHaveProperty('nodeVersion');
|
||||
});
|
||||
|
||||
it('should log error when provided', async () => {
|
||||
const logger = new OpenAILogger(testTempDir);
|
||||
await logger.initialize();
|
||||
|
||||
const request = {
|
||||
model: 'gpt-4',
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
};
|
||||
const error = new Error('Test error');
|
||||
|
||||
const logPath = await logger.logInteraction(request, undefined, error);
|
||||
const logContent = JSON.parse(await fs.readFile(logPath, 'utf-8'));
|
||||
|
||||
expect(logContent).toHaveProperty('error');
|
||||
expect(logContent.error).toHaveProperty('message', 'Test error');
|
||||
expect(logContent.error).toHaveProperty('stack');
|
||||
expect(logContent.response).toBeNull();
|
||||
});
|
||||
|
||||
it('should use custom directory when provided', async () => {
|
||||
const customDir = path.join(testTempDir, 'custom-logs');
|
||||
const logger = new OpenAILogger(customDir);
|
||||
await logger.initialize();
|
||||
|
||||
const request = {
|
||||
model: 'gpt-4',
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
};
|
||||
const response = { id: 'test-id', choices: [] };
|
||||
|
||||
const logPath = await logger.logInteraction(request, response);
|
||||
|
||||
expect(logPath).toContain(customDir);
|
||||
expect(logPath.startsWith(customDir)).toBe(true);
|
||||
});
|
||||
|
||||
it('should resolve relative path correctly', async () => {
|
||||
const relativeDir = 'relative-logs';
|
||||
const logger = new OpenAILogger(relativeDir);
|
||||
await logger.initialize();
|
||||
|
||||
const request = {
|
||||
model: 'gpt-4',
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
};
|
||||
const response = { id: 'test-id', choices: [] };
|
||||
|
||||
const logPath = await logger.logInteraction(request, response);
|
||||
const expectedDir = path.resolve(process.cwd(), relativeDir);
|
||||
createdDirs.push(expectedDir);
|
||||
|
||||
expect(logPath).toContain(expectedDir);
|
||||
});
|
||||
|
||||
it('should expand ~ correctly', async () => {
|
||||
const customDir = '~/test-openai-logs';
|
||||
const logger = new OpenAILogger(customDir);
|
||||
await logger.initialize();
|
||||
|
||||
const request = {
|
||||
model: 'gpt-4',
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
};
|
||||
const response = { id: 'test-id', choices: [] };
|
||||
|
||||
const logPath = await logger.logInteraction(request, response);
|
||||
const expectedDir = path.join(os.homedir(), 'test-openai-logs');
|
||||
createdDirs.push(expectedDir);
|
||||
|
||||
expect(logPath).toContain(expectedDir);
|
||||
});
|
||||
});
|
||||
|
||||
describe('getLogFiles', () => {
|
||||
it('should return empty array when directory does not exist', async () => {
|
||||
const logger = new OpenAILogger(testTempDir);
|
||||
const files = await logger.getLogFiles();
|
||||
expect(files).toEqual([]);
|
||||
});
|
||||
|
||||
it('should return log files after initialization', async () => {
|
||||
const logger = new OpenAILogger(testTempDir);
|
||||
await logger.initialize();
|
||||
|
||||
const request = {
|
||||
model: 'gpt-4',
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
};
|
||||
const response = { id: 'test-id', choices: [] };
|
||||
|
||||
await logger.logInteraction(request, response);
|
||||
const files = await logger.getLogFiles();
|
||||
|
||||
expect(files.length).toBeGreaterThan(0);
|
||||
expect(files[0]).toMatch(/openai-.*\.json$/);
|
||||
});
|
||||
|
||||
it('should return only log files matching pattern', async () => {
|
||||
const logger = new OpenAILogger(testTempDir);
|
||||
await logger.initialize();
|
||||
|
||||
// Create a log file
|
||||
await logger.logInteraction({ test: 'request' }, { test: 'response' });
|
||||
|
||||
// Create a non-log file
|
||||
await fs.writeFile(path.join(testTempDir, 'other-file.txt'), 'content');
|
||||
|
||||
const files = await logger.getLogFiles();
|
||||
expect(files.length).toBe(1);
|
||||
expect(files[0]).toMatch(/openai-.*\.json$/);
|
||||
});
|
||||
|
||||
it('should respect limit parameter', async () => {
|
||||
const logger = new OpenAILogger(testTempDir);
|
||||
await logger.initialize();
|
||||
|
||||
// Create multiple log files
|
||||
for (let i = 0; i < 5; i++) {
|
||||
await logger.logInteraction(
|
||||
{ test: `request-${i}` },
|
||||
{ test: `response-${i}` },
|
||||
);
|
||||
// Small delay to ensure different timestamps
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
|
||||
const allFiles = await logger.getLogFiles();
|
||||
expect(allFiles.length).toBe(5);
|
||||
|
||||
const limitedFiles = await logger.getLogFiles(3);
|
||||
expect(limitedFiles.length).toBe(3);
|
||||
});
|
||||
|
||||
it('should return files sorted by most recent first', async () => {
|
||||
const logger = new OpenAILogger(testTempDir);
|
||||
await logger.initialize();
|
||||
|
||||
const files: string[] = [];
|
||||
for (let i = 0; i < 3; i++) {
|
||||
const logPath = await logger.logInteraction(
|
||||
{ test: `request-${i}` },
|
||||
{ test: `response-${i}` },
|
||||
);
|
||||
files.push(logPath);
|
||||
await new Promise((resolve) => setTimeout(resolve, 10));
|
||||
}
|
||||
|
||||
const retrievedFiles = await logger.getLogFiles();
|
||||
expect(retrievedFiles[0]).toBe(files[2]); // Most recent first
|
||||
expect(retrievedFiles[1]).toBe(files[1]);
|
||||
expect(retrievedFiles[2]).toBe(files[0]);
|
||||
});
|
||||
});
|
||||
|
||||
describe('readLogFile', () => {
|
||||
it('should read and parse log file correctly', async () => {
|
||||
const logger = new OpenAILogger(testTempDir);
|
||||
await logger.initialize();
|
||||
|
||||
const request = {
|
||||
model: 'gpt-4',
|
||||
messages: [{ role: 'user', content: 'test' }],
|
||||
};
|
||||
const response = { id: 'test-id', choices: [] };
|
||||
|
||||
const logPath = await logger.logInteraction(request, response);
|
||||
const logData = await logger.readLogFile(logPath);
|
||||
|
||||
expect(logData).toHaveProperty('timestamp');
|
||||
expect(logData).toHaveProperty('request', request);
|
||||
expect(logData).toHaveProperty('response', response);
|
||||
});
|
||||
|
||||
it('should throw error when file does not exist', async () => {
|
||||
const logger = new OpenAILogger(testTempDir);
|
||||
const nonExistentPath = path.join(testTempDir, 'non-existent.json');
|
||||
|
||||
await expect(logger.readLogFile(nonExistentPath)).rejects.toThrow();
|
||||
});
|
||||
});
|
||||
|
||||
describe('path resolution', () => {
|
||||
it('should normalize absolute paths', () => {
|
||||
const absolutePath = '/tmp/test/logs';
|
||||
const logger = new OpenAILogger(absolutePath);
|
||||
expect(logger).toBeInstanceOf(OpenAILogger);
|
||||
});
|
||||
|
||||
it('should resolve relative paths based on current working directory', async () => {
|
||||
const relativePath = 'test-relative-logs';
|
||||
const logger = new OpenAILogger(relativePath);
|
||||
await logger.initialize();
|
||||
|
||||
const request = { test: 'request' };
|
||||
const response = { test: 'response' };
|
||||
|
||||
const logPath = await logger.logInteraction(request, response);
|
||||
const expectedBaseDir = path.resolve(process.cwd(), relativePath);
|
||||
createdDirs.push(expectedBaseDir);
|
||||
|
||||
expect(logPath).toContain(expectedBaseDir);
|
||||
});
|
||||
|
||||
it('should handle paths with special characters', async () => {
|
||||
const specialPath = path.join(testTempDir, 'logs-with-special-chars');
|
||||
const logger = new OpenAILogger(specialPath);
|
||||
await logger.initialize();
|
||||
|
||||
const request = { test: 'request' };
|
||||
const response = { test: 'response' };
|
||||
|
||||
const logPath = await logger.logInteraction(request, response);
|
||||
expect(logPath).toContain(specialPath);
|
||||
});
|
||||
});
|
||||
});
|
||||
@@ -18,10 +18,23 @@ export class OpenAILogger {
|
||||
|
||||
/**
|
||||
* Creates a new OpenAI logger
|
||||
* @param customLogDir Optional custom log directory path
|
||||
* @param customLogDir Optional custom log directory path (supports relative paths, absolute paths, and ~ expansion)
|
||||
*/
|
||||
constructor(customLogDir?: string) {
|
||||
this.logDir = customLogDir || path.join(process.cwd(), 'logs', 'openai');
|
||||
if (customLogDir) {
|
||||
// Resolve relative paths to absolute paths
|
||||
// Handle ~ expansion
|
||||
let resolvedPath = customLogDir;
|
||||
if (customLogDir === '~' || customLogDir.startsWith('~/')) {
|
||||
resolvedPath = path.join(os.homedir(), customLogDir.slice(1));
|
||||
} else if (!path.isAbsolute(customLogDir)) {
|
||||
// If it's a relative path, resolve it relative to current working directory
|
||||
resolvedPath = path.resolve(process.cwd(), customLogDir);
|
||||
}
|
||||
this.logDir = path.normalize(resolvedPath);
|
||||
} else {
|
||||
this.logDir = path.join(process.cwd(), 'logs', 'openai');
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code-test-utils",
|
||||
"version": "0.1.3",
|
||||
"version": "0.1.4",
|
||||
"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.1.3",
|
||||
"version": "0.1.4",
|
||||
"publisher": "qwenlm",
|
||||
"icon": "assets/icon.png",
|
||||
"repository": {
|
||||
|
||||
@@ -85,7 +85,7 @@ const distPackageJson = {
|
||||
bin: {
|
||||
qwen: 'cli.js',
|
||||
},
|
||||
files: ['cli.js', 'vendor', 'README.md', 'LICENSE'],
|
||||
files: ['cli.js', 'vendor', '*.sb', 'README.md', 'LICENSE'],
|
||||
config: rootPackageJson.config,
|
||||
dependencies: runtimeDependencies,
|
||||
optionalDependencies: {
|
||||
|
||||
@@ -69,7 +69,14 @@ if (process.env.DEBUG) {
|
||||
// than the relaunched process making it harder to debug.
|
||||
env.GEMINI_CLI_NO_RELAUNCH = 'true';
|
||||
}
|
||||
const child = spawn('node', nodeArgs, { stdio: 'inherit', env });
|
||||
// Use process.cwd() to inherit the working directory from launch.json cwd setting
|
||||
// This allows debugging from a specific directory (e.g., .todo)
|
||||
const workingDir = process.env.QWEN_WORKING_DIR || process.cwd();
|
||||
const child = spawn('node', nodeArgs, {
|
||||
stdio: 'inherit',
|
||||
env,
|
||||
cwd: workingDir,
|
||||
});
|
||||
|
||||
child.on('close', (code) => {
|
||||
process.exit(code);
|
||||
|
||||
Reference in New Issue
Block a user