openai 0.77.1 → 0.79.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.
Files changed (178) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +98 -0
  3. data/README.md +341 -10
  4. data/SECURITY.md +6 -22
  5. data/azure.md +110 -0
  6. data/bedrock.md +132 -0
  7. data/examples/mtls_custom_http_client.rb +61 -0
  8. data/lib/openai/auth/workload_identity.rb +13 -2
  9. data/lib/openai/auth/workload_identity_auth.rb +2 -1
  10. data/lib/openai/client.rb +34 -14
  11. data/lib/openai/errors.rb +11 -1
  12. data/lib/openai/helpers/streaming/chat_completion_stream.rb +3 -3
  13. data/lib/openai/helpers/streaming/response_stream.rb +5 -6
  14. data/lib/openai/helpers/structured_output/base_model.rb +20 -19
  15. data/lib/openai/helpers/structured_output/json_schema_converter.rb +12 -5
  16. data/lib/openai/http_client.rb +189 -0
  17. data/lib/openai/internal/conversation_cursor_page.rb +5 -6
  18. data/lib/openai/internal/cursor_page.rb +3 -3
  19. data/lib/openai/internal/logging.rb +419 -0
  20. data/lib/openai/internal/next_cursor_page.rb +5 -6
  21. data/lib/openai/internal/page.rb +2 -2
  22. data/lib/openai/internal/provider.rb +7 -1
  23. data/lib/openai/internal/read_io_adapter.rb +161 -0
  24. data/lib/openai/internal/stream.rb +2 -2
  25. data/lib/openai/internal/transport/base_client.rb +216 -57
  26. data/lib/openai/internal/type/array_of.rb +6 -4
  27. data/lib/openai/internal/type/base_model.rb +39 -1
  28. data/lib/openai/internal/type/base_page.rb +19 -2
  29. data/lib/openai/internal/type/base_stream.rb +40 -8
  30. data/lib/openai/internal/type/hash_of.rb +3 -3
  31. data/lib/openai/internal/util.rb +59 -112
  32. data/lib/openai/models/admin/organization/audit_log_list_params.rb +3 -1
  33. data/lib/openai/models/admin/organization/audit_log_list_response.rb +33 -11
  34. data/lib/openai/models/admin/organization/groups/role_list_response.rb +5 -1
  35. data/lib/openai/models/admin/organization/groups/role_retrieve_response.rb +5 -1
  36. data/lib/openai/models/admin/organization/projects/groups/role_list_response.rb +6 -1
  37. data/lib/openai/models/admin/organization/projects/groups/role_retrieve_response.rb +6 -1
  38. data/lib/openai/models/admin/organization/projects/service_account_create_response.rb +3 -1
  39. data/lib/openai/models/admin/organization/projects/users/role_list_response.rb +6 -1
  40. data/lib/openai/models/admin/organization/projects/users/role_retrieve_response.rb +6 -1
  41. data/lib/openai/models/admin/organization/users/role_list_response.rb +5 -1
  42. data/lib/openai/models/admin/organization/users/role_retrieve_response.rb +5 -1
  43. data/lib/openai/models/all_models.rb +3 -0
  44. data/lib/openai/models/audio/transcription_create_params.rb +7 -1
  45. data/lib/openai/models/audio/translation_create_params.rb +7 -1
  46. data/lib/openai/models/beta/beta_response.rb +9 -0
  47. data/lib/openai/models/beta/beta_response_stream_event.rb +1 -1
  48. data/lib/openai/models/beta/beta_responses_client_event.rb +28 -3
  49. data/lib/openai/models/beta/beta_responses_server_event.rb +1037 -61
  50. data/lib/openai/models/beta/response_compact_params.rb +9 -0
  51. data/lib/openai/models/beta/response_create_params.rb +9 -0
  52. data/lib/openai/models/containers/file_create_params.rb +4 -0
  53. data/lib/openai/models/content_provenance_check_create_params.rb +4 -0
  54. data/lib/openai/models/file_create_params.rb +4 -0
  55. data/lib/openai/models/image_create_variation_params.rb +4 -0
  56. data/lib/openai/models/image_edit_params.rb +8 -0
  57. data/lib/openai/models/responses/response_compact_params.rb +9 -0
  58. data/lib/openai/models/responses/response_stream_event.rb +1 -1
  59. data/lib/openai/models/responses/responses_client_event.rb +13 -1
  60. data/lib/openai/models/responses/responses_server_event.rb +1027 -54
  61. data/lib/openai/models/responses_model.rb +3 -0
  62. data/lib/openai/models/skill_create_params.rb +4 -0
  63. data/lib/openai/models/skills/version_create_params.rb +4 -0
  64. data/lib/openai/models/uploads/part_create_params.rb +4 -0
  65. data/lib/openai/models/vector_store_search_response.rb +3 -1
  66. data/lib/openai/models/video_create_character_params.rb +4 -0
  67. data/lib/openai/models/video_create_params.rb +4 -0
  68. data/lib/openai/models/video_edit_params.rb +4 -0
  69. data/lib/openai/models/video_extend_params.rb +4 -0
  70. data/lib/openai/models.rb +9 -1
  71. data/lib/openai/net_http_client.rb +295 -0
  72. data/lib/openai/providers/azure.rb +216 -0
  73. data/lib/openai/providers/bedrock.rb +3 -1
  74. data/lib/openai/resources/admin/organization/projects/groups/roles.rb +1 -1
  75. data/lib/openai/resources/admin/organization/projects/groups.rb +1 -1
  76. data/lib/openai/resources/admin/organization/projects/users/roles.rb +1 -1
  77. data/lib/openai/resources/admin/organization/projects.rb +4 -2
  78. data/lib/openai/resources/audio/transcriptions.rb +8 -0
  79. data/lib/openai/resources/audio/translations.rb +4 -0
  80. data/lib/openai/resources/beta/responses.rb +1 -1
  81. data/lib/openai/resources/beta/threads/runs/steps.rb +2 -2
  82. data/lib/openai/resources/containers/files.rb +4 -0
  83. data/lib/openai/resources/content_provenance_checks.rb +4 -0
  84. data/lib/openai/resources/conversations/items.rb +1 -1
  85. data/lib/openai/resources/evals/runs/output_items.rb +1 -1
  86. data/lib/openai/resources/files.rb +4 -0
  87. data/lib/openai/resources/images.rb +12 -0
  88. data/lib/openai/resources/responses.rb +3 -2
  89. data/lib/openai/resources/skills/versions.rb +4 -0
  90. data/lib/openai/resources/skills.rb +4 -0
  91. data/lib/openai/resources/uploads/parts.rb +4 -0
  92. data/lib/openai/resources/vector_stores/file_batches.rb +1 -1
  93. data/lib/openai/resources/videos.rb +38 -0
  94. data/lib/openai/resources/webhooks.rb +18 -6
  95. data/lib/openai/version.rb +1 -1
  96. data/lib/openai.rb +113 -108
  97. data/rbi/openai/auth.rbi +4 -4
  98. data/rbi/openai/client.rbi +15 -3
  99. data/rbi/openai/errors.rbi +7 -0
  100. data/rbi/openai/helpers/streaming/events.rbi +12 -0
  101. data/rbi/openai/http_client.rbi +123 -0
  102. data/rbi/openai/internal/logging.rbi +192 -0
  103. data/rbi/openai/internal/provider.rbi +3 -0
  104. data/rbi/openai/internal/read_io_adapter.rbi +52 -0
  105. data/rbi/openai/internal/transport/base_client.rbi +80 -10
  106. data/rbi/openai/internal/type/base_model.rbi +24 -1
  107. data/rbi/openai/internal/type/base_page.rbi +13 -2
  108. data/rbi/openai/internal/type/base_stream.rbi +16 -5
  109. data/rbi/openai/internal/util.rbi +26 -42
  110. data/rbi/openai/models/admin/organization/audit_log_list_params.rbi +5 -0
  111. data/rbi/openai/models/admin/organization/audit_log_list_response.rbi +5 -0
  112. data/rbi/openai/models/all_models.rbi +15 -0
  113. data/rbi/openai/models/audio/transcription_create_params.rbi +14 -2
  114. data/rbi/openai/models/audio/translation_create_params.rbi +14 -2
  115. data/rbi/openai/models/beta/beta_response.rbi +15 -0
  116. data/rbi/openai/models/beta/beta_response_stream_event.rbi +1 -1
  117. data/rbi/openai/models/beta/beta_responses_client_event.rbi +37 -1
  118. data/rbi/openai/models/beta/beta_responses_server_event.rbi +3791 -54
  119. data/rbi/openai/models/beta/response_compact_params.rbi +15 -0
  120. data/rbi/openai/models/beta/response_create_params.rbi +15 -0
  121. data/rbi/openai/models/containers/file_create_params.rbi +8 -0
  122. data/rbi/openai/models/content_provenance_check_create_params.rbi +8 -0
  123. data/rbi/openai/models/file_create_params.rbi +8 -0
  124. data/rbi/openai/models/image_create_variation_params.rbi +8 -0
  125. data/rbi/openai/models/image_edit_params.rbi +16 -0
  126. data/rbi/openai/models/responses/response_compact_params.rbi +15 -0
  127. data/rbi/openai/models/responses/response_stream_event.rbi +1 -1
  128. data/rbi/openai/models/responses/responses_client_event.rbi +19 -0
  129. data/rbi/openai/models/responses/responses_server_event.rbi +3282 -53
  130. data/rbi/openai/models/responses_model.rbi +15 -0
  131. data/rbi/openai/models/skill_create_params.rbi +8 -0
  132. data/rbi/openai/models/skills/version_create_params.rbi +8 -0
  133. data/rbi/openai/models/uploads/part_create_params.rbi +8 -0
  134. data/rbi/openai/models/video_create_character_params.rbi +8 -0
  135. data/rbi/openai/models/video_create_params.rbi +8 -0
  136. data/rbi/openai/models/video_edit_params.rbi +8 -0
  137. data/rbi/openai/models/video_extend_params.rbi +8 -0
  138. data/rbi/openai/net_http_client.rbi +64 -0
  139. data/rbi/openai/providers.rbi +9 -0
  140. data/rbi/openai/resources/audio/transcriptions.rbi +14 -2
  141. data/rbi/openai/resources/audio/translations.rbi +7 -1
  142. data/rbi/openai/resources/containers/files.rbi +4 -0
  143. data/rbi/openai/resources/content_provenance_checks.rbi +4 -0
  144. data/rbi/openai/resources/files.rbi +4 -0
  145. data/rbi/openai/resources/images.rbi +20 -0
  146. data/rbi/openai/resources/skills/versions.rbi +4 -0
  147. data/rbi/openai/resources/skills.rbi +4 -0
  148. data/rbi/openai/resources/uploads/parts.rbi +4 -0
  149. data/rbi/openai/resources/videos.rbi +16 -0
  150. data/sig/openai/client.rbs +7 -2
  151. data/sig/openai/errors.rbs +5 -0
  152. data/sig/openai/http_client.rbs +76 -0
  153. data/sig/openai/internal/logging.rbs +88 -0
  154. data/sig/openai/internal/provider.rbs +3 -1
  155. data/sig/openai/internal/read_io_adapter.rbs +23 -0
  156. data/sig/openai/internal/transport/base_client.rbs +41 -6
  157. data/sig/openai/internal/type/base_model.rbs +13 -0
  158. data/sig/openai/internal/type/base_page.rbs +8 -1
  159. data/sig/openai/internal/type/base_stream.rbs +9 -3
  160. data/sig/openai/internal/util.rbs +12 -16
  161. data/sig/openai/models/admin/organization/audit_log_list_params.rbs +2 -0
  162. data/sig/openai/models/admin/organization/audit_log_list_response.rbs +2 -0
  163. data/sig/openai/models/all_models.rbs +6 -0
  164. data/sig/openai/models/beta/beta_response.rbs +6 -0
  165. data/sig/openai/models/beta/beta_responses_client_event.rbs +13 -0
  166. data/sig/openai/models/beta/beta_responses_server_event.rbs +1946 -54
  167. data/sig/openai/models/beta/response_compact_params.rbs +6 -0
  168. data/sig/openai/models/beta/response_create_params.rbs +6 -0
  169. data/sig/openai/models/responses/response_compact_params.rbs +6 -0
  170. data/sig/openai/models/responses/responses_client_event.rbs +7 -0
  171. data/sig/openai/models/responses/responses_server_event.rbs +1786 -53
  172. data/sig/openai/models/responses_model.rbs +6 -0
  173. data/sig/openai/net_http_client.rbs +34 -0
  174. data/sig/openai/providers.rbs +6 -0
  175. metadata +33 -4
  176. data/lib/openai/internal/transport/pooled_net_requester.rb +0 -210
  177. data/rbi/openai/internal/transport/pooled_net_requester.rbi +0 -82
  178. data/sig/openai/internal/transport/pooled_net_requester.rbs +0 -48
