From 52c824fee89e6cb27935cc53a01539bc1fb0d713 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Wed, 19 Nov 2025 02:17:21 +0000 Subject: [PATCH] feat: add native tool calling support to LiteLLM provider - Add tool call accumulator in createMessage for streaming support - Handle tool_calls delta and accumulate arguments incrementally - Yield complete tool calls when finish_reason is "tool_calls" - Pass tools and tool_choice from metadata to API requests - Add metadata parameter to completePrompt for non-streaming support - Set parallel_tool_calls to false to ensure sequential execution - Follow same implementation pattern as OpenRouter and OpenAI providers --- src/api/providers/lite-llm.ts | 45 ++++++++++++++++++++++++++++++++++- 1 file changed, 44 insertions(+), 1 deletion(-) diff --git a/src/api/providers/lite-llm.ts b/src/api/providers/lite-llm.ts index 43bf33c38b..5be1c7042e 100644 --- a/src/api/providers/lite-llm.ts +++ b/src/api/providers/lite-llm.ts @@ -123,6 +123,9 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa stream_options: { include_usage: true, }, + parallel_tool_calls: false, // Ensure only one tool call at a time + ...(metadata?.tools && { tools: metadata.tools }), + ...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }), } // GPT-5 models require max_completion_tokens instead of the deprecated max_tokens parameter @@ -140,15 +143,53 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa const { data: completion } = await this.client.chat.completions.create(requestOptions).withResponse() let lastUsage + const toolCallAccumulator = new Map() for await (const chunk of completion) { const delta = chunk.choices[0]?.delta + const finishReason = chunk.choices[0]?.finish_reason const usage = chunk.usage as LiteLLMUsage if (delta?.content) { yield { type: "text", text: delta.content } } + // Check for tool calls in delta + if (delta?.tool_calls) { + for (const toolCall of delta.tool_calls) { + const index = toolCall.index + const existing = toolCallAccumulator.get(index) + + if (existing) { + // Accumulate arguments for existing tool call + if (toolCall.function?.arguments) { + existing.arguments += toolCall.function.arguments + } + } else { + // Start new tool call accumulation + toolCallAccumulator.set(index, { + id: toolCall.id || "", + name: toolCall.function?.name || "", + arguments: toolCall.function?.arguments || "", + }) + } + } + } + + // When finish_reason is 'tool_calls', yield all accumulated tool calls + if (finishReason === "tool_calls" && toolCallAccumulator.size > 0) { + for (const toolCall of toolCallAccumulator.values()) { + yield { + type: "tool_call", + id: toolCall.id, + name: toolCall.name, + arguments: toolCall.arguments, + } + } + // Clear accumulator after yielding + toolCallAccumulator.clear() + } + if (usage) { lastUsage = usage } @@ -192,7 +233,7 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa } } - async completePrompt(prompt: string): Promise { + async completePrompt(prompt: string, metadata?: ApiHandlerCreateMessageMetadata): Promise { const { id: modelId, info } = await this.fetchModel() // Check if this is a GPT-5 model that requires max_completion_tokens instead of max_tokens @@ -202,6 +243,8 @@ export class LiteLLMHandler extends RouterProvider implements SingleCompletionHa const requestOptions: OpenAI.Chat.Completions.ChatCompletionCreateParamsNonStreaming = { model: modelId, messages: [{ role: "user", content: prompt }], + ...(metadata?.tools && { tools: metadata.tools }), + ...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }), } if (this.supportsTemperature(modelId)) {