openai 0.77.0 → 0.78.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -55,7 +55,7 @@ module OpenAI
55
55
  # @param url [URI::Generic]
56
56
  # @param status [Integer]
57
57
  # @param headers [Hash{String=>String}]
58
- # @param response [Net::HTTPResponse]
58
+ # @param response [OpenAI::HTTPClient::Response]
59
59
  # @param unwrap [Symbol, Integer, Array<Symbol, Integer>, Proc]
60
60
  # @param stream [Enumerable<Object>]
61
61
  def initialize(model:, url:, status:, headers:, response:, unwrap:, stream:)
@@ -4,6 +4,11 @@ module OpenAI
4
4
  module Internal
5
5
  # @api private
6
6
  module Util
7
+ # Marks enumerators whose overridden rewind safely closes the iterator.
8
+ module FusedEnumerator
9
+ end
10
+ private_constant :FusedEnumerator
11
+
7
12
  # @api private
8
13
  #
9
14
  # @return [Float]
@@ -397,90 +402,6 @@ module OpenAI
397
402
  end
398
403
  end
399
404
 
400
- # @api private
401
- #
402
- # An adapter that satisfies the IO interface required by `::IO.copy_stream`
403
- class ReadIOAdapter
404
- # @api private
405
- #
406
- # @return [Boolean, nil]
407
- def close? = @closing
408
-
409
- # @api private
410
- def close
411
- case @stream
412
- in Enumerator
413
- OpenAI::Internal::Util.close_fused!(@stream)
414
- in IO if close?
415
- @stream.close
416
- else
417
- end
418
- end
419
-
420
- # @api private
421
- #
422
- # @param max_len [Integer, nil]
423
- #
424
- # @return [String]
425
- private def read_enum(max_len)
426
- case max_len
427
- in nil
428
- @stream.to_a.join
429
- in Integer
430
- @buf << @stream.next while @buf.length < max_len
431
- @buf.slice!(..max_len)
432
- end
433
- rescue StopIteration
434
- @stream = nil
435
- @buf.slice!(0..)
436
- end
437
-
438
- # @api private
439
- #
440
- # @param max_len [Integer, nil]
441
- # @param out_string [String, nil]
442
- #
443
- # @return [String, nil]
444
- def read(max_len = nil, out_string = nil)
445
- case @stream
446
- in nil
447
- nil
448
- in IO | StringIO
449
- @stream.read(max_len, out_string)
450
- in Enumerator
451
- read = read_enum(max_len)
452
- case out_string
453
- in String
454
- out_string.replace(read)
455
- in nil
456
- read
457
- end
458
- end
459
- .tap(&@blk)
460
- end
461
-
462
- # @api private
463
- #
464
- # @param src [String, Pathname, StringIO, Enumerable<String>]
465
- # @param blk [Proc]
466
- #
467
- # @yieldparam [String]
468
- def initialize(src, &blk)
469
- @stream =
470
- case src
471
- in String
472
- StringIO.new(src)
473
- in Pathname
474
- @closing = true
475
- src.open(binmode: true)
476
- else
477
- src
478
- end
479
- @buf = String.new
480
- @blk = blk
481
- end
482
- end
483
-
484
405
  class << self
485
406
  # @param blk [Proc]
486
407
  #
@@ -768,6 +689,7 @@ module OpenAI
768
689
  close&.call
769
690
  close = nil
770
691
  end
692
+ iter.extend(FusedEnumerator)
771
693
 
772
694
  iter.define_singleton_method(:rewind) do
773
695
  fused = true
@@ -780,7 +702,7 @@ module OpenAI
780
702
  #
781
703
  # @param enum [Enumerable<Object>, nil]
782
704
  def close_fused!(enum)
783
- return unless enum.is_a?(Enumerator)
705
+ return unless enum.is_a?(FusedEnumerator)
784
706
 
785
707
  # rubocop:disable Lint/UnreachableLoop
786
708
  enum.rewind.each { break }
