Repository: sashabaranov/go-openai
Pull request #506 (2023-10-06 06:16:01)

Feat Add headers to openai responses
**Describe the change**
This branch adds headers to all opeanai http responses.

**Describe your solution**
This was done embedding a struct `httpHeader` to each response struct. The `httpHeader` type has attached methods to set and get `http.Header`. The low level `sendRequest` uses a `Response` interface defining the `SetHeader` method of `httpHeader`.

**Tests**
`TestChatCompletionsWithHeaders` has been added to check a test header. 


codecov[bot]: ## [Codecov](https://app.codecov.io/gh/sashabaranov/go-openai/pull/506?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#506](https://app.codecov.io/gh/sashabaranov/go-openai/pull/506?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (7ea8119) into [master](https://app.codecov.io/gh/sashabaranov/go-openai/commit/84f77a0acda6eb541f3312ed8f7711c89e661443?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (84f77a0) will **increase** coverage by `0.04%`.
> The diff coverage is `100.00%`.

```diff
@@            Coverage Diff             @@
##           master     #506      +/-   ##
==========================================
+ Coverage   97.48%   97.52%   +0.04%     
==========================================
  Files          18       18              
  Lines         836      850      +14     
==========================================
+ Hits          815      829      +14     
  Misses         15       15              
  Partials        6        6              
```


