Skip to content

Commit ece942c

Browse files
feat: support specifying content-type with FilePart class
1 parent bf84473 commit ece942c

Some content is hidden

Large Commits have some content hidden by default. Use the searchbox below for content that may be hidden.

46 files changed

+342
-174
lines changed

lib/openai.rb

+2-1
Original file line numberDiff line numberDiff line change
@@ -31,7 +31,7 @@
3131
require_relative "openai/internal/type/converter"
3232
require_relative "openai/internal/type/unknown"
3333
require_relative "openai/internal/type/boolean"
34-
require_relative "openai/internal/type/io_like"
34+
require_relative "openai/internal/type/file_input"
3535
require_relative "openai/internal/type/enum"
3636
require_relative "openai/internal/type/union"
3737
require_relative "openai/internal/type/array_of"
@@ -42,6 +42,7 @@
4242
require_relative "openai/internal/type/request_parameters"
4343
require_relative "openai/internal"
4444
require_relative "openai/request_options"
45+
require_relative "openai/file_part"
4546
require_relative "openai/errors"
4647
require_relative "openai/internal/transport/base_client"
4748
require_relative "openai/internal/transport/pooled_net_requester"

lib/openai/file_part.rb

+55
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,55 @@
1+
# frozen_string_literal: true
2+
3+
module OpenAI
4+
class FilePart
5+
# @return [Pathname, StringIO, IO, String]
6+
attr_reader :content
7+
8+
# @return [String, nil]
9+
attr_reader :content_type
10+
11+
# @return [String, nil]
12+
attr_reader :filename
13+
14+
# @api private
15+
#
16+
# @return [String]
17+
private def read
18+
case contents
19+
in Pathname
20+
contents.read(binmode: true)
21+
in StringIO
22+
contents.string
23+
in IO
24+
contents.read
25+
in String
26+
contents
27+
end
28+
end
29+
30+
# @param a [Object]
31+
#
32+
# @return [String]
33+
def to_json(*a) = read.to_json(*a)
34+
35+
# @param a [Object]
36+
#
37+
# @return [String]
38+
def to_yaml(*a) = read.to_yaml(*a)
39+
40+
# @param content [Pathname, StringIO, IO, String]
41+
# @param filename [String, nil]
42+
# @param content_type [String, nil]
43+
def initialize(content, filename: nil, content_type: nil)
44+
@content = content
45+
@filename =
46+
case content
47+
in Pathname
48+
filename.nil? ? content.basename.to_path : File.basename(filename)
49+
else
50+
filename.nil? ? nil : File.basename(filename)
51+
end
52+
@content_type = content_type
53+
end
54+
end
55+
end

lib/openai/internal/type/converter.rb

+1-1
Original file line numberDiff line numberDiff line change
@@ -43,7 +43,7 @@ def dump(value, state:)
4343
value.string
4444
in Pathname | IO
4545
state[:can_retry] = false if value.is_a?(IO)
46-
OpenAI::Internal::Util::SerializationAdapter.new(value)
46+
OpenAI::FilePart.new(value)
4747
else
4848
value
4949
end

lib/openai/internal/type/io_like.rb renamed to lib/openai/internal/type/file_input.rb

+8-4
Original file line numberDiff line numberDiff line change
@@ -7,8 +7,12 @@ module Type
77
#
88
# @abstract
99
#
10-
# Either `Pathname` or `StringIO`.
11-
class IOLike
10+
# Either `Pathname` or `StringIO`, or `IO`, or
11+
# `OpenAI::Internal::Type::FileInput`.
12+
#
13+
# Note: when `IO` is used, all retries are disabled, since many IO` streams are
14+
# not rewindable.
15+
class FileInput
1216
extend OpenAI::Internal::Type::Converter
1317

