openai 0.81.0 → 0.82.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.
Files changed (37) hide show
  1. checksums.yaml +4 -4
  2. data/CHANGELOG.md +13 -0
  3. data/README.md +53 -1
  4. data/lib/openai/helpers/sorbet.rb +324 -0
  5. data/lib/openai/helpers/streaming/response_event_decoder.rb +44 -0
  6. data/lib/openai/helpers/streaming/response_stream.rb +10 -3
  7. data/lib/openai/helpers/streaming/unknown_stream_event.rb +51 -0
  8. data/lib/openai/internal/stream.rb +4 -1
  9. data/lib/openai/models/beta/beta_response_function_tool_call_output_item.rb +9 -9
  10. data/lib/openai/models/beta/beta_response_input_item.rb +9 -9
  11. data/lib/openai/models/responses/response_function_tool_call_output_item.rb +9 -9
  12. data/lib/openai/models/responses/response_input_item.rb +9 -9
  13. data/lib/openai/resources/beta/responses.rb +2 -2
  14. data/lib/openai/resources/responses.rb +3 -3
  15. data/lib/openai/version.rb +1 -1
  16. data/rbi/openai/helpers/sorbet.rbi +55 -0
  17. data/rbi/openai/helpers/streaming/response_event_decoder.rbi +28 -0
  18. data/rbi/openai/helpers/streaming/response_stream.rbi +5 -0
  19. data/rbi/openai/helpers/streaming/unknown_stream_event.rbi +29 -0
  20. data/rbi/openai/models/beta/beta_response_function_tool_call_output_item.rbi +13 -10
  21. data/rbi/openai/models/beta/beta_response_input_item.rbi +10 -10
  22. data/rbi/openai/models/responses/response_function_tool_call_output_item.rbi +13 -10
  23. data/rbi/openai/models/responses/response_input_item.rbi +10 -10
  24. data/rbi/openai/resources/beta/responses.rbi +10 -2
  25. data/rbi/openai/resources/responses.rbi +2 -2
  26. data/rbi/openai/streaming.rbi +1 -0
  27. data/sig/openai/helpers/sorbet.rbs +54 -0
  28. data/sig/openai/helpers/streaming/response_event_decoder.rbs +16 -0
  29. data/sig/openai/helpers/streaming/unknown_stream_event.rbs +15 -0
  30. data/sig/openai/models/beta/beta_response_function_tool_call_output_item.rbs +7 -5
  31. data/sig/openai/models/beta/beta_response_input_item.rbs +5 -5
  32. data/sig/openai/models/responses/response_function_tool_call_output_item.rbs +7 -5
  33. data/sig/openai/models/responses/response_input_item.rbs +5 -5
  34. data/sig/openai/resources/beta/responses.rbs +4 -2
  35. data/sig/openai/resources/responses.rbs +6 -3
  36. data/sig/openai/streaming.rbs +5 -0
  37. metadata +11 -1
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: 97ce6ca6902b7dc85c97989c0555036a7fe45783129a2139186d0cee87f36cb0
4
- data.tar.gz: 15132d79dd284edb8fa80e3f6d0a72a7b0345285770182b93e6d3e376218c9ae
3
+ metadata.gz: 46609364ce91a3198dceda84c59f20bc29f5bb205f4078289748b41b8df4cdbe
4
+ data.tar.gz: 749aa50031731341acedf30ef7011cdba8591140cf932eb835793295f2f4328f
5
5
  SHA512:
6
- metadata.gz: 21a457fa9fcbf0a59f7ed109acee5a0d55b47a757d9d1a0dfbc12fee70a07cf3766d0369c512a923d117f6a1f6c2179810d8f81c5edf48ba7e7de18c611c083f
7
- data.tar.gz: 1d0bc3c3de8bd6180b293e14ae6b4b15cead9feb6a8fe6ae49bf65fc140c3d7f6a473fa75d7f85a1436be47ab3ec7fdaaf38b9164746a7b293c19a861482d107
6
+ metadata.gz: dae2c1a9b16e8a6cc0ecca16aa60d0cc1da965a289ba618df760e6bf1a9afa5c9a0333a0c80cb5717957969b86f98506c3fcc0e33b9bc3b2ad47a1dae8a90b79
7
+ data.tar.gz: 19c8eb602f862152a728350ca05386d7b9ea4951296bd62e3a07313294adc0fbdfe775c25bb04bc41995e5ea2b1ff005e001bcd76b4144a5673b9bdceb7e98a3
data/CHANGELOG.md CHANGED
@@ -1,5 +1,18 @@
1
1
  # Changelog
