openai 0.78.0 → 0.80.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 (239) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +99 -0
  3. data/README.md +241 -8
  4. data/SECURITY.md +6 -22
  5. data/azure.md +110 -0
  6. data/bedrock.md +197 -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 +29 -6
  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 +99 -7
  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 +42 -18
  24. data/lib/openai/internal/stream.rb +2 -2
  25. data/lib/openai/internal/transport/base_client.rb +137 -37
  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 +39 -7
  30. data/lib/openai/internal/type/hash_of.rb +3 -3
  31. data/lib/openai/internal/util.rb +52 -27
  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 +5 -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_mcp_tool_call_error.rb +84 -0
  47. data/lib/openai/models/beta/beta_response.rb +27 -0
  48. data/lib/openai/models/beta/beta_response_input_item.rb +3 -3
  49. data/lib/openai/models/beta/beta_response_item.rb +3 -3
  50. data/lib/openai/models/beta/beta_response_output_item.rb +3 -3
  51. data/lib/openai/models/beta/beta_response_output_item_added_event.rb +5 -2
  52. data/lib/openai/models/beta/beta_response_output_text_annotation_added_event.rb +201 -4
  53. data/lib/openai/models/beta/beta_response_reasoning_item.rb +5 -0
  54. data/lib/openai/models/beta/beta_response_stream_event.rb +1 -1
  55. data/lib/openai/models/beta/beta_responses_client_event.rb +46 -3
  56. data/lib/openai/models/beta/beta_responses_server_event.rb +1143 -63
  57. data/lib/openai/models/beta/beta_web_search_tool.rb +11 -1
  58. data/lib/openai/models/beta/response_compact_params.rb +18 -0
  59. data/lib/openai/models/beta/response_create_params.rb +27 -0
  60. data/lib/openai/models/chat_model.rb +1 -0
  61. data/lib/openai/models/completion_usage.rb +26 -2
  62. data/lib/openai/models/containers/file_create_params.rb +4 -0
  63. data/lib/openai/models/content_provenance_check_create_params.rb +4 -0
  64. data/lib/openai/models/conversations/conversation_item.rb +3 -3
  65. data/lib/openai/models/file_create_params.rb +4 -0
  66. data/lib/openai/models/image_create_variation_params.rb +4 -0
  67. data/lib/openai/models/image_edit_params.rb +8 -0
  68. data/lib/openai/models/model.rb +9 -1
  69. data/lib/openai/models/responses/mcp_tool_call_error.rb +82 -0
  70. data/lib/openai/models/responses/response.rb +9 -0
  71. data/lib/openai/models/responses/response_compact_params.rb +18 -0
  72. data/lib/openai/models/responses/response_create_params.rb +9 -0
  73. data/lib/openai/models/responses/response_input_item.rb +3 -3
  74. data/lib/openai/models/responses/response_item.rb +3 -3
  75. data/lib/openai/models/responses/response_output_item.rb +3 -3
  76. data/lib/openai/models/responses/response_output_item_added_event.rb +5 -2
  77. data/lib/openai/models/responses/response_output_text_annotation_added_event.rb +201 -4
  78. data/lib/openai/models/responses/response_reasoning_item.rb +5 -0
  79. data/lib/openai/models/responses/response_stream_event.rb +1 -1
  80. data/lib/openai/models/responses/responses_client_event.rb +22 -1
  81. data/lib/openai/models/responses/responses_server_event.rb +1107 -56
  82. data/lib/openai/models/responses/web_search_tool.rb +11 -1
  83. data/lib/openai/models/responses_model.rb +5 -0
  84. data/lib/openai/models/skill_create_params.rb +4 -0
  85. data/lib/openai/models/skills/version_create_params.rb +4 -0
  86. data/lib/openai/models/uploads/part_create_params.rb +4 -0
  87. data/lib/openai/models/vector_store_search_response.rb +3 -1
  88. data/lib/openai/models/video_create_character_params.rb +4 -0
  89. data/lib/openai/models/video_create_params.rb +4 -0
  90. data/lib/openai/models/video_edit_params.rb +4 -0
  91. data/lib/openai/models/video_extend_params.rb +4 -0
  92. data/lib/openai/models.rb +9 -1
  93. data/lib/openai/net_http_client.rb +20 -6
  94. data/lib/openai/providers/azure.rb +216 -0
  95. data/lib/openai/providers/bedrock.rb +130 -17
  96. data/lib/openai/resources/admin/organization/projects/groups/roles.rb +1 -1
  97. data/lib/openai/resources/admin/organization/projects/groups.rb +1 -1
  98. data/lib/openai/resources/admin/organization/projects/users/roles.rb +1 -1
  99. data/lib/openai/resources/admin/organization/projects.rb +4 -2
  100. data/lib/openai/resources/audio/transcriptions.rb +8 -0
  101. data/lib/openai/resources/audio/translations.rb +4 -0
  102. data/lib/openai/resources/beta/responses.rb +1 -1
  103. data/lib/openai/resources/beta/threads/runs/steps.rb +2 -2
  104. data/lib/openai/resources/containers/files.rb +4 -0
  105. data/lib/openai/resources/content_provenance_checks.rb +4 -0
  106. data/lib/openai/resources/conversations/items.rb +1 -1
  107. data/lib/openai/resources/evals/runs/output_items.rb +1 -1
  108. data/lib/openai/resources/files.rb +5 -1
  109. data/lib/openai/resources/images.rb +12 -0
  110. data/lib/openai/resources/responses.rb +3 -2
  111. data/lib/openai/resources/skills/versions.rb +4 -0
  112. data/lib/openai/resources/skills.rb +4 -0
  113. data/lib/openai/resources/uploads/parts.rb +4 -0
  114. data/lib/openai/resources/vector_stores/file_batches.rb +1 -1
  115. data/lib/openai/resources/videos.rb +38 -0
  116. data/lib/openai/resources/webhooks.rb +18 -6
  117. data/lib/openai/version.rb +1 -1
  118. data/lib/openai.rb +111 -105
  119. data/rbi/openai/auth.rbi +4 -4
  120. data/rbi/openai/client.rbi +13 -3
  121. data/rbi/openai/errors.rbi +7 -0
  122. data/rbi/openai/helpers/streaming/events.rbi +12 -0
  123. data/rbi/openai/http_client.rbi +62 -2
  124. data/rbi/openai/internal/logging.rbi +192 -0
  125. data/rbi/openai/internal/provider.rbi +3 -0
  126. data/rbi/openai/internal/read_io_adapter.rbi +6 -1
  127. data/rbi/openai/internal/transport/base_client.rbi +61 -7
  128. data/rbi/openai/internal/type/base_model.rbi +24 -1
  129. data/rbi/openai/internal/type/base_page.rbi +13 -2
  130. data/rbi/openai/internal/type/base_stream.rbi +15 -4
  131. data/rbi/openai/internal/util.rbi +26 -2
  132. data/rbi/openai/models/admin/organization/audit_log_list_params.rbi +5 -0
  133. data/rbi/openai/models/admin/organization/audit_log_list_response.rbi +5 -0
  134. data/rbi/openai/models/all_models.rbi +25 -0
  135. data/rbi/openai/models/audio/transcription_create_params.rbi +14 -2
  136. data/rbi/openai/models/audio/translation_create_params.rbi +14 -2
  137. data/rbi/openai/models/beta/beta_mcp_tool_call_error.rbi +122 -0
  138. data/rbi/openai/models/beta/beta_response.rbi +47 -0
  139. data/rbi/openai/models/beta/beta_response_input_item.rbi +27 -3
  140. data/rbi/openai/models/beta/beta_response_item.rbi +27 -3
  141. data/rbi/openai/models/beta/beta_response_output_item.rbi +27 -3
  142. data/rbi/openai/models/beta/beta_response_output_item_added_event.rbi +8 -2
  143. data/rbi/openai/models/beta/beta_response_output_text_annotation_added_event.rbi +297 -5
  144. data/rbi/openai/models/beta/beta_response_reasoning_item.rbi +10 -0
  145. data/rbi/openai/models/beta/beta_response_stream_event.rbi +1 -1
  146. data/rbi/openai/models/beta/beta_responses_client_event.rbi +69 -1
  147. data/rbi/openai/models/beta/beta_responses_server_event.rbi +3945 -54
  148. data/rbi/openai/models/beta/beta_web_search_tool.rbi +15 -0
  149. data/rbi/openai/models/beta/response_compact_params.rbi +30 -0
  150. data/rbi/openai/models/beta/response_create_params.rbi +47 -0
  151. data/rbi/openai/models/chat_model.rbi +2 -0
  152. data/rbi/openai/models/completion_usage.rbi +39 -6
  153. data/rbi/openai/models/containers/file_create_params.rbi +8 -0
  154. data/rbi/openai/models/content_provenance_check_create_params.rbi +8 -0
  155. data/rbi/openai/models/conversations/conversation_item.rbi +12 -3
  156. data/rbi/openai/models/file_create_params.rbi +8 -0
  157. data/rbi/openai/models/image_create_variation_params.rbi +8 -0
  158. data/rbi/openai/models/image_edit_params.rbi +16 -0
  159. data/rbi/openai/models/model.rbi +14 -1
  160. data/rbi/openai/models/responses/mcp_tool_call_error.rbi +120 -0
  161. data/rbi/openai/models/responses/response.rbi +17 -0
  162. data/rbi/openai/models/responses/response_compact_params.rbi +30 -0
  163. data/rbi/openai/models/responses/response_create_params.rbi +17 -0
  164. data/rbi/openai/models/responses/response_input_item.rbi +27 -3
  165. data/rbi/openai/models/responses/response_item.rbi +27 -3
  166. data/rbi/openai/models/responses/response_output_item.rbi +27 -3
  167. data/rbi/openai/models/responses/response_output_item_added_event.rbi +8 -2
  168. data/rbi/openai/models/responses/response_output_text_annotation_added_event.rbi +297 -5
  169. data/rbi/openai/models/responses/response_reasoning_item.rbi +10 -0
  170. data/rbi/openai/models/responses/response_stream_event.rbi +1 -1
  171. data/rbi/openai/models/responses/responses_client_event.rbi +36 -0
  172. data/rbi/openai/models/responses/responses_server_event.rbi +3385 -53
  173. data/rbi/openai/models/responses/web_search_tool.rbi +15 -0
  174. data/rbi/openai/models/responses_model.rbi +25 -0
  175. data/rbi/openai/models/skill_create_params.rbi +8 -0
  176. data/rbi/openai/models/skills/version_create_params.rbi +8 -0
  177. data/rbi/openai/models/uploads/part_create_params.rbi +8 -0
  178. data/rbi/openai/models/video_create_character_params.rbi +8 -0
  179. data/rbi/openai/models/video_create_params.rbi +8 -0
  180. data/rbi/openai/models/video_edit_params.rbi +8 -0
  181. data/rbi/openai/models/video_extend_params.rbi +8 -0
  182. data/rbi/openai/net_http_client.rbi +2 -2
  183. data/rbi/openai/providers.rbi +11 -0
  184. data/rbi/openai/resources/audio/transcriptions.rbi +14 -2
  185. data/rbi/openai/resources/audio/translations.rbi +7 -1
  186. data/rbi/openai/resources/beta/responses.rbi +8 -0
  187. data/rbi/openai/resources/containers/files.rbi +4 -0
  188. data/rbi/openai/resources/content_provenance_checks.rbi +4 -0
  189. data/rbi/openai/resources/files.rbi +5 -1
  190. data/rbi/openai/resources/images.rbi +20 -0
  191. data/rbi/openai/resources/responses.rbi +8 -0
  192. data/rbi/openai/resources/skills/versions.rbi +4 -0
  193. data/rbi/openai/resources/skills.rbi +4 -0
  194. data/rbi/openai/resources/uploads/parts.rbi +4 -0
  195. data/rbi/openai/resources/videos.rbi +16 -0
  196. data/sig/openai/client.rbs +6 -2
  197. data/sig/openai/errors.rbs +5 -0
  198. data/sig/openai/http_client.rbs +37 -2
  199. data/sig/openai/internal/logging.rbs +88 -0
  200. data/sig/openai/internal/provider.rbs +3 -1
  201. data/sig/openai/internal/read_io_adapter.rbs +3 -1
  202. data/sig/openai/internal/transport/base_client.rbs +33 -5
  203. data/sig/openai/internal/type/base_model.rbs +13 -0
  204. data/sig/openai/internal/type/base_page.rbs +8 -1
  205. data/sig/openai/internal/type/base_stream.rbs +8 -2
  206. data/sig/openai/internal/util.rbs +12 -0
  207. data/sig/openai/models/admin/organization/audit_log_list_params.rbs +2 -0
  208. data/sig/openai/models/admin/organization/audit_log_list_response.rbs +2 -0
  209. data/sig/openai/models/all_models.rbs +10 -0
  210. data/sig/openai/models/beta/beta_mcp_tool_call_error.rbs +75 -0
  211. data/sig/openai/models/beta/beta_response.rbs +14 -1
  212. data/sig/openai/models/beta/beta_response_input_item.rbs +4 -4
  213. data/sig/openai/models/beta/beta_response_item.rbs +4 -4
  214. data/sig/openai/models/beta/beta_response_output_item.rbs +4 -4
  215. data/sig/openai/models/beta/beta_response_output_text_annotation_added_event.rbs +149 -4
  216. data/sig/openai/models/beta/beta_responses_client_event.rbs +21 -1
  217. data/sig/openai/models/beta/beta_responses_server_event.rbs +2005 -53
  218. data/sig/openai/models/beta/beta_web_search_tool.rbs +7 -0
  219. data/sig/openai/models/beta/response_compact_params.rbs +12 -0
  220. data/sig/openai/models/beta/response_create_params.rbs +14 -1
  221. data/sig/openai/models/chat_model.rbs +2 -0
  222. data/sig/openai/models/completion_usage.rbs +27 -6
  223. data/sig/openai/models/conversations/conversation_item.rbs +4 -4
  224. data/sig/openai/models/model.rbs +12 -2
  225. data/sig/openai/models/responses/mcp_tool_call_error.rbs +73 -0
  226. data/sig/openai/models/responses/response.rbs +2 -1
  227. data/sig/openai/models/responses/response_compact_params.rbs +12 -0
  228. data/sig/openai/models/responses/response_create_params.rbs +2 -1
  229. data/sig/openai/models/responses/response_input_item.rbs +4 -4
  230. data/sig/openai/models/responses/response_item.rbs +4 -4
  231. data/sig/openai/models/responses/response_output_item.rbs +4 -4
  232. data/sig/openai/models/responses/response_output_text_annotation_added_event.rbs +149 -4
  233. data/sig/openai/models/responses/responses_client_event.rbs +9 -1
  234. data/sig/openai/models/responses/responses_server_event.rbs +1834 -53
  235. data/sig/openai/models/responses/web_search_tool.rbs +7 -0
  236. data/sig/openai/models/responses_model.rbs +10 -0
  237. data/sig/openai/net_http_client.rbs +2 -2
  238. data/sig/openai/providers.rbs +7 -0
  239. metadata +30 -1
