Repository: sashabaranov/go-openai
Pull request #800 (2024-07-19 10:27:33)

feat: Completion request support seed
feat: #788 

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/sashabaranov/go-openai/pull/800?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alex+Baranov) Report
All modified and coverable lines are covered by tests :white_check_mark:
> Project coverage is 98.88%. Comparing base [(`774fc9d`)](https://app.codecov.io/gh/sashabaranov/go-openai/commit/774fc9dd12ed60c10a9f9f03319ddb9cd5f8780c?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alex+Baranov) to head [(`9b15daf`)](https://app.codecov.io/gh/sashabaranov/go-openai/commit/9b15daf0540fcae0b4c9691836c95945444093d2?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alex+Baranov).
> Report is 27 commits behind head on master.

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


```diff
@@            Coverage Diff             @@
##           master     #800      +/-   ##
==========================================
+ Coverage   98.46%   98.88%   +0.42%     
==========================================
  Files          24       26       +2     
  Lines        1364     1343      -21     
==========================================
- Hits         1343     1328      -15     
+ Misses         15        9       -6     
  Partials        6        6              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/sashabaranov/go-openai/pull/800?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alex+Baranov).   
: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=Alex+Baranov).


Patch:
diff --git a/assistant.go b/assistant.go
index cc13a30..4c89c1b 100644
--- a/assistant.go
+++ b/assistant.go
@@ -62,14 +62,17 @@ type AssistantToolResource struct {
 // If Tools is empty slice it will effectively delete all of the Assistant's tools.
 // If Tools is populated, it will replace all of the existing Assistant's tools with the provided tools.
 type AssistantRequest struct {
-	Model         string                 `json:"model"`
-	Name          *string                `json:"name,omitempty"`
-	Description   *string                `json:"description,omitempty"`
-	Instructions  *string                `json:"instructions,omitempty"`
-	Tools         []AssistantTool        `json:"-"`
-	FileIDs       []string               `json:"file_ids,omitempty"`
-	Metadata      map[string]any         `json:"metadata,omitempty"`
-	ToolResources *AssistantToolResource `json:"tool_resources,omitempty"`
+	Model          string                 `json:"model"`
+	Name           *string                `json:"name,omitempty"`
+	Description    *string                `json:"description,omitempty"`
+	Instructions   *string                `json:"instructions,omitempty"`
+	Tools          []AssistantTool        `json:"-"`
+	FileIDs        []string               `json:"file_ids,omitempty"`
+	Metadata       map[string]any         `json:"metadata,omitempty"`
+	ToolResources  *AssistantToolResource `json:"tool_resources,omitempty"`
+	ResponseFormat any                    `json:"response_format,omitempty"`
+	Temperature    *float32               `json:"temperature,omitempty"`
+	TopP           *float32               `json:"top_p,omitempty"`
 }
 
 // MarshalJSON provides a custom marshaller for the assistant request to handle the API use cases
diff --git a/batch.go b/batch.go
index a43d401..3c1a9d0 100644
--- a/batch.go
+++ b/batch.go
@@ -65,7 +65,7 @@ type Batch struct {
 	Endpoint BatchEndpoint `json:"endpoint"`
 	Errors   *struct {
 		Object string `json:"object,omitempty"`
-		Data   struct {
+		Data   []struct {
 			Code    string  `json:"code,omitempty"`
 			Message string  `json:"message,omitempty"`
 			Param   *string `json:"param,omitempty"`
diff --git a/chat.go b/chat.go
index eb494f4..31fa887 100644
--- a/chat.go
+++ b/chat.go
@@ -5,6 +5,8 @@ import (
 	"encoding/json"
 	"errors"
 	"net/http"
+
+	"github.com/sashabaranov/go-openai/jsonschema"
 )
 
 // Chat message role defined by the OpenAI API.
@@ -175,11 +177,20 @@ type ChatCompletionResponseFormatType string
 
 const (
 	ChatCompletionResponseFormatTypeJSONObject ChatCompletionResponseFormatType = "json_object"
+	ChatCompletionResponseFormatTypeJSONSchema ChatCompletionResponseFormatType = "json_schema"
 	ChatCompletionResponseFormatTypeText       ChatCompletionResponseFormatType = "text"
 )
 
 type ChatCompletionResponseFormat struct {
-	Type ChatCompletionResponseFormatType `json:"type,omitempty"`
+	Type       ChatCompletionResponseFormatType        `json:"type,omitempty"`
+	JSONSchema *ChatCompletionResponseFormatJSONSchema `json:"json_schema,omitempty"`
+}
+
+type ChatCompletionResponseFormatJSONSchema struct {
+	Name        string                `json:"name"`
+	Description string                `json:"description,omitempty"`
+	Schema      jsonschema.Definition `json:"schema"`
+	Strict      bool                  `json:"strict"`
 }
 
 // ChatCompletionRequest represents a request structure for chat completion API.
diff --git a/client.go b/client.go
index 7bc28e9..d5d555c 100644
--- a/client.go
+++ b/client.go
@@ -7,6 +7,7 @@ import (
 	"fmt"
 	"io"
 	"net/http"
+	"net/url"
 	"strings"
 
 	utils "github.com/sashabaranov/go-openai/internal"
@@ -228,10 +229,13 @@ func (c *Client) fullURL(suffix string, args ...any) string {
 	if c.config.APIType == APITypeAzure || c.config.APIType == APITypeAzureAD {
 		baseURL := c.config.BaseURL
 		baseURL = strings.TrimRight(baseURL, "/")
+		parseURL, _ := url.Parse(baseURL)
+		query := parseURL.Query()
+		query.Add("api-version", c.config.APIVersion)
 		// if suffix is /models change to {endpoint}/openai/models?api-version=2022-12-01
 		// https://learn.microsoft.com/en-us/rest/api/cognitiveservices/azureopenaistable/models/list?tabs=HTTP
 		if containsSubstr([]string{"/models", "/assistants", "/threads", "/files"}, suffix) {
-			return fmt.Sprintf("%s/%s%s?api-version=%s", baseURL, azureAPIPrefix, suffix, c.config.APIVersion)
+			return fmt.Sprintf("%s/%s%s?%s", baseURL, azureAPIPrefix, suffix, query.Encode())
 		}
 		azureDeploymentName := "UNKNOWN"
 		if len(args) > 0 {
@@ -240,9 +244,9 @@ func (c *Client) fullURL(suffix string, args ...any) string {
 				azureDeploymentName = c.config.GetAzureDeploymentByModel(model)
 			}
 		}
-		return fmt.Sprintf("%s/%s/%s/%s%s?api-version=%s",
+		return fmt.Sprintf("%s/%s/%s/%s%s?%s",
 			baseURL, azureAPIPrefix, azureDeploymentsPrefix,
-			azureDeploymentName, suffix, c.config.APIVersion,
+			azureDeploymentName, suffix, query.Encode(),
 		)
 	}
 
diff --git a/completion.go b/completion.go
index 024f09b..bc2a637 100644
--- a/completion.go
+++ b/completion.go
@@ -24,6 +24,9 @@ const (
 	GPT40314              = "gpt-4-0314"
 	GPT4o                 = "gpt-4o"
 	GPT4o20240513         = "gpt-4o-2024-05-13"
+	GPT4o20240806         = "gpt-4o-2024-08-06"
+	GPT4oMini             = "gpt-4o-mini"
+	GPT4oMini20240718     = "gpt-4o-mini-2024-07-18"
 	GPT4Turbo             = "gpt-4-turbo"
 	GPT4Turbo20240409     = "gpt-4-turbo-2024-04-09"
 	GPT4Turbo0125         = "gpt-4-0125-preview"
@@ -89,6 +92,9 @@ var disabledModelsForEndpoints = map[string]map[string]bool{
 		GPT4:                 true,
 		GPT4o:                true,
 		GPT4o20240513:        true,
+		GPT4o20240806:        true,
+		GPT4oMini:            true,
+		GPT4oMini20240718:    true,
 		GPT4TurboPreview:     true,
 		GPT4VisionPreview:    true,
 		GPT4Turbo1106:        true,
@@ -132,25 +138,26 @@ func checkPromptType(prompt any) bool {
 
 // CompletionRequest represents a request structure for completion API.
 type CompletionRequest struct {
-	Model            string   `json:"model"`
-	Prompt           any      `json:"prompt,omitempty"`
-	Suffix           string   `json:"suffix,omitempty"`
-	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"`
-	LogProbs         int      `json:"logprobs,omitempty"`
-	Echo             bool     `json:"echo,omitempty"`
-	Stop             []string `json:"stop,omitempty"`
-	PresencePenalty  float32  `json:"presence_penalty,omitempty"`
-	FrequencyPenalty float32  `json:"frequency_penalty,omitempty"`
-	BestOf           int      `json:"best_of,omitempty"`
+	Model            string  `json:"model"`
+	Prompt           any     `json:"prompt,omitempty"`
+	BestOf           int     `json:"best_of,omitempty"`
+	Echo             bool    `json:"echo,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/completions/create#completions/create-logit_bias
-	LogitBias map[string]int `json:"logit_bias,omitempty"`
-	User      string         `json:"user,omitempty"`
+	LogitBias       map[string]int `json:"logit_bias,omitempty"`
+	LogProbs        int            `json:"logprobs,omitempty"`
+	MaxTokens       int            `json:"max_tokens,omitempty"`
+	N               int            `json:"n,omitempty"`
+	PresencePenalty float32        `json:"presence_penalty,omitempty"`
+	Seed            *int           `json:"seed,omitempty"`
+	Stop            []string       `json:"stop,omitempty"`
+	Stream          bool           `json:"stream,omitempty"`
+	Suffix          string         `json:"suffix,omitempty"`
+	Temperature     float32        `json:"temperature,omitempty"`
+	TopP            float32        `json:"top_p,omitempty"`
+	User            string         `json:"user,omitempty"`
 }
 
 // CompletionChoice represents one of possible completions.
diff --git a/files.go b/files.go
index 26ad6bd..edc9f2a 100644
--- a/files.go
+++ b/files.go
@@ -102,6 +102,7 @@ func (c *Client) CreateFile(ctx context.Context, request FileRequest) (file File
 	if err != nil {
 		return
 	}
+	defer fileData.Close()
 
 	err = builder.CreateFormFile("file", fileData)
 	if err != nil {
diff --git a/fine_tuning_job.go b/fine_tuning_job.go
index 9dcb49d..5a9f54a 100644
--- a/fine_tuning_job.go
+++ b/fine_tuning_job.go
@@ -26,7 +26,9 @@ type FineTuningJob struct {
 }
 
 type Hyperparameters struct {
-	Epochs any `json:"n_epochs,omitempty"`
+	Epochs                 any `json:"n_epochs,omitempty"`
+	LearningRateMultiplier any `json:"learning_rate_multiplier,omitempty"`
+	BatchSize              any `json:"batch_size,omitempty"`
 }
 
 type FineTuningJobRequest struct {
diff --git a/jsonschema/json.go b/jsonschema/json.go
index cb941eb..7fd1e11 100644
--- a/jsonschema/json.go
+++ b/jsonschema/json.go
@@ -29,11 +29,17 @@ type Definition struct {
 	// one element, where each element is unique. You will probably only use this with strings.
 	Enum []string `json:"enum,omitempty"`
 	// Properties describes the properties of an object, if the schema type is Object.
-	Properties map[string]Definition `json:"properties"`
+	Properties map[string]Definition `json:"properties,omitempty"`
 	// Required specifies which properties are required, if the schema type is Object.
 	Required []string `json:"required,omitempty"`
 	// Items specifies which data type an array contains, if the schema type is Array.
 	Items *Definition `json:"items,omitempty"`
+	// AdditionalProperties is used to control the handling of properties in an object
+	// that are not explicitly defined in the properties section of the schema. example:
+	// additionalProperties: true
+	// additionalProperties: false
+	// additionalProperties: jsonschema.Definition{Type: jsonschema.String}
+	AdditionalProperties any `json:"additionalProperties,omitempty"`
 }
 
 func (d Definition) MarshalJSON() ([]byte, error) {
diff --git a/thread.go b/thread.go
index 6f75214..bc08e2b 100644
--- a/thread.go
+++ b/thread.go
@@ -83,14 +83,25 @@ type ModifyThreadRequest struct {
 type ThreadMessageRole string
 
 const (
-	ThreadMessageRoleUser ThreadMessageRole = "user"
+	ThreadMessageRoleAssistant ThreadMessageRole = "assistant"
+	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"`
+	Role        ThreadMessageRole  `json:"role"`
+	Content     string             `json:"content"`
+	FileIDs     []string           `json:"file_ids,omitempty"`
+	Attachments []ThreadAttachment `json:"attachments,omitempty"`
+	Metadata    map[string]any     `json:"metadata,omitempty"`
+}
+
+type ThreadAttachment struct {
+	FileID string                 `json:"file_id"`
+	Tools  []ThreadAttachmentTool `json:"tools"`
+}
+
+type ThreadAttachmentTool struct {
+	Type string `json:"type"`
 }
 
 type ThreadDeleteResponse struct {


Test patch:
diff --git a/api_integration_test.go b/api_integration_test.go
index f346851..3084268 100644
--- a/api_integration_test.go
+++ b/api_integration_test.go
@@ -4,6 +4,7 @@ package openai_test
 
 import (
 	"context"
+	"encoding/json"
 	"errors"
 	"io"
 	"os"
@@ -178,3 +179,63 @@ func TestAPIError(t *testing.T) {
 		t.Fatal("Empty error message occurred")
 	}
 }
+
+func TestChatCompletionResponseFormat_JSONSchema(t *testing.T) {
+	apiToken := os.Getenv("OPENAI_TOKEN")
+	if apiToken == "" {
+		t.Skip("Skipping testing against production OpenAI API. Set OPENAI_TOKEN environment variable to enable it.")
+	}
+
+	var err error
+	c := openai.NewClient(apiToken)
+	ctx := context.Background()
+
+	resp, err := c.CreateChatCompletion(
+		ctx,
+		openai.ChatCompletionRequest{
+			Model: openai.GPT4oMini,
+			Messages: []openai.ChatCompletionMessage{
+				{
+					Role: openai.ChatMessageRoleSystem,
+					Content: "Please enter a string, and we will convert it into the following naming conventions:" +
+						"1. PascalCase: Each word starts with an uppercase letter, with no spaces or separators." +
+						"2. CamelCase: The first word starts with a lowercase letter, " +
+						"and subsequent words start with an uppercase letter, with no spaces or separators." +
+						"3. KebabCase: All letters are lowercase, with words separated by hyphens `-`." +
+						"4. SnakeCase: All letters are lowercase, with words separated by underscores `_`.",
+				},
+				{
+					Role:    openai.ChatMessageRoleUser,
+					Content: "Hello World",
+				},
+			},
+			ResponseFormat: &openai.ChatCompletionResponseFormat{
+				Type: openai.ChatCompletionResponseFormatTypeJSONSchema,
+				JSONSchema: &openai.ChatCompletionResponseFormatJSONSchema{
+					Name: "cases",
+					Schema: jsonschema.Definition{
+						Type: jsonschema.Object,
+						Properties: map[string]jsonschema.Definition{
+							"PascalCase": jsonschema.Definition{Type: jsonschema.String},
+							"CamelCase":  jsonschema.Definition{Type: jsonschema.String},
+							"KebabCase":  jsonschema.Definition{Type: jsonschema.String},
+							"SnakeCase":  jsonschema.Definition{Type: jsonschema.String},
+						},
+						Required:             []string{"PascalCase", "CamelCase", "KebabCase", "SnakeCase"},
+						AdditionalProperties: false,
+					},
+					Strict: true,
+				},
+			},
+		},
+	)
+	checks.NoError(t, err, "CreateChatCompletion (use json_schema response) returned error")
+	var result = make(map[string]string)
+	err = json.Unmarshal([]byte(resp.Choices[0].Message.Content), &result)
+	checks.NoError(t, err, "CreateChatCompletion (use json_schema response) unmarshal error")
+	for _, key := range []string{"PascalCase", "CamelCase", "KebabCase", "SnakeCase"} {
+		if _, ok := result[key]; !ok {
+			t.Errorf("key:%s does not exist.", key)
+		}
+	}
+}
diff --git a/fine_tuning_job_test.go b/fine_tuning_job_test.go
index d2fbcd4..5f63ef2 100644
--- a/fine_tuning_job_test.go
+++ b/fine_tuning_job_test.go
@@ -33,7 +33,9 @@ func TestFineTuningJob(t *testing.T) {
 				ValidationFile: "",
 				TrainingFile:   "file-abc123",
 				Hyperparameters: openai.Hyperparameters{
-					Epochs: "auto",
+					Epochs:                 "auto",
+					LearningRateMultiplier: "auto",
+					BatchSize:              "auto",
 				},
 				TrainedTokens: 5768,
 			})