Skip to content

Commit

Permalink
openai: do not force API key, fix error processing
Browse files Browse the repository at this point in the history
OpenAI backends will no longer require an API key to be provided. This
is because users may wish to use a service such as Portkey with its
Virtual Keys feature, which doesn't require an API key. If an API
key is not provided, an Authorization header is not sent.

Also, error handling in the openai implementation is both fixed and
expanded to support Portkey error format.
  • Loading branch information
ido50 committed Jul 8, 2024
1 parent 7c28aed commit a070ae6
Showing 1 changed file with 70 additions and 59 deletions.
129 changes: 70 additions & 59 deletions libaiac/openai/openai.go
Original file line number Diff line number Diff line change
Expand Up @@ -2,7 +2,6 @@ package openai

import (
"encoding/json"
"errors"
"fmt"
"io"
"net/http"
Expand All @@ -26,7 +25,8 @@ type OpenAI struct {
// Options is a struct containing all the parameters accepted by the New
// constructor.
type Options struct {
// APIKey is the OpenAI API key to use for requests. This is required.
// APIKey is the OpenAI API key to use for requests. Optional, but most
// OpenAI-compatible deployments will require it.
ApiKey string

// URL is the OpenAI API URL to userequests. Optional, defaults to OpenAIBackend.
Expand All @@ -53,79 +53,90 @@ func New(opts *Options) (*OpenAI, error) {
return nil, nil
}

if opts.ApiKey == "" {
return nil, errors.New("OpenAI backends require an API key")
}

// Trim "Bearer " prefix if user accidentally included it, probably by
// copy-pasting from somewhere.
opts.ApiKey = strings.TrimPrefix(opts.ApiKey, "Bearer ")

if opts.URL == "" {
opts.URL = OpenAIBackend
}

authHeaderKey := "Authorization"
authHeaderVal := fmt.Sprintf("Bearer %s", opts.ApiKey)

// If user provided a different authorization header, use it, and if that
// header is neither "Authorization" nor "Proxy-Authorization", remove the
// "Bearer " prefix from its value.
if opts.AuthHeader != "" && opts.AuthHeader != authHeaderKey {
authHeaderKey = opts.AuthHeader
if authHeaderKey != "Proxy-Authorization" {
authHeaderVal = opts.ApiKey
}
}

// The above section depends on the user telling us to use a different
// header for authorization. Previously, though, we used 'api-key' as the
// header if the URL was anything other than the OpenAI URL. This worked for
// Azure OpenAI users, but since many more providers now implement the same
// API (e.g. Portkey), that check was no longer correct. To maintain
// backwards compatibility for Azure OpenAI users, though, we can change the
// auth header by ourselves if the URL is *.openai.azure.com
if opts.AuthHeader == "" && strings.Contains(opts.URL, ".openai.azure.com") {
authHeaderKey = "api-key"
authHeaderVal = opts.ApiKey
}

backend := &OpenAI{
apiKey: opts.ApiKey,
apiVersion: opts.APIVersion,
}

backend.HTTPClient = requests.NewClient(opts.URL).
Accept("application/json").
Header(authHeaderKey, authHeaderVal).
ErrorHandler(func(
httpStatus int,
contentType string,
body io.Reader,
) error {
var res struct {
Error struct {
Message string `json:"Message"`
Type string `json:"type"`
} `json:"error"`
}
HTTPClient: requests.NewClient(opts.URL).
Accept("application/json").
ErrorHandler(func(
httpStatus int,
contentType string,
body io.Reader,
) error {
var res struct {
Error struct {
Message string `json:"message"`
Type string `json:"type"`
} `json:"error"`
Message string `json:"message"`
Status string `json:"status"`
}

err := json.NewDecoder(body).Decode(&res)
if err == nil {
if res.Error.Type != "" {
return fmt.Errorf(
"%w: [%s]: %s",
types.ErrRequestFailed,
res.Error.Type,
res.Error.Message,
)
} else if res.Message != "" {
return fmt.Errorf(
"%w: [%s]: %s",
types.ErrRequestFailed,
res.Status,
res.Message,
)
}
}

err := json.NewDecoder(body).Decode(&res)
if err != nil {
return fmt.Errorf(
"%w %s",
types.ErrUnexpectedStatus,
http.StatusText(httpStatus),
)
}),
}

if opts.ApiKey != "" {
// Trim "Bearer " prefix if user accidentally included it, probably by
// copy-pasting from somewhere.
backend.apiKey = strings.TrimPrefix(backend.apiKey, "Bearer ")

authHeaderKey := "Authorization"
authHeaderVal := fmt.Sprintf("Bearer %s", backend.apiKey)

// If user provided a different authorization header, use it, and if
// that header is neither "Authorization" nor "Proxy-Authorization",
// remove the "Bearer " prefix from its value.
if opts.AuthHeader != "" && opts.AuthHeader != authHeaderKey {
authHeaderKey = opts.AuthHeader
if authHeaderKey != "Proxy-Authorization" {
authHeaderVal = backend.apiKey
}
}

// The above section depends on the user telling us to use a different
// header for authorization. Previously, though, we used 'api-key' as
// the header if the URL was anything other than the OpenAI URL. This
// worked for Azure OpenAI users, but since many more providers now
// implement the same API (e.g. Portkey), that check was no longer
// correct. To maintain backwards compatibility for Azure OpenAI users,
// though, we can change the auth header by ourselves if the URL is
// *.openai.azure.com
if opts.AuthHeader == "" && strings.Contains(opts.URL, ".openai.azure.com") {
authHeaderKey = "api-key"
authHeaderVal = backend.apiKey
}

return fmt.Errorf(
"%w: [%s]: %s",
types.ErrRequestFailed,
res.Error.Type,
res.Error.Message,
)
})
backend.HTTPClient.Header(authHeaderKey, authHeaderVal)
}

for header, value := range opts.ExtraHeaders {
backend.HTTPClient.Header(header, value)
Expand Down

0 comments on commit a070ae6

Please sign in to comment.