Repository: sashabaranov/go-openai
Pull request #926 (2025-01-24 14:29:11)

Make "Content" field in "ChatCompletionMessage" omitempty
**Describe the change**
According to the OpenAI API spec, the content field of ChatCompletionMessage should be nullable. Right now when we try to marshal the struct with empty string, we get `"content": ""`, but ideally, the content key should exist if that field is empty

**Provide OpenAI documentation link**
https://platform.openai.com/docs/api-reference/chat/object
Under: choices -> message -> content
<img width="607" alt="image" src="https://github.com/user-attachments/assets/4e8ebbfe-5fb1-4c89-8b77-06d9c571c99c" />


**Describe your solution**
We can fix this my adding an omitempty json tag to the "content" field in the "ChatCompletionMessage" struct

**Tests**
Tested this by sending a completion request with empty content field to a Nvidia NIM Inference Server



codecov[bot]: ## [Codecov](https://app.codecov.io/gh/sashabaranov/go-openai/pull/926?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.69%. 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 [(`7f7d2c5`)](https://app.codecov.io/gh/sashabaranov/go-openai/commit/7f7d2c55a3ce6a1392c4639409357320949a2f11?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alex+Baranov).
> Report is 78 commits behind head on master.

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


```diff
@@            Coverage Diff             @@
##           master     #926      +/-   ##
==========================================
+ Coverage   98.46%   98.69%   +0.23%     
==========================================
  Files          24       26       +2     
  Lines        1364     1761     +397     
==========================================
+ Hits         1343     1738     +395     
- Misses         15       16       +1     
- Partials        6        7       +1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/sashabaranov/go-openai/pull/926?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/.golangci.yml b/.golangci.yml
index 724cb73..9d22d9b 100644
--- a/.golangci.yml
+++ b/.golangci.yml
@@ -206,6 +206,7 @@ linters:
     - tparallel # tparallel detects inappropriate usage of t.Parallel() method in your Go test codes
     - unconvert # Remove unnecessary type conversions
     - unparam # Reports unused function parameters
+    - usetesting # Reports uses of functions with replacement inside the testing package
     - wastedassign # wastedassign finds wasted assignment statements.
     - whitespace # Tool for detection of leading and trailing whitespace
     ## you may want to enable
diff --git a/chat.go b/chat.go
index fcaf79c..8ea7238 100644
--- a/chat.go
+++ b/chat.go
@@ -93,7 +93,7 @@ type ChatMessagePart struct {
 
 type ChatCompletionMessage struct {
 	Role         string `json:"role"`
-	Content      string `json:"content"`
+	Content      string `json:"content,omitempty"`
 	Refusal      string `json:"refusal,omitempty"`
 	MultiContent []ChatMessagePart
 
@@ -132,7 +132,7 @@ func (m ChatCompletionMessage) MarshalJSON() ([]byte, error) {
 
 	msg := struct {
 		Role         string            `json:"role"`
-		Content      string            `json:"content"`
+		Content      string            `json:"content,omitempty"`
 		Refusal      string            `json:"refusal,omitempty"`
 		MultiContent []ChatMessagePart `json:"-"`
 		Name         string            `json:"name,omitempty"`
@@ -146,7 +146,7 @@ func (m ChatCompletionMessage) MarshalJSON() ([]byte, error) {
 func (m *ChatCompletionMessage) UnmarshalJSON(bs []byte) error {
 	msg := struct {
 		Role         string `json:"role"`
-		Content      string `json:"content"`
+		Content      string `json:"content,omitempty"`
 		Refusal      string `json:"refusal,omitempty"`
 		MultiContent []ChatMessagePart
 		Name         string        `json:"name,omitempty"`
@@ -258,6 +258,8 @@ type ChatCompletionRequest struct {
 	// Store can be set to true to store the output of this completion request for use in distillations and evals.
 	// https://platform.openai.com/docs/api-reference/chat/create#chat-create-store
 	Store bool `json:"store,omitempty"`
+	// Controls effort on reasoning for reasoning models. It can be set to "low", "medium", or "high".
+	ReasoningEffort string `json:"reasoning_effort,omitempty"`
 	// Metadata to store with the completion.
 	Metadata map[string]string `json:"metadata,omitempty"`
 }
diff --git a/completion.go b/completion.go
index f115660..6272468 100644
--- a/completion.go
+++ b/completion.go
@@ -29,6 +29,8 @@ const (
 	O1Mini20240912        = "o1-mini-2024-09-12"
 	O1Preview             = "o1-preview"
 	O1Preview20240912     = "o1-preview-2024-09-12"
+	O1                    = "o1"
+	O120241217            = "o1-2024-12-17"
 	GPT432K0613           = "gpt-4-32k-0613"
 	GPT432K0314           = "gpt-4-32k-0314"
 	GPT432K               = "gpt-4-32k"


Test patch:
diff --git a/audio_api_test.go b/audio_api_test.go
index c245984..6c6a356 100644
--- a/audio_api_test.go
+++ b/audio_api_test.go
@@ -40,12 +40,9 @@ func TestAudio(t *testing.T) {
 
 	ctx := context.Background()
 
-	dir, cleanup := test.CreateTestDirectory(t)
-	defer cleanup()
-
 	for _, tc := range testcases {
 		t.Run(tc.name, func(t *testing.T) {
-			path := filepath.Join(dir, "fake.mp3")
+			path := filepath.Join(t.TempDir(), "fake.mp3")
 			test.CreateTestFile(t, path)
 
 			req := openai.AudioRequest{
@@ -90,12 +87,9 @@ func TestAudioWithOptionalArgs(t *testing.T) {
 
 	ctx := context.Background()
 
-	dir, cleanup := test.CreateTestDirectory(t)
-	defer cleanup()
-
 	for _, tc := range testcases {
 		t.Run(tc.name, func(t *testing.T) {
-			path := filepath.Join(dir, "fake.mp3")
+			path := filepath.Join(t.TempDir(), "fake.mp3")
 			test.CreateTestFile(t, path)
 
 			req := openai.AudioRequest{
diff --git a/audio_test.go b/audio_test.go
index 235931f..9f32d54 100644
--- a/audio_test.go
+++ b/audio_test.go
@@ -13,9 +13,7 @@ import (
 )
 
 func TestAudioWithFailingFormBuilder(t *testing.T) {
-	dir, cleanup := test.CreateTestDirectory(t)
-	defer cleanup()
-	path := filepath.Join(dir, "fake.mp3")
+	path := filepath.Join(t.TempDir(), "fake.mp3")
 	test.CreateTestFile(t, path)
 
 	req := AudioRequest{
@@ -63,9 +61,7 @@ func TestAudioWithFailingFormBuilder(t *testing.T) {
 
 func TestCreateFileField(t *testing.T) {
 	t.Run("createFileField failing file", func(t *testing.T) {
-		dir, cleanup := test.CreateTestDirectory(t)
-		defer cleanup()
-		path := filepath.Join(dir, "fake.mp3")
+		path := filepath.Join(t.TempDir(), "fake.mp3")
 		test.CreateTestFile(t, path)
 
 		req := AudioRequest{
diff --git a/chat_test.go b/chat_test.go
index 134026c..cea549c 100644
--- a/chat_test.go
+++ b/chat_test.go
@@ -631,7 +631,7 @@ func TestMultipartChatMessageSerialization(t *testing.T) {
 		t.Fatalf("Unexpected error")
 	}
 	res = strings.ReplaceAll(string(s), " ", "")
-	if res != `{"role":"user","content":""}` {
+	if res != `{"role":"user"}` {
 		t.Fatalf("invalid message: %s", string(s))
 	}
 }
diff --git a/image_api_test.go b/image_api_test.go
index 48416b1..f6057b7 100644
--- a/image_api_test.go
+++ b/image_api_test.go
@@ -7,6 +7,7 @@ import (
 	"io"
 	"net/http"
 	"os"
+	"path/filepath"
 	"testing"
 	"time"
 
@@ -86,24 +87,17 @@ func TestImageEdit(t *testing.T) {
 	defer teardown()
 	server.RegisterHandler("/v1/images/edits", handleEditImageEndpoint)
 
-	origin, err := os.Create("image.png")
+	origin, err := os.Create(filepath.Join(t.TempDir(), "image.png"))
 	if err != nil {
-		t.Error("open origin file error")
-		return
+		t.Fatalf("open origin file error: %v", err)
 	}
+	defer origin.Close()
 
-	mask, err := os.Create("mask.png")
+	mask, err := os.Create(filepath.Join(t.TempDir(), "mask.png"))
 	if err != nil {
-		t.Error("open mask file error")
-		return
+		t.Fatalf("open mask file error: %v", err)
 	}
-
-	defer func() {
-		mask.Close()
-		origin.Close()
-		os.Remove("mask.png")
-		os.Remove("image.png")
-	}()
+	defer mask.Close()
 
 	_, err = client.CreateEditImage(context.Background(), openai.ImageEditRequest{
 		Image:          origin,
@@ -121,16 +115,11 @@ func TestImageEditWithoutMask(t *testing.T) {
 	defer teardown()
 	server.RegisterHandler("/v1/images/edits", handleEditImageEndpoint)
 
-	origin, err := os.Create("image.png")
+	origin, err := os.Create(filepath.Join(t.TempDir(), "image.png"))
 	if err != nil {
-		t.Error("open origin file error")
-		return
+		t.Fatalf("open origin file error: %v", err)
 	}
-
-	defer func() {
-		origin.Close()
-		os.Remove("image.png")
-	}()
+	defer origin.Close()
 
 	_, err = client.CreateEditImage(context.Background(), openai.ImageEditRequest{
 		Image:          origin,
@@ -178,16 +167,11 @@ func TestImageVariation(t *testing.T) {
 	defer teardown()
 	server.RegisterHandler("/v1/images/variations", handleVariateImageEndpoint)
 
-	origin, err := os.Create("image.png")
+	origin, err := os.Create(filepath.Join(t.TempDir(), "image.png"))
 	if err != nil {
-		t.Error("open origin file error")
-		return
+		t.Fatalf("open origin file error: %v", err)
 	}
-
-	defer func() {
-		origin.Close()
-		os.Remove("image.png")
-	}()
+	defer origin.Close()
 
 	_, err = client.CreateVariImage(context.Background(), openai.ImageVariRequest{
 		Image:          origin,
diff --git a/internal/form_builder_test.go b/internal/form_builder_test.go
index d3faf99..8df989e 100644
--- a/internal/form_builder_test.go
+++ b/internal/form_builder_test.go
@@ -1,7 +1,6 @@
 package openai //nolint:testpackage // testing private field
 
 import (
-	"github.com/sashabaranov/go-openai/internal/test"
 	"github.com/sashabaranov/go-openai/internal/test/checks"
 
 	"bytes"
@@ -20,15 +19,11 @@ func (*failingWriter) Write([]byte) (int, error) {
 }
 
 func TestFormBuilderWithFailingWriter(t *testing.T) {
-	dir, cleanup := test.CreateTestDirectory(t)
-	defer cleanup()
-
-	file, err := os.CreateTemp(dir, "")
+	file, err := os.CreateTemp(t.TempDir(), "")
 	if err != nil {
-		t.Errorf("Error creating tmp file: %v", err)
+		t.Fatalf("Error creating tmp file: %v", err)
 	}
 	defer file.Close()
-	defer os.Remove(file.Name())
 
 	builder := NewFormBuilder(&failingWriter{})
 	err = builder.CreateFormFile("file", file)
@@ -36,15 +31,11 @@ func TestFormBuilderWithFailingWriter(t *testing.T) {
 }
 
 func TestFormBuilderWithClosedFile(t *testing.T) {
-	dir, cleanup := test.CreateTestDirectory(t)
-	defer cleanup()
-
-	file, err := os.CreateTemp(dir, "")
+	file, err := os.CreateTemp(t.TempDir(), "")
 	if err != nil {
-		t.Errorf("Error creating tmp file: %v", err)
+		t.Fatalf("Error creating tmp file: %v", err)
 	}
 	file.Close()
-	defer os.Remove(file.Name())
 
 	body := &bytes.Buffer{}
 	builder := NewFormBuilder(body)
diff --git a/internal/test/helpers.go b/internal/test/helpers.go
index 0e63ae8..dc5fa66 100644
--- a/internal/test/helpers.go
+++ b/internal/test/helpers.go
@@ -19,16 +19,6 @@ func CreateTestFile(t *testing.T, path string) {
 	file.Close()
 }
 
-// CreateTestDirectory creates a temporary folder which will be deleted when cleanup is called.
-func CreateTestDirectory(t *testing.T) (path string, cleanup func()) {
-	t.Helper()
-
-	path, err := os.MkdirTemp(os.TempDir(), "")
-	checks.NoError(t, err)
-
-	return path, func() { os.RemoveAll(path) }
-}
-
 // TokenRoundTripper is a struct that implements the RoundTripper
 // interface, specifically to handle the authentication token by adding a token
 // to the request header. We need this because the API requires that each
diff --git a/openai_test.go b/openai_test.go
index 729d888..6c26eeb 100644
--- a/openai_test.go
+++ b/openai_test.go
@@ -29,9 +29,9 @@ func setupAzureTestServer() (client *openai.Client, server *test.ServerTest, tea
 
 // numTokens Returns the number of GPT-3 encoded tokens in the given text.
 // This function approximates based on the rule of thumb stated by OpenAI:
-// https://beta.openai.com/tokenizer
+// https://beta.openai.com/tokenizer/
 //
-// TODO: implement an actual tokenizer for GPT-3 and Codex (once available)
+// TODO: implement an actual tokenizer for GPT-3 and Codex (once available).
 func numTokens(s string) int {
 	return int(float32(len(s)) / 4)
 }
diff --git a/speech_test.go b/speech_test.go
index f1e405c..67a3fea 100644
--- a/speech_test.go
+++ b/speech_test.go
@@ -21,10 +21,8 @@ func TestSpeechIntegration(t *testing.T) {
 	defer teardown()
 
 	server.RegisterHandler("/v1/audio/speech", func(w http.ResponseWriter, r *http.Request) {
-		dir, cleanup := test.CreateTestDirectory(t)
-		path := filepath.Join(dir, "fake.mp3")
+		path := filepath.Join(t.TempDir(), "fake.mp3")
 		test.CreateTestFile(t, path)
-		defer cleanup()
 
 		// audio endpoints only accept POST requests
 		if r.Method != "POST" {