openai 0.82.0 → 0.84.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 (88) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +47 -0
  3. data/README.md +1 -1
  4. data/lib/openai/auth/workload_identity_auth.rb +17 -2
  5. data/lib/openai/client.rb +20 -6
  6. data/lib/openai/errors.rb +1 -1
  7. data/lib/openai/helpers/realtime/client_extension.rb +107 -0
  8. data/lib/openai/helpers/streaming/chat_completion_stream.rb +1 -2
  9. data/lib/openai/helpers/streaming/response_events.rb +2 -2
  10. data/lib/openai/helpers/streaming/response_stream.rb +73 -42
  11. data/lib/openai/helpers/structured_output/chat_completion_parser.rb +6 -0
  12. data/lib/openai/helpers/structured_output/response_parser.rb +1 -1
  13. data/lib/openai/http_client.rb +40 -1
  14. data/lib/openai/internal/logging.rb +7 -1
  15. data/lib/openai/internal/transport/base_client.rb +35 -4
  16. data/lib/openai/internal/type/base_model.rb +2 -2
  17. data/lib/openai/internal/type/converter.rb +1 -1
  18. data/lib/openai/internal/util.rb +8 -4
  19. data/lib/openai/models/admin/organization/audit_log_list_params.rb +2 -0
  20. data/lib/openai/models/admin/organization/audit_log_list_response.rb +2 -0
  21. data/lib/openai/models/admin/organization/usage_costs_params.rb +10 -1
  22. data/lib/openai/models/batch.rb +4 -4
  23. data/lib/openai/models/beta/beta_response.rb +9 -12
  24. data/lib/openai/models/beta/beta_responses_client_event.rb +8 -12
  25. data/lib/openai/models/beta/response_compact_params.rb +7 -7
  26. data/lib/openai/models/beta/response_create_params.rb +8 -12
  27. data/lib/openai/models/chat/completion_create_params.rb +8 -8
  28. data/lib/openai/models/eval_create_response.rb +1 -1
  29. data/lib/openai/models/eval_list_response.rb +1 -1
  30. data/lib/openai/models/eval_retrieve_response.rb +1 -1
  31. data/lib/openai/models/eval_update_response.rb +1 -1
  32. data/lib/openai/models/evals/create_eval_completions_run_data_source.rb +2 -2
  33. data/lib/openai/models/reasoning.rb +0 -2
  34. data/lib/openai/models/responses/response.rb +6 -7
  35. data/lib/openai/models/responses/response_compact_params.rb +7 -7
  36. data/lib/openai/models/responses/response_create_params.rb +5 -7
  37. data/lib/openai/models/responses/responses_client_event.rb +5 -7
  38. data/lib/openai/request_options.rb +8 -0
  39. data/lib/openai/resources/admin/organization/usage.rb +3 -1
  40. data/lib/openai/resources/beta/responses.rb +5 -5
  41. data/lib/openai/resources/chat/completions.rb +2 -2
  42. data/lib/openai/resources/responses.rb +7 -6
  43. data/lib/openai/version.rb +1 -1
  44. data/lib/openai.rb +1 -0
  45. data/rbi/openai/errors.rbi +1 -1
  46. data/rbi/openai/helpers/streaming/events.rbi +2 -2
  47. data/rbi/openai/helpers/streaming/response_stream.rbi +3 -3
  48. data/rbi/openai/http_client.rbi +25 -2
  49. data/rbi/openai/internal/logging.rbi +12 -2
  50. data/rbi/openai/internal/transport/base_client.rbi +12 -2
  51. data/rbi/openai/internal/util.rbi +3 -2
  52. data/rbi/openai/models/admin/organization/audit_log_list_params.rbi +8 -0
  53. data/rbi/openai/models/admin/organization/audit_log_list_response.rbi +8 -0
  54. data/rbi/openai/models/admin/organization/usage_costs_params.rbi +15 -0
  55. data/rbi/openai/models/batch.rbi +6 -6
  56. data/rbi/openai/models/beta/beta_response.rbi +10 -15
  57. data/rbi/openai/models/beta/beta_responses_client_event.rbi +9 -15
  58. data/rbi/openai/models/beta/response_compact_params.rbi +9 -9
  59. data/rbi/openai/models/beta/response_create_params.rbi +9 -15
  60. data/rbi/openai/models/chat/completion_create_params.rbi +9 -9
  61. data/rbi/openai/models/eval_create_response.rbi +1 -1
  62. data/rbi/openai/models/eval_list_response.rbi +1 -1
  63. data/rbi/openai/models/eval_retrieve_response.rbi +1 -1
  64. data/rbi/openai/models/eval_update_response.rbi +1 -1
  65. data/rbi/openai/models/evals/create_eval_completions_run_data_source.rbi +2 -2
  66. data/rbi/openai/models/reasoning.rbi +0 -2
  67. data/rbi/openai/models/responses/response.rbi +7 -10
  68. data/rbi/openai/models/responses/response_compact_params.rbi +9 -9
  69. data/rbi/openai/models/responses/response_create_params.rbi +6 -10
  70. data/rbi/openai/models/responses/responses_client_event.rbi +6 -10
  71. data/rbi/openai/request_options.rbi +4 -0
  72. data/rbi/openai/resources/admin/organization/usage.rbi +4 -0
  73. data/rbi/openai/resources/beta/responses.rbi +7 -11
  74. data/rbi/openai/resources/chat/completions.rbi +6 -6
  75. data/rbi/openai/resources/responses.rbi +9 -13
  76. data/sig/openai/errors.rbs +1 -1
  77. data/sig/openai/http_client.rbs +14 -1
  78. data/sig/openai/internal/logging.rbs +6 -1
  79. data/sig/openai/internal/transport/base_client.rbs +11 -20
  80. data/sig/openai/internal/util.rbs +3 -1
  81. data/sig/openai/models/admin/organization/audit_log_list_params.rbs +4 -0
  82. data/sig/openai/models/admin/organization/audit_log_list_response.rbs +4 -0
  83. data/sig/openai/models/admin/organization/usage_costs_params.rbs +7 -0
  84. data/sig/openai/models/beta/beta_response.rbs +2 -1
  85. data/sig/openai/models/responses/response.rbs +2 -1
  86. data/sig/openai/request_options.rbs +4 -1
  87. data/sig/openai/resources/admin/organization/usage.rbs +1 -0
  88. metadata +1 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 46609364ce91a3198dceda84c59f20bc29f5bb205f4078289748b41b8df4cdbe
