openai 0.78.0 → 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 (175) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +80 -0
  3. data/README.md +241 -8
  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 +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 +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 +20 -6
  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 +111 -107
  97. data/rbi/openai/auth.rbi +4 -4
  98. data/rbi/openai/client.rbi +13 -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 +62 -2
  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 +6 -1
  105. data/rbi/openai/internal/transport/base_client.rbi +61 -7
  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 +15 -4
  109. data/rbi/openai/internal/util.rbi +26 -2
  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 +2 -2
  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 +6 -2
  151. data/sig/openai/errors.rbs +5 -0
  152. data/sig/openai/http_client.rbs +37 -2
  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 +3 -1
  156. data/sig/openai/internal/transport/base_client.rbs +33 -5
  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 +8 -2
  160. data/sig/openai/internal/util.rbs +12 -0
  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 +2 -2
  174. data/sig/openai/providers.rbs +6 -0
  175. metadata +24 -1
@@ -32,7 +32,7 @@ module OpenAI
32
32
  def next_page
33
33
  unless next_page?
34
34
  message = "No more pages available. Please check #next_page? before calling ##{__method__}"
35
- raise RuntimeError.new(message)
35
+ raise message
36
36
  end
37
37
 
38
38
  req = OpenAI::Internal::Util.deep_merge(@req, {query: {after: data&.last&.id}})
@@ -60,9 +60,9 @@ module OpenAI
60
60
  #
61
61
  # @param client [OpenAI::Internal::Transport::BaseClient]
62
62
  # @param req [Hash{Symbol=>Object}]
63
- # @param headers [Hash{String=>String}]
63
+ # @param response_metadata [OpenAI::ResponseMetadata]
64
64
  # @param page_data [Hash{Symbol=>Object}]
65
- def initialize(client:, req:, headers:, page_data:)
65
+ def initialize(client:, req:, response_metadata:, page_data:)
66
66
  super
67
67
 
68
68
  case page_data
