openai 0.9.0 → 0.11.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 (164) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +40 -0
  3. data/README.md +79 -1
  4. data/lib/openai/client.rb +11 -0
  5. data/lib/openai/errors.rb +25 -0
  6. data/lib/openai/internal/type/array_of.rb +6 -1
  7. data/lib/openai/internal/type/base_model.rb +76 -24
  8. data/lib/openai/internal/type/boolean.rb +7 -1
  9. data/lib/openai/internal/type/converter.rb +42 -34
  10. data/lib/openai/internal/type/enum.rb +10 -2
  11. data/lib/openai/internal/type/file_input.rb +6 -1
  12. data/lib/openai/internal/type/hash_of.rb +6 -1
  13. data/lib/openai/internal/type/union.rb +12 -7
  14. data/lib/openai/internal/type/unknown.rb +7 -1
  15. data/lib/openai/models/all_models.rb +4 -0
  16. data/lib/openai/models/audio/speech_create_params.rb +23 -2
  17. data/lib/openai/models/audio/transcription.rb +118 -1
  18. data/lib/openai/models/audio/transcription_text_done_event.rb +80 -1
  19. data/lib/openai/models/audio/transcription_verbose.rb +31 -1
  20. data/lib/openai/models/chat/chat_completion.rb +32 -31
  21. data/lib/openai/models/chat/chat_completion_chunk.rb +30 -29
  22. data/lib/openai/models/chat/completion_create_params.rb +34 -31
  23. data/lib/openai/models/fine_tuning/checkpoints/permission_retrieve_response.rb +60 -25
  24. data/lib/openai/models/images_response.rb +92 -1
  25. data/lib/openai/models/responses/response.rb +59 -35
  26. data/lib/openai/models/responses/response_code_interpreter_call_code_delta_event.rb +17 -8
  27. data/lib/openai/models/responses/response_code_interpreter_call_code_done_event.rb +14 -10
  28. data/lib/openai/models/responses/response_code_interpreter_call_completed_event.rb +11 -10
  29. data/lib/openai/models/responses/response_code_interpreter_call_in_progress_event.rb +11 -10
  30. data/lib/openai/models/responses/response_code_interpreter_call_interpreting_event.rb +11 -10
  31. data/lib/openai/models/responses/response_code_interpreter_tool_call.rb +49 -78
  32. data/lib/openai/models/responses/response_create_params.rb +92 -67
  33. data/lib/openai/models/responses/response_function_web_search.rb +115 -1
  34. data/lib/openai/models/responses/response_includable.rb +8 -6
  35. data/lib/openai/models/responses/response_output_text.rb +18 -2
  36. data/lib/openai/models/responses/response_stream_event.rb +2 -2
  37. data/lib/openai/models/responses/tool_choice_mcp.rb +40 -0
  38. data/lib/openai/models/responses/tool_choice_types.rb +0 -3
  39. data/lib/openai/models/responses_model.rb +4 -0
  40. data/lib/openai/models/webhooks/batch_cancelled_webhook_event.rb +84 -0
  41. data/lib/openai/models/webhooks/batch_completed_webhook_event.rb +84 -0
  42. data/lib/openai/models/webhooks/batch_expired_webhook_event.rb +84 -0
  43. data/lib/openai/models/webhooks/batch_failed_webhook_event.rb +84 -0
  44. data/lib/openai/models/webhooks/eval_run_canceled_webhook_event.rb +84 -0
  45. data/lib/openai/models/webhooks/eval_run_failed_webhook_event.rb +84 -0
  46. data/lib/openai/models/webhooks/eval_run_succeeded_webhook_event.rb +84 -0
  47. data/lib/openai/models/webhooks/fine_tuning_job_cancelled_webhook_event.rb +85 -0
  48. data/lib/openai/models/webhooks/fine_tuning_job_failed_webhook_event.rb +85 -0
  49. data/lib/openai/models/webhooks/fine_tuning_job_succeeded_webhook_event.rb +85 -0
  50. data/lib/openai/models/webhooks/response_cancelled_webhook_event.rb +85 -0
  51. data/lib/openai/models/webhooks/response_completed_webhook_event.rb +85 -0
  52. data/lib/openai/models/webhooks/response_failed_webhook_event.rb +84 -0
  53. data/lib/openai/models/webhooks/response_incomplete_webhook_event.rb +85 -0
  54. data/lib/openai/models/webhooks/unwrap_webhook_event.rb +59 -0
  55. data/lib/openai/models/webhooks/webhook_unwrap_params.rb +16 -0
  56. data/lib/openai/models.rb +2 -0
  57. data/lib/openai/resources/audio/speech.rb +3 -1
  58. data/lib/openai/resources/chat/completions.rb +10 -2
  59. data/lib/openai/resources/fine_tuning/checkpoints/permissions.rb +1 -2
  60. data/lib/openai/resources/responses.rb +24 -16
  61. data/lib/openai/resources/webhooks.rb +124 -0
  62. data/lib/openai/version.rb +1 -1
  63. data/lib/openai.rb +18 -0
  64. data/rbi/openai/client.rbi +3 -0
  65. data/rbi/openai/errors.rbi +16 -0
  66. data/rbi/openai/internal/type/boolean.rbi +2 -0
  67. data/rbi/openai/internal/type/converter.rbi +15 -15
  68. data/rbi/openai/internal/type/union.rbi +5 -0
  69. data/rbi/openai/internal/type/unknown.rbi +2 -0
  70. data/rbi/openai/models/all_models.rbi +20 -0
  71. data/rbi/openai/models/audio/speech_create_params.rbi +59 -2
  72. data/rbi/openai/models/audio/transcription.rbi +213 -3
  73. data/rbi/openai/models/audio/transcription_text_done_event.rbi +146 -1
  74. data/rbi/openai/models/audio/transcription_verbose.rbi +47 -0
  75. data/rbi/openai/models/chat/chat_completion.rbi +47 -42
  76. data/rbi/openai/models/chat/chat_completion_chunk.rbi +47 -42
  77. data/rbi/openai/models/chat/completion_create_params.rbi +51 -42
  78. data/rbi/openai/models/fine_tuning/checkpoints/permission_retrieve_response.rbi +95 -26
  79. data/rbi/openai/models/images_response.rbi +146 -0
  80. data/rbi/openai/models/responses/response.rbi +75 -44
  81. data/rbi/openai/models/responses/response_code_interpreter_call_code_delta_event.rbi +17 -7
  82. data/rbi/openai/models/responses/response_code_interpreter_call_code_done_event.rbi +13 -5
  83. data/rbi/openai/models/responses/response_code_interpreter_call_completed_event.rbi +13 -21
  84. data/rbi/openai/models/responses/response_code_interpreter_call_in_progress_event.rbi +13 -21
  85. data/rbi/openai/models/responses/response_code_interpreter_call_interpreting_event.rbi +13 -21
  86. data/rbi/openai/models/responses/response_code_interpreter_tool_call.rbi +83 -125
  87. data/rbi/openai/models/responses/response_create_params.rbi +174 -115
  88. data/rbi/openai/models/responses/response_function_web_search.rbi +163 -0
  89. data/rbi/openai/models/responses/response_includable.rbi +17 -11
  90. data/rbi/openai/models/responses/response_output_text.rbi +26 -4
  91. data/rbi/openai/models/responses/tool_choice_mcp.rbi +53 -0
  92. data/rbi/openai/models/responses/tool_choice_types.rbi +0 -5
  93. data/rbi/openai/models/responses_model.rbi +20 -0
  94. data/rbi/openai/models/webhooks/batch_cancelled_webhook_event.rbi +154 -0
  95. data/rbi/openai/models/webhooks/batch_completed_webhook_event.rbi +154 -0
  96. data/rbi/openai/models/webhooks/batch_expired_webhook_event.rbi +150 -0
  97. data/rbi/openai/models/webhooks/batch_failed_webhook_event.rbi +149 -0
  98. data/rbi/openai/models/webhooks/eval_run_canceled_webhook_event.rbi +154 -0
  99. data/rbi/openai/models/webhooks/eval_run_failed_webhook_event.rbi +151 -0
  100. data/rbi/openai/models/webhooks/eval_run_succeeded_webhook_event.rbi +154 -0
  101. data/rbi/openai/models/webhooks/fine_tuning_job_cancelled_webhook_event.rbi +158 -0
  102. data/rbi/openai/models/webhooks/fine_tuning_job_failed_webhook_event.rbi +156 -0
  103. data/rbi/openai/models/webhooks/fine_tuning_job_succeeded_webhook_event.rbi +158 -0
  104. data/rbi/openai/models/webhooks/response_cancelled_webhook_event.rbi +154 -0
  105. data/rbi/openai/models/webhooks/response_completed_webhook_event.rbi +154 -0
  106. data/rbi/openai/models/webhooks/response_failed_webhook_event.rbi +154 -0
  107. data/rbi/openai/models/webhooks/response_incomplete_webhook_event.rbi +155 -0
  108. data/rbi/openai/models/webhooks/unwrap_webhook_event.rbi +40 -0
  109. data/rbi/openai/models/webhooks/webhook_unwrap_params.rbi +32 -0
  110. data/rbi/openai/models.rbi +2 -0
  111. data/rbi/openai/resources/audio/speech.rbi +6 -1
  112. data/rbi/openai/resources/chat/completions.rbi +34 -30
  113. data/rbi/openai/resources/fine_tuning/checkpoints/permissions.rbi +1 -3
  114. data/rbi/openai/resources/responses.rbi +108 -84
  115. data/rbi/openai/resources/webhooks.rbi +68 -0
  116. data/sig/openai/client.rbs +2 -0
  117. data/sig/openai/errors.rbs +9 -0
  118. data/sig/openai/internal/type/converter.rbs +7 -1
  119. data/sig/openai/models/all_models.rbs +8 -0
  120. data/sig/openai/models/audio/speech_create_params.rbs +21 -1
  121. data/sig/openai/models/audio/transcription.rbs +95 -3
  122. data/sig/openai/models/audio/transcription_text_done_event.rbs +72 -2
  123. data/sig/openai/models/audio/transcription_verbose.rbs +21 -0
  124. data/sig/openai/models/chat/chat_completion.rbs +2 -1
  125. data/sig/openai/models/chat/chat_completion_chunk.rbs +2 -1
  126. data/sig/openai/models/chat/completion_create_params.rbs +2 -1
  127. data/sig/openai/models/fine_tuning/checkpoints/permission_retrieve_response.rbs +53 -16
  128. data/sig/openai/models/images_response.rbs +83 -0
  129. data/sig/openai/models/responses/response.rbs +13 -1
  130. data/sig/openai/models/responses/response_code_interpreter_call_code_delta_event.rbs +5 -0
  131. data/sig/openai/models/responses/response_code_interpreter_call_code_done_event.rbs +5 -0
  132. data/sig/openai/models/responses/response_code_interpreter_call_completed_event.rbs +4 -4
  133. data/sig/openai/models/responses/response_code_interpreter_call_in_progress_event.rbs +4 -4
  134. data/sig/openai/models/responses/response_code_interpreter_call_interpreting_event.rbs +4 -4
  135. data/sig/openai/models/responses/response_code_interpreter_tool_call.rbs +31 -52
  136. data/sig/openai/models/responses/response_create_params.rbs +31 -11
  137. data/sig/openai/models/responses/response_function_web_search.rbs +54 -0
  138. data/sig/openai/models/responses/response_includable.rbs +7 -5
  139. data/sig/openai/models/responses/response_output_text.rbs +15 -1
  140. data/sig/openai/models/responses/tool_choice_mcp.rbs +23 -0
  141. data/sig/openai/models/responses/tool_choice_types.rbs +0 -2
  142. data/sig/openai/models/responses_model.rbs +8 -0
  143. data/sig/openai/models/webhooks/batch_cancelled_webhook_event.rbs +66 -0
  144. data/sig/openai/models/webhooks/batch_completed_webhook_event.rbs +66 -0
  145. data/sig/openai/models/webhooks/batch_expired_webhook_event.rbs +66 -0
  146. data/sig/openai/models/webhooks/batch_failed_webhook_event.rbs +66 -0
  147. data/sig/openai/models/webhooks/eval_run_canceled_webhook_event.rbs +66 -0
  148. data/sig/openai/models/webhooks/eval_run_failed_webhook_event.rbs +66 -0
  149. data/sig/openai/models/webhooks/eval_run_succeeded_webhook_event.rbs +66 -0
  150. data/sig/openai/models/webhooks/fine_tuning_job_cancelled_webhook_event.rbs +66 -0
  151. data/sig/openai/models/webhooks/fine_tuning_job_failed_webhook_event.rbs +66 -0
  152. data/sig/openai/models/webhooks/fine_tuning_job_succeeded_webhook_event.rbs +66 -0
  153. data/sig/openai/models/webhooks/response_cancelled_webhook_event.rbs +66 -0
  154. data/sig/openai/models/webhooks/response_completed_webhook_event.rbs +66 -0
  155. data/sig/openai/models/webhooks/response_failed_webhook_event.rbs +66 -0
  156. data/sig/openai/models/webhooks/response_incomplete_webhook_event.rbs +66 -0
  157. data/sig/openai/models/webhooks/unwrap_webhook_event.rbs +27 -0
  158. data/sig/openai/models/webhooks/webhook_unwrap_params.rbs +17 -0
  159. data/sig/openai/models.rbs +2 -0
  160. data/sig/openai/resources/audio/speech.rbs +1 -0
  161. data/sig/openai/resources/fine_tuning/checkpoints/permissions.rbs +1 -1
  162. data/sig/openai/resources/responses.rbs +8 -4
  163. data/sig/openai/resources/webhooks.rbs +33 -0
  164. metadata +56 -2
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: e0a273910412c6b05028da1b9e7fceab0b048d4d34df4b1bee6a781dcbad33dd
4
- data.tar.gz: 51a3719def2827a793c2b9974cc4104e00000cea78893ff36d327254bb8e586e
3
+ metadata.gz: 252a9ce9833b0a9f66be94b76081f34716e348ebca732e63c3e16c105ed42ea1
4
+ data.tar.gz: d4d8b36822ee74af77508ec8e48b472d72d619333e468055695158e242c49760
5
5
  SHA512:
