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
@@ -7,6 +7,147 @@ module OpenAI
7
7
  #
8
8
  # @api private
9
9
  class AsyncWebSocket < OpenAI::WebSocket::AsyncWebSocketTransport
10
+ # HTTP/1's buffered stream can inspect RSV1 without consuming bytes.
11
+ # Leave every part of frame parsing to the installed native protocol gem.
12
+ module PeekableFramer
13
+ # @api private
14
+ def next_frame_compressed?
15
+ first_byte = @stream.peek(1)&.getbyte(0) || 0
16
+ (first_byte & (::Protocol::WebSocket::Frame::RSV1 << 4)) != 0
17
+ end
18
+ end
19
+
20
+ private_constant :PeekableFramer
21
+
22
+ # Native frame parsing bounds each allocation before reading its body. Keep
23
+ # room for legal control frames, then enforce the exact data-only total.
24
+ class BoundedFramer
25
+ def initialize(framer, max_bytes:, max_frames:, max_plain_bytes: nil)
26
+ @framer = framer
27
+ @max_bytes = max_bytes
28
+ @max_frames = max_frames
29
+ @max_plain_bytes = max_plain_bytes
30
+ @message_bytes = 0
31
+ @message_frames = 0
32
+ @aborted = false
33
+ @compressed = false
34
+ @compression_negotiated = false
35
+ end
36
+
37
+ # @api private
38
+ attr_writer :compression_negotiated
39
+
40
+ def read_frame
41
+ limit = @max_bytes
42
+ if @max_plain_bytes
43
+ # Continuations of compressed messages never set RSV1 themselves.
44
+ # An unsolicited RSV1 cannot enlarge the limit when no compression
45
+ # was negotiated. The gem still validates reserved bits and opcodes.
46
+ compressed = @compression_negotiated &&
47
+ (@message_frames.zero? ? @framer.next_frame_compressed? : @compressed)
48
+ limit = [limit, @max_plain_bytes].min unless compressed
49
+ end
50
+
51
+ maximum_size = if @max_frames && @message_frames >= @max_frames
52
+ 125
53
+ elsif limit
54
+ [limit - @message_bytes, 125].max
55
+ else
56
+ ::Protocol::WebSocket::MAXIMUM_ALLOWED_FRAME_SIZE
57
+ end
58
+
59
+ frame = @framer.read_frame(maximum_size)
60
+ unless frame.control?
61
+ if @message_frames.zero?
62
+ @compressed = @compression_negotiated && frame.flag?(::Protocol::WebSocket::Frame::RSV1)
63
+ end
64
+
65
+ @message_bytes += frame.length
66
+ @message_frames += 1
67
+ if (@max_bytes && @message_bytes > @max_bytes) ||
68
+ (@max_frames && @message_frames > @max_frames) ||
69
+ (!@compressed && @max_plain_bytes && @message_bytes > @max_plain_bytes)
70
+ raise ::Protocol::WebSocket::ProtocolError, "Responses WebSocket message exceeds configured limit."
71
+ end
72
+
73
+ if frame.finished?
74
+ @message_bytes = 0
75
+ @message_frames = 0
76
+ end
77
+ end
78
+
79
+ frame
80
+ rescue ::Protocol::WebSocket::ProtocolError
81
+ # Native protocol-error cleanup would flush to the rejected connection.
82
+ self.abort
83
+ raise
84
+ end
85
+
86
+ def write_frame(frame) = @framer.write_frame(frame)
87
+
88
+ def flush = @framer.flush
89
+
90
+ def close
91
+ @framer.close unless @aborted
92
+ end
93
+
94
+ def abort
95
+ return if @aborted
96
+
97
+ @aborted = true
98
+ @framer.abort
99
+ end
100
+ end
101
+
102
+ private_constant :BoundedFramer
103
+
104
+ # Keep the negotiated native reader, frame parsing and RSV checks. Bound
105
+ # inflation before collecting its full output; Zlib yields at most a
106
+ # native output chunk beyond the configured decoded-message limit.
107
+ module BoundedInflate
108
+ # @api private
109
+ def bound_decoded_messages(max_bytes, framer)
110
+ @responses_decoded_limit = max_bytes
111
+ @responses_bounded_framer = framer
112
+ end
113
+
114
+ private def inflate(buffer)
115
+ inflater = @responses_inflater ||= ::Zlib::Inflate.new(-window_bits)
116
+ start_out = inflater.total_out
117
+ decoded = +"".b
118
+ begin
119
+ inflater.inflate(buffer + ::Protocol::WebSocket::Extension::Compression::Inflate::TRAILER) do |chunk|
120
+ if inflater.total_out - start_out > @responses_decoded_limit
121
+ raise ::Protocol::WebSocket::ProtocolError, "Responses WebSocket message exceeds configured limit."
122
+ end
123
+
124
+ decoded << chunk
125
+ end
126
+ # A PMCE SYNC_FLUSH does not yield the partial final chunk.
127
+ # Check the produced byte count before extracting that tail.
128
+ if inflater.total_out - start_out > @responses_decoded_limit
129
+ raise ::Protocol::WebSocket::ProtocolError, "Responses WebSocket message exceeds configured limit."
130
+ end
131
+
132
+ decoded << inflater.flush_next_out
133
+ rescue ::Protocol::WebSocket::ProtocolError
134
+ @responses_bounded_framer.abort
135
+ inflater.close
136
+ @responses_inflater = nil
137
+ raise
138
+ ensure
139
+ unless context_takeover || @responses_inflater.nil?
140
+ inflater.close
141
+ @responses_inflater = nil
142
+ end
143
+ end
144
+
145
+ decoded
146
+ end
147
+ end
148
+
149
+ private_constant :BoundedInflate
150
+
10
151
  def initialize