@@ -54,9 +54,9 @@ module OpenAI
54
54
  #
55
55
  # @param client [OpenAI::Internal::Transport::BaseClient]
56
56
  # @param req [Hash{Symbol=>Object}]
57
- # @param headers [Hash{String=>String}]
57
+ # @param response_metadata [OpenAI::ResponseMetadata]
58
58
  # @param page_data [Array<Object>]
59
- def initialize(client:, req:, headers:, page_data:)
59
+ def initialize(client:, req:, response_metadata:, page_data:)
60
60
  super
61
61
 
62
62
  case page_data
@@ -7,7 +7,13 @@ module OpenAI
7
7
  # Provider factories return opaque OpenAI::Provider instances whose public
8
8
  # representation does not expose their definition.
9
9
  module Provider
10
- Runtime = Struct.new(:name, :base_url, :prepare_request, keyword_init: true)
10
+ Runtime = Struct.new(
11
+ :name,
12
+ :base_url,
13
+ :prepare_request,
14
+ :authentication_headers,
15
+ keyword_init: true
16
+ )
11
17
 
12
18
  class << self
13
19
  # @api private
@@ -0,0 +1,161 @@
1
+ # frozen_string_literal: true
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
+ class InterruptibleEnumerator
11
+ # Cancellation must bypass application rescues so a suspended source cannot
12
+ # swallow it and keep the request teardown blocked.
13
+ class Interrupt < Exception # rubocop:disable Lint/InheritException
14
+ end
15
+ private_constant :Interrupt
16
+
17
+ def initialize(source)
18
+ @started = false
19
+ @fiber = Fiber.new do
20
+ source.each { Fiber.yield(_1) }
21
+ rescue Interrupt
22
+ nil
23
+ end
24
+ end
25
+
26
+ def next
27
+ raise StopIteration unless @fiber&.alive?
28
+
29
+ @started = true
30
+ value = @fiber.resume
31
+ raise StopIteration unless @fiber.alive?
32
+
33
+ value
34
+ end
35
+
36
+ def to_a
37
+ values = []
38
+ loop { values << self.next }
39
+ values
40
+ end
41
+
42
+ def close
43
+ @fiber&.raise(Interrupt) if @started && @fiber&.alive?
44
+ rescue Interrupt
45
+ nil
46
+ ensure
47
+ @fiber = nil
48
+ end
49
+ end
50
+ private_constant :InterruptibleEnumerator
51
+
52
+ # @api private
53
+ #
54
+ # @return [Boolean, nil]
55
+ def close? = @closing
56
+
57
+ # @api private
58
+ def close
59
+ case @stream
60
+ in Enumerator
61
+ OpenAI::Internal::Util.close_fused!(@stream)
62
+ in InterruptibleEnumerator | IO if close?
63
+ @stream.close
64
+ else
65
+ end
66
+ end
67
+
68
+ # @api private
69
+ #
70
+ # @param max_len [Integer, nil]
71
+ #
72
+ # @return [String, nil]
73
+ private def read_enum(max_len)
74
+ case max_len
75
+ in nil
76
+ # `loop` rescues StopIteration, but this method handles it below.
77
+ # rubocop:disable Style/InfiniteLoop
78
+ @buf << @stream.next.b while true
79
+ # rubocop:enable Style/InfiniteLoop
80
+ in Integer
81
+ @buf << @stream.next.b while @buf.bytesize < max_len
82
+ read_buffer(max_len)
83
+ end
84
+ rescue StopIteration
85
+ return @buf.slice!(0..) if max_len.nil?
86
+
87
+ @stream = nil
88
+ return nil if @buf.bytesize.zero?
89
+
90
+ read_buffer(max_len)
91
+ end
92
+
93
+ # @api private
94
+ #
95
+ # @param max_len [Integer]
96
+ #
97
+ # @return [String]
98
+ private def read_buffer(max_len)
99
+ read = @buf.byteslice(0, max_len)
100
+ @buf = @buf.byteslice(max_len..) || String.new.b
101
+ read
102
+ end
103
+
104
+ # @api private
105
+ #
106
+ # @param max_len [Integer, nil]
107
+ # @param out_string [String, nil]
108
+ #
109
+ # @return [String, nil]
110
+ def read(max_len = nil, out_string = nil)
111
+ if max_len.is_a?(Integer) && max_len.negative?
112
+ raise ArgumentError, "negative length #{max_len} given"
113
+ end
114
+
115
+ read =
116
+ case @stream
117
+ in nil
118
+ max_len.nil? || max_len.zero? ? +"" : nil
119
+ in IO | StringIO
120
+ return @stream.read(max_len, out_string).tap(&@blk)
121
+ in Enumerator | InterruptibleEnumerator
122
+ read_enum(max_len)
123
+ end
124
+
125
+ case out_string
126
+ in String
127
+ out_string.replace(read || +"")
128
+ read.nil? ? nil : out_string
129
+ in nil
130
+ read
131
+ end
132
+ .tap(&@blk)
133
+ end
134
+
135
+ # @api private
136
+ #
137
+ # @param src [String, Pathname, StringIO, Enumerable<String>]
138
+ # @param blk [Proc]
139
+ #
140
+ # @yieldparam [String]
141
+ def initialize(src, &blk)
142
+ @stream =
143
+ case src
144
+ in String
145
+ StringIO.new(src)
146
+ in Pathname
147
+ @closing = true
148
+ src.open(binmode: true)
149
+ in Enumerator
150
+ @closing = true
151
+ InterruptibleEnumerator.new(src)
152
+ else
153
+ src
154
+ end
155
+ @buf = String.new.b
156
+ @blk = blk
157
+ end
158
+ end
159
+ end
160
+ end
161
+ end
@@ -40,8 +40,8 @@ module OpenAI
40
40
  end