4
- data.tar.gz: 749aa50031731341acedf30ef7011cdba8591140cf932eb835793295f2f4328f
3
+ metadata.gz: 03f5297b1df9de7c2e182a5f76a72a5a15f2e9a34468b554eb92eb7e8022182b
4
+ data.tar.gz: 414ac43f94cee9591533b94c02d1d904cdd6a6ba0beb8694e88745b1b30dfad6
5
5
  SHA512:
6
- metadata.gz: dae2c1a9b16e8a6cc0ecca16aa60d0cc1da965a289ba618df760e6bf1a9afa5c9a0333a0c80cb5717957969b86f98506c3fcc0e33b9bc3b2ad47a1dae8a90b79
7
- data.tar.gz: 19c8eb602f862152a728350ca05386d7b9ea4951296bd62e3a07313294adc0fbdfe775c25bb04bc41995e5ea2b1ff005e001bcd76b4144a5673b9bdceb7e98a3
6
+ metadata.gz: a2dda8c58eb47a26113b553b1428d23502d0c6740808954b740c0a5cf5dac361a798f6a14f963cff9d1230256cd800295017dc07489f7c6798f50a59c4066b1c
7
+ data.tar.gz: 72d931bd9cc9fa963b2f9d0d6f6e66078eb778456097d041850013b814ec20c3b5c9661a4b68c9fa2ea56a829aaaccc657e544a8cb68863f6c9ae4dd653aeb01
data/CHANGELOG.md CHANGED
@@ -1,5 +1,52 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.84.0](https://github.com/openai/openai-ruby/compare/v0.83.0...v0.84.0) (2026-09-02)
4
+
5
+
6
+ ### Features
7
+
8
+ * **api:** update usage APIs and documentation ([#566](https://github.com/openai/openai-ruby/issues/566)) ([caac1f0](https://github.com/openai/openai-ruby/commit/caac1f0d0932dff46e25f5879f5a1410425275e9))
9
+
10
+
11
+ ### Bug Fixes
12
+
13
+ * accept custom request hash in RBS ([#554](https://github.com/openai/openai-ruby/issues/554)) ([8d32aa1](https://github.com/openai/openai-ruby/commit/8d32aa1764ecff22d2cb3114fa1a4b6f7faa309b))
14
+ * avoid dispatching nil checks during coercion ([#552](https://github.com/openai/openai-ruby/issues/552)) ([c8d6d32](https://github.com/openai/openai-ruby/commit/c8d6d32e178563e38585d3ad928136e3c6f10ac1))
15
+ * avoid duplicate late-choice stream logprobs ([#564](https://github.com/openai/openai-ruby/issues/564)) ([2359a02](https://github.com/openai/openai-ruby/commit/2359a02dd22fbf599250aaa97e755863562abf53))
16
+ * delimit JSONL request records ([#553](https://github.com/openai/openai-ruby/issues/553)) ([dea0408](https://github.com/openai/openai-ruby/commit/dea04081076da3fde34f4a9b15f445dd92556b0f))
17
+ * enforce workload identity request deadlines ([#547](https://github.com/openai/openai-ruby/issues/547)) ([da421b5](https://github.com/openai/openai-ruby/commit/da421b507c24c35f62ff8679dd5c468ce8a0dab9))
18
+ * handle custom chat completion tool calls ([#556](https://github.com/openai/openai-ruby/issues/556)) ([dd49e31](https://github.com/openai/openai-ruby/commit/dd49e31134cf8dd1442f1b3fb428bab4dbc663d1))
19
+ * load YAML for BaseModel serialization ([#555](https://github.com/openai/openai-ruby/issues/555)) ([5420931](https://github.com/openai/openai-ruby/commit/54209313bca8a7159ad8ee4768678ac61b871d69))
20
+ * make error cause contract read-only ([#557](https://github.com/openai/openai-ruby/issues/557)) ([f3169ca](https://github.com/openai/openai-ruby/commit/f3169caae9a6115c3725c8d4da33e9ffb65085c5))
21
+ * preserve formatter path boundaries ([#559](https://github.com/openai/openai-ruby/issues/559)) ([f8b128c](https://github.com/openai/openai-ruby/commit/f8b128cf0db3198a297caec7e7ca8dfa991f16e8))
22
+ * respect StringIO cursor in multipart uploads ([#558](https://github.com/openai/openai-ruby/issues/558)) ([e8ea7c7](https://github.com/openai/openai-ruby/commit/e8ea7c73e7a233abaf7c919ce10117729918df5e))
23
+ * resume Responses streams from server-side cursors ([#548](https://github.com/openai/openai-ruby/issues/548)) ([3b88261](https://github.com/openai/openai-ruby/commit/3b88261e7cd9a8736028c1867c0c23d21aba54b5))
24
+ * support flattened responses typed function tools ([#550](https://github.com/openai/openai-ruby/issues/550)) ([ab16d33](https://github.com/openai/openai-ruby/commit/ab16d33e18c862608125cf12fac07e72702b8752))
25
+ * support structured chat output with typed function tools ([#546](https://github.com/openai/openai-ruby/issues/546)) ([087d1e2](https://github.com/openai/openai-ruby/commit/087d1e2041099342ed59be211259b81dce14392c))
26
+
27
+
28
+ ### Chores
29
+
30
+ * **deps-dev:** bump the development-dependencies group with 3 updates ([#560](https://github.com/openai/openai-ruby/issues/560)) ([f728e07](https://github.com/openai/openai-ruby/commit/f728e072a0d65e2d66b2e647d36843b76fd92606))
31
+
32
+ ## [0.83.0](https://github.com/openai/openai-ruby/compare/v0.82.0...v0.83.0) (2026-08-27)
33
+
34
+
35
+ ### Features
36
+
37
+ * **api:** add compute_units to Responses and Chat Completions usage ([#543](https://github.com/openai/openai-ruby/issues/543)) ([b128f89](https://github.com/openai/openai-ruby/commit/b128f89672416a083addeb0e2bbc337f8ddb7f9f))
38
+ * expose opt-in raw response bodies on response metadata ([#544](https://github.com/openai/openai-ruby/issues/544)) ([1529e96](https://github.com/openai/openai-ruby/commit/1529e96781c217828915ed3f69faa61bf6605129))
39
+
40
+
41
+ ### Bug Fixes
42
+
43
+ * **realtime:** clean up interrupted WebRTC call allocations ([#534](https://github.com/openai/openai-ruby/issues/534)) ([2057039](https://github.com/openai/openai-ruby/commit/205703935bafe5e78b90b5424cc43d3b7b978772))
44
+
45
+
46
+ ### Chores
47
+
48
+ * **deps:** bump the codeql group with 2 updates ([#541](https://github.com/openai/openai-ruby/issues/541)) ([b15451c](https://github.com/openai/openai-ruby/commit/b15451c0df8ba890f136c86dc7f5299876bb9b5d))
49
+
3
50
  ## [0.82.0](https://github.com/openai/openai-ruby/compare/v0.81.0...v0.82.0) (2026-08-27)
4
51
 
5
52
 
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.82.0"
18
+ gem "openai", "~> 0.84.0"
19
19
  ```
20
20
 
21
21
  <!-- x-release-please-end -->
@@ -15,6 +15,12 @@ module OpenAI
15
15
  DEFAULT_REFRESH_BUFFER_SECONDS = 1200
16
16
  MAX_REJECTED_TOKEN_REFRESH_ATTEMPTS = 3
17
17
  private_constant :MAX_REJECTED_TOKEN_REFRESH_ATTEMPTS
18
+ # Provider implementations wrap StandardError, but scheduler deadlines
19
+ # must cross that boundary before normalization to Timeout::Error.
20
+ # rubocop:disable Lint/InheritException
21
+ PROVIDER_DEADLINE_ERROR = Class.new(Exception)
22
+ # rubocop:enable Lint/InheritException
23
+ private_constant :PROVIDER_DEADLINE_ERROR
18
24
 
19
25
  def initialize(
20
26
  config,
@@ -274,10 +280,19 @@ module OpenAI
274
280
  private def fetch_token_from_exchange(deadline:)
275
281
  return @token_exchange.fetch(deadline: deadline) unless @token_exchange.nil?
276
282
 
277
- subject_token = @config.provider.get_token
283
+ message = "request timed out during workload identity authentication"
284
+ subject_token, token_type = begin
285
+ timeout = remaining_timeout(deadline)
286
+ Timeout.timeout(timeout, PROVIDER_DEADLINE_ERROR, message) do
287
+ [@config.provider.get_token, @config.provider.token_type]
288
+ end
289
+
290
+ rescue PROVIDER_DEADLINE_ERROR
291
+ raise Timeout::Error, message
292
+ end
293
+
278
294
  check_deadline!(deadline)
279
295
 
280
- token_type = @config.provider.token_type
281
296
  subject_token_type = SUBJECT_TOKEN_TYPES.fetch(token_type) do
282
297
  raise(
283
298
  ArgumentError,
data/lib/openai/client.rb CHANGED
@@ -18,6 +18,8 @@ module OpenAI
18
18
  DEFAULT_MAX_RETRY_DELAY = 8.0
19
19
 
20
20
  WORKLOAD_IDENTITY_API_KEY_PLACEHOLDER = "workload-identity-auth"
21
+ WORKLOAD_IDENTITY_DEADLINE_ERROR = Class.new(Timeout::Error)
22
+ private_constant :WORKLOAD_IDENTITY_DEADLINE_ERROR
21
23
 
22
24
  # @return [String, nil]
23
25
  attr_reader :api_key
@@ -248,9 +250,9 @@ module OpenAI
248
250
 
249
251
  # @api private
250
252
  private def validate_retry_delay!(request, delay:)
251
- deadline = request[:x509_request_context]&.fetch(:deadline)
253
+ deadline = request[:x509_request_context]&.fetch(:deadline) || request[:workload_identity_deadline]
252
254
  if deadline && delay >= deadline - OpenAI::Internal::Util.monotonic_secs
253
- raise Timeout::Error, "request timed out during workload identity authentication"
255
+ raise WORKLOAD_IDENTITY_DEADLINE_ERROR, "request timed out during workload identity authentication"
254
256
  end
255
257
 
256
258
  super
@@ -281,7 +283,8 @@ module OpenAI
281
283
  request[:workload_identity_deadline] ||
282
284
  request[:timeout]&.then { OpenAI::Internal::Util.monotonic_secs + _1 }
283
285
  else
284
- request[:timeout]&.then { OpenAI::Internal::Util.monotonic_secs + _1 }
286
+ request[:workload_identity_deadline] ||
287
+ request[:timeout]&.then { OpenAI::Internal::Util.monotonic_secs + _1 }
285
288
  end
286
289
 
287
290
  request = request.merge(workload_identity_deadline: deadline)
@@ -359,8 +362,16 @@ module OpenAI
359
362
  end
360
363
  end
361
364
 
362
- rescue Timeout::Error
363
- raise unless x509_request
365
+ rescue Timeout::Error => error
366
+ unless x509_request
367
+ raise unless error.is_a?(WORKLOAD_IDENTITY_DEADLINE_ERROR)
368
+
369
+ url = request.fetch(:url).dup
370
+ url.query = nil
371
+ url.fragment = nil
372
+ cause = Timeout::Error.new(error.message)
373
+ raise OpenAI::Errors::APITimeoutError.new(url: url, message: error.message), cause: cause
374
+ end
364
375
 
365
376
  url = request.fetch(:url).dup
366
377
  url.query = nil
@@ -443,7 +454,10 @@ module OpenAI
443
454
  rescue Timeout::Error => e
444
455
  raise if x509_transport?(@requester)
445
456
 
446
- raise OpenAI::Errors::APITimeoutError.new(url: request.fetch(:url), message: e.message)
457
+ url = request.fetch(:url).dup
458
+ url.query = nil
459
+ url.fragment = nil
460
+ raise OpenAI::Errors::APITimeoutError.new(url: url, message: e.message)
447
461
  end
448
462
 
449
463
  # @api private
data/lib/openai/errors.rb CHANGED
@@ -3,7 +3,7 @@
3
3
  module OpenAI
4
4
  module Errors
5
5
  class Error < StandardError
6
- # @!attribute cause
6
+ # @!attribute [r] cause
7
7
  #
8
8
  # @return [StandardError, nil]
9
9
  end
@@ -5,6 +5,113 @@ module OpenAI
5
5
  module Realtime
6
6
  # Realtime request integration kept outside the generated client implementation.
7
7
  module ClientExtension
8
+ MAX_CALL_CLEANUP_SECONDS = 5.0
9
+ private_constant :MAX_CALL_CLEANUP_SECONDS
10
+
11
+ # A generated WebRTC allocation belongs to the SDK until its complete raw
12
+ # response can be returned. Keep this lifecycle outside generated resources.
13
+ def request(req)
14
+ unless req.is_a?(Hash) &&
15
+ req[:method] == :post &&
16
+ req[:path] == "realtime/calls" &&
17
+ req[:model] == OpenAI::HTTPClient::Response
18
+ return super
19
+ end
20
+
21
+ delivered = false
22
+ allocated_response = nil
23
+ call_id = nil
24
+ begin
25
+ url, response, log_context = perform_request(req) do |received_response, request_url, response_url|
26
+ if received_response.status == 201
27
+ allocated_response = received_response
28
+ if response_url.path == request_url.path &&
29
+ realtime_call_origin(response_url).casecmp?(realtime_call_origin(request_url))
30
+ call_id = realtime_call_id_from_location(
31
+ received_response.headers["location"],
32
+ request_url: request_url
33
+ )
34
+ end
35
+ end
36
+ end
37
+
38
+ result = finish_request(log_context, response) do
39
+ parse_response(req, url: url, response: response)
40
+ end
41
+
42
+ delivered = true
43
+ result
44
+ ensure
45
+ unless delivered
46
+ begin
47
+ OpenAI::Internal::Util.close_fused!(allocated_response.body) if allocated_response
48
+ rescue StandardError, *(defined?(::Async::Stop) ? [::Async::Stop] : [])
49
+ nil
50
+ ensure
51
+ cleanup_created_realtime_call(call_id, options: req[:options]) if call_id
52
+ end
53
+ end
54
+ end
55
+ end
56
+
57
+ private def realtime_call_id_from_location(location, request_url:)
58
+ return if location.nil? || location.empty?
59
+
60
+ call_url = URI.join(request_url.to_s, location)
61
+ unless call_url.is_a?(URI::HTTP) &&
62
+ !call_url.host.to_s.empty? &&
63
+ call_url.userinfo.nil? &&
64
+ call_url.fragment.nil?
65
+ return
66
+ end
67
+
68
+ return unless realtime_call_origin(request_url).casecmp?(realtime_call_origin(call_url))
69
+
70
+ prefix = "#{request_url.path}/"
71
+ return unless call_url.path.start_with?(prefix)
72
+
73
+ call_id = call_url.path.delete_prefix(prefix)
74
+ call_id if /\A[A-Za-z0-9_-]+\z/.match?(call_id)
75
+ rescue URI::InvalidURIError, ArgumentError
76
+ nil
77
+ end
78
+
79
+ private def realtime_call_origin(url)
80
+ origin = OpenAI::Internal::Util.uri_origin(url)
81
+ if url.host.start_with?("[") && !url.hostname.start_with?("v", "V")
82
+ origin = origin.sub(url.host, "[#{IPAddr.new(url.hostname)}]")
83
+ end
84
+
85
+ origin
86
+ end
87
+
88
+ private def cleanup_created_realtime_call(call_id, options:)
89
+ cleanup_options = options.to_h.slice(:extra_headers, :extra_query, :timeout)
90
+ timeout = cleanup_options.fetch(:timeout, @timeout)
91
+ timeout = MAX_CALL_CLEANUP_SECONDS unless timeout&.positive?
92
+ cleanup_options[:timeout] = [timeout, MAX_CALL_CLEANUP_SECONDS].min
93
+ cleanup_options[:max_retries] = 0
94
+ task = ::Async::Task.current? if defined?(::Async::Task)
95
+ cleanup = lambda do
96
+ Timeout.timeout(cleanup_options.fetch(:timeout)) do
97
+ realtime.calls.hangup(call_id, request_options: cleanup_options)
98
+ end
99
+ end
100
+
101
+ if task
102
+ if task.respond_to?(:defer_cancel)
103
+ task.defer_cancel(&cleanup)
104
+ else
105
+ task.defer_stop(&cleanup)
106
+ end
107
+ else
108
+ cleanup.call
109
+ end
110
+
111
+ rescue StandardError, *(defined?(::Async::Stop) ? [::Async::Stop] : [])
112
+ nil
113
+ end
114
+
8
115
  # Build a fully authenticated Realtime WebSocket handshake request. Realtime
9
116
  # transports use this boundary so provider authentication and request options stay
10
117
  # consistent with ordinary SDK requests.
@@ -156,6 +156,7 @@ module OpenAI
156
156
  insert_choice_snapshot!(completion_snapshot.choices, choice_snapshot)
157
157
  else
158
158
  update_existing_choice_snapshot(choice, choice_snapshot)
159
+ accumulate_logprobs!(choice.logprobs, choice_snapshot)
159
160
  end
160
161
 
161
162
  tool_calls_by_index = index_choice_snapshot!(choice_snapshot)
@@ -166,8 +167,6 @@ module OpenAI
166
167
  end
167
168
 
168
169
  parse_tool_calls!(choice.delta.tool_calls, tool_calls_by_index)
169
-
170
- accumulate_logprobs!(choice.logprobs, choice_snapshot)
171
170
  end
172
171
 
173
172
  def create_new_choice_snapshot(choice)
@@ -4,7 +4,7 @@ module OpenAI
4
4
  module Helpers
5
5
  module Streaming
6
6
  class ResponseTextDeltaEvent < OpenAI::Models::Responses::ResponseTextDeltaEvent
7
- required :snapshot, String
7
+ optional :snapshot, String, nil?: true
8
8
  end
9
9
 
10
10
  class ResponseTextDoneEvent < OpenAI::Models::Responses::ResponseTextDoneEvent
@@ -12,7 +12,7 @@ module OpenAI
12
12
  end
13
13
 
14
14
  class ResponseFunctionCallArgumentsDeltaEvent < OpenAI::Models::Responses::ResponseFunctionCallArgumentsDeltaEvent
15
- required :snapshot, String
15
+ optional :snapshot, String, nil?: true
16
16
  end
17
17
 
18
18
  class ResponseCompletedEvent < OpenAI::Models::Responses::ResponseCompletedEvent
@@ -13,7 +13,8 @@ module OpenAI
13
13
  @last_response = raw_stream.last_response
14
14
  @iterator = iterator
15
15
  @state = ResponseStreamState.new(
16
- text_format: text_format
16
+ text_format: text_format,
17
+ starting_after: starting_after
17
18
  )
18
19
  end
19
20
 
@@ -26,7 +27,7 @@ module OpenAI
26
27
  OpenAI::Internal::Util.chain_fused(@iterator) do |yielder|
27
28
  @iterator.each do |event|
28
29
  case event
29
- when OpenAI::Streaming::ResponseTextDeltaEvent
30
+ when OpenAI::Models::Responses::ResponseTextDeltaEvent
30
31
  yielder << event.delta
31
32
  end
32
33
  end
@@ -80,10 +81,11 @@ module OpenAI
80
81
  class ResponseStreamState
81
82
  attr_reader :completed_response
82
83
 
83
- def initialize(text_format:)
84
+ def initialize(text_format:, starting_after: nil)
84
85
  @current_snapshot = nil
85
86
  @completed_response = nil
86
87
  @text_format = text_format
88
+ @resumed = !starting_after.nil?
87
89
  end
88
90
 
89
91
  def handle_event(event)
@@ -98,31 +100,43 @@ module OpenAI
98
100
 
99
101
  case event
100
102
  when OpenAI::Models::Responses::ResponseTextDeltaEvent
101
- output = @current_snapshot.output[event.output_index]
102
- assert_type(output, :message)
103
+ if @current_snapshot
104
+ output = @current_snapshot.output[event.output_index]
105
+ assert_type(output, :message)
103
106
 
104
- content = output.content[event.content_index]
105
- assert_type(content, :output_text)
106
-
107
- events_to_yield <<
108
- OpenAI::Streaming::ResponseTextDeltaEvent.new(
109
- content_index: event.content_index,
110
- delta: event.delta,
111
- item_id: event.item_id,
112
- output_index: event.output_index,
113
- sequence_number: event.sequence_number,
114
- type: event.type,
115
- snapshot: content.text
116
- )
107
+ content = output.content[event.content_index]
108
+ assert_type(content, :output_text)
109
+ events_to_yield <<
110
+ OpenAI::Streaming::ResponseTextDeltaEvent.new(
111
+ content_index: event.content_index,
112
+ delta: event.delta,
113
+ item_id: event.item_id,
114
+ output_index: event.output_index,
115
+ sequence_number: event.sequence_number,
116
+ type: event.type,
117
+ snapshot: content.text
118
+ )
119
+ else
120
+ # A server-directed resumed stream may begin after response.created.
121
+ # Without the omitted prefix, a snapshot would be incomplete and
122
+ # materializing every partial prefix would make streaming quadratic.
123
+ events_to_yield <<
124
+ OpenAI::Streaming::ResponseTextDeltaEvent.new(event.to_h.merge(snapshot: nil))
125
+ end
117
126
 
118
127
  when OpenAI::Models::Responses::ResponseTextDoneEvent
119
- output = @current_snapshot.output[event.output_index]
120
- assert_type(output, :message)
128
+ text = if @current_snapshot
129
+ output = @current_snapshot.output[event.output_index]
130
+ assert_type(output, :message)
121
131
 
122
- content = output.content[event.content_index]
123
- assert_type(content, :output_text)
132
+ content = output.content[event.content_index]
133
+ assert_type(content, :output_text)
134
+ content.text
135
+ else
136
+ event.text
137
+ end
124
138
 
125
- parsed = parse_structured_text(content.text)
139
+ parsed = parse_structured_text(text)
126
140
 
127
141
  events_to_yield <<
128
142
  OpenAI::Streaming::ResponseTextDoneEvent.new(
@@ -136,18 +150,26 @@ module OpenAI
136
150
  )
137
151
 
138
152
  when OpenAI::Models::Responses::ResponseFunctionCallArgumentsDeltaEvent
139
- output = @current_snapshot.output[event.output_index]
140
- assert_type(output, :function_call)
141
-
142
- events_to_yield <<
143
- OpenAI::Streaming::ResponseFunctionCallArgumentsDeltaEvent.new(
144
- delta: event.delta,
145
- item_id: event.item_id,
146
- output_index: event.output_index,
147
- sequence_number: event.sequence_number,
148
- type: event.type,
149
- snapshot: output.arguments
150
- )
153
+ if @current_snapshot
154
+ output = @current_snapshot.output[event.output_index]
155
+ assert_type(output, :function_call)
156
+ events_to_yield <<
157
+ OpenAI::Streaming::ResponseFunctionCallArgumentsDeltaEvent.new(
158
+ delta: event.delta,
159
+ item_id: event.item_id,
160
+ output_index: event.output_index,
161
+ sequence_number: event.sequence_number,
162
+ type: event.type,
163
+ snapshot: output.arguments
164
+ )
165
+ else
166
+ # See the text-delta branch above: a partial server resume has no
167
+ # complete argument prefix from which to build a truthful snapshot.
168
+ events_to_yield <<
169
+ OpenAI::Streaming::ResponseFunctionCallArgumentsDeltaEvent.new(
170
+ event.to_h.merge(snapshot: nil)
171
+ )
172
+ end
151
173
 
152
174
  when OpenAI::Models::Responses::ResponseCompletedEvent
153
175
  events_to_yield <<
@@ -167,16 +189,25 @@ module OpenAI
167
189
 
168
190
  def accumulate_event(event:, current_snapshot:)
169
191
  if current_snapshot.nil?
170
- unless event.is_a?(OpenAI::Models::Responses::ResponseCreatedEvent)
192
+ if event.is_a?(OpenAI::Models::Responses::ResponseCreatedEvent)
193
+ # Use the converter to create a new, isolated copy of the response object.
194
+ # This ensures proper type validation and prevents shared object references.
195
+ return OpenAI::Internal::Type::Converter.coerce(
196
+ OpenAI::Models::Responses::Response,
197
+ event.response
198
+ )
199
+ end
200
+
201
+ unless @resumed
171
202
  raise "Expected first event to be response.created"
172
203
  end
173
204
 
174
- # Use the converter to create a new, isolated copy of the response object.
175
- # This ensures proper type validation and prevents shared object references.
176
- return OpenAI::Internal::Type::Converter.coerce(
177
- OpenAI::Models::Responses::Response,
178
- event.response
179
- )
205
+ if event.is_a?(OpenAI::Models::Responses::ResponseCompletedEvent)
206
+ @completed_response = event.response
207
+ return event.response
208
+ end
209
+
210
+ return nil
180
211
  end
181
212
 
182
213
  case event
@@ -28,6 +28,8 @@ module OpenAI
28
28
 
29
29
  raw[:choices]&.each do |choice|
30
30
  choice.dig(:message, :tool_calls)&.each do |tool_call|
31
+ next if tool_call[:type] == "custom"
32
+
31
33
  func = tool_call.fetch(:function)
32
34
  next if (model = tool_models[func.fetch(:name)]).nil?
33
35
 
@@ -81,6 +83,10 @@ module OpenAI
81
83
  }
82
84
  }
83
85
  parsed.dig(:response_format, :json_schema).store(:schema, model.to_json_schema)
86
+ else
87
+ end
88
+
89
+ case parsed
84
90
  in {tools: Array => tools}
85
91
  mapped = tools.map do |tool|
86
92
  case tool
@@ -109,7 +109,7 @@ module OpenAI
109
109
  parameters: tool.to_json_schema
110
110
  }
111
111
  in {type: :function, parameters: OpenAI::StructuredOutput::JsonSchemaConverter => params}
112
- func = tool.fetch(:function)
112
+ func = tool.fetch(:function, tool)
113
113
  name = func[:name] ||= params.name.split("::").last
114
114
  tool_models.store(name, params)
115
115
  func.update(parameters: params.to_json_schema)
@@ -20,11 +20,18 @@ module OpenAI
20
20
  # @return [String, nil]
21
21
  attr_reader :request_id
22
22
 
23
+ # The exact response body when `include_raw_body: true` was requested.
24
+ # Bodies are never retained by default or for streaming responses.
25
+ #
26
+ # @return [String, nil]
27
+ attr_reader :body
28
+
23
29
  # @api private
24
30
  #
25
31
  # @param status [Integer]
26
32
  # @param headers [Hash{String=>String}]
27
- def initialize(status:, headers:)
33
+ # @param body [String, nil]
34
+ def initialize(status:, headers:, body: nil)
28
35
  @status = Integer(status)
29
36
  @headers = headers
30
37
  .to_h do |name, value|
@@ -32,6 +39,7 @@ module OpenAI
32
39
  end
33
40
  .freeze
34
41
  @request_id = @headers["x-request-id"]
42
+ @body = body.nil? || body.frozen? ? body : body.dup.freeze
35
43
  freeze
36
44
  end
37
45
 
@@ -39,6 +47,37 @@ module OpenAI
39
47
  #
40
48
  # @return [String]
41
49
  def inspect = "#<#{self.class} status=#{@status} request_id=#{@request_id.inspect}>"
50
+
51
+ # Keep retained response bodies out of Psych object serialization.
52
+ #
53
+ # @api private
54
+ # @param coder [Psych::Coder]
55
+ # @return [void]
56
+ def encode_with(coder)
57
+ coder["status"] = @status
58
+ coder["headers"] = @headers
59
+ end
60
+
61
+ # @api private
62
+ # @param coder [Psych::Coder]
63
+ # @return [void]
64
+ def init_with(coder)
65
+ initialize(status: coder["status"], headers: coder["headers"])
66
+ end
67
+
68
+ # Keep retained response bodies out of Marshal object serialization.
69
+ #
70
+ # @api private
71
+ # @return [Array(Integer, Hash{String=>String})]
72
+ def marshal_dump = [@status, @headers]
73
+
74
+ # @api private
75
+ # @param values [Array(Integer, Hash{String=>String})]
76
+ # @return [void]
77
+ def marshal_load(values)
78
+ status, headers = values
79
+ initialize(status: status, headers: headers)
80
+ end
42
81
  end
43
82
 
44
83
  # Details about an API request retry that is about to run.
@@ -26,19 +26,24 @@ module OpenAI
26
26
  URL_HEADER_KEY = /(?:\A|[-_])(?:location|url|uri)\z|\A(?:link|refresh)\z/i
27
27
 
28
28
  class Context
29
- def initialize(logger:, log_level:, on_retry:, method:, url:)
29
+ def initialize(logger:, log_level:, on_retry:, method:, url:, on_response: nil)
30
30
  @logger = logger
31
31
  @log_level = log_level
32
32
  @on_retry = on_retry
33
+ @on_response = on_response
33
34
  @id = nil
34
35
  @method = method.to_s.upcase
35
36
  @url = url
37
+ @request_url = url
38
+ @first_request_url = nil
36
39
  @started_at = OpenAI::Internal::Util.monotonic_secs
37
40
  @attempt_started_at = @started_at
38
41
  @attempts = 0
39
42
  end
40
43
 
41
44
  def request_started(request, redirect_count:)
45
+ @request_url = request.url
46
+ @first_request_url ||= request.url
42
47
  @attempts += 1
43
48
  log(:debug) do
44
49
  "[openai] request started log_id=#{id} attempt=#{@attempts} " \
@@ -52,6 +57,7 @@ module OpenAI
52
57
  end
53
58
 
54
59
  def response_received(response)
60
+ @on_response&.call(response, @first_request_url || @url, @request_url)
55
61
  log(:debug) do
56
62
  "[openai] response received log_id=#{id} attempt=#{@attempts} " \
57
63
  "status=#{response.status} request_id=#{safe_field(response.headers["x-request-id"])} " \