@@ -0,0 +1,281 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "internal/read_io_adapter"
4
+
5
+ module OpenAI
6
+ # The SDK's pooled Net::HTTP implementation.
7
+ #
8
+ # Pass a block to configure each SDK-created connection before it is pooled
9
+ # and started.
10
+ class NetHTTPClient < HTTPClient
11
+ # from the golang stdlib
12
+ # https://github.com/golang/go/blob/c8eced8580028328fde7c03cbfcb720ce15b2358/src/net/http/transport.go#L49
13
+ KEEP_ALIVE_TIMEOUT = 30
14
+
15
+ DEFAULT_MAX_CONNECTIONS = [Etc.nprocessors, 99].max
16
+
17
+ NETWORK_ERRORS = [
18
+ EOFError,
19
+ IOError,
20
+ SocketError,
21
+ SystemCallError,
22
+ OpenSSL::SSL::SSLError,
23
+ Net::HTTPBadResponse,
24
+ Net::HTTPHeaderSyntaxError,
25
+ Net::ProtocolError,
26
+ (defined?(Zlib::Error) ? Zlib::Error : nil),
27
+ ConnectionPool::PoolShuttingDownError
28
+ ].compact.freeze
29
+ private_constant :NETWORK_ERRORS
30
+
31
+ class ConnectionConfigurationError < StandardError
32
+ attr_reader :original
33
+
34
+ def initialize(original)
35
+ @original = original
36
+ super()
37
+ end
38
+ end
39
+ private_constant :ConnectionConfigurationError
40
+
41
+ # @api private
42
+ #
43
+ # @param url [URI::Generic]
44
+ #
45
+ # @return [Net::HTTP]
46
+ private def connect(url:)
47
+ port =
48
+ case [url.port, url.scheme]
49
+ in [Integer, _]
50
+ url.port
51
+ in [nil, "http" | "ws"]
52
+ Net::HTTP.http_default_port
53
+ in [nil, "https" | "wss"]
54
+ Net::HTTP.https_default_port
55
+ end
56
+
57
+ Net::HTTP.new(url.host, port).tap do
58
+ _1.use_ssl = %w[https wss].include?(url.scheme)
59
+ _1.keep_alive_timeout = KEEP_ALIVE_TIMEOUT
60
+ _1.max_retries = 0
61
+
62
+ (_1.cert_store = @cert_store) if _1.use_ssl?
63
+ end
64
+ end
65
+
66
+ # @api private
67
+ #
68
+ # @param conn [Net::HTTP]
69
+ # @param deadline [Float]
70
+ private def calibrate_socket_timeout(conn, deadline)
71
+ timeout = remaining_timeout(deadline)
72
+ conn.open_timeout = conn.read_timeout = conn.write_timeout = conn.continue_timeout = timeout
73
+ end
74
+
75
+ # @api private
76
+ #
77
+ # @param deadline [Float]
78
+ # @return [Float]
79
+ # @raise [Timeout::Error]
80
+ private def remaining_timeout(deadline)
81
+ timeout = deadline - OpenAI::Internal::Util.monotonic_secs
82
+ raise Timeout::Error, "request timed out" unless timeout.positive?
83
+
84
+ timeout
85
+ end
86
+
87
+ # @api private
88
+ #
89
+ # @param request [OpenAI::HTTPClient::Request]
90
+ # @param blk [Proc]
91
+ #
92
+ # @yieldparam [String]
93
+ # @return [Array(Net::HTTPGenericRequest, Proc)]
94
+ private def build_request(request, &blk)
95
+ method = request.method
96
+ body = request.body
97
+ req = Net::HTTPGenericRequest.new(
98
+ method.to_s.upcase,
99
+ !body.nil?,
100
+ method != :head,
101
+ URI(request.url.to_s)
102
+ )
103
+
104
+ request.headers.each { req[_1] = _2 }
105
+
106
+ case body
107
+ in nil
108
+ req["content-length"] ||= 0 unless req["transfer-encoding"]
109
+ in String
110
+ req["content-length"] ||= body.bytesize.to_s unless req["transfer-encoding"]
111
+ req.body_stream = OpenAI::Internal::Util::ReadIOAdapter.new(body, &blk)
112
+ in StringIO
113
+ req["content-length"] ||= body.size.to_s unless req["transfer-encoding"]
114
+ req.body_stream = OpenAI::Internal::Util::ReadIOAdapter.new(body, &blk)
115
+ in Pathname | IO | Enumerator
116
+ req["transfer-encoding"] ||= "chunked" unless req["content-length"]
117
+ req.body_stream = OpenAI::Internal::Util::ReadIOAdapter.new(body, &blk)
118
+ end
119
+
120
+ [req, req.body_stream&.method(:close)]
121
+ end
122
+
123
+ # @api private
124
+ #
125
+ # @param url [URI::Generic]
126
+ # @param deadline [Float]
127
+ # @param blk [Proc]
128
+ #
129
+ # @raise [Timeout::Error]
130
+ # @yieldparam [Net::HTTP]
131
+ private def with_pool(url, deadline:, &blk)
132
+ origin = OpenAI::Internal::Util.uri_origin(url)
133
+ timeout = remaining_timeout(deadline)
134
+ pool =
135
+ @mutex.synchronize do
136
+ @pools[origin] ||= ConnectionPool.new(size: @size) do
137
+ configured_connection(url)
138
+ end
139
+ end
140
+
141
+ pool.with(timeout: timeout, &blk)
142
+ end
143
+
144
+ # @api private
145
+ #
146
+ # @param url [URI::Generic]
147
+ #
148
+ # @return [Net::HTTP]
149
+ private def configured_connection(url)
150
+ connection = nil
151
+ connection = connect(url: url)
152
+ begin
153
+ @connection_configurator&.call(connection)
154
+ rescue StandardError => e
155
+ raise ConnectionConfigurationError.new(e)
156
+ end
157
+
158
+ if connection.started?
159
+ raise ArgumentError, "connection configuration must leave the connection unstarted"
160
+ end
161
+
162
+ expected_ssl = %w[https wss].include?(url.scheme)
163
+ unless connection.use_ssl? == expected_ssl
164
+ raise ArgumentError, "connection configuration must preserve TLS for the requested URL"
165
+ end
166
+
167
+ connection.max_retries = 0
168
+ connection
169
+ rescue StandardError
170
+ begin
171
+ connection.finish if connection&.started?
172
+ rescue StandardError
173
+ nil
174
+ end
175
+ raise
176
+ end
177
+
178
+ # Closes current pooled connections. The client remains reusable and will
179
+ # create fresh pools on subsequent requests.
180
+ #
181
+ # In-flight requests are allowed to finish before their connection closes.
182
+ #
183
+ # @return [void]
184
+ def close
185
+ pools =
186
+ @mutex.synchronize do
187
+ current_pools = @pools
188
+ @pools = {}
189
+ current_pools
190
+ end
191
+ pools.each_value do |pool|
192
+ pool.shutdown { |connection| connection.finish if connection.started? }
193
+ end
194
+ nil
195
+ end
196
+
197
+ # Executes a request using a pooled Net::HTTP connection.
198
+ #
199
+ # @param request [OpenAI::HTTPClient::Request]
200
+ # @return [OpenAI::HTTPClient::Response]
201
+ def execute(request)
202
+ url = request.url
203
+ deadline = OpenAI::Internal::Util.monotonic_secs + request.timeout
204
+
205
+ req = nil
206
+ finished = false
207
+
208
+ # rubocop:disable Metrics/BlockLength
209
+ enum = Enumerator.new do |y|
210
+ next if finished
211
+
212
+ with_pool(url, deadline: deadline) do |conn|
213
+ eof = false
214
+ closing = nil
215
+ ::Thread.handle_interrupt(Object => :never) do
216
+ ::Thread.handle_interrupt(Object => :immediate) do
217
+ req, closing = build_request(request) do
218
+ calibrate_socket_timeout(conn, deadline)
219
+ end
220
+
221
+ calibrate_socket_timeout(conn, deadline)
222
+ conn.start unless conn.started?
223
+
224
+ calibrate_socket_timeout(conn, deadline)
225
+ ::Kernel.catch(:jump) do
226
+ conn.request(req) do |rsp|
227
+ y << [req, rsp]
228
+ ::Kernel.throw(:jump) if finished
229
+
230
+ rsp.read_body do |bytes|
231
+ y << bytes.force_encoding(Encoding::BINARY)
232
+ ::Kernel.throw(:jump) if finished
233
+
234
+ calibrate_socket_timeout(conn, deadline)
235
+ end
236
+ eof = true
237
+ end
238
+ end
239
+ end
240
+ ensure
241
+ begin
242
+ conn.finish if !eof && conn&.started?
243
+ ensure
244
+ closing&.call
245
+ end
246
+ end
247
+ end
248
+ rescue ConnectionConfigurationError => e
249
+ raise e.original, cause: e.original.cause
250
+ rescue Timeout::Error
251
+ raise OpenAI::Errors::APITimeoutError.new(url: url, request: req)
252
+ rescue *NETWORK_ERRORS
253
+ raise OpenAI::Errors::APIConnectionError.new(url: url, request: req)
254
+ end
255
+ # rubocop:enable Metrics/BlockLength
256
+
257
+ _, response = enum.next
258
+ body = OpenAI::Internal::Util.fused_enum(enum, external: true) do
259
+ finished = true
260
+ loop { enum.next }
261
+ end
262
+ OpenAI::HTTPClient::Response.new(
263
+ status: Integer(response.code),
264
+ headers: response.each_header.to_h,
265
+ body: body
266
+ )
267
+ end
268
+
269
+ # @param size [Integer]
270
+ # @param connection_configurator [#call, nil] A block that configures every
271
+ # SDK-created Net::HTTP connection before it is pooled and started.
272
+ def initialize(size: self.class::DEFAULT_MAX_CONNECTIONS, &connection_configurator)
273
+ super()
274
+ @mutex = Mutex.new
275
+ @size = size
276
+ @cert_store = OpenSSL::X509::Store.new.tap(&:set_default_paths)
277
+ @connection_configurator = connection_configurator
278
+ @pools = {}
279
+ end
280
+ end
281
+ end
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module OpenAI
4
- VERSION = "0.77.0"
4
+ VERSION = "0.78.0"
5
5
  end
