mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-09-07 08:26:51 +00:00
feat: add tool alias support for model-specific tool customization (#9989)
Co-authored-by: Hannes Rudolph <hrudolph@gmail.com>
This commit is contained in:
parent
23a214c519
commit
ba7c5535ba
6 changed files with 317 additions and 63 deletions
|
|
@ -6,6 +6,7 @@ import {
|
|||
toolParamNames,
|
||||
type NativeToolArgs,
|
||||
} from "../../shared/tools"
|
||||
import { resolveToolAlias } from "../prompts/tools/filter-tools-for-mode"
|
||||
import { parseJSON } from "partial-json"
|
||||
import type {
|
||||
ApiStreamToolCallStartChunk,
|
||||
|
|
@ -246,12 +247,18 @@ export class NativeToolCallParser {
|
|||
try {
|
||||
const partialArgs = parseJSON(toolCall.argumentsAccumulator)
|
||||
|
||||
// Resolve tool alias to canonical name
|
||||
const resolvedName = resolveToolAlias(toolCall.name) as ToolName
|
||||
// Preserve original name if it differs from resolved (i.e., it was an alias)
|
||||
const originalName = toolCall.name !== resolvedName ? toolCall.name : undefined
|
||||
|
||||
// Create partial ToolUse with extracted values
|
||||
return this.createPartialToolUse(
|
||||
toolCall.id,
|
||||
toolCall.name as ToolName,
|
||||
resolvedName,
|
||||
partialArgs || {},
|
||||
true, // partial
|
||||
originalName,
|
||||
)
|
||||
} catch {
|
||||
// Even partial-json-parser can fail on severely malformed JSON
|
||||
|
|
@ -327,12 +334,14 @@ export class NativeToolCallParser {
|
|||
/**
|
||||
* Create a partial ToolUse from currently parsed arguments.
|
||||
* Used during streaming to show progress.
|
||||
* @param originalName - The original tool name as called by the model (if different from canonical name)
|
||||
*/
|
||||
private static createPartialToolUse(
|
||||
id: string,
|
||||
name: ToolName,
|
||||
partialArgs: Record<string, any>,
|
||||
partial: boolean,
|
||||
originalName?: string,
|
||||
): ToolUse | null {
|
||||
// Build legacy params for display
|
||||
// NOTE: For streaming partial updates, we MUST populate params even for complex types
|
||||
|
|
@ -505,18 +514,33 @@ export class NativeToolCallParser {
|
|||
}
|
||||
break
|
||||
|
||||
// Add other tools as needed
|
||||
case "search_and_replace":
|
||||
if (partialArgs.path !== undefined || partialArgs.operations !== undefined) {
|
||||
nativeArgs = {
|
||||
path: partialArgs.path,
|
||||
operations: partialArgs.operations,
|
||||
}
|
||||
}
|
||||
break
|
||||
|
||||
default:
|
||||
break
|
||||
}
|
||||
|
||||
return {
|
||||
const result: ToolUse = {
|
||||
type: "tool_use" as const,
|
||||
name,
|
||||
params,
|
||||
partial,
|
||||
nativeArgs,
|
||||
}
|
||||
|
||||
// Preserve original name for API history when an alias was used
|
||||
if (originalName) {
|
||||
result.originalName = originalName
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -535,9 +559,12 @@ export class NativeToolCallParser {
|
|||
return this.parseDynamicMcpTool(toolCall)
|
||||
}
|
||||
|
||||
// Validate tool name
|
||||
if (!toolNames.includes(toolCall.name as ToolName)) {
|
||||
console.error(`Invalid tool name: ${toolCall.name}`)
|
||||
// Resolve tool alias to canonical name (e.g., "edit_file" -> "apply_diff", "temp_edit_file" -> "search_and_replace")
|
||||
const resolvedName = resolveToolAlias(toolCall.name as string) as TName
|
||||
|
||||
// Validate tool name (after alias resolution)
|
||||
if (!toolNames.includes(resolvedName as ToolName)) {
|
||||
console.error(`Invalid tool name: ${toolCall.name} (resolved: ${resolvedName})`)
|
||||
console.error(`Valid tool names:`, toolNames)
|
||||
return null
|
||||
}
|
||||
|
|
@ -554,13 +581,13 @@ export class NativeToolCallParser {
|
|||
// Skip complex parameters that have been migrated to nativeArgs.
|
||||
// For read_file, the 'files' parameter is a FileEntry[] array that can't be
|
||||
// meaningfully stringified. The properly typed data is in nativeArgs instead.
|
||||
if (toolCall.name === "read_file" && key === "files") {
|
||||
if (resolvedName === "read_file" && key === "files") {
|
||||
continue
|
||||
}
|
||||
|
||||
// Validate parameter name
|
||||
if (!toolParamNames.includes(key as ToolParamName)) {
|
||||
console.warn(`Unknown parameter '${key}' for tool '${toolCall.name}'`)
|
||||
console.warn(`Unknown parameter '${key}' for tool '${resolvedName}'`)
|
||||
console.warn(`Valid param names:`, toolParamNames)
|
||||
continue
|
||||
}
|
||||
|
|
@ -580,7 +607,7 @@ export class NativeToolCallParser {
|
|||
// will fall back to legacy parameter parsing if supported.
|
||||
let nativeArgs: NativeArgsFor<TName> | undefined = undefined
|
||||
|
||||
switch (toolCall.name) {
|
||||
switch (resolvedName) {
|
||||
case "read_file":
|
||||
if (args.files && Array.isArray(args.files)) {
|
||||
nativeArgs = { files: this.convertFileEntries(args.files) } as NativeArgsFor<TName>
|
||||
|
|
@ -761,12 +788,17 @@ export class NativeToolCallParser {
|
|||
|
||||
const result: ToolUse<TName> = {
|
||||
type: "tool_use" as const,
|
||||
name: toolCall.name,
|
||||
name: resolvedName,
|
||||
params,
|
||||
partial: false, // Native tool calls are always complete when yielded
|
||||
nativeArgs,
|
||||
}
|
||||
|
||||
// Preserve original name for API history when an alias was used
|
||||
if (toolCall.name !== resolvedName) {
|
||||
result.originalName = toolCall.name
|
||||
}
|
||||
|
||||
return result
|
||||
} catch (error) {
|
||||
console.error(
|
||||
|
|
|
|||
|
|
@ -695,7 +695,11 @@ export async function presentAssistantMessage(cline: Task) {
|
|||
// potentially causing the stream to appear frozen.
|
||||
if (!block.partial) {
|
||||
const modelInfo = cline.api.getModel()
|
||||
const includedTools = modelInfo?.info?.includedTools
|
||||
// Resolve aliases in includedTools before validation
|
||||
// e.g., "edit_file" should resolve to "apply_diff"
|
||||
const rawIncludedTools = modelInfo?.info?.includedTools
|
||||
const { resolveToolAlias } = await import("../prompts/tools/filter-tools-for-mode")
|
||||
const includedTools = rawIncludedTools?.map((tool) => resolveToolAlias(tool))
|
||||
|
||||
try {
|
||||
validateToolUse(
|
||||
|
|
|
|||
|
|
@ -487,7 +487,7 @@ describe("filterMcpToolsForMode", () => {
|
|||
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)
|
||||
expect(result.allowedTools).toEqual(tools)
|
||||
})
|
||||
|
||||
it("should exclude tools specified in excludedTools", () => {
|
||||
|
|
@ -498,9 +498,9 @@ describe("filterMcpToolsForMode", () => {
|
|||
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)
|
||||
expect(result.allowedTools.has("read_file")).toBe(true)
|
||||
expect(result.allowedTools.has("write_to_file")).toBe(true)
|
||||
expect(result.allowedTools.has("apply_diff")).toBe(false)
|
||||
})
|
||||
|
||||
it("should exclude multiple tools", () => {
|
||||
|
|
@ -511,10 +511,10 @@ describe("filterMcpToolsForMode", () => {
|
|||
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)
|
||||
expect(result.allowedTools.has("read_file")).toBe(true)
|
||||
expect(result.allowedTools.has("execute_command")).toBe(true)
|
||||
expect(result.allowedTools.has("write_to_file")).toBe(false)
|
||||
expect(result.allowedTools.has("apply_diff")).toBe(false)
|
||||
})
|
||||
|
||||
it("should include tools only if they belong to allowed groups", () => {
|
||||
|
|
@ -525,9 +525,9 @@ describe("filterMcpToolsForMode", () => {
|
|||
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)
|
||||
expect(result.allowedTools.has("read_file")).toBe(true)
|
||||
expect(result.allowedTools.has("write_to_file")).toBe(true)
|
||||
expect(result.allowedTools.has("apply_diff")).toBe(true)
|
||||
})
|
||||
|
||||
it("should NOT include tools from groups not allowed by mode", () => {
|
||||
|
|
@ -539,9 +539,9 @@ describe("filterMcpToolsForMode", () => {
|
|||
}
|
||||
// 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
|
||||
expect(result.allowedTools.has("read_file")).toBe(true)
|
||||
expect(result.allowedTools.has("write_to_file")).toBe(false) // Not in allowed groups
|
||||
expect(result.allowedTools.has("apply_diff")).toBe(false) // Not in allowed groups
|
||||
})
|
||||
|
||||
it("should apply both exclude and include operations", () => {
|
||||
|
|
@ -553,10 +553,10 @@ describe("filterMcpToolsForMode", () => {
|
|||
includedTools: ["search_and_replace"], // Another edit tool (customTool)
|
||||
}
|
||||
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("search_and_replace")).toBe(true) // Included
|
||||
expect(result.allowedTools.has("read_file")).toBe(true)
|
||||
expect(result.allowedTools.has("write_to_file")).toBe(true)
|
||||
expect(result.allowedTools.has("apply_diff")).toBe(false) // Excluded
|
||||
expect(result.allowedTools.has("search_and_replace")).toBe(true) // Included
|
||||
})
|
||||
|
||||
it("should handle empty excludedTools and includedTools arrays", () => {
|
||||
|
|
@ -568,7 +568,7 @@ describe("filterMcpToolsForMode", () => {
|
|||
includedTools: [],
|
||||
}
|
||||
const result = applyModelToolCustomization(tools, codeMode, modelInfo)
|
||||
expect(result).toEqual(tools)
|
||||
expect(result.allowedTools).toEqual(tools)
|
||||
})
|
||||
|
||||
it("should ignore excluded tools that are not in the original set", () => {
|
||||
|
|
@ -579,9 +579,9 @@ describe("filterMcpToolsForMode", () => {
|
|||
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)
|
||||
expect(result.allowedTools.has("read_file")).toBe(true)
|
||||
expect(result.allowedTools.has("write_to_file")).toBe(true)
|
||||
expect(result.allowedTools.size).toBe(2)
|
||||
})
|
||||
|
||||
it("should NOT include customTools by default", () => {
|
||||
|
|
@ -594,8 +594,8 @@ describe("filterMcpToolsForMode", () => {
|
|||
}
|
||||
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)
|
||||
expect(result.allowedTools.has("read_file")).toBe(true)
|
||||
expect(result.allowedTools.has("write_to_file")).toBe(true)
|
||||
})
|
||||
|
||||
it("should NOT include tools that are not in any TOOL_GROUPS", () => {
|
||||
|
|
@ -606,8 +606,8 @@ describe("filterMcpToolsForMode", () => {
|
|||
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)
|
||||
expect(result.allowedTools.has("read_file")).toBe(true)
|
||||
expect(result.allowedTools.has("my_custom_tool")).toBe(false)
|
||||
})
|
||||
|
||||
it("should NOT include undefined tools even with allowed groups", () => {
|
||||
|
|
@ -619,8 +619,8 @@ describe("filterMcpToolsForMode", () => {
|
|||
}
|
||||
// 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)
|
||||
expect(result.allowedTools.has("read_file")).toBe(true)
|
||||
expect(result.allowedTools.has("custom_edit_tool")).toBe(false)
|
||||
})
|
||||
|
||||
describe("with customTools defined in TOOL_GROUPS", () => {
|
||||
|
|
@ -647,9 +647,9 @@ describe("filterMcpToolsForMode", () => {
|
|||
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
|
||||
expect(result.allowedTools.has("read_file")).toBe(true)
|
||||
expect(result.allowedTools.has("write_to_file")).toBe(true)
|
||||
expect(result.allowedTools.has("special_edit_tool")).toBe(true) // customTool should be included
|
||||
})
|
||||
|
||||
it("should NOT include customTools when not specified in includedTools", () => {
|
||||
|
|
@ -660,9 +660,9 @@ describe("filterMcpToolsForMode", () => {
|
|||
// 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
|
||||
expect(result.allowedTools.has("read_file")).toBe(true)
|
||||
expect(result.allowedTools.has("write_to_file")).toBe(true)
|
||||
expect(result.allowedTools.has("special_edit_tool")).toBe(false) // customTool should NOT be included by default
|
||||
})
|
||||
|
||||
it("should NOT include customTools from groups not allowed by mode", () => {
|
||||
|
|
@ -674,8 +674,8 @@ describe("filterMcpToolsForMode", () => {
|
|||
}
|
||||
// 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
|
||||
expect(result.allowedTools.has("read_file")).toBe(true)
|
||||
expect(result.allowedTools.has("special_edit_tool")).toBe(false) // customTool should NOT be included
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
@ -822,5 +822,31 @@ describe("filterMcpToolsForMode", () => {
|
|||
expect(toolNames).toContain("search_and_replace") // Included
|
||||
expect(toolNames).not.toContain("apply_diff") // Excluded
|
||||
})
|
||||
|
||||
it("should rename tools to alias names when model includes aliases", () => {
|
||||
const codeMode: ModeConfig = {
|
||||
slug: "code",
|
||||
name: "Code",
|
||||
roleDefinition: "Test",
|
||||
groups: ["read", "edit", "browser", "command", "mcp"] as const,
|
||||
}
|
||||
|
||||
const modelInfo: ModelInfo = {
|
||||
contextWindow: 100000,
|
||||
supportsPromptCache: false,
|
||||
includedTools: ["edit_file", "write_file"],
|
||||
}
|
||||
|
||||
const filtered = filterNativeToolsForMode(mockNativeTools, "code", [codeMode], {}, undefined, {
|
||||
modelInfo,
|
||||
})
|
||||
|
||||
const toolNames = filtered.map((t) => ("function" in t ? t.function.name : ""))
|
||||
|
||||
expect(toolNames).toContain("edit_file")
|
||||
expect(toolNames).toContain("write_file")
|
||||
expect(toolNames).not.toContain("apply_diff")
|
||||
expect(toolNames).not.toContain("write_to_file")
|
||||
})
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -1,11 +1,131 @@
|
|||
import type OpenAI from "openai"
|
||||
import type { ModeConfig, ToolName, ToolGroup, ModelInfo } from "@roo-code/types"
|
||||
import { getModeBySlug, getToolsForMode, isToolAllowedForMode } from "../../../shared/modes"
|
||||
import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS } from "../../../shared/tools"
|
||||
import { TOOL_GROUPS, ALWAYS_AVAILABLE_TOOLS, TOOL_ALIASES } from "../../../shared/tools"
|
||||
import { defaultModeSlug } from "../../../shared/modes"
|
||||
import type { CodeIndexManager } from "../../../services/code-index/manager"
|
||||
import type { McpHub } from "../../../services/mcp/McpHub"
|
||||
|
||||
/**
|
||||
* Reverse lookup map - maps alias name to canonical tool name.
|
||||
* Built once at module load from the central TOOL_ALIASES constant.
|
||||
*/
|
||||
const ALIAS_TO_CANONICAL: Map<string, string> = new Map(
|
||||
Object.entries(TOOL_ALIASES).map(([alias, canonical]) => [alias, canonical]),
|
||||
)
|
||||
|
||||
/**
|
||||
* Canonical to aliases map - maps canonical tool name to array of alias names.
|
||||
* Built once at module load from the central TOOL_ALIASES constant.
|
||||
*/
|
||||
const CANONICAL_TO_ALIASES: Map<string, string[]> = new Map()
|
||||
|
||||
// Build the reverse mapping (canonical -> aliases)
|
||||
for (const [alias, canonical] of Object.entries(TOOL_ALIASES)) {
|
||||
const existing = CANONICAL_TO_ALIASES.get(canonical) ?? []
|
||||
existing.push(alias)
|
||||
CANONICAL_TO_ALIASES.set(canonical, existing)
|
||||
}
|
||||
|
||||
/**
|
||||
* Pre-computed alias groups map - maps any tool name (canonical or alias) to its full group.
|
||||
* Built once at module load for O(1) lookup.
|
||||
*/
|
||||
const ALIAS_GROUPS: Map<string, readonly string[]> = new Map()
|
||||
|
||||
// Build alias groups for all tools
|
||||
for (const [canonical, aliases] of CANONICAL_TO_ALIASES.entries()) {
|
||||
const group = Object.freeze([canonical, ...aliases])
|
||||
// Map canonical to group
|
||||
ALIAS_GROUPS.set(canonical, group)
|
||||
// Map each alias to the same group
|
||||
for (const alias of aliases) {
|
||||
ALIAS_GROUPS.set(alias, group)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Cache for renamed tool definitions.
|
||||
* Maps "canonicalName:aliasName" to the pre-built tool definition.
|
||||
* This avoids creating new objects via spread operators on every assistant message.
|
||||
*/
|
||||
const RENAMED_TOOL_CACHE: Map<string, OpenAI.Chat.ChatCompletionTool> = new Map()
|
||||
|
||||
/**
|
||||
* Gets or creates a renamed tool definition with the alias name.
|
||||
* Uses RENAMED_TOOL_CACHE to avoid repeated object allocation.
|
||||
*
|
||||
* @param tool - The original tool definition
|
||||
* @param aliasName - The alias name to use
|
||||
* @returns Cached or newly created renamed tool definition
|
||||
*/
|
||||
function getOrCreateRenamedTool(
|
||||
tool: OpenAI.Chat.ChatCompletionTool,
|
||||
aliasName: string,
|
||||
): OpenAI.Chat.ChatCompletionTool {
|
||||
if (!("function" in tool) || !tool.function) {
|
||||
return tool
|
||||
}
|
||||
|
||||
const cacheKey = `${tool.function.name}:${aliasName}`
|
||||
let renamedTool = RENAMED_TOOL_CACHE.get(cacheKey)
|
||||
|
||||
if (!renamedTool) {
|
||||
renamedTool = {
|
||||
...tool,
|
||||
function: {
|
||||
...tool.function,
|
||||
name: aliasName,
|
||||
},
|
||||
}
|
||||
RENAMED_TOOL_CACHE.set(cacheKey, renamedTool)
|
||||
}
|
||||
|
||||
return renamedTool
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves a tool name to its canonical name.
|
||||
* If the tool name is an alias, returns the canonical tool name.
|
||||
* If it's already a canonical name or unknown, returns as-is.
|
||||
*
|
||||
* @param toolName - The tool name to resolve (may be an alias)
|
||||
* @returns The canonical tool name
|
||||
*/
|
||||
export function resolveToolAlias(toolName: string): string {
|
||||
const canonical = ALIAS_TO_CANONICAL.get(toolName)
|
||||
return canonical ?? toolName
|
||||
}
|
||||
|
||||
/**
|
||||
* Applies tool alias resolution to a set of allowed tools.
|
||||
* Resolves any aliases to their canonical tool names.
|
||||
*
|
||||
* @param allowedTools - Set of tools that may contain aliases
|
||||
* @returns Set with aliases resolved to canonical names
|
||||
*/
|
||||
export function applyToolAliases(allowedTools: Set<string>): Set<string> {
|
||||
const result = new Set<string>()
|
||||
|
||||
for (const tool of allowedTools) {
|
||||
// Resolve alias to canonical name
|
||||
result.add(resolveToolAlias(tool))
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all tools in an alias group (including the canonical tool).
|
||||
* Uses pre-computed ALIAS_GROUPS map for O(1) lookup.
|
||||
*
|
||||
* @param toolName - Any tool name in the alias group
|
||||
* @returns Array of all tool names in the alias group, or just the tool if not aliased
|
||||
*/
|
||||
export function getToolAliasGroup(toolName: string): readonly string[] {
|
||||
return ALIAS_GROUPS.get(toolName) ?? [toolName]
|
||||
}
|
||||
|
||||
/**
|
||||
* Apply model-specific tool customization to a set of allowed tools.
|
||||
*
|
||||
|
|
@ -18,21 +138,33 @@ import type { McpHub } from "../../../services/mcp/McpHub"
|
|||
* @param modelInfo - Model configuration with tool customization
|
||||
* @returns Modified set of tools after applying model customization
|
||||
*/
|
||||
/**
|
||||
* Result of applying model tool customization.
|
||||
* Contains the set of allowed tools and any alias renames to apply.
|
||||
*/
|
||||
interface ModelToolCustomizationResult {
|
||||
allowedTools: Set<string>
|
||||
/** Maps canonical tool name to alias name for tools that should be renamed */
|
||||
aliasRenames: Map<string, string>
|
||||
}
|
||||
|
||||
export function applyModelToolCustomization(
|
||||
allowedTools: Set<string>,
|
||||
modeConfig: ModeConfig,
|
||||
modelInfo?: ModelInfo,
|
||||
): Set<string> {
|
||||
): ModelToolCustomizationResult {
|
||||
if (!modelInfo) {
|
||||
return allowedTools
|
||||
return { allowedTools, aliasRenames: new Map() }
|
||||
}
|
||||
|
||||
const result = new Set(allowedTools)
|
||||
const aliasRenames = new Map<string, string>()
|
||||
|
||||
// Apply excluded tools (remove from allowed set)
|
||||
if (modelInfo.excludedTools && modelInfo.excludedTools.length > 0) {
|
||||
modelInfo.excludedTools.forEach((tool) => {
|
||||
result.delete(tool)
|
||||
const resolvedTool = resolveToolAlias(tool)
|
||||
result.delete(resolvedTool)
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -59,16 +191,21 @@ export function applyModelToolCustomization(
|
|||
)
|
||||
|
||||
// Add included tools only if they belong to an allowed group
|
||||
// This includes both regular tools and customTools
|
||||
// If the tool was specified as an alias, track the rename
|
||||
modelInfo.includedTools.forEach((tool) => {
|
||||
const toolGroup = toolToGroup.get(tool)
|
||||
const resolvedTool = resolveToolAlias(tool)
|
||||
const toolGroup = toolToGroup.get(resolvedTool)
|
||||
if (toolGroup && allowedGroups.has(toolGroup)) {
|
||||
result.add(tool)
|
||||
result.add(resolvedTool)
|
||||
// If the tool was specified as an alias, rename it in the API
|
||||
if (tool !== resolvedTool) {
|
||||
aliasRenames.set(resolvedTool, tool)
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
return result
|
||||
return { allowedTools: result, aliasRenames }
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -123,7 +260,12 @@ export function filterNativeToolsForMode(
|
|||
|
||||
// Apply model-specific tool customization
|
||||
const modelInfo = settings?.modelInfo as ModelInfo | undefined
|
||||
allowedToolNames = applyModelToolCustomization(allowedToolNames, modeConfig, modelInfo)
|
||||
const { allowedTools: customizedTools, aliasRenames } = applyModelToolCustomization(
|
||||
allowedToolNames,
|
||||
modeConfig,
|
||||
modelInfo,
|
||||
)
|
||||
allowedToolNames = customizedTools
|
||||
|
||||
// Conditionally exclude codebase_search if feature is disabled or not configured
|
||||
if (
|
||||
|
|
@ -163,14 +305,27 @@ export function filterNativeToolsForMode(
|
|||
allowedToolNames.delete("access_mcp_resource")
|
||||
}
|
||||
|
||||
// Filter native tools based on allowed tool names
|
||||
return nativeTools.filter((tool) => {
|
||||
// Filter native tools based on allowed tool names and apply alias renames
|
||||
const filteredTools: OpenAI.Chat.ChatCompletionTool[] = []
|
||||
|
||||
for (const tool of nativeTools) {
|
||||
// Handle both ChatCompletionTool and ChatCompletionCustomTool
|
||||
if ("function" in tool && tool.function) {
|
||||
return allowedToolNames.has(tool.function.name)
|
||||
const toolName = tool.function.name
|
||||
if (allowedToolNames.has(toolName)) {
|
||||
// Check if this tool should be renamed to an alias
|
||||
const aliasName = aliasRenames.get(toolName)
|
||||
if (aliasName) {
|
||||
// Use cached renamed tool definition to avoid per-message object allocation
|
||||
filteredTools.push(getOrCreateRenamedTool(tool, aliasName))
|
||||
} else {
|
||||
filteredTools.push(tool)
|
||||
}
|
||||
}
|
||||
}
|
||||
return false
|
||||
})
|
||||
}
|
||||
|
||||
return filteredTools
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
@ -232,7 +387,16 @@ export function isToolAllowedInMode(
|
|||
}
|
||||
|
||||
// Check if the tool is allowed by the mode's groups
|
||||
return isToolAllowedForMode(toolName, modeSlug, customModes ?? [], undefined, undefined, experiments ?? {})
|
||||
// Resolve to canonical name and check that single value
|
||||
const canonicalTool = resolveToolAlias(toolName)
|
||||
return isToolAllowedForMode(
|
||||
canonicalTool as ToolName,
|
||||
modeSlug,
|
||||
customModes ?? [],
|
||||
undefined,
|
||||
undefined,
|
||||
experiments ?? {},
|
||||
)
|
||||
}
|
||||
|
||||
/**
|
||||
|
|
|
|||
|
|
@ -3122,10 +3122,16 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
|
|||
// nativeArgs is already in the correct API format for all tools
|
||||
const input = toolUse.nativeArgs || toolUse.params
|
||||
|
||||
// Use originalName (alias) if present for API history consistency.
|
||||
// When tool aliases are used (e.g., "edit_file" -> "search_and_replace"),
|
||||
// we want the alias name in the conversation history to match what the model
|
||||
// was told the tool was named, preventing confusion in multi-turn conversations.
|
||||
const toolNameForHistory = toolUse.originalName ?? toolUse.name
|
||||
|
||||
assistantContent.push({
|
||||
type: "tool_use" as const,
|
||||
id: toolCallId,
|
||||
name: toolUse.name,
|
||||
name: toolNameForHistory,
|
||||
input,
|
||||
})
|
||||
}
|
||||
|
|
|
|||
|
|
@ -120,6 +120,12 @@ export interface ToolUse<TName extends ToolName = ToolName> {
|
|||
type: "tool_use"
|
||||
id?: string // Optional ID to track tool calls
|
||||
name: TName
|
||||
/**
|
||||
* The original tool name as called by the model (e.g. an alias like "edit_file"),
|
||||
* if it differs from the canonical tool name used for execution.
|
||||
* Used to preserve tool names in API conversation history.
|
||||
*/
|
||||
originalName?: string
|
||||
// params is a partial record, allowing only some or none of the possible parameters to be used
|
||||
params: Partial<Record<ToolParamName, string>>
|
||||
partial: boolean
|
||||
|
|
@ -293,6 +299,22 @@ export const ALWAYS_AVAILABLE_TOOLS: ToolName[] = [
|
|||
"run_slash_command",
|
||||
] as const
|
||||
|
||||
/**
|
||||
* Central registry of tool aliases.
|
||||
* Maps alias name -> canonical tool name.
|
||||
*
|
||||
* This allows models to use alternative names for tools (e.g., "edit_file" instead of "apply_diff").
|
||||
* When a model calls a tool by its alias, the system resolves it to the canonical name for execution,
|
||||
* but preserves the alias in API conversation history for consistency.
|
||||
*
|
||||
* To add a new alias, simply add an entry here. No other files need to be modified.
|
||||
*/
|
||||
export const TOOL_ALIASES: Record<string, ToolName> = {
|
||||
edit_file: "apply_diff",
|
||||
write_file: "write_to_file",
|
||||
temp_edit_file: "search_and_replace",
|
||||
} as const
|
||||
|
||||
export type DiffResult =
|
||||
| { success: true; content: string; failParts?: DiffResult[] }
|
||||
| ({
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue