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
|
@@ -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
|
-
|
|
223
|
+
items.push(isolated_value(event.item))
|
|
213
224
|
|
|
214
225
|
when OpenAI::Models::Responses::ResponseContentPartAddedEvent
|
|
215
|
-
output =
|
|
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
|
-
|
|
229
|
+
items[event.output_index] = output
|
|
219
230
|
end
|
|
220
231
|
|
|
221
232
|
when OpenAI::Models::Responses::ResponseTextDeltaEvent
|
|
222
|
-
output =
|
|
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
|
-
|
|
239
|
+
items[event.output_index] = output
|
|
229
240
|
end
|
|
230
241
|
end
|
|
231
242
|
|
|
232
243
|
when OpenAI::Models::Responses::ResponseFunctionCallArgumentsDeltaEvent
|
|
233
|
-
output =
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
383
|
+
port = url.port || default_port
|
|
384
|
+
return host if !include_default_port && port == default_port
|
|
380
385
|
|
|
381
|
-
"#{host}:#{
|
|
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),
|
|
@@ -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
|
-
#
|
|
59
|
-
# @api private
|
|
58
|
+
# @api private
|
|
60
59
|
#
|
|
61
|
-
#
|
|
60
|
+
# Traverse arbitrary JSON containers without consuming the Ruby stack.
|
|
61
|
+
# Delegate model, file, and scalar conversion to the existing converter.
|
|
62
62
|
#
|
|
63
|
-
#
|
|
63
|
+
# @param value [Object]
|
|
64
64
|
#
|
|
65
|
-
#
|
|
65
|
+
# @param state [Hash{Symbol=>Object}] .
|
|
66
66
|
#
|
|
67
|
-
# @
|
|
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
|
#
|
data/lib/openai/internal/util.rb
CHANGED
|
@@ -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
|