Repository: sashabaranov/go-openai
Pull request #331 (2023-05-28 12:47:17)

Implement optional io.Reader in AudioRequest (#303) (#265)
This change adds an optional `io.Reader` to the `AudioRequest` structure. This addition makes this structure more versatile and allows you to use a different source other than an existing file from your filesystem. It also maintaining backwards compatibility.

I'd be glad to implement the same logic for the other Request types (`FileRequest`, `ImageEditRequest`, `ImageVariRequest`) in preferably subsequent PR's.


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/sashabaranov/go-openai/pull/331?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#331](https://app.codecov.io/gh/sashabaranov/go-openai/pull/331?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (dcb9e59) into [master](https://app.codecov.io/gh/sashabaranov/go-openai/commit/62eb4beed29f4e821a59088f043fb56761d79fa7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (62eb4be) will **decrease** coverage by `0.69%`.
> The diff coverage is `50.00%`.

```diff
@@            Coverage Diff             @@
##           master     #331      +/-   ##
==========================================
- Coverage   92.36%   91.67%   -0.69%     
==========================================
  Files          19       19              
  Lines         655      661       +6     
==========================================
+ Hits          605      606       +1     
- Misses         37       41       +4     
- Partials       13       14       +1     
```


| [Impacted Files](https://app.codecov.io/gh/sashabaranov/go-openai/pull/331?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [audio.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/331?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-YXVkaW8uZ28=) | `76.05% <50.00%> (-5.49%)` | :arrow_down: |


0xNF: It'd be nice to include this feature in the Files Upload, Image Create, Image Edit, and Image Variation endpoints as well

Patch:
diff --git a/audio.go b/audio.go
index bf23653..20e865f 100644
--- a/audio.go
+++ b/audio.go
@@ -4,6 +4,7 @@ import (
 	"bytes"
 	"context"
 	"fmt"
+	"io"
 	"net/http"
 	"os"
 
@@ -27,8 +28,14 @@ const (
 // AudioRequest represents a request structure for audio API.
 // ResponseFormat is not supported for now. We only return JSON text, which may be sufficient.
 type AudioRequest struct {
-	Model       string
-	FilePath    string
+	Model string
+
+	// FilePath is either an existing file in your filesystem or a filename representing the contents of Reader.
+	FilePath string
+
+	// Reader is an optional io.Reader when you do not want to use an existing file.
+	Reader io.Reader
+
 	Prompt      string // For translation, it should be in English
 	Temperature float32
 	Language    string // For translation, just do not use it. It seems "en" works, not confirmed...
@@ -95,15 +102,9 @@ func (r AudioRequest) HasJSONResponse() bool {
 // audioMultipartForm creates a form with audio file contents and the name of the model to use for
 // audio processing.
 func audioMultipartForm(request AudioRequest, b utils.FormBuilder) error {
-	f, err := os.Open(request.FilePath)
-	if err != nil {
-		return fmt.Errorf("opening audio file: %w", err)
-	}
-	defer f.Close()
-
-	err = b.CreateFormFile("file", f)
+	err := createFileField(request, b)
 	if err != nil {
-		return fmt.Errorf("creating form file: %w", err)
+		return err
 	}
 
 	err = b.WriteField("model", request.Model)
@@ -146,3 +147,27 @@ func audioMultipartForm(request AudioRequest, b utils.FormBuilder) error {
 	// Close the multipart writer
 	return b.Close()
 }
+
+// createFileField creates the "file" form field from either an existing file or by using the reader.
+func createFileField(request AudioRequest, b utils.FormBuilder) error {
+	if request.Reader != nil {
+		err := b.CreateFormFileReader("file", request.Reader, request.FilePath)
+		if err != nil {
+			return fmt.Errorf("creating form using reader: %w", err)
+		}
+		return nil
+	}
+
+	f, err := os.Open(request.FilePath)
+	if err != nil {
+		return fmt.Errorf("opening audio file: %w", err)
+	}
+	defer f.Close()
+
+	err = b.CreateFormFile("file", f)
+	if err != nil {
+		return fmt.Errorf("creating form file: %w", err)
+	}
+
+	return nil
+}
diff --git a/chat.go b/chat.go
index 312ef8e..a7ce548 100644
--- a/chat.go
+++ b/chat.go
@@ -77,7 +77,7 @@ func (c *Client) CreateChatCompletion(
 		return
 	}
 
-	req, err := c.requestBuilder.build(ctx, http.MethodPost, c.fullURL(urlSuffix, request.Model), request)
+	req, err := c.requestBuilder.Build(ctx, http.MethodPost, c.fullURL(urlSuffix, request.Model), request)
 	if err != nil {
 		return
 	}
diff --git a/client.go b/client.go
index c55166a..2486e36 100644
--- a/client.go
+++ b/client.go
@@ -15,7 +15,7 @@ import (
 type Client struct {
 	config ClientConfig
 
-	requestBuilder    requestBuilder
+	requestBuilder    utils.RequestBuilder
 	createFormBuilder func(io.Writer) utils.FormBuilder
 }
 
@@ -29,7 +29,7 @@ func NewClient(authToken string) *Client {
 func NewClientWithConfig(config ClientConfig) *Client {
 	return &Client{
 		config:         config,
-		requestBuilder: newRequestBuilder(),
+		requestBuilder: utils.NewRequestBuilder(),
 		createFormBuilder: func(body io.Writer) utils.FormBuilder {
 			return utils.NewFormBuilder(body)
 		},
@@ -135,7 +135,7 @@ func (c *Client) newStreamRequest(
 	urlSuffix string,
 	body any,
 	model string) (*http.Request, error) {
-	req, err := c.requestBuilder.build(ctx, method, c.fullURL(urlSuffix, model), body)
+	req, err := c.requestBuilder.Build(ctx, method, c.fullURL(urlSuffix, model), body)
 	if err != nil {
 		return nil, err
 	}
diff --git a/completion.go b/completion.go
index e3d1b85..de1360f 100644
--- a/completion.go
+++ b/completion.go
@@ -155,7 +155,7 @@ func (c *Client) CreateCompletion(
 		return
 	}
 
-	req, err := c.requestBuilder.build(ctx, http.MethodPost, c.fullURL(urlSuffix, request.Model), request)
+	req, err := c.requestBuilder.Build(ctx, http.MethodPost, c.fullURL(urlSuffix, request.Model), request)
 	if err != nil {
 		return
 	}
diff --git a/edits.go b/edits.go
index c2c8db7..23b1a64 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.requestBuilder.Build(ctx, http.MethodPost, c.fullURL("/edits", fmt.Sprint(request.Model)), request)
 	if err != nil {
 		return
 	}
diff --git a/embeddings.go b/embeddings.go
index 7fb432e..942f3ea 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.requestBuilder.Build(ctx, http.MethodPost, c.fullURL("/embeddings", request.Model.String()), request)
 	if err != nil {
 		return
 	}
diff --git a/engines.go b/engines.go
index bb6a66c..ac01a00 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.requestBuilder.Build(ctx, http.MethodGet, c.fullURL("/engines"), nil)
 	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.requestBuilder.Build(ctx, http.MethodGet, c.fullURL(urlSuffix), nil)
 	if err != nil {
 		return
 	}
diff --git a/files.go b/files.go
index 5667ec8..36c0243 100644
--- a/files.go
+++ b/files.go
@@ -70,7 +70,7 @@ func (c *Client) CreateFile(ctx context.Context, request FileRequest) (file File
 
 // 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.requestBuilder.Build(ctx, http.MethodDelete, c.fullURL("/files/"+fileID), nil)
 	if err != nil {
 		return
 	}
@@ -82,7 +82,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.requestBuilder.Build(ctx, http.MethodGet, c.fullURL("/files"), nil)
 	if err != nil {
 		return
 	}
@@ -95,7 +95,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.requestBuilder.Build(ctx, http.MethodGet, c.fullURL(urlSuffix), nil)
 	if err != nil {
 		return
 	}
diff --git a/fine_tunes.go b/fine_tunes.go
index a121867..069ddcc 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.requestBuilder.Build(ctx, http.MethodPost, c.fullURL(urlSuffix), 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.requestBuilder.Build(ctx, http.MethodPost, c.fullURL("/fine-tunes/"+fineTuneID+"/cancel"), nil)
 	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.requestBuilder.Build(ctx, http.MethodGet, c.fullURL("/fine-tunes"), nil)
 	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.requestBuilder.Build(ctx, http.MethodGet, c.fullURL(urlSuffix), nil)
 	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.requestBuilder.Build(ctx, http.MethodDelete, c.fullURL("/fine-tunes/"+fineTuneID), nil)
 	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.requestBuilder.Build(ctx, http.MethodGet, c.fullURL("/fine-tunes/"+fineTuneID+"/events"), nil)
 	if err != nil {
 		return
 	}
diff --git a/image.go b/image.go
index 87ffea2..df73638 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.requestBuilder.Build(ctx, http.MethodPost, c.fullURL(urlSuffix), request)
 	if err != nil {
 		return
 	}
diff --git a/internal/form_builder.go b/internal/form_builder.go
index 359dd7e..2224fad 100644
--- a/internal/form_builder.go
+++ b/internal/form_builder.go
@@ -1,13 +1,16 @@
 package openai
 
 import (
+	"fmt"
 	"io"
 	"mime/multipart"
 	"os"
+	"path"
 )
 
 type FormBuilder interface {
 	CreateFormFile(fieldname string, file *os.File) error
+	CreateFormFileReader(fieldname string, r io.Reader, filename string) error
 	WriteField(fieldname, value string) error
 	Close() error
 	FormDataContentType() string
@@ -24,15 +27,28 @@ func NewFormBuilder(body io.Writer) *DefaultFormBuilder {
 }
 
 func (fb *DefaultFormBuilder) CreateFormFile(fieldname string, file *os.File) error {
-	fieldWriter, err := fb.writer.CreateFormFile(fieldname, file.Name())
+	return fb.createFormFile(fieldname, file, file.Name())
+}
+
+func (fb *DefaultFormBuilder) CreateFormFileReader(fieldname string, r io.Reader, filename string) error {
+	return fb.createFormFile(fieldname, r, path.Base(filename))
+}
+
+func (fb *DefaultFormBuilder) createFormFile(fieldname string, r io.Reader, filename string) error {
+	if filename == "" {
+		return fmt.Errorf("filename cannot be empty")
+	}
+
+	fieldWriter, err := fb.writer.CreateFormFile(fieldname, filename)
 	if err != nil {
 		return err
 	}
 
-	_, err = io.Copy(fieldWriter, file)
+	_, err = io.Copy(fieldWriter, r)
 	if err != nil {
 		return err
 	}
+
 	return nil
 }
 
diff --git a/request_builder.go b/internal/request_builder.go
similarity index 52%
rename from request_builder.go
rename to internal/request_builder.go
index b4db07c..0a9eabf 100644
--- a/request_builder.go
+++ b/internal/request_builder.go
@@ -4,25 +4,23 @@ import (
 	"bytes"
 	"context"
 	"net/http"
-
-	utils "github.com/sashabaranov/go-openai/internal"
 )
 
-type requestBuilder interface {
-	build(ctx context.Context, method, url string, request any) (*http.Request, error)
+type RequestBuilder interface {
+	Build(ctx context.Context, method, url string, request any) (*http.Request, error)
 }
 
-type httpRequestBuilder struct {
-	marshaller utils.Marshaller
+type HTTPRequestBuilder struct {
+	marshaller Marshaller
 }
 
-func newRequestBuilder() *httpRequestBuilder {
-	return &httpRequestBuilder{
-		marshaller: &utils.JSONMarshaller{},
+func NewRequestBuilder() *HTTPRequestBuilder {
+	return &HTTPRequestBuilder{
+		marshaller: &JSONMarshaller{},
 	}
 }
 
-func (b *httpRequestBuilder) build(ctx context.Context, method, url string, request any) (*http.Request, error) {
+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)
 	}
diff --git a/models.go b/models.go
index 2be91aa..485433b 100644
--- a/models.go
+++ b/models.go
@@ -40,7 +40,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.requestBuilder.Build(ctx, http.MethodGet, c.fullURL("/models"), nil)
 	if err != nil {
 		return
 	}
diff --git a/moderation.go b/moderation.go
index ebd66af..bae7880 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.requestBuilder.Build(ctx, http.MethodPost, c.fullURL("/moderations", request.Model), request)
 	if err != nil {
 		return
 	}


Test patch:
diff --git a/audio_test.go b/audio_test.go
index daf51f2..6452e2e 100644
--- a/audio_test.go
+++ b/audio_test.go
@@ -2,6 +2,7 @@ package openai //nolint:testpackage // testing private field
 
 import (
 	"bytes"
+	"context"
 	"errors"
 	"fmt"
 	"io"
@@ -11,12 +12,10 @@ import (
 	"os"
 	"path/filepath"
 	"strings"
+	"testing"
 
 	"github.com/sashabaranov/go-openai/internal/test"
 	"github.com/sashabaranov/go-openai/internal/test/checks"
-
-	"context"
-	"testing"
 )
 
 // TestAudio Tests the transcription and translation endpoints of the API using the mocked server.
@@ -65,6 +64,16 @@ func TestAudio(t *testing.T) {
 			_, err = tc.createFn(ctx, req)
 			checks.NoError(t, err, "audio API error")
 		})
+
+		t.Run(tc.name+" (with reader)", func(t *testing.T) {
+			req := AudioRequest{
+				FilePath: "fake.webm",
+				Reader:   bytes.NewBuffer([]byte(`some webm binary data`)),
+				Model:    "whisper-3",
+			}
+			_, err = tc.createFn(ctx, req)
+			checks.NoError(t, err, "audio API error")
+		})
 	}
 }
 
@@ -213,3 +222,54 @@ func TestAudioWithFailingFormBuilder(t *testing.T) {
 		checks.ErrorIs(t, err, mockFailedErr, "audioMultipartForm should return error if form builder fails")
 	}
 }
+
+func TestCreateFileField(t *testing.T) {
+	t.Run("createFileField failing file", func(t *testing.T) {
+		dir, cleanup := test.CreateTestDirectory(t)
+		defer cleanup()
+		path := filepath.Join(dir, "fake.mp3")
+		test.CreateTestFile(t, path)
+
+		req := AudioRequest{
+			FilePath: path,
+		}
+
+		mockFailedErr := fmt.Errorf("mock form builder fail")
+		mockBuilder := &mockFormBuilder{
+			mockCreateFormFile: func(string, *os.File) error {
+				return mockFailedErr
+			},
+		}
+
+		err := createFileField(req, mockBuilder)
+		checks.ErrorIs(t, err, mockFailedErr, "createFileField using a file should return error if form builder fails")
+	})
+
+	t.Run("createFileField failing reader", func(t *testing.T) {
+		req := AudioRequest{
+			FilePath: "test.wav",
+			Reader:   bytes.NewBuffer([]byte(`wav test contents`)),
+		}
+
+		mockFailedErr := fmt.Errorf("mock form builder fail")
+		mockBuilder := &mockFormBuilder{
+			mockCreateFormFileReader: func(string, io.Reader, string) error {
+				return mockFailedErr
+			},
+		}
+
+		err := createFileField(req, mockBuilder)
+		checks.ErrorIs(t, err, mockFailedErr, "createFileField using a reader should return error if form builder fails")
+	})
+
+	t.Run("createFileField failing open", func(t *testing.T) {
+		req := AudioRequest{
+			FilePath: "non_existing_file.wav",
+		}
+
+		mockBuilder := &mockFormBuilder{}
+
+		err := createFileField(req, mockBuilder)
+		checks.HasError(t, err, "createFileField using file should return error when open file fails")
+	})
+}
diff --git a/client_test.go b/client_test.go
index e30fa39..862cbe8 100644
--- a/client_test.go
+++ b/client_test.go
@@ -2,13 +2,24 @@ package openai //nolint:testpackage // testing private field
 
 import (
 	"bytes"
+	"context"
 	"errors"
 	"fmt"
 	"io"
 	"net/http"
 	"testing"
+
+	"github.com/sashabaranov/go-openai/internal/test"
 )
 
+var errTestRequestBuilderFailed = errors.New("test request builder failed")
+
+type failingRequestBuilder struct{}
+
+func (*failingRequestBuilder) Build(_ context.Context, _, _ string, _ any) (*http.Request, error) {
+	return nil, errTestRequestBuilderFailed
+}
+
 func TestClient(t *testing.T) {
 	const mockToken = "mock token"
 	client := NewClient(mockToken)
@@ -145,3 +156,141 @@ func TestHandleErrorResp(t *testing.T) {
 		})
 	}
 }
+
+func TestClientReturnsRequestBuilderErrors(t *testing.T) {
+	var err error
+	ts := test.NewTestServer().OpenAITestServer()
+	ts.Start()
+	defer ts.Close()
+
+	config := DefaultConfig(test.GetTestToken())
+	config.BaseURL = ts.URL + "/v1"
+	client := NewClientWithConfig(config)
+	client.requestBuilder = &failingRequestBuilder{}
+
+	ctx := context.Background()
+
+	_, err = client.CreateCompletion(ctx, CompletionRequest{Prompt: "testing"})
+	if !errors.Is(err, errTestRequestBuilderFailed) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+
+	_, err = client.CreateChatCompletion(ctx, ChatCompletionRequest{Model: GPT3Dot5Turbo})
+	if !errors.Is(err, errTestRequestBuilderFailed) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+
+	_, err = client.CreateChatCompletionStream(ctx, ChatCompletionRequest{Model: GPT3Dot5Turbo})
+	if !errors.Is(err, errTestRequestBuilderFailed) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+
+	_, err = client.CreateFineTune(ctx, FineTuneRequest{})
+	if !errors.Is(err, errTestRequestBuilderFailed) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+
+	_, err = client.ListFineTunes(ctx)
+	if !errors.Is(err, errTestRequestBuilderFailed) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+
+	_, err = client.CancelFineTune(ctx, "")
+	if !errors.Is(err, errTestRequestBuilderFailed) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+
+	_, err = client.GetFineTune(ctx, "")
+	if !errors.Is(err, errTestRequestBuilderFailed) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+
+	_, err = client.DeleteFineTune(ctx, "")
+	if !errors.Is(err, errTestRequestBuilderFailed) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+
+	_, err = client.ListFineTuneEvents(ctx, "")
+	if !errors.Is(err, errTestRequestBuilderFailed) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+
+	_, err = client.Moderations(ctx, ModerationRequest{})
+	if !errors.Is(err, errTestRequestBuilderFailed) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+
+	_, err = client.Edits(ctx, EditsRequest{})
+	if !errors.Is(err, errTestRequestBuilderFailed) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+
+	_, err = client.CreateEmbeddings(ctx, EmbeddingRequest{})
+	if !errors.Is(err, errTestRequestBuilderFailed) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+
+	_, err = client.CreateImage(ctx, ImageRequest{})
+	if !errors.Is(err, errTestRequestBuilderFailed) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+
+	err = client.DeleteFile(ctx, "")
+	if !errors.Is(err, errTestRequestBuilderFailed) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+
+	_, err = client.GetFile(ctx, "")
+	if !errors.Is(err, errTestRequestBuilderFailed) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+
+	_, err = client.ListFiles(ctx)
+	if !errors.Is(err, errTestRequestBuilderFailed) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+
+	_, err = client.ListEngines(ctx)
+	if !errors.Is(err, errTestRequestBuilderFailed) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+
+	_, err = client.GetEngine(ctx, "")
+	if !errors.Is(err, errTestRequestBuilderFailed) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+
+	_, err = client.ListModels(ctx)
+	if !errors.Is(err, errTestRequestBuilderFailed) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+
+	_, err = client.CreateCompletionStream(ctx, CompletionRequest{Prompt: ""})
+	if !errors.Is(err, errTestRequestBuilderFailed) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+}
+
+func TestClientReturnsRequestBuilderErrorsAddtion(t *testing.T) {
+	var err error
+	ts := test.NewTestServer().OpenAITestServer()
+	ts.Start()
+	defer ts.Close()
+
+	config := DefaultConfig(test.GetTestToken())
+	config.BaseURL = ts.URL + "/v1"
+	client := NewClientWithConfig(config)
+	client.requestBuilder = &failingRequestBuilder{}
+
+	ctx := context.Background()
+
+	_, err = client.CreateCompletion(ctx, CompletionRequest{Prompt: 1})
+	if !errors.Is(err, ErrCompletionRequestPromptTypeNotSupported) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+
+	_, err = client.CreateCompletionStream(ctx, CompletionRequest{Prompt: 1})
+	if !errors.Is(err, ErrCompletionRequestPromptTypeNotSupported) {
+		t.Fatalf("Did not return error when request builder failed: %v", err)
+	}
+}
diff --git a/engines_test.go b/engines_test.go
new file mode 100644
index 0000000..dfa3187
--- /dev/null
+++ b/engines_test.go
@@ -0,0 +1,34 @@
+package openai_test
+
+import (
+	"context"
+	"encoding/json"
+	"fmt"
+	"net/http"
+	"testing"
+
+	. "github.com/sashabaranov/go-openai"
+	"github.com/sashabaranov/go-openai/internal/test"
+	"github.com/sashabaranov/go-openai/internal/test/checks"
+)
+
+// TestGetEngine Tests the retrieve engine endpoint of the API using the mocked server.
+func TestGetEngine(t *testing.T) {
+	server := test.NewTestServer()
+	server.RegisterHandler("/v1/engines/text-davinci-003", func(w http.ResponseWriter, r *http.Request) {
+		resBytes, _ := json.Marshal(Engine{})
+		fmt.Fprintln(w, string(resBytes))
+	})
+	// create the test server
+	ts := server.OpenAITestServer()
+	ts.Start()
+	defer ts.Close()
+
+	config := DefaultConfig(test.GetTestToken())
+	config.BaseURL = ts.URL + "/v1"
+	client := NewClientWithConfig(config)
+	ctx := context.Background()
+
+	_, err := client.GetEngine(ctx, "text-davinci-003")
+	checks.NoError(t, err, "GetEngine error")
+}
diff --git a/image_test.go b/image_test.go
index 5cf6a26..ca9faed 100644
--- a/image_test.go
+++ b/image_test.go
@@ -264,15 +264,20 @@ func handleVariateImageEndpoint(w http.ResponseWriter, r *http.Request) {
 }
 
 type mockFormBuilder struct {
-	mockCreateFormFile func(string, *os.File) error
-	mockWriteField     func(string, string) error
-	mockClose          func() error
+	mockCreateFormFile       func(string, *os.File) error
+	mockCreateFormFileReader func(string, io.Reader, string) error
+	mockWriteField           func(string, string) error
+	mockClose                func() error
 }
 
 func (fb *mockFormBuilder) CreateFormFile(fieldname string, file *os.File) error {
 	return fb.mockCreateFormFile(fieldname, file)
 }
 
+func (fb *mockFormBuilder) CreateFormFileReader(fieldname string, r io.Reader, filename string) error {
+	return fb.mockCreateFormFileReader(fieldname, r, filename)
+}
+
 func (fb *mockFormBuilder) WriteField(fieldname, value string) error {
 	return fb.mockWriteField(fieldname, value)
 }
diff --git a/internal/request_builder_test.go b/internal/request_builder_test.go
new file mode 100644
index 0000000..e47d0f6
--- /dev/null
+++ b/internal/request_builder_test.go
@@ -0,0 +1,61 @@
+package openai //nolint:testpackage // testing private field
+
+import (
+	"bytes"
+	"context"
+	"errors"
+	"net/http"
+	"reflect"
+	"testing"
+)
+
+var errTestMarshallerFailed = errors.New("test marshaller failed")
+
+type failingMarshaller struct{}
+
+func (*failingMarshaller) Marshal(_ any) ([]byte, error) {
+	return []byte{}, errTestMarshallerFailed
+}
+
+func TestRequestBuilderReturnsMarshallerErrors(t *testing.T) {
+	builder := HTTPRequestBuilder{
+		marshaller: &failingMarshaller{},
+	}
+
+	_, err := builder.Build(context.Background(), "", "", struct{}{})
+	if !errors.Is(err, errTestMarshallerFailed) {
+		t.Fatalf("Did not return error when marshaller failed: %v", err)
+	}
+}
+
+func TestRequestBuilderReturnsRequest(t *testing.T) {
+	b := NewRequestBuilder()
+	var (
+		ctx         = context.Background()
+		method      = http.MethodPost
+		url         = "/foo"
+		request     = map[string]string{"foo": "bar"}
+		reqBytes, _ = b.marshaller.Marshal(request)
+		want, _     = http.NewRequestWithContext(ctx, method, url, bytes.NewBuffer(reqBytes))
+	)
+	got, _ := b.Build(ctx, method, url, request)
+	if !reflect.DeepEqual(got.Body, want.Body) ||
+		!reflect.DeepEqual(got.URL, want.URL) ||
+		!reflect.DeepEqual(got.Method, want.Method) {
+		t.Errorf("Build() got = %v, want %v", got, want)
+	}
+}
+
+func TestRequestBuilderReturnsRequestWhenRequestOfArgsIsNil(t *testing.T) {
+	var (
+		ctx     = context.Background()
+		method  = http.MethodGet
+		url     = "/foo"
+		want, _ = http.NewRequestWithContext(ctx, method, url, nil)
+	)
+	b := NewRequestBuilder()
+	got, _ := b.Build(ctx, method, url, nil)
+	if !reflect.DeepEqual(got, want) {
+		t.Errorf("Build() got = %v, want %v", got, want)
+	}
+}
diff --git a/request_builder_test.go b/request_builder_test.go
deleted file mode 100644
index ed4b691..0000000
--- a/request_builder_test.go
+++ /dev/null
@@ -1,177 +0,0 @@
-package openai //nolint:testpackage // testing private field
-
-import (
-	"github.com/sashabaranov/go-openai/internal/test"
-
-	"context"
-	"errors"
-	"net/http"
-	"testing"
-)
-
-var (
-	errTestMarshallerFailed     = errors.New("test marshaller failed")
-	errTestRequestBuilderFailed = errors.New("test request builder failed")
-)
-
-type (
-	failingRequestBuilder struct{}
-	failingMarshaller     struct{}
-)
-
-func (*failingMarshaller) Marshal(_ any) ([]byte, error) {
-	return []byte{}, errTestMarshallerFailed
-}
-
-func (*failingRequestBuilder) build(_ context.Context, _, _ string, _ any) (*http.Request, error) {
-	return nil, errTestRequestBuilderFailed
-}
-
-func TestRequestBuilderReturnsMarshallerErrors(t *testing.T) {
-	builder := httpRequestBuilder{
-		marshaller: &failingMarshaller{},
-	}
-
-	_, err := builder.build(context.Background(), "", "", struct{}{})
-	if !errors.Is(err, errTestMarshallerFailed) {
-		t.Fatalf("Did not return error when marshaller failed: %v", err)
-	}
-}
-
-func TestClientReturnsRequestBuilderErrors(t *testing.T) {
-	var err error
-	ts := test.NewTestServer().OpenAITestServer()
-	ts.Start()
-	defer ts.Close()
-
-	config := DefaultConfig(test.GetTestToken())
-	config.BaseURL = ts.URL + "/v1"
-	client := NewClientWithConfig(config)
-	client.requestBuilder = &failingRequestBuilder{}
-
-	ctx := context.Background()
-
-	_, err = client.CreateCompletion(ctx, CompletionRequest{Prompt: "testing"})
-	if !errors.Is(err, errTestRequestBuilderFailed) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-
-	_, err = client.CreateChatCompletion(ctx, ChatCompletionRequest{Model: GPT3Dot5Turbo})
-	if !errors.Is(err, errTestRequestBuilderFailed) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-
-	_, err = client.CreateChatCompletionStream(ctx, ChatCompletionRequest{Model: GPT3Dot5Turbo})
-	if !errors.Is(err, errTestRequestBuilderFailed) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-
-	_, err = client.CreateFineTune(ctx, FineTuneRequest{})
-	if !errors.Is(err, errTestRequestBuilderFailed) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-
-	_, err = client.ListFineTunes(ctx)
-	if !errors.Is(err, errTestRequestBuilderFailed) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-
-	_, err = client.CancelFineTune(ctx, "")
-	if !errors.Is(err, errTestRequestBuilderFailed) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-
-	_, err = client.GetFineTune(ctx, "")
-	if !errors.Is(err, errTestRequestBuilderFailed) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-
-	_, err = client.DeleteFineTune(ctx, "")
-	if !errors.Is(err, errTestRequestBuilderFailed) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-
-	_, err = client.ListFineTuneEvents(ctx, "")
-	if !errors.Is(err, errTestRequestBuilderFailed) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-
-	_, err = client.Moderations(ctx, ModerationRequest{})
-	if !errors.Is(err, errTestRequestBuilderFailed) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-
-	_, err = client.Edits(ctx, EditsRequest{})
-	if !errors.Is(err, errTestRequestBuilderFailed) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-
-	_, err = client.CreateEmbeddings(ctx, EmbeddingRequest{})
-	if !errors.Is(err, errTestRequestBuilderFailed) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-
-	_, err = client.CreateImage(ctx, ImageRequest{})
-	if !errors.Is(err, errTestRequestBuilderFailed) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-
-	err = client.DeleteFile(ctx, "")
-	if !errors.Is(err, errTestRequestBuilderFailed) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-
-	_, err = client.GetFile(ctx, "")
-	if !errors.Is(err, errTestRequestBuilderFailed) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-
-	_, err = client.ListFiles(ctx)
-	if !errors.Is(err, errTestRequestBuilderFailed) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-
-	_, err = client.ListEngines(ctx)
-	if !errors.Is(err, errTestRequestBuilderFailed) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-
-	_, err = client.GetEngine(ctx, "")
-	if !errors.Is(err, errTestRequestBuilderFailed) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-
-	_, err = client.ListModels(ctx)
-	if !errors.Is(err, errTestRequestBuilderFailed) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-
-	_, err = client.CreateCompletionStream(ctx, CompletionRequest{Prompt: ""})
-	if !errors.Is(err, errTestRequestBuilderFailed) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-}
-
-func TestReturnsRequestBuilderErrorsAddtion(t *testing.T) {
-	var err error
-	ts := test.NewTestServer().OpenAITestServer()
-	ts.Start()
-	defer ts.Close()
-
-	config := DefaultConfig(test.GetTestToken())
-	config.BaseURL = ts.URL + "/v1"
-	client := NewClientWithConfig(config)
-	client.requestBuilder = &failingRequestBuilder{}
-
-	ctx := context.Background()
-
-	_, err = client.CreateCompletion(ctx, CompletionRequest{Prompt: 1})
-	if !errors.Is(err, ErrCompletionRequestPromptTypeNotSupported) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-
-	_, err = client.CreateCompletionStream(ctx, CompletionRequest{Prompt: 1})
-	if !errors.Is(err, ErrCompletionRequestPromptTypeNotSupported) {
-		t.Fatalf("Did not return error when request builder failed: %v", err)
-	}
-}