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
- 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.
- 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.
- 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.
- 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
- Point the client at the official Gemini/Vertex endpoint or a proxy proven to set candidates[0].finishReason on the final chunk (map upstream finish_reason).
- Set client and proxy read/idle timeouts above the model's longest expected thinking time so long streams are not cut before the finishReason chunk.
- In tests, always end mock streams with a chunk carrying finishReason: 'STOP' (mirror geminiChat.test.ts fixtures).
- Subscribe to the library's retry-attempt events/logs to notice when truncation starts recurring — a rising rate usually means a network middleman, not the model.
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
- NO_RESPONSE_TEXT
- [A2AClientManager] sendMessageStream Error [${agentName}]: $
- Failed to create chat object: ${getErrorMessage(e)}
- MALFORMED_FUNCTION_CALL
- Download failed: HTTP ${response.status} ${response.statusTe
AI-assisted analysis of google-gemini/gemini-cli@3c311beac2 (2026-08-21).
Data as JSON: /api/errors/7d61f30a30b2c698.
Report an issue: GitHub.