openai 0.97.0 → 0.98.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 +24 -0
- data/README.md +76 -1
- data/lib/openai/helpers/agents/session_stream.rb +60 -3
- data/lib/openai/helpers/beta/agents/artifacts.rb +72 -0
- data/lib/openai/helpers/beta/agents/creation_stream.rb +69 -0
- data/lib/openai/helpers/beta/agents/files.rb +236 -0
- data/lib/openai/helpers/beta/agents/function_tool.rb +65 -0
- data/lib/openai/helpers/beta/agents/model_adapter.rb +27 -0
- data/lib/openai/helpers/beta/agents/result.rb +170 -0
- data/lib/openai/helpers/beta/agents/typed_output.rb +191 -0
- data/lib/openai/helpers/live/websocket.rb +7 -1
- data/lib/openai/helpers/realtime/conversation_item_variant_resolver.rb +1 -1
- data/lib/openai/helpers/responses_websocket/connection.rb +7 -1
- data/lib/openai/helpers/responses_websocket/session.rb +5 -1
- data/lib/openai/helpers/responses_websocket/transports/async_websocket.rb +12 -2
- data/lib/openai/internal/transport/base_client.rb +18 -0
- data/lib/openai/internal/type/array_of.rb +1 -1
- data/lib/openai/internal/type/base_model.rb +6 -3
- data/lib/openai/internal/type/converter.rb +50 -3
- data/lib/openai/internal/type/hash_of.rb +1 -1
- data/lib/openai/internal/type/union.rb +6 -6
- data/lib/openai/models/beta/agents/sessions/session_trace.rb +64 -0
- data/lib/openai/models/beta/agents/sessions/trace_list_params.rb +68 -0
- data/lib/openai/models/beta/agents/vaults/credential.rb +8 -4
- data/lib/openai/models/beta/beta_response_output_text.rb +2 -2
- data/lib/openai/models/beta/beta_response_output_text_annotation_added_event.rb +2 -2
- data/lib/openai/models/beta/environment.rb +33 -1
- data/lib/openai/models/beta/environment_param.rb +24 -1
- data/lib/openai/models/beta/session_turn_error.rb +3 -0
- data/lib/openai/models/chat/chat_completion_content_part_image.rb +1 -0
- data/lib/openai/models/live/fork_server_event.rb +14 -3
- data/lib/openai/models/live/server_event.rb +14 -3
- data/lib/openai/models/realtime/translations/client_secret_create_params.rb +28 -0
- data/lib/openai/models/responses/response_output_text.rb +2 -2
- data/lib/openai/models/responses/response_output_text_annotation_added_event.rb +2 -2
- data/lib/openai/resources/beta/agents/environments/files.rb +31 -0
- data/lib/openai/resources/beta/agents/sessions/artifacts.rb +8 -0
- data/lib/openai/resources/beta/agents/sessions/events.rb +6 -5
- data/lib/openai/resources/beta/agents/sessions/traces.rb +58 -0
- data/lib/openai/resources/beta/agents/sessions.rb +18 -5
- data/lib/openai/resources/evals/runs.rb +1 -1
- data/lib/openai/resources/realtime/translations/client_secrets.rb +58 -0
- data/lib/openai/resources/realtime/translations.rb +20 -0
- data/lib/openai/resources/realtime.rb +4 -0
- data/lib/openai/version.rb +1 -1
- data/lib/openai.rb +9 -1
- data/rbi/openai/helpers/agents.rbi +20 -1
- data/rbi/openai/helpers/beta/agents/artifacts.rbi +29 -0
- data/rbi/openai/helpers/beta/agents/files.rbi +65 -0
- data/rbi/openai/helpers/beta/agents/function_tool.rbi +40 -0
- data/rbi/openai/helpers/beta/agents/model_adapter.rbi +22 -0
- data/rbi/openai/helpers/beta/agents.rbi +146 -0
- data/rbi/openai/internal/transport/base_client.rbi +12 -0
- data/rbi/openai/internal/type/base_model.rbi +2 -2
- data/rbi/openai/internal/type/converter.rbi +21 -3
- data/rbi/openai/internal/type/union.rbi +6 -2
- data/rbi/openai/models/beta/agents/sessions/session_trace.rbi +97 -0
- data/rbi/openai/models/beta/agents/sessions/trace_list_params.rbi +121 -0
- data/rbi/openai/models/beta/agents/vaults/credential.rbi +5 -5
- data/rbi/openai/models/beta/beta_response_output_text.rbi +2 -2
- data/rbi/openai/models/beta/beta_response_output_text_annotation_added_event.rbi +2 -2
- data/rbi/openai/models/beta/environment.rbi +29 -0
- data/rbi/openai/models/beta/environment_param.rbi +31 -0
- data/rbi/openai/models/beta/session_turn_error.rbi +3 -0
- data/rbi/openai/models/chat/chat_completion_content_part_image.rbi +1 -0
- data/rbi/openai/models/live/fork_server_event.rbi +14 -2
- data/rbi/openai/models/live/server_event.rbi +14 -2
- data/rbi/openai/models/realtime/translations/client_secret_create_params.rbi +68 -0
- data/rbi/openai/models/responses/response_output_text.rbi +2 -2
- data/rbi/openai/models/responses/response_output_text_annotation_added_event.rbi +2 -2
- data/rbi/openai/resources/beta/agents/environments/files.rbi +31 -0
- data/rbi/openai/resources/beta/agents/sessions/artifacts.rbi +8 -0
- data/rbi/openai/resources/beta/agents/sessions/events.rbi +6 -5
- data/rbi/openai/resources/beta/agents/sessions/traces.rbi +54 -0
- data/rbi/openai/resources/beta/agents/sessions.rbi +6 -1
- data/rbi/openai/resources/realtime/translations/client_secrets.rbi +54 -0
- data/rbi/openai/resources/realtime/translations.rbi +22 -0
- data/rbi/openai/resources/realtime.rbi +3 -0
- data/sig/openai/helpers/agents.rbs +7 -0
- data/sig/openai/helpers/beta/agents/artifacts.rbs +20 -0
- data/sig/openai/helpers/beta/agents/files.rbs +41 -0
- data/sig/openai/helpers/beta/agents/function_tool.rbs +24 -0
- data/sig/openai/helpers/beta/agents/model_adapter.rbs +16 -0
- data/sig/openai/helpers/beta/agents.rbs +71 -0
- data/sig/openai/helpers/structured_output.rbs +12 -0
- data/sig/openai/internal/transport/base_client.rbs +8 -0
- data/sig/openai/internal/type/base_model.rbs +3 -1
- data/sig/openai/internal/type/converter.rbs +16 -2
- data/sig/openai/internal/type/union.rbs +2 -1
- data/sig/openai/models/beta/agents/sessions/session_trace.rbs +65 -0
- data/sig/openai/models/beta/agents/sessions/trace_list_params.rbs +81 -0
- data/sig/openai/models/beta/agents/vaults/credential.rbs +5 -5
- data/sig/openai/models/beta/beta_response_output_text.rbs +2 -2
- data/sig/openai/models/beta/beta_response_output_text_annotation_added_event.rbs +2 -2
- data/sig/openai/models/beta/environment.rbs +22 -0
- data/sig/openai/models/beta/environment_param.rbs +25 -0
- data/sig/openai/models/beta/session_turn_error.rbs +4 -0
- data/sig/openai/models/chat/chat_completion_content_part_image.rbs +2 -1
- data/sig/openai/models/live/fork_server_event.rbs +17 -2
- data/sig/openai/models/live/server_event.rbs +17 -2
- data/sig/openai/models/realtime/translations/client_secret_create_params.rbs +39 -0
- data/sig/openai/models/responses/response_output_text.rbs +2 -2
- data/sig/openai/models/responses/response_output_text_annotation_added_event.rbs +2 -2
- data/sig/openai/resources/beta/agents/environments/files.rbs +17 -0
- data/sig/openai/resources/beta/agents/sessions/artifacts.rbs +4 -0
- data/sig/openai/resources/beta/agents/sessions/traces.rbs +21 -0
- data/sig/openai/resources/beta/agents/sessions.rbs +4 -1
- data/sig/openai/resources/realtime/translations/client_secrets.rbs +17 -0
- data/sig/openai/resources/realtime/translations.rbs +11 -0
- data/sig/openai/resources/realtime.rbs +2 -0
- metadata +37 -1
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenAI
|
|
4
|
+
module Helpers
|
|
5
|
+
module Beta
|
|
6
|
+
module Agents
|
|
7
|
+
# Shared native model checks; JSON and wire-schema policies stay with each caller.
|
|
8
|
+
# @api private
|
|
9
|
+
module ModelAdapter
|
|
10
|
+
def self.validate!(model, parameter:)
|
|
11
|
+
unless model.is_a?(Class) && model < OpenAI::BaseModel
|
|
12
|
+
raise ArgumentError, "#{parameter} must be an OpenAI::BaseModel subclass"
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
def self.coerce(model, value, memoize: false)
|
|
17
|
+
state = OpenAI::Internal::Type::Converter.new_coerce_state(memoize: memoize)
|
|
18
|
+
parsed = OpenAI::Internal::Type::Converter.coerce(model, value, state: state)
|
|
19
|
+
parsed if parsed.is_a?(model) && state[:exactness][:no].zero?
|
|
20
|
+
end
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
private_constant :ModelAdapter
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
@@ -0,0 +1,170 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module OpenAI
|
|
4
|
+
module Helpers
|
|
5
|
+
module Beta
|
|
6
|
+
module Agents
|
|
7
|
+
# The final output of one completed root turn (beta).
|
|
8
|
+
# Messages contain final assistant answers, not the session transcript.
|
|
9
|
+
class TurnResult
|
|
10
|
+
attr_reader :turn, :messages
|
|
11
|
+
|
|
12
|
+
# @api private
|
|
13
|
+
def initialize(turn:, messages:)
|
|
14
|
+
@turn = turn
|
|
15
|
+
@messages = messages.freeze
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def session_id = turn.session_id
|
|
19
|
+
def turn_id = turn.id
|
|
20
|
+
|
|
21
|
+
# Joins text without adding separators or making requests.
|
|
22
|
+
def output_text = messages.map(&:output_text).join
|
|
23
|
+
|
|
24
|
+
# A raw result has no parser attached.
|
|
25
|
+
def output_parsed = nil
|
|
26
|
+
|
|
27
|
+
# Parse this completed answer without changing the hosted session.
|
|
28
|
+
def parse(output_type:)
|
|
29
|
+
OutputParser.new(output_type).parse(self)
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
# Collection failed, or the observed turn did not complete successfully.
|
|
34
|
+
# A transport failure describes observation, not failure of hosted work.
|
|
35
|
+
class ResultError < OpenAI::Errors::Error
|
|
36
|
+
attr_reader :reason, :session_id, :turn, :messages, :required_actions
|
|
37
|
+
|
|
38
|
+
# @api private
|
|
39
|
+
def initialize(reason:, session_id:, turn:, messages:, required_actions: [])
|
|
40
|
+
@reason = reason
|
|
41
|
+
@session_id = session_id
|
|
42
|
+
@turn = turn
|
|
43
|
+
@messages = messages.freeze
|
|
44
|
+
@required_actions = required_actions.freeze
|
|
45
|
+
super("Unable to collect the agent turn result: #{reason}")
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def turn_id = turn&.id
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
# @api private
|
|
52
|
+
class ResultCollector
|
|
53
|
+
def initialize(session_id: nil, handler_names: [])
|
|
54
|
+
@session_id = session_id
|
|
55
|
+
@handler_names = handler_names
|
|
56
|
+
@messages = {}
|
|
57
|
+
@required_actions = []
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def enable
|
|
61
|
+
return if @enabled
|
|
62
|
+
if @started
|
|
63
|
+
raise ArgumentError, "Call with_result_collection before consuming events to collect a final result"
|
|
64
|
+
end
|
|
65
|
+
|
|
66
|
+
@enabled = true
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def observe(event)
|
|
70
|
+
@started = true
|
|
71
|
+
return unless @enabled
|
|
72
|
+
return if @finished || !event.is_a?(OpenAI::Internal::Type::BaseModel)
|
|
73
|
+
|
|
74
|
+
case event.type
|
|
75
|
+
when :"agent.session.created"
|
|
76
|
+
@session_id ||= event.session.id.dup
|
|
77
|
+
when :"agent.session.turn.created"
|
|
78
|
+
if @turn.nil? && event.turn.subagent_id.nil? && (@session_id.nil? || event.session_id == @session_id)
|
|
79
|
+
@turn = copy(event.turn)
|
|
80
|
+
@session_id = @turn.session_id
|
|
81
|
+
end
|
|
82
|
+
|
|
83
|
+
when :"agent.session.turn.completed", :"agent.session.turn.failed", :"agent.session.turn.cancelled"
|
|
84
|
+
if @turn && event.turn_id == @turn.id
|
|
85
|
+
@turn = copy(event.turn)
|
|
86
|
+
@terminal = true
|
|
87
|
+
@required_actions = []
|
|
88
|
+
end
|
|
89
|
+
|
|
90
|
+
when :"agent.session.turn.item.done"
|
|
91
|
+
item = event.item
|
|
92
|
+
if @turn &&
|
|
93
|
+
item.is_a?(OpenAI::Models::Beta::AgentSessionAssistantMessage) &&
|
|
94
|
+
item.turn_id == @turn.id &&
|
|
95
|
+
item.role == :assistant &&
|
|
96
|
+
item.status == :completed &&
|
|
97
|
+
item.phase != :commentary
|
|
98
|
+
@messages[item.id] = [event.output_index, copy(item, OpenAI::Models::Beta::AgentSessionMessage)]
|
|
99
|
+
end
|
|
100
|
+
|
|
101
|
+
when :"agent.session.requires_action"
|
|
102
|
+
@required_actions = event.session.required_actions.filter_map do |action|
|
|
103
|
+
if action.is_a?(OpenAI::Models::Beta::AgentSession::RequiredAction::FunctionCall) &&
|
|
104
|
+
@handler_names.include?(action.name)
|
|
105
|
+
next
|
|
106
|
+
end
|
|
107
|
+
|
|
108
|
+
copy(action)
|
|
109
|
+
end
|
|
110
|
+
|
|
111
|
+
when :"agent.session.in_progress"
|
|
112
|
+
@required_actions = [] if event.session.id == @session_id
|
|
113
|
+
when :"agent.session.idle"
|
|
114
|
+
@finished = true if @terminal && event.session.id == @session_id
|
|
115
|
+
when :"agent.session.failed"
|
|
116
|
+
@session_id ||= event.session.id.dup
|
|
117
|
+
@failure = :failed
|
|
118
|
+
@finished = true
|
|
119
|
+
end
|
|
120
|
+
end
|
|
121
|
+
|
|
122
|
+
def stopped?
|
|
123
|
+
@finished || !@required_actions.empty? || (@terminal && @turn.status != :completed)
|
|
124
|
+
end
|
|
125
|
+
|
|
126
|
+
def observe_error(error)
|
|
127
|
+
@cause ||= error if @enabled && !@finished
|
|
128
|
+
end
|
|
129
|
+
|
|
130
|
+
def result
|
|
131
|
+
return @result if @result
|
|
132
|
+
raise @result_error if @result_error
|
|
133
|
+
raise error(:observation_error), cause: @cause if @cause
|
|
134
|
+
raise error(@failure) if @failure
|
|
135
|
+
raise error(@turn.status) if @terminal && @turn.status != :completed
|
|
136
|
+
raise error(:requires_action) unless @required_actions.empty?
|
|
137
|
+
raise error(:incomplete) unless @finished && @turn&.status == :completed
|
|
138
|
+
|
|
139
|
+
@result = TurnResult.new(turn: @turn, messages: take_messages)
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
private
|
|
143
|
+
|
|
144
|
+
def take_messages
|
|
145
|
+
messages = @messages.values.sort_by(&:first).map(&:last)
|
|
146
|
+
@messages.clear
|
|
147
|
+
messages
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def error(reason)
|
|
151
|
+
@result_error = ResultError.new(
|
|
152
|
+
reason: reason,
|
|
153
|
+
session_id: @session_id,
|
|
154
|
+
turn: @turn,
|
|
155
|
+
messages: take_messages,
|
|
156
|
+
required_actions: @required_actions
|
|
157
|
+
)
|
|
158
|
+
end
|
|
159
|
+
|
|
160
|
+
def copy(value, model = value.class)
|
|
161
|
+
raw = JSON.parse(JSON.generate(value), symbolize_names: true)
|
|
162
|
+
OpenAI::Internal::Type::Converter.coerce(model, raw)
|
|
163
|
+
end
|
|
164
|
+
end
|
|
165
|
+
end
|
|
166
|
+
end
|
|
167
|
+
end
|
|
168
|
+
end
|
|
169
|
+
|
|
170
|
+
require_relative "typed_output"
|
|
@@ -0,0 +1,191 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative "model_adapter"
|
|
4
|
+
|
|
5
|
+
module OpenAI
|
|
6
|
+
module Helpers
|
|
7
|
+
module Beta
|
|
8
|
+
module Agents
|
|
9
|
+
# A completed turn with output hydrated into the requested Ruby model (beta).
|
|
10
|
+
class ParsedTurnResult < TurnResult
|
|
11
|
+
attr_reader :raw_result, :output_parsed
|
|
12
|
+
|
|
13
|
+
# @api private
|
|
14
|
+
def initialize(raw_result:, output_parsed:)
|
|
15
|
+
@raw_result = raw_result
|
|
16
|
+
@output_parsed = output_parsed
|
|
17
|
+
super(turn: raw_result.turn, messages: raw_result.messages)
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
# Hosted work completed, but its answer did not match the requested model.
|
|
22
|
+
class OutputParseError < OpenAI::Errors::Error
|
|
23
|
+
attr_reader :raw_result
|
|
24
|
+
|
|
25
|
+
# @api private
|
|
26
|
+
def initialize(raw_result:)
|
|
27
|
+
@raw_result = raw_result
|
|
28
|
+
super("The completed agent output does not match output_type")
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
# @api private
|
|
33
|
+
class OutputParser
|
|
34
|
+
def initialize(model)
|
|
35
|
+
ModelAdapter.validate!(model, parameter: "output_type")
|
|
36
|
+
|
|
37
|
+
@model = model
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def prepare_request(params)
|
|
41
|
+
agent = params.fetch(:agent, nil).to_h.transform_keys { _1.is_a?(String) ? _1.to_sym : _1 }
|
|
42
|
+
text = agent.fetch(:text, nil).to_h.transform_keys { _1.is_a?(String) ? _1.to_sym : _1 }
|
|
43
|
+
if text.key?(:format) || text.key?(:format_)
|
|
44
|
+
raise ArgumentError, "output_type cannot be combined with agent.text.format"
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
@schema = JSON.parse(JSON.generate(@model.to_json_schema, max_nesting: false), max_nesting: false)
|
|
48
|
+
normalize_references
|
|
49
|
+
params[:agent] = agent.merge(text: text.merge(format: {type: :json_schema, schema: @schema}))
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def parse(result)
|
|
53
|
+
return @result if @result && @result.raw_result.equal?(result)
|
|
54
|
+
raise @error if @error && @error.raw_result.equal?(result)
|
|
55
|
+
|
|
56
|
+
first = nil
|
|
57
|
+
result.messages.each do |message|
|
|
58
|
+
message.content.each do |part|
|
|
59
|
+
next unless (part[:type] || (part["type"] if part.is_a?(Hash))).to_s == "output_text"
|
|
60
|
+
text = part[:text] || (part["text"] if part.is_a?(Hash))
|
|
61
|
+
value = JSON.parse(text, symbolize_names: true, max_nesting: false)
|
|
62
|
+
parsed = ModelAdapter.coerce(@model, value, memoize: true)
|
|
63
|
+
raise TypeError, "Output cannot be parsed into output_type" unless parsed
|
|
64
|
+
first ||= parsed
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
|
|
68
|
+
raise TypeError, "Output does not contain structured text" unless first
|
|
69
|
+
|
|
70
|
+
@result = ParsedTurnResult.new(raw_result: result, output_parsed: first)
|
|
71
|
+
rescue StandardError, SystemStackError => error
|
|
72
|
+
raise if error.equal?(@error)
|
|
73
|
+
@error = OutputParseError.new(raw_result: result)
|
|
74
|
+
raise @error, cause: nil
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
private
|
|
78
|
+
|
|
79
|
+
def resolve(ref)
|
|
80
|
+
return unless ref.is_a?(String) && ref.start_with?("#/")
|
|
81
|
+
|
|
82
|
+
URI::RFC2396_PARSER
|
|
83
|
+
.unescape(ref.delete_prefix("#/"))
|
|
84
|
+
.split("/", -1)
|
|
85
|
+
.reduce(@schema) { |node, token| node[token.gsub("~1", "/").gsub("~0", "~")] if node.is_a?(Hash) }
|
|
86
|
+
end
|
|
87
|
+
|
|
88
|
+
def normalize_references
|
|
89
|
+
return unless @schema.is_a?(Hash) && @schema["$defs"].is_a?(Hash)
|
|
90
|
+
return if @schema.key?("$ref") && !(@schema.keys - %w[$defs $ref]).empty?
|
|
91
|
+
definitions = @schema.fetch("$defs")
|
|
92
|
+
return if definitions.empty? || !definitions.values.all? { _1.is_a?(Hash) }
|
|
93
|
+
|
|
94
|
+
names = {}.compare_by_identity
|
|
95
|
+
definitions.each_value.with_index { |definition, index| names[definition] = "model_#{index}" }
|
|
96
|
+
nodes = schema_nodes
|
|
97
|
+
return unless references_covered?(nodes)
|
|
98
|
+
references = nodes.select { _1.key?("$ref") }
|
|
99
|
+
return if references.empty?
|
|
100
|
+
targets = references.map { resolve(_1["$ref"]) }
|
|
101
|
+
# Native Ruby pointers encode definition names; Agents looks up raw suffixes.
|
|
102
|
+
# Unfamiliar references keep their schema and definition names intact.
|
|
103
|
+
return unless targets.all? { names.key?(_1) }
|
|
104
|
+
root = resolve(@schema["$ref"])
|
|
105
|
+
expand_root = root.is_a?(Hash) && !root.key?("$defs") && (@schema.keys - %w[$defs $ref]).empty?
|
|
106
|
+
|
|
107
|
+
references.zip(targets).each do |node, target|
|
|
108
|
+
node["$ref"] = "#/$defs/#{names.fetch(target)}"
|
|
109
|
+
if (node.key?("description") || node.key?("title")) && !node.key?("anyOf")
|
|
110
|
+
node["anyOf"] = [{"$ref" => node.delete("$ref")}]
|
|
111
|
+
end
|
|
112
|
+
end
|
|
113
|
+
|
|
114
|
+
@schema["$defs"] = definitions.to_h { |_name, definition| [names.fetch(definition), definition] }
|
|
115
|
+
@schema = @schema.except("$ref").merge(root) if expand_root
|
|
116
|
+
end
|
|
117
|
+
|
|
118
|
+
def references_covered?(nodes)
|
|
119
|
+
known = nodes.each_with_object({}.compare_by_identity) { |node, seen| seen[node] = true }
|
|
120
|
+
pending = [@schema]
|
|
121
|
+
until pending.empty?
|
|
122
|
+
value = pending.pop
|
|
123
|
+
case value
|
|
124
|
+
when Hash
|
|
125
|
+
if known[value]
|
|
126
|
+
unless (value.keys &
|
|
127
|
+
%w[$id id $anchor $dynamicAnchor $dynamicRef $recursiveAnchor $recursiveRef discriminator])
|
|
128
|
+
.empty?
|
|
129
|
+
return false
|
|
130
|
+
end
|
|
131
|
+
|
|
132
|
+
pending.concat(
|
|
133
|
+
value.except("$ref", "title", "description", "default", "const", "enum", "examples").values
|
|
134
|
+
)
|
|
135
|
+
else
|
|
136
|
+
return false if %w[$ref $dynamicRef $recursiveRef].any? { value[_1].is_a?(String) }
|
|
137
|
+
pending.concat(value.values)
|
|
138
|
+
end
|
|
139
|
+
|
|
140
|
+
when Array
|
|
141
|
+
pending.concat(value)
|
|
142
|
+
when String
|
|
143
|
+
return false if value.start_with?("#/")
|
|
144
|
+
end
|
|
145
|
+
end
|
|
146
|
+
|
|
147
|
+
true
|
|
148
|
+
end
|
|
149
|
+
|
|
150
|
+
def schema_nodes
|
|
151
|
+
nodes = []
|
|
152
|
+
pending = [@schema]
|
|
153
|
+
until pending.empty?
|
|
154
|
+
node = pending.pop
|
|
155
|
+
next unless node.is_a?(Hash)
|
|
156
|
+
nodes << node
|
|
157
|
+
# Visit schema positions only: defaults, examples and enum values are data.
|
|
158
|
+
%w[properties patternProperties $defs definitions dependentSchemas dependencies].each do |key|
|
|
159
|
+
pending.concat(node[key].values.grep(Hash)) if node[key].is_a?(Hash)
|
|
160
|
+
end
|
|
161
|
+
|
|
162
|
+
%w[anyOf allOf oneOf prefixItems].each do |key|
|
|
163
|
+
pending.concat(node[key]) if node[key].is_a?(Array)
|
|
164
|
+
end
|
|
165
|
+
|
|
166
|
+
%w[
|
|
167
|
+
items
|
|
168
|
+
additionalProperties
|
|
169
|
+
additionalItems
|
|
170
|
+
contains
|
|
171
|
+
not
|
|
172
|
+
if
|
|
173
|
+
then
|
|
174
|
+
else
|
|
175
|
+
propertyNames
|
|
176
|
+
unevaluatedProperties
|
|
177
|
+
unevaluatedItems
|
|
178
|
+
]
|
|
179
|
+
.each do |key|
|
|
180
|
+
pending.concat(node[key].is_a?(Array) ? node[key] : [node[key]]) if node.key?(key)
|
|
181
|
+
end
|
|
182
|
+
end
|
|
183
|
+
|
|
184
|
+
nodes
|
|
185
|
+
end
|
|
186
|
+
|
|
187
|
+
end
|
|
188
|
+
end
|
|
189
|
+
end
|
|
190
|
+
end
|
|
191
|
+
end
|
|
@@ -99,10 +99,16 @@ module OpenAI
|
|
|
99
99
|
end
|
|
100
100
|
|
|
101
101
|
private def write_text(text)
|
|
102
|
+
complete = false
|
|
102
103
|
super
|
|
104
|
+
complete = true
|
|
105
|
+
nil
|
|
103
106
|
rescue StandardError
|
|
104
|
-
@poisoned = true
|
|
105
107
|
raise OpenAI::Errors::LiveConnectionError.new(url: @url), cause: nil
|
|
108
|
+
ensure
|
|
109
|
+
# Also retire a possibly buffered write when cancellation bypasses
|
|
110
|
+
# StandardError, before another sender or normal cleanup can flush it.
|
|
111
|
+
@poisoned = true unless complete
|
|
106
112
|
end
|
|
107
113
|
|
|
108
114
|
private def read_raw_message
|
|
@@ -7,7 +7,7 @@ module OpenAI
|
|
|
7
7
|
# exceptions local to Realtime instead of weakening SDK-wide union behavior.
|
|
8
8
|
module ConversationItem
|
|
9
9
|
class << self
|
|
10
|
-
private def resolve_variant(value)
|
|
10
|
+
private def resolve_variant(value, state: nil)
|
|
11
11
|
return super unless value.is_a?(Hash)
|
|
12
12
|
|
|
13
13
|
type = value.fetch(:type) { value.fetch("type", OpenAI::Internal::OMIT) }
|
|
@@ -177,10 +177,16 @@ module OpenAI
|
|
|
177
177
|
end
|
|
178
178
|
|
|
179
179
|
private def write_text(text)
|
|
180
|
+
complete = false
|
|
180
181
|
super
|
|
182
|
+
complete = true
|
|
183
|
+
nil
|
|
181
184
|
rescue StandardError
|
|
182
|
-
@poisoned = true
|
|
183
185
|
raise OpenAI::Errors::ResponsesSendError.new(url: @url), cause: nil
|
|
186
|
+
ensure
|
|
187
|
+
# Cancellation can bypass StandardError. Poison before another lane
|
|
188
|
+
# can send, so normal cleanup cannot flush an uncertain write.
|
|
189
|
+
@poisoned = true unless complete
|
|
184
190
|
end
|
|
185
191
|
|
|
186
192
|
private def with_read_lease
|
|
@@ -495,8 +495,12 @@ module OpenAI
|
|
|
495
495
|
raise
|
|
496
496
|
rescue OpenAI::Errors::ResponsesSendError => error
|
|
497
497
|
@error = error
|
|
498
|
-
stop_reader
|
|
499
498
|
raise
|
|
499
|
+
ensure
|
|
500
|
+
if @connection.poisoned?
|
|
501
|
+
@error ||= OpenAI::Errors::ResponsesSendError.new(url: @connection.url)
|
|
502
|
+
stop_reader
|
|
503
|
+
end
|
|
500
504
|
end
|
|
501
505
|
|
|
502
506
|
# @api private
|
|
@@ -117,7 +117,10 @@ module OpenAI
|
|
|
117
117
|
decoded = +"".b
|
|
118
118
|
begin
|
|
119
119
|
inflater.inflate(buffer + ::Protocol::WebSocket::Extension::Compression::Inflate::TRAILER) do |chunk|
|
|
120
|
-
if inflater.total_out -
|
|
120
|
+
if inflater.total_out -
|
|
121
|
+
start_out > @responses_decoded_limit ||
|
|
122
|
+
decoded.bytesize +
|
|
123
|
+
chunk.bytesize > @responses_decoded_limit
|
|
121
124
|
raise ::Protocol::WebSocket::ProtocolError, "Responses WebSocket message exceeds configured limit."
|
|
122
125
|
end
|
|
123
126
|
|
|
@@ -129,7 +132,14 @@ module OpenAI
|
|
|
129
132
|
raise ::Protocol::WebSocket::ProtocolError, "Responses WebSocket message exceeds configured limit."
|
|
130
133
|
end
|
|
131
134
|
|
|
132
|
-
|
|
135
|
+
# Finished streams can put unused input in this buffer without
|
|
136
|
+
# increasing total_out. Count every byte before collecting it.
|
|
137
|
+
tail = inflater.flush_next_out
|
|
138
|
+
if decoded.bytesize + tail.bytesize > @responses_decoded_limit
|
|
139
|
+
raise ::Protocol::WebSocket::ProtocolError, "Responses WebSocket message exceeds configured limit."
|
|
140
|
+
end
|
|
141
|
+
|
|
142
|
+
decoded << tail
|
|
133
143
|
rescue ::Protocol::WebSocket::ProtocolError
|
|
134
144
|
@responses_bounded_framer.abort
|
|
135
145
|
inflater.close
|
|
@@ -823,6 +823,24 @@ module OpenAI
|
|
|
823
823
|
end
|
|
824
824
|
end
|
|
825
825
|
|
|
826
|
+
# @api private
|
|
827
|
+
# Consume binary response chunks without buffering a complete body.
|
|
828
|
+
def request_streaming_body(req, on_response = nil)
|
|
829
|
+
if req[:options].to_h[:include_raw_body]
|
|
830
|
+
raise ArgumentError, "include_raw_body is not supported for streaming downloads"
|
|
831
|
+
end
|
|
832
|
+
|
|
833
|
+
_url, response, log_context = perform_request(req)
|
|
834
|
+
finish_request(log_context, response) do
|
|
835
|
+
on_response&.call
|
|
836
|
+
response.body.each { |chunk| yield chunk }
|
|
837
|
+
nil
|
|
838
|
+
end
|
|
839
|
+
|
|
840
|
+
ensure
|
|
841
|
+
OpenAI::Internal::Util.close_fused!(response.body) if response
|
|
842
|
+
end
|
|
843
|
+
|
|
826
844
|
# @api private
|
|
827
845
|
#
|
|
828
846
|
# @param req [Hash{Symbol=>Object}]
|
|
@@ -28,9 +28,12 @@ module OpenAI
|
|
|
28
28
|
# @api private
|
|
29
29
|
#
|
|
30
30
|
# @return [Hash{Symbol=>Hash{Symbol=>Object}}]
|
|
31
|
-
def fields
|
|
31
|
+
def fields(state: nil)
|
|
32
32
|
known_fields.transform_values do |field|
|
|
33
|
-
{
|
|
33
|
+
{
|
|
34
|
+
**field.except(:type_fn),
|
|
35
|
+
type: OpenAI::Internal::Type::Converter.resolve_type(field.fetch(:type_fn), state: state)
|
|
36
|
+
}
|
|
34
37
|
end
|
|
35
38
|
end
|
|
36
39
|
|
|
@@ -297,7 +300,7 @@ module OpenAI
|
|
|
297
300
|
data = instance.to_h
|
|
298
301
|
viability = instance.instance_variable_get(:@coerced)
|
|
299
302
|
|
|
300
|
-
fields.each do |name, field|
|
|
303
|
+
fields(state: state).each do |name, field|
|
|
301
304
|
mode, required, target = field.fetch_values(:mode, :required, :type)
|
|
302
305
|
api_name, nilable, const = field.fetch_values(:api_name, :nilable, :const)
|
|
303
306
|
src_name = state.fetch(:translate_names) ? api_name : name
|
|
@@ -135,17 +135,26 @@ module OpenAI
|
|
|
135
135
|
# @param request_only [Boolean] Exclude fields omitted by the request serializer.
|
|
136
136
|
#
|
|
137
137
|
# @return [Hash{Symbol=>Object}]
|
|
138
|
-
def new_coerce_state(translate_names: true, request_only: false)
|
|
138
|
+
def new_coerce_state(translate_names: true, request_only: false, memoize: false)
|
|
139
139
|
{
|
|
140
140
|
translate_names: translate_names,
|
|
141
141
|
request_only: request_only,
|
|
142
142
|
strictness: true,
|
|
143
143
|
exactness: {yes: 0, no: 0, maybe: 0},
|
|
144
144
|
error: nil,
|
|
145
|
-
branched: 0
|
|
145
|
+
branched: 0,
|
|
146
|
+
memo: memoize ? {}.compare_by_identity : nil
|
|
146
147
|
}
|
|
147
148
|
end
|
|
148
149
|
|
|
150
|
+
# @api private
|
|
151
|
+
# Resolve lazy schema types once within an opted-in parse.
|
|
152
|
+
def resolve_type(type_fn, state: nil)
|
|
153
|
+
return type_fn.call unless state && state[:memo]
|
|
154
|
+
cache = state[:memo][:resolved_types] ||= {}.compare_by_identity
|
|
155
|
+
cache.fetch(type_fn) { cache[type_fn] = type_fn.call }
|
|
156
|
+
end
|
|
157
|
+
|
|
149
158
|
# @api private
|
|
150
159
|
#
|
|
151
160
|
# Based on `target`, transform `value` into `target`, to the extent possible:
|
|
@@ -191,6 +200,12 @@ module OpenAI
|
|
|
191
200
|
#
|
|
192
201
|
# @return [Object]
|
|
193
202
|
def coerce(target, value, state: OpenAI::Internal::Type::Converter.new_coerce_state)
|
|
203
|
+
if state[:memo] &&
|
|
204
|
+
target.is_a?(OpenAI::Internal::Type::Converter) &&
|
|
205
|
+
(value.is_a?(Hash) || value.is_a?(Array))
|
|
206
|
+
return coerce_cached(target, value, state: state)
|
|
207
|
+
end
|
|
208
|
+
|
|
194
209
|
exactness = state.fetch(:exactness)
|
|
195
210
|
|
|
196
211
|
case target
|
|
@@ -248,6 +263,12 @@ module OpenAI
|
|
|
248
263
|
state[:error] = TypeError.new("#{value.class} can't be coerced into #{String}")
|
|
249
264
|
end
|
|
250
265
|
|
|
266
|
+
in -> { _1 <= Symbol }
|
|
267
|
+
if value.is_a?(String)
|
|
268
|
+
exactness[:yes] += 1
|
|
269
|
+
return value.to_sym
|
|
270
|
+
end
|
|
271
|
+
|
|
251
272
|
in -> { _1 <= Date || _1 <= Time }
|
|
252
273
|
Kernel.then do
|
|
253
274
|
return target.parse(value).tap { exactness[:yes] += 1 }
|
|
@@ -282,6 +303,31 @@ module OpenAI
|
|
|
282
303
|
value
|
|
283
304
|
end
|
|
284
305
|
|
|
306
|
+
# @api private
|
|
307
|
+
# Reuse converter/value pairs within one parse, including recursive collections.
|
|
308
|
+
# Identity keys avoid recursively hashing model and converter definitions.
|
|
309
|
+
def coerce_cached(target, value, state:)
|
|
310
|
+
converters = state.fetch(:memo)
|
|
311
|
+
values = converters[target] ||= {}.compare_by_identity
|
|
312
|
+
outcomes = values[value] ||= {}
|
|
313
|
+
key = state.values_at(:translate_names, :request_only, :strictness, :error)
|
|
314
|
+
exactness = state.fetch(:exactness)
|
|
315
|
+
if (cached = outcomes[key])
|
|
316
|
+
parsed, delta, error, branched = cached
|
|
317
|
+
delta.each { exactness[_1] += _2 }
|
|
318
|
+
state[:error] = error
|
|
319
|
+
state[:branched] += branched
|
|
320
|
+
return parsed
|
|
321
|
+
end
|
|
322
|
+
|
|
323
|
+
before = exactness.dup
|
|
324
|
+
branched = state.fetch(:branched)
|
|
325
|
+
parsed = target.coerce(value, state: state)
|
|
326
|
+
delta = exactness.to_h { |name, count| [name, count - before.fetch(name)] }
|
|
327
|
+
outcomes[key] = [parsed, delta, state[:error], state[:branched] - branched]
|
|
328
|
+
parsed
|
|
329
|
+
end
|
|
330
|
+
|
|
285
331
|
# @api private
|
|
286
332
|
#
|
|
287
333
|
# Coerces a value while isolating its error from sibling coercions.
|
|
@@ -350,7 +396,8 @@ module OpenAI
|
|
|
350
396
|
strictness: T::Boolean,
|
|
351
397
|
exactness: {yes: Integer, no: Integer, maybe: Integer},
|
|
352
398
|
error: T.nilable(StandardError),
|
|
353
|
-
branched: Integer
|
|
399
|
+
branched: Integer,
|
|
400
|
+
memo: T.nilable(T::Hash[T.untyped, T.untyped])
|
|
354
401
|
}
|
|
355
402
|
end
|
|
356
403
|
end
|