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.
Files changed (147) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +44 -0
  3. data/README.md +5 -2
  4. data/docs/architecture/security-model.md +2 -2
  5. data/examples/audio/record_and_play.rb +1 -1
  6. data/lib/openai/helpers/live/transcript_grouper.rb +264 -0
  7. data/lib/openai/helpers/live/transcript_grouping.rb +309 -0
  8. data/lib/openai/helpers/live/websocket.rb +454 -0
  9. data/lib/openai/helpers/realtime/translation.rb +440 -0
  10. data/lib/openai/helpers/realtime.rb +1 -0
  11. data/lib/openai/helpers/responses_websocket/incremental.rb +246 -0
  12. data/lib/openai/helpers/responses_websocket/transports/async_websocket.rb +183 -0
  13. data/lib/openai/helpers/responses_websocket.rb +1 -0
  14. data/lib/openai/helpers/streaming/response_stream.rb +19 -11
  15. data/lib/openai/helpers/structured_output/chat_completion_parser.rb +0 -2
  16. data/lib/openai/helpers/structured_output/response_parser.rb +1 -2
  17. data/lib/openai/helpers/structured_output/union_of.rb +0 -2
  18. data/lib/openai/helpers/websocket/async_websocket_transport.rb +11 -6
  19. data/lib/openai/helpers/websocket/connection_manager.rb +9 -0
  20. data/lib/openai/internal/read_io_adapter.rb +0 -2
  21. data/lib/openai/internal/stream.rb +0 -2
  22. data/lib/openai/internal/type/base_model.rb +4 -6
  23. data/lib/openai/internal/type/converter.rb +5 -3
  24. data/lib/openai/internal/type/enum.rb +0 -2
  25. data/lib/openai/internal/type/union.rb +0 -6
  26. data/lib/openai/internal/type/unknown.rb +34 -6
  27. data/lib/openai/internal/util.rb +0 -4
  28. data/lib/openai/models/audio/speech_create_params.rb +9 -0
  29. data/lib/openai/models/audio/transcription_create_response.rb +4 -1
  30. data/lib/openai/models/audio/translation_create_response.rb +4 -1
  31. data/lib/openai/models/beta/agent.rb +1 -0
  32. data/lib/openai/models/beta/agent_create_params.rb +3 -0
  33. data/lib/openai/models/beta/agent_session.rb +1 -0
  34. data/lib/openai/models/beta/agent_update_params.rb +3 -0
  35. data/lib/openai/models/beta/agents/environments/template_create_params.rb +3 -3
  36. data/lib/openai/models/beta/agents/environments/template_update_params.rb +6 -6
  37. data/lib/openai/models/beta/agents/session_create_params.rb +3 -0
  38. data/lib/openai/models/beta/agents/session_update_params.rb +3 -0
  39. data/lib/openai/models/beta/agents/vaults/credential.rb +10 -1
  40. data/lib/openai/models/beta/agents/vaults/credential_create_params.rb +12 -1
  41. data/lib/openai/models/beta/agents/vaults/credential_update_params.rb +14 -3
  42. data/lib/openai/models/beta/beta_response.rb +38 -1
  43. data/lib/openai/models/beta/beta_responses_client_event.rb +73 -1
  44. data/lib/openai/models/beta/beta_web_search_preview_tool.rb +9 -3
  45. data/lib/openai/models/beta/beta_web_search_tool.rb +9 -3
  46. data/lib/openai/models/beta/environment_param.rb +3 -3
  47. data/lib/openai/models/beta/response_compact_params.rb +3 -0
  48. data/lib/openai/models/beta/response_create_params.rb +68 -1
  49. data/lib/openai/models/beta/session_turn_error.rb +3 -0
  50. data/lib/openai/models/chat_model.rb +1 -0
  51. data/lib/openai/models/responses/response.rb +35 -1
  52. data/lib/openai/models/responses/response_compact_params.rb +3 -0
  53. data/lib/openai/models/responses/response_create_params.rb +65 -1
  54. data/lib/openai/models/responses/responses_client_event.rb +70 -1
  55. data/lib/openai/models/responses/web_search_preview_tool.rb +9 -3
  56. data/lib/openai/models/responses/web_search_tool.rb +9 -3
  57. data/lib/openai/net_http_client.rb +0 -2
  58. data/lib/openai/resources/audio/transcriptions.rb +5 -3
  59. data/lib/openai/resources/audio/translations.rb +1 -1
  60. data/lib/openai/resources/beta/agents/environments/templates.rb +3 -3
  61. data/lib/openai/resources/beta/agents/vaults/credentials.rb +12 -4
  62. data/lib/openai/resources/beta/responses.rb +8 -2
  63. data/lib/openai/resources/responses.rb +8 -1
  64. data/lib/openai/version.rb +1 -1
  65. data/lib/openai.rb +1 -0
  66. data/rbi/openai/helpers/live/transcript_grouper.rbi +67 -0
  67. data/rbi/openai/helpers/live/websocket.rbi +299 -0
  68. data/rbi/openai/helpers/realtime/translation.rbi +137 -0
  69. data/rbi/openai/helpers/responses_websocket/incremental.rbi +32 -0
  70. data/rbi/openai/helpers/responses_websocket/transports/async_websocket.rbi +24 -0
  71. data/rbi/openai/helpers/streaming/response_stream.rbi +5 -0
  72. data/rbi/openai/internal/type/converter.rbi +3 -2
  73. data/rbi/openai/models/audio/speech_create_params.rbi +3 -0
  74. data/rbi/openai/models/audio/transcription_create_response.rbi +2 -1
  75. data/rbi/openai/models/audio/translation_create_response.rbi +2 -1
  76. data/rbi/openai/models/beta/agent.rbi +1 -0
  77. data/rbi/openai/models/beta/agent_create_params.rbi +3 -0
  78. data/rbi/openai/models/beta/agent_session.rbi +1 -0
  79. data/rbi/openai/models/beta/agent_update_params.rbi +3 -0
  80. data/rbi/openai/models/beta/agents/environments/template_create_params.rbi +3 -3
  81. data/rbi/openai/models/beta/agents/environments/template_update_params.rbi +6 -6
  82. data/rbi/openai/models/beta/agents/session_create_params.rbi +3 -0
  83. data/rbi/openai/models/beta/agents/session_update_params.rbi +3 -0
  84. data/rbi/openai/models/beta/agents/vaults/credential.rbi +10 -0
  85. data/rbi/openai/models/beta/agents/vaults/credential_create_params.rbi +15 -0
  86. data/rbi/openai/models/beta/agents/vaults/credential_update_params.rbi +35 -6
  87. data/rbi/openai/models/beta/beta_response.rbi +64 -0
  88. data/rbi/openai/models/beta/beta_responses_client_event.rbi +128 -0
  89. data/rbi/openai/models/beta/beta_web_search_preview_tool.rbi +9 -3
  90. data/rbi/openai/models/beta/beta_web_search_tool.rbi +9 -3
  91. data/rbi/openai/models/beta/environment_param.rbi +3 -3
  92. data/rbi/openai/models/beta/response_compact_params.rbi +1 -0
  93. data/rbi/openai/models/beta/response_create_params.rbi +100 -0
  94. data/rbi/openai/models/beta/session_turn_error.rbi +6 -0
  95. data/rbi/openai/models/chat_model.rbi +1 -0
  96. data/rbi/openai/models/responses/response.rbi +63 -0
  97. data/rbi/openai/models/responses/response_compact_params.rbi +1 -0
  98. data/rbi/openai/models/responses/response_create_params.rbi +106 -0
  99. data/rbi/openai/models/responses/responses_client_event.rbi +125 -0
  100. data/rbi/openai/models/responses/web_search_preview_tool.rbi +9 -3
  101. data/rbi/openai/models/responses/web_search_tool.rbi +9 -3
  102. data/rbi/openai/resources/audio/transcriptions.rbi +4 -2
  103. data/rbi/openai/resources/beta/agents/environments/templates.rbi +3 -3
  104. data/rbi/openai/resources/beta/agents/vaults/credentials.rbi +11 -3
  105. data/rbi/openai/resources/beta/responses.rbi +6 -0
  106. data/rbi/openai/resources/responses.rbi +11 -0
  107. data/responses-websocket.md +55 -0
  108. data/sig/openai/helpers/live/transcript_grouper.rbs +42 -0
  109. data/sig/openai/helpers/live/websocket.rbs +152 -0
  110. data/sig/openai/helpers/realtime/translation.rbs +81 -0
  111. data/sig/openai/helpers/responses_websocket/incremental.rbs +14 -0
  112. data/sig/openai/helpers/responses_websocket/transports/async_websocket.rbs +21 -0
  113. data/sig/openai/helpers/streaming/response_stream.rbs +9 -0
  114. data/sig/openai/internal/type/converter.rbs +3 -1
  115. data/sig/openai/models/audio/speech_create_params.rbs +6 -0
  116. data/sig/openai/models/audio/transcription_create_response.rbs +1 -0
  117. data/sig/openai/models/audio/translation_create_response.rbs +3 -1
  118. data/sig/openai/models/beta/agent.rbs +3 -1
  119. data/sig/openai/models/beta/agent_create_params.rbs +5 -1
  120. data/sig/openai/models/beta/agent_session.rbs +3 -1
  121. data/sig/openai/models/beta/agent_update_params.rbs +5 -1
  122. data/sig/openai/models/beta/agents/environments/template_create_params.rbs +2 -2
  123. data/sig/openai/models/beta/agents/environments/template_update_params.rbs +4 -4
  124. data/sig/openai/models/beta/agents/session_create_params.rbs +5 -1
  125. data/sig/openai/models/beta/agents/session_update_params.rbs +5 -1
  126. data/sig/openai/models/beta/agents/vaults/credential.rbs +8 -0
  127. data/sig/openai/models/beta/agents/vaults/credential_create_params.rbs +14 -0
  128. data/sig/openai/models/beta/agents/vaults/credential_update_params.rbs +23 -4
  129. data/sig/openai/models/beta/beta_response.rbs +37 -0
  130. data/sig/openai/models/beta/beta_responses_client_event.rbs +79 -0
  131. data/sig/openai/models/beta/beta_web_search_preview_tool.rbs +6 -2
  132. data/sig/openai/models/beta/beta_web_search_tool.rbs +6 -2
  133. data/sig/openai/models/beta/environment_param.rbs +2 -2
  134. data/sig/openai/models/beta/response_compact_params.rbs +2 -0
  135. data/sig/openai/models/beta/response_create_params.rbs +79 -0
  136. data/sig/openai/models/beta/session_turn_error.rbs +4 -0
  137. data/sig/openai/models/chat_model.rbs +2 -0
  138. data/sig/openai/models/responses/response.rbs +35 -0
  139. data/sig/openai/models/responses/response_compact_params.rbs +2 -0
  140. data/sig/openai/models/responses/response_create_params.rbs +77 -0
  141. data/sig/openai/models/responses/responses_client_event.rbs +77 -0
  142. data/sig/openai/models/responses/web_search_preview_tool.rbs +6 -2
  143. data/sig/openai/models/responses/web_search_tool.rbs +6 -2
  144. data/sig/openai/resources/beta/agents/vaults/credentials.rbs +3 -1
  145. data/sig/openai/resources/beta/responses.rbs +2 -0
  146. data/sig/openai/resources/responses.rbs +4 -0
  147. 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