Skip to content

Commit ff507d0

Browse files
authored
fix!: structured output desc should go on array items not array itself (#799)
1 parent 48f8b12 commit ff507d0

File tree

9 files changed

+60
-55
lines changed

9 files changed

+60
-55
lines changed

examples/structured_outputs_chat_completions.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -22,7 +22,7 @@ class CalendarEvent < OpenAI::BaseModel
2222
required :name, String
2323
required :date, String
2424
required :participants, OpenAI::ArrayOf[Participant]
25-
required :optional_participants, OpenAI::ArrayOf[Participant], nil?: true
25+
required :optional_participants, OpenAI::ArrayOf[Participant, doc: "who might not show up"], nil?: true
2626
required :is_virtual, OpenAI::Boolean
2727
required :location,
2828
OpenAI::UnionOf[String, Location],

examples/structured_outputs_responses.rb

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -21,7 +21,7 @@ class CalendarEvent < OpenAI::BaseModel
2121
required :name, String
2222
required :date, String
2323
required :participants, OpenAI::ArrayOf[Participant]
24-
required :optional_participants, OpenAI::ArrayOf[Participant], nil?: true
24+
required :optional_participants, OpenAI::ArrayOf[Participant, doc: "who might not show up"], nil?: true
2525
required :is_virtual, OpenAI::Boolean
2626
required :location,
2727
OpenAI::UnionOf[String, Location],

lib/openai/helpers/structured_output/array_of.rb

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -30,19 +30,11 @@ def to_json_schema_inner(state:)
3030
state: state
3131
)
3232
items = OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.to_nilable(items) if nilable?
33+
OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.assoc_meta!(items, meta: @meta)
3334

34-
schema = {type: "array", items: items}
35-
description.nil? ? schema : schema.update(description: description)
35+
{type: "array", items: items}
3636
end
3737
end
38-
39-
# @return [String, nil]
40-
attr_reader :description
41-
42-
def initialize(type_info, spec = {})
43-
super
44-
@description = [type_info, spec].grep(Hash).filter_map { _1[:doc] }.first
45-
end
4638
end
4739
end
4840
end

lib/openai/helpers/structured_output/base_model.rb

Lines changed: 4 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -28,22 +28,22 @@ def to_json_schema_inner(state:)
2828
OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.cache_def!(state, type: self) do
2929
path = state.fetch(:path)
3030
properties = fields.to_h do |name, field|
31-
type, nilable = field.fetch_values(:type, :nilable)
31+
type, nilable, meta = field.fetch_values(:type, :nilable, :meta)
3232
new_state = {**state, path: [*path, ".#{name}"]}
3333

3434
schema =
3535
case type
36-
in {"$ref": String}
37-
type
3836
in OpenAI::Helpers::StructuredOutput::JsonSchemaConverter
39-
type.to_json_schema_inner(state: new_state).update(field.slice(:description))
37+
type.to_json_schema_inner(state: new_state)
4038
else
4139
OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.to_json_schema_inner(
4240
type,
4341
state: new_state
4442
)
4543
end
4644
schema = OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.to_nilable(schema) if nilable
45+
OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.assoc_meta!(schema, meta: meta)
46+
4747
[name, schema]
4848
end
4949

@@ -58,13 +58,6 @@ def to_json_schema_inner(state:)
5858
end
5959

6060
class << self
61-
def required(name_sym, type_info, spec = {})
62-
super
63-
64-
doc = [type_info, spec].grep(Hash).filter_map { _1[:doc] }.first
65-
known_fields.fetch(name_sym).update(description: doc) unless doc.nil?
66-
end
67-
6861
def optional(...)
6962
# rubocop:disable Layout/LineLength
7063
message = "`optional` is not supported for structured output APIs, use `#required` with `nil?: true` instead"

lib/openai/helpers/structured_output/json_schema_converter.rb

Lines changed: 19 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -46,7 +46,7 @@ def to_nilable(schema)
4646
in {"$ref": String}
4747
{
4848
anyOf: [
49-
schema.update(OpenAI::Helpers::StructuredOutput::JsonSchemaConverter::NO_REF => true),
49+
schema.merge!(OpenAI::Helpers::StructuredOutput::JsonSchemaConverter::NO_REF => true),
5050
{type: null}
5151
]
5252
}
@@ -60,6 +60,17 @@ def to_nilable(schema)
6060
end
6161
end
6262

63+
# @api private
64+
#
65+
# @param schema [Hash{Symbol=>Object}]
66+
def assoc_meta!(schema, meta:)
67+
xformed = meta.transform_keys(doc: :description)
68+
if schema.key?(:$ref) && !xformed.empty?
69+
schema.merge!(OpenAI::Helpers::StructuredOutput::JsonSchemaConverter::NO_REF => true)
70+
end
71+
schema.merge!(xformed)
72+
end
73+
6374
# @api private
6475
#
6576
# @param state [Hash{Symbol=>Object}]
@@ -116,12 +127,17 @@ def to_json_schema(type)
116127

117128
case refs
118129
in [ref]
119-
ref.replace(sch)
130+
ref.replace(ref.except(:$ref).merge(sch))
120131
in [_, ref, *]
121132
reused_defs.store(ref.fetch(:$ref), sch)
133+
refs.each do
134+
unless (meta = _1.except(:$ref)).empty?
135+
_1.replace(allOf: [_1.slice(:$ref), meta])
136+
end
137+
end
122138
else
123139
end
124-
no_refs.each { _1.replace(sch) }
140+
no_refs.each { _1.replace(_1.except(:$ref).merge(sch)) }
125141
end
126142