41
41
  err = OpenAI::Errors::APIStatusError.for(
42
42
  url: @url,
43
- status: @status,
44
- headers: @headers,
43
+ status: status,
44
+ headers: headers,
45
45
  body: data,
46
46
  request: nil,
47
47
  response: @response,
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../logging"
4
+
3
5
  module OpenAI
4
6
  module Internal
5
7
  module Transport
@@ -79,6 +81,28 @@ module OpenAI
79
81
  end
80
82
  end
81
83
 
84
+ # @api private
85
+ #
86
+ # @param body [Object]
87
+ #
88
+ # @return [Boolean]
89
+ def request_body_replayable?(body)
90
+ case body
91
+ in nil | String | StringIO | Pathname
92
+ true
93
+ in OpenAI::FilePart
94
+ request_body_replayable?(body.content)
95
+ in Hash
96
+ body.each_value.all? { request_body_replayable?(_1) }
97
+ in Array
98
+ body.all? { request_body_replayable?(_1) }
99
+ in IO | Enumerable
100
+ false
101
+ else
102
+ !body.respond_to?(:read)
103
+ end
104
+ end
105
+
82
106
  # @api private
83
107
  #
84
108
  # @param request [Hash{Symbol=>Object}] .
@@ -143,8 +167,19 @@ module OpenAI
143
167
 
144
168
  # from undici
145
169
  if OpenAI::Internal::Util.uri_origin(url) != OpenAI::Internal::Util.uri_origin(location)
146
- drop = %w[authorization cookie host proxy-authorization]
147
- request = {**request, headers: request.fetch(:headers).except(*drop)}
170
+ headers = request.fetch(:headers).reject do |name, _|
171
+ name == "host" || OpenAI::Internal::Logging.credential_header?(name)
172
+ end
173
+ request = {**request, headers: headers}
174
+ end
175
+
176
+ unless request_body_replayable?(request[:body])
177
+ message = "Cannot follow a body-preserving redirect with a non-replayable request body."
178
+ raise OpenAI::Errors::APIConnectionError.new(
179
+ url: location,
180
+ response: response_headers,
181
+ message: message
182
+ )
148
183
  end
149
184
 
150
185
  request
@@ -168,7 +203,7 @@ module OpenAI
168
203
  # @return [URI::Generic]
169
204
  attr_reader :base_url
170
205
 
171
- # @return [Float]
206
+ # @return [Float, nil]
172
207
  attr_reader :timeout
173
208
 
174
209
  # @return [Integer]
@@ -186,19 +221,32 @@ module OpenAI
186
221
  # @return [String, nil]
187
222
  attr_reader :idempotency_header
188
223
 
224
+ # @return [#debug, #info, #warn, #error, nil]
225
+ attr_reader :logger
226
+
227
+ # @return [Symbol]
228
+ attr_reader :log_level
229
+
230
+ # @return [Proc, nil]
231
+ attr_reader :on_retry
232
+
189
233
  # @api private
190
- # @return [OpenAI::Internal::Transport::PooledNetRequester]
234
+ # @return [#execute]
191
235
  attr_reader :requester
192
236
 
193
237
  # @api private
194
238
  #
195
239
  # @param base_url [String]
196
- # @param timeout [Float]
240
+ # @param timeout [Float, nil]
197
241
  # @param max_retries [Integer]
198
242
  # @param initial_retry_delay [Float]
199
243
  # @param max_retry_delay [Float]
200
244
  # @param headers [Hash{String=>String, Integer, Array<String, Integer, nil>, nil}]
201
245
  # @param idempotency_header [String, nil]
246
+ # @param http_client [#execute, nil]
247
+ # @param logger [#debug, #info, #warn, #error, nil]
248
+ # @param log_level [Symbol, String, nil]
249
+ # @param on_retry [Proc, nil]
202
250
  def initialize(
203
251
  base_url:,
204
252
  timeout: 0.0,
@@ -206,9 +254,28 @@ module OpenAI
206
254
  initial_retry_delay: 0.0,
207
255
  max_retry_delay: 0.0,
208
256
  headers: {},
209
- idempotency_header: nil
257
+ idempotency_header: nil,
258
+ http_client: nil,
259
+ logger: nil,
260
+ log_level: nil,
261
+ on_retry: nil
210
262
  )
211
- @requester = OpenAI::Internal::Transport::PooledNetRequester.new
263
+ unless http_client.nil? || http_client.respond_to?(:execute)
264
+ raise ArgumentError, "`http_client` must respond to `execute`"
265
+ end
266
+ unless on_retry.nil? || on_retry.respond_to?(:call)
267
+ raise ArgumentError, "`on_retry` must respond to `call`"
268
+ end
269
+
270
+ if log_level.nil?
271
+ log_level = ENV.fetch("OPENAI_LOG", logger.nil? ? :off : :info)
272
+ end
273
+ @log_level = OpenAI::Internal::Logging.normalize_level(log_level)
274
+ OpenAI::Internal::Logging.validate_logger!(logger)
275
+ @logger = logger
276
+ @logger ||= OpenAI::Internal::Logging.default_logger unless @log_level == :off
277
+ @on_retry = on_retry
278
+ @requester = http_client || OpenAI::NetHTTPClient.new
212
279
  @headers = OpenAI::Internal::Util.normalized_headers(
213
280
  self.class::PLATFORM_HEADERS,
214
281
  {
@@ -318,8 +385,9 @@ module OpenAI
318
385
  headers["x-stainless-retry-count"] = "0"
319
386
  end
320
387
 
321
- timeout = opts.fetch(:timeout, @timeout).to_f.clamp(0..)
322
- unless headers.key?("x-stainless-timeout") || timeout.zero?
388
+ timeout = opts.fetch(:timeout, @timeout)
389
+ timeout = timeout.to_f.clamp(0..) unless timeout.nil?
390
+ unless headers.key?("x-stainless-timeout") || timeout.nil? || timeout.zero?
323
391
  headers["x-stainless-timeout"] = timeout.to_s
324
392
  end
325
393
 
@@ -342,17 +410,23 @@ module OpenAI
342
410
  @base_url_components,
343
411
  {**req, path: path, query: query}
344
412
  )
345
- headers, encoded = OpenAI::Internal::Util.encode_content(headers, body)
413
+ max_retries = opts.fetch(:max_retries, @max_retries)
414
+ max_retries = 0 unless self.class.request_body_replayable?(body)
346
415
  {
347
416
  method: method,
348
417
  url: url,
349
418
  headers: headers,
350
- body: encoded,
351
- max_retries: opts.fetch(:max_retries, @max_retries),
419
+ body: body,
420
+ max_retries: max_retries,
352
421
  timeout: timeout
353
422
  }
354
423
  end
355
424
 
425
+ # @api private
426
+ private def request_replayable?(request)
427
+ self.class.request_body_replayable?(request[:body])
428
+ end
429
+
356
430
  # @api private
357
431
  #
358
432
  # @param headers [Hash{String=>String}]
@@ -360,23 +434,22 @@ module OpenAI
360
434
  #
361
435
  # @return [Float]
362
436
  private def retry_delay(headers, retry_count:)
363
- # Non-standard extension
364
- span = Float(headers["retry-after-ms"], exception: false)&.then { _1 / 1000 }
365
- return span if span
366
-
367
437
  retry_header = headers["retry-after"]
368
- return span if (span = Float(retry_header, exception: false))
369
-
370
- span = retry_header&.then do
371
- Time.httpdate(_1) - Time.now
372
- rescue ArgumentError
373
- nil
374
- end
375
- return span if span
438
+ delays = [
439
+ Float(headers["retry-after-ms"], exception: false)&.then { _1 / 1000 },
440
+ Float(retry_header, exception: false),
441
+ retry_header&.then do
442
+ Time.httpdate(_1) - Time.now
443
+ rescue ArgumentError
444
+ nil
445
+ end
446
+ ]
447
+ server_delay = delays.find { _1&.finite? && !_1.negative? }
448
+ return [server_delay, @max_retry_delay].min if server_delay
376
449
 
377
- scale = retry_count**2
450
+ delay = (@initial_retry_delay * (2**retry_count)).clamp(0, @max_retry_delay)
378
451
  jitter = 1 - (0.25 * rand)
379
- (@initial_retry_delay * scale * jitter).clamp(0, @max_retry_delay)
452
+ delay * jitter
380
453
  end
381
454
 
382
455
  # @api private
@@ -411,7 +484,7 @@ module OpenAI
411
484
  #
412
485
  # @option request [Integer] :max_retries
413
486
  #
414
- # @option request [Float] :timeout
487
+ # @option request [Float, nil] :timeout
415
488
  #
416
489
  # @param redirect_count [Integer]
417
490
  #
@@ -419,32 +492,57 @@ module OpenAI
419
492
  #
420
493
  # @param send_retry_header [Boolean]
421
494
  #
495
+ # @yieldreturn [OpenAI::Internal::Logging::Context]
496
+ #
422
497
  # @raise [OpenAI::Errors::APIError]
423
- # @return [Array(Integer, Net::HTTPResponse, Enumerable<String>)]
424
- def send_request(request, redirect_count:, retry_count:, send_retry_header:)
498
+ # @return [OpenAI::HTTPClient::Response]
499
+ def send_request(request, redirect_count:, retry_count:, send_retry_header:, &context_provider)
500
+ # Generated clients override this hook. A block keeps observability
501
+ # state out of their stable keyword signature and is forwarded by
502
+ # their existing `super` calls.
503
+ log_context = context_provider.call
425
504
  if send_retry_header
426
505
  request = request.merge(
427
506
  headers: request.fetch(:headers).merge("x-stainless-retry-count" => retry_count.to_s)
428
507
  )
429
508
  end
430
509
 
510
+ encoded_headers, encoded_body = OpenAI::Internal::Util.encode_content(
511
+ request.fetch(:headers),
512
+ request[:body]
513
+ )
514
+ attempt_request = request.merge(headers: encoded_headers, body: encoded_body)
431
515
  prepared_request = prepare_request(
432
- request,
516
+ attempt_request,
433
517
  redirect_count: redirect_count,
434
518
  retry_count: retry_count
435
519
  )
436
520
  url, max_retries, timeout = prepared_request.fetch_values(:url, :max_retries, :timeout)
437
- input = {
438
- **prepared_request.except(:timeout, :follow_redirects, :provider_auth),
439
- deadline: OpenAI::Internal::Util.monotonic_secs + timeout
440
- }
521
+ input = OpenAI::HTTPClient::Request.new(
522
+ method: prepared_request.fetch(:method),
523
+ url: url,
524
+ headers: prepared_request.fetch(:headers),
525
+ body: prepared_request[:body],
526
+ timeout: timeout
527
+ )
528
+ log_context.request_started(input, redirect_count: redirect_count)
441
529
 
442
530
  begin
443
- status, response, stream = @requester.execute(input)
531
+ http_response = @requester.execute(input)
532
+ unless http_response.is_a?(OpenAI::HTTPClient::Response)
533
+ raise TypeError, "`http_client#execute` must return an OpenAI::HTTPClient::Response"
534
+ end
535
+
536
+ status = http_response.status
537
+ headers = http_response.headers
538
+ http_response = log_context.response_received(http_response)
539
+ stream = http_response.body
444
540
  rescue OpenAI::Errors::APIConnectionError => e
445
541
  status = e
542
+ stream = nil
543
+ headers = {}
544
+ log_context.attempt_failed(e)
446
545
  end
447
- headers = OpenAI::Internal::Util.normalized_headers(response&.each_header&.to_h)
448
546
 
449
547
  terminal_status =
450
548
  case status
@@ -460,24 +558,25 @@ module OpenAI
460
558
  url: url,
461
559
  status: status,
462
560
  headers: headers,
463
- response: response,
561
+ response: http_response,
464
562
  stream: stream
465
563
  )
466
564
  end
467
565
 
468
566
  case status
469
567
  in ..299
470
- [status, response, stream]
568
+ http_response
471
569
  in 300..399 if redirect_count >= self.class::MAX_REDIRECTS
472
570
  self.class.reap_connection!(status, stream: stream)
473
571
 
474
572
  message = "Failed to complete the request within #{self.class::MAX_REDIRECTS} redirects."
475
- raise OpenAI::Errors::APIConnectionError.new(url: url, response: response, message: message)
573
+ raise OpenAI::Errors::APIConnectionError.new(url: url, response: http_response, message: message)
476
574
  in 300..399
477
575
  self.class.reap_connection!(status, stream: stream)
478
576
 
577
+ redirect_source = request.merge(url: prepared_request.fetch(:url))
479
578
  redirected_request = self.class.follow_redirect(
480
- prepared_request,
579
+ redirect_source,
481
580
  status: status,
482
581
  response_headers: headers
483
582
  )
@@ -485,21 +584,30 @@ module OpenAI
485
584
  redirected_request,
486
585
  redirect_count: redirect_count + 1,
487
586
  retry_count: retry_count,
488
- send_retry_header: send_retry_header
587
+ send_retry_header: send_retry_header,
588
+ &context_provider
489
589
  )
