Repository: sashabaranov/go-openai
Pull request #543 (2023-11-07 18:02:12)

Add SystemFingerprint and chatMsg.ToolCallID field
A few small improvements:
- capture the new `system_fingerprint` response field to chat calls
- on ChatCompletionMessage capture the `tool_call_id` that needs to be set for role=TOOL prompts
- fix a typo (ToolChoiche)
- Add omitempty to ChatCompletionMessage optional fields
- Add required ToolCall.type field

sashabaranov: @gburt thank you for the PR! Seems like there's a conflicting change here
gburt: > @gburt thank you for the PR! Seems like there's a conflicting change here

you're welcome! thanks for the great library

Patch:
diff --git a/chat.go b/chat.go
index 609e0c3..9ad31c4 100644
--- a/chat.go
+++ b/chat.go
@@ -62,11 +62,17 @@ type ChatCompletionMessage struct {
 	Name string `json:"name,omitempty"`
 
 	FunctionCall *FunctionCall `json:"function_call,omitempty"`
-	ToolCalls    []ToolCall    `json:"tool_calls,omitempty"`
+
+	// For Role=assistant prompts this may be set to the tool calls generated by the model, such as function calls.
+	ToolCalls []ToolCall `json:"tool_calls,omitempty"`
+
+	// For Role=tool prompts this should be set to the ID given in the assistant's prior request to call a tool.
+	ToolCallID string `json:"tool_call_id,omitempty"`
 }
 
 type ToolCall struct {
 	ID       string       `json:"id"`
+	Type     ToolType     `json:"type"`
 	Function FunctionCall `json:"function"`
 }
 
@@ -84,7 +90,7 @@ const (
 )
 
 type ChatCompletionResponseFormat struct {
-	Type ChatCompletionResponseFormatType `json:"type"`
+	Type ChatCompletionResponseFormatType `json:"type,omitempty"`
 }
 
 // ChatCompletionRequest represents a request structure for chat completion API.
@@ -112,7 +118,7 @@ type ChatCompletionRequest struct {
 	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"`
+	ToolChoice any `json:"tool_choice,omitempty"`
 }
 
 type ToolType string
@@ -126,7 +132,7 @@ type Tool struct {
 	Function FunctionDefinition `json:"function,omitempty"`
 }
 
-type ToolChoiche struct {
+type ToolChoice struct {
 	Type     ToolType     `json:"type"`
 	Function ToolFunction `json:"function,omitempty"`
 }
@@ -182,12 +188,13 @@ type ChatCompletionChoice struct {
 
 // ChatCompletionResponse represents a response structure for chat completion API.
 type ChatCompletionResponse struct {
-	ID      string                 `json:"id"`
-	Object  string                 `json:"object"`
-	Created int64                  `json:"created"`
-	Model   string                 `json:"model"`
-	Choices []ChatCompletionChoice `json:"choices"`
-	Usage   Usage                  `json:"usage"`
+	ID                string                 `json:"id"`
+	Object            string                 `json:"object"`
+	Created           int64                  `json:"created"`
+	Model             string                 `json:"model"`
+	Choices           []ChatCompletionChoice `json:"choices"`
+	Usage             Usage                  `json:"usage"`
+	SystemFingerprint string                 `json:"system_fingerprint"`
 
 	httpHeader
 }
diff --git a/thread.go b/thread.go
new file mode 100644
index 0000000..291f3dc
--- /dev/null
+++ b/thread.go
@@ -0,0 +1,107 @@
+package openai
+
+import (
+	"context"
+	"net/http"
+)
+
+const (
+	threadsSuffix = "/threads"
+)
+
+type Thread struct {
+	ID        string         `json:"id"`
+	Object    string         `json:"object"`
+	CreatedAt int64          `json:"created_at"`
+	Metadata  map[string]any `json:"metadata"`
+
+	httpHeader
+}
+
+type ThreadRequest struct {
+	Messages []ThreadMessage `json:"messages,omitempty"`
+	Metadata map[string]any  `json:"metadata,omitempty"`
+}
+
+type ModifyThreadRequest struct {
+	Metadata map[string]any `json:"metadata"`
+}
+
+type ThreadMessageRole string
+
+const (
+	ThreadMessageRoleUser ThreadMessageRole = "user"
+)
+
+type ThreadMessage struct {
+	Role     ThreadMessageRole `json:"role"`
+	Content  string            `json:"content"`
+	FileIDs  []string          `json:"file_ids,omitempty"`
+	Metadata map[string]any    `json:"metadata,omitempty"`
+}
+
+type ThreadDeleteResponse struct {
+	ID      string `json:"id"`
+	Object  string `json:"object"`
+	Deleted bool   `json:"deleted"`
+
+	httpHeader
+}
+
+// CreateThread creates a new thread.
+func (c *Client) CreateThread(ctx context.Context, request ThreadRequest) (response Thread, err error) {
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(threadsSuffix), withBody(request),
+		withBetaAssistantV1())
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}
+
+// RetrieveThread retrieves a thread.
+func (c *Client) RetrieveThread(ctx context.Context, threadID string) (response Thread, err error) {
+	urlSuffix := threadsSuffix + "/" + threadID
+	req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix),
+		withBetaAssistantV1())
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}
+
+// ModifyThread modifies a thread.
+func (c *Client) ModifyThread(
+	ctx context.Context,
+	threadID string,
+	request ModifyThreadRequest,
+) (response Thread, err error) {
+	urlSuffix := threadsSuffix + "/" + threadID
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request),
+		withBetaAssistantV1())
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}
+
+// DeleteThread deletes a thread.
+func (c *Client) DeleteThread(
+	ctx context.Context,
+	threadID string,
+) (response ThreadDeleteResponse, err error) {
+	urlSuffix := threadsSuffix + "/" + threadID
+	req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL(urlSuffix),
+		withBetaAssistantV1())
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &response)
+	return
+}


