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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +47 -0
- data/README.md +1 -1
- data/lib/openai/auth/workload_identity_auth.rb +17 -2
- data/lib/openai/client.rb +20 -6
- data/lib/openai/errors.rb +1 -1
- data/lib/openai/helpers/realtime/client_extension.rb +107 -0
- data/lib/openai/helpers/streaming/chat_completion_stream.rb +1 -2
- data/lib/openai/helpers/streaming/response_events.rb +2 -2
- data/lib/openai/helpers/streaming/response_stream.rb +73 -42
- data/lib/openai/helpers/structured_output/chat_completion_parser.rb +6 -0
- data/lib/openai/helpers/structured_output/response_parser.rb +1 -1
- data/lib/openai/http_client.rb +40 -1
- data/lib/openai/internal/logging.rb +7 -1
- data/lib/openai/internal/transport/base_client.rb +35 -4
- data/lib/openai/internal/type/base_model.rb +2 -2
- data/lib/openai/internal/type/converter.rb +1 -1
- data/lib/openai/internal/util.rb +8 -4
- data/lib/openai/models/admin/organization/audit_log_list_params.rb +2 -0
- data/lib/openai/models/admin/organization/audit_log_list_response.rb +2 -0
- data/lib/openai/models/admin/organization/usage_costs_params.rb +10 -1
- data/lib/openai/models/batch.rb +4 -4
- data/lib/openai/models/beta/beta_response.rb +9 -12
- data/lib/openai/models/beta/beta_responses_client_event.rb +8 -12
- data/lib/openai/models/beta/response_compact_params.rb +7 -7
- data/lib/openai/models/beta/response_create_params.rb +8 -12
- data/lib/openai/models/chat/completion_create_params.rb +8 -8
- data/lib/openai/models/eval_create_response.rb +1 -1
- data/lib/openai/models/eval_list_response.rb +1 -1
- data/lib/openai/models/eval_retrieve_response.rb +1 -1
- data/lib/openai/models/eval_update_response.rb +1 -1
- data/lib/openai/models/evals/create_eval_completions_run_data_source.rb +2 -2
- data/lib/openai/models/reasoning.rb +0 -2
- data/lib/openai/models/responses/response.rb +6 -7
- data/lib/openai/models/responses/response_compact_params.rb +7 -7
- data/lib/openai/models/responses/response_create_params.rb +5 -7
- data/lib/openai/models/responses/responses_client_event.rb +5 -7
- data/lib/openai/request_options.rb +8 -0
- data/lib/openai/resources/admin/organization/usage.rb +3 -1
- data/lib/openai/resources/beta/responses.rb +5 -5
- data/lib/openai/resources/chat/completions.rb +2 -2
- data/lib/openai/resources/responses.rb +7 -6
- data/lib/openai/version.rb +1 -1
- data/lib/openai.rb +1 -0
- data/rbi/openai/errors.rbi +1 -1
- data/rbi/openai/helpers/streaming/events.rbi +2 -2
- data/rbi/openai/helpers/streaming/response_stream.rbi +3 -3
- data/rbi/openai/http_client.rbi +25 -2
- data/rbi/openai/internal/logging.rbi +12 -2
- data/rbi/openai/internal/transport/base_client.rbi +12 -2
- data/rbi/openai/internal/util.rbi +3 -2
- data/rbi/openai/models/admin/organization/audit_log_list_params.rbi +8 -0
- data/rbi/openai/models/admin/organization/audit_log_list_response.rbi +8 -0
- data/rbi/openai/models/admin/organization/usage_costs_params.rbi +15 -0
- data/rbi/openai/models/batch.rbi +6 -6
- data/rbi/openai/models/beta/beta_response.rbi +10 -15
- data/rbi/openai/models/beta/beta_responses_client_event.rbi +9 -15
- data/rbi/openai/models/beta/response_compact_params.rbi +9 -9
- data/rbi/openai/models/beta/response_create_params.rbi +9 -15
- data/rbi/openai/models/chat/completion_create_params.rbi +9 -9
- data/rbi/openai/models/eval_create_response.rbi +1 -1
- data/rbi/openai/models/eval_list_response.rbi +1 -1
- data/rbi/openai/models/eval_retrieve_response.rbi +1 -1
- data/rbi/openai/models/eval_update_response.rbi +1 -1
- data/rbi/openai/models/evals/create_eval_completions_run_data_source.rbi +2 -2
- data/rbi/openai/models/reasoning.rbi +0 -2
- data/rbi/openai/models/responses/response.rbi +7 -10
- data/rbi/openai/models/responses/response_compact_params.rbi +9 -9
- data/rbi/openai/models/responses/response_create_params.rbi +6 -10
- data/rbi/openai/models/responses/responses_client_event.rbi +6 -10
- data/rbi/openai/request_options.rbi +4 -0
- data/rbi/openai/resources/admin/organization/usage.rbi +4 -0
- data/rbi/openai/resources/beta/responses.rbi +7 -11
- data/rbi/openai/resources/chat/completions.rbi +6 -6
- data/rbi/openai/resources/responses.rbi +9 -13
- data/sig/openai/errors.rbs +1 -1
- data/sig/openai/http_client.rbs +14 -1
- data/sig/openai/internal/logging.rbs +6 -1
- data/sig/openai/internal/transport/base_client.rbs +11 -20
- data/sig/openai/internal/util.rbs +3 -1
- data/sig/openai/models/admin/organization/audit_log_list_params.rbs +4 -0
- data/sig/openai/models/admin/organization/audit_log_list_response.rbs +4 -0
- data/sig/openai/models/admin/organization/usage_costs_params.rbs +7 -0
- data/sig/openai/models/beta/beta_response.rbs +2 -1
- data/sig/openai/models/responses/response.rbs +2 -1
- data/sig/openai/request_options.rbs +4 -1
- data/sig/openai/resources/admin/organization/usage.rbs +1 -0
- metadata +1 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 03f5297b1df9de7c2e182a5f76a72a5a15f2e9a34468b554eb92eb7e8022182b
|
|
4
|
+
data.tar.gz: 414ac43f94cee9591533b94c02d1d904cdd6a6ba0beb8694e88745b1b30dfad6
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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,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
|
-
|
|
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
|
|
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[:
|
|
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
|
-
|
|
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
|
-
|
|
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
|
@@ -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
|
-
|
|
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
|
-
|
|
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::
|
|
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
|
-
|
|
102
|
-
|
|
103
|
+
if @current_snapshot
|
|
104
|
+
output = @current_snapshot.output[event.output_index]
|
|
105
|
+
assert_type(output, :message)
|
|
103
106
|
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
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
|
-
|
|
120
|
-
|
|
128
|
+
text = if @current_snapshot
|
|
129
|
+
output = @current_snapshot.output[event.output_index]
|
|
130
|
+
assert_type(output, :message)
|
|
121
131
|
|
|
122
|
-
|
|
123
|
-
|
|
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(
|
|
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
|
-
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
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
|
-
|
|
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
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
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)
|
data/lib/openai/http_client.rb
CHANGED
|
@@ -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
|
-
|
|
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"])} " \
|