@@ -36,7 +36,6 @@ module OpenAI
36
36
  def to_a
37
37
  values = []
38
38
  loop { values << self.next }
39
- rescue StopIteration
40
39
  values
41
40
  end
42
41
 
@@ -70,18 +69,36 @@ module OpenAI
70
69
  #
71
70
  # @param max_len [Integer, nil]
72
71
  #
73
- # @return [String]
72
+ # @return [String, nil]
74
73
  private def read_enum(max_len)
75
74
  case max_len
76
75
  in nil
77
- @stream.to_a.join
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
78
80
  in Integer
79
- @buf << @stream.next while @buf.length < max_len
80
- @buf.slice!(..max_len)
81
+ @buf << @stream.next.b while @buf.bytesize < max_len
82
+ read_buffer(max_len)
81
83
  end
82
84
  rescue StopIteration
85
+ return @buf.slice!(0..) if max_len.nil?
86
+
83
87
  @stream = nil
84
- @buf.slice!(0..)
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
85
102
  end
86
103
 
87
104
  # @api private
@@ -91,19 +108,26 @@ module OpenAI
91
108
  #
92
109
  # @return [String, nil]
93
110
  def read(max_len = nil, out_string = nil)
94
- case @stream
95
- in nil
96
- nil
97
- in IO | StringIO
98
- @stream.read(max_len, out_string)
99
- in Enumerator | InterruptibleEnumerator
100
- read = read_enum(max_len)
101
- case out_string
102
- in String
103
- out_string.replace(read)
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
104
117
  in nil
