{"record":{"id":"7d61f30a30b2c698","repo":"google-gemini/gemini-cli","slug":"no-finish-reason","errorCode":"NO_FINISH_REASON","errorMessage":"Model stream ended without a finish reason.","messagePattern":"Model stream ended without a finish reason\\.","errorType":"exception","errorClass":"InvalidStreamError","httpStatus":null,"severity":"error","filePath":"packages/core/src/core/geminiChat.ts","lineNumber":1576,"sourceCode":"    let previous: string;\n    do {\n      previous = responseText;\n      responseText = responseText.replace(/<!--[\\s\\S]*?-->/g, '');\n    } while (responseText !== previous);\n    responseText = responseText.trim();\n\n    // Stream validation logic: A stream is considered successful if:\n    // 1. There's a tool call OR\n    // 2. A not MALFORMED_FUNCTION_CALL finish reason and a non-mepty resp\n    //\n    // We throw an error only when there's no tool call AND:\n    // - No finish reason, OR\n    // - MALFORMED_FUNCTION_CALL finish reason OR\n    // - Empty response text (e.g., only thoughts with no actual content)\n    if (!hasToolCall) {\n      if (!finishReason) {\n        if (!isOriginalFunctionResponse) {\n          throw new InvalidStreamError(\n            'Model stream ended without a finish reason.',\n            'NO_FINISH_REASON',\n          );\n        }\n      }\n      if (finishReason === FinishReason.MALFORMED_FUNCTION_CALL) {\n        throw new InvalidStreamError(\n          'Model stream ended with malformed function call.',\n          'MALFORMED_FUNCTION_CALL',\n        );\n      }\n      if (finishReason === FinishReason.UNEXPECTED_TOOL_CALL) {\n        throw new InvalidStreamError(\n          'Model stream ended with unexpected tool call.',\n          'UNEXPECTED_TOOL_CALL',\n        );\n      }\n      if (!responseText) {","sourceCodeStart":1558,"sourceCodeEnd":1594,"githubUrl":"https://github.com/google-gemini/gemini-cli/blob/3c311beac2e78336816dd4a123db39743f9fbf85/packages/core/src/core/geminiChat.ts#L1558-L1594","documentation":"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.","triggerScenarios":"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.","commonSituations":"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.","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."],"exampleFix":"// before: caller gives up on the first failure\ntry {\n  for await (const chunk of chat.sendMessageStream(msg)) { /* ... */ }\n} catch (e) {\n  throw e; // NO_FINISH_REASON bubbles up after library retries\n}\n\n// after: outer retry with coarse backoff for truncation-class errors\nimport { InvalidStreamError } from './packages/core/src/core/geminiChat.js';\n\nasync function sendWithRetry(chat, msg, outerAttempts = 3) {\n  for (let i = 0; i < outerAttempts; i++) {\n    try {\n      const chunks = [];\n      for await (const chunk of chat.sendMessageStream(msg)) chunks.push(chunk);\n      return chunks;\n    } catch (e) {\n      if (e instanceof InvalidStreamError && e.type === 'NO_FINISH_REASON' && i < outerAttempts - 1) {\n        await new Promise((r) => setTimeout(r, 2000 * (i + 1)));\n        continue;\n      }\n      throw e;\n    }\n  }\n}","handlingStrategy":"retry","validationCode":null,"typeGuard":"import { InvalidStreamError } from './packages/core/src/core/geminiChat.js';\n\nfunction isNoFinishReasonError(e: unknown): e is InvalidStreamError & { type: 'NO_FINISH_REASON' } {\n  return e instanceof InvalidStreamError && e.type === 'NO_FINISH_REASON';\n}","tryCatchPattern":"try {\n  for await (const chunk of chat.sendMessageStream(userMsg)) { handle(chunk); }\n} catch (e) {\n  if (isNoFinishReasonError(e)) {\n    // Stream was cut before the terminating chunk on all 4 internal attempts.\n    // Coarse outer backoff; roll your own cap (e.g., 2 extra tries).\n    await sleep(3000);\n    return runTurn(chat, userMsg); // resend the same turn\n  }\n  throw e;\n}","preventionTips":["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."],"tags":["gemini","streaming","finish-reason","retry","network","invalid-stream"],"backgroundTag":"llm-stream-truncated","analyzedSha":"3c311beac2e78336816dd4a123db39743f9fbf85","analyzedAt":"2026-08-21T17:03:46.656Z","contentChangedAt":null,"schemaVersion":2},"datasetVersion":"2026-09-14T05:17:10.506Z"}