diff --git a/.release-please-manifest.json b/.release-please-manifest.json index 463488b6..4ad3fef3 100644 --- a/.release-please-manifest.json +++ b/.release-please-manifest.json @@ -1,3 +1,3 @@ { - ".": "0.17.1" + ".": "0.18.0" } \ No newline at end of file diff --git a/.stats.yml b/.stats.yml index 65629665..bbf5750e 100644 --- a/.stats.yml +++ b/.stats.yml @@ -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 diff --git a/CHANGELOG.md b/CHANGELOG.md index a1823fa6..9e1360ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -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) diff --git a/Gemfile.lock b/Gemfile.lock index cbdbfc91..2a86b134 100644 --- a/Gemfile.lock +++ b/Gemfile.lock @@ -11,7 +11,7 @@ GIT PATH remote: . specs: - openai (0.17.1) + openai (0.18.0) connection_pool GEM diff --git a/README.md b/README.md index bac257a3..631e0c9c 100644 --- a/README.md +++ b/README.md @@ -15,7 +15,7 @@ To use this gem, install via Bundler by adding the following to your application ```ruby -gem "openai", "~> 0.17.1" +gem "openai", "~> 0.18.0" ``` diff --git a/examples/structured_outputs_chat_completions.rb b/examples/structured_outputs_chat_completions.rb index 11aa6728..707c741f 100755 --- a/examples/structured_outputs_chat_completions.rb +++ b/examples/structured_outputs_chat_completions.rb @@ -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], diff --git a/examples/structured_outputs_responses.rb b/examples/structured_outputs_responses.rb index 5a7be6ab..25aa4442 100755 --- a/examples/structured_outputs_responses.rb +++ b/examples/structured_outputs_responses.rb @@ -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], diff --git a/lib/openai/helpers/structured_output/array_of.rb b/lib/openai/helpers/structured_output/array_of.rb index cf134ea9..fb49cce1 100644 --- a/lib/openai/helpers/structured_output/array_of.rb +++ b/lib/openai/helpers/structured_output/array_of.rb @@ -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 diff --git a/lib/openai/helpers/structured_output/base_model.rb b/lib/openai/helpers/structured_output/base_model.rb index 5294df80..a6914c26 100644 --- a/lib/openai/helpers/structured_output/base_model.rb +++ b/lib/openai/helpers/structured_output/base_model.rb @@ -28,15 +28,13 @@ 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, @@ -44,6 +42,8 @@ def to_json_schema_inner(state:) ) end schema = OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.to_nilable(schema) if nilable + OpenAI::Helpers::StructuredOutput::JsonSchemaConverter.assoc_meta!(schema, meta: meta) + [name, schema] end @@ -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" diff --git a/lib/openai/helpers/structured_output/json_schema_converter.rb b/lib/openai/helpers/structured_output/json_schema_converter.rb index 3a6c2ba9..35994f76 100644 --- a/lib/openai/helpers/structured_output/json_schema_converter.rb +++ b/lib/openai/helpers/structured_output/json_schema_converter.rb @@ -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} ] } @@ -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}] @@ -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/") } diff --git a/lib/openai/helpers/structured_output/union_of.rb b/lib/openai/helpers/structured_output/union_of.rb index 5ae7086b..64dd3fff 100644 --- a/lib/openai/helpers/structured_output/union_of.rb +++ b/lib/openai/helpers/structured_output/union_of.rb @@ -56,16 +56,8 @@ def self.[](...) = new(...) # @param variants [Array>] 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 diff --git a/lib/openai/version.rb b/lib/openai/version.rb index 32d93453..a326627c 100644 --- a/lib/openai/version.rb +++ b/lib/openai/version.rb @@ -1,5 +1,5 @@ # frozen_string_literal: true module OpenAI - VERSION = "0.17.1" + VERSION = "0.18.0" end diff --git a/rbi/openai/helpers/structured_output/array_of.rbi b/rbi/openai/helpers/structured_output/array_of.rbi index b0c70ce6..a9d9cf14 100644 --- a/rbi/openai/helpers/structured_output/array_of.rbi +++ b/rbi/openai/helpers/structured_output/array_of.rbi @@ -7,9 +7,6 @@ module OpenAI include OpenAI::Helpers::StructuredOutput::JsonSchemaConverter Elem = type_member(:out) - - sig { returns(String) } - attr_reader :description end end end diff --git a/rbi/openai/helpers/structured_output/json_schema_converter.rbi b/rbi/openai/helpers/structured_output/json_schema_converter.rbi index f32e2d04..0174fade 100644 --- a/rbi/openai/helpers/structured_output/json_schema_converter.rbi +++ b/rbi/openai/helpers/structured_output/json_schema_converter.rbi @@ -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( diff --git a/test/openai/helpers/structured_output_test.rb b/test/openai/helpers/structured_output_test.rb index 01cf9b0c..0cacaf9f 100644 --- a/test/openai/helpers/structured_output_test.rb +++ b/test/openai/helpers/structured_output_test.rb @@ -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 @@ -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 @@ -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 => { @@ -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 @@ -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 } } diff --git a/test/openai/resources/audio/speech_test.rb b/test/openai/resources/audio/speech_test.rb index f50614b3..af4189d5 100644 --- a/test/openai/resources/audio/speech_test.rb +++ b/test/openai/resources/audio/speech_test.rb @@ -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) diff --git a/test/openai/resources/containers/files/content_test.rb b/test/openai/resources/containers/files/content_test.rb index 4d4252d4..0a57b6cb 100644 --- a/test/openai/resources/containers/files/content_test.rb +++ b/test/openai/resources/containers/files/content_test.rb @@ -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") diff --git a/test/openai/resources/files_test.rb b/test/openai/resources/files_test.rb index de03395e..5833751a 100644 --- a/test/openai/resources/files_test.rb +++ b/test/openai/resources/files_test.rb @@ -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")