From 8d5317a4a3035e24de78d226658a821a8893a283 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 20:48:26 +0000 Subject: [PATCH 1/8] feat: support streaming uploads (#1) --- lib/openai/pooled_net_requester.rb | 30 ++-- lib/openai/util.rb | 181 ++++++++++++++++++------ rbi/lib/openai/pooled_net_requester.rbi | 11 +- rbi/lib/openai/util.rbi | 36 ++++- sig/openai/pooled_net_requester.rbs | 8 +- sig/openai/util.rbs | 22 ++- test/openai/util_test.rb | 33 ++++- 7 files changed, 254 insertions(+), 67 deletions(-) diff --git a/lib/openai/pooled_net_requester.rb b/lib/openai/pooled_net_requester.rb index 57bd2e81..d15f367e 100644 --- a/lib/openai/pooled_net_requester.rb +++ b/lib/openai/pooled_net_requester.rb @@ -48,9 +48,11 @@ def calibrate_socket_timeout(conn, deadline) # # @option request [Hash{String=>String}] :headers # + # @param blk [Proc] + # # @return [Net::HTTPGenericRequest] # - def build_request(request) + def build_request(request, &) method, url, headers, body = request.fetch_values(:method, :url, :headers, :body) req = Net::HTTPGenericRequest.new( method.to_s.upcase, @@ -64,12 +66,14 @@ def build_request(request) case body in nil in String - req.body = body + req["content-length"] ||= body.bytesize.to_s unless req["transfer-encoding"] + req.body_stream = OpenAI::Util::ReadIOAdapter.new(body, &) in StringIO - req.body = body.string - in IO - body.rewind - req.body_stream = body + req["content-length"] ||= body.size.to_s unless req["transfer-encoding"] + req.body_stream = OpenAI::Util::ReadIOAdapter.new(body, &) + in IO | Enumerator + req["transfer-encoding"] ||= "chunked" unless req["content-length"] + req.body_stream = OpenAI::Util::ReadIOAdapter.new(body, &) end req @@ -97,7 +101,7 @@ def build_request(request) pool = @mutex.synchronize do - @pools[origin] ||= ConnectionPool.new(size: Etc.nprocessors) do + @pools[origin] ||= ConnectionPool.new(size: @size) do self.class.connect(url) end end @@ -128,7 +132,6 @@ def build_request(request) # def execute(request) url, deadline = request.fetch_values(:url, :deadline) - req = self.class.build_request(request) eof = false finished = false @@ -136,6 +139,10 @@ def execute(request) with_pool(url) do |conn| next if finished + req = self.class.build_request(request) do + self.class.calibrate_socket_timeout(conn, deadline) + end + self.class.calibrate_socket_timeout(conn, deadline) conn.start unless conn.started? @@ -168,8 +175,13 @@ def execute(request) [response, (response.body = body)] end - def initialize + # @private + # + # @param size [Integer] + # + def initialize(size: Etc.nprocessors) @mutex = Mutex.new + @size = size @pools = {} end end diff --git a/lib/openai/util.rb b/lib/openai/util.rb index 29f6258a..10368046 100644 --- a/lib/openai/util.rb +++ b/lib/openai/util.rb @@ -399,41 +399,152 @@ def normalized_headers(*headers) end end + # @private + # + # An adapter that satisfies the IO interface required by `::IO.copy_stream` + class ReadIOAdapter + # @private + # + # @param max_len [Integer, nil] + # + # @return [String] + # + private def read_enum(max_len) + case max_len + in nil + @stream.to_a.join + in Integer + @buf << @stream.next while @buf.length < max_len + @buf.slice!(..max_len) + end + rescue StopIteration + @stream = nil + @buf.slice!(0..) + end + + # @private + # + # @param max_len [Integer, nil] + # @param out_string [String, nil] + # + # @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) + in nil + read + end + end + .tap(&@blk) + end + + # @private + # + # @param stream [String, IO, StringIO, Enumerable] + # @param blk [Proc] + # + def initialize(stream, &blk) + @stream = stream.is_a?(String) ? StringIO.new(stream) : stream + @buf = String.new.b + @blk = blk + end + end + + class << self + # @param blk [Proc] + # + # @return [Enumerable] + # + def string_io(&blk) + Enumerator.new do |y| + y.define_singleton_method(:write) do + self << _1.clone + _1.bytesize + end + + blk.call(y) + end + end + end + class << self # @private # - # @param io [StringIO] + # @param y [Enumerator::Yielder] # @param boundary [String] # @param key [Symbol, String] # @param val [Object] # - private def encode_multipart_formdata(io, boundary:, key:, val:) - io << "--#{boundary}\r\n" - io << "Content-Disposition: form-data" + private def encode_multipart_formdata(y, boundary:, key:, val:) + y << "--#{boundary}\r\n" + y << "Content-Disposition: form-data" unless key.nil? name = ERB::Util.url_encode(key.to_s) - io << "; name=\"#{name}\"" + y << "; name=\"#{name}\"" end if val.is_a?(IO) filename = ERB::Util.url_encode(File.basename(val.to_path)) - io << "; filename=\"#{filename}\"" + y << "; filename=\"#{filename}\"" end - io << "\r\n" + y << "\r\n" case val - in IO | StringIO - io << "Content-Type: application/octet-stream\r\n\r\n" - IO.copy_stream(val, io) + in IO + y << "Content-Type: application/octet-stream\r\n\r\n" + IO.copy_stream(val, y) + in StringIO + y << "Content-Type: application/octet-stream\r\n\r\n" + y << val.string in String - io << "Content-Type: application/octet-stream\r\n\r\n" - io << val.to_s + y << "Content-Type: application/octet-stream\r\n\r\n" + y << val.to_s in true | false | Integer | Float | Symbol - io << "Content-Type: text/plain\r\n\r\n" - io << val.to_s + y << "Content-Type: text/plain\r\n\r\n" + y << val.to_s else - io << "Content-Type: application/json\r\n\r\n" - io << JSON.fast_generate(val) + y << "Content-Type: application/json\r\n\r\n" + y << JSON.fast_generate(val) end - io << "\r\n" + y << "\r\n" + end + + # @private + # + # @param body [Object] + # + # @return [Array(String, Enumerable)] + # + private def encode_multipart_streaming(body) + boundary = SecureRandom.urlsafe_base64(60) + + strio = string_io do |y| + case body + in Hash + body.each do |key, val| + case val + in Array if val.all? { primitive?(_1) } + val.each do |v| + encode_multipart_formdata(y, boundary: boundary, key: key, val: v) + end + else + encode_multipart_formdata(y, boundary: boundary, key: key, val: val) + end + end + else + encode_multipart_formdata(y, boundary: boundary, key: nil, val: body) + end + y << "--#{boundary}--\r\n" + end + + [boundary, strio] end # @private @@ -449,37 +560,11 @@ def encode_content(headers, body) in ["application/json", Hash | Array] [headers, JSON.fast_generate(body)] in [%r{^multipart/form-data}, Hash | IO | StringIO] - boundary = SecureRandom.urlsafe_base64(60) - strio = StringIO.new.tap do |io| - case body - in Hash - body.each do |key, val| - case val - in Array if val.all? { primitive?(_1) } - val.each do |v| - encode_multipart_formdata(io, boundary: boundary, key: key, val: v) - end - else - encode_multipart_formdata(io, boundary: boundary, key: key, val: val) - end - end - else - encode_multipart_formdata(io, boundary: boundary, key: nil, val: body) - end - io << "--#{boundary}--\r\n" - io.rewind - end - headers = { - **headers, - "content-type" => "#{content_type}; boundary=#{boundary}", - "transfer-encoding" => "chunked" - } + boundary, strio = encode_multipart_streaming(body) + headers = {**headers, "content-type" => "#{content_type}; boundary=#{boundary}"} [headers, strio] in [_, StringIO] [headers, body.string] - in [_, IO] - headers = {**headers, "transfer-encoding" => "chunked"} - [headers, body] else [headers, body] end @@ -589,8 +674,9 @@ def decode_lines(enum) chain_fused(enum) do |y| enum.each do |row| + offset = buffer.bytesize buffer << row - while (match = re.match(buffer, cr_seen.to_i)) + while (match = re.match(buffer, cr_seen&.to_i || offset)) case [match.captures.first, cr_seen] in ["\r", nil] cr_seen = match.end(1) @@ -600,6 +686,7 @@ def decode_lines(enum) else y << buffer.slice!(..(match.end(1).pred)) end + offset = 0 cr_seen = nil end end @@ -637,7 +724,7 @@ def decode_sse(lines) in "event" current.merge!(event: value) in "data" - (current[:data] ||= String.new.b) << value << "\n" + (current[:data] ||= String.new.b) << (value << "\n") in "id" unless value.include?("\0") current.merge!(id: value) in "retry" if /^\d+$/ =~ value diff --git a/rbi/lib/openai/pooled_net_requester.rbi b/rbi/lib/openai/pooled_net_requester.rbi index e94f1912..9d80cd5f 100644 --- a/rbi/lib/openai/pooled_net_requester.rbi +++ b/rbi/lib/openai/pooled_net_requester.rbi @@ -15,8 +15,11 @@ module OpenAI def calibrate_socket_timeout(conn, deadline) end - sig { params(request: OpenAI::PooledNetRequester::RequestShape).returns(Net::HTTPGenericRequest) } - def build_request(request) + sig do + params(request: OpenAI::PooledNetRequester::RequestShape, blk: T.proc.params(arg0: String).void) + .returns(Net::HTTPGenericRequest) + end + def build_request(request, &blk) end end @@ -31,8 +34,8 @@ module OpenAI def execute(request) end - sig { returns(T.attached_class) } - def self.new + sig { params(size: Integer).returns(T.attached_class) } + def self.new(size: Etc.nprocessors) end end end diff --git a/rbi/lib/openai/util.rbi b/rbi/lib/openai/util.rbi index 8b354b0d..246a7672 100644 --- a/rbi/lib/openai/util.rbi +++ b/rbi/lib/openai/util.rbi @@ -130,9 +130,41 @@ module OpenAI end end + class ReadIOAdapter + sig { params(max_len: T.nilable(Integer)).returns(String) } + private def read_enum(max_len) + end + + sig { params(max_len: T.nilable(Integer), out_string: T.nilable(String)).returns(T.nilable(String)) } + def read(max_len = nil, out_string = nil) + end + + sig do + params( + stream: T.any(String, IO, StringIO, T::Enumerable[String]), + blk: T.proc.params(arg0: String).void + ) + .returns(T.attached_class) + end + def self.new(stream, &blk) + end + end + + class << self + sig { params(blk: T.proc.params(y: Enumerator::Yielder).void).returns(T::Enumerable[String]) } + def string_io(&blk) + end + end + class << self - sig { params(io: StringIO, boundary: String, key: T.any(Symbol, String), val: T.anything).void } - private def encode_multipart_formdata(io, boundary:, key:, val:) + sig do + params(y: Enumerator::Yielder, boundary: String, key: T.any(Symbol, String), val: T.anything).void + end + private def encode_multipart_formdata(y, boundary:, key:, val:) + end + + sig { params(body: T.anything).returns([String, T::Enumerable[String]]) } + private def encode_multipart_streaming(body) end sig { params(headers: T::Hash[String, String], body: T.anything).returns(T.anything) } diff --git a/sig/openai/pooled_net_requester.rbs b/sig/openai/pooled_net_requester.rbs index 4f89cf8d..9e7daafb 100644 --- a/sig/openai/pooled_net_requester.rbs +++ b/sig/openai/pooled_net_requester.rbs @@ -13,7 +13,11 @@ module OpenAI def self.calibrate_socket_timeout: (top conn, Float deadline) -> void - def self.build_request: (OpenAI::PooledNetRequester::request request) -> top + def self.build_request: ( + OpenAI::PooledNetRequester::request request + ) { + (String arg0) -> void + } -> top private def with_pool: (URI::Generic url) { (top arg0) -> void } -> void @@ -21,6 +25,6 @@ module OpenAI OpenAI::PooledNetRequester::request request ) -> [top, Enumerable[String]] - def initialize: -> void + def initialize: (size: Integer) -> void end end diff --git a/sig/openai/util.rbs b/sig/openai/util.rbs index 2781e634..2ee3b4df 100644 --- a/sig/openai/util.rbs +++ b/sig/openai/util.rbs @@ -70,13 +70,33 @@ module OpenAI | ::Array[(String | Integer)?])?] headers ) -> ::Hash[String, String] + class ReadIOAdapter + private def read_enum: (Integer? max_len) -> String + + def read: (?Integer? max_len, ?String? out_string) -> String? + + def initialize: ( + String | IO | StringIO | Enumerable[String] stream + ) { + (String arg0) -> void + } -> void + end + + def self?.string_io: { + (Enumerator::Yielder y) -> void + } -> Enumerable[String] + def self?.encode_multipart_formdata: ( - StringIO io, + Enumerator::Yielder y, boundary: String, key: Symbol | String, val: top ) -> void + def self?.encode_multipart_streaming: ( + top body + ) -> [String, Enumerable[String]] + def self?.encode_content: (::Hash[String, String] headers, top body) -> top def self?.decode_content: ( diff --git a/test/openai/util_test.rb b/test/openai/util_test.rb index 91c7bd24..d319e2f9 100644 --- a/test/openai/util_test.rb +++ b/test/openai/util_test.rb @@ -161,7 +161,8 @@ class OpenAI::Test::UtilFormDataEncodingTest < Minitest::Test class FakeCGI < CGI def initialize(headers, io) @ctype = headers["content-type"] - @io = io + @io = OpenAI::Util::ReadIOAdapter.new(io) {} + @c_len = io.to_a.join.bytesize.to_s super() end @@ -171,7 +172,7 @@ def env_table { "REQUEST_METHOD" => "POST", "CONTENT_TYPE" => @ctype, - "CONTENT_LENGTH" => stdinput.string.length + "CONTENT_LENGTH" => @c_len } end end @@ -208,6 +209,34 @@ def test_hash_encode end end +class OpenAI::Test::UtilIOAdapterTest < Minitest::Test + def test_copy_read + cases = { + StringIO.new("abc") => "abc", + Enumerator.new { _1 << "abc" } => "abc" + } + cases.each do |input, expected| + io = StringIO.new + adapter = OpenAI::Util::ReadIOAdapter.new(input) {} + IO.copy_stream(adapter, io) + assert_equal(expected, io.string) + end + end + + def test_copy_write + cases = { + StringIO.new => "", + StringIO.new("abc") => "abc" + } + cases.each do |input, expected| + enum = OpenAI::Util.string_io do |y| + IO.copy_stream(input, y) + end + assert_equal(expected, enum.to_a.join) + end + end +end + class OpenAI::Test::UtilFusedEnumTest < Minitest::Test def test_closing arr = [1, 2, 3] From 0f4c842a52c679ab0793fcdc32e5555fbbea4178 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 21:06:12 +0000 Subject: [PATCH 2/8] fix: enums should only coerce matching symbols into strings (#3) --- lib/openai/base_model.rb | 29 +++++++++++++++++++++++++---- test/openai/base_model_test.rb | 16 +++++++++++++++- 2 files changed, 40 insertions(+), 5 deletions(-) diff --git a/lib/openai/base_model.rb b/lib/openai/base_model.rb index 8f58a6ab..798e49ca 100644 --- a/lib/openai/base_model.rb +++ b/lib/openai/base_model.rb @@ -55,11 +55,11 @@ def type_info(spec) type_info(spec.slice(:const, :enum, :union).first&.last) in Proc spec - in OpenAI::Converter | Class + in OpenAI::Converter | Class | Symbol -> { spec } in true | false -> { OpenAI::BooleanModel } - in NilClass | true | false | Symbol | Integer | Float + in NilClass | Integer | Float -> { spec.class } end end @@ -82,6 +82,13 @@ def coerce(target, value) case target in OpenAI::Converter target.coerce(value) + in Symbol + case value + in Symbol | String if (val = value.to_sym) == target + val + else + value + end in Class case target in -> { _1 <= NilClass } @@ -140,6 +147,13 @@ def try_strict_coerce(target, value) case target in OpenAI::Converter target.try_strict_coerce(value) + in Symbol + case value + in Symbol | String if (val = value.to_sym) == target + [true, val, 1] + else + [false, false, 0] + end in Class case [target, value] in [-> { _1 <= NilClass }, _] @@ -367,7 +381,14 @@ class << self # # @return [Symbol, Object] # - def coerce(value) = (value.is_a?(String) ? value.to_sym : value) + def coerce(value) + case value + in Symbol | String if values.include?(val = value.to_sym) + val + else + value + end + end # @!parse # # @private @@ -388,7 +409,7 @@ def try_strict_coerce(value) return [true, value, 1] if values.include?(value) case value - in String if values.include?(val = value.to_sym) + in Symbol | String if values.include?(val = value.to_sym) [true, val, 1] else case [value, values.first] diff --git a/test/openai/base_model_test.rb b/test/openai/base_model_test.rb index 0444fb0e..f6e598d0 100644 --- a/test/openai/base_model_test.rb +++ b/test/openai/base_model_test.rb @@ -22,7 +22,7 @@ def test_basic_coerce end assert_pattern do - OpenAI::Converter.coerce(A2, %w[a b c]) => [:a, :b, :c] + OpenAI::Converter.coerce(A2, %w[a b c]) => [:a, :b, "c"] end end @@ -338,4 +338,18 @@ def test_basic_eql refute_equal(U1, U2) assert_equal(U1, U3) end + + class U4 < OpenAI::Union + variant :a, const: :a + variant :b, const: :b + end + + def test_basic_const_union + assert_pattern do + U4.coerce(nil) => nil + U4.coerce("") => "" + U4.coerce(:a) => :a + U4.coerce("a") => :a + end + end end From 69e8be897fe8cff1ed9b0ea7ef53a759a9a089ff Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Wed, 12 Mar 2025 23:02:56 +0000 Subject: [PATCH 3/8] chore: remove stale thread local checks (#4) --- lib/openai/pooled_net_requester.rb | 21 ++------------------- 1 file changed, 2 insertions(+), 19 deletions(-) diff --git a/lib/openai/pooled_net_requester.rb b/lib/openai/pooled_net_requester.rb index d15f367e..d1a15ffe 100644 --- a/lib/openai/pooled_net_requester.rb +++ b/lib/openai/pooled_net_requester.rb @@ -85,20 +85,8 @@ def build_request(request, &) # @param url [URI::Generic] # @param blk [Proc] # - private def with_pool(url, &blk) + private def with_pool(url, &) origin = OpenAI::Util.uri_origin(url) - th = Thread.current - key = :"#{object_id}-#{self.class.name}-connection_in_use_for_#{origin}" - - if th[key] - tap do - conn = self.class.connect(url) - return blk.call(conn) - ensure - conn.finish if conn&.started? - end - end - pool = @mutex.synchronize do @pools[origin] ||= ConnectionPool.new(size: @size) do @@ -106,12 +94,7 @@ def build_request(request, &) end end - pool.with do |conn| - th[key] = true - blk.call(conn) - ensure - th[key] = nil - end + pool.with(&) end # @private From 43e80fa252a9b1ae4d5c219dd9d346f0b0c3194f Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 13 Mar 2025 15:23:39 +0000 Subject: [PATCH 4/8] chore: refactor BasePage to have initializer (#5) --- lib/openai.rb | 1 + lib/openai/base_page.rb | 21 +++++++++++-------- lib/openai/base_stream.rb | 38 +++++++++++++++++----------------- lib/openai/cursor_page.rb | 6 +----- lib/openai/page.rb | 6 +----- manifest.yaml | 1 + rbi/lib/openai/base_stream.rbi | 30 +++++++++++++-------------- sig/openai/base_stream.rbs | 18 ++++++++-------- 8 files changed, 59 insertions(+), 62 deletions(-) diff --git a/lib/openai.rb b/lib/openai.rb index 2c73d487..77ad926a 100644 --- a/lib/openai.rb +++ b/lib/openai.rb @@ -7,6 +7,7 @@ require "etc" require "json" require "net/http" +require "pathname" require "rbconfig" require "securerandom" require "set" diff --git a/lib/openai/base_page.rb b/lib/openai/base_page.rb index 762b38d7..9f315c7b 100644 --- a/lib/openai/base_page.rb +++ b/lib/openai/base_page.rb @@ -47,14 +47,17 @@ def to_enum = super(:auto_paging_each) alias_method :enum_for, :to_enum - # @!parse - # # @private - # # - # # @param client [OpenAI::BaseClient] - # # @param req [Hash{Symbol=>Object}] - # # @param headers [Hash{String=>String}, Net::HTTPHeader] - # # @param page_data [Object] - # # - # def initialize(client:, req:, headers:, page_data:); end + # @private + # + # @param client [OpenAI::BaseClient] + # @param req [Hash{Symbol=>Object}] + # @param headers [Hash{String=>String}, Net::HTTPHeader] + # @param page_data [Object] + # + def initialize(client:, req:, headers:, page_data:) + @client = client + @req = req + super() + end end end diff --git a/lib/openai/base_stream.rb b/lib/openai/base_stream.rb index 51e8dee6..082d045d 100644 --- a/lib/openai/base_stream.rb +++ b/lib/openai/base_stream.rb @@ -17,22 +17,9 @@ module OpenAI # messages => Array # ``` class BaseStream - # @private - # - # @param model [Class, OpenAI::Converter] - # @param url [URI::Generic] - # @param status [Integer] - # @param response [Net::HTTPResponse] - # @param messages [Enumerable] + # @return [void] # - def initialize(model:, url:, status:, response:, messages:) - @model = model - @url = url - @status = status - @response = response - @messages = messages - @iterator = iterator - end + def close = OpenAI::Util.close_fused!(@iterator) # @private # @@ -40,10 +27,6 @@ def initialize(model:, url:, status:, response:, messages:) # private def iterator = (raise NotImplementedError) - # @return [void] - # - def close = OpenAI::Util.close_fused!(@iterator) - # @param blk [Proc] # # @return [void] @@ -60,5 +43,22 @@ def for_each(&) def to_enum = @iterator alias_method :enum_for, :to_enum + + # @private + # + # @param model [Class, OpenAI::Converter] + # @param url [URI::Generic] + # @param status [Integer] + # @param response [Net::HTTPResponse] + # @param messages [Enumerable] + # + def initialize(model:, url:, status:, response:, messages:) + @model = model + @url = url + @status = status + @response = response + @messages = messages + @iterator = iterator + end end end diff --git a/lib/openai/cursor_page.rb b/lib/openai/cursor_page.rb index f9f84dcd..0e74ea9e 100644 --- a/lib/openai/cursor_page.rb +++ b/lib/openai/cursor_page.rb @@ -30,7 +30,6 @@ class CursorPage # @return [Boolean] attr_accessor :has_more - # rubocop:disable Lint/UnusedMethodArgument # @private # # @param client [OpenAI::BaseClient] @@ -39,8 +38,7 @@ class CursorPage # @param page_data [Hash{Symbol=>Object}] # def initialize(client:, req:, headers:, page_data:) - @client = client - @req = req + super model = req.fetch(:model) case page_data @@ -55,10 +53,8 @@ def initialize(client:, req:, headers:, page_data:) else end end - # rubocop:enable Lint/UnusedMethodArgument # @return [Boolean] - # def next_page? has_more end diff --git a/lib/openai/page.rb b/lib/openai/page.rb index 54fc4d39..fa3bd198 100644 --- a/lib/openai/page.rb +++ b/lib/openai/page.rb @@ -30,7 +30,6 @@ class Page # @return [String] attr_accessor :object - # rubocop:disable Lint/UnusedMethodArgument # @private # # @param client [OpenAI::BaseClient] @@ -39,8 +38,7 @@ class Page # @param page_data [Array] # def initialize(client:, req:, headers:, page_data:) - @client = client - @req = req + super model = req.fetch(:model) case page_data @@ -55,10 +53,8 @@ def initialize(client:, req:, headers:, page_data:) else end end - # rubocop:enable Lint/UnusedMethodArgument # @return [Boolean] - # def next_page? false end diff --git a/manifest.yaml b/manifest.yaml index e2306edd..fa9c3e5e 100644 --- a/manifest.yaml +++ b/manifest.yaml @@ -5,6 +5,7 @@ dependencies: - etc - json - net/http + - pathname - rbconfig - securerandom - set diff --git a/rbi/lib/openai/base_stream.rbi b/rbi/lib/openai/base_stream.rbi index edd7627b..f527b849 100644 --- a/rbi/lib/openai/base_stream.rbi +++ b/rbi/lib/openai/base_stream.rbi @@ -4,27 +4,14 @@ module OpenAI class BaseStream Elem = type_member(:out) - sig do - params( - model: T.any(T::Class[T.anything], OpenAI::Converter), - url: URI::Generic, - status: Integer, - response: Net::HTTPResponse, - messages: T::Enumerable[OpenAI::Util::SSEMessage] - ) - .returns(T.attached_class) - end - def self.new(model:, url:, status:, response:, messages:) + sig { void } + def close end sig { overridable.returns(T::Enumerable[Elem]) } private def iterator end - sig { void } - def close - end - sig { params(blk: T.proc.params(arg0: Elem).void).void } def for_each(&blk) end @@ -34,5 +21,18 @@ module OpenAI end alias_method :enum_for, :to_enum + + sig do + params( + model: T.any(T::Class[T.anything], OpenAI::Converter), + url: URI::Generic, + status: Integer, + response: Net::HTTPResponse, + messages: T::Enumerable[OpenAI::Util::SSEMessage] + ) + .returns(T.attached_class) + end + def self.new(model:, url:, status:, response:, messages:) + end end end diff --git a/sig/openai/base_stream.rbs b/sig/openai/base_stream.rbs index e80fcba0..397d46e5 100644 --- a/sig/openai/base_stream.rbs +++ b/sig/openai/base_stream.rbs @@ -1,21 +1,21 @@ module OpenAI class BaseStream[Elem] - def initialize: ( - model: Class | OpenAI::Converter, - url: URI::Generic, - status: Integer, - response: top, - messages: Enumerable[OpenAI::Util::sse_message] - ) -> void + def close: -> void private def iterator: -> Enumerable[Elem] - def close: -> void - def for_each: { (Elem arg0) -> void } -> void def to_enum: -> Enumerable[Elem] alias enum_for to_enum + + def initialize: ( + model: Class | OpenAI::Converter, + url: URI::Generic, + status: Integer, + response: top, + messages: Enumerable[OpenAI::Util::sse_message] + ) -> void end end From 3450adf1f6af58b6626e874866276414b71dcf22 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 13 Mar 2025 16:32:08 +0000 Subject: [PATCH 5/8] chore: improve rbi typedef for page classes (#6) --- rbi/lib/openai/cursor_page.rbi | 4 ++-- rbi/lib/openai/page.rbi | 4 ++-- 2 files changed, 4 insertions(+), 4 deletions(-) diff --git a/rbi/lib/openai/cursor_page.rbi b/rbi/lib/openai/cursor_page.rbi index d7c63bcf..ffbe0d60 100644 --- a/rbi/lib/openai/cursor_page.rbi +++ b/rbi/lib/openai/cursor_page.rbi @@ -29,9 +29,9 @@ module OpenAI headers: T.any(T::Hash[String, String], Net::HTTPHeader), page_data: T::Hash[Symbol, T.anything] ) - .void + .returns(T.attached_class) end - def initialize(client:, req:, headers:, page_data:) + def self.new(client:, req:, headers:, page_data:) end end end diff --git a/rbi/lib/openai/page.rbi b/rbi/lib/openai/page.rbi index 876486a4..9f33956d 100644 --- a/rbi/lib/openai/page.rbi +++ b/rbi/lib/openai/page.rbi @@ -29,9 +29,9 @@ module OpenAI headers: T.any(T::Hash[String, String], Net::HTTPHeader), page_data: T::Array[T.anything] ) - .void + .returns(T.attached_class) end - def initialize(client:, req:, headers:, page_data:) + def self.new(client:, req:, headers:, page_data:) end end end From 8ce4f87ca9496143d8c85000b0fb1f8b00eedea6 Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 13 Mar 2025 16:48:55 +0000 Subject: [PATCH 6/8] chore(internal): remove extra empty newlines (#7) --- .github/workflows/ci.yml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index c4d79b2d..de0c9185 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -43,4 +43,3 @@ jobs: - name: Run tests run: ./scripts/test - From c83ab3717be335b49ef38663b759d14c92841f5c Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 13 Mar 2025 17:55:23 +0000 Subject: [PATCH 7/8] chore: more accurate generic params for stream classes (#8) --- lib/openai/base_stream.rb | 2 +- lib/openai/stream.rb | 4 +++- rbi/lib/openai/base_client.rbi | 4 ++-- rbi/lib/openai/base_stream.rbi | 9 +++++---- rbi/lib/openai/stream.rbi | 18 +++++++++++++++++- sig/openai/base_stream.rbs | 4 ++-- sig/openai/stream.rbs | 12 +++++++++++- 7 files changed, 41 insertions(+), 12 deletions(-) diff --git a/lib/openai/base_stream.rb b/lib/openai/base_stream.rb index 082d045d..c2beb4b9 100644 --- a/lib/openai/base_stream.rb +++ b/lib/openai/base_stream.rb @@ -16,7 +16,7 @@ module OpenAI # # messages => Array # ``` - class BaseStream + module BaseStream # @return [void] # def close = OpenAI::Util.close_fused!(@iterator) diff --git a/lib/openai/stream.rb b/lib/openai/stream.rb index 801b6247..f9319992 100644 --- a/lib/openai/stream.rb +++ b/lib/openai/stream.rb @@ -16,7 +16,9 @@ module OpenAI # # messages => Array # ``` - class Stream < OpenAI::BaseStream + class Stream + include OpenAI::BaseStream + # @private # # @return [Enumerable] diff --git a/rbi/lib/openai/base_client.rbi b/rbi/lib/openai/base_client.rbi index ae1f372e..fd80c3c5 100644 --- a/rbi/lib/openai/base_client.rbi +++ b/rbi/lib/openai/base_client.rbi @@ -22,7 +22,7 @@ module OpenAI body: T.nilable(T.anything), unwrap: T.nilable(Symbol), page: T.nilable(T::Class[OpenAI::BasePage[OpenAI::BaseModel]]), - stream: T.nilable(T::Class[OpenAI::BaseStream[OpenAI::BaseModel]]), + stream: T.nilable(T::Class[OpenAI::BaseStream[T.anything, OpenAI::BaseModel]]), model: T.nilable(OpenAI::Converter::Input), options: T.nilable(T.any(OpenAI::RequestOptions, T::Hash[Symbol, T.anything])) } @@ -148,7 +148,7 @@ module OpenAI body: T.nilable(T.anything), unwrap: T.nilable(Symbol), page: T.nilable(T::Class[OpenAI::BasePage[OpenAI::BaseModel]]), - stream: T.nilable(T::Class[OpenAI::BaseStream[OpenAI::BaseModel]]), + stream: T.nilable(T::Class[OpenAI::BaseStream[T.anything, OpenAI::BaseModel]]), model: T.nilable(OpenAI::Converter::Input), options: T.nilable(T.any(OpenAI::RequestOptions, T::Hash[Symbol, T.anything])) ) diff --git a/rbi/lib/openai/base_stream.rbi b/rbi/lib/openai/base_stream.rbi index f527b849..8b829bd1 100644 --- a/rbi/lib/openai/base_stream.rbi +++ b/rbi/lib/openai/base_stream.rbi @@ -1,7 +1,8 @@ # typed: strong module OpenAI - class BaseStream + module BaseStream + Message = type_member(:in) Elem = type_member(:out) sig { void } @@ -28,11 +29,11 @@ module OpenAI url: URI::Generic, status: Integer, response: Net::HTTPResponse, - messages: T::Enumerable[OpenAI::Util::SSEMessage] + messages: T::Enumerable[Message] ) - .returns(T.attached_class) + .void end - def self.new(model:, url:, status:, response:, messages:) + def initialize(model:, url:, status:, response:, messages:) end end end diff --git a/rbi/lib/openai/stream.rbi b/rbi/lib/openai/stream.rbi index f4bf6fa9..3dc46e28 100644 --- a/rbi/lib/openai/stream.rbi +++ b/rbi/lib/openai/stream.rbi @@ -1,11 +1,27 @@ # typed: strong module OpenAI - class Stream < OpenAI::BaseStream + class Stream + include OpenAI::BaseStream + + Message = type_member(:in) { {fixed: OpenAI::Util::SSEMessage} } Elem = type_member(:out) sig { override.returns(T::Enumerable[Elem]) } private def iterator end + + sig do + params( + model: T.any(T::Class[T.anything], OpenAI::Converter), + url: URI::Generic, + status: Integer, + response: Net::HTTPResponse, + messages: T::Enumerable[OpenAI::Util::SSEMessage] + ) + .returns(T.attached_class) + end + def self.new(model:, url:, status:, response:, messages:) + end end end diff --git a/sig/openai/base_stream.rbs b/sig/openai/base_stream.rbs index 397d46e5..e5d9ec89 100644 --- a/sig/openai/base_stream.rbs +++ b/sig/openai/base_stream.rbs @@ -1,5 +1,5 @@ module OpenAI - class BaseStream[Elem] + module BaseStream[Message, Elem] def close: -> void private def iterator: -> Enumerable[Elem] @@ -15,7 +15,7 @@ module OpenAI url: URI::Generic, status: Integer, response: top, - messages: Enumerable[OpenAI::Util::sse_message] + messages: Enumerable[Message] ) -> void end end diff --git a/sig/openai/stream.rbs b/sig/openai/stream.rbs index 78d58b92..675ecb74 100644 --- a/sig/openai/stream.rbs +++ b/sig/openai/stream.rbs @@ -1,5 +1,15 @@ module OpenAI - class Stream[Elem] < OpenAI::BaseStream[Elem] + class Stream[Elem] + include OpenAI::BaseStream[OpenAI::Util::sse_message, Elem] + private def iterator: -> Enumerable[Elem] + + def initialize: ( + model: Class | OpenAI::Converter, + url: URI::Generic, + status: Integer, + response: top, + messages: Enumerable[OpenAI::Util::sse_message] + ) -> void end end From 90d10de7ddea71c2ef820e95baa6bfb83509fd8e Mon Sep 17 00:00:00 2001 From: "stainless-app[bot]" <142633134+stainless-app[bot]@users.noreply.github.com> Date: Thu, 13 Mar 2025 17:55:42 +0000 Subject: [PATCH 8/8] release: 0.1.0-alpha.1 --- .release-please-manifest.json | 2 +- CHANGELOG.md | 24 ++++++++++++++++++++++++ Gemfile.lock | 2 +- lib/openai/version.rb | 2 +- rbi/lib/openai/version.rbi | 2 +- 5 files changed, 28 insertions(+), 4 deletions(-) create mode 100644 CHANGELOG.md diff --git a/.release-please-manifest.json b/.release-please-manifest.json index c4762802..ba6c3483 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.0.1-alpha.0" + ".": "0.1.0-alpha.1" } \ No newline at end of file diff --git a/CHANGELOG.md b/CHANGELOG.md new file mode 100644 index 00000000..31ecea67 --- /dev/null +++ b/CHANGELOG.md @@ -0,0 +1,24 @@ +# Changelog + +## 0.1.0-alpha.1 (2025-03-13) + +Full Changelog: [v0.0.1-alpha.0...v0.1.0-alpha.1](https://github.com/openai/openai-ruby/compare/v0.0.1-alpha.0...v0.1.0-alpha.1) + +### Features + +* support streaming uploads ([#1](https://github.com/openai/openai-ruby/issues/1)) ([8d5317a](https://github.com/openai/openai-ruby/commit/8d5317a4a3035e24de78d226658a821a8893a283)) + + +### Bug Fixes + +* enums should only coerce matching symbols into strings ([#3](https://github.com/openai/openai-ruby/issues/3)) ([0f4c842](https://github.com/openai/openai-ruby/commit/0f4c842a52c679ab0793fcdc32e5555fbbea4178)) + + +### Chores + +* improve documentation ([d9ce7ce](https://github.com/openai/openai-ruby/commit/d9ce7cedcc96ea81529fbaabf18dcab871dd412f)) +* improve rbi typedef for page classes ([#6](https://github.com/openai/openai-ruby/issues/6)) ([3450adf](https://github.com/openai/openai-ruby/commit/3450adf1f6af58b6626e874866276414b71dcf22)) +* **internal:** remove extra empty newlines ([#7](https://github.com/openai/openai-ruby/issues/7)) ([8ce4f87](https://github.com/openai/openai-ruby/commit/8ce4f87ca9496143d8c85000b0fb1f8b00eedea6)) +* more accurate generic params for stream classes ([#8](https://github.com/openai/openai-ruby/issues/8)) ([c83ab37](https://github.com/openai/openai-ruby/commit/c83ab3717be335b49ef38663b759d14c92841f5c)) +* refactor BasePage to have initializer ([#5](https://github.com/openai/openai-ruby/issues/5)) ([43e80fa](https://github.com/openai/openai-ruby/commit/43e80fa252a9b1ae4d5c219dd9d346f0b0c3194f)) +* remove stale thread local checks ([#4](https://github.com/openai/openai-ruby/issues/4)) ([69e8be8](https://github.com/openai/openai-ruby/commit/69e8be897fe8cff1ed9b0ea7ef53a759a9a089ff)) diff --git a/Gemfile.lock b/Gemfile.lock index 37e29d5a..341cc850 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,7 +11,7 @@ GIT PATH remote: . specs: - openai (0.0.1.pre.alpha.0) + openai (0.1.0.pre.alpha.1) connection_pool GEM diff --git a/lib/openai/version.rb b/lib/openai/version.rb index 804492b4..bcab79ff 100644 --- a/lib/openai/version.rb +++ b/lib/openai/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module OpenAI - VERSION = "0.0.1-alpha.0" + VERSION = "0.1.0-alpha.1" end diff --git a/rbi/lib/openai/version.rbi b/rbi/lib/openai/version.rbi index 4cab3ef4..5366ece5 100644 --- a/rbi/lib/openai/version.rbi +++ b/rbi/lib/openai/version.rbi @@ -1,5 +1,5 @@ # typed: strong module OpenAI - VERSION = "0.0.1-alpha.0" + VERSION = "0.1.0-alpha.1" end