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

  1. 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.
  2. 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.
  3. Upgrade/verify the ruby-openai gem version so ChatStreamParser's chunk types match the Responses API events the gem emits.
  4. 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.
  5. 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

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


AI-assisted analysis of we-promise/sure@e69894adb9 (2026-08-21). Data as JSON: /api/errors/267e710d1ce5d66a. Report an issue: GitHub.