490
590
  in OpenAI::Errors::APIConnectionError if retry_count >= max_retries
491
591
  raise status
492
592
  in (400..) | OpenAI::Errors::APIConnectionError
493
593
  self.class.reap_connection!(status, stream: stream)
494
594
 
495
- delay = retry_delay(response || {}, retry_count: retry_count)
595
+ delay = retry_delay(headers, retry_count: retry_count)
596
+ log_context.retry_scheduled(
597
+ status,
598
+ delay: delay,
599
+ response: http_response&.metadata,
600
+ retry_count: retry_count,
601
+ max_retries: max_retries
602
+ )
496
603
  sleep(delay)
497
604
 
498
605
  send_request(
499
606
  request,
500
607
  redirect_count: redirect_count,
501
608
  retry_count: retry_count + 1,
502
- send_retry_header: send_retry_header
609
+ send_retry_header: send_retry_header,
610
+ &context_provider
503
611
  )
504
612
  end
505
613
  end
@@ -546,41 +654,93 @@ module OpenAI
546
654
  # @raise [OpenAI::Errors::APIError]
547
655
  # @return [Object]
548
656
  def request(req)
657
+ url, response, log_context = perform_request(req)
658
+ finish_request(log_context, response) do
659
+ parse_response(req, url: url, response: response)
660
+ end
661
+ end
662
+
663
+ # @api private
664
+ #
665
+ # @param req [Hash{Symbol=>Object}]
666
+ # @return [Array(URI::Generic, OpenAI::HTTPClient::Response, OpenAI::Internal::Logging::Context)]
667
+ private def perform_request(req)
549
668
  self.class.validate!(req)
