feat: add toolAliases support for model-specific tool renaming

Add the ability to rename tools for a model using toolAliases config.
Format: toolAliases: ["apply_diff:edit_file", "write_to_file:write_file"]

This allows models to see tools with alternative names while the
internal execution uses the original tool names.
This commit is contained in:
Hannes Rudolph 2025-12-09 23:46:35 -07:00
parent 03912d8399
commit 700c2531c7
8 changed files with 742 additions and 13 deletions

View file

@ -120,6 +120,10 @@ export const modelInfoSchema = z.object({
// 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(),
// Define aliases for specific tools in this model (only applies to native protocol)
// Format: ["originalName:aliasName", ...] e.g., ["apply_diff:edit"]
// The tool will be presented to the model with the alias name, but executed as the original
toolAliases: z.array(z.string()).optional(),
/**
* Service tiers with pricing information.
* Each tier can have a name (for OpenAI service tiers) and pricing overrides.

View file

@ -68,6 +68,48 @@ export class NativeToolCallParser {
}
>()
// Reverse alias map for mapping aliased tools back to original names
// Key: alias tool name (e.g., "edit"), Value: original tool name (e.g., "apply_diff")
private static toolAliasReverseMap = new Map<string, string>()
/**
* Set the reverse alias map for the current request.
* This maps alias tool names back to their original names.
* Should be called before processing tool calls for each new request.
*
* @param reverseMap - Map from alias tool name to original tool name
*/
public static setToolAliasReverseMap(reverseMap: Map<string, string>): void {
this.toolAliasReverseMap = reverseMap
}
/**
* Clear the tool alias reverse map.
* Should be called when starting a new request to prevent stale mappings.
*/
public static clearToolAliasReverseMap(): void {
this.toolAliasReverseMap.clear()
}
/**
* Map a tool name back to its original name if it was aliased.
* Returns the original name if found, otherwise returns the input name.
*
* @param name - The tool name to resolve (possibly aliased)
* @returns The original tool name
*/
public static resolveOriginalToolName(name: string): string {
return this.toolAliasReverseMap.get(name) || name
}
/**
* Resolve and return both presented (aliased) and original tool names.
*/
public static resolveToolNames(name: string): { original: string; presented: string } {
const original = this.toolAliasReverseMap.get(name) || name
return { original, presented: name }
}
/**
* Process a raw tool call chunk from the API stream.
* Handles tracking, buffering, and emits start/delta/end events.
@ -247,12 +289,17 @@ export class NativeToolCallParser {
try {
const partialArgs = parseJSON(toolCall.argumentsAccumulator)
// Resolve the tool name in case it was renamed and retain presented name
const { original, presented } = this.resolveToolNames(toolCall.name)
const resolvedName = original as ToolName
// Create partial ToolUse with extracted values
return this.createPartialToolUse(
toolCall.id,
toolCall.name as ToolName,
resolvedName,
partialArgs || {},
true, // partial
presented,
)
} catch {
// Even partial-json-parser can fail on severely malformed JSON
@ -334,6 +381,7 @@ export class NativeToolCallParser {
name: ToolName,
partialArgs: Record<string, any>,
partial: boolean,
presentedName?: string,
): ToolUse | null {
// Build legacy params for display
// NOTE: For streaming partial updates, we MUST populate params even for complex types
@ -525,11 +573,13 @@ export class NativeToolCallParser {
params,
partial,
nativeArgs,
...(presentedName ? { presentedName } : {}),
}
}
/**
* Convert a native tool call chunk to a ToolUse object.
* Handles renamed tools by resolving to their original names for execution.
*
* @param toolCall - The native tool call from the API stream
* @returns A properly typed ToolUse object
@ -544,9 +594,16 @@ export class NativeToolCallParser {
return this.parseDynamicMcpTool(toolCall)
}
// Validate tool name
if (!toolNames.includes(toolCall.name as ToolName)) {
// Resolve renamed tools back to their original names
// This handles cases where a tool was renamed via modelInfo.renamedTools
// e.g., if "apply_diff" was renamed to "edit_file", we resolve "edit_file" back to "apply_diff"
const { original, presented } = this.resolveToolNames(toolCall.name as string)
const resolvedName = original as TName
// Validate tool name (using resolved name)
if (!toolNames.includes(resolvedName as ToolName)) {
console.error(`Invalid tool name: ${toolCall.name}`)
console.error(`Resolved name: ${resolvedName}`)
console.error(`Valid tool names:`, toolNames)
return null
}
@ -563,13 +620,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
}
@ -589,7 +646,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>
@ -778,10 +835,11 @@ 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,
...(presented !== resolvedName ? { presentedName: presented } : {}),
}
return result

View file

@ -0,0 +1,259 @@
// npx vitest run core/assistant-message/__tests__/NativeToolCallParser.tool-aliasing.spec.ts
import { NativeToolCallParser } from "../NativeToolCallParser"
describe("NativeToolCallParser Tool Aliasing", () => {
beforeEach(() => {
// Clear any existing reverse map before each test
NativeToolCallParser.clearToolAliasReverseMap()
})
afterEach(() => {
// Clean up after each test
NativeToolCallParser.clearToolAliasReverseMap()
})
describe("setToolAliasReverseMap", () => {
it("should store the reverse map", () => {
const reverseMap = new Map([
["edit_file", "apply_diff"],
["create_file", "write_to_file"],
])
NativeToolCallParser.setToolAliasReverseMap(reverseMap)
// Verify by resolving names
expect(NativeToolCallParser.resolveOriginalToolName("edit_file")).toBe("apply_diff")
expect(NativeToolCallParser.resolveOriginalToolName("create_file")).toBe("write_to_file")
})
it("should overwrite existing map", () => {
const firstMap = new Map([["edit_file", "apply_diff"]])
const secondMap = new Map([["new_alias", "original_name"]])
NativeToolCallParser.setToolAliasReverseMap(firstMap)
NativeToolCallParser.setToolAliasReverseMap(secondMap)
// First map should no longer be effective
expect(NativeToolCallParser.resolveOriginalToolName("edit_file")).toBe("edit_file")
// Second map should be effective
expect(NativeToolCallParser.resolveOriginalToolName("new_alias")).toBe("original_name")
})
})
describe("clearToolAliasReverseMap", () => {
it("should clear the reverse map", () => {
const reverseMap = new Map([["edit_file", "apply_diff"]])
NativeToolCallParser.setToolAliasReverseMap(reverseMap)
// Verify it works
expect(NativeToolCallParser.resolveOriginalToolName("edit_file")).toBe("apply_diff")
// Clear and verify it's gone
NativeToolCallParser.clearToolAliasReverseMap()
expect(NativeToolCallParser.resolveOriginalToolName("edit_file")).toBe("edit_file")
})
})
describe("resolveOriginalToolName", () => {
it("should return original name for aliased tool", () => {
const reverseMap = new Map([["edit_file", "apply_diff"]])
NativeToolCallParser.setToolAliasReverseMap(reverseMap)
expect(NativeToolCallParser.resolveOriginalToolName("edit_file")).toBe("apply_diff")
})
it("should resolve both presented and original names", () => {
const reverseMap = new Map([["edit", "apply_diff"]])
NativeToolCallParser.setToolAliasReverseMap(reverseMap)
const result = NativeToolCallParser.resolveToolNames("edit")
expect(result.original).toBe("apply_diff")
expect(result.presented).toBe("edit")
})
it("should return same name if not in reverse map", () => {
const reverseMap = new Map([["edit_file", "apply_diff"]])
NativeToolCallParser.setToolAliasReverseMap(reverseMap)
expect(NativeToolCallParser.resolveOriginalToolName("read_file")).toBe("read_file")
})
it("should return same name when reverse map is empty", () => {
expect(NativeToolCallParser.resolveOriginalToolName("apply_diff")).toBe("apply_diff")
})
it("should handle multiple aliased tools", () => {
const reverseMap = new Map([
["edit_file", "apply_diff"],
["create_file", "write_to_file"],
["view_file", "read_file"],
])
NativeToolCallParser.setToolAliasReverseMap(reverseMap)
expect(NativeToolCallParser.resolveOriginalToolName("edit_file")).toBe("apply_diff")
expect(NativeToolCallParser.resolveOriginalToolName("create_file")).toBe("write_to_file")
expect(NativeToolCallParser.resolveOriginalToolName("view_file")).toBe("read_file")
expect(NativeToolCallParser.resolveOriginalToolName("list_files")).toBe("list_files") // Not aliased
})
})
describe("parseToolCall with aliased tools", () => {
beforeEach(() => {
// Set up a reverse map for testing
const reverseMap = new Map([
["edit_file", "apply_diff"],
["create_file", "write_to_file"],
])
NativeToolCallParser.setToolAliasReverseMap(reverseMap)
})
it("should resolve aliased tool back to original name", () => {
const toolCall = {
id: "test-id-1",
name: "edit_file" as any, // Model calls it "edit_file"
arguments: JSON.stringify({ path: "test.ts", diff: "some diff content" }),
}
const result = NativeToolCallParser.parseToolCall(toolCall)
expect(result).not.toBeNull()
expect(result!.type).toBe("tool_use")
// The resolved name should be the original "apply_diff"
if (result!.type === "tool_use") {
expect(result!.name).toBe("apply_diff")
}
})
it("should parse non-aliased tools normally", () => {
const toolCall = {
id: "test-id-2",
name: "read_file" as any,
arguments: JSON.stringify({
files: [{ path: "test.ts" }],
}),
}
const result = NativeToolCallParser.parseToolCall(toolCall)
expect(result).not.toBeNull()
expect(result!.type).toBe("tool_use")
if (result!.type === "tool_use") {
expect(result!.name).toBe("read_file") // Still "read_file", not aliased
}
})
it("should preserve tool arguments when resolving aliased tool", () => {
const toolCall = {
id: "test-id-3",
name: "edit_file" as any,
arguments: JSON.stringify({
path: "src/test.ts",
diff: "<<<<<<< SEARCH\nold\n=======\nnew\n>>>>>>> REPLACE",
}),
}
const result = NativeToolCallParser.parseToolCall(toolCall)
expect(result).not.toBeNull()
if (result!.type === "tool_use") {
expect(result!.name).toBe("apply_diff")
expect(result!.nativeArgs).toEqual({
path: "src/test.ts",
diff: "<<<<<<< SEARCH\nold\n=======\nnew\n>>>>>>> REPLACE",
})
}
})
})
describe("processStreamingChunk with aliased tools", () => {
beforeEach(() => {
// Set up a reverse map for testing
const reverseMap = new Map([
["edit_file", "apply_diff"],
["edit", "apply_diff"],
["create_file", "write_to_file"],
])
NativeToolCallParser.setToolAliasReverseMap(reverseMap)
NativeToolCallParser.clearAllStreamingToolCalls()
})
afterEach(() => {
NativeToolCallParser.clearAllStreamingToolCalls()
})
it("should resolve aliased tool in streaming chunks", () => {
// Start streaming with aliased tool name
NativeToolCallParser.startStreamingToolCall("stream-1", "edit_file")
// Process a chunk with partial arguments
const partialResult = NativeToolCallParser.processStreamingChunk(
"stream-1",
'{"path": "test.ts", "diff": "some diff"',
)
// The result should have the original tool name, not the aliased one
expect(partialResult).not.toBeNull()
if (partialResult) {
expect(partialResult.name).toBe("apply_diff") // Resolved to original name
expect(partialResult.partial).toBe(true)
}
})
it("should resolve short aliased tool names in streaming chunks", () => {
// Start streaming with a very short aliased tool name like "edit"
NativeToolCallParser.startStreamingToolCall("stream-2", "edit")
// Process a chunk with partial arguments
const partialResult = NativeToolCallParser.processStreamingChunk(
"stream-2",
'{"path": "file.ts", "diff": "x"}',
)
// The result should have the original tool name
expect(partialResult).not.toBeNull()
if (partialResult) {
expect(partialResult.name).toBe("apply_diff") // Resolved to original name
expect(partialResult.nativeArgs).toEqual({
path: "file.ts",
diff: "x",
})
}
})
it("should handle non-aliased tools in streaming", () => {
// Start streaming with a non-aliased tool
NativeToolCallParser.startStreamingToolCall("stream-3", "read_file")
// Process a chunk
const partialResult = NativeToolCallParser.processStreamingChunk(
"stream-3",
'{"files": [{"path": "test.ts"}]}',
)
// The result should keep the original name
expect(partialResult).not.toBeNull()
if (partialResult) {
expect(partialResult.name).toBe("read_file")
}
})
it("should finalize streaming tool call with resolved name", () => {
// Start streaming with aliased tool name
NativeToolCallParser.startStreamingToolCall("stream-4", "edit_file")
// Process some chunks
NativeToolCallParser.processStreamingChunk("stream-4", '{"path": "test.ts", ')
NativeToolCallParser.processStreamingChunk("stream-4", '"diff": "complete diff"}')
// Finalize
const finalResult = NativeToolCallParser.finalizeStreamingToolCall("stream-4")
expect(finalResult).not.toBeNull()
if (finalResult && finalResult.type === "tool_use") {
expect(finalResult.name).toBe("apply_diff") // Resolved to original name
expect(finalResult.partial).toBe(false)
}
})
})
})

View file

@ -0,0 +1,292 @@
// npx vitest run core/prompts/tools/__tests__/tool-aliasing.spec.ts
import type OpenAI from "openai"
import { parseToolAliases, createReverseAliasMap, applyToolAliases } from "../filter-tools-for-mode"
// End-to-end test for order-independent behavior
describe("Tool Aliasing Order Independence", () => {
const createMockTool = (name: string): OpenAI.Chat.ChatCompletionTool => ({
type: "function",
function: {
name,
description: `Mock ${name} tool`,
parameters: { type: "object", properties: {}, required: [] },
},
})
const mockTools: OpenAI.Chat.ChatCompletionTool[] = [
createMockTool("apply_diff"),
createMockTool("write_to_file"),
createMockTool("read_file"),
]
it("should alias to short names (e.g., edit)", () => {
const aliasMap = parseToolAliases(["apply_diff:edit"])
const reverseMap = createReverseAliasMap(aliasMap)
const aliasedTools = applyToolAliases(mockTools, aliasMap)
expect(aliasMap.get("apply_diff")).toBe("edit")
expect(reverseMap.get("edit")).toBe("apply_diff")
const toolNames = aliasedTools.map((t: OpenAI.Chat.ChatCompletionTool) =>
t.type === "function" ? t.function.name : "",
)
expect(toolNames).toContain("edit")
expect(toolNames).not.toContain("apply_diff")
})
it("should alias both tools regardless of order in config (order 1)", () => {
const toolAliasesConfig = ["write_to_file:write_file", "apply_diff:replace"]
const aliasMap = parseToolAliases(toolAliasesConfig)
const reverseMap = createReverseAliasMap(aliasMap)
const aliasedTools = applyToolAliases(mockTools, aliasMap)
// Check alias map has both
expect(aliasMap.size).toBe(2)
expect(aliasMap.get("write_to_file")).toBe("write_file")
expect(aliasMap.get("apply_diff")).toBe("replace")
// Check reverse map has both
expect(reverseMap.size).toBe(2)
expect(reverseMap.get("write_file")).toBe("write_to_file")
expect(reverseMap.get("replace")).toBe("apply_diff")
// Check tools are aliased
const toolNames = aliasedTools.map((t: OpenAI.Chat.ChatCompletionTool) =>
t.type === "function" ? t.function.name : "",
)
expect(toolNames).toContain("write_file")
expect(toolNames).toContain("replace")
expect(toolNames).toContain("read_file") // Not aliased
expect(toolNames).not.toContain("write_to_file")
expect(toolNames).not.toContain("apply_diff")
})
it("should alias both tools regardless of order in config (order 2)", () => {
const toolAliasesConfig = ["apply_diff:replace", "write_to_file:write_file"]
const aliasMap = parseToolAliases(toolAliasesConfig)
const reverseMap = createReverseAliasMap(aliasMap)
const aliasedTools = applyToolAliases(mockTools, aliasMap)
// Check alias map has both
expect(aliasMap.size).toBe(2)
expect(aliasMap.get("apply_diff")).toBe("replace")
expect(aliasMap.get("write_to_file")).toBe("write_file")
// Check reverse map has both
expect(reverseMap.size).toBe(2)
expect(reverseMap.get("replace")).toBe("apply_diff")
expect(reverseMap.get("write_file")).toBe("write_to_file")
// Check tools are aliased
const toolNames = aliasedTools.map((t: OpenAI.Chat.ChatCompletionTool) =>
t.type === "function" ? t.function.name : "",
)
expect(toolNames).toContain("replace")
expect(toolNames).toContain("write_file")
expect(toolNames).toContain("read_file") // Not aliased
expect(toolNames).not.toContain("apply_diff")
expect(toolNames).not.toContain("write_to_file")
})
})
/**
* Helper to get function name from a ChatCompletionTool
*/
function getFunctionName(tool: OpenAI.Chat.ChatCompletionTool): string {
if ("function" in tool && tool.function) {
return tool.function.name
}
throw new Error("Tool does not have function property")
}
/**
* Helper to get function description from a ChatCompletionTool
*/
function getFunctionDescription(tool: OpenAI.Chat.ChatCompletionTool): string | undefined {
if ("function" in tool && tool.function) {
return tool.function.description
}
throw new Error("Tool does not have function property")
}
/**
* Helper to get function parameters from a ChatCompletionTool
*/
function getFunctionParameters(tool: OpenAI.Chat.ChatCompletionTool): OpenAI.FunctionParameters | undefined {
if ("function" in tool && tool.function) {
return tool.function.parameters
}
throw new Error("Tool does not have function property")
}
describe("Tool Aliasing", () => {
describe("parseToolAliases", () => {
it("should return empty map for undefined input", () => {
const result = parseToolAliases(undefined)
expect(result.size).toBe(0)
})
it("should return empty map for empty array", () => {
const result = parseToolAliases([])
expect(result.size).toBe(0)
})
it("should parse single alias specification", () => {
const result = parseToolAliases(["apply_diff:edit_file"])
expect(result.size).toBe(1)
expect(result.get("apply_diff")).toBe("edit_file")
})
it("should parse multiple alias specifications", () => {
const result = parseToolAliases(["apply_diff:edit_file", "write_to_file:create_file"])
expect(result.size).toBe(2)
expect(result.get("apply_diff")).toBe("edit_file")
expect(result.get("write_to_file")).toBe("create_file")
})
it("should ignore invalid specs without colon", () => {
const result = parseToolAliases(["apply_diff", "valid:alias"])
expect(result.size).toBe(1)
expect(result.get("valid")).toBe("alias")
})
it("should ignore specs with colon at start", () => {
const result = parseToolAliases([":alias_name"])
expect(result.size).toBe(0)
})
it("should ignore specs with colon at end", () => {
const result = parseToolAliases(["original_name:"])
expect(result.size).toBe(0)
})
it("should handle tool names with underscores", () => {
const result = parseToolAliases(["my_long_tool_name:short_name"])
expect(result.size).toBe(1)
expect(result.get("my_long_tool_name")).toBe("short_name")
})
it("should handle alias names with multiple colons (only first colon is delimiter)", () => {
const result = parseToolAliases(["original:name:with:colons"])
expect(result.size).toBe(1)
expect(result.get("original")).toBe("name:with:colons")
})
})
describe("createReverseAliasMap", () => {
it("should create empty map from empty input", () => {
const aliasMap = new Map<string, string>()
const result = createReverseAliasMap(aliasMap)
expect(result.size).toBe(0)
})
it("should reverse single mapping", () => {
const aliasMap = new Map([["apply_diff", "edit_file"]])
const result = createReverseAliasMap(aliasMap)
expect(result.size).toBe(1)
expect(result.get("edit_file")).toBe("apply_diff")
})
it("should reverse multiple mappings", () => {
const aliasMap = new Map([
["apply_diff", "edit_file"],
["write_to_file", "create_file"],
])
const result = createReverseAliasMap(aliasMap)
expect(result.size).toBe(2)
expect(result.get("edit_file")).toBe("apply_diff")
expect(result.get("create_file")).toBe("write_to_file")
})
})
describe("applyToolAliases", () => {
const createMockTool = (name: string): OpenAI.Chat.ChatCompletionTool => ({
type: "function",
function: {
name,
description: `Mock ${name} tool`,
parameters: {
type: "object",
properties: {},
},
},
})
it("should return original tools when alias map is empty", () => {
const tools = [createMockTool("apply_diff"), createMockTool("read_file")]
const aliasMap = new Map<string, string>()
const result = applyToolAliases(tools, aliasMap)
expect(result).toEqual(tools)
expect(getFunctionName(result[0])).toBe("apply_diff")
expect(getFunctionName(result[1])).toBe("read_file")
})
it("should alias a single tool", () => {
const tools = [createMockTool("apply_diff"), createMockTool("read_file")]
const aliasMap = new Map([["apply_diff", "edit_file"]])
const result = applyToolAliases(tools, aliasMap)
expect(getFunctionName(result[0])).toBe("edit_file")
expect(getFunctionName(result[1])).toBe("read_file")
})
it("should alias multiple tools", () => {
const tools = [createMockTool("apply_diff"), createMockTool("write_to_file"), createMockTool("read_file")]
const aliasMap = new Map([
["apply_diff", "edit_file"],
["write_to_file", "create_file"],
])
const result = applyToolAliases(tools, aliasMap)
expect(getFunctionName(result[0])).toBe("edit_file")
expect(getFunctionName(result[1])).toBe("create_file")
expect(getFunctionName(result[2])).toBe("read_file")
})
it("should not modify tools that are not in the alias map", () => {
const tools = [createMockTool("read_file"), createMockTool("list_files")]
const aliasMap = new Map([["apply_diff", "edit_file"]])
const result = applyToolAliases(tools, aliasMap)
expect(getFunctionName(result[0])).toBe("read_file")
expect(getFunctionName(result[1])).toBe("list_files")
})
it("should preserve tool description and parameters after aliasing", () => {
const tool: OpenAI.Chat.ChatCompletionTool = {
type: "function",
function: {
name: "apply_diff",
description: "Apply a diff to a file",
parameters: {
type: "object",
properties: {
path: { type: "string" },
diff: { type: "string" },
},
required: ["path", "diff"],
},
},
}
const aliasMap = new Map([["apply_diff", "edit_file"]])
const result = applyToolAliases([tool], aliasMap)
expect(getFunctionName(result[0])).toBe("edit_file")
expect(getFunctionDescription(result[0])).toBe("Apply a diff to a file")
expect(getFunctionParameters(result[0])).toEqual(tool.function.parameters)
})
it("should return new array without mutating original tools", () => {
const tools = [createMockTool("apply_diff")]
const aliasMap = new Map([["apply_diff", "edit_file"]])
const result = applyToolAliases(tools, aliasMap)
// Original should be unchanged
expect(getFunctionName(tools[0])).toBe("apply_diff")
// Result should be aliased
expect(getFunctionName(result[0])).toBe("edit_file")
})
})
})

View file

@ -6,6 +6,81 @@ import { defaultModeSlug } from "../../../shared/modes"
import type { CodeIndexManager } from "../../../services/code-index/manager"
import type { McpHub } from "../../../services/mcp/McpHub"
/**
* Parses a toolAliases configuration array and returns a map from original name to alias name.
*
* @param toolAliases - Array of alias strings in format "originalName:aliasName"
* @returns Map from original tool name to alias tool name
*/
export function parseToolAliases(toolAliases: string[] | undefined): Map<string, string> {
const aliasMap = new Map<string, string>()
if (!toolAliases || toolAliases.length === 0) {
return aliasMap
}
for (const aliasSpec of toolAliases) {
const colonIndex = aliasSpec.indexOf(":")
if (colonIndex > 0 && colonIndex < aliasSpec.length - 1) {
const originalName = aliasSpec.substring(0, colonIndex)
const aliasName = aliasSpec.substring(colonIndex + 1)
aliasMap.set(originalName, aliasName)
}
}
return aliasMap
}
/**
* Creates a reverse mapping from alias tool names back to original names.
*
* @param aliasMap - Map from original name to alias name
* @returns Map from alias tool name to original tool name
*/
export function createReverseAliasMap(aliasMap: Map<string, string>): Map<string, string> {
const reverseMap = new Map<string, string>()
for (const [original, alias] of aliasMap) {
reverseMap.set(alias, original)
}
return reverseMap
}
/**
* Applies tool aliasing to a set of native tools.
*
* @param tools - Array of native tools to alias
* @param aliasMap - Map from original tool name to alias name
* @returns Array of tools with names replaced according to the alias map
*/
export function applyToolAliases(
tools: OpenAI.Chat.ChatCompletionTool[],
aliasMap: Map<string, string>,
): OpenAI.Chat.ChatCompletionTool[] {
if (aliasMap.size === 0) {
return tools
}
return tools.map((tool) => {
if (tool.type !== "function") {
return tool
}
const aliasName = aliasMap.get(tool.function.name)
if (!aliasName) {
return tool
}
// Create a new tool object with the aliased function name
return {
...tool,
function: {
...tool.function,
name: aliasName,
},
}
})
}
/**
* Apply model-specific tool customization to a set of allowed tools.
*

View file

@ -2389,6 +2389,8 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
// Clear any leftover streaming tool call state from previous interrupted streams
NativeToolCallParser.clearAllStreamingToolCalls()
NativeToolCallParser.clearRawChunkState()
// Clear previous tool alias mappings (will be re-set when building tools for this request)
NativeToolCallParser.clearToolAliasReverseMap()
await this.diffViewProvider.reset()
@ -3109,7 +3111,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
assistantContent.push({
type: "tool_use" as const,
id: toolCallId,
name: toolUse.name,
// Use presentedName for API history so the model sees consistent tool names
// (the name it originally called, which may be a renamed version)
name: toolUse.presentedName || toolUse.name,
input,
})
}
@ -3703,7 +3707,7 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
throw new Error("Provider reference lost during tool building")
}
allTools = await buildNativeToolsArray({
const toolsResult = await buildNativeToolsArray({
provider,
cwd: this.cwd,
mode,
@ -3715,6 +3719,9 @@ export class Task extends EventEmitter<TaskEvents> implements TaskLike {
modelInfo,
diffEnabled: this.diffEnabled,
})
allTools = toolsResult.tools
// Set the reverse alias map for parsing tool calls back to original names
NativeToolCallParser.setToolAliasReverseMap(toolsResult.toolAliasReverseMap)
}
// Parallel tool calls are disabled - feature is on hold

View file

@ -2,7 +2,13 @@ import type OpenAI from "openai"
import type { ProviderSettings, ModeConfig, ModelInfo } from "@roo-code/types"
import type { ClineProvider } from "../webview/ClineProvider"
import { getNativeTools, getMcpServerTools } from "../prompts/tools/native-tools"
import { filterNativeToolsForMode, filterMcpToolsForMode } from "../prompts/tools/filter-tools-for-mode"
import {
filterNativeToolsForMode,
filterMcpToolsForMode,
parseToolAliases,
applyToolAliases,
createReverseAliasMap,
} from "../prompts/tools/filter-tools-for-mode"
interface BuildToolsOptions {
provider: ClineProvider
@ -17,14 +23,25 @@ interface BuildToolsOptions {
diffEnabled: boolean
}
/**
* Result of building native tools array.
*/
export interface BuildToolsResult {
/** Array of filtered and optionally aliased native and MCP tools */
tools: OpenAI.Chat.ChatCompletionTool[]
/** Map from alias tool name back to original name (for reverse lookup when parsing tool calls) */
toolAliasReverseMap: Map<string, string>
}
/**
* Builds the complete tools array for native protocol requests.
* Combines native tools and MCP tools, filtered by mode restrictions.
* Applies any tool aliases specified in modelInfo.toolAliases.
*
* @param options - Configuration options for building the tools
* @returns Array of filtered native and MCP tools
* @returns Object containing filtered tools and reverse alias map
*/
export async function buildNativeToolsArray(options: BuildToolsOptions): Promise<OpenAI.Chat.ChatCompletionTool[]> {
export async function buildNativeToolsArray(options: BuildToolsOptions): Promise<BuildToolsResult> {
const {
provider,
cwd,
@ -73,5 +90,20 @@ export async function buildNativeToolsArray(options: BuildToolsOptions): Promise
const mcpTools = getMcpServerTools(mcpHub)
const filteredMcpTools = filterMcpToolsForMode(mcpTools, mode, customModes, experiments)
return [...filteredNativeTools, ...filteredMcpTools]
// Combine filtered tools
let allTools = [...filteredNativeTools, ...filteredMcpTools]
// Apply tool aliases if specified in modelInfo
const aliasMap = parseToolAliases(modelInfo?.toolAliases)
if (aliasMap.size > 0) {
allTools = applyToolAliases(allTools, aliasMap)
}
// Create reverse map for parsing tool calls back to original names
const toolAliasReverseMap = createReverseAliasMap(aliasMap)
return {
tools: allTools,
toolAliasReverseMap,
}
}

View file

@ -121,6 +121,8 @@ export interface ToolUse<TName extends ToolName = ToolName> {
type: "tool_use"
id?: string // Optional ID to track tool calls
name: TName
/** The name as presented by the model (may be renamed). Falls back to `name` when not renamed. */
presentedName?: 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