105
- read
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)
106
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
107
131
  end
108
132
  .tap(&@blk)
109
133
  end
@@ -128,7 +152,7 @@ module OpenAI
128
152
  else
129
153
  src
130
154
  end
131
- @buf = String.new
155
+ @buf = String.new.b
132
156
  @blk = blk
133
157
  end
134
158
  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
@@ -165,8 +167,10 @@ module OpenAI
165
167
 
166
168
  # from undici
167
169
  if OpenAI::Internal::Util.uri_origin(url) != OpenAI::Internal::Util.uri_origin(location)
168
- drop = %w[authorization cookie host proxy-authorization]
169
- 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}
170
174
  end
171
175
 
172
176
  unless request_body_replayable?(request[:body])
@@ -199,7 +203,7 @@ module OpenAI
199
203
  # @return [URI::Generic]
200
204
  attr_reader :base_url
201
205
 
202
- # @return [Float]
206
+ # @return [Float, nil]
203
207
  attr_reader :timeout
204
208
 
205
209
  # @return [Integer]
@@ -217,6 +221,15 @@ module OpenAI
217
221
  # @return [String, nil]
218
222
  attr_reader :idempotency_header
219
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
+
220
233
  # @api private
221
234
  # @return [#execute]
222
235
  attr_reader :requester