550
- model = req.fetch(:model) { OpenAI::Internal::Type::Unknown }
551
669
  opts = req[:options].to_h
552
- unwrap = req[:unwrap]
553
670
  OpenAI::RequestOptions.validate!(opts)
554
671
  request = build_request(req.except(:options), opts)
555
672
  url = request.fetch(:url)
673
+ log_context = OpenAI::Internal::Logging::Context.new(
674
+ logger: @logger,
675
+ log_level: @log_level,
676
+ on_retry: @on_retry,
677
+ method: request.fetch(:method),
678
+ url: url
679
+ )
556
680
 
557
681
  # Don't send the current retry count in the headers if the caller modified the header defaults.
558
682
  send_retry_header = request.fetch(:headers)["x-stainless-retry-count"] == "0"
559
- status, response, stream = send_request(
683
+ response = send_request(
560
684
  request,
561
685
  redirect_count: 0,
562
686
  retry_count: 0,
563
687
  send_retry_header: send_retry_header
564
- )
688
+ ) { log_context }
689
+ [url, response, log_context]
690
+ rescue StandardError => e
691
+ log_context&.request_failed(e)
692
+ raise
693
+ end
694
+
695
+ # @api private
696
+ #
697
+ # @param log_context [OpenAI::Internal::Logging::Context]
698
+ # @param response [OpenAI::HTTPClient::Response]
699
+ # @return [Object]
700
+ private def finish_request(log_context, response)
701
+ result = yield
702
+ if result.is_a?(OpenAI::Internal::Type::BaseStream)
703
+ return log_context.observe_stream(result, response: response)
704
+ end
705
+
706
+ log_context.completed(response)
707
+ result
708
+ rescue StandardError => e
709
+ log_context.request_failed(e)
710
+ raise
711
+ end
712
+
713
+ # @api private
714
+ #
715
+ # @param req [Hash{Symbol=>Object}]
716
+ # @param url [URI::Generic]
717
+ # @param response [OpenAI::HTTPClient::Response]
718
+ # @return [Object]
719
+ private def parse_response(req, url:, response:)
720
+ model = req.fetch(:model) { OpenAI::Internal::Type::Unknown }
721
+ unwrap = req[:unwrap]
722
+ response_metadata = response.metadata
565
723
 
