mirror of
https://github.com/QwenLM/qwen-code.git
synced 2026-04-30 04:30:48 +00:00
Merge branch 'main' into feat/image-attachment
This commit is contained in:
commit
c92e2b8351
301 changed files with 33924 additions and 5940 deletions
|
|
@ -168,7 +168,7 @@ describe('validateAuthMethod', () => {
|
|||
expect(validateAuthMethod(AuthType.USE_VERTEX_AI)).toBeNull();
|
||||
});
|
||||
|
||||
it('should use config.modelsConfig.getModel() when Config is provided', () => {
|
||||
it('should use config.getModelsConfig().getModel() when Config is provided', () => {
|
||||
// Settings has a different model
|
||||
vi.mocked(settings.loadSettings).mockReturnValue({
|
||||
merged: {
|
||||
|
|
@ -184,18 +184,18 @@ describe('validateAuthMethod', () => {
|
|||
|
||||
// Mock Config object that returns a different model (e.g., from CLI args)
|
||||
const mockConfig = {
|
||||
modelsConfig: {
|
||||
getModelsConfig: vi.fn().mockReturnValue({
|
||||
getModel: vi.fn().mockReturnValue('cli-model'),
|
||||
},
|
||||
}),
|
||||
} as unknown as import('@qwen-code/qwen-code-core').Config;
|
||||
|
||||
// Set the env key for the CLI model, not the settings model
|
||||
process.env['CLI_API_KEY'] = 'cli-key';
|
||||
|
||||
// Should use 'cli-model' from config.modelsConfig.getModel(), not 'settings-model'
|
||||
// Should use 'cli-model' from config.getModelsConfig().getModel(), not 'settings-model'
|
||||
const result = validateAuthMethod(AuthType.USE_OPENAI, mockConfig);
|
||||
expect(result).toBeNull();
|
||||
expect(mockConfig.modelsConfig.getModel).toHaveBeenCalled();
|
||||
expect(mockConfig.getModelsConfig).toHaveBeenCalled();
|
||||
});
|
||||
|
||||
it('should fail validation when Config provides different model without matching env key', () => {
|
||||
|
|
@ -217,9 +217,9 @@ describe('validateAuthMethod', () => {
|
|||
} as unknown as ReturnType<typeof settings.loadSettings>);
|
||||
|
||||
const mockConfig = {
|
||||
modelsConfig: {
|
||||
getModelsConfig: vi.fn().mockReturnValue({
|
||||
getModel: vi.fn().mockReturnValue('cli-model'),
|
||||
},
|
||||
}),
|
||||
} as unknown as import('@qwen-code/qwen-code-core').Config;
|
||||
|
||||
// Don't set CLI_API_KEY - validation should fail
|
||||
|
|
|
|||
|
|
@ -60,9 +60,9 @@ function hasApiKeyForAuth(
|
|||
| ModelProvidersConfig
|
||||
| undefined;
|
||||
|
||||
// Use config.modelsConfig.getModel() if available for accurate model ID resolution
|
||||
// Use config.getModelsConfig().getModel() if available for accurate model ID resolution
|
||||
// that accounts for CLI args, env vars, and settings. Fall back to settings.model.name.
|
||||
const modelId = config?.modelsConfig.getModel() ?? settings.model?.name;
|
||||
const modelId = config?.getModelsConfig().getModel() ?? settings.model?.name;
|
||||
|
||||
// Try to find model-specific envKey from modelProviders
|
||||
const modelConfig = findModelConfig(modelProviders, authType, modelId);
|
||||
|
|
@ -184,9 +184,9 @@ export function validateAuthMethod(
|
|||
const modelProviders = settings.merged.modelProviders as
|
||||
| ModelProvidersConfig
|
||||
| undefined;
|
||||
// Use config.modelsConfig.getModel() if available for accurate model ID
|
||||
// Use config.getModelsConfig().getModel() if available for accurate model ID
|
||||
const modelId =
|
||||
config?.modelsConfig.getModel() ?? settings.merged.model?.name;
|
||||
config?.getModelsConfig().getModel() ?? settings.merged.model?.name;
|
||||
const modelConfig = findModelConfig(modelProviders, authMethod, modelId);
|
||||
|
||||
if (modelConfig && !modelConfig.baseUrl) {
|
||||
|
|
|
|||
|
|
@ -13,18 +13,48 @@ import {
|
|||
WriteFileTool,
|
||||
DEFAULT_QWEN_MODEL,
|
||||
OutputFormat,
|
||||
NativeLspService,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import { loadCliConfig, parseArguments, type CliArgs } from './config.js';
|
||||
import type { Settings } from './settings.js';
|
||||
import * as ServerConfig from '@qwen-code/qwen-code-core';
|
||||
import { isWorkspaceTrusted } from './trustedFolders.js';
|
||||
|
||||
const createNativeLspServiceInstance = () => ({
|
||||
discoverAndPrepare: vi.fn(),
|
||||
start: vi.fn(),
|
||||
definitions: vi.fn().mockResolvedValue([]),
|
||||
references: vi.fn().mockResolvedValue([]),
|
||||
workspaceSymbols: vi.fn().mockResolvedValue([]),
|
||||
hover: vi.fn().mockResolvedValue(null),
|
||||
documentSymbols: vi.fn().mockResolvedValue([]),
|
||||
implementations: vi.fn().mockResolvedValue([]),
|
||||
prepareCallHierarchy: vi.fn().mockResolvedValue([]),
|
||||
incomingCalls: vi.fn().mockResolvedValue([]),
|
||||
outgoingCalls: vi.fn().mockResolvedValue([]),
|
||||
diagnostics: vi.fn().mockResolvedValue([]),
|
||||
workspaceDiagnostics: vi.fn().mockResolvedValue([]),
|
||||
codeActions: vi.fn().mockResolvedValue([]),
|
||||
applyWorkspaceEdit: vi.fn().mockResolvedValue(false),
|
||||
});
|
||||
|
||||
vi.mock('./trustedFolders.js', () => ({
|
||||
isWorkspaceTrusted: vi
|
||||
.fn()
|
||||
.mockReturnValue({ isTrusted: true, source: 'file' }), // Default to trusted
|
||||
}));
|
||||
|
||||
const nativeLspServiceMock = vi.mocked(NativeLspService);
|
||||
const getLastLspInstance = () => {
|
||||
const results = nativeLspServiceMock.mock.results;
|
||||
if (results.length === 0) {
|
||||
return undefined;
|
||||
}
|
||||
return results[results.length - 1]?.value as ReturnType<
|
||||
typeof createNativeLspServiceInstance
|
||||
>;
|
||||
};
|
||||
|
||||
vi.mock('fs', async (importOriginal) => {
|
||||
const actualFs = await importOriginal<typeof import('fs')>();
|
||||
const pathMod = await import('node:path');
|
||||
|
|
@ -79,6 +109,9 @@ vi.mock('@qwen-code/qwen-code-core', async (importOriginal) => {
|
|||
const actualServer = await importOriginal<typeof ServerConfig>();
|
||||
return {
|
||||
...actualServer,
|
||||
NativeLspService: vi
|
||||
.fn()
|
||||
.mockImplementation(() => createNativeLspServiceInstance()),
|
||||
IdeClient: {
|
||||
getInstance: vi.fn().mockResolvedValue({
|
||||
getConnectionStatus: vi.fn(),
|
||||
|
|
@ -514,6 +547,10 @@ describe('loadCliConfig', () => {
|
|||
|
||||
beforeEach(() => {
|
||||
vi.resetAllMocks();
|
||||
nativeLspServiceMock.mockReset();
|
||||
nativeLspServiceMock.mockImplementation(
|
||||
() => createNativeLspServiceInstance() as unknown as NativeLspService,
|
||||
);
|
||||
vi.mocked(os.homedir).mockReturnValue('/mock/home/user');
|
||||
vi.stubEnv('GEMINI_API_KEY', 'test-api-key');
|
||||
});
|
||||
|
|
@ -543,6 +580,22 @@ describe('loadCliConfig', () => {
|
|||
expect(config.getIncludePartialMessages()).toBe(true);
|
||||
});
|
||||
|
||||
it('should initialize native LSP service when enabled', async () => {
|
||||
process.argv = ['node', 'script.js', '--experimental-lsp'];
|
||||
const argv = await parseArguments({} as Settings);
|
||||
const settings: Settings = {};
|
||||
|
||||
const config = await loadCliConfig(settings, argv);
|
||||
|
||||
// LSP is enabled via --experimental-lsp flag
|
||||
expect(config.isLspEnabled()).toBe(true);
|
||||
expect(nativeLspServiceMock).toHaveBeenCalledTimes(1);
|
||||
const lspInstance = getLastLspInstance();
|
||||
expect(lspInstance).toBeDefined();
|
||||
expect(lspInstance?.discoverAndPrepare).toHaveBeenCalledTimes(1);
|
||||
expect(lspInstance?.start).toHaveBeenCalledTimes(1);
|
||||
});
|
||||
|
||||
describe('Proxy configuration', () => {
|
||||
const originalProxyEnv: { [key: string]: string | undefined } = {};
|
||||
const proxyEnvVars = [
|
||||
|
|
|
|||
|
|
@ -20,11 +20,15 @@ import {
|
|||
OutputFormat,
|
||||
isToolEnabled,
|
||||
SessionService,
|
||||
ideContextStore,
|
||||
type ResumedSessionData,
|
||||
type LspClient,
|
||||
type ToolName,
|
||||
EditTool,
|
||||
ShellTool,
|
||||
WriteFileTool,
|
||||
NativeLspClient,
|
||||
NativeLspService,
|
||||
} from '@qwen-code/qwen-code-core';
|
||||
import { extensionsCommand } from '../commands/extensions.js';
|
||||
import type { Settings } from './settings.js';
|
||||
|
|
@ -113,6 +117,7 @@ export interface CliArgs {
|
|||
acp: boolean | undefined;
|
||||
experimentalAcp: boolean | undefined;
|
||||
experimentalSkills: boolean | undefined;
|
||||
experimentalLsp: boolean | undefined;
|
||||
extensions: string[] | undefined;
|
||||
listExtensions: boolean | undefined;
|
||||
openaiLogging: boolean | undefined;
|
||||
|
|
@ -331,6 +336,12 @@ export async function parseArguments(settings: Settings): Promise<CliArgs> {
|
|||
return settings.experimental?.skills ?? legacySkills ?? false;
|
||||
})(),
|
||||
})
|
||||
.option('experimental-lsp', {
|
||||
type: 'boolean',
|
||||
description:
|
||||
'Enable experimental LSP (Language Server Protocol) feature for code intelligence',
|
||||
default: false,
|
||||
})
|
||||
.option('channel', {
|
||||
type: 'string',
|
||||
choices: ['VSCode', 'ACP', 'SDK', 'CI'],
|
||||
|
|
@ -713,6 +724,9 @@ export async function loadCliConfig(
|
|||
.map(resolvePath)
|
||||
.concat((argv.includeDirectories || []).map(resolvePath));
|
||||
|
||||
// LSP configuration: enabled only via --experimental-lsp flag
|
||||
const lspEnabled = argv.experimentalLsp === true;
|
||||
let lspClient: LspClient | undefined;
|
||||
const question = argv.promptInteractive || argv.prompt || '';
|
||||
const inputFormat: InputFormat =
|
||||
(argv.inputFormat as InputFormat | undefined) ?? InputFormat.TEXT;
|
||||
|
|
@ -924,7 +938,7 @@ export async function loadCliConfig(
|
|||
|
||||
const modelProvidersConfig = settings.modelProviders;
|
||||
|
||||
return new Config({
|
||||
const config = new Config({
|
||||
sessionId,
|
||||
sessionData,
|
||||
embeddingModel: DEFAULT_QWEN_EMBEDDING_MODEL,
|
||||
|
|
@ -1016,7 +1030,34 @@ export async function loadCliConfig(
|
|||
// always be true and the settings file can never disable recording.
|
||||
chatRecording:
|
||||
argv.chatRecording ?? settings.general?.chatRecording ?? true,
|
||||
lsp: {
|
||||
enabled: lspEnabled,
|
||||
},
|
||||
});
|
||||
|
||||
if (lspEnabled) {
|
||||
try {
|
||||
const lspService = new NativeLspService(
|
||||
config,
|
||||
config.getWorkspaceContext(),
|
||||
appEvents,
|
||||
fileService,
|
||||
ideContextStore,
|
||||
{
|
||||
requireTrustedWorkspace: folderTrust,
|
||||
},
|
||||
);
|
||||
|
||||
await lspService.discoverAndPrepare();
|
||||
await lspService.start();
|
||||
lspClient = new NativeLspClient(lspService);
|
||||
config.setLspClient(lspClient);
|
||||
} catch (err) {
|
||||
logger.warn('Failed to initialize native LSP service:', err);
|
||||
}
|
||||
}
|
||||
|
||||
return config;
|
||||
}
|
||||
|
||||
function mergeExcludeTools(
|
||||
|
|
|
|||
|
|
@ -70,7 +70,6 @@ export interface SettingDefinition {
|
|||
default: SettingsValue;
|
||||
description?: string;
|
||||
parentKey?: string;
|
||||
childKey?: string;
|
||||
key?: string;
|
||||
properties?: SettingsSchema;
|
||||
showInDialog?: boolean;
|
||||
|
|
@ -598,7 +597,6 @@ const SETTINGS_SCHEMA = {
|
|||
default: undefined as number | undefined,
|
||||
description: 'Request timeout in milliseconds.',
|
||||
parentKey: 'generationConfig',
|
||||
childKey: 'timeout',
|
||||
showInDialog: false,
|
||||
},
|
||||
maxRetries: {
|
||||
|
|
@ -609,7 +607,6 @@ const SETTINGS_SCHEMA = {
|
|||
default: undefined as number | undefined,
|
||||
description: 'Maximum number of retries for failed requests.',
|
||||
parentKey: 'generationConfig',
|
||||
childKey: 'maxRetries',
|
||||
showInDialog: false,
|
||||
},
|
||||
disableCacheControl: {
|
||||
|
|
@ -620,7 +617,6 @@ const SETTINGS_SCHEMA = {
|
|||
default: false,
|
||||
description: 'Disable cache control for DashScope providers.',
|
||||
parentKey: 'generationConfig',
|
||||
childKey: 'disableCacheControl',
|
||||
showInDialog: false,
|
||||
},
|
||||
schemaCompliance: {
|
||||
|
|
@ -632,13 +628,23 @@ const SETTINGS_SCHEMA = {
|
|||
description:
|
||||
'The compliance mode for tool schemas sent to the model. Use "openapi_30" for strict OpenAPI 3.0 compatibility (e.g., for Gemini).',
|
||||
parentKey: 'generationConfig',
|
||||
childKey: 'schemaCompliance',
|
||||
showInDialog: false,
|
||||
options: [
|
||||
{ value: 'auto', label: 'Auto (Default)' },
|
||||
{ value: 'openapi_30', label: 'OpenAPI 3.0 Strict' },
|
||||
],
|
||||
},
|
||||
contextWindowSize: {
|
||||
type: 'number',
|
||||
label: 'Context Window Size',
|
||||
category: 'Generation Configuration',
|
||||
requiresRestart: false,
|
||||
default: undefined,
|
||||
description:
|
||||
"Overrides the default context window size for the selected model. Use this setting when a provider's effective context limit differs from Qwen Code's default. This value defines the model's assumed maximum context capacity, not a per-request token limit.",
|
||||
parentKey: 'generationConfig',
|
||||
showInDialog: false,
|
||||
},
|
||||
},
|
||||
},
|
||||
},
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue