Repository: sashabaranov/go-openai
Pull request #819 (2024-08-07 16:13:46)

Improve handling of JSON Schema in OpenAI API Response Context
* add: jsonschema.Unmarshal
* add: jsonschema.Validate
* add: jsonschema.GenerateSchemaForType
* update: openai.ChatCompletionResponseFormatJSONSchema


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

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


```diff
@@            Coverage Diff             @@
##           master     #819      +/-   ##
==========================================
+ Coverage   98.46%   98.88%   +0.42%     
==========================================
  Files          24       26       +2     
  Lines        1364     1347      -17     
==========================================
- Hits         1343     1332      -11     
+ 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/819?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).

gspeicher: If this PR is not going to be finished soon, would it be possible to lift the small change to `openai.ChatCompletionResponseFormatJSONSchema` promoting `Schema` from `jsonschema.Definition` to `json.Marshaler` as a separate PR? The requirement to use `jsonschema` is currently a roadblock for myself and others who use this package, as evidenced by PR #824.

Also, I feel that using `json.Marshaler` is both cleaner and more in the spirit of Go than adding a new `SchemaRaw` field side-by-side with `Schema` as proposed by that PR. If you already have a schema in string form then you can simply wrap it in a custom type that satisfies `json.Marshaler` like so:

```
// MySchema is a custom type that wraps a string
type MySchema string

// MarshalJSON implements the json.Marshaler interface for MySchema
func (m MySchema) MarshalJSON() ([]byte, error) {
	return []byte(m), nil
}
```
gspeicher: @eiixy thanks for pulling this together quickly. My feedback is centered around `GenerateSchemaForType`.

1. The name feels a little misleading to me, because it implies that you would pass the type corresponding to the schema you want to generate, but instead it expects an object of that type. Seems like either the function name or the parameter type should change so they agree. I defer to @sashabaranov since the name was his suggestion and it's his package.
2. Do we still need `SchemaWrapper` and its `Wrap` method if we have `GenerateSchemaForType`? It's not clear to me when you would use `SchemaWrapper` and it feels like an unnecessarily abstraction of the native JSON package.
eiixy: > @eiixy thanks for pulling this together quickly. My feedback is centered around `GenerateSchemaForType`.
> 
> 1. The name feels a little misleading to me, because it implies that you would pass the type corresponding to the schema you want to generate, but instead it expects an object of that type. Seems like either the function name or the parameter type should change so they agree. I defer to @sashabaranov since the name was his suggestion and it's his package.
> 2. Do we still need `SchemaWrapper` and its `Wrap` method if we have `GenerateSchemaForType`? It's not clear to me when you would use `SchemaWrapper` and it feels like an unnecessarily abstraction of the native JSON package.


`jsonschema.Wrap` is indeed not necessary; it's just an alternative way of doing it. You can refer to the example below.
```go
type Result struct {
	Steps []struct {
		Explanation string `json:"explanation"`
		Output      string `json:"output"`
	} `json:"steps"`
	FinalAnswer string `json:"final_answer"`
}
sw, err := jsonschema.Wrap(Result{})
if err != nil {
	log.Fatalf("JSONSchema Wrap error: %v", err)
}
resp, err := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
	Model: openai.GPT4oMini,
	Messages: []openai.ChatCompletionMessage{
		{
			Role:    openai.ChatMessageRoleSystem,
			Content: "You are a helpful math tutor. Guide the user through the solution step by step.",
		},
		{
			Role:    openai.ChatMessageRoleUser,
			Content: "how can I solve 8x + 7 = -23",
		},
	},
	ResponseFormat: &openai.ChatCompletionResponseFormat{
		Type: openai.ChatCompletionResponseFormatTypeJSONSchema,
		JSONSchema: &openai.ChatCompletionResponseFormatJSONSchema{
			Name:   "math_reasoning",
			Schema: sw,
			Strict: true,
		},
	},
})
if err != nil {
	log.Fatalf("CreateChatCompletion error: %v", err)
}
result, err := sw.Unmarshal(resp.Choices[0].Message.Content)
if err != nil {
	log.Fatalf("Unmarshal error: %v", err)
}
```
gspeicher: Taking your example and converting it to use `GenerateSchemaForType`, it seems like the only difference would be these two lines:
```
sw, err := jsonschema.Wrap(Result{})
// check for errors and call the API
result, err := sw.Unmarshal(resp.Choices[0].Message.Content)
```
would become:
```
var result Result
sw, err := jsonschema.GenerateSchemaForType(result)
// check for errors and call the API
err := json.Unmarshal([]byte(resp.Choices[0].Message.Content), &result)
```
Again, this is not my library so I defer to @sashabaranov but I personally don't see the point in including that alternative since the standard `encoding/json` library is already simple and ubiquitous, not to mention the complexity and cognitive load are effectively the same between the two options. Overall, adding this as an alternative actually adds unnecessary complexity to the package.

Having said that, I'm chomping at the bit to see this PR get merged and start using Structured Output so I'm not going to die on this hill.
eiixy: > Taking your example and converting it to use `GenerateSchemaForType`, it seems like the only difference would be these two lines:
> 
> ```
> sw, err := jsonschema.Wrap(Result{})
> // check for errors and call the API
> result, err := sw.Unmarshal(resp.Choices[0].Message.Content)
> ```
> 
> would become:
> 
> ```
> var result Result
> sw, err := jsonschema.GenerateSchemaForType(result)
> // check for errors and call the API
> err := json.Unmarshal([]byte(resp.Choices[0].Message.Content), &result)
> ```
> 
> Again, this is not my library so I defer to @sashabaranov but I personally don't see the point in including that alternative since the standard `encoding/json` library is already simple and ubiquitous, not to mention the complexity and cognitive load are effectively the same between the two options. Overall, adding this as an alternative actually adds unnecessary complexity to the package.
> 
> Having said that, I'm chomping at the bit to see this PR get merged and start using Structured Output so I'm not going to die on this hill.

I've removed the `jsonschema.SchemaWrapper`. Thanks for your suggestion! 

Patch:
diff --git a/README.md b/README.md
index 799dc60..0d6aafa 100644
--- a/README.md
+++ b/README.md
@@ -743,6 +743,70 @@ func main() {
 }
 ```
 </details>
