Skip to content

release: 0.18.0 #179

New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Open
wants to merge 4 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "0.17.1"
".": "0.18.0"
}
4 changes: 2 additions & 2 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 109
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai%2Fopenai-6a1bfd4738fff02ef5becc3fdb2bf0cd6c026f2c924d4147a2a515474477dd9a.yml
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai%2Fopenai-9cadfad609f94f20ebf74fdc06a80302f1a324dc69700a309a8056aabca82fd2.yml
openapi_spec_hash: 3eb8d86c06f0bb5e1190983e5acfc9ba
config_hash: a67c5e195a59855fe8a5db0dc61a3e7f
config_hash: 68337b532875626269c304372a669f67
17 changes: 17 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,22 @@
# Changelog

## 0.18.0 (2025-08-11)

Full Changelog: [v0.17.1...v0.18.0](https://github.com/openai/openai-ruby/compare/v0.17.1...v0.18.0)

### ⚠ BREAKING CHANGES

* structured output desc should go on array items not array itself ([#799](https://github.com/openai/openai-ruby/issues/799))

### Bug Fixes

* structured output desc should go on array items not array itself ([#799](https://github.com/openai/openai-ruby/issues/799)) ([ff507d0](https://github.com/openai/openai-ruby/commit/ff507d095ff703ba3b44ab82b06eb4314688d4eb))


### Chores

* **internal:** update test skipping reason ([c815703](https://github.com/openai/openai-ruby/commit/c815703062ce79d2cb14f252ee5d23cf4ebf15ca))

## 0.17.1 (2025-08-09)

Full Changelog: [v0.17.0...v0.17.1](https://github.com/openai/openai-ruby/compare/v0.17.0...v0.17.1)
Expand Down
2 changes: 1 addition & 1 deletion Gemfile.lock
Original file line number Diff line number Diff line change
Expand Up @@ -11,7 +11,7 @@ GIT
PATH
remote: .
specs:
openai (0.17.1)
openai (0.18.0)
connection_pool

GEM
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ To use this gem, install via Bundler by adding the following to your application
<!-- x-release-please-start-version -->

```ruby
gem "openai", "~> 0.17.1"
gem "openai", "~> 0.18.0"
```

<!-- x-release-please-end -->
Expand Down
2 changes: 1 addition & 1 deletion examples/structured_outputs_chat_completions.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,7 +22,7 @@ class CalendarEvent < OpenAI::BaseModel
required :name, String
required :date, String
required :participants, OpenAI::ArrayOf[Participant]
required :optional_participants, OpenAI::ArrayOf[Participant], nil?: true
required :optional_participants, OpenAI::ArrayOf[Participant, doc: "who might not show up"], nil?: true
required :is_virtual, OpenAI::Boolean
required :location,
OpenAI::UnionOf[String, Location],
Expand Down
2 changes: 1 addition & 1 deletion examples/structured_outputs_responses.rb
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,7 @@ class CalendarEvent < OpenAI::BaseModel
required :name, String
required :date, String
required :participants, OpenAI::ArrayOf[Participant]
required :optional_participants, OpenAI::ArrayOf[Participant], nil?: true
required :optional_participants, OpenAI::ArrayOf[Participant, doc: "who might not show up"], nil?: true
required :is_virtual, OpenAI::Boolean
required :location,
OpenAI::UnionOf[String, Location],
Expand Down
12 changes: 2 additions & 10 deletions lib/openai/helpers/structured_output/array_of.rb
Original file line number Diff line number Diff line change
Expand Up @@ -30,19 +30,11 @@ def to_json_schema_inner(state:)
state: state
)
items = OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.to_nilable(items) if nilable?
OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.assoc_meta!(items, meta: @meta)

schema = {type: "array", items: items}
description.nil? ? schema : schema.update(description: description)
{type: "array", items: items}
end
end

# @return [String, nil]
attr_reader :description

def initialize(type_info, spec = {})
super
@description = [type_info, spec].grep(Hash).filter_map { _1[:doc] }.first
end
end
end
end
Expand Down
15 changes: 4 additions & 11 deletions lib/openai/helpers/structured_output/base_model.rb
Original file line number Diff line number Diff line change
Expand Up @@ -28,22 +28,22 @@ def to_json_schema_inner(state:)
OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.cache_def!(state, type: self) do
path = state.fetch(:path)
properties = fields.to_h do |name, field|
type, nilable = field.fetch_values(:type, :nilable)
type, nilable, meta = field.fetch_values(:type, :nilable, :meta)
new_state = {**state, path: [*path, ".#{name}"]}

schema =
case type
in {"$ref": String}
type
in OpenAI::Helpers::StructuredOutput::JsonSchemaConverter
type.to_json_schema_inner(state: new_state).update(field.slice(:description))
type.to_json_schema_inner(state: new_state)
else
OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.to_json_schema_inner(
type,
state: new_state
)
end
schema = OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.to_nilable(schema) if nilable
OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.assoc_meta!(schema, meta: meta)

[name, schema]
end

Expand All @@ -58,13 +58,6 @@ def to_json_schema_inner(state:)
end

class << self
def required(name_sym, type_info, spec = {})
super

doc = [type_info, spec].grep(Hash).filter_map { _1[:doc] }.first
known_fields.fetch(name_sym).update(description: doc) unless doc.nil?
end

def optional(...)
# rubocop:disable Layout/LineLength
message = "`optional` is not supported for structured output APIs, use `#required` with `nil?: true` instead"
Expand Down
22 changes: 19 additions & 3 deletions lib/openai/helpers/structured_output/json_schema_converter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -46,7 +46,7 @@ def to_nilable(schema)
in {"$ref": String}
{
anyOf: [
schema.update(OpenAI::Helpers::StructuredOutput::JsonSchemaConverter::NO_REF => true),
schema.merge!(OpenAI::Helpers::StructuredOutput::JsonSchemaConverter::NO_REF => true),
{type: null}
]
}
Expand All @@ -60,6 +60,17 @@ def to_nilable(schema)
end
end

# @api private
#
# @param schema [Hash{Symbol=>Object}]
def assoc_meta!(schema, meta:)
xformed = meta.transform_keys(doc: :description)
if schema.key?(:$ref) && !xformed.empty?
schema.merge!(OpenAI::Helpers::StructuredOutput::JsonSchemaConverter::NO_REF => true)
end
schema.merge!(xformed)
end

# @api private
#
# @param state [Hash{Symbol=>Object}]
Expand Down Expand Up @@ -116,12 +127,17 @@ def to_json_schema(type)

case refs
in [ref]
ref.replace(sch)
ref.replace(ref.except(:$ref).merge(sch))
in [_, ref, *]
reused_defs.store(ref.fetch(:$ref), sch)
refs.each do
unless (meta = _1.except(:$ref)).empty?
_1.replace(allOf: [_1.slice(:$ref), meta])
end
end
else
end
no_refs.each { _1.replace(sch) }
no_refs.each { _1.replace(_1.except(:$ref).merge(sch)) }
end

xformed = reused_defs.transform_keys { _1.delete_prefix("#/$defs/") }
Expand Down
12 changes: 2 additions & 10 deletions lib/openai/helpers/structured_output/union_of.rb
Original file line number Diff line number Diff line change
Expand Up @@ -56,16 +56,8 @@ def self.[](...) = new(...)

# @param variants [Array<generic<Member>>]
def initialize(*variants)
case variants
in [Symbol => d, Hash => vs]
discriminator(d)
vs.each do |k, v|
v.is_a?(Proc) ? variant(k, v) : variant(k, -> { v })
end
else
variants.each do |v|
v.is_a?(Proc) ? variant(v) : variant(-> { v })
end
variants.each do |v|
v.is_a?(Proc) ? variant(v) : variant(-> { v })
end
end
end
Expand Down
2 changes: 1 addition & 1 deletion lib/openai/version.rb
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
# frozen_string_literal: true

module OpenAI
VERSION = "0.17.1"
VERSION = "0.18.0"
end
3 changes: 0 additions & 3 deletions rbi/openai/helpers/structured_output/array_of.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,6 @@ module OpenAI
include OpenAI::Helpers::StructuredOutput::JsonSchemaConverter

Elem = type_member(:out)

sig { returns(String) }
attr_reader :description
end
end
end
Expand Down
10 changes: 10 additions & 0 deletions rbi/openai/helpers/structured_output/json_schema_converter.rbi
Original file line number Diff line number Diff line change
Expand Up @@ -46,6 +46,16 @@ module OpenAI
def to_nilable(schema)
end

# @api private
sig do
params(
schema: OpenAI::Helpers::StructuredOutput::JsonSchema,
meta: OpenAI::Internal::AnyHash
).void
end
def assoc_meta!(schema, meta:)
end

# @api private
sig do
params(
Expand Down
37 changes: 21 additions & 16 deletions test/openai/helpers/structured_output_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -22,9 +22,10 @@ def test_misuse
E1 = OpenAI::Helpers::StructuredOutput::EnumOf[:one]

class M1 < OpenAI::Helpers::StructuredOutput::BaseModel
required :a, String
required :a, String, doc: "dog"
required :b, Integer, nil?: true
required :c, E1, nil?: true
required :c, E1, nil?: true, doc: "dog"
required :d, E1, doc: "dog"
end

class M2 < OpenAI::Helpers::StructuredOutput::BaseModel
Expand All @@ -36,7 +37,7 @@ class M3 < OpenAI::Helpers::StructuredOutput::BaseModel
end

U1 = OpenAI::Helpers::StructuredOutput::UnionOf[Integer, A1]
U2 = OpenAI::Helpers::StructuredOutput::UnionOf[:type, m2: M2, m3: M3]
U2 = OpenAI::Helpers::StructuredOutput::UnionOf[M2, M3]
U3 = OpenAI::Helpers::StructuredOutput::UnionOf[A1, A1]

def test_coerce
Expand Down Expand Up @@ -78,18 +79,21 @@ def test_to_schema
A1 => {type: "array", items: {type: "string"}},
OpenAI::Helpers::StructuredOutput::ArrayOf[String, nil?: true, doc: "a1"] => {
type: "array",
items: {type: %w[string null]},
description: "a1"
items: {type: %w[string null], description: "a1"}
},
E1 => {type: "string", enum: ["one"]},
M1 => {
type: "object",
properties: {
a: {type: "string"},
a: {type: "string", description: "dog"},
b: {type: %w[integer null]},
c: {anyOf: [{type: "string", enum: %w[one]}, {type: "null"}]}
c: {
anyOf: [{type: "string", enum: ["one"]}, {type: "null"}],
description: "dog"
},
d: {description: "dog", type: "string", enum: ["one"]}
},
required: %w[a b c],
required: %w[a b c d],
additionalProperties: false
},
U1 => {
Expand Down Expand Up @@ -162,8 +166,9 @@ class M10 < OpenAI::Helpers::StructuredOutput::BaseModel

class M11 < OpenAI::Helpers::StructuredOutput::BaseModel
required :a, U3
required :b, A1
required :b, A1, doc: "dog"
required :c, A1
required :d, A1, doc: "dawg"
end

def test_definition_reusing
Expand Down Expand Up @@ -311,20 +316,20 @@ def test_definition_reusing
]
},
M11 => {
:$defs => {".a/?.0/[]" => {type: "array", items: {type: "string"}}},
:type => "object",
:properties => {
type: "object",
properties: {
a: {
anyOf: [
{type: "array", items: {type: "string"}},
{type: "array", items: {type: "string"}}
]
},
b: {:$ref => "#/$defs/.a/?.0/[]"},
c: {:$ref => "#/$defs/.a/?.0/[]"}
b: {description: "dog", type: "array", items: {type: "string"}},
c: {type: "array", items: {type: "string"}},
d: {description: "dawg", type: "array", items: {type: "string"}}
},
:required => %w[a b c],
:additionalProperties => false
required: %w[a b c d],
additionalProperties: false
}
}

Expand Down
2 changes: 1 addition & 1 deletion test/openai/resources/audio/speech_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

class OpenAI::Test::Resources::Audio::SpeechTest < OpenAI::Test::ResourceTest
def test_create_required_params
skip("skipped: test server currently has no support for method content-type")
skip("Prism doesn't support application/octet-stream responses")

response = @openai.audio.speech.create(input: "input", model: :"tts-1", voice: :alloy)

Expand Down
2 changes: 1 addition & 1 deletion test/openai/resources/containers/files/content_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -4,7 +4,7 @@

class OpenAI::Test::Resources::Containers::Files::ContentTest < OpenAI::Test::ResourceTest
def test_retrieve_required_params
skip("skipped: test server currently has no support for method content-type")
skip("Prism doesn't support application/binary responses")

response = @openai.containers.files.content.retrieve("file_id", container_id: "container_id")

Expand Down
2 changes: 1 addition & 1 deletion test/openai/resources/files_test.rb
Original file line number Diff line number Diff line change
Expand Up @@ -93,7 +93,7 @@ def test_delete
end

def test_content
skip("skipped: test server currently has no support for method content-type")
skip("Prism doesn't support application/binary responses")

response = @openai.files.content("file_id")

Expand Down