Repository: sashabaranov/go-openai
Pull request #485 (2023-09-02 00:00:18)

Chore Support base64 embedding format
Issue: #473 

This PR adds the support for base64 embedding format. As discussed in the issue, this format is not officially documented, however it is used in the official python library.

The following go example can be used to test the change:

```go
package main

import (
	"context"
	"encoding/json"
	"fmt"
	"os"

	"github.com/sashabaranov/go-openai"
)

func main() {

	client := openai.NewClient(os.Getenv("OPENAI_API_KEY"))

	res, err := client.CreateEmbeddings(
		context.Background(),
		openai.EmbeddingRequest{
			Input:          "Your text string goes here",
			Model:          openai.AdaEmbeddingV2,
			EncodingFormat: openai.EmbeddingEncodingFormatBase64,
		},
	)
	if err != nil {
		panic(err)
	}

	data, err := json.MarshalIndent(res, "", "  ")
	if err != nil {
		panic(err)
	}

	fmt.Println(string(data))
}
```

The following example is provided to compare the result using the python implementation:

```python
import openai
import os

openai.api_key = os.getenv("OPENAI_API_KEY")

text = "Your text string goes here"

response = openai.Embedding.create(
    model="text-embedding-ada-002", 
    input=text, 
    kwargs={"encoding_format": "base64"},
    )
print(response)
```