+
+<details>
+<summary>Structured Outputs</summary>
+
+```go
+package main
+
+import (
+	"context"
+	"fmt"
+	"log"
+
+	"github.com/sashabaranov/go-openai"
+	"github.com/sashabaranov/go-openai/jsonschema"
+)
+
+func main() {
+	client := openai.NewClient("your token")
+	ctx := context.Background()
+
+	type Result struct {
+		Steps []struct {
+			Explanation string `json:"explanation"`
+			Output      string `json:"output"`
+		} `json:"steps"`
+		FinalAnswer string `json:"final_answer"`
+	}
+	var result Result
+	schema, err := jsonschema.GenerateSchemaForType(result)
+	if err != nil {
+		log.Fatalf("GenerateSchemaForType error: %v", err)
+	}
+	resp, err := client.CreateChatCompletion(ctx, openai.ChatCompletionRequest{
+		Model: openai.GPT4oMini,
+		Messages: []openai.ChatCompletionMessage{
+			{
+				Role:    openai.ChatMessageRoleSystem,
+				Content: "You are a helpful math tutor. Guide the user through the solution step by step.",
+			},
+			{
+				Role:    openai.ChatMessageRoleUser,
+				Content: "how can I solve 8x + 7 = -23",
+			},
+		},
+		ResponseFormat: &openai.ChatCompletionResponseFormat{
+			Type: openai.ChatCompletionResponseFormatTypeJSONSchema,
+			JSONSchema: &openai.ChatCompletionResponseFormatJSONSchema{
+				Name:   "math_reasoning",
+				Schema: schema,
+				Strict: true,
+			},
+		},
+	})
+	if err != nil {
+		log.Fatalf("CreateChatCompletion error: %v", err)
+	}
+	err = schema.Unmarshal(resp.Choices[0].Message.Content, &result)
+	if err != nil {
+		log.Fatalf("Unmarshal schema error: %v", err)
+	}
+	fmt.Println(result)
+}
+```
+</details>
 See the `examples/` folder for more.
 
 ## Frequently Asked Questions
diff --git a/audio.go b/audio.go
index dbc26d1..f321f93 100644
--- a/audio.go
+++ b/audio.go
@@ -122,8 +122,13 @@ func (c *Client) callAudioAPI(
 	}
 
 	urlSuffix := fmt.Sprintf("/audio/%s", endpointSuffix)
-	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix, request.Model),
-		withBody(&formBody), withContentType(builder.FormDataContentType()))
+	req, err := c.newRequest(
+		ctx,
+		http.MethodPost,
+		c.fullURL(urlSuffix, withModel(request.Model)),
+		withBody(&formBody),
+		withContentType(builder.FormDataContentType()),
+	)
 	if err != nil {
 		return AudioResponse{}, err
 	}
diff --git a/chat.go b/chat.go
index 31fa887..56e99a7 100644
--- a/chat.go
+++ b/chat.go
@@ -5,8 +5,6 @@ import (
 	"encoding/json"
 	"errors"
 	"net/http"
-
-	"github.com/sashabaranov/go-openai/jsonschema"
 )
 
 // Chat message role defined by the OpenAI API.
@@ -187,10 +185,10 @@ type ChatCompletionResponseFormat struct {
 }
 
 type ChatCompletionResponseFormatJSONSchema struct {
-	Name        string                `json:"name"`
-	Description string                `json:"description,omitempty"`
-	Schema      jsonschema.Definition `json:"schema"`
-	Strict      bool                  `json:"strict"`
+	Name        string         `json:"name"`
+	Description string         `json:"description,omitempty"`
+	Schema      json.Marshaler `json:"schema"`
+	Strict      bool           `json:"strict"`
 }
 
 // ChatCompletionRequest represents a request structure for chat completion API.
@@ -264,6 +262,7 @@ type ToolFunction struct {
 type FunctionDefinition struct {
 	Name        string `json:"name"`
 	Description string `json:"description,omitempty"`
+	Strict      bool   `json:"strict,omitempty"`
 	// Parameters is an object describing the function.
 	// You can pass json.RawMessage to describe the schema,
 	// or you can pass in a struct which serializes to the proper JSON schema.
@@ -358,7 +357,12 @@ func (c *Client) CreateChatCompletion(
 		return
 	}
 
-	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix, request.Model), withBody(request))
+	req, err := c.newRequest(
+		ctx,
+		http.MethodPost,
+		c.fullURL(urlSuffix, withModel(request.Model)),
+		withBody(request),
+	)
 	if err != nil {
 		return
 	}
diff --git a/chat_stream.go b/chat_stream.go
index ffd512f..3f90bc0 100644
--- a/chat_stream.go
+++ b/chat_stream.go
@@ -60,7 +60,12 @@ func (c *Client) CreateChatCompletionStream(
 	}
 
 	request.Stream = true
-	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix, request.Model), withBody(request))
+	req, err := c.newRequest(
+		ctx,
+		http.MethodPost,
+		c.fullURL(urlSuffix, withModel(request.Model)),
+		withBody(request),
+	)
 	if err != nil {
 		return nil, err
 	}
diff --git a/client.go b/client.go
index d5d555c..9f547e7 100644
--- a/client.go
+++ b/client.go
@@ -222,42 +222,66 @@ func decodeString(body io.Reader, output *string) error {
 	return nil
 }
 
+type fullURLOptions struct {
+	model string
+}
+
+type fullURLOption func(*fullURLOptions)
+
+func withModel(model string) fullURLOption {
+	return func(args *fullURLOptions) {
+		args.model = model
+	}
+}
+
+var azureDeploymentsEndpoints = []string{
+	"/completions",
+	"/embeddings",
+	"/chat/completions",
+	"/audio/transcriptions",
+	"/audio/translations",
+	"/audio/speech",
+	"/images/generations",
+}
+
 // fullURL returns full URL for request.
-// args[0] is model name, if API type is Azure, model name is required to get deployment name.
-func (c *Client) fullURL(suffix string, args ...any) string {
-	// /openai/deployments/{model}/chat/completions?api-version={api_version}
+func (c *Client) fullURL(suffix string, setters ...fullURLOption) string {
+	baseURL := strings.TrimRight(c.config.BaseURL, "/")
+	args := fullURLOptions{}
+	for _, setter := range setters {
+		setter(&args)
+	}
+
 	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?%s", baseURL, azureAPIPrefix, suffix, query.Encode())
-		}
-		azureDeploymentName := "UNKNOWN"
-		if len(args) > 0 {
-			model, ok := args[0].(string)
-			if ok {
-				azureDeploymentName = c.config.GetAzureDeploymentByModel(model)
-			}
-		}
-		return fmt.Sprintf("%s/%s/%s/%s%s?%s",
-			baseURL, azureAPIPrefix, azureDeploymentsPrefix,
-			azureDeploymentName, suffix, query.Encode(),
-		)
+		baseURL = c.baseURLWithAzureDeployment(baseURL, suffix, args.model)
+	}
+
+	if c.config.APIVersion != "" {
+		suffix = c.suffixWithAPIVersion(suffix)
 	}
