From 157146aff2a15fa74c32014893a006589c3ff207 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Sat, 20 Dec 2025 19:35:16 +0000 Subject: [PATCH] fix: exclude parallel_tool_calls for Bedrock models in LiteLLM provider --- src/api/providers/__tests__/lite-llm.spec.ts | 190 +++++++++++++++++++ src/api/providers/lite-llm.ts | 19 +- 2 files changed, 208 insertions(+), 1 deletion(-) diff --git a/src/api/providers/__tests__/lite-llm.spec.ts b/src/api/providers/__tests__/lite-llm.spec.ts index a95118469e..a13ca420a6 100644 --- a/src/api/providers/__tests__/lite-llm.spec.ts +++ b/src/api/providers/__tests__/lite-llm.spec.ts @@ -40,6 +40,17 @@ vi.mock("../fetchers/modelCache", () => ({ "claude-3-opus": { ...litellmDefaultModelInfo, maxTokens: 8192 }, "llama-3": { ...litellmDefaultModelInfo, maxTokens: 8192 }, "gpt-4-turbo": { ...litellmDefaultModelInfo, maxTokens: 8192 }, + "bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0": { + ...litellmDefaultModelInfo, + maxTokens: 8192, + supportsNativeTools: true, + }, + "anthropic.claude-sonnet-4-20250514-v1:0": { + ...litellmDefaultModelInfo, + maxTokens: 8192, + supportsNativeTools: true, + }, + "amazon.titan-text-express-v1": { ...litellmDefaultModelInfo, maxTokens: 8192, supportsNativeTools: true }, }) }), getModelsFromCache: vi.fn().mockReturnValue(undefined), @@ -388,4 +399,183 @@ describe("LiteLLMHandler", () => { expect(createCall.max_completion_tokens).toBeUndefined() }) }) + + describe("Bedrock model handling", () => { + it("should exclude parallel_tool_calls for Bedrock models when using native tools", async () => { + const bedrockModels = [ + "bedrock/anthropic.claude-3-5-sonnet-20241022-v2:0", + "anthropic.claude-sonnet-4-20250514-v1:0", + "amazon.titan-text-express-v1", + ] + + for (const modelId of bedrockModels) { + vi.clearAllMocks() + + const options: ApiHandlerOptions = { + ...mockOptions, + litellmModelId: modelId, + } + handler = new LiteLLMHandler(options) + + const systemPrompt = "You are a helpful assistant" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test" }] + + // Mock the stream response + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { + choices: [{ delta: { content: "Response" } }], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + }, + } + }, + } + + mockCreate.mockReturnValue({ + withResponse: vi.fn().mockResolvedValue({ data: mockStream }), + }) + + const metadata = { + taskId: "test-task", + tools: [ + { + type: "function" as const, + function: { + name: "test_tool", + description: "A test tool", + parameters: { type: "object", properties: {} }, + }, + }, + ], + toolProtocol: "native" as const, + parallelToolCalls: true, + } + + const generator = handler.createMessage(systemPrompt, messages, metadata) + for await (const chunk of generator) { + // Consume the generator + } + + // Verify that parallel_tool_calls is NOT included for Bedrock models + const createCall = mockCreate.mock.calls[0][0] + expect(createCall.parallel_tool_calls).toBeUndefined() + expect(createCall.tools).toBeDefined() // Tools should still be present + } + }) + + it("should include parallel_tool_calls for non-Bedrock models when using native tools", async () => { + const nonBedrockModels = ["gpt-4", "claude-3-opus", "gpt-4-turbo"] + + for (const modelId of nonBedrockModels) { + vi.clearAllMocks() + + const options: ApiHandlerOptions = { + ...mockOptions, + litellmModelId: modelId, + } + handler = new LiteLLMHandler(options) + + const systemPrompt = "You are a helpful assistant" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test" }] + + // Mock the stream response + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { + choices: [{ delta: { content: "Response" } }], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + }, + } + }, + } + + mockCreate.mockReturnValue({ + withResponse: vi.fn().mockResolvedValue({ data: mockStream }), + }) + + const metadata = { + taskId: "test-task", + tools: [ + { + type: "function" as const, + function: { + name: "test_tool", + description: "A test tool", + parameters: { type: "object", properties: {} }, + }, + }, + ], + toolProtocol: "native" as const, + parallelToolCalls: true, + } + + const generator = handler.createMessage(systemPrompt, messages, metadata) + for await (const chunk of generator) { + // Consume the generator + } + + // Verify that parallel_tool_calls IS included for non-Bedrock models + const createCall = mockCreate.mock.calls[0][0] + expect(createCall.parallel_tool_calls).toBe(true) + expect(createCall.tools).toBeDefined() + } + }) + + it("should default parallel_tool_calls to false for non-Bedrock models when not specified", async () => { + const options: ApiHandlerOptions = { + ...mockOptions, + litellmModelId: "gpt-4", + } + handler = new LiteLLMHandler(options) + + const systemPrompt = "You are a helpful assistant" + const messages: Anthropic.Messages.MessageParam[] = [{ role: "user", content: "Test" }] + + // Mock the stream response + const mockStream = { + async *[Symbol.asyncIterator]() { + yield { + choices: [{ delta: { content: "Response" } }], + usage: { + prompt_tokens: 10, + completion_tokens: 5, + }, + } + }, + } + + mockCreate.mockReturnValue({ + withResponse: vi.fn().mockResolvedValue({ data: mockStream }), + }) + + const metadata = { + taskId: "test-task", + tools: [ + { + type: "function" as const, + function: { + name: "test_tool", + description: "A test tool", + parameters: { type: "object", properties: {} }, + }, + }, + ], + toolProtocol: "native" as const, + // parallelToolCalls not specified + } + + const generator = handler.createMessage(systemPrompt, messages, metadata) + for await (const chunk of generator) { + // Consume the generator + } + + // Verify that parallel_tool_calls defaults to false + const createCall = mockCreate.mock.calls[0][0] + expect(createCall.parallel_tool_calls).toBe(false) + }) + }) }) diff --git a/src/api/providers/lite-llm.ts b/src/api/providers/lite-llm.ts index 9acbc35d07..76fdd3731c 100644 --- a/src/api/providers/lite-llm.ts +++ b/src/api/providers/lite-llm.ts @@ -38,6 +38,21 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa return /\bgpt-?5(?!\d)/i.test(modelId) } + /** + * Check if the model is routed through AWS Bedrock + * Bedrock doesn't support the parallel_tool_calls parameter + */ + private isBedrockModel(modelId: string): boolean { + const lowerModel = modelId.toLowerCase() + return ( + lowerModel.includes("bedrock") || + lowerModel.startsWith("anthropic.") || + lowerModel.includes("amazon.") || + // Match AWS Bedrock model ID patterns + /^(anthropic|amazon|ai21|cohere|meta|mistral)\./.test(lowerModel) + ) + } + override async *createMessage( systemPrompt: string, messages: Anthropic.Messages.MessageParam[], @@ -133,7 +148,9 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa }, ...(useNativeTools && { tools: this.convertToolsForOpenAI(metadata.tools) }), ...(useNativeTools && metadata.tool_choice && { tool_choice: metadata.tool_choice }), - ...(useNativeTools && { parallel_tool_calls: metadata?.parallelToolCalls ?? false }), + // Bedrock doesn't support parallel_tool_calls parameter, so exclude it for Bedrock models + ...(useNativeTools && + !this.isBedrockModel(modelId) && { parallel_tool_calls: metadata?.parallelToolCalls ?? false }), } // GPT-5 models require max_completion_tokens instead of the deprecated max_tokens parameter