Skip to content
Merged
Show file tree
Hide file tree
Changes from 4 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
7 changes: 7 additions & 0 deletions packages/types/src/model.ts
Original file line number Diff line number Diff line change
Expand Up @@ -111,6 +111,13 @@ export const modelInfoSchema = z.object({
supportsNativeTools: z.boolean().optional(),
// Default tool protocol preferred by this model (if not specified, falls back to capability/provider defaults)
defaultToolProtocol: z.enum(["xml", "native"]).optional(),
// Exclude specific native tools from being available (only applies to native protocol)
// These tools will be removed from the set of tools available to the model
excludedTools: z.array(z.string()).optional(),
// Include specific native tools (only applies to native protocol)
// These tools will be added if they belong to an allowed group in the current mode
// Cannot force-add tools from groups the mode doesn't allow
includedTools: z.array(z.string()).optional(),
/**
* Service tiers with pricing information.
* Each tier can have a name (for OpenAI service tiers) and pricing overrides.
Expand Down
363 changes: 360 additions & 3 deletions src/core/prompts/tools/__tests__/filter-tools-for-mode.spec.ts
Original file line number Diff line number Diff line change
@@ -1,7 +1,8 @@
import { describe, it, expect } from "vitest"
import { describe, it, expect, vi, beforeEach, afterEach } from "vitest"
import type OpenAI from "openai"
import type { ModeConfig } from "@roo-code/types"
import { filterNativeToolsForMode, filterMcpToolsForMode } from "../filter-tools-for-mode"
import type { ModeConfig, ModelInfo } from "@roo-code/types"
import { filterNativeToolsForMode, filterMcpToolsForMode, applyModelToolCustomization } from "../filter-tools-for-mode"
import * as toolsModule from "../../../../shared/tools"

describe("filterNativeToolsForMode", () => {
const mockNativeTools: OpenAI.Chat.ChatCompletionTool[] = [
Expand Down Expand Up @@ -467,4 +468,360 @@ describe("filterMcpToolsForMode", () => {
// Should include MCP tools since default mode has mcp group
expect(filtered.length).toBeGreaterThan(0)
})

describe("applyModelToolCustomization", () => {
const codeMode: ModeConfig = {
slug: "code",
name: "Code",
roleDefinition: "Test",
groups: ["read", "edit", "browser", "command", "mcp"] as const,
}

const architectMode: ModeConfig = {
slug: "architect",
name: "Architect",
roleDefinition: "Test",
groups: ["read", "browser", "mcp"] as const,
}

it("should return original tools when modelInfo is undefined", () => {
const tools = new Set(["read_file", "write_to_file", "apply_diff"])
const result = applyModelToolCustomization(tools, codeMode, undefined)
expect(result).toEqual(tools)
})

it("should exclude tools specified in excludedTools", () => {
const tools = new Set(["read_file", "write_to_file", "apply_diff"])
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
excludedTools: ["apply_diff"],
}
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
expect(result.has("read_file")).toBe(true)
expect(result.has("write_to_file")).toBe(true)
expect(result.has("apply_diff")).toBe(false)
})

it("should exclude multiple tools", () => {
const tools = new Set(["read_file", "write_to_file", "apply_diff", "execute_command"])
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
excludedTools: ["apply_diff", "write_to_file"],
}
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
expect(result.has("read_file")).toBe(true)
expect(result.has("execute_command")).toBe(true)
expect(result.has("write_to_file")).toBe(false)
expect(result.has("apply_diff")).toBe(false)
})

it("should include tools only if they belong to allowed groups", () => {
const tools = new Set(["read_file"])
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
includedTools: ["write_to_file", "apply_diff"], // Both in edit group
}
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
expect(result.has("read_file")).toBe(true)
expect(result.has("write_to_file")).toBe(true)
expect(result.has("apply_diff")).toBe(true)
})

it("should NOT include tools from groups not allowed by mode", () => {
const tools = new Set(["read_file"])
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
includedTools: ["write_to_file", "apply_diff"], // Edit group tools
}
// Architect mode doesn't have edit group
const result = applyModelToolCustomization(tools, architectMode, modelInfo)
expect(result.has("read_file")).toBe(true)
expect(result.has("write_to_file")).toBe(false) // Not in allowed groups
expect(result.has("apply_diff")).toBe(false) // Not in allowed groups
})

it("should apply both exclude and include operations", () => {
const tools = new Set(["read_file", "write_to_file", "apply_diff"])
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
excludedTools: ["apply_diff"],
includedTools: ["insert_content"], // Another edit tool
}
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
expect(result.has("read_file")).toBe(true)
expect(result.has("write_to_file")).toBe(true)
expect(result.has("apply_diff")).toBe(false) // Excluded
expect(result.has("insert_content")).toBe(true) // Included
})

it("should handle empty excludedTools and includedTools arrays", () => {
const tools = new Set(["read_file", "write_to_file"])
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
excludedTools: [],
includedTools: [],
}
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
expect(result).toEqual(tools)
})

it("should ignore excluded tools that are not in the original set", () => {
const tools = new Set(["read_file", "write_to_file"])
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
excludedTools: ["apply_diff", "nonexistent_tool"],
}
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
expect(result.has("read_file")).toBe(true)
expect(result.has("write_to_file")).toBe(true)
expect(result.size).toBe(2)
})

it("should NOT include customTools by default", () => {
const tools = new Set(["read_file", "write_to_file"])
// Assume 'edit' group has a customTool defined in TOOL_GROUPS
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
// No includedTools specified
}
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
// customTools should not be in the result unless explicitly included
expect(result.has("read_file")).toBe(true)
expect(result.has("write_to_file")).toBe(true)
})

it("should NOT include tools that are not in any TOOL_GROUPS", () => {
const tools = new Set(["read_file"])
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
includedTools: ["my_custom_tool"], // Not in any tool group
}
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
expect(result.has("read_file")).toBe(true)
expect(result.has("my_custom_tool")).toBe(false)
})

it("should NOT include undefined tools even with allowed groups", () => {
const tools = new Set(["read_file"])
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
includedTools: ["custom_edit_tool"], // Not in any tool group
}
// Even though architect mode has read group, undefined tools are not added
const result = applyModelToolCustomization(tools, architectMode, modelInfo)
expect(result.has("read_file")).toBe(true)
expect(result.has("custom_edit_tool")).toBe(false)
})

describe("with customTools defined in TOOL_GROUPS", () => {
const originalToolGroups = { ...toolsModule.TOOL_GROUPS }

beforeEach(() => {
// Add a customTool to the edit group
;(toolsModule.TOOL_GROUPS as any).edit = {
...originalToolGroups.edit,
customTools: ["special_edit_tool"],
}
})

afterEach(() => {
// Restore original TOOL_GROUPS
;(toolsModule.TOOL_GROUPS as any).edit = originalToolGroups.edit
})

it("should include customTools when explicitly specified in includedTools", () => {
const tools = new Set(["read_file", "write_to_file"])
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
includedTools: ["special_edit_tool"], // customTool from edit group
}
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
expect(result.has("read_file")).toBe(true)
expect(result.has("write_to_file")).toBe(true)
expect(result.has("special_edit_tool")).toBe(true) // customTool should be included
})

it("should NOT include customTools when not specified in includedTools", () => {
const tools = new Set(["read_file", "write_to_file"])
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
// No includedTools specified
}
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
expect(result.has("read_file")).toBe(true)
expect(result.has("write_to_file")).toBe(true)
expect(result.has("special_edit_tool")).toBe(false) // customTool should NOT be included by default
})

it("should NOT include customTools from groups not allowed by mode", () => {
const tools = new Set(["read_file"])
const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
includedTools: ["special_edit_tool"], // customTool from edit group
}
// Architect mode doesn't have edit group
const result = applyModelToolCustomization(tools, architectMode, modelInfo)
expect(result.has("read_file")).toBe(true)
expect(result.has("special_edit_tool")).toBe(false) // customTool should NOT be included
})
})
})

describe("filterNativeToolsForMode with model customization", () => {
const mockNativeTools: OpenAI.Chat.ChatCompletionTool[] = [
{
type: "function",
function: {
name: "read_file",
description: "Read files",
parameters: {},
},
},
{
type: "function",
function: {
name: "write_to_file",
description: "Write files",
parameters: {},
},
},
{
type: "function",
function: {
name: "apply_diff",
description: "Apply diff",
parameters: {},
},
},
{
type: "function",
function: {
name: "insert_content",
description: "Insert content",
parameters: {},
},
},
{
type: "function",
function: {
name: "execute_command",
description: "Execute command",
parameters: {},
},
},
]

it("should exclude tools when model specifies excludedTools", () => {
const codeMode: ModeConfig = {
slug: "code",
name: "Code",
roleDefinition: "Test",
groups: ["read", "edit", "browser", "command", "mcp"] as const,
}

const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
excludedTools: ["apply_diff"],
}

const filtered = filterNativeToolsForMode(mockNativeTools, "code", [codeMode], {}, undefined, {
modelInfo,
})

const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))

expect(toolNames).toContain("read_file")
expect(toolNames).toContain("write_to_file")
expect(toolNames).toContain("insert_content")
expect(toolNames).not.toContain("apply_diff") // Excluded by model
})

it("should include tools when model specifies includedTools from allowed groups", () => {
const modeWithOnlyRead: ModeConfig = {
slug: "limited",
name: "Limited",
roleDefinition: "Test",
groups: ["read", "edit"] as const,
}

const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
includedTools: ["insert_content"], // Edit group tool
}

const filtered = filterNativeToolsForMode(mockNativeTools, "limited", [modeWithOnlyRead], {}, undefined, {
modelInfo,
})

const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))

expect(toolNames).toContain("insert_content") // Included by model
})

it("should NOT include tools from groups not allowed by mode", () => {
const architectMode: ModeConfig = {
slug: "architect",
name: "Architect",
roleDefinition: "Test",
groups: ["read", "browser"] as const, // No edit group
}

const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
includedTools: ["write_to_file", "apply_diff"], // Edit group tools
}

const filtered = filterNativeToolsForMode(mockNativeTools, "architect", [architectMode], {}, undefined, {
modelInfo,
})

const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))

expect(toolNames).toContain("read_file")
expect(toolNames).not.toContain("write_to_file") // Not in mode's allowed groups
expect(toolNames).not.toContain("apply_diff") // Not in mode's allowed groups
})

it("should combine excludedTools and includedTools", () => {
const codeMode: ModeConfig = {
slug: "code",
name: "Code",
roleDefinition: "Test",
groups: ["read", "edit", "browser", "command", "mcp"] as const,
}

const modelInfo: ModelInfo = {
contextWindow: 100000,
supportsPromptCache: false,
excludedTools: ["apply_diff"],
includedTools: ["insert_content"],
}

const filtered = filterNativeToolsForMode(mockNativeTools, "code", [codeMode], {}, undefined, {
modelInfo,
})

const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))

expect(toolNames).toContain("write_to_file")
expect(toolNames).toContain("insert_content") // Included
expect(toolNames).not.toContain("apply_diff") // Excluded
})
})
})
Loading
Loading