@@ -224,13 +237,16 @@ module OpenAI
224
237
  # @api private
225
238
  #
226
239
  # @param base_url [String]
227
- # @param timeout [Float]
240
+ # @param timeout [Float, nil]
228
241
  # @param max_retries [Integer]
229
242
  # @param initial_retry_delay [Float]
230
243
  # @param max_retry_delay [Float]
231
244
  # @param headers [Hash{String=>String, Integer, Array<String, Integer, nil>, nil}]
232
245
  # @param idempotency_header [String, nil]
233
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]
234
250
  def initialize(
235
251
  base_url:,
236
252
  timeout: 0.0,
@@ -239,12 +255,26 @@ module OpenAI
239
255
  max_retry_delay: 0.0,
240
256
  headers: {},
241
257
  idempotency_header: nil,
242
- http_client: nil
258
+ http_client: nil,
259
+ logger: nil,
260
+ log_level: nil,
261
+ on_retry: nil
243
262
  )
244
263
  unless http_client.nil? || http_client.respond_to?(:execute)
245
264
  raise ArgumentError, "`http_client` must respond to `execute`"
246
265
  end
266
+ unless on_retry.nil? || on_retry.respond_to?(:call)
267
+ raise ArgumentError, "`on_retry` must respond to `call`"
268
+ end
247
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
248
278
  @requester = http_client || OpenAI::NetHTTPClient.new
