Repository: sashabaranov/go-openai
Pull request #856 (2024-09-20 10:38:40)

Optimize Client Error Return
fix: #836 #806 #777

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/sashabaranov/go-openai/pull/856?dropdown=coverage&src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alex+Baranov) Report
Attention: Patch coverage is `77.77778%` with `2 lines` in your changes missing coverage. Please review.
> 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 [(`17c79a0`)](https://app.codecov.io/gh/sashabaranov/go-openai/commit/17c79a024e8e4c1f4ab1db62d4f8681d8c0aa22a?dropdown=coverage&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alex+Baranov).
> Report is 49 commits behind head on master.

| [Files with missing lines](https://app.codecov.io/gh/sashabaranov/go-openai/pull/856?dropdown=coverage&src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alex+Baranov) | Patch % | Lines |
|---|---|---|
| [client.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/856?src=pr&el=tree&filepath=client.go&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alex+Baranov#diff-Y2xpZW50Lmdv) | 71.42% | [1 Missing and 1 partial :warning: ](https://app.codecov.io/gh/sashabaranov/go-openai/pull/856?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alex+Baranov) |

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


```diff
@@            Coverage Diff             @@
##           master     #856      +/-   ##
==========================================
+ Coverage   98.46%   98.88%   +0.41%     
==========================================
  Files          24       26       +2     
  Lines        1364     1429      +65     
==========================================
+ Hits         1343     1413      +70     
+ Misses         15        9       -6     
- Partials        6        7       +1     
```

</details>

[:umbrella: View full report in Codecov by Sentry](https://app.codecov.io/gh/sashabaranov/go-openai/pull/856?dropdown=coverage&src=pr&el=continue&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alex+Baranov).   
:loudspeaker: Have feedback on the report? [Share it here](https://about.codecov.io/codecov-pr-comment-feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=Alex+Baranov).


Patch:
diff --git a/.zshrc b/.zshrc
new file mode 100644
index 0000000..e69de29
diff --git a/chat.go b/chat.go
index dc60f35..d47c95e 100644
--- a/chat.go
+++ b/chat.go
@@ -200,18 +200,25 @@ type ChatCompletionResponseFormatJSONSchema struct {
 
 // ChatCompletionRequest represents a request structure for chat completion API.
 type ChatCompletionRequest struct {
-	Model            string                        `json:"model"`
-	Messages         []ChatCompletionMessage       `json:"messages"`
-	MaxTokens        int                           `json:"max_tokens,omitempty"`
-	Temperature      float32                       `json:"temperature,omitempty"`
-	TopP             float32                       `json:"top_p,omitempty"`
-	N                int                           `json:"n,omitempty"`
-	Stream           bool                          `json:"stream,omitempty"`
-	Stop             []string                      `json:"stop,omitempty"`
-	PresencePenalty  float32                       `json:"presence_penalty,omitempty"`
-	ResponseFormat   *ChatCompletionResponseFormat `json:"response_format,omitempty"`
-	Seed             *int                          `json:"seed,omitempty"`
-	FrequencyPenalty float32                       `json:"frequency_penalty,omitempty"`
+	Model    string                  `json:"model"`
+	Messages []ChatCompletionMessage `json:"messages"`
+	// MaxTokens The maximum number of tokens that can be generated in the chat completion.
+	// This value can be used to control costs for text generated via API.
+	// This value is now deprecated in favor of max_completion_tokens, and is not compatible with o1 series models.
+	// refs: https://platform.openai.com/docs/api-reference/chat/create#chat-create-max_tokens
+	MaxTokens int `json:"max_tokens,omitempty"`
+	// MaxCompletionsTokens An upper bound for the number of tokens that can be generated for a completion,
+	// including visible output tokens and reasoning tokens https://platform.openai.com/docs/guides/reasoning
+	MaxCompletionsTokens int                           `json:"max_completions_tokens,omitempty"`
+	Temperature          float32                       `json:"temperature,omitempty"`
+	TopP                 float32                       `json:"top_p,omitempty"`
+	N                    int                           `json:"n,omitempty"`
+	Stream               bool                          `json:"stream,omitempty"`
+	Stop                 []string                      `json:"stop,omitempty"`
+	PresencePenalty      float32                       `json:"presence_penalty,omitempty"`
+	ResponseFormat       *ChatCompletionResponseFormat `json:"response_format,omitempty"`
+	Seed                 *int                          `json:"seed,omitempty"`
+	FrequencyPenalty     float32                       `json:"frequency_penalty,omitempty"`
 	// LogitBias is must be a token id string (specified by their token ID in the tokenizer), not a word string.
 	// incorrect: `"logit_bias":{"You": 6}`, correct: `"logit_bias":{"1639": 6}`
 	// refs: https://platform.openai.com/docs/api-reference/chat/create#chat/create-logit_bias
@@ -364,6 +371,10 @@ func (c *Client) CreateChatCompletion(
 		return
 	}
 
+	if err = validateRequestForO1Models(request); err != nil {
+		return
+	}
+
 	req, err := c.newRequest(
 		ctx,
 		http.MethodPost,
diff --git a/chat_stream.go b/chat_stream.go
index 3f90bc0..f43d018 100644
--- a/chat_stream.go
+++ b/chat_stream.go
@@ -60,6 +60,10 @@ func (c *Client) CreateChatCompletionStream(
 	}
 
 	request.Stream = true
+	if err = validateRequestForO1Models(request); err != nil {
+		return
+	}
+
 	req, err := c.newRequest(
 		ctx,
 		http.MethodPost,
diff --git a/client.go b/client.go
index 9f547e7..583244f 100644
--- a/client.go
+++ b/client.go
@@ -285,10 +285,18 @@ func (c *Client) baseURLWithAzureDeployment(baseURL, suffix, model string) (newB
 }
 
 func (c *Client) handleErrorResp(resp *http.Response) error {
+	if !strings.HasPrefix(resp.Header.Get("Content-Type"), "application/json") {
+		body, err := io.ReadAll(resp.Body)
+		if err != nil {
+			return fmt.Errorf("error, reading response body: %w", err)
+		}
+		return fmt.Errorf("error, status code: %d, status: %s, body: %s", resp.StatusCode, resp.Status, body)
+	}
 	var errRes ErrorResponse
 	err := json.NewDecoder(resp.Body).Decode(&errRes)
 	if err != nil || errRes.Error == nil {
 		reqErr := &RequestError{
+			HTTPStatus:     resp.Status,
 			HTTPStatusCode: resp.StatusCode,
 			Err:            err,
 		}
@@ -298,6 +306,7 @@ func (c *Client) handleErrorResp(resp *http.Response) error {
 		return reqErr
 	}
 
+	errRes.Error.HTTPStatus = resp.Status
 	errRes.Error.HTTPStatusCode = resp.StatusCode
 	return errRes.Error
 }
diff --git a/completion.go b/completion.go
index e1e065a..8e3172a 100644
--- a/completion.go
+++ b/completion.go
@@ -7,11 +7,20 @@ import (
 )
 
 var (
+	ErrO1MaxTokensDeprecated                   = errors.New("this model is not supported MaxTokens, please use MaxCompletionsTokens")                              //nolint:lll
 	ErrCompletionUnsupportedModel              = errors.New("this model is not supported with this method, please use CreateChatCompletion client method instead") //nolint:lll
 	ErrCompletionStreamNotSupported            = errors.New("streaming is not supported with this method, please use CreateCompletionStream")                      //nolint:lll
 	ErrCompletionRequestPromptTypeNotSupported = errors.New("the type of CompletionRequest.Prompt only supports string and []string")                              //nolint:lll
 )
 
+var (
+	ErrO1BetaLimitationsMessageTypes = errors.New("this model has beta-limitations, user and assistant messages only, system messages are not supported")                                  //nolint:lll
+	ErrO1BetaLimitationsStreaming    = errors.New("this model has beta-limitations, streaming not supported")                                                                              //nolint:lll
+	ErrO1BetaLimitationsTools        = errors.New("this model has beta-limitations, tools, function calling, and response format parameters are not supported")                            //nolint:lll
+	ErrO1BetaLimitationsLogprobs     = errors.New("this model has beta-limitations, logprobs not supported")                                                                               //nolint:lll
+	ErrO1BetaLimitationsOther        = errors.New("this model has beta-limitations, temperature, top_p and n are fixed at 1, while presence_penalty and frequency_penalty are fixed at 0") //nolint:lll
+)
+
 // GPT3 Defines the models provided by OpenAI to use when generating
 // completions from OpenAI.
 // GPT3 Models are designed for text-based tasks. For code-specific
@@ -85,6 +94,15 @@ const (
 	CodexCodeDavinci001 = "code-davinci-001"
 )
 
+// O1SeriesModels List of new Series of OpenAI models.
+// Some old api attributes not supported.
+var O1SeriesModels = map[string]struct{}{
+	O1Mini:            {},
+	O1Mini20240912:    {},
+	O1Preview:         {},
+	O1Preview20240912: {},
+}
+
 var disabledModelsForEndpoints = map[string]map[string]bool{
 	"/completions": {
 		O1Mini:               true,
@@ -146,6 +164,70 @@ func checkPromptType(prompt any) bool {
 	return isString || isStringSlice
 }
 
+var unsupportedToolsForO1Models = map[ToolType]struct{}{
+	ToolTypeFunction: {},
+}
+
+var availableMessageRoleForO1Models = map[string]struct{}{
+	ChatMessageRoleUser:      {},
+	ChatMessageRoleAssistant: {},
+}
+
+// validateRequestForO1Models checks for deprecated fields of OpenAI models.
+func validateRequestForO1Models(request ChatCompletionRequest) error {
+	if _, found := O1SeriesModels[request.Model]; !found {
+		return nil
+	}
+
+	if request.MaxTokens > 0 {
+		return ErrO1MaxTokensDeprecated
+	}
+
+	// Beta Limitations
+	// refs:https://platform.openai.com/docs/guides/reasoning/beta-limitations
+	// Streaming: not supported
+	if request.Stream {
+		return ErrO1BetaLimitationsStreaming
+	}
+	// Logprobs: not supported.
+	if request.LogProbs {
+		return ErrO1BetaLimitationsLogprobs
+	}
+
+	// Message types: user and assistant messages only, system messages are not supported.
+	for _, m := range request.Messages {
+		if _, found := availableMessageRoleForO1Models[m.Role]; !found {
+			return ErrO1BetaLimitationsMessageTypes
+		}
+	}
+
+	// Tools: tools, function calling, and response format parameters are not supported
+	for _, t := range request.Tools {
+		if _, found := unsupportedToolsForO1Models[t.Type]; found {
+			return ErrO1BetaLimitationsTools
+		}
+	}
+
+	// Other: temperature, top_p and n are fixed at 1, while presence_penalty and frequency_penalty are fixed at 0.
+	if request.Temperature > 0 && request.Temperature != 1 {
+		return ErrO1BetaLimitationsOther
+	}
+	if request.TopP > 0 && request.TopP != 1 {
+		return ErrO1BetaLimitationsOther
+	}
+	if request.N > 0 && request.N != 1 {
+		return ErrO1BetaLimitationsOther
+	}
+	if request.PresencePenalty > 0 {
+		return ErrO1BetaLimitationsOther
+	}
+	if request.FrequencyPenalty > 0 {
+		return ErrO1BetaLimitationsOther
+	}
+
+	return nil
+}
+
 // CompletionRequest represents a request structure for completion API.
 type CompletionRequest struct {
 	Model            string  `json:"model"`
diff --git a/error.go b/error.go
index 37959a2..1f6a897 100644
--- a/error.go
+++ b/error.go
@@ -13,6 +13,7 @@ type APIError struct {
 	Message        string      `json:"message"`
 	Param          *string     `json:"param,omitempty"`
 	Type           string      `json:"type"`
+	HTTPStatus     string      `json:"-"`
 	HTTPStatusCode int         `json:"-"`
 	InnerError     *InnerError `json:"innererror,omitempty"`
 }
@@ -25,6 +26,7 @@ type InnerError struct {
 
 // RequestError provides information about generic request errors.
 type RequestError struct {
+	HTTPStatus     string
 	HTTPStatusCode int
 	Err            error
 }
@@ -35,7 +37,7 @@ type ErrorResponse struct {
 
 func (e *APIError) Error() string {
 	if e.HTTPStatusCode > 0 {
-		return fmt.Sprintf("error, status code: %d, message: %s", e.HTTPStatusCode, e.Message)
+		return fmt.Sprintf("error, status code: %d, status: %s, message: %s", e.HTTPStatusCode, e.HTTPStatus, e.Message)
 	}
 
 	return e.Message
@@ -101,7 +103,7 @@ func (e *APIError) UnmarshalJSON(data []byte) (err error) {
 }
 
 func (e *RequestError) Error() string {
-	return fmt.Sprintf("error, status code: %d, message: %s", e.HTTPStatusCode, e.Err)
+	return fmt.Sprintf("error, status code: %d, status: %s, message: %s", e.HTTPStatusCode, e.HTTPStatus, e.Err)
 }
 
 func (e *RequestError) Unwrap() error {
diff --git a/jsonschema/validate.go b/jsonschema/validate.go
index f14ffd4..49f9b88 100644
--- a/jsonschema/validate.go
+++ b/jsonschema/validate.go
@@ -36,6 +36,10 @@ func Validate(schema Definition, data any) bool {
 		_, ok := data.(bool)
 		return ok
 	case Integer:
+		// Golang unmarshals all numbers as float64, so we need to check if the float64 is an integer
+		if num, ok := data.(float64); ok {
+			return num == float64(int64(num))
+		}
 		_, ok := data.(int)
 		return ok
 	case Null:
diff --git a/messages.go b/messages.go
index 1fddd63..eefc29a 100644
--- a/messages.go
+++ b/messages.go
@@ -100,6 +100,7 @@ func (c *Client) ListMessage(ctx context.Context, threadID string,
 	order *string,
 	after *string,
 	before *string,
+	runID *string,
 ) (messages MessagesList, err error) {
 	urlValues := url.Values{}
 	if limit != nil {
@@ -114,6 +115,10 @@ func (c *Client) ListMessage(ctx context.Context, threadID string,
 	if before != nil {
 		urlValues.Add("before", *before)
 	}
+	if runID != nil {
+		urlValues.Add("run_id", *runID)
+	}
+
 	encodedValues := ""
 	if len(urlValues) > 0 {
 		encodedValues = "?" + urlValues.Encode()
diff --git a/run.go b/run.go
index 5598f1d..0cdec2b 100644
--- a/run.go
+++ b/run.go
@@ -37,6 +37,8 @@ type Run struct {
 	MaxCompletionTokens int `json:"max_completion_tokens,omitempty"`
 	// ThreadTruncationStrategy defines the truncation strategy to use for the thread.
 	TruncationStrategy *ThreadTruncationStrategy `json:"truncation_strategy,omitempty"`
+	// Disable the default behavior of parallel tool calls by setting it: false.
+	ParallelToolCalls any `json:"parallel_tool_calls,omitempty"`
 
 	httpHeader
 }


Test patch:
diff --git a/chat_stream_test.go b/chat_stream_test.go
index 63e45ee..2e7c99b 100644
--- a/chat_stream_test.go
+++ b/chat_stream_test.go
@@ -36,6 +36,27 @@ func TestChatCompletionsStreamWrongModel(t *testing.T) {
 	}
 }
 
+func TestChatCompletionsStreamWithO1BetaLimitations(t *testing.T) {
+	config := openai.DefaultConfig("whatever")
+	config.BaseURL = "http://localhost/v1/chat/completions"
+	client := openai.NewClientWithConfig(config)
+	ctx := context.Background()
+
+	req := openai.ChatCompletionRequest{
+		Model: openai.O1Preview,
+		Messages: []openai.ChatCompletionMessage{
+			{
+				Role:    openai.ChatMessageRoleUser,
+				Content: "Hello!",
+			},
+		},
+	}
+	_, err := client.CreateChatCompletionStream(ctx, req)
+	if !errors.Is(err, openai.ErrO1BetaLimitationsStreaming) {
+		t.Fatalf("CreateChatCompletion should return ErrO1BetaLimitationsStreaming, but returned: %v", err)
+	}
+}
+
 func TestCreateChatCompletionStream(t *testing.T) {
 	client, server, teardown := setupOpenAITestServer()
 	defer teardown()
diff --git a/chat_test.go b/chat_test.go
index 37dc09d..a54dd35 100644
--- a/chat_test.go
+++ b/chat_test.go
@@ -52,6 +52,199 @@ func TestChatCompletionsWrongModel(t *testing.T) {
 	checks.ErrorIs(t, err, openai.ErrChatCompletionInvalidModel, msg)
 }
 
+func TestO1ModelsChatCompletionsDeprecatedFields(t *testing.T) {
+	tests := []struct {
+		name          string
+		in            openai.ChatCompletionRequest
+		expectedError error
+	}{
+		{
+			name: "o1-preview_MaxTokens_deprecated",
+			in: openai.ChatCompletionRequest{
+				MaxTokens: 5,
+				Model:     openai.O1Preview,
+			},
+			expectedError: openai.ErrO1MaxTokensDeprecated,
+		},
+		{
+			name: "o1-mini_MaxTokens_deprecated",
+			in: openai.ChatCompletionRequest{
+				MaxTokens: 5,
+				Model:     openai.O1Mini,
+			},
+			expectedError: openai.ErrO1MaxTokensDeprecated,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			config := openai.DefaultConfig("whatever")
+			config.BaseURL = "http://localhost/v1"
+			client := openai.NewClientWithConfig(config)
+			ctx := context.Background()
+
+			_, err := client.CreateChatCompletion(ctx, tt.in)
+			checks.HasError(t, err)
+			msg := fmt.Sprintf("CreateChatCompletion should return wrong model error, returned: %s", err)
+			checks.ErrorIs(t, err, tt.expectedError, msg)
+		})
+	}
+}
+
+func TestO1ModelsChatCompletionsBetaLimitations(t *testing.T) {
+	tests := []struct {
+		name          string
+		in            openai.ChatCompletionRequest
+		expectedError error
+	}{
+		{
+			name: "log_probs_unsupported",
+			in: openai.ChatCompletionRequest{
+				MaxCompletionsTokens: 1000,
+				LogProbs:             true,
+				Model:                openai.O1Preview,
+			},
+			expectedError: openai.ErrO1BetaLimitationsLogprobs,
+		},
+		{
+			name: "message_type_unsupported",
+			in: openai.ChatCompletionRequest{
+				MaxCompletionsTokens: 1000,
+				Model:                openai.O1Mini,
+				Messages: []openai.ChatCompletionMessage{
+					{
+						Role: openai.ChatMessageRoleSystem,
+					},
+				},
+			},
+			expectedError: openai.ErrO1BetaLimitationsMessageTypes,
+		},
+		{
+			name: "tool_unsupported",
+			in: openai.ChatCompletionRequest{
+				MaxCompletionsTokens: 1000,
+				Model:                openai.O1Mini,
+				Messages: []openai.ChatCompletionMessage{
+					{
+						Role: openai.ChatMessageRoleUser,
+					},
+					{
+						Role: openai.ChatMessageRoleAssistant,
+					},
+				},
+				Tools: []openai.Tool{
+					{
+						Type: openai.ToolTypeFunction,
+					},
+				},
+			},
+			expectedError: openai.ErrO1BetaLimitationsTools,
+		},
+		{
+			name: "set_temperature_unsupported",
+			in: openai.ChatCompletionRequest{
+				MaxCompletionsTokens: 1000,
+				Model:                openai.O1Mini,
+				Messages: []openai.ChatCompletionMessage{
+					{
+						Role: openai.ChatMessageRoleUser,
+					},
+					{
+						Role: openai.ChatMessageRoleAssistant,
+					},
+				},
+				Temperature: float32(2),
+			},
+			expectedError: openai.ErrO1BetaLimitationsOther,
+		},
+		{
+			name: "set_top_unsupported",
+			in: openai.ChatCompletionRequest{
+				MaxCompletionsTokens: 1000,
+				Model:                openai.O1Mini,
+				Messages: []openai.ChatCompletionMessage{
+					{
+						Role: openai.ChatMessageRoleUser,
+					},
+					{
+						Role: openai.ChatMessageRoleAssistant,
+					},
+				},
+				Temperature: float32(1),
+				TopP:        float32(0.1),
+			},
+			expectedError: openai.ErrO1BetaLimitationsOther,
+		},
+		{
+			name: "set_n_unsupported",
+			in: openai.ChatCompletionRequest{
+				MaxCompletionsTokens: 1000,
+				Model:                openai.O1Mini,
+				Messages: []openai.ChatCompletionMessage{
+					{
+						Role: openai.ChatMessageRoleUser,
+					},
+					{
+						Role: openai.ChatMessageRoleAssistant,
+					},
+				},
+				Temperature: float32(1),
+				TopP:        float32(1),
+				N:           2,
+			},
+			expectedError: openai.ErrO1BetaLimitationsOther,
+		},
+		{
+			name: "set_presence_penalty_unsupported",
+			in: openai.ChatCompletionRequest{
+				MaxCompletionsTokens: 1000,
+				Model:                openai.O1Mini,
+				Messages: []openai.ChatCompletionMessage{
+					{
+						Role: openai.ChatMessageRoleUser,
+					},
+					{
+						Role: openai.ChatMessageRoleAssistant,
+					},
+				},
+				PresencePenalty: float32(1),
+			},
+			expectedError: openai.ErrO1BetaLimitationsOther,
+		},
+		{
+			name: "set_frequency_penalty_unsupported",
+			in: openai.ChatCompletionRequest{
+				MaxCompletionsTokens: 1000,
+				Model:                openai.O1Mini,
+				Messages: []openai.ChatCompletionMessage{
+					{
+						Role: openai.ChatMessageRoleUser,
+					},
+					{
+						Role: openai.ChatMessageRoleAssistant,
+					},
+				},
+				FrequencyPenalty: float32(0.1),
+			},
+			expectedError: openai.ErrO1BetaLimitationsOther,
+		},
+	}
+
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			config := openai.DefaultConfig("whatever")
+			config.BaseURL = "http://localhost/v1"
+			client := openai.NewClientWithConfig(config)
+			ctx := context.Background()
+
+			_, err := client.CreateChatCompletion(ctx, tt.in)
+			checks.HasError(t, err)
+			msg := fmt.Sprintf("CreateChatCompletion should return wrong model error, returned: %s", err)
+			checks.ErrorIs(t, err, tt.expectedError, msg)
+		})
+	}
+}
+
 func TestChatRequestOmitEmpty(t *testing.T) {
 	data, err := json.Marshal(openai.ChatCompletionRequest{
 		// We set model b/c it's required, so omitempty doesn't make sense
@@ -97,6 +290,24 @@ func TestChatCompletions(t *testing.T) {
 	checks.NoError(t, err, "CreateChatCompletion error")
 }
 
+// TestCompletions Tests the completions endpoint of the API using the mocked server.
+func TestO1ModelChatCompletions(t *testing.T) {
+	client, server, teardown := setupOpenAITestServer()
+	defer teardown()
+	server.RegisterHandler("/v1/chat/completions", handleChatCompletionEndpoint)
+	_, err := client.CreateChatCompletion(context.Background(), openai.ChatCompletionRequest{
+		Model:                openai.O1Preview,
+		MaxCompletionsTokens: 1000,
+		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()
diff --git a/client_test.go b/client_test.go
index 7119d8a..18da787 100644
--- a/client_test.go
+++ b/client_test.go
@@ -134,14 +134,17 @@ func TestHandleErrorResp(t *testing.T) {
 	client := NewClient(mockToken)
 
 	testCases := []struct {
-		name     string
-		httpCode int
-		body     io.Reader
-		expected string
+		name        string
+		httpCode    int
+		httpStatus  string
+		contentType string
+		body        io.Reader
+		expected    string
 	}{
 		{
-			name:     "401 Invalid Authentication",
-			httpCode: http.StatusUnauthorized,
+			name:        "401 Invalid Authentication",
+			httpCode:    http.StatusUnauthorized,
+			contentType: "application/json",
 			body: bytes.NewReader([]byte(
 				`{
 					"error":{
@@ -152,11 +155,12 @@ func TestHandleErrorResp(t *testing.T) {
 					}
 				}`,
 			)),
-			expected: "error, status code: 401, message: You didn't provide an API key. ....",
+			expected: "error, status code: 401, status: , message: You didn't provide an API key. ....",
 		},
 		{
-			name:     "401 Azure Access Denied",
-			httpCode: http.StatusUnauthorized,
+			name:        "401 Azure Access Denied",
+			httpCode:    http.StatusUnauthorized,
+			contentType: "application/json",
 			body: bytes.NewReader([]byte(
 				`{
 					"error":{
@@ -165,11 +169,12 @@ func TestHandleErrorResp(t *testing.T) {
 					}
 				}`,
 			)),
-			expected: "error, status code: 401, message: Access denied due to Virtual Network/Firewall rules.",
+			expected: "error, status code: 401, status: , message: Access denied due to Virtual Network/Firewall rules.",
 		},
 		{
-			name:     "503 Model Overloaded",
-			httpCode: http.StatusServiceUnavailable,
+			name:        "503 Model Overloaded",
+			httpCode:    http.StatusServiceUnavailable,
+			contentType: "application/json",
 			body: bytes.NewReader([]byte(`
 				{
 					"error":{
@@ -179,22 +184,53 @@ func TestHandleErrorResp(t *testing.T) {
 						"code":null
 					}
 				}`)),
-			expected: "error, status code: 503, message: That model...",
+			expected: "error, status code: 503, status: , message: That model...",
 		},
 		{
-			name:     "503 no message (Unknown response)",
-			httpCode: http.StatusServiceUnavailable,
+			name:        "503 no message (Unknown response)",
+			httpCode:    http.StatusServiceUnavailable,
+			contentType: "application/json",
 			body: bytes.NewReader([]byte(`
 				{
 					"error":{}
 				}`)),
-			expected: "error, status code: 503, message: ",
+			expected: "error, status code: 503, status: , message: ",
+		},
+		{
+			name:        "413 Request Entity Too Large",
+			httpCode:    http.StatusRequestEntityTooLarge,
+			contentType: "text/html",
+			body: bytes.NewReader([]byte(`<html>
+<head><title>413 Request Entity Too Large</title></head>
+<body>
+<center><h1>413 Request Entity Too Large</h1></center>
+<hr><center>nginx</center>
+</body>
+</html>`)),
+			expected: `error, status code: 413, status: , body: <html>
+<head><title>413 Request Entity Too Large</title></head>
+<body>
+<center><h1>413 Request Entity Too Large</h1></center>
+<hr><center>nginx</center>
+</body>
+</html>`,
+		},
+		{
+			name:        "errorReader",
+			httpCode:    http.StatusRequestEntityTooLarge,
+			contentType: "text/html",
+			body:        &errorReader{err: errors.New("errorReader")},
+			expected:    "error, reading response body: errorReader",
 		},
 	}
 
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
-			testCase := &http.Response{}
+			testCase := &http.Response{
+				Header: map[string][]string{
+					"Content-Type": {tc.contentType},
+				},
+			}
 			testCase.StatusCode = tc.httpCode
 			testCase.Body = io.NopCloser(tc.body)
 			err := client.handleErrorResp(testCase)
@@ -203,12 +239,6 @@ func TestHandleErrorResp(t *testing.T) {
 				t.Errorf("Unexpected error: %v , expected: %s", err, tc.expected)
 				t.Fail()
 			}
-
-			e := &APIError{}
-			if !errors.As(err, &e) {
-				t.Errorf("(%s) Expected error to be of type APIError", tc.name)
-				t.Fail()
-			}
 		})
 	}
 }
@@ -340,7 +370,7 @@ func TestClientReturnsRequestBuilderErrors(t *testing.T) {
 			return client.CreateMessage(ctx, "", MessageRequest{})
 		}},
 		{"ListMessage", func() (any, error) {
-			return client.ListMessage(ctx, "", nil, nil, nil, nil)
+			return client.ListMessage(ctx, "", nil, nil, nil, nil, nil)
 		}},
 		{"RetrieveMessage", func() (any, error) {
 			return client.RetrieveMessage(ctx, "", "")
diff --git a/files_api_test.go b/files_api_test.go
index c92162a..aa4fda4 100644
--- a/files_api_test.go
+++ b/files_api_test.go
@@ -152,6 +152,7 @@ func TestGetFileContentReturnError(t *testing.T) {
 	client, server, teardown := setupOpenAITestServer()
 	defer teardown()
 	server.RegisterHandler("/v1/files/deadbeef/content", func(w http.ResponseWriter, _ *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
 		w.WriteHeader(http.StatusBadRequest)
 		fmt.Fprint(w, wantErrorResp)
 	})
diff --git a/jsonschema/validate_test.go b/jsonschema/validate_test.go
index c2c47a2..6fa30ab 100644
--- a/jsonschema/validate_test.go
+++ b/jsonschema/validate_test.go
@@ -86,14 +86,6 @@ func TestUnmarshal(t *testing.T) {
 		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
@@ -108,7 +100,10 @@ func TestUnmarshal(t *testing.T) {
 				},
 			},
 			content: []byte(`{"string":"abc","number":123.4}`),
-			v:       &result1,
+			v: &struct {
+				String string  `json:"string"`
+				Number float64 `json:"number"`
+			}{},
 		}, false},
 		{"", args{
 			schema: jsonschema.Definition{
@@ -120,7 +115,40 @@ func TestUnmarshal(t *testing.T) {
 				Required: []string{"string", "number"},
 			},
 			content: []byte(`{"string":"abc"}`),
-			v:       result2,
+			v: struct {
+				String string  `json:"string"`
+				Number float64 `json:"number"`
+			}{},
+		}, true},
+		{"validate integer", args{
+			schema: jsonschema.Definition{
+				Type: jsonschema.Object,
+				Properties: map[string]jsonschema.Definition{
+					"string":  {Type: jsonschema.String},
+					"integer": {Type: jsonschema.Integer},
+				},
+				Required: []string{"string", "integer"},
+			},
+			content: []byte(`{"string":"abc","integer":123}`),
+			v: &struct {
+				String  string `json:"string"`
+				Integer int    `json:"integer"`
+			}{},
+		}, false},
+		{"validate integer failed", args{
+			schema: jsonschema.Definition{
+				Type: jsonschema.Object,
+				Properties: map[string]jsonschema.Definition{
+					"string":  {Type: jsonschema.String},
+					"integer": {Type: jsonschema.Integer},
+				},
+				Required: []string{"string", "integer"},
+			},
+			content: []byte(`{"string":"abc","integer":123.4}`),
+			v: &struct {
+				String  string `json:"string"`
+				Integer int    `json:"integer"`
+			}{},
 		}, true},
 	}
 	for _, tt := range tests {
diff --git a/messages_test.go b/messages_test.go
index 71ceb4d..b25755f 100644
--- a/messages_test.go
+++ b/messages_test.go
@@ -208,7 +208,7 @@ func TestMessages(t *testing.T) {
 	}
 
 	var msgs openai.MessagesList
-	msgs, err = client.ListMessage(ctx, threadID, nil, nil, nil, nil)
+	msgs, err = client.ListMessage(ctx, threadID, nil, nil, nil, nil, nil)
 	checks.NoError(t, err, "ListMessages error")
 	if len(msgs.Messages) != 1 {
 		t.Fatalf("unexpected length of fetched messages")
@@ -219,7 +219,8 @@ func TestMessages(t *testing.T) {
 	order := "desc"
 	after := "obj_foo"
 	before := "obj_bar"
-	msgs, err = client.ListMessage(ctx, threadID, &limit, &order, &after, &before)
+	runID := "run_abc123"
+	msgs, err = client.ListMessage(ctx, threadID, &limit, &order, &after, &before, &runID)
 	checks.NoError(t, err, "ListMessages error")
 	if len(msgs.Messages) != 1 {
 		t.Fatalf("unexpected length of fetched messages")