Skip to content

Commit 71848cc

Browse files
authored
feat: support direct bytes for file upload (#568)
* feat: support direct bytes for file upload * add test for errors * add coverage
1 parent fe67abb commit 71848cc

File tree

4 files changed

+115
-0
lines changed

4 files changed

+115
-0
lines changed

client_test.go

+3
Original file line numberDiff line numberDiff line change
@@ -247,6 +247,9 @@ func TestClientReturnsRequestBuilderErrors(t *testing.T) {
247247
{"CreateImage", func() (any, error) {
248248
return client.CreateImage(ctx, ImageRequest{})
249249
}},
250+
{"CreateFileBytes", func() (any, error) {
251+
return client.CreateFileBytes(ctx, FileBytesRequest{})
252+
}},
250253
{"DeleteFile", func() (any, error) {
251254
return nil, client.DeleteFile(ctx, "")
252255
}},

files.go

+49
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,24 @@ type FileRequest struct {
1515
Purpose string `json:"purpose"`
1616
}
1717

18+
// PurposeType represents the purpose of the file when uploading.
19+
type PurposeType string
20+
21+
const (
22+
PurposeFineTune PurposeType = "fine-tune"
23+
PurposeAssistants PurposeType = "assistants"
24+
)
25+
26+
// FileBytesRequest represents a file upload request.
27+
type FileBytesRequest struct {
28+
// the name of the uploaded file in OpenAI
29+
Name string
30+
// the bytes of the file
31+
Bytes []byte
32+
// the purpose of the file
33+
Purpose PurposeType
34+
}
35+
1836
// File struct represents an OpenAPI file.
1937
type File struct {
2038
Bytes int `json:"bytes"`
@@ -36,6 +54,37 @@ type FilesList struct {
3654
httpHeader
3755
}
3856

57+
// CreateFileBytes uploads bytes directly to OpenAI without requiring a local file.
58+
func (c *Client) CreateFileBytes(ctx context.Context, request FileBytesRequest) (file File, err error) {
59+
var b bytes.Buffer
60+
reader := bytes.NewReader(request.Bytes)
61+
builder := c.createFormBuilder(&b)
62+
63+
err = builder.WriteField("purpose", string(request.Purpose))
64+
if err != nil {
65+
return
66+
}
67+
68+
err = builder.CreateFormFileReader("file", reader, request.Name)
69+
if err != nil {
70+
return
71+
}
72+
73+
err = builder.Close()
74+
if err != nil {
75+
return
76+
}
77+
78+
req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/files"),
79+
withBody(&b), withContentType(builder.FormDataContentType()))
80+
if err != nil {
81+
return
82+
}
83+
84+
err = c.sendRequest(req, &file)
85+
return
86+
}
87+
3988
// CreateFile uploads a jsonl file to GPT3
4089
// FilePath must be a local file path.
4190
func (c *Client) CreateFile(ctx context.Context, request FileRequest) (file File, err error) {

files_api_test.go

+13
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,19 @@ import (
1616
"github.com/sashabaranov/go-openai/internal/test/checks"
1717
)
1818

19+
func TestFileBytesUpload(t *testing.T) {
20+
client, server, teardown := setupOpenAITestServer()
21+
defer teardown()
22+
server.RegisterHandler("/v1/files", handleCreateFile)
23+
req := openai.FileBytesRequest{
24+
Name: "foo",
25+
Bytes: []byte("foo"),
26+
Purpose: openai.PurposeFineTune,
27+
}
28+
_, err := client.CreateFileBytes(context.Background(), req)
29+
checks.NoError(t, err, "CreateFile error")
30+
}
31+
1932
func TestFileUpload(t *testing.T) {
2033
client, server, teardown := setupOpenAITestServer()
2134
defer teardown()

files_test.go

+50
Original file line numberDiff line numberDiff line change
@@ -11,6 +11,53 @@ import (
1111
"github.com/sashabaranov/go-openai/internal/test/checks"
1212
)
1313

14+
func TestFileBytesUploadWithFailingFormBuilder(t *testing.T) {
15+
config := DefaultConfig("")
16+
config.BaseURL = ""
17+
client := NewClientWithConfig(config)
18+
mockBuilder := &mockFormBuilder{}
19+
client.createFormBuilder = func(io.Writer) utils.FormBuilder {
20+
return mockBuilder
21+
}
22+
23+
ctx := context.Background()
24+
req := FileBytesRequest{
25+
Name: "foo",
26+
Bytes: []byte("foo"),
27+
Purpose: PurposeAssistants,
28+
}
29+
30+
mockError := fmt.Errorf("mockWriteField error")
31+
mockBuilder.mockWriteField = func(string, string) error {
32+
return mockError
33+
}
34+
_, err := client.CreateFileBytes(ctx, req)
35+
checks.ErrorIs(t, err, mockError, "CreateFile should return error if form builder fails")
36+
37+
mockError = fmt.Errorf("mockCreateFormFile error")
38+
mockBuilder.mockWriteField = func(string, string) error {
39+
return nil
40+
}
41+
mockBuilder.mockCreateFormFileReader = func(string, io.Reader, string) error {
42+
return mockError
43+
}
44+
_, err = client.CreateFileBytes(ctx, req)
45+
checks.ErrorIs(t, err, mockError, "CreateFile should return error if form builder fails")
46+
47+
mockError = fmt.Errorf("mockClose error")
48+
mockBuilder.mockWriteField = func(string, string) error {
49+
return nil
50+
}
51+
mockBuilder.mockCreateFormFileReader = func(string, io.Reader, string) error {
52+
return nil
53+
}
54+
mockBuilder.mockClose = func() error {
55+
return mockError
56+
}
57+
_, err = client.CreateFileBytes(ctx, req)
58+
checks.ErrorIs(t, err, mockError, "CreateFile should return error if form builder fails")
59+
}
60+
1461
func TestFileUploadWithFailingFormBuilder(t *testing.T) {
1562
config := DefaultConfig("")
1663
config.BaseURL = ""
@@ -55,6 +102,9 @@ func TestFileUploadWithFailingFormBuilder(t *testing.T) {
55102
return mockError
56103
}
57104
_, err = client.CreateFile(ctx, req)
105+
if err == nil {
106+
t.Fatal("CreateFile should return error if form builder fails")
107+
}
58108
checks.ErrorIs(t, err, mockError, "CreateFile should return error if form builder fails")
59109
}
60110

0 commit comments

Comments
 (0)