249
279
  @headers = OpenAI::Internal::Util.normalized_headers(
250
280
  self.class::PLATFORM_HEADERS,
@@ -355,8 +385,9 @@ module OpenAI
355
385
  headers["x-stainless-retry-count"] = "0"
356
386
  end
357
387
 
358
- timeout = opts.fetch(:timeout, @timeout).to_f.clamp(0..)
359
- 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?
360
391
  headers["x-stainless-timeout"] = timeout.to_s
361
392
  end
362
393
 
@@ -403,23 +434,22 @@ module OpenAI
403
434
  #
404
435
  # @return [Float]
405
436
  private def retry_delay(headers, retry_count:)
406
- # Non-standard extension
407
- span = Float(headers["retry-after-ms"], exception: false)&.then { _1 / 1000 }
408
- return span if span
409
-
410
437
  retry_header = headers["retry-after"]
411
- return span if (span = Float(retry_header, exception: false))
412
-
413
- span = retry_header&.then do
414
- Time.httpdate(_1) - Time.now
415
- rescue ArgumentError
416
- nil
417
- end
418
- 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
419
449
 
420
- scale = retry_count**2
450
+ delay = (@initial_retry_delay * (2**retry_count)).clamp(0, @max_retry_delay)
421
451
  jitter = 1 - (0.25 * rand)
422
- (@initial_retry_delay * scale * jitter).clamp(0, @max_retry_delay)
452
+ delay * jitter
423
453
  end
424
454
 
425
455
  # @api private
@@ -454,7 +484,7 @@ module OpenAI
454
484
  #
455
485
  # @option request [Integer] :max_retries
456
486
  #
457
- # @option request [Float] :timeout
487
+ # @option request [Float, nil] :timeout
458
488
  #
459
489
  # @param redirect_count [Integer]
460
490
  #
@@ -462,9 +492,15 @@ module OpenAI
462
492
  #
463
493
  # @param send_retry_header [Boolean]
464
494
  #
495
+ # @yieldreturn [OpenAI::Internal::Logging::Context]
496
+ #
465
497
  # @raise [OpenAI::Errors::APIError]
466
498
  # @return [OpenAI::HTTPClient::Response]
467
- def send_request(request, redirect_count:, retry_count:, send_retry_header:)
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
468
504
  if send_retry_header
469
505
  request = request.merge(
470
506
  headers: request.fetch(:headers).merge("x-stainless-retry-count" => retry_count.to_s)
@@ -489,6 +525,7 @@ module OpenAI
489
525
  body: prepared_request[:body],
490
526
  timeout: timeout
491
527
  )
528
+ log_context.request_started(input, redirect_count: redirect_count)
492
529
 
493
530
  begin
494
531
  http_response = @requester.execute(input)
@@ -497,12 +534,14 @@ module OpenAI
497
534
  end
498
535
 
499
536
  status = http_response.status
500
- stream = http_response.body
501
537
  headers = http_response.headers
538
+ http_response = log_context.response_received(http_response)
539
+ stream = http_response.body
502
540
  rescue OpenAI::Errors::APIConnectionError => e
503
541
  status = e
504
542
  stream = nil
505
543
  headers = {}
544
+ log_context.attempt_failed(e)
506
545
  end
507
546
 
508
547
  terminal_status =
@@ -545,7 +584,8 @@ module OpenAI
545
584
  redirected_request,
546
585
  redirect_count: redirect_count + 1,
547
586
  retry_count: retry_count,
548
- send_retry_header: send_retry_header
587
+ send_retry_header: send_retry_header,
588
+ &context_provider
549
589
  )