2
2
 
3
+ ## [0.82.0](https://github.com/openai/openai-ruby/compare/v0.81.0...v0.82.0) (2026-08-27)
4
+
5
+
6
+ ### Features
7
+
8
+ * add optional Sorbet structured-output models ([#537](https://github.com/openai/openai-ruby/issues/537)) ([53ccf39](https://github.com/openai/openai-ruby/commit/53ccf39b464d8b2052456fa78aaea4dcdee8158c))
9
+ * **api:** make function call output call IDs optional ([#539](https://github.com/openai/openai-ruby/issues/539)) ([81d385b](https://github.com/openai/openai-ruby/commit/81d385be58ece0c5db798ed79d01c0fd4c458f60))
10
+
11
+
12
+ ### Bug Fixes
13
+
14
+ * **streaming:** preserve unknown Responses stream events ([#538](https://github.com/openai/openai-ruby/issues/538)) ([1d728f7](https://github.com/openai/openai-ruby/commit/1d728f74649c039ad29ca55986f9f5f48436d9f0))
15
+
3
16
  ## [0.81.0](https://github.com/openai/openai-ruby/compare/v0.80.0...v0.81.0) (2026-08-26)
4
17
 
5
18
 
data/README.md CHANGED
@@ -15,7 +15,7 @@ To use this gem, install via Bundler by adding the following to your application
15
15
  <!-- x-release-please-start-version -->
16
16
 
17
17
  ```ruby
18
- gem "openai", "~> 0.81.0"
18
+ gem "openai", "~> 0.82.0"
19
19
  ```
20
20
 
21
21
  <!-- x-release-please-end -->
@@ -764,6 +764,58 @@ response = client.responses.create(
764
764
 
765
765
  </details>
766
766
 
767
+ #### Sorbet structured-output models
768
+
769
+ Applications that use Sorbet can provide their own `T::Struct` models instead of
770
+ subclassing `OpenAI::BaseModel`. Add `sorbet-runtime` to your application's
771
+ dependencies and explicitly load the optional integration:
772
+
773
+ ```ruby
774
+ require "openai"
775
+ require "openai/helpers/sorbet"
776
+
777
+ class Participant < T::Struct
778
+ const :name, String
779
+ const :email, T.nilable(String)
780
+ end
781
+
782
+ class CalendarEvent < T::Struct
783
+ const :title, String
784
+ const :participants, T::Array[Participant]
785
+ end
786
+
787
+ schema = OpenAI::StructuredOutput.from_sorbet(CalendarEvent)
788
+ client = OpenAI::Client.new
789
+
790
+ response = client.responses.create(
791
+ model: "gpt-5.2",
792
+ input: "Extract the event information.",
793
+ text: schema
794
+ )
795
+
796
+ message = response.output.grep(OpenAI::Responses::ResponseOutputMessage).fetch(0)
797
+ output_text = message.content.grep(OpenAI::Responses::ResponseOutputText).fetch(0)
798
+ event = T.cast(output_text.parsed, CalendarEvent)
799
+ event.participants.fetch(0).name
800
+
801
+ completion = client.chat.completions.create(
802
+ model: "gpt-5.2",
803
+ messages: [{role: :user, content: "Extract the event information."}],
804
+ response_format: schema
805
+ )
806
+
807
+ T.cast(completion.choices.fetch(0).message.parsed, CalendarEvent)
808
+ ```
809
+
810
+ The integration supports nested structs, arrays, nullable values, boolean and
811
+ numeric fields, application-defined `T::Enum` values, and Sorbet's `name:` field
812
+ aliases. All structured-output fields are required, including nullable fields.
813
+ Unsupported unions, hashes, recursive models, and enum values that do not
814
+ serialize to strings are rejected. `sorbet-runtime` remains optional and is not
815
+ loaded by ordinary SDK usage. This integration supports non-streaming
816
+ `responses.create` and `chat.completions.create` requests only; streaming
817
+ structured outputs and function-tool schemas are not supported.
818
+
767
819
  ### Handling errors
768
820
 
769
821
  When the library is unable to connect to the API, or if the API returns a non-success status code (i.e., 4xx or 5xx response), a subclass of `OpenAI::Errors::APIError` will be thrown:
@@ -0,0 +1,324 @@
1
+ # frozen_string_literal: true
2
+
3
+ begin
4
+ require "sorbet-runtime"
5
+
6
+ rescue LoadError => e
7
+ raise unless e.path == "sorbet-runtime"
8
+
9
+ raise LoadError, "Sorbet structured outputs require the optional sorbet-runtime gem"
10
+ end
11
+
12
+ require "openai"
13
+
14
+ module OpenAI
15
+ module Helpers
16
+ module StructuredOutput
17
+ # Adapts application-owned Sorbet models to structured output requests.
18
+ #
19
+ # Load explicitly with `require "openai/helpers/sorbet"`.
20
+ class SorbetAdapter
21
+ include OpenAI::Internal::Type::Converter
22
+ include OpenAI::Helpers::StructuredOutput::JsonSchemaConverter
23
+
24
+ class HydrationError < TypeError
25
+ end
26
+
27
+ Field = Data.define(:name, :wire_name, :node)
28
+ Node = Data.define(:kind, :value, :nullable)
29
+ private_constant :Field, :Node
30
+
31
+ # @return [String]
32
+ attr_reader :name
33
+
34
+ # @param model [Class<T::Struct>]
35
+ def initialize(model)
36
+ unless model.is_a?(Class) && model < T::Struct && !model.name.to_s.empty?
37
+ raise ArgumentError, "#{model} must be a named T::Struct subclass"
38
+ end
39
+
40
+ @name = model.name
41
+ @compiled_structs = {}
42
+ @node = compile_struct(model, path: @name, ancestors: [])
43
+ end
44
+
45
+ # @return [Hash{Symbol=>Object}]
46
+ def to_json_schema = OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.to_json_schema(self)
47
+
48
+ # @api private
49
+ def to_json_schema_inner(state:) = schema_for(@node, state: state)
50
+
51
+ # @api private
52
+ def coerce(value, state:)
53
+ return value if value.nil? || value.is_a?(JSON::ParserError)
54
+
55
+ hydrated = hydrate(@node, value, path: @name.split("::").last)
56
+ state.fetch(:exactness)[:yes] += 1
57
+ hydrated
58
+ end
59
+
60
+ private def compile_struct(model, path:, ancestors:)
61
+ if ancestors.include?(model)
62
+ raise ArgumentError, "#{path}: recursive Sorbet models are not supported"
63
+ end
64
+
65
+ return @compiled_structs.fetch(model) if @compiled_structs.key?(model)
66
+
67
+ fields = model.props.map do |field_name, rules|
68
+ wire_name = rules.fetch(:serialized_form)
69
+ type = rules.fetch(:type_object)
70
+ child = compile_type(type, path: "#{path}.#{field_name}", ancestors: [*ancestors, model])
71
+
72
+ Field.new(name: field_name, wire_name: wire_name, node: child)
73
+ end
74
+
75
+ if (duplicate = fields.map(&:wire_name).tally.find { _2 > 1 })
76
+ raise ArgumentError, "#{path}: duplicate serialized field name #{duplicate.first.inspect}"
77
+ end
78
+
79
+ @compiled_structs.store(model, Node.new(kind: :object, value: [model, fields], nullable: false))
80
+ end
81
+
82
+ private def compile_type(type, path:, ancestors:)
83
+ case type
84
+ in T::Types::TypedArray
85
+ item = compile_type(type.type, path: "#{path}[]", ancestors: ancestors)
86
+ Node.new(kind: :array, value: item, nullable: false)
87
+ in T::Types::Union
88
+ compile_union(type, path: path, ancestors: ancestors)
89
+ in T::Types::Simple
90
+ compile_class(type.raw_type, path: path, ancestors: ancestors)
91
+ else
92
+ raise ArgumentError, "#{path}: unsupported Sorbet type #{type}"
93
+ end
94
+ end
95
+
96
+ private def compile_union(type, path:, ancestors:)
97
+ members = type.types
98
+ nullable, remaining = members.partition do |member|
99
+ member.is_a?(T::Types::Simple) && member.raw_type == NilClass
100
+ end
101
+
102
+ node = if boolean_members?(remaining)
103
+ Node.new(kind: :boolean, value: nil, nullable: false)
104
+ elsif remaining.length == 1 && nullable.length == 1
105
+ compile_type(remaining.first, path: path, ancestors: ancestors)
106
+ else
107
+ raise ArgumentError, "#{path}: unsupported Sorbet union #{type}"
108
+ end
109
+
110
+ node.with(nullable: !nullable.empty?)
111
+ end
112
+
113
+ private def boolean_members?(members)
114
+ members.length == 2 &&
115
+ members.all? { _1.is_a?(T::Types::Simple) } &&
116
+ members.map(&:raw_type).to_set == [TrueClass, FalseClass].to_set
117
+ end
118
+
119
+ private def compile_class(type, path:, ancestors:)
120
+ if [String, Integer, Float].include?(type)
121
+ Node.new(kind: :scalar, value: type, nullable: false)
122
+ elsif type < T::Enum
123
+ unless type.values.all? { _1.serialize.is_a?(String) }
124
+ raise ArgumentError, "#{path}: Sorbet enum values must serialize to strings"
125
+ end
126
+
127
+ Node.new(kind: :enum, value: type, nullable: false)
128
+ elsif type < T::Struct
129
+ compile_struct(type, path: path, ancestors: ancestors)
130
+ else
131
+ raise ArgumentError, "#{path}: unsupported Sorbet class #{type}"
132
+ end
133
+ end
134
+
135
+ private def schema_for(node, state:)
136
+ schema = case node.kind
137
+ in :object
138
+ model, fields = node.value
139
+ OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.cache_def!(state, type: model) do
140
+ properties = fields.to_h do |field|
141
+ child_state = {**state, path: [*state.fetch(:path), ".#{field.wire_name}"]}
142
+
143
+ [field.wire_name.to_sym, schema_for(field.node, state: child_state)]
144
+ end
145
+
146
+ {
147
+ type: "object",
148
+ properties: properties,
149
+ required: fields.map(&:wire_name),
150
+ additionalProperties: false
151
+ }
152
+ end
153
+
154
+ in :array
155
+ {type: "array", items: schema_for(node.value, state: state)}
156
+ in :boolean
157
+ {type: "boolean"}
158
+ in :enum
159
+ {type: "string", enum: node.value.values.map(&:serialize)}
160
+ in :scalar
161
+ {type: {String => "string", Integer => "integer", Float => "number"}.fetch(node.value)}
162
+ end
163
+
164
+ if node.nullable
165
+ if [:object, :enum].include?(node.kind)
166
+ {anyOf: [schema, {type: "null"}]}
167
+ else
168
+ OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.to_nilable(schema)
169
+ end
170
+ else
171
+ schema
172
+ end
173
+ end
174
+
175
+ private def hydrate(node, value, path:)
176
+ return nil if value.nil? && node.nullable
177
+
178
+ case node.kind
179
+ in :object
180
+ hydrate_struct(node.value, value, path: path)
181
+ in :array
182
+ unless value.is_a?(Array)
183
+ fail_hydration(path, expected: "Array", actual: value)
184
+ end
185
+
186
+ value.map.with_index { |item, index| hydrate(node.value, item, path: "#{path}[#{index}]") }
187
+ in :enum
188
+ unless value.is_a?(String)
189
+ fail_hydration(path, expected: node.value.name, actual: value)
190
+ end
191
+
192
+ begin
193
+ node.value.deserialize(value)
194
+ rescue StandardError
195
+ fail_hydration(path, expected: node.value.name, actual: value)
196
+ end
197
+
198
+ in :boolean
199
+ unless value.equal?(true) || value.equal?(false)
200
+ fail_hydration(path, expected: "Boolean", actual: value)
201
+ end
202
+
203
+ value
204
+ in :scalar
205
+ value = value.to_f if node.value == Float && value.is_a?(Integer)
206
+
207
+ unless value.is_a?(node.value) && (node.value != Float || value.finite?)
208
+ fail_hydration(path, expected: node.value.name, actual: value)
209
+ end
210
+
211
+ value
212
+ end
213
+ end
214
+
215
+ private def hydrate_struct(shape, value, path:)
216
+ model, fields = shape
217
+ unless value.is_a?(Hash)
218
+ fail_hydration(path, expected: model.name, actual: value)
219
+ end
220
+
221
+ expected_names = fields.map(&:wire_name).to_set
222
+ unless value.keys.all? { (_1.is_a?(Symbol) || _1.is_a?(String)) && expected_names.include?(_1.to_s) }
223
+ raise HydrationError, "#{path}: unexpected structured-output field", cause: nil
224
+ end
225
+
226
+ attributes = fields.to_h do |field|
227
+ keys = [field.wire_name.to_sym, field.wire_name].select { value.key?(_1) }
228
+ field_path = "#{path}.#{field.name}"
229
+ unless keys.one?
230
+ reason = keys.empty? ? "missing required field" : "duplicate field"
231
+ raise HydrationError, "#{field_path}: #{reason}", cause: nil
232
+ end
233
+
234
+ [field.name, hydrate(field.node, value.fetch(keys.first), path: field_path)]
235
+ end
236
+
237
+ begin
238
+ model.new(**attributes)
239
+ rescue StandardError
240
+ raise HydrationError, "#{path}: invalid #{model.name} structured output", cause: nil
241
+ end
242
+ end
243
+
244
+ private def fail_hydration(path, expected:, actual:)
245
+ raise HydrationError, "#{path}: expected #{expected}, got #{actual.class}", cause: nil
246
+ end
247
+ end
248
+
249
+ # @param model [Class<T::Struct>]
250
+ # @return [OpenAI::Helpers::StructuredOutput::SorbetAdapter]
251
+ def self.from_sorbet(model) = OpenAI::Helpers::StructuredOutput::SorbetAdapter.new(model)
252
+
253
+ # Reject unsupported Sorbet request shapes before generated resources dispatch requests.
254
+ #
255
+ # @api private
256
+ module SorbetRequestGuard
257
+ def create(...)
258
+ reject_sorbet_function_tools!(...)
259
+ super
260
+ end
261
+
262
+ def stream(params)
263
+ reject_sorbet_streaming_formats!(params)
264
+ super
265
+ end
266
+
267
+ def stream_raw(...)
268
+ reject_sorbet_streaming_formats!(...)
269
+ super
270
+ end
271
+
272
+ private def reject_sorbet_streaming_formats!(params = {})
273
+ formats = [params[:text], params[:response_format], *Array(params[:tools])]
274
+ if params.is_a?(Hash)
275
+ formats.concat([params["text"], params["response_format"], *Array(params["tools"])])
276
+ end
277
+
278
+ if formats.any? { sorbet_adapter?(_1) }
279
+ raise ArgumentError, "Sorbet structured-output models are not supported for streaming"
280
+ end
281
+ end
282
+
283
+ private def reject_sorbet_function_tools!(params = {})
284
+ tools = Array(params[:tools])
285
+ tools.concat(Array(params["tools"])) if params.is_a?(Hash)
286
+
287
+ if tools.any? { sorbet_adapter?(_1) }
288
+ raise ArgumentError, "Sorbet structured-output function tools are not supported"
289
+ end
290
+ end
291
+
292
+ private def sorbet_adapter?(format)
293
+ return true if format.is_a?(SorbetAdapter)
294
+ format = format.to_h if format.is_a?(OpenAI::Internal::Type::BaseModel)
295
+ return false unless format.is_a?(Hash)
296
+
297
+ keys = [
298
+ :format,
299
+ "format",
300
+ :format_,
301
+ "format_",
302
+ :json_schema,
303
+ "json_schema",
304
+ :schema,
305
+ "schema",
306
+ :parameters,
307
+ "parameters",
308
+ :function,
309
+ "function"
310
+ ]
311
+ format.values_at(*keys).any? do |nested|
312
+ nested = nested.to_h if nested.is_a?(OpenAI::Internal::Type::BaseModel)
313
+ nested.is_a?(SorbetAdapter) ||
314
+ (nested.is_a?(Hash) && nested.values_at(:schema, "schema", :parameters, "parameters").any?(SorbetAdapter))
315
+ end
316
+ end
317
+ end
318
+
319
+ private_constant :SorbetRequestGuard
320
+ OpenAI::Resources::Responses.prepend(SorbetRequestGuard)
321
+ OpenAI::Resources::Chat::Completions.prepend(SorbetRequestGuard)
322
+ end
323
+ end
324
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "unknown_stream_event"
4
+
5
+ module OpenAI
6
+ module Helpers
7
+ module Streaming
8
+ # @api private
9
+ class ResponseEventDecoder
10
+ def initialize(model:)
11
+ @model = model
12
+ return unless response_stream_model?
13
+
14
+ @known_types = model.variants.to_h do |variant|
15
+ type = variant.known_fields.fetch(:type).fetch(:const)
16
+ [type.to_s, true]
17
+ end
18
+ end
19
+
20
+ def decode(data)
21
+ if unknown_response_event?(data)
22
+ UnknownStreamEvent.new(data: data)
23
+ else
24
+ OpenAI::Internal::Type::Converter.coerce(@model, data)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ def response_stream_model?
31
+ @model.equal?(OpenAI::Models::Responses::ResponseStreamEvent) ||
32
+ @model.equal?(OpenAI::Models::Beta::BetaResponseStreamEvent)
33
+ end
34
+
35
+ def unknown_response_event?(data)
36
+ return false unless @known_types && data.is_a?(Hash)
37
+
38
+ type = data[:type]
39
+ (type.is_a?(String) || type.is_a?(Symbol)) && !@known_types.key?(type.to_s)
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
@@ -63,13 +63,18 @@ module OpenAI
63
63
  @raw_stream.each do |raw_event|
64
64
  events_to_yield = @state.handle_event(raw_event)
65
65
  events_to_yield.each do |event|
66
- if @starting_after.nil? || event.sequence_number > @starting_after
67
- y << event
68
- end
66
+ y << event if after_starting_event?(event)
69
67
  end
70
68
  end
71
69
  end
72
70
  end
71
+
72
+ def after_starting_event?(event)
73
+ return true if @starting_after.nil?
74
+ return true if event.is_a?(UnknownStreamEvent) && event.sequence_number.nil?
75
+
76
+ event.sequence_number > @starting_after
77
+ end
73
78
  end
74
79
 
75
80
  class ResponseStreamState
@@ -82,6 +87,8 @@ module OpenAI
82
87
  end
83
88
 
84
89
  def handle_event(event)
90
+ return [event] if event.is_a?(UnknownStreamEvent)
91
+
85
92
  @current_snapshot = accumulate_event(
86
93
  event: event,
87
94
  current_snapshot: @current_snapshot
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module OpenAI
4
+ module Helpers
5
+ module Streaming
6
+ # A Responses event whose discriminator is newer than this SDK version.
7
+ class UnknownStreamEvent
8
+ # @return [Symbol]
9
+ attr_reader :type
10
+
11
+ # @return [Integer, nil]
12
+ attr_reader :sequence_number
13
+
14
+ # @return [Hash{Symbol=>Object}]
15
+ attr_reader :data
16
+
17
+ # @api private
18
+ def initialize(data:)
19
+ value = data.fetch(:type)
20
+ unless value.is_a?(String) || value.is_a?(Symbol)
21
+ raise ArgumentError, "Responses stream event type must be a string or symbol"
22
+ end
23
+
24
+ @type = value.to_sym
25
+ sequence_number = data[:sequence_number]
26
+ @sequence_number = sequence_number if sequence_number.is_a?(Integer)
27
+ @data = freeze_json(data)
28
+ freeze
29
+ end
30
+
31
+ # @return [Hash{Symbol=>Object}]
32
+ def to_h = @data
33
+
34
+ private def freeze_json(value)
35
+ case value
36
+ when Hash
37
+ value.each do |key, item|
38
+ freeze_json(key)
39
+ freeze_json(item)
40
+ end
41
+
42
+ when Array
43
+ value.each { |item| freeze_json(item) }
44
+ end
45
+
46
+ value.freeze
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -1,5 +1,7 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require_relative "../helpers/streaming/response_event_decoder"
4
+
3
5
  module OpenAI
4
6
  module Internal
5
7
  # @generic Elem
@@ -18,6 +20,7 @@ module OpenAI
18
20
  # rubocop:disable Metrics/BlockLength
19
21
  @iterator ||= OpenAI::Internal::Util.chain_fused(@stream) do |y|
20
22
  consume = false
23
+ decoder = OpenAI::Helpers::Streaming::ResponseEventDecoder.new(model: @model)
21
24
 
22
25
  @stream.each do |msg|
23
26
  next if consume
@@ -53,7 +56,7 @@ module OpenAI
53
56
  # Some providers emit transport heartbeats as JSON events. They are not
54
57
  # part of the API's typed streaming event surface.
55
58
  next if unwrapped in {type: "keepalive"}
56
- y << OpenAI::Internal::Type::Converter.coerce(@model, unwrapped)
59
+ y << decoder.decode(unwrapped)
57
60
  end
58
61
  else
59
62
  end
@@ -10,12 +10,6 @@ module OpenAI
10
10
  # @return [String]
11
11
  required :id, String
12
12
 
13
- # @!attribute call_id
14
- # The unique ID of the function tool call generated by the model.
15
- #
16
- # @return [String]
17
- required :call_id, String
18
-
19
13
  # @!attribute output
20
14
  # The output from the function call generated by your code. Can be a string or an
21
15
  # list of output content.
@@ -42,6 +36,12 @@ module OpenAI
42
36
  # @return [OpenAI::Models::Beta::BetaResponseFunctionToolCallOutputItem::Agent, nil]
43
37
  optional :agent, -> { OpenAI::Beta::BetaResponseFunctionToolCallOutputItem::Agent }, nil?: true
44
38
 
39
+ # @!attribute call_id
40
+ # The unique ID of the function tool call generated by the model.
41
+ #
42
+ # @return [String, nil]
43
+ optional :call_id, String
44
+
45
45
  # @!attribute caller_
46
46
  # The execution context that produced this tool call.
47
47
  #
@@ -71,20 +71,20 @@ module OpenAI
71
71
  # @return [String, nil]
72
72
  optional :namespace, String
73
73
 
74
- # @!method initialize(id:, call_id:, output:, status:, agent: nil, caller_: nil, created_by: nil, name: nil, namespace: nil, type: :function_call_output)
74
+ # @!method initialize(id:, output:, status:, agent: nil, call_id: nil, caller_: nil, created_by: nil, name: nil, namespace: nil, type: :function_call_output)
75
75
  # Some parameter documentations has been truncated, see
76
76
  # {OpenAI::Models::Beta::BetaResponseFunctionToolCallOutputItem} for more details.
77
77
  #
78
78
  # @param id [String] The unique ID of the function call tool output.
79
79
  #
80
- # @param call_id [String] The unique ID of the function tool call generated by the model.
81
- #
82
80
  # @param output [String, Array<OpenAI::Models::Beta::BetaResponseInputText, OpenAI::Models::Beta::BetaResponseInputImage, OpenAI::Models::Beta::BetaResponseInputFile>] The output from the function call generated by your code.
83
81
  #
84
82
  # @param status [Symbol, OpenAI::Models::Beta::BetaResponseFunctionToolCallOutputItem::Status] The status of the item. One of `in_progress`, `completed`, or
85
83
  #
86
84
  # @param agent [OpenAI::Models::Beta::BetaResponseFunctionToolCallOutputItem::Agent, nil] The agent that produced this item.
87
85
  #
86
+ # @param call_id [String] The unique ID of the function tool call generated by the model.
87
+ #
88
88
  # @param caller_ [OpenAI::Models::Beta::BetaResponseFunctionToolCallOutputItem::Caller::Direct, OpenAI::Models::Beta::BetaResponseFunctionToolCallOutputItem::Caller::Program, nil] The execution context that produced this tool call.
89
89
  #
90
90
  # @param created_by [String] The identifier of the actor that created the item.
@@ -371,12 +371,6 @@ module OpenAI
371
371
  end
372
372
 
373
373
  class FunctionCallOutput < OpenAI::Internal::Type::BaseModel
374
- # @!attribute call_id
375
- # The unique ID of the function tool call generated by the model.
376
- #
377
- # @return [String]
378
- required :call_id, String
379
-
380
374
  # @!attribute output
381
375
  # Text, image, or file output of the function tool call.
382
376
  #
@@ -402,6 +396,12 @@ module OpenAI
402
396
  # @return [OpenAI::Models::Beta::BetaResponseInputItem::FunctionCallOutput::Agent, nil]
403
397
  optional :agent, -> { OpenAI::Beta::BetaResponseInputItem::FunctionCallOutput::Agent }, nil?: true
404
398
 
399
+ # @!attribute call_id
400
+ # The unique ID of the function tool call generated by the model.
401
+ #
402
+ # @return [String, nil]
403
+ optional :call_id, String, nil?: true
404
+
405
405
  # @!attribute caller_
406
406
  # The execution context that produced this tool call.
407
407
  #
@@ -438,21 +438,21 @@ module OpenAI
438
438
  nil?: true
439
439
  )
440
440
 
441
- # @!method initialize(call_id:, output:, id: nil, agent: nil, caller_: nil, name: nil, namespace: nil, status: nil, type: :function_call_output)
441
+ # @!method initialize(output:, id: nil, agent: nil, call_id: nil, caller_: nil, name: nil, namespace: nil, status: nil, type: :function_call_output)
442
442
  # Some parameter documentations has been truncated, see
443
443
  # {OpenAI::Models::Beta::BetaResponseInputItem::FunctionCallOutput} for more
444
444
  # details.
445
445
  #
446
446
  # The output of a function tool call.
447
447
  #
448
- # @param call_id [String] The unique ID of the function tool call generated by the model.
449
- #
450
448
  # @param output [String, Array<OpenAI::Models::Beta::BetaResponseInputTextContent, OpenAI::Models::Beta::BetaResponseInputImageContent, OpenAI::Models::Beta::BetaResponseInputFileContent>] Text, image, or file output of the function tool call.
451
449
  #
452
450
  # @param id [String, nil] The unique ID of the function tool call output. Populated when this item is retu
453
451
  #
454
452
  # @param agent [OpenAI::Models::Beta::BetaResponseInputItem::FunctionCallOutput::Agent, nil] The agent that produced this item.
455
453
  #
454
+ # @param call_id [String, nil] The unique ID of the function tool call generated by the model.
455
+ #
456
456
  # @param caller_ [OpenAI::Models::Beta::BetaResponseInputItem::FunctionCallOutput::Caller::Direct, OpenAI::Models::Beta::BetaResponseInputItem::FunctionCallOutput::Caller::Program, nil] The execution context that produced this tool call.
457
457
  #
458
458
  # @param name [String, nil] The name of the tool that produced the output.