Repository: sashabaranov/go-openai
Pull request #579 (2023-11-14 17:24:40)

Add Assistant FileIDs and Metadata
## Describe the change

The [API docs](https://platform.openai.com/docs/api-reference/assistants/object) define the `Assistant` object as:

```json
{
  "id": "asst_abc123",
  "object": "assistant",
  "created_at": 1698984975,
  "name": "Math Tutor",
  "description": null,
  "model": "gpt-4",
  "instructions": "You are a personal math tutor. When asked a question, write and run Python code to answer the question.",
  "tools": [
    {
      "type": "code_interpreter"
    }
  ],
  "file_ids": [],
  "metadata": {}
}
```

However the `Assistant` type in this package does not have the following properties:

- `file_ids`
- `metadata`

## Describe your solution

Add them to the `Assistant` type.

## Additional context**

No issues related to this, I noticed it when wanting to validate the metadata of an existing Assistant. 


Patch:
diff --git a/assistant.go b/assistant.go
index de49be6..bd33583 100644
--- a/assistant.go
+++ b/assistant.go
@@ -22,6 +22,8 @@ type Assistant struct {
 	Model        string          `json:"model"`
 	Instructions *string         `json:"instructions,omitempty"`
 	Tools        []AssistantTool `json:"tools,omitempty"`
+	FileIDs      []string        `json:"file_ids,omitempty"`
+	Metadata     map[string]any  `json:"metadata,omitempty"`
 
 	httpHeader
 }
@@ -52,7 +54,9 @@ type AssistantRequest struct {
 // AssistantsList is a list of assistants.
 type AssistantsList struct {
 	Assistants []Assistant `json:"data"`
-
+	LastID     *string     `json:"last_id"`
+	FirstID    *string     `json:"first_id"`
+	HasMore    bool        `json:"has_more"`
 	httpHeader
 }
 
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
 	}
diff --git a/run.go b/run.go
index 7ff730f..f95bf0e 100644
--- a/run.go
+++ b/run.go
@@ -157,6 +157,10 @@ type StepDetailsToolCalls struct {
 type RunStepList struct {
 	RunSteps []RunStep `json:"data"`
 
+	FirstID string `json:"first_id"`
+	LastID  string `json:"last_id"`
+	HasMore bool   `json:"has_more"`
+
 	httpHeader
 }
 


Test patch:
diff --git a/assistant_test.go b/assistant_test.go
index eb6f424..30daec2 100644
--- a/assistant_test.go
+++ b/assistant_test.go
@@ -142,6 +142,8 @@ When asked a question, write and run Python code to answer the question.`
 				fmt.Fprintln(w, string(resBytes))
 			} else if r.Method == http.MethodGet {
 				resBytes, _ := json.Marshal(openai.AssistantsList{
+					LastID:  &assistantID,
+					FirstID: &assistantID,
 					Assistants: []openai.Assistant{
 						{
 							ID:           assistantID,
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")
 }