Repository: sashabaranov/go-openai
Pull request #925 (2025-01-23 09:38:30)

Add support for reasoning_content field in chat completion messages for DeepSeek R1
feat: Add support for reasoning_content field in chat completion messages

This PR adds support for the `reasoning_content` field in the ChatCompletionMessage struct to accommodate models that return their reasoning process separately from the main content. This feature is particularly useful for:

- DeepSeek R1 model 

Both models return their step-by-step reasoning process through this field.

Changes:
- Added `reasoning_content` field to ChatCompletionMessage struct
- Updated JSON marshaling/unmarshaling to handle the new field
- Field is optional (omitempty) to maintain backwards compatibility

Example response structure:
```json
{
    "role": "assistant",
    "reasoning_content": "1. First, I analyze the question...\n2. Based on the given information...",
    "content": "The final answer is..."
}


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/sashabaranov/go-openai/pull/925?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 [(`5fef03f`)](https://app.codecov.io/gh/sashabaranov/go-openai/commit/5fef03f97fbe5bf0fc1733241e059a826bd68968?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     #925      +/-   ##
==========================================
+ Coverage   98.46%   98.69%   +0.23%     
==========================================
  Files          24       26       +2     
  Lines        1364     1765     +401     
==========================================
+ Hits         1343     1742     +399     
- 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/925?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).

sashabaranov: @goodenough227 could you please point to the OpenAI API reference for this one? Or is this field only supported by DeepSeek currently?
yayanet: > @goodenough227 could you please point to the OpenAI API reference for this one? Or is this field only supported by DeepSeek currently?

I also have a need for this feature.
The `reasoning_content` field is currently specific to `deepseek-reasoner` model ([docs](https://api-docs.deepseek.com/api/create-chat-completion#responses))
goodenough227: > @goodenough227 could you please point to the OpenAI API reference for this one? Or is this field only supported by DeepSeek currently?

sorry for this wrong description generated by AI code hepler. "reasoning_content" filed is only supported by  deepseek R1 currently.
sashabaranov: Originally, this repository was targeted on the official OpenAI API. But, in the context of the [official client](https://github.com/openai/openai-go) popping up — I'm willing to merge this `reasoning_content` field that OpenAI API might not support.

My only ask here is: can we please clearly indicate that this field is **not** supported by OpenAI? Ideally, it should be obvious from the variable name and from reading the docs. Thank you!
goodenough227: > Originally, this repository was targeted on the official OpenAI API. But, in the context of the [official client](https://github.com/openai/openai-go) popping up — I'm willing to merge this `reasoning_content` field that OpenAI API might not support.
> 
> My only ask here is: can we please clearly indicate that this field is **not** supported by OpenAI? Ideally, it should be obvious from the variable name and from reading the docs. Thank you!

Get it. So what should I do? Could I write a comment after this field?
goodenough227: > Originally, this repository was targeted on the official OpenAI API. But, in the context of the [official client](https://github.com/openai/openai-go) popping up — I'm willing to merge this `reasoning_content` field that OpenAI API might not support.
> 
> My only ask here is: can we please clearly indicate that this field is **not** supported by OpenAI? Ideally, it should be obvious from the variable name and from reading the docs. Thank you!



> Originally, this repository was targeted on the official OpenAI API. But, in the context of the [official client](https://github.com/openai/openai-go) popping up — I'm willing to merge this `reasoning_content` field that OpenAI API might not support.
> 
> My only ask here is: can we please clearly indicate that this field is **not** supported by OpenAI? Ideally, it should be obvious from the variable name and from reading the docs. Thank you!

I have added comments in a similar way as in the previous code, please review
He-hongjun: Hope to merge and support this as soon as possible. Thank you for your contribution! @sashabaranov @goodenough227 
xiaozhi747: mark
agclqq: I am exactly what I want, please approve, thank you @sashabaranov @goodenough227
goodenough227: > I see that [openai/openai-python](https://github.com/openai/openai-python/compare/v1.66.1...v1.66.2) has already supported ResponseReasoning, and I believe it will be supported soon in [official client](https://github.com/openai/openai-go). Whether we use the current PR or build another one, I hope we can support it soon, thank you!
> 
> @sashabaranov @goodenough227 @goodenough227
I believe it would be better to open a separate PR, as ResponseReasoning is a distinct data structure.

agclqq: Any other questions, rush 
Degfy: openAI or closeAI, maybe never support output reasoning content

Patch:
diff --git a/.github/workflows/pr.yml b/.github/workflows/pr.yml
index 818a884..f4cbe7c 100644
--- a/.github/workflows/pr.yml
+++ b/.github/workflows/pr.yml
@@ -22,6 +22,6 @@ jobs:
       with:
         version: v1.64.5
     - name: Run tests
-      run: go test -race -covermode=atomic -coverprofile=coverage.out -v .
+      run: go test -race -covermode=atomic -coverprofile=coverage.out -v ./...
     - name: Upload coverage reports to Codecov
       uses: codecov/codecov-action@v4
diff --git a/chat.go b/chat.go
index 995860c..7112dc7 100644
--- a/chat.go
+++ b/chat.go
@@ -104,6 +104,12 @@ type ChatCompletionMessage struct {
 	// - https://github.com/openai/openai-cookbook/blob/main/examples/How_to_count_tokens_with_tiktoken.ipynb
 	Name string `json:"name,omitempty"`
 
+	// This property is used for the "reasoning" feature supported by deepseek-reasoner
+	// which is not in the official documentation.
+	// the doc from deepseek:
+	// - https://api-docs.deepseek.com/api/create-chat-completion#responses
+	ReasoningContent string `json:"reasoning_content,omitempty"`
+
 	FunctionCall *FunctionCall `json:"function_call,omitempty"`
 
 	// For Role=assistant prompts this may be set to the tool calls generated by the model, such as function calls.
@@ -119,41 +125,44 @@ func (m ChatCompletionMessage) MarshalJSON() ([]byte, error) {
 	}
 	if len(m.MultiContent) > 0 {
 		msg := struct {
-			Role         string            `json:"role"`
-			Content      string            `json:"-"`
-			Refusal      string            `json:"refusal,omitempty"`
-			MultiContent []ChatMessagePart `json:"content,omitempty"`
-			Name         string            `json:"name,omitempty"`
-			FunctionCall *FunctionCall     `json:"function_call,omitempty"`
-			ToolCalls    []ToolCall        `json:"tool_calls,omitempty"`
-			ToolCallID   string            `json:"tool_call_id,omitempty"`
+			Role             string            `json:"role"`
+			Content          string            `json:"-"`
+			Refusal          string            `json:"refusal,omitempty"`
+			MultiContent     []ChatMessagePart `json:"content,omitempty"`
+			Name             string            `json:"name,omitempty"`
+			ReasoningContent string            `json:"reasoning_content,omitempty"`
+			FunctionCall     *FunctionCall     `json:"function_call,omitempty"`
+			ToolCalls        []ToolCall        `json:"tool_calls,omitempty"`
+			ToolCallID       string            `json:"tool_call_id,omitempty"`
 		}(m)
 		return json.Marshal(msg)
 	}
 
 	msg := struct {
-		Role         string            `json:"role"`
-		Content      string            `json:"content,omitempty"`
-		Refusal      string            `json:"refusal,omitempty"`
-		MultiContent []ChatMessagePart `json:"-"`
-		Name         string            `json:"name,omitempty"`
-		FunctionCall *FunctionCall     `json:"function_call,omitempty"`
-		ToolCalls    []ToolCall        `json:"tool_calls,omitempty"`
-		ToolCallID   string            `json:"tool_call_id,omitempty"`
+		Role             string            `json:"role"`
+		Content          string            `json:"content,omitempty"`
+		Refusal          string            `json:"refusal,omitempty"`
+		MultiContent     []ChatMessagePart `json:"-"`
+		Name             string            `json:"name,omitempty"`
+		ReasoningContent string            `json:"reasoning_content,omitempty"`
+		FunctionCall     *FunctionCall     `json:"function_call,omitempty"`
+		ToolCalls        []ToolCall        `json:"tool_calls,omitempty"`
+		ToolCallID       string            `json:"tool_call_id,omitempty"`
 	}(m)
 	return json.Marshal(msg)
 }
 
 func (m *ChatCompletionMessage) UnmarshalJSON(bs []byte) error {
 	msg := struct {
-		Role         string `json:"role"`
-		Content      string `json:"content,omitempty"`
-		Refusal      string `json:"refusal,omitempty"`
-		MultiContent []ChatMessagePart
-		Name         string        `json:"name,omitempty"`
-		FunctionCall *FunctionCall `json:"function_call,omitempty"`
-		ToolCalls    []ToolCall    `json:"tool_calls,omitempty"`
-		ToolCallID   string        `json:"tool_call_id,omitempty"`
+		Role             string `json:"role"`
+		Content          string `json:"content"`
+		Refusal          string `json:"refusal,omitempty"`
+		MultiContent     []ChatMessagePart
+		Name             string        `json:"name,omitempty"`
+		ReasoningContent string        `json:"reasoning_content,omitempty"`
+		FunctionCall     *FunctionCall `json:"function_call,omitempty"`
+		ToolCalls        []ToolCall    `json:"tool_calls,omitempty"`
+		ToolCallID       string        `json:"tool_call_id,omitempty"`
 	}{}
 
 	if err := json.Unmarshal(bs, &msg); err == nil {
@@ -161,14 +170,15 @@ func (m *ChatCompletionMessage) UnmarshalJSON(bs []byte) error {
 		return nil
 	}
 	multiMsg := struct {
-		Role         string `json:"role"`
-		Content      string
-		Refusal      string            `json:"refusal,omitempty"`
-		MultiContent []ChatMessagePart `json:"content"`
-		Name         string            `json:"name,omitempty"`
-		FunctionCall *FunctionCall     `json:"function_call,omitempty"`
-		ToolCalls    []ToolCall        `json:"tool_calls,omitempty"`
-		ToolCallID   string            `json:"tool_call_id,omitempty"`
+		Role             string `json:"role"`
+		Content          string
+		Refusal          string            `json:"refusal,omitempty"`
+		MultiContent     []ChatMessagePart `json:"content"`
+		Name             string            `json:"name,omitempty"`
+		ReasoningContent string            `json:"reasoning_content,omitempty"`
+		FunctionCall     *FunctionCall     `json:"function_call,omitempty"`
+		ToolCalls        []ToolCall        `json:"tool_calls,omitempty"`
+		ToolCallID       string            `json:"tool_call_id,omitempty"`
 	}{}
 	if err := json.Unmarshal(bs, &multiMsg); err != nil {
 		return err
diff --git a/chat_stream.go b/chat_stream.go
index 525b445..80d16cc 100644
--- a/chat_stream.go
+++ b/chat_stream.go
@@ -11,6 +11,12 @@ type ChatCompletionStreamChoiceDelta struct {
 	FunctionCall *FunctionCall `json:"function_call,omitempty"`
 	ToolCalls    []ToolCall    `json:"tool_calls,omitempty"`
 	Refusal      string        `json:"refusal,omitempty"`
+
+	// This property is used for the "reasoning" feature supported by deepseek-reasoner
+	// which is not in the official documentation.
+	// the doc from deepseek:
+	// - https://api-docs.deepseek.com/api/create-chat-completion#responses
+	ReasoningContent string `json:"reasoning_content,omitempty"`
 }
 
 type ChatCompletionStreamChoiceLogprobs struct {
diff --git a/completion.go b/completion.go
index 015fa2a..0d0c1a8 100644
--- a/completion.go
+++ b/completion.go
@@ -37,6 +37,12 @@ const (
 	GPT4TurboPreview        = "gpt-4-turbo-preview"
 	GPT4VisionPreview       = "gpt-4-vision-preview"
 	GPT4                    = "gpt-4"
+	GPT4Dot1                = "gpt-4.1"
+	GPT4Dot120250414        = "gpt-4.1-2025-04-14"
+	GPT4Dot1Mini            = "gpt-4.1-mini"
+	GPT4Dot1Mini20250414    = "gpt-4.1-mini-2025-04-14"
+	GPT4Dot1Nano            = "gpt-4.1-nano"
+	GPT4Dot1Nano20250414    = "gpt-4.1-nano-2025-04-14"
 	GPT4Dot5Preview         = "gpt-4.5-preview"
 	GPT4Dot5Preview20250227 = "gpt-4.5-preview-2025-02-27"
 	GPT3Dot5Turbo0125       = "gpt-3.5-turbo-0125"
@@ -121,6 +127,13 @@ var disabledModelsForEndpoints = map[string]map[string]bool{
 		GPT432K:                 true,
 		GPT432K0314:             true,
 		GPT432K0613:             true,
+		O1:                      true,
+		GPT4Dot1:                true,
+		GPT4Dot120250414:        true,
+		GPT4Dot1Mini:            true,
+		GPT4Dot1Mini20250414:    true,
+		GPT4Dot1Nano:            true,
+		GPT4Dot1Nano20250414:    true,
 	},
 	chatCompletionsSuffix: {
 		CodexCodeDavinci002:     true,
diff --git a/jsonschema/json.go b/jsonschema/json.go
index bcb253f..d458418 100644
--- a/jsonschema/json.go
+++ b/jsonschema/json.go
@@ -46,6 +46,8 @@ type Definition struct {
 	// additionalProperties: false
 	// additionalProperties: jsonschema.Definition{Type: jsonschema.String}
 	AdditionalProperties any `json:"additionalProperties,omitempty"`
+	// Whether the schema is nullable or not.
+	Nullable bool `json:"nullable,omitempty"`
 }
 
 func (d *Definition) MarshalJSON() ([]byte, error) {
@@ -139,6 +141,16 @@ func reflectSchemaObject(t reflect.Type) (*Definition, error) {
 		if description != "" {
 			item.Description = description
 		}
+		enum := field.Tag.Get("enum")
+		if enum != "" {
+			item.Enum = strings.Split(enum, ",")
+		}
+
+		if n := field.Tag.Get("nullable"); n != "" {
+			nullable, _ := strconv.ParseBool(n)
+			item.Nullable = nullable
+		}
+
 		properties[jsonTag] = *item
 
 		if s := field.Tag.Get("required"); s != "" {
diff --git a/speech.go b/speech.go
index 20b52e3..60e7694 100644
--- a/speech.go
+++ b/speech.go
@@ -8,20 +8,25 @@ import (
 type SpeechModel string
 
 const (
-	TTSModel1      SpeechModel = "tts-1"
-	TTSModel1HD    SpeechModel = "tts-1-hd"
-	TTSModelCanary SpeechModel = "canary-tts"
+	TTSModel1         SpeechModel = "tts-1"
+	TTSModel1HD       SpeechModel = "tts-1-hd"
+	TTSModelCanary    SpeechModel = "canary-tts"
+	TTSModelGPT4oMini SpeechModel = "gpt-4o-mini-tts"
 )
 
 type SpeechVoice string
 
 const (
 	VoiceAlloy   SpeechVoice = "alloy"
+	VoiceAsh     SpeechVoice = "ash"
+	VoiceBallad  SpeechVoice = "ballad"
+	VoiceCoral   SpeechVoice = "coral"
 	VoiceEcho    SpeechVoice = "echo"
 	VoiceFable   SpeechVoice = "fable"
 	VoiceOnyx    SpeechVoice = "onyx"
 	VoiceNova    SpeechVoice = "nova"
 	VoiceShimmer SpeechVoice = "shimmer"
+	VoiceVerse   SpeechVoice = "verse"
 )
 
 type SpeechResponseFormat string
@@ -39,6 +44,7 @@ type CreateSpeechRequest struct {
 	Model          SpeechModel          `json:"model"`
 	Input          string               `json:"input"`
 	Voice          SpeechVoice          `json:"voice"`
+	Instructions   string               `json:"instructions,omitempty"`    // Optional, Doesnt work with tts-1 or tts-1-hd.
 	ResponseFormat SpeechResponseFormat `json:"response_format,omitempty"` // Optional, default to mp3
 	Speed          float64              `json:"speed,omitempty"`           // Optional, default to 1.0
 }


Test patch:
diff --git a/chat_test.go b/chat_test.go
index e90142d..514706c 100644
--- a/chat_test.go
+++ b/chat_test.go
@@ -411,6 +411,23 @@ func TestO3ModelChatCompletions(t *testing.T) {
 	checks.NoError(t, err, "CreateChatCompletion error")
 }
 
+func TestDeepseekR1ModelChatCompletions(t *testing.T) {
+	client, server, teardown := setupOpenAITestServer()
+	defer teardown()
+	server.RegisterHandler("/v1/chat/completions", handleDeepseekR1ChatCompletionEndpoint)
+	_, err := client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{
+		Model:               "deepseek-reasoner",
+		MaxCompletionTokens: 100,
+		Messages: []openai.ChatCompletionMessage{
+			{
+				Role:    openai.ChatMessageRoleUser,
+				Content: "Hello!",
+			},
+		},
+	})
+	checks.NoError(t, err, "CreateChatCompletion error")
+}
+
 // TestCompletions Tests the completions endpoint of the API using the mocked server.
 func TestChatCompletionsWithHeaders(t *testing.T) {
 	client, server, teardown := setupOpenAITestServer()
@@ -822,6 +839,68 @@ func handleChatCompletionEndpoint(w http.ResponseWriter, r *http.Request) {
 	fmt.Fprintln(w, string(resBytes))
 }
 
+func handleDeepseekR1ChatCompletionEndpoint(w http.ResponseWriter, r *http.Request) {
+	var err error
+	var resBytes []byte
+
+	// completions only accepts POST requests
+	if r.Method != "POST" {
+		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
+	}
+	var completionReq openai.ChatCompletionRequest
+	if completionReq, err = getChatCompletionBody(r); err != nil {
+		http.Error(w, "could not read request", http.StatusInternalServerError)
+		return
+	}
+	res := openai.ChatCompletionResponse{
+		ID:      strconv.Itoa(int(time.Now().Unix())),
+		Object:  "test-object",
+		Created: time.Now().Unix(),
+		// would be nice to validate Model during testing, but
+		// this may not be possible with how much upkeep
+		// would be required / wouldn't make much sense
+		Model: completionReq.Model,
+	}
+	// create completions
+	n := completionReq.N
+	if n == 0 {
+		n = 1
+	}
+	if completionReq.MaxCompletionTokens == 0 {
+		completionReq.MaxCompletionTokens = 1000
+	}
+	for i := 0; i < n; i++ {
+		reasoningContent := "User says hello! And I need to reply"
+		completionStr := strings.Repeat("a", completionReq.MaxCompletionTokens-numTokens(reasoningContent))
+		res.Choices = append(res.Choices, openai.ChatCompletionChoice{
+			Message: openai.ChatCompletionMessage{
+				Role:             openai.ChatMessageRoleAssistant,
+				ReasoningContent: reasoningContent,
+				Content:          completionStr,
+			},
+			Index: i,
+		})
+	}
+	inputTokens := numTokens(completionReq.Messages[0].Content) * n
+	completionTokens := completionReq.MaxTokens * n
+	res.Usage = openai.Usage{
+		PromptTokens:     inputTokens,
+		CompletionTokens: completionTokens,
+		TotalTokens:      inputTokens + completionTokens,
+	}
+	resBytes, _ = json.Marshal(res)
+	w.Header().Set(xCustomHeader, xCustomHeaderValue)
+	for k, v := range rateLimitHeaders {
+		switch val := v.(type) {
+		case int:
+			w.Header().Set(k, strconv.Itoa(val))
+		default:
+			w.Header().Set(k, fmt.Sprintf("%s", v))
+		}
+	}
+	fmt.Fprintln(w, string(resBytes))
+}
+
 // getChatCompletionBody Returns the body of the request to create a completion.
 func getChatCompletionBody(r *http.Request) (openai.ChatCompletionRequest, error) {
 	completion := openai.ChatCompletionRequest{}
diff --git a/completion_test.go b/completion_test.go
index 935bbe8..83bd899 100644
--- a/completion_test.go
+++ b/completion_test.go
@@ -181,3 +181,86 @@ func getCompletionBody(r *http.Request) (openai.CompletionRequest, error) {
 	}
 	return completion, nil
 }
+
+// TestCompletionWithO1Model Tests that O1 model is not supported for completion endpoint.
+func TestCompletionWithO1Model(t *testing.T) {
+	config := openai.DefaultConfig("whatever")
+	config.BaseURL = "http://localhost/v1"
+	client := openai.NewClientWithConfig(config)
+
+	_, err := client.CreateCompletion(
+		context.Background(),
+		openai.CompletionRequest{
+			MaxTokens: 5,
+			Model:     openai.O1,
+		},
+	)
+	if !errors.Is(err, openai.ErrCompletionUnsupportedModel) {
+		t.Fatalf("CreateCompletion should return ErrCompletionUnsupportedModel for O1 model, but returned: %v", err)
+	}
+}
+
+// TestCompletionWithGPT4DotModels Tests that newer GPT4 models are not supported for completion endpoint.
+func TestCompletionWithGPT4DotModels(t *testing.T) {
+	config := openai.DefaultConfig("whatever")
+	config.BaseURL = "http://localhost/v1"
+	client := openai.NewClientWithConfig(config)
+
+	models := []string{
+		openai.GPT4Dot1,
+		openai.GPT4Dot120250414,
+		openai.GPT4Dot1Mini,
+		openai.GPT4Dot1Mini20250414,
+		openai.GPT4Dot1Nano,
+		openai.GPT4Dot1Nano20250414,
+		openai.GPT4Dot5Preview,
+		openai.GPT4Dot5Preview20250227,
+	}
+
+	for _, model := range models {
+		t.Run(model, func(t *testing.T) {
+			_, err := client.CreateCompletion(
+				context.Background(),
+				openai.CompletionRequest{
+					MaxTokens: 5,
+					Model:     model,
+				},
+			)
+			if !errors.Is(err, openai.ErrCompletionUnsupportedModel) {
+				t.Fatalf("CreateCompletion should return ErrCompletionUnsupportedModel for %s model, but returned: %v", model, err)
+			}
+		})
+	}
+}
+
+// TestCompletionWithGPT4oModels Tests that GPT4o models are not supported for completion endpoint.
+func TestCompletionWithGPT4oModels(t *testing.T) {
+	config := openai.DefaultConfig("whatever")
+	config.BaseURL = "http://localhost/v1"
+	client := openai.NewClientWithConfig(config)
+
+	models := []string{
+		openai.GPT4o,
+		openai.GPT4o20240513,
+		openai.GPT4o20240806,
+		openai.GPT4o20241120,
+		openai.GPT4oLatest,
+		openai.GPT4oMini,
+		openai.GPT4oMini20240718,
+	}
+
+	for _, model := range models {
+		t.Run(model, func(t *testing.T) {
+			_, err := client.CreateCompletion(
+				context.Background(),
+				openai.CompletionRequest{
+					MaxTokens: 5,
+					Model:     model,
+				},
+			)
+			if !errors.Is(err, openai.ErrCompletionUnsupportedModel) {
+				t.Fatalf("CreateCompletion should return ErrCompletionUnsupportedModel for %s model, but returned: %v", model, err)
+			}
+		})
+	}
+}
diff --git a/jsonschema/json_test.go b/jsonschema/json_test.go
index 7447060..17f0aba 100644
--- a/jsonschema/json_test.go
+++ b/jsonschema/json_test.go
@@ -17,7 +17,7 @@ func TestDefinition_MarshalJSON(t *testing.T) {
 		{
 			name: "Test with empty Definition",
 			def:  jsonschema.Definition{},
-			want: `{"properties":{}}`,
+			want: `{}`,
 		},
 		{
 			name: "Test with Definition properties set",
@@ -31,15 +31,14 @@ func TestDefinition_MarshalJSON(t *testing.T) {
 				},
 			},
 			want: `{
-   "type":"string",
-   "description":"A string type",
-   "properties":{
-      "name":{
-         "type":"string",
-         "properties":{}
-      }
-   }
-}`,
+				"type":"string",
+				"description":"A string type",
+				"properties":{
+						"name":{
+							"type":"string"
+						}
+				}
+			}`,
 		},
 		{
 			name: "Test with nested Definition properties",
@@ -60,23 +59,21 @@ func TestDefinition_MarshalJSON(t *testing.T) {
 				},
 			},
 			want: `{
-   "type":"object",
-   "properties":{
-      "user":{
-         "type":"object",
-         "properties":{
-            "name":{
-               "type":"string",
-               "properties":{}
-            },
-            "age":{
-               "type":"integer",
-               "properties":{}
-            }
-         }
-      }
-   }
-}`,
+				"type":"object",
+				"properties":{
+						"user":{
+							"type":"object",
+							"properties":{
+									"name":{
+										"type":"string"
+									},
+									"age":{
+										"type":"integer"
+									}
+							}
+						}
+				}
+			}`,
 		},
 		{
 			name: "Test with complex nested Definition",
@@ -108,36 +105,32 @@ func TestDefinition_MarshalJSON(t *testing.T) {
 				},
 			},
 			want: `{
-   "type":"object",
-   "properties":{
-      "user":{
-         "type":"object",
-         "properties":{
-            "name":{
-               "type":"string",
-               "properties":{}
-            },
-            "age":{
-               "type":"integer",
-               "properties":{}
-            },
-            "address":{
-               "type":"object",
-               "properties":{
-                  "city":{
-                     "type":"string",
-                     "properties":{}
-                  },
-                  "country":{
-                     "type":"string",
-                     "properties":{}
-                  }
-               }
-            }
-         }
-      }
-   }
-}`,
+				"type":"object",
+				"properties":{
+						"user":{
+							"type":"object",
+							"properties":{
+									"name":{
+										"type":"string"
+									},
+									"age":{
+										"type":"integer"
+									},
+									"address":{
+										"type":"object",
+										"properties":{
+												"city":{
+													"type":"string"
+												},
+												"country":{
+													"type":"string"
+												}
+										}
+									}
+							}
+						}
+				}
+			}`,
 		},
 		{
 			name: "Test with Array type Definition",
@@ -153,20 +146,16 @@ func TestDefinition_MarshalJSON(t *testing.T) {
 				},
 			},
 			want: `{
-   "type":"array",
-   "items":{
-      "type":"string",
-      "properties":{
-         
-      }
-   },
-   "properties":{
-      "name":{
-         "type":"string",
-         "properties":{}
-      }
-   }
-}`,
+				"type":"array",
+				"items":{
+						"type":"string"
+				},
+				"properties":{
+						"name":{
+							"type":"string"
+						}
+				}
+			}`,
 		},
 	}
 
@@ -193,6 +182,185 @@ func TestDefinition_MarshalJSON(t *testing.T) {
 	}
 }
 
+func TestStructToSchema(t *testing.T) {
+	tests := []struct {
+		name string
+		in   any
+		want string
+	}{
+		{
+			name: "Test with empty struct",
+			in:   struct{}{},
+			want: `{
+				"type":"object",
+				"additionalProperties":false
+			}`,
+		},
+		{
+			name: "Test with struct containing many fields",
+			in: struct {
+				Name   string  `json:"name"`
+				Age    int     `json:"age"`
+				Active bool    `json:"active"`
+				Height float64 `json:"height"`
+				Cities []struct {
+					Name  string `json:"name"`
+					State string `json:"state"`
+				} `json:"cities"`
+			}{
+				Name: "John Doe",
+				Age:  30,
+				Cities: []struct {
+					Name  string `json:"name"`
+					State string `json:"state"`
+				}{
+					{Name: "New York", State: "NY"},
+					{Name: "Los Angeles", State: "CA"},
+				},
+			},
+			want: `{
+				"type":"object",
+				"properties":{
+					"name":{
+						"type":"string"
+					},
+					"age":{
+						"type":"integer"
+					},
+					"active":{
+						"type":"boolean"
+					},
+					"height":{
+						"type":"number"
+					},
+					"cities":{
+						"type":"array",
+						"items":{
+							"additionalProperties":false,
+							"type":"object",
+							"properties":{
+								"name":{
+									"type":"string"
+								},
+								"state":{
+									"type":"string"
+								}
+							},
+							"required":["name","state"]
+						}
+					}
+				},
+				"required":["name","age","active","height","cities"],
+				"additionalProperties":false
+			}`,
+		},
+		{
+			name: "Test with description tag",
+			in: struct {
+				Name string `json:"name" description:"The name of the person"`
+			}{
+				Name: "John Doe",
+			},
+			want: `{
+				"type":"object",
+				"properties":{
+					"name":{
+						"type":"string",
+						"description":"The name of the person"
+					}
+				},
+				"required":["name"],
+				"additionalProperties":false
+			}`,
+		},
+		{
+			name: "Test with required tag",
+			in: struct {
+				Name string `json:"name" required:"false"`
+			}{
+				Name: "John Doe",
+			},
+			want: `{
+				"type":"object",
+				"properties":{
+					"name":{
+						"type":"string"
+					}
+				},
+				"additionalProperties":false
+			}`,
+		},
+		{
+			name: "Test with enum tag",
+			in: struct {
+				Color string `json:"color" enum:"red,green,blue"`
+			}{
+				Color: "red",
+			},
+			want: `{
+				"type":"object",
+				"properties":{
+					"color":{
+						"type":"string",
+						"enum":["red","green","blue"]
+					}
+				},
+				"required":["color"],
+				"additionalProperties":false
+			}`,
+		},
+		{
+			name: "Test with nullable tag",
+			in: struct {
+				Name *string `json:"name" nullable:"true"`
+			}{
+				Name: nil,
+			},
+			want: `{
+
+				"type":"object",
+				"properties":{
+					"name":{
+						"type":"string",
+						"nullable":true
+					}
+				},
+				"required":["name"],
+				"additionalProperties":false
+			}`,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			wantBytes := []byte(tt.want)
+
+			schema, err := jsonschema.GenerateSchemaForType(tt.in)
+			if err != nil {
+				t.Errorf("Failed to generate schema: error = %v", err)
+				return
+			}
+
+			var want map[string]interface{}
+			err = json.Unmarshal(wantBytes, &want)
+			if err != nil {
+				t.Errorf("Failed to Unmarshal JSON: error = %v", err)
+				return
+			}
+
+			got := structToMap(t, schema)
+			gotPtr := structToMap(t, &schema)
+
+			if !reflect.DeepEqual(got, want) {
+				t.Errorf("MarshalJSON() got = %v, want %v", got, want)
+			}
+			if !reflect.DeepEqual(gotPtr, want) {
+				t.Errorf("MarshalJSON() gotPtr = %v, want %v", gotPtr, want)
+			}
+		})
+	}
+}
+
 func structToMap(t *testing.T, v any) map[string]any {
 	t.Helper()
 	gotBytes, err := json.Marshal(v)
diff --git a/openai_test.go b/openai_test.go
index 6c26eeb..a55f3a8 100644
--- a/openai_test.go
+++ b/openai_test.go
@@ -29,7 +29,7 @@ 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).
 func numTokens(s string) int {