data/lib/openai.rb CHANGED
@@ -53,11 +53,12 @@ require_relative "openai/internal"
53
53
  require_relative "openai/request_options"
54
54
  require_relative "openai/file_part"
55
55
  require_relative "openai/errors"
56
+ require_relative "openai/http_client"
57
+ require_relative "openai/net_http_client"
56
58
  require_relative "openai/provider"
57
59
  require_relative "openai/internal/provider"
58
60
  require_relative "openai/providers/bedrock"
59
61
  require_relative "openai/internal/transport/base_client"
60
- require_relative "openai/internal/transport/pooled_net_requester"
61
62
  require_relative "openai/client"
62
63
  require_relative "openai/internal/stream"
63
64
  require_relative "openai/internal/conversation_cursor_page"
@@ -162,7 +162,8 @@ module OpenAI
162
162
  max_retries: Integer,
163
163
  timeout: Float,
164
164
  initial_retry_delay: Float,
165
- max_retry_delay: Float
165
+ max_retry_delay: Float,
166
+ http_client: T.untyped
166
167
  ).returns(T.attached_class)
167
168
  end
168
169
  def self.new(
@@ -185,7 +186,8 @@ module OpenAI
185
186
  max_retries: OpenAI::Client::DEFAULT_MAX_RETRIES,
186
187
  timeout: OpenAI::Client::DEFAULT_TIMEOUT_IN_SECONDS,
187
188
  initial_retry_delay: OpenAI::Client::DEFAULT_INITIAL_RETRY_DELAY,
188
- max_retry_delay: OpenAI::Client::DEFAULT_MAX_RETRY_DELAY
189
+ max_retry_delay: OpenAI::Client::DEFAULT_MAX_RETRY_DELAY,
190
+ http_client: nil
189
191
  )
