Repository: sashabaranov/go-openai
Pull request #560 (2023-11-09 07:27:34)

Feat implement Run APIs
**Describe the change**
This branch implements [Run APIs](https://platform.openai.com/docs/api-reference/runs)

Issue: #533 

⚠️  It's a lot of code, I need some help from the go-openai community to test/review this one before merging it.




codecov[bot]: ## [Codecov](https://app.codecov.io/gh/sashabaranov/go-openai/pull/560?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#560](https://app.codecov.io/gh/sashabaranov/go-openai/pull/560?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (b3e49f9) into [master](https://app.codecov.io/gh/sashabaranov/go-openai/commit/08c167fecf6953619d1905ab2959ed341bfb063d?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (08c167f) will **decrease** coverage by `0.89%`.
> The diff coverage is `0.00%`.

```diff
@@            Coverage Diff             @@
##           master     #560      +/-   ##
==========================================
- Coverage   97.88%   97.00%   -0.89%     
==========================================
  Files          20       21       +1     
  Lines         991     1000       +9     
==========================================
  Hits          970      970              
- Misses         15       24       +9     
  Partials        6        6              
```


| [Files](https://app.codecov.io/gh/sashabaranov/go-openai/pull/560?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [run.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/560?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-cnVuLmdv) | `0.00% <0.00%> (ø)` | |


sashabaranov: Thank you for the PR! ❤️  We would need to add some integration tests for Assistants API at some point in the future

Patch:
diff --git a/run.go b/run.go
new file mode 100644
index 0000000..5d6ea58
--- /dev/null
+++ b/run.go
@@ -0,0 +1,399 @@
+package openai
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"net/url"
+)
+
+type Run struct {
+	ID             string             `json:"id"`
+	Object         string             `json:"object"`
+	CreatedAt      int64              `json:"created_at"`
+	ThreadID       string             `json:"thread_id"`
+	AssistantID    string             `json:"assistant_id"`
+	Status         RunStatus          `json:"status"`
+	RequiredAction *RunRequiredAction `json:"required_action,omitempty"`
+	LastError      *RunLastError      `json:"last_error,omitempty"`
+	ExpiresAt      int64              `json:"expires_at"`
+	StartedAt      *int64             `json:"started_at,omitempty"`
+	CancelledAt    *int64             `json:"cancelled_at,omitempty"`
+	FailedAt       *int64             `json:"failed_at,omitempty"`
+	CompletedAt    *int64             `json:"completed_at,omitempty"`
+	Model          string             `json:"model"`
+	Instructions   string             `json:"instructions,omitempty"`
+	Tools          []Tool             `json:"tools"`
+	FileIDS        []string           `json:"file_ids"`
+	Metadata       map[string]any     `json:"metadata"`
+
+	httpHeader
+}
+
+type RunStatus string
+
+const (
+	RunStatusQueued         RunStatus = "queued"
+	RunStatusInProgress     RunStatus = "in_progress"
+	RunStatusRequiresAction RunStatus = "requires_action"
+	RunStatusCancelling     RunStatus = "cancelling"
+	RunStatusFailed         RunStatus = "failed"
+	RunStatusCompleted      RunStatus = "completed"
+	RunStatusExpired        RunStatus = "expired"
+)
+
+type RunRequiredAction struct {
+	Type              RequiredActionType `json:"type"`
+	SubmitToolOutputs *SubmitToolOutputs `json:"submit_tool_outputs,omitempty"`
+}
+
+type RequiredActionType string
+
+const (
+	RequiredActionTypeSubmitToolOutputs RequiredActionType = "submit_tool_outputs"
+)
+
+type SubmitToolOutputs struct {
+	ToolCalls []ToolCall `json:"tool_calls"`
+}
+
+type RunLastError struct {
+	Code    RunError `json:"code"`
+	Message string   `json:"message"`
+}
+
+type RunError string
+
+const (
+	RunErrorServerError       RunError = "server_error"
+	RunErrorRateLimitExceeded RunError = "rate_limit_exceeded"
+)
+
+type RunRequest struct {
+	AssistantID  string  `json:"assistant_id"`
+	Model        *string `json:"model,omitempty"`
+	Instructions *string `json:"instructions,omitempty"`
+	Tools        []Tool  `json:"tools,omitempty"`
+	Metadata     map[string]any
+}
+
+type RunModifyRequest struct {
+	Metadata map[string]any `json:"metadata,omitempty"`
+}
+
+// RunList is a list of runs.
+type RunList struct {
+	Runs []Run `json:"data"`
+
+	httpHeader
+}
+
+type SubmitToolOutputsRequest struct {
+	ToolOutputs []ToolOutput `json:"tool_outputs"`
+}
+
+type ToolOutput struct {
+	ToolCallID string `json:"tool_call_id"`
+	Output     any    `json:"output"`
+}
+
+type CreateThreadAndRunRequest struct {
+	RunRequest
+	Thread ThreadRequest `json:"thread"`
+}
+
+type RunStep struct {
+	ID          string         `json:"id"`
+	Object      string         `json:"object"`
+	CreatedAt   int64          `json:"created_at"`
+	AssistantID string         `json:"assistant_id"`
+	ThreadID    string         `json:"thread_id"`
+	RunID       string         `json:"run_id"`
+	Type        RunStepType    `json:"type"`
+	Status      RunStepStatus  `json:"status"`
+	StepDetails StepDetails    `json:"step_details"`
+	LastError   *RunLastError  `json:"last_error,omitempty"`
+	ExpiredAt   *int64         `json:"expired_at,omitempty"`
+	CancelledAt *int64         `json:"cancelled_at,omitempty"`
+	FailedAt    *int64         `json:"failed_at,omitempty"`
+	CompletedAt *int64         `json:"completed_at,omitempty"`
+	Metadata    map[string]any `json:"metadata"`
+
+	httpHeader
+}
+
+type RunStepStatus string
+
+const (
+	RunStepStatusInProgress RunStepStatus = "in_progress"
+	RunStepStatusCancelling RunStepStatus = "cancelled"
+	RunStepStatusFailed     RunStepStatus = "failed"
+	RunStepStatusCompleted  RunStepStatus = "completed"
+	RunStepStatusExpired    RunStepStatus = "expired"
+)
+
+type RunStepType string
+
+const (
+	RunStepTypeMessageCreation RunStepType = "message_creation"
+	RunStepTypeToolCalls       RunStepType = "tool_calls"
+)
+
+type StepDetails struct {
+	Type            RunStepType                 `json:"type"`
+	MessageCreation *StepDetailsMessageCreation `json:"message_creation,omitempty"`
+	ToolCalls       *StepDetailsToolCalls       `json:"tool_calls,omitempty"`
+}
+
+type StepDetailsMessageCreation struct {
+	MessageID string `json:"message_id"`
+}
+
+type StepDetailsToolCalls struct {
+	ToolCalls []ToolCall `json:"tool_calls"`
+}
+
+// RunStepList is a list of steps.
+type RunStepList struct {
+	RunSteps []RunStep `json:"data"`
+
+	httpHeader
+}
+
+type Pagination struct {
+	Limit  *int
+	Order  *string
+	After  *string
+	Before *string
+}
+
+// CreateRun creates a new run.
+func (c *Client) CreateRun(
+	ctx context.Context,
+	threadID string,
+	request RunRequest,
+) (response Run, err error) {
+	urlSuffix := fmt.Sprintf("/threads/%s/runs", threadID)
+	req, err := c.newRequest(
+		ctx,
+		http.MethodPost,
+		c.fullURL(urlSuffix),
+		withBody(request),
+		withBetaAssistantV1(),
+	)
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}
+
+// RetrieveRun retrieves a run.
+func (c *Client) RetrieveRun(
+	ctx context.Context,
+	threadID string,
+	runID string,
+) (response Run, err error) {
+	urlSuffix := fmt.Sprintf("/threads/%s/runs/%s", threadID, runID)
+	req, err := c.newRequest(
+		ctx,
+		http.MethodGet,
+		c.fullURL(urlSuffix),
+		withBetaAssistantV1(),
+	)
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}
+
+// ModifyRun modifies a run.
+func (c *Client) ModifyRun(
+	ctx context.Context,
+	threadID string,
+	runID string,
+	request RunModifyRequest,
+) (response Run, err error) {
+	urlSuffix := fmt.Sprintf("/threads/%s/runs/%s", threadID, runID)
+	req, err := c.newRequest(
+		ctx,
+		http.MethodPost,
+		c.fullURL(urlSuffix),
+		withBody(request),
+		withBetaAssistantV1(),
+	)
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}
+
+// ListRuns lists runs.
+func (c *Client) ListRuns(
+	ctx context.Context,
+	threadID string,
+	pagination Pagination,
+) (response RunList, err error) {
+	urlValues := url.Values{}
+	if pagination.Limit != nil {
+		urlValues.Add("limit", fmt.Sprintf("%d", *pagination.Limit))
+	}
+	if pagination.Order != nil {
+		urlValues.Add("order", *pagination.Order)
+	}
+	if pagination.After != nil {
+		urlValues.Add("after", *pagination.After)
+	}
+	if pagination.Before != nil {
+		urlValues.Add("before", *pagination.Before)
+	}
+
+	encodedValues := ""
+	if len(urlValues) > 0 {
+		encodedValues = "?" + urlValues.Encode()
+	}
+
+	urlSuffix := fmt.Sprintf("/threads/%s/runs%s", threadID, encodedValues)
+	req, err := c.newRequest(
+		ctx,
+		http.MethodGet,
+		c.fullURL(urlSuffix),
+		withBetaAssistantV1(),
+	)
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}
+
+// SubmitToolOutputs submits tool outputs.
+func (c *Client) SubmitToolOutputs(
+	ctx context.Context,
+	threadID string,
+	runID string,
+	request SubmitToolOutputsRequest) (response Run, err error) {
+	urlSuffix := fmt.Sprintf("/threads/%s/runs/%s/submit_tool_outputs", threadID, runID)
+	req, err := c.newRequest(
+		ctx,
+		http.MethodPost,
+		c.fullURL(urlSuffix),
+		withBody(request),
+		withBetaAssistantV1(),
+	)
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}
+
+// CancelRun cancels a run.
+func (c *Client) CancelRun(
+	ctx context.Context,
+	threadID string,
+	runID string) (response Run, err error) {
+	urlSuffix := fmt.Sprintf("/threads/%s/runs/%s/cancel", threadID, runID)
+	req, err := c.newRequest(
+		ctx,
+		http.MethodPost,
+		c.fullURL(urlSuffix),
+		withBetaAssistantV1(),
+	)
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}
+
+// CreateThreadAndRun submits tool outputs.
+func (c *Client) CreateThreadAndRun(
+	ctx context.Context,
+	request CreateThreadAndRunRequest) (response Run, err error) {
+	urlSuffix := "/threads/runs"
+	req, err := c.newRequest(
+		ctx,
+		http.MethodPost,
+		c.fullURL(urlSuffix),
+		withBody(request),
+		withBetaAssistantV1(),
+	)
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}
+
+// RetrieveRunStep retrieves a run step.
+func (c *Client) RetrieveRunStep(
+	ctx context.Context,
+	threadID string,
+	runID string,
+	stepID string,
+) (response RunStep, err error) {
+	urlSuffix := fmt.Sprintf("/threads/%s/runs/%s/steps/%s", threadID, runID, stepID)
+	req, err := c.newRequest(
+		ctx,
+		http.MethodGet,
+		c.fullURL(urlSuffix),
+		withBetaAssistantV1(),
+	)
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}
+
+// ListRunSteps lists run steps.
+func (c *Client) ListRunSteps(
+	ctx context.Context,
+	threadID string,
+	runID string,
+	pagination Pagination,
+) (response RunStepList, err error) {
+	urlValues := url.Values{}
+	if pagination.Limit != nil {
+		urlValues.Add("limit", fmt.Sprintf("%d", *pagination.Limit))
+	}
+	if pagination.Order != nil {
+		urlValues.Add("order", *pagination.Order)
+	}
+	if pagination.After != nil {
+		urlValues.Add("after", *pagination.After)
+	}
+	if pagination.Before != nil {
+		urlValues.Add("before", *pagination.Before)
+	}
+
+	encodedValues := ""
+	if len(urlValues) > 0 {
+		encodedValues = "?" + urlValues.Encode()
+	}
+
+	urlSuffix := fmt.Sprintf("/threads/%s/runs/%s/steps%s", threadID, runID, encodedValues)
+	req, err := c.newRequest(
+		ctx,
+		http.MethodGet,
+		c.fullURL(urlSuffix),
+		withBetaAssistantV1(),
+	)
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}


Test patch:
diff --git a/client_test.go b/client_test.go
index b2f28f9..d5d3e26 100644
--- a/client_test.go
+++ b/client_test.go
@@ -313,6 +313,33 @@ func TestClientReturnsRequestBuilderErrors(t *testing.T) {
 		{"DeleteThread", func() (any, error) {
 			return client.DeleteThread(ctx, "")
 		}},
+		{"CreateRun", func() (any, error) {
+			return client.CreateRun(ctx, "", RunRequest{})
+		}},
+		{"RetrieveRun", func() (any, error) {
+			return client.RetrieveRun(ctx, "", "")
+		}},
+		{"ModifyRun", func() (any, error) {
+			return client.ModifyRun(ctx, "", "", RunModifyRequest{})
+		}},
+		{"ListRuns", func() (any, error) {
+			return client.ListRuns(ctx, "", Pagination{})
+		}},
+		{"SubmitToolOutputs", func() (any, error) {
+			return client.SubmitToolOutputs(ctx, "", "", SubmitToolOutputsRequest{})
+		}},
+		{"CancelRun", func() (any, error) {
+			return client.CancelRun(ctx, "", "")
+		}},
+		{"CreateThreadAndRun", func() (any, error) {
+			return client.CreateThreadAndRun(ctx, CreateThreadAndRunRequest{})
+		}},
+		{"RetrieveRunStep", func() (any, error) {
+			return client.RetrieveRunStep(ctx, "", "", "")
+		}},
+		{"ListRunSteps", func() (any, error) {
+			return client.ListRunSteps(ctx, "", "", Pagination{})
+		}},
 	}
 
 	for _, testCase := range testCases {
diff --git a/run_test.go b/run_test.go
new file mode 100644
index 0000000..cdf99db
--- /dev/null
+++ b/run_test.go
@@ -0,0 +1,237 @@
+package openai_test
+
+import (
+	"context"
+
+	openai "github.com/sashabaranov/go-openai"
+	"github.com/sashabaranov/go-openai/internal/test/checks"
+
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"testing"
+)
+
+// TestAssistant Tests the assistant endpoint of the API using the mocked server.
+func TestRun(t *testing.T) {
+	assistantID := "asst_abc123"
+	threadID := "thread_abc123"
+	runID := "run_abc123"
+	stepID := "step_abc123"
+	limit := 20
+	order := "desc"
+	after := "asst_abc122"
+	before := "asst_abc124"
+
+	client, server, teardown := setupOpenAITestServer()
+	defer teardown()
+
+	server.RegisterHandler(
+		"/v1/threads/"+threadID+"/runs/"+runID+"/steps/"+stepID,
+		func(w http.ResponseWriter, r *http.Request) {
+			if r.Method == http.MethodGet {
+				resBytes, _ := json.Marshal(openai.RunStep{
+					ID:        runID,
+					Object:    "run",
+					CreatedAt: 1234567890,
+					Status:    openai.RunStepStatusCompleted,
+				})
+				fmt.Fprintln(w, string(resBytes))
+			}
+		},
+	)
+
+	server.RegisterHandler(
+		"/v1/threads/"+threadID+"/runs/"+runID+"/steps",
+		func(w http.ResponseWriter, r *http.Request) {
+			if r.Method == http.MethodGet {
+				resBytes, _ := json.Marshal(openai.RunStepList{
+					RunSteps: []openai.RunStep{
+						{
+							ID:        runID,
+							Object:    "run",
+							CreatedAt: 1234567890,
+							Status:    openai.RunStepStatusCompleted,
+						},
+					},
+				})
+				fmt.Fprintln(w, string(resBytes))
+			}
+		},
+	)
+
+	server.RegisterHandler(
+		"/v1/threads/"+threadID+"/runs/"+runID+"/cancel",
+		func(w http.ResponseWriter, r *http.Request) {
+			if r.Method == http.MethodPost {
+				resBytes, _ := json.Marshal(openai.Run{
+					ID:        runID,
+					Object:    "run",
+					CreatedAt: 1234567890,
+					Status:    openai.RunStatusCancelling,
+				})
+				fmt.Fprintln(w, string(resBytes))
+			}
+		},
+	)
+
+	server.RegisterHandler(
+		"/v1/threads/"+threadID+"/runs/"+runID+"/submit_tool_outputs",
+		func(w http.ResponseWriter, r *http.Request) {
+			if r.Method == http.MethodPost {
+				resBytes, _ := json.Marshal(openai.Run{
+					ID:        runID,
+					Object:    "run",
+					CreatedAt: 1234567890,
+					Status:    openai.RunStatusCancelling,
+				})
+				fmt.Fprintln(w, string(resBytes))
+			}
+		},
+	)
+
+	server.RegisterHandler(
+		"/v1/threads/"+threadID+"/runs/"+runID,
+		func(w http.ResponseWriter, r *http.Request) {
+			if r.Method == http.MethodGet {
+				resBytes, _ := json.Marshal(openai.Run{
+					ID:        runID,
+					Object:    "run",
+					CreatedAt: 1234567890,
+					Status:    openai.RunStatusQueued,
+				})
+				fmt.Fprintln(w, string(resBytes))
+			} else if r.Method == http.MethodPost {
+				var request openai.RunModifyRequest
+				err := json.NewDecoder(r.Body).Decode(&request)
+				checks.NoError(t, err, "Decode error")
+
+				resBytes, _ := json.Marshal(openai.Run{
+					ID:        runID,
+					Object:    "run",
+					CreatedAt: 1234567890,
+					Status:    openai.RunStatusQueued,
+					Metadata:  request.Metadata,
+				})
+				fmt.Fprintln(w, string(resBytes))
+			}
+		},
+	)
+
+	server.RegisterHandler(
+		"/v1/threads/"+threadID+"/runs",
+		func(w http.ResponseWriter, r *http.Request) {
+			if r.Method == http.MethodPost {
+				var request openai.RunRequest
+				err := json.NewDecoder(r.Body).Decode(&request)
+				checks.NoError(t, err, "Decode error")
+
+				resBytes, _ := json.Marshal(openai.Run{
+					ID:        runID,
+					Object:    "run",
+					CreatedAt: 1234567890,
+					Status:    openai.RunStatusQueued,
+				})
+				fmt.Fprintln(w, string(resBytes))
+			} else if r.Method == http.MethodGet {
+				resBytes, _ := json.Marshal(openai.RunList{
+					Runs: []openai.Run{
+						{
+							ID:        runID,
+							Object:    "run",
+							CreatedAt: 1234567890,
+							Status:    openai.RunStatusQueued,
+						},
+					},
+				})
+				fmt.Fprintln(w, string(resBytes))
+			}
+		},
+	)
+
+	server.RegisterHandler(
+		"/v1/threads/runs",
+		func(w http.ResponseWriter, r *http.Request) {
+			if r.Method == http.MethodPost {
+				var request openai.CreateThreadAndRunRequest
+				err := json.NewDecoder(r.Body).Decode(&request)
+				checks.NoError(t, err, "Decode error")
+
+				resBytes, _ := json.Marshal(openai.Run{
+					ID:        runID,
+					Object:    "run",
+					CreatedAt: 1234567890,
+					Status:    openai.RunStatusQueued,
+				})
+				fmt.Fprintln(w, string(resBytes))
+			}
+		},
+	)
+
+	ctx := context.Background()
+
+	_, err := client.CreateRun(ctx, threadID, openai.RunRequest{
+		AssistantID: assistantID,
+	})
+	checks.NoError(t, err, "CreateRun error")
+
+	_, err = client.RetrieveRun(ctx, threadID, runID)
+	checks.NoError(t, err, "RetrieveRun error")
+
+	_, err = client.ModifyRun(ctx, threadID, runID, openai.RunModifyRequest{
+		Metadata: map[string]any{
+			"key": "value",
+		},
+	})
+	checks.NoError(t, err, "ModifyRun error")
+
+	_, err = client.ListRuns(
+		ctx,
+		threadID,
+		openai.Pagination{
+			Limit:  &limit,
+			Order:  &order,
+			After:  &after,
+			Before: &before,
+		},
+	)
+	checks.NoError(t, err, "ListRuns error")
+
+	_, err = client.SubmitToolOutputs(ctx, threadID, runID,
+		openai.SubmitToolOutputsRequest{})
+	checks.NoError(t, err, "SubmitToolOutputs error")
+
+	_, err = client.CancelRun(ctx, threadID, runID)
+	checks.NoError(t, err, "CancelRun error")
+
+	_, err = client.CreateThreadAndRun(ctx, openai.CreateThreadAndRunRequest{
+		RunRequest: openai.RunRequest{
+			AssistantID: assistantID,
+		},
+		Thread: openai.ThreadRequest{
+			Messages: []openai.ThreadMessage{
+				{
+					Role:    openai.ThreadMessageRoleUser,
+					Content: "Hello, World!",
+				},
+			},
+		},
+	})
+	checks.NoError(t, err, "CreateThreadAndRun error")
+
+	_, err = client.RetrieveRunStep(ctx, threadID, runID, stepID)
+	checks.NoError(t, err, "RetrieveRunStep error")
+
+	_, err = client.ListRunSteps(
+		ctx,
+		threadID,
+		runID,
+		openai.Pagination{
+			Limit:  &limit,
+			Order:  &order,
+			After:  &after,
+			Before: &before,
+		},
+	)
+	checks.NoError(t, err, "ListRunSteps error")
+}