feat: add native tool calling support to OpenAI-compatible providers

- Updated FeatherlessHandler to support tools in DeepSeek-R1 path
- Updated ZAiHandler to pass tools and tool_choice in createStream
- Updated GroqHandler to handle tool calls in createMessage
- All providers now utilize the native tool calling support from BaseOpenAiCompatibleProvider
This commit is contained in:
Roo Code 2025-11-19 14:25:28 +00:00
parent 18c4d1ac41
commit 7a6ee82554
3 changed files with 80 additions and 3 deletions

View file

@ -31,6 +31,7 @@ export class FeatherlessHandler extends BaseOpenAiCompatibleProvider<Featherless
private getCompletionParams(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
metadata?: any,
): OpenAI.Chat.Completions.ChatCompletionCreateParamsStreaming {
const {
id: model,
@ -46,15 +47,21 @@ export class FeatherlessHandler extends BaseOpenAiCompatibleProvider<Featherless
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
...(metadata?.tools && { tools: this.convertToolsForOpenAI(metadata.tools) }),
...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }),
}
}
override async *createMessage(systemPrompt: string, messages: Anthropic.Messages.MessageParam[]): ApiStream {
override async *createMessage(
systemPrompt: string,
messages: Anthropic.Messages.MessageParam[],
metadata?: any,
): ApiStream {
const model = this.getModel()
if (model.id.includes("DeepSeek-R1")) {
const stream = await this.client.chat.completions.create({
...this.getCompletionParams(systemPrompt, messages),
...this.getCompletionParams(systemPrompt, messages, metadata),
messages: convertToR1Format([{ role: "user", content: systemPrompt }, ...messages]),
})
@ -67,8 +74,11 @@ export class FeatherlessHandler extends BaseOpenAiCompatibleProvider<Featherless
}) as const,
)
const toolCallAccumulator = new Map<number, { id: string; name: string; arguments: string }>()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const finishReason = chunk.choices[0]?.finish_reason
if (delta?.content) {
for (const processedChunk of matcher.update(delta.content)) {
@ -76,6 +86,37 @@ export class FeatherlessHandler extends BaseOpenAiCompatibleProvider<Featherless
}
}
if (delta?.tool_calls) {
for (const toolCall of delta.tool_calls) {
const index = toolCall.index
const existing = toolCallAccumulator.get(index)
if (existing) {
if (toolCall.function?.arguments) {
existing.arguments += toolCall.function.arguments
}
} else {
toolCallAccumulator.set(index, {
id: toolCall.id || "",
name: toolCall.function?.name || "",
arguments: toolCall.function?.arguments || "",
})
}
}
}
if (finishReason === "tool_calls") {
for (const toolCall of toolCallAccumulator.values()) {
yield {
type: "tool_call",
id: toolCall.id,
name: toolCall.name,
arguments: toolCall.arguments,
}
}
toolCallAccumulator.clear()
}
if (chunk.usage) {
yield {
type: "usage",
@ -90,7 +131,7 @@ export class FeatherlessHandler extends BaseOpenAiCompatibleProvider<Featherless
yield processedChunk
}
} else {
yield* super.createMessage(systemPrompt, messages)
yield* super.createMessage(systemPrompt, messages, metadata)
}
}

View file

@ -37,8 +37,11 @@ export class GroqHandler extends BaseOpenAiCompatibleProvider<GroqModelId> {
): ApiStream {
const stream = await this.createStream(systemPrompt, messages, metadata)
const toolCallAccumulator = new Map<number, { id: string; name: string; arguments: string }>()
for await (const chunk of stream) {
const delta = chunk.choices[0]?.delta
const finishReason = chunk.choices[0]?.finish_reason
if (delta?.content) {
yield {
@ -47,6 +50,37 @@ export class GroqHandler extends BaseOpenAiCompatibleProvider<GroqModelId> {
}
}
if (delta?.tool_calls) {
for (const toolCall of delta.tool_calls) {
const index = toolCall.index
const existing = toolCallAccumulator.get(index)
if (existing) {
if (toolCall.function?.arguments) {
existing.arguments += toolCall.function.arguments
}
} else {
toolCallAccumulator.set(index, {
id: toolCall.id || "",
name: toolCall.function?.name || "",
arguments: toolCall.function?.arguments || "",
})
}
}
}
if (finishReason === "tool_calls") {
for (const toolCall of toolCallAccumulator.values()) {
yield {
type: "tool_call",
id: toolCall.id,
name: toolCall.name,
arguments: toolCall.arguments,
}
}
toolCallAccumulator.clear()
}
if (chunk.usage) {
yield* this.yieldUsage(chunk.usage as GroqUsage)
}

View file

@ -64,6 +64,8 @@ export class ZAiHandler extends BaseOpenAiCompatibleProvider<string> {
messages: [{ role: "system", content: systemPrompt }, ...convertToOpenAiMessages(messages)],
stream: true,
stream_options: { include_usage: true },
...(metadata?.tools && { tools: this.convertToolsForOpenAI(metadata.tools) }),
...(metadata?.tool_choice && { tool_choice: metadata.tool_choice }),
}
// Add thinking parameter if reasoning is enabled and model supports it