Repository: sashabaranov/go-openai
Pull request #270 (2023-04-20 05:32:08)

fix:model param type, add moderation Model Name const.

codecov[bot]: ## [Codecov](https://codecov.io/gh/sashabaranov/go-openai/pull/270?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#270](https://codecov.io/gh/sashabaranov/go-openai/pull/270?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (6cd1c33) into [master](https://codecov.io/gh/sashabaranov/go-openai/commit/d6ab1b3a4f86d82a06ab601b3d3db85c1662a939?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (d6ab1b3) will **not change** coverage.
> The diff coverage is `n/a`.

```diff
@@           Coverage Diff           @@
##           master     #270   +/-   ##
=======================================
  Coverage   91.99%   91.99%           
=======================================
  Files          22       22           
  Lines         637      637           
=======================================
  Hits          586      586           
  Misses         37       37           
  Partials       14       14           
```


| [Impacted Files](https://codecov.io/gh/sashabaranov/go-openai/pull/270?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [moderation.go](https://codecov.io/gh/sashabaranov/go-openai/pull/270?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-bW9kZXJhdGlvbi5nbw==) | `100.00% <ø> (ø)` | |

Help us with your feedback. Take ten seconds to tell us [how you rate us](https://about.codecov.io/nps?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None). Have a feature suggestion? [Share it here.](https://app.codecov.io/gh/feedback/?utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None)


Patch:
diff --git a/README.md b/README.md
index 898465c..7526ea3 100644
--- a/README.md
+++ b/README.md
@@ -223,6 +223,47 @@ func main() {
 ```
 </details>
 
+<details>
+<summary>Audio Captions</summary>
+
+```go
+package main
+
+import (
+	"context"
+	"fmt"
+	"os"
+
+	openai "github.com/sashabaranov/go-openai"
+)
+
+func main() {
+	c := openai.NewClient(os.Getenv("OPENAI_KEY"))
+
+	req := openai.AudioRequest{
+		Model:    openai.Whisper1,
+		FilePath: os.Args[1],
+		Format:   openai.AudioResponseFormatSRT,
+	}
+	resp, err := c.CreateTranscription(context.Background(), req)
+	if err != nil {
+		fmt.Printf("Transcription error: %v\n", err)
+		return
+	}
+	f, err := os.Create(os.Args[1] + ".srt")
+	if err != nil {
+		fmt.Printf("Could not open file: %v\n", err)
+		return
+	}
+	defer f.Close()
+	if _, err := f.WriteString(resp.Text); err != nil {
+		fmt.Printf("Error writing to file: %v\n", err)
+		return
+	}
+}
+```
+</details>
+
 <details>
 <summary>DALL-E 2 image generation</summary>
 
@@ -420,4 +461,4 @@ func main() {
 	fmt.Println(resp.Choices[0].Message.Content)
 }
 ```
-</details>
\ No newline at end of file
+</details>
diff --git a/audio.go b/audio.go
index 9db9298..46c3711 100644
--- a/audio.go
+++ b/audio.go
@@ -13,6 +13,15 @@ const (
 	Whisper1 = "whisper-1"
 )
 
+// Response formats; Whisper uses AudioResponseFormatJSON by default.
+type AudioResponseFormat string
+
+const (
+	AudioResponseFormatJSON AudioResponseFormat = "json"
+	AudioResponseFormatSRT  AudioResponseFormat = "srt"
+	AudioResponseFormatVTT  AudioResponseFormat = "vtt"
+)
+
 // 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 {
@@ -21,6 +30,7 @@ type AudioRequest struct {
 	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...
+	Format      AudioResponseFormat
 }
 
 // AudioResponse represents a response structure for audio API.
@@ -66,10 +76,19 @@ func (c *Client) callAudioAPI(
 	}
 	req.Header.Add("Content-Type", builder.formDataContentType())
 
-	err = c.sendRequest(req, &response)
+	if request.HasJSONResponse() {
+		err = c.sendRequest(req, &response)
+	} else {
+		err = c.sendRequest(req, &response.Text)
+	}
 	return
 }
 
+// HasJSONResponse returns true if the response format is JSON.
+func (r AudioRequest) HasJSONResponse() bool {
+	return r.Format == "" || r.Format == AudioResponseFormatJSON
+}
+
 // audioMultipartForm creates a form with audio file contents and the name of the model to use for
 // audio processing.
 func audioMultipartForm(request AudioRequest, b formBuilder) error {
@@ -97,6 +116,14 @@ func audioMultipartForm(request AudioRequest, b formBuilder) error {
 		}
 	}
 
+	// Create a form field for the format (if provided)
+	if request.Format != "" {
+		err = b.writeField("response_format", string(request.Format))
+		if err != nil {
+			return fmt.Errorf("writing format: %w", err)
+		}
+	}
+
 	// Create a form field for the temperature (if provided)
 	if request.Temperature != 0 {
 		err = b.writeField("temperature", fmt.Sprintf("%.2f", request.Temperature))
diff --git a/client.go b/client.go
index b15a18a..368947b 100644
--- a/client.go
+++ b/client.go
@@ -43,7 +43,7 @@ func NewOrgClient(authToken, org string) *Client {
 	return NewClientWithConfig(config)
 }
 
-func (c *Client) sendRequest(req *http.Request, v interface{}) error {
+func (c *Client) sendRequest(req *http.Request, v any) error {
 	req.Header.Set("Accept", "application/json; charset=utf-8")
 	// Azure API Key authentication
 	if c.config.APIType == APITypeAzure {
@@ -75,12 +75,26 @@ func (c *Client) sendRequest(req *http.Request, v interface{}) error {
 		return c.handleErrorResp(res)
 	}
 
-	if v != nil {
-		if err = json.NewDecoder(res.Body).Decode(v); err != nil {
-			return err
-		}
+	return decodeResponse(res.Body, v)
+}
+
+func decodeResponse(body io.Reader, v any) error {
+	if v == nil {
+		return nil
 	}
 
+	if result, ok := v.(*string); ok {
+		return decodeString(body, result)
+	}
+	return json.NewDecoder(body).Decode(v)
+}
+
+func decodeString(body io.Reader, output *string) error {
+	b, err := io.ReadAll(body)
+	if err != nil {
+		return err
+	}
+	*output = string(b)
 	return nil
 }
 
@@ -120,6 +134,9 @@ func (c *Client) newStreamRequest(
 		// OpenAI or Azure AD authentication
 		req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.config.authToken))
 	}
+	if c.config.OrgID != "" {
+		req.Header.Set("OpenAI-Organization", c.config.OrgID)
+	}
 	return req, nil
 }
 
diff --git a/moderation.go b/moderation.go
index ff789a6..b386ddb 100644
--- a/moderation.go
+++ b/moderation.go
@@ -5,10 +5,23 @@ import (
 	"net/http"
 )
 
+// The moderation endpoint is a tool you can use to check whether content complies with OpenAI's usage policies.
+// Developers can thus identify content that our usage policies prohibits and take action, for instance by filtering it.
+
+// The default is text-moderation-latest which will be automatically upgraded over time.
+// This ensures you are always using our most accurate model.
+// If you use text-moderation-stable, we will provide advanced notice before updating the model.
+// Accuracy of text-moderation-stable may be slightly lower than for text-moderation-latest.
+const (
+	ModerationTextStable = "text-moderation-stable"
+	ModerationTextLatest = "text-moderation-latest"
+	ModerationText001    = "text-moderation-001"
+)
+
 // ModerationRequest represents a request structure for moderation API.
 type ModerationRequest struct {
-	Input string  `json:"input,omitempty"`
-	Model *string `json:"model,omitempty"`
+	Input string `json:"input,omitempty"`
+	Model string `json:"model,omitempty"`
 }
 
 // Result represents one of possible moderation results.


Test patch:
diff --git a/api_internal_test.go b/api_internal_test.go
index 83dcafc..9651ad4 100644
--- a/api_internal_test.go
+++ b/api_internal_test.go
@@ -42,6 +42,7 @@ func TestRequestAuthHeader(t *testing.T) {
 		APIType   APIType
 		HeaderKey string
 		Token     string
+		OrgID     string
 		Expect    string
 	}{
 		{
@@ -49,6 +50,15 @@ func TestRequestAuthHeader(t *testing.T) {
 			"",
 			"Authorization",
 			"dummy-token-openai",
+			"",
+			"Bearer dummy-token-openai",
+		},
+		{
+			"OpenAIOrg",
+			APITypeOpenAI,
+			"Authorization",
+			"dummy-token-openai",
+			"dummy-org-openai",
 			"Bearer dummy-token-openai",
 		},
 		{
@@ -56,6 +66,7 @@ func TestRequestAuthHeader(t *testing.T) {
 			APITypeOpenAI,
 			"Authorization",
 			"dummy-token-openai",
+			"",
 			"Bearer dummy-token-openai",
 		},
 		{
@@ -63,6 +74,7 @@ func TestRequestAuthHeader(t *testing.T) {
 			APITypeAzureAD,
 			"Authorization",
 			"dummy-token-azure",
+			"",
 			"Bearer dummy-token-azure",
 		},
 		{
@@ -70,6 +82,7 @@ func TestRequestAuthHeader(t *testing.T) {
 			APITypeAzure,
 			AzureAPIKeyHeader,
 			"dummy-api-key-here",
+			"",
 			"dummy-api-key-here",
 		},
 	}
@@ -78,6 +91,7 @@ func TestRequestAuthHeader(t *testing.T) {
 		t.Run(c.Name, func(t *testing.T) {
 			az := DefaultConfig(c.Token)
 			az.APIType = c.APIType
+			az.OrgID = c.OrgID
 
 			cli := NewClientWithConfig(az)
 			req, err := cli.newStreamRequest(context.Background(), "POST", "/chat/completions", nil)
diff --git a/audio_test.go b/audio_test.go
index 9d2abfc..daf51f2 100644
--- a/audio_test.go
+++ b/audio_test.go
@@ -112,6 +112,7 @@ func TestAudioWithOptionalArgs(t *testing.T) {
 				Prompt:      "用简体中文",
 				Temperature: 0.5,
 				Language:    "zh",
+				Format:      AudioResponseFormatSRT,
 			}
 			_, err = tc.createFn(ctx, req)
 			checks.NoError(t, err, "audio API error")
@@ -179,6 +180,7 @@ func TestAudioWithFailingFormBuilder(t *testing.T) {
 		Prompt:      "test",
 		Temperature: 0.5,
 		Language:    "en",
+		Format:      AudioResponseFormatSRT,
 	}
 
 	mockFailedErr := fmt.Errorf("mock form builder fail")
@@ -202,7 +204,7 @@ func TestAudioWithFailingFormBuilder(t *testing.T) {
 		return nil
 	}
 
-	failOn := []string{"model", "prompt", "temperature", "language"}
+	failOn := []string{"model", "prompt", "temperature", "language", "response_format"}
 	for _, failingField := range failOn {
 		failForField = failingField
 		mockFailedErr = fmt.Errorf("mock form builder fail on field %s", failingField)
diff --git a/client_test.go b/client_test.go
index 1c15985..7bea6dd 100644
--- a/client_test.go
+++ b/client_test.go
@@ -1,6 +1,8 @@
 package openai //nolint:testpackage // testing private field
 
 import (
+	"bytes"
+	"io"
 	"testing"
 )
 
@@ -20,3 +22,38 @@ func TestClient(t *testing.T) {
 		t.Errorf("Client does not contain proper orgID")
 	}
 }
+
+func TestDecodeResponse(t *testing.T) {
+	stringInput := ""
+
+	testCases := []struct {
+		name  string
+		value interface{}
+		body  io.Reader
+	}{
+		{
+			name:  "nil input",
+			value: nil,
+			body:  bytes.NewReader([]byte("")),
+		},
+		{
+			name:  "string input",
+			value: &stringInput,
+			body:  bytes.NewReader([]byte("test")),
+		},
+		{
+			name:  "map input",
+			value: &map[string]interface{}{},
+			body:  bytes.NewReader([]byte(`{"test": "test"}`)),
+		},
+	}
+
+	for _, tc := range testCases {
+		t.Run(tc.name, func(t *testing.T) {
+			err := decodeResponse(tc.body, tc.value)
+			if err != nil {
+				t.Errorf("Unexpected error: %v", err)
+			}
+		})
+	}
+}
diff --git a/moderation_test.go b/moderation_test.go
index 3535bc8..2c11456 100644
--- a/moderation_test.go
+++ b/moderation_test.go
@@ -34,7 +34,7 @@ func TestModerations(t *testing.T) {
 	// create an edit request
 	model := "text-moderation-stable"
 	moderationReq := ModerationRequest{
-		Model: &model,
+		Model: model,
 		Input: "I want to kill them.",
 	}
 	_, err = client.Moderations(ctx, moderationReq)
@@ -77,7 +77,7 @@ func handleModerationEndpoint(w http.ResponseWriter, r *http.Request) {
 
 	res := ModerationResponse{
 		ID:    strconv.Itoa(int(time.Now().Unix())),
-		Model: *moderationReq.Model,
+		Model: moderationReq.Model,
 	}
 	res.Results = append(res.Results, result)