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
@@ -1,6 +1,95 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require "logger"
4
+
3
5
  module OpenAI
6
+ # Metadata from the HTTP response backing a top-level SDK value.
7
+ class ResponseMetadata
8
+ # The HTTP response status code.
9
+ #
10
+ # @return [Integer]
11
+ attr_reader :status
12
+
13
+ # Immutable response headers with lowercase String keys.
14
+ #
15
+ # @return [Hash{String=>String}]
16
+ attr_reader :headers
17
+
18
+ # The request ID returned in the `x-request-id` response header.
19
+ #
20
+ # @return [String, nil]
21
+ attr_reader :request_id
22
+
23
+ # @api private
24
+ #
25
+ # @param status [Integer]
26
+ # @param headers [Hash{String=>String}]
27
+ def initialize(status:, headers:)
28
+ @status = Integer(status)
29
+ @headers =
30
+ headers.to_h do |name, value|
31
+ [name.to_s.downcase.freeze, value.to_s.dup.freeze]
32
+ end.freeze
33
+ @request_id = @headers["x-request-id"]
34
+ freeze
35
+ end
36
+ end
37
+
38
+ # Details about an API request retry that is about to run.
39
+ class RetryEvent
40
+ # The one-based number of the attempt that will run after the delay.
41
+ #
42
+ # @return [Integer]
43
+ attr_reader :attempt
44
+
45
+ # The maximum number of attempts, including the initial request.
46
+ #
47
+ # @return [Integer]
48
+ attr_reader :max_attempts
49
+
50
+ # The number of seconds the SDK will wait before the next attempt.
51
+ #
52
+ # @return [Float]
53
+ attr_reader :delay
54
+
55
+ # Metadata from the retryable HTTP response, if one was received.
56
+ #
57
+ # @return [OpenAI::ResponseMetadata, nil]
58
+ attr_reader :response
59
+
60
+ # The connection error that caused the retry, if the request failed before
61
+ # an HTTP response was received.
62
+ #
63
+ # @return [OpenAI::Errors::APIConnectionError, nil]
64
+ attr_reader :error
65
+
66
+ # @api private
67
+ #
68
+ # @param attempt [Integer]
69
+ # @param max_attempts [Integer]
70
+ # @param delay [Float]
71
+ # @param response [OpenAI::ResponseMetadata, nil]
72
+ # @param error [OpenAI::Errors::APIConnectionError, nil]
73
+ def initialize(attempt:, max_attempts:, delay:, response:, error:)
74
+ @attempt = Integer(attempt)
75
+ @max_attempts = Integer(max_attempts)
76
+ @delay = Float(delay)
77
+ @response = response
78
+ @error = error
79
+ freeze
80
+ end
81
+
82
+ # The status of the retryable response, if one was received.
83
+ #
84
+ # @return [Integer, nil]
85
+ def status = @response&.status
86
+
87
+ # The request ID of the retryable response, if one was received.
88
+ #
89
+ # @return [String, nil]
90
+ def request_id = @response&.request_id
91
+ end
92
+
4
93
  # The transport boundary used by {OpenAI::Client}.
5
94
  #
6
95
  # Implement {#execute} to replace the SDK's HTTP transport. Subclassing this
@@ -21,14 +110,14 @@ module OpenAI
21
110
  # @return [Object, nil]
22
111
  attr_reader :body
23
112
 
24
- # @return [Float]
113
+ # @return [Float, nil]
25
114
  attr_reader :timeout
26
115
 
27
116
  # @param method [Symbol]
28
117
  # @param url [URI::Generic]
29
118
  # @param headers [Hash{String=>String}]
30
119
  # @param body [Object, nil]
31
- # @param timeout [Float]
120
+ # @param timeout [Float, nil]
32
121
  def initialize(method:, url:, headers:, body:, timeout:)
33
122
  @method = method
34
123
  @url = url
@@ -54,6 +143,11 @@ module OpenAI
54
143
  # @return [Enumerable<String>]
55
144
  attr_reader :body
56
145
 
146
+ # Immutable metadata safe to retain after the response body is consumed.
147
+ #
148
+ # @return [OpenAI::ResponseMetadata]
149
+ attr_reader :metadata
150
+
57
151
  # @param status [Integer]
58
152
  # @param headers [Hash{String=>String}]
59
153
  # @param body [String, Enumerable<String>]
@@ -62,11 +156,9 @@ module OpenAI
62
156
  raise ArgumentError, "`body` must be a String or respond to `each`"
63
157
  end
64
158
 
65
- @status = Integer(status)
66
- @headers =
67
- headers.to_h do |name, value|
68
- [name.to_s.downcase, value.to_s]
69
- end.freeze
159
+ @metadata = OpenAI::ResponseMetadata.new(status: status, headers: headers)
160
+ @status = @metadata.status
161
+ @headers = @metadata.headers
70
162
  source = body.is_a?(String) ? [body].freeze : body
71
163
  @body = OpenAI::Internal::Util.fused_enum(source) do
72
164
  if source.respond_to?(:close)
@@ -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: last_id}})
@@ -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} last_id=#{last_id.inspect}>"
88
- # rubocop:enable Layout/LineLength
86
+ "#<#{self.class}[#{model}]:0x#{object_id.to_s(16)} " \
87
+ "has_more=#{has_more.inspect} last_id=#{last_id.inspect}>"
89
88
  end
90
89
  end
91
90
  end
@@ -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