From 7add883c981504b41d904194da8db3b7be67229b Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Sat, 27 Jun 2026 00:29:48 +0200 Subject: [PATCH 1/2] Fix ReadIOAdapter max_len handling --- lib/openai/internal/util.rb | 4 ++-- test/openai/internal/util_test.rb | 17 +++++++++++++++++ 2 files changed, 19 insertions(+), 2 deletions(-) diff --git a/lib/openai/internal/util.rb b/lib/openai/internal/util.rb index f463559f..7d4f07ad 100644 --- a/lib/openai/internal/util.rb +++ b/lib/openai/internal/util.rb @@ -428,11 +428,11 @@ def close @stream.to_a.join in Integer @buf << @stream.next while @buf.length < max_len - @buf.slice!(..max_len) + @buf.slice!(0, max_len) end rescue StopIteration @stream = nil - @buf.slice!(0..) + @buf.empty? ? nil : @buf.slice!(0..) end # @api private diff --git a/test/openai/internal/util_test.rb b/test/openai/internal/util_test.rb index 8566246b..dbf085f9 100644 --- a/test/openai/internal/util_test.rb +++ b/test/openai/internal/util_test.rb @@ -329,6 +329,23 @@ def test_copy_read end end + def test_read_respects_max_len_for_enumerator + input = + Enumerator.new do |y| + y << "ab" + y << "cd" + y << "ef" + end + + # rubocop:disable Lint/EmptyBlock + adapter = OpenAI::Internal::Util::ReadIOAdapter.new(input) {} + # rubocop:enable Lint/EmptyBlock + + assert_equal("abc", adapter.read(3)) + assert_equal("def", adapter.read(3)) + assert_nil(adapter.read(3)) + end + def test_copy_write cases = { StringIO.new => "", From e3520960a2b11ad27cbadcdff7b1c1d2943f1fad Mon Sep 17 00:00:00 2001 From: Minh Vu Date: Sat, 27 Jun 2026 00:37:41 +0200 Subject: [PATCH 2/2] Handle EOF with output buffers in ReadIOAdapter --- lib/openai/internal/util.rb | 28 +++++++++++++------------ test/openai/internal/util_test.rb | 35 +++++++++++++++++++++++++++++++ 2 files changed, 50 insertions(+), 13 deletions(-) diff --git a/lib/openai/internal/util.rb b/lib/openai/internal/util.rb index 7d4f07ad..d028d8a4 100644 --- a/lib/openai/internal/util.rb +++ b/lib/openai/internal/util.rb @@ -442,21 +442,23 @@ def close # # @return [String, nil] def read(max_len = nil, out_string = nil) - case @stream - in nil - nil - in IO | StringIO - @stream.read(max_len, out_string) - in Enumerator - read = read_enum(max_len) - case out_string - in String - out_string.replace(read) + read = + case @stream in nil - read + nil + in IO | StringIO + return @stream.read(max_len, out_string).tap(&@blk) + in Enumerator + read_enum(max_len) end - end - .tap(&@blk) + + case out_string + in String + out_string.replace(read || "") + read.nil? ? nil : out_string + in nil + read + end.tap(&@blk) end # @api private diff --git a/test/openai/internal/util_test.rb b/test/openai/internal/util_test.rb index dbf085f9..9f52ee4d 100644 --- a/test/openai/internal/util_test.rb +++ b/test/openai/internal/util_test.rb @@ -346,6 +346,41 @@ def test_read_respects_max_len_for_enumerator assert_nil(adapter.read(3)) end + def test_read_clears_out_string_at_eof_for_enumerator + input = + Enumerator.new do |y| + y << "ab" + y << "cd" + y << "ef" + end + + # rubocop:disable Lint/EmptyBlock + adapter = OpenAI::Internal::Util::ReadIOAdapter.new(input) {} + # rubocop:enable Lint/EmptyBlock + out = +"seed" + + assert_same(out, adapter.read(3, out)) + assert_equal("abc", out) + assert_same(out, adapter.read(3, out)) + assert_equal("def", out) + assert_nil(adapter.read(3, out)) + assert_equal("", out) + end + + def test_copy_read_for_enumerator_exactly_on_copy_stream_boundary + body = "a" * 16_384 + input = Enumerator.new { _1 << body } + io = StringIO.new + + # rubocop:disable Lint/EmptyBlock + adapter = OpenAI::Internal::Util::ReadIOAdapter.new(input) {} + # rubocop:enable Lint/EmptyBlock + + IO.copy_stream(adapter, io) + + assert_equal(body, io.string) + end + def test_copy_write cases = { StringIO.new => "",