Repository: sashabaranov/go-openai
Pull request #395 (2023-06-20 04:48:29)

refactor: refactoring http request creation and sending
`http.Request` creation and sending were duplicated, so they have been aggregated into a common function.

- `RequestBuilder#Build` function calls were aggregated into the `Client#newRequest` function.
- All public functions build `http.Request` in the `Client#newRequest` function.
- The sending of `http.Request` is now aggregated into the following three functions.
  - `Client#sendRequest` function: marshalling the response into a structure
  - `Client#sendRequestRaw` function: returns the Body of the response as is 
  - `Client#sendRequestStream` function: use generics to specify a `streamable` type.

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/sashabaranov/go-openai/pull/395?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#395](https://app.codecov.io/gh/sashabaranov/go-openai/pull/395?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (598eb5d) into [master](https://app.codecov.io/gh/sashabaranov/go-openai/commit/b0959382c8fc01bf12de71a843d961f0d579f6f8?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (b095938) will **increase** coverage by `0.33%`.
> The diff coverage is `96.66%`.

```diff
@@            Coverage Diff             @@
##           master     #395      +/-   ##
==========================================
+ Coverage   95.26%   95.60%   +0.33%     
==========================================
  Files          17       17              
  Lines         676      682       +6     
==========================================
+ Hits          644      652       +8     
+ Misses         22       21       -1     
+ Partials       10        9       -1     
```