190
192
  end
191
193
  end
@@ -0,0 +1,63 @@
1
+ # typed: strong
2
+
3
+ module OpenAI
4
+ class HTTPClient
5
+ class Request
6
+ sig { returns(Symbol) }
7
+ attr_reader :method
8
+
9
+ sig { returns(URI::Generic) }
10
+ attr_reader :url
11
+
12
+ sig { returns(T::Hash[String, String]) }
13
+ attr_reader :headers
14
+
15
+ sig { returns(T.anything) }
16
+ attr_reader :body
17
+
18
+ sig { returns(Float) }
19
+ attr_reader :timeout
20
+
21
+ sig do
22
+ params(
23
+ method: Symbol,
24
+ url: URI::Generic,
25
+ headers: T::Hash[String, String],
26
+ body: T.anything,
27
+ timeout: Float
28
+ ).returns(T.attached_class)
29
+ end
30
+ def self.new(method:, url:, headers:, body:, timeout:)
31
+ end
32
+ end
33
+
34
+ class Response
35
+ sig { returns(Integer) }
36
+ attr_reader :status
37
+
38
+ sig { returns(T::Hash[String, String]) }
39
+ attr_reader :headers
40
+
41
+ sig { returns(T::Enumerable[String]) }
42
+ attr_reader :body
43
+
44
+ sig do
45
+ params(
46
+ status: Integer,
47
+ headers: T::Hash[String, String],
48
+ body: T.any(String, T::Enumerable[String])
49
+ ).returns(T.attached_class)
50
+ end
51
+ def self.new(status:, headers:, body:)
52
+ end
53
+ end
54
+
55
+ sig do
56
+ params(request: OpenAI::HTTPClient::Request).returns(
57
+ OpenAI::HTTPClient::Response
58
+ )
59
+ end
60
+ def execute(request)
61
+ end
62
+ end
63
+ end
@@ -0,0 +1,47 @@
1
+ # typed: strong
2
+
3
+ module OpenAI
4
+ module Internal
5
+ module Util
6
+ # @api private
7
+ #
8
+ # An adapter that satisfies the IO interface required by `::IO.copy_stream`
9
+ class ReadIOAdapter
10
+ # @api private
11
+ sig { returns(T.nilable(T::Boolean)) }
12
+ def close?
13
+ end
14
+
15
+ # @api private
16
+ sig { void }
17
+ def close
18
+ end
19
+
20
+ # @api private
21
+ sig { params(max_len: T.nilable(Integer)).returns(String) }
22
+ private def read_enum(max_len)
23
+ end
24
+
25
+ # @api private
26
+ sig do
27
+ params(
28
+ max_len: T.nilable(Integer),
29
+ out_string: T.nilable(String)
30
+ ).returns(T.nilable(String))
31
+ end
32
+ def read(max_len = nil, out_string = nil)
33
+ end
34
+
35
+ # @api private
36
+ sig do
37
+ params(
38
+ src: T.any(String, Pathname, StringIO, T::Enumerable[String]),
39
+ blk: T.proc.params(arg0: String).void
40
+ ).returns(T.attached_class)
41
+ end
42
+ def self.new(src, &blk)
43
+ end
44
+ end
45
+ end
46
+ end
47
+ end
@@ -103,6 +103,11 @@ module OpenAI
103
103
  def should_retry?(status, headers:)
