From d2b938ae89c138c8f62c6c7aaaead3a33cad2fb0 Mon Sep 17 00:00:00 2001 From: Alex Castillo Date: Sun, 8 Nov 2026 20:17:34 -0400 Subject: [PATCH] fix: preserve Responses streaming error details --- CHANGELOG.md | 4 ++ .../providers/open_ai/responses_provider.rb | 7 +++ .../responses/streaming_errors_test.rb | 53 +++++++++++++++++++ 3 files changed, 64 insertions(+) create mode 100644 test/providers/open_ai/responses/streaming_errors_test.rb diff --git a/CHANGELOG.md b/CHANGELOG.md index bee258049..2aa4aaa6b 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -23,6 +23,10 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0 ### Fixed +- **Responses streaming failures retain their error details.** `error` and + `response.failed` events raise a provider error with the API code and message + instead of an unexpected chunk type error. + - **A self-hosted dashboard shows its own trace endpoint** (`actionagent`). The Organization page's Telemetry card showed the platform's `/v1/traces`. A self-hosted engine takes traces at `/api/traces`, and the card diff --git a/lib/active_agent/providers/open_ai/responses_provider.rb b/lib/active_agent/providers/open_ai/responses_provider.rb index cf124dda1..b0032a1a3 100644 --- a/lib/active_agent/providers/open_ai/responses_provider.rb +++ b/lib/active_agent/providers/open_ai/responses_provider.rb @@ -110,6 +110,7 @@ def api_response_normalize(api_response) # reasoning progress, kept from the reasoning item once it is done # - `:"response.content_part.done"` - content part completed # - `:"response.output_item.done"` - message, reasoning or function call completed + # - `:error`, `:"response.failed"` - provider failure # - `:"response.completed"` - response finished # - `:"response.incomplete"` - response cut short (e.g. max_output_tokens) # @@ -174,6 +175,12 @@ def process_stream_chunk(api_response_event) when :"response.output_item.done" process_stream_output_item_done(api_response_event) + # Errors arrive as events even though the HTTP request succeeded. + when :error, :"response.failed" + error = api_response_event.type == :error ? api_response_event : api_response_event.response.error + message = [ error&.code, error&.message ].compact.join(": ") + raise Errors::ProviderError.new(message.presence || "OpenAI response failed", provider_tag: tag_name) + # Response Completed, or cut short (e.g. by max_output_tokens) with # the output it has so far when :"response.completed", :"response.incomplete" diff --git a/test/providers/open_ai/responses/streaming_errors_test.rb b/test/providers/open_ai/responses/streaming_errors_test.rb new file mode 100644 index 000000000..37dc54dee --- /dev/null +++ b/test/providers/open_ai/responses/streaming_errors_test.rb @@ -0,0 +1,53 @@ +# frozen_string_literal: true + +require "test_helper" +require_relative "../../../../lib/active_agent/providers/open_ai/responses_provider" + +module Providers + module OpenAI + module Responses + class StreamingErrorsTest < ActiveSupport::TestCase + include WebMock::API + + test "an error event preserves the API error message and code" do + assert_stream_error({ type: "error", code: "server_error", message: "Please try again", param: nil }, + "server_error: Please try again") + end + + test "a failed response preserves its nested error" do + assert_stream_error({ type: "response.failed", response: { + id: "resp_failed", status: "failed", error: { code: "server_error", message: "Generation failed" } + } }, "server_error: Generation failed") + end + + test "a failed response without error details still raises a provider error" do + assert_stream_error({ type: "response.failed", response: { + id: "resp_failed", status: "failed", error: nil + } }, "OpenAI response failed") + end + + private + + def assert_stream_error(event, message) + events = [ { type: "response.created", response: { + id: "resp_failed", status: "in_progress", output: [] + } }, event ] + body = events.each_with_index.map do |chunk, index| + "event: #{chunk[:type]}\ndata: #{chunk.merge(sequence_number: index).to_json}\n\n" + end.join + stub_request(:post, "https://api.openai.com/v1/responses").to_return( + status: 200, headers: { "Content-Type" => "text/event-stream" }, body: + ) + provider = ActiveAgent::Providers::OpenAI::ResponsesProvider.new( + service: "OpenAI", api_key: "test-key", model: "gpt-5", stream: true, + messages: [ { role: "user", content: "Hello" } ], + stream_broadcaster: ->(_message, _delta, _event_type) { } + ) + + error = assert_raises(ActiveAgent::Providers::Errors::ProviderError) { provider.prompt } + assert_equal message, error.message + end + end + end + end +end