11
152
  error_factory = lambda do |url:, message: nil, http_status: nil, **_options|
12
153
  OpenAI::Errors::ResponsesConnectionError.new(
@@ -23,6 +164,48 @@ module OpenAI
23
164
  dependency_message: "Responses WebSockets require the async-websocket gem. Add it to your Gemfile."
24
165
  )
25
166
  end
167
+
168
+ private def negotiation_options(endpoint_options)
169
+ max_bytes = endpoint_options.delete(:max_message_bytes)
170
+ max_frames = endpoint_options.delete(:max_message_frames)
171
+ max_wire_bytes = endpoint_options.delete(:max_wire_message_bytes)
172
+ return {} if max_bytes.nil? && max_frames.nil? && max_wire_bytes.nil?
173
+
174
+ {max_message_bytes: max_bytes, max_message_frames: max_frames, max_wire_message_bytes: max_wire_bytes}.each do |
175
+ name,
176
+ value
177
+ |
178
+ unless value.nil? || (value.is_a?(Integer) && value.positive?)
179
+ raise ArgumentError, "#{name} must be a positive Integer or nil"
180
+ end
181
+ end
182
+
183
+ handler = lambda do |framer, protocol, extensions, **options|
184
+ framer.extend(AbortableFramer, PeekableFramer)
185
+ bounded = BoundedFramer.new(
186
+ framer,
187
+ max_bytes: max_wire_bytes || max_bytes,
188
+ max_frames: max_frames,
189
+ max_plain_bytes: max_wire_bytes ? max_bytes : nil
190
+ )
191
+ connection = ::Async::WebSocket::Connection.call(bounded, protocol, extensions, **options)
192
+ if max_wire_bytes &&
193
+ max_bytes &&
194
+ connection.reader.is_a?(::Protocol::WebSocket::Extension::Compression::Inflate)
195
+ connection.reader.extend(BoundedInflate)
196
+ connection.reader.bound_decoded_messages(max_bytes, bounded)
197
+ bounded.compression_negotiated = true
198
+ end
199
+
200
+ connection
201
+ end
202
+
203
+ options = {handler: handler}
204
+ # A decoded-byte limit cannot safely use the gem's whole-message inflate.
205
+ # No byte limit keeps the gem's default compression negotiation unchanged.
206
+ options[:extensions] = ::Protocol::WebSocket::Extensions::Client.new([]) if max_bytes && !max_wire_bytes
207
+ options
208
+ end
26
209
  end