| [Impacted Files](https://app.codecov.io/gh/sashabaranov/go-openai/pull/395?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [client.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/395?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y2xpZW50Lmdv) | `94.00% <94.33%> (-0.60%)` | :arrow_down: |
| [audio.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/395?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-YXVkaW8uZ28=) | `88.15% <100.00%> (-0.16%)` | :arrow_down: |
| [chat.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/395?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y2hhdC5nbw==) | `100.00% <100.00%> (ø)` | |
| [chat\_stream.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/395?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y2hhdF9zdHJlYW0uZ28=) | `100.00% <100.00%> (+10.71%)` | :arrow_up: |
| [completion.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/395?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y29tcGxldGlvbi5nbw==) | `100.00% <100.00%> (ø)` | |
| [edits.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/395?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-ZWRpdHMuZ28=) | `100.00% <100.00%> (ø)` | |
| [embeddings.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/395?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-ZW1iZWRkaW5ncy5nbw==) | `100.00% <100.00%> (ø)` | |
| [engines.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/395?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-ZW5naW5lcy5nbw==) | `100.00% <100.00%> (ø)` | |
| [files.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/395?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-ZmlsZXMuZ28=) | `94.73% <100.00%> (-0.92%)` | :arrow_down: |
| [fine\_tunes.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/395?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-ZmluZV90dW5lcy5nbw==) | `100.00% <100.00%> (ø)` | |
| ... and [4 more](https://app.codecov.io/gh/sashabaranov/go-openai/pull/395?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | |

... and [1 file with indirect coverage changes](https://app.codecov.io/gh/sashabaranov/go-openai/pull/395/indirect-changes?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)


vvatanabe: @sashabaranov Please check it. 

Patch:
diff --git a/.github/FUNDING.yml b/.github/FUNDING.yml
index d9fd885..e36c382 100644
--- a/.github/FUNDING.yml
+++ b/.github/FUNDING.yml
@@ -1,3 +1,3 @@
 # These are supported funding model platforms
 
-github: [sashabaranov]
+github: [sashabaranov, vvatanabe]
diff --git a/README.md b/README.md
index 9a72623..522a85e 100644
--- a/README.md
+++ b/README.md
@@ -14,7 +14,7 @@ This library provides unofficial Go clients for [OpenAI API](https://platform.op
 ```
 go get github.com/sashabaranov/go-openai
 ```
-
+Currently, go-openai requires Go version 1.18 or greater.
 
 ### ChatGPT example usage:
 
@@ -554,4 +554,4 @@ These tests send real network traffic to the OpenAI API and may reach rate limit
 OPENAI_TOKEN=XXX go test -v -tags=integration ./api_integration_test.go
 ```
 
-If `OPENAI_TOKEN` environment variables are not available, integration tests will be skipped.
\ No newline at end of file
+If the `OPENAI_TOKEN` environment variable is not available, integration tests will be skipped.
diff --git a/audio.go b/audio.go
index adfc527..9f46915 100644
--- a/audio.go
+++ b/audio.go
@@ -95,11 +95,11 @@ func (c *Client) callAudioAPI(
 	}
 
 	urlSuffix := fmt.Sprintf("/audio/%s", endpointSuffix)
-	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.fullURL(urlSuffix, request.Model), &formBody)
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix, request.Model),
+		withBody(&formBody), withContentType(builder.FormDataContentType()))
 	if err != nil {
 		return AudioResponse{}, err
 	}
-	req.Header.Add("Content-Type", builder.FormDataContentType())
 
 	if request.HasJSONResponse() {
 		err = c.sendRequest(req, &response)
diff --git a/chat.go b/chat.go
index 4764e36..b74720d 100644
--- a/chat.go
+++ b/chat.go
@@ -54,23 +54,23 @@ type ChatCompletionRequest struct {
 	FrequencyPenalty float32                 `json:"frequency_penalty,omitempty"`
 	LogitBias        map[string]int          `json:"logit_bias,omitempty"`
 	User             string                  `json:"user,omitempty"`
-	Functions        []*FunctionDefine       `json:"functions,omitempty"`
-	FunctionCall     string                  `json:"function_call,omitempty"`
+	Functions        []FunctionDefinition    `json:"functions,omitempty"`
+	FunctionCall     any                     `json:"function_call,omitempty"`
 }
 
-type FunctionDefine struct {
+type FunctionDefinition struct {
 	Name        string `json:"name"`
 	Description string `json:"description,omitempty"`
-	// it's required in function call
-	Parameters *FunctionParams `json:"parameters"`
+	// Parameters is an object describing the function.
+	// You can pass a raw byte array describing the schema,
+	// or you can pass in a struct which serializes to the proper JSONSchema.
+	// The JSONSchemaDefinition struct is provided for convenience, but you should
+	// consider another specialized library for more complex schemas.
+	Parameters any `json:"parameters"`
 }
 
-type FunctionParams struct {
-	// the Type must be JSONSchemaTypeObject
-	Type       JSONSchemaType               `json:"type"`
-	Properties map[string]*JSONSchemaDefine `json:"properties,omitempty"`
-	Required   []string                     `json:"required,omitempty"`
-}
+// Deprecated: use FunctionDefinition instead.
+type FunctionDefine = FunctionDefinition
 
 type JSONSchemaType string
 
@@ -83,8 +83,9 @@ const (
 	JSONSchemaTypeBoolean JSONSchemaType = "boolean"
 )
 
-// JSONSchemaDefine is a struct for JSON Schema.
-type JSONSchemaDefine struct {
+// JSONSchemaDefinition is a struct for JSON Schema.
+// It is fairly limited and you may have better luck using a third-party library.
+type JSONSchemaDefinition struct {
 	// Type is a type of JSON Schema.
 	Type JSONSchemaType `json:"type,omitempty"`
 	// Description is a description of JSON Schema.
@@ -92,13 +93,16 @@ type JSONSchemaDefine struct {
 	// Enum is a enum of JSON Schema. It used if Type is JSONSchemaTypeString.
 	Enum []string `json:"enum,omitempty"`
 	// Properties is a properties of JSON Schema. It used if Type is JSONSchemaTypeObject.
-	Properties map[string]*JSONSchemaDefine `json:"properties,omitempty"`
+	Properties map[string]JSONSchemaDefinition `json:"properties,omitempty"`
 	// Required is a required of JSON Schema. It used if Type is JSONSchemaTypeObject.
 	Required []string `json:"required,omitempty"`
 	// Items is a property of JSON Schema. It used if Type is JSONSchemaTypeArray.
-	Items *JSONSchemaDefine `json:"items,omitempty"`
+	Items *JSONSchemaDefinition `json:"items,omitempty"`
 }
 
+// Deprecated: use JSONSchemaDefinition instead.
+type JSONSchemaDefine = JSONSchemaDefinition
+
 type FinishReason string
 
 const (
@@ -148,7 +152,7 @@ func (c *Client) CreateChatCompletion(
 		return
 	}
 
-	req, err := c.requestBuilder.Build(ctx, http.MethodPost, c.fullURL(urlSuffix, request.Model), request)
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix, request.Model), withBody(request))
 	if err != nil {
 		return
 	}
diff --git a/chat_stream.go b/chat_stream.go
index 9093bde..9f4e80c 100644
--- a/chat_stream.go
+++ b/chat_stream.go
@@ -1,15 +1,14 @@
 package openai
 
 import (
-	"bufio"
 	"context"
-
-	utils "github.com/sashabaranov/go-openai/internal"
+	"net/http"
 )
 
 type ChatCompletionStreamChoiceDelta struct {
-	Content string `json:"content,omitempty"`
-	Role    string `json:"role,omitempty"`
+	Content      string        `json:"content,omitempty"`
+	Role         string        `json:"role,omitempty"`
+	FunctionCall *FunctionCall `json:"function_call,omitempty"`
 }
 
 type ChatCompletionStreamChoice struct {
@@ -47,27 +46,17 @@ func (c *Client) CreateChatCompletionStream(
 	}
 
 	request.Stream = true
-	req, err := c.newStreamRequest(ctx, "POST", urlSuffix, request, request.Model)
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix, request.Model), withBody(request))
 	if err != nil {
-		return
+		return nil, err
 	}
 
-	resp, err := c.config.HTTPClient.Do(req) //nolint:bodyclose // body is closed in stream.Close()
+	resp, err := sendRequestStream[ChatCompletionStreamResponse](c, req)
 	if err != nil {
 		return
 	}
-	if isFailureStatusCode(resp) {
-		return nil, c.handleErrorResp(resp)
-	}
-
 	stream = &ChatCompletionStream{
-		streamReader: &streamReader[ChatCompletionStreamResponse]{
-			emptyMessagesLimit: c.config.EmptyMessagesLimit,
-			reader:             bufio.NewReader(resp.Body),
-			response:           resp,
-			errAccumulator:     utils.NewErrorAccumulator(),
-			unmarshaler:        &utils.JSONUnmarshaler{},
-		},
+		streamReader: resp,
 	}
 	return
 }
diff --git a/client.go b/client.go
index f38c1df..5779a8e 100644
--- a/client.go
+++ b/client.go
@@ -1,6 +1,7 @@
 package openai
 
 import (
+	"bufio"
 	"context"
 	"encoding/json"
 	"fmt"
@@ -45,6 +46,42 @@ func NewOrgClient(authToken, org string) *Client {
 	return NewClientWithConfig(config)
 }
 
+type requestOptions struct {
+	body   any
+	header http.Header
+}
+
+type requestOption func(*requestOptions)
+
+func withBody(body any) requestOption {
+	return func(args *requestOptions) {
+		args.body = body
+	}
+}
+
+func withContentType(contentType string) requestOption {
+	return func(args *requestOptions) {
+		args.header.Set("Content-Type", contentType)
+	}
+}
+
+func (c *Client) newRequest(ctx context.Context, method, url string, setters ...requestOption) (*http.Request, error) {
+	// Default Options
+	args := &requestOptions{
+		body:   nil,
+		header: make(http.Header),
+	}
+	for _, setter := range setters {
+		setter(args)
+	}
+	req, err := c.requestBuilder.Build(ctx, method, url, args.body, args.header)
+	if err != nil {
+		return nil, err
+	}
+	c.setCommonHeaders(req)
+	return req, nil
+}
+
 func (c *Client) sendRequest(req *http.Request, v any) error {
 	req.Header.Set("Accept", "application/json; charset=utf-8")
 
@@ -55,8 +92,6 @@ func (c *Client) sendRequest(req *http.Request, v any) error {
 		req.Header.Set("Content-Type", "application/json; charset=utf-8")
 	}
 
-	c.setCommonHeaders(req)
-
 	res, err := c.config.HTTPClient.Do(req)
 	if err != nil {
 		return err
@@ -71,6 +106,41 @@ func (c *Client) sendRequest(req *http.Request, v any) error {
 	return decodeResponse(res.Body, v)
 }
 
+func (c *Client) sendRequestRaw(req *http.Request) (body io.ReadCloser, err error) {
+	resp, err := c.config.HTTPClient.Do(req)
+	if err != nil {
+		return
+	}
+
+	if isFailureStatusCode(resp) {
+		err = c.handleErrorResp(resp)
+		return
+	}
+	return resp.Body, nil
+}
+
+func sendRequestStream[T streamable](client *Client, req *http.Request) (*streamReader[T], error) {
+	req.Header.Set("Content-Type", "application/json")
+	req.Header.Set("Accept", "text/event-stream")
+	req.Header.Set("Cache-Control", "no-cache")
+	req.Header.Set("Connection", "keep-alive")
+
+	resp, err := client.config.HTTPClient.Do(req) //nolint:bodyclose // body is closed in stream.Close()
+	if err != nil {
+		return new(streamReader[T]), err
+	}
+	if isFailureStatusCode(resp) {
+		return new(streamReader[T]), client.handleErrorResp(resp)
+	}
+	return &streamReader[T]{
+		emptyMessagesLimit: client.config.EmptyMessagesLimit,
+		reader:             bufio.NewReader(resp.Body),
+		response:           resp,
+		errAccumulator:     utils.NewErrorAccumulator(),
+		unmarshaler:        &utils.JSONUnmarshaler{},
+	}, nil
+}
+
 func (c *Client) setCommonHeaders(req *http.Request) {
 	// https://learn.microsoft.com/en-us/azure/cognitive-services/openai/reference#authentication
 	// Azure API Key authentication
@@ -138,26 +208,6 @@ func (c *Client) fullURL(suffix string, args ...any) string {
 	return fmt.Sprintf("%s%s", c.config.BaseURL, suffix)
 }
 
-func (c *Client) newStreamRequest(
-	ctx context.Context,
-	method string,
-	urlSuffix string,
-	body any,
-	model string) (*http.Request, error) {
-	req, err := c.requestBuilder.Build(ctx, method, c.fullURL(urlSuffix, model), body)
-	if err != nil {
-		return nil, err
-	}
-
-	req.Header.Set("Content-Type", "application/json")
-	req.Header.Set("Accept", "text/event-stream")
-	req.Header.Set("Cache-Control", "no-cache")
-	req.Header.Set("Connection", "keep-alive")
-
-	c.setCommonHeaders(req)
-	return req, nil
-}
-
 func (c *Client) handleErrorResp(resp *http.Response) error {
 	var errRes ErrorResponse
 	err := json.NewDecoder(resp.Body).Decode(&errRes)
diff --git a/completion.go b/completion.go
index e0571b0..b3b3abd 100644
--- a/completion.go
+++ b/completion.go
@@ -165,7 +165,7 @@ func (c *Client) CreateCompletion(
 		return
 	}
 
-	req, err := c.requestBuilder.Build(ctx, http.MethodPost, c.fullURL(urlSuffix, request.Model), request)
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix, request.Model), withBody(request))
 	if err != nil {
 		return
 	}
diff --git a/edits.go b/edits.go
index 23b1a64..3d3fc89 100644
--- a/edits.go
+++ b/edits.go
@@ -32,7 +32,7 @@ type EditsResponse struct {
 
 // Perform an API call to the Edits endpoint.
 func (c *Client) Edits(ctx context.Context, request EditsRequest) (response EditsResponse, err error) {
-	req, err := c.requestBuilder.Build(ctx, http.MethodPost, c.fullURL("/edits", fmt.Sprint(request.Model)), request)
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/edits", fmt.Sprint(request.Model)), withBody(request))
 	if err != nil {
 		return
 	}
diff --git a/embeddings.go b/embeddings.go
index 942f3ea..ba327ce 100644
--- a/embeddings.go
+++ b/embeddings.go
@@ -132,7 +132,7 @@ type EmbeddingRequest struct {
 // CreateEmbeddings returns an EmbeddingResponse which will contain an Embedding for every item in |request.Input|.
 // https://beta.openai.com/docs/api-reference/embeddings/create
 func (c *Client) CreateEmbeddings(ctx context.Context, request EmbeddingRequest) (resp EmbeddingResponse, err error) {
-	req, err := c.requestBuilder.Build(ctx, http.MethodPost, c.fullURL("/embeddings", request.Model.String()), request)
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/embeddings", request.Model.String()), withBody(request))
 	if err != nil {
 		return
 	}
diff --git a/engines.go b/engines.go
index ac01a00..adf6025 100644
--- a/engines.go
+++ b/engines.go
@@ -22,7 +22,7 @@ type EnginesList struct {
 // ListEngines Lists the currently available engines, and provides basic
 // information about each option such as the owner and availability.
 func (c *Client) ListEngines(ctx context.Context) (engines EnginesList, err error) {
-	req, err := c.requestBuilder.Build(ctx, http.MethodGet, c.fullURL("/engines"), nil)
+	req, err := c.newRequest(ctx, http.MethodGet, c.fullURL("/engines"))
 	if err != nil {
 		return
 	}
@@ -38,7 +38,7 @@ func (c *Client) GetEngine(
 	engineID string,
 ) (engine Engine, err error) {
 	urlSuffix := fmt.Sprintf("/engines/%s", engineID)
-	req, err := c.requestBuilder.Build(ctx, http.MethodGet, c.fullURL(urlSuffix), nil)
+	req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix))
 	if err != nil {
 		return
 	}
diff --git a/files.go b/files.go
index fb9937b..ea1f50a 100644
--- a/files.go
+++ b/files.go
@@ -57,21 +57,19 @@ func (c *Client) CreateFile(ctx context.Context, request FileRequest) (file File
 		return
 	}
 
-	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.fullURL("/files"), &b)
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/files"),
+		withBody(&b), withContentType(builder.FormDataContentType()))
 	if err != nil {
 		return
 	}
 
-	req.Header.Set("Content-Type", builder.FormDataContentType())
-
 	err = c.sendRequest(req, &file)
-
 	return
 }
 
 // DeleteFile deletes an existing file.
 func (c *Client) DeleteFile(ctx context.Context, fileID string) (err error) {
-	req, err := c.requestBuilder.Build(ctx, http.MethodDelete, c.fullURL("/files/"+fileID), nil)
+	req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL("/files/"+fileID))
 	if err != nil {
 		return
 	}
@@ -83,7 +81,7 @@ func (c *Client) DeleteFile(ctx context.Context, fileID string) (err error) {
 // ListFiles Lists the currently available files,
 // and provides basic information about each file such as the file name and purpose.
 func (c *Client) ListFiles(ctx context.Context) (files FilesList, err error) {
-	req, err := c.requestBuilder.Build(ctx, http.MethodGet, c.fullURL("/files"), nil)
+	req, err := c.newRequest(ctx, http.MethodGet, c.fullURL("/files"))
 	if err != nil {
 		return
 	}
@@ -96,7 +94,7 @@ func (c *Client) ListFiles(ctx context.Context) (files FilesList, err error) {
 // such as the file name and purpose.
 func (c *Client) GetFile(ctx context.Context, fileID string) (file File, err error) {
 	urlSuffix := fmt.Sprintf("/files/%s", fileID)
-	req, err := c.requestBuilder.Build(ctx, http.MethodGet, c.fullURL(urlSuffix), nil)
+	req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix))
 	if err != nil {
 		return
 	}
@@ -107,23 +105,11 @@ func (c *Client) GetFile(ctx context.Context, fileID string) (file File, err err
 
 func (c *Client) GetFileContent(ctx context.Context, fileID string) (content io.ReadCloser, err error) {
 	urlSuffix := fmt.Sprintf("/files/%s/content", fileID)
-	req, err := c.requestBuilder.Build(ctx, http.MethodGet, c.fullURL(urlSuffix), nil)
-	if err != nil {
-		return
-	}
-
-	c.setCommonHeaders(req)
-
-	res, err := c.config.HTTPClient.Do(req)
+	req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix))
 	if err != nil {
 		return
 	}
 
-	if isFailureStatusCode(res) {
-		err = c.handleErrorResp(res)
-		return
-	}
-
-	content = res.Body
+	content, err = c.sendRequestRaw(req)
 	return
 }
diff --git a/fine_tunes.go b/fine_tunes.go
index 069ddcc..96e731d 100644
--- a/fine_tunes.go
+++ b/fine_tunes.go
@@ -68,7 +68,7 @@ type FineTuneDeleteResponse struct {
 
 func (c *Client) CreateFineTune(ctx context.Context, request FineTuneRequest) (response FineTune, err error) {
 	urlSuffix := "/fine-tunes"
-	req, err := c.requestBuilder.Build(ctx, http.MethodPost, c.fullURL(urlSuffix), request)
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request))
 	if err != nil {
 		return
 	}
@@ -79,7 +79,7 @@ func (c *Client) CreateFineTune(ctx context.Context, request FineTuneRequest) (r
 
 // CancelFineTune cancel a fine-tune job.
 func (c *Client) CancelFineTune(ctx context.Context, fineTuneID string) (response FineTune, err error) {
-	req, err := c.requestBuilder.Build(ctx, http.MethodPost, c.fullURL("/fine-tunes/"+fineTuneID+"/cancel"), nil)
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/fine-tunes/"+fineTuneID+"/cancel"))
 	if err != nil {
 		return
 	}
@@ -89,7 +89,7 @@ func (c *Client) CancelFineTune(ctx context.Context, fineTuneID string) (respons
 }
 
 func (c *Client) ListFineTunes(ctx context.Context) (response FineTuneList, err error) {
-	req, err := c.requestBuilder.Build(ctx, http.MethodGet, c.fullURL("/fine-tunes"), nil)
+	req, err := c.newRequest(ctx, http.MethodGet, c.fullURL("/fine-tunes"))
 	if err != nil {
 		return
 	}
@@ -100,7 +100,7 @@ func (c *Client) ListFineTunes(ctx context.Context) (response FineTuneList, err
 
 func (c *Client) GetFineTune(ctx context.Context, fineTuneID string) (response FineTune, err error) {
 	urlSuffix := fmt.Sprintf("/fine-tunes/%s", fineTuneID)
-	req, err := c.requestBuilder.Build(ctx, http.MethodGet, c.fullURL(urlSuffix), nil)
+	req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix))
 	if err != nil {
 		return
 	}
@@ -110,7 +110,7 @@ func (c *Client) GetFineTune(ctx context.Context, fineTuneID string) (response F
 }
 
 func (c *Client) DeleteFineTune(ctx context.Context, fineTuneID string) (response FineTuneDeleteResponse, err error) {
-	req, err := c.requestBuilder.Build(ctx, http.MethodDelete, c.fullURL("/fine-tunes/"+fineTuneID), nil)
+	req, err := c.newRequest(ctx, http.MethodDelete, c.fullURL("/fine-tunes/"+fineTuneID))
 	if err != nil {
 		return
 	}
@@ -120,7 +120,7 @@ func (c *Client) DeleteFineTune(ctx context.Context, fineTuneID string) (respons
 }
 
 func (c *Client) ListFineTuneEvents(ctx context.Context, fineTuneID string) (response FineTuneEventList, err error) {
-	req, err := c.requestBuilder.Build(ctx, http.MethodGet, c.fullURL("/fine-tunes/"+fineTuneID+"/events"), nil)
+	req, err := c.newRequest(ctx, http.MethodGet, c.fullURL("/fine-tunes/"+fineTuneID+"/events"))
 	if err != nil {
 		return
 	}
diff --git a/image.go b/image.go
index df73638..cb96f4f 100644
--- a/image.go
+++ b/image.go
@@ -44,7 +44,7 @@ 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.requestBuilder.Build(ctx, http.MethodPost, c.fullURL(urlSuffix), request)
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL(urlSuffix), withBody(request))
 	if err != nil {
 		return
 	}
@@ -107,13 +107,12 @@ func (c *Client) CreateEditImage(ctx context.Context, request ImageEditRequest)
 		return
 	}
 
-	urlSuffix := "/images/edits"
-	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.fullURL(urlSuffix), body)
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/images/edits"),
+		withBody(body), withContentType(builder.FormDataContentType()))
 	if err != nil {
 		return
 	}
 
-	req.Header.Set("Content-Type", builder.FormDataContentType())
 	err = c.sendRequest(req, &response)
 	return
 }
@@ -158,14 +157,12 @@ func (c *Client) CreateVariImage(ctx context.Context, request ImageVariRequest)
 		return
 	}
 
-	//https://platform.openai.com/docs/api-reference/images/create-variation
-	urlSuffix := "/images/variations"
-	req, err := http.NewRequestWithContext(ctx, http.MethodPost, c.fullURL(urlSuffix), body)
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/images/variations"),
+		withBody(body), withContentType(builder.FormDataContentType()))
 	if err != nil {
 		return
 	}
 
-	req.Header.Set("Content-Type", builder.FormDataContentType())
 	err = c.sendRequest(req, &response)
 	return
 }
diff --git a/internal/request_builder.go b/internal/request_builder.go
index 0a9eabf..5699f6b 100644
--- a/internal/request_builder.go
+++ b/internal/request_builder.go
@@ -3,11 +3,12 @@ package openai
 import (
 	"bytes"
 	"context"
+	"io"
 	"net/http"
 )
 
 type RequestBuilder interface {
-	Build(ctx context.Context, method, url string, request any) (*http.Request, error)
+	Build(ctx context.Context, method, url string, body any, header http.Header) (*http.Request, error)
 }
 
 type HTTPRequestBuilder struct {
@@ -20,21 +21,32 @@ func NewRequestBuilder() *HTTPRequestBuilder {
 	}
 }
 
-func (b *HTTPRequestBuilder) Build(ctx context.Context, method, url string, request any) (*http.Request, error) {
-	if request == nil {
-		return http.NewRequestWithContext(ctx, method, url, nil)
+func (b *HTTPRequestBuilder) Build(
+	ctx context.Context,
+	method string,
+	url string,
+	body any,
+	header http.Header,
+) (req *http.Request, err error) {
+	var bodyReader io.Reader
+	if body != nil {
+		if v, ok := body.(io.Reader); ok {
+			bodyReader = v
+		} else {
+			var reqBytes []byte
+			reqBytes, err = b.marshaller.Marshal(body)
+			if err != nil {
+				return
+			}
+			bodyReader = bytes.NewBuffer(reqBytes)
+		}
 	}
-
-	var reqBytes []byte
-	reqBytes, err := b.marshaller.Marshal(request)
+	req, err = http.NewRequestWithContext(ctx, method, url, bodyReader)
 	if err != nil {
-		return nil, err
+		return
 	}
-
-	return http.NewRequestWithContext(
-		ctx,
-		method,
-		url,
-		bytes.NewBuffer(reqBytes),
-	)
+	if header != nil {
+		req.Header = header
+	}
+	return
 }
diff --git a/models.go b/models.go
index b3d4583..560402e 100644
--- a/models.go
+++ b/models.go
@@ -41,7 +41,7 @@ type ModelsList struct {
 // ListModels Lists the currently available models,
 // and provides basic information about each model such as the model id and parent.
 func (c *Client) ListModels(ctx context.Context) (models ModelsList, err error) {
-	req, err := c.requestBuilder.Build(ctx, http.MethodGet, c.fullURL("/models"), nil)
+	req, err := c.newRequest(ctx, http.MethodGet, c.fullURL("/models"))
 	if err != nil {
 		return
 	}
@@ -54,7 +54,7 @@ func (c *Client) ListModels(ctx context.Context) (models ModelsList, err error)
 // the model such as the owner and permissioning.
 func (c *Client) GetModel(ctx context.Context, modelID string) (model Model, err error) {
 	urlSuffix := fmt.Sprintf("/models/%s", modelID)
-	req, err := c.requestBuilder.Build(ctx, http.MethodGet, c.fullURL(urlSuffix), nil)
+	req, err := c.newRequest(ctx, http.MethodGet, c.fullURL(urlSuffix))
 	if err != nil {
 		return
 	}
diff --git a/moderation.go b/moderation.go
index bae7880..a58d759 100644
--- a/moderation.go
+++ b/moderation.go
@@ -63,7 +63,7 @@ type ModerationResponse struct {
 // Moderations — perform a moderation api call over a string.
 // Input can be an array or slice but a string will reduce the complexity.
 func (c *Client) Moderations(ctx context.Context, request ModerationRequest) (response ModerationResponse, err error) {
-	req, err := c.requestBuilder.Build(ctx, http.MethodPost, c.fullURL("/moderations", request.Model), request)
+	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/moderations", request.Model), withBody(&request))
 	if err != nil {
 		return
 	}
diff --git a/stream.go b/stream.go
index 94cc0a0..b277f3c 100644
--- a/stream.go
+++ b/stream.go
@@ -1,11 +1,8 @@
 package openai
 
 import (
-	"bufio"
 	"context"
 	"errors"
-
-	utils "github.com/sashabaranov/go-openai/internal"
 )
 
 var (
@@ -36,27 +33,17 @@ func (c *Client) CreateCompletionStream(
 	}
 
 	request.Stream = true
-	req, err := c.newStreamRequest(ctx, "POST", urlSuffix, request, request.Model)
+	req, err := c.newRequest(ctx, "POST", c.fullURL(urlSuffix, request.Model), withBody(request))
 	if err != nil {
-		return
+		return nil, err
 	}
 
-	resp, err := c.config.HTTPClient.Do(req) //nolint:bodyclose // body is closed in stream.Close()
+	resp, err := sendRequestStream[CompletionResponse](c, req)
 	if err != nil {
 		return
 	}
-	if isFailureStatusCode(resp) {
-		return nil, c.handleErrorResp(resp)
-	}
-
 	stream = &CompletionStream{
-		streamReader: &streamReader[CompletionResponse]{
-			emptyMessagesLimit: c.config.EmptyMessagesLimit,
-			reader:             bufio.NewReader(resp.Body),
-			response:           resp,
-			errAccumulator:     utils.NewErrorAccumulator(),
-			unmarshaler:        &utils.JSONUnmarshaler{},
-		},
+		streamReader: resp,
 	}
 	return
 }
diff --git a/stream_reader.go b/stream_reader.go
index 3416198..87e59e0 100644
--- a/stream_reader.go
+++ b/stream_reader.go
@@ -10,6 +10,11 @@ import (
 	utils "github.com/sashabaranov/go-openai/internal"
 )
 
+var (
+	headerData  = []byte("data: ")
+	errorPrefix = []byte(`data: {"error":`)
+)
+
 type streamable interface {
 	ChatCompletionStreamResponse | CompletionResponse
 }
@@ -34,12 +39,16 @@ func (stream *streamReader[T]) Recv() (response T, err error) {
 	return
 }
 
+//nolint:gocognit
 func (stream *streamReader[T]) processLines() (T, error) {
-	var emptyMessagesCount uint
+	var (
+		emptyMessagesCount uint
+		hasErrorPrefix     bool
+	)
 
 	for {
 		rawLine, readErr := stream.reader.ReadBytes('\n')
-		if readErr != nil {
+		if readErr != nil || hasErrorPrefix {
 			respErr := stream.unmarshalError()
 			if respErr != nil {
 				return *new(T), fmt.Errorf("error, %w", respErr.Error)
@@ -47,9 +56,14 @@ func (stream *streamReader[T]) processLines() (T, error) {
 			return *new(T), readErr
 		}
 
-		var headerData = []byte("data: ")
 		noSpaceLine := bytes.TrimSpace(rawLine)
-		if !bytes.HasPrefix(noSpaceLine, headerData) {
+		if bytes.HasPrefix(noSpaceLine, errorPrefix) {
+			hasErrorPrefix = true
+		}
+		if !bytes.HasPrefix(noSpaceLine, headerData) || hasErrorPrefix {
+			if hasErrorPrefix {
+				noSpaceLine = bytes.TrimPrefix(noSpaceLine, headerData)
+			}
 			writeErr := stream.errAccumulator.Write(noSpaceLine)
 			if writeErr != nil {
 				return *new(T), writeErr


Test patch:
diff --git a/api_internal_test.go b/api_internal_test.go
index 214b627..0fb0f89 100644
--- a/api_internal_test.go
+++ b/api_internal_test.go
@@ -94,7 +94,7 @@ func TestRequestAuthHeader(t *testing.T) {
 			az.OrgID = c.OrgID
 
 			cli := NewClientWithConfig(az)
-			req, err := cli.newStreamRequest(context.Background(), "POST", "/chat/completions", nil, "")
+			req, err := cli.newRequest(context.Background(), "POST", "/chat/completions")
 			if err != nil {
 				t.Errorf("Failed to create request: %v", err)
 			}
diff --git a/chat_stream_test.go b/chat_stream_test.go
index c3cb9f3..5fc70b0 100644
--- a/chat_stream_test.go
+++ b/chat_stream_test.go
@@ -178,6 +178,45 @@ func TestCreateChatCompletionStreamError(t *testing.T) {
 	t.Logf("%+v\n", apiErr)
 }
 
+func TestCreateChatCompletionStreamErrorWithDataPrefix(t *testing.T) {
+	client, server, teardown := setupOpenAITestServer()
+	defer teardown()
+	server.RegisterHandler("/v1/chat/completions", func(w http.ResponseWriter, r *http.Request) {
+		w.Header().Set("Content-Type", "text/event-stream")
+
+		// Send test responses
+		//nolint:lll
+		dataBytes := []byte(`data: {"error":{"message":"The server had an error while processing your request. Sorry about that!", "type":"server_ error", "param":null,"code":null}}`)
+		dataBytes = append(dataBytes, []byte("\n\ndata: [DONE]\n\n")...)
+
+		_, err := w.Write(dataBytes)
+		checks.NoError(t, err, "Write error")
+	})
+
+	stream, err := client.CreateChatCompletionStream(context.Background(), ChatCompletionRequest{
+		MaxTokens: 5,
+		Model:     GPT3Dot5Turbo,
+		Messages: []ChatCompletionMessage{
+			{
+				Role:    ChatMessageRoleUser,
+				Content: "Hello!",
+			},
+		},
+		Stream: true,
+	})
+	checks.NoError(t, err, "CreateCompletionStream returned error")
+	defer stream.Close()
+
+	_, streamErr := stream.Recv()
+	checks.HasError(t, streamErr, "stream.Recv() did not return error")
+
+	var apiErr *APIError
+	if !errors.As(streamErr, &apiErr) {
+		t.Errorf("stream.Recv() did not return APIError")
+	}
+	t.Logf("%+v\n", apiErr)
+}
+
 func TestCreateChatCompletionStreamRateLimitError(t *testing.T) {
 	client, server, teardown := setupOpenAITestServer()
 	defer teardown()
diff --git a/chat_test.go b/chat_test.go
index a43bb4a..3c759b3 100644
--- a/chat_test.go
+++ b/chat_test.go
@@ -67,6 +67,130 @@ func TestChatCompletions(t *testing.T) {
 	checks.NoError(t, err, "CreateChatCompletion error")
 }
 
+// TestChatCompletionsFunctions tests including a function call.
+func TestChatCompletionsFunctions(t *testing.T) {
+	client, server, teardown := setupOpenAITestServer()
+	defer teardown()
+	server.RegisterHandler("/v1/chat/completions", handleChatCompletionEndpoint)
+	t.Run("bytes", func(t *testing.T) {
+		//nolint:lll
+		msg := json.RawMessage(`{"properties":{"count":{"type":"integer","description":"total number of words in sentence"},"words":{"items":{"type":"string"},"type":"array","description":"list of words in sentence"}},"type":"object","required":["count","words"]}`)
+		_, err := client.CreateChatCompletion(context.Background(), ChatCompletionRequest{
+			MaxTokens: 5,
+			Model:     GPT3Dot5Turbo0613,
+			Messages: []ChatCompletionMessage{
+				{
+					Role:    ChatMessageRoleUser,
+					Content: "Hello!",
+				},
+			},
+			Functions: []FunctionDefine{{
+				Name:       "test",
+				Parameters: &msg,
+			}},
+		})
+		checks.NoError(t, err, "CreateChatCompletion with functions error")
+	})
+	t.Run("struct", 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(), ChatCompletionRequest{
+			MaxTokens: 5,
+			Model:     GPT3Dot5Turbo0613,
+			Messages: []ChatCompletionMessage{
+				{
+					Role:    ChatMessageRoleUser,
+					Content: "Hello!",
+				},
+			},
+			Functions: []FunctionDefinition{{
+				Name:       "test",
+				Parameters: &msg,
+			}},
+		})
+		checks.NoError(t, err, "CreateChatCompletion with functions error")
+	})
+	t.Run("JSONSchemaDefine", func(t *testing.T) {
+		_, err := client.CreateChatCompletion(context.Background(), ChatCompletionRequest{
+			MaxTokens: 5,
+			Model:     GPT3Dot5Turbo0613,
+			Messages: []ChatCompletionMessage{
+				{
+					Role:    ChatMessageRoleUser,
+					Content: "Hello!",
+				},
+			},
+			Functions: []FunctionDefinition{{
+				Name: "test",
+				Parameters: &JSONSchemaDefinition{
+					Type: JSONSchemaTypeObject,
+					Properties: map[string]JSONSchemaDefinition{
+						"count": {
+							Type:        JSONSchemaTypeNumber,
+							Description: "total number of words in sentence",
+						},
+						"words": {
+							Type:        JSONSchemaTypeArray,
+							Description: "list of words in sentence",
+							Items: &JSONSchemaDefinition{
+								Type: JSONSchemaTypeString,
+							},
+						},
+						"enumTest": {
+							Type: JSONSchemaTypeString,
+							Enum: []string{"hello", "world"},
+						},
+					},
+				},
+			}},
+		})
+		checks.NoError(t, err, "CreateChatCompletion with functions error")
+	})
+	t.Run("JSONSchemaDefineWithFunctionDefine", func(t *testing.T) {
+		// this is a compatibility check
+		_, err := client.CreateChatCompletion(context.Background(), ChatCompletionRequest{
+			MaxTokens: 5,
+			Model:     GPT3Dot5Turbo0613,
+			Messages: []ChatCompletionMessage{
+				{
+					Role:    ChatMessageRoleUser,
+					Content: "Hello!",
+				},
+			},
+			Functions: []FunctionDefine{{
+				Name: "test",
+				Parameters: &JSONSchemaDefine{
+					Type: JSONSchemaTypeObject,
+					Properties: map[string]JSONSchemaDefine{
+						"count": {
+							Type:        JSONSchemaTypeNumber,
+							Description: "total number of words in sentence",
+						},
+						"words": {
+							Type:        JSONSchemaTypeArray,
+							Description: "list of words in sentence",
+							Items: &JSONSchemaDefine{
+								Type: JSONSchemaTypeString,
+							},
+						},
+						"enumTest": {
+							Type: JSONSchemaTypeString,
+							Enum: []string{"hello", "world"},
+						},
+					},
+				},
+			}},
+		})
+		checks.NoError(t, err, "CreateChatCompletion with functions error")
+	})
+}
+
 func TestAzureChatCompletions(t *testing.T) {
 	client, server, teardown := setupAzureTestServer()
 	defer teardown()
@@ -109,7 +233,34 @@ func handleChatCompletionEndpoint(w http.ResponseWriter, r *http.Request) {
 		Model: completionReq.Model,
 	}
 	// create completions
-	for i := 0; i < completionReq.N; i++ {
+	n := completionReq.N
+	if n == 0 {
+		n = 1
+	}
+	for i := 0; i < n; i++ {
+		// if there are functions, include them
+		if len(completionReq.Functions) > 0 {
+			var fcb []byte
+			b := completionReq.Functions[0].Parameters
+			fcb, err = json.Marshal(b)
+			if err != nil {
+				http.Error(w, "could not marshal function parameters", http.StatusInternalServerError)
+				return
+			}
+
+			res.Choices = append(res.Choices, ChatCompletionChoice{
+				Message: ChatCompletionMessage{
+					Role: ChatMessageRoleFunction,
+					// this is valid json so it should be fine
+					FunctionCall: &FunctionCall{
+						Name:      completionReq.Functions[0].Name,
+						Arguments: string(fcb),
+					},
+				},
+				Index: i,
+			})
+			continue
+		}
 		// generate a random string of length completionReq.Length
 		completionStr := strings.Repeat("a", completionReq.MaxTokens)
 
@@ -121,8 +272,8 @@ func handleChatCompletionEndpoint(w http.ResponseWriter, r *http.Request) {
 			Index: i,
 		})
 	}
-	inputTokens := numTokens(completionReq.Messages[0].Content) * completionReq.N
-	completionTokens := completionReq.MaxTokens * completionReq.N
+	inputTokens := numTokens(completionReq.Messages[0].Content) * n
+	completionTokens := completionReq.MaxTokens * n
 	res.Usage = Usage{
 		PromptTokens:     inputTokens,
 		CompletionTokens: completionTokens,
diff --git a/client_test.go b/client_test.go
index 00b66fe..29d84ed 100644
--- a/client_test.go
+++ b/client_test.go
@@ -16,7 +16,7 @@ var errTestRequestBuilderFailed = errors.New("test request builder failed")
 
 type failingRequestBuilder struct{}
 
-func (*failingRequestBuilder) Build(_ context.Context, _, _ string, _ any) (*http.Request, error) {
+func (*failingRequestBuilder) Build(_ context.Context, _, _ string, _ any, _ http.Header) (*http.Request, error) {
 	return nil, errTestRequestBuilderFailed
 }
 
@@ -41,9 +41,10 @@ func TestDecodeResponse(t *testing.T) {
 	stringInput := ""
 
 	testCases := []struct {
-		name  string
-		value interface{}
-		body  io.Reader
+		name     string
+		value    interface{}
+		body     io.Reader
+		hasError bool
 	}{
 		{
 			name:  "nil input",
@@ -60,18 +61,32 @@ func TestDecodeResponse(t *testing.T) {
 			value: &map[string]interface{}{},
 			body:  bytes.NewReader([]byte(`{"test": "test"}`)),
 		},
+		{
+			name:     "reader return error",
+			value:    &stringInput,
+			body:     &errorReader{err: errors.New("dummy")},
+			hasError: true,
+		},
 	}
 
 	for _, tc := range testCases {
 		t.Run(tc.name, func(t *testing.T) {
 			err := decodeResponse(tc.body, tc.value)
-			if err != nil {
+			if (err != nil) != tc.hasError {
 				t.Errorf("Unexpected error: %v", err)
 			}
 		})
 	}
 }
 
+type errorReader struct {
+	err error
+}
+
+func (e *errorReader) Read(_ []byte) (n int, err error) {
+	return 0, e.err
+}
+
 func TestHandleErrorResp(t *testing.T) {
 	// var errRes *ErrorResponse
 	var errRes ErrorResponse
diff --git a/completion_test.go b/completion_test.go
index aeddcfc..844ef48 100644
--- a/completion_test.go
+++ b/completion_test.go
@@ -83,7 +83,11 @@ func handleCompletionEndpoint(w http.ResponseWriter, r *http.Request) {
 		Model: completionReq.Model,
 	}
 	// create completions
-	for i := 0; i < completionReq.N; i++ {
+	n := completionReq.N
+	if n == 0 {
+		n = 1
+	}
+	for i := 0; i < n; i++ {
 		// generate a random string of length completionReq.Length
 		completionStr := strings.Repeat("a", completionReq.MaxTokens)
 		if completionReq.Echo {
@@ -94,8 +98,8 @@ func handleCompletionEndpoint(w http.ResponseWriter, r *http.Request) {
 			Index: i,
 		})
 	}
-	inputTokens := numTokens(completionReq.Prompt.(string)) * completionReq.N
-	completionTokens := completionReq.MaxTokens * completionReq.N
+	inputTokens := numTokens(completionReq.Prompt.(string)) * n
+	completionTokens := completionReq.MaxTokens * n
 	res.Usage = Usage{
 		PromptTokens:     inputTokens,
 		CompletionTokens: completionTokens,
diff --git a/internal/request_builder_test.go b/internal/request_builder_test.go
index e47d0f6..e26022a 100644
--- a/internal/request_builder_test.go
+++ b/internal/request_builder_test.go
@@ -22,7 +22,7 @@ func TestRequestBuilderReturnsMarshallerErrors(t *testing.T) {
 		marshaller: &failingMarshaller{},
 	}
 
-	_, err := builder.Build(context.Background(), "", "", struct{}{})
+	_, err := builder.Build(context.Background(), "", "", struct{}{}, nil)
 	if !errors.Is(err, errTestMarshallerFailed) {
 		t.Fatalf("Did not return error when marshaller failed: %v", err)
 	}
@@ -38,7 +38,7 @@ func TestRequestBuilderReturnsRequest(t *testing.T) {
 		reqBytes, _ = b.marshaller.Marshal(request)
 		want, _     = http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(reqBytes))
 	)
-	got, _ := b.Build(ctx, method, url, request)
+	got, _ := b.Build(ctx, method, url, request, nil)
 	if !reflect.DeepEqual(got.Body, want.Body) ||
 		!reflect.DeepEqual(got.URL, want.URL) ||
 		!reflect.DeepEqual(got.Method, want.Method) {
@@ -54,7 +54,7 @@ func TestRequestBuilderReturnsRequestWhenRequestOfArgsIsNil(t *testing.T) {
 		want, _ = http.NewRequestWithContext(ctx, method, url, nil)
 	)
 	b := NewRequestBuilder()
-	got, _ := b.Build(ctx, method, url, nil)
+	got, _ := b.Build(ctx, method, url, nil, nil)
 	if !reflect.DeepEqual(got, want) {
 		t.Errorf("Build() got = %v, want %v", got, want)
 	}
diff --git a/models_test.go b/models_test.go
index 0b4daf4..59b4f5e 100644
--- a/models_test.go
+++ b/models_test.go
@@ -1,6 +1,9 @@
 package openai_test
 
 import (
+	"os"
+	"time"
+
 	. "github.com/sashabaranov/go-openai"
 	"github.com/sashabaranov/go-openai/internal/test/checks"
 
@@ -56,3 +59,22 @@ func handleGetModelEndpoint(w http.ResponseWriter, _ *http.Request) {
 	resBytes, _ := json.Marshal(Model{})
 	fmt.Fprintln(w, string(resBytes))
 }
+
+func TestGetModelReturnTimeoutError(t *testing.T) {
+	client, server, teardown := setupOpenAITestServer()
+	defer teardown()
+	server.RegisterHandler("/v1/models/text-davinci-003", func(w http.ResponseWriter, r *http.Request) {
+		time.Sleep(10 * time.Nanosecond)
+	})
+	ctx := context.Background()
+	ctx, cancel := context.WithTimeout(ctx, time.Nanosecond)
+	defer cancel()
+
+	_, err := client.GetModel(ctx, "text-davinci-003")
+	if err == nil {
+		t.Fatal("Did not return error")
+	}
+	if !os.IsTimeout(err) {
+		t.Fatal("Did not return timeout error")
+	}
+}
diff --git a/stream_test.go b/stream_test.go
index 5997f27..f3f8f85 100644
--- a/stream_test.go
+++ b/stream_test.go
@@ -6,7 +6,9 @@ import (
 	"errors"
 	"io"
 	"net/http"
+	"os"
 	"testing"
+	"time"
 
 	. "github.com/sashabaranov/go-openai"
 	"github.com/sashabaranov/go-openai/internal/test/checks"
@@ -300,6 +302,30 @@ func TestCreateCompletionStreamBrokenJSONError(t *testing.T) {
 	}
 }
 
+func TestCreateCompletionStreamReturnTimeoutError(t *testing.T) {
+	client, server, teardown := setupOpenAITestServer()
+	defer teardown()
+	server.RegisterHandler("/v1/completions", func(w http.ResponseWriter, r *http.Request) {
+		time.Sleep(10 * time.Nanosecond)
+	})
+	ctx := context.Background()
+	ctx, cancel := context.WithTimeout(ctx, time.Nanosecond)
+	defer cancel()
+
+	_, err := client.CreateCompletionStream(ctx, CompletionRequest{
+		Prompt:    "Ex falso quodlibet",
+		Model:     "text-davinci-002",
+		MaxTokens: 10,
+		Stream:    true,
+	})
+	if err == nil {
+		t.Fatal("Did not return error")
+	}
+	if !os.IsTimeout(err) {
+		t.Fatal("Did not return timeout error")
+	}
+}
+
 // Helper funcs.
 func compareResponses(r1, r2 CompletionResponse) bool {
 	if r1.ID != r2.ID || r1.Object != r2.Object || r1.Created != r2.Created || r1.Model != r2.Model {