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,309 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenAI
4
+ module Live
5
+ class TranscriptGrouper
6
+ # @api private
7
+ # The established Live display policy, driven only by public text intervals.
8
+ class Grouping
9
+ Turn = Struct.new(
10
+ :id,
11
+ :previous_id,
12
+ :speaker,
13
+ :text,
14
+ :start_ms,
15
+ :end_ms,
16
+ :emitted_bytes,
17
+ :can_drop,
18
+ :acknowledgment
19
+ ) do
20
+ def snapshot
21
+ Segment.new(id, previous_id, speaker, text.dup.freeze, start_ms, end_ms)
22
+ end
23
+ end
24
+
25
+ Acknowledgment = Data.define(:characters, :text)
26
+ ACKNOWLEDGMENTS = [
27
+ "aha",
28
+ "alright",
29
+ "gotcha",
30
+ "hm",
31
+ "hmm",
32
+ "mhm",
33
+ "mm",
34
+ "mm hmm",
35
+ "okay",
36
+ "ok",
37
+ "right",
38
+ "sure",
39
+ "uh huh",
40
+ "yeah",
41
+ "yep",
42
+ "yes"
43
+ ].freeze
44
+ # Match the JavaScript/Python policy, including BOM but not NEL.
45
+ SPACE = "\u0009\u000a\u000b\u000c\u000d\u0020\u00a0\u1680\u2000\u2001\u2002\u2003\u2004\u2005\u2006\u2007\u2008\u2009\u200a\u2028\u2029\u202f\u205f\u3000\ufeff"
46
+ SEPARATORS = SPACE + ".,!?;:\"'()[]{}"
47
+ TRIM = /\A[#{Regexp.escape(SEPARATORS)}]+|[#{Regexp.escape(SEPARATORS)}]+\z/
48
+ WHITESPACE = /[#{SPACE}]+/
49
+ # Ruby downcase omits the contextual final sigma used by JavaScript/Python.
50
+ FINAL_SIGMA = /([\p{Cased}&&\P{Case_Ignorable}]\p{Case_Ignorable}*)Σ(?!\p{Case_Ignorable}*[\p{Cased}&&\P{Case_Ignorable}])/
51
+
52
+ def initialize(options, id_prefix)
53
+ @options = options
54
+ @id_prefix = id_prefix
55
+ @next_id = 0
56
+ @acknowledgments = ACKNOWLEDGMENTS +
57
+ options.additional_acknowledgments.map { |text| normalize(text) }.reject(&:empty?)
58
+ @max_acknowledgment_length = @acknowledgments.map(&:length).max
59
+ end
60
+
61
+ def speaker = @current&.speaker
62
+
63
+ def process(fragments)
64
+ preferred = speaker || :user
65
+ ordered = fragments.partition { |part| part.speaker == preferred }.flatten(1)
66
+ first = ordered.first
67
+ return [] unless first
68
+
69
+ emitted = []
70
+ while (due = deadline) && due <= first.start_ms
71
+ emitted.concat(advance(due))
72
+ end
73
+
74
+ if @current && ordered.none? { |part| part.speaker == speaker }
75
+ emitted.concat(advance(first.start_ms, ordered.any? { |part| part.speaker == :user }))
76
+ end
77
+
78
+ ordered.each { |part| emitted.concat(ingest(part)) }
79
+ emitted
80
+ end
81
+
82
+ def advance(time_ms, incoming_user = false)
83
+ # Match #deadline exactly: subtracting fractional timings can round
84
+ # below the threshold and prevent the source-time loop from advancing.
85
+ if @current &&
86
+ @buffered &&
87
+ time_ms >= @current.end_ms +
88
+ @options.min_turn_separation_ms &&
89
+ !keep_backchannel?(time_ms)
90
+ @buffered = maybe_drop_backchannel(time_ms)
91
+ return promote if @buffered
92
+ end
93
+
94
+ if speaker == :assistant && !incoming_user && time_ms >= @current.end_ms + @options.assistant_silence_ms
95
+ return finish_current(:inactivity)
96
+ end
97
+
98
+ []
99
+ end
100
+
101
+ def deadline
102
+ return unless @current
103
+ if @buffered
104
+ separation = @current.end_ms + @options.min_turn_separation_ms
105
+ if might_be_backchannel? && @buffered.can_drop && !user_continued? && !recent_assistant?
106
+ return [separation, @buffered.end_ms + @options.backchannel_isolation_ms].max
107
+ end
108
+
109
+ return separation
110
+ end
111
+
112
+ @current.end_ms + @options.assistant_silence_ms if speaker == :assistant
113
+ end
114
+
115
+ def update_pending?
116
+ @current && @current.emitted_bytes && @current.text.bytesize > @current.emitted_bytes
117
+ end
118
+
119
+ def flush_update
120
+ update_pending? ? emit(@current, true) : []
121
+ end
122
+
123
+ def close(time_ms, reason)
124
+ pending = maybe_drop_backchannel(time_ms)
125
+ emitted = finish_current(reason)
126
+ @buffered = nil
127
+ if pending && !pending.text.empty?
128
+ emitted.concat(emit(pending)).concat(finish(pending, reason))
129
+ end
130
+
131
+ @last_assistant_end = nil if reason == :timestamp_reset
132
+ emitted
133
+ end
134
+
135
+ private
136
+
137
+ def ingest(fragment)
138
+ unless @current
139
+ @current = new_turn(fragment)
140
+ return emit(@current)
141
+ end
142
+
143
+ if fragment.speaker == speaker
144
+ append(@current, fragment, !@buffered.nil?)
145
+ return emit(@current)
146
+ end
147
+
148
+ @buffered = nil if speaker == :user && user_continued?
149
+ if speaker == :assistant
150
+ buffer(fragment)
151
+ return promote
152
+ end
153
+
154
+ within_duration = fragment.end_ms -
155
+ (@buffered&.start_ms || fragment.start_ms) < @options.backchannel_max_duration_ms
156
+ ack = acknowledgment(fragment, within_duration)
157
+ normalized = ack.text if within_duration
158
+ if fragment.start_ms < @current.end_ms + @options.min_turn_separation_ms
159
+ can_drop = normalized &&
160
+ !normalized.empty? &&
161
+ @acknowledgments.any? { |phrase| phrase.start_with?(normalized) }
162
+ buffer(fragment, !!can_drop, ack)
163
+ return []
164
+ end
165
+
166
+ if normalized && @acknowledgments.include?(normalized)
167
+ buffer(fragment, !@buffered.nil?, ack)
168
+ return []
169
+ end
170
+
171
+ @buffered = maybe_drop_backchannel(nil, fragment)
172
+ emitted = finish_current(:speaker_change)
173
+ if @buffered
174
+ @current = @buffered
175
+ @buffered = nil
176
+ append(@current, fragment)
177
+ else
178
+ @current = new_turn(fragment)
179
+ end
180
+
181
+ emitted.concat(emit(@current))
182
+ end
183
+
184
+ def new_turn(fragment)
185
+ id = "#{@id_prefix}_#{@next_id}".freeze
186
+ @next_id += 1
187
+ Turn.new(id, nil, fragment.speaker, fragment.text.dup, fragment.start_ms, fragment.end_ms, nil, true, nil)
188
+ end
189
+
190
+ def append(turn, fragment, separate = false)
191
+ separator = separate && /[\p{L}\p{N}]\z/.match?(turn.text) && /\A[\p{L}\p{N}]/.match?(fragment.text) ? " " : ""
192
+ turn.text << separator << fragment.text
193
+ turn.end_ms = [turn.end_ms, fragment.end_ms].max
194
+ end
195
+
196
+ def buffer(fragment, can_drop = nil, ack = nil)
197
+ if @buffered
198
+ append(@buffered, fragment)
199
+ else
200
+ @buffered = new_turn(fragment)
201
+ end
202
+
203
+ @buffered.can_drop = can_drop unless can_drop.nil?
204
+ @buffered.acknowledgment = ack
205
+ end
206
+
207
+ def promote
208
+ return [] unless @buffered
209
+
210
+ emitted = finish_current(:speaker_change)
211
+ @current = @buffered
212
+ @buffered = nil
213
+ emitted.concat(emit(@current))
214
+ end
215
+
216
+ def finish_current(reason)
217
+ turn = @current
218
+ @current = nil
219
+ turn ? finish(turn, reason) : []
220
+ end
221
+
222
+ def finish(turn, reason)
223
+ @last_assistant_end = turn.end_ms if turn.speaker == :assistant
224
+ turn.emitted_bytes ? emit(turn, true) + [Update.new(turn.snapshot, reason)] : []
225
+ end
226
+
227
+ def emit(turn, force = false)
228
+ return [] if turn.text.empty?
229
+ bytes = turn.text.bytesize
230
+ if turn.emitted_bytes
231
+ return [] if bytes == turn.emitted_bytes || (!force && bytes < turn.emitted_bytes * 2)
232
+ else
233
+ turn.previous_id = @last_id
234
+ @last_id = turn.id
235
+ end
236
+
237
+ turn.emitted_bytes = bytes
238
+
239
+ [Update.new(turn.snapshot, nil)]
240
+ end
241
+
242
+ def might_be_backchannel?
243
+ speaker == :user &&
244
+ @buffered&.speaker == :assistant &&
245
+ @buffered.end_ms -
246
+ @buffered.start_ms < @options.backchannel_max_duration_ms
247
+ end
248
+
249
+ def user_continued?
250
+ might_be_backchannel? && @buffered.can_drop && @current.end_ms > @buffered.end_ms
251
+ end
252
+
253
+ def recent_assistant?
254
+ @current &&
255
+ @buffered &&
256
+ @last_assistant_end &&
257
+ @buffered.start_ms < @last_assistant_end +
258
+ @options.backchannel_isolation_ms &&
259
+ @buffered.start_ms <= @current.start_ms
260
+ end
261
+
262
+ def keep_backchannel?(time_ms)
263
+ might_be_backchannel? &&
264
+ @buffered.can_drop &&
265
+ !user_continued? &&
266
+ !recent_assistant? &&
267
+ time_ms < @buffered.end_ms +
268
+ @options.backchannel_isolation_ms
269
+ end
270
+
271
+ def maybe_drop_backchannel(time_ms = nil, following = nil)
272
+ return @buffered unless might_be_backchannel?
273
+ return nil if user_continued?
274
+ return @buffered if recent_assistant?
275
+ return @buffered if following && following.start_ms < @buffered.end_ms + @options.backchannel_isolation_ms
276
+ return @buffered if !following && (!time_ms || time_ms < @buffered.end_ms + @options.backchannel_isolation_ms)
277
+
278
+ @buffered unless @buffered.can_drop
279
+ end
280
+
281
+ def acknowledgment(fragment, within_duration)
282
+ previous = @buffered&.acknowledgment
283
+ previous_characters = previous&.characters || 0
284
+ characters = previous_characters
285
+ fragment.text.each_char do |character|
286
+ break if characters > @max_acknowledgment_length
287
+
288
+ characters += 1 unless SEPARATORS.include?(character) || character == "-"
289
+ end
290
+
291
+ text = if characters == previous_characters && previous&.text
292
+ previous.text
293
+ elsif within_duration && characters <= @max_acknowledgment_length
294
+ normalize((@buffered&.text || "") + fragment.text)
295
+ end
296
+
297
+ Acknowledgment.new(characters, text)
298
+ end
299
+
300
+ def normalize(text)
301
+ text = text.gsub(FINAL_SIGMA) { "#{Regexp.last_match(1)}ς" }
302
+ text.downcase.tr("-", " ").gsub(TRIM, "").split(WHITESPACE).join(" ")
303
+ end
304
+ end
305
+
306
+ private_constant :Grouping
307
+ end
308
+ end
309
+ end