codecov[bot]: ## [Codecov](https://app.codecov.io/gh/sashabaranov/go-openai/pull/485?src=pr&el=h1&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) Report
> Merging [#485](https://app.codecov.io/gh/sashabaranov/go-openai/pull/485?src=pr&el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (7009b74) into [master](https://app.codecov.io/gh/sashabaranov/go-openai/commit/25da859c189c62c2454717fb2214da079017ff8e?el=desc&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) (25da859) will **decrease** coverage by `4.73%`.
> The diff coverage is `20.00%`.

```diff
@@            Coverage Diff             @@
##           master     #485      +/-   ##
==========================================
- Coverage   97.30%   92.57%   -4.73%     
==========================================
  Files          18       18              
  Lines         780      822      +42     
==========================================
+ Hits          759      761       +2     
- Misses         15       52      +37     
- Partials        6        9       +3     
```


| [Files Changed](https://app.codecov.io/gh/sashabaranov/go-openai/pull/485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None) | Coverage Δ | |
|---|---|---|
| [embeddings.go](https://app.codecov.io/gh/sashabaranov/go-openai/pull/485?src=pr&el=tree&utm_medium=referral&utm_source=github&utm_content=comment&utm_campaign=pr+comments&utm_term=None#diff-ZW1iZWRkaW5ncy5nbw==) | `48.05% <20.00%> (-51.95%)` | :arrow_down: |



Patch:
diff --git a/embeddings.go b/embeddings.go
index 1d31995..5ba91f2 100644
--- a/embeddings.go
+++ b/embeddings.go
@@ -2,6 +2,9 @@ package openai
 
 import (
 	"context"
+	"encoding/base64"
+	"encoding/binary"
+	"math"
 	"net/http"
 )
 
@@ -129,15 +132,83 @@ type EmbeddingResponse struct {
 	Usage  Usage          `json:"usage"`
 }
 
+type base64String string
+
+func (b base64String) Decode() ([]float32, error) {
+	decodedData, err := base64.StdEncoding.DecodeString(string(b))
+	if err != nil {
+		return nil, err
+	}
+
+	const sizeOfFloat32 = 4
+	floats := make([]float32, len(decodedData)/sizeOfFloat32)
+	for i := 0; i < len(floats); i++ {
+		floats[i] = math.Float32frombits(binary.LittleEndian.Uint32(decodedData[i*4 : (i+1)*4]))
+	}
+
+	return floats, nil
+}
+
+// Base64Embedding is a container for base64 encoded embeddings.
+type Base64Embedding struct {
+	Object    string       `json:"object"`
+	Embedding base64String `json:"embedding"`
+	Index     int          `json:"index"`
+}
+
+// EmbeddingResponseBase64 is the response from a Create embeddings request with base64 encoding format.
+type EmbeddingResponseBase64 struct {
+	Object string            `json:"object"`
+	Data   []Base64Embedding `json:"data"`
+	Model  EmbeddingModel    `json:"model"`
+	Usage  Usage             `json:"usage"`
+}
+
+// ToEmbeddingResponse converts an embeddingResponseBase64 to an EmbeddingResponse.
+func (r *EmbeddingResponseBase64) ToEmbeddingResponse() (EmbeddingResponse, error) {
+	data := make([]Embedding, len(r.Data))
+
+	for i, base64Embedding := range r.Data {
+		embedding, err := base64Embedding.Embedding.Decode()
+		if err != nil {
+			return EmbeddingResponse{}, err
+		}
+
+		data[i] = Embedding{
+			Object:    base64Embedding.Object,
+			Embedding: embedding,
+			Index:     base64Embedding.Index,
+		}
+	}
+
+	return EmbeddingResponse{
+		Object: r.Object,
+		Model:  r.Model,
+		Data:   data,
+		Usage:  r.Usage,
+	}, nil
+}
+
 type EmbeddingRequestConverter interface {
 	// Needs to be of type EmbeddingRequestStrings or EmbeddingRequestTokens
 	Convert() EmbeddingRequest
 }
 
+// EmbeddingEncodingFormat is the format of the embeddings data.
+// Currently, only "float" and "base64" are supported, however, "base64" is not officially documented.
+// If not specified OpenAI will use "float".
+type EmbeddingEncodingFormat string
+
+const (
+	EmbeddingEncodingFormatFloat  EmbeddingEncodingFormat = "float"
+	EmbeddingEncodingFormatBase64 EmbeddingEncodingFormat = "base64"
+)
+
 type EmbeddingRequest struct {
-	Input any            `json:"input"`
-	Model EmbeddingModel `json:"model"`
-	User  string         `json:"user"`
+	Input          any                     `json:"input"`
+	Model          EmbeddingModel          `json:"model"`
+	User           string                  `json:"user"`
+	EncodingFormat EmbeddingEncodingFormat `json:"encoding_format,omitempty"`
 }
 
 func (r EmbeddingRequest) Convert() EmbeddingRequest {
@@ -158,13 +229,18 @@ type EmbeddingRequestStrings struct {
 	Model EmbeddingModel `json:"model"`
 	// A unique identifier representing your end-user, which will help OpenAI to monitor and detect abuse.
 	User string `json:"user"`
+	// EmbeddingEncodingFormat is the format of the embeddings data.
+	// Currently, only "float" and "base64" are supported, however, "base64" is not officially documented.
+	// If not specified OpenAI will use "float".
+	EncodingFormat EmbeddingEncodingFormat `json:"encoding_format,omitempty"`
 }
 
 func (r EmbeddingRequestStrings) Convert() EmbeddingRequest {
 	return EmbeddingRequest{
-		Input: r.Input,
-		Model: r.Model,
-		User:  r.User,
+		Input:          r.Input,
+		Model:          r.Model,
+		User:           r.User,
+		EncodingFormat: r.EncodingFormat,
 	}
 }
 
@@ -181,13 +257,18 @@ type EmbeddingRequestTokens struct {
 	Model EmbeddingModel `json:"model"`
 	// A unique identifier representing your end-user, which will help OpenAI to monitor and detect abuse.
 	User string `json:"user"`
+	// EmbeddingEncodingFormat is the format of the embeddings data.
+	// Currently, only "float" and "base64" are supported, however, "base64" is not officially documented.
+	// If not specified OpenAI will use "float".
+	EncodingFormat EmbeddingEncodingFormat `json:"encoding_format,omitempty"`
 }
 
 func (r EmbeddingRequestTokens) Convert() EmbeddingRequest {
 	return EmbeddingRequest{
-		Input: r.Input,
-		Model: r.Model,
-		User:  r.User,
+		Input:          r.Input,
+		Model:          r.Model,
+		User:           r.User,
+		EncodingFormat: r.EncodingFormat,
 	}
 }
 
@@ -196,14 +277,27 @@ func (r EmbeddingRequestTokens) Convert() EmbeddingRequest {
 //
 // Body should be of type EmbeddingRequestStrings for embedding strings or EmbeddingRequestTokens
 // for embedding groups of text already converted to tokens.
-func (c *Client) CreateEmbeddings(ctx context.Context, conv EmbeddingRequestConverter) (res EmbeddingResponse, err error) { //nolint:lll
+func (c *Client) CreateEmbeddings(
+	ctx context.Context,
+	conv EmbeddingRequestConverter,
+) (res EmbeddingResponse, err error) {
 	baseReq := conv.Convert()
 	req, err := c.newRequest(ctx, http.MethodPost, c.fullURL("/embeddings", baseReq.Model.String()), withBody(baseReq))
 	if err != nil {
 		return
 	}
 
-	err = c.sendRequest(req, &res)
+	if baseReq.EncodingFormat != EmbeddingEncodingFormatBase64 {
+		err = c.sendRequest(req, &res)
+		return
+	}
+
+	base64Response := &EmbeddingResponseBase64{}
+	err = c.sendRequest(req, base64Response)
+	if err != nil {
+		return
+	}
 
+	res, err = base64Response.ToEmbeddingResponse()
 	return
 }
diff --git a/files.go b/files.go
index ea1f50a..8b933c3 100644
--- a/files.go
+++ b/files.go
@@ -17,13 +17,14 @@ type FileRequest struct {
 
 // File struct represents an OpenAPI file.
 type File struct {
-	Bytes     int    `json:"bytes"`
-	CreatedAt int64  `json:"created_at"`
-	ID        string `json:"id"`
-	FileName  string `json:"filename"`
-	Object    string `json:"object"`
-	Owner     string `json:"owner"`
-	Purpose   string `json:"purpose"`
+	Bytes         int    `json:"bytes"`
+	CreatedAt     int64  `json:"created_at"`
+	ID            string `json:"id"`
+	FileName      string `json:"filename"`
+	Object        string `json:"object"`
+	Status        string `json:"status"`
+	Purpose       string `json:"purpose"`
+	StatusDetails string `json:"status_details"`
 }
 
 // FilesList is a list of files that belong to the user or organization.


Test patch:
diff --git a/embeddings_test.go b/embeddings_test.go
index 47c4f51..9c48c5b 100644
--- a/embeddings_test.go
+++ b/embeddings_test.go
@@ -1,15 +1,16 @@
 package openai_test
 
 import (
-	. "github.com/sashabaranov/go-openai"
-	"github.com/sashabaranov/go-openai/internal/test/checks"
-
 	"bytes"
 	"context"
 	"encoding/json"
 	"fmt"
 	"net/http"
+	"reflect"
 	"testing"
+
+	. "github.com/sashabaranov/go-openai"
+	"github.com/sashabaranov/go-openai/internal/test/checks"
 )
 
 func TestEmbedding(t *testing.T) {
@@ -97,22 +98,138 @@ func TestEmbeddingModel(t *testing.T) {
 func TestEmbeddingEndpoint(t *testing.T) {
 	client, server, teardown := setupOpenAITestServer()
 	defer teardown()
+
+	sampleEmbeddings := []Embedding{
+		{Embedding: []float32{1.23, 4.56, 7.89}},
+		{Embedding: []float32{-0.006968617, -0.0052718227, 0.011901081}},
+	}
+
+	sampleBase64Embeddings := []Base64Embedding{
+		{Embedding: "pHCdP4XrkUDhevxA"},
+		{Embedding: "/1jku0G/rLvA/EI8"},
+	}
+
 	server.RegisterHandler(
 		"/v1/embeddings",
 		func(w http.ResponseWriter, r *http.Request) {
-			resBytes, _ := json.Marshal(EmbeddingResponse{})
+			var req struct {
+				EncodingFormat EmbeddingEncodingFormat `json:"encoding_format"`
+				User           string                  `json:"user"`
+			}
+			_ = json.NewDecoder(r.Body).Decode(&req)
+
+			var resBytes []byte
+			switch {
+			case req.User == "invalid":
+				w.WriteHeader(http.StatusBadRequest)
+				return
+			case req.EncodingFormat == EmbeddingEncodingFormatBase64:
+				resBytes, _ = json.Marshal(EmbeddingResponseBase64{Data: sampleBase64Embeddings})
+			default:
+				resBytes, _ = json.Marshal(EmbeddingResponse{Data: sampleEmbeddings})
+			}
 			fmt.Fprintln(w, string(resBytes))
 		},
 	)
 	// test create embeddings with strings (simple embedding request)
-	_, err := client.CreateEmbeddings(context.Background(), EmbeddingRequest{})
+	res, err := client.CreateEmbeddings(context.Background(), EmbeddingRequest{})
+	checks.NoError(t, err, "CreateEmbeddings error")
+	if !reflect.DeepEqual(res.Data, sampleEmbeddings) {
+		t.Errorf("Expected %#v embeddings, got %#v", sampleEmbeddings, res.Data)
+	}
+
+	// test create embeddings with strings (simple embedding request)
+	res, err = client.CreateEmbeddings(
+		context.Background(),
+		EmbeddingRequest{
+			EncodingFormat: EmbeddingEncodingFormatBase64,
+		},
+	)
 	checks.NoError(t, err, "CreateEmbeddings error")
+	if !reflect.DeepEqual(res.Data, sampleEmbeddings) {
+		t.Errorf("Expected %#v embeddings, got %#v", sampleEmbeddings, res.Data)
+	}
 
 	// test create embeddings with strings
-	_, err = client.CreateEmbeddings(context.Background(), EmbeddingRequestStrings{})
+	res, err = client.CreateEmbeddings(context.Background(), EmbeddingRequestStrings{})
 	checks.NoError(t, err, "CreateEmbeddings strings error")
+	if !reflect.DeepEqual(res.Data, sampleEmbeddings) {
+		t.Errorf("Expected %#v embeddings, got %#v", sampleEmbeddings, res.Data)
+	}
 
 	// test create embeddings with tokens
-	_, err = client.CreateEmbeddings(context.Background(), EmbeddingRequestTokens{})
+	res, err = client.CreateEmbeddings(context.Background(), EmbeddingRequestTokens{})
 	checks.NoError(t, err, "CreateEmbeddings tokens error")
+	if !reflect.DeepEqual(res.Data, sampleEmbeddings) {
+		t.Errorf("Expected %#v embeddings, got %#v", sampleEmbeddings, res.Data)
+	}
+
+	// test failed sendRequest
+	_, err = client.CreateEmbeddings(context.Background(), EmbeddingRequest{
+		User:           "invalid",
+		EncodingFormat: EmbeddingEncodingFormatBase64,
+	})
+	checks.HasError(t, err, "CreateEmbeddings error")
+}
+
+func TestEmbeddingResponseBase64_ToEmbeddingResponse(t *testing.T) {
+	type fields struct {
+		Object string
+		Data   []Base64Embedding
+		Model  EmbeddingModel
+		Usage  Usage
+	}
+	tests := []struct {
+		name    string
+		fields  fields
+		want    EmbeddingResponse
+		wantErr bool
+	}{
+		{
+			name: "test embedding response base64 to embedding response",
+			fields: fields{
+				Data: []Base64Embedding{
+					{Embedding: "pHCdP4XrkUDhevxA"},
+					{Embedding: "/1jku0G/rLvA/EI8"},
+				},
+			},
+			want: EmbeddingResponse{
+				Data: []Embedding{
+					{Embedding: []float32{1.23, 4.56, 7.89}},
+					{Embedding: []float32{-0.006968617, -0.0052718227, 0.011901081}},
+				},
+			},
+			wantErr: false,
+		},
+		{
+			name: "Invalid embedding",
+			fields: fields{
+				Data: []Base64Embedding{
+					{
+						Embedding: "----",
+					},
+				},
+			},
+			want:    EmbeddingResponse{},
+			wantErr: true,
+		},
+	}
+	for _, tt := range tests {
+		t.Run(tt.name, func(t *testing.T) {
+			r := &EmbeddingResponseBase64{
+				Object: tt.fields.Object,
+				Data:   tt.fields.Data,
+				Model:  tt.fields.Model,
+				Usage:  tt.fields.Usage,
+			}
+			got, err := r.ToEmbeddingResponse()
+			if (err != nil) != tt.wantErr {
+				t.Errorf("EmbeddingResponseBase64.ToEmbeddingResponse() error = %v, wantErr %v", err, tt.wantErr)
+				return
+			}
+			if !reflect.DeepEqual(got, tt.want) {
+				t.Errorf("EmbeddingResponseBase64.ToEmbeddingResponse() = %v, want %v", got, tt.want)
+			}
+		})
+	}
 }
diff --git a/files_api_test.go b/files_api_test.go
index f0a0876..1cbc728 100644
--- a/files_api_test.go
+++ b/files_api_test.go
@@ -64,7 +64,6 @@ func handleCreateFile(w http.ResponseWriter, r *http.Request) {
 		Purpose:   purpose,
 		CreatedAt: time.Now().Unix(),
 		Object:    "test-objecct",
-		Owner:     "test-owner",
 	}
 
 	resBytes, _ = json.Marshal(fileReq)