{"record":{"id":"267e710d1ce5d66a","repo":"we-promise/sure","slug":"openai-stream-ended-without-a-completion-event-th","errorCode":null,"errorMessage":"OpenAI stream ended without a completion event. This usually means the upstream call was cut short — common causes: expired previous_response_id (Responses API state TTL), context-length overflow, or a transient OpenAI error.","messagePattern":"OpenAI stream ended without a completion event\\. This usually means the upstream call was cut short — common causes: expired previous_response_id \\(Responses API state TTL\\), context-length overflow, or a transient OpenAI error\\.","errorType":"exception","errorClass":"Provider::Openai::Error","httpStatus":null,"severity":"error","filePath":"app/models/provider/openai.rb","lineNumber":385,"sourceCode":"\n        begin\n          raw_response = client.responses.create(parameters: {\n            model: model,\n            input: input_payload,\n            instructions: instructions,\n            tools: chat_config.tools,\n            previous_response_id: previous_response_id,\n            stream: stream_proxy\n          })\n\n          # If streaming, Ruby OpenAI does not return anything, so to normalize this method's API, we search\n          # for the \"response chunk\" in the stream and return it (it is already parsed)\n          if stream_proxy.present?\n            error_chunk = collected_chunks.find { |chunk| chunk.type == \"error\" }\n            response_chunk = collected_chunks.find { |chunk| chunk.type == \"response\" }\n\n            if response_chunk.nil?\n              raise Error.new(\n                build_stream_error_message(error_chunk),\n                details: error_chunk&.data&.details\n              )\n            end\n\n            response = response_chunk.data\n            usage = response_chunk.usage\n            Rails.logger.debug(\"Stream response usage: #{usage.inspect}\")\n            log_langfuse_generation(\n              name: \"chat_response\",\n              model: model,\n              input: input_payload,\n              output: response.messages.map(&:output_text).join(\"\\n\"),\n              usage: usage,\n              session_id: session_id,\n              user_identifier: user_identifier\n            )\n            record_llm_usage(family: family, model: model, operation: \"chat\", usage: usage)","sourceCodeStart":367,"sourceCodeEnd":403,"githubUrl":"https://github.com/we-promise/sure/blob/e69894adb92547273377398c15f45c979cd9416a/app/models/provider/openai.rb#L367-L403","documentation":"Raised in Provider::Openai#native_chat_response after a streaming Responses API call: the stream proxy collected chunks, but none had type == \"response\" (the terminal completion event), and build_stream_error_message found no error_chunk with a data.message — so it falls back to this generic diagnosis. It means Ruby OpenAI's stream ended without delivering the final response object the client normalizes on.","triggerScenarios":"Passing a previous_response_id whose stored state expired (Responses API conversation state TTL) so OpenAI aborts mid-stream without a well-formed error event; input+instructions exceeding the model context window so generation is cut; transient OpenAI-side failures or dropped connections that close the SSE stream before the response.completed event; a streamer proc raising or filtering the terminal chunk.","commonSituations":"Resuming an assistant conversation hours/days later with the old previous_response_id; long chat histories or large function_results payloads overflowing context; upgrading the ruby-openai gem so chunk types change and the 'response' chunk no longer matches; intermittent OpenAI incidents during streaming.","solutions":["Retry once WITHOUT previous_response_id (send the full input payload) — the most common cause is expired Responses API state, and a fresh call sidesteps it.","If it recurs, check the model's context window against your input size (input_payload plus instructions and tools) and trim history or function results.","Upgrade/verify the ruby-openai gem version so ChatStreamParser's chunk types match the Responses API events the gem emits.","Capture collected chunk types on failure (they are available where the raise happens) to confirm whether any 'response.incomplete'/'response.failed' event arrived without a message.","If using a custom streamer proc, ensure it never raises and never swallows the terminal chunk."],"exampleFix":"# before\nresponse = provider.native_chat_response(\n  prompt:, model:, streamer:,\n  previous_response_id: session.last_response_id\n)\n\n# after\nbegin\n  response = provider.native_chat_response(\n    prompt:, model:, streamer:,\n    previous_response_id: session.last_response_id\n  )\nrescue Provider::Openai::Error\n  # expired previous_response_id state — fall back to a stateless call\n  response = provider.native_chat_response(\n    prompt:, model:, streamer:,\n    previous_response_id: nil\n  )\nend","handlingStrategy":"retry","validationCode":"# drop state that may have expired server-side before it can poison the call\nif previous_response_id.present? && last_exchange_at < 30.minutes.ago\n  previous_response_id = nil # send full input instead of resuming\nend","typeGuard":null,"tryCatchPattern":"begin\n  response = provider.native_chat_response(prompt:, model:, streamer:, previous_response_id: session.last_response_id)\nrescue Provider::Openai::Error\n  # expired response state / transient cut: retry statelessly once\n  response = provider.native_chat_response(prompt:, model:, streamer:, previous_response_id: nil)\nend","preventionTips":["Expire previous_response_id aggressively client-side (Responses API state is short-lived) and resume with full input instead.","Bound conversation history and function_results size against the model's context window.","Keep the ruby-openai gem current so stream chunk types stay aligned with the Responses API events."],"tags":["openai","responses-api","streaming","previous-response-id","context-length","llm"],"backgroundTag":"llm-stream-interrupted","analyzedSha":"e69894adb92547273377398c15f45c979cd9416a","analyzedAt":"2026-08-21T18:22:41.165Z","schemaVersion":2},"datasetVersion":"2026-08-21T23:17:16.201Z"}