27
210
  end
28
211
  end
@@ -11,3 +11,4 @@ require_relative "responses_websocket/transports/async_websocket"
11
11
  require_relative "responses_websocket/client_extension"
12
12
  require_relative "responses_websocket/resources/responses_extension"
13
13
  require_relative "responses_websocket/session"
14
+ require_relative "responses_websocket/incremental"
@@ -207,40 +207,48 @@ module OpenAI
207
207
  return nil
208
208
  end
209
209
 
210
+ if event.is_a?(OpenAI::Models::Responses::ResponseCompletedEvent)
211
+ @completed_response = event.response
212
+ else
213
+ accumulate_output(event, current_snapshot.output)
214
+ end
215
+
216
+ current_snapshot
217
+ end
218
+
219
+ # @api private
220
+ def accumulate_output(event, items)
210
221
  case event
211
222
  when OpenAI::Models::Responses::ResponseOutputItemAddedEvent
212
- current_snapshot.output.push(isolated_value(event.item))
223
+ items.push(isolated_value(event.item))
213
224
 
214
225
  when OpenAI::Models::Responses::ResponseContentPartAddedEvent
215
- output = current_snapshot.output[event.output_index]
226
+ output = items[event.output_index]
216
227
  if output.is_a?(OpenAI::Models::Responses::ResponseOutputMessage)
217
228
  output.content.push(isolated_value(event.part))
218
- current_snapshot.output[event.output_index] = output
229
+ items[event.output_index] = output
219
230
  end
220
231
 
221
232
  when OpenAI::Models::Responses::ResponseTextDeltaEvent
222
- output = current_snapshot.output[event.output_index]
233
+ output = items[event.output_index]
223
234
  if output.is_a?(OpenAI::Models::Responses::ResponseOutputMessage)
224
235
  content = output.content[event.content_index]
225
236
  if content.is_a?(OpenAI::Models::Responses::ResponseOutputText)
226
237
  content.text = append_delta(content.text, event.delta)
227
238
  output.content[event.content_index] = content
228
- current_snapshot.output[event.output_index] = output
239
+ items[event.output_index] = output
229
240
  end
230
241
  end
231
242
 
232
243
  when OpenAI::Models::Responses::ResponseFunctionCallArgumentsDeltaEvent
233
- output = current_snapshot.output[event.output_index]
244
+ output = items[event.output_index]
234
245
  if output.is_a?(OpenAI::Models::Responses::ResponseFunctionToolCall)
235
246
  output.arguments = append_delta(output.arguments || "", event.delta)
236
- current_snapshot.output[event.output_index] = output
247
+ items[event.output_index] = output
237
248
  end
238
-
239
- when OpenAI::Models::Responses::ResponseCompletedEvent
240
- @completed_response = event.response
241
249
  end
242
250
 
243
- current_snapshot
251
+ nil
244
252
  end
245
253
 
246
254
  private
@@ -9,7 +9,6 @@ module OpenAI
9
9
  module ChatCompletionParser
10
10
  # @api private
11
11
  def self.build_unwrap(model, tool_models)
12
- # rubocop:disable Metrics/BlockLength
13
12
  -> (raw) do
14
13
  if model.is_a?(OpenAI::StructuredOutput::JsonSchemaConverter)
15
14
  raw[:choices]&.each do |choice|
@@ -47,7 +46,6 @@ module OpenAI
47
46
 
48
47
  raw
49
48
  end
50
- # rubocop:enable Metrics/BlockLength
51
49
  end
52
50
 
53
51
  # @api private