1418
private_class_method :new
@@ -20,7 +24,7 @@ class IOLike
2024
# @return [Boolean]
2125
def self.===(other)
2226
case other
23-
in StringIO | Pathname | IO
27+
in Pathname | StringIO | IO | String | OpenAI::FilePart
2428
true
2529
else
2630
false
@@ -32,7 +36,7 @@ def self.===(other)
3236
# @param other [Object]
3337
#
3438
# @return [Boolean]
35-
def self.==(other) = other.is_a?(Class) && other <= OpenAI::Internal::Type::IOLike
39+
def self.==(other) = other.is_a?(Class) && other <= OpenAI::Internal::Type::FileInput
3640

3741
class << self
3842
# @api private

lib/openai/internal/util.rb

+49-47
Original file line numberDiff line numberDiff line change
@@ -348,27 +348,6 @@ def normalized_headers(*headers)
348348
end
349349
end
350350

351-
# @api private
352-
class SerializationAdapter
353-
# @return [Pathname, IO]
354-
attr_reader :inner
355-
356-
# @param a [Object]
357-
#
358-
# @return [String]
359-
def to_json(*a) = (inner.is_a?(IO) ? inner.read : inner.read(binmode: true)).to_json(*a)
360-
361-
# @param a [Object]
362-
#
363-
# @return [String]
364-
def to_yaml(*a) = (inner.is_a?(IO) ? inner.read : inner.read(binmode: true)).to_yaml(*a)
365-
366-
# @api private
367-
#
368-
# @param inner [Pathname, IO]
369-
def initialize(inner) = @inner = inner
370-
end
371-
372351
# @api private
373352
#
374353
# An adapter that satisfies the IO interface required by `::IO.copy_stream`
@@ -480,42 +459,35 @@ class << self
480459
# @api private
481460
#
482461
# @param y [Enumerator::Yielder]
483-
# @param boundary [String]
484-
# @param key [Symbol, String]
485462
# @param val [Object]
486463
# @param closing [Array<Proc>]
487-
private def write_multipart_chunk(y, boundary:, key:, val:, closing:)
488-
val = val.inner if val.is_a?(OpenAI::Internal::Util::SerializationAdapter)
464+
# @param content_type [String, nil]
465+
private def write_multipart_content(y, val:, closing:, content_type: nil)
466+
content_type ||= "application/octet-stream"
489467

490-
y << "--#{boundary}\r\n"
491-
y << "Content-Disposition: form-data"
492-
unless key.nil?
493-
name = ERB::Util.url_encode(key.to_s)
494-
y << "; name=\"#{name}\""
495-
end
496-
case val
497-
in Pathname | IO
498-
filename = ERB::Util.url_encode(File.basename(val.to_path))
499-
y << "; filename=\"#{filename}\""
500-
else
501-
end
502-
y << "\r\n"
503468
case val
469+
in OpenAI::FilePart
470+
return write_multipart_content(
471+
y,
472+
val: val.content,
473+
closing: closing,
474+
content_type: val.content_type
475+
)
504476
in Pathname
505-
y << "Content-Type: application/octet-stream\r\n\r\n"
477+
y << "Content-Type: #{content_type}\r\n\r\n"
506478
io = val.open(binmode: true)
507479
closing << io.method(:close)
508480
IO.copy_stream(io, y)
509481
in IO
510-
y << "Content-Type: application/octet-stream\r\n\r\n"
482+
y << "Content-Type: #{content_type}\r\n\r\n"
511483
IO.copy_stream(val, y)
512484
in StringIO
513-
y << "Content-Type: application/octet-stream\r\n\r\n"
485+
y << "Content-Type: #{content_type}\r\n\r\n"
514486
y << val.string
515487
in String
516-
y << "Content-Type: application/octet-stream\r\n\r\n"
488+
y << "Content-Type: #{content_type}\r\n\r\n"
517489
y << val.to_s
518-
in _ if primitive?(val)
490+
in -> { primitive?(_1) }
519491
y << "Content-Type: text/plain\r\n\r\n"
520492
y << val.to_s
521493
else
@@ -525,6 +497,36 @@ class << self
525497
y << "\r\n"
526498
end
527499

