Repository: sashabaranov/go-openai (go)

Issue:
Bug:  The `CreateChatCompletionStream` do not  return ` Rate limit ` and  ` Quota exceed `  error correctly.
The `CreateChatCompletionStream` do not return ` Rate limit` and `Quota exceed` error correctly, which is important for user to retry their requests.
```
stream, err := client.CreateChatCompletionStream(
		context.Background(),
		openai.ChatCompletionRequest{
			Model:    model,
			Messages: messages,
			Stream:   true,
		},
	)
```

I always get the `err` with `nil` value when actually meet a ` Rate limit` and `Quota exceed` issue, ` client.CreateChatCompletion` works well.

Pull request:
fix: chat stream resp error
fix #250 

Patch:
diff --git a/chat_stream.go b/chat_stream.go
index 8211292..b5257cc 100644
--- a/chat_stream.go
+++ b/chat_stream.go
@@ -3,6 +3,7 @@ package openai
 import (
 	"bufio"
 	"context"
+	"net/http"
 )
 
 type ChatCompletionStreamChoiceDelta struct {
@@ -53,6 +54,9 @@ func (c *Client) CreateChatCompletionStream(
 	if err != nil {
 		return
 	}
+	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest {
+		return nil, c.handleErrorResp(resp)
+	}
 
 	stream = &ChatCompletionStream{
 		streamReader: &streamReader[ChatCompletionStreamResponse]{
diff --git a/client.go b/client.go
index c1f76d7..b15a18a 100644
--- a/client.go
+++ b/client.go
@@ -72,17 +72,7 @@ func (c *Client) sendRequest(req *http.Request, v interface{}) error {
 	defer res.Body.Close()
 
 	if res.StatusCode < http.StatusOK || res.StatusCode >= http.StatusBadRequest {
-		var errRes ErrorResponse
-		err = json.NewDecoder(res.Body).Decode(&errRes)
-		if err != nil || errRes.Error == nil {
-			reqErr := RequestError{
-				StatusCode: res.StatusCode,
-				Err:        err,
-			}
-			return fmt.Errorf("error, %w", &reqErr)
-		}
-		errRes.Error.StatusCode = res.StatusCode
-		return fmt.Errorf("error, status code: %d, message: %w", res.StatusCode, errRes.Error)
+		return c.handleErrorResp(res)
 	}
 
 	if v != nil {
@@ -132,3 +122,17 @@ func (c *Client) newStreamRequest(
 	}
 	return req, nil
 }
+
+func (c *Client) handleErrorResp(resp *http.Response) error {
+	var errRes ErrorResponse
+	err := json.NewDecoder(resp.Body).Decode(&errRes)
+	if err != nil || errRes.Error == nil {
+		reqErr := RequestError{
+			HTTPStatusCode: resp.StatusCode,
+			Err:            err,
+		}
+		return fmt.Errorf("error, %w", &reqErr)
+	}
+	errRes.Error.HTTPStatusCode = resp.StatusCode
+	return fmt.Errorf("error, status code: %d, message: %w", resp.StatusCode, errRes.Error)
+}
diff --git a/error.go b/error.go
index 32ffa6c..8aee670 100644
--- a/error.go
+++ b/error.go
@@ -7,17 +7,17 @@ import (
 
 // APIError provides error information returned by the OpenAI API.
 type APIError struct {
-	Code       any     `json:"code,omitempty"`
-	Message    string  `json:"message"`
-	Param      *string `json:"param,omitempty"`
-	Type       string  `json:"type"`
-	StatusCode int     `json:"-"`
+	Code           any     `json:"code,omitempty"`
+	Message        string  `json:"message"`
+	Param          *string `json:"param,omitempty"`
+	Type           string  `json:"type"`
+	HTTPStatusCode int     `json:"-"`
 }
 
 // RequestError provides informations about generic request errors.
 type RequestError struct {
-	StatusCode int
-	Err        error
+	HTTPStatusCode int
+	Err            error
 }
 
 type ErrorResponse struct {
@@ -73,7 +73,7 @@ func (e *RequestError) Error() string {
 	if e.Err != nil {
 		return e.Err.Error()
 	}
-	return fmt.Sprintf("status code %d", e.StatusCode)
+	return fmt.Sprintf("status code %d", e.HTTPStatusCode)
 }
 
 func (e *RequestError) Unwrap() error {
diff --git a/image.go b/image.go
index bda23f7..21703bd 100644
--- a/image.go
+++ b/image.go
@@ -55,11 +55,12 @@ func (c *Client) CreateImage(ctx context.Context, request ImageRequest) (respons
 
 // ImageEditRequest represents the request structure for the image API.
 type ImageEditRequest struct {
-	Image  *os.File `json:"image,omitempty"`
-	Mask   *os.File `json:"mask,omitempty"`
-	Prompt string   `json:"prompt,omitempty"`
-	N      int      `json:"n,omitempty"`
-	Size   string   `json:"size,omitempty"`
+	Image          *os.File `json:"image,omitempty"`
+	Mask           *os.File `json:"mask,omitempty"`
+	Prompt         string   `json:"prompt,omitempty"`
+	N              int      `json:"n,omitempty"`
+	Size           string   `json:"size,omitempty"`
+	ResponseFormat string   `json:"response_format,omitempty"`
 }
 
 // CreateEditImage - API call to create an image. This is the main endpoint of the DALL-E API.
@@ -85,14 +86,22 @@ func (c *Client) CreateEditImage(ctx context.Context, request ImageEditRequest)
 	if err != nil {
 		return
 	}
+
 	err = builder.writeField("n", strconv.Itoa(request.N))
 	if err != nil {
 		return
 	}
+
 	err = builder.writeField("size", request.Size)
 	if err != nil {
 		return
 	}
+
+	err = builder.writeField("response_format", request.ResponseFormat)
+	if err != nil {
+		return
+	}
+
 	err = builder.close()
 	if err != nil {
 		return
@@ -111,9 +120,10 @@ func (c *Client) CreateEditImage(ctx context.Context, request ImageEditRequest)
 
 // ImageVariRequest represents the request structure for the image API.
 type ImageVariRequest struct {
-	Image *os.File `json:"image,omitempty"`
-	N     int      `json:"n,omitempty"`
-	Size  string   `json:"size,omitempty"`
+	Image          *os.File `json:"image,omitempty"`
+	N              int      `json:"n,omitempty"`
+	Size           string   `json:"size,omitempty"`
+	ResponseFormat string   `json:"response_format,omitempty"`
 }
 
 // CreateVariImage - API call to create an image variation. This is the main endpoint of the DALL-E API.
@@ -132,10 +142,17 @@ func (c *Client) CreateVariImage(ctx context.Context, request ImageVariRequest)
 	if err != nil {
 		return
 	}
+
 	err = builder.writeField("size", request.Size)
 	if err != nil {
 		return
 	}
+
+	err = builder.writeField("response_format", request.ResponseFormat)
+	if err != nil {
+		return
+	}
+
 	err = builder.close()
 	if err != nil {
 		return
diff --git a/stream.go b/stream.go
index 64688cd..95662db 100644
--- a/stream.go
+++ b/stream.go
@@ -4,6 +4,7 @@ import (
 	"bufio"
 	"context"
 	"errors"
+	"net/http"
 )
 
 var (
@@ -43,6 +44,9 @@ func (c *Client) CreateCompletionStream(
 	if err != nil {
 		return
 	}
+	if resp.StatusCode < http.StatusOK || resp.StatusCode >= http.StatusBadRequest {
+		return nil, c.handleErrorResp(resp)
+	}
 
 	stream = &CompletionStream{
 		streamReader: &streamReader[CompletionResponse]{


Test patch:
diff --git a/api_test.go b/api_test.go
index d6ad789..ecba256 100644
--- a/api_test.go
+++ b/api_test.go
@@ -1,16 +1,15 @@
 package openai_test
 
 import (
-	"encoding/json"
-
-	. "github.com/sashabaranov/go-openai"
-	"github.com/sashabaranov/go-openai/internal/test/checks"
-
 	"context"
+	"encoding/json"
 	"errors"
 	"io"
 	"os"
 	"testing"
+
+	. "github.com/sashabaranov/go-openai"
+	"github.com/sashabaranov/go-openai/internal/test/checks"
 )
 
 func TestAPI(t *testing.T) {
@@ -119,8 +118,8 @@ func TestAPIError(t *testing.T) {
 		t.Fatalf("Error is not an APIError: %+v", err)
 	}
 
-	if apiErr.StatusCode != 401 {
-		t.Fatalf("Unexpected API error status code: %d", apiErr.StatusCode)
+	if apiErr.HTTPStatusCode != 401 {
+		t.Fatalf("Unexpected API error status code: %d", apiErr.HTTPStatusCode)
 	}
 
 	switch v := apiErr.Code.(type) {
@@ -239,8 +238,8 @@ func TestRequestError(t *testing.T) {
 		t.Fatalf("Error is not a RequestError: %+v", err)
 	}
 
-	if reqErr.StatusCode != 418 {
-		t.Fatalf("Unexpected request error status code: %d", reqErr.StatusCode)
+	if reqErr.HTTPStatusCode != 418 {
+		t.Fatalf("Unexpected request error status code: %d", reqErr.HTTPStatusCode)
 	}
 
 	if reqErr.Unwrap() == nil {
diff --git a/chat_stream_test.go b/chat_stream_test.go
index 24046db..afcb86d 100644
--- a/chat_stream_test.go
+++ b/chat_stream_test.go
@@ -204,6 +204,57 @@ func TestCreateChatCompletionStreamError(t *testing.T) {
 	t.Logf("%+v\n", apiErr)
 }
 
+func TestCreateChatCompletionStreamRateLimitError(t *testing.T) {
+	server := test.NewTestServer()
+	server.RegisterHandler("/v1/chat/completions", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(429)
+
+		// Send test responses
+		dataBytes := []byte(`{"error":{` +
+			`"message": "You are sending requests too quickly.",` +
+			`"type":"rate_limit_reached",` +
+			`"param":null,` +
+			`"code":"rate_limit_reached"}}`)
+
+		_, err := w.Write(dataBytes)
+		checks.NoError(t, err, "Write error")
+	})
+	ts := server.OpenAITestServer()
+	ts.Start()
+	defer ts.Close()
+
+	// Client portion of the test
+	config := DefaultConfig(test.GetTestToken())
+	config.BaseURL = ts.URL + "/v1"
+	config.HTTPClient.Transport = &tokenRoundTripper{
+		test.GetTestToken(),
+		http.DefaultTransport,
+	}
+
+	client := NewClientWithConfig(config)
+	ctx := context.Background()
+
+	request := ChatCompletionRequest{
+		MaxTokens: 5,
+		Model:     GPT3Dot5Turbo,
+		Messages: []ChatCompletionMessage{
+			{
+				Role:    ChatMessageRoleUser,
+				Content: "Hello!",
+			},
+		},
+		Stream: true,
+	}
+
+	var apiErr *APIError
+	_, err := client.CreateChatCompletionStream(ctx, request)
+	if !errors.As(err, &apiErr) {
+		t.Errorf("TestCreateChatCompletionStreamRateLimitError did not return APIError")
+	}
+	t.Logf("%+v\n", apiErr)
+}
+
 // Helper funcs.
 func compareChatResponses(r1, r2 ChatCompletionStreamResponse) bool {
 	if r1.ID != r2.ID || r1.Object != r2.Object || r1.Created != r2.Created || r1.Model != r2.Model {
diff --git a/error_accumulator_test.go b/error_accumulator_test.go
index 637bf36..ecf954d 100644
--- a/error_accumulator_test.go
+++ b/error_accumulator_test.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"context"
 	"errors"
+	"net/http"
 	"testing"
 
 	"github.com/sashabaranov/go-openai/internal/test"
@@ -71,7 +72,11 @@ func TestErrorByteWriteErrors(t *testing.T) {
 
 func TestErrorAccumulatorWriteErrors(t *testing.T) {
 	var err error
-	ts := test.NewTestServer().OpenAITestServer()
+	server := test.NewTestServer()
+	server.RegisterHandler("/v1/chat/completions", func(w http.ResponseWriter, r *http.Request) {
+		http.Error(w, "error", 200)
+	})
+	ts := server.OpenAITestServer()
 	ts.Start()
 	defer ts.Close()
 
diff --git a/image_test.go b/image_test.go
index 34367b8..4a7dad5 100644
--- a/image_test.go
+++ b/image_test.go
@@ -118,11 +118,12 @@ func TestImageEdit(t *testing.T) {
 	}()
 
 	req := ImageEditRequest{
-		Image:  origin,
-		Mask:   mask,
-		Prompt: "There is a turtle in the pool",
-		N:      3,
-		Size:   CreateImageSize1024x1024,
+		Image:          origin,
+		Mask:           mask,
+		Prompt:         "There is a turtle in the pool",
+		N:              3,
+		Size:           CreateImageSize1024x1024,
+		ResponseFormat: CreateImageResponseFormatURL,
 	}
 	_, err = client.CreateEditImage(ctx, req)
 	checks.NoError(t, err, "CreateImage error")
@@ -154,10 +155,11 @@ func TestImageEditWithoutMask(t *testing.T) {
 	}()
 
 	req := ImageEditRequest{
-		Image:  origin,
-		Prompt: "There is a turtle in the pool",
-		N:      3,
-		Size:   CreateImageSize1024x1024,
+		Image:          origin,
+		Prompt:         "There is a turtle in the pool",
+		N:              3,
+		Size:           CreateImageSize1024x1024,
+		ResponseFormat: CreateImageResponseFormatURL,
 	}
 	_, err = client.CreateEditImage(ctx, req)
 	checks.NoError(t, err, "CreateImage error")
@@ -220,9 +222,10 @@ func TestImageVariation(t *testing.T) {
 	}()
 
 	req := ImageVariRequest{
-		Image: origin,
-		N:     3,
-		Size:  CreateImageSize1024x1024,
+		Image:          origin,
+		N:              3,
+		Size:           CreateImageSize1024x1024,
+		ResponseFormat: CreateImageResponseFormatURL,
 	}
 	_, err = client.CreateVariImage(ctx, req)
 	checks.NoError(t, err, "CreateImage error")
@@ -336,6 +339,10 @@ func TestImageFormBuilderFailures(t *testing.T) {
 	_, err = client.CreateEditImage(ctx, req)
 	checks.ErrorIs(t, err, mockFailedErr, "CreateImage should return error if form builder fails")
 
+	failForField = "response_format"
+	_, err = client.CreateEditImage(ctx, req)
+	checks.ErrorIs(t, err, mockFailedErr, "CreateImage should return error if form builder fails")
+
 	failForField = ""
 	mockBuilder.mockClose = func() error {
 		return mockFailedErr
@@ -384,6 +391,10 @@ func TestVariImageFormBuilderFailures(t *testing.T) {
 	_, err = client.CreateVariImage(ctx, req)
 	checks.ErrorIs(t, err, mockFailedErr, "CreateVariImage should return error if form builder fails")
 
+	failForField = "response_format"
+	_, err = client.CreateVariImage(ctx, req)
+	checks.ErrorIs(t, err, mockFailedErr, "CreateVariImage should return error if form builder fails")
+
 	failForField = ""
 	mockBuilder.mockClose = func() error {
 		return mockFailedErr
diff --git a/stream_test.go b/stream_test.go
index a80504d..a5c591f 100644
--- a/stream_test.go
+++ b/stream_test.go
@@ -1,16 +1,16 @@
 package openai_test
 
 import (
-	. "github.com/sashabaranov/go-openai"
-	"github.com/sashabaranov/go-openai/internal/test"
-	"github.com/sashabaranov/go-openai/internal/test/checks"
-
 	"context"
 	"errors"
 	"io"
 	"net/http"
 	"net/http/httptest"
 	"testing"
+
+	. "github.com/sashabaranov/go-openai"
+	"github.com/sashabaranov/go-openai/internal/test"
+	"github.com/sashabaranov/go-openai/internal/test/checks"
 )
 
 func TestCompletionsStreamWrongModel(t *testing.T) {
@@ -171,6 +171,52 @@ func TestCreateCompletionStreamError(t *testing.T) {
 	t.Logf("%+v\n", apiErr)
 }
 
+func TestCreateCompletionStreamRateLimitError(t *testing.T) {
+	server := test.NewTestServer()
+	server.RegisterHandler("/v1/completions", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "application/json")
+		w.WriteHeader(429)
+
+		// Send test responses
+		dataBytes := []byte(`{"error":{` +
+			`"message": "You are sending requests too quickly.",` +
+			`"type":"rate_limit_reached",` +
+			`"param":null,` +
+			`"code":"rate_limit_reached"}}`)
+
+		_, err := w.Write(dataBytes)
+		checks.NoError(t, err, "Write error")
+	})
+	ts := server.OpenAITestServer()
+	ts.Start()
+	defer ts.Close()
+
+	// Client portion of the test
+	config := DefaultConfig(test.GetTestToken())
+	config.BaseURL = ts.URL + "/v1"
+	config.HTTPClient.Transport = &tokenRoundTripper{
+		test.GetTestToken(),
+		http.DefaultTransport,
+	}
+
+	client := NewClientWithConfig(config)
+	ctx := context.Background()
+
+	request := CompletionRequest{
+		MaxTokens: 5,
+		Model:     GPT3Ada,
+		Prompt:    "Hello!",
+		Stream:    true,
+	}
+
+	var apiErr *APIError
+	_, err := client.CreateCompletionStream(ctx, request)
+	if !errors.As(err, &apiErr) {
+		t.Errorf("TestCreateCompletionStreamRateLimitError did not return APIError")
+	}
+	t.Logf("%+v\n", apiErr)
+}
+
 // A "tokenRoundTripper" is a struct that implements the RoundTripper
 // interface, specifically to handle the authentication token by adding a token
 // to the request header. We need this because the API requires that each