Skip to content

Commit 10cc60b

Browse files
committed
feat: add model-specific tool customization via excludedTools and includedTools
- Add excludedTools and includedTools to ModelInfo schema - Implement applyModelToolCustomization helper to filter tools based on model config - Integrate model tool filtering into filterNativeToolsForMode for native protocol - Add comprehensive tests for tool customization functionality - Wire up modelInfo through buildNativeToolsArray and Task.ts This allows providers to override which native tools are available on a per-model basis via MODEL_DEFAULTS, enabling better control over tool selection for models with specific needs.
1 parent ba09228 commit 10cc60b

File tree

5 files changed

+350
-9
lines changed

5 files changed

+350
-9
lines changed

packages/types/src/model.ts

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -111,6 +111,13 @@ export const modelInfoSchema = z.object({
111111
supportsNativeTools: z.boolean().optional(),
112112
// Default tool protocol preferred by this model (if not specified, falls back to capability/provider defaults)
113113
defaultToolProtocol: z.enum(["xml", "native"]).optional(),
114+
// Exclude specific native tools from being available (only applies to native protocol)
115+
// These tools will be removed from the set of tools available to the model
116+
excludedTools: z.array(z.string()).optional(),
117+
// Include specific native tools (only applies to native protocol)
118+
// These tools will be added if they belong to an allowed group in the current mode
119+
// Cannot force-add tools from groups the mode doesn't allow
120+
includedTools: z.array(z.string()).optional(),
114121
/**
115122
* Service tiers with pricing information.
116123
* Each tier can have a name (for OpenAI service tiers) and pricing overrides.

src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts

Lines changed: 263 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,7 @@
11
import { describe, it, expect } from "vitest"
22
import type OpenAI from "openai"
3-
import type { ModeConfig } from "@roo-code/types"
4-
import { filterNativeToolsForMode, filterMcpToolsForMode } from "../filter-tools-for-mode"
3+
import type { ModeConfig, ModelInfo } from "@roo-code/types"
4+
import { filterNativeToolsForMode, filterMcpToolsForMode, applyModelToolCustomization } from "../filter-tools-for-mode"
55

66
describe("filterNativeToolsForMode", () => {
77
const mockNativeTools: OpenAI.Chat.ChatCompletionTool[] = [
@@ -467,4 +467,265 @@ describe("filterMcpToolsForMode", () => {
467467
// Should include MCP tools since default mode has mcp group
468468
expect(filtered.length).toBeGreaterThan(0)
469469
})
470+
471+
describe("applyModelToolCustomization", () => {
472+
const codeMode: ModeConfig = {
473+
slug: "code",
474+
name: "Code",
475+
roleDefinition: "Test",
476+
groups: ["read", "edit", "browser", "command", "mcp"] as const,
477+
}
478+
479+
const architectMode: ModeConfig = {
480+
slug: "architect",
481+
name: "Architect",
482+
roleDefinition: "Test",
483+
groups: ["read", "browser", "mcp"] as const,
484+
}
485+
486+
it("should return original tools when modelInfo is undefined", () => {
487+
const tools = new Set(["read_file", "write_to_file", "apply_diff"])
488+
const result = applyModelToolCustomization(tools, codeMode, undefined)
489+
expect(result).toEqual(tools)
490+
})
491+
492+
it("should exclude tools specified in excludedTools", () => {
493+
const tools = new Set(["read_file", "write_to_file", "apply_diff"])
494+
const modelInfo: ModelInfo = {
495+
contextWindow: 100000,
496+
supportsPromptCache: false,
497+
excludedTools: ["apply_diff"],
498+
}
499+
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
500+
expect(result.has("read_file")).toBe(true)
501+
expect(result.has("write_to_file")).toBe(true)
502+
expect(result.has("apply_diff")).toBe(false)
503+
})
504+
505+
it("should exclude multiple tools", () => {
506+
const tools = new Set(["read_file", "write_to_file", "apply_diff", "execute_command"])
507+
const modelInfo: ModelInfo = {
508+
contextWindow: 100000,
509+
supportsPromptCache: false,
510+
excludedTools: ["apply_diff", "write_to_file"],
511+
}
512+
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
513+
expect(result.has("read_file")).toBe(true)
514+
expect(result.has("execute_command")).toBe(true)
515+
expect(result.has("write_to_file")).toBe(false)
516+
expect(result.has("apply_diff")).toBe(false)
517+
})
518+
519+
it("should include tools only if they belong to allowed groups", () => {
520+
const tools = new Set(["read_file"])
521+
const modelInfo: ModelInfo = {
522+
contextWindow: 100000,
523+
supportsPromptCache: false,
524+
includedTools: ["write_to_file", "apply_diff"], // Both in edit group
525+
}
526+
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
527+
expect(result.has("read_file")).toBe(true)
528+
expect(result.has("write_to_file")).toBe(true)
529+
expect(result.has("apply_diff")).toBe(true)
530+
})
531+
532+
it("should NOT include tools from groups not allowed by mode", () => {
533+
const tools = new Set(["read_file"])
534+
const modelInfo: ModelInfo = {
535+
contextWindow: 100000,
536+
supportsPromptCache: false,
537+
includedTools: ["write_to_file", "apply_diff"], // Edit group tools
538+
}
539+
// Architect mode doesn't have edit group
540+
const result = applyModelToolCustomization(tools, architectMode, modelInfo)
541+
expect(result.has("read_file")).toBe(true)
542+
expect(result.has("write_to_file")).toBe(false) // Not in allowed groups
543+
expect(result.has("apply_diff")).toBe(false) // Not in allowed groups
544+
})
545+
546+
it("should apply both exclude and include operations", () => {
547+
const tools = new Set(["read_file", "write_to_file", "apply_diff"])
548+
const modelInfo: ModelInfo = {
549+
contextWindow: 100000,
550+
supportsPromptCache: false,
551+
excludedTools: ["apply_diff"],
552+
includedTools: ["insert_content"], // Another edit tool
553+
}
554+
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
555+
expect(result.has("read_file")).toBe(true)
556+
expect(result.has("write_to_file")).toBe(true)
557+
expect(result.has("apply_diff")).toBe(false) // Excluded
558+
expect(result.has("insert_content")).toBe(true) // Included
559+
})
560+
561+
it("should handle empty excludedTools and includedTools arrays", () => {
562+
const tools = new Set(["read_file", "write_to_file"])
563+
const modelInfo: ModelInfo = {
564+
contextWindow: 100000,
565+
supportsPromptCache: false,
566+
excludedTools: [],
567+
includedTools: [],
568+
}
569+
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
570+
expect(result).toEqual(tools)
571+
})
572+
573+
it("should ignore excluded tools that are not in the original set", () => {
574+
const tools = new Set(["read_file", "write_to_file"])
575+
const modelInfo: ModelInfo = {
576+
contextWindow: 100000,
577+
supportsPromptCache: false,
578+
excludedTools: ["apply_diff", "nonexistent_tool"],
579+
}
580+
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
581+
expect(result.has("read_file")).toBe(true)
582+
expect(result.has("write_to_file")).toBe(true)
583+
expect(result.size).toBe(2)
584+
})
585+
})
586+
587+
describe("filterNativeToolsForMode with model customization", () => {
588+
const mockNativeTools: OpenAI.Chat.ChatCompletionTool[] = [
589+
{
590+
type: "function",
591+
function: {
592+
name: "read_file",
593+
description: "Read files",
594+
parameters: {},
595+
},
596+
},
597+
{
598+
type: "function",
599+
function: {
600+
name: "write_to_file",
601+
description: "Write files",
602+
parameters: {},
603+
},
604+
},
605+
{
606+
type: "function",
607+
function: {
608+
name: "apply_diff",
609+
description: "Apply diff",
610+
parameters: {},
611+
},
612+
},
613+
{
614+
type: "function",
615+
function: {
616+
name: "insert_content",
617+
description: "Insert content",
618+
parameters: {},
619+
},
620+
},
621+
{
622+
type: "function",
623+
function: {
624+
name: "execute_command",
625+
description: "Execute command",
626+
parameters: {},
627+
},
628+
},
629+
]
630+
631+
it("should exclude tools when model specifies excludedTools", () => {
632+
const codeMode: ModeConfig = {
633+
slug: "code",
634+
name: "Code",
635+
roleDefinition: "Test",
636+
groups: ["read", "edit", "browser", "command", "mcp"] as const,
637+
}
638+
639+
const modelInfo: ModelInfo = {
640+
contextWindow: 100000,
641+
supportsPromptCache: false,
642+
excludedTools: ["apply_diff"],
643+
}
644+
645+
const filtered = filterNativeToolsForMode(mockNativeTools, "code", [codeMode], {}, undefined, {
646+
modelInfo,
647+
})
648+
649+
const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
650+
651+
expect(toolNames).toContain("read_file")
652+
expect(toolNames).toContain("write_to_file")
653+
expect(toolNames).toContain("insert_content")
654+
expect(toolNames).not.toContain("apply_diff") // Excluded by model
655+
})
656+
657+
it("should include tools when model specifies includedTools from allowed groups", () => {
658+
const modeWithOnlyRead: ModeConfig = {
659+
slug: "limited",
660+
name: "Limited",
661+
roleDefinition: "Test",
662+
groups: ["read", "edit"] as const,
663+
}
664+
665+
const modelInfo: ModelInfo = {
666+
contextWindow: 100000,
667+
supportsPromptCache: false,
668+
includedTools: ["insert_content"], // Edit group tool
669+
}
670+
671+
const filtered = filterNativeToolsForMode(mockNativeTools, "limited", [modeWithOnlyRead], {}, undefined, {
672+
modelInfo,
673+
})
674+
675+
const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
676+
677+
expect(toolNames).toContain("insert_content") // Included by model
678+
})
679+
680+
it("should NOT include tools from groups not allowed by mode", () => {
681+
const architectMode: ModeConfig = {
682+
slug: "architect",
683+
name: "Architect",
684+
roleDefinition: "Test",
685+
groups: ["read", "browser"] as const, // No edit group
686+
}
687+
688+
const modelInfo: ModelInfo = {
689+
contextWindow: 100000,
690+
supportsPromptCache: false,
691+
includedTools: ["write_to_file", "apply_diff"], // Edit group tools
692+
}
693+
694+
const filtered = filterNativeToolsForMode(mockNativeTools, "architect", [architectMode], {}, undefined, {
695+
modelInfo,
696+
})
697+
698+
const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
699+
700+
expect(toolNames).toContain("read_file")
701+
expect(toolNames).not.toContain("write_to_file") // Not in mode's allowed groups
702+
expect(toolNames).not.toContain("apply_diff") // Not in mode's allowed groups
703+
})
704+
705+
it("should combine excludedTools and includedTools", () => {
706+
const codeMode: ModeConfig = {
707+
slug: "code",
708+
name: "Code",
709+
roleDefinition: "Test",
710+
groups: ["read", "edit", "browser", "command", "mcp"] as const,
711+
}
712+
713+
const modelInfo: ModelInfo = {
714+
contextWindow: 100000,
715+
supportsPromptCache: false,
716+
excludedTools: ["apply_diff"],
717+
includedTools: ["insert_content"],
718+
}
719+
720+
const filtered = filterNativeToolsForMode(mockNativeTools, "code", [codeMode], {}, undefined, {
721+
modelInfo,
722+
})
723+
724+
const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
725+
726+
expect(toolNames).toContain("write_to_file")
727+
expect(toolNames).toContain("insert_content") // Included
728+
expect(toolNames).not.toContain("apply_diff") // Excluded
729+
})
730+
})
470731
})

src/core/prompts/tools/filter-tools-for-mode.ts

Lines changed: 65 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,78 @@
11
import type OpenAI from "openai"
2-
import type { ModeConfig, ToolName, ToolGroup } from "@roo-code/types"
2+
import type { ModeConfig, ToolName, ToolGroup, ModelInfo } from "@roo-code/types"
33
import { getModeBySlug, getToolsForMode, isToolAllowedForMode } from "../../../shared/modes"
44
import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS } from "../../../shared/tools"
55
import { defaultModeSlug } from "../../../shared/modes"
66
import type { CodeIndexManager } from "../../../services/code-index/manager"
77
import type { McpHub } from "../../../services/mcp/McpHub"
88

99
/**
10-
* Filters native tools based on mode restrictions.
10+
* Apply model-specific tool customization to a set of allowed tools.
11+
*
12+
* This function filters tools based on model configuration:
13+
* 1. Removes tools specified in modelInfo.excludedTools
14+
* 2. Adds tools from modelInfo.includedTools (only if they belong to allowed groups)
15+
*
16+
* @param allowedTools - Set of tools already allowed by mode configuration
17+
* @param modeConfig - Current mode configuration to check tool groups
18+
* @param modelInfo - Model configuration with tool customization
19+
* @returns Modified set of tools after applying model customization
20+
*/
21+
export function applyModelToolCustomization(
22+
allowedTools: Set<string>,
23+
modeConfig: ModeConfig,
24+
modelInfo?: ModelInfo,
25+
): Set<string> {
26+
if (!modelInfo) {
27+
return allowedTools
28+
}
29+
30+
const result = new Set(allowedTools)
31+
32+
// Apply excluded tools (remove from allowed set)
33+
if (modelInfo.excludedTools && modelInfo.excludedTools.length > 0) {
34+
modelInfo.excludedTools.forEach((tool) => {
35+
result.delete(tool)
36+
})
37+
}
38+
39+
// Apply included tools (add to allowed set, but only if they belong to an allowed group)
40+
if (modelInfo.includedTools && modelInfo.includedTools.length > 0) {
41+
// Build a map of tool -> group for all tools in TOOL_GROUPS
42+
const toolToGroup = new Map<string, ToolGroup>()
43+
for (const [groupName, groupConfig] of Object.entries(TOOL_GROUPS)) {
44+
groupConfig.tools.forEach((tool) => {
45+
toolToGroup.set(tool, groupName as ToolGroup)
46+
})
47+
}
48+
49+
// Get the list of allowed groups for this mode
50+
const allowedGroups = new Set(
51+
modeConfig.groups.map((groupEntry) => (Array.isArray(groupEntry) ? groupEntry[0] : groupEntry)),
52+
)
53+
54+
// Add included tools only if they belong to an allowed group
55+
modelInfo.includedTools.forEach((tool) => {
56+
const toolGroup = toolToGroup.get(tool)
57+
if (toolGroup && allowedGroups.has(toolGroup)) {
58+
result.add(tool)
59+
}
60+
})
61+
}
62+
63+
return result
64+
}
65+
66+
/**
67+
* Filters native tools based on mode restrictions and model customization.
1168
* This ensures native tools are filtered the same way XML tools are filtered in the system prompt.
1269
*
1370
* @param nativeTools - Array of all available native tools
1471
* @param mode - Current mode slug
1572
* @param customModes - Custom mode configurations
1673
* @param experiments - Experiment flags
1774
* @param codeIndexManager - Code index manager for codebase_search feature check
18-
* @param settings - Additional settings for tool filtering
75+
* @param settings - Additional settings for tool filtering (includes modelInfo for model-specific customization)
1976
* @param mcpHub - MCP hub for checking available resources
2077
* @returns Filtered array of tools allowed for the mode
2178
*/
@@ -43,7 +100,7 @@ export function filterNativeToolsForMode(
43100
const allToolsForMode = getToolsForMode(modeConfig.groups)
44101

45102
// Filter to only tools that pass permission checks
46-
const allowedToolNames = new Set(
103+
let allowedToolNames = new Set(
47104
allToolsForMode.filter((tool) =>
48105
isToolAllowedForMode(
49106
tool as ToolName,
@@ -56,6 +113,10 @@ export function filterNativeToolsForMode(
56113
),
57114
)
58115

116+
// Apply model-specific tool customization
117+
const modelInfo = settings?.modelInfo as ModelInfo | undefined
118+
allowedToolNames = applyModelToolCustomization(allowedToolNames, modeConfig, modelInfo)
119+
59120
// Conditionally exclude codebase_search if feature is disabled or not configured
60121
if (
61122
!codeIndexManager ||

src/core/task/Task.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -3479,6 +3479,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
34793479
apiConfiguration,
34803480
maxReadFileLine: state?.maxReadFileLine ?? -1,
34813481
browserToolEnabled: state?.browserToolEnabled ?? true,
3482+
modelInfo,
34823483
})
34833484
}
34843485

0 commit comments

Comments
 (0)