+	return fmt.Sprintf("%s%s", baseURL, suffix)
+}
 
-	// https://developers.cloudflare.com/ai-gateway/providers/azureopenai/
-	if c.config.APIType == APITypeCloudflareAzure {
-		baseURL := c.config.BaseURL
-		baseURL = strings.TrimRight(baseURL, "/")
-		return fmt.Sprintf("%s%s?api-version=%s", baseURL, suffix, c.config.APIVersion)
+func (c *Client) suffixWithAPIVersion(suffix string) string {
+	parsedSuffix, err := url.Parse(suffix)
+	if err != nil {
+		panic("failed to parse url suffix")
 	}
+	query := parsedSuffix.Query()
+	query.Add("api-version", c.config.APIVersion)
+	return fmt.Sprintf("%s?%s", parsedSuffix.Path, query.Encode())
+}
 
-	return fmt.Sprintf("%s%s", c.config.BaseURL, suffix)
+func (c *Client) baseURLWithAzureDeployment(baseURL, suffix, model string) (newBaseURL string) {
+	baseURL = fmt.Sprintf("%s/%s", strings.TrimRight(baseURL, "/"), azureAPIPrefix)
+	if containsSubstr(azureDeploymentsEndpoints, suffix) {
+		azureDeploymentName := c.config.GetAzureDeploymentByModel(model)
+		if azureDeploymentName == "" {
+			azureDeploymentName = "UNKNOWN"
+		}
+		baseURL = fmt.Sprintf("%s/%s/%s", baseURL, azureDeploymentsPrefix, azureDeploymentName)
+	}
+	return baseURL
 }
 
 func (c *Client) handleErrorResp(resp *http.Response) error {
diff --git a/completion.go b/completion.go
index bc2a637..e8e9242 100644
--- a/completion.go
+++ b/completion.go
@@ -213,7 +213,12 @@ func (c *Client) CreateCompletion(
 		return
 	}
 
-	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix, request.Model), withBody(request))
+	req, err := c.newRequest(
+		ctx,
+		http.MethodPost,
+		c.fullURL(urlSuffix, withModel(request.Model)),
+		withBody(request),
+	)
 	if err != nil {
 		return
 	}
diff --git a/config.go b/config.go
index 1347567..8a91835 100644
--- a/config.go
+++ b/config.go
@@ -26,6 +26,10 @@ const AzureAPIKeyHeader = "api-key"
 
 const defaultAssistantVersion = "v2" // upgrade to v2 to support vector store
 
