diff --git a/packages/cli/src/config/config.test.ts b/packages/cli/src/config/config.test.ts index c060ba634e0..e7ff46b461a 100644 --- a/packages/cli/src/config/config.test.ts +++ b/packages/cli/src/config/config.test.ts @@ -1310,6 +1310,63 @@ describe('mergeExcludeTools', () => { ); expect(config.getPermissionsDeny()).toHaveLength(2); }); + + it('should add tool_search to deny list when tools.toolSearch.enabled is false', async () => { + process.argv = ['node', 'script.js']; + const argv = await parseArguments(); + const settings: Settings = { + tools: { toolSearch: { enabled: false } }, + }; + const config = await loadCliConfig(settings, argv, undefined, []); + expect(config.getPermissionsDeny()).toContain('tool_search'); + }); + + it('should auto-disable tool_search for deepseek-v4 models', async () => { + process.argv = ['node', 'script.js', '--model', 'deepseek-v4-flash']; + const argv = await parseArguments(); + const settings: Settings = {}; + const config = await loadCliConfig(settings, argv, undefined, []); + expect(config.getPermissionsDeny()).toContain('tool_search'); + }); + + it('should auto-disable tool_search for deepseek-v3 models', async () => { + process.argv = ['node', 'script.js', '--model', 'deepseek-v3']; + const argv = await parseArguments(); + const settings: Settings = {}; + const config = await loadCliConfig(settings, argv, undefined, []); + expect(config.getPermissionsDeny()).toContain('tool_search'); + }); + + it('should auto-disable tool_search for deepseek-chat models with provider prefix', async () => { + process.argv = [ + 'node', + 'script.js', + '--model', + 'openrouter/deepseek/deepseek-chat', + ]; + const argv = await parseArguments(); + const settings: Settings = {}; + const config = await loadCliConfig(settings, argv, undefined, []); + expect(config.getPermissionsDeny()).toContain('tool_search'); + }); + + it('should not auto-disable tool_search for non-deepseek models', async () => { + process.argv = ['node', 'script.js', '--model', 'qwen-max']; + const argv = await parseArguments(); + const settings: Settings = {}; + const config = await loadCliConfig(settings, argv, undefined, []); + expect(config.getPermissionsDeny()).not.toContain('tool_search'); + }); + + it('should respect explicit enabled:true override for deepseek models', async () => { + process.argv = ['node', 'script.js', '--model', 'deepseek-v4-flash']; + const argv = await parseArguments(); + const settings: Settings = { + tools: { toolSearch: { enabled: true } }, + }; + const config = await loadCliConfig(settings, argv, undefined, []); + expect(config.getPermissionsDeny()).not.toContain('tool_search'); + }); }); describe('Approval mode tool exclusion logic', () => { diff --git a/packages/cli/src/config/config.ts b/packages/cli/src/config/config.ts index a765a140950..f829665b1c0 100755 --- a/packages/cli/src/config/config.ts +++ b/packages/cli/src/config/config.ts @@ -1472,6 +1472,27 @@ export async function loadCliConfig( const { model: resolvedModel } = resolvedCliConfig; + // Disable ToolSearch when explicitly configured or for models that benefit + // from prefix-based KV caching. DeepSeek models (v3, v4, deepseek-chat) + // all use prefix-based disk KV caching with heavily discounted cached + // token pricing (up to 1/120 for v4). When tool_search is in the deny + // list, client.ts eagerly reveals all deferred tools so every MCP tool + // schema is in the initial declaration list, keeping the prompt prefix + // stable and maximizing cache hit rates. + // Note: no `^` anchor — model names may include a provider prefix + // (e.g. "openrouter/deepseek/deepseek-v4-flash"). + const toolSearchExplicitlyEnabled = settings.tools?.toolSearch?.enabled; + const shouldDisableToolSearch = + toolSearchExplicitlyEnabled === false || + (toolSearchExplicitlyEnabled === undefined && + resolvedModel !== undefined && + /deepseek-(v3|v4|chat)/i.test(resolvedModel)); + if (shouldDisableToolSearch) { + if (!mergedDeny.includes('tool_search')) { + mergedDeny.push('tool_search'); + } + } + const sandboxConfig = await loadSandboxConfig( bareMode ? ({} as Settings) : settings, argv, diff --git a/packages/cli/src/config/settingsSchema.ts b/packages/cli/src/config/settingsSchema.ts index 709d14b08c9..07af4b0a77f 100644 --- a/packages/cli/src/config/settingsSchema.ts +++ b/packages/cli/src/config/settingsSchema.ts @@ -1461,6 +1461,27 @@ const SETTINGS_SCHEMA = { 'Sandbox image URI used by Docker/Podman when --sandbox-image and QWEN_SANDBOX_IMAGE are not set.', showInDialog: false, }, + toolSearch: { + type: 'object', + label: 'Tool Search', + category: 'Tools', + requiresRestart: true, + default: {}, + description: 'Settings for the ToolSearch discovery mechanism.', + showInDialog: false, + properties: { + enabled: { + type: 'boolean', + label: 'Enable ToolSearch', + category: 'Tools', + requiresRestart: true, + default: true, + description: + 'When enabled, MCP tools are loaded on-demand via ToolSearch to reduce prompt size. Disable this for models that rely on prefix-based KV caching (e.g. DeepSeek) to keep the prompt prefix stable and maximize cache hit rates.', + showInDialog: true, + }, + }, + }, shell: { type: 'object', label: 'Shell', diff --git a/packages/vscode-ide-companion/schemas/settings.schema.json b/packages/vscode-ide-companion/schemas/settings.schema.json index fe4776b1083..7a6bb99c312 100644 --- a/packages/vscode-ide-companion/schemas/settings.schema.json +++ b/packages/vscode-ide-companion/schemas/settings.schema.json @@ -643,6 +643,17 @@ "description": "Sandbox image URI used by Docker/Podman when --sandbox-image and QWEN_SANDBOX_IMAGE are not set.", "type": "string" }, + "toolSearch": { + "description": "Settings for the ToolSearch discovery mechanism.", + "type": "object", + "properties": { + "enabled": { + "description": "When enabled, MCP tools are loaded on-demand via ToolSearch to reduce prompt size. Disable this for models that rely on prefix-based KV caching (e.g. DeepSeek) to keep the prompt prefix stable and maximize cache hit rates.", + "type": "boolean", + "default": true + } + } + }, "shell": { "description": "Settings for shell execution.", "type": "object",