Repository: sashabaranov/go-openai
Pull request #568 (2023-11-13 23:47:00)

feat: support direct bytes for file upload
**Describe the change**
`CreateFile` currently requires a "local" file which it reads from the filesystem and uploads.  For various reasons, we require uploading bytes directly rather than relying on local filesystem.

**Describe your solution**
Add `CreateFileBytes` which takes a `FileBytesRequest` that allows directly passing a `[]byte`.  This reuses the exsting `CreateFormFileReader` passing a `*bytes.Reader` directly.


**Tests**
Added a test case to `files_api_test.go`.   Also validated that it works in a separate app.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/sashabaranov/go-openai/pull/568?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
Attention: `12 lines` in your changes are missing coverage. Please review.
> Comparison is base [(`515de02`)](https://app.codecov.io/gh/sashabaranov/go-openai/commit/515de0219d3b4d30351d44d8a0f508599de6c053?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) 98.35% compared to head [(`e5caf22`)](https://app.codecov.io/gh/sashabaranov/go-openai/pull/568?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) 97.46%.

| [Files](https://app.codecov.io/gh/sashabaranov/go-openai/pull/568?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Patch % | Lines |
|---|---|---|
| [files.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/568?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-ZmlsZXMuZ28=) | 50.00% | [8 Missing and 4 partials :warning: ](https://app.codecov.io/gh/sashabaranov/go-openai/pull/568?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) |

<details><summary>Additional details and impacted files</summary>


```diff
@@            Coverage Diff             @@
##           master     #568      +/-   ##
==========================================
- Coverage   98.35%   97.46%   -0.90%     
==========================================
  Files          24       24              
  Lines        1276     1300      +24     
==========================================
+ Hits         1255     1267      +12     
- Misses         15       23       +8     
- Partials        6       10       +4     
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/sashabaranov/go-openai/pull/568?src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None).


Patch:
diff --git a/files.go b/files.go
index 9e521fb..371d06c 100644
--- a/files.go
+++ b/files.go
@@ -15,6 +15,24 @@ type FileRequest struct {
 	Purpose  string `json:"purpose"`
 }
 
+// PurposeType represents the purpose of the file when uploading.
+type PurposeType string
+
+const (
+	PurposeFineTune   PurposeType = "fine-tune"
+	PurposeAssistants PurposeType = "assistants"
+)
+
+// FileBytesRequest represents a file upload request.
+type FileBytesRequest struct {
+	// the name of the uploaded file in OpenAI
+	Name string
+	// the bytes of the file
+	Bytes []byte
+	// the purpose of the file
+	Purpose PurposeType
+}
+
 // File struct represents an OpenAPI file.
 type File struct {
 	Bytes         int    `json:"bytes"`
@@ -36,6 +54,37 @@ type FilesList struct {
 	httpHeader
 }
 
+// CreateFileBytes uploads bytes directly to OpenAI without requiring a local file.
+func (c *Client) CreateFileBytes(ctx context.Context, request FileBytesRequest) (file File, err error) {
+	var b bytes.Buffer
+	reader := bytes.NewReader(request.Bytes)
+	builder := c.createFormBuilder(&b)
+
+	err = builder.WriteField("purpose", string(request.Purpose))
+	if err != nil {
+		return
+	}
+
+	err = builder.CreateFormFileReader("file", reader, request.Name)
+	if err != nil {
+		return
+	}
+
+	err = builder.Close()
+	if err != nil {
+		return
+	}
+
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/files"),
+		withBody(&b), withContentType(builder.FormDataContentType()))
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &file)
+	return
+}
+
 // CreateFile uploads a jsonl file to GPT3
 // FilePath must be a local file path.
 func (c *Client) CreateFile(ctx context.Context, request FileRequest) (file File, err error) {
diff --git a/messages.go b/messages.go
index 4e691a8..3fd377f 100644
--- a/messages.go
+++ b/messages.go
@@ -71,7 +71,7 @@ type MessageFilesList struct {
 // CreateMessage creates a new message.
 func (c *Client) CreateMessage(ctx context.Context, threadID string, request MessageRequest) (msg Message, err error) {
 	urlSuffix := fmt.Sprintf("/threads/%s/%s", threadID, messagesSuffix)
-	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request))
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request), withBetaAssistantV1())
 	if err != nil {
 		return
 	}


Test patch:
diff --git a/client_test.go b/client_test.go
index 1c90845..664f9fb 100644
--- a/client_test.go
+++ b/client_test.go
@@ -247,6 +247,9 @@ func TestClientReturnsRequestBuilderErrors(t *testing.T) {
 		{"CreateImage", func() (any, error) {
 			return client.CreateImage(ctx, ImageRequest{})
 		}},
+		{"CreateFileBytes", func() (any, error) {
+			return client.CreateFileBytes(ctx, FileBytesRequest{})
+		}},
 		{"DeleteFile", func() (any, error) {
 			return nil, client.DeleteFile(ctx, "")
 		}},
diff --git a/files_api_test.go b/files_api_test.go
index 330b881..6f62a3f 100644
--- a/files_api_test.go
+++ b/files_api_test.go
@@ -16,6 +16,19 @@ import (
 	"github.com/sashabaranov/go-openai/internal/test/checks"
 )
 
+func TestFileBytesUpload(t *testing.T) {
+	client, server, teardown := setupOpenAITestServer()
+	defer teardown()
+	server.RegisterHandler("/v1/files", handleCreateFile)
+	req := openai.FileBytesRequest{
+		Name:    "foo",
+		Bytes:   []byte("foo"),
+		Purpose: openai.PurposeFineTune,
+	}
+	_, err := client.CreateFileBytes(context.Background(), req)
+	checks.NoError(t, err, "CreateFile error")
+}
+
 func TestFileUpload(t *testing.T) {
 	client, server, teardown := setupOpenAITestServer()
 	defer teardown()
diff --git a/files_test.go b/files_test.go
index f588b30..3c1b99f 100644
--- a/files_test.go
+++ b/files_test.go
@@ -11,6 +11,53 @@ import (
 	"github.com/sashabaranov/go-openai/internal/test/checks"
 )
 
+func TestFileBytesUploadWithFailingFormBuilder(t *testing.T) {
+	config := DefaultConfig("")
+	config.BaseURL = ""
+	client := NewClientWithConfig(config)
+	mockBuilder := &mockFormBuilder{}
+	client.createFormBuilder = func(io.Writer) utils.FormBuilder {
+		return mockBuilder
+	}
+
+	ctx := context.Background()
+	req := FileBytesRequest{
+		Name:    "foo",
+		Bytes:   []byte("foo"),
+		Purpose: PurposeAssistants,
+	}
+
+	mockError := fmt.Errorf("mockWriteField error")
+	mockBuilder.mockWriteField = func(string, string) error {
+		return mockError
+	}
+	_, err := client.CreateFileBytes(ctx, req)
+	checks.ErrorIs(t, err, mockError, "CreateFile should return error if form builder fails")
+
+	mockError = fmt.Errorf("mockCreateFormFile error")
+	mockBuilder.mockWriteField = func(string, string) error {
+		return nil
+	}
+	mockBuilder.mockCreateFormFileReader = func(string, io.Reader, string) error {
+		return mockError
+	}
+	_, err = client.CreateFileBytes(ctx, req)
+	checks.ErrorIs(t, err, mockError, "CreateFile should return error if form builder fails")
+
+	mockError = fmt.Errorf("mockClose error")
+	mockBuilder.mockWriteField = func(string, string) error {
+		return nil
+	}
+	mockBuilder.mockCreateFormFileReader = func(string, io.Reader, string) error {
+		return nil
+	}
+	mockBuilder.mockClose = func() error {
+		return mockError
+	}
+	_, err = client.CreateFileBytes(ctx, req)
+	checks.ErrorIs(t, err, mockError, "CreateFile should return error if form builder fails")
+}
+
 func TestFileUploadWithFailingFormBuilder(t *testing.T) {
 	config := DefaultConfig("")
 	config.BaseURL = ""
@@ -55,6 +102,9 @@ func TestFileUploadWithFailingFormBuilder(t *testing.T) {
 		return mockError
 	}
 	_, err = client.CreateFile(ctx, req)
+	if err == nil {
+		t.Fatal("CreateFile should return error if form builder fails")
+	}
 	checks.ErrorIs(t, err, mockError, "CreateFile should return error if form builder fails")
 }