openai 0.115.0 → 0.116.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 +26 -0
- data/README.md +1 -1
- data/lib/openai/helpers/responses_websocket/incremental.rb +500 -154
- data/lib/openai/helpers/responses_websocket/session.rb +75 -2
- data/lib/openai/models/live/responses_delegation_config.rb +999 -6
- data/lib/openai/models/live/responses_delegation_update_config.rb +1043 -6
- data/lib/openai/version.rb +1 -1
- data/rbi/openai/helpers/responses_websocket/incremental.rbi +4 -0
- data/rbi/openai/models/live/responses_delegation_config.rbi +2253 -24
- data/rbi/openai/models/live/responses_delegation_update_config.rbi +2327 -24
- data/responses-websocket.md +40 -2
- data/sig/openai/helpers/responses_websocket/incremental.rbs +1 -0
- data/sig/openai/models/live/responses_delegation_config.rbs +851 -0
- data/sig/openai/models/live/responses_delegation_update_config.rbs +871 -0
- metadata +19 -1
|
@@ -5,30 +5,88 @@ module OpenAI
|
|
|
5
5
|
# Optional, caller-fed provisional output for one WebSocket lane.
|
|
6
6
|
# Feed the events returned by lane.receive; use a separate instance per lane.
|
|
7
7
|
# This helper never reads, closes, or writes to a connection. Tools are data.
|
|
8
|
-
#
|
|
9
|
-
# Output is provisional and may be unavailable when the wire omitted its
|
|
10
|
-
# scaffolding, or a known output update cannot be represented. Terminal events
|
|
11
|
-
# are retained exactly as received, never filled from provisional output.
|
|
12
|
-
# Call reset to discard all retained state.
|
|
8
|
+
# Terminal events stay exact; provisional facts never manufacture a Response.
|
|
13
9
|
class IncrementalResponse
|
|
14
10
|
include OpenAI::WebSocket::Protocol
|
|
15
11
|
|
|
12
|
+
STATUS_ITEMS = {
|
|
13
|
+
"response.code_interpreter_call.in_progress" => ["code_interpreter_call", "in_progress"],
|
|
14
|
+
"response.code_interpreter_call.interpreting" => ["code_interpreter_call", "interpreting"],
|
|
15
|
+
"response.code_interpreter_call.completed" => ["code_interpreter_call", "completed"],
|
|
16
|
+
"response.file_search_call.in_progress" => ["file_search_call", "in_progress"],
|
|
17
|
+
"response.file_search_call.searching" => ["file_search_call", "searching"],
|
|
18
|
+
"response.file_search_call.completed" => ["file_search_call", "completed"],
|
|
19
|
+
"response.web_search_call.in_progress" => ["web_search_call", "in_progress"],
|
|
20
|
+
"response.web_search_call.searching" => ["web_search_call", "searching"],
|
|
21
|
+
"response.web_search_call.completed" => ["web_search_call", "completed"],
|
|
22
|
+
"response.image_generation_call.in_progress" => ["image_generation_call", "in_progress"],
|
|
23
|
+
"response.image_generation_call.generating" => ["image_generation_call", "generating"],
|
|
24
|
+
"response.image_generation_call.completed" => ["image_generation_call", "completed"],
|
|
25
|
+
"response.mcp_call.in_progress" => ["mcp_call", "in_progress"],
|
|
26
|
+
"response.mcp_call.completed" => ["mcp_call", "completed"],
|
|
27
|
+
"response.mcp_call.failed" => ["mcp_call", "failed"]
|
|
28
|
+
}.freeze
|
|
29
|
+
private_constant :STATUS_ITEMS
|
|
30
|
+
|
|
16
31
|
attr_reader :phase
|
|
17
32
|
|
|
18
33
|
def initialize
|
|
19
34
|
reset
|
|
20
35
|
end
|
|
21
36
|
|
|
22
|
-
#
|
|
23
|
-
#
|
|
24
|
-
#
|
|
25
|
-
#
|
|
26
|
-
# at boundaries such as response.output_item.done when a full, corrected
|
|
27
|
-
# view is needed; replace a displayed view instead of appending its prefix.
|
|
28
|
-
# Repeated full snapshots of a growing response necessarily copy growing
|
|
29
|
-
# amounts of data.
|
|
37
|
+
# Detached typed output: the truthful contiguous projectable prefix.
|
|
38
|
+
# Sparse or incomplete records stop this prefix; detailed_snapshot retains
|
|
39
|
+
# their actual indices. Full reads cost time proportional to retained data.
|
|
40
|
+
# Use original deltas for per-event progress and read snapshots at boundaries.
|
|
30
41
|
def output
|
|
31
|
-
|
|
42
|
+
return unless @phase == :provisional
|
|
43
|
+
|
|
44
|
+
result = []
|
|
45
|
+
while (record = @items[result.length])
|
|
46
|
+
break unless record[:full]
|
|
47
|
+
|
|
48
|
+
result <<
|
|
49
|
+
OpenAI::Internal::Type::Converter.coerce(
|
|
50
|
+
OpenAI::Responses::ResponseOutputItem,
|
|
51
|
+
project_item(record)
|
|
52
|
+
)
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
result
|
|
56
|
+
end
|
|
57
|
+
|
|
58
|
+
# Detached observed facts, including sparse indices and response metadata.
|
|
59
|
+
# This is not a complete Response. No absent item/part type is inferred.
|
|
60
|
+
def detailed_snapshot
|
|
61
|
+
return if @phase.nil? || @phase == :unavailable
|
|
62
|
+
|
|
63
|
+
if @phase == :terminal
|
|
64
|
+
data = @terminal_event.deep_to_h
|
|
65
|
+
response = data[:response]
|
|
66
|
+
items = response.is_a?(Hash) && response[:output].is_a?(Array) ? response[:output] : []
|
|
67
|
+
records = items.map { |item| make_record(item, full: true) }
|
|
68
|
+
rows = records.each_with_index.map { |record, index| detailed_item(index, record) }
|
|
69
|
+
return copy(
|
|
70
|
+
phase: @phase,
|
|
71
|
+
stream_id: data[:stream_id],
|
|
72
|
+
response_id: response.is_a?(Hash) ? response[:id] : nil,
|
|
73
|
+
terminal_type: data[:type],
|
|
74
|
+
response: response.is_a?(Hash) ? response.reject { |key, _| key == :output } : nil,
|
|
75
|
+
output: rows,
|
|
76
|
+
output_text: observed_text(records)
|
|
77
|
+
)
|
|
78
|
+
end
|
|
79
|
+
|
|
80
|
+
indices = @items.keys.sort
|
|
81
|
+
copy(
|
|
82
|
+
phase: @phase,
|
|
83
|
+
stream_id: @stream_id,
|
|
84
|
+
response_id: @response && @response[:id],
|
|
85
|
+
terminal_type: nil,
|
|
86
|
+
response: @response,
|
|
87
|
+
output: indices.map { |index| detailed_item(index, @items.fetch(index)) },
|
|
88
|
+
output_text: observed_text(indices.map { |index| @items.fetch(index) })
|
|
89
|
+
)
|
|
32
90
|
end
|
|
33
91
|
|
|
34
92
|
# The exact consumed completion, failure, incomplete, or error event.
|
|
@@ -37,60 +95,65 @@ module OpenAI
|
|
|
37
95
|
end
|
|
38
96
|
|
|
39
97
|
def reset
|
|
40
|
-
@state = OpenAI::Helpers::Streaming::ResponseStreamState.new(text_format: nil)
|
|
41
98
|
@phase = nil
|
|
42
|
-
@
|
|
99
|
+
@items = {}
|
|
100
|
+
@ids = {}
|
|
101
|
+
@command_bounds = {}
|
|
102
|
+
@response = nil
|
|
103
|
+
@stream_id = nil
|
|
43
104
|
@terminal_event = nil
|
|
44
105
|
nil
|
|
45
106
|
end
|
|
46
107
|
|
|
47
108
|
def add(event)
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
OpenAI::Responses::ResponseCompletedEvent,
|
|
53
|
-
OpenAI::Responses::ResponseFailedEvent,
|
|
54
|
-
OpenAI::Responses::ResponseIncompleteEvent,
|
|
55
|
-
OpenAI::Responses::ResponsesServerEvent::ResponseWsError
|
|
109
|
+
return if event.is_a?(OpenAI::Responses::UnknownServerEvent)
|
|
110
|
+
|
|
111
|
+
type = event[:type].to_s
|
|
112
|
+
if %w[response.completed response.failed response.incomplete error].include?(type)
|
|
56
113
|
reset
|
|
57
114
|
@terminal_event = copy(event)
|
|
58
115
|
@phase = :terminal
|
|
59
|
-
|
|
116
|
+
return
|
|
117
|
+
end
|
|
118
|
+
|
|
119
|
+
return if @phase == :terminal && type != "response.created"
|
|
120
|
+
|
|
121
|
+
if %w[response.created response.queued response.in_progress].include?(type)
|
|
60
122
|
response = copy(event)[:response]
|
|
61
123
|
unless response.is_a?(OpenAI::Responses::Response) &&
|
|
62
124
|
(response[:output].nil? || response[:output].is_a?(Array))
|
|
63
125
|
raise SessionError, "Invalid Responses WebSocket created response."
|
|
64
126
|
end
|
|
65
127
|
|
|
66
|
-
reset
|
|
67
|
-
@
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
|
|
79
|
-
|
|
80
|
-
OpenAI::Responses::ResponseRefusalDeltaEvent,
|
|
81
|
-
OpenAI::Responses::ResponseRefusalDoneEvent,
|
|
82
|
-
OpenAI::Responses::ResponseOutputTextAnnotationAddedEvent
|
|
83
|
-
return if @phase == :unavailable || @phase == :terminal
|
|
84
|
-
|
|
85
|
-
apply_output(copy(event))
|
|
86
|
-
else
|
|
87
|
-
# A full output snapshot must not leave stale fields after unsupported
|
|
88
|
-
# indexed updates. Unknown raw events and progress events are not output.
|
|
89
|
-
if @phase != :terminal &&
|
|
90
|
-
event.class.known_fields.key?(:output_index) &&
|
|
91
|
-
event[:type].to_s.end_with?(".added", ".delta", ".done", ".partial_image")
|
|
92
|
-
unavailable
|
|
128
|
+
reset if type == "response.created"
|
|
129
|
+
return if @phase == :unavailable
|
|
130
|
+
|
|
131
|
+
data = response.deep_to_h
|
|
132
|
+
@response = data.reject { |key, _| key == :output }
|
|
133
|
+
@stream_id = copy(event[:stream_id])
|
|
134
|
+
if data[:output].is_a?(Array) || type == "response.created"
|
|
135
|
+
@items = {}
|
|
136
|
+
@ids = {}
|
|
137
|
+
@command_bounds = {}
|
|
138
|
+
(data[:output] || []).each_with_index do |item, index|
|
|
139
|
+
replace_item(index, item)
|
|
140
|
+
break if @phase == :unavailable
|
|
141
|
+
end
|
|
93
142
|
end
|
|
143
|
+
|
|
144
|
+
@phase = :provisional unless @phase == :unavailable
|
|
145
|
+
return
|
|
146
|
+
end
|
|
147
|
+
|
|
148
|
+
return if @phase == :unavailable
|
|
149
|
+
|
|
150
|
+
data = copy(event).deep_to_h
|
|
151
|
+
@stream_id ||= data[:stream_id]
|
|
152
|
+
if (status = STATUS_ITEMS[type])
|
|
153
|
+
record = item_for(data, status.first)
|
|
154
|
+
record[:item][:status] = status.last if record
|
|
155
|
+
else
|
|
156
|
+
apply_output(type, data, event.class)
|
|
94
157
|
end
|
|
95
158
|
|
|
96
159
|
nil
|
|
@@ -98,134 +161,417 @@ module OpenAI
|
|
|
98
161
|
|
|
99
162
|
private
|
|
100
163
|
|
|
101
|
-
def apply_output(
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
return unavailable
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
164
|
+
def apply_output(type, data, event_class)
|
|
165
|
+
case type
|
|
166
|
+
when "response.output_item.added", "response.output_item.done"
|
|
167
|
+
return unavailable unless valid_index?(data[:output_index]) && data[:item].is_a?(Hash)
|
|
168
|
+
|
|
169
|
+
replace_item(data[:output_index], data[:item])
|
|
170
|
+
when
|
|
171
|
+
"response.content_part.added",
|
|
172
|
+
"response.content_part.done",
|
|
173
|
+
"response.reasoning_summary_part.added",
|
|
174
|
+
"response.reasoning_summary_part.done"
|
|
175
|
+
summary = type.start_with?("response.reasoning_summary")
|
|
176
|
+
index = data[summary ? :summary_index : :content_index]
|
|
177
|
+
part = data[:part]
|
|
178
|
+
return unavailable unless valid_index?(index) && part.is_a?(Hash)
|
|
179
|
+
|
|
180
|
+
kind = summary || part[:type].to_s == "reasoning_text" ? "reasoning" : "message"
|
|
181
|
+
record = item_for(data, kind)
|
|
182
|
+
put_part(record, summary ? :summary : :content, index, part) if record
|
|
183
|
+
when
|
|
184
|
+
"response.function_call_arguments.delta",
|
|
185
|
+
"response.function_call_arguments.done",
|
|
186
|
+
"response.custom_tool_call_input.delta",
|
|
187
|
+
"response.custom_tool_call_input.done",
|
|
188
|
+
"response.mcp_call_arguments.delta",
|
|
189
|
+
"response.mcp_call_arguments.done",
|
|
190
|
+
"response.code_interpreter_call_code.delta",
|
|
191
|
+
"response.code_interpreter_call_code.done"
|
|
192
|
+
kind, field = case type
|
|
193
|
+
when /function_call_arguments/
|
|
194
|
+
["function_call", :arguments]
|
|
195
|
+
when /custom_tool_call_input/
|
|
196
|
+
["custom_tool_call", :input]
|
|
197
|
+
when /mcp_call_arguments/
|
|
198
|
+
["mcp_call", :arguments]
|
|
199
|
+
else
|
|
200
|
+
["code_interpreter_call", :code]
|
|
201
|
+
end
|
|
202
|
+
|
|
203
|
+
done = type.end_with?(".done")
|
|
204
|
+
value = data[done ? field : :delta]
|
|
205
|
+
return unavailable unless value.is_a?(String)
|
|
206
|
+
|
|
207
|
+
record = item_for(data, kind)
|
|
208
|
+
if record
|
|
209
|
+
update_string(record[:item], field, value, done: done, nullable: field == :code, required: record[:full])
|
|
122
210
|
end
|
|
123
211
|
|
|
124
|
-
item.arguments = event[:arguments]
|
|
125
212
|
when
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
|
|
213
|
+
"response.output_text.delta",
|
|
214
|
+
"response.output_text.done",
|
|
215
|
+
"response.refusal.delta",
|
|
216
|
+
"response.refusal.done",
|
|
217
|
+
"response.reasoning_text.delta",
|
|
218
|
+
"response.reasoning_text.done",
|
|
219
|
+
"response.reasoning_summary_text.delta",
|
|
220
|
+
"response.reasoning_summary_text.done"
|
|
221
|
+
summary = type.start_with?("response.reasoning_summary")
|
|
222
|
+
reasoning = type.start_with?("response.reasoning")
|
|
223
|
+
refusal = type.start_with?("response.refusal")
|
|
224
|
+
field = refusal ? :refusal : :text
|
|
225
|
+
done = type.end_with?(".done")
|
|
226
|
+
value = data[done ? field : :delta]
|
|
227
|
+
index = data[summary ? :summary_index : :content_index]
|
|
228
|
+
return unavailable unless value.is_a?(String) && valid_index?(index)
|
|
229
|
+
return unavailable unless data[:logprobs].nil? || data[:logprobs].is_a?(Array)
|
|
230
|
+
|
|
231
|
+
record = item_for(data, reasoning ? "reasoning" : "message")
|
|
232
|
+
return unless record
|
|
233
|
+
|
|
234
|
+
kind = reasoning ? (summary ? "summary_text" : "reasoning_text") : (refusal ? "refusal" : "output_text")
|
|
235
|
+
part = part_for(record, summary ? :summary : :content, index, kind)
|
|
236
|
+
return unless part
|
|
237
|
+
return unless update_string(part[:part], field, value, done: done, required: part[:full])
|
|
238
|
+
|
|
239
|
+
if type.start_with?("response.output_text") && !data[:logprobs].nil?
|
|
240
|
+
if done
|
|
241
|
+
part[:part][:logprobs] = data[:logprobs]
|
|
242
|
+
else
|
|
243
|
+
old = part[:part][:logprobs]
|
|
244
|
+
return unavailable unless old.nil? || old.is_a?(Array)
|
|
245
|
+
|
|
246
|
+
(part[:part][:logprobs] ||= []).concat(data[:logprobs])
|
|
247
|
+
end
|
|
248
|
+
end
|
|
249
|
+
|
|
250
|
+
when "response.output_text.annotation.added"
|
|
251
|
+
index = data[:content_index]
|
|
252
|
+
annotation_index = data[:annotation_index]
|
|
253
|
+
unless valid_index?(index) &&
|
|
254
|
+
valid_index?(annotation_index) &&
|
|
255
|
+
data.key?(:annotation) &&
|
|
256
|
+
(data[:annotation].nil? || data[:annotation].is_a?(Hash))
|
|
130
257
|
return unavailable
|
|
131
258
|
end
|
|
132
259
|
|
|
133
|
-
|
|
134
|
-
|
|
260
|
+
record = item_for(data, "message")
|
|
261
|
+
return unless record
|
|
262
|
+
|
|
263
|
+
part = part_for(record, :content, index, "output_text")
|
|
264
|
+
return unless part
|
|
265
|
+
return unavailable if part[:part].key?(:annotations) && !part[:part][:annotations].is_a?(Array)
|
|
266
|
+
|
|
267
|
+
part[:annotations][annotation_index] = data[:annotation]
|
|
268
|
+
sync_array(part[:part][:annotations], part[:annotations], annotation_index)
|
|
269
|
+
when
|
|
270
|
+
"response.shell_call_command.added",
|
|
271
|
+
"response.shell_call_command.delta",
|
|
272
|
+
"response.shell_call_command.done"
|
|
273
|
+
done = !type.end_with?(".delta")
|
|
274
|
+
value = data[done ? :command : :delta]
|
|
275
|
+
index = data[:command_index]
|
|
276
|
+
return unavailable unless value.is_a?(String) && valid_index?(index)
|
|
277
|
+
|
|
278
|
+
record = item_for(data, "shell_call", identity: false)
|
|
279
|
+
return unless record
|
|
135
280
|
|
|
136
|
-
|
|
281
|
+
if done
|
|
282
|
+
record[:commands][index] = value
|
|
137
283
|
else
|
|
138
|
-
return unavailable
|
|
284
|
+
return unavailable if record[:commands].key?(index) && !record[:commands][index].is_a?(String)
|
|
139
285
|
|
|
140
|
-
|
|
286
|
+
(record[:commands][index] ||= +"") << value
|
|
141
287
|
end
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
288
|
+
|
|
289
|
+
commands = record[:item].dig(:action, :commands)
|
|
290
|
+
sync_array(commands, record[:commands], index)
|
|
291
|
+
update_bound(record, data[:output_index])
|
|
292
|
+
when "response.shell_call_output_content.delta", "response.shell_call_output_content.done"
|
|
293
|
+
index = data[:command_index]
|
|
294
|
+
done = type.end_with?(".done")
|
|
295
|
+
value = data[done ? :output : :delta]
|
|
296
|
+
return unavailable unless valid_index?(index)
|
|
297
|
+
if done
|
|
298
|
+
return unavailable unless value.is_a?(Array)
|
|
299
|
+
else
|
|
300
|
+
unless value.is_a?(Hash) &&
|
|
301
|
+
value.all? { |key, entry| !%i[stdout stderr].include?(key) || entry.is_a?(String) }
|
|
148
302
|
return unavailable
|
|
149
303
|
end
|
|
304
|
+
end
|
|
305
|
+
|
|
306
|
+
record = item_for(data, "shell_call_output")
|
|
307
|
+
return unless record
|
|
308
|
+
|
|
309
|
+
bound = @command_bounds[record[:item][:call_id]]
|
|
310
|
+
return unavailable if bound && index >= bound[:count]
|
|
311
|
+
|
|
312
|
+
if done
|
|
313
|
+
record[:content][index] = make_part({output: value}, full: false)
|
|
150
314
|
else
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
315
|
+
part = record[:content][index] ||= make_part({}, full: false)
|
|
316
|
+
value.each do |key, entry|
|
|
317
|
+
if %i[stdout stderr].include?(key)
|
|
318
|
+
update_string(part[:part], key, entry, done: false)
|
|
319
|
+
else
|
|
320
|
+
part[:part][key] = entry
|
|
321
|
+
end
|
|
322
|
+
end
|
|
323
|
+
end
|
|
324
|
+
|
|
325
|
+
when "response.image_generation_call.partial_image"
|
|
326
|
+
# Image bytes are deliberately not accumulated. Status remains useful.
|
|
327
|
+
nil
|
|
328
|
+
else
|
|
329
|
+
if event_class.known_fields.key?(:output_index) &&
|
|
330
|
+
type.end_with?(".added", ".delta", ".done", ".partial_image")
|
|
331
|
+
unavailable
|
|
332
|
+
end
|
|
333
|
+
end
|
|
334
|
+
end
|
|
335
|
+
|
|
336
|
+
def item_for(data, kind, identity: true)
|
|
337
|
+
index = data[:output_index]
|
|
338
|
+
return unavailable unless valid_index?(index)
|
|
339
|
+
|
|
340
|
+
record = @items[index]
|
|
341
|
+
return unavailable if record && record[:kind] && record[:kind] != kind
|
|
342
|
+
if identity
|
|
343
|
+
id = data[:item_id]
|
|
344
|
+
return unavailable unless id.is_a?(String)
|
|
345
|
+
return unavailable if @ids.key?(id) && @ids[id] != index
|
|
346
|
+
if record
|
|
347
|
+
item = record[:item]
|
|
348
|
+
if item.key?(:id)
|
|
349
|
+
return unavailable unless item[:id] == id
|
|
350
|
+
elsif record[:full] && !%w[function_call custom_tool_call].include?(kind)
|
|
158
351
|
return unavailable
|
|
159
352
|
end
|
|
160
353
|
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
return
|
|
165
|
-
when OpenAI::Responses::ResponseContentPartAddedEvent
|
|
166
|
-
return unavailable unless part_index == item.content.length
|
|
167
|
-
when OpenAI::Responses::ResponseTextDoneEvent
|
|
168
|
-
part = item.content[part_index]
|
|
169
|
-
unless part.is_a?(OpenAI::Responses::ResponseOutputText) &&
|
|
170
|
-
event[:text].is_a?(String) &&
|
|
171
|
-
(event[:logprobs].nil? || event[:logprobs].is_a?(Array))
|
|
172
|
-
return unavailable
|
|
173
|
-
end
|
|
354
|
+
return unavailable if record[:bound_id] && record[:bound_id] != id
|
|
355
|
+
end
|
|
356
|
+
end
|
|
174
357
|
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
event.delta.is_a?(String) &&
|
|
183
|
-
(event[:logprobs].nil? || event[:logprobs].is_a?(Array)) &&
|
|
184
|
-
(part[:logprobs].nil? || part[:logprobs].is_a?(Array))
|
|
185
|
-
return unavailable
|
|
186
|
-
end
|
|
358
|
+
record ||= (@items[index] = make_record({}, full: false))
|
|
359
|
+
record[:kind] ||= kind
|
|
360
|
+
if identity
|
|
361
|
+
record[:bound_id] = data[:item_id]
|
|
362
|
+
record[:item][:id] = data[:item_id] unless record[:full]
|
|
363
|
+
@ids[data[:item_id]] = index
|
|
364
|
+
end
|
|
187
365
|
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
end
|
|
366
|
+
@phase = :provisional
|
|
367
|
+
record
|
|
368
|
+
end
|
|
192
369
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
370
|
+
def replace_item(index, item)
|
|
371
|
+
id = item[:id]
|
|
372
|
+
return unavailable if id.is_a?(String) && @ids.key?(id) && @ids[id] != index
|
|
196
373
|
|
|
197
|
-
|
|
198
|
-
|
|
374
|
+
if (old = @items[index])
|
|
375
|
+
@ids.delete(old[:bound_id]) if old[:bound_id]
|
|
376
|
+
call_id = old[:item][:call_id]
|
|
377
|
+
@command_bounds.delete(call_id) if @command_bounds[call_id]&.fetch(:index) == index
|
|
378
|
+
end
|
|
199
379
|
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
380
|
+
record = @items[index] = make_record(item, full: true)
|
|
381
|
+
@ids[id] = index if id.is_a?(String)
|
|
382
|
+
update_bound(record, index)
|
|
383
|
+
@phase = :provisional
|
|
384
|
+
end
|
|
203
385
|
|
|
204
|
-
|
|
205
|
-
|
|
386
|
+
def make_record(item, full:)
|
|
387
|
+
kind = item[:type]&.to_s
|
|
388
|
+
{
|
|
389
|
+
item: item,
|
|
390
|
+
full: full,
|
|
391
|
+
kind: kind,
|
|
392
|
+
bound_id: item[:id],
|
|
393
|
+
content: part_collection?(kind, :content) ? indexed_parts(item[:content]) : {},
|
|
394
|
+
summary: part_collection?(kind, :summary) ? indexed_parts(item[:summary]) : {},
|
|
395
|
+
commands: kind == "shell_call" && item[:action].is_a?(Hash) && item[:action][:commands].is_a?(Array) ? item[
|
|
396
|
+
:action
|
|
397
|
+
][:commands]
|
|
398
|
+
.each_with_index
|
|
399
|
+
.to_h { |value, index| [index, value] } : {}
|
|
400
|
+
}
|
|
401
|
+
end
|
|
206
402
|
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
210
|
-
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
403
|
+
def indexed_parts(parts)
|
|
404
|
+
return {} unless parts.is_a?(Array)
|
|
405
|
+
|
|
406
|
+
parts.each_with_index.to_h { |part, index| [index, make_part(part, full: true)] }
|
|
407
|
+
end
|
|
408
|
+
|
|
409
|
+
def make_part(part, full:)
|
|
410
|
+
{
|
|
411
|
+
part: part,
|
|
412
|
+
full: full,
|
|
413
|
+
kind: part[:type]&.to_s,
|
|
414
|
+
annotations: part[:type].to_s == "output_text" && part[:annotations].is_a?(Array) ? part[:annotations]
|
|
415
|
+
.each_with_index
|
|
416
|
+
.to_h { |value, index|
|
|
417
|
+
[index, value]
|
|
418
|
+
} : {}
|
|
419
|
+
}
|
|
420
|
+
end
|
|
421
|
+
|
|
422
|
+
def part_for(record, collection, index, kind)
|
|
423
|
+
part = record[collection][index]
|
|
424
|
+
return unavailable if part && part[:kind] && part[:kind] != kind
|
|
425
|
+
|
|
426
|
+
part ||= (record[collection][index] = make_part({}, full: false))
|
|
427
|
+
part[:kind] ||= kind
|
|
428
|
+
part
|
|
429
|
+
end
|
|
430
|
+
|
|
431
|
+
def put_part(record, collection, index, part)
|
|
432
|
+
record[collection][index] = make_part(part, full: true)
|
|
433
|
+
sync_array(record[:item][collection], record[collection], index) { |value| value[:part] }
|
|
434
|
+
end
|
|
435
|
+
|
|
436
|
+
# Grow only through actual contiguous observations, never through a hole.
|
|
437
|
+
def sync_array(array, values, index)
|
|
438
|
+
return unless array.is_a?(Array)
|
|
439
|
+
|
|
440
|
+
if index < array.length
|
|
441
|
+
value = values[index]
|
|
442
|
+
array[index] = block_given? ? yield(value) : value
|
|
443
|
+
end
|
|
444
|
+
|
|
445
|
+
while values.key?(array.length)
|
|
446
|
+
value = values[array.length]
|
|
447
|
+
array << (block_given? ? yield(value) : value)
|
|
448
|
+
end
|
|
449
|
+
end
|
|
450
|
+
|
|
451
|
+
def update_bound(record, index)
|
|
452
|
+
return unless record[:kind] == "shell_call"
|
|
453
|
+
|
|
454
|
+
call_id = record[:item][:call_id]
|
|
455
|
+
commands = record[:item].dig(:action, :commands)
|
|
456
|
+
return unless call_id.is_a?(String)
|
|
457
|
+
|
|
458
|
+
if commands.is_a?(Array) && commands.length == record[:commands].length
|
|
459
|
+
@command_bounds[call_id] = {index: index, count: commands.length}
|
|
460
|
+
else
|
|
461
|
+
@command_bounds.delete(call_id)
|
|
462
|
+
end
|
|
463
|
+
end
|
|
464
|
+
|
|
465
|
+
def update_string(fields, field, value, done:, nullable: false, required: false)
|
|
466
|
+
if done
|
|
467
|
+
fields[field] = value
|
|
468
|
+
else
|
|
469
|
+
return unavailable if required && !fields.key?(field)
|
|
470
|
+
|
|
471
|
+
old = fields[field]
|
|
472
|
+
return unavailable if fields.key?(field) && !old.is_a?(String) && !(nullable && old.nil?)
|
|
219
473
|
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
474
|
+
(fields[field] ||= +"") << value
|
|
475
|
+
end
|
|
476
|
+
|
|
477
|
+
true
|
|
478
|
+
end
|
|
479
|
+
|
|
480
|
+
def project_item(record)
|
|
481
|
+
item = record[:item].dup
|
|
482
|
+
%i[content summary].each do |collection|
|
|
483
|
+
parts = record[collection]
|
|
484
|
+
unless part_collection?(record[:kind], collection) &&
|
|
485
|
+
(item[collection].is_a?(Array) || parts[0]&.fetch(:full))
|
|
486
|
+
next
|
|
487
|
+
end
|
|
488
|
+
|
|
489
|
+
item[collection] = []
|
|
490
|
+
while (part = parts[item[collection].length]) && part[:full]
|
|
491
|
+
value = part[:part].dup
|
|
492
|
+
if part[:kind] == "output_text" && (value[:annotations].is_a?(Array) || part[:annotations].key?(0))
|
|
493
|
+
value[:annotations] = prefix(part[:annotations])
|
|
224
494
|
end
|
|
495
|
+
|
|
496
|
+
item[collection] << value
|
|
225
497
|
end
|
|
498
|
+
end
|
|
499
|
+
|
|
500
|
+
if record[:kind] == "shell_call" && item.dig(:action, :commands).is_a?(Array)
|
|
501
|
+
item[:action] = item[:action].dup
|
|
502
|
+
item[:action][:commands] = prefix(record[:commands])
|
|
503
|
+
end
|
|
504
|
+
|
|
505
|
+
copy(item)
|
|
506
|
+
end
|
|
226
507
|
|
|
227
|
-
|
|
508
|
+
def prefix(values)
|
|
509
|
+
result = []
|
|
510
|
+
while values.key?(result.length)
|
|
511
|
+
value = values[result.length]
|
|
512
|
+
break if value.nil?
|
|
513
|
+
|
|
514
|
+
result << value
|
|
228
515
|
end
|
|
516
|
+
|
|
517
|
+
result
|
|
518
|
+
end
|
|
519
|
+
|
|
520
|
+
def detailed_item(index, record)
|
|
521
|
+
item = record[:item].dup
|
|
522
|
+
result = {output_index: index, item: item}
|
|
523
|
+
%i[content summary].each do |collection|
|
|
524
|
+
parts = record[collection]
|
|
525
|
+
full_array = part_collection?(record[:kind], collection) && item[collection].is_a?(Array)
|
|
526
|
+
next unless full_array || !parts.empty?
|
|
527
|
+
|
|
528
|
+
item.delete(collection) if full_array
|
|
529
|
+
key = record[:kind] == "shell_call_output" ? :command_index : (collection == :summary ? :summary_index : :content_index)
|
|
530
|
+
result[collection] = parts.keys.sort.map do |part_index|
|
|
531
|
+
part = parts.fetch(part_index)
|
|
532
|
+
value = part[:part].dup
|
|
533
|
+
if part[:kind] == "output_text" && (value[:annotations].is_a?(Array) || !part[:annotations].empty?)
|
|
534
|
+
value.delete(:annotations)
|
|
535
|
+
value[:annotations] = part[:annotations].keys.sort.map do |annotation_index|
|
|
536
|
+
{annotation_index: annotation_index, annotation: part[:annotations].fetch(annotation_index)}
|
|
537
|
+
end
|
|
538
|
+
end
|
|
539
|
+
|
|
540
|
+
{key => part_index, :part => value}
|
|
541
|
+
end
|
|
542
|
+
end
|
|
543
|
+
|
|
544
|
+
if record[:kind] == "shell_call" && (item.dig(:action, :commands).is_a?(Array) || !record[:commands].empty?)
|
|
545
|
+
item[:action] = item[:action].dup if item[:action].is_a?(Hash)
|
|
546
|
+
item[:action]&.delete(:commands)
|
|
547
|
+
result[:commands] = record[:commands].keys.sort.map do |command_index|
|
|
548
|
+
{command_index: command_index, command: record[:commands].fetch(command_index)}
|
|
549
|
+
end
|
|
550
|
+
end
|
|
551
|
+
|
|
552
|
+
result
|
|
553
|
+
end
|
|
554
|
+
|
|
555
|
+
def observed_text(records)
|
|
556
|
+
records
|
|
557
|
+
.flat_map do |record|
|
|
558
|
+
next [] unless record[:kind] == "message"
|
|
559
|
+
|
|
560
|
+
record[:content].keys.sort.filter_map do |index|
|
|
561
|
+
part = record[:content].fetch(index)
|
|
562
|
+
text = part[:part][:text]
|
|
563
|
+
text if part[:kind] == "output_text" && text.is_a?(String)
|
|
564
|
+
end
|
|
565
|
+
end
|
|
566
|
+
.join
|
|
567
|
+
end
|
|
568
|
+
|
|
569
|
+
def part_collection?(kind, collection)
|
|
570
|
+
kind == "reasoning" || (kind == "message" && collection == :content)
|
|
571
|
+
end
|
|
572
|
+
|
|
573
|
+
def valid_index?(index)
|
|
574
|
+
index.is_a?(Integer) && index >= 0
|
|
229
575
|
end
|
|
230
576
|
|
|
231
577
|
def unavailable
|
|
@@ -234,8 +580,8 @@ module OpenAI
|
|
|
234
580
|
nil
|
|
235
581
|
end
|
|
236
582
|
|
|
237
|
-
# BaseModel's recursive representation
|
|
238
|
-
#
|
|
583
|
+
# BaseModel's recursive representation retains strings. Both incoming data
|
|
584
|
+
# and read views must cross this copy boundary before their owner can mutate.
|
|
239
585
|
def copy(value)
|
|
240
586
|
data = OpenAI::Internal::Type::BaseModel.recursively_to_h(value, convert: false)
|
|
241
587
|
data = parse_json(generate_json(data))
|