Repository: sashabaranov/go-openai
Pull request #526 (2023-11-07 01:09:56)

Feat Support tools and tools choice new fileds
**Describe the change**
This branch adds support for [new fields](https://platform.openai.com/docs/api-reference/chat/create#chat-create-tools) `tools` and `tool_choice`


**Describe your solution**
New types have been added to support new features


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/sashabaranov/go-openai/pull/526?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#526](https://app.codecov.io/gh/sashabaranov/go-openai/pull/526?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (104fabc) into [master](https://app.codecov.io/gh/sashabaranov/go-openai/commit/d07833e19bfbb2f26011c8881f7fb61366c07e75?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (d07833e) will **not change** coverage.
> The diff coverage is `n/a`.

```diff
@@           Coverage Diff           @@
##           master     #526   +/-   ##
=======================================
  Coverage   97.59%   97.59%           
=======================================
  Files          19       19           
  Lines         872      872           
=======================================
  Hits          851      851           
  Misses         15       15           
  Partials        6        6           
```


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



Patch:
diff --git a/assistant.go b/assistant.go
new file mode 100644
index 0000000..d75eebe
--- /dev/null
+++ b/assistant.go
@@ -0,0 +1,260 @@
+package openai
+
+import (
+	"context"
+	"fmt"
+	"net/http"
+	"net/url"
+)
+
+const (
+	assistantsSuffix      = "/assistants"
+	assistantsFilesSuffix = "/files"
+)
+
+type Assistant struct {
+	ID           string  `json:"id"`
+	Object       string  `json:"object"`
+	CreatedAt    int64   `json:"created_at"`
+	Name         *string `json:"name,omitempty"`
+	Description  *string `json:"description,omitempty"`
+	Model        string  `json:"model"`
+	Instructions *string `json:"instructions,omitempty"`
+	Tools        []any   `json:"tools,omitempty"`
+
+	httpHeader
+}
+
+type AssistantTool struct {
+	Type string `json:"type"`
+}
+
+type AssistantToolCodeInterpreter struct {
+	AssistantTool
+}
+
+type AssistantToolRetrieval struct {
+	AssistantTool
+}
+
+type AssistantToolFunction struct {
+	AssistantTool
+	Function FunctionDefinition `json:"function"`
+}
+
+type AssistantRequest struct {
+	Model        string         `json:"model"`
+	Name         *string        `json:"name,omitempty"`
+	Description  *string        `json:"description,omitempty"`
+	Instructions *string        `json:"instructions,omitempty"`
+	Tools        []any          `json:"tools,omitempty"`
+	FileIDs      []string       `json:"file_ids,omitempty"`
+	Metadata     map[string]any `json:"metadata,omitempty"`
+}
+
+// AssistantsList is a list of assistants.
+type AssistantsList struct {
+	Assistants []Assistant `json:"data"`
+
+	httpHeader
+}
+
+type AssistantFile struct {
+	ID          string `json:"id"`
+	Object      string `json:"object"`
+	CreatedAt   int64  `json:"created_at"`
+	AssistantID string `json:"assistant_id"`
+
+	httpHeader
+}
+
+type AssistantFileRequest struct {
+	FileID string `json:"file_id"`
+}
+
+type AssistantFilesList struct {
+	AssistantFiles []AssistantFile `json:"data"`
+
+	httpHeader
+}
+
+// CreateAssistant creates a new assistant.
+func (c *Client) CreateAssistant(ctx context.Context, request AssistantRequest) (response Assistant, err error) {
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(assistantsSuffix), withBody(request))
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}
+
+// RetrieveAssistant retrieves an assistant.
+func (c *Client) RetrieveAssistant(
+	ctx context.Context,
+	assistantID string,
+) (response Assistant, err error) {
+	urlSuffix := fmt.Sprintf("%s/%s", assistantsSuffix, assistantID)
+	req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix))
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}
+
+// ModifyAssistant modifies an assistant.
+func (c *Client) ModifyAssistant(
+	ctx context.Context,
+	assistantID string,
+	request AssistantRequest,
+) (response Assistant, err error) {
+	urlSuffix := fmt.Sprintf("%s/%s", assistantsSuffix, assistantID)
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request))
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}
+
+// DeleteAssistant deletes an assistant.
+func (c *Client) DeleteAssistant(
+	ctx context.Context,
+	assistantID string,
+) (response Assistant, err error) {
+	urlSuffix := fmt.Sprintf("%s/%s", assistantsSuffix, assistantID)
+	req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL(urlSuffix))
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}
+
+// ListAssistants Lists the currently available assistants.
+func (c *Client) ListAssistants(
+	ctx context.Context,
+	limit *int,
+	order *string,
+	after *string,
+	before *string,
+) (reponse AssistantsList, err error) {
+	urlValues := url.Values{}
+	if limit != nil {
+		urlValues.Add("limit", fmt.Sprintf("%d", *limit))
+	}
+	if order != nil {
+		urlValues.Add("order", *order)
+	}
+	if after != nil {
+		urlValues.Add("after", *after)
+	}
+	if before != nil {
+		urlValues.Add("before", *before)
+	}
+
+	encodedValues := ""
+	if len(urlValues) > 0 {
+		encodedValues = "?" + urlValues.Encode()
+	}
+
+	urlSuffix := fmt.Sprintf("%s%s", assistantsSuffix, encodedValues)
+	req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix))
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &reponse)
+	return
+}
+
+// CreateAssistantFile creates a new assistant file.
+func (c *Client) CreateAssistantFile(
+	ctx context.Context,
+	assistantID string,
+	request AssistantFileRequest,
+) (response AssistantFile, err error) {
+	urlSuffix := fmt.Sprintf("%s/%s%s", assistantsSuffix, assistantID, assistantsFilesSuffix)
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix),
+		withBody(request))
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}
+
+// RetrieveAssistantFile retrieves an assistant file.
+func (c *Client) RetrieveAssistantFile(
+	ctx context.Context,
+	assistantID string,
+	fileID string,
+) (response AssistantFile, err error) {
+	urlSuffix := fmt.Sprintf("%s/%s%s/%s", assistantsSuffix, assistantID, assistantsFilesSuffix, fileID)
+	req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix))
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}
+
+// DeleteAssistantFile deletes an existing file.
+func (c *Client) DeleteAssistantFile(
+	ctx context.Context,
+	assistantID string,
+	fileID string,
+) (err error) {
+	urlSuffix := fmt.Sprintf("%s/%s%s/%s", assistantsSuffix, assistantID, assistantsFilesSuffix, fileID)
+	req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL(urlSuffix))
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, nil)
+	return
+}
+
+// ListAssistantFiles Lists the currently available files for an assistant.
+func (c *Client) ListAssistantFiles(
+	ctx context.Context,
+	assistantID string,
+	limit *int,
+	order *string,
+	after *string,
+	before *string,
+) (response AssistantFilesList, err error) {
+	urlValues := url.Values{}
+	if limit != nil {
+		urlValues.Add("limit", fmt.Sprintf("%d", *limit))
+	}
+	if order != nil {
+		urlValues.Add("order", *order)
+	}
+	if after != nil {
+		urlValues.Add("after", *after)
+	}
+	if before != nil {
+		urlValues.Add("before", *before)
+	}
+
+	encodedValues := ""
+	if len(urlValues) > 0 {
+		encodedValues = "?" + urlValues.Encode()
+	}
+
+	urlSuffix := fmt.Sprintf("%s/%s%s%s", assistantsSuffix, assistantID, assistantsFilesSuffix, encodedValues)
+	req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix))
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}
diff --git a/chat.go b/chat.go
index df0e5f9..0430318 100644
--- a/chat.go
+++ b/chat.go
@@ -12,6 +12,7 @@ const (
 	ChatMessageRoleUser      = "user"
 	ChatMessageRoleAssistant = "assistant"
 	ChatMessageRoleFunction  = "function"
+	ChatMessageRoleTool      = "tool"
 )
 
 const chatCompletionsSuffix = "/chat/completions"