566
- headers = OpenAI::Internal::Util.normalized_headers(response.each_header.to_h)
567
- decoded = OpenAI::Internal::Util.decode_content(headers, stream: stream)
724
+ decoded = OpenAI::Internal::Util.decode_content(response.headers, stream: response.body)
568
725
  case req
569
726
  in {stream: Class => st}
570
727
  st.new(
571
728
  model: model,
572
729
  url: url,
573
- status: status,
574
- headers: headers,
730
+ response_metadata: response_metadata,
575
731
  response: response,
576
732
  unwrap: unwrap,
577
733
  stream: decoded
578
734
  )
579
735
  in {page: Class => page}
580
- page.new(client: self, req: req, headers: headers, page_data: decoded)
736
+ page.new(client: self, req: req, response_metadata: response_metadata, page_data: decoded)
581
737
  else
582
738
  unwrapped = OpenAI::Internal::Util.dig(decoded, unwrap)
583
- OpenAI::Internal::Type::Converter.coerce(model, unwrapped)
739
+ OpenAI::Internal::Type::Converter.coerce(model, unwrapped).tap do |result|
740
+ if result.is_a?(OpenAI::Internal::Type::BaseModel)
741
+ result._set_last_response(response_metadata)
742
+ end
743
+ end
584
744
  end
