{"record":{"id":"f7cc0dd7a94cbfd1","repo":"can1357/oh-my-pi","slug":"google-api-stream-ended-without-a-finish-reason-c","errorCode":null,"errorMessage":"Google API stream ended without a finish reason (connection dropped or response truncated)","messagePattern":"Google API stream ended without a finish reason \\(connection dropped or response truncated\\)","errorType":"exception","errorClass":"AIError.ProviderResponseError","httpStatus":null,"severity":"error","filePath":"packages/ai/src/providers/google-shared.ts","lineNumber":767,"sourceCode":"\t\t\t\t\tinput: 0,\n\t\t\t\t\toutput: 0,\n\t\t\t\t\tcacheRead: 0,\n\t\t\t\t\tcacheWrite: 0,\n\t\t\t\t\ttotal: 0,\n\t\t\t\t},\n\t\t\t};\n\t\t\tcalculateCost(model, output.usage);\n\t\t}\n\t}\n\n\tflushCurrent();\n\n\tif (options?.signal?.aborted) {\n\t\tthrow new AIError.AbortError();\n\t}\n\n\tif (!sawFinishReason) {\n\t\tthrow new AIError.ProviderResponseError(\n\t\t\t\"Google API stream ended without a finish reason (connection dropped or response truncated)\",\n\t\t\t{ provider: model.provider, kind: \"incomplete-stream\" },\n\t\t);\n\t}\n\n\tif (output.stopReason === \"aborted\" || output.stopReason === \"error\") {\n\t\tthrow new AIError.ProviderResponseError(output.errorMessage ?? \"An unknown error occurred\", {\n\t\t\tprovider: model.provider,\n\t\t\tkind: \"output\",\n\t\t});\n\t}\n}\n\n/**\n * Generation/sampling fields that map directly onto Gemini's `GenerateContentConfig`.\n * Excludes any provider-specific extensions (`topP`/`topK`/etc are all forwarded as-is).\n */\ninterface GoogleGenerationConfig extends GenerateContentConfig {","sourceCodeStart":749,"sourceCodeEnd":785,"githubUrl":"https://github.com/can1357/oh-my-pi/blob/969062200754ea02cfac922e5ebb8c608c079e15/packages/ai/src/providers/google-shared.ts#L749-L785","documentation":"consumeGoogleStream reached end-of-stream without ever seeing a candidate finishReason, meaning the SSE stream from Google terminated abnormally. The library throws AIError.ProviderResponseError with kind \"incomplete-stream\" because a complete Google response always ends with a finish reason.","triggerScenarios":"The HTTP connection to Google dropped mid-stream; a proxy/load-balancer cut the SSE connection; server-side crash mid-generation; network interruption; response truncated by infrastructure timeout.","commonSituations":"Long generations over flaky networks; corporate proxies with idle/stream timeouts; Cloud Run / API gateway response limits; transient Google infrastructure errors.","solutions":["Implement retry with exponential backoff — these are usually transient network failures","Catch AIError.ProviderResponseError with kind \"incomplete-stream\" and re-issue the request, keeping any partial output if acceptable","Check network path: disable VPN/proxy or raise its stream timeout","Reduce max output tokens so responses complete within connection lifetime"],"exampleFix":"// before\nconst result = await streamGoogle(model, params);\n// after\nasync function withRetry() {\n  for (let attempt = 0; attempt < 3; attempt++) {\n    try {\n      return await streamGoogle(model, params);\n    } catch (err) {\n      if (err instanceof AIError.ProviderResponseError && err.context?.kind === \"incomplete-stream\" && attempt < 2) {\n        await Bun.sleep(2 ** attempt * 500);\n        continue;\n      }\n      throw err;\n    }\n  }\n}","handlingStrategy":"retry","validationCode":"null","typeGuard":"null","tryCatchPattern":"try {\n  return await streamGoogle(model, params, { signal });\n} catch (err) {\n  if (err instanceof AIError.ProviderResponseError && err.context?.kind === \"incomplete-stream\" && !signal.aborted) {\n    return await withBackoff(() => streamGoogle(model, params, { signal }), 3);\n  }\n  throw err;\n}","preventionTips":["Always wrap streaming calls in retry-with-backoff; drops are transient","Use keep-alive aware HTTP agents and avoid proxies with aggressive idle timeouts","Cap maxOutputTokens to short enough generations that connections survive","Handle partial output gracefully: accumulate chunks so a retry can resume or discard cleanly"],"tags":["network","streaming","google","truncated-response"],"backgroundTag":"stream-truncated","analyzedSha":"969062200754ea02cfac922e5ebb8c608c079e15","analyzedAt":"2026-08-31T10:29:35.737Z","schemaVersion":2},"datasetVersion":"2026-08-31T14:17:45.589Z"}