Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 3 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
Expand Up @@ -39,6 +39,9 @@ and this project adheres to [Semantic Versioning](https://semver.org/spec/v2.0.0

### Fixed

- **Responses streaming failures retain their error details** (`activeagent`).
`error` and `response.failed` events raise a provider error with the API
code and message instead of an unexpected chunk type error.
- **The dashboard keeps a catalog's pass bars** (`actionagent`). A
product's and a set's `pass` blocks (`min_pass_rate`, `threshold`,
`gated`) parsed in `ActiveAgent::Evals::Catalog`, but neither
Expand Down
7 changes: 7 additions & 0 deletions lib/active_agent/providers/open_ai/responses_provider.rb
Original file line number Diff line number Diff line change
Expand Up @@ -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)
#
Expand Down Expand Up @@ -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"
Expand Down
53 changes: 53 additions & 0 deletions test/providers/open_ai/responses/streaming_errors_test.rb
Original file line number Diff line number Diff line change
@@ -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
Loading