@@ -0,0 +1,419 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenAI
4
+ module Internal
5
+ # Request logging helpers shared by generated clients.
6
+ #
7
+ # @api private
8
+ module Logging
9
+ LOG_LEVELS = {off: 0, error: 1, warn: 2, info: 3, debug: 4}.freeze
10
+ REDACTED_HEADERS = %w[
11
+ api-key
12
+ authorization
13
+ cookie
14
+ proxy-authorization
15
+ set-cookie
16
+ x-amz-security-token
17
+ x-api-key
18
+ ].freeze
19
+ MAX_BODY_BYTES = 16 * 1024
20
+ MAX_ARRAY_ITEMS = 100
21
+ OPAQUE_STRING_BYTES = 1_024
22
+ SENSITIVE_BODY_KEY = /(?:api[-_]?key|authorization|credential|password|secret|signature|token)/i
23
+ SENSITIVE_QUERY_KEY = /
24
+ (?:
25
+ (?:\A|[-_\[])(?:key|sig)
26
+ | (?-i:K)ey
27
+ | api[-_]?key
28
+ | authorization
29
+ | credentials?
30
+ | password
31
+ | secret
32
+ | signature
33
+ | token
34
+ )
35
+ (?:\[|\]|\z)
36
+ /ix
37
+ URL_HEADER_KEY = /(?:\A|[-_])(?:location|url|uri)\z|\A(?:link|refresh)\z/i
38
+
39
+ class Context
40
+ def initialize(logger:, log_level:, on_retry:, method:, url:)
41
+ @logger = logger
42
+ @log_level = log_level
43
+ @on_retry = on_retry
44
+ @id = nil
45
+ @method = method.to_s.upcase
46
+ @url = url
47
+ @started_at = OpenAI::Internal::Util.monotonic_secs
48
+ @attempt_started_at = @started_at
49
+ @attempts = 0
50
+ end
51
+
52
+ def request_started(request, redirect_count:)
53
+ @attempts += 1
54
+ log(:debug) do
55
+ "[openai] request started log_id=#{id} attempt=#{@attempts} " \
56
+ "redirect=#{redirect_count} method=#{request.method.to_s.upcase} " \
57
+ "url=#{OpenAI::Internal::Logging.safe_url(request.url)} " \
58
+ "headers=#{OpenAI::Internal::Logging.format_headers(request.headers)} " \
59
+ "body=#{OpenAI::Internal::Logging.format_body(request.body, headers: request.headers)}"
60
+ end
61
+ @attempt_started_at = OpenAI::Internal::Util.monotonic_secs
62
+ end
63
+
64
+ def response_received(response)
65
+ log(:debug) do
66
+ "[openai] response received log_id=#{id} attempt=#{@attempts} " \
67
+ "status=#{response.status} request_id=#{safe_field(response.headers['x-request-id'])} " \
68
+ "duration_ms=#{attempt_duration_ms} " \
69
+ "headers=#{OpenAI::Internal::Logging.format_headers(response.headers)}"
70
+ end
71
+ return response unless enabled?(:debug)
72
+
73
+ observed = ObservedBody.new(
74
+ body: response.body,
75
+ headers: response.headers,
76
+ context: self,
77
+ attempt: @attempts
78
+ )
79
+ OpenAI::HTTPClient::Response.new(
80
+ status: response.status,
81
+ headers: response.headers,
82
+ body: observed
83
+ )
84
+ end
85
+
86
+ def attempt_failed(error)
87
+ log(:debug) do
88
+ "[openai] request attempt failed log_id=#{id} attempt=#{@attempts} " \
89
+ "error=#{error.class} duration_ms=#{attempt_duration_ms}"
90
+ end
91
+ end
92
+
93
+ def retry_scheduled(cause, delay:, response:, retry_count:, max_retries:)
94
+ reason = cause.is_a?(Integer) ? "status=#{cause}" : "error=#{cause.class}"
95
+ log(:warn) do
96
+ "[openai] request retry log_id=#{id} attempt=#{@attempts} " \
97
+ "#{reason} delay_seconds=#{delay}"
98
+ end
99
+
100
+ event = OpenAI::RetryEvent.new(
101
+ attempt: retry_count + 2,
102
+ max_attempts: max_retries + 1,
103
+ delay: delay,
104
+ response: response,
105
+ error: cause.is_a?(OpenAI::Errors::APIConnectionError) ? cause : nil
106
+ )
107
+ @on_retry&.call(event)
108
+ rescue StandardError
109
+ nil
110
+ end
111
+
112
+ def completed(response)
113
+ log(:info) do
114
+ "[openai] request complete log_id=#{id} method=#{@method} " \
115
+ "path=#{OpenAI::Internal::Logging.safe_path(@url)} " \
116
+ "status=#{response.status} request_id=#{safe_field(response.headers['x-request-id'])} " \
117
+ "attempts=#{@attempts} duration_ms=#{duration_ms}"
118
+ end
119
+ end
120
+
121
+ def observe_stream(stream, response:)
122
+ return stream unless enabled?(:error)
123
+
124
+ stream.observe(context: self, response: response)
125
+ end
126
+
127
+ def request_failed(error)
128
+ status = error.is_a?(OpenAI::Errors::APIError) ? error.status : nil
129
+ request_id = error.is_a?(OpenAI::Errors::APIError) ? error.request_id : nil
130
+ log(:error) do
131
+ "[openai] request failed log_id=#{id} method=#{@method} " \
132
+ "path=#{OpenAI::Internal::Logging.safe_path(@url)} " \
133
+ "status=#{status || 'none'} request_id=#{safe_field(request_id)} " \
134
+ "error=#{error.class} attempts=#{@attempts} duration_ms=#{duration_ms}"
135
+ end
136
+ end
137
+
138
+ def response_body(body, headers:, complete:, total_bytes:, attempt:)
139
+ log(:debug) do
140
+ formatted = OpenAI::Internal::Logging.format_observed_body(
141
+ body,
142
+ headers: headers,
143
+ complete: complete,
144
+ total_bytes: total_bytes
145
+ )
146
+ "[openai] response body log_id=#{id} " \
147
+ "attempt=#{attempt} duration_ms=#{duration_ms} body=#{formatted}"
148
+ end
149
+ end
150
+
151
+ private def duration_ms
152
+ ((OpenAI::Internal::Util.monotonic_secs - @started_at) * 1_000).round(2)
153
+ end
154
+
155
+ private def id = @id ||= "log_#{SecureRandom.hex(6)}"
156
+
157
+ private def attempt_duration_ms
158
+ ((OpenAI::Internal::Util.monotonic_secs - @attempt_started_at) * 1_000).round(2)
159
+ end
160
+
161
+ private def enabled?(event_level)
162
+ !@logger.nil? && OpenAI::Internal::Logging.enabled?(@log_level, event_level)
163
+ end
164
+
165
+ private def safe_field(value) = OpenAI::Internal::Logging.safe_field(value)
166
+
167
+ private def log(event_level)
168
+ return unless enabled?(event_level)
169
+
170
+ message = yield
171
+ case event_level
172
+ in :debug
173
+ @logger.debug(message)
174
+ in :info
175
+ @logger.info(message)
176
+ in :warn
177
+ @logger.warn(message)
178
+ in :error
179
+ @logger.error(message)
180
+ end
181
+ rescue StandardError
182
+ nil
183
+ end
184
+ end
185
+
186
+ class ObservedBody
187
+ include Enumerable
188
+
189
+ def initialize(body:, headers:, context:, attempt:)
190
+ @body = body
191
+ @headers = headers
192
+ @context = context
193
+ @attempt = attempt
194
+ @captured = String.new(encoding: Encoding::BINARY)
195
+ @capture_body = OpenAI::Internal::Logging.capture_response_body?(headers)
196
+ @bytes = 0
197
+ @complete = false
198
+ @closed = false
199
+ end
200
+
201
+ def each
202
+ return enum_for unless block_given?
203
+ return if @closed
204
+
205
+ begin
206
+ @body.each do |chunk|
207
+ capture(chunk)
208
+ yield(chunk)
209
+ end
210
+ @complete = true
211
+ ensure
212
+ close
213
+ end
214
+ end
215
+
216
+ def close
217
+ return if @closed
218
+
219
+ @closed = true
220
+ OpenAI::Internal::Util.close_fused!(@body)
221
+ @context.response_body(
222
+ @captured,
223
+ headers: @headers,
224
+ complete: @complete,
225
+ total_bytes: @bytes,
226
+ attempt: @attempt
227
+ )
228
+ end
229
+
230
+ private def capture(chunk)
231
+ @bytes += chunk.bytesize
232
+ return unless @capture_body
233
+
234
+ remaining = OpenAI::Internal::Logging::MAX_BODY_BYTES - @captured.bytesize
235
+ @captured << chunk.byteslice(0, remaining) if remaining.positive?
236
+ end
237
+ end
238
+
239
+ class << self
240
+ def normalize_level(value)
241
+ level = value.to_s.downcase.to_sym if value.is_a?(String) || value.is_a?(Symbol)
242
+ return level if LOG_LEVELS.key?(level)
243
+
244
+ raise ArgumentError, "`log_level` must be one of :off, :error, :warn, :info, or :debug"
245
+ end
246
+
247
+ def validate_logger!(logger)
248
+ return if logger.nil?
249
+
250
+ methods = [:debug, :info, :warn, :error]
251
+ return if methods.all? { logger.respond_to?(_1) }
252
+
253
+ raise ArgumentError, "`logger` must respond to `debug`, `info`, `warn`, and `error`"
254
+ end
255
+
256
+ def default_logger = ::Logger.new($stderr)
257
+
258
+ def enabled?(configured_level, event_level)
259
+ LOG_LEVELS.fetch(configured_level) >= LOG_LEVELS.fetch(event_level)
260
+ end
261
+
262
+ def safe_path(url)
263
+ uri = sanitized_uri(url)
264
+ path = uri.path.to_s.empty? ? "/" : uri.path
265
+ uri.query.nil? ? path : "#{path}?#{uri.query}"
266
+ rescue ArgumentError, URI::Error
267
+ "[URL OMITTED]"
268
+ end
269
+
270
+ def safe_url(url)
271
+ sanitized_uri(url).to_s
272
+ rescue ArgumentError, URI::Error
273
+ "[URL OMITTED]"
274
+ end
275
+
276
+ def safe_field(value)
277
+ return "none" if value.nil?
278
+
279
+ value.to_s.dump.delete_prefix('"').delete_suffix('"')
280
+ end
281
+
282
+ # @api private
283
+ def sensitive_header?(name)
284
+ normalized_name = name.to_s.downcase
285
+ REDACTED_HEADERS.include?(normalized_name) || SENSITIVE_QUERY_KEY.match?(normalized_name)
286
+ end
287
+
288
+ # @api private
289
+ def credential_header?(name)
290
+ normalized_name = name.to_s.downcase
291
+ idempotency = normalized_name.match(/(?:\A|[-_])idempotency[-_]key\z/)
292
+ return sensitive_header?(normalized_name) unless idempotency
293
+
294
+ prefix = normalized_name[...idempotency.begin(0)]
295
+ prefix.split(/[-_]/).any? { sensitive_header?(_1) }
296
+ end
297
+
298
+ def format_headers(headers)
299
+ redacted =
300
+ headers.sort.to_h do |name, value|
301
+ normalized_name = name.to_s.downcase
302
+ rendered =
303
+ if sensitive_header?(normalized_name)
304
+ "[REDACTED]"
305
+ elsif URL_HEADER_KEY.match?(normalized_name)
306
+ sanitized_header_url(value)
307
+ else
308
+ value
309
+ end
310
+ [name, rendered]
311
+ end
312
+ JSON.generate(redacted)
313
+ end
314
+
315
+ def format_body(body, headers:)
316
+ content_type = headers["content-type"].to_s
317
+ return "[NO BODY]" if body.nil?
318
+ return "[MULTIPART BODY OMITTED]" if content_type.match?(%r{\Amultipart/}i)
319
+ return "[STREAMING BODY OMITTED]" unless body.is_a?(String)
320
+ return "[BINARY BODY OMITTED] bytes=#{body.bytesize}" unless textual_content_type?(content_type)
321
+
322
+ format_text_body(body, content_type: content_type, total_bytes: body.bytesize)
323
+ end
324
+
325
+ def capture_response_body?(headers)
326
+ content_type = headers["content-type"].to_s
327
+ textual_content_type?(content_type) && !content_type.match?(%r{\Atext/event-stream}i)
328
+ end
329
+
330
+ def format_observed_body(body, headers:, complete:, total_bytes:)
331
+ content_type = headers["content-type"].to_s
332
+ return "[STREAM BODY OMITTED] bytes=#{total_bytes}" if content_type.match?(%r{\Atext/event-stream}i)
333
+ return "[BODY CLOSED EARLY] bytes=#{total_bytes}" unless complete
334
+ unless textual_content_type?(content_type)
335
+ return "[BINARY BODY OMITTED] bytes=#{total_bytes}"
336
+ end
337
+ if total_bytes > MAX_BODY_BYTES && json_content_type?(content_type)
338
+ return "[JSON BODY OMITTED] bytes=#{total_bytes} reason=too_large"
339
+ end
340
+
341
+ format_text_body(body, content_type: content_type, total_bytes: total_bytes)
342
+ end
343
+
344
+ private def sanitized_uri(url)
345
+ uri = url.dup
346
+ uri.user = nil
347
+ uri.password = nil
348
+ return uri if uri.query.nil?
349
+
350
+ uri.query = sanitized_query(uri.query)
351
+ uri
352
+ end
353
+
354
+ private def sanitized_query(query)
355
+ pairs = URI.decode_www_form(query).map do |name, value|
356
+ [name, SENSITIVE_QUERY_KEY.match?(name) ? "[REDACTED]" : value]
357
+ end
358
+ URI.encode_www_form(pairs)
359
+ rescue ArgumentError
360
+ nil
361
+ end
362
+
363
+ private def sanitized_header_url(value)
364
+ safe_url(URI(value.to_s))
365
+ rescue URI::InvalidURIError
366
+ "[URL OMITTED]"
367
+ end
368
+
369
+ private def textual_content_type?(content_type)
370
+ content_type.empty? ||
371
+ content_type.match?(%r{\Atext/}i) ||
372
+ content_type.match?(/(?:json|xml|x-www-form-urlencoded)/i)
373
+ end
374
+
375
+ private def json_content_type?(content_type) = content_type.match?(%r{(?:\A|[+/])json(?:;|\z)}i)
376
+
377
+ private def format_text_body(body, content_type:, total_bytes:)
378
+ if json_content_type?(content_type)
379
+ return "[JSON BODY OMITTED] bytes=#{total_bytes} reason=too_large" if total_bytes > MAX_BODY_BYTES
380
+
381
+ parsed = JSON.parse(body)
382
+ return summarize_json_body(parsed, total_bytes: total_bytes)
383
+ end
384
+
385
+ unless content_type.match?(%r{\Aapplication/x-www-form-urlencoded(?:;|\z)}i)
386
+ return "[TEXT BODY OMITTED] bytes=#{total_bytes}"
387
+ end
388
+ return "[FORM BODY OMITTED] bytes=#{total_bytes} reason=too_large" if total_bytes > MAX_BODY_BYTES
389
+
390
+ fields = URI.decode_www_form(body).length
391
+ "[FORM BODY] bytes=#{total_bytes} fields=#{fields}"
392
+ rescue JSON::ParserError
393
+ "[INVALID JSON BODY OMITTED] bytes=#{total_bytes}"
394
+ rescue ArgumentError
395
+ "[INVALID FORM BODY OMITTED] bytes=#{total_bytes}"
396
+ end
397
+
398
+ private def summarize_json_body(value, total_bytes:)
399
+ shape =
400
+ case value
401
+ in Hash
402
+ "object fields=#{value.length}"
403
+ in Array
404
+ "array items=#{value.length}"
405
+ in String
406
+ "string"
407
+ in Integer | Float
408
+ "number"
409
+ in true | false
410
+ "boolean"
411
+ in nil
412
+ "null"
413
+ end
414
+ "[JSON BODY] bytes=#{total_bytes} type=#{shape}"
415
+ end
416
+ end
417
+ end
418
+ end
419
+ end
@@ -35,7 +35,7 @@ module OpenAI
35
35
  def next_page
36
36
  unless next_page?
37
37
  message = "No more pages available. Please check #next_page? before calling ##{__method__}"
38
- raise RuntimeError.new(message)
38
+ raise message
39
39
  end
40
40
 
41
41
  req = OpenAI::Internal::Util.deep_merge(@req, {query: {after: next_}})
@@ -63,9 +63,9 @@ module OpenAI
63
63
  #
64
64
  # @param client [OpenAI::Internal::Transport::BaseClient]
65
65
  # @param req [Hash{Symbol=>Object}]
66
- # @param headers [Hash{String=>String}]
66
+ # @param response_metadata [OpenAI::ResponseMetadata]
67
67
  # @param page_data [Hash{Symbol=>Object}]
68
- def initialize(client:, req:, headers:, page_data:)
68
+ def initialize(client:, req:, response_metadata:, page_data:)
69
69
  super
70
70
 
71
71
  case page_data
@@ -81,11 +81,10 @@ module OpenAI
81
81
  #
82
82
  # @return [String]
83
83
  def inspect
84
- # rubocop:disable Layout/LineLength
85
84
  model = OpenAI::Internal::Type::Converter.inspect(@model, depth: 1)
86
85
 
87
- "#<#{self.class}[#{model}]:0x#{object_id.to_s(16)} has_more=#{has_more.inspect} next_=#{next_.inspect}>"
88
- # rubocop:enable Layout/LineLength
86
+ "#<#{self.class}[#{model}]:0x#{object_id.to_s(16)} " \
87
+ "has_more=#{has_more.inspect} next_=#{next_.inspect}>"
89
88
  end
90
89
  end
91
90
  end
@@ -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
@@ -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,