550
590
  in OpenAI::Errors::APIConnectionError if retry_count >= max_retries
551
591
  raise status
@@ -553,13 +593,21 @@ module OpenAI
553
593
  self.class.reap_connection!(status, stream: stream)
554
594
 
555
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
+ )
556
603
  sleep(delay)
557
604
 
558
605
  send_request(
559
606
  request,
560
607
  redirect_count: redirect_count,
561
608
  retry_count: retry_count + 1,
562
- send_retry_header: send_retry_header
609
+ send_retry_header: send_retry_header,
610
+ &context_provider
563
611
  )
564
612
  end
565
613
  end
@@ -606,13 +654,29 @@ module OpenAI
606
654
  # @raise [OpenAI::Errors::APIError]
607
655
  # @return [Object]
608
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)
609
668
  self.class.validate!(req)
610
- model = req.fetch(:model) { OpenAI::Internal::Type::Unknown }
611
669
  opts = req[:options].to_h
612
- unwrap = req[:unwrap]
613
670
  OpenAI::RequestOptions.validate!(opts)
614
671
  request = build_request(req.except(:options), opts)
615
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
+ )
616
680
 
617
681
  # Don't send the current retry count in the headers if the caller modified the header defaults.
618
682
  send_retry_header = request.fetch(:headers)["x-stainless-retry-count"] == "0"
@@ -621,7 +685,41 @@ module OpenAI
621
685
  redirect_count: 0,
622
686
  retry_count: 0,
623
687
  send_retry_header: send_retry_header
624
- )
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
625
723
 
626
724
  decoded = OpenAI::Internal::Util.decode_content(response.headers, stream: response.body)
627
725
  case req
