google-gemini/gemini-cli · error · InvalidStreamError

NO_FINISH_REASON

NO_FINISH_REASON

Error message

Model stream ended without a finish reason.

What it means

Thrown after a Gemini streaming response completes without any chunk ever carrying candidates[].finishReason, and without a single valid tool call. geminiChat.ts treats a stream as successful only when it produced a tool call OR a usable finishReason plus text (see the validation block starting at geminiChat.ts:1499), so a stream that just stops is classified as invalid. InvalidStreamError (geminiChat.ts:248) is explicitly a retryable signal: the library already retries it internally with backoff (up to 3 retries / 4 attempts per MID_STREAM_RETRY_OPTIONS, geminiChat.ts:668-709) and appends a nudge to the system instruction on retry. If you see it, the stream terminated abnormally on every attempt.

Source

Thrown at packages/core/src/core/geminiChat.ts:1576

    let previous: string;
    do {
      previous = responseText;
      responseText = responseText.replace(/<!--[\s\S]*?-->/g, '');
    } while (responseText !== previous);
    responseText = responseText.trim();

    // Stream validation logic: A stream is considered successful if:
    // 1. There's a tool call OR
    // 2. A not MALFORMED_FUNCTION_CALL finish reason and a non-mepty resp
    //
    // We throw an error only when there's no tool call AND:
    // - No finish reason, OR
    // - MALFORMED_FUNCTION_CALL finish reason OR
    // - Empty response text (e.g., only thoughts with no actual content)
    if (!hasToolCall) {
      if (!finishReason) {
        if (!isOriginalFunctionResponse) {
          throw new InvalidStreamError(
            'Model stream ended without a finish reason.',
            'NO_FINISH_REASON',
          );
        }
      }
      if (finishReason === FinishReason.MALFORMED_FUNCTION_CALL) {
        throw new InvalidStreamError(
          'Model stream ended with malformed function call.',
          'MALFORMED_FUNCTION_CALL',
        );
      }
      if (finishReason === FinishReason.UNEXPECTED_TOOL_CALL) {
        throw new InvalidStreamError(
          'Model stream ended with unexpected tool call.',
          'UNEXPECTED_TOOL_CALL',
        );
      }
      if (!responseText) {

View on GitHub (pinned to 3c311beac2)

Solutions

  1. Retry the whole turn after a pause — the error is transient in the vast majority of cases; the library's 3 internal retries were exhausted, so an outer retry with a longer delay (seconds, not milliseconds) usually succeeds.
  2. If you use an OpenAI-compatible proxy/gateway, verify it emits a final chunk with candidates[0].finishReason='STOP' (map finish_reason from the upstream); test with the official @google/genai endpoint to isolate the middleman.
  3. Check for network instability between you and the API (proxy timeouts, idle-connection kills) and increase client/proxy read timeouts so long thinking streams are not cut before the finishReason chunk arrives.
  4. If it reproduces deterministically with the official endpoint, capture the raw chunks (each yielded chunk's candidates) and file an issue with the chunk dump — a compliant Gemini stream always ends with a finishReason.

Example fix

// before: caller gives up on the first failure
try {
  for await (const chunk of chat.sendMessageStream(msg)) { /* ... */ }
} catch (e) {
  throw e; // NO_FINISH_REASON bubbles up after library retries
}

// after: outer retry with coarse backoff for truncation-class errors
import { InvalidStreamError } from './packages/core/src/core/geminiChat.js';

async function sendWithRetry(chat, msg, outerAttempts = 3) {
  for (let i = 0; i < outerAttempts; i++) {
    try {
      const chunks = [];
      for await (const chunk of chat.sendMessageStream(msg)) chunks.push(chunk);
      return chunks;
    } catch (e) {
      if (e instanceof InvalidStreamError && e.type === 'NO_FINISH_REASON' && i < outerAttempts - 1) {
        await new Promise((r) => setTimeout(r, 2000 * (i + 1)));
        continue;
      }
      throw e;
    }
  }
}
Defensive patterns

Strategy: retry

Type guard

import { InvalidStreamError } from './packages/core/src/core/geminiChat.js';

function isNoFinishReasonError(e: unknown): e is InvalidStreamError & { type: 'NO_FINISH_REASON' } {
  return e instanceof InvalidStreamError && e.type === 'NO_FINISH_REASON';
}

Try / catch

try {
  for await (const chunk of chat.sendMessageStream(userMsg)) { handle(chunk); }
} catch (e) {
  if (isNoFinishReasonError(e)) {
    // Stream was cut before the terminating chunk on all 4 internal attempts.
    // Coarse outer backoff; roll your own cap (e.g., 2 extra tries).
    await sleep(3000);
    return runTurn(chat, userMsg); // resend the same turn
  }
  throw e;
}

Prevention

When it happens

Trigger: A generateContentStream/sendMessageStream call where the generator finishes but no chunk had candidates[n].finishReason (it is only captured when a candidate carries one, geminiChat.ts:1318-1324), hasToolCall stayed false, and the outgoing user turn is not a functionResponse (isOriginalFunctionResponse=false, so the exemption at geminiChat.ts:1509 does not apply). Typical shapes: the server closes the SSE stream before the terminating chunk; the response contains only usageMetadata or thought-only chunks with no candidate finishReason; an OpenAI-compatible proxy or gateway that never maps finish_reason into candidates[0].finishReason.

Common situations: Routing Gemini traffic through LiteLLM/an OpenAI-compatible proxy/mock that omits finish_reason on the last chunk; flaky VPN/proxy connections that drop the tail of the stream; transient server-side truncation under load or rate-limit pressure; test doubles that yield text chunks but forget the final STOP chunk (see geminiChat.test.ts:1811 'no tool call and no finish reason'); upgrading @google/genai or switching endpoints where chunk layout differs.

Related errors


AI-assisted analysis of google-gemini/gemini-cli@3c311beac2 (2026-08-21). Data as JSON: /api/errors/7d61f30a30b2c698. Report an issue: GitHub.