585
745
  end
586
746
 
@@ -588,9 +748,8 @@ module OpenAI
588
748
  #
589
749
  # @return [String]
590
750
  def inspect
591
- # rubocop:disable Layout/LineLength
592
- "#<#{self.class.name}:0x#{object_id.to_s(16)} base_url=#{@base_url} max_retries=#{@max_retries} timeout=#{@timeout}>"
593
- # rubocop:enable Layout/LineLength
751
+ "#<#{self.class.name}:0x#{object_id.to_s(16)} base_url=#{@base_url} " \
752
+ "max_retries=#{@max_retries} timeout=#{@timeout}>"
594
753
  end
595
754
 
596
755
  define_sorbet_constant!(:RequestComponents) do
@@ -634,7 +793,7 @@ module OpenAI
634
793
  headers: T::Hash[String, String],
635
794
  body: T.anything,
636
795
  max_retries: Integer,
637
- timeout: Float
796
+ timeout: T.nilable(Float)
638
797
  }
639
798
  end
640
799
  end
@@ -46,9 +46,9 @@ module OpenAI
46
46
  #
47
47
  # @return [Boolean]
48
48
  def ==(other)
49
- # rubocop:disable Layout/LineLength
50
- other.is_a?(OpenAI::Internal::Type::ArrayOf) && other.nilable? == nilable? && other.item_type == item_type
51
- # rubocop:enable Layout/LineLength
49
+ other.is_a?(OpenAI::Internal::Type::ArrayOf) &&
50
+ other.nilable? == nilable? &&
51
+ other.item_type == item_type
52
52
  end
53
53
 
54
54
  # @api public
@@ -120,7 +120,9 @@ module OpenAI
120
120
  #
121
121
  # @return [Object]
122
122
  def to_sorbet_type
123
- T::Array[OpenAI::Internal::Util::SorbetRuntimeSupport.to_sorbet_type(item_type)]
123
+ type = OpenAI::Internal::Util::SorbetRuntimeSupport.to_sorbet_type(item_type)
124
+ type = T.nilable(type) if nilable?
125
+ T::Array[type]
124
126
  end
125
127
 
126
128
  # @api private