From f80136f66fa906f6de73ec6cd7df12c23711f5a9 Mon Sep 17 00:00:00 2001 From: Roo Code Date: Tue, 16 Dec 2025 19:39:52 +0000 Subject: [PATCH] fix: handle empty response with reasoning in OpenRouter provider --- src/api/providers/openrouter.ts | 19 +++++++++++++++++++ 1 file changed, 19 insertions(+) diff --git a/src/api/providers/openrouter.ts b/src/api/providers/openrouter.ts index 20a0f58b994..00f5913c5fc 100644 --- a/src/api/providers/openrouter.ts +++ b/src/api/providers/openrouter.ts @@ -1,6 +1,7 @@ import { Anthropic } from "@anthropic-ai/sdk" import OpenAI from "openai" import { z } from "zod" +import { t } from "i18next" import { openRouterDefaultModelId, @@ -374,6 +375,12 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH } >() + // Track whether we received actual content vs just reasoning + // This prevents "Empty assistant response" errors when models output + // reasoning but no text/tool content (e.g., malformed tool calls in thinking) + let hasContent = false + let hasReasoning = false + for await (const chunk of stream) { // OpenRouter returns an error object instead of the OpenAI SDK throwing an error. if ("error" in chunk) { @@ -445,17 +452,20 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH // Note: reasoning.encrypted types are intentionally skipped as they contain redacted content if (reasoningText) { + hasReasoning = true yield { type: "reasoning", text: reasoningText } } } } else if ("reasoning" in delta && delta.reasoning && typeof delta.reasoning === "string") { // Handle legacy reasoning format - only if reasoning_details is not present // See: https://openrouter.ai/docs/use-cases/reasoning-tokens + hasReasoning = true yield { type: "reasoning", text: delta.reasoning } } // Emit raw tool call chunks - NativeToolCallParser handles state management if ("tool_calls" in delta && Array.isArray(delta.tool_calls)) { + hasContent = true for (const toolCall of delta.tool_calls) { yield { type: "tool_call_partial", @@ -468,6 +478,7 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH } if (delta.content) { + hasContent = true yield { type: "text", text: delta.content } } } @@ -491,6 +502,14 @@ export class OpenRouterHandler extends BaseProvider implements SingleCompletionH this.currentReasoningDetails = Array.from(reasoningDetailsAccumulator.values()) } + // If we had reasoning but no content, emit a placeholder text to prevent + // "Empty assistant response" errors. This typically happens when some models + // output malformed tool calls in their thinking content (e.g., tags) + // instead of using proper tool calling mechanisms. + if (hasReasoning && !hasContent) { + yield { type: "text", text: t("common:errors.gemini.thinking_complete_no_output") } + } + if (lastUsage) { yield { type: "usage",