6
- metadata.gz: d65777601ceac3d125750949f80a0ca4d50f62593e971d067f7c164ab1dbbddf51266cee8ab752ef058f2313f09b7ad48a6312c8df7a465e0807e956ed55cedf
7
- data.tar.gz: 9156aee0d88ad6be1e794acede9961d774c40ade621282aff54899879e718f6f514f61007d25f614e507796eefefbf694508042e1e126fc0412aac76b003ff44
6
+ metadata.gz: f27c40e40df727c8da570a4f6b1e20f72a87baa710efd3957bb0fbbcc23eb1dd78d03b1bcc3c3cf5684d4db4b062c392c1c3d970093c8e0fac89ef85db053db3
7
+ data.tar.gz: 21fe44d1605c6196d7321dcb7efa7d951a2523ba71859eab1bf242ff3314afe0cce25201c21b141f9f3902b684cb577ba51c520d12602d1279eccb770769e3a4
data/CHANGELOG.md CHANGED
@@ -1,5 +1,45 @@
1
1
  # Changelog
2
2
 
3
+ ## 0.11.0 (2025-06-26)
4
+
5
+ Full Changelog: [v0.10.0...v0.11.0](https://github.com/openai/openai-ruby/compare/v0.10.0...v0.11.0)
6
+
7
+ ### Features
8
+
9
+ * **api:** webhook and deep research support ([6228400](https://github.com/openai/openai-ruby/commit/6228400e19aadefc5f87e24b3c104fc0b44d3cee))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * **ci:** release-doctor — report correct token name ([c12c991](https://github.com/openai/openai-ruby/commit/c12c9911beaeb8b1c72d7c5cc5f14dcb9cd5452e))
15
+
16
+
17
+ ### Chores
18
+
19
+ * **api:** remove unsupported property ([1073c3a](https://github.com/openai/openai-ruby/commit/1073c3a6059f2d1e1ef92937326699e0240503e5))
20
+ * **client:** throw specific errors ([0cf937e](https://github.com/openai/openai-ruby/commit/0cf937ea8abebc05e52a419e19e275a45b5da646))
21
+ * **docs:** update README to include links to docs on Webhooks ([2d8f23e](https://github.com/openai/openai-ruby/commit/2d8f23ecb245c88f3f082f93eb906af857d64c7d))
22
+
23
+ ## 0.10.0 (2025-06-23)
24
+
25
+ Full Changelog: [v0.9.0...v0.10.0](https://github.com/openai/openai-ruby/compare/v0.9.0...v0.10.0)
26
+
27
+ ### Features
28
+
29
+ * **api:** make model and inputs not required to create response ([2087fb5](https://github.com/openai/openai-ruby/commit/2087fb53d775f6481dd34737f6d554c5c35f65e7))
30
+ * **api:** update api shapes for usage and code interpreter ([733ebfb](https://github.com/openai/openai-ruby/commit/733ebfbafe14d9733149b174c99d41d471a42865))
31
+
32
+
33
+ ### Bug Fixes
34
+
35
+ * **internal:** fix: should publish to ruby gems when a release is created ([aebd8eb](https://github.com/openai/openai-ruby/commit/aebd8eb2855d6a8f4fe685bdb5a458346d509e50))
36
+ * issue where we cannot mutate arrays on base model derivatives ([266d072](https://github.com/openai/openai-ruby/commit/266d072946c75f93abeff45eec9787ce4e7fea56))
37
+
38
+
39
+ ### Chores
40
+
41
+ * allow more free formatted json response input ([#726](https://github.com/openai/openai-ruby/issues/726)) ([69fb0af](https://github.com/openai/openai-ruby/commit/69fb0afabf86ecc3d1ca469d9700c42447569f3b))
42
+
3
43
  ## 0.9.0 (2025-06-17)
4
44
 
5
45
  Full Changelog: [v0.8.0...v0.9.0](https://github.com/openai/openai-ruby/compare/v0.8.0...v0.9.0)
data/README.md CHANGED
@@ -15,7 +15,7 @@ To use this gem, install via Bundler by adding the following to your application
15
15
  <!-- x-release-please-start-version -->
16
16
 
17
17
  ```ruby
18
- gem "openai", "~> 0.9.0"
18
+ gem "openai", "~> 0.11.0"
19
19
  ```
20
20
 
21
21
  <!-- x-release-please-end -->
@@ -112,6 +112,84 @@ puts(edited.data.first)
112
112
 
113
113
  Note that you can also pass a raw `IO` descriptor, but this disables retries, as the library can't be sure if the descriptor is a file or pipe (which cannot be rewound).
114
114
 
115
+ ## Webhook Verification
116
+
117
+ Verifying webhook signatures is _optional but encouraged_.
118
+
119
+ For more information about webhooks, see [the API docs](https://platform.openai.com/docs/guides/webhooks).
120
+
121
+ ### Parsing webhook payloads
122
+
123
+ For most use cases, you will likely want to verify the webhook and parse the payload at the same time. To achieve this, we provide the method `client.webhooks.unwrap`, which parses a webhook request and verifies that it was sent by OpenAI. This method will raise an error if the signature is invalid.
124
+
125
+ Note that the `body` parameter must be the raw JSON string sent from the server (do not parse it first). The `unwrap` method will parse this JSON for you into an event object after verifying the webhook was sent from OpenAI.
126
+
127
+ ```ruby
128
+ require 'sinatra'
129
+ require 'openai'
130
+
131
+ # Set up the client with webhook secret from environment variable
132
+ client = OpenAI::Client.new(webhook_secret: ENV['OPENAI_WEBHOOK_SECRET'])
133
+
134
+ post '/webhook' do
135
+ request_body = request.body.read
136
+
137
+ begin
138
+ event = client.webhooks.unwrap(request_body, request.env)
139
+
140
+ case event.type
141
+ when 'response.completed'
142
+ puts "Response completed: #{event.data}"
143
+ when 'response.failed'
144
+ puts "Response failed: #{event.data}"
145
+ else
146
+ puts "Unhandled event type: #{event.type}"
147
+ end
148
+
149
+ status 200
150
+ 'ok'
151
+ rescue StandardError => e
152
+ puts "Invalid signature: #{e}"
153
+ status 400
154
+ 'Invalid signature'
155
+ end
156
+ end
157
+ ```
158
+
159
+ ### Verifying webhook payloads directly
160
+
161
+ In some cases, you may want to verify the webhook separately from parsing the payload. If you prefer to handle these steps separately, we provide the method `client.webhooks.verify_signature` to _only verify_ the signature of a webhook request. Like `unwrap`, this method will raise an error if the signature is invalid.
162
+
163
+ Note that the `body` parameter must be the raw JSON string sent from the server (do not parse it first). You will then need to parse the body after verifying the signature.
164
+
165
+ ```ruby
166
+ require 'sinatra'
167
+ require 'json'
168
+ require 'openai'
169
+
170
+ # Set up the client with webhook secret from environment variable
171
+ client = OpenAI::Client.new(webhook_secret: ENV['OPENAI_WEBHOOK_SECRET'])
172
+
173
+ post '/webhook' do
174
+ request_body = request.body.read
175
+
176
+ begin
177
+ client.webhooks.verify_signature(request_body, request.env)
178
+
179
+ # Parse the body after verification
180
+ event = JSON.parse(request_body)
181
+ puts "Verified event: #{event}"
182
+
183
+ status 200
184
+ 'ok'
185
+ rescue StandardError => e
186
+ puts "Invalid signature: #{e}"
187
+ status 400
188
+ 'Invalid signature'
189
+ end
190
+ end
191
+ ```
192
+
115
193
  ### [Structured outputs](https://platform.openai.com/docs/guides/structured-outputs) and function calling
116
194
 
117
195
  This SDK ships with helpers in `OpenAI::BaseModel`, `OpenAI::ArrayOf`, `OpenAI::EnumOf`, and `OpenAI::UnionOf` to help you define the supported JSON schemas used in making structured outputs and function calling requests.
data/lib/openai/client.rb CHANGED
@@ -24,6 +24,9 @@ module OpenAI
24
24
  # @return [String, nil]
25
25
  attr_reader :project
26
26
 
27
+ # @return [String, nil]
28
+ attr_reader :webhook_secret
29
+
27
30
  # @return [OpenAI::Resources::Completions]
28
31
  attr_reader :completions
29
32
 
@@ -57,6 +60,9 @@ module OpenAI
57
60
  # @return [OpenAI::Resources::VectorStores]
58
61
  attr_reader :vector_stores
59
62
 
63
+ # @return [OpenAI::Resources::Webhooks]
64
+ attr_reader :webhooks
65
+
60
66
  # @return [OpenAI::Resources::Beta]
61
67
  attr_reader :beta
62
68
 
@@ -92,6 +98,8 @@ module OpenAI
92
98
  #
93
99
  # @param project [String, nil] Defaults to `ENV["OPENAI_PROJECT_ID"]`
94
100
  #
101
+ # @param webhook_secret [String, nil] Defaults to `ENV["OPENAI_WEBHOOK_SECRET"]`
102
+ #
95
103
  # @param base_url [String, nil] Override the default base URL for the API, e.g.,
96
104
  # `"https://api.example.com/v2/"`. Defaults to `ENV["OPENAI_BASE_URL"]`
97
105
  #
@@ -106,6 +114,7 @@ module OpenAI
106
114
  api_key: ENV["OPENAI_API_KEY"],
107
115
  organization: ENV["OPENAI_ORG_ID"],
108
116
  project: ENV["OPENAI_PROJECT_ID"],
117
+ webhook_secret: ENV["OPENAI_WEBHOOK_SECRET"],
109
118
  base_url: ENV["OPENAI_BASE_URL"],
110
119
  max_retries: self.class::DEFAULT_MAX_RETRIES,
111
120
  timeout: self.class::DEFAULT_TIMEOUT_IN_SECONDS,
@@ -124,6 +133,7 @@ module OpenAI
124
133
  }
125
134
 
126
135
  @api_key = api_key.to_s
136
+ @webhook_secret = webhook_secret&.to_s
127
137
 
128
138
  super(
129
139
  base_url: base_url,
@@ -145,6 +155,7 @@ module OpenAI
145
155
  @fine_tuning = OpenAI::Resources::FineTuning.new(client: self)
146
156
  @graders = OpenAI::Resources::Graders.new(client: self)
147
157
  @vector_stores = OpenAI::Resources::VectorStores.new(client: self)
158
+ @webhooks = OpenAI::Resources::Webhooks.new(client: self)
148
159
  @beta = OpenAI::Resources::Beta.new(client: self)
149
160
  @batches = OpenAI::Resources::Batches.new(client: self)
150
161
  @uploads = OpenAI::Resources::Uploads.new(client: self)
data/lib/openai/errors.rb CHANGED
@@ -8,7 +8,32 @@ module OpenAI
8
8
  # @return [StandardError, nil]
9
9
  end
10
10
 
11
+ class InvalidWebhookSignatureError < OpenAI::Errors::Error
12
+ end
13
+
11
14
  class ConversionError < OpenAI::Errors::Error
15
+ # @return [StandardError, nil]
16
+ def cause = @cause.nil? ? super : @cause
17
+
18
+ # @api private
19
+ #
20
+ # @param on [Class<StandardError>]
21
+ # @param method [Symbol]
22
+ # @param target [Object]
23
+ # @param value [Object]
24
+ # @param cause [StandardError, nil]
25
+ def initialize(on:, method:, target:, value:, cause: nil)
26
+ cls = on.name.split("::").last
27
+
28
+ message = [
29
+ "Failed to parse #{cls}.#{method} from #{value.class} to #{target.inspect}.",
30
+ "To get the unparsed API response, use #{cls}[#{method.inspect}].",
31
+ cause && "Cause: #{cause.message}"
32
+ ].filter(&:itself).join(" ")
33
+
34
+ @cause = cause
35
+ super(message)
36
+ end
12
37
  end
13
38
 
14
39
  class APIError < OpenAI::Errors::Error
@@ -62,10 +62,14 @@ module OpenAI
62
62
  #
63
63
  # @param state [Hash{Symbol=>Object}] .
64
64
  #
65
- # @option state [Boolean, :strong] :strictness
65
+ # @option state [Boolean] :translate_names
66
+ #
67
+ # @option state [Boolean] :strictness
66
68
  #
67
69
  # @option state [Hash{Symbol=>Object}] :exactness
68
70
  #
71
+ # @option state [Class<StandardError>] :error
72
+ #
69
73
  # @option state [Integer] :branched
70
74
  #
71
75
  # @return [Array<Object>, Object]
@@ -74,6 +78,7 @@ module OpenAI
74
78
 
75
79
  unless value.is_a?(Array)
76
80
  exactness[:no] += 1
81
+ state[:error] = TypeError.new("#{value.class} can't be coerced into #{Array}")
77
82
  return value
78
83
  end
79
84
 
@@ -60,7 +60,7 @@ module OpenAI
60
60
  [OpenAI::Internal::Type::Converter.type_info(type_info), type_info]
61
61
  end
62
62
 
63
- setter = "#{name_sym}="
63
+ setter = :"#{name_sym}="
64
64
  api_name = info.fetch(:api_name, name_sym)
65
65
  nilable = info.fetch(:nil?, false)
66
66
  const = required && !nilable ? info.fetch(:const, OpenAI::Internal::OMIT) : OpenAI::Internal::OMIT
@@ -77,30 +77,61 @@ module OpenAI
77
77
  type_fn: type_fn
78
78
  }
79
79
 
80
- define_method(setter) { @data.store(name_sym, _1) }
80
+ define_method(setter) do |value|
81
+ target = type_fn.call
82
+ state = OpenAI::Internal::Type::Converter.new_coerce_state(translate_names: false)
83
+ coerced = OpenAI::Internal::Type::Converter.coerce(target, value, state: state)
84
+ error = @coerced.store(name_sym, state.fetch(:error) || true)
85
+ stored =
86
+ case [target, error]
87
+ in [OpenAI::Internal::Type::Converter | Symbol, nil]
88
+ coerced
89
+ else
90
+ value
91
+ end
92
+ @data.store(name_sym, stored)
93
+ end
81
94
 
95
+ # rubocop:disable Style/CaseEquality
96
+ # rubocop:disable Metrics/BlockLength
82
97
  define_method(name_sym) do
83
98
  target = type_fn.call
84
- value = @data.fetch(name_sym) { const == OpenAI::Internal::OMIT ? nil : const }
85
- state = {strictness: :strong, exactness: {yes: 0, no: 0, maybe: 0}, branched: 0}
86
- if (nilable || !required) && value.nil?
87
- nil
88
- else
89
- OpenAI::Internal::Type::Converter.coerce(
90
- target,
91
- value,
92
- state: state
99
+
100
+ case @coerced[name_sym]
101
+ in true | false if OpenAI::Internal::Type::Converter === target
102
+ @data.fetch(name_sym)
103
+ in ::StandardError => e
104
+ raise OpenAI::Errors::ConversionError.new(
105
+ on: self.class,
106
+ method: __method__,
107
+ target: target,
108
+ value: @data.fetch(name_sym),
109
+ cause: e
93
110
  )
111
+ else
112
+ Kernel.then do
113
+ value = @data.fetch(name_sym) { const == OpenAI::Internal::OMIT ? nil : const }
114
+ state = OpenAI::Internal::Type::Converter.new_coerce_state(translate_names: false)
115
+ if (nilable || !required) && value.nil?
116
+ nil
117
+ else
118
+ OpenAI::Internal::Type::Converter.coerce(
119
+ target, value, state: state
120
+ )
121
+ end
122
+ rescue StandardError => e
123
+ raise OpenAI::Errors::ConversionError.new(
124
+ on: self.class,
125
+ method: __method__,
126
+ target: target,
127
+ value: value,
128
+ cause: e
129
+ )
130
+ end
94
131
  end
95
- rescue StandardError => e
96
- cls = self.class.name.split("::").last
97
- message = [
98
- "Failed to parse #{cls}.#{__method__} from #{value.class} to #{target.inspect}.",
99
- "To get the unparsed API response, use #{cls}[#{__method__.inspect}].",
100
- "Cause: #{e.message}"
101
- ].join(" ")
102
- raise OpenAI::Errors::ConversionError.new(message)
103
132
  end
133
+ # rubocop:enable Metrics/BlockLength
134
+ # rubocop:enable Style/CaseEquality
104
135
  end
105
136
 
106
137
  # @api private
@@ -200,10 +231,14 @@ module OpenAI
200
231
  #
201
232
  # @param state [Hash{Symbol=>Object}] .
202
233
  #
203
- # @option state [Boolean, :strong] :strictness
234
+ # @option state [Boolean] :translate_names
235
+ #
236
+ # @option state [Boolean] :strictness
204
237
  #
205
238
  # @option state [Hash{Symbol=>Object}] :exactness
206
239
  #
240
+ # @option state [Class<StandardError>] :error
241
+ #
207
242
  # @option state [Integer] :branched
208
243
  #
209
244
  # @return [self, Object]
@@ -217,6 +252,7 @@ module OpenAI
217
252
 
218
253
  unless (val = OpenAI::Internal::Util.coerce_hash(value)).is_a?(Hash)
219
254
  exactness[:no] += 1
255
+ state[:error] = TypeError.new("#{value.class} can't be coerced into #{Hash}")
220
256
  return value
221
257
  end
222
258
  exactness[:yes] += 1
@@ -224,13 +260,15 @@ module OpenAI
224
260
  keys = val.keys.to_set
225
261
  instance = new
226
262
  data = instance.to_h
263
+ viability = instance.instance_variable_get(:@coerced)
227
264
 
228
265
  # rubocop:disable Metrics/BlockLength
229
266
  fields.each do |name, field|
230
267
  mode, required, target = field.fetch_values(:mode, :required, :type)
231
268
  api_name, nilable, const = field.fetch_values(:api_name, :nilable, :const)
269
+ src_name = state.fetch(:translate_names) ? api_name : name
232
270
 
233
- unless val.key?(api_name)
271
+ unless val.key?(src_name)
234
272
  if required && mode != :dump && const == OpenAI::Internal::OMIT
235
273
  exactness[nilable ? :maybe : :no] += 1
236
274
  else
@@ -239,9 +277,10 @@ module OpenAI
239
277
  next
240
278
  end
241
279
 
242
- item = val.fetch(api_name)
243
- keys.delete(api_name)
280
+ item = val.fetch(src_name)
281
+ keys.delete(src_name)
244
282
 
283
+ state[:error] = nil
245
284
  converted =
246
285
  if item.nil? && (nilable || !required)
247
286
  exactness[nilable ? :yes : :maybe] += 1
@@ -255,6 +294,8 @@ module OpenAI
255
294
  item
256
295
  end
257
296
  end
297
+
298
+ viability.store(name, state.fetch(:error) || true)
258
299
  data.store(name, converted)
259
300
  end
260
301
  # rubocop:enable Metrics/BlockLength
@@ -430,7 +471,18 @@ module OpenAI
430
471
  # Create a new instance of a model.
431
472
  #
432
473
  # @param data [Hash{Symbol=>Object}, self]
433
- def initialize(data = {}) = (@data = OpenAI::Internal::Util.coerce_hash!(data).to_h)
474
+ def initialize(data = {})
475
+ @data = {}
476
+ @coerced = {}
477
+ OpenAI::Internal::Util.coerce_hash!(data).each do
478
+ if self.class.known_fields.key?(_1)
479
+ public_send(:"#{_1}=", _2)
480
+ else
481
+ @data.store(_1, _2)
482
+ @coerced.store(_1, false)
483
+ end
484
+ end
485
+ end
434
486
 
435
487
  class << self
436
488
  # @api private
@@ -31,14 +31,20 @@ module OpenAI
31
31
  class << self
32
32
  # @api private
33
33
  #
34
+ # Coerce value to Boolean if possible, otherwise return the original value.
35
+ #
34
36
  # @param value [Boolean, Object]
35
37
  #
36
38
  # @param state [Hash{Symbol=>Object}] .
37
39
  #
38
- # @option state [Boolean, :strong] :strictness
40
+ # @option state [Boolean] :translate_names
41
+ #
42
+ # @option state [Boolean] :strictness
39
43
  #
40
44
  # @option state [Hash{Symbol=>Object}] :exactness
41
45
  #
46
+ # @option state [Class<StandardError>] :error
47
+ #
42
48
  # @option state [Integer] :branched
43
49
  #
44
50
  # @return [Boolean, Object]
@@ -15,10 +15,14 @@ module OpenAI
15
15
  #
16
16
  # @param state [Hash{Symbol=>Object}] .
17
17
  #
18
- # @option state [Boolean, :strong] :strictness
18
+ # @option state [Boolean] :translate_names
19
+ #
20
+ # @option state [Boolean] :strictness
19
21
  #
20
22
  # @option state [Hash{Symbol=>Object}] :exactness
21
23
  #
24
+ # @option state [Class<StandardError>] :error
25
+ #
22
26
  # @option state [Integer] :branched
23
27
  #
24
28
  # @return [Object]
@@ -94,6 +98,21 @@ module OpenAI
94
98
  end
95
99
  end
96
100
 
101
+ # @api private
102
+ #
103
+ # @param translate_names [Boolean]
104
+ #
105
+ # @return [Hash{Symbol=>Object}]
106
+ def new_coerce_state(translate_names: true)
107
+ {
108
+ translate_names: translate_names,
109
+ strictness: true,
110
+ exactness: {yes: 0, no: 0, maybe: 0},
111
+ error: nil,
112
+ branched: 0
113
+ }
114
+ end
115
+
97
116
  # @api private
98
117
  #
99
118
  # Based on `target`, transform `value` into `target`, to the extent possible:
@@ -110,14 +129,11 @@ module OpenAI
110
129
  #
111
130
  # @param value [Object]
112
131
  #
113
- # @param state [Hash{Symbol=>Object}] The `strictness` is one of `true`, `false`, or `:strong`. This informs the
114
- # coercion strategy when we have to decide between multiple possible conversion
115
- # targets:
132
+ # @param state [Hash{Symbol=>Object}] The `strictness` is one of `true`, `false`. This informs the coercion strategy
133
+ # when we have to decide between multiple possible conversion targets:
116
134
  #
117
135
  # - `true`: the conversion must be exact, with minimum coercion.
118
136
  # - `false`: the conversion can be approximate, with some coercion.
119
- # - `:strong`: the conversion must be exact, with no coercion, and raise an error
120
- # if not possible.
121
137
  #
122
138
  # The `exactness` is `Hash` with keys being one of `yes`, `no`, or `maybe`. For
123
139
  # any given conversion attempt, the exactness will be updated based on how closely
@@ -130,21 +146,20 @@ module OpenAI
130
146
  #
131
147
  # See implementation below for more details.
132
148
  #
133
- # @option state [Boolean, :strong] :strictness
149
+ # @option state [Boolean] :translate_names
150
+ #
151
+ # @option state [Boolean] :strictness
134
152
  #
135
153
  # @option state [Hash{Symbol=>Object}] :exactness
136
154
  #
155
+ # @option state [Class<StandardError>] :error
156
+ #
137
157
  # @option state [Integer] :branched
138
158
  #
139
159
  # @return [Object]
140
- def coerce(
141
- target,
142
- value,
143
- state: {strictness: true, exactness: {yes: 0, no: 0, maybe: 0}, branched: 0}
144
- )
145
- # rubocop:disable Lint/SuppressedException
160
+ def coerce(target, value, state: OpenAI::Internal::Type::Converter.new_coerce_state)
146
161
  # rubocop:disable Metrics/BlockNesting
147
- strictness, exactness = state.fetch_values(:strictness, :exactness)
162
+ exactness = state.fetch(:exactness)
148
163
 
149
164
  case target
150
165
  in OpenAI::Internal::Type::Converter
@@ -160,29 +175,26 @@ module OpenAI
160
175
  exactness[value.nil? ? :yes : :maybe] += 1
161
176
  return nil
162
177
  in -> { _1 <= Integer }
163
- if value.is_a?(Integer)
178
+ case value
179
+ in Integer
164
180
  exactness[:yes] += 1
165
181
  return value
166
- elsif strictness == :strong && Integer(value, exception: false) != value
167
- message = "no implicit conversion of #{value.class} into #{target.inspect}"
168
- raise value.is_a?(Numeric) ? ArgumentError.new(message) : TypeError.new(message)
169
182
  else
170
183
  Kernel.then do
171
184
  return Integer(value).tap { exactness[:maybe] += 1 }
172
- rescue ArgumentError, TypeError
185
+ rescue ArgumentError, TypeError => e
186
+ state[:error] = e
173
187
  end
174
188
  end
175
189
  in -> { _1 <= Float }
176
190
  if value.is_a?(Numeric)
177
191
  exactness[:yes] += 1
178
192
  return Float(value)
179
- elsif strictness == :strong
180
- message = "no implicit conversion of #{value.class} into #{target.inspect}"
181
- raise TypeError.new(message)
182
193
  else
183
194
  Kernel.then do
184
195
  return Float(value).tap { exactness[:maybe] += 1 }
185
- rescue ArgumentError, TypeError
196
+ rescue ArgumentError, TypeError => e
197
+ state[:error] = e
186
198
  end
187
199
  end
188
200
  in -> { _1 <= String }
@@ -194,16 +206,13 @@ module OpenAI
194
206
  exactness[:yes] += 1
195
207
  return value.string
196
208
  else
197
- if strictness == :strong
198
- message = "no implicit conversion of #{value.class} into #{target.inspect}"
199
- raise TypeError.new(message)
200
- end
209
+ state[:error] = TypeError.new("#{value.class} can't be coerced into #{String}")
201
210
  end
202
211
  in -> { _1 <= Date || _1 <= Time }
203
212
  Kernel.then do
204
213
  return target.parse(value).tap { exactness[:yes] += 1 }
205
214
  rescue ArgumentError, TypeError => e
206
- raise e if strictness == :strong
215
+ state[:error] = e
207
216
  end
208
217
  in -> { _1 <= StringIO } if value.is_a?(String)
209
218
  exactness[:yes] += 1
@@ -221,10 +230,8 @@ module OpenAI
221
230
  return value
222
231
  end
223
232
  else
224
- if strictness == :strong
225
- message = "cannot convert non-matching #{value.class} into #{target.inspect}"
226
- raise ArgumentError.new(message)
227
- end
233
+ message = "cannot convert non-matching #{value.class} into #{target.inspect}"
234
+ state[:error] = ArgumentError.new(message)
228
235
  end
229
236
  else
230
237
  end
@@ -232,7 +239,6 @@ module OpenAI
232
239
  exactness[:no] += 1
233
240
  value
234
241
  # rubocop:enable Metrics/BlockNesting
235
- # rubocop:enable Lint/SuppressedException
236
242
  end
237
243
 
238
244
  # @api private
@@ -277,8 +283,10 @@ module OpenAI
277
283
  define_sorbet_constant!(:CoerceState) do
278
284
  T.type_alias do
279
285
  {
280
- strictness: T.any(T::Boolean, Symbol),
286
+ translate_names: T::Boolean,
287
+ strictness: T::Boolean,
281
288
  exactness: {yes: Integer, no: Integer, maybe: Integer},
289
+ error: T::Class[StandardError],
282
290
  branched: Integer
283
291
  }
284
292
  end
@@ -81,10 +81,14 @@ module OpenAI
81
81
  #
82
82
  # @param state [Hash{Symbol=>Object}] .
83
83
  #
84
- # @option state [Boolean, :strong] :strictness
84
+ # @option state [Boolean] :translate_names
85
+ #
86
+ # @option state [Boolean] :strictness
85
87
  #
86
88
  # @option state [Hash{Symbol=>Object}] :exactness
87
89
  #
90
+ # @option state [Class<StandardError>] :error
91
+ #
88
92
  # @option state [Integer] :branched
89
93
  #
90
94
  # @return [Symbol, Object]
@@ -95,8 +99,12 @@ module OpenAI
95
99
  if values.include?(val)
96
100
  exactness[:yes] += 1
97
101
  val
102
+ elsif values.first&.class == val.class
103
+ exactness[:maybe] += 1
104
+ value
98
105
  else
99
- exactness[values.first&.class == val.class ? :maybe : :no] += 1
106
+ exactness[:no] += 1
107
+ state[:error] = TypeError.new("#{value.class} can't be coerced into #{self}")
100
108
  value
101
109
  end
102
110
  end
@@ -45,10 +45,14 @@ module OpenAI
45
45
  #
46
46
  # @param state [Hash{Symbol=>Object}] .
47
47
  #
48
- # @option state [Boolean, :strong] :strictness
48
+ # @option state [Boolean] :translate_names
49
+ #
50
+ # @option state [Boolean] :strictness
49
51
  #
50
52
  # @option state [Hash{Symbol=>Object}] :exactness
51
53
  #
54
+ # @option state [Class<StandardError>] :error
55
+ #
52
56
  # @option state [Integer] :branched
53
57
  #
54
58
  # @return [StringIO, Object]
@@ -62,6 +66,7 @@ module OpenAI
62
66
  exactness[:yes] += 1
63
67
  value
64
68
  else
69
+ state[:error] = TypeError.new("#{value.class} can't be coerced into #{StringIO}")
65
70
  exactness[:no] += 1
66
71
  value
67
72
  end