@@ -96,7 +96,6 @@ module OpenAI
96
96
 
97
97
  case parsed
98
98
  in {tools: Array => tools}
99
- # rubocop:disable Metrics/BlockLength
100
99
  mapped = tools.map do |tool|
101
100
  case tool
102
101
  in OpenAI::StructuredOutput::JsonSchemaConverter
@@ -132,7 +131,7 @@ module OpenAI
132
131
  tool
133
132
  end
134
133
  end
135
- # rubocop:enable Metrics/BlockLength
134
+
136
135
  tools.replace(mapped)
137
136
  else
138
137
  end
@@ -21,7 +21,6 @@ module OpenAI
21
21
  #
22
22
  # @return [Hash{Symbol=>Object}]
23
23
  def to_json_schema_inner(state:)
24
- # rubocop:disable Metrics/BlockLength
25
24
  OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.cache_def!(state, type: self) do
26
25
  path = state.fetch(:path)
27
26
  mergeable_keys = {[:anyOf] => 0, [:type] => 0}
@@ -52,7 +51,6 @@ module OpenAI
52
51
  }
53
52
  end
54
53
  end
55
- # rubocop:enable Metrics/BlockLength
56
54
  end
57
55
 
58
56
  private_class_method :new
@@ -192,7 +192,7 @@ module OpenAI
192
192
  # @api private
193
193
  def abort
194
194
  framer = @connection.framer
195
- framer.extend(AbortableFramer)
195
+ framer.extend(AbortableFramer) unless framer.respond_to?(:abort)
196
196
  framer.abort
197
197
  @aborted = true
198
198
  rescue StandardError => e
@@ -208,6 +208,7 @@ module OpenAI
208
208
 
209
209
  def open(url:, headers:, timeout:, **endpoint_options)
210
210
  load_dependencies(url)
211
+ connect_options = negotiation_options(endpoint_options)
211
212
 
212
213
  # Proxy credentials belong only on the CONNECT request assembled from
213
214
  # proxy configuration. Never forward a caller-supplied value to the
@@ -274,7 +275,8 @@ module OpenAI
274
275
  endpoint,
275
276
  request_target: request_target,
276
277
  headers: headers,
277
- timeout: timeout
278
+ timeout: timeout,
279
+ connect_options: connect_options
278
280
  )
279
281
  socket = build_socket(connection, url: url)
280
282
  begin
@@ -303,10 +305,12 @@ module OpenAI
303
305
  raise error, cause: error.cause
304
306
  end
305
307
 
306
- private def negotiate(client, endpoint, request_target:, headers:, timeout:)
308
+ private def negotiation_options(_endpoint_options) = {}
309
+
310
+ private def negotiate(client, endpoint, request_target:, headers:, timeout:, connect_options:)
307
311
  safe_headers = trace_safe_headers(headers)
308
312
  operation = lambda do
309
- client.connect(authority(endpoint.url), request_target, headers: safe_headers)
313
+ client.connect(authority(endpoint.url), request_target, headers: safe_headers, **connect_options)
310
314
  end
311
315
 
312
316
  return operation.call if timeout.nil?
@@ -376,9 +380,10 @@ module OpenAI
376
380
  host = url.hostname
377
381
  host = "[#{host}]" if host.include?(":")
378
382
  default_port = %w[https wss].include?(url.scheme) ? 443 : 80
379
- return host if !include_default_port && url.port == default_port
383
+ port = url.port || default_port
384
+ return host if !include_default_port && port == default_port
380
385
 
381
- "#{host}:#{url.port}"
386
+ "#{host}:#{port}"
382
387
  end
383
388
 
384
389
  private def proxy_uri(url)
@@ -27,6 +27,7 @@ module OpenAI
27
27
  request:,
28
28
  block_error_message:,
29
29
  abort_after_block:,
30
+ transport_error_factory: nil,
30
31
  transport_error_message: "transport must respond to open",
31
32
  reserved_options_error_message: nil
