openai 0.90.0 → 0.91.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 +19 -0
- data/README.md +1 -1
- data/examples/realtime/README.md +8 -0
- data/examples/realtime/browser/README.md +167 -0
- data/examples/realtime/browser/index.html +19 -0
- data/examples/realtime/browser/page.js +21 -0
- data/examples/realtime/browser/peer.js +180 -0
- data/examples/realtime/browser/server.rb +281 -0
- data/examples/responses/websocket_workflows.rb +66 -6
- data/lib/openai/auth/workload_identity.rb +5 -1
- data/lib/openai/auth/workload_identity_auth.rb +2 -2
- data/lib/openai/auth/x509_token_exchange.rb +1 -1
- data/lib/openai/helpers/local_audio/process.rb +1 -1
- data/lib/openai/helpers/responses_websocket/connection.rb +2 -1
- data/lib/openai/helpers/responses_websocket/session.rb +551 -0
- data/lib/openai/helpers/responses_websocket.rb +1 -0
- data/lib/openai/helpers/sorbet.rb +1 -1
- data/lib/openai/helpers/websocket/async_websocket_transport.rb +6 -1
- data/lib/openai/internal/poller.rb +2 -2
- data/lib/openai/internal/transport/base_client.rb +1 -1
- data/lib/openai/internal/util.rb +1 -1
- data/lib/openai/models/beta/agents/session_update_params.rb +108 -1
- data/lib/openai/models/beta/beta_response.rb +9 -3
- data/lib/openai/models/beta/beta_response_compaction_compacting_event.rb +75 -0
- data/lib/openai/models/beta/beta_response_stream_event.rb +4 -1
- data/lib/openai/models/beta/beta_responses_client_event.rb +25 -4
- data/lib/openai/models/beta/beta_responses_server_event.rb +39 -1
- data/lib/openai/models/beta/response_compact_params.rb +9 -3
- data/lib/openai/models/beta/response_create_params.rb +21 -4
- data/lib/openai/models/beta/session_turn_error.rb +3 -0
- data/lib/openai/models/chat_model.rb +3 -1
- data/lib/openai/models/responses/response_compact_params.rb +9 -3
- data/lib/openai/models/responses/response_compaction_compacting_event.rb +49 -0
- data/lib/openai/models/responses/response_create_params.rb +12 -1
- data/lib/openai/models/responses/response_stream_event.rb +4 -1
- data/lib/openai/models/responses/responses_client_event.rb +12 -1
- data/lib/openai/models/responses/responses_server_event.rb +36 -1
- data/lib/openai/models/webhooks/realtime_call_incoming_webhook_event.rb +6 -8
- data/lib/openai/resources/beta/agents/sessions.rb +6 -2
- data/lib/openai/resources/beta/responses.rb +2 -2
- data/lib/openai/resources/responses.rb +2 -2
- data/lib/openai/version.rb +1 -1
- data/lib/openai.rb +2 -0
- data/rbi/openai/helpers/responses_websocket/connection.rbi +5 -0
- data/rbi/openai/helpers/responses_websocket/session.rbi +206 -0
- data/rbi/openai/models/beta/agents/session_update_params.rbi +178 -0
- data/rbi/openai/models/beta/beta_response.rbi +6 -1
- data/rbi/openai/models/beta/beta_response_compaction_compacting_event.rbi +136 -0
- data/rbi/openai/models/beta/beta_response_stream_event.rbi +1 -0
- data/rbi/openai/models/beta/beta_responses_client_event.rbi +27 -4
- data/rbi/openai/models/beta/beta_responses_server_event.rbi +77 -0
- data/rbi/openai/models/beta/response_compact_params.rbi +6 -1
- data/rbi/openai/models/beta/response_create_params.rbi +21 -1
- data/rbi/openai/models/beta/session_turn_error.rbi +6 -0
- data/rbi/openai/models/chat_model.rbi +3 -1
- data/rbi/openai/models/responses/response_compact_params.rbi +6 -1
- data/rbi/openai/models/responses/response_compaction_compacting_event.rbi +78 -0
- data/rbi/openai/models/responses/response_create_params.rbi +15 -0
- data/rbi/openai/models/responses/response_stream_event.rbi +1 -0
- data/rbi/openai/models/responses/responses_client_event.rbi +15 -0
- data/rbi/openai/models/responses/responses_server_event.rbi +71 -0
- data/rbi/openai/models/webhooks/realtime_call_incoming_webhook_event.rbi +6 -8
- data/rbi/openai/resources/beta/agents/sessions.rbi +5 -1
- data/responses-websocket.md +117 -10
- data/sig/openai/helpers/responses_websocket/connection.rbs +5 -0
- data/sig/openai/helpers/responses_websocket/session.rbs +123 -0
- data/sig/openai/models/beta/agents/session_update_params.rbs +99 -0
- data/sig/openai/models/beta/beta_response.rbs +6 -2
- data/sig/openai/models/beta/beta_response_compaction_compacting_event.rbs +58 -0
- data/sig/openai/models/beta/beta_response_stream_event.rbs +1 -0
- data/sig/openai/models/beta/beta_responses_client_event.rbs +13 -2
- data/sig/openai/models/beta/beta_responses_server_event.rbs +35 -0
- data/sig/openai/models/beta/response_compact_params.rbs +6 -2
- data/sig/openai/models/beta/response_create_params.rbs +13 -2
- data/sig/openai/models/beta/session_turn_error.rbs +4 -0
- data/sig/openai/models/chat_model.rbs +6 -2
- data/sig/openai/models/responses/response_compact_params.rbs +6 -2
- data/sig/openai/models/responses/response_compaction_compacting_event.rbs +37 -0
- data/sig/openai/models/responses/response_create_params.rbs +7 -0
- data/sig/openai/models/responses/response_stream_event.rbs +1 -0
- data/sig/openai/models/responses/responses_client_event.rbs +7 -0
- data/sig/openai/models/responses/responses_server_event.rbs +32 -0
- data/sig/openai/resources/beta/agents/sessions.rbs +1 -0
- metadata +16 -2
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
#!/usr/bin/env ruby
|
|
2
|
+
# frozen_string_literal: true
|
|
3
|
+
|
|
4
|
+
require "digest"
|
|
5
|
+
require "json"
|
|
6
|
+
require "timeout"
|
|
7
|
+
require "uri"
|
|
8
|
+
|
|
9
|
+
require_relative "../../../lib/openai"
|
|
10
|
+
require_relative "../sideband"
|
|
11
|
+
|
|
12
|
+
module OpenAI::Examples::Realtime
|
|
13
|
+
# Single-operator, loopback-only application. This is not SDK runtime code.
|
|
14
|
+
class BrowserApp
|
|
15
|
+
class Rejected < StandardError
|
|
16
|
+
attr_reader :status
|
|
17
|
+
def initialize(status)
|
|
18
|
+
@status = status
|
|
19
|
+
super("Request rejected")
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
ASSETS = {
|
|
24
|
+
"/" => ["index.html", "text/html"],
|
|
25
|
+
"/direct" => ["index.html", "text/html"],
|
|
26
|
+
"/backend" => ["index.html", "text/html"],
|
|
27
|
+
"/peer.js" => ["peer.js", "text/javascript"],
|
|
28
|
+
"/page.js" => ["page.js", "text/javascript"]
|
|
29
|
+
}.freeze
|
|
30
|
+
|
|
31
|
+
def initialize(
|
|
32
|
+
client:,
|
|
33
|
+
token:,
|
|
34
|
+
port: 9292,
|
|
35
|
+
model: "gpt-realtime-2.1",
|
|
36
|
+
sideband: false,
|
|
37
|
+
manual_turns: false,
|
|
38
|
+
clock: -> { Process.clock_gettime(Process::CLOCK_MONOTONIC) },
|
|
39
|
+
output: $stderr
|
|
40
|
+
)
|
|
41
|
+
raise ArgumentError, "Use an application token of at least 32 characters" if token.bytesize < 32
|
|
42
|
+
@client, @token, @model, @sideband = client, token, model, sideband
|
|
43
|
+
@manual_turns = manual_turns
|
|
44
|
+
@origin = URI::HTTP.build(host: "127.0.0.1", port: port).to_s
|
|
45
|
+
@authority = @origin.delete_prefix("http://")
|
|
46
|
+
@clock, @output = clock, output
|
|
47
|
+
@mutex = Mutex.new
|
|
48
|
+
@call = nil
|
|
49
|
+
@cancelled = {}
|
|
50
|
+
@next_create = 0
|
|
51
|
+
@closing = false
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def service(request, response)
|
|
55
|
+
response["Cache-Control"] = "no-store"
|
|
56
|
+
response["Referrer-Policy"] = "no-referrer"
|
|
57
|
+
response["X-Content-Type-Options"] = "nosniff"
|
|
58
|
+
response["Cross-Origin-Resource-Policy"] = "same-origin"
|
|
59
|
+
response["Permissions-Policy"] = "microphone=(self), camera=()"
|
|
60
|
+
response["Content-Security-Policy"] = "default-src 'none'; script-src 'self'; connect-src 'self' https://api.openai.com; media-src 'self' blob:; base-uri 'none'; frame-ancestors 'none'; form-action 'none'"
|
|
61
|
+
raise Rejected.new(403) unless request["host"] == @authority
|
|
62
|
+
|
|
63
|
+
if request.request_method == "GET" && ASSETS.key?(request.path)
|
|
64
|
+
file, content_type = ASSETS.fetch(request.path)
|
|
65
|
+
response["Content-Type"] = content_type
|
|
66
|
+
response.body = File.read(File.join(__dir__, file))
|
|
67
|
+
return
|
|
68
|
+
end
|
|
69
|
+
|
|
70
|
+
authenticate!(request)
|
|
71
|
+
@mutex.synchronize do
|
|
72
|
+
raise Rejected.new(503) if @closing
|
|
73
|
+
dispatch(request, response)
|
|
74
|
+
end
|
|
75
|
+
|
|
76
|
+
rescue Rejected => e
|
|
77
|
+
response.status = e.status
|
|
78
|
+
response.body = "Request rejected."
|
|
79
|
+
rescue StandardError
|
|
80
|
+
response.status = 502
|
|
81
|
+
response.body = "Signaling failed."
|
|
82
|
+
@output.puts("[browser] signaling failed")
|
|
83
|
+
end
|
|
84
|
+
|
|
85
|
+
# Called by the server timer even when the browser disappears without Stop.
|
|
86
|
+
def reap
|
|
87
|
+
@mutex.synchronize { cleanup if @call && @clock.call >= @call.fetch(:deadline) }
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
def shutdown
|
|
91
|
+
@mutex.synchronize do
|
|
92
|
+
@closing = true
|
|
93
|
+
cleanup if @call
|
|
94
|
+
end
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
private
|
|
98
|
+
|
|
99
|
+
def authenticate!(request)
|
|
100
|
+
raise Rejected.new(405) unless request.request_method == "POST"
|
|
101
|
+
raise Rejected.new(403) unless request["origin"] == @origin
|
|
102
|
+
expected = Digest::SHA256.digest("Bearer #{@token}")
|
|
103
|
+
supplied = Digest::SHA256.digest(request["authorization"].to_s)
|
|
104
|
+
difference = expected.bytes.zip(supplied.bytes).reduce(0) { |memo, (a, b)| memo | (a ^ b) }
|
|
105
|
+
raise Rejected.new(401) unless difference.zero?
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
def dispatch(request, response)
|
|
109
|
+
response["Content-Type"] = "application/json"
|
|
110
|
+
case request.path
|
|
111
|
+
when "/api/secret"
|
|
112
|
+
rate_limit!
|
|
113
|
+
secret = @client.realtime.client_secrets.create(
|
|
114
|
+
expires_after: {anchor: :created_at, seconds: 30},
|
|
115
|
+
session: session,
|
|
116
|
+
request_options: creation_options
|
|
117
|
+
)
|
|
118
|
+
# No call exists on this backend. Failed delivery leaves a secret which
|
|
119
|
+
# expires; there is no public secret revocation endpoint to invent here.
|
|
120
|
+
response.body = JSON.generate({value: secret.value})
|
|
121
|
+
when "/api/calls"
|
|
122
|
+
raise Rejected.new(415) unless request["content-type"] == "application/sdp"
|
|
123
|
+
id = operation_id(request)
|
|
124
|
+
raise Rejected.new(409) if @call || @cancelled.include?(id)
|
|
125
|
+
rate_limit!
|
|
126
|
+
create_call(id, request.body.to_s, response)
|
|
127
|
+
when "/api/ack", "/api/renew"
|
|
128
|
+
owned_call!(request)
|
|
129
|
+
raise Rejected.new(409) if @call[:ending] || @clock.call >= @call[:deadline]
|
|
130
|
+
raise Rejected.new(409) if request.path == "/api/renew" && !@call[:acknowledged]
|
|
131
|
+
@call[:acknowledged] = true
|
|
132
|
+
@call[:deadline] = @clock.call + 60
|
|
133
|
+
response.body = "{}"
|
|
134
|
+
when "/api/stop"
|
|
135
|
+
# Only application operation IDs are accepted, never browser call IDs.
|
|
136
|
+
id = operation_id(request)
|
|
137
|
+
# A Stop can overtake its create on another HTTP connection. Retain the
|
|
138
|
+
# cancellation for this process lifetime; never allocate that ID later.
|
|
139
|
+
@cancelled[id] = true
|
|
140
|
+
if @call && @call[:id] == id
|
|
141
|
+
cleanup
|
|
142
|
+
raise Rejected.new(503) if @call
|
|
143
|
+
end
|
|
144
|
+
|
|
145
|
+
response.body = "{}"
|
|
146
|
+
else
|
|
147
|
+
raise Rejected.new(404)
|
|
148
|
+
end
|
|
149
|
+
end
|
|
150
|
+
|
|
151
|
+
def session
|
|
152
|
+
config = {type: :realtime, model: @model, instructions: "Keep answers short.", max_output_tokens: 128, tools: []}
|
|
153
|
+
config[:audio] = {input: {turn_detection: nil}} if @manual_turns
|
|
154
|
+
config
|
|
155
|
+
end
|
|
156
|
+
|
|
157
|
+
def creation_options
|
|
158
|
+
{
|
|
159
|
+
timeout: 15,
|
|
160
|
+
max_retries: 0,
|
|
161
|
+
extra_headers: {"OpenAI-Safety-Identifier" => Digest::SHA256.hexdigest(@token)}
|
|
162
|
+
}
|
|
163
|
+
end
|
|
164
|
+
|
|
165
|
+
def rate_limit!
|
|
166
|
+
raise Rejected.new(429) if @clock.call < @next_create
|
|
167
|
+
@next_create = @clock.call + 5
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
def operation_id(request)
|
|
171
|
+
id = request["x-operation-id"].to_s
|
|
172
|
+
raise Rejected.new(400) unless /\A[0-9a-f]{8}-(?:[0-9a-f]{4}-){3}[0-9a-f]{12}\z/.match?(id)
|
|
173
|
+
id
|
|
174
|
+
end
|
|
175
|
+
|
|
176
|
+
def owned_call!(request)
|
|
177
|
+
raise Rejected.new(404) unless @call && @call[:id] == operation_id(request)
|
|
178
|
+
end
|
|
179
|
+
|
|
180
|
+
def create_call(id, offer, response)
|
|
181
|
+
# Shipped SDK owns allocation/response buffering until this returns. Do not
|
|
182
|
+
# duplicate its interrupted-creation cleanup or retry uncertain creates.
|
|
183
|
+
result = @client.realtime.calls.create(sdp: offer, session: session, request_options: creation_options)
|
|
184
|
+
call_id = call_id_from_location(result.headers["location"])
|
|
185
|
+
raise "Creation returned no usable call identifier" unless call_id
|
|
186
|
+
@call = {id: id, call_id: call_id, deadline: @clock.call + 20}
|
|
187
|
+
begin
|
|
188
|
+
answer = result.body.to_a.join
|
|
189
|
+
if @sideband
|
|
190
|
+
Timeout.timeout(15) do
|
|
191
|
+
Sideband.run(client: @client, call_id: call_id, instructions: "Keep answers short.", output: @output)
|
|
192
|
+
end
|
|
193
|
+
end
|
|
194
|
+
|
|
195
|
+
payload = JSON.generate({sdp: answer})
|
|
196
|
+
@call[:deadline] = @clock.call + 20
|
|
197
|
+
response.chunked = true
|
|
198
|
+
response.body = proc do |socket|
|
|
199
|
+
begin
|
|
200
|
+
socket.write(payload)
|
|
201
|
+
rescue StandardError
|
|
202
|
+
@mutex.synchronize { cleanup if @call && @call[:id] == id }
|
|
203
|
+
raise
|
|
204
|
+
end
|
|
205
|
+
end
|
|
206
|
+
rescue StandardError
|
|
207
|
+
cleanup
|
|
208
|
+
raise
|
|
209
|
+
end
|
|
210
|
+
end
|
|
211
|
+
|
|
212
|
+
def call_id_from_location(location)
|
|
213
|
+
return unless location.is_a?(String)
|
|
214
|
+
url = URI.join("https://api.openai.com/v1/realtime/calls", location)
|
|
215
|
+
return unless url.scheme == "https" && url.host == "api.openai.com" && url.port == 443
|
|
216
|
+
return if url.userinfo || url.query || url.fragment
|
|
217
|
+
%r{\A/v1/realtime/calls/([A-Za-z0-9_-]+)\z}.match(url.path)&.captures&.first
|
|
218
|
+
rescue URI::InvalidURIError
|
|
219
|
+
nil
|
|
220
|
+
end
|
|
221
|
+
|
|
222
|
+
def cleanup
|
|
223
|
+
@call[:ending] = true
|
|
224
|
+
Timeout.timeout(5) do
|
|
225
|
+
@client.realtime.calls.hangup(@call.fetch(:call_id), request_options: {timeout: 5, max_retries: 0})
|
|
226
|
+
end
|
|
227
|
+
|
|
228
|
+
@call = nil
|
|
229
|
+
@output.puts("[browser] call released")
|
|
230
|
+
rescue OpenAI::Errors::NotFoundError
|
|
231
|
+
@call = nil
|
|
232
|
+
@output.puts("[browser] call released")
|
|
233
|
+
rescue StandardError
|
|
234
|
+
@call[:deadline] = @clock.call + 5
|
|
235
|
+
@output.puts("[browser] hangup failed; retry pending")
|
|
236
|
+
end
|
|
237
|
+
end
|
|
238
|
+
end
|
|
239
|
+
|
|
240
|
+
if $PROGRAM_NAME == __FILE__
|
|
241
|
+
require "webrick"
|
|
242
|
+
|
|
243
|
+
port = Integer(ENV.fetch("PORT", "9292"))
|
|
244
|
+
app = OpenAI::Examples::Realtime::BrowserApp.new(
|
|
245
|
+
client: OpenAI::Client.new(
|
|
246
|
+
api_key: ENV.fetch("OPENAI_API_KEY"),
|
|
247
|
+
base_url: "https://api.openai.com/v1",
|
|
248
|
+
max_retries: 0
|
|
249
|
+
),
|
|
250
|
+
token: ENV.fetch("BROWSER_APP_TOKEN"),
|
|
251
|
+
port: port,
|
|
252
|
+
model: ENV.fetch("OPENAI_REALTIME_MODEL", "gpt-realtime-2.1"),
|
|
253
|
+
sideband: ENV["BROWSER_SIDEBAND"] == "1",
|
|
254
|
+
manual_turns: ENV["BROWSER_MANUAL_TURNS"] == "1"
|
|
255
|
+
)
|
|
256
|
+
# Suppress HTTP logs: they can contain bearer credentials, SDP, or paths.
|
|
257
|
+
server = WEBrick::HTTPServer.new(
|
|
258
|
+
BindAddress: "127.0.0.1",
|
|
259
|
+
Port: port,
|
|
260
|
+
AccessLog: [],
|
|
261
|
+
Logger: WEBrick::Log.new(File::NULL),
|
|
262
|
+
RequestTimeout: 20
|
|
263
|
+
)
|
|
264
|
+
server.mount_proc("/") { |request, response| app.service(request, response) }
|
|
265
|
+
%w[INT TERM].each { |signal| Signal.trap(signal) { server.shutdown } }
|
|
266
|
+
sweeper = Thread.new do
|
|
267
|
+
loop do
|
|
268
|
+
sleep(1)
|
|
269
|
+
app.reap
|
|
270
|
+
end
|
|
271
|
+
end
|
|
272
|
+
|
|
273
|
+
begin
|
|
274
|
+
warn("[browser] ready http://127.0.0.1:#{port}/direct and /backend")
|
|
275
|
+
server.start
|
|
276
|
+
ensure
|
|
277
|
+
sweeper.kill
|
|
278
|
+
sweeper.join
|
|
279
|
+
app.shutdown
|
|
280
|
+
end
|
|
281
|
+
end
|
|
@@ -68,19 +68,79 @@ module OpenAI
|
|
|
68
68
|
end
|
|
69
69
|
end
|
|
70
70
|
|
|
71
|
+
def session_limits
|
|
72
|
+
# Application budgets for these short text examples, not service limits.
|
|
73
|
+
OpenAI::Responses::SessionLimits.new(
|
|
74
|
+
max_lanes: 8,
|
|
75
|
+
max_events_per_lane: 128,
|
|
76
|
+
max_events: 512,
|
|
77
|
+
max_bytes_per_lane: 16 * 1024 * 1024,
|
|
78
|
+
max_bytes: 32 * 1024 * 1024,
|
|
79
|
+
max_response_bytes: 64 * 1024 * 1024
|
|
80
|
+
)
|
|
81
|
+
end
|
|
82
|
+
|
|
71
83
|
def multiplex(client:, model:)
|
|
72
|
-
client
|
|
73
|
-
%w[planner critic].
|
|
74
|
-
|
|
84
|
+
OpenAI::Responses::Session.open(client: client, limits: session_limits) do |session|
|
|
85
|
+
lanes = %w[planner critic].to_h { |name| [name, session.lane(name)] }
|
|
86
|
+
lanes.each_value do |lane|
|
|
87
|
+
lane.send_event(
|
|
88
|
+
type: :"response.create",
|
|
75
89
|
model: model,
|
|
76
|
-
input: "Say hello as the #{lane}.",
|
|
77
|
-
stream_id: lane,
|
|
90
|
+
input: "Say hello as the #{lane.stream_id}.",
|
|
78
91
|
store: false
|
|
79
92
|
)
|
|
80
93
|
end
|
|
81
94
|
|
|
82
|
-
|
|
95
|
+
results = Thread::Queue.new
|
|
96
|
+
readers = []
|
|
97
|
+
begin
|
|
98
|
+
readers <<
|
|
99
|
+
Async::Task.current.async do
|
|
100
|
+
loop do
|
|
101
|
+
event = session.default.receive
|
|
102
|
+
next unless event.type.to_s == "error"
|
|
103
|
+
|
|
104
|
+
results.push(OpenAI::Responses::RequestError.new(event))
|
|
105
|
+
break
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
rescue StandardError
|
|
109
|
+
# Named readers report connection failure, or drain terminal
|
|
110
|
+
# responses already queued before a normal connection close.
|
|
111
|
+
nil
|
|
112
|
+
end
|
|
113
|
+
lanes.each do |name, lane|
|
|
114
|
+
readers <<
|
|
115
|
+
Async::Task.current.async do
|
|
116
|
+
response = lane.get_final_response
|
|
117
|
+
unless response.status.to_s == "completed"
|
|
118
|
+
raise("Responses WebSocket operation did not complete.")
|
|
119
|
+
end
|
|
120
|
+
|
|
121
|
+
results.push([name, response])
|
|
122
|
+
rescue StandardError => error
|
|
123
|
+
results.push(error)
|
|
124
|
+
end
|
|
125
|
+
end
|
|
126
|
+
|
|
127
|
+
lanes.length.times.to_h do
|
|
128
|
+
result = results.pop
|
|
129
|
+
raise result if result.is_a?(StandardError)
|
|
130
|
+
|
|
131
|
+
result
|
|
132
|
+
end
|
|
133
|
+
|
|
134
|
+
ensure
|
|
135
|
+
readers.each(&:stop)
|
|
136
|
+
readers.each(&:wait)
|
|
137
|
+
end
|
|
83
138
|
end
|
|
139
|
+
|
|
140
|
+
rescue OpenAI::Responses::RequestError
|
|
141
|
+
raise "Responses WebSocket operation did not complete."
|
|
142
|
+
rescue OpenAI::Responses::SessionError
|
|
143
|
+
raise "Responses WebSocket closed with unfinished work."
|
|
84
144
|
end
|
|
85
145
|
|
|
86
146
|
# Deliberately rotate a healthy connection after a completed turn. This does
|
|
@@ -30,7 +30,11 @@ module OpenAI
|
|
|
30
30
|
@identity_provider_id = identity_provider_id.to_s
|
|
31
31
|
@service_account_id = service_account_id.to_s
|
|
32
32
|
@provider = provider
|
|
33
|
-
@refresh_buffer_seconds = refresh_buffer_seconds.
|
|
33
|
+
@refresh_buffer_seconds = if refresh_buffer_seconds.is_a?(String)
|
|
34
|
+
Integer(refresh_buffer_seconds, 10)
|
|
35
|
+
else
|
|
36
|
+
Integer(refresh_buffer_seconds)
|
|
37
|
+
end
|
|
34
38
|
end
|
|
35
39
|
end
|
|
36
40
|
end
|
|
@@ -353,7 +353,7 @@ module OpenAI
|
|
|
353
353
|
in Net::HTTPBadRequest | Net::HTTPUnauthorized | Net::HTTPForbidden
|
|
354
354
|
raise(
|
|
355
355
|
OpenAI::Errors::OAuthError.new(
|
|
356
|
-
status: response.code
|
|
356
|
+
status: Integer(response.code, 10),
|
|
357
357
|
body: body,
|
|
358
358
|
headers: response.each_header.to_h
|
|
359
359
|
)
|
|
@@ -367,7 +367,7 @@ module OpenAI
|
|
|
367
367
|
raise(
|
|
368
368
|
OpenAI::Errors::APIError.new(
|
|
369
369
|
url: @token_exchange_url,
|
|
370
|
-
status: response.code
|
|
370
|
+
status: Integer(response.code, 10),
|
|
371
371
|
headers: response.each_header.to_h,
|
|
372
372
|
body: body,
|
|
373
373
|
message: "Token exchange failed with status #{response.code}"
|
|
@@ -149,7 +149,7 @@ module OpenAI
|
|
|
149
149
|
raise invalid_response("expires_in must be greater than zero and at most one hour", response: response)
|
|
150
150
|
end
|
|
151
151
|
|
|
152
|
-
{id: token, expires_in: expires_in
|
|
152
|
+
{id: token, expires_in: Float(expires_in)}
|
|
153
153
|
end
|
|
154
154
|
|
|
155
155
|
private def raise_error(response, body)
|
|
@@ -154,7 +154,8 @@ module OpenAI
|
|
|
154
154
|
data
|
|
155
155
|
end
|
|
156
156
|
|
|
157
|
-
|
|
157
|
+
# @api private
|
|
158
|
+
def encode_client_event(event)
|
|
158
159
|
validate_event_tree!(event)
|
|
159
160
|
payload = if event.is_a?(Hash)
|
|
160
161
|
OpenAI::Internal::Type::Unknown.dump(event, state: {can_retry: true})
|