104
104
  end
105
105
 
106
+ # @api private
107
+ sig { params(body: T.untyped).returns(T::Boolean) }
108
+ def request_body_replayable?(body)
109
+ end
110
+
106
111
  # @api private
107
112
  sig do
108
113
  params(
@@ -147,7 +152,7 @@ module OpenAI
147
152
  attr_reader :idempotency_header
148
153
 
149
154
  # @api private
150
- sig { returns(OpenAI::Internal::Transport::PooledNetRequester) }
155
+ sig { returns(T.untyped) }
151
156
  attr_reader :requester
152
157
 
153
158
  # @api private
@@ -169,7 +174,8 @@ module OpenAI
169
174
  )
170
175
  )
171
176
  ],
172
- idempotency_header: T.nilable(String)
177
+ idempotency_header: T.nilable(String),
178
+ http_client: T.untyped
173
179
  ).returns(T.attached_class)
174
180
  end
175
181
  def self.new(
@@ -179,7 +185,8 @@ module OpenAI
179
185
  initial_retry_delay: 0.0,
180
186
  max_retry_delay: 0.0,
181
187
  headers: {},
182
- idempotency_header: nil
188
+ idempotency_header: nil,
189
+ http_client: nil
183
190
  )
184
191
  end
185
192
 
@@ -223,6 +230,15 @@ module OpenAI
223
230
  private def build_request(req, opts)