500+
# @api private
501+
#
502+
# @param y [Enumerator::Yielder]
503+
# @param boundary [String]
504+
# @param key [Symbol, String]
505+
# @param val [Object]
506+
# @param closing [Array<Proc>]
507+
private def write_multipart_chunk(y, boundary:, key:, val:, closing:)
508+
y << "--#{boundary}\r\n"
509+
y << "Content-Disposition: form-data"
510+
511+
unless key.nil?
512+
name = ERB::Util.url_encode(key.to_s)
513+
y << "; name=\"#{name}\""
514+
end
515+
516+
case val
517+
in OpenAI::FilePart unless val.filename.nil?
518+
filename = ERB::Util.url_encode(val.filename)
519+
y << "; filename=\"#{filename}\""
520+
in Pathname | IO
521+
filename = ERB::Util.url_encode(File.basename(val.to_path))
522+
y << "; filename=\"#{filename}\""
523+
else
524+
end
525+
y << "\r\n"
526+
527+
write_multipart_content(y, val: val, closing: closing)
528+
end
529+
528530
# @api private
529531
#
530532
# @param body [Object]
@@ -565,21 +567,21 @@ class << self
565567
# @return [Object]
566568
def encode_content(headers, body)
567569
content_type = headers["content-type"]
568-
body = body.inner if body.is_a?(OpenAI::Internal::Util::SerializationAdapter)
569-
570570
case [content_type, body]
571571
in [OpenAI::Internal::Util::JSON_CONTENT, Hash | Array | -> { primitive?(_1) }]
572572
[headers, JSON.fast_generate(body)]
573-
in [OpenAI::Internal::Util::JSONL_CONTENT, Enumerable] unless body.is_a?(StringIO) || body.is_a?(IO)
573+
in [OpenAI::Internal::Util::JSONL_CONTENT, Enumerable] unless body.is_a?(OpenAI::Internal::Type::FileInput)
574574
[headers, body.lazy.map { JSON.fast_generate(_1) }]
575-
in [%r{^multipart/form-data}, Hash | Pathname | StringIO | IO]
575+
in [%r{^multipart/form-data}, Hash | OpenAI::Internal::Type::FileInput]
576576
boundary, strio = encode_multipart_streaming(body)
577577
headers = {**headers, "content-type" => "#{content_type}; boundary=#{boundary}"}
578578
[headers, strio]
579579
in [_, Symbol | Numeric]
580580
[headers, body.to_s]
581581
in [_, StringIO]
582582
[headers, body.string]
583+
in [_, OpenAI::FilePart]
584+
[headers, body.content]
583585
else
584586
[headers, body]
585587
end

lib/openai/models/audio/transcription_create_params.rb

+3-3
Original file line numberDiff line numberDiff line change
@@ -14,8 +14,8 @@ class TranscriptionCreateParams < OpenAI::Internal::Type::BaseModel
1414
# The audio file object (not file name) to transcribe, in one of these formats:
1515
# flac, mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm.
1616
#
17-
# @return [Pathname, StringIO]
18-
required :file, OpenAI::Internal::Type::IOLike
17+
# @return [Pathname, StringIO, IO, OpenAI::FilePart]
18+
required :file, OpenAI::Internal::Type::FileInput
1919

2020
# @!attribute model
2121
# ID of the model to use. The options are `gpt-4o-transcribe`,
@@ -86,7 +86,7 @@ class TranscriptionCreateParams < OpenAI::Internal::Type::BaseModel
8686
# Some parameter documentations has been truncated, see
8787
# {OpenAI::Models::Audio::TranscriptionCreateParams} for more details.
8888
#
89-
# @param file [Pathname, StringIO] The audio file object (not file name) to transcribe, in one of these formats: fl
89+
# @param file [Pathname, StringIO, IO, OpenAI::FilePart] The audio file object (not file name) to transcribe, in one of these formats: fl
9090
# ...
9191
#
9292
# @param model [String, Symbol, OpenAI::Models::AudioModel] ID of the model to use. The options are `gpt-4o-transcribe`, `gpt-4o-mini-transc

