Skip to content

Commit e96c046

Browse files
authored
Merge pull request #420 from simonx1/upload-file-support-for-file-object
Add support for File and StringIO `file` parameter in `upload`.
2 parents cde9a70 + fbe5045 commit e96c046

File tree

6 files changed

+162
-26
lines changed

6 files changed

+162
-26
lines changed

README.md

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -599,7 +599,7 @@ Put your data in a `.jsonl` file like this:
599599
{"prompt":"@lakers disappoint for a third straight night ->", "completion":" negative"}
600600
```
601601

602-
and pass the path to `client.files.upload` to upload it to OpenAI, and then interact with it:
602+
and pass the path (or a StringIO object) to `client.files.upload` to upload it to OpenAI, and then interact with it:
603603

604604
```ruby
605605
client.files.upload(parameters: { file: "path/to/sentiment.jsonl", purpose: "fine-tune" })

lib/openai/files.rb

Lines changed: 36 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,11 @@
11
module OpenAI
22
class Files
3+
PURPOSES = %w[
4+
assistants
5+
batch
6+
fine-tune
7+
].freeze
8+
39
def initialize(client:)
410
@client = client
511
end
@@ -9,12 +15,17 @@ def list
915
end
1016

1117
def upload(parameters: {})
12-
validate(file: parameters[:file]) if parameters[:file].include?(".jsonl")
18+
file_input = parameters[:file]
19+
file = prepare_file_input(file_input: file_input)
20+
21+
validate(file: file, purpose: parameters[:purpose], file_input: file_input)
1322

1423
@client.multipart_post(
1524
path: "/files",
16-
parameters: parameters.merge(file: File.open(parameters[:file]))
25+
parameters: parameters.merge(file: file)
1726
)
27+
ensure
28+
file.close if file.is_a?(File)
1829
end
1930

2031
def retrieve(id:)
@@ -31,12 +42,33 @@ def delete(id:)
3142

3243
private
3344

34-
def validate(file:)
35-
File.open(file).each_line.with_index do |line, index|
45+
def prepare_file_input(file_input:)
46+
if file_input.is_a?(String)
47+
File.open(file_input)
48+
elsif file_input.respond_to?(:read) && file_input.respond_to?(:rewind)
49+
file_input
50+
else
51+
raise ArgumentError, "Invalid file - must be a StringIO object or a path to a file."
52+
end
53+
end
54+
55+
def validate(file:, purpose:, file_input:)
56+
raise ArgumentError, "`file` is required" if file.nil?
57+
unless PURPOSES.include?(purpose)
58+
raise ArgumentError, "`purpose` must be one of `#{PURPOSES.join(',')}`"
59+
end
60+
61+
validate_jsonl(file: file) if file_input.is_a?(String) && file_input.end_with?(".jsonl")
62+
end
63+
64+
def validate_jsonl(file:)
65+
file.each_line.with_index do |line, index|
3666
JSON.parse(line)
3767
rescue JSON::ParserError => e
3868
raise JSON::ParserError, "#{e.message} - found on line #{index + 1} of #{file}"
3969
end
70+
ensure
71+
file.rewind
4072
end
4173
end
4274
end

lib/openai/http.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -99,10 +99,14 @@ def multipart_parameters(parameters)
9999
parameters&.transform_values do |value|
100100
next value unless value.respond_to?(:close) # File or IO object.
101101

102+
# Faraday::UploadIO does not require a path, so we will pass it
103+
# only if it is available. This allows StringIO objects to be
104+
# passed in as well.
105+
path = value.respond_to?(:path) ? value.path : nil
102106
# Doesn't seem like OpenAI needs mime_type yet, so not worth
103107
# the library to figure this out. Hence the empty string
104108
# as the second argument.
105-
Faraday::UploadIO.new(value, "", value.path)
109+
Faraday::UploadIO.new(value, "", path)
106110
end
107111
end
108112

spec/fixtures/cassettes/files_upload.yml renamed to spec/fixtures/cassettes/files_upload_file.yml

Lines changed: 15 additions & 15 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

spec/fixtures/cassettes/files_upload_stringio.yml

Lines changed: 80 additions & 0 deletions
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

spec/openai/client/files_spec.rb

Lines changed: 25 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -11,18 +11,38 @@
1111
let(:upload_id) { upload["id"] }
1212

1313
describe "#upload" do
14-
let(:upload_cassette) { "files upload" }
14+
let(:upload_cassette) { "files upload #{cassette_label}" }
15+
16+
context "with an invalid file" do
17+
let(:cassette_label) { "unused" }
18+
let(:filename) { File.join("errors", "missing_quote.jsonl") }
19+
20+
it { expect { upload }.to raise_error(JSON::ParserError) }
21+
end
22+
23+
context "with an invalid purpose" do
24+
let(:cassette_label) { "unused" }
25+
let(:upload_purpose) { "invalid" }
26+
27+
it { expect { upload }.to raise_error(ArgumentError) }
28+
end
29+
30+
context "with a `File` instance content" do
31+
let(:cassette_label) { "file" }
32+
let(:file) { File.open(File.join(RSPEC_ROOT, "fixtures/files", filename)) }
1533

16-
context "with a valid JSON lines file" do
1734
it "succeeds" do
1835
expect(upload["filename"]).to eq(filename)
1936
end
2037
end
2138

22-
context "with an invalid file" do
23-
let(:filename) { File.join("errors", "missing_quote.jsonl") }
39+
context "with a `StringIO` instance content" do
40+
let(:cassette_label) { "stringio" }
41+
let(:file) { StringIO.new(File.read(File.join(RSPEC_ROOT, "fixtures/files", filename))) }
2442

25-
it { expect { upload }.to raise_error(JSON::ParserError) }
43+
it "succeeds" do
44+
expect(upload["filename"]).to eq("local.path")
45+
end
2646
end
2747
end
2848

0 commit comments

Comments
 (0)