Repository: sashabaranov/go-openai
Pull request #341 (2023-06-03 08:00:02)

Support Retrieve model API (#340)
This is a pull request for #340, I implemented the Retrieve model API.

I added some test code to the `TestClientReturnsRequestBuilderErrors` function and golangci-lint threw the following error.

```
client_test.go:160:1: cognitive complexity 21 of func `TestClientReturnsRequestBuilderErrors` is high (> 20) (gocognit)
```

It seems that the cognitive complexity has increased. I refactored the `TestClientReturnsRequestBuilderErrors` function to reduce its cognitive complexity.  => https://github.com/sashabaranov/go-openai/pull/341/commits/b1cb1fec1295b16964b1f63ca2044dff12d43407





















codecov[bot]: ## [Codecov](https://app.codecov.io/gh/sashabaranov/go-openai/pull/341?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#341](https://app.codecov.io/gh/sashabaranov/go-openai/pull/341?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (3e82973) into [master](https://app.codecov.io/gh/sashabaranov/go-openai/commit/61ba5f33698020982b4be97bf98fe654c043bea7?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (61ba5f3) will **decrease** coverage by `0.38%`.
> The diff coverage is `62.50%`.

```diff
@@            Coverage Diff             @@
##           master     #341      +/-   ##
==========================================
- Coverage   92.45%   92.08%   -0.38%     
==========================================
  Files          18       18              
  Lines         636      644       +8     
==========================================
+ Hits          588      593       +5     
- Misses         35       37       +2     
- Partials       13       14       +1     
```


| [Impacted Files](https://app.codecov.io/gh/sashabaranov/go-openai/pull/341?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [models.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/341?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-bW9kZWxzLmdv) | `80.00% <62.50%> (-20.00%)` | :arrow_down: |


vvatanabe: @sashabaranov Please check this PR.

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_stream.go b/chat_stream.go
index 842835e..9378c71 100644
--- a/chat_stream.go
+++ b/chat_stream.go
@@ -66,7 +66,7 @@ func (c *Client) CreateChatCompletionStream(
 			emptyMessagesLimit: c.config.EmptyMessagesLimit,
 			reader:             bufio.NewReader(resp.Body),
 			response:           resp,
-			errAccumulator:     newErrorAccumulator(),
+			errAccumulator:     utils.NewErrorAccumulator(),
 			unmarshaler:        &utils.JSONUnmarshaler{},
 		},
 	}
diff --git a/error_accumulator.go b/error_accumulator.go
deleted file mode 100644
index 568afdb..0000000
--- a/error_accumulator.go
+++ /dev/null
@@ -1,53 +0,0 @@
-package openai
-
-import (
-	"bytes"
-	"fmt"
-	"io"
-
-	utils "github.com/sashabaranov/go-openai/internal"
-)
-
-type errorAccumulator interface {
-	write(p []byte) error
-	unmarshalError() *ErrorResponse
-}
-
-type errorBuffer interface {
-	io.Writer
-	Len() int
-	Bytes() []byte
-}
-
-type defaultErrorAccumulator struct {
-	buffer      errorBuffer
-	unmarshaler utils.Unmarshaler
-}
-
-func newErrorAccumulator() errorAccumulator {
-	return &defaultErrorAccumulator{
-		buffer:      &bytes.Buffer{},
-		unmarshaler: &utils.JSONUnmarshaler{},
-	}
-}
-
-func (e *defaultErrorAccumulator) write(p []byte) error {
-	_, err := e.buffer.Write(p)
-	if err != nil {
-		return fmt.Errorf("error accumulator write error, %w", err)
-	}
-	return nil
-}
-
-func (e *defaultErrorAccumulator) unmarshalError() (errResp *ErrorResponse) {
-	if e.buffer.Len() == 0 {
-		return
-	}
-
-	err := e.unmarshaler.Unmarshal(e.buffer.Bytes(), &errResp)
-	if err != nil {
-		errResp = nil
-	}
-
-	return
-}
diff --git a/internal/error_accumulator.go b/internal/error_accumulator.go
new file mode 100644
index 0000000..3d3e805
--- /dev/null
+++ b/internal/error_accumulator.go
@@ -0,0 +1,44 @@
+package openai
+
+import (
+	"bytes"
+	"fmt"
+	"io"
+)
+
+type ErrorAccumulator interface {
+	Write(p []byte) error
+	Bytes() []byte
+}
+
+type errorBuffer interface {
+	io.Writer
+	Len() int
+	Bytes() []byte
+}
+
+type DefaultErrorAccumulator struct {
+	Buffer errorBuffer
+}
+
+func NewErrorAccumulator() ErrorAccumulator {
+	return &DefaultErrorAccumulator{
+		Buffer: &bytes.Buffer{},
+	}
+}
+
+func (e *DefaultErrorAccumulator) Write(p []byte) error {
+	_, err := e.Buffer.Write(p)
+	if err != nil {
+		return fmt.Errorf("error accumulator write error, %w", err)
+	}
+	return nil
+}
+
+func (e *DefaultErrorAccumulator) Bytes() (errBytes []byte) {
+	if e.Buffer.Len() == 0 {
+		return
+	}
+	errBytes = e.Buffer.Bytes()
+	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/models.go b/models.go
index 485433b..b3d4583 100644
--- a/models.go
+++ b/models.go
@@ -2,6 +2,7 @@ package openai
 
 import (
 	"context"
+	"fmt"
 	"net/http"
 )
 
@@ -48,3 +49,16 @@ func (c *Client) ListModels(ctx context.Context) (models ModelsList, err error)
 	err = c.sendRequest(req, &models)
 	return
 }
+
+// GetModel Retrieves a model instance, providing basic information about
+// 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)
+	if err != nil {
+		return
+	}
+
+	err = c.sendRequest(req, &model)
+	return
+}
diff --git a/stream.go b/stream.go
index b9e784a..d4e3523 100644
--- a/stream.go
+++ b/stream.go
@@ -55,7 +55,7 @@ func (c *Client) CreateCompletionStream(
 			emptyMessagesLimit: c.config.EmptyMessagesLimit,
 			reader:             bufio.NewReader(resp.Body),
 			response:           resp,
-			errAccumulator:     newErrorAccumulator(),
+			errAccumulator:     utils.NewErrorAccumulator(),
 			unmarshaler:        &utils.JSONUnmarshaler{},
 		},
 	}