lib/openai/models/audio/translation_create_params.rb

+3-3
Original file line numberDiff line numberDiff line change
@@ -12,8 +12,8 @@ class TranslationCreateParams < OpenAI::Internal::Type::BaseModel
1212
# The audio file object (not file name) translate, in one of these formats: flac,
1313
# mp3, mp4, mpeg, mpga, m4a, ogg, wav, or webm.
1414
#
15-
# @return [Pathname, StringIO]
16-
required :file, OpenAI::Internal::Type::IOLike
15+
# @return [Pathname, StringIO, IO, OpenAI::FilePart]
16+
required :file, OpenAI::Internal::Type::FileInput
1717

1818
# @!attribute model
1919
# ID of the model to use. Only `whisper-1` (which is powered by our open source
@@ -52,7 +52,7 @@ class TranslationCreateParams < OpenAI::Internal::Type::BaseModel
5252
# Some parameter documentations has been truncated, see
5353
# {OpenAI::Models::Audio::TranslationCreateParams} for more details.
5454
#
55-
# @param file [Pathname, StringIO] The audio file object (not file name) translate, in one of these formats: flac,
55+
# @param file [Pathname, StringIO, IO, OpenAI::FilePart] The audio file object (not file name) translate, in one of these formats: flac,
5656
# ...
5757
#
5858
# @param model [String, Symbol, OpenAI::Models::AudioModel] ID of the model to use. Only `whisper-1` (which is powered by our open source Wh

lib/openai/models/file_create_params.rb

+3-3
Original file line numberDiff line numberDiff line change
@@ -10,8 +10,8 @@ class FileCreateParams < OpenAI::Internal::Type::BaseModel
1010
# @!attribute file
1111
# The File object (not file name) to be uploaded.
1212
#
13-
# @return [Pathname, StringIO]
14-
required :file, OpenAI::Internal::Type::IOLike
13+
# @return [Pathname, StringIO, IO, OpenAI::FilePart]
14+
required :file, OpenAI::Internal::Type::FileInput
1515

1616
# @!attribute purpose
1717
# The intended purpose of the uploaded file. One of: - `assistants`: Used in the
@@ -26,7 +26,7 @@ class FileCreateParams < OpenAI::Internal::Type::BaseModel
2626
# Some parameter documentations has been truncated, see
2727
# {OpenAI::Models::FileCreateParams} for more details.
2828
#
29-
# @param file [Pathname, StringIO] The File object (not file name) to be uploaded. ...
29+
# @param file [Pathname, StringIO, IO, OpenAI::FilePart] The File object (not file name) to be uploaded. ...
3030
#
3131
# @param purpose [Symbol, OpenAI::Models::FilePurpose] The intended purpose of the uploaded file. One of: - `assistants`: Used in the A
3232
# ...

lib/openai/models/image_create_variation_params.rb

+3-3
Original file line numberDiff line numberDiff line change
@@ -11,8 +11,8 @@ class ImageCreateVariationParams < OpenAI::Internal::Type::BaseModel
1111
# The image to use as the basis for the variation(s). Must be a valid PNG file,
1212
# less than 4MB, and square.
1313
#
14-
# @return [Pathname, StringIO]
15-
required :image, OpenAI::Internal::Type::IOLike
14+
# @return [Pathname, StringIO, IO, OpenAI::FilePart]
15+
required :image, OpenAI::Internal::Type::FileInput
1616

