openai 0.95.0 → 0.96.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 +44 -0
- data/README.md +5 -2
- data/docs/architecture/security-model.md +2 -2
- data/examples/audio/record_and_play.rb +1 -1
- data/lib/openai/helpers/live/transcript_grouper.rb +264 -0
- data/lib/openai/helpers/live/transcript_grouping.rb +309 -0
- data/lib/openai/helpers/live/websocket.rb +454 -0
- data/lib/openai/helpers/realtime/translation.rb +440 -0
- data/lib/openai/helpers/realtime.rb +1 -0
- data/lib/openai/helpers/responses_websocket/incremental.rb +246 -0
- data/lib/openai/helpers/responses_websocket/transports/async_websocket.rb +183 -0
- data/lib/openai/helpers/responses_websocket.rb +1 -0
- data/lib/openai/helpers/streaming/response_stream.rb +19 -11
- data/lib/openai/helpers/structured_output/chat_completion_parser.rb +0 -2
- data/lib/openai/helpers/structured_output/response_parser.rb +1 -2
- data/lib/openai/helpers/structured_output/union_of.rb +0 -2
- data/lib/openai/helpers/websocket/async_websocket_transport.rb +11 -6
- data/lib/openai/helpers/websocket/connection_manager.rb +9 -0
- data/lib/openai/internal/read_io_adapter.rb +0 -2
- data/lib/openai/internal/stream.rb +0 -2
- data/lib/openai/internal/type/base_model.rb +4 -6
- data/lib/openai/internal/type/converter.rb +5 -3
- data/lib/openai/internal/type/enum.rb +0 -2
- data/lib/openai/internal/type/union.rb +0 -6
- data/lib/openai/internal/type/unknown.rb +34 -6
- data/lib/openai/internal/util.rb +0 -4
- data/lib/openai/models/audio/speech_create_params.rb +9 -0
- data/lib/openai/models/audio/transcription_create_response.rb +4 -1
- data/lib/openai/models/audio/translation_create_response.rb +4 -1
- data/lib/openai/models/beta/agent.rb +1 -0
- data/lib/openai/models/beta/agent_create_params.rb +3 -0
- data/lib/openai/models/beta/agent_session.rb +1 -0
- data/lib/openai/models/beta/agent_update_params.rb +3 -0
- data/lib/openai/models/beta/agents/environments/template_create_params.rb +3 -3
- data/lib/openai/models/beta/agents/environments/template_update_params.rb +6 -6
- data/lib/openai/models/beta/agents/session_create_params.rb +3 -0
- data/lib/openai/models/beta/agents/session_update_params.rb +3 -0
- data/lib/openai/models/beta/agents/vaults/credential.rb +10 -1
- data/lib/openai/models/beta/agents/vaults/credential_create_params.rb +12 -1
- data/lib/openai/models/beta/agents/vaults/credential_update_params.rb +14 -3
- data/lib/openai/models/beta/beta_response.rb +38 -1
- data/lib/openai/models/beta/beta_responses_client_event.rb +73 -1
- data/lib/openai/models/beta/beta_web_search_preview_tool.rb +9 -3
- data/lib/openai/models/beta/beta_web_search_tool.rb +9 -3
- data/lib/openai/models/beta/environment_param.rb +3 -3
- data/lib/openai/models/beta/response_compact_params.rb +3 -0
- data/lib/openai/models/beta/response_create_params.rb +68 -1
- data/lib/openai/models/beta/session_turn_error.rb +3 -0
- data/lib/openai/models/chat_model.rb +1 -0
- data/lib/openai/models/responses/response.rb +35 -1
- data/lib/openai/models/responses/response_compact_params.rb +3 -0
- data/lib/openai/models/responses/response_create_params.rb +65 -1
- data/lib/openai/models/responses/responses_client_event.rb +70 -1
- data/lib/openai/models/responses/web_search_preview_tool.rb +9 -3
- data/lib/openai/models/responses/web_search_tool.rb +9 -3
- data/lib/openai/net_http_client.rb +0 -2
- data/lib/openai/resources/audio/transcriptions.rb +5 -3
- data/lib/openai/resources/audio/translations.rb +1 -1
- data/lib/openai/resources/beta/agents/environments/templates.rb +3 -3
- data/lib/openai/resources/beta/agents/vaults/credentials.rb +12 -4
- data/lib/openai/resources/beta/responses.rb +8 -2
- data/lib/openai/resources/responses.rb +8 -1
- data/lib/openai/version.rb +1 -1
- data/lib/openai.rb +1 -0
- data/rbi/openai/helpers/live/transcript_grouper.rbi +67 -0
- data/rbi/openai/helpers/live/websocket.rbi +299 -0
- data/rbi/openai/helpers/realtime/translation.rbi +137 -0
- data/rbi/openai/helpers/responses_websocket/incremental.rbi +32 -0
- data/rbi/openai/helpers/responses_websocket/transports/async_websocket.rbi +24 -0
- data/rbi/openai/helpers/streaming/response_stream.rbi +5 -0
- data/rbi/openai/internal/type/converter.rbi +3 -2
- data/rbi/openai/models/audio/speech_create_params.rbi +3 -0
- data/rbi/openai/models/audio/transcription_create_response.rbi +2 -1
- data/rbi/openai/models/audio/translation_create_response.rbi +2 -1
- data/rbi/openai/models/beta/agent.rbi +1 -0
- data/rbi/openai/models/beta/agent_create_params.rbi +3 -0
- data/rbi/openai/models/beta/agent_session.rbi +1 -0
- data/rbi/openai/models/beta/agent_update_params.rbi +3 -0
- data/rbi/openai/models/beta/agents/environments/template_create_params.rbi +3 -3
- data/rbi/openai/models/beta/agents/environments/template_update_params.rbi +6 -6
- data/rbi/openai/models/beta/agents/session_create_params.rbi +3 -0
- data/rbi/openai/models/beta/agents/session_update_params.rbi +3 -0
- data/rbi/openai/models/beta/agents/vaults/credential.rbi +10 -0
- data/rbi/openai/models/beta/agents/vaults/credential_create_params.rbi +15 -0
- data/rbi/openai/models/beta/agents/vaults/credential_update_params.rbi +35 -6
- data/rbi/openai/models/beta/beta_response.rbi +64 -0
- data/rbi/openai/models/beta/beta_responses_client_event.rbi +128 -0
- data/rbi/openai/models/beta/beta_web_search_preview_tool.rbi +9 -3
- data/rbi/openai/models/beta/beta_web_search_tool.rbi +9 -3
- data/rbi/openai/models/beta/environment_param.rbi +3 -3
- data/rbi/openai/models/beta/response_compact_params.rbi +1 -0
- data/rbi/openai/models/beta/response_create_params.rbi +100 -0
- data/rbi/openai/models/beta/session_turn_error.rbi +6 -0
- data/rbi/openai/models/chat_model.rbi +1 -0
- data/rbi/openai/models/responses/response.rbi +63 -0
- data/rbi/openai/models/responses/response_compact_params.rbi +1 -0
- data/rbi/openai/models/responses/response_create_params.rbi +106 -0
- data/rbi/openai/models/responses/responses_client_event.rbi +125 -0
- data/rbi/openai/models/responses/web_search_preview_tool.rbi +9 -3
- data/rbi/openai/models/responses/web_search_tool.rbi +9 -3
- data/rbi/openai/resources/audio/transcriptions.rbi +4 -2
- data/rbi/openai/resources/beta/agents/environments/templates.rbi +3 -3
- data/rbi/openai/resources/beta/agents/vaults/credentials.rbi +11 -3
- data/rbi/openai/resources/beta/responses.rbi +6 -0
- data/rbi/openai/resources/responses.rbi +11 -0
- data/responses-websocket.md +55 -0
- data/sig/openai/helpers/live/transcript_grouper.rbs +42 -0
- data/sig/openai/helpers/live/websocket.rbs +152 -0
- data/sig/openai/helpers/realtime/translation.rbs +81 -0
- data/sig/openai/helpers/responses_websocket/incremental.rbs +14 -0
- data/sig/openai/helpers/responses_websocket/transports/async_websocket.rbs +21 -0
- data/sig/openai/helpers/streaming/response_stream.rbs +9 -0
- data/sig/openai/internal/type/converter.rbs +3 -1
- data/sig/openai/models/audio/speech_create_params.rbs +6 -0
- data/sig/openai/models/audio/transcription_create_response.rbs +1 -0
- data/sig/openai/models/audio/translation_create_response.rbs +3 -1
- data/sig/openai/models/beta/agent.rbs +3 -1
- data/sig/openai/models/beta/agent_create_params.rbs +5 -1
- data/sig/openai/models/beta/agent_session.rbs +3 -1
- data/sig/openai/models/beta/agent_update_params.rbs +5 -1
- data/sig/openai/models/beta/agents/environments/template_create_params.rbs +2 -2
- data/sig/openai/models/beta/agents/environments/template_update_params.rbs +4 -4
- data/sig/openai/models/beta/agents/session_create_params.rbs +5 -1
- data/sig/openai/models/beta/agents/session_update_params.rbs +5 -1
- data/sig/openai/models/beta/agents/vaults/credential.rbs +8 -0
- data/sig/openai/models/beta/agents/vaults/credential_create_params.rbs +14 -0
- data/sig/openai/models/beta/agents/vaults/credential_update_params.rbs +23 -4
- data/sig/openai/models/beta/beta_response.rbs +37 -0
- data/sig/openai/models/beta/beta_responses_client_event.rbs +79 -0
- data/sig/openai/models/beta/beta_web_search_preview_tool.rbs +6 -2
- data/sig/openai/models/beta/beta_web_search_tool.rbs +6 -2
- data/sig/openai/models/beta/environment_param.rbs +2 -2
- data/sig/openai/models/beta/response_compact_params.rbs +2 -0
- data/sig/openai/models/beta/response_create_params.rbs +79 -0
- data/sig/openai/models/beta/session_turn_error.rbs +4 -0
- data/sig/openai/models/chat_model.rbs +2 -0
- data/sig/openai/models/responses/response.rbs +35 -0
- data/sig/openai/models/responses/response_compact_params.rbs +2 -0
- data/sig/openai/models/responses/response_create_params.rbs +77 -0
- data/sig/openai/models/responses/responses_client_event.rbs +77 -0
- data/sig/openai/models/responses/web_search_preview_tool.rbs +6 -2
- data/sig/openai/models/responses/web_search_tool.rbs +6 -2
- data/sig/openai/resources/beta/agents/vaults/credentials.rbs +3 -1
- data/sig/openai/resources/beta/responses.rbs +2 -0
- data/sig/openai/resources/responses.rbs +4 -0
- metadata +14 -1
|
@@ -0,0 +1,454 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenAI
|
|
4
|
+
module Errors
|
|
5
|
+
class LiveConnectionError < OpenAI::Errors::WebSocketConnectionError
|
|
6
|
+
private def default_message = "Live WebSocket connection error."
|
|
7
|
+
|
|
8
|
+
private def sanitized_error_url(url)
|
|
9
|
+
sanitized = url.dup
|
|
10
|
+
sanitized.user = nil if sanitized.respond_to?(:user=)
|
|
11
|
+
sanitized.password = nil if sanitized.respond_to?(:password=)
|
|
12
|
+
sanitized.query = nil if sanitized.respond_to?(:query=)
|
|
13
|
+
sanitized.fragment = nil if sanitized.respond_to?(:fragment=)
|
|
14
|
+
sanitized
|
|
15
|
+
rescue ArgumentError, URI::Error
|
|
16
|
+
URI("wss://invalid")
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
class LiveProtocolError < OpenAI::Errors::WebSocketProtocolError
|
|
21
|
+
def initialize = super("Invalid Live WebSocket event.")
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
module Live
|
|
26
|
+
# A future server event retains its raw data, but routine diagnostics do not
|
|
27
|
+
# render potentially sensitive audio, transcript, or session fields.
|
|
28
|
+
class UnknownServerEvent
|
|
29
|
+
attr_reader :type, :data
|
|
30
|
+
|
|
31
|
+
def initialize(data:)
|
|
32
|
+
@type = data.fetch(:type) { data.fetch("type") }.to_sym
|
|
33
|
+
@data = freeze_json(data)
|
|
34
|
+
freeze
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def to_h = @data
|
|
38
|
+
def inspect = "#<#{self.class} type=#{@type.inspect}>"
|
|
39
|
+
alias to_s inspect
|
|
40
|
+
|
|
41
|
+
private def freeze_json(value)
|
|
42
|
+
pending = [value]
|
|
43
|
+
visited = {}.compare_by_identity
|
|
44
|
+
until pending.empty?
|
|
45
|
+
item = pending.pop
|
|
46
|
+
next if visited.key?(item)
|
|
47
|
+
visited[item] = true
|
|
48
|
+
case item
|
|
49
|
+
when Hash
|
|
50
|
+
item.each { |key, val| pending.push(key, val) }
|
|
51
|
+
when Array
|
|
52
|
+
pending.concat(item)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
item.freeze
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
value
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
# A transport-open primary connection. Startup is owned by the caller:
|
|
63
|
+
# send session.start and observe session.started before further commands.
|
|
64
|
+
class Connection < OpenAI::WebSocket::Connection
|
|
65
|
+
include OpenAI::WebSocket::Protocol
|
|
66
|
+
|
|
67
|
+
# @api private
|
|
68
|
+
def initialize(socket:, url:)
|
|
69
|
+
super
|
|
70
|
+
@poisoned = false
|
|
71
|
+
@closed = false
|
|
72
|
+
@server_event_names = discriminator_values(server_event_type)
|
|
73
|
+
@client_event_names = discriminator_values(client_event_type)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
def send_event(event)
|
|
77
|
+
send_raw(encode_client_event(event))
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
def closed? = @poisoned || @closed || super
|
|
81
|
+
|
|
82
|
+
def close(code: 1000, reason: "")
|
|
83
|
+
@poisoned || @closed ? abort : super
|
|
84
|
+
@closed = true
|
|
85
|
+
nil
|
|
86
|
+
rescue StandardError
|
|
87
|
+
@poisoned = true
|
|
88
|
+
raise OpenAI::Errors::LiveConnectionError.new(url: @url), cause: nil
|
|
89
|
+
end
|
|
90
|
+
|
|
91
|
+
# @api private
|
|
92
|
+
def abort
|
|
93
|
+
super
|
|
94
|
+
@closed = true
|
|
95
|
+
nil
|
|
96
|
+
rescue StandardError
|
|
97
|
+
@poisoned = true
|
|
98
|
+
raise OpenAI::Errors::LiveConnectionError.new(url: @url), cause: nil
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
private def write_text(text)
|
|
102
|
+
super
|
|
103
|
+
rescue StandardError
|
|
104
|
+
@poisoned = true
|
|
105
|
+
raise OpenAI::Errors::LiveConnectionError.new(url: @url), cause: nil
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
private def read_raw_message
|
|
109
|
+
raise connection_error("Cannot read from a failed Live WebSocket.") if @poisoned
|
|
110
|
+
return nil if @closed
|
|
111
|
+
text = super
|
|
112
|
+
@closed = text.nil?
|
|
113
|
+
text
|
|
114
|
+
rescue StandardError
|
|
115
|
+
@poisoned = true
|
|
116
|
+
raise OpenAI::Errors::LiveConnectionError.new(url: @url), cause: nil
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
private def socket_closed?
|
|
120
|
+
super
|
|
121
|
+
rescue StandardError
|
|
122
|
+
@poisoned = true
|
|
123
|
+
raise OpenAI::Errors::LiveConnectionError.new(url: @url), cause: nil
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
private def encode_client_event(event)
|
|
127
|
+
validate_event_tree!(event)
|
|
128
|
+
payload = OpenAI::Internal::Type::Converter.dump(client_event_type, event)
|
|
129
|
+
type = payload[:type] || payload["type"] if payload.is_a?(Hash)
|
|
130
|
+
raise ArgumentError unless (type.is_a?(String) || type.is_a?(Symbol)) && @client_event_names.key?(type.to_s)
|
|
131
|
+
|
|
132
|
+
coerced = coerce_event(client_event_type, payload, outbound: true)
|
|
133
|
+
serialized = OpenAI::Internal::Type::Converter.dump(client_event_type, coerced)
|
|
134
|
+
validate_event_tree!(serialized, json_only: true)
|
|
135
|
+
JSON.generate(serialized, max_nesting: false)
|
|
136
|
+
rescue StandardError, SystemStackError
|
|
137
|
+
raise ArgumentError, "Invalid Live client event.", cause: nil
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
private def parse_event(data)
|
|
141
|
+
parsed = JSON.parse(data, symbolize_names: true, max_nesting: false)
|
|
142
|
+
type = event_type(parsed, message: "Live server event must be an object with a string type")
|
|
143
|
+
unless @server_event_names.key?(type.to_s)
|
|
144
|
+
return OpenAI::Live::UnknownServerEvent.new(data: parsed)
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
event = coerce_event(server_event_type, parsed)
|
|
148
|
+
validate_event_tree!(event)
|
|
149
|
+
event
|
|
150
|
+
rescue StandardError, SystemStackError
|
|
151
|
+
raise OpenAI::Errors::LiveProtocolError.new, cause: nil
|
|
152
|
+
end
|
|
153
|
+
|
|
154
|
+
private def coerce_event(union, payload, outbound: false)
|
|
155
|
+
state = OpenAI::Internal::Type::Converter.new_coerce_state(request_only: outbound)
|
|
156
|
+
event = OpenAI::Internal::Type::Converter.coerce(union, payload, state: state)
|
|
157
|
+
raise ArgumentError if state[:error] || !state.fetch(:exactness).fetch(:no).zero?
|
|
158
|
+
pending = [event]
|
|
159
|
+
until pending.empty?
|
|
160
|
+
value = pending.pop
|
|
161
|
+
case value
|
|
162
|
+
when OpenAI::Internal::Type::BaseModel
|
|
163
|
+
value.class.fields.each do |name, field|
|
|
164
|
+
if field.fetch(:required) &&
|
|
165
|
+
field.fetch(:mode) != (outbound ? :coerce : :dump) &&
|
|
166
|
+
(!outbound || field.fetch(:const) == OpenAI::Internal::OMIT) &&
|
|
167
|
+
!value.to_h.key?(name)
|
|
168
|
+
raise ArgumentError
|
|
169
|
+
end
|
|
170
|
+
end
|
|
171
|
+
|
|
172
|
+
pending.concat(value.to_h.values)
|
|
173
|
+
when Hash
|
|
174
|
+
pending.concat(value.values)
|
|
175
|
+
when Array
|
|
176
|
+
pending.concat(value)
|
|
177
|
+
end
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
event
|
|
181
|
+
end
|
|
182
|
+
|
|
183
|
+
private def validate_event_tree!(event, json_only: false)
|
|
184
|
+
ancestors = {}.compare_by_identity
|
|
185
|
+
pending = [[event, false]]
|
|
186
|
+
until pending.empty?
|
|
187
|
+
value, exiting = pending.pop
|
|
188
|
+
if exiting
|
|
189
|
+
ancestors.delete(value)
|
|
190
|
+
next
|
|
191
|
+
end
|
|
192
|
+
|
|
193
|
+
children = case value
|
|
194
|
+
when OpenAI::Internal::Type::BaseModel
|
|
195
|
+
raise ArgumentError if json_only
|
|
196
|
+
data = value.to_h
|
|
197
|
+
keys = {}
|
|
198
|
+
data.each do |key, val|
|
|
199
|
+
name = key.is_a?(String) ? key.to_sym : key
|
|
200
|
+
field = value.class.known_fields[name]
|
|
201
|
+
serialized_name = field ? field.fetch(:api_name).to_s : name.to_s
|
|
202
|
+
raise ArgumentError if keys.key?(serialized_name)
|
|
203
|
+
keys[serialized_name] = true
|
|
204
|
+
next unless field
|
|
205
|
+
const = field.fetch(:const)
|
|
206
|
+
next if const == OpenAI::Internal::OMIT
|
|
207
|
+
if const.is_a?(Symbol)
|
|
208
|
+
raise ArgumentError unless (val.is_a?(String) || val.is_a?(Symbol)) && val.to_s == const.to_s
|
|
209
|
+
elsif val != const
|
|
210
|
+
raise ArgumentError
|
|
211
|
+
end
|
|
212
|
+
end
|
|
213
|
+
|
|
214
|
+
[data]
|
|
215
|
+
when Hash
|
|
216
|
+
keys = {}
|
|
217
|
+
value.each_key do |key|
|
|
218
|
+
raise ArgumentError unless key.is_a?(String) || key.is_a?(Symbol)
|
|
219
|
+
name = key.to_s
|
|
220
|
+
raise ArgumentError if keys.key?(name)
|
|
221
|
+
keys[name] = true
|
|
222
|
+
end
|
|
223
|
+
|
|
224
|
+
value.values
|
|
225
|
+
when Array
|
|
226
|
+
value
|
|
227
|
+
when NilClass, TrueClass, FalseClass, String, Integer, Float, Symbol
|
|
228
|
+
next
|
|
229
|
+
else
|
|
230
|
+
raise ArgumentError if json_only
|
|
231
|
+
next
|
|
232
|
+
end
|
|
233
|
+
|
|
234
|
+
raise ArgumentError if ancestors.key?(value)
|
|
235
|
+
ancestors[value] = true
|
|
236
|
+
pending << [value, true]
|
|
237
|
+
children.each { |child| pending << [child, false] }
|
|
238
|
+
end
|
|
239
|
+
end
|
|
240
|
+
|
|
241
|
+
private def connection_error(message)
|
|
242
|
+
OpenAI::Errors::LiveConnectionError.new(url: @url, message: message)
|
|
243
|
+
end
|
|
244
|
+
|
|
245
|
+
private def client_event_type = OpenAI::Live::ClientEvent
|
|
246
|
+
private def server_event_type = OpenAI::Live::ServerEvent
|
|
247
|
+
end
|
|
248
|
+
|
|
249
|
+
# Attached to an existing session; no new session.start handshake is sent.
|
|
250
|
+
class SidebandConnection < Connection
|
|
251
|
+
# @api private
|
|
252
|
+
def initialize(socket:, url:)
|
|
253
|
+
super
|
|
254
|
+
@client_event_names = @client_event_names.except("session.start", "session.input_audio.append")
|
|
255
|
+
end
|
|
256
|
+
end
|
|
257
|
+
|
|
258
|
+
# Caller-driven startup using overrides for an eligible stored session.
|
|
259
|
+
class ForkConnection < Connection
|
|
260
|
+
def send_event(event)
|
|
261
|
+
if event.is_a?(OpenAI::Live::SessionStartEvent)
|
|
262
|
+
raise ArgumentError, "Invalid Live client event."
|
|
263
|
+
end
|
|
264
|
+
|
|
265
|
+
super
|
|
266
|
+
end
|
|
267
|
+
|
|
268
|
+
private def client_event_type = OpenAI::Live::ForkClientEvent
|
|
269
|
+
private def server_event_type = OpenAI::Live::ForkServerEvent
|
|
270
|
+
|
|
271
|
+
private def coerce_event(union, payload, outbound: false)
|
|
272
|
+
event = super
|
|
273
|
+
if outbound && event.is_a?(OpenAI::Live::ForkSessionStartEvent)
|
|
274
|
+
overrides = event.session.to_h
|
|
275
|
+
raise ArgumentError if overrides.key?(:model) || overrides.key?(:client)
|
|
276
|
+
end
|
|
277
|
+
|
|
278
|
+
event
|
|
279
|
+
end
|
|
280
|
+
end
|
|
281
|
+
end
|
|
282
|
+
|
|
283
|
+
module Helpers
|
|
284
|
+
module LiveWebSocket
|
|
285
|
+
# @api private
|
|
286
|
+
module ClientExtension
|
|
287
|
+
include OpenAI::WebSocket::ClientRequest
|
|
288
|
+
|
|
289
|
+
# @api private
|
|
290
|
+
def with_live_websocket_connection_request(
|
|
291
|
+
path: "live/sessions",
|
|
292
|
+
query: {},
|
|
293
|
+
websocket_base_url: nil,
|
|
294
|
+
options: nil,
|
|
295
|
+
&block
|
|
296
|
+
)
|
|
297
|
+
path = OpenAI::Internal::Util.interpolate_path(path).freeze
|
|
298
|
+
query = query.dup.freeze
|
|
299
|
+
websocket_base_url = websocket_base_url&.to_s&.dup&.freeze
|
|
300
|
+
build = lambda do |deadline|
|
|
301
|
+
build_shared_websocket_connection_request(
|
|
302
|
+
path: path,
|
|
303
|
+
query: query,
|
|
304
|
+
websocket_base_url: websocket_base_url,
|
|
305
|
+
options: options,
|
|
306
|
+
deadline: deadline,
|
|
307
|
+
validate: -> (_) { validate_live_websocket_request! },
|
|
308
|
+
invalid_base_url_message: "websocket_base_url must be an absolute HTTP or WebSocket URL " \
|
|
309
|
+
"without credentials, query, or fragment",
|
|
310
|
+
malformed_base_url_message: "websocket_base_url is not a valid URL",
|
|
311
|
+
preserve_base_url_cause: false,
|
|
312
|
+
extra_query_message: "request_options extra_query is not supported for Live WebSocket connections",
|
|
313
|
+
max_retries_message: "request_options max_retries is not supported for Live WebSocket connections",
|
|
314
|
+
timeout_error: -> (url, _) { OpenAI::Errors::LiveConnectionError.new(url: url) }
|
|
315
|
+
)
|
|
316
|
+
end
|
|
317
|
+
|
|
318
|
+
with_websocket_connection_retry(
|
|
319
|
+
error_class: OpenAI::Errors::LiveConnectionError,
|
|
320
|
+
build: build,
|
|
321
|
+
&block
|
|
322
|
+
)
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
private def validate_live_websocket_request!
|
|
326
|
+
if x509_identity?(@copy_options.fetch(:workload_identity))
|
|
327
|
+
raise OpenAI::Errors::Error, "X.509 workload identity does not support Live WebSocket connections"
|
|
328
|
+
end
|
|
329
|
+
|
|
330
|
+
if @provider_runtime
|
|
331
|
+
raise OpenAI::Errors::Error, "Live WebSocket connections are not supported by providers."
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
module Connections
|
|
337
|
+
# Open a primary Live WebSocket; the model is supplied in session.start,
|
|
338
|
+
# not in the URL. This opens only the transport, never starts a session.
|
|
339
|
+
def connect(websocket_base_url: nil, request_options: nil, transport: nil, transport_options: {}, &block)
|
|
340
|
+
open_live_websocket(
|
|
341
|
+
path: "live/sessions",
|
|
342
|
+
connection_class: OpenAI::Live::Connection,
|
|
343
|
+
websocket_base_url: websocket_base_url,
|
|
344
|
+
request_options: request_options,
|
|
345
|
+
transport: transport,
|
|
346
|
+
transport_options: transport_options,
|
|
347
|
+
&block
|
|
348
|
+
)
|
|
349
|
+
end
|
|
350
|
+
|
|
351
|
+
private def open_live_websocket(
|
|
352
|
+
path:,
|
|
353
|
+
connection_class:,
|
|
354
|
+
websocket_base_url:,
|
|
355
|
+
request_options:,
|
|
356
|
+
transport:,
|
|
357
|
+
transport_options:,
|
|
358
|
+
query: {},
|
|
359
|
+
&block
|
|
360
|
+
)
|
|
361
|
+
raise ArgumentError, "A block is required to open a Live WebSocket." unless block
|
|
362
|
+
|
|
363
|
+
request = lambda do |&request_block|
|
|
364
|
+
@client.with_live_websocket_connection_request(
|
|
365
|
+
path: path,
|
|
366
|
+
query: query,
|
|
367
|
+
websocket_base_url: websocket_base_url,
|
|
368
|
+
options: request_options,
|
|
369
|
+
&request_block
|
|
370
|
+
)
|
|
371
|
+
end
|
|
372
|
+
|
|
373
|
+
default_transport = lambda do
|
|
374
|
+
OpenAI::WebSocket::AsyncWebSocketTransport.new(
|
|
375
|
+
product_name: "Live",
|
|
376
|
+
error_class: OpenAI::Errors::LiveConnectionError,
|
|
377
|
+
error_factory: -> (url:, message: nil, http_status: nil, **) {
|
|
378
|
+
OpenAI::Errors::LiveConnectionError.new(url: url, message: message, http_status: http_status)
|
|
379
|
+
}
|
|
380
|
+
)
|
|
381
|
+
end
|
|
382
|
+
|
|
383
|
+
OpenAI::WebSocket::ConnectionManager
|
|
384
|
+
.new(
|
|
385
|
+
transport: transport,
|
|
386
|
+
transport_options: transport_options,
|
|
387
|
+
default_transport: default_transport,
|
|
388
|
+
connection_class: connection_class,
|
|
389
|
+
request: request,
|
|
390
|
+
block_error_message: "A block is required to open a Live WebSocket.",
|
|
391
|
+
abort_after_block: -> (_connection, pending_error) { !pending_error.nil? },
|
|
392
|
+
transport_error_factory: -> (url:, error:) {
|
|
393
|
+
status = error.http_status if error.is_a?(OpenAI::Errors::WebSocketConnectionError)
|
|
394
|
+
OpenAI::Errors::LiveConnectionError.new(url: url, http_status: status)
|
|
395
|
+
}
|
|
396
|
+
)
|
|
397
|
+
.open(&block)
|
|
398
|
+
end
|
|
399
|
+
end
|
|
400
|
+
end
|
|
401
|
+
end
|
|
402
|
+
end
|
|
403
|
+
|
|
404
|
+
OpenAI::Client.include(OpenAI::Helpers::LiveWebSocket::ClientExtension)
|
|
405
|
+
OpenAI::Resources::Live.include(OpenAI::Helpers::LiveWebSocket::Connections)
|
|
406
|
+
|
|
407
|
+
class OpenAI::Resources::Live::Sideband
|
|
408
|
+
include OpenAI::Helpers::LiveWebSocket::Connections
|
|
409
|
+
|
|
410
|
+
# Attach to an eligible existing session with its selected observer credentials.
|
|
411
|
+
# The service may replay recent events; attachment does not start a new session.
|
|
412
|
+
def connect(
|
|
413
|
+
session_id,
|
|
414
|
+
graceful_close: nil,
|
|
415
|
+
websocket_base_url: nil,
|
|
416
|
+
request_options: nil,
|
|
417
|
+
transport: nil,
|
|
418
|
+
transport_options: {},
|
|
419
|
+
&block
|
|
420
|
+
)
|
|
421
|
+
unless graceful_close.nil? || graceful_close == true || graceful_close == false
|
|
422
|
+
raise ArgumentError, "graceful_close must be true, false, or nil"
|
|
423
|
+
end
|
|
424
|
+
|
|
425
|
+
open_live_websocket(
|
|
426
|
+
path: ["live/sessions/%1$s/attach", session_id],
|
|
427
|
+
query: graceful_close.nil? ? {} : {"graceful_close" => graceful_close.to_s},
|
|
428
|
+
connection_class: OpenAI::Live::SidebandConnection,
|
|
429
|
+
websocket_base_url: websocket_base_url,
|
|
430
|
+
request_options: request_options,
|
|
431
|
+
transport: transport,
|
|
432
|
+
transport_options: transport_options,
|
|
433
|
+
&block
|
|
434
|
+
)
|
|
435
|
+
end
|
|
436
|
+
end
|
|
437
|
+
|
|
438
|
+
class OpenAI::Resources::Live::Forks
|
|
439
|
+
include OpenAI::Helpers::LiveWebSocket::Connections
|
|
440
|
+
|
|
441
|
+
# Open a fork of an eligible stored recording. Send session.start with
|
|
442
|
+
# session: {} to inherit the recording, then wait for session.started.
|
|
443
|
+
def connect(session_id, websocket_base_url: nil, request_options: nil, transport: nil, transport_options: {}, &block)
|
|
444
|
+
open_live_websocket(
|
|
445
|
+
path: ["live/sessions/%1$s/fork", session_id],
|
|
446
|
+
connection_class: OpenAI::Live::ForkConnection,
|
|
447
|
+
websocket_base_url: websocket_base_url,
|
|
448
|
+
request_options: request_options,
|
|
449
|
+
transport: transport,
|
|
450
|
+
transport_options: transport_options,
|
|
451
|
+
&block
|
|
452
|
+
)
|
|
453
|
+
end
|
|
454
|
+
end
|