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.
- checksums.yaml +4 -4
- data/CHANGELOG.md +13 -0
- data/README.md +53 -1
- data/lib/openai/helpers/sorbet.rb +324 -0
- data/lib/openai/helpers/streaming/response_event_decoder.rb +44 -0
- data/lib/openai/helpers/streaming/response_stream.rb +10 -3
- data/lib/openai/helpers/streaming/unknown_stream_event.rb +51 -0
- data/lib/openai/internal/stream.rb +4 -1
- data/lib/openai/models/beta/beta_response_function_tool_call_output_item.rb +9 -9
- data/lib/openai/models/beta/beta_response_input_item.rb +9 -9
- data/lib/openai/models/responses/response_function_tool_call_output_item.rb +9 -9
- data/lib/openai/models/responses/response_input_item.rb +9 -9
- data/lib/openai/resources/beta/responses.rb +2 -2
- data/lib/openai/resources/responses.rb +3 -3
- data/lib/openai/version.rb +1 -1
- data/rbi/openai/helpers/sorbet.rbi +55 -0
- data/rbi/openai/helpers/streaming/response_event_decoder.rbi +28 -0
- data/rbi/openai/helpers/streaming/response_stream.rbi +5 -0
- data/rbi/openai/helpers/streaming/unknown_stream_event.rbi +29 -0
- data/rbi/openai/models/beta/beta_response_function_tool_call_output_item.rbi +13 -10
- data/rbi/openai/models/beta/beta_response_input_item.rbi +10 -10
- data/rbi/openai/models/responses/response_function_tool_call_output_item.rbi +13 -10
- data/rbi/openai/models/responses/response_input_item.rbi +10 -10
- data/rbi/openai/resources/beta/responses.rbi +10 -2
- data/rbi/openai/resources/responses.rbi +2 -2
- data/rbi/openai/streaming.rbi +1 -0
- data/sig/openai/helpers/sorbet.rbs +54 -0
- data/sig/openai/helpers/streaming/response_event_decoder.rbs +16 -0
- data/sig/openai/helpers/streaming/unknown_stream_event.rbs +15 -0
- data/sig/openai/models/beta/beta_response_function_tool_call_output_item.rbs +7 -5
- data/sig/openai/models/beta/beta_response_input_item.rbs +5 -5
- data/sig/openai/models/responses/response_function_tool_call_output_item.rbs +7 -5
- data/sig/openai/models/responses/response_input_item.rbs +5 -5
- data/sig/openai/resources/beta/responses.rbs +4 -2
- data/sig/openai/resources/responses.rbs +6 -3
- data/sig/openai/streaming.rbs +5 -0
- metadata +11 -1
checksums.yaml
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
---
|
|
2
2
|
SHA256:
|
|
3
|
-
metadata.gz:
|
|
4
|
-
data.tar.gz:
|
|
3
|
+
metadata.gz: 46609364ce91a3198dceda84c59f20bc29f5bb205f4078289748b41b8df4cdbe
|
|
4
|
+
data.tar.gz: 749aa50031731341acedf30ef7011cdba8591140cf932eb835793295f2f4328f
|
|
5
5
|
SHA512:
|
|
6
|
-
metadata.gz:
|
|
7
|
-
data.tar.gz:
|
|
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.
|
|
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
|
-
|
|
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 <<
|
|
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:,
|
|
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(
|
|
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.
|