openai 0.86.0 → 0.87.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 +9 -0
- data/README.md +10 -1
- data/examples/audio/record_and_play.rb +19 -0
- data/examples/responses/websocket_workflows.rb +159 -0
- data/lib/openai/helpers/local_audio/errors.rb +27 -0
- data/lib/openai/helpers/local_audio/process.rb +158 -0
- data/lib/openai/helpers/local_audio.rb +157 -0
- data/lib/openai/helpers/realtime/connection.rb +20 -2
- data/lib/openai/helpers/realtime/errors.rb +18 -0
- data/lib/openai/helpers/realtime/recovery.rb +489 -0
- data/lib/openai/helpers/realtime/resources/realtime_extension.rb +26 -1
- data/lib/openai/helpers/realtime.rb +1 -0
- data/lib/openai/models/beta/beta_response_input_item.rb +84 -1
- data/lib/openai/models/beta/beta_response_item.rb +72 -1
- data/lib/openai/models/beta/beta_response_output_item.rb +84 -1
- data/lib/openai/models/beta/beta_responses_client_event.rb +3 -0
- data/lib/openai/models/beta/beta_tool.rb +69 -37
- data/lib/openai/models/beta/response_create_params.rb +3 -0
- data/lib/openai/models/conversations/conversation_item.rb +82 -1
- data/lib/openai/models/image_edit_completed_event.rb +34 -8
- data/lib/openai/models/image_edit_params.rb +64 -44
- data/lib/openai/models/image_edit_partial_image_event.rb +34 -8
- data/lib/openai/models/image_gen_completed_event.rb +34 -8
- data/lib/openai/models/image_gen_partial_image_event.rb +34 -8
- data/lib/openai/models/image_generate_params.rb +51 -35
- data/lib/openai/models/image_model.rb +4 -0
- data/lib/openai/models/images_response.rb +37 -13
- data/lib/openai/models/responses/response_create_params.rb +3 -0
- data/lib/openai/models/responses/response_input_item.rb +84 -1
- data/lib/openai/models/responses/response_item.rb +72 -1
- data/lib/openai/models/responses/response_output_item.rb +84 -1
- data/lib/openai/models/responses/responses_client_event.rb +3 -0
- data/lib/openai/models/responses/tool.rb +69 -37
- data/lib/openai/models/webhooks/live_call_incoming_webhook_event.rb +3 -3
- data/lib/openai/models/webhooks/realtime_call_incoming_webhook_event.rb +5 -3
- data/lib/openai/resources/images.rb +12 -14
- data/lib/openai/version.rb +1 -1
- data/local-audio.md +146 -0
- data/rbi/openai/helpers/local_audio/interface.rbi +110 -0
- data/rbi/openai/helpers/realtime/connection.rbi +18 -2
- data/rbi/openai/helpers/realtime/extensions.rbi +8 -0
- data/rbi/openai/helpers/realtime/recovery.rbi +27 -0
- data/rbi/openai/models/beta/beta_response_input_item.rbi +86 -1
- data/rbi/openai/models/beta/beta_response_item.rbi +78 -1
- data/rbi/openai/models/beta/beta_response_output_item.rbi +84 -1
- data/rbi/openai/models/beta/beta_responses_client_event.rbi +6 -0
- data/rbi/openai/models/beta/beta_tool.rbi +90 -51
- data/rbi/openai/models/beta/response_create_params.rbi +6 -0
- data/rbi/openai/models/conversations/conversation_item.rbi +88 -1
- data/rbi/openai/models/image_edit_completed_event.rbi +20 -9
- data/rbi/openai/models/image_edit_params.rbi +89 -60
- data/rbi/openai/models/image_edit_partial_image_event.rbi +20 -9
- data/rbi/openai/models/image_gen_completed_event.rbi +20 -9
- data/rbi/openai/models/image_gen_partial_image_event.rbi +20 -9
- data/rbi/openai/models/image_generate_params.rbi +71 -48
- data/rbi/openai/models/image_model.rbi +4 -0
- data/rbi/openai/models/images_response.rbi +27 -16
- data/rbi/openai/models/responses/response_create_params.rbi +6 -0
- data/rbi/openai/models/responses/response_input_item.rbi +88 -1
- data/rbi/openai/models/responses/response_item.rbi +80 -1
- data/rbi/openai/models/responses/response_output_item.rbi +84 -1
- data/rbi/openai/models/responses/responses_client_event.rbi +6 -0
- data/rbi/openai/models/responses/tool.rbi +90 -51
- data/rbi/openai/models/webhooks/live_call_incoming_webhook_event.rbi +4 -4
- data/rbi/openai/models/webhooks/realtime_call_incoming_webhook_event.rbi +8 -4
- data/rbi/openai/resources/beta/responses.rbi +6 -2
- data/rbi/openai/resources/images.rbi +106 -76
- data/rbi/openai/resources/responses.rbi +6 -0
- data/realtime.md +101 -0
- data/responses-websocket.md +182 -0
- data/sig/openai/helpers/local_audio/interface.rbs +62 -0
- data/sig/openai/helpers/realtime/connection.rbs +9 -1
- data/sig/openai/helpers/realtime/extensions.rbs +4 -0
- data/sig/openai/helpers/realtime/recovery.rbs +19 -0
- data/sig/openai/models/beta/beta_response_input_item.rbs +39 -2
- data/sig/openai/models/beta/beta_response_item.rbs +39 -2
- data/sig/openai/models/beta/beta_response_output_item.rbs +39 -2
- data/sig/openai/models/beta/beta_tool.rbs +11 -1
- data/sig/openai/models/conversations/conversation_item.rbs +39 -2
- data/sig/openai/models/image_edit_completed_event.rbs +11 -4
- data/sig/openai/models/image_edit_params.rbs +3 -1
- data/sig/openai/models/image_edit_partial_image_event.rbs +11 -4
- data/sig/openai/models/image_gen_completed_event.rbs +11 -4
- data/sig/openai/models/image_gen_partial_image_event.rbs +11 -4
- data/sig/openai/models/image_generate_params.rbs +4 -1
- data/sig/openai/models/image_model.rbs +8 -0
- data/sig/openai/models/images_response.rbs +10 -4
- data/sig/openai/models/responses/response_input_item.rbs +39 -2
- data/sig/openai/models/responses/response_item.rbs +39 -2
- data/sig/openai/models/responses/response_output_item.rbs +39 -2
- data/sig/openai/models/responses/tool.rbs +11 -1
- metadata +15 -1
|
@@ -0,0 +1,489 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenAI
|
|
4
|
+
module Realtime
|
|
5
|
+
# Owns successive block-scoped sockets on one Async reactor. Calls from other
|
|
6
|
+
# fibers or threads use mailboxes; the physical socket never leaves its owner.
|
|
7
|
+
# No incoming events are prefetched: a receive request drives each read.
|
|
8
|
+
#
|
|
9
|
+
# @api private
|
|
10
|
+
class Recovery
|
|
11
|
+
def self.validate_options!(max_reconnect_attempts:, max_queue_bytes:, on_reconnected:)
|
|
12
|
+
{max_reconnect_attempts: max_reconnect_attempts, max_queue_bytes: max_queue_bytes}.each do |name, value|
|
|
13
|
+
unless value.is_a?(Integer) && value >= 0
|
|
14
|
+
raise ArgumentError, "`#{name}` must be a nonnegative Integer."
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
unless on_reconnected.nil? || on_reconnected.respond_to?(:call)
|
|
19
|
+
raise ArgumentError, "`on_reconnected` must respond to `call`."
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(manager:, max_reconnect_attempts:, max_queue_bytes:, on_reconnected:)
|
|
24
|
+
@manager = manager
|
|
25
|
+
@max_attempts = max_reconnect_attempts
|
|
26
|
+
@max_queue_bytes = max_queue_bytes
|
|
27
|
+
@on_reconnected = on_reconnected
|
|
28
|
+
@mutex = Mutex.new
|
|
29
|
+
@commands = Queue.new
|
|
30
|
+
@reads = Queue.new
|
|
31
|
+
@stop_requests = Queue.new
|
|
32
|
+
@ready = Queue.new
|
|
33
|
+
@waiters = {}
|
|
34
|
+
@pending = []
|
|
35
|
+
@pending_bytes = 0
|
|
36
|
+
@state = :connecting
|
|
37
|
+
@generation = 0
|
|
38
|
+
@attempts = 0
|
|
39
|
+
@flushing = false
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def inspect = "#<#{self.class.name}>"
|
|
43
|
+
|
|
44
|
+
def open
|
|
45
|
+
load_async
|
|
46
|
+
::Kernel.Sync() do |root|
|
|
47
|
+
stopper = root.async do
|
|
48
|
+
@stop_requests.pop
|
|
49
|
+
@lifecycle&.stop
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
@lifecycle = root.async { run }
|
|
53
|
+
begin
|
|
54
|
+
kind, value = @ready.pop
|
|
55
|
+
raise value, cause: nil if kind == :error
|
|
56
|
+
|
|
57
|
+
result = yield(value)
|
|
58
|
+
raise @error, cause: nil if @error
|
|
59
|
+
|
|
60
|
+
result
|
|
61
|
+
ensure
|
|
62
|
+
$! ? abort : close
|
|
63
|
+
@lifecycle.wait
|
|
64
|
+
stopper.stop
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def closed? = @mutex.synchronize { @state == :closed }
|
|
70
|
+
|
|
71
|
+
def reconnecting?
|
|
72
|
+
@mutex.synchronize { @state == :recovering || @state == :restoring }
|
|
73
|
+
end
|
|
74
|
+
|
|
75
|
+
def pending_messages = @mutex.synchronize { @pending.dup }
|
|
76
|
+
|
|
77
|
+
def take_pending_messages
|
|
78
|
+
@mutex.synchronize do
|
|
79
|
+
raise state_error("Cannot remove queued events while flushing.") if @flushing
|
|
80
|
+
|
|
81
|
+
messages = @pending
|
|
82
|
+
@pending = []
|
|
83
|
+
@pending_bytes = 0
|
|
84
|
+
messages
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def read
|
|
89
|
+
return @physical.receive_raw if Fiber.current.equal?(@owner_fiber)
|
|
90
|
+
|
|
91
|
+
reply = @mutex.synchronize do
|
|
92
|
+
raise @error, cause: nil if @error
|
|
93
|
+
return nil if @state == :closed
|
|
94
|
+
|
|
95
|
+
register(:read).tap { @reads << _1 }
|
|
96
|
+
end
|
|
97
|
+
|
|
98
|
+
await_reply(reply)
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
def write(text)
|
|
102
|
+
return write_once(text) if Fiber.current.equal?(@owner_fiber)
|
|
103
|
+
|
|
104
|
+
reply = @mutex.synchronize do
|
|
105
|
+
raise write_error(text), cause: nil if @error || [:closed, :ending].include?(@state)
|
|
106
|
+
|
|
107
|
+
if @state != :open || @flushing
|
|
108
|
+
if @max_queue_bytes.zero? || @pending_bytes + [text.bytesize, 1].max > @max_queue_bytes
|
|
109
|
+
raise OpenAI::Errors::RealtimeQueueFullError
|
|
110
|
+
end
|
|
111
|
+
|
|
112
|
+
@pending << text.dup.freeze
|
|
113
|
+
@pending_bytes += [text.bytesize, 1].max
|
|
114
|
+
return nil
|
|
115
|
+
end
|
|
116
|
+
|
|
117
|
+
encoded = text.dup.freeze
|
|
118
|
+
register(:write, encoded).tap { @commands << [:write, encoded, _1, @generation] }
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
await_reply(reply)
|
|
122
|
+
nil
|
|
123
|
+
end
|
|
124
|
+
|
|
125
|
+
def flush_pending
|
|
126
|
+
if Fiber.current.equal?(@owner_fiber)
|
|
127
|
+
raise state_error("Queue flush interrupted; remaining events were not sent.") unless flush
|
|
128
|
+
return nil
|
|
129
|
+
end
|
|
130
|
+
|
|
131
|
+
reply = @mutex.synchronize do
|
|
132
|
+
raise @error, cause: nil if @error
|
|
133
|
+
raise state_error("Cannot flush until the replacement connection is ready.") unless @state == :open
|
|
134
|
+
|
|
135
|
+
register(:flush).tap { @commands << [:flush, nil, _1, @generation] }
|
|
136
|
+
end
|
|
137
|
+
|
|
138
|
+
await_reply(reply)
|
|
139
|
+
nil
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
# Cancel outstanding work and make a bounded attempt to send the close frame.
|
|
143
|
+
def close(code: 1000, reason: "")
|
|
144
|
+
stop({code: code, reason: reason})
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
def abort = stop(nil)
|
|
148
|
+
|
|
149
|
+
private def stop(close_options)
|
|
150
|
+
changed = @mutex.synchronize do
|
|
151
|
+
next false if @state == :closed
|
|
152
|
+
if @inflight
|
|
153
|
+
fail_locked(
|
|
154
|
+
"Realtime connection closed during a send; delivery is uncertain.",
|
|
155
|
+
uncertain_message: @inflight
|
|
156
|
+
)
|
|
157
|
+
else
|
|
158
|
+
@close_options = close_options
|
|
159
|
+
finish_locked
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
true
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
@stop_requests << true if changed
|
|
166
|
+
nil
|
|
167
|
+
end
|
|
168
|
+
|
|
169
|
+
private def register(kind, text = nil)
|
|
170
|
+
Queue.new.tap { @waiters[_1] = [kind, text] }
|
|
171
|
+
end
|
|
172
|
+
|
|
173
|
+
private def load_async
|
|
174
|
+
require "async"
|
|
175
|
+
|
|
176
|
+
rescue LoadError => e
|
|
177
|
+
raise(
|
|
178
|
+
LoadError.new(
|
|
179
|
+
"Realtime reconnect requires Async. Add `gem \"async\"` (or `gem \"async-websocket\"`) to your Gemfile."
|
|
180
|
+
),
|
|
181
|
+
cause: e
|
|
182
|
+
)
|
|
183
|
+
end
|
|
184
|
+
|
|
185
|
+
private def await_reply(reply)
|
|
186
|
+
kind, value = reply.pop
|
|
187
|
+
raise value, cause: nil if kind == :error
|
|
188
|
+
|
|
189
|
+
value
|
|
190
|
+
ensure
|
|
191
|
+
# Cancellation must not leave a read consuming an event for an abandoned
|
|
192
|
+
# caller, or a write happening later without an observable outcome.
|
|
193
|
+
abort if @mutex.synchronize { @waiters.key?(reply) }
|
|
194
|
+
end
|
|
195
|
+
|
|
196
|
+
private def respond(reply, value = nil, error: nil)
|
|
197
|
+
return unless reply
|
|
198
|
+
|
|
199
|
+
@mutex.synchronize { respond_locked(reply, value, error: error) }
|
|
200
|
+
end
|
|
201
|
+
|
|
202
|
+
private def respond_locked(reply, value = nil, error: nil)
|
|
203
|
+
if (waiter = @waiters.delete(reply))
|
|
204
|
+
failure = if error && waiter.first == :write
|
|
205
|
+
write_error(waiter.last, attempted: reply.equal?(@write_reply))
|
|
206
|
+
else
|
|
207
|
+
error
|
|
208
|
+
end
|
|
209
|
+
|
|
210
|
+
reply << [failure ? :error : :ok, failure || value]
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
private def run
|
|
215
|
+
loop do
|
|
216
|
+
break if closed?
|
|
217
|
+
|
|
218
|
+
begin
|
|
219
|
+
@manager.open { |physical| serve(physical) }
|
|
220
|
+
break
|
|
221
|
+
rescue StandardError, LoadError => e
|
|
222
|
+
break if closed?
|
|
223
|
+
if @callback_failed || e.is_a?(OpenAI::Errors::RealtimeReconnectError) || e.is_a?(LoadError)
|
|
224
|
+
finish(e)
|
|
225
|
+
break
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
unless recoverable?(e) && @attempts < @max_attempts
|
|
229
|
+
@mutex.synchronize { fail_locked("Realtime reconnect stopped; the connection could not be recovered.") }
|
|
230
|
+
break
|
|
231
|
+
end
|
|
232
|
+
|
|
233
|
+
@mutex.synchronize { @state = :recovering unless @state == :closed }
|
|
234
|
+
@attempts += 1
|
|
235
|
+
sleep(retry_delay)
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
|
|
239
|
+
ensure
|
|
240
|
+
finish
|
|
241
|
+
@ready << [:error, @error || state_error("Realtime connection closed before opening.")] unless @connection
|
|
242
|
+
end
|
|
243
|
+
|
|
244
|
+
private def serve(physical)
|
|
245
|
+
@owner_fiber = Fiber.current
|
|
246
|
+
@physical = physical
|
|
247
|
+
@mutex.synchronize do
|
|
248
|
+
return if @state == :closed
|
|
249
|
+
|
|
250
|
+
@generation += 1
|
|
251
|
+
@state = @connection ? :restoring : :open
|
|
252
|
+
end
|
|
253
|
+
|
|
254
|
+
if @connection
|
|
255
|
+
begin
|
|
256
|
+
@on_reconnected&.call(@connection)
|
|
257
|
+
rescue StandardError, LoadError
|
|
258
|
+
@callback_failed = true
|
|
259
|
+
raise
|
|
260
|
+
end
|
|
261
|
+
else
|
|
262
|
+
@connection = OpenAI::Realtime::Connection.new(socket: self, url: physical.url, recovery: self)
|
|
263
|
+
@ready << [:ok, @connection]
|
|
264
|
+
end
|
|
265
|
+
|
|
266
|
+
return if closed?
|
|
267
|
+
|
|
268
|
+
@mutex.synchronize { @state = :open unless @state == :closed }
|
|
269
|
+
reader = ::Async::Task.current.async { read_loop }
|
|
270
|
+
until closed?
|
|
271
|
+
kind, data, reply, generation = @commands.pop
|
|
272
|
+
case kind
|
|
273
|
+
when :lost
|
|
274
|
+
raise data
|
|
275
|
+
when :end
|
|
276
|
+
break
|
|
277
|
+
else
|
|
278
|
+
writable = @mutex.synchronize { @state == :open && generation == @generation }
|
|
279
|
+
unless writable
|
|
280
|
+
respond(reply, error: state_error("Connection interrupted before sending; event was not sent."))
|
|
281
|
+
next
|
|
282
|
+
end
|
|
283
|
+
|
|
284
|
+
if kind == :write
|
|
285
|
+
write_once(data, reply: reply)
|
|
286
|
+
elsif flush
|
|
287
|
+
respond(reply)
|
|
288
|
+
else
|
|
289
|
+
respond(reply, error: state_error("Queue flush interrupted; remaining events were not sent."))
|
|
290
|
+
end
|
|
291
|
+
end
|
|
292
|
+
end
|
|
293
|
+
|
|
294
|
+
ensure
|
|
295
|
+
begin
|
|
296
|
+
reader&.stop
|
|
297
|
+
ensure
|
|
298
|
+
begin
|
|
299
|
+
if @close_options
|
|
300
|
+
::Async::Task.current.with_timeout(1) { physical.close(**@close_options) }
|
|
301
|
+
end
|
|
302
|
+
|
|
303
|
+
rescue StandardError
|
|
304
|
+
# Shutdown must not mask the operation's result or prevent cleanup.
|
|
305
|
+
nil
|
|
306
|
+
ensure
|
|
307
|
+
physical.abort
|
|
308
|
+
@physical = nil
|
|
309
|
+
end
|
|
310
|
+
end
|
|
311
|
+
end
|
|
312
|
+
|
|
313
|
+
private def read_loop
|
|
314
|
+
loop do
|
|
315
|
+
@read_reply ||= @reads.pop
|
|
316
|
+
value = @physical.receive_raw
|
|
317
|
+
if value.nil?
|
|
318
|
+
@mutex.synchronize do
|
|
319
|
+
if @inflight || !@pending.empty?
|
|
320
|
+
fail_locked(
|
|
321
|
+
"Realtime connection closed with pending sends; in-flight delivery is uncertain.",
|
|
322
|
+
uncertain_message: @inflight
|
|
323
|
+
)
|
|
324
|
+
elsif @state != :closed
|
|
325
|
+
@state = :ending
|
|
326
|
+
end
|
|
327
|
+
end
|
|
328
|
+
|
|
329
|
+
if closed?
|
|
330
|
+
@stop_requests << true
|
|
331
|
+
return
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
|
|
335
|
+
respond(@read_reply, value)
|
|
336
|
+
@read_reply = nil
|
|
337
|
+
if value.nil?
|
|
338
|
+
@commands << [:end]
|
|
339
|
+
break
|
|
340
|
+
end
|
|
341
|
+
end
|
|
342
|
+
|
|
343
|
+
rescue StandardError => e
|
|
344
|
+
@mutex.synchronize do
|
|
345
|
+
if @inflight
|
|
346
|
+
fail_locked(
|
|
347
|
+
"Realtime connection interrupted during a send; delivery is uncertain.",
|
|
348
|
+
uncertain_message: @inflight
|
|
349
|
+
)
|
|
350
|
+
elsif @state != :closed
|
|
351
|
+
@state = :recovering
|
|
352
|
+
end
|
|
353
|
+
end
|
|
354
|
+
|
|
355
|
+
if closed?
|
|
356
|
+
@stop_requests << true
|
|
357
|
+
else
|
|
358
|
+
@commands << [:lost, e]
|
|
359
|
+
end
|
|
360
|
+
end
|
|
361
|
+
|
|
362
|
+
private def write_once(text, queued: false, reply: nil)
|
|
363
|
+
@mutex.synchronize do
|
|
364
|
+
raise write_error(text), cause: nil if @state == :closed
|
|
365
|
+
@inflight = text.dup.freeze
|
|
366
|
+
@write_reply = reply
|
|
367
|
+
end
|
|
368
|
+
|
|
369
|
+
begin
|
|
370
|
+
@physical.send_raw(text)
|
|
371
|
+
sent = true
|
|
372
|
+
rescue StandardError
|
|
373
|
+
error = @mutex.synchronize do
|
|
374
|
+
global_error = fail_locked(
|
|
375
|
+
"Realtime send failed; delivery is uncertain and the event was not retried.",
|
|
376
|
+
uncertain_message: text
|
|
377
|
+
)
|
|
378
|
+
queued ? global_error : write_error(text, attempted: true)
|
|
379
|
+
end
|
|
380
|
+
|
|
381
|
+
raise error, cause: nil
|
|
382
|
+
ensure
|
|
383
|
+
@mutex.synchronize do
|
|
384
|
+
respond_locked(reply) if sent && reply
|
|
385
|
+
@inflight = nil
|
|
386
|
+
@write_reply = nil
|
|
387
|
+
if queued && @pending.first.equal?(text)
|
|
388
|
+
@pending.shift
|
|
389
|
+
@pending_bytes -= [text.bytesize, 1].max
|
|
390
|
+
end
|
|
391
|
+
end
|
|
392
|
+
end
|
|
393
|
+
end
|
|
394
|
+
|
|
395
|
+
private def flush
|
|
396
|
+
@mutex.synchronize { @flushing = true }
|
|
397
|
+
loop do
|
|
398
|
+
text = @mutex.synchronize do
|
|
399
|
+
return false unless [:open, :restoring].include?(@state)
|
|
400
|
+
@pending.first
|
|
401
|
+
end
|
|
402
|
+
|
|
403
|
+
break unless text
|
|
404
|
+
|
|
405
|
+
write_once(text, queued: true)
|
|
406
|
+
end
|
|
407
|
+
|
|
408
|
+
true
|
|
409
|
+
ensure
|
|
410
|
+
@mutex.synchronize { @flushing = false }
|
|
411
|
+
end
|
|
412
|
+
|
|
413
|
+
private def finish(error = nil)
|
|
414
|
+
@mutex.synchronize { finish_locked(error) }
|
|
415
|
+
end
|
|
416
|
+
|
|
417
|
+
private def finish_locked(error = nil)
|
|
418
|
+
return if @state == :closed
|
|
419
|
+
|
|
420
|
+
@error = error
|
|
421
|
+
@state = :closed
|
|
422
|
+
@waiters.each do |reply, (kind, text)|
|
|
423
|
+
failure = if kind == :write
|
|
424
|
+
write_error(text, attempted: reply.equal?(@write_reply))
|
|
425
|
+
else
|
|
426
|
+
error || (kind == :read ? nil : state_error("Realtime connection closed before completing the operation."))
|
|
427
|
+
end
|
|
428
|
+
|
|
429
|
+
reply << [failure ? :error : :ok, failure]
|
|
430
|
+
end
|
|
431
|
+
|
|
432
|
+
@waiters.clear
|
|
433
|
+
@commands.clear
|
|
434
|
+
@reads.clear
|
|
435
|
+
@read_reply = nil
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
private def write_error(text, attempted: false)
|
|
439
|
+
encoded = text.dup.freeze
|
|
440
|
+
OpenAI::Errors::RealtimeReconnectError.new(
|
|
441
|
+
message: attempted ? "Realtime send interrupted; delivery is uncertain." : "Realtime send interrupted; event was not sent.",
|
|
442
|
+
unsent_messages: attempted ? [] : [encoded],
|
|
443
|
+
uncertain_message: attempted ? encoded : nil
|
|
444
|
+
)
|
|
445
|
+
end
|
|
446
|
+
|
|
447
|
+
private def fail_locked(message, uncertain_message: nil)
|
|
448
|
+
return @error if @state == :closed && @error
|
|
449
|
+
|
|
450
|
+
if @flushing && uncertain_message && @pending.first == uncertain_message
|
|
451
|
+
@pending.shift
|
|
452
|
+
@pending_bytes -= [uncertain_message.bytesize, 1].max
|
|
453
|
+
end
|
|
454
|
+
|
|
455
|
+
error = OpenAI::Errors::RealtimeReconnectError.new(
|
|
456
|
+
message: message,
|
|
457
|
+
unsent_messages: @pending,
|
|
458
|
+
uncertain_message: uncertain_message&.dup&.freeze
|
|
459
|
+
)
|
|
460
|
+
finish_locked(error)
|
|
461
|
+
error
|
|
462
|
+
end
|
|
463
|
+
|
|
464
|
+
private def state_error(message)
|
|
465
|
+
OpenAI::Errors::RealtimeReconnectError.new(message: message)
|
|
466
|
+
end
|
|
467
|
+
|
|
468
|
+
private def retry_delay = [0.5 * (2 ** [@attempts - 1, 4].min), 8.0].min * (0.75 + (rand * 0.25))
|
|
469
|
+
|
|
470
|
+
private def recoverable?(error)
|
|
471
|
+
return false unless error.is_a?(OpenAI::Errors::RealtimeConnectionError)
|
|
472
|
+
return [408, 429, 500, 502, 503, 504].include?(error.http_status) if error.http_status
|
|
473
|
+
|
|
474
|
+
cause = error.cause
|
|
475
|
+
if defined?(::Protocol::WebSocket::ClosedError) && cause.is_a?(::Protocol::WebSocket::ClosedError)
|
|
476
|
+
return [1001, 1005, 1006, 1011, 1012, 1013].include?(cause.code)
|
|
477
|
+
end
|
|
478
|
+
|
|
479
|
+
cause.is_a?(EOFError) ||
|
|
480
|
+
cause.is_a?(Errno::ECONNRESET) ||
|
|
481
|
+
cause.is_a?(Errno::ECONNREFUSED) ||
|
|
482
|
+
cause.is_a?(Errno::ETIMEDOUT) ||
|
|
483
|
+
cause.is_a?(Errno::EPIPE) ||
|
|
484
|
+
cause.is_a?(Timeout::Error) ||
|
|
485
|
+
cause.is_a?(::Async::TimeoutError)
|
|
486
|
+
end
|
|
487
|
+
end
|
|
488
|
+
end
|
|
489
|
+
end
|
|
@@ -14,9 +14,25 @@ module OpenAI
|
|
|
14
14
|
request_options: nil,
|
|
15
15
|
transport: nil,
|
|
16
16
|
transport_options: {},
|
|
17
|
+
reconnect: false,
|
|
18
|
+
max_reconnect_attempts: 5,
|
|
19
|
+
max_queue_bytes: 0,
|
|
20
|
+
on_reconnected: nil,
|
|
17
21
|
&block
|
|
18
22
|
)
|
|
19
23
|
raise ArgumentError, "A block is required to open a Realtime WebSocket." unless block
|
|
24
|
+
unless reconnect == true || reconnect == false
|
|
25
|
+
raise ArgumentError, "`reconnect` must be true or false."
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
OpenAI::Realtime::Recovery.validate_options!(
|
|
29
|
+
max_reconnect_attempts: max_reconnect_attempts,
|
|
30
|
+
max_queue_bytes: max_queue_bytes,
|
|
31
|
+
on_reconnected: on_reconnected
|
|
32
|
+
)
|
|
33
|
+
if !reconnect && (max_queue_bytes.positive? || on_reconnected)
|
|
34
|
+
raise ArgumentError, "Queueing and recovery callbacks require `reconnect: true`."
|
|
35
|
+
end
|
|
20
36
|
|
|
21
37
|
manager = OpenAI::Realtime::ConnectionManager.new(
|
|
22
38
|
client: @client,
|
|
@@ -26,7 +42,16 @@ module OpenAI
|
|
|
26
42
|
request_options: request_options,
|
|
27
43
|
transport_options: transport_options
|
|
28
44
|
)
|
|
29
|
-
manager.open(&block)
|
|
45
|
+
return manager.open(&block) unless reconnect
|
|
46
|
+
|
|
47
|
+
OpenAI::Realtime::Recovery
|
|
48
|
+
.new(
|
|
49
|
+
manager: manager,
|
|
50
|
+
max_reconnect_attempts: max_reconnect_attempts,
|
|
51
|
+
max_queue_bytes: max_queue_bytes,
|
|
52
|
+
on_reconnected: on_reconnected
|
|
53
|
+
)
|
|
54
|
+
.open(&block)
|
|
30
55
|
end
|
|
31
56
|
|
|
32
57
|
# Attach a server-side control WebSocket to an existing WebRTC or SIP call.
|
|
@@ -10,6 +10,7 @@ require_relative "realtime/connection_resources"
|
|
|
10
10
|
require_relative "realtime/connection"
|
|
11
11
|
require_relative "realtime/sideband_connection"
|
|
12
12
|
require_relative "realtime/connection_manager"
|
|
13
|
+
require_relative "realtime/recovery"
|
|
13
14
|
require_relative "realtime/transports/async_websocket"
|
|
14
15
|
require_relative "realtime/client_extension"
|
|
15
16
|
require_relative "realtime/resources/realtime_extension"
|
|
@@ -1224,7 +1224,30 @@ module OpenAI
|
|
|
1224
1224
|
# @return [OpenAI::Models::Beta::BetaResponseInputItem::ImageGenerationCall::Agent, nil]
|
|
1225
1225
|
optional :agent, -> { OpenAI::Beta::BetaResponseInputItem::ImageGenerationCall::Agent }, nil?: true
|
|
1226
1226
|
|
|
1227
|
-
# @!
|
|
1227
|
+
# @!attribute quality
|
|
1228
|
+
# The quality of the image generated by the image generation tool call. One of
|
|
1229
|
+
# `low`, `medium`, `high`, `xhigh`, `max`, or `auto`.
|
|
1230
|
+
#
|
|
1231
|
+
# @return [Symbol, OpenAI::Models::Beta::BetaResponseInputItem::ImageGenerationCall::Quality, nil]
|
|
1232
|
+
optional(
|
|
1233
|
+
:quality,
|
|
1234
|
+
enum: -> { OpenAI::Beta::BetaResponseInputItem::ImageGenerationCall::Quality },
|
|
1235
|
+
nil?: true
|
|
1236
|
+
)
|
|
1237
|
+
|
|
1238
|
+
# @!attribute size
|
|
1239
|
+
# The image dimensions as a `WIDTHxHEIGHT` string, for example `1536x864`.
|
|
1240
|
+
#
|
|
1241
|
+
# @return [String, Symbol, OpenAI::Models::Beta::BetaResponseInputItem::ImageGenerationCall::Size, nil]
|
|
1242
|
+
optional(
|
|
1243
|
+
:size,
|
|
1244
|
+
union: -> {
|
|
1245
|
+
OpenAI::Beta::BetaResponseInputItem::ImageGenerationCall::Size
|
|
1246
|
+
},
|
|
1247
|
+
nil?: true
|
|
1248
|
+
)
|
|
1249
|
+
|
|
1250
|
+
# @!method initialize(id:, result:, status:, agent: nil, quality: nil, size: nil, type: :image_generation_call)
|
|
1228
1251
|
# Some parameter documentations has been truncated, see
|
|
1229
1252
|
# {OpenAI::Models::Beta::BetaResponseInputItem::ImageGenerationCall} for more
|
|
1230
1253
|
# details.
|
|
@@ -1239,6 +1262,10 @@ module OpenAI
|
|
|
1239
1262
|
#
|
|
1240
1263
|
# @param agent [OpenAI::Models::Beta::BetaResponseInputItem::ImageGenerationCall::Agent, nil] The agent that produced this item.
|
|
1241
1264
|
#
|
|
1265
|
+
# @param quality [Symbol, OpenAI::Models::Beta::BetaResponseInputItem::ImageGenerationCall::Quality, nil] The quality of the image generated by the image generation tool call. One of `lo
|
|
1266
|
+
#
|
|
1267
|
+
# @param size [String, Symbol, OpenAI::Models::Beta::BetaResponseInputItem::ImageGenerationCall::Size, nil] The image dimensions as a `WIDTHxHEIGHT` string, for example `1536x864`.
|
|
1268
|
+
#
|
|
1242
1269
|
# @param type [Symbol, :image_generation_call] The type of the image generation call. Always `image_generation_call`.
|
|
1243
1270
|
|
|
1244
1271
|
# The status of the image generation call.
|
|
@@ -1269,6 +1296,62 @@ module OpenAI
|
|
|
1269
1296
|
#
|
|
1270
1297
|
# @param agent_name [String] The canonical name of the agent that produced this item.
|
|
1271
1298
|
end
|
|
1299
|
+
|
|
1300
|
+
# The quality of the image generated by the image generation tool call. One of
|
|
1301
|
+
# `low`, `medium`, `high`, `xhigh`, `max`, or `auto`.
|
|
1302
|
+
#
|
|
1303
|
+
# @see OpenAI::Models::Beta::BetaResponseInputItem::ImageGenerationCall#quality
|
|
1304
|
+
module Quality
|
|
1305
|
+
extend OpenAI::Internal::Type::Enum
|
|
1306
|
+
|
|
1307
|
+
LOW = :low
|
|
1308
|
+
MEDIUM = :medium
|
|
1309
|
+
HIGH = :high
|
|
1310
|
+
XHIGH = :xhigh
|
|
1311
|
+
MAX = :max
|
|
1312
|
+
AUTO = :auto
|
|
1313
|
+
|
|
1314
|
+
# @!method self.values
|
|
1315
|
+
# @return [Array<Symbol>]
|
|
1316
|
+
end
|
|
1317
|
+
|
|
1318
|
+
# The image dimensions as a `WIDTHxHEIGHT` string, for example `1536x864`.
|
|
1319
|
+
#
|
|
1320
|
+
# @see OpenAI::Models::Beta::BetaResponseInputItem::ImageGenerationCall#size
|
|
1321
|
+
module Size
|
|
1322
|
+
extend OpenAI::Internal::Type::Union
|
|
1323
|
+
|
|
1324
|
+
variant String
|
|
1325
|
+
|
|
1326
|
+
variant(
|
|
1327
|
+
const: -> { OpenAI::Models::Beta::BetaResponseInputItem::ImageGenerationCall::Size::SIZE_1024X1024 }
|
|
1328
|
+
)
|
|
1329
|
+
|
|
1330
|
+
variant(
|
|
1331
|
+
const: -> { OpenAI::Models::Beta::BetaResponseInputItem::ImageGenerationCall::Size::SIZE_1024X1536 }
|
|
1332
|
+
)
|
|
1333
|
+
|
|
1334
|
+
variant(
|
|
1335
|
+
const: -> { OpenAI::Models::Beta::BetaResponseInputItem::ImageGenerationCall::Size::SIZE_1536X1024 }
|
|
1336
|
+
)
|
|
1337
|
+
|
|
1338
|
+
# @!method self.variants
|
|
1339
|
+
# @return [Array(String, Symbol)]
|
|
1340
|
+
|
|
1341
|
+
define_sorbet_constant!(:Variants) do
|
|
1342
|
+
T.type_alias {
|
|
1343
|
+
T.any(String, OpenAI::Beta::BetaResponseInputItem::ImageGenerationCall::Size::TaggedSymbol)
|
|
1344
|
+
}
|
|
1345
|
+
end
|
|
1346
|
+
|
|
1347
|
+
# @!group
|
|
1348
|
+
|
|
1349
|
+
SIZE_1024X1024 = :"1024x1024"
|
|
1350
|
+
SIZE_1024X1536 = :"1024x1536"
|
|
1351
|
+
SIZE_1536X1024 = :"1536x1024"
|
|
1352
|
+
|
|
1353
|
+
# @!endgroup
|
|
1354
|
+
end
|
|
1272
1355
|
end
|
|
1273
1356
|
|
|
1274
1357
|
class LocalShellCall < OpenAI::Internal::Type::BaseModel
|