127143
xformed = reused_defs.transform_keys { _1.delete_prefix("#/$defs/") }

lib/openai/helpers/structured_output/union_of.rb

Lines changed: 2 additions & 10 deletions
Original file line numberDiff line numberDiff line change
@@ -56,16 +56,8 @@ def self.[](...) = new(...)
5656

5757
# @param variants [Array<generic<Member>>]
5858
def initialize(*variants)
59-
case variants
60-
in [Symbol => d, Hash => vs]
61-
discriminator(d)
62-
vs.each do |k, v|
63-
v.is_a?(Proc) ? variant(k, v) : variant(k, -> { v })
64-
end
65-
else
66-
variants.each do |v|
67-
v.is_a?(Proc) ? variant(v) : variant(-> { v })
68-
end
59+
variants.each do |v|
60+
v.is_a?(Proc) ? variant(v) : variant(-> { v })
6961
end
7062
end
7163
end

rbi/openai/helpers/structured_output/array_of.rbi

Lines changed: 0 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -7,9 +7,6 @@ module OpenAI
77
include OpenAI::Helpers::StructuredOutput::JsonSchemaConverter
88

99
Elem = type_member(:out)
10-
11-
sig { returns(String) }
12-
attr_reader :description
1310
end
1411
end
1512
end

rbi/openai/helpers/structured_output/json_schema_converter.rbi

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -46,6 +46,16 @@ module OpenAI
4646
def to_nilable(schema)
4747
end
4848

49+
# @api private
50+
sig do
51+
params(
52+
schema: OpenAI::Helpers::StructuredOutput::JsonSchema,
53+
meta: OpenAI::Internal::AnyHash
54+
).void
55+
end
56+
def assoc_meta!(schema, meta:)
57+
end
58+
4959
# @api private
5060
sig do
5161
params(

test/openai/helpers/structured_output_test.rb

Lines changed: 21 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -22,9 +22,10 @@ def test_misuse
2222
E1 = OpenAI::Helpers::StructuredOutput::EnumOf[:one]
2323

2424
class M1 < OpenAI::Helpers::StructuredOutput::BaseModel
25-
required :a, String
25+
required :a, String, doc: "dog"
2626
required :b, Integer, nil?: true
27-
required :c, E1, nil?: true
27+
required :c, E1, nil?: true, doc: "dog"
28+
required :d, E1, doc: "dog"
2829
end
2930

3031
class M2 < OpenAI::Helpers::StructuredOutput::BaseModel
@@ -36,7 +37,7 @@ class M3 < OpenAI::Helpers::StructuredOutput::BaseModel
3637
end
3738

3839
U1 = OpenAI::Helpers::StructuredOutput::UnionOf[Integer, A1]
39-
U2 = OpenAI::Helpers::StructuredOutput::UnionOf[:type, m2: M2, m3: M3]
40+
U2 = OpenAI::Helpers::StructuredOutput::UnionOf[M2, M3]
4041
U3 = OpenAI::Helpers::StructuredOutput::UnionOf[A1, A1]
4142

4243
def test_coerce
@@ -78,18 +79,21 @@ def test_to_schema
7879
A1 => {type: "array", items: {type: "string"}},
7980
OpenAI::Helpers::StructuredOutput::ArrayOf[String, nil?: true, doc: "a1"] => {
8081
type: "array",
81-
items: {type: %w[string null]},
82-
description: "a1"
82+
items: {type: %w[string null], description: "a1"}
8383
},
8484
E1 => {type: "string", enum: ["one"]},
8585
M1 => {
8686
type: "object",
8787
properties: {
88-
a: {type: "string"},
88+
a: {type: "string", description: "dog"},
8989
b: {type: %w[integer null]},
90-
c: {anyOf: [{type: "string", enum: %w[one]}, {type: "null"}]}
90+
c: {
91+
anyOf: [{type: "string", enum: ["one"]}, {type: "null"}],
92+
description: "dog"
93+
},
94+
d: {description: "dog", type: "string", enum: ["one"]}
9195
},
92-
required: %w[a b c],
96+
required: %w[a b c d],
9397
additionalProperties: false
9498
},
9599
U1 => {
@@ -162,8 +166,9 @@ class M10 < OpenAI::Helpers::StructuredOutput::BaseModel
162166

163167
class M11 < OpenAI::Helpers::StructuredOutput::BaseModel
164168
required :a, U3
165-
required :b, A1
169+
required :b, A1, doc: "dog"
166170
required :c, A1
171+
required :d, A1, doc: "dawg"
167172
end
168173

169174
def test_definition_reusing
@@ -311,20 +316,20 @@ def test_definition_reusing
311316
]
312317
},
313318
M11 => {
314-
:$defs => {".a/?.0/[]" => {type: "array", items: {type: "string"}}},
315-
:type => "object",
316-
:properties => {
319+
type: "object",
320+
properties: {
317321
a: {
318322
anyOf: [
319323
{type: "array", items: {type: "string"}},
320324
{type: "array", items: {type: "string"}}
321325
]
322326
},
323-
b: {:$ref => "#/$defs/.a/?.0/[]"},
324-
c: {:$ref => "#/$defs/.a/?.0/[]"}
327+
b: {description: "dog", type: "array", items: {type: "string"}},
328+
c: {type: "array", items: {type: "string"}},
329+
d: {description: "dawg", type: "array", items: {type: "string"}}
325330
},
326-
:required => %w[a b c],
327-
:additionalProperties => false
331+
required: %w[a b c d],
332+
additionalProperties: false
328333
}
329334
}
330335

0 commit comments

Comments
 (0)