Repository: sashabaranov/go-openai
Pull request #958 (2025-03-26 05:35:31)

feat: Add missing TTS models and voices
**Describe the change**
This adds the new OpenAI TTS model, includes a new instructions field, and adds missing speech voices.

**Provide OpenAI documentation link**
https://platform.openai.com/docs/api-reference/audio

**Describe your solution**
- Add `TTSModelGPT4oMini` to `SpeechModel` type
- Adds missing voices to `SpeechVoice` type
- Added an optional `instructions` field to the `CreateSpeechRequest` object, specific to the `gpt-4o-mini-tts` model.

**Tests**
No test changes were required. The `SpeechModel` and `SpeechVoice` types were already defined, and the `instructions` field is omitted when empty.

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

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


```diff
@@            Coverage Diff             @@
##           master     #958      +/-   ##
==========================================
+ Coverage   98.46%   98.88%   +0.41%     
==========================================
  Files          24       27       +3     
  Lines        1364     1786     +422     
==========================================
+ Hits         1343     1766     +423     
+ Misses         15       14       -1     
  Partials        6        6              
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/sashabaranov/go-openai/pull/958?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).

<details><summary> :rocket: New features to boost your workflow: </summary>

- :snowflake: [Test Analytics](https://docs.codecov.com/docs/test-analytics): Detect flaky tests, report on failures, and find test suite problems.
</details>

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/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/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)