openai 0.106.0 → 0.107.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 +51 -0
- data/README.md +1 -1
- data/lib/openai/helpers/live/websocket.rb +12 -141
- data/lib/openai/helpers/local_audio/process.rb +13 -3
- data/lib/openai/helpers/models/embedding.rb +42 -0
- data/lib/openai/helpers/realtime/connection.rb +22 -5
- data/lib/openai/helpers/realtime/recovery.rb +11 -5
- data/lib/openai/helpers/realtime/translation.rb +37 -169
- data/lib/openai/helpers/realtime/unknown_server_event.rb +5 -12
- data/lib/openai/helpers/responses_websocket/connection.rb +56 -35
- data/lib/openai/helpers/responses_websocket/unknown_server_event.rb +2 -14
- data/lib/openai/helpers/websocket/async_websocket_transport.rb +8 -6
- data/lib/openai/helpers/websocket/client_request.rb +1 -1
- data/lib/openai/helpers/websocket/connection.rb +21 -10
- data/lib/openai/helpers/websocket/connection_manager.rb +2 -1
- data/lib/openai/helpers/websocket/protocol.rb +160 -0
- data/lib/openai/helpers/websocket/typed_connection.rb +22 -16
- data/lib/openai/helpers/websocket/typed_resource.rb +15 -0
- data/lib/openai/internal/transport/base_client.rb +1 -1
- data/lib/openai/models/admin/organization/usage_audio_speeches_response.rb +12 -1
- data/lib/openai/models/admin/organization/usage_audio_transcriptions_response.rb +12 -1
- data/lib/openai/models/admin/organization/usage_code_interpreter_sessions_response.rb +12 -1
- data/lib/openai/models/admin/organization/usage_completions_response.rb +12 -1
- data/lib/openai/models/admin/organization/usage_costs_response.rb +12 -1
- data/lib/openai/models/admin/organization/usage_decisions_response.rb +12 -1
- data/lib/openai/models/admin/organization/usage_embeddings_response.rb +12 -1
- data/lib/openai/models/admin/organization/usage_file_search_calls_response.rb +12 -1
- data/lib/openai/models/admin/organization/usage_images_response.rb +12 -1
- data/lib/openai/models/admin/organization/usage_moderations_response.rb +12 -1
- data/lib/openai/models/admin/organization/usage_vector_stores_response.rb +12 -1
- data/lib/openai/models/admin/organization/usage_web_search_calls_response.rb +12 -1
- data/lib/openai/models/beta/beta_response_input_item.rb +10 -1
- data/lib/openai/models/responses/response_input_item.rb +10 -1
- data/lib/openai/version.rb +1 -1
- data/lib/openai.rb +1 -0
- data/rbi/openai/client.rbi +1 -8
- data/rbi/openai/helpers/realtime/translation.rbi +0 -5
- data/rbi/openai/helpers/realtime/unknown_server_event.rbi +6 -0
- data/rbi/openai/helpers/websocket.rbi +44 -6
- data/rbi/openai/internal/transport/base_client.rbi +2 -6
- data/rbi/openai/models/admin/organization/usage_audio_speeches_response.rbi +12 -0
- data/rbi/openai/models/admin/organization/usage_audio_transcriptions_response.rbi +12 -0
- data/rbi/openai/models/admin/organization/usage_code_interpreter_sessions_response.rbi +12 -0
- data/rbi/openai/models/admin/organization/usage_completions_response.rbi +12 -0
- data/rbi/openai/models/admin/organization/usage_costs_response.rbi +12 -0
- data/rbi/openai/models/admin/organization/usage_decisions_response.rbi +12 -0
- data/rbi/openai/models/admin/organization/usage_embeddings_response.rbi +12 -0
- data/rbi/openai/models/admin/organization/usage_file_search_calls_response.rbi +12 -0
- data/rbi/openai/models/admin/organization/usage_images_response.rbi +12 -0
- data/rbi/openai/models/admin/organization/usage_moderations_response.rbi +12 -0
- data/rbi/openai/models/admin/organization/usage_vector_stores_response.rbi +12 -0
- data/rbi/openai/models/admin/organization/usage_web_search_calls_response.rbi +12 -0
- data/rbi/openai/models/beta/beta_response_input_item.rbi +14 -1
- data/rbi/openai/models/responses/response_input_item.rbi +10 -1
- data/realtime.md +22 -3
- data/sig/openai/client.rbs +1 -1
- data/sig/openai/helpers/realtime/translation.rbs +0 -2
- data/sig/openai/helpers/realtime/unknown_server_event.rbs +2 -0
- data/sig/openai/helpers/websocket.rbs +23 -6
- data/sig/openai/models/admin/organization/usage_audio_speeches_response.rbs +10 -0
- data/sig/openai/models/admin/organization/usage_audio_transcriptions_response.rbs +10 -0
- data/sig/openai/models/admin/organization/usage_code_interpreter_sessions_response.rbs +10 -0
- data/sig/openai/models/admin/organization/usage_completions_response.rbs +10 -0
- data/sig/openai/models/admin/organization/usage_costs_response.rbs +10 -0
- data/sig/openai/models/admin/organization/usage_decisions_response.rbs +10 -0
- data/sig/openai/models/admin/organization/usage_embeddings_response.rbs +10 -0
- data/sig/openai/models/admin/organization/usage_file_search_calls_response.rbs +10 -0
- data/sig/openai/models/admin/organization/usage_images_response.rbs +10 -0
- data/sig/openai/models/admin/organization/usage_moderations_response.rbs +10 -0
- data/sig/openai/models/admin/organization/usage_vector_stores_response.rbs +10 -0
- data/sig/openai/models/admin/organization/usage_web_search_calls_response.rbs +10 -0
- data/sig/openai/models/beta/beta_response_input_item.rbs +8 -0
- data/sig/openai/models/responses/response_input_item.rbs +8 -3
- metadata +3 -1
|
@@ -25,36 +25,11 @@ module OpenAI
|
|
|
25
25
|
module Realtime
|
|
26
26
|
# Future event data is accessible explicitly without leaking in diagnostics.
|
|
27
27
|
class UnknownTranslationServerEvent < OpenAI::Realtime::UnknownServerEvent
|
|
28
|
-
def inspect = "#<#{self.class} type=#{@type.inspect}>"
|
|
29
|
-
alias to_s inspect
|
|
30
|
-
|
|
31
|
-
private def freeze_json(value)
|
|
32
|
-
pending = [value]
|
|
33
|
-
visited = {}.compare_by_identity
|
|
34
|
-
until pending.empty?
|
|
35
|
-
item = pending.pop
|
|
36
|
-
next if visited.key?(item)
|
|
37
|
-
visited[item] = true
|
|
38
|
-
case item
|
|
39
|
-
when Hash
|
|
40
|
-
item.each { |key, val| pending.push(key, val) }
|
|
41
|
-
when Array
|
|
42
|
-
pending.concat(item)
|
|
43
|
-
end
|
|
44
|
-
|
|
45
|
-
item.freeze
|
|
46
|
-
end
|
|
47
|
-
|
|
48
|
-
value
|
|
49
|
-
end
|
|
50
28
|
end
|
|
51
29
|
|
|
52
30
|
# Opening does not send any events. session.close flushes remaining output;
|
|
53
31
|
# read through session.closed before leaving the block to consume it.
|
|
54
|
-
class TranslationConnection < OpenAI::WebSocket::
|
|
55
|
-
include OpenAI::WebSocket::Protocol
|
|
56
|
-
include OpenAI::WebSocket::GuardedConnection
|
|
57
|
-
|
|
32
|
+
class TranslationConnection < OpenAI::WebSocket::TypedConnection
|
|
58
33
|
# Timeout implementations before Ruby 4 raise StandardError through the
|
|
59
34
|
# scheduler. Cross transport error wrappers without changing raw callers.
|
|
60
35
|
# rubocop:disable Lint/InheritException
|
|
@@ -66,21 +41,17 @@ module OpenAI
|
|
|
66
41
|
|
|
67
42
|
# @api private
|
|
68
43
|
def initialize(socket:, url:)
|
|
69
|
-
super
|
|
44
|
+
super(
|
|
45
|
+
socket: socket,
|
|
46
|
+
url: url,
|
|
47
|
+
client_event: OpenAI::Realtime::RealtimeTranslationClientEvent,
|
|
48
|
+
server_event: OpenAI::Realtime::RealtimeTranslationServerEvent
|
|
49
|
+
)
|
|
70
50
|
@poisoned = false
|
|
71
51
|
@closed = false
|
|
72
52
|
@write_mutex = Mutex.new
|
|
73
53
|
@read_state_mutex = Mutex.new
|
|
74
54
|
@active_reads = 0
|
|
75
|
-
@server_event_types = OpenAI::Realtime::RealtimeTranslationServerEvent.variants.to_h do |variant|
|
|
76
|
-
[variant.fields.fetch(:type).fetch(:const).to_s, variant]
|
|
77
|
-
end
|
|
78
|
-
|
|
79
|
-
@client_event_names = discriminator_values(OpenAI::Realtime::RealtimeTranslationClientEvent)
|
|
80
|
-
end
|
|
81
|
-
|
|
82
|
-
def send_event(event)
|
|
83
|
-
send_raw(encode_client_event(event))
|
|
84
55
|
end
|
|
85
56
|
|
|
86
57
|
# Request a final flush and yield every trailing event, including
|
|
@@ -101,10 +72,21 @@ module OpenAI
|
|
|
101
72
|
require "timeout"
|
|
102
73
|
|
|
103
74
|
@read_state_mutex.synchronize do
|
|
75
|
+
if @finish_owner
|
|
76
|
+
raise(
|
|
77
|
+
connection_error("Translation finish must run on the existing receive owner, between reads."),
|
|
78
|
+
cause: nil
|
|
79
|
+
)
|
|
80
|
+
end
|
|
81
|
+
|
|
82
|
+
# SDK .cause accessors can expose diagnostics hidden from native rendering.
|
|
83
|
+
raise @finish_error, cause: Exception.instance_method(:cause).bind_call(@finish_error) if @finish_error
|
|
104
84
|
return @finish_terminal if @finish_terminal
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
85
|
+
if @active_reads.positive? || @finish_requested
|
|
86
|
+
raise(
|
|
87
|
+
connection_error("Translation finish must run on the existing receive owner, between reads."),
|
|
88
|
+
cause: nil
|
|
89
|
+
)
|
|
108
90
|
end
|
|
109
91
|
|
|
110
92
|
@finish_owner = Fiber.current
|
|
@@ -114,14 +96,13 @@ module OpenAI
|
|
|
114
96
|
Timeout.timeout(seconds, FinishTimeout) do
|
|
115
97
|
write_text(encode_client_event(type: "session.close"), finish: true)
|
|
116
98
|
while (event = receive)
|
|
99
|
+
terminal = event.is_a?(OpenAI::Realtime::RealtimeTranslationSessionClosedEvent)
|
|
100
|
+
@read_state_mutex.synchronize { @finish_terminal = event } if terminal
|
|
117
101
|
yield(event)
|
|
118
|
-
if
|
|
119
|
-
@read_state_mutex.synchronize { @finish_terminal = event }
|
|
120
|
-
return event
|
|
121
|
-
end
|
|
102
|
+
return event if terminal
|
|
122
103
|
end
|
|
123
104
|
|
|
124
|
-
raise connection_error("Translation socket closed before session.closed.")
|
|
105
|
+
raise connection_error("Translation socket closed before session.closed."), cause: nil
|
|
125
106
|
end
|
|
126
107
|
|
|
127
108
|
rescue FinishTimeout
|
|
@@ -139,10 +120,10 @@ module OpenAI
|
|
|
139
120
|
private def write_text(text, finish: false)
|
|
140
121
|
@write_mutex.synchronize do
|
|
141
122
|
if @finish_requested || (@finish_owner && !finish)
|
|
142
|
-
raise connection_error("Cannot send after Translation finish was requested.")
|
|
123
|
+
raise connection_error("Cannot send after Translation finish was requested."), cause: nil
|
|
143
124
|
end
|
|
144
125
|
|
|
145
|
-
raise connection_error("Cannot send on a closed Realtime Translation WebSocket.") if closed?
|
|
126
|
+
raise connection_error("Cannot send on a closed Realtime Translation WebSocket."), cause: nil if closed?
|
|
146
127
|
@finish_requested = true if finish
|
|
147
128
|
super(text)
|
|
148
129
|
end
|
|
@@ -151,7 +132,10 @@ module OpenAI
|
|
|
151
132
|
private def read_raw_message
|
|
152
133
|
@read_state_mutex.synchronize do
|
|
153
134
|
if @finish_owner && @finish_owner != Fiber.current
|
|
154
|
-
raise
|
|
135
|
+
raise(
|
|
136
|
+
connection_error("Translation finish must run on the existing receive owner, between reads."),
|
|
137
|
+
cause: nil
|
|
138
|
+
)
|
|
155
139
|
end
|
|
156
140
|
|
|
157
141
|
@active_reads += 1
|
|
@@ -164,133 +148,17 @@ module OpenAI
|
|
|
164
148
|
end
|
|
165
149
|
end
|
|
166
150
|
|
|
167
|
-
private
|
|
168
|
-
validate_client_tree!(event)
|
|
169
|
-
union = OpenAI::Realtime::RealtimeTranslationClientEvent
|
|
170
|
-
payload = OpenAI::Internal::Type::Converter.dump(union, event)
|
|
171
|
-
type = payload[:type] || payload["type"] if payload.is_a?(Hash)
|
|
172
|
-
raise ArgumentError unless (type.is_a?(String) || type.is_a?(Symbol)) && @client_event_names.key?(type.to_s)
|
|
173
|
-
|
|
174
|
-
coerced = coerce_event(union, payload, outbound: true)
|
|
175
|
-
serialized = OpenAI::Internal::Type::Converter.dump(union, coerced)
|
|
176
|
-
validate_client_tree!(serialized, json_only: true)
|
|
177
|
-
JSON.generate(serialized, max_nesting: false)
|
|
178
|
-
rescue StandardError, SystemStackError
|
|
179
|
-
raise ArgumentError, "Invalid Realtime Translation client event.", cause: nil
|
|
180
|
-
end
|
|
181
|
-
|
|
182
|
-
# A merged string-keyed JSON event and symbol-keyed override must not
|
|
183
|
-
# silently change the command or the configuration that is sent.
|
|
184
|
-
private def validate_client_tree!(event, json_only: false)
|
|
185
|
-
visited = {}.compare_by_identity
|
|
186
|
-
pending = [event]
|
|
187
|
-
until pending.empty?
|
|
188
|
-
item = pending.pop
|
|
189
|
-
next if visited.key?(item)
|
|
190
|
-
visited[item] = true
|
|
191
|
-
case item
|
|
192
|
-
when OpenAI::Internal::Type::BaseModel
|
|
193
|
-
raise ArgumentError if json_only
|
|
194
|
-
validate_model_constants!(item)
|
|
195
|
-
pending << item.to_h
|
|
196
|
-
when Hash
|
|
197
|
-
names = {}
|
|
198
|
-
item.each do |key, value|
|
|
199
|
-
raise ArgumentError unless key.is_a?(String) || key.is_a?(Symbol)
|
|
200
|
-
name = key.to_s
|
|
201
|
-
raise ArgumentError if names.key?(name)
|
|
202
|
-
names[name] = true
|
|
203
|
-
pending << value
|
|
204
|
-
end
|
|
205
|
-
|
|
206
|
-
when Array
|
|
207
|
-
pending.concat(item)
|
|
208
|
-
when NilClass, TrueClass, FalseClass, String, Integer, Float, Symbol
|
|
209
|
-
next
|
|
210
|
-
else
|
|
211
|
-
raise ArgumentError if json_only
|
|
212
|
-
end
|
|
213
|
-
end
|
|
214
|
-
end
|
|
215
|
-
|
|
216
|
-
private def validate_model_constants!(model)
|
|
217
|
-
model.to_h.each do |name, actual|
|
|
218
|
-
key = name.is_a?(String) ? name.to_sym : name
|
|
219
|
-
field = model.class.known_fields[key]
|
|
220
|
-
next unless field
|
|
221
|
-
const = field.fetch(:const)
|
|
222
|
-
next if const == OpenAI::Internal::OMIT
|
|
223
|
-
if const.is_a?(Symbol)
|
|
224
|
-
raise ArgumentError unless (actual.is_a?(String) || actual.is_a?(Symbol)) && actual.to_s == const.to_s
|
|
225
|
-
elsif actual != const
|
|
226
|
-
raise ArgumentError
|
|
227
|
-
end
|
|
228
|
-
end
|
|
229
|
-
end
|
|
151
|
+
private :parse_event
|
|
230
152
|
|
|
231
|
-
private def
|
|
232
|
-
parsed = JSON.parse(data, symbolize_names: true, max_nesting: false)
|
|
233
|
-
type = event_type(parsed, message: "Realtime Translation server event must be an object with a string type")
|
|
234
|
-
model = @server_event_types[type.to_s]
|
|
235
|
-
unless model
|
|
236
|
-
return OpenAI::Realtime::UnknownTranslationServerEvent.new(data: parsed)
|
|
237
|
-
end
|
|
153
|
+
private def coerce_client_event(payload) = coerce_event(@client_event, payload, outbound: true)
|
|
238
154
|
|
|
239
|
-
|
|
240
|
-
# key spelled like a renamed Ruby field must not overwrite typed data.
|
|
241
|
-
model.fields.each do |name, field|
|
|
242
|
-
raise ArgumentError if name != field.fetch(:api_name) && parsed.key?(name)
|
|
243
|
-
end
|
|
155
|
+
private def coerce(type, value) = coerce_event(type, value)
|
|
244
156
|
|
|
245
|
-
|
|
246
|
-
rescue StandardError, SystemStackError
|
|
247
|
-
raise OpenAI::Errors::TranslationProtocolError.new, cause: nil
|
|
248
|
-
end
|
|
157
|
+
private def client_event_error_message = "Invalid Realtime Translation client event."
|
|
249
158
|
|
|
250
|
-
private def
|
|
251
|
-
state = OpenAI::Internal::Type::Converter.new_coerce_state(request_only: outbound)
|
|
252
|
-
event = OpenAI::Internal::Type::Converter.coerce(union, payload, state: state)
|
|
253
|
-
raise ArgumentError if state[:error] || !state.fetch(:exactness).fetch(:no).zero?
|
|
254
|
-
ancestors = {}.compare_by_identity
|
|
255
|
-
pending = [[event, false]]
|
|
256
|
-
until pending.empty?
|
|
257
|
-
value, exiting = pending.pop
|
|
258
|
-
if exiting
|
|
259
|
-
ancestors.delete(value)
|
|
260
|
-
next
|
|
261
|
-
end
|
|
159
|
+
private def unknown_server_event(payload) = OpenAI::Realtime::UnknownTranslationServerEvent.new(data: payload)
|
|
262
160
|
|
|
263
|
-
|
|
264
|
-
when OpenAI::Internal::Type::BaseModel
|
|
265
|
-
data = value.to_h
|
|
266
|
-
validate_model_constants!(value)
|
|
267
|
-
value.class.fields.each do |name, field|
|
|
268
|
-
const = field.fetch(:const)
|
|
269
|
-
if field.fetch(:required) &&
|
|
270
|
-
field.fetch(:mode) != (outbound ? :coerce : :dump) &&
|
|
271
|
-
(!outbound || const == OpenAI::Internal::OMIT) &&
|
|
272
|
-
!data.key?(name)
|
|
273
|
-
raise ArgumentError
|
|
274
|
-
end
|
|
275
|
-
end
|
|
276
|
-
|
|
277
|
-
data.values
|
|
278
|
-
when Hash
|
|
279
|
-
value.values
|
|
280
|
-
when Array
|
|
281
|
-
value
|
|
282
|
-
else
|
|
283
|
-
next
|
|
284
|
-
end
|
|
285
|
-
|
|
286
|
-
raise ArgumentError if ancestors.key?(value)
|
|
287
|
-
ancestors[value] = true
|
|
288
|
-
pending << [value, true]
|
|
289
|
-
children.each { |child| pending << [child, false] }
|
|
290
|
-
end
|
|
291
|
-
|
|
292
|
-
event
|
|
293
|
-
end
|
|
161
|
+
private def protocol_error = OpenAI::Errors::TranslationProtocolError.new
|
|
294
162
|
|
|
295
163
|
private def transport_error = OpenAI::Errors::TranslationConnectionError.new(url: @url)
|
|
296
164
|
private def failed_read_message = "Cannot read from a failed Realtime Translation WebSocket."
|
|
@@ -4,6 +4,8 @@ module OpenAI
|
|
|
4
4
|
module Realtime
|
|
5
5
|
# A valid JSON event whose discriminator is newer than this SDK version.
|
|
6
6
|
class UnknownServerEvent
|
|
7
|
+
include OpenAI::WebSocket::Protocol
|
|
8
|
+
|
|
7
9
|
# @return [Symbol]
|
|
8
10
|
attr_reader :type
|
|
9
11
|
|
|
@@ -25,20 +27,11 @@ module OpenAI
|
|
|
25
27
|
# @return [Hash{Symbol=>Object}]
|
|
26
28
|
def to_h = @data
|
|
27
29
|
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
when Hash
|
|
31
|
-
value.each do |key, item|
|
|
32
|
-
freeze_json(key)
|
|
33
|
-
freeze_json(item)
|
|
34
|
-
end
|
|
30
|
+
# Remote values, including the discriminator, require explicit access.
|
|
31
|
+
def inspect = "#<#{self.class}>"
|
|
35
32
|
|
|
36
|
-
|
|
37
|
-
value.each { |item| freeze_json(item) }
|
|
38
|
-
end
|
|
33
|
+
alias to_s inspect
|
|
39
34
|
|
|
40
|
-
value.freeze
|
|
41
|
-
end
|
|
42
35
|
end
|
|
43
36
|
end
|
|
44
37
|
end
|
|
@@ -51,7 +51,13 @@ module OpenAI
|
|
|
51
51
|
assert_owner!
|
|
52
52
|
raise connection_error("Cannot send on a closed Responses WebSocket.") if poisoned? || closed?
|
|
53
53
|
|
|
54
|
-
|
|
54
|
+
encoded = begin
|
|
55
|
+
generate_json(encode_client_event(event))
|
|
56
|
+
rescue SystemStackError
|
|
57
|
+
raise OpenAI::Errors::ResponsesClientEventError.new, cause: nil
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
write_encoded(encoded)
|
|
55
61
|
rescue OpenAI::Errors::ResponsesConnectionError,
|
|
56
62
|
OpenAI::Errors::ResponsesClientEventError,
|
|
57
63
|
OpenAI::Errors::ResponsesSendError
|
|
@@ -107,21 +113,22 @@ module OpenAI
|
|
|
107
113
|
def closed? = @state == :closed || super
|
|
108
114
|
|
|
109
115
|
private def parse_event(data)
|
|
110
|
-
parsed =
|
|
116
|
+
parsed = parse_json(data)
|
|
111
117
|
type = event_type(parsed, message: "Responses server event must be a JSON object")
|
|
112
118
|
unless @server_event_names.key?(type.to_s)
|
|
113
119
|
return OpenAI::Responses::UnknownServerEvent.new(data: parsed)
|
|
114
120
|
end
|
|
115
121
|
|
|
116
122
|
state = OpenAI::Internal::Type::Converter.new_coerce_state
|
|
117
|
-
OpenAI::Internal::Type::Converter.coerce(
|
|
123
|
+
event = OpenAI::Internal::Type::Converter.coerce(
|
|
118
124
|
OpenAI::Responses::ResponsesServerEvent,
|
|
119
125
|
parsed,
|
|
120
126
|
state: state
|
|
121
127
|
)
|
|
128
|
+
validate_wire_field_names!(event, parsed)
|
|
122
129
|
rescue OpenAI::Errors::ResponsesProtocolError
|
|
123
130
|
raise
|
|
124
|
-
rescue StandardError
|
|
131
|
+
rescue StandardError, SystemStackError
|
|
125
132
|
raise OpenAI::Errors::ResponsesProtocolError.new, cause: nil
|
|
126
133
|
end
|
|
127
134
|
|
|
@@ -209,34 +216,33 @@ module OpenAI
|
|
|
209
216
|
raise connection_error("Responses WebSocket connections are single-owner.")
|
|
210
217
|
end
|
|
211
218
|
|
|
212
|
-
private def validate_event_tree!(value
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
reject_duplicate_serialized_model_keys!(value.class, data)
|
|
219
|
-
validate_event_tree!(data, active)
|
|
220
|
-
ensure
|
|
219
|
+
private def validate_event_tree!(value)
|
|
220
|
+
active = {}.compare_by_identity
|
|
221
|
+
pending = [[value, false]]
|
|
222
|
+
until pending.empty?
|
|
223
|
+
value, exiting = pending.pop
|
|
224
|
+
if exiting
|
|
221
225
|
active.delete(value)
|
|
226
|
+
next
|
|
222
227
|
end
|
|
223
228
|
|
|
224
|
-
|
|
225
|
-
|
|
226
|
-
|
|
227
|
-
|
|
228
|
-
|
|
229
|
-
|
|
230
|
-
|
|
229
|
+
children = case value
|
|
230
|
+
when OpenAI::Internal::Type::BaseModel
|
|
231
|
+
data = value.to_h
|
|
232
|
+
reject_duplicate_serialized_model_keys!(value.class, data)
|
|
233
|
+
[data]
|
|
234
|
+
when Hash
|
|
235
|
+
reject_duplicate_semantic_keys!(value)
|
|
236
|
+
value.values
|
|
237
|
+
when Array
|
|
238
|
+
value
|
|
239
|
+
else
|
|
240
|
+
next
|
|
231
241
|
end
|
|
232
242
|
|
|
233
|
-
when Array
|
|
234
243
|
guard_cycle!(value, active)
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
ensure
|
|
238
|
-
active.delete(value)
|
|
239
|
-
end
|
|
244
|
+
pending << [value, true]
|
|
245
|
+
children.reverse_each { |child| pending << [child, false] }
|
|
240
246
|
end
|
|
241
247
|
|
|
242
248
|
nil
|
|
@@ -272,18 +278,33 @@ module OpenAI
|
|
|
272
278
|
end
|
|
273
279
|
|
|
274
280
|
private def normalize_event_keys(value)
|
|
275
|
-
|
|
276
|
-
|
|
277
|
-
|
|
281
|
+
return value unless value.is_a?(Hash) || value.is_a?(Array)
|
|
282
|
+
|
|
283
|
+
result = value.is_a?(Hash) ? {} : []
|
|
284
|
+
copies = {}.compare_by_identity
|
|
285
|
+
copies[value] = result
|
|
286
|
+
pending = [[value, result]]
|
|
287
|
+
until pending.empty?
|
|
288
|
+
source, destination = pending.pop
|
|
289
|
+
entries = source.is_a?(Hash) ? source.each_pair : source.each_with_index.lazy.map { |item, index|
|
|
290
|
+
[index, item]
|
|
291
|
+
}
|
|
292
|
+
entries.each do |key, item|
|
|
293
|
+
if item.is_a?(Hash) || item.is_a?(Array)
|
|
294
|
+
unless copies.key?(item)
|
|
295
|
+
copies[item] = item.is_a?(Hash) ? {} : []
|
|
296
|
+
pending << [item, copies.fetch(item)]
|
|
297
|
+
end
|
|
298
|
+
|
|
299
|
+
item = copies.fetch(item)
|
|
300
|
+
end
|
|
301
|
+
|
|
278
302
|
normalized_key = key.is_a?(String) ? key.to_sym : key
|
|
279
|
-
[normalized_key
|
|
303
|
+
destination[normalized_key] = item
|
|
280
304
|
end
|
|
281
|
-
|
|
282
|
-
when Array
|
|
283
|
-
value.map { |item| normalize_event_keys(item) }
|
|
284
|
-
else
|
|
285
|
-
value
|
|
286
305
|
end
|
|
306
|
+
|
|
307
|
+
result
|
|
287
308
|
end
|
|
288
309
|
|
|
289
310
|
private def connection_error(message)
|
|
@@ -4,6 +4,8 @@ module OpenAI
|
|
|
4
4
|
module Responses
|
|
5
5
|
# A valid JSON event whose discriminator is newer than this SDK version.
|
|
6
6
|
class UnknownServerEvent
|
|
7
|
+
include OpenAI::WebSocket::Protocol
|
|
8
|
+
|
|
7
9
|
# @return [Symbol]
|
|
8
10
|
attr_reader :type
|
|
9
11
|
|
|
@@ -35,20 +37,6 @@ module OpenAI
|
|
|
35
37
|
|
|
36
38
|
alias to_s inspect
|
|
37
39
|
|
|
38
|
-
private def freeze_json(value)
|
|
39
|
-
case value
|
|
40
|
-
when Hash
|
|
41
|
-
value.each do |key, item|
|
|
42
|
-
freeze_json(key)
|
|
43
|
-
freeze_json(item)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
when Array
|
|
47
|
-
value.each { |item| freeze_json(item) }
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
value.freeze
|
|
51
|
-
end
|
|
52
40
|
end
|
|
53
41
|
end
|
|
54
42
|
end
|
|
@@ -171,7 +171,9 @@ module OpenAI
|
|
|
171
171
|
@connection.read
|
|
172
172
|
rescue StandardError => e
|
|
173
173
|
error = @error_factory.call(url: @url, cause: e)
|
|
174
|
-
|
|
174
|
+
# The factory retains explicit diagnostics; do not render them as a
|
|
175
|
+
# native exception chain, which can include payloads or credentials.
|
|
176
|
+
raise error, cause: nil
|
|
175
177
|
end
|
|
176
178
|
|
|
177
179
|
def write(message)
|
|
@@ -179,14 +181,14 @@ module OpenAI
|
|
|
179
181
|
@connection.flush
|
|
180
182
|
rescue StandardError => e
|
|
181
183
|
error = @error_factory.call(url: @url, cause: e)
|
|
182
|
-
raise error, cause:
|
|
184
|
+
raise error, cause: nil
|
|
183
185
|
end
|
|
184
186
|
|
|
185
187
|
def close(code: 1000, reason: "")
|
|
186
188
|
@connection.close(code, reason)
|
|
187
189
|
rescue StandardError => e
|
|
188
190
|
error = @error_factory.call(url: @url, cause: e)
|
|
189
|
-
raise error, cause:
|
|
191
|
+
raise error, cause: nil
|
|
190
192
|
end
|
|
191
193
|
|
|
192
194
|
# @api private
|
|
@@ -197,7 +199,7 @@ module OpenAI
|
|
|
197
199
|
@aborted = true
|
|
198
200
|
rescue StandardError => e
|
|
199
201
|
error = @error_factory.call(url: @url, cause: e)
|
|
200
|
-
raise error, cause:
|
|
202
|
+
raise error, cause: nil
|
|
201
203
|
end
|
|
202
204
|
|
|
203
205
|
# @api private
|
|
@@ -302,7 +304,7 @@ module OpenAI
|
|
|
302
304
|
raise if e.equal?(block_error)
|
|
303
305
|
|
|
304
306
|
error = @error_factory.call(url: url, cause: e, http_status: handshake_status(e))
|
|
305
|
-
raise error, cause:
|
|
307
|
+
raise error, cause: nil
|
|
306
308
|
end
|
|
307
309
|
|
|
308
310
|
private def negotiation_options(_endpoint_options) = {}
|
|
@@ -373,7 +375,7 @@ module OpenAI
|
|
|
373
375
|
"#{@product_name} WebSockets require the `async-websocket` gem. " \
|
|
374
376
|
"Add `gem \"async-websocket\"` to your Gemfile."
|
|
375
377
|
error = @error_factory.call(url: url, message: message, cause: e)
|
|
376
|
-
raise error, cause:
|
|
378
|
+
raise error, cause: nil
|
|
377
379
|
end
|
|
378
380
|
|
|
379
381
|
private def authority(url, include_default_port: false)
|
|
@@ -140,7 +140,7 @@ module OpenAI
|
|
|
140
140
|
[request, deadline]
|
|
141
141
|
rescue Timeout::Error => e
|
|
142
142
|
error = timeout_error.call(error_url, e)
|
|
143
|
-
raise error, cause:
|
|
143
|
+
raise error, cause: nil
|
|
144
144
|
end
|
|
145
145
|
|
|
146
146
|
private def with_shared_websocket_base_url(request, path:, base_url:)
|
|
@@ -28,20 +28,13 @@ module OpenAI
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
private def write_text(text)
|
|
31
|
-
complete = false
|
|
32
31
|
super
|
|
33
|
-
complete = true
|
|
34
|
-
nil
|
|
35
32
|
rescue StandardError
|
|
36
33
|
raise transport_error, cause: nil
|
|
37
|
-
ensure
|
|
38
|
-
# Also retire a possibly buffered write when cancellation bypasses
|
|
39
|
-
# StandardError, before another sender or normal cleanup can flush it.
|
|
40
|
-
@poisoned = true unless complete
|
|
41
34
|
end
|
|
42
35
|
|
|
43
36
|
private def read_raw_message
|
|
44
|
-
raise connection_error(failed_read_message) if @poisoned
|
|
37
|
+
raise connection_error(failed_read_message), cause: nil if @poisoned
|
|
45
38
|
return nil if @closed
|
|
46
39
|
text = super
|
|
47
40
|
@closed = text.nil?
|
|
@@ -73,6 +66,7 @@ module OpenAI
|
|
|
73
66
|
def initialize(socket:, url:)
|
|
74
67
|
@socket = socket
|
|
75
68
|
@url = url
|
|
69
|
+
@poisoned = false
|
|
76
70
|
end
|
|
77
71
|
|
|
78
72
|
# Yield server events until the remote peer closes the connection.
|
|
@@ -100,6 +94,8 @@ module OpenAI
|
|
|
100
94
|
def receive_raw = read_raw_message
|
|
101
95
|
|
|
102
96
|
private def read_raw_message
|
|
97
|
+
raise connection_error("Cannot read from a failed WebSocket."), cause: nil if @poisoned
|
|
98
|
+
|
|
103
99
|
message = @socket.read
|
|
104
100
|
message&.to_str
|
|
105
101
|
end
|
|
@@ -108,7 +104,7 @@ module OpenAI
|
|
|
108
104
|
#
|
|
109
105
|
# @api private
|
|
110
106
|
def send_raw(data)
|
|
111
|
-
raise connection_error(closed_send_message) if closed?
|
|
107
|
+
raise connection_error(closed_send_message), cause: nil if closed?
|
|
112
108
|
|
|
113
109
|
text = data.dup
|
|
114
110
|
text.force_encoding(Encoding::UTF_8) if text.encoding == Encoding::BINARY
|
|
@@ -120,6 +116,7 @@ module OpenAI
|
|
|
120
116
|
|
|
121
117
|
# Close the connection.
|
|
122
118
|
def close(code: 1000, reason: "")
|
|
119
|
+
return abort if @poisoned
|
|
123
120
|
return if socket_closed?
|
|
124
121
|
|
|
125
122
|
@socket.close(code: code, reason: reason)
|
|
@@ -137,7 +134,7 @@ module OpenAI
|
|
|
137
134
|
end
|
|
138
135
|
|
|
139
136
|
# @return [Boolean]
|
|
140
|
-
def closed? = socket_closed?
|
|
137
|
+
def closed? = @poisoned || socket_closed?
|
|
141
138
|
|
|
142
139
|
private def parse_event(_data)
|
|
143
140
|
raise NotImplementedError
|
|
@@ -154,8 +151,22 @@ module OpenAI
|
|
|
154
151
|
private def socket_closed? = @socket.closed?
|
|
155
152
|
|
|
156
153
|
private def write_text(text)
|
|
154
|
+
complete = false
|
|
157
155
|
@socket.write(text)
|
|
156
|
+
complete = true
|
|
158
157
|
nil
|
|
158
|
+
ensure
|
|
159
|
+
unless complete
|
|
160
|
+
# Retire the physical socket immediately: an already-running reader
|
|
161
|
+
# can otherwise flush buffered application bytes while replying to close.
|
|
162
|
+
@poisoned = true
|
|
163
|
+
begin
|
|
164
|
+
@socket.abort
|
|
165
|
+
rescue StandardError
|
|
166
|
+
# Preserve the original write failure or cancellation.
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
end
|
|
159
170
|
end
|
|
160
171
|
end
|
|
161
172
|
end
|
|
@@ -73,7 +73,8 @@ module OpenAI
|
|
|
73
73
|
|
|
74
74
|
rescue StandardError => error
|
|
75
75
|
raise if @transport_error_factory.nil?
|
|
76
|
-
|
|
76
|
+
# Preserve the native chain, not an SDK accessor's explicit diagnostics.
|
|
77
|
+
raise block_error, cause: Exception.instance_method(:cause).bind_call(block_error) if block_error
|
|
77
78
|
raise @transport_error_factory.call(url: request.fetch(:url), error: error), cause: nil
|
|
78
79
|
end
|
|
79
80
|
end
|