Skip to content

bug: duplicate finish_reason chunks from OpenRouter cause "Model stream ended with empty response text" #2402

@simon100500

Description

@simon100500

Bug description

When using OpenRouter with certain models (e.g. google/gemini-3.1-flash-lite-preview), the SDK throws:

[API Error: Model stream ended with empty response text.]
turns: 1

This happens when a tool call is made — pure text responses work fine.

Root cause

OpenRouter sends two consecutive SSE chunks with finish_reason: "tool_calls":

chunk[3]  finish=tool_calls  tool_calls=false  delta={content:"", role:"assistant"}
chunk[4]  finish=tool_calls  tool_calls=false  (duplicate, empty)

The streaming pipeline in pipeline.ts handles the first finish chunk (chunk[3]) correctly:

  1. convertOpenAIChunkToGemini calls getCompletedToolCalls() → gets accumulated tool call → adds functionCall part ✓
  2. streamingToolCallParser.reset() is called
  3. handleChunkMerging stores the response as pendingFinishResponse

Then chunk[4] arrives:

  1. convertOpenAIChunkToGemini — parser was already reset → getCompletedToolCalls() returns [] → no functionCall parts ✗
  2. handleChunkMerging overwrites pendingFinishResponse with the empty chunk[4] ✗

After the loop, the empty chunk[4] is yielded. processStreamResponse sees hasToolCall=false + hasFinishReason=true + empty contentText → throws InvalidStreamError("Model stream ended with empty response text.", "NO_RESPONSE_TEXT").

Affected code

packages/core/src/core/openaiContentGenerator/pipeline.ts, handleChunkMerging:

if (isFinishChunk) {
  collectedGeminiResponses.push(response);
  setPendingFinish(response); // ← always overwrites, even with empty duplicate
  return false;
}

Fix

When a second finish chunk arrives and hasPendingFinish is already true, merge only usageMetadata and keep the candidates (with functionCall parts) from the first finish chunk.

See PR #… for the fix.

Reproduction

Configure the SDK with OpenRouter and a model that performs tool calls:

query({
  prompt: "create a task",
  options: {
    authType: "openai",
    model: "google/gemini-3.1-flash-lite-preview",
    env: {
      OPENAI_API_KEY: "sk-or-v1-...",
      OPENAI_BASE_URL: "https://openrouter.ai/api/v1",
    },
    mcpServers: { /* any MCP server with tools */ },
  },
});
// → [API Error: Model stream ended with empty response text.]

Metadata

Metadata

Assignees

Labels

No labels
No labels

Type

No type
No fields configured for issues without a type.

Projects

No projects

Milestone

No milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions