Repository: sashabaranov/go-openai
Pull request #573 (2023-11-14 08:44:49)

feat(runapi):  add RunStepList response args 


A similar PR may already be submitted!
Please search among the [Pull request](https://github.com/sashabaranov/go-openai/pulls) before creating one.

If your changes introduce breaking changes, please prefix the title of your pull request with "[BREAKING_CHANGES]". This allows for clear identification of such changes in the 'What's Changed' section on the release page, making it developer-friendly.

Thanks for submitting a pull request! Please provide enough information so that others can review your pull request.

**Describe the change**
add RunStepList response args
```
type RunStepList struct {
	...

	FirstID string `json:"first_id"`
	LastID  string `json:"last_id"`
	HasMore bool   `json:"has_more"`

	...
}

```

**Describe your solution**
ref: https://platform.openai.com/docs/api-reference/runs/listRunSteps

**Tests**
Briefly describe how you have tested these changes. 



codecov[bot]: ## [Codecov](https://app.codecov.io/gh/sashabaranov/go-openai/pull/573?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
All modified and coverable lines are covered by tests :white_check_mark:
> 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 [(`81a97b0`)](https://app.codecov.io/gh/sashabaranov/go-openai/pull/573?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) 98.35%.


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


```diff
@@           Coverage Diff           @@
##           master     #573   +/-   ##
=======================================
  Coverage   98.35%   98.35%           
=======================================
  Files          24       24           
  Lines        1276     1276           
=======================================
  Hits         1255     1255           
  Misses         15       15           
  Partials        6        6           
```



</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/sashabaranov/go-openai/pull/573?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/assistant.go b/assistant.go
index de49be6..59f7828 100644
--- a/assistant.go
+++ b/assistant.go
@@ -52,7 +52,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")
 }