Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
57 changes: 57 additions & 0 deletions packages/cli/src/config/config.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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', () => {
Expand Down
21 changes: 21 additions & 0 deletions packages/cli/src/config/config.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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,
Expand Down
21 changes: 21 additions & 0 deletions packages/cli/src/config/settingsSchema.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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',
Expand Down
11 changes: 11 additions & 0 deletions packages/vscode-ide-companion/schemas/settings.schema.json
Original file line number Diff line number Diff line change
Expand Up @@ -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",
Expand Down
Loading