+type HTTPDoer interface {
+	Do(req *http.Request) (*http.Response, error)
+}
+
 // ClientConfig is a configuration of a client.
 type ClientConfig struct {
 	authToken string
@@ -36,7 +40,7 @@ type ClientConfig struct {
 	APIVersion           string // required when APIType is APITypeAzure or APITypeAzureAD
 	AssistantVersion     string
 	AzureModelMapperFunc func(model string) string // replace model to azure deployment name func
-	HTTPClient           *http.Client
+	HTTPClient           HTTPDoer
 
 	EmptyMessagesLimit uint
 }
diff --git a/edits.go b/edits.go
index 97d0260..fe8ecd0 100644
--- a/edits.go
+++ b/edits.go
@@ -38,7 +38,12 @@ will need to migrate to GPT-3.5 Turbo by January 4, 2024.
 You can use CreateChatCompletion or CreateChatCompletionStream instead.
 */
 func (c *Client) Edits(ctx context.Context, request EditsRequest) (response EditsResponse, err error) {
-	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/edits", fmt.Sprint(request.Model)), withBody(request))
+	req, err := c.newRequest(
+		ctx,
+		http.MethodPost,
+		c.fullURL("/edits", withModel(fmt.Sprint(request.Model))),
+		withBody(request),
+	)
 	if err != nil {
 		return
 	}
diff --git a/embeddings.go b/embeddings.go
index b513ba6..74eb8aa 100644
--- a/embeddings.go
+++ b/embeddings.go
@@ -241,7 +241,12 @@ func (c *Client) CreateEmbeddings(
 	conv EmbeddingRequestConverter,
 ) (res EmbeddingResponse, err error) {
 	baseReq := conv.Convert()
-	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/embeddings", string(baseReq.Model)), withBody(baseReq))
+	req, err := c.newRequest(
+		ctx,
+		http.MethodPost,
+		c.fullURL("/embeddings", withModel(string(baseReq.Model))),
+		withBody(baseReq),
+	)
 	if err != nil {
 		return
 	}
diff --git a/fine_tunes.go b/fine_tunes.go
index ca84078..74b47bf 100644
--- a/fine_tunes.go
+++ b/fine_tunes.go
@@ -115,7 +115,7 @@ func (c *Client) CreateFineTune(ctx context.Context, request FineTuneRequest) (r
 // This API will be officially deprecated on January 4th, 2024.
 // OpenAI recommends to migrate to the new fine tuning API implemented in fine_tuning_job.go.
 func (c *Client) CancelFineTune(ctx context.Context, fineTuneID string) (response FineTune, err error) {
-	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/fine-tunes/"+fineTuneID+"/cancel"))
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/fine-tunes/"+fineTuneID+"/cancel")) //nolint:lll //this method is deprecated
 	if err != nil {
 		return
 	}
diff --git a/image.go b/image.go
index 665de1a..577d7db 100644
--- a/image.go
+++ b/image.go
@@ -68,7 +68,12 @@ type ImageResponseDataInner struct {
 // CreateImage - API call to create an image. This is the main endpoint of the DALL-E API.
 func (c *Client) CreateImage(ctx context.Context, request ImageRequest) (response ImageResponse, err error) {
 	urlSuffix := "/images/generations"
-	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix, request.Model), withBody(request))
+	req, err := c.newRequest(
+		ctx,
+		http.MethodPost,
+		c.fullURL(urlSuffix, withModel(request.Model)),
+		withBody(request),
+	)
 	if err != nil {
 		return
 	}
@@ -132,8 +137,13 @@ func (c *Client) CreateEditImage(ctx context.Context, request ImageEditRequest)
 		return
 	}
 
-	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/images/edits", request.Model),
-		withBody(body), withContentType(builder.FormDataContentType()))
+	req, err := c.newRequest(
+		ctx,
+		http.MethodPost,
+		c.fullURL("/images/edits", withModel(request.Model)),
+		withBody(body),
+		withContentType(builder.FormDataContentType()),
+	)
 	if err != nil {
 		return
 	}
@@ -183,8 +193,13 @@ func (c *Client) CreateVariImage(ctx context.Context, request ImageVariRequest)
 		return
 	}
 
-	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/images/variations", request.Model),
-		withBody(body), withContentType(builder.FormDataContentType()))
+	req, err := c.newRequest(
+		ctx,
+		http.MethodPost,
+		c.fullURL("/images/variations", withModel(request.Model)),
+		withBody(body),
+		withContentType(builder.FormDataContentType()),
+	)
 	if err != nil {
 		return
 	}
diff --git a/jsonschema/json.go b/jsonschema/json.go
index 7fd1e11..bcb253f 100644
--- a/jsonschema/json.go
+++ b/jsonschema/json.go
@@ -4,7 +4,13 @@
 // and/or pass in the schema in []byte format.
 package jsonschema
 
-import "encoding/json"
+import (
+	"encoding/json"
+	"fmt"
+	"reflect"
+	"strconv"
+	"strings"
+)
 
 type DataType string
 
@@ -42,7 +48,7 @@ type Definition struct {
 	AdditionalProperties any `json:"additionalProperties,omitempty"`
 }
 
-func (d Definition) MarshalJSON() ([]byte, error) {
+func (d *Definition) MarshalJSON() ([]byte, error) {
 	if d.Properties == nil {
 		d.Properties = make(map[string]Definition)
 	}
@@ -50,6 +56,99 @@ func (d Definition) MarshalJSON() ([]byte, error) {
 	return json.Marshal(struct {
 		Alias
 	}{
-		Alias: (Alias)(d),
+		Alias: (Alias)(*d),
 	})
 }
+
+func (d *Definition) Unmarshal(content string, v any) error {
+	return VerifySchemaAndUnmarshal(*d, []byte(content), v)
+}
+
+func GenerateSchemaForType(v any) (*Definition, error) {
+	return reflectSchema(reflect.TypeOf(v))
+}
+
+func reflectSchema(t reflect.Type) (*Definition, error) {
+	var d Definition
+	switch t.Kind() {
+	case reflect.String:
+		d.Type = String
+	case reflect.Int, reflect.Int8, reflect.Int16, reflect.Int32, reflect.Int64,
+		reflect.Uint, reflect.Uint8, reflect.Uint16, reflect.Uint32, reflect.Uint64:
+		d.Type = Integer
+	case reflect.Float32, reflect.Float64:
+		d.Type = Number
+	case reflect.Bool:
+		d.Type = Boolean
+	case reflect.Slice, reflect.Array:
+		d.Type = Array
+		items, err := reflectSchema(t.Elem())
+		if err != nil {
+			return nil, err
+		}
+		d.Items = items
+	case reflect.Struct:
+		d.Type = Object
+		d.AdditionalProperties = false
+		object, err := reflectSchemaObject(t)
+		if err != nil {
+			return nil, err
+		}
+		d = *object
+	case reflect.Ptr:
+		definition, err := reflectSchema(t.Elem())
+		if err != nil {
+			return nil, err
+		}
+		d = *definition
+	case reflect.Invalid, reflect.Uintptr, reflect.Complex64, reflect.Complex128,
+		reflect.Chan, reflect.Func, reflect.Interface, reflect.Map,
+		reflect.UnsafePointer:
+		return nil, fmt.Errorf("unsupported type: %s", t.Kind().String())
+	default:
+	}
+	return &d, nil
+}
+
+func reflectSchemaObject(t reflect.Type) (*Definition, error) {
+	var d = Definition{
+		Type:                 Object,
+		AdditionalProperties: false,
+	}
+	properties := make(map[string]Definition)
+	var requiredFields []string
+	for i := 0; i < t.NumField(); i++ {
+		field := t.Field(i)
+		if !field.IsExported() {
+			continue
+		}
+		jsonTag := field.Tag.Get("json")
+		var required = true
+		if jsonTag == "" {
+			jsonTag = field.Name
+		} else if strings.HasSuffix(jsonTag, ",omitempty") {
+			jsonTag = strings.TrimSuffix(jsonTag, ",omitempty")
+			required = false
+		}
+
+		item, err := reflectSchema(field.Type)
+		if err != nil {
+			return nil, err
+		}
+		description := field.Tag.Get("description")
+		if description != "" {
+			item.Description = description
+		}
+		properties[jsonTag] = *item
+
+		if s := field.Tag.Get("required"); s != "" {
+			required, _ = strconv.ParseBool(s)
+		}
+		if required {
+			requiredFields = append(requiredFields, jsonTag)
+		}
+	}
+	d.Required = requiredFields
+	d.Properties = properties
+	return &d, nil
+}
diff --git a/jsonschema/validate.go b/jsonschema/validate.go
new file mode 100644
index 0000000..f14ffd4
--- /dev/null
+++ b/jsonschema/validate.go
@@ -0,0 +1,89 @@
+package jsonschema
+
+import (
+	"encoding/json"
+	"errors"
+)
+
+func VerifySchemaAndUnmarshal(schema Definition, content []byte, v any) error {
+	var data any
+	err := json.Unmarshal(content, &data)
+	if err != nil {
+		return err
+	}
+	if !Validate(schema, data) {
+		return errors.New("data validation failed against the provided schema")
+	}
+	return json.Unmarshal(content, &v)
+}
+
+func Validate(schema Definition, data any) bool {
+	switch schema.Type {
+	case Object:
+		return validateObject(schema, data)
+	case Array:
+		return validateArray(schema, data)
+	case String:
+		_, ok := data.(string)
+		return ok
+	case Number: // float64 and int
+		_, ok := data.(float64)
+		if !ok {
+			_, ok = data.(int)
+		}
+		return ok
+	case Boolean:
+		_, ok := data.(bool)
+		return ok
+	case Integer:
+		_, ok := data.(int)
+		return ok
+	case Null:
+		return data == nil
+	default:
+		return false
+	}
+}
+
+func validateObject(schema Definition, data any) bool {
+	dataMap, ok := data.(map[string]any)
+	if !ok {
+		return false
+	}
+	for _, field := range schema.Required {
+		if _, exists := dataMap[field]; !exists {
+			return false
+		}
+	}
+	for key, valueSchema := range schema.Properties {
+		value, exists := dataMap[key]
+		if exists && !Validate(valueSchema, value) {
+			return false
+		} else if !exists && contains(schema.Required, key) {
+			return false
+		}
+	}
+	return true
+}
+
+func validateArray(schema Definition, data any) bool {
+	dataArray, ok := data.([]any)
+	if !ok {
+		return false
+	}
+	for _, item := range dataArray {
+		if !Validate(*schema.Items, item) {
+			return false
+		}
+	}
+	return true
+}
+
+func contains[S ~[]E, E comparable](s S, v E) bool {
+	for i := range s {
+		if v == s[i] {
+			return true
+		}
+	}
+	return false
+}
diff --git a/messages.go b/messages.go
index 6af1184..1fddd63 100644
--- a/messages.go
+++ b/messages.go
@@ -73,6 +73,14 @@ type MessageFilesList struct {
 	httpHeader
 }
 
+type MessageDeletionStatus struct {
+	ID      string `json:"id"`
+	Object  string `json:"object"`
+	Deleted bool   `json:"deleted"`
+
+	httpHeader
+}
+
 // CreateMessage creates a new message.
 func (c *Client) CreateMessage(ctx context.Context, threadID string, request MessageRequest) (msg Message, err error) {
 	urlSuffix := fmt.Sprintf("/threads/%s/%s", threadID, messagesSuffix)
@@ -186,3 +194,19 @@ func (c *Client) ListMessageFiles(
 	err = c.sendRequest(req, &files)
 	return
 }
+
+// DeleteMessage deletes a message..
+func (c *Client) DeleteMessage(
+	ctx context.Context,
+	threadID, messageID string,
+) (status MessageDeletionStatus, err error) {
+	urlSuffix := fmt.Sprintf("/threads/%s/%s/%s", threadID, messagesSuffix, messageID)
+	req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL(urlSuffix),
+		withBetaAssistantVersion(c.config.AssistantVersion))
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &status)
+	return
+}
diff --git a/moderation.go b/moderation.go
index ae285ef..c8652ef 100644
--- a/moderation.go
+++ b/moderation.go
@@ -88,7 +88,12 @@ func (c *Client) Moderations(ctx context.Context, request ModerationRequest) (re
 		err = ErrModerationInvalidModel
 		return
 	}
-	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/moderations", request.Model), withBody(&request))
+	req, err := c.newRequest(
+		ctx,
+		http.MethodPost,
+		c.fullURL("/moderations", withModel(request.Model)),
+		withBody(&request),
+	)
 	if err != nil {
 		return
 	}
diff --git a/speech.go b/speech.go
index 19b21bd..20b52e3 100644
--- a/speech.go
+++ b/speech.go
@@ -44,7 +44,10 @@ type CreateSpeechRequest struct {
 }
 
 func (c *Client) CreateSpeech(ctx context.Context, request CreateSpeechRequest) (response RawResponse, err error) {
-	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/audio/speech", string(request.Model)),
+	req, err := c.newRequest(
+		ctx,
+		http.MethodPost,
+		c.fullURL("/audio/speech", withModel(string(request.Model))),
 		withBody(request),
 		withContentType("application/json"),
 	)
diff --git a/stream.go b/stream.go
index b277f3c..a61c7c9 100644
--- a/stream.go
+++ b/stream.go
@@ -3,6 +3,7 @@ package openai
 import (
 	"context"
 	"errors"
+	"net/http"
 )
 
 var (
@@ -33,7 +34,12 @@ func (c *Client) CreateCompletionStream(
 	}
 
 	request.Stream = true
-	req, err := c.newRequest(ctx, "POST", c.fullURL(urlSuffix, request.Model), withBody(request))
+	req, err := c.newRequest(
+		ctx,
+		http.MethodPost,
+		c.fullURL(urlSuffix, withModel(request.Model)),
+		withBody(request),
+	)
 	if err != nil {
 		return nil, err
 	}


Test patch:
diff --git a/api_integration_test.go b/api_integration_test.go
index 3084268..8c9f338 100644
--- a/api_integration_test.go
+++ b/api_integration_test.go
@@ -4,7 +4,6 @@ package openai_test
 
 import (
 	"context"
-	"encoding/json"
 	"errors"
 	"io"
 	"os"
@@ -190,6 +189,17 @@ func TestChatCompletionResponseFormat_JSONSchema(t *testing.T) {
 	c := openai.NewClient(apiToken)
 	ctx := context.Background()
 
+	type MyStructuredResponse struct {
+		PascalCase string `json:"pascal_case" required:"true" description:"PascalCase"`
+		CamelCase  string `json:"camel_case" required:"true" description:"CamelCase"`
+		KebabCase  string `json:"kebab_case" required:"true" description:"KebabCase"`
+		SnakeCase  string `json:"snake_case" required:"true" description:"SnakeCase"`
+	}
+	var result MyStructuredResponse
+	schema, err := jsonschema.GenerateSchemaForType(result)
+	if err != nil {
+		t.Fatal("CreateChatCompletion (use json_schema response) GenerateSchemaForType error")
+	}
 	resp, err := c.CreateChatCompletion(
 		ctx,
 		openai.ChatCompletionRequest{
@@ -212,27 +222,89 @@ func TestChatCompletionResponseFormat_JSONSchema(t *testing.T) {
 			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,
-					},
+					Name:   "cases",
+					Schema: schema,
 					Strict: true,
 				},
 			},
 		},
 	)
 	checks.NoError(t, err, "CreateChatCompletion (use json_schema response) returned error")
+	if err == nil {
+		err = schema.Unmarshal(resp.Choices[0].Message.Content, &result)
+		checks.NoError(t, err, "CreateChatCompletion (use json_schema response) unmarshal error")
+	}
+}
+
+func TestChatCompletionStructuredOutputsFunctionCalling(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",
+				},
+			},
+			Tools: []openai.Tool{
+				{
+					Type: openai.ToolTypeFunction,
+					Function: &openai.FunctionDefinition{
+						Name:   "display_cases",
+						Strict: true,
+						Parameters: &jsonschema.Definition{
+							Type: jsonschema.Object,
+							Properties: map[string]jsonschema.Definition{
+								"PascalCase": {
+									Type: jsonschema.String,
+								},
+								"CamelCase": {
+									Type: jsonschema.String,
+								},
+								"KebabCase": {
+									Type: jsonschema.String,
+								},
+								"SnakeCase": {
+									Type: jsonschema.String,
+								},
+							},
+							Required:             []string{"PascalCase", "CamelCase", "KebabCase", "SnakeCase"},
+							AdditionalProperties: false,
+						},
+					},
+				},
+			},
+			ToolChoice: openai.ToolChoice{
+				Type: openai.ToolTypeFunction,
+				Function: openai.ToolFunction{
+					Name: "display_cases",
+				},
+			},
+		},
+	)
+	checks.NoError(t, err, "CreateChatCompletion (use structured outputs 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")
+	err = json.Unmarshal([]byte(resp.Choices[0].Message.ToolCalls[0].Function.Arguments), &result)
+	checks.NoError(t, err, "CreateChatCompletion (use structured outputs 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/api_internal_test.go b/api_internal_test.go
index a590ec9..0967796 100644
--- a/api_internal_test.go
+++ b/api_internal_test.go
@@ -112,6 +112,7 @@ func TestAzureFullURL(t *testing.T) {
 		Name             string
 		BaseURL          string
 		AzureModelMapper map[string]string
+		Suffix           string
 		Model            string
 		Expect           string
 	}{
@@ -119,6 +120,7 @@ func TestAzureFullURL(t *testing.T) {
 			"AzureBaseURLWithSlashAutoStrip",
 			"https://httpbin.org/",
 			nil,
+			"/chat/completions",
 			"chatgpt-demo",
 			"https://httpbin.org/" +
 				"openai/deployments/chatgpt-demo" +
@@ -128,11 +130,20 @@ func TestAzureFullURL(t *testing.T) {
 			"AzureBaseURLWithoutSlashOK",
 			"https://httpbin.org",
 			nil,
+			"/chat/completions",
 			"chatgpt-demo",
 			"https://httpbin.org/" +
 				"openai/deployments/chatgpt-demo" +
 				"/chat/completions?api-version=2023-05-15",
 		},
+		{
+			"",
+			"https://httpbin.org",
+			nil,
+			"/assistants?limit=10",
+			"chatgpt-demo",
+			"https://httpbin.org/openai/assistants?api-version=2023-05-15&limit=10",
+		},
 	}
 
 	for _, c := range cases {
@@ -140,7 +151,7 @@ func TestAzureFullURL(t *testing.T) {
 			az := DefaultAzureConfig("dummy", c.BaseURL)
 			cli := NewClientWithConfig(az)
 			// /openai/deployments/{engine}/chat/completions?api-version={api_version}
-			actual := cli.fullURL("/chat/completions", c.Model)
+			actual := cli.fullURL(c.Suffix, withModel(c.Model))
 			if actual != c.Expect {
 				t.Errorf("Expected %s, got %s", c.Expect, actual)
 			}
@@ -153,19 +164,22 @@ func TestCloudflareAzureFullURL(t *testing.T) {
 	cases := []struct {
 		Name    string
 		BaseURL string
+		Suffix  string
 		Expect  string
 	}{
 		{
 			"CloudflareAzureBaseURLWithSlashAutoStrip",
 			"https://gateway.ai.cloudflare.com/v1/dnekeim2i39dmm4mldemakiem3i4mkw3/demo/azure-openai/resource/chatgpt-demo/",
+			"/chat/completions",
 			"https://gateway.ai.cloudflare.com/v1/dnekeim2i39dmm4mldemakiem3i4mkw3/demo/azure-openai/resource/chatgpt-demo/" +
 				"chat/completions?api-version=2023-05-15",
 		},
 		{
-			"CloudflareAzureBaseURLWithoutSlashOK",
+			"",
 			"https://gateway.ai.cloudflare.com/v1/dnekeim2i39dmm4mldemakiem3i4mkw3/demo/azure-openai/resource/chatgpt-demo",
-			"https://gateway.ai.cloudflare.com/v1/dnekeim2i39dmm4mldemakiem3i4mkw3/demo/azure-openai/resource/chatgpt-demo/" +
-				"chat/completions?api-version=2023-05-15",
+			"/assistants?limit=10",
+			"https://gateway.ai.cloudflare.com/v1/dnekeim2i39dmm4mldemakiem3i4mkw3/demo/azure-openai/resource/chatgpt-demo" +
+				"/assistants?api-version=2023-05-15&limit=10",
 		},
 	}
 
@@ -176,7 +190,7 @@ func TestCloudflareAzureFullURL(t *testing.T) {
 
 			cli := NewClientWithConfig(az)
 
-			actual := cli.fullURL("/chat/completions")
+			actual := cli.fullURL(c.Suffix)
 			if actual != c.Expect {
 				t.Errorf("Expected %s, got %s", c.Expect, actual)
 			}
diff --git a/chat_test.go b/chat_test.go
index 520bf5c..37dc09d 100644
--- a/chat_test.go
+++ b/chat_test.go
@@ -277,6 +277,32 @@ func TestChatCompletionsFunctions(t *testing.T) {
 		})
 		checks.NoError(t, err, "CreateChatCompletion with functions error")
 	})
+	t.Run("StructuredOutputs", func(t *testing.T) {
+		type testMessage struct {
+			Count int      `json:"count"`
+			Words []string `json:"words"`
+		}
+		msg := testMessage{
+			Count: 2,
+			Words: []string{"hello", "world"},
+		}
+		_, err := client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{
+			MaxTokens: 5,
+			Model:     openai.GPT3Dot5Turbo0613,
+			Messages: []openai.ChatCompletionMessage{
+				{
+					Role:    openai.ChatMessageRoleUser,
+					Content: "Hello!",
+				},
+			},
+			Functions: []openai.FunctionDefinition{{
+				Name:       "test",
+				Strict:     true,
+				Parameters: &msg,
+			}},
+		})
+		checks.NoError(t, err, "CreateChatCompletion with functions error")
+	})
 }
 
 func TestAzureChatCompletions(t *testing.T) {
diff --git a/client_test.go b/client_test.go
index e49da9b..7119d8a 100644
--- a/client_test.go
+++ b/client_test.go
@@ -348,6 +348,9 @@ func TestClientReturnsRequestBuilderErrors(t *testing.T) {
 		{"ModifyMessage", func() (any, error) {
 			return client.ModifyMessage(ctx, "", "", nil)
 		}},
+		{"DeleteMessage", func() (any, error) {
+			return client.DeleteMessage(ctx, "", "")
+		}},
 		{"RetrieveMessageFile", func() (any, error) {
 			return client.RetrieveMessageFile(ctx, "", "", "")
 		}},
@@ -431,3 +434,99 @@ func TestClientReturnsRequestBuilderErrorsAddition(t *testing.T) {
 		t.Fatalf("Did not return error when request builder failed: %v", err)
 	}
 }
+
+func TestClient_suffixWithAPIVersion(t *testing.T) {
+	type fields struct {
+		apiVersion string
+	}
+	type args struct {
+		suffix string
+	}
+	tests := []struct {
+		name      string
+		fields    fields
+		args      args
+		want      string
+		wantPanic string
+	}{
+		{
+			"",
+			fields{apiVersion: "2023-05"},
+			args{suffix: "/assistants"},
+			"/assistants?api-version=2023-05",
+			"",
+		},
+		{
+			"",
+			fields{apiVersion: "2023-05"},
+			args{suffix: "/assistants?limit=5"},
+			"/assistants?api-version=2023-05&limit=5",
+			"",
+		},
+		{
+			"",
+			fields{apiVersion: "2023-05"},
+			args{suffix: "123:assistants?limit=5"},
+			"/assistants?api-version=2023-05&limit=5",
+			"failed to parse url suffix",
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			c := &Client{
+				config: ClientConfig{APIVersion: tt.fields.apiVersion},
+			}
+			defer func() {
+				if r := recover(); r != nil {
+					if r.(string) != tt.wantPanic {
+						t.Errorf("suffixWithAPIVersion() = %v, want %v", r, tt.wantPanic)
+					}
+				}
+			}()
+			if got := c.suffixWithAPIVersion(tt.args.suffix); got != tt.want {
+				t.Errorf("suffixWithAPIVersion() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestClient_baseURLWithAzureDeployment(t *testing.T) {
+	type args struct {
+		baseURL string
+		suffix  string
+		model   string
+	}
+	tests := []struct {
+		name           string
+		args           args
+		wantNewBaseURL string
+	}{
+		{
+			"",
+			args{baseURL: "https://test.openai.azure.com/", suffix: assistantsSuffix, model: GPT4oMini},
+			"https://test.openai.azure.com/openai",
+		},
+		{
+			"",
+			args{baseURL: "https://test.openai.azure.com/", suffix: chatCompletionsSuffix, model: GPT4oMini},
+			"https://test.openai.azure.com/openai/deployments/gpt-4o-mini",
+		},
+		{
+			"",
+			args{baseURL: "https://test.openai.azure.com/", suffix: chatCompletionsSuffix, model: ""},
+			"https://test.openai.azure.com/openai/deployments/UNKNOWN",
+		},
+	}
+	client := NewClient("")
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if gotNewBaseURL := client.baseURLWithAzureDeployment(
+				tt.args.baseURL,
+				tt.args.suffix,
+				tt.args.model,
+			); gotNewBaseURL != tt.wantNewBaseURL {
+				t.Errorf("baseURLWithAzureDeployment() = %v, want %v", gotNewBaseURL, tt.wantNewBaseURL)
+			}
+		})
+	}
+}
diff --git a/example_test.go b/example_test.go
index de67c57..e5dbf44 100644
--- a/example_test.go
+++ b/example_test.go
@@ -59,7 +59,7 @@ func ExampleClient_CreateChatCompletionStream() {
 	}
 	defer stream.Close()
 
-	fmt.Printf("Stream response: ")
+	fmt.Print("Stream response: ")
 	for {
 		var response openai.ChatCompletionStreamResponse
 		response, err = stream.Recv()
@@ -73,7 +73,7 @@ func ExampleClient_CreateChatCompletionStream() {
 			return
 		}
 
-		fmt.Printf(response.Choices[0].Delta.Content)
+		fmt.Println(response.Choices[0].Delta.Content)
 	}
 }
 
diff --git a/jsonschema/validate_test.go b/jsonschema/validate_test.go
new file mode 100644
index 0000000..c2c47a2
--- /dev/null
+++ b/jsonschema/validate_test.go
@@ -0,0 +1,136 @@
+package jsonschema_test
+
+import (
+	"testing"
+
+	"github.com/sashabaranov/go-openai/jsonschema"
+)
+
+func Test_Validate(t *testing.T) {
+	type args struct {
+		data   any
+		schema jsonschema.Definition
+	}
+	tests := []struct {
+		name string
+		args args
+		want bool
+	}{
+		// string integer number boolean
+		{"", args{data: "ABC", schema: jsonschema.Definition{Type: jsonschema.String}}, true},
+		{"", args{data: 123, schema: jsonschema.Definition{Type: jsonschema.String}}, false},
+		{"", args{data: 123, schema: jsonschema.Definition{Type: jsonschema.Integer}}, true},
+		{"", args{data: 123.4, schema: jsonschema.Definition{Type: jsonschema.Integer}}, false},
+		{"", args{data: "ABC", schema: jsonschema.Definition{Type: jsonschema.Number}}, false},
+		{"", args{data: 123, schema: jsonschema.Definition{Type: jsonschema.Number}}, true},
+		{"", args{data: false, schema: jsonschema.Definition{Type: jsonschema.Boolean}}, true},
+		{"", args{data: 123, schema: jsonschema.Definition{Type: jsonschema.Boolean}}, false},
+		{"", args{data: nil, schema: jsonschema.Definition{Type: jsonschema.Null}}, true},
+		{"", args{data: 0, schema: jsonschema.Definition{Type: jsonschema.Null}}, false},
+		// array
+		{"", args{data: []any{"a", "b", "c"}, schema: jsonschema.Definition{
+			Type: jsonschema.Array, Items: &jsonschema.Definition{Type: jsonschema.String}},
+		}, true},
+		{"", args{data: []any{1, 2, 3}, schema: jsonschema.Definition{
+			Type: jsonschema.Array, Items: &jsonschema.Definition{Type: jsonschema.String}},
+		}, false},
+		{"", args{data: []any{1, 2, 3}, schema: jsonschema.Definition{
+			Type: jsonschema.Array, Items: &jsonschema.Definition{Type: jsonschema.Integer}},
+		}, true},
+		{"", args{data: []any{1, 2, 3.4}, schema: jsonschema.Definition{
+			Type: jsonschema.Array, Items: &jsonschema.Definition{Type: jsonschema.Integer}},
+		}, false},
+		// object
+		{"", args{data: map[string]any{
+			"string":  "abc",
+			"integer": 123,
+			"number":  123.4,
+			"boolean": false,
+			"array":   []any{1, 2, 3},
+		}, schema: jsonschema.Definition{Type: jsonschema.Object, Properties: map[string]jsonschema.Definition{
+			"string":  {Type: jsonschema.String},
+			"integer": {Type: jsonschema.Integer},
+			"number":  {Type: jsonschema.Number},
+			"boolean": {Type: jsonschema.Boolean},
+			"array":   {Type: jsonschema.Array, Items: &jsonschema.Definition{Type: jsonschema.Number}},
+		},
+			Required: []string{"string"},
+		}}, true},
+		{"", args{data: map[string]any{
+			"integer": 123,
+			"number":  123.4,
+			"boolean": false,
+			"array":   []any{1, 2, 3},
+		}, schema: jsonschema.Definition{Type: jsonschema.Object, Properties: map[string]jsonschema.Definition{
+			"string":  {Type: jsonschema.String},
+			"integer": {Type: jsonschema.Integer},
+			"number":  {Type: jsonschema.Number},
+			"boolean": {Type: jsonschema.Boolean},
+			"array":   {Type: jsonschema.Array, Items: &jsonschema.Definition{Type: jsonschema.Number}},
+		},
+			Required: []string{"string"},
+		}}, false},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			if got := jsonschema.Validate(tt.args.schema, tt.args.data); got != tt.want {
+				t.Errorf("Validate() = %v, want %v", got, tt.want)
+			}
+		})
+	}
+}
+
+func TestUnmarshal(t *testing.T) {
+	type args struct {
+		schema  jsonschema.Definition
+		content []byte
+		v       any
+	}
+	var result1 struct {
+		String string  `json:"string"`
+		Number float64 `json:"number"`
+	}
+	var result2 struct {
+		String string  `json:"string"`
+		Number float64 `json:"number"`
+	}
+	tests := []struct {
+		name    string
+		args    args
+		wantErr bool
+	}{
+		{"", args{
+			schema: jsonschema.Definition{
+				Type: jsonschema.Object,
+				Properties: map[string]jsonschema.Definition{
+					"string": {Type: jsonschema.String},
+					"number": {Type: jsonschema.Number},
+				},
+			},
+			content: []byte(`{"string":"abc","number":123.4}`),
+			v:       &result1,
+		}, false},
+		{"", args{
+			schema: jsonschema.Definition{
+				Type: jsonschema.Object,
+				Properties: map[string]jsonschema.Definition{
+					"string": {Type: jsonschema.String},
+					"number": {Type: jsonschema.Number},
+				},
+				Required: []string{"string", "number"},
+			},
+			content: []byte(`{"string":"abc"}`),
+			v:       result2,
+		}, true},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			err := jsonschema.VerifySchemaAndUnmarshal(tt.args.schema, tt.args.content, tt.args.v)
+			if (err != nil) != tt.wantErr {
+				t.Errorf("Unmarshal() error = %v, wantErr %v", err, tt.wantErr)
+			} else if err == nil {
+				t.Logf("Unmarshal() v = %+v\n", tt.args.v)
+			}
+		})
+	}
+}
diff --git a/messages_test.go b/messages_test.go
index a18be20..71ceb4d 100644
--- a/messages_test.go
+++ b/messages_test.go
@@ -8,20 +8,17 @@ import (
 	"testing"
 
 	"github.com/sashabaranov/go-openai"
+	"github.com/sashabaranov/go-openai/internal/test"
 	"github.com/sashabaranov/go-openai/internal/test/checks"
 )
 
 var emptyStr = ""
 