1717
# @!attribute model
1818
# The model to use for image generation. Only `dall-e-2` is supported at this
@@ -56,7 +56,7 @@ class ImageCreateVariationParams < OpenAI::Internal::Type::BaseModel
5656
# Some parameter documentations has been truncated, see
5757
# {OpenAI::Models::ImageCreateVariationParams} for more details.
5858
#
59-
# @param image [Pathname, StringIO] The image to use as the basis for the variation(s). Must be a valid PNG file, le
59+
# @param image [Pathname, StringIO, IO, OpenAI::FilePart] The image to use as the basis for the variation(s). Must be a valid PNG file, le
6060
# ...
6161
#
6262
# @param model [String, Symbol, OpenAI::Models::ImageModel, nil] The model to use for image generation. Only `dall-e-2` is supported at this time

lib/openai/models/image_edit_params.rb

+7-7
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,7 @@ class ImageEditParams < OpenAI::Internal::Type::BaseModel
1313
# 25MB. For `dall-e-2`, you can only provide one image, and it should be a square
1414
# `png` file less than 4MB.
1515
#
16-
# @return [Pathname, StringIO, Array<Pathname, StringIO>]
16+
# @return [Pathname, StringIO, IO, OpenAI::FilePart, Array<Pathname, StringIO, IO, OpenAI::FilePart>]
1717
required :image, union: -> { OpenAI::Models::ImageEditParams::Image }
1818

1919
# @!attribute prompt
@@ -29,8 +29,8 @@ class ImageEditParams < OpenAI::Internal::Type::BaseModel
2929
# the mask will be applied on the first image. Must be a valid PNG file, less than
3030
# 4MB, and have the same dimensions as `image`.
3131
#
32-
# @return [Pathname, StringIO, nil]
33-
optional :mask, OpenAI::Internal::Type::IOLike
32+
# @return [Pathname, StringIO, IO, OpenAI::FilePart, nil]
33+
optional :mask, OpenAI::Internal::Type::FileInput
3434

3535
# @!attribute model
3636
# The model to use for image generation. Only `dall-e-2` and `gpt-image-1` are
@@ -83,13 +83,13 @@ class ImageEditParams < OpenAI::Internal::Type::BaseModel
8383
# Some parameter documentations has been truncated, see
8484
# {OpenAI::Models::ImageEditParams} for more details.
8585
#
86-
# @param image [Pathname, StringIO, Array<Pathname, StringIO>] The image(s) to edit. Must be a supported image file or an array of images. For
86+
# @param image [Pathname, StringIO, IO, OpenAI::FilePart, Array<Pathname, StringIO, IO, OpenAI::FilePart>] The image(s) to edit. Must be a supported image file or an array of images. For
8787
# ...
8888
#
8989
# @param prompt [String] A text description of the desired image(s). The maximum length is 1000 character
9090
# ...
9191
#
92-
# @param mask [Pathname, StringIO] An additional image whose fully transparent areas (e.g. where alpha is zero) ind
92+
# @param mask [Pathname, StringIO, IO, OpenAI::FilePart] An additional image whose fully transparent areas (e.g. where alpha is zero) ind
9393
# ...
9494
#
9595
# @param model [String, Symbol, OpenAI::Models::ImageModel, nil] The model to use for image generation. Only `dall-e-2` and `gpt-image-1` are sup
@@ -118,14 +118,14 @@ class ImageEditParams < OpenAI::Internal::Type::BaseModel
118118
module Image
119119
extend OpenAI::Internal::Type::Union
120120

121-
variant OpenAI::Internal::Type::IOLike
121+
variant OpenAI::Internal::Type::FileInput
122122

123123
variant -> { OpenAI::Models::ImageEditParams::Image::StringArray }
124124

125125
# @!method self.variants
126126
# @return [Array(StringIO, Array<StringIO>)]
127127

128-
StringArray = OpenAI::Internal::Type::ArrayOf[OpenAI::Internal::Type::IOLike]
128+
StringArray = OpenAI::Internal::Type::ArrayOf[OpenAI::Internal::Type::FileInput]
129129
end
130130

131131
# The model to use for image generation. Only `dall-e-2` and `gpt-image-1` are

0 commit comments

Comments
 (0)