diff --git a/stream_reader.go b/stream_reader.go
index 5eb6df7..a9940b0 100644
--- a/stream_reader.go
+++ b/stream_reader.go
@@ -20,7 +20,7 @@ type streamReader[T streamable] struct {
 
 	reader         *bufio.Reader
 	response       *http.Response
-	errAccumulator errorAccumulator
+	errAccumulator utils.ErrorAccumulator
 	unmarshaler    utils.Unmarshaler
 }
 
@@ -35,7 +35,7 @@ func (stream *streamReader[T]) Recv() (response T, err error) {
 waitForData:
 	line, err := stream.reader.ReadBytes('\n')
 	if err != nil {
-		respErr := stream.errAccumulator.unmarshalError()
+		respErr := stream.unmarshalError()
 		if respErr != nil {
 			err = fmt.Errorf("error, %w", respErr.Error)
 		}
@@ -45,7 +45,7 @@ waitForData:
 	var headerData = []byte("data: ")
 	line = bytes.TrimSpace(line)
 	if !bytes.HasPrefix(line, headerData) {
-		if writeErr := stream.errAccumulator.write(line); writeErr != nil {
+		if writeErr := stream.errAccumulator.Write(line); writeErr != nil {
 			err = writeErr
 			return
 		}
@@ -69,6 +69,20 @@ waitForData:
 	return
 }
 
+func (stream *streamReader[T]) unmarshalError() (errResp *ErrorResponse) {
+	errBytes := stream.errAccumulator.Bytes()
+	if len(errBytes) == 0 {
+		return
+	}
+
+	err := stream.unmarshaler.Unmarshal(errBytes, &errResp)
+	if err != nil {
+		errResp = nil
+	}
+
+	return
+}
+
 func (stream *streamReader[T]) Close() {
 	stream.response.Body.Close()
 }


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/chat_stream_test.go b/chat_stream_test.go
index afcb86d..77d373c 100644
--- a/chat_stream_test.go
+++ b/chat_stream_test.go
@@ -1,7 +1,7 @@
-package openai_test
+package openai //nolint:testpackage // testing private field
 
 import (
-	. "github.com/sashabaranov/go-openai"
+	utils "github.com/sashabaranov/go-openai/internal"
 	"github.com/sashabaranov/go-openai/internal/test"
 	"github.com/sashabaranov/go-openai/internal/test/checks"
 
@@ -63,9 +63,9 @@ func TestCreateChatCompletionStream(t *testing.T) {
 	// Client portion of the test
 	config := DefaultConfig(test.GetTestToken())
 	config.BaseURL = server.URL + "/v1"
-	config.HTTPClient.Transport = &tokenRoundTripper{
-		test.GetTestToken(),
-		http.DefaultTransport,
+	config.HTTPClient.Transport = &test.TokenRoundTripper{
+		Token:    test.GetTestToken(),
+		Fallback: http.DefaultTransport,
 	}
 
 	client := NewClientWithConfig(config)
@@ -170,9 +170,9 @@ func TestCreateChatCompletionStreamError(t *testing.T) {
 	// Client portion of the test
 	config := DefaultConfig(test.GetTestToken())
 	config.BaseURL = server.URL + "/v1"
-	config.HTTPClient.Transport = &tokenRoundTripper{
-		test.GetTestToken(),
-		http.DefaultTransport,
+	config.HTTPClient.Transport = &test.TokenRoundTripper{
+		Token:    test.GetTestToken(),
+		Fallback: http.DefaultTransport,
 	}
 
 	client := NewClientWithConfig(config)
@@ -227,9 +227,9 @@ func TestCreateChatCompletionStreamRateLimitError(t *testing.T) {
 	// Client portion of the test
 	config := DefaultConfig(test.GetTestToken())
 	config.BaseURL = ts.URL + "/v1"
-	config.HTTPClient.Transport = &tokenRoundTripper{
-		test.GetTestToken(),
-		http.DefaultTransport,
+	config.HTTPClient.Transport = &test.TokenRoundTripper{
+		Token:    test.GetTestToken(),
+		Fallback: http.DefaultTransport,
 	}
 
 	client := NewClientWithConfig(config)
@@ -255,6 +255,33 @@ func TestCreateChatCompletionStreamRateLimitError(t *testing.T) {
 	t.Logf("%+v\n", apiErr)
 }
 
+func TestCreateChatCompletionStreamErrorAccumulatorWriteErrors(t *testing.T) {
+	var err error
+	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()
+
+	config := DefaultConfig(test.GetTestToken())
+	config.BaseURL = ts.URL + "/v1"
+	client := NewClientWithConfig(config)
+
+	ctx := context.Background()
+
+	stream, err := client.CreateChatCompletionStream(ctx, ChatCompletionRequest{})
+	checks.NoError(t, err)
+
+	stream.errAccumulator = &utils.DefaultErrorAccumulator{
+		Buffer: &test.FailingErrorBuffer{},
+	}
+
+	_, err = stream.Recv()
+	checks.ErrorIs(t, err, test.ErrTestErrorAccumulatorWriteFailed, "Did not return error when Write failed", err.Error())
+}
+
 // 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/client_test.go b/client_test.go
index 862cbe8..5e63539 100644
--- a/client_test.go
+++ b/client_test.go
@@ -170,104 +170,82 @@ func TestClientReturnsRequestBuilderErrors(t *testing.T) {
 
 	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)
+	type TestCase struct {
+		Name     string
+		TestFunc func() (any, error)
 	}
 
-	_, 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)
+	testCases := []TestCase{
+		{"CreateCompletion", func() (any, error) {
+			return client.CreateCompletion(ctx, CompletionRequest{Prompt: "testing"})
+		}},
+		{"CreateCompletionStream", func() (any, error) {
+			return client.CreateCompletionStream(ctx, CompletionRequest{Prompt: ""})
+		}},
+		{"CreateChatCompletion", func() (any, error) {
+			return client.CreateChatCompletion(ctx, ChatCompletionRequest{Model: GPT3Dot5Turbo})
+		}},
+		{"CreateChatCompletionStream", func() (any, error) {
+			return client.CreateChatCompletionStream(ctx, ChatCompletionRequest{Model: GPT3Dot5Turbo})
+		}},
+		{"CreateFineTune", func() (any, error) {
+			return client.CreateFineTune(ctx, FineTuneRequest{})
+		}},
+		{"ListFineTunes", func() (any, error) {
+			return client.ListFineTunes(ctx)
+		}},
+		{"CancelFineTune", func() (any, error) {
+			return client.CancelFineTune(ctx, "")
+		}},
+		{"GetFineTune", func() (any, error) {
+			return client.GetFineTune(ctx, "")
+		}},
+		{"DeleteFineTune", func() (any, error) {
+			return client.DeleteFineTune(ctx, "")
+		}},
+		{"ListFineTuneEvents", func() (any, error) {
+			return client.ListFineTuneEvents(ctx, "")
+		}},
+		{"Moderations", func() (any, error) {
+			return client.Moderations(ctx, ModerationRequest{})
+		}},
+		{"Edits", func() (any, error) {
+			return client.Edits(ctx, EditsRequest{})
+		}},
+		{"CreateEmbeddings", func() (any, error) {
+			return client.CreateEmbeddings(ctx, EmbeddingRequest{})
+		}},
+		{"CreateImage", func() (any, error) {
+			return client.CreateImage(ctx, ImageRequest{})
+		}},
+		{"DeleteFile", func() (any, error) {
+			return nil, client.DeleteFile(ctx, "")
+		}},
+		{"GetFile", func() (any, error) {
+			return client.GetFile(ctx, "")
+		}},
+		{"ListFiles", func() (any, error) {
+			return client.ListFiles(ctx)
+		}},
+		{"ListEngines", func() (any, error) {
+			return client.ListEngines(ctx)
+		}},
+		{"GetEngine", func() (any, error) {
+			return client.GetEngine(ctx, "")
+		}},
+		{"ListModels", func() (any, error) {
+			return client.ListModels(ctx)
+		}},
+		{"GetModel", func() (any, error) {
+			return client.GetModel(ctx, "text-davinci-003")
+		}},
 	}
 
-	_, 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)
+	for _, testCase := range testCases {
+		_, err = testCase.TestFunc()
+		if !errors.Is(err, errTestRequestBuilderFailed) {
+			t.Fatalf("%s did not return error when request builder failed: %v", testCase.Name, err)
+		}
 	}
 }
 
diff --git a/error_accumulator_test.go b/error_accumulator_test.go
deleted file mode 100644
index 821eb21..0000000
--- a/error_accumulator_test.go
+++ /dev/null
@@ -1,100 +0,0 @@
-package openai //nolint:testpackage // testing private field
-
-import (
-	"bytes"
-	"context"
-	"errors"
-	"net/http"
-	"testing"
-
-	utils "github.com/sashabaranov/go-openai/internal"
-	"github.com/sashabaranov/go-openai/internal/test"
-	"github.com/sashabaranov/go-openai/internal/test/checks"
-)
-
-var (
-	errTestUnmarshalerFailed           = errors.New("test unmarshaler failed")
-	errTestErrorAccumulatorWriteFailed = errors.New("test error accumulator failed")
-)
-
-type (
-	failingUnMarshaller struct{}
-	failingErrorBuffer  struct{}
-)
-
-func (b *failingErrorBuffer) Write(_ []byte) (n int, err error) {
-	return 0, errTestErrorAccumulatorWriteFailed
-}
-
-func (b *failingErrorBuffer) Len() int {
-	return 0
-}
-
-func (b *failingErrorBuffer) Bytes() []byte {
-	return []byte{}
-}
-
-func (*failingUnMarshaller) Unmarshal(_ []byte, _ any) error {
-	return errTestUnmarshalerFailed
-}
-
-func TestErrorAccumulatorReturnsUnmarshalerErrors(t *testing.T) {
-	accumulator := &defaultErrorAccumulator{
-		buffer:      &bytes.Buffer{},
-		unmarshaler: &failingUnMarshaller{},
-	}
-
-	respErr := accumulator.unmarshalError()
-	if respErr != nil {
-		t.Fatalf("Did not return nil with empty buffer: %v", respErr)
-	}
-
-	err := accumulator.write([]byte("{"))
-	if err != nil {
-		t.Fatalf("%+v", err)
-	}
-
-	respErr = accumulator.unmarshalError()
-	if respErr != nil {
-		t.Fatalf("Did not return nil when unmarshaler failed: %v", respErr)
-	}
-}
-
-func TestErrorByteWriteErrors(t *testing.T) {
-	accumulator := &defaultErrorAccumulator{
-		buffer:      &failingErrorBuffer{},
-		unmarshaler: &utils.JSONUnmarshaler{},
-	}
-	err := accumulator.write([]byte("{"))
-	if !errors.Is(err, errTestErrorAccumulatorWriteFailed) {
-		t.Fatalf("Did not return error when write failed: %v", err)
-	}
-}
-
-func TestErrorAccumulatorWriteErrors(t *testing.T) {
-	var err error
-	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()
-
-	config := DefaultConfig(test.GetTestToken())
-	config.BaseURL = ts.URL + "/v1"
-	client := NewClientWithConfig(config)
-
-	ctx := context.Background()
-
-	stream, err := client.CreateChatCompletionStream(ctx, ChatCompletionRequest{})
-	checks.NoError(t, err)
-
-	stream.errAccumulator = &defaultErrorAccumulator{
-		buffer:      &failingErrorBuffer{},
-		unmarshaler: &utils.JSONUnmarshaler{},
-	}
-
-	_, err = stream.Recv()
-	checks.ErrorIs(t, err, errTestErrorAccumulatorWriteFailed, "Did not return error when write failed", err.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/error_accumulator_test.go b/internal/error_accumulator_test.go
new file mode 100644
index 0000000..d48f281
--- /dev/null
+++ b/internal/error_accumulator_test.go
@@ -0,0 +1,41 @@
+package openai_test
+
+import (
+	"bytes"
+	"errors"
+	"testing"
+
+	utils "github.com/sashabaranov/go-openai/internal"
+	"github.com/sashabaranov/go-openai/internal/test"
+)
+
+func TestErrorAccumulatorBytes(t *testing.T) {
+	accumulator := &utils.DefaultErrorAccumulator{
+		Buffer: &bytes.Buffer{},
+	}
+
+	errBytes := accumulator.Bytes()
+	if len(errBytes) != 0 {
+		t.Fatalf("Did not return nil with empty bytes: %s", string(errBytes))
+	}
+
+	err := accumulator.Write([]byte("{}"))
+	if err != nil {
+		t.Fatalf("%+v", err)
+	}
+
+	errBytes = accumulator.Bytes()
+	if len(errBytes) == 0 {
+		t.Fatalf("Did not return error bytes when has error: %s", string(errBytes))
+	}
+}
+
+func TestErrorByteWriteErrors(t *testing.T) {
+	accumulator := &utils.DefaultErrorAccumulator{
+		Buffer: &test.FailingErrorBuffer{},
+	}
+	err := accumulator.Write([]byte("{"))
+	if !errors.Is(err, test.ErrTestErrorAccumulatorWriteFailed) {
+		t.Fatalf("Did not return error when write failed: %v", err)
+	}
+}
diff --git a/internal/test/failer.go b/internal/test/failer.go
new file mode 100644
index 0000000..10ca64e
--- /dev/null
+++ b/internal/test/failer.go
@@ -0,0 +1,21 @@
+package test
+
+import "errors"
+
+var (
+	ErrTestErrorAccumulatorWriteFailed = errors.New("test error accumulator failed")
+)
+
+type FailingErrorBuffer struct{}
+
+func (b *FailingErrorBuffer) Write(_ []byte) (n int, err error) {
+	return 0, ErrTestErrorAccumulatorWriteFailed
+}
+
+func (b *FailingErrorBuffer) Len() int {
+	return 0
+}
+
+func (b *FailingErrorBuffer) Bytes() []byte {
+	return []byte{}
+}
diff --git a/internal/test/helpers.go b/internal/test/helpers.go
index 8461e53..0e63ae8 100644
--- a/internal/test/helpers.go
+++ b/internal/test/helpers.go
@@ -3,6 +3,7 @@ package test
 import (
 	"github.com/sashabaranov/go-openai/internal/test/checks"
 
+	"net/http"
 	"os"
 	"testing"
 )
@@ -27,3 +28,26 @@ func CreateTestDirectory(t *testing.T) (path string, cleanup func()) {
 
 	return path, func() { os.RemoveAll(path) }
 }
+
+// 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
+// request include a valid API token in the headers for authentication and
+// authorization.
+type TokenRoundTripper struct {
+	Token    string
+	Fallback http.RoundTripper
+}
+
+// RoundTrip takes an *http.Request as input and returns an
+// *http.Response and an error.
+//
+// It is expected to use the provided request to create a connection to an HTTP
+// server and return the response, or an error if one occurred. The returned
+// Response should have its Body closed. If the RoundTrip method returns an
+// error, the Client's Get, Head, Post, and PostForm methods return the same
+// error.
+func (t *TokenRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
+	req.Header.Set("Authorization", "Bearer "+t.Token)
+	return t.Fallback.RoundTrip(req)
+}
diff --git a/models_test.go b/models_test.go
index b017800..834c849 100644
--- a/models_test.go
+++ b/models_test.go
@@ -54,3 +54,44 @@ func handleModelsEndpoint(w http.ResponseWriter, _ *http.Request) {
 	resBytes, _ := json.Marshal(ModelsList{})
 	fmt.Fprintln(w, string(resBytes))
 }
+
+// TestGetModel Tests the retrieve model endpoint of the API using the mocked server.
+func TestGetModel(t *testing.T) {
+	server := test.NewTestServer()
+	server.RegisterHandler("/v1/models/text-davinci-003", handleGetModelEndpoint)
+	// 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.GetModel(ctx, "text-davinci-003")
+	checks.NoError(t, err, "GetModel error")
+}
+
+func TestAzureGetModel(t *testing.T) {
+	server := test.NewTestServer()
+	server.RegisterHandler("/openai/models/text-davinci-003", handleModelsEndpoint)
+	// create the test server
+	ts := server.OpenAITestServer()
+	ts.Start()
+	defer ts.Close()
+
+	config := DefaultAzureConfig(test.GetTestToken(), "https://dummylab.openai.azure.com/")
+	config.BaseURL = ts.URL
+	client := NewClientWithConfig(config)
+	ctx := context.Background()
+
+	_, err := client.GetModel(ctx, "text-davinci-003")
+	checks.NoError(t, err, "GetModel error")
+}
+
+// handleModelsEndpoint Handles the models endpoint by the test server.
+func handleGetModelEndpoint(w http.ResponseWriter, _ *http.Request) {
+	resBytes, _ := json.Marshal(Model{})
+	fmt.Fprintln(w, string(resBytes))
+}
diff --git a/stream_reader_test.go b/stream_reader_test.go
new file mode 100644
index 0000000..0e45c0b
--- /dev/null
+++ b/stream_reader_test.go
@@ -0,0 +1,53 @@
+package openai //nolint:testpackage // testing private field
+
+import (
+	"bufio"
+	"bytes"
+	"errors"
+	"testing"
+
+	utils "github.com/sashabaranov/go-openai/internal"
+)
+
+var errTestUnmarshalerFailed = errors.New("test unmarshaler failed")
+
+type failingUnMarshaller struct{}
+
+func (*failingUnMarshaller) Unmarshal(_ []byte, _ any) error {
+	return errTestUnmarshalerFailed
+}
+
+func TestStreamReaderReturnsUnmarshalerErrors(t *testing.T) {
+	stream := &streamReader[ChatCompletionStreamResponse]{
+		errAccumulator: utils.NewErrorAccumulator(),
+		unmarshaler:    &failingUnMarshaller{},
+	}
+
+	respErr := stream.unmarshalError()
+	if respErr != nil {
+		t.Fatalf("Did not return nil with empty buffer: %v", respErr)
+	}
+
+	err := stream.errAccumulator.Write([]byte("{"))
+	if err != nil {
+		t.Fatalf("%+v", err)
+	}
+
+	respErr = stream.unmarshalError()
+	if respErr != nil {
+		t.Fatalf("Did not return nil when unmarshaler failed: %v", respErr)
+	}
+}
+
+func TestStreamReaderReturnsErrTooManyEmptyStreamMessages(t *testing.T) {
+	stream := &streamReader[ChatCompletionStreamResponse]{
+		emptyMessagesLimit: 3,
+		reader:             bufio.NewReader(bytes.NewReader([]byte("\n\n\n\n"))),
+		errAccumulator:     utils.NewErrorAccumulator(),
+		unmarshaler:        &utils.JSONUnmarshaler{},
+	}
+	_, err := stream.Recv()
+	if !errors.Is(err, ErrTooManyEmptyStreamMessages) {
+		t.Fatalf("Did not return error when recv failed: %v", err)
+	}
+}
diff --git a/stream_test.go b/stream_test.go
index a5c591f..589fc9e 100644
--- a/stream_test.go
+++ b/stream_test.go
@@ -57,9 +57,9 @@ func TestCreateCompletionStream(t *testing.T) {
 	// Client portion of the test
 	config := DefaultConfig(test.GetTestToken())
 	config.BaseURL = server.URL + "/v1"
-	config.HTTPClient.Transport = &tokenRoundTripper{
-		test.GetTestToken(),
-		http.DefaultTransport,
+	config.HTTPClient.Transport = &test.TokenRoundTripper{
+		Token:    test.GetTestToken(),
+		Fallback: http.DefaultTransport,
 	}
 
 	client := NewClientWithConfig(config)
@@ -142,9 +142,9 @@ func TestCreateCompletionStreamError(t *testing.T) {
 	// Client portion of the test
 	config := DefaultConfig(test.GetTestToken())
 	config.BaseURL = server.URL + "/v1"
-	config.HTTPClient.Transport = &tokenRoundTripper{
-		test.GetTestToken(),
-		http.DefaultTransport,
+	config.HTTPClient.Transport = &test.TokenRoundTripper{
+		Token:    test.GetTestToken(),
+		Fallback: http.DefaultTransport,
 	}
 
 	client := NewClientWithConfig(config)
@@ -194,9 +194,9 @@ func TestCreateCompletionStreamRateLimitError(t *testing.T) {
 	// Client portion of the test
 	config := DefaultConfig(test.GetTestToken())
 	config.BaseURL = ts.URL + "/v1"
-	config.HTTPClient.Transport = &tokenRoundTripper{
-		test.GetTestToken(),
-		http.DefaultTransport,
+	config.HTTPClient.Transport = &test.TokenRoundTripper{
+		Token:    test.GetTestToken(),
+		Fallback: http.DefaultTransport,
 	}
 
 	client := NewClientWithConfig(config)
@@ -217,29 +217,6 @@ func TestCreateCompletionStreamRateLimitError(t *testing.T) {
 	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
-// request include a valid API token in the headers for authentication and
-// authorization.
-type tokenRoundTripper struct {
-	token    string
-	fallback http.RoundTripper
-}
-
-// RoundTrip takes an *http.Request as input and returns an
-// *http.Response and an error.
-//
-// It is expected to use the provided request to create a connection to an HTTP
-// server and return the response, or an error if one occurred. The returned
-// Response should have its Body closed. If the RoundTrip method returns an
-// error, the Client's Get, Head, Post, and PostForm methods return the same
-// error.
-func (t *tokenRoundTripper) RoundTrip(req *http.Request) (*http.Response, error) {
-	req.Header.Set("Authorization", "Bearer "+t.token)
-	return t.fallback.RoundTrip(req)
-}
-
 // 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 {