diff --git a/lib/openai/resources/images.rb b/lib/openai/resources/images.rb index f4f0c5f0..68ef112f 100644 --- a/lib/openai/resources/images.rb +++ b/lib/openai/resources/images.rb @@ -87,6 +87,7 @@ def create_variation(params) # @see OpenAI::Models::ImageEditParams def edit(params) parsed, options = OpenAI::ImageEditParams.dump_request(params) + parsed = prepare_edit_body(parsed) if parsed[:stream] message = "Please use `#edit_stream_raw` for the streaming use case." raise ArgumentError.new(message) @@ -148,6 +149,7 @@ def edit(params) # @see OpenAI::Models::ImageEditParams def edit_stream_raw(params) parsed, options = OpenAI::ImageEditParams.dump_request(params) + parsed = prepare_edit_body(parsed) unless parsed.fetch(:stream, true) message = "Please use `#edit` for the non-streaming use case." raise ArgumentError.new(message) @@ -286,6 +288,18 @@ def generate_stream_raw(params) ) end + # @api private + # + # @param parsed [Hash{Symbol=>Object}] + # + # @return [Hash{Symbol=>Object}] + private def prepare_edit_body(parsed) + # The Images API uses a bracketed multipart field name for multiple image uploads. + return parsed unless parsed[:image].is_a?(Array) + + parsed.transform_keys { _1 == :image ? :"image[]" : _1 } + end + # @api private # # @param client [OpenAI::Client] diff --git a/test/openai/resources/images_test.rb b/test/openai/resources/images_test.rb index 4bd40bf7..37d5bdd2 100644 --- a/test/openai/resources/images_test.rb +++ b/test/openai/resources/images_test.rb @@ -47,6 +47,41 @@ def test_edit_required_params end end + def test_edit_encodes_multiple_images_as_an_array_multipart_field + requests = [] + client = Object.new + client.define_singleton_method(:request) do |**request| + requests << request + request + end + images = OpenAI::Resources::Images.new(client: client) + files = [ + OpenAI::FilePart.new("first", filename: "first.png"), + OpenAI::FilePart.new("second", filename: "second.png") + ] + + images.edit(image: files, prompt: "Combine them") + images.edit_stream_raw(image: files, prompt: "Combine them") + + requests.each do |request| + names = multipart_field_names(request) + assert_equal(2, names.count("image[]")) + refute_includes(names, "image") + end + end + + def test_edit_preserves_the_scalar_image_multipart_field + client = Object.new + client.define_singleton_method(:request) { |**request| request } + images = OpenAI::Resources::Images.new(client: client) + + request = images.edit(image: OpenAI::FilePart.new("first", filename: "first.png"), prompt: "Edit it") + + names = multipart_field_names(request) + assert_equal(1, names.count("image")) + refute_includes(names, "image[]") + end + def test_generate_required_params response = @openai.images.generate(prompt: "A cute baby sea otter") @@ -66,4 +101,10 @@ def test_generate_required_params } end end + + private def multipart_field_names(request) + _headers, stream = OpenAI::Internal::Util.encode_content(request.fetch(:headers), request.fetch(:body)) + body = stream.respond_to?(:read) ? stream.read : stream.to_a.join + body.scan(/Content-Disposition: form-data; name="([^"]+)"/).flatten + end end