-// TestMessages Tests the messages endpoint of the API using the mocked server.
-func TestMessages(t *testing.T) {
+func setupServerForTestMessage(t *testing.T, server *test.ServerTest) {
 	threadID := "thread_abc123"
 	messageID := "msg_abc123"
 	fileID := "file_abc123"
 
-	client, server, teardown := setupOpenAITestServer()
-	defer teardown()
-
 	server.RegisterHandler(
 		"/v1/threads/"+threadID+"/messages/"+messageID+"/files/"+fileID,
 		func(w http.ResponseWriter, r *http.Request) {
@@ -115,6 +112,13 @@ func TestMessages(t *testing.T) {
 						Metadata:    nil,
 					})
 				fmt.Fprintln(w, string(resBytes))
+			case http.MethodDelete:
+				resBytes, _ := json.Marshal(openai.MessageDeletionStatus{
+					ID:      messageID,
+					Object:  "thread.message.deleted",
+					Deleted: true,
+				})
+				fmt.Fprintln(w, string(resBytes))
 			default:
 				t.Fatalf("unsupported messages http method: %s", r.Method)
 			}
@@ -176,7 +180,18 @@ func TestMessages(t *testing.T) {
 			}
 		},
 	)
+}
 
+// TestMessages Tests the messages endpoint of the API using the mocked server.
+func TestMessages(t *testing.T) {
+	threadID := "thread_abc123"
+	messageID := "msg_abc123"
+	fileID := "file_abc123"
+
+	client, server, teardown := setupOpenAITestServer()
+	defer teardown()
+
+	setupServerForTestMessage(t, server)
 	ctx := context.Background()
 
 	// static assertion of return type
@@ -225,6 +240,17 @@ func TestMessages(t *testing.T) {
 		t.Fatalf("expected message metadata to get modified")
 	}
 
+	msgDel, err := client.DeleteMessage(ctx, threadID, messageID)
+	checks.NoError(t, err, "DeleteMessage error")
+	if msgDel.ID != messageID {
+		t.Fatalf("unexpected message id: '%s'", msg.ID)
+	}
+	if !msgDel.Deleted {
+		t.Fatalf("expected deleted is true")
+	}
+	_, err = client.DeleteMessage(ctx, threadID, "not_exist_id")
+	checks.HasError(t, err, "DeleteMessage error")
+
 	// message files
 	var msgFile openai.MessageFile
 	msgFile, err = client.RetrieveMessageFile(ctx, threadID, messageID, fileID)