we-promise/sure · error · Provider::Openai::Error
OpenAI stream ended without a completion event. This usually
Error message
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.
What it means
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.
Source
Thrown at app/models/provider/openai.rb:385
begin
raw_response = client.responses.create(parameters: {
model: model,
input: input_payload,
instructions: instructions,
tools: chat_config.tools,
previous_response_id: previous_response_id,
stream: stream_proxy
})
# If streaming, Ruby OpenAI does not return anything, so to normalize this method's API, we search
# for the "response chunk" in the stream and return it (it is already parsed)
if stream_proxy.present?
error_chunk = collected_chunks.find { |chunk| chunk.type == "error" }
response_chunk = collected_chunks.find { |chunk| chunk.type == "response" }
if response_chunk.nil?
raise Error.new(
build_stream_error_message(error_chunk),
details: error_chunk&.data&.details
)
end
response = response_chunk.data
usage = response_chunk.usage
Rails.logger.debug("Stream response usage: #{usage.inspect}")
log_langfuse_generation(
name: "chat_response",
model: model,
input: input_payload,
output: response.messages.map(&:output_text).join("\n"),
usage: usage,
session_id: session_id,
user_identifier: user_identifier
)
record_llm_usage(family: family, model: model, operation: "chat", usage: usage)View on GitHub (pinned to e69894adb9)
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.
Example fix
# before
response = provider.native_chat_response(
prompt:, model:, streamer:,
previous_response_id: session.last_response_id
)
# after
begin
response = provider.native_chat_response(
prompt:, model:, streamer:,
previous_response_id: session.last_response_id
)
rescue Provider::Openai::Error
# expired previous_response_id state — fall back to a stateless call
response = provider.native_chat_response(
prompt:, model:, streamer:,
previous_response_id: nil
)
end Defensive patterns
Strategy: retry
Validate before calling
# drop state that may have expired server-side before it can poison the call if previous_response_id.present? && last_exchange_at < 30.minutes.ago previous_response_id = nil # send full input instead of resuming end
Try / catch
begin response = provider.native_chat_response(prompt:, model:, streamer:, previous_response_id: session.last_response_id) rescue Provider::Openai::Error # expired response state / transient cut: retry statelessly once response = provider.native_chat_response(prompt:, model:, streamer:, previous_response_id: nil) end
Prevention
- 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.
When it happens
Trigger: 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.
Common situations: 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.
Related errors
- No message content found in response
- No message content found in response
- {e.record.errors.full_messages.to_sentence.presence || e.mes
- Assistant returned neither text nor tool calls
- Model is required when using a custom OpenAI‑compatible prov
AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21).
Data as JSON: /api/errors/267e710d1ce5d66a.
Report an issue: GitHub.