mirror of
https://github.com/RooVetGit/Roo-Code.git
synced 2026-10-07 02:58:15 +00:00
fix: handle empty response with reasoning in Requesty provider
Add safeguard to emit placeholder text when model returns only reasoning/thinking content without actual text or tool calls. This prevents "empty assistant response" errors that can occur when models output malformed tool call syntax in their reasoning. Includes tests for reasoning-only response handling.
This commit is contained in:
parent
3c05cae722
commit
5f5c00ffc1
2 changed files with 136 additions and 0 deletions
|
|
@ -2,6 +2,7 @@
|
|||
|
||||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { t } from "i18next"
|
||||
|
||||
import { TOOL_PROTOCOL } from "@roo-code/types"
|
||||
|
||||
|
|
@ -378,6 +379,125 @@ describe("RequestyHandler", () => {
|
|||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("reasoning-only response handling", () => {
|
||||
it("should emit placeholder text when model returns only reasoning content", async () => {
|
||||
const handler = new RequestyHandler(mockOptions)
|
||||
|
||||
// Mock stream that only returns reasoning content without actual text or tool calls
|
||||
const mockStreamWithOnlyReasoning = {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
yield {
|
||||
id: "test-id",
|
||||
choices: [
|
||||
{
|
||||
delta: {
|
||||
reasoning_content: "I am thinking about how to respond...",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
yield {
|
||||
id: "test-id",
|
||||
choices: [
|
||||
{
|
||||
delta: {
|
||||
reasoning_content:
|
||||
"The user wants me to use a tool, but I'll format it wrong: <tool_call><function=get_weather>",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
yield {
|
||||
id: "test-id",
|
||||
choices: [{ delta: {} }],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 20 },
|
||||
}
|
||||
},
|
||||
}
|
||||
mockCreate.mockResolvedValue(mockStreamWithOnlyReasoning)
|
||||
|
||||
const systemPrompt = "test system prompt"
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user" as const, content: "test message" }]
|
||||
|
||||
const chunks = []
|
||||
for await (const chunk of handler.createMessage(systemPrompt, messages)) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
// Expect two reasoning chunks, one fallback text chunk, and one usage chunk
|
||||
expect(chunks).toHaveLength(4)
|
||||
expect(chunks[0]).toEqual({ type: "reasoning", text: "I am thinking about how to respond..." })
|
||||
expect(chunks[1]).toEqual({
|
||||
type: "reasoning",
|
||||
text: "The user wants me to use a tool, but I'll format it wrong: <tool_call><function=get_weather>",
|
||||
})
|
||||
// The fallback text to prevent empty response error
|
||||
expect(chunks[2]).toEqual({
|
||||
type: "text",
|
||||
text: t("common:errors.gemini.thinking_complete_no_output"),
|
||||
})
|
||||
expect(chunks[3]).toMatchObject({
|
||||
type: "usage",
|
||||
inputTokens: 10,
|
||||
outputTokens: 20,
|
||||
})
|
||||
})
|
||||
|
||||
it("should not emit placeholder when model returns actual content", async () => {
|
||||
const handler = new RequestyHandler(mockOptions)
|
||||
|
||||
// Mock stream that returns both reasoning and text content
|
||||
const mockStreamWithContent = {
|
||||
async *[Symbol.asyncIterator]() {
|
||||
yield {
|
||||
id: "test-id",
|
||||
choices: [
|
||||
{
|
||||
delta: {
|
||||
reasoning_content: "Thinking...",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
yield {
|
||||
id: "test-id",
|
||||
choices: [
|
||||
{
|
||||
delta: {
|
||||
content: "Here is my actual response",
|
||||
},
|
||||
},
|
||||
],
|
||||
}
|
||||
yield {
|
||||
id: "test-id",
|
||||
choices: [{ delta: {} }],
|
||||
usage: { prompt_tokens: 10, completion_tokens: 20 },
|
||||
}
|
||||
},
|
||||
}
|
||||
mockCreate.mockResolvedValue(mockStreamWithContent)
|
||||
|
||||
const systemPrompt = "test system prompt"
|
||||
const messages: Anthropic.Messages.MessageParam[] = [{ role: "user" as const, content: "test message" }]
|
||||
|
||||
const chunks = []
|
||||
for await (const chunk of handler.createMessage(systemPrompt, messages)) {
|
||||
chunks.push(chunk)
|
||||
}
|
||||
|
||||
// Expect one reasoning chunk, one text chunk, and one usage chunk (no fallback)
|
||||
expect(chunks).toHaveLength(3)
|
||||
expect(chunks[0]).toEqual({ type: "reasoning", text: "Thinking..." })
|
||||
expect(chunks[1]).toEqual({ type: "text", text: "Here is my actual response" })
|
||||
expect(chunks[2]).toMatchObject({
|
||||
type: "usage",
|
||||
inputTokens: 10,
|
||||
outputTokens: 20,
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe("completePrompt", () => {
|
||||
|
|
|
|||
|
|
@ -1,5 +1,6 @@
|
|||
import { Anthropic } from "@anthropic-ai/sdk"
|
||||
import OpenAI from "openai"
|
||||
import { t } from "i18next"
|
||||
|
||||
import { type ModelInfo, requestyDefaultModelId, requestyDefaultModelInfo, TOOL_PROTOCOL } from "@roo-code/types"
|
||||
|
||||
|
|
@ -166,19 +167,26 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan
|
|||
}
|
||||
let lastUsage: any = undefined
|
||||
|
||||
// Track whether we've received actual content vs just reasoning
|
||||
let hasContent = false
|
||||
let hasReasoning = false
|
||||
|
||||
for await (const chunk of stream) {
|
||||
const delta = chunk.choices[0]?.delta
|
||||
|
||||
if (delta?.content) {
|
||||
hasContent = true
|
||||
yield { type: "text", text: delta.content }
|
||||
}
|
||||
|
||||
if (delta && "reasoning_content" in delta && delta.reasoning_content) {
|
||||
hasReasoning = true
|
||||
yield { type: "reasoning", text: (delta.reasoning_content as string | undefined) || "" }
|
||||
}
|
||||
|
||||
// Handle native tool calls
|
||||
if (delta && "tool_calls" in delta && Array.isArray(delta.tool_calls)) {
|
||||
hasContent = true
|
||||
for (const toolCall of delta.tool_calls) {
|
||||
yield {
|
||||
type: "tool_call_partial",
|
||||
|
|
@ -195,6 +203,14 @@ export class RequestyHandler extends BaseProvider implements SingleCompletionHan
|
|||
}
|
||||
}
|
||||
|
||||
// If model produced reasoning but no actual content (text or tool calls),
|
||||
// emit a placeholder to prevent "empty assistant response" errors.
|
||||
// This can happen when models output malformed tool call syntax in their
|
||||
// reasoning/thinking content (e.g., <tool_call><function=...> tags).
|
||||
if (hasReasoning && !hasContent) {
|
||||
yield { type: "text", text: t("common:errors.gemini.thinking_complete_no_output") }
|
||||
}
|
||||
|
||||
if (lastUsage) {
|
||||
yield this.processUsageMetrics(lastUsage, info)
|
||||
}
|
||||
|
|
|
|||
Loading…
Add table
Reference in a new issue