openai 0.106.0 → 0.108.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 +74 -0
- data/README.md +49 -1
- data/examples/chat/tool_runner.rb +33 -0
- data/lib/openai/client.rb +2 -2
- data/lib/openai/helpers/chat/tool_runner.rb +259 -0
- 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 +19 -7
- 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/incremental.rb +5 -1
- data/lib/openai/helpers/responses_websocket/session.rb +11 -4
- data/lib/openai/helpers/responses_websocket/unknown_server_event.rb +2 -14
- data/lib/openai/helpers/streaming/chat_completion_resource.rb +52 -0
- 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 +16 -5
- data/lib/openai/models/admin/organization/usage_audio_transcriptions_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_code_interpreter_sessions_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_completions_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_costs_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_decisions_response.rb +14 -3
- data/lib/openai/models/admin/organization/usage_embeddings_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_file_search_calls_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_images_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_moderations_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_vector_stores_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_web_search_calls_response.rb +16 -5
- data/lib/openai/models/audio/voice_consent.rb +58 -0
- data/lib/openai/models/audio/voice_consent_create_params.rb +57 -0
- data/lib/openai/models/audio/voice_consent_delete_params.rb +22 -0
- data/lib/openai/models/audio/voice_consent_deleted.rb +34 -0
- data/lib/openai/models/audio/voice_consent_list_params.rb +42 -0
- data/lib/openai/models/audio/voice_consent_retrieve_params.rb +22 -0
- data/lib/openai/models/audio/voice_consent_update_params.rb +32 -0
- 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/resources/audio/voice_consents.rb +163 -0
- data/lib/openai/resources/audio.rb +5 -0
- data/lib/openai/version.rb +1 -1
- data/lib/openai.rb +9 -0
- data/rbi/openai/client.rbi +1 -8
- data/rbi/openai/helpers/chat/tool_runner.rbi +74 -0
- 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 +14 -2
- data/rbi/openai/models/admin/organization/usage_audio_transcriptions_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_code_interpreter_sessions_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_completions_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_costs_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_decisions_response.rbi +12 -0
- data/rbi/openai/models/admin/organization/usage_embeddings_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_file_search_calls_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_images_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_moderations_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_vector_stores_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_web_search_calls_response.rbi +14 -2
- data/rbi/openai/models/audio/voice_consent.rbi +86 -0
- data/rbi/openai/models/audio/voice_consent_create_params.rbi +92 -0
- data/rbi/openai/models/audio/voice_consent_delete_params.rbi +53 -0
- data/rbi/openai/models/audio/voice_consent_deleted.rbi +62 -0
- data/rbi/openai/models/audio/voice_consent_list_params.rbi +78 -0
- data/rbi/openai/models/audio/voice_consent_retrieve_params.rbi +53 -0
- data/rbi/openai/models/audio/voice_consent_update_params.rbi +62 -0
- data/rbi/openai/models/beta/beta_response_input_item.rbi +14 -1
- data/rbi/openai/models/response_format_json_schema.rbi +13 -11
- data/rbi/openai/models/responses/response_input_item.rbi +10 -1
- data/rbi/openai/resources/audio/voice_consents.rbi +108 -0
- data/rbi/openai/resources/audio.rbi +4 -0
- data/realtime.md +22 -3
- data/sig/openai/client.rbs +1 -1
- data/sig/openai/helpers/chat/tool_runner.rbs +48 -0
- 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 +15 -5
- data/sig/openai/models/admin/organization/usage_audio_transcriptions_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_code_interpreter_sessions_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_completions_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_costs_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_decisions_response.rbs +11 -1
- data/sig/openai/models/admin/organization/usage_embeddings_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_file_search_calls_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_images_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_moderations_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_vector_stores_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_web_search_calls_response.rbs +15 -5
- data/sig/openai/models/audio/voice_consent.rbs +57 -0
- data/sig/openai/models/audio/voice_consent_create_params.rbs +61 -0
- data/sig/openai/models/audio/voice_consent_delete_params.rbs +25 -0
- data/sig/openai/models/audio/voice_consent_deleted.rbs +31 -0
- data/sig/openai/models/audio/voice_consent_list_params.rbs +57 -0
- data/sig/openai/models/audio/voice_consent_retrieve_params.rbs +25 -0
- data/sig/openai/models/audio/voice_consent_update_params.rbs +36 -0
- data/sig/openai/models/beta/beta_response_input_item.rbs +8 -0
- data/sig/openai/models/responses/response_input_item.rbs +8 -3
- data/sig/openai/resources/audio/voice_consents.rbs +38 -0
- data/sig/openai/resources/audio.rbs +2 -0
- metadata +31 -1
|
@@ -70,6 +70,7 @@ module OpenAI
|
|
|
70
70
|
if @error_reader && !@error_reader.closed?
|
|
71
71
|
chunk = @error_reader.read_nonblock(65_536, exception: false)
|
|
72
72
|
@errors = true if chunk.is_a?(String) && !chunk.empty?
|
|
73
|
+
@error_reader.close if chunk.nil?
|
|
73
74
|
end
|
|
74
75
|
|
|
75
76
|
!!@errors
|
|
@@ -86,7 +87,9 @@ module OpenAI
|
|
|
86
87
|
raise PlaybackError, "Audio player exited before the input ended." if @status
|
|
87
88
|
end
|
|
88
89
|
|
|
89
|
-
io
|
|
90
|
+
readers = [io]
|
|
91
|
+
readers << @error_reader if @error_reader && !@error_reader.closed?
|
|
92
|
+
IO.select(readers, nil, nil, 0.05)
|
|
90
93
|
end
|
|
91
94
|
end
|
|
92
95
|
|
|
@@ -96,7 +99,8 @@ module OpenAI
|
|
|
96
99
|
check_deadline
|
|
97
100
|
result = io.write_nonblock(bytes.byteslice(offset, 65_536), exception: false)
|
|
98
101
|
if result == :wait_writable
|
|
99
|
-
|
|
102
|
+
readers = @error_reader && !@error_reader.closed? ? [@error_reader] : []
|
|
103
|
+
IO.select(readers, [io], nil, 0.05)
|
|
100
104
|
else
|
|
101
105
|
offset += result
|
|
102
106
|
end
|
|
@@ -110,7 +114,13 @@ module OpenAI
|
|
|
110
114
|
until @status
|
|
111
115
|
check_deadline
|
|
112
116
|
reap
|
|
113
|
-
|
|
117
|
+
unless @status
|
|
118
|
+
if @error_reader && !@error_reader.closed?
|
|
119
|
+
@error_reader.wait_readable(0.01)
|
|
120
|
+
else
|
|
121
|
+
sleep(0.01)
|
|
122
|
+
end
|
|
123
|
+
end
|
|
114
124
|
end
|
|
115
125
|
|
|
116
126
|
@status
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "../../models/embedding"
|
|
4
|
+
|
|
5
|
+
module OpenAI
|
|
6
|
+
module Models
|
|
7
|
+
class Embedding
|
|
8
|
+
alias_method :embedding_without_base64, :embedding
|
|
9
|
+
private :embedding_without_base64
|
|
10
|
+
|
|
11
|
+
def embedding
|
|
12
|
+
value = self[:embedding]
|
|
13
|
+
return embedding_without_base64 unless value.is_a?(String)
|
|
14
|
+
|
|
15
|
+
# The generated setter replaces this per-field state on assignment.
|
|
16
|
+
cached = @coerced[:embedding]
|
|
17
|
+
return cached if cached.is_a?(Array)
|
|
18
|
+
|
|
19
|
+
# Base64 embedding values contain little-endian float32 samples.
|
|
20
|
+
decoded = begin
|
|
21
|
+
value.unpack1("m0")
|
|
22
|
+
rescue ArgumentError
|
|
23
|
+
nil
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
return embedding_without_base64 unless decoded && (decoded.bytesize % 4).zero?
|
|
27
|
+
|
|
28
|
+
@coerced[:embedding] = decoded.unpack("e*")
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# Equal embeddings still have equal raw data, so the inherited hash remains valid.
|
|
32
|
+
def ==(other)
|
|
33
|
+
return false unless super
|
|
34
|
+
return true unless self[:embedding].is_a?(String)
|
|
35
|
+
|
|
36
|
+
embedding == other.embedding
|
|
37
|
+
rescue OpenAI::Errors::ConversionError
|
|
38
|
+
true
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -33,7 +33,7 @@ module OpenAI
|
|
|
33
33
|
# Parse raw JSON as a typed server event. Valid events that are newer than this
|
|
34
34
|
# SDK remain observable as {UnknownServerEvent} values.
|
|
35
35
|
def parse_event(data)
|
|
36
|
-
parsed =
|
|
36
|
+
parsed = parse_json(data)
|
|
37
37
|
type = event_type(parsed)
|
|
38
38
|
unless @server_event_names.key?(type.to_s)
|
|
39
39
|
return OpenAI::Realtime::UnknownServerEvent.new(data: parsed)
|
|
@@ -46,14 +46,16 @@ module OpenAI
|
|
|
46
46
|
state: state
|
|
47
47
|
)
|
|
48
48
|
if (cause = coercion_error(state))
|
|
49
|
-
raise OpenAI::Errors::RealtimeProtocolError.new(data: data, cause: cause)
|
|
49
|
+
raise OpenAI::Errors::RealtimeProtocolError.new(data: data, cause: cause), cause: nil
|
|
50
50
|
end
|
|
51
51
|
|
|
52
|
-
event
|
|
52
|
+
validate_wire_field_names!(event, parsed)
|
|
53
53
|
rescue OpenAI::Errors::RealtimeProtocolError
|
|
54
54
|
raise
|
|
55
|
+
rescue SystemStackError
|
|
56
|
+
raise OpenAI::Errors::RealtimeProtocolError.new(data: data), cause: nil
|
|
55
57
|
rescue StandardError => e
|
|
56
|
-
raise OpenAI::Errors::RealtimeProtocolError.new(data: data, cause: e)
|
|
58
|
+
raise OpenAI::Errors::RealtimeProtocolError.new(data: data, cause: e), cause: nil
|
|
57
59
|
end
|
|
58
60
|
|
|
59
61
|
# Validate, encode, and send a typed client event.
|
|
@@ -61,6 +63,15 @@ module OpenAI
|
|
|
61
63
|
send_raw(encode_client_event(event))
|
|
62
64
|
end
|
|
63
65
|
|
|
66
|
+
private def write_text(text)
|
|
67
|
+
return super unless @recovery
|
|
68
|
+
|
|
69
|
+
# Recovery owns admission and uncertain sends; rejecting a full queue
|
|
70
|
+
# has not attempted a physical write and must leave this connection usable.
|
|
71
|
+
@recovery.write(text)
|
|
72
|
+
nil
|
|
73
|
+
end
|
|
74
|
+
|
|
64
75
|
# Whether automatic recovery is opening or preparing a replacement socket.
|
|
65
76
|
def reconnecting? = @recovery ? @recovery.reconnecting? : false
|
|
66
77
|
|
|
@@ -83,6 +94,10 @@ module OpenAI
|
|
|
83
94
|
validate_discriminator!(event, @client_event_names, kind: "client")
|
|
84
95
|
end
|
|
85
96
|
|
|
97
|
+
# Preserve permissive raw JSON keys while checking original typed models
|
|
98
|
+
# before union conversion can replace their identities or renamed fields.
|
|
99
|
+
validate_event_tree!(event, strict_keys: false)
|
|
100
|
+
|
|
86
101
|
normalized = OpenAI::Internal::Type::Converter.dump(
|
|
87
102
|
OpenAI::Realtime::RealtimeClientEvent,
|
|
88
103
|
event
|
|
@@ -99,7 +114,9 @@ module OpenAI
|
|
|
99
114
|
|
|
100
115
|
payload = OpenAI::Internal::Type::Converter.dump(OpenAI::Realtime::RealtimeClientEvent, coerced)
|
|
101
116
|
validate_discriminator!(payload, @client_event_names, kind: "client")
|
|
102
|
-
|
|
117
|
+
generate_json(payload)
|
|
118
|
+
rescue SystemStackError
|
|
119
|
+
raise ArgumentError, "Invalid Realtime client event.", cause: nil
|
|
103
120
|
rescue StandardError => e
|
|
104
121
|
raise ArgumentError.new("Invalid Realtime client event."), cause: e
|
|
105
122
|
end
|
|
@@ -76,7 +76,7 @@ module OpenAI
|
|
|
76
76
|
|
|
77
77
|
def take_pending_messages
|
|
78
78
|
@mutex.synchronize do
|
|
79
|
-
raise state_error("Cannot remove queued events while flushing.") if @flushing
|
|
79
|
+
raise state_error("Cannot remove queued events while flushing."), cause: nil if @flushing
|
|
80
80
|
|
|
81
81
|
messages = @pending
|
|
82
82
|
@pending = []
|
|
@@ -106,7 +106,7 @@ module OpenAI
|
|
|
106
106
|
|
|
107
107
|
if @state != :open || @flushing
|
|
108
108
|
if @max_queue_bytes.zero? || @pending_bytes + [text.bytesize, 1].max > @max_queue_bytes
|
|
109
|
-
raise OpenAI::Errors::RealtimeQueueFullError
|
|
109
|
+
raise OpenAI::Errors::RealtimeQueueFullError, cause: nil
|
|
110
110
|
end
|
|
111
111
|
|
|
112
112
|
@pending << text.dup.freeze
|
|
@@ -124,13 +124,15 @@ module OpenAI
|
|
|
124
124
|
|
|
125
125
|
def flush_pending
|
|
126
126
|
if Fiber.current.equal?(@owner_fiber)
|
|
127
|
-
raise state_error("Queue flush interrupted; remaining events were not sent.") unless flush
|
|
127
|
+
raise state_error("Queue flush interrupted; remaining events were not sent."), cause: nil unless flush
|
|
128
128
|
return nil
|
|
129
129
|
end
|
|
130
130
|
|
|
131
131
|
reply = @mutex.synchronize do
|
|
132
132
|
raise @error, cause: nil if @error
|
|
133
|
-
|
|
133
|
+
unless @state == :open
|
|
134
|
+
raise state_error("Cannot flush until the replacement connection is ready."), cause: nil
|
|
135
|
+
end
|
|
134
136
|
|
|
135
137
|
register(:flush).tap { @commands << [:flush, nil, _1, @generation] }
|
|
136
138
|
end
|
|
@@ -304,8 +306,14 @@ module OpenAI
|
|
|
304
306
|
# Shutdown must not mask the operation's result or prevent cleanup.
|
|
305
307
|
nil
|
|
306
308
|
ensure
|
|
307
|
-
|
|
308
|
-
|
|
309
|
+
pending_error = $ERROR_INFO
|
|
310
|
+
begin
|
|
311
|
+
physical.abort
|
|
312
|
+
rescue StandardError
|
|
313
|
+
raise if pending_error.nil?
|
|
314
|
+
ensure
|
|
315
|
+
@physical = nil
|
|
316
|
+
end
|
|
309
317
|
end
|
|
310
318
|
end
|
|
311
319
|
end
|
|
@@ -348,7 +356,11 @@ module OpenAI
|
|
|
348
356
|
uncertain_message: @inflight
|
|
349
357
|
)
|
|
350
358
|
elsif @state != :closed
|
|
351
|
-
@
|
|
359
|
+
if recoverable?(e) && @attempts < @max_attempts
|
|
360
|
+
@state = :recovering
|
|
361
|
+
else
|
|
362
|
+
fail_locked("Realtime reconnect stopped; the connection could not be recovered.")
|
|
363
|
+
end
|
|
352
364
|
end
|
|
353
365
|
end
|
|
354
366
|
|
|
@@ -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)
|
|
@@ -11,6 +11,8 @@ module OpenAI
|
|
|
11
11
|
# are retained exactly as received, never filled from provisional output.
|
|
12
12
|
# Call reset to discard all retained state.
|
|
13
13
|
class IncrementalResponse
|
|
14
|
+
include OpenAI::WebSocket::Protocol
|
|
15
|
+
|
|
14
16
|
attr_reader :phase
|
|
15
17
|
|
|
16
18
|
def initialize
|
|
@@ -236,10 +238,12 @@ module OpenAI
|
|
|
236
238
|
# references. Returned snapshots and incoming events must not share strings.
|
|
237
239
|
def copy(value)
|
|
238
240
|
data = OpenAI::Internal::Type::BaseModel.recursively_to_h(value, convert: false)
|
|
239
|
-
data =
|
|
241
|
+
data = parse_json(generate_json(data))
|
|
240
242
|
return data unless value.is_a?(OpenAI::Internal::Type::BaseModel)
|
|
241
243
|
|
|
242
244
|
OpenAI::Internal::Type::Converter.coerce(value.class, data)
|
|
245
|
+
rescue SystemStackError
|
|
246
|
+
raise SessionError, "Could not copy the Responses WebSocket event.", cause: nil
|
|
243
247
|
end
|
|
244
248
|
end
|
|
245
249
|
end
|