@@ -61,6 +62,12 @@ type ChatCompletionMessage struct {
 	Name string `json:"name,omitempty"`
 
 	FunctionCall *FunctionCall `json:"function_call,omitempty"`
+	ToolCalls    []ToolCall    `json:"tool_calls,omitempty"`
+}
+
+type ToolCall struct {
+	ID       string       `json:"id"`
+	Function FunctionCall `json:"function"`
 }
 
 type FunctionCall struct {
@@ -69,25 +76,63 @@ type FunctionCall struct {
 	Arguments string `json:"arguments,omitempty"`
 }
 
+type ChatCompletionResponseFormatType string
+
+const (
+	ChatCompletionResponseFormatTypeJSONObject ChatCompletionResponseFormatType = "json_object"
+	ChatCompletionResponseFormatTypeText       ChatCompletionResponseFormatType = "text"
+)
+
+type ChatCompletionResponseFormat struct {
+	Type ChatCompletionResponseFormatType `json:"type"`
+}
+
 // ChatCompletionRequest represents a request structure for chat completion API.
 type ChatCompletionRequest struct {
-	Model            string                  `json:"model"`
-	Messages         []ChatCompletionMessage `json:"messages"`
-	MaxTokens        int                     `json:"max_tokens,omitempty"`
-	Temperature      float32                 `json:"temperature,omitempty"`
-	TopP             float32                 `json:"top_p,omitempty"`
-	N                int                     `json:"n,omitempty"`
-	Stream           bool                    `json:"stream,omitempty"`
-	Stop             []string                `json:"stop,omitempty"`
-	PresencePenalty  float32                 `json:"presence_penalty,omitempty"`
-	FrequencyPenalty float32                 `json:"frequency_penalty,omitempty"`
+	Model            string                       `json:"model"`
+	Messages         []ChatCompletionMessage      `json:"messages"`
+	MaxTokens        int                          `json:"max_tokens,omitempty"`
+	Temperature      float32                      `json:"temperature,omitempty"`
+	TopP             float32                      `json:"top_p,omitempty"`
+	N                int                          `json:"n,omitempty"`
+	Stream           bool                         `json:"stream,omitempty"`
+	Stop             []string                     `json:"stop,omitempty"`
+	PresencePenalty  float32                      `json:"presence_penalty,omitempty"`
+	ResponseFormat   ChatCompletionResponseFormat `json:"response_format,omitempty"`
+	Seed             *int                         `json:"seed,omitempty"`
+	FrequencyPenalty float32                      `json:"frequency_penalty,omitempty"`
 	// LogitBias is must be a token id string (specified by their token ID in the tokenizer), not a word string.
 	// incorrect: `"logit_bias":{"You": 6}`, correct: `"logit_bias":{"1639": 6}`
 	// refs: https://platform.openai.com/docs/api-reference/chat/create#chat/create-logit_bias
-	LogitBias    map[string]int       `json:"logit_bias,omitempty"`
-	User         string               `json:"user,omitempty"`
-	Functions    []FunctionDefinition `json:"functions,omitempty"`
-	FunctionCall any                  `json:"function_call,omitempty"`
+	LogitBias map[string]int `json:"logit_bias,omitempty"`
+	User      string         `json:"user,omitempty"`
+	// Deprecated: use Tools instead.
+	Functions []FunctionDefinition `json:"functions,omitempty"`
+	// Deprecated: use ToolChoice instead.
+	FunctionCall any    `json:"function_call,omitempty"`
+	Tools        []Tool `json:"tools,omitempty"`
+	// This can be either a string or an ToolChoice object.
+	ToolChoiche any `json:"tool_choice,omitempty"`
+}
+
+type ToolType string
+
+const (
+	ToolTypeFunction ToolType = "function"
+)
+
+type Tool struct {
+	Type     ToolType           `json:"type"`
+	Function FunctionDefinition `json:"function,omitempty"`
+}
+
+type ToolChoiche struct {
+	Type     ToolType     `json:"type"`
+	Function ToolFunction `json:"function,omitempty"`
+}
+
+type ToolFunction struct {
+	Name string `json:"name"`
 }
 
 type FunctionDefinition struct {
@@ -110,6 +155,7 @@ const (
 	FinishReasonStop          FinishReason = "stop"
 	FinishReasonLength        FinishReason = "length"
 	FinishReasonFunctionCall  FinishReason = "function_call"
+	FinishReasonToolCalls     FinishReason = "tool_calls"
 	FinishReasonContentFilter FinishReason = "content_filter"
 	FinishReasonNull          FinishReason = "null"
 )
diff --git a/chat_stream.go b/chat_stream.go
index f1faa39..57cfa78 100644
--- a/chat_stream.go
+++ b/chat_stream.go
@@ -9,6 +9,7 @@ type ChatCompletionStreamChoiceDelta struct {
 	Content      string        `json:"content,omitempty"`
 	Role         string        `json:"role,omitempty"`
 	FunctionCall *FunctionCall `json:"function_call,omitempty"`
+	ToolCalls    []ToolCall    `json:"tool_calls,omitempty"`
 }
 
 type ChatCompletionStreamChoice struct {


Test patch:
diff --git a/assistant_test.go b/assistant_test.go
new file mode 100644
index 0000000..eb6f424
--- /dev/null
+++ b/assistant_test.go
@@ -0,0 +1,202 @@
+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 TestAssistant(t *testing.T) {
+	assistantID := "asst_abc123"
+	assistantName := "Ambrogio"
+	assistantDescription := "Ambrogio is a friendly assistant."
+	assitantInstructions := `You are a personal math tutor. 
+When asked a question, write and run Python code to answer the question.`
+	assistantFileID := "file-wB6RM6wHdA49HfS2DJ9fEyrH"
+	limit := 20
+	order := "desc"
+	after := "asst_abc122"
+	before := "asst_abc124"
+
+	client, server, teardown := setupOpenAITestServer()
+	defer teardown()
+
+	server.RegisterHandler(
+		"/v1/assistants/"+assistantID+"/files/"+assistantFileID,
+		func(w http.ResponseWriter, r *http.Request) {
+			if r.Method == http.MethodGet {
+				resBytes, _ := json.Marshal(openai.AssistantFile{
+					ID:          assistantFileID,
+					Object:      "assistant.file",
+					CreatedAt:   1234567890,
+					AssistantID: assistantID,
+				})
+				fmt.Fprintln(w, string(resBytes))
+			} else if r.Method == http.MethodDelete {
+				fmt.Fprintln(w, `{
+					id: "file-wB6RM6wHdA49HfS2DJ9fEyrH",
+					object: "assistant.file.deleted",
+					deleted: true
+				  }`)
+			}
+		},
+	)
+
+	server.RegisterHandler(
+		"/v1/assistants/"+assistantID+"/files",
+		func(w http.ResponseWriter, r *http.Request) {
+			if r.Method == http.MethodGet {
+				resBytes, _ := json.Marshal(openai.AssistantFilesList{
+					AssistantFiles: []openai.AssistantFile{
+						{
+							ID:          assistantFileID,
+							Object:      "assistant.file",
+							CreatedAt:   1234567890,
+							AssistantID: assistantID,
+						},
+					},
+				})
+				fmt.Fprintln(w, string(resBytes))
+			} else if r.Method == http.MethodPost {
+				var request openai.AssistantFileRequest
+				err := json.NewDecoder(r.Body).Decode(&request)
+				checks.NoError(t, err, "Decode error")
+
+				resBytes, _ := json.Marshal(openai.AssistantFile{
+					ID:          request.FileID,
+					Object:      "assistant.file",
+					CreatedAt:   1234567890,
+					AssistantID: assistantID,
+				})
+				fmt.Fprintln(w, string(resBytes))
+			}
+		},
+	)
+
+	server.RegisterHandler(
+		"/v1/assistants/"+assistantID,
+		func(w http.ResponseWriter, r *http.Request) {
+			switch r.Method {
+			case http.MethodGet:
+				resBytes, _ := json.Marshal(openai.Assistant{
+					ID:           assistantID,
+					Object:       "assistant",
+					CreatedAt:    1234567890,
+					Name:         &assistantName,
+					Model:        openai.GPT4TurboPreview,
+					Description:  &assistantDescription,
+					Instructions: &assitantInstructions,
+				})
+				fmt.Fprintln(w, string(resBytes))
+			case http.MethodPost:
+				var request openai.AssistantRequest
+				err := json.NewDecoder(r.Body).Decode(&request)
+				checks.NoError(t, err, "Decode error")
+
+				resBytes, _ := json.Marshal(openai.Assistant{
+					ID:           assistantID,
+					Object:       "assistant",
+					CreatedAt:    1234567890,
+					Name:         request.Name,
+					Model:        request.Model,
+					Description:  request.Description,
+					Instructions: request.Instructions,
+					Tools:        request.Tools,
+				})
+				fmt.Fprintln(w, string(resBytes))
+			case http.MethodDelete:
+				fmt.Fprintln(w, `{
+					"id": "asst_abc123",
+					"object": "assistant.deleted",
+					"deleted": true
+				  }`)
+			}
+		},
+	)
+
+	server.RegisterHandler(
+		"/v1/assistants",
+		func(w http.ResponseWriter, r *http.Request) {
+			if r.Method == http.MethodPost {
+				var request openai.AssistantRequest
+				err := json.NewDecoder(r.Body).Decode(&request)
+				checks.NoError(t, err, "Decode error")
+
+				resBytes, _ := json.Marshal(openai.Assistant{
+					ID:           assistantID,
+					Object:       "assistant",
+					CreatedAt:    1234567890,
+					Name:         request.Name,
+					Model:        request.Model,
+					Description:  request.Description,
+					Instructions: request.Instructions,
+					Tools:        request.Tools,
+				})
+				fmt.Fprintln(w, string(resBytes))
+			} else if r.Method == http.MethodGet {
+				resBytes, _ := json.Marshal(openai.AssistantsList{
+					Assistants: []openai.Assistant{
+						{
+							ID:           assistantID,
+							Object:       "assistant",
+							CreatedAt:    1234567890,
+							Name:         &assistantName,
+							Model:        openai.GPT4TurboPreview,
+							Description:  &assistantDescription,
+							Instructions: &assitantInstructions,
+						},
+					},
+				})
+				fmt.Fprintln(w, string(resBytes))
+			}
+		},
+	)
+
+	ctx := context.Background()
+
+	_, err := client.CreateAssistant(ctx, openai.AssistantRequest{
+		Name:         &assistantName,
+		Description:  &assistantDescription,
+		Model:        openai.GPT4TurboPreview,
+		Instructions: &assitantInstructions,
+	})
+	checks.NoError(t, err, "CreateAssistant error")
+
+	_, err = client.RetrieveAssistant(ctx, assistantID)
+	checks.NoError(t, err, "RetrieveAssistant error")
+
+	_, err = client.ModifyAssistant(ctx, assistantID, openai.AssistantRequest{
+		Name:         &assistantName,
+		Description:  &assistantDescription,
+		Model:        openai.GPT4TurboPreview,
+		Instructions: &assitantInstructions,
+	})
+	checks.NoError(t, err, "ModifyAssistant error")
+
+	_, err = client.DeleteAssistant(ctx, assistantID)
+	checks.NoError(t, err, "DeleteAssistant error")
+
+	_, err = client.ListAssistants(ctx, &limit, &order, &after, &before)
+	checks.NoError(t, err, "ListAssistants error")
+
+	_, err = client.CreateAssistantFile(ctx, assistantID, openai.AssistantFileRequest{
+		FileID: assistantFileID,
+	})
+	checks.NoError(t, err, "CreateAssistantFile error")
+
+	_, err = client.ListAssistantFiles(ctx, assistantID, &limit, &order, &after, &before)
+	checks.NoError(t, err, "ListAssistantFiles error")
+
+	_, err = client.RetrieveAssistantFile(ctx, assistantID, assistantFileID)
+	checks.NoError(t, err, "RetrieveAssistantFile error")
+
+	err = client.DeleteAssistantFile(ctx, assistantID, assistantFileID)
+	checks.NoError(t, err, "DeleteAssistantFile error")
+}
diff --git a/client_test.go b/client_test.go
index 2c1d749..bff2597 100644
--- a/client_test.go
+++ b/client_test.go
@@ -274,6 +274,33 @@ func TestClientReturnsRequestBuilderErrors(t *testing.T) {
 		{"DeleteFineTuneModel", func() (any, error) {
 			return client.DeleteFineTuneModel(ctx, "")
 		}},
+		{"CreateAssistant", func() (any, error) {
+			return client.CreateAssistant(ctx, AssistantRequest{})
+		}},
+		{"RetrieveAssistant", func() (any, error) {
+			return client.RetrieveAssistant(ctx, "")
+		}},
+		{"ModifyAssistant", func() (any, error) {
+			return client.ModifyAssistant(ctx, "", AssistantRequest{})
+		}},
+		{"DeleteAssistant", func() (any, error) {
+			return client.DeleteAssistant(ctx, "")
+		}},
+		{"ListAssistants", func() (any, error) {
+			return client.ListAssistants(ctx, nil, nil, nil, nil)
+		}},
+		{"CreateAssistantFile", func() (any, error) {
+			return client.CreateAssistantFile(ctx, "", AssistantFileRequest{})
+		}},
+		{"ListAssistantFiles", func() (any, error) {
+			return client.ListAssistantFiles(ctx, "", nil, nil, nil, nil)
+		}},
+		{"RetrieveAssistantFile", func() (any, error) {
+			return client.RetrieveAssistantFile(ctx, "", "")
+		}},
+		{"DeleteAssistantFile", func() (any, error) {
+			return nil, client.DeleteAssistantFile(ctx, "", "")
+		}},
 	}
 
 	for _, testCase := range testCases {