Test patch:
diff --git a/chat_test.go b/chat_test.go
index 5bf1eaf..a8155ed 100644
--- a/chat_test.go
+++ b/chat_test.go
@@ -51,6 +51,20 @@ func TestChatCompletionsWrongModel(t *testing.T) {
 	checks.ErrorIs(t, err, openai.ErrChatCompletionInvalidModel, msg)
 }
 
+func TestChatRequestOmitEmpty(t *testing.T) {
+	data, err := json.Marshal(openai.ChatCompletionRequest{
+		// We set model b/c it's required, so omitempty doesn't make sense
+		Model: "gpt-4",
+	})
+	checks.NoError(t, err)
+
+	// messages is also required so isn't omitted
+	const expected = `{"model":"gpt-4","messages":null}`
+	if string(data) != expected {
+		t.Errorf("expected JSON with all empty fields to be %v but was %v", expected, string(data))
+	}
+}
+
 func TestChatCompletionsWithStream(t *testing.T) {
 	config := openai.DefaultConfig("whatever")
 	config.BaseURL = "http://localhost/v1"
diff --git a/client_test.go b/client_test.go
index bff2597..b2f28f9 100644
--- a/client_test.go
+++ b/client_test.go
@@ -301,6 +301,18 @@ func TestClientReturnsRequestBuilderErrors(t *testing.T) {
 		{"DeleteAssistantFile", func() (any, error) {
 			return nil, client.DeleteAssistantFile(ctx, "", "")
 		}},
+		{"CreateThread", func() (any, error) {
+			return client.CreateThread(ctx, ThreadRequest{})
+		}},
+		{"RetrieveThread", func() (any, error) {
+			return client.RetrieveThread(ctx, "")
+		}},
+		{"ModifyThread", func() (any, error) {
+			return client.ModifyThread(ctx, "", ModifyThreadRequest{})
+		}},
+		{"DeleteThread", func() (any, error) {
+			return client.DeleteThread(ctx, "")
+		}},
 	}
 
 	for _, testCase := range testCases {
diff --git a/thread_test.go b/thread_test.go
new file mode 100644
index 0000000..227ab63
--- /dev/null
+++ b/thread_test.go
@@ -0,0 +1,95 @@
+package openai_test
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"testing"
+
+	openai "github.com/sashabaranov/go-openai"
+	"github.com/sashabaranov/go-openai/internal/test/checks"
+)
+
+// TestThread Tests the thread endpoint of the API using the mocked server.
+func TestThread(t *testing.T) {
+	threadID := "thread_abc123"
+	client, server, teardown := setupOpenAITestServer()
+	defer teardown()
+
+	server.RegisterHandler(
+		"/v1/threads/"+threadID,
+		func(w http.ResponseWriter, r *http.Request) {
+			switch r.Method {
+			case http.MethodGet:
+				resBytes, _ := json.Marshal(openai.Thread{
+					ID:        threadID,
+					Object:    "thread",
+					CreatedAt: 1234567890,
+				})
+				fmt.Fprintln(w, string(resBytes))
+			case http.MethodPost:
+				var request openai.ThreadRequest
+				err := json.NewDecoder(r.Body).Decode(&request)
+				checks.NoError(t, err, "Decode error")
+
+				resBytes, _ := json.Marshal(openai.Thread{
+					ID:        threadID,
+					Object:    "thread",
+					CreatedAt: 1234567890,
+				})
+				fmt.Fprintln(w, string(resBytes))
+			case http.MethodDelete:
+				fmt.Fprintln(w, `{
+					"id": "thread_abc123",
+					"object": "thread.deleted",
+					"deleted": true
+					}`)
+			}
+		},
+	)
+
+	server.RegisterHandler(
+		"/v1/threads",
+		func(w http.ResponseWriter, r *http.Request) {
+			if r.Method == http.MethodPost {
+				var request openai.ModifyThreadRequest
+				err := json.NewDecoder(r.Body).Decode(&request)
+				checks.NoError(t, err, "Decode error")
+
+				resBytes, _ := json.Marshal(openai.Thread{
+					ID:        threadID,
+					Object:    "thread",
+					CreatedAt: 1234567890,
+					Metadata:  request.Metadata,
+				})
+				fmt.Fprintln(w, string(resBytes))
+			}
+		},
+	)
+
+	ctx := context.Background()
+
+	_, err := client.CreateThread(ctx, openai.ThreadRequest{
+		Messages: []openai.ThreadMessage{
+			{
+				Role:    openai.ThreadMessageRoleUser,
+				Content: "Hello, World!",
+			},
+		},
+	})
+	checks.NoError(t, err, "CreateThread error")
+
+	_, err = client.RetrieveThread(ctx, threadID)
+	checks.NoError(t, err, "RetrieveThread error")
+
+	_, err = client.ModifyThread(ctx, threadID, openai.ModifyThreadRequest{
+		Metadata: map[string]interface{}{
+			"key": "value",
+		},
+	})
+	checks.NoError(t, err, "ModifyThread error")
+
+	_, err = client.DeleteThread(ctx, threadID)
+	checks.NoError(t, err, "DeleteThread error")
+}