224
231
  end
225
232
 
233
+ # @api private
234
+ sig do
235
+ params(
236
+ request: OpenAI::Internal::Transport::BaseClient::RequestInput
237
+ ).returns(T::Boolean)
238
+ end
239
+ private def request_replayable?(request)
240
+ end
241
+
226
242
  # @api private
227
243
  sig do
228
244
  params(
@@ -239,7 +255,7 @@ module OpenAI
239
255
  url: URI::Generic,
240
256
  status: Integer,
241
257
  headers: T::Hash[String, String],
242
- response: Net::HTTPResponse,
258
+ response: OpenAI::HTTPClient::Response,
243
259
  stream: T::Enumerable[String]
244
260
  ).returns(T.noreturn)
245
261
  end
@@ -259,7 +275,7 @@ module OpenAI
259
275
  redirect_count: Integer,
260
276
  retry_count: Integer,
261
277
  send_retry_header: T::Boolean
262
- ).returns([Integer, Net::HTTPResponse, T::Enumerable[String]])
278
+ ).returns(OpenAI::HTTPClient::Response)
263
279
  end
264
280
  def send_request(
265
281
  request,
@@ -43,7 +43,7 @@ module OpenAI
43
43
  url: URI::Generic,
44
44
  status: Integer,
45
45
  headers: T::Hash[String, String],
46
- response: Net::HTTPResponse,
46
+ response: OpenAI::HTTPClient::Response,
47
47
  unwrap:
48
48
  T.any(
49
49
  Symbol,
@@ -253,46 +253,6 @@ module OpenAI
253
253
  end
254
254
  end
255
255
 
256
- # @api private
257
- #
258
- # An adapter that satisfies the IO interface required by `::IO.copy_stream`
259
- class ReadIOAdapter
260
- # @api private
261
- sig { returns(T.nilable(T::Boolean)) }
262
- def close?
263
- end
264
-
265
- # @api private
266
- sig { void }
267
- def close
268
- end
269
-
270
- # @api private
271
- sig { params(max_len: T.nilable(Integer)).returns(String) }
272
- private def read_enum(max_len)
273
- end
274
-
275
- # @api private
276
- sig do
277
- params(
278
- max_len: T.nilable(Integer),
279
- out_string: T.nilable(String)
280
- ).returns(T.nilable(String))
281
- end
282
- def read(max_len = nil, out_string = nil)
283
- end
284
-
285
- # @api private
286
- sig do
287
- params(
288
- src: T.any(String, Pathname, StringIO, T::Enumerable[String]),
289
- blk: T.proc.params(arg0: String).void
290
- ).returns(T.attached_class)
291
- end
292
- def self.new(src, &blk)
293
- end
294
- end
295
-
296
256
  class << self
297
257
  sig do
298
258
  params(blk: T.proc.params(y: Enumerator::Yielder).void).returns(