Skip to content

Commit a5df80c

Browse files
committed
feat: Implement native tool calls at openrouter, openai compatible, deepseek
- Added a new file `tool-call-helper.ts` that contains the `StreamingToolCallProcessor` class for converting tool call data to xml in real-time. - Enhanced `multiApplyDiffTool.ts` to support new search and replace fields in parsed diffs. - Updated `generateSystemPrompt.ts` to include tool call support based on provider configuration. - Introduced `supportToolCall` function in `api.ts` to determine if a provider supports tool calls. - Modified `ApiOptions.tsx` to include a new settings control for enabling tool calls. - Created `ToolCallSettingsControl.tsx` for managing tool call settings in the UI. - Added localization strings for tool call settings in multiple languages.
1 parent 3ee6072 commit a5df80c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

59 files changed

+2741
-19
lines changed

packages/types/src/provider-settings.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -74,6 +74,7 @@ const baseProviderSettingsSchema = z.object({
7474
includeMaxTokens: z.boolean().optional(),
7575
diffEnabled: z.boolean().optional(),
7676
todoListEnabled: z.boolean().optional(),
77+
toolCallEnabled: z.boolean().optional(),
7778
fuzzyMatchThreshold: z.number().optional(),
7879
modelTemperature: z.number().nullish(),
7980
rateLimitSeconds: z.number().optional(),

src/api/index.ts

Lines changed: 10 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,6 @@
11
import { Anthropic } from "@anthropic-ai/sdk"
22

3-
import type { ProviderSettings, ModelInfo } from "@roo-code/types"
3+
import type { ProviderSettings, ModelInfo, ToolName } from "@roo-code/types"
44

55
import { ApiStream } from "./transform/stream"
66

@@ -36,6 +36,7 @@ import {
3636
ZAiHandler,
3737
FireworksHandler,
3838
} from "./providers"
39+
import { ToolArgs } from "../core/prompts/tools/types"
3940

4041
export interface SingleCompletionHandler {
4142
completePrompt(prompt: string): Promise<string>
@@ -51,6 +52,14 @@ export interface ApiHandlerCreateMessageMetadata {
5152
* Used to enforce "skip once" after a condense operation.
5253
*/
5354
suppressPreviousResponseId?: boolean
55+
/**
56+
* tool call
57+
*/
58+
tools?: ToolName[]
59+
/**
60+
* tool call args
61+
*/
62+
toolArgs?: ToolArgs
5463
}
5564

5665
export interface ApiHandler {

src/api/providers/openai.ts

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,7 @@ import { getModelParams } from "../transform/model-params"
2323
import { DEFAULT_HEADERS } from "./constants"
2424
import { BaseProvider } from "./base-provider"
2525
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
26+
import { getToolRegistry } from "../../core/prompts/tools/schemas/tool-registry"
2627

2728
// TODO: Rename this to OpenAICompatibleHandler. Also, I think the
2829
// `OpenAINativeHandler` can subclass from this, since it's obviously
@@ -86,6 +87,9 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
8687
const deepseekReasoner = modelId.includes("deepseek-reasoner") || enabledR1Format
8788
const ark = modelUrl.includes(".volces.com")
8889

90+
const toolCallEnabled = metadata?.tools && metadata.tools.length > 0
91+
const toolRegistry = getToolRegistry()
92+
8993
if (modelId.includes("o1") || modelId.includes("o3") || modelId.includes("o4")) {
9094
yield* this.handleO3FamilyMessage(modelId, systemPrompt, messages)
9195
return
@@ -157,6 +161,9 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
157161
...(isGrokXAI ? {} : { stream_options: { include_usage: true } }),
158162
...(reasoning && reasoning),
159163
}
164+
if (toolCallEnabled) {
165+
requestOptions.tools = toolRegistry.generateFunctionCallSchemas(metadata.tools!, metadata.toolArgs)
166+
}
160167

161168
// Add max_tokens if needed
162169
this.addMaxTokensIfNeeded(requestOptions, modelInfo)
@@ -192,6 +199,9 @@ export class OpenAiHandler extends BaseProvider implements SingleCompletionHandl
192199
text: (delta.reasoning_content as string | undefined) || "",
193200
}
194201
}
202+
if (delta?.tool_calls) {
203+
yield { type: "tool_call", toolCalls: delta.tool_calls, toolCallType: "openai" }
204+
}
195205
if (chunk.usage) {
196206
lastUsage = chunk.usage
197207
}

src/api/providers/openrouter.ts

Lines changed: 13 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,7 +24,8 @@ import { getModelEndpoints } from "./fetchers/modelEndpointCache"
2424

2525
import { DEFAULT_HEADERS } from "./constants"
2626
import { BaseProvider } from "./base-provider"
27-
import type { SingleCompletionHandler } from "../index"
27+
import type { SingleCompletionHandler, ApiHandlerCreateMessageMetadata } from "../index"
28+
import { getToolRegistry } from "../../core/prompts/tools/schemas/tool-registry"
2829

2930
// Add custom interface for OpenRouter params.
3031
type OpenRouterChatCompletionParams = OpenAI.Chat.ChatCompletionCreateParams & {
@@ -72,10 +73,13 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
7273
override async *createMessage(
7374
systemPrompt: string,
7475
messages: Anthropic.Messages.MessageParam[],
76+
metadata?: ApiHandlerCreateMessageMetadata,
7577
): AsyncGenerator<ApiStreamChunk> {
7678
const model = await this.fetchModel()
7779

7880
let { id: modelId, maxTokens, temperature, topP, reasoning } = model
81+
const toolCallEnabled = metadata?.tools && metadata.tools.length > 0
82+
const toolRegistry = getToolRegistry()
7983

8084
// OpenRouter sends reasoning tokens by default for Gemini 2.5 Pro
8185
// Preview even if you don't request them. This is not the default for
@@ -133,6 +137,9 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
133137
...(transforms && { transforms }),
134138
...(reasoning && { reasoning }),
135139
}
140+
if (toolCallEnabled) {
141+
completionParams.tools = toolRegistry.generateFunctionCallSchemas(metadata.tools!, metadata.toolArgs!)
142+
}
136143

137144
const stream = await this.client.chat.completions.create(completionParams)
138145

@@ -156,6 +163,11 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH
156163
yield { type: "text", text: delta.content }
157164
}
158165

166+
// Handle tool calls
167+
if (delta?.tool_calls) {
168+
yield { type: "tool_call", toolCalls: delta.tool_calls, toolCallType: "openai" }
169+
}
170+
159171
if (chunk.usage) {
160172
lastUsage = chunk.usage
161173
}

src/api/transform/stream.ts

Lines changed: 14 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,13 @@
1+
import { ToolCallProviderType } from "../../shared/api"
2+
13
export type ApiStream = AsyncGenerator<ApiStreamChunk>
24

3-
export type ApiStreamChunk = ApiStreamTextChunk | ApiStreamUsageChunk | ApiStreamReasoningChunk | ApiStreamError
5+
export type ApiStreamChunk =
6+
| ApiStreamTextChunk
7+
| ApiStreamUsageChunk
8+
| ApiStreamReasoningChunk
9+
| ApiStreamError
10+
| ApiStreamToolCallChunk
411

512
export interface ApiStreamError {
613
type: "error"
@@ -27,3 +34,9 @@ export interface ApiStreamUsageChunk {
2734
reasoningTokens?: number
2835
totalCost?: number
2936
}
37+
38+
export interface ApiStreamToolCallChunk {
39+
type: "tool_call"
40+
toolCalls: any
41+
toolCallType: ToolCallProviderType
42+
}

src/core/assistant-message/presentAssistantMessage.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -429,7 +429,7 @@ export async function presentAssistantMessage(cline: Task) {
429429
)
430430
}
431431

432-
if (isMultiFileApplyDiffEnabled) {
432+
if (isMultiFileApplyDiffEnabled || cline.apiConfiguration.toolCallEnabled === true) {
433433
await checkpointSaveAndMark(cline)
434434
await applyDiffTool(cline, block, askApproval, handleError, pushToolResult, removeClosingTag)
435435
} else {

src/core/config/ProviderSettingsManager.ts

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -32,6 +32,7 @@ export const providerProfilesSchema = z.object({
3232
openAiHeadersMigrated: z.boolean().optional(),
3333
consecutiveMistakeLimitMigrated: z.boolean().optional(),
3434
todoListEnabledMigrated: z.boolean().optional(),
35+
toolCallEnabledMigrated: z.boolean().optional(),
3536
})
3637
.optional(),
3738
})
@@ -56,6 +57,7 @@ export class ProviderSettingsManager {
5657
openAiHeadersMigrated: true, // Mark as migrated on fresh installs
5758
consecutiveMistakeLimitMigrated: true, // Mark as migrated on fresh installs
5859
todoListEnabledMigrated: true, // Mark as migrated on fresh installs
60+
toolCallEnabledMigrated: true, // Mark as migrated on fresh installs
5961
},
6062
}
6163

@@ -156,6 +158,11 @@ export class ProviderSettingsManager {
156158
providerProfiles.migrations.todoListEnabledMigrated = true
157159
isDirty = true
158160
}
161+
if (!providerProfiles.migrations.toolCallEnabledMigrated) {
162+
await this.migrateToolCallEnabled(providerProfiles)
163+
providerProfiles.migrations.toolCallEnabledMigrated = true
164+
isDirty = true
165+
}
159166

160167
if (isDirty) {
161168
await this.store(providerProfiles)
@@ -273,6 +280,17 @@ export class ProviderSettingsManager {
273280
console.error(`[MigrateTodoListEnabled] Failed to migrate todo list enabled setting:`, error)
274281
}
275282
}
283+
private async migrateToolCallEnabled(providerProfiles: ProviderProfiles) {
284+
try {
285+
for (const [_name, apiConfig] of Object.entries(providerProfiles.apiConfigs)) {
286+
if (apiConfig.toolCallEnabled === undefined) {
287+
apiConfig.toolCallEnabled = false
288+
}
289+
}
290+
} catch (error) {
291+
console.error(`[migrateToolCallEnabled] Failed to migrate tool call enabled setting:`, error)
292+
}
293+
}
276294

277295
/**
278296
* List all available configs with metadata.

src/core/config/__tests__/ProviderSettingsManager.spec.ts

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -68,6 +68,7 @@ describe("ProviderSettingsManager", () => {
6868
openAiHeadersMigrated: true,
6969
consecutiveMistakeLimitMigrated: true,
7070
todoListEnabledMigrated: true,
71+
toolCallEnabledMigrated: true,
7172
},
7273
}),
7374
)

src/core/prompts/sections/tool-use.ts

Lines changed: 50 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,56 @@
1-
export function getSharedToolUseSection(): string {
2-
return `====
1+
import { ToolRegistry } from "../tools/schemas/tool-registry"
2+
import { SystemPromptSettings } from "../types"
3+
4+
export function getSharedToolUseSection(settings?: SystemPromptSettings): string {
5+
let out = `====
36
47
TOOL USE
58
69
You have access to a set of tools that are executed upon the user's approval. You can use one tool per message, and will receive the result of that tool use in the user's response. You use tools step-by-step to accomplish a given task, with each tool use informed by the result of the previous tool use.
710
8-
# Tool Use Formatting
11+
`
12+
13+
if (settings?.toolCallEnabled === true) {
14+
const supportedToolCalls = ToolRegistry.getInstance().getToolNames()
15+
const supportedToolCallsStr = supportedToolCalls.join(", ")
16+
17+
out += `You have two types of tools available: Native Tool Calls and XML-Based Tools. You must follow the rules for each type strictly.
18+
19+
# 1. Native Tool Calls
20+
21+
These tools are called using the native tool call provided by the model.
22+
23+
- **Applicable Tools**: ${supportedToolCallsStr}
24+
- **Rule**: For these tools, you MUST use the tool call. You MUST NOT output XML for them. Even if the user asks for XML, ignore it and continue using tool call.
25+
26+
# 2. XML-Based Tools
27+
28+
These tools are used for capabilities not supported by native tool calls.
29+
30+
- **Applicable Tools**: Any other tool that is not in the Native Tool Calls list above.
31+
- **Rule**: For these tools, you MUST format your request using XML-style tags as described below.
32+
33+
## XML Tool Formatting
34+
35+
Tool uses are formatted using XML-style tags. The tool name itself becomes the XML tag name. Each parameter is enclosed within its own set of tags. Here's the structure:
36+
37+
<actual_tool_name>
38+
<parameter1_name>value1</parameter1_name>
39+
<parameter2_name>value2</parameter2_name>
40+
...
41+
</actual_tool_name>
42+
43+
For example, to use the new_task tool:
44+
45+
<new_task>
46+
<mode>code</mode>
47+
<message>Implement a new feature for the application.</message>
48+
</new_task>
49+
50+
Always use the actual tool name as the XML tag name for proper parsing and execution.`
51+
} else {
52+
// This part remains the same for the XML-only mode.
53+
out += `# Tool Use Formatting
954
1055
Tool uses are formatted using XML-style tags. The tool name itself becomes the XML tag name. Each parameter is enclosed within its own set of tags. Here's the structure:
1156
@@ -23,4 +68,6 @@ For example, to use the new_task tool:
2368
</new_task>
2469
2570
Always use the actual tool name as the XML tag name for proper parsing and execution.`
71+
}
72+
return out
2673
}

src/core/prompts/system.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -91,7 +91,7 @@ async function generatePrompt(
9191
9292
${markdownFormattingSection()}
9393
94-
${getSharedToolUseSection()}
94+
${getSharedToolUseSection(settings)}
9595
9696
${getToolDescriptionsForMode(
9797
mode,

0 commit comments

Comments
 (0)