@@ -629,17 +727,20 @@ module OpenAI
629
727
  st.new(
630
728
  model: model,
631
729
  url: url,
632
- status: response.status,
633
- headers: response.headers,
730
+ response_metadata: response_metadata,
634
731
  response: response,
635
732
  unwrap: unwrap,
636
733
  stream: decoded
637
734
  )
638
735
  in {page: Class => page}
639
- page.new(client: self, req: req, headers: response.headers, page_data: decoded)
736
+ page.new(client: self, req: req, response_metadata: response_metadata, page_data: decoded)
640
737
  else
641
738
  unwrapped = OpenAI::Internal::Util.dig(decoded, unwrap)
642
- 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
643
744
  end
644
745
  end
645
746
 
@@ -647,9 +748,8 @@ module OpenAI
647
748
  #
648
749
  # @return [String]
649
750
  def inspect
650
- # rubocop:disable Layout/LineLength
651
- "#<#{self.class.name}:0x#{object_id.to_s(16)} base_url=#{@base_url} max_retries=#{@max_retries} timeout=#{@timeout}>"
652
- # 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}>"
653
753
  end
654
754
 
655
755
  define_sorbet_constant!(:RequestComponents) do
@@ -693,7 +793,7 @@ module OpenAI
693
793
  headers: T::Hash[String, String],
694
794
  body: T.anything,
695
795
  max_retries: Integer,
696
- timeout: Float
796
+ timeout: T.nilable(Float)
697
797
  }
698
798
  end
699
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
@@ -226,6 +226,32 @@ module OpenAI
226
226
  # @return [Integer]
227
227
  def hash = [self.class, @data].hash
228
228
 
229
+ # The ID of the API request, returned via the `x-request-id` response
230
+ # header. This is only populated on top-level response objects returned
231
+ # by the client.
232
+ #
233
+ # @api public
234
+ #
235
+ # @return [String, nil]
236
+ def _request_id = @last_response&.request_id
237
+
238
+ # Metadata from the HTTP response that produced this model. This is
239
+ # only populated on top-level response objects returned by the client.
240
+ #
241
+ # @api public
242
+ #
243
+ # @return [OpenAI::ResponseMetadata, nil]
244
+ attr_reader :last_response
245
+
246
+ # @api private
247
+ #
248
+ # @param response [OpenAI::ResponseMetadata]
249
+ # @return [self]
250
+ def _set_last_response(response)
251
+ @last_response = response
252
+ self
253
+ end
254
+
229
255
  class << self
230
256
  # @api private
231
257
  #
@@ -247,7 +273,7 @@ module OpenAI
247
273
  def coerce(value, state:)
248
274
  exactness = state.fetch(:exactness)
249
275
 
250
- if value.is_a?(self.class)
276
+ if value.is_a?(self)
251
277
  exactness[:yes] += 1
252
278
  return value
253
279
  end
@@ -470,12 +496,24 @@ module OpenAI
470
496
  # @return [String]
471
497
  def to_yaml(*a) = OpenAI::Internal::Type::Converter.dump(self.class, self).to_yaml(*a)
472
498
 
499
+ # Keep transport metadata outside Psych's object serialization path.
500
+ #
501
+ # @api private
502
+ #
503
+ # @param coder [Psych::Coder]
504
+ # @return [void]
505
+ def encode_with(coder)
506
+ coder["data"] = @data
507
+ coder["coerced"] = @coerced
508
+ end
509
+
473
510
  # Create a new instance of a model.
474
511
  #
475
512
  # @param data [Hash{Symbol=>Object}, self]
476
513
  def initialize(data = {})
477
514
  @data = {}
478
515
  @coerced = {}
516
+ @last_response = nil
479
517
  OpenAI::Internal::Util.coerce_hash!(data).each do
480
518
  if self.class.known_fields.key?(_1)
481
519
  public_send(:"#{_1}=", _2)
@@ -11,6 +11,22 @@ module OpenAI
11
11
  module BasePage
12
12
  # rubocop:disable Lint/UnusedMethodArgument
13
13
 
14
+ # The ID of the API request, returned via the `x-request-id` response
15
+ # header. This is only populated on top-level response objects returned
16
+ # by the client.
17
+ #
18
+ # @api public
19
+ #
20
+ # @return [String, nil]
21
+ def _request_id = last_response.request_id
22
+
23
+ # Metadata from the HTTP response that produced this page.
24
+ #
25
+ # @api public
26
+ #
27
+ # @return [OpenAI::ResponseMetadata]
28
+ attr_reader :last_response
29
+
14
30
  # @api public