32
33
  )
@@ -36,6 +37,7 @@ module OpenAI
36
37
  @request = request
37
38
  @block_error_message = block_error_message
38
39
  @abort_after_block = abort_after_block
40
+ @transport_error_factory = transport_error_factory
39
41
  @transport_error_message = transport_error_message
40
42
  @reserved_options_error_message = reserved_options_error_message
41
43
  @transport_options = validated_transport_options(transport_options)
@@ -49,6 +51,7 @@ module OpenAI
49
51
  raise ArgumentError, @transport_error_message unless transport.respond_to?(:open)
50
52
 
51
53
  @request.call do |request, mark_handshake_completed|
54
+ block_error = nil
52
55
  transport
53
56
  .open(
54
57
  url: request.fetch(:url),
@@ -61,9 +64,15 @@ module OpenAI
61
64
  begin
62
65
  yield(connection)
63
66
  ensure
67
+ block_error = $ERROR_INFO
64
68
  cleanup(connection)
65
69
  end
66
70
  end
71
+
72
+ rescue StandardError => error
73
+ raise if @transport_error_factory.nil?
74
+ raise block_error, cause: block_error.cause if block_error
75
+ raise @transport_error_factory.call(url: request.fetch(:url), error: error), cause: nil
67
76
  end
68
77
  end
69
78
 
@@ -78,9 +78,7 @@ module OpenAI
78
78
  case max_len
79
79
  in nil
80
80
  # `loop` rescues StopIteration, but this method handles it below.
81
- # rubocop:disable Style/InfiniteLoop
82
81
  @buf << @stream.next.b while true
83
- # rubocop:enable Style/InfiniteLoop
84
82
  in Integer
85
83
  @buf << @stream.next.b while @buf.bytesize < max_len
86
84
  read_buffer(max_len)
@@ -17,7 +17,6 @@ module OpenAI
17
17
  #
18
18
  # @return [Enumerable<generic<Elem>>]
19
19
  private def iterator
20
- # rubocop:disable Metrics/BlockLength
21
20
  @iterator ||= OpenAI::Internal::Util.chain_fused(@stream) do |y|
22
21
  consume = false
23
22
  decoder = OpenAI::Helpers::Streaming::ResponseEventDecoder.new(model: @model)
@@ -62,7 +61,6 @@ module OpenAI
62
61
  end
63
62
  end
64
63
  end
65
- # rubocop:enable Metrics/BlockLength
66
64
  end
67
65
  end
68
66
  end
@@ -97,8 +97,6 @@ module OpenAI
97
97
  end
98
98
  end
99
99
 
100
- # rubocop:disable Style/CaseEquality
101
- # rubocop:disable Metrics/BlockLength
102
100
  define_method(name_sym) do
103
101
  target = type_fn.call
104
102
 
@@ -142,8 +140,6 @@ module OpenAI
142
140
  end
143
141
  end
144
142
  end
145
- # rubocop:enable Metrics/BlockLength
146
- # rubocop:enable Style/CaseEquality
147
143
  end
148
144
 
149
145
  # @api private
@@ -301,11 +297,14 @@ module OpenAI
301
297
  data = instance.to_h
302
298
  viability = instance.instance_variable_get(:@coerced)
303
299
 
304
- # rubocop:disable Metrics/BlockLength
305
300
  fields.each do |name, field|
306
301
  mode, required, target = field.fetch_values(:mode, :required, :type)
307
302
  api_name, nilable, const = field.fetch_values(:api_name, :nilable, :const)
308
303
  src_name = state.fetch(:translate_names) ? api_name : name
304
+ if state[:request_only] && mode == :coerce
305
+ keys.delete(src_name)
306
+ next
307
+ end
309
308
 
310
309
  unless val.key?(src_name)
311
310
  if required && mode != :dump && const == OpenAI::Internal::OMIT
@@ -337,7 +336,6 @@ module OpenAI
337
336
  viability.store(name, state.fetch(:error) || true)
338
337
  data.store(name, converted)
339
338
  end
340
- # rubocop:enable Metrics/BlockLength
341
339
 
342
340
  keys.each { data.store(_1, val.fetch(_1)) }
343
341
  instance
@@ -132,10 +132,13 @@ module OpenAI
132
132
  #
133
133
  # @param translate_names [Boolean]
134
134
  #
135
+ # @param request_only [Boolean] Exclude fields omitted by the request serializer.
136
+ #
135
137
  # @return [Hash{Symbol=>Object}]
136
- def new_coerce_state(translate_names: true)
138
+ def new_coerce_state(translate_names: true, request_only: false)
137
139
  {
138
140
  translate_names: translate_names,
141
+ request_only: request_only,
139
142
  strictness: true,
140
143
  exactness: {yes: 0, no: 0, maybe: 0},
141
144
  error: nil,
@@ -188,7 +191,6 @@ module OpenAI
188
191
  #
189
192
  # @return [Object]
190
193
  def coerce(target, value, state: OpenAI::Internal::Type::Converter.new_coerce_state)
191
- # rubocop:disable Metrics/BlockNesting
192
194
  exactness = state.fetch(:exactness)
193
195
 
194
196
  case target
@@ -278,7 +280,6 @@ module OpenAI
278
280
 
279
281
  exactness[:no] += 1
280
282
  value
281
- # rubocop:enable Metrics/BlockNesting
282
283
  end
283
284
 
284
285
  # @api private
@@ -345,6 +346,7 @@ module OpenAI
345
346
  T.type_alias do
346
347
  {
347
348
  translate_names: T::Boolean,
349
+ request_only: T::Boolean,
348
350
  strictness: T::Boolean,
349
351
  exactness: {yes: Integer, no: Integer, maybe: Integer},
350
352
  error: T.nilable(StandardError),
@@ -62,9 +62,7 @@ module OpenAI
62
62
  #
63
63
  # @return [Boolean]
64
64
  def ==(other)
65
- # rubocop:disable Style/CaseEquality
66
65
  OpenAI::Internal::Type::Enum === other && other.values.to_set == values.to_set
67
- # rubocop:enable Style/CaseEquality
68
66
  end
69
67
 
70
68
  # @api public
@@ -124,9 +124,6 @@ module OpenAI
124
124
  end
125
125
  end
126
126
 
127
- # rubocop:disable Style/HashEachMethods
128
- # rubocop:disable Style/CaseEquality
129
-
130
127
  # @api public
131
128
  #
132
129
  # @param other [Object]
@@ -256,9 +253,6 @@ module OpenAI
256
253
  end
257
254
  end
258
255
 
259
- # rubocop:enable Style/CaseEquality
260
- # rubocop:enable Style/HashEachMethods
261
-
262
256
  # @api private
263
257
  #
264
258
  # @param depth [Integer]
@@ -55,16 +55,44 @@ module OpenAI
55
55
  value
56
56
  end
57
57
 
58
- # @!method dump(value, state:)
59
- # @api private
58
+ # @api private
60
59
  #
61
- # @param value [Object]
60
+ # Traverse arbitrary JSON containers without consuming the Ruby stack.
61
+ # Delegate model, file, and scalar conversion to the existing converter.
62
62
  #
63
- # @param state [Hash{Symbol=>Object}] .
63
+ # @param value [Object]
64
64
  #
65
- # @option state [Boolean] :can_retry
65
+ # @param state [Hash{Symbol=>Object}] .
66
66
  #
67
- # @return [Object]
67
+ # @option state [Boolean] :can_retry
68
+ #
69
+ # @return [Object]
70
+ def dump(value, state:)
71
+ return super unless value.is_a?(Hash) || value.is_a?(Array)
72
+
73
+ result = value.is_a?(Hash) ? {} : []
74
+ copies = {}.compare_by_identity
75
+ copies[value] = result
76
+ pending = [[value, result]]
77
+ until pending.empty?
78
+ source, destination = pending.pop
79
+ entries = source.is_a?(Hash) ? source.each_pair : source.each_with_index.lazy.map { |item, i| [i, item] }
80
+ entries.each do |key, item|
81
+ if item.is_a?(Hash) || item.is_a?(Array)
82
+ unless copies.key?(item)
83
+ copies[item] = item.is_a?(Hash) ? {} : []
84
+ pending << [item, copies.fetch(item)]
85
+ end
86
+
87
+ destination[key] = copies.fetch(item)
88
+ else
89
+ destination[key] = super(item, state: state)
90
+ end
91
+ end
92
+ end
93
+
94
+ result
95
+ end
68
96
 
69
97
  # @api private
70
98
  #
@@ -629,7 +629,6 @@ module OpenAI
629
629
  #
630
630
  # @return [Object]
631
631
  def encode_content(headers, body)
632
- # rubocop:disable Style/CaseEquality
633
632
  content_type = headers["content-type"]
634
633
  case [content_type, body]
635
634
  in [OpenAI::Internal::Util::JSON_CONTENT, Hash | Array | -> { primitive?(_1) }]
@@ -652,7 +651,6 @@ module OpenAI
652
651
  else
653
652
  [headers, body]
654
653
  end
655
- # rubocop:enable Style/CaseEquality
656
654
  end
657
655
 
658
656
  # @api private
@@ -934,7 +932,6 @@ module OpenAI
934
932
  #
935
933
  # @return [Enumerable<Hash{Symbol=>Object}>]
936
934
  def decode_sse(lines)
937
- # rubocop:disable Metrics/BlockLength
938
935
  chain_fused(lines) do |y|
939
936
  blank = {event: nil, data: nil, id: nil, retry: nil}
940
937
  current = {}
@@ -973,7 +970,6 @@ module OpenAI
973
970
  else
974
971
  end
975
972
  end
976
- # rubocop:enable Metrics/BlockLength
977
973
 
978
974
  y << {**blank, **current} unless current.empty?
979
975
  end
@@ -142,6 +142,12 @@ module OpenAI
142
142
 
143
143
  variant const: -> { OpenAI::Models::Audio::SpeechCreateParams::Voice::CEDAR }
144
144
 
145
+ variant const: -> { OpenAI::Models::Audio::SpeechCreateParams::Voice::FABLE }
146
+
147
+ variant const: -> { OpenAI::Models::Audio::SpeechCreateParams::Voice::ONYX }
148
+
149
+ variant const: -> { OpenAI::Models::Audio::SpeechCreateParams::Voice::NOVA }
150
+
145
151
  # Custom voice reference.
146
152
  variant -> { OpenAI::Audio::SpeechCreateParams::Voice::ID }
147
153
 
@@ -184,6 +190,9 @@ module OpenAI
184
190
  VERSE = :verse
185
191
  MARIN = :marin
186
192
  CEDAR = :cedar
193
+ FABLE = :fable
194
+ ONYX = :onyx
195
+ NOVA = :nova
187
196
 
188
197
  # @!endgroup
189
198
  end
@@ -21,8 +21,11 @@ module OpenAI
21
21
  # Represents a transcription response returned by model, based on the provided input.
22
22
  variant -> { OpenAI::Audio::Transcription }
23
23
 
24
+ # Text, SRT, and VTT responses retain the SDK's readable IO return value.
25
+ variant StringIO
26
+
24
27
  # @!method self.variants
25
- # @return [Array(OpenAI::Models::Audio::TranscriptionVerbose, OpenAI::Models::Audio::TranscriptionDiarized, OpenAI::Models::Audio::Transcription)]
28
+ # @return [Array(OpenAI::Models::Audio::TranscriptionVerbose, OpenAI::Models::Audio::TranscriptionDiarized, OpenAI::Models::Audio::Transcription, StringIO)]
26
29
  end
27
30
  end
28
31
  end