| [Files](https://app.codecov.io/gh/sashabaranov/go-openai/pull/506?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/506?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-YXVkaW8uZ28=) | `89.15% <100.00%> (+0.99%)` | :arrow_up: |
| [chat.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/506?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y2hhdC5nbw==) | `100.00% <ø> (ø)` | |
| [client.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/506?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y2xpZW50Lmdv) | `100.00% <100.00%> (ø)` | |
| [completion.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/506?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-Y29tcGxldGlvbi5nbw==) | `100.00% <ø> (ø)` | |
| [edits.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/506?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-ZWRpdHMuZ28=) | `100.00% <ø> (ø)` | |
| [embeddings.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/506?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-ZW1iZWRkaW5ncy5nbw==) | `100.00% <ø> (ø)` | |
| [engines.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/506?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-ZW5naW5lcy5nbw==) | `100.00% <ø> (ø)` | |
| [files.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/506?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-ZmlsZXMuZ28=) | `94.73% <ø> (ø)` | |
| [fine\_tunes.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/506?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-ZmluZV90dW5lcy5nbw==) | `100.00% <ø> (ø)` | |
| [fine\_tuning\_job.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/506?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-ZmluZV90dW5pbmdfam9iLmdv) | `100.00% <ø> (ø)` | |
| ... and [3 more](https://app.codecov.io/gh/sashabaranov/go-openai/pull/506?src=pr&el=tree-more&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | |



Patch:
diff --git a/audio.go b/audio.go
index 9f46915..4cbe4fe 100644
--- a/audio.go
+++ b/audio.go
@@ -63,6 +63,21 @@ type AudioResponse struct {
 		Transient        bool    `json:"transient"`
 	} `json:"segments"`
 	Text string `json:"text"`
+
+	httpHeader
+}
+
+type audioTextResponse struct {
+	Text string `json:"text"`
+
+	httpHeader
+}
+
+func (r *audioTextResponse) ToAudioResponse() AudioResponse {
+	return AudioResponse{
+		Text:       r.Text,
+		httpHeader: r.httpHeader,
+	}
 }
 
 // CreateTranscription — API call to create a transcription. Returns transcribed text.
@@ -104,7 +119,9 @@ func (c *Client) callAudioAPI(
 	if request.HasJSONResponse() {
 		err = c.sendRequest(req, &response)
 	} else {
-		err = c.sendRequest(req, &response.Text)
+		var textResponse audioTextResponse
+		err = c.sendRequest(req, &textResponse)
+		response = textResponse.ToAudioResponse()
 	}
 	if err != nil {
 		return AudioResponse{}, err
diff --git a/chat.go b/chat.go
index 8d29b32..df0e5f9 100644
--- a/chat.go
+++ b/chat.go
@@ -142,6 +142,8 @@ type ChatCompletionResponse struct {
 	Model   string                 `json:"model"`
 	Choices []ChatCompletionChoice `json:"choices"`
 	Usage   Usage                  `json:"usage"`
+
+	httpHeader
 }
 
 // CreateChatCompletion — API call to Create a completion for the chat message.
diff --git a/client.go b/client.go
index 5779a8e..1990228 100644
--- a/client.go
+++ b/client.go
@@ -20,6 +20,20 @@ type Client struct {
 	createFormBuilder func(io.Writer) utils.FormBuilder
 }
 
+type Response interface {
+	SetHeader(http.Header)
+}
+
+type httpHeader http.Header
+
+func (h *httpHeader) SetHeader(header http.Header) {
+	*h = httpHeader(header)
+}
+
+func (h httpHeader) Header() http.Header {
+	return http.Header(h)
+}
+
 // NewClient creates new OpenAI API client.
 func NewClient(authToken string) *Client {
 	config := DefaultConfig(authToken)
@@ -82,7 +96,7 @@ func (c *Client) newRequest(ctx context.Context, method, url string, setters ...
 	return req, nil
 }
 
-func (c *Client) sendRequest(req *http.Request, v any) error {
+func (c *Client) sendRequest(req *http.Request, v Response) error {
 	req.Header.Set("Accept", "application/json; charset=utf-8")
 
 	// Check whether Content-Type is already set, Upload Files API requires
@@ -103,6 +117,10 @@ func (c *Client) sendRequest(req *http.Request, v any) error {
 		return c.handleErrorResp(res)
 	}
 
+	if v != nil {
+		v.SetHeader(res.Header)
+	}
+
 	return decodeResponse(res.Body, v)
 }
 
diff --git a/completion.go b/completion.go
index 7b9ae89..c7ff94a 100644
--- a/completion.go
+++ b/completion.go
@@ -154,6 +154,8 @@ type CompletionResponse struct {
 	Model   string             `json:"model"`
 	Choices []CompletionChoice `json:"choices"`
 	Usage   Usage              `json:"usage"`
+
+	httpHeader
 }
 
 // CreateCompletion — API call to create a completion. This is the main endpoint of the API. Returns new text as well
diff --git a/edits.go b/edits.go
index 831aade..97d0260 100644
--- a/edits.go
+++ b/edits.go
@@ -28,6 +28,8 @@ type EditsResponse struct {
 	Created int64         `json:"created"`
 	Usage   Usage         `json:"usage"`
 	Choices []EditsChoice `json:"choices"`
+
+	httpHeader
 }
 
 // Edits Perform an API call to the Edits endpoint.
diff --git a/embeddings.go b/embeddings.go
index 660bc24..7e2aa7e 100644
--- a/embeddings.go
+++ b/embeddings.go
@@ -150,6 +150,8 @@ type EmbeddingResponse struct {
 	Data   []Embedding    `json:"data"`
 	Model  EmbeddingModel `json:"model"`
 	Usage  Usage          `json:"usage"`
+
+	httpHeader
 }
 
 type base64String string
@@ -182,6 +184,8 @@ type EmbeddingResponseBase64 struct {
 	Data   []Base64Embedding `json:"data"`
 	Model  EmbeddingModel    `json:"model"`
 	Usage  Usage             `json:"usage"`
+
+	httpHeader
 }
 
 // ToEmbeddingResponse converts an embeddingResponseBase64 to an EmbeddingResponse.
diff --git a/engines.go b/engines.go
index adf6025..5a0dba8 100644
--- a/engines.go
+++ b/engines.go
@@ -12,11 +12,15 @@ type Engine struct {
 	Object string `json:"object"`
 	Owner  string `json:"owner"`
 	Ready  bool   `json:"ready"`
+
+	httpHeader
 }
 
 // EnginesList is a list of engines.
 type EnginesList struct {
 	Engines []Engine `json:"data"`
+
+	httpHeader
 }
 
 // ListEngines Lists the currently available engines, and provides basic
diff --git a/files.go b/files.go
index 8b933c3..9e521fb 100644
--- a/files.go
+++ b/files.go
@@ -25,11 +25,15 @@ type File struct {
 	Status        string `json:"status"`
 	Purpose       string `json:"purpose"`
 	StatusDetails string `json:"status_details"`
+
+	httpHeader
 }
 
 // FilesList is a list of files that belong to the user or organization.
 type FilesList struct {
 	Files []File `json:"data"`
+
+	httpHeader
 }
 
 // CreateFile uploads a jsonl file to GPT3
diff --git a/fine_tunes.go b/fine_tunes.go
index 7d3b59d..ca84078 100644
--- a/fine_tunes.go
+++ b/fine_tunes.go
@@ -41,6 +41,8 @@ type FineTune struct {
 	ValidationFiles   []File              `json:"validation_files"`
 	TrainingFiles     []File              `json:"training_files"`
 	UpdatedAt         int64               `json:"updated_at"`
+
+	httpHeader
 }
 
 // Deprecated: On August 22nd, 2023, OpenAI announced the deprecation of the /v1/fine-tunes API.
@@ -69,6 +71,8 @@ type FineTuneHyperParams struct {
 type FineTuneList struct {
 	Object string     `json:"object"`
 	Data   []FineTune `json:"data"`
+
+	httpHeader
 }
 
 // Deprecated: On August 22nd, 2023, OpenAI announced the deprecation of the /v1/fine-tunes API.
@@ -77,6 +81,8 @@ type FineTuneList struct {
 type FineTuneEventList struct {
 	Object string          `json:"object"`
 	Data   []FineTuneEvent `json:"data"`
+
+	httpHeader
 }
 
 // Deprecated: On August 22nd, 2023, OpenAI announced the deprecation of the /v1/fine-tunes API.
@@ -86,6 +92,8 @@ type FineTuneDeleteResponse struct {
 	ID      string `json:"id"`
 	Object  string `json:"object"`
 	Deleted bool   `json:"deleted"`
+
+	httpHeader
 }
 
 // Deprecated: On August 22nd, 2023, OpenAI announced the deprecation of the /v1/fine-tunes API.
diff --git a/fine_tuning_job.go b/fine_tuning_job.go
index a840b7e..9dcb49d 100644
--- a/fine_tuning_job.go
+++ b/fine_tuning_job.go
@@ -21,10 +21,12 @@ type FineTuningJob struct {
 	ValidationFile  string          `json:"validation_file,omitempty"`
 	ResultFiles     []string        `json:"result_files"`
 	TrainedTokens   int             `json:"trained_tokens"`
+
+	httpHeader
 }
 
 type Hyperparameters struct {
-	Epochs int `json:"n_epochs"`
+	Epochs any `json:"n_epochs,omitempty"`
 }
 
 type FineTuningJobRequest struct {
@@ -39,6 +41,8 @@ type FineTuningJobEventList struct {
 	Object  string          `json:"object"`
 	Data    []FineTuneEvent `json:"data"`
 	HasMore bool            `json:"has_more"`
+
+	httpHeader
 }
 
 type FineTuningJobEvent struct {
diff --git a/image.go b/image.go
index cb96f4f..4addcdb 100644
--- a/image.go
+++ b/image.go
@@ -33,6 +33,8 @@ type ImageRequest struct {
 type ImageResponse struct {
 	Created int64                    `json:"created,omitempty"`
 	Data    []ImageResponseDataInner `json:"data,omitempty"`
+
+	httpHeader
 }
 
 // ImageResponseDataInner represents a response data structure for image API.
diff --git a/models.go b/models.go
index c207f0a..d94f988 100644
--- a/models.go
+++ b/models.go
@@ -15,6 +15,8 @@ type Model struct {
 	Permission []Permission `json:"permission"`
 	Root       string       `json:"root"`
 	Parent     string       `json:"parent"`
+
+	httpHeader
 }
 
 // Permission struct represents an OpenAPI permission.
@@ -38,11 +40,15 @@ type FineTuneModelDeleteResponse struct {
 	ID      string `json:"id"`
 	Object  string `json:"object"`
 	Deleted bool   `json:"deleted"`
+
+	httpHeader
 }
 
 // ModelsList is a list of models, including those that belong to the user or organization.
 type ModelsList struct {
 	Models []Model `json:"data"`
+
+	httpHeader
 }
 
 // ListModels Lists the currently available models,
diff --git a/moderation.go b/moderation.go
index a32f123..f8d20ee 100644
--- a/moderation.go
+++ b/moderation.go
@@ -69,6 +69,8 @@ type ModerationResponse struct {
 	ID      string   `json:"id"`
 	Model   string   `json:"model"`
 	Results []Result `json:"results"`
+
+	httpHeader
 }
 
 // Moderations — perform a moderation api call over a string.


Test patch:
diff --git a/chat_test.go b/chat_test.go
index 38d66fa..52cd0bd 100644
--- a/chat_test.go
+++ b/chat_test.go
@@ -16,6 +16,11 @@ import (
 	"github.com/sashabaranov/go-openai/jsonschema"
 )
 
+const (
+	xCustomHeader      = "X-CUSTOM-HEADER"
+	xCustomHeaderValue = "test"
+)
+
 func TestChatCompletionsWrongModel(t *testing.T) {
 	config := DefaultConfig("whatever")
 	config.BaseURL = "http://localhost/v1"
@@ -68,6 +73,30 @@ func TestChatCompletions(t *testing.T) {
 	checks.NoError(t, err, "CreateChatCompletion error")
 }
 
+// TestCompletions Tests the completions endpoint of the API using the mocked server.
+func TestChatCompletionsWithHeaders(t *testing.T) {
+	client, server, teardown := setupOpenAITestServer()
+	defer teardown()
+	server.RegisterHandler("/v1/chat/completions", handleChatCompletionEndpoint)
+	resp, err := client.CreateChatCompletion(context.Background(), ChatCompletionRequest{
+		MaxTokens: 5,
+		Model:     GPT3Dot5Turbo,
+		Messages: []ChatCompletionMessage{
+			{
+				Role:    ChatMessageRoleUser,
+				Content: "Hello!",
+			},
+		},
+	})
+	checks.NoError(t, err, "CreateChatCompletion error")
+
+	a := resp.Header().Get(xCustomHeader)
+	_ = a
+	if resp.Header().Get(xCustomHeader) != xCustomHeaderValue {
+		t.Errorf("expected header %s to be %s", xCustomHeader, xCustomHeaderValue)
+	}
+}
+
 // TestChatCompletionsFunctions tests including a function call.
 func TestChatCompletionsFunctions(t *testing.T) {
 	client, server, teardown := setupOpenAITestServer()
@@ -281,6 +310,7 @@ func handleChatCompletionEndpoint(w http.ResponseWriter, r *http.Request) {
 		TotalTokens:      inputTokens + completionTokens,
 	}
 	resBytes, _ = json.Marshal(res)
+	w.Header().Set(xCustomHeader, xCustomHeaderValue)
 	fmt.Fprintln(w, string(resBytes))
 }
 
diff --git a/fine_tuning_job_test.go b/fine_tuning_job_test.go
index 519c6cd..f6d41c3 100644
--- a/fine_tuning_job_test.go
+++ b/fine_tuning_job_test.go
@@ -21,8 +21,23 @@ func TestFineTuningJob(t *testing.T) {
 	server.RegisterHandler(
 		"/v1/fine_tuning/jobs",
 		func(w http.ResponseWriter, r *http.Request) {
-			var resBytes []byte
-			resBytes, _ = json.Marshal(FineTuningJob{})
+			resBytes, _ := json.Marshal(FineTuningJob{
+				Object:         "fine_tuning.job",
+				ID:             testFineTuninigJobID,
+				Model:          "davinci-002",
+				CreatedAt:      1692661014,
+				FinishedAt:     1692661190,
+				FineTunedModel: "ft:davinci-002:my-org:custom_suffix:7q8mpxmy",
+				OrganizationID: "org-123",
+				ResultFiles:    []string{"file-abc123"},
+				Status:         "succeeded",
+				ValidationFile: "",
+				TrainingFile:   "file-abc123",
+				Hyperparameters: Hyperparameters{
+					Epochs: "auto",
+				},
+				TrainedTokens: 5768,
+			})
 			fmt.Fprintln(w, string(resBytes))
 		},
 	)