mirror of
https://github.com/QwenLM/qwen-code.git
synced 2025-12-27 03:59:15 +00:00
Compare commits
14 Commits
fix-slash-
...
v0.1.0-pre
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a52e043c00 | ||
|
|
bf91aa4e08 | ||
|
|
5835db0877 | ||
|
|
9b86e821bc | ||
|
|
d482ad28dd | ||
|
|
570b745f30 | ||
|
|
10f7968bc6 | ||
|
|
c7f5aaa702 | ||
|
|
5e65d6cae7 | ||
|
|
1dd21537ce | ||
|
|
23ecddc574 | ||
|
|
b9dfff6e83 | ||
|
|
0fa9b88a14 | ||
|
|
ab28db1da8 |
6
.github/workflows/release.yml
vendored
6
.github/workflows/release.yml
vendored
@@ -167,11 +167,7 @@ jobs:
|
||||
RELEASE_TAG: '${{ steps.version.outputs.RELEASE_TAG }}'
|
||||
run: |-
|
||||
git add package.json package-lock.json packages/*/package.json
|
||||
if git diff --staged --quiet; then
|
||||
echo "No version changes to commit"
|
||||
else
|
||||
git commit -m "chore(release): ${RELEASE_TAG}"
|
||||
fi
|
||||
git commit -m "chore(release): ${RELEASE_TAG}"
|
||||
if [[ "${IS_DRY_RUN}" == "false" ]]; then
|
||||
echo "Pushing release branch to remote..."
|
||||
git push --set-upstream origin "${BRANCH_NAME}" --follow-tags
|
||||
|
||||
@@ -66,6 +66,17 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
- **Description:** Display all directories added by `/directory add` and `--include-directories`.
|
||||
- **Usage:** `/directory show`
|
||||
|
||||
- **`/directory`** (or **`/dir`**)
|
||||
- **Description:** Manage workspace directories for multi-directory support.
|
||||
- **Sub-commands:**
|
||||
- **`add`**:
|
||||
- **Description:** Add a directory to the workspace. The path can be absolute or relative to the current working directory. Moreover, the reference from home directory is supported as well.
|
||||
- **Usage:** `/directory add <path1>,<path2>`
|
||||
- **Note:** Disabled in restrictive sandbox profiles. If you're using that, use `--include-directories` when starting the session instead.
|
||||
- **`show`**:
|
||||
- **Description:** Display all directories added by `/directory add` and `--include-directories`.
|
||||
- **Usage:** `/directory show`
|
||||
|
||||
- **`/editor`**
|
||||
- **Description:** Open a dialog for selecting supported editors.
|
||||
|
||||
@@ -97,20 +108,6 @@ Slash commands provide meta-level control over the CLI itself.
|
||||
- **Description:** Reload the hierarchical instructional memory from all context files (default: `QWEN.md`) found in the configured locations (global, project/ancestors, and sub-directories). This updates the model with the latest context content.
|
||||
- **Note:** For more details on how context files contribute to hierarchical memory, see the [CLI Configuration documentation](./configuration.md#context-files-hierarchical-instructional-context).
|
||||
|
||||
- **`/model`**
|
||||
- **Description:** Switch the model for the current session. Opens a dialog to select from available models based on your authentication type.
|
||||
- **Usage:** `/model`
|
||||
- **Features:**
|
||||
- Shows a dialog with all available models for your current authentication type
|
||||
- Displays model descriptions and capabilities (e.g., vision support)
|
||||
- Changes the model for the current session only
|
||||
- Supports both Qwen models (via OAuth) and OpenAI models (via API key)
|
||||
- **Available Models:**
|
||||
- **Qwen Coder:** The latest Qwen Coder model from Alibaba Cloud ModelStudio (version: qwen3-coder-plus-2025-09-23)
|
||||
- **Qwen Vision:** The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23) - supports image analysis
|
||||
- **OpenAI Models:** Available when using OpenAI authentication (configured via `OPENAI_MODEL` environment variable)
|
||||
- **Note:** Model selection is session-specific and does not persist across different Qwen Code sessions. To set a default model, use the `model.name` setting in your configuration.
|
||||
|
||||
- **`/restore`**
|
||||
- **Description:** Restores the project files to the state they were in just before a tool was executed. This is particularly useful for undoing file edits made by a tool. If run without a tool call ID, it will list available checkpoints to restore from.
|
||||
- **Usage:** `/restore [tool_call_id]`
|
||||
|
||||
@@ -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.
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@ export default {
|
||||
subagents: 'Subagents',
|
||||
checkpointing: 'Checkpointing',
|
||||
sandbox: 'Sandbox Support',
|
||||
headless: 'Headless Mode',
|
||||
'headless-mode': 'Headless Mode',
|
||||
'welcome-back': 'Welcome Back',
|
||||
'token-caching': 'Token Caching',
|
||||
};
|
||||
|
||||
@@ -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',
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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
12
package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.0-preview.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.0-preview.0",
|
||||
"workspaces": [
|
||||
"packages/*"
|
||||
],
|
||||
@@ -16024,7 +16024,7 @@
|
||||
},
|
||||
"packages/cli": {
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.0-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.0-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.0-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.0-preview.0",
|
||||
"license": "LICENSE",
|
||||
"dependencies": {
|
||||
"@modelcontextprotocol/sdk": "^1.15.1",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.0-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.0-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",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.0-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.0-preview.0"
|
||||
},
|
||||
"dependencies": {
|
||||
"@google/genai": "1.16.0",
|
||||
|
||||
@@ -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', () => {
|
||||
|
||||
@@ -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;
|
||||
};
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
||||
@@ -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);
|
||||
|
||||
|
||||
@@ -555,7 +555,7 @@ export const AppContainer = (props: AppContainerProps) => {
|
||||
historyManager.addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
text: 'Refreshing hierarchical memory (QWEN.md or other context files)...',
|
||||
text: 'Refreshing hierarchical memory (GEMINI.md or other context files)...',
|
||||
},
|
||||
Date.now(),
|
||||
);
|
||||
|
||||
@@ -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 = () => {
|
||||
|
||||
@@ -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);
|
||||
|
||||
@@ -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);
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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}
|
||||
`;
|
||||
|
||||
@@ -139,8 +139,8 @@ describe('chatCommand', () => {
|
||||
.match(/(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/);
|
||||
const formattedDate = isoDate ? `${isoDate[1]} ${isoDate[2]}` : '';
|
||||
expect(content).toContain(formattedDate);
|
||||
const index1 = content.indexOf('- test1');
|
||||
const index2 = content.indexOf('- test2');
|
||||
const index1 = content.indexOf('- \u001b[36mtest1\u001b[0m');
|
||||
const index2 = content.indexOf('- \u001b[36mtest2\u001b[0m');
|
||||
expect(index1).toBeGreaterThanOrEqual(0);
|
||||
expect(index2).toBeGreaterThan(index1);
|
||||
});
|
||||
|
||||
@@ -89,9 +89,9 @@ const listCommand: SlashCommand = {
|
||||
const isoString = chat.mtime.toISOString();
|
||||
const match = isoString.match(/(\d{4}-\d{2}-\d{2})T(\d{2}:\d{2}:\d{2})/);
|
||||
const formattedDate = match ? `${match[1]} ${match[2]}` : 'Invalid Date';
|
||||
message += ` - ${paddedName} (saved on ${formattedDate})\n`;
|
||||
message += ` - \u001b[36m${paddedName}\u001b[0m \u001b[90m(saved on ${formattedDate})\u001b[0m\n`;
|
||||
}
|
||||
message += `\nNote: Newest last, oldest first`;
|
||||
message += `\n\u001b[90mNote: Newest last, oldest first\u001b[0m`;
|
||||
return {
|
||||
type: 'message',
|
||||
messageType: 'info',
|
||||
|
||||
@@ -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('')
|
||||
|
||||
@@ -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');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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,
|
||||
);
|
||||
|
||||
|
||||
@@ -45,7 +45,7 @@ export const useEditorSettings = (
|
||||
}
|
||||
|
||||
try {
|
||||
loadedSettings.setValue(scope, 'general.preferredEditor', editorType);
|
||||
loadedSettings.setValue(scope, 'preferredEditor', editorType);
|
||||
addItem(
|
||||
{
|
||||
type: MessageType.INFO,
|
||||
|
||||
@@ -20,14 +20,12 @@ export const AVAILABLE_MODELS_QWEN: AvailableModel[] = [
|
||||
{
|
||||
id: MAINLINE_CODER,
|
||||
label: MAINLINE_CODER,
|
||||
description:
|
||||
'The latest Qwen Coder model from Alibaba Cloud ModelStudio (version: qwen3-coder-plus-2025-09-23)',
|
||||
description: 'Optimized for code generation and understanding',
|
||||
},
|
||||
{
|
||||
id: MAINLINE_VLM,
|
||||
label: MAINLINE_VLM,
|
||||
description:
|
||||
'The latest Qwen Vision model from Alibaba Cloud ModelStudio (version: qwen3-vl-plus-2025-09-23)',
|
||||
description: 'Vision model with multimodal capabilities',
|
||||
isVision: true,
|
||||
},
|
||||
];
|
||||
|
||||
@@ -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,
|
||||
|
||||
@@ -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;
|
||||
}
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code-core",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.0-preview.0",
|
||||
"description": "Qwen Code Core",
|
||||
"repository": {
|
||||
"type": "git",
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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();
|
||||
});
|
||||
});
|
||||
|
||||
@@ -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(
|
||||
|
||||
@@ -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';
|
||||
|
||||
@@ -7,7 +7,6 @@
|
||||
import { describe, it, expect, beforeEach } from 'vitest';
|
||||
import { OpenAIContentConverter } from './converter.js';
|
||||
import type { StreamingToolCallParser } from './streamingToolCallParser.js';
|
||||
import type { GenerateContentParameters, Content } from '@google/genai';
|
||||
|
||||
describe('OpenAIContentConverter', () => {
|
||||
let converter: OpenAIContentConverter;
|
||||
@@ -69,77 +68,4 @@ describe('OpenAIContentConverter', () => {
|
||||
expect(parser.getBuffer(0)).toBe('');
|
||||
});
|
||||
});
|
||||
|
||||
describe('convertGeminiRequestToOpenAI', () => {
|
||||
const createRequestWithFunctionResponse = (
|
||||
response: Record<string, unknown>,
|
||||
): GenerateContentParameters => {
|
||||
const contents: Content[] = [
|
||||
{
|
||||
role: 'model',
|
||||
parts: [
|
||||
{
|
||||
functionCall: {
|
||||
id: 'call_1',
|
||||
name: 'shell',
|
||||
args: {},
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
{
|
||||
role: 'user',
|
||||
parts: [
|
||||
{
|
||||
functionResponse: {
|
||||
id: 'call_1',
|
||||
name: 'shell',
|
||||
response,
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
];
|
||||
return {
|
||||
model: 'models/test',
|
||||
contents,
|
||||
};
|
||||
};
|
||||
|
||||
it('should extract raw output from function response objects', () => {
|
||||
const request = createRequestWithFunctionResponse({
|
||||
output: 'Raw output text',
|
||||
});
|
||||
|
||||
const messages = converter.convertGeminiRequestToOpenAI(request);
|
||||
const toolMessage = messages.find((message) => message.role === 'tool');
|
||||
|
||||
expect(toolMessage).toBeDefined();
|
||||
expect(toolMessage?.content).toBe('Raw output text');
|
||||
});
|
||||
|
||||
it('should prioritize error field when present', () => {
|
||||
const request = createRequestWithFunctionResponse({
|
||||
error: 'Command failed',
|
||||
});
|
||||
|
||||
const messages = converter.convertGeminiRequestToOpenAI(request);
|
||||
const toolMessage = messages.find((message) => message.role === 'tool');
|
||||
|
||||
expect(toolMessage).toBeDefined();
|
||||
expect(toolMessage?.content).toBe('Command failed');
|
||||
});
|
||||
|
||||
it('should stringify non-string responses', () => {
|
||||
const request = createRequestWithFunctionResponse({
|
||||
data: { value: 42 },
|
||||
});
|
||||
|
||||
const messages = converter.convertGeminiRequestToOpenAI(request);
|
||||
const toolMessage = messages.find((message) => message.role === 'tool');
|
||||
|
||||
expect(toolMessage).toBeDefined();
|
||||
expect(toolMessage?.content).toBe('{"data":{"value":42}}');
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
@@ -276,7 +276,10 @@ export class OpenAIContentConverter {
|
||||
messages.push({
|
||||
role: 'tool' as const,
|
||||
tool_call_id: funcResponse.id || '',
|
||||
content: this.extractFunctionResponseContent(funcResponse.response),
|
||||
content:
|
||||
typeof funcResponse.response === 'string'
|
||||
? funcResponse.response
|
||||
: JSON.stringify(funcResponse.response),
|
||||
});
|
||||
}
|
||||
return;
|
||||
@@ -356,36 +359,6 @@ export class OpenAIContentConverter {
|
||||
return { textParts, functionCalls, functionResponses, mediaParts };
|
||||
}
|
||||
|
||||
private extractFunctionResponseContent(response: unknown): string {
|
||||
if (response === null || response === undefined) {
|
||||
return '';
|
||||
}
|
||||
|
||||
if (typeof response === 'string') {
|
||||
return response;
|
||||
}
|
||||
|
||||
if (typeof response === 'object') {
|
||||
const responseObject = response as Record<string, unknown>;
|
||||
const output = responseObject['output'];
|
||||
if (typeof output === 'string') {
|
||||
return output;
|
||||
}
|
||||
|
||||
const error = responseObject['error'];
|
||||
if (typeof error === 'string') {
|
||||
return error;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
const serialized = JSON.stringify(response);
|
||||
return serialized ?? String(response);
|
||||
} catch {
|
||||
return String(response);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Determine media type from MIME type
|
||||
*/
|
||||
|
||||
@@ -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;
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -131,14 +131,16 @@ describe('ExitPlanModeTool', () => {
|
||||
}
|
||||
|
||||
const result = await invocation.execute(signal);
|
||||
const expectedLlmMessage =
|
||||
'User has approved your plan. You can now start coding. Start with updating your todo list if applicable.';
|
||||
|
||||
expect(result.llmContent).toContain(
|
||||
'User has approved your plan. You can now start coding',
|
||||
);
|
||||
expect(result.returnDisplay).toEqual({
|
||||
type: 'plan_summary',
|
||||
message: 'User approved the plan.',
|
||||
plan: params.plan,
|
||||
expect(result).toEqual({
|
||||
llmContent: expectedLlmMessage,
|
||||
returnDisplay: {
|
||||
type: 'plan_summary',
|
||||
message: 'User approved the plan.',
|
||||
plan: params.plan,
|
||||
},
|
||||
});
|
||||
|
||||
expect(mockConfig.setApprovalMode).toHaveBeenCalledWith(
|
||||
@@ -186,12 +188,15 @@ describe('ExitPlanModeTool', () => {
|
||||
|
||||
const result = await invocation.execute(signal);
|
||||
|
||||
expect(result.llmContent).toBe(
|
||||
'Plan execution was not approved. Remaining in plan mode.',
|
||||
);
|
||||
expect(result.returnDisplay).toBe(
|
||||
'Plan execution was not approved. Remaining in plan mode.',
|
||||
);
|
||||
expect(result).toEqual({
|
||||
llmContent: JSON.stringify({
|
||||
success: false,
|
||||
plan: params.plan,
|
||||
error: 'Plan execution was not approved. Remaining in plan mode.',
|
||||
}),
|
||||
returnDisplay:
|
||||
'Plan execution was not approved. Remaining in plan mode.',
|
||||
});
|
||||
|
||||
expect(mockConfig.setApprovalMode).toHaveBeenCalledWith(
|
||||
ApprovalMode.PLAN,
|
||||
@@ -210,6 +215,50 @@ describe('ExitPlanModeTool', () => {
|
||||
);
|
||||
});
|
||||
|
||||
it('should handle execution errors gracefully', async () => {
|
||||
const params: ExitPlanModeParams = {
|
||||
plan: 'Test plan',
|
||||
};
|
||||
|
||||
const invocation = tool.build(params);
|
||||
const confirmation = await invocation.shouldConfirmExecute(
|
||||
new AbortController().signal,
|
||||
);
|
||||
if (confirmation) {
|
||||
// Don't approve the plan so we go through the rejection path
|
||||
await confirmation.onConfirm(ToolConfirmationOutcome.Cancel);
|
||||
}
|
||||
|
||||
// Create a spy to simulate an error during the execution
|
||||
const consoleSpy = vi
|
||||
.spyOn(console, 'error')
|
||||
.mockImplementation(() => {});
|
||||
|
||||
// Mock JSON.stringify to throw an error in the rejection path
|
||||
const originalStringify = JSON.stringify;
|
||||
vi.spyOn(JSON, 'stringify').mockImplementationOnce(() => {
|
||||
throw new Error('JSON stringify error');
|
||||
});
|
||||
|
||||
const result = await invocation.execute(new AbortController().signal);
|
||||
|
||||
expect(result).toEqual({
|
||||
llmContent: JSON.stringify({
|
||||
success: false,
|
||||
error: 'Failed to present plan. Detail: JSON stringify error',
|
||||
}),
|
||||
returnDisplay: 'Error presenting plan: JSON stringify error',
|
||||
});
|
||||
|
||||
expect(consoleSpy).toHaveBeenCalledWith(
|
||||
'[ExitPlanModeTool] Error executing exit_plan_mode: JSON stringify error',
|
||||
);
|
||||
|
||||
// Restore original JSON.stringify
|
||||
JSON.stringify = originalStringify;
|
||||
consoleSpy.mockRestore();
|
||||
});
|
||||
|
||||
it('should return empty tool locations', () => {
|
||||
const params: ExitPlanModeParams = {
|
||||
plan: 'Test plan',
|
||||
|
||||
@@ -115,12 +115,17 @@ class ExitPlanModeToolInvocation extends BaseToolInvocation<
|
||||
const rejectionMessage =
|
||||
'Plan execution was not approved. Remaining in plan mode.';
|
||||
return {
|
||||
llmContent: rejectionMessage,
|
||||
llmContent: JSON.stringify({
|
||||
success: false,
|
||||
plan,
|
||||
error: rejectionMessage,
|
||||
}),
|
||||
returnDisplay: rejectionMessage,
|
||||
};
|
||||
}
|
||||
|
||||
const llmMessage = `User has approved your plan. You can now start coding. Start with updating your todo list if applicable.`;
|
||||
const llmMessage =
|
||||
'User has approved your plan. You can now start coding. Start with updating your todo list if applicable.';
|
||||
const displayMessage = 'User approved the plan.';
|
||||
|
||||
return {
|
||||
@@ -137,11 +142,11 @@ class ExitPlanModeToolInvocation extends BaseToolInvocation<
|
||||
console.error(
|
||||
`[ExitPlanModeTool] Error executing exit_plan_mode: ${errorMessage}`,
|
||||
);
|
||||
|
||||
const errorLlmContent = `Failed to present plan: ${errorMessage}`;
|
||||
|
||||
return {
|
||||
llmContent: errorLlmContent,
|
||||
llmContent: JSON.stringify({
|
||||
success: false,
|
||||
error: `Failed to present plan. Detail: ${errorMessage}`,
|
||||
}),
|
||||
returnDisplay: `Error presenting plan: ${errorMessage}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -241,7 +241,9 @@ describe('MemoryTool', () => {
|
||||
expectedFsArgument,
|
||||
);
|
||||
const successMessage = `Okay, I've remembered that in global memory: "${params.fact}"`;
|
||||
expect(result.llmContent).toBe(successMessage);
|
||||
expect(result.llmContent).toBe(
|
||||
JSON.stringify({ success: true, message: successMessage }),
|
||||
);
|
||||
expect(result.returnDisplay).toBe(successMessage);
|
||||
});
|
||||
|
||||
@@ -269,7 +271,9 @@ describe('MemoryTool', () => {
|
||||
expectedFsArgument,
|
||||
);
|
||||
const successMessage = `Okay, I've remembered that in project memory: "${params.fact}"`;
|
||||
expect(result.llmContent).toBe(successMessage);
|
||||
expect(result.llmContent).toBe(
|
||||
JSON.stringify({ success: true, message: successMessage }),
|
||||
);
|
||||
expect(result.returnDisplay).toBe(successMessage);
|
||||
});
|
||||
|
||||
@@ -294,7 +298,10 @@ describe('MemoryTool', () => {
|
||||
const result = await invocation.execute(mockAbortSignal);
|
||||
|
||||
expect(result.llmContent).toBe(
|
||||
`Error saving memory: ${underlyingError.message}`,
|
||||
JSON.stringify({
|
||||
success: false,
|
||||
error: `Failed to save memory. Detail: ${underlyingError.message}`,
|
||||
}),
|
||||
);
|
||||
expect(result.returnDisplay).toBe(
|
||||
`Error saving memory: ${underlyingError.message}`,
|
||||
@@ -312,8 +319,6 @@ describe('MemoryTool', () => {
|
||||
expect(result.llmContent).toContain(
|
||||
'Please specify where to save this memory',
|
||||
);
|
||||
expect(result.llmContent).toContain('Global:');
|
||||
expect(result.llmContent).toContain('Project:');
|
||||
expect(result.returnDisplay).toContain('Global:');
|
||||
expect(result.returnDisplay).toContain('Project:');
|
||||
});
|
||||
|
||||
@@ -309,7 +309,7 @@ Preview of changes to be made to GLOBAL memory:
|
||||
if (!fact || typeof fact !== 'string' || fact.trim() === '') {
|
||||
const errorMessage = 'Parameter "fact" must be a non-empty string.';
|
||||
return {
|
||||
llmContent: `Error: ${errorMessage}`,
|
||||
llmContent: JSON.stringify({ success: false, error: errorMessage }),
|
||||
returnDisplay: `Error: ${errorMessage}`,
|
||||
};
|
||||
}
|
||||
@@ -324,7 +324,10 @@ Global: ${globalPath} (shared across all projects)
|
||||
Project: ${projectPath} (current project only)`;
|
||||
|
||||
return {
|
||||
llmContent: errorMessage,
|
||||
llmContent: JSON.stringify({
|
||||
success: false,
|
||||
error: 'Please specify where to save this memory',
|
||||
}),
|
||||
returnDisplay: errorMessage,
|
||||
};
|
||||
}
|
||||
@@ -341,7 +344,10 @@ Project: ${projectPath} (current project only)`;
|
||||
await fs.writeFile(memoryFilePath, modified_content, 'utf-8');
|
||||
const successMessage = `Okay, I've updated the ${scope} memory file with your modifications.`;
|
||||
return {
|
||||
llmContent: successMessage,
|
||||
llmContent: JSON.stringify({
|
||||
success: true,
|
||||
message: successMessage,
|
||||
}),
|
||||
returnDisplay: successMessage,
|
||||
};
|
||||
} else {
|
||||
@@ -353,7 +359,10 @@ Project: ${projectPath} (current project only)`;
|
||||
});
|
||||
const successMessage = `Okay, I've remembered that in ${scope} memory: "${fact}"`;
|
||||
return {
|
||||
llmContent: successMessage,
|
||||
llmContent: JSON.stringify({
|
||||
success: true,
|
||||
message: successMessage,
|
||||
}),
|
||||
returnDisplay: successMessage,
|
||||
};
|
||||
}
|
||||
@@ -363,9 +372,11 @@ Project: ${projectPath} (current project only)`;
|
||||
console.error(
|
||||
`[MemoryTool] Error executing save_memory for fact "${fact}" in ${scope}: ${errorMessage}`,
|
||||
);
|
||||
|
||||
return {
|
||||
llmContent: `Error saving memory: ${errorMessage}`,
|
||||
llmContent: JSON.stringify({
|
||||
success: false,
|
||||
error: `Failed to save memory. Detail: ${errorMessage}`,
|
||||
}),
|
||||
returnDisplay: `Error saving memory: ${errorMessage}`,
|
||||
error: {
|
||||
message: errorMessage,
|
||||
|
||||
@@ -141,12 +141,7 @@ describe('TodoWriteTool', () => {
|
||||
const invocation = tool.build(params);
|
||||
const result = await invocation.execute(mockAbortSignal);
|
||||
|
||||
expect(result.llmContent).toContain(
|
||||
'Todos have been modified successfully',
|
||||
);
|
||||
expect(result.llmContent).toContain('<system-reminder>');
|
||||
expect(result.llmContent).toContain('Your todo list has changed');
|
||||
expect(result.llmContent).toContain(JSON.stringify(params.todos));
|
||||
expect(result.llmContent).toContain('success');
|
||||
expect(result.returnDisplay).toEqual({
|
||||
type: 'todo_list',
|
||||
todos: [
|
||||
@@ -183,12 +178,7 @@ describe('TodoWriteTool', () => {
|
||||
const invocation = tool.build(params);
|
||||
const result = await invocation.execute(mockAbortSignal);
|
||||
|
||||
expect(result.llmContent).toContain(
|
||||
'Todos have been modified successfully',
|
||||
);
|
||||
expect(result.llmContent).toContain('<system-reminder>');
|
||||
expect(result.llmContent).toContain('Your todo list has changed');
|
||||
expect(result.llmContent).toContain(JSON.stringify(params.todos));
|
||||
expect(result.llmContent).toContain('success');
|
||||
expect(result.returnDisplay).toEqual({
|
||||
type: 'todo_list',
|
||||
todos: [
|
||||
@@ -218,10 +208,7 @@ describe('TodoWriteTool', () => {
|
||||
const invocation = tool.build(params);
|
||||
const result = await invocation.execute(mockAbortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('Failed to modify todos');
|
||||
expect(result.llmContent).toContain('<system-reminder>');
|
||||
expect(result.llmContent).toContain('Todo list modification failed');
|
||||
expect(result.llmContent).toContain('Write failed');
|
||||
expect(result.llmContent).toContain('"success":false');
|
||||
expect(result.returnDisplay).toContain('Error writing todos');
|
||||
});
|
||||
|
||||
@@ -236,10 +223,7 @@ describe('TodoWriteTool', () => {
|
||||
const invocation = tool.build(params);
|
||||
const result = await invocation.execute(mockAbortSignal);
|
||||
|
||||
expect(result.llmContent).toContain('Todo list has been cleared');
|
||||
expect(result.llmContent).toContain('<system-reminder>');
|
||||
expect(result.llmContent).toContain('Your todo list is now empty');
|
||||
expect(result.llmContent).toContain('no pending tasks');
|
||||
expect(result.llmContent).toContain('success');
|
||||
expect(result.returnDisplay).toEqual({
|
||||
type: 'todo_list',
|
||||
todos: [],
|
||||
|
||||
@@ -340,30 +340,11 @@ class TodoWriteToolInvocation extends BaseToolInvocation<
|
||||
todos: finalTodos,
|
||||
};
|
||||
|
||||
// Create plain string format with system reminder
|
||||
const todosJson = JSON.stringify(finalTodos);
|
||||
let llmContent: string;
|
||||
|
||||
if (finalTodos.length === 0) {
|
||||
// Special message for empty todos
|
||||
llmContent = `Todo list has been cleared.
|
||||
|
||||
<system-reminder>
|
||||
Your todo list is now empty. DO NOT mention this explicitly to the user. You have no pending tasks in your todo list.
|
||||
</system-reminder>`;
|
||||
} else {
|
||||
// Normal message for todos with items
|
||||
llmContent = `Todos have been modified successfully. Ensure that you continue to use the todo list to track your progress. Please proceed with the current tasks if applicable
|
||||
|
||||
<system-reminder>
|
||||
Your todo list has changed. DO NOT mention this explicitly to the user. Here are the latest contents of your todo list:
|
||||
|
||||
${todosJson}. Continue on with the tasks at hand if applicable.
|
||||
</system-reminder>`;
|
||||
}
|
||||
|
||||
return {
|
||||
llmContent,
|
||||
llmContent: JSON.stringify({
|
||||
success: true,
|
||||
todos: finalTodos,
|
||||
}),
|
||||
returnDisplay: todoResultDisplay,
|
||||
};
|
||||
} catch (error) {
|
||||
@@ -372,16 +353,11 @@ ${todosJson}. Continue on with the tasks at hand if applicable.
|
||||
console.error(
|
||||
`[TodoWriteTool] Error executing todo_write: ${errorMessage}`,
|
||||
);
|
||||
|
||||
// Create plain string format for error with system reminder
|
||||
const errorLlmContent = `Failed to modify todos. An error occurred during the operation.
|
||||
|
||||
<system-reminder>
|
||||
Todo list modification failed with error: ${errorMessage}. You may need to retry or handle this error appropriately.
|
||||
</system-reminder>`;
|
||||
|
||||
return {
|
||||
llmContent: errorLlmContent,
|
||||
llmContent: JSON.stringify({
|
||||
success: false,
|
||||
error: `Failed to write todos. Detail: ${errorMessage}`,
|
||||
}),
|
||||
returnDisplay: `Error writing todos: ${errorMessage}`,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -339,7 +339,6 @@ describe('editor utils', () => {
|
||||
diffCommand.args,
|
||||
{
|
||||
stdio: 'inherit',
|
||||
shell: process.platform === 'win32',
|
||||
},
|
||||
);
|
||||
expect(mockSpawnOn).toHaveBeenCalledWith('close', expect.any(Function));
|
||||
|
||||
@@ -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) => {
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@qwen-code/qwen-code-test-utils",
|
||||
"version": "0.1.2",
|
||||
"version": "0.1.0-preview.0",
|
||||
"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.2",
|
||||
"version": "0.1.0-preview.0",
|
||||
"publisher": "qwenlm",
|
||||
"icon": "assets/icon.png",
|
||||
"repository": {
|
||||
|
||||
Reference in New Issue
Block a user