15
31
  #
16
32
  # @return [Boolean]
@@ -39,12 +55,13 @@ module OpenAI
39
55
  #
40
56
  # @param client [OpenAI::Internal::Transport::BaseClient]
41
57
  # @param req [Hash{Symbol=>Object}]
42
- # @param headers [Hash{String=>String}]
58
+ # @param response_metadata [OpenAI::ResponseMetadata]
43
59
  # @param page_data [Object]
44
- def initialize(client:, req:, headers:, page_data:)
60
+ def initialize(client:, req:, response_metadata:, page_data:)
45
61
  @client = client
46
62
  @req = req
47
63
  @model = req.fetch(:model)
64
+ @last_response = response_metadata
48
65
  super()
49
66
  end
50
67
 
@@ -14,16 +14,50 @@ module OpenAI
14
14
  include Enumerable
15
15
 
16
16
  # @return [Integer]
17
- attr_reader :status
17
+ def status = last_response.status
18
18
 
19
19
  # @return [Hash{String=>String}]
20
- attr_reader :headers
20
+ def headers = last_response.headers
21
+
22
+ # Metadata from the HTTP response that opened this stream.
23
+ #
24
+ # @api public
25
+ #
26
+ # @return [OpenAI::ResponseMetadata]
27
+ attr_reader :last_response
21
28
 
22
29
  # @api public
23
30
  #
24
31
  # @return [void]
25
32
  def close = OpenAI::Internal::Util.close_fused!(@iterator)
26
33
 
34
+ # Attach request lifecycle logging without changing the stream's identity.
35
+ #
36
+ # @api private
37
+ #
38
+ # @param context [OpenAI::Internal::Logging::Context]
39
+ # @param response [OpenAI::HTTPClient::Response]
40
+ # @return [self]
41
+ def observe(context:, response:)
42
+ source = @iterator
43
+ @iterator = OpenAI::Internal::Util.chain_fused(source) do |yielder|
44
+ loop do
45
+ event =
46
+ begin
47
+ source.next
48
+ rescue StopIteration
49
+ context.completed(response)
50
+ break
51
+ rescue StandardError => e
52
+ context.request_failed(e)
53
+ raise
54
+ end
55
+ yielder << event
56
+ end
57
+ end
58
+ self
59
+ end
60
+
27
61
  # @api private
28
62
  #
29
63
  # @return [Enumerable<generic<Elem>>]
@@ -53,16 +87,14 @@ module OpenAI
53
87
  #
54
88
  # @param model [Class, OpenAI::Internal::Type::Converter]
55
89
  # @param url [URI::Generic]
56
- # @param status [Integer]
57
- # @param headers [Hash{String=>String}]
90
+ # @param response_metadata [OpenAI::ResponseMetadata]
58
91
  # @param response [OpenAI::HTTPClient::Response]
59
92
  # @param unwrap [Symbol, Integer, Array<Symbol, Integer>, Proc]
60
93
  # @param stream [Enumerable<Object>]
61
- def initialize(model:, url:, status:, headers:, response:, unwrap:, stream:)
94
+ def initialize(model:, url:, response_metadata:, response:, unwrap:, stream:)
62
95
  @model = model
63
96
  @url = url
64
- @status = status
65
- @headers = headers
97
+ @last_response = response_metadata
66
98
  @response = response
67
99
  @unwrap = unwrap
68
100
  @stream = stream
@@ -61,9 +61,9 @@ module OpenAI
61
61
  #
62
62
  # @return [Boolean]
63
63
  def ==(other)
64
- # rubocop:disable Layout/LineLength
65
- other.is_a?(OpenAI::Internal::Type::HashOf) && other.nilable? == nilable? && other.item_type == item_type
66
- # rubocop:enable Layout/LineLength
64
+ other.is_a?(OpenAI::Internal::Type::HashOf) &&
65
+ other.nilable? == nilable? &&
66
+ other.item_type == item_type
67
67
  end
68
68
 
69
69
  # @api public