openai 0.106.0 → 0.108.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 +74 -0
- data/README.md +49 -1
- data/examples/chat/tool_runner.rb +33 -0
- data/lib/openai/client.rb +2 -2
- data/lib/openai/helpers/chat/tool_runner.rb +259 -0
- data/lib/openai/helpers/live/websocket.rb +12 -141
- data/lib/openai/helpers/local_audio/process.rb +13 -3
- data/lib/openai/helpers/models/embedding.rb +42 -0
- data/lib/openai/helpers/realtime/connection.rb +22 -5
- data/lib/openai/helpers/realtime/recovery.rb +19 -7
- data/lib/openai/helpers/realtime/translation.rb +37 -169
- data/lib/openai/helpers/realtime/unknown_server_event.rb +5 -12
- data/lib/openai/helpers/responses_websocket/connection.rb +56 -35
- data/lib/openai/helpers/responses_websocket/incremental.rb +5 -1
- data/lib/openai/helpers/responses_websocket/session.rb +11 -4
- data/lib/openai/helpers/responses_websocket/unknown_server_event.rb +2 -14
- data/lib/openai/helpers/streaming/chat_completion_resource.rb +52 -0
- data/lib/openai/helpers/websocket/async_websocket_transport.rb +8 -6
- data/lib/openai/helpers/websocket/client_request.rb +1 -1
- data/lib/openai/helpers/websocket/connection.rb +21 -10
- data/lib/openai/helpers/websocket/connection_manager.rb +2 -1
- data/lib/openai/helpers/websocket/protocol.rb +160 -0
- data/lib/openai/helpers/websocket/typed_connection.rb +22 -16
- data/lib/openai/helpers/websocket/typed_resource.rb +15 -0
- data/lib/openai/internal/transport/base_client.rb +1 -1
- data/lib/openai/models/admin/organization/usage_audio_speeches_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_audio_transcriptions_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_code_interpreter_sessions_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_completions_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_costs_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_decisions_response.rb +14 -3
- data/lib/openai/models/admin/organization/usage_embeddings_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_file_search_calls_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_images_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_moderations_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_vector_stores_response.rb +16 -5
- data/lib/openai/models/admin/organization/usage_web_search_calls_response.rb +16 -5
- data/lib/openai/models/audio/voice_consent.rb +58 -0
- data/lib/openai/models/audio/voice_consent_create_params.rb +57 -0
- data/lib/openai/models/audio/voice_consent_delete_params.rb +22 -0
- data/lib/openai/models/audio/voice_consent_deleted.rb +34 -0
- data/lib/openai/models/audio/voice_consent_list_params.rb +42 -0
- data/lib/openai/models/audio/voice_consent_retrieve_params.rb +22 -0
- data/lib/openai/models/audio/voice_consent_update_params.rb +32 -0
- data/lib/openai/models/beta/beta_response_input_item.rb +10 -1
- data/lib/openai/models/responses/response_input_item.rb +10 -1
- data/lib/openai/resources/audio/voice_consents.rb +163 -0
- data/lib/openai/resources/audio.rb +5 -0
- data/lib/openai/version.rb +1 -1
- data/lib/openai.rb +9 -0
- data/rbi/openai/client.rbi +1 -8
- data/rbi/openai/helpers/chat/tool_runner.rbi +74 -0
- data/rbi/openai/helpers/realtime/translation.rbi +0 -5
- data/rbi/openai/helpers/realtime/unknown_server_event.rbi +6 -0
- data/rbi/openai/helpers/websocket.rbi +44 -6
- data/rbi/openai/internal/transport/base_client.rbi +2 -6
- data/rbi/openai/models/admin/organization/usage_audio_speeches_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_audio_transcriptions_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_code_interpreter_sessions_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_completions_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_costs_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_decisions_response.rbi +12 -0
- data/rbi/openai/models/admin/organization/usage_embeddings_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_file_search_calls_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_images_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_moderations_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_vector_stores_response.rbi +14 -2
- data/rbi/openai/models/admin/organization/usage_web_search_calls_response.rbi +14 -2
- data/rbi/openai/models/audio/voice_consent.rbi +86 -0
- data/rbi/openai/models/audio/voice_consent_create_params.rbi +92 -0
- data/rbi/openai/models/audio/voice_consent_delete_params.rbi +53 -0
- data/rbi/openai/models/audio/voice_consent_deleted.rbi +62 -0
- data/rbi/openai/models/audio/voice_consent_list_params.rbi +78 -0
- data/rbi/openai/models/audio/voice_consent_retrieve_params.rbi +53 -0
- data/rbi/openai/models/audio/voice_consent_update_params.rbi +62 -0
- data/rbi/openai/models/beta/beta_response_input_item.rbi +14 -1
- data/rbi/openai/models/response_format_json_schema.rbi +13 -11
- data/rbi/openai/models/responses/response_input_item.rbi +10 -1
- data/rbi/openai/resources/audio/voice_consents.rbi +108 -0
- data/rbi/openai/resources/audio.rbi +4 -0
- data/realtime.md +22 -3
- data/sig/openai/client.rbs +1 -1
- data/sig/openai/helpers/chat/tool_runner.rbs +48 -0
- data/sig/openai/helpers/realtime/translation.rbs +0 -2
- data/sig/openai/helpers/realtime/unknown_server_event.rbs +2 -0
- data/sig/openai/helpers/websocket.rbs +23 -6
- data/sig/openai/models/admin/organization/usage_audio_speeches_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_audio_transcriptions_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_code_interpreter_sessions_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_completions_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_costs_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_decisions_response.rbs +11 -1
- data/sig/openai/models/admin/organization/usage_embeddings_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_file_search_calls_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_images_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_moderations_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_vector_stores_response.rbs +15 -5
- data/sig/openai/models/admin/organization/usage_web_search_calls_response.rbs +15 -5
- data/sig/openai/models/audio/voice_consent.rbs +57 -0
- data/sig/openai/models/audio/voice_consent_create_params.rbs +61 -0
- data/sig/openai/models/audio/voice_consent_delete_params.rbs +25 -0
- data/sig/openai/models/audio/voice_consent_deleted.rbs +31 -0
- data/sig/openai/models/audio/voice_consent_list_params.rbs +57 -0
- data/sig/openai/models/audio/voice_consent_retrieve_params.rbs +25 -0
- data/sig/openai/models/audio/voice_consent_update_params.rbs +36 -0
- data/sig/openai/models/beta/beta_response_input_item.rbs +8 -0
- data/sig/openai/models/responses/response_input_item.rbs +8 -3
- data/sig/openai/resources/audio/voice_consents.rbs +38 -0
- data/sig/openai/resources/audio.rbs +2 -0
- metadata +31 -1
|
@@ -55,6 +55,8 @@ module OpenAI
|
|
|
55
55
|
|
|
56
56
|
# @api private
|
|
57
57
|
class SessionAccumulator
|
|
58
|
+
include OpenAI::WebSocket::Protocol
|
|
59
|
+
|
|
58
60
|
attr_reader :final_response, :error
|
|
59
61
|
|
|
60
62
|
def initialize(limit)
|
|
@@ -93,10 +95,10 @@ module OpenAI
|
|
|
93
95
|
if type == "response.output_item.done"
|
|
94
96
|
# Retain wire data independently of the mutable event returned by receive.
|
|
95
97
|
completed_item = OpenAI::Internal::Type::BaseModel.recursively_to_h(event.item, convert: false)
|
|
96
|
-
@items[event.output_index] =
|
|
98
|
+
@items[event.output_index] = parse_json(generate_json(completed_item))
|
|
97
99
|
elsif %w[response.completed response.failed response.incomplete].include?(type)
|
|
98
100
|
data = OpenAI::Internal::Type::BaseModel.recursively_to_h(event, convert: false)
|
|
99
|
-
data =
|
|
101
|
+
data = parse_json(generate_json(data))
|
|
100
102
|
response = data.fetch(:response)
|
|
101
103
|
if response[:output].nil?
|
|
102
104
|
response = response.merge(output: @items.sort.map { |_index, item| item.to_h })
|
|
@@ -117,7 +119,7 @@ module OpenAI
|
|
|
117
119
|
@state.handle_event(@decoder.decode(data))
|
|
118
120
|
end
|
|
119
121
|
|
|
120
|
-
rescue StandardError => error
|
|
122
|
+
rescue StandardError, SystemStackError => error
|
|
121
123
|
@error = error.is_a?(SessionError) ? error : SessionError.new("Could not collect the Responses final response.")
|
|
122
124
|
clear
|
|
123
125
|
end
|
|
@@ -313,6 +315,8 @@ module OpenAI
|
|
|
313
315
|
|
|
314
316
|
# Owns one connection and one Async reader. Use only its lanes to receive.
|
|
315
317
|
class Session
|
|
318
|
+
include OpenAI::WebSocket::Protocol
|
|
319
|
+
|
|
316
320
|
attr_reader :limits, :default
|
|
317
321
|
|
|
318
322
|
def self.open(
|
|
@@ -531,7 +535,7 @@ module OpenAI
|
|
|
531
535
|
value = @lanes.fetch(data[:stream_id], @lanes[nil])
|
|
532
536
|
return unless value
|
|
533
537
|
|
|
534
|
-
bytes =
|
|
538
|
+
bytes = generate_json(data).bytesize
|
|
535
539
|
if @events >= @limits.max_events || @bytes + bytes > @limits.max_bytes
|
|
536
540
|
raise BufferError, "The Responses session exceeded its buffered event budget."
|
|
537
541
|
end
|
|
@@ -544,6 +548,9 @@ module OpenAI
|
|
|
544
548
|
consumed(bytes)
|
|
545
549
|
raise
|
|
546
550
|
end
|
|
551
|
+
|
|
552
|
+
rescue SystemStackError
|
|
553
|
+
raise SessionError, "Could not buffer the Responses WebSocket event.", cause: nil
|
|
547
554
|
end
|
|
548
555
|
|
|
549
556
|
private def stop_reader
|
|
@@ -4,6 +4,8 @@ module OpenAI
|
|
|
4
4
|
module Responses
|
|
5
5
|
# A valid JSON event whose discriminator is newer than this SDK version.
|
|
6
6
|
class UnknownServerEvent
|
|
7
|
+
include OpenAI::WebSocket::Protocol
|
|
8
|
+
|
|
7
9
|
# @return [Symbol]
|
|
8
10
|
attr_reader :type
|
|
9
11
|
|
|
@@ -35,20 +37,6 @@ module OpenAI
|
|
|
35
37
|
|
|
36
38
|
alias to_s inspect
|
|
37
39
|
|
|
38
|
-
private def freeze_json(value)
|
|
39
|
-
case value
|
|
40
|
-
when Hash
|
|
41
|
-
value.each do |key, item|
|
|
42
|
-
freeze_json(key)
|
|
43
|
-
freeze_json(item)
|
|
44
|
-
end
|
|
45
|
-
|
|
46
|
-
when Array
|
|
47
|
-
value.each { |item| freeze_json(item) }
|
|
48
|
-
end
|
|
49
|
-
|
|
50
|
-
value.freeze
|
|
51
|
-
end
|
|
52
40
|
end
|
|
53
41
|
end
|
|
54
42
|
end
|
|
@@ -1,9 +1,61 @@
|
|
|
1
1
|
# frozen_string_literal: true
|
|
2
2
|
|
|
3
|
+
require_relative "../chat/tool_runner"
|
|
4
|
+
|
|
3
5
|
module OpenAI
|
|
4
6
|
module Resources
|
|
5
7
|
class Chat
|
|
6
8
|
class Completions
|
|
9
|
+
# Run function tools until the model returns a final, non-streaming completion.
|
|
10
|
+
# Handlers receive a JSON object with string keys. String results are sent
|
|
11
|
+
# verbatim; other results are JSON encoded. Callback errors propagate without
|
|
12
|
+
# retrying callbacks or sending their exception text to the model.
|
|
13
|
+
# max_rounds bounds API calls (including the final answer), defaulting to 10.
|
|
14
|
+
# A forced tool choice applies only to the first round. Callbacks run serially.
|
|
15
|
+
#
|
|
16
|
+
# @param tool_handlers [Hash{String=>#call}]
|
|
17
|
+
# @param max_rounds [Integer]
|
|
18
|
+
# @param params [Hash] Other Chat Completions create parameters.
|
|
19
|
+
# @return [OpenAI::Models::Chat::ChatCompletion]
|
|
20
|
+
def run_tools(model:, messages:, tools:, tool_handlers:, max_rounds: 10, **params)
|
|
21
|
+
OpenAI::Helpers::Chat::ToolRunner
|
|
22
|
+
.new(
|
|
23
|
+
completions: self,
|
|
24
|
+
params: params.merge(model: model, messages: messages, tools: tools),
|
|
25
|
+
tool_handlers: tool_handlers,
|
|
26
|
+
max_rounds: max_rounds
|
|
27
|
+
)
|
|
28
|
+
.run
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
# @api private
|
|
32
|
+
# Buffer through the normal transport, then decode inside the runner's
|
|
33
|
+
# error boundary. Transport exceptions remain outside that boundary.
|
|
34
|
+
def create_for_tool_runner(params)
|
|
35
|
+
parsed, options = OpenAI::Chat::CompletionCreateParams.dump_request(params)
|
|
36
|
+
model, tool_models = get_structured_output_models(parsed)
|
|
37
|
+
parse = OpenAI::Helpers::StructuredOutput::ChatCompletionParser.build_unwrap(model, tool_models)
|
|
38
|
+
response = @client.request(
|
|
39
|
+
method: :post,
|
|
40
|
+
path: "chat/completions",
|
|
41
|
+
body: parsed,
|
|
42
|
+
model: StringIO,
|
|
43
|
+
security: {bearer_auth: true},
|
|
44
|
+
options: options
|
|
45
|
+
)
|
|
46
|
+
|
|
47
|
+
begin
|
|
48
|
+
raw = JSON.parse(response.string, symbolize_names: true)
|
|
49
|
+
completion = OpenAI::Internal::Type::Converter.coerce(OpenAI::Chat::ChatCompletion, parse.call(raw))
|
|
50
|
+
completion._set_last_response(response.last_response)
|
|
51
|
+
completion
|
|
52
|
+
rescue JSON::ParserError, KeyError, NoMethodError, TypeError, OpenAI::Errors::ConversionError
|
|
53
|
+
raise OpenAI::Helpers::Chat::ToolRunner::Error, "Malformed Chat Completion response", cause: nil
|
|
54
|
+
ensure
|
|
55
|
+
response.close
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
|
|
7
59
|
def stream(params)
|
|
8
60
|
parsed, options = OpenAI::Chat::CompletionCreateParams.dump_request(params)
|
|
9
61
|
|
|
@@ -171,7 +171,9 @@ module OpenAI
|
|
|
171
171
|
@connection.read
|
|
172
172
|
rescue StandardError => e
|
|
173
173
|
error = @error_factory.call(url: @url, cause: e)
|
|
174
|
-
|
|
174
|
+
# The factory retains explicit diagnostics; do not render them as a
|
|
175
|
+
# native exception chain, which can include payloads or credentials.
|
|
176
|
+
raise error, cause: nil
|
|
175
177
|
end
|
|
176
178
|
|
|
177
179
|
def write(message)
|
|
@@ -179,14 +181,14 @@ module OpenAI
|
|
|
179
181
|
@connection.flush
|
|
180
182
|
rescue StandardError => e
|
|
181
183
|
error = @error_factory.call(url: @url, cause: e)
|
|
182
|
-
raise error, cause:
|
|
184
|
+
raise error, cause: nil
|
|
183
185
|
end
|
|
184
186
|
|
|
185
187
|
def close(code: 1000, reason: "")
|
|
186
188
|
@connection.close(code, reason)
|
|
187
189
|
rescue StandardError => e
|
|
188
190
|
error = @error_factory.call(url: @url, cause: e)
|
|
189
|
-
raise error, cause:
|
|
191
|
+
raise error, cause: nil
|
|
190
192
|
end
|
|
191
193
|
|
|
192
194
|
# @api private
|
|
@@ -197,7 +199,7 @@ module OpenAI
|
|
|
197
199
|
@aborted = true
|
|
198
200
|
rescue StandardError => e
|
|
199
201
|
error = @error_factory.call(url: @url, cause: e)
|
|
200
|
-
raise error, cause:
|
|
202
|
+
raise error, cause: nil
|
|
201
203
|
end
|
|
202
204
|
|
|
203
205
|
# @api private
|
|
@@ -302,7 +304,7 @@ module OpenAI
|
|
|
302
304
|
raise if e.equal?(block_error)
|
|
303
305
|
|
|
304
306
|
error = @error_factory.call(url: url, cause: e, http_status: handshake_status(e))
|
|
305
|
-
raise error, cause:
|
|
307
|
+
raise error, cause: nil
|
|
306
308
|
end
|
|
307
309
|
|
|
308
310
|
private def negotiation_options(_endpoint_options) = {}
|
|
@@ -373,7 +375,7 @@ module OpenAI
|
|
|
373
375
|
"#{@product_name} WebSockets require the `async-websocket` gem. " \
|
|
374
376
|
"Add `gem \"async-websocket\"` to your Gemfile."
|
|
375
377
|
error = @error_factory.call(url: url, message: message, cause: e)
|
|
376
|
-
raise error, cause:
|
|
378
|
+
raise error, cause: nil
|
|
377
379
|
end
|
|
378
380
|
|
|
379
381
|
private def authority(url, include_default_port: false)
|
|
@@ -140,7 +140,7 @@ module OpenAI
|
|
|
140
140
|
[request, deadline]
|
|
141
141
|
rescue Timeout::Error => e
|
|
142
142
|
error = timeout_error.call(error_url, e)
|
|
143
|
-
raise error, cause:
|
|
143
|
+
raise error, cause: nil
|
|
144
144
|
end
|
|
145
145
|
|
|
146
146
|
private def with_shared_websocket_base_url(request, path:, base_url:)
|
|
@@ -28,20 +28,13 @@ module OpenAI
|
|
|
28
28
|
end
|
|
29
29
|
|
|
30
30
|
private def write_text(text)
|
|
31
|
-
complete = false
|
|
32
31
|
super
|
|
33
|
-
complete = true
|
|
34
|
-
nil
|
|
35
32
|
rescue StandardError
|
|
36
33
|
raise transport_error, cause: nil
|
|
37
|
-
ensure
|
|
38
|
-
# Also retire a possibly buffered write when cancellation bypasses
|
|
39
|
-
# StandardError, before another sender or normal cleanup can flush it.
|
|
40
|
-
@poisoned = true unless complete
|
|
41
34
|
end
|
|
42
35
|
|
|
43
36
|
private def read_raw_message
|
|
44
|
-
raise connection_error(failed_read_message) if @poisoned
|
|
37
|
+
raise connection_error(failed_read_message), cause: nil if @poisoned
|
|
45
38
|
return nil if @closed
|
|
46
39
|
text = super
|
|
47
40
|
@closed = text.nil?
|
|
@@ -73,6 +66,7 @@ module OpenAI
|
|
|
73
66
|
def initialize(socket:, url:)
|
|
74
67
|
@socket = socket
|
|
75
68
|
@url = url
|
|
69
|
+
@poisoned = false
|
|
76
70
|
end
|
|
77
71
|
|
|
78
72
|
# Yield server events until the remote peer closes the connection.
|
|
@@ -100,6 +94,8 @@ module OpenAI
|
|
|
100
94
|
def receive_raw = read_raw_message
|
|
101
95
|
|
|
102
96
|
private def read_raw_message
|
|
97
|
+
raise connection_error("Cannot read from a failed WebSocket."), cause: nil if @poisoned
|
|
98
|
+
|
|
103
99
|
message = @socket.read
|
|
104
100
|
message&.to_str
|
|
105
101
|
end
|
|
@@ -108,7 +104,7 @@ module OpenAI
|
|
|
108
104
|
#
|
|
109
105
|
# @api private
|
|
110
106
|
def send_raw(data)
|
|
111
|
-
raise connection_error(closed_send_message) if closed?
|
|
107
|
+
raise connection_error(closed_send_message), cause: nil if closed?
|
|
112
108
|
|
|
113
109
|
text = data.dup
|
|
114
110
|
text.force_encoding(Encoding::UTF_8) if text.encoding == Encoding::BINARY
|
|
@@ -120,6 +116,7 @@ module OpenAI
|
|
|
120
116
|
|
|
121
117
|
# Close the connection.
|
|
122
118
|
def close(code: 1000, reason: "")
|
|
119
|
+
return abort if @poisoned
|
|
123
120
|
return if socket_closed?
|
|
124
121
|
|
|
125
122
|
@socket.close(code: code, reason: reason)
|
|
@@ -137,7 +134,7 @@ module OpenAI
|
|
|
137
134
|
end
|
|
138
135
|
|
|
139
136
|
# @return [Boolean]
|
|
140
|
-
def closed? = socket_closed?
|
|
137
|
+
def closed? = @poisoned || socket_closed?
|
|
141
138
|
|
|
142
139
|
private def parse_event(_data)
|
|
143
140
|
raise NotImplementedError
|
|
@@ -154,8 +151,22 @@ module OpenAI
|
|
|
154
151
|
private def socket_closed? = @socket.closed?
|
|
155
152
|
|
|
156
153
|
private def write_text(text)
|
|
154
|
+
complete = false
|
|
157
155
|
@socket.write(text)
|
|
156
|
+
complete = true
|
|
158
157
|
nil
|
|
158
|
+
ensure
|
|
159
|
+
unless complete
|
|
160
|
+
# Retire the physical socket immediately: an already-running reader
|
|
161
|
+
# can otherwise flush buffered application bytes while replying to close.
|
|
162
|
+
@poisoned = true
|
|
163
|
+
begin
|
|
164
|
+
@socket.abort
|
|
165
|
+
rescue StandardError
|
|
166
|
+
# Preserve the original write failure or cancellation.
|
|
167
|
+
nil
|
|
168
|
+
end
|
|
169
|
+
end
|
|
159
170
|
end
|
|
160
171
|
end
|
|
161
172
|
end
|
|
@@ -73,7 +73,8 @@ module OpenAI
|
|
|
73
73
|
|
|
74
74
|
rescue StandardError => error
|
|
75
75
|
raise if @transport_error_factory.nil?
|
|
76
|
-
|
|
76
|
+
# Preserve the native chain, not an SDK accessor's explicit diagnostics.
|
|
77
|
+
raise block_error, cause: Exception.instance_method(:cause).bind_call(block_error) if block_error
|
|
77
78
|
raise @transport_error_factory.call(url: request.fetch(:url), error: error), cause: nil
|
|
78
79
|
end
|
|
79
80
|
end
|
|
@@ -6,6 +6,166 @@ module OpenAI
|
|
|
6
6
|
#
|
|
7
7
|
# @api private
|
|
8
8
|
module Protocol
|
|
9
|
+
# JSON schemas and extension data do not have a transport-specific depth limit.
|
|
10
|
+
private def parse_json(data) = JSON.parse(data, symbolize_names: true, max_nesting: false)
|
|
11
|
+
|
|
12
|
+
private def generate_json(value) = JSON.generate(value, max_nesting: false)
|
|
13
|
+
|
|
14
|
+
private def freeze_json(value)
|
|
15
|
+
pending = [value]
|
|
16
|
+
visited = {}.compare_by_identity
|
|
17
|
+
until pending.empty?
|
|
18
|
+
item = pending.pop
|
|
19
|
+
next if visited.key?(item)
|
|
20
|
+
visited[item] = true
|
|
21
|
+
case item
|
|
22
|
+
when Hash
|
|
23
|
+
item.each { |key, val| pending.push(key, val) }
|
|
24
|
+
when Array
|
|
25
|
+
pending.concat(item)
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
item.freeze
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
value
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
# Unknown wire keys must not replace a selected model's renamed Ruby fields.
|
|
35
|
+
private def validate_wire_field_names!(event, payload)
|
|
36
|
+
pending = [[event, payload]]
|
|
37
|
+
until pending.empty?
|
|
38
|
+
value, raw = pending.pop
|
|
39
|
+
case value
|
|
40
|
+
when OpenAI::Internal::Type::BaseModel
|
|
41
|
+
value.class.fields.each do |name, field|
|
|
42
|
+
raise ArgumentError if name != field.fetch(:api_name) && raw.is_a?(Hash) && raw.key?(name)
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
value.to_h.each do |name, child|
|
|
46
|
+
field = value.class.fields[name]
|
|
47
|
+
key = field ? field.fetch(:api_name) : name
|
|
48
|
+
pending << [child, raw.is_a?(Hash) ? raw[key] : nil]
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
when Hash
|
|
52
|
+
value.each { |name, child| pending << [child, raw.is_a?(Hash) ? raw[name] : nil] }
|
|
53
|
+
when Array
|
|
54
|
+
value.each_with_index { |child, index| pending << [child, raw.is_a?(Array) ? raw[index] : nil] }
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
event
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
private def coerce_event(union, payload, outbound: false)
|
|
62
|
+
state = OpenAI::Internal::Type::Converter.new_coerce_state(request_only: outbound)
|
|
63
|
+
event = OpenAI::Internal::Type::Converter.coerce(union, payload, state: state)
|
|
64
|
+
raise ArgumentError if state[:error] || !state.fetch(:exactness).fetch(:no).zero?
|
|
65
|
+
ancestors = {}.compare_by_identity
|
|
66
|
+
pending = [[event, false]]
|
|
67
|
+
until pending.empty?
|
|
68
|
+
value, exiting = pending.pop
|
|
69
|
+
if exiting
|
|
70
|
+
ancestors.delete(value)
|
|
71
|
+
next
|
|
72
|
+
end
|
|
73
|
+
|
|
74
|
+
children = case value
|
|
75
|
+
when OpenAI::Internal::Type::BaseModel
|
|
76
|
+
data = value.to_h
|
|
77
|
+
validate_model_fields!(value)
|
|
78
|
+
value.class.fields.each do |name, field|
|
|
79
|
+
const = field.fetch(:const)
|
|
80
|
+
if field.fetch(:required) &&
|
|
81
|
+
field.fetch(:mode) != (outbound ? :coerce : :dump) &&
|
|
82
|
+
(!outbound || const == OpenAI::Internal::OMIT) &&
|
|
83
|
+
!data.key?(name)
|
|
84
|
+
raise ArgumentError
|
|
85
|
+
end
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
data.values
|
|
89
|
+
when Hash
|
|
90
|
+
value.values
|
|
91
|
+
when Array
|
|
92
|
+
value
|
|
93
|
+
else
|
|
94
|
+
next
|
|
95
|
+
end
|
|
96
|
+
|
|
97
|
+
raise ArgumentError if ancestors.key?(value)
|
|
98
|
+
ancestors[value] = true
|
|
99
|
+
pending << [value, true]
|
|
100
|
+
children.each { |child| pending << [child, false] }
|
|
101
|
+
end
|
|
102
|
+
|
|
103
|
+
event
|
|
104
|
+
end
|
|
105
|
+
|
|
106
|
+
private def validate_event_tree!(event, json_only: false, strict_keys: true)
|
|
107
|
+
ancestors = {}.compare_by_identity
|
|
108
|
+
pending = [[event, false]]
|
|
109
|
+
until pending.empty?
|
|
110
|
+
value, exiting = pending.pop
|
|
111
|
+
if exiting
|
|
112
|
+
ancestors.delete(value)
|
|
113
|
+
next
|
|
114
|
+
end
|
|
115
|
+
|
|
116
|
+
children = case value
|
|
117
|
+
when OpenAI::Internal::Type::BaseModel
|
|
118
|
+
raise ArgumentError if json_only
|
|
119
|
+
data = value.to_h
|
|
120
|
+
validate_model_fields!(value)
|
|
121
|
+
[data]
|
|
122
|
+
when Hash
|
|
123
|
+
if strict_keys
|
|
124
|
+
keys = {}
|
|
125
|
+
value.each_key do |key|
|
|
126
|
+
raise ArgumentError unless key.is_a?(String) || key.is_a?(Symbol)
|
|
127
|
+
name = key.to_s
|
|
128
|
+
raise ArgumentError if keys.key?(name)
|
|
129
|
+
keys[name] = true
|
|
130
|
+
end
|
|
131
|
+
end
|
|
132
|
+
|
|
133
|
+
value.values
|
|
134
|
+
when Array
|
|
135
|
+
value
|
|
136
|
+
when NilClass, TrueClass, FalseClass, String, Integer, Float, Symbol
|
|
137
|
+
next
|
|
138
|
+
else
|
|
139
|
+
raise ArgumentError if json_only
|
|
140
|
+
next
|
|
141
|
+
end
|
|
142
|
+
|
|
143
|
+
raise ArgumentError if ancestors.key?(value)
|
|
144
|
+
ancestors[value] = true
|
|
145
|
+
pending << [value, true]
|
|
146
|
+
children.each { |child| pending << [child, false] }
|
|
147
|
+
end
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
private def validate_model_fields!(model)
|
|
151
|
+
names = {}
|
|
152
|
+
model.to_h.each do |name, actual|
|
|
153
|
+
key = name.is_a?(String) ? name.to_sym : name
|
|
154
|
+
field = model.class.known_fields[key]
|
|
155
|
+
serialized_name = field ? field.fetch(:api_name).to_s : key.to_s
|
|
156
|
+
raise ArgumentError if names.key?(serialized_name)
|
|
157
|
+
names[serialized_name] = true
|
|
158
|
+
next unless field
|
|
159
|
+
const = field.fetch(:const)
|
|
160
|
+
next if const == OpenAI::Internal::OMIT
|
|
161
|
+
if const.is_a?(Symbol)
|
|
162
|
+
raise ArgumentError unless (actual.is_a?(String) || actual.is_a?(Symbol)) && actual.to_s == const.to_s
|
|
163
|
+
elsif actual != const
|
|
164
|
+
raise ArgumentError
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
|
|
9
169
|
private def discriminator_values(union)
|
|
10
170
|
union.variants.to_h do |variant|
|
|
11
171
|
value = variant.fields.fetch(:type).fetch(:const)
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
module OpenAI
|
|
4
4
|
module WebSocket
|
|
5
|
-
#
|
|
5
|
+
# Shared event codec for generated bindings and specialized product facades.
|
|
6
6
|
#
|
|
7
7
|
# @api private
|
|
8
8
|
class TypedConnection < Connection
|
|
@@ -30,36 +30,36 @@ module OpenAI
|
|
|
30
30
|
end
|
|
31
31
|
|
|
32
32
|
private def encode_client_event(event)
|
|
33
|
-
|
|
33
|
+
validate_event_tree!(event)
|
|
34
|
+
event_type(event, message: client_event_error_message) if event.is_a?(Hash)
|
|
34
35
|
payload = @client_event ? OpenAI::Internal::Type::Converter.dump(@client_event, event) : event
|
|
35
|
-
type = event_type(payload, message:
|
|
36
|
+
type = event_type(payload, message: client_event_error_message)
|
|
36
37
|
if @client_event && !@client_event_types.any? { _1.nil? || _1 === type.to_sym || _1 === type.to_s }
|
|
37
|
-
raise ArgumentError,
|
|
38
|
+
raise ArgumentError, client_event_error_message
|
|
38
39
|
end
|
|
39
40
|
|
|
40
|
-
payload =
|
|
41
|
+
payload = coerce_client_event(payload) if @client_event
|
|
41
42
|
payload = OpenAI::Internal::Type::Converter.dump(@client_event, payload) if @client_event
|
|
42
|
-
|
|
43
|
+
validate_event_tree!(payload, json_only: true)
|
|
44
|
+
generate_json(payload)
|
|
43
45
|
rescue StandardError, SystemStackError
|
|
44
|
-
raise ArgumentError,
|
|
46
|
+
raise ArgumentError, client_event_error_message, cause: nil
|
|
45
47
|
end
|
|
46
48
|
|
|
47
49
|
def parse_event(data)
|
|
48
|
-
parsed =
|
|
50
|
+
parsed = parse_json(data)
|
|
49
51
|
type = event_type(parsed, message: "WebSocket server event must be an object with a string type")
|
|
50
52
|
_, model = @server_event_types.find { |event_type,| event_type === type.to_sym }
|
|
51
|
-
return parsed unless model
|
|
53
|
+
return unknown_server_event(parsed) unless model
|
|
52
54
|
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
raise ArgumentError if name != field.fetch(:api_name) && parsed.key?(name)
|
|
56
|
-
end
|
|
57
|
-
|
|
58
|
-
coerce(@server_event, parsed)
|
|
55
|
+
event = coerce(@server_event, parsed)
|
|
56
|
+
validate_wire_field_names!(event, parsed)
|
|
59
57
|
rescue StandardError, SystemStackError
|
|
60
|
-
raise
|
|
58
|
+
raise protocol_error, cause: nil
|
|
61
59
|
end
|
|
62
60
|
|
|
61
|
+
private def coerce_client_event(payload) = coerce(@client_event, payload)
|
|
62
|
+
|
|
63
63
|
private def coerce(type, value)
|
|
64
64
|
state = OpenAI::Internal::Type::Converter.new_coerce_state
|
|
65
65
|
result = OpenAI::Internal::Type::Converter.coerce(type, value, state: state)
|
|
@@ -70,6 +70,12 @@ module OpenAI
|
|
|
70
70
|
result
|
|
71
71
|
end
|
|
72
72
|
|
|
73
|
+
private def client_event_error_message = "Invalid WebSocket client event."
|
|
74
|
+
|
|
75
|
+
private def unknown_server_event(payload) = payload
|
|
76
|
+
|
|
77
|
+
private def protocol_error = OpenAI::Errors::WebSocketProtocolError.new("Invalid WebSocket server event.")
|
|
78
|
+
|
|
73
79
|
private def connection_error(message) = BindingConnectionError.new(url: @url, message: message)
|
|
74
80
|
|
|
75
81
|
private def transport_error = connection_error("WebSocket transport failed.")
|
|
@@ -0,0 +1,15 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "resource"
|
|
4
|
+
|
|
5
|
+
module OpenAI
|
|
6
|
+
module WebSocket
|
|
7
|
+
# Supplies the generic signature contract for generated event bindings.
|
|
8
|
+
# Connection lifecycle and event handling are implemented by Resource.
|
|
9
|
+
#
|
|
10
|
+
# @api private
|
|
11
|
+
module TypedResource
|
|
12
|
+
include Resource
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
@@ -1012,7 +1012,7 @@ module OpenAI
|
|
|
1012
1012
|
T::Class[OpenAI::Internal::Type::BaseStream[T.anything, OpenAI::Internal::Type::BaseModel]]
|
|
1013
1013
|
),
|
|
1014
1014
|
model: T.nilable(OpenAI::Internal::Type::Converter::Input),
|
|
1015
|
-
security: T.nilable(
|
|
1015
|
+
security: T.nilable(T::Hash[Symbol, T::Boolean]),
|
|
1016
1016
|
options: T.nilable(OpenAI::RequestOptions::OrHash)
|
|
1017
1017
|
}
|
|
1018
1018
|
end
|
|
@@ -235,9 +235,16 @@ module OpenAI
|
|
|
235
235
|
# @return [Integer, nil]
|
|
236
236
|
optional :input_cache_write_12h_tokens, Integer
|
|
237
237
|
|
|
238
|
-
# @!attribute
|
|
238
|
+
# @!attribute input_cache_write_30m_tokens
|
|
239
239
|
# The aggregated number of input tokens written to the cache with a 30-minute
|
|
240
|
-
# retention period.
|
|
240
|
+
# retention period. May be absent or null for older responses.
|
|
241
|
+
#
|
|
242
|
+
# @return [Integer, nil]
|
|
243
|
+
optional :input_cache_write_30m_tokens, Integer, nil?: true
|
|
244
|
+
|
|
245
|
+
# @!attribute input_cache_write_tokens
|
|
246
|
+
# The aggregated number of input tokens written to the cache across 30-minute and
|
|
247
|
+
# 12-hour retention periods.
|
|
241
248
|
#
|
|
242
249
|
# @return [Integer, nil]
|
|
243
250
|
optional :input_cache_write_tokens, Integer
|
|
@@ -333,7 +340,7 @@ module OpenAI
|
|
|
333
340
|
# @return [String, nil]
|
|
334
341
|
optional :user_id, String, nil?: true
|
|
335
342
|
|
|
336
|
-
# @!method initialize(input_tokens:, num_model_requests:, output_tokens:, api_key_id: nil, api_source: nil, batch: nil, input_audio_tokens: nil, input_cache_write_12h_tokens: nil, input_cache_write_tokens: nil, input_cached_audio_tokens: nil, input_cached_image_tokens: nil, input_cached_text_tokens: nil, input_cached_tokens: nil, input_image_tokens: nil, input_text_tokens: nil, input_uncached_tokens: nil, model: nil, output_audio_tokens: nil, output_image_tokens: nil, output_text_tokens: nil, project_id: nil, service_tier: nil, user_id: nil, object: :"organization.usage.completions.result")
|
|
343
|
+
# @!method initialize(input_tokens:, num_model_requests:, output_tokens:, api_key_id: nil, api_source: nil, batch: nil, input_audio_tokens: nil, input_cache_write_12h_tokens: nil, input_cache_write_30m_tokens: nil, input_cache_write_tokens: nil, input_cached_audio_tokens: nil, input_cached_image_tokens: nil, input_cached_text_tokens: nil, input_cached_tokens: nil, input_image_tokens: nil, input_text_tokens: nil, input_uncached_tokens: nil, model: nil, output_audio_tokens: nil, output_image_tokens: nil, output_text_tokens: nil, project_id: nil, service_tier: nil, user_id: nil, object: :"organization.usage.completions.result")
|
|
337
344
|
# The aggregated completions usage details of the specific time bucket.
|
|
338
345
|
#
|
|
339
346
|
# @param input_tokens [Integer]
|
|
@@ -370,9 +377,13 @@ module OpenAI
|
|
|
370
377
|
# The aggregated number of input tokens written to the cache with a 12-hour
|
|
371
378
|
# retention period.
|
|
372
379
|
#
|
|
373
|
-
# @param
|
|
380
|
+
# @param input_cache_write_30m_tokens [Integer, nil]
|
|
374
381
|
# The aggregated number of input tokens written to the cache with a 30-minute
|
|
375
|
-
# retention period.
|
|
382
|
+
# retention period. May be absent or null for older responses.
|
|
383
|
+
#
|
|
384
|
+
# @param input_cache_write_tokens [Integer]
|
|
385
|
+
# The aggregated number of input tokens written to the cache across 30-minute and
|
|
386
|
+
# 12-hour retention periods.
|
|
376
387
|
#
|
|
377
388
|
# @param input_cached_audio_tokens [Integer]
|
|
378
389
|
# The aggregated number of cached audio input tokens used.
|
|
@@ -237,9 +237,16 @@ module OpenAI
|
|
|
237
237
|
# @return [Integer, nil]
|
|
238
238
|
optional :input_cache_write_12h_tokens, Integer
|
|
239
239
|
|
|
240
|
-
# @!attribute
|
|
240
|
+
# @!attribute input_cache_write_30m_tokens
|
|
241
241
|
# The aggregated number of input tokens written to the cache with a 30-minute
|
|
242
|
-
# retention period.
|
|
242
|
+
# retention period. May be absent or null for older responses.
|
|
243
|
+
#
|
|
244
|
+
# @return [Integer, nil]
|
|
245
|
+
optional :input_cache_write_30m_tokens, Integer, nil?: true
|
|
246
|
+
|
|
247
|
+
# @!attribute input_cache_write_tokens
|
|
248
|
+
# The aggregated number of input tokens written to the cache across 30-minute and
|
|
249
|
+
# 12-hour retention periods.
|
|
243
250
|
#
|
|
244
251
|
# @return [Integer, nil]
|
|
245
252
|
optional :input_cache_write_tokens, Integer
|
|
@@ -335,7 +342,7 @@ module OpenAI
|
|
|
335
342
|
# @return [String, nil]
|
|
336
343
|
optional :user_id, String, nil?: true
|
|
337
344
|
|
|
338
|
-
# @!method initialize(input_tokens:, num_model_requests:, output_tokens:, api_key_id: nil, api_source: nil, batch: nil, input_audio_tokens: nil, input_cache_write_12h_tokens: nil, input_cache_write_tokens: nil, input_cached_audio_tokens: nil, input_cached_image_tokens: nil, input_cached_text_tokens: nil, input_cached_tokens: nil, input_image_tokens: nil, input_text_tokens: nil, input_uncached_tokens: nil, model: nil, output_audio_tokens: nil, output_image_tokens: nil, output_text_tokens: nil, project_id: nil, service_tier: nil, user_id: nil, object: :"organization.usage.completions.result")
|
|
345
|
+
# @!method initialize(input_tokens:, num_model_requests:, output_tokens:, api_key_id: nil, api_source: nil, batch: nil, input_audio_tokens: nil, input_cache_write_12h_tokens: nil, input_cache_write_30m_tokens: nil, input_cache_write_tokens: nil, input_cached_audio_tokens: nil, input_cached_image_tokens: nil, input_cached_text_tokens: nil, input_cached_tokens: nil, input_image_tokens: nil, input_text_tokens: nil, input_uncached_tokens: nil, model: nil, output_audio_tokens: nil, output_image_tokens: nil, output_text_tokens: nil, project_id: nil, service_tier: nil, user_id: nil, object: :"organization.usage.completions.result")
|
|
339
346
|
# The aggregated completions usage details of the specific time bucket.
|
|
340
347
|
#
|
|
341
348
|
# @param input_tokens [Integer]
|
|
@@ -372,9 +379,13 @@ module OpenAI
|
|
|
372
379
|
# The aggregated number of input tokens written to the cache with a 12-hour
|
|
373
380
|
# retention period.
|
|
374
381
|
#
|
|
375
|
-
# @param
|
|
382
|
+
# @param input_cache_write_30m_tokens [Integer, nil]
|
|
376
383
|
# The aggregated number of input tokens written to the cache with a 30-minute
|
|
377
|
-
# retention period.
|
|
384
|
+
# retention period. May be absent or null for older responses.
|
|
385
|
+
#
|
|
386
|
+
# @param input_cache_write_tokens [Integer]
|
|
387
|
+
# The aggregated number of input tokens written to the cache across 30-minute and
|
|
388
|
+
# 12-hour retention periods.
|
|
378
389
|
#
|
|
379
390
|
# @param input_cached_audio_tokens [Integer]
|
|
380
391
|
# The aggregated number of cached audio input tokens used.
|