From 0481c13ee1c4fa263c63d92d8d15a99275c8657e Mon Sep 17 00:00:00 2001 From: Nyah Check <4006891+ch3ck@users.noreply.github.com> Date: Thu, 24 Jul 2025 22:56:18 +0000 Subject: [PATCH] fix(azure): correct Azure OpenAI API URL construction and auth - Fixes Azure OpenAI authentication issues by implementing the correct URL format: /openai/deployments/{deployment-id}/{operation} --- azure/azure.go | 26 +++++++++++++------------- azure/azure_test.go | 18 ++++++++++++++++-- 2 files changed, 29 insertions(+), 15 deletions(-) diff --git a/azure/azure.go b/azure/azure.go index df6b55d8..1a8b7a47 100644 --- a/azure/azure.go +++ b/azure/azure.go @@ -38,7 +38,7 @@ import ( // WithEndpoint configures this client to connect to an Azure OpenAI endpoint. // // - endpoint - the Azure OpenAI endpoint to connect to. Ex: https://.openai.azure.com -// - apiVersion - the Azure OpenAI API version to target (ex: 2024-06-01). See [Azure OpenAI apiversions] for current API versions. This value cannot be empty. +// - apiVersion - the Azure OpenAI API version to target (ex: 2024-10-21). See [Azure OpenAI apiversions] for current API versions. This value cannot be empty. // // This function should be paired with a call to authenticate, like [azure.WithAPIKey] or [azure.WithTokenCredential], similar to this: // @@ -54,8 +54,6 @@ func WithEndpoint(endpoint string, apiVersion string) option.RequestOption { endpoint += "/" } - endpoint += "openai/" - withQueryAdd := option.WithQueryAdd("api-version", apiVersion) withEndpoint := option.WithBaseURL(endpoint) @@ -72,7 +70,7 @@ func WithEndpoint(endpoint string, apiVersion string) option.RequestOption { return requestconfig.RequestOptionFunc(func(rc *requestconfig.RequestConfig) error { if apiVersion == "" { - return errors.New("apiVersion is an empty string, but needs to be set. See https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#rest-api-versioning for details.") + return errors.New("apiVersion is an empty string, but needs to be set. See https://learn.microsoft.com/en-us/azure/ai-services/openai/reference#rest-api-versioning for details") } if err := withQueryAdd.Apply(rc); err != nil { @@ -129,18 +127,18 @@ func WithAPIKey(apiKey string) option.RequestOption { // jsonRoutes have JSON payloads - we'll deserialize looking for a .model field in there // so we won't have to worry about individual types for completions vs embeddings, etc... var jsonRoutes = map[string]bool{ - "/openai/completions": true, - "/openai/chat/completions": true, - "/openai/embeddings": true, - "/openai/audio/speech": true, - "/openai/images/generations": true, + "/completions": true, + "/chat/completions": true, + "/embeddings": true, + "/audio/speech": true, + "/images/generations": true, } // audioMultipartRoutes have mime/multipart payloads. These are less generic - we're very much // expecting a transcription or translation payload for these. var audioMultipartRoutes = map[string]bool{ - "/openai/audio/transcriptions": true, - "/openai/audio/translations": true, + "/audio/transcriptions": true, + "/audio/translations": true, } // getReplacementPathWithDeployment parses the request body to extract out the Model parameter (or equivalent) @@ -178,7 +176,8 @@ func getJSONRoute(req *http.Request) (string, error) { } escapedDeployment := url.PathEscape(v.Model) - return strings.Replace(req.URL.Path, "/openai/", "/openai/deployments/"+escapedDeployment+"/", 1), nil + // Convert path from /chat/completions to /openai/deployments/{deployment-id}/chat/completions + return "/openai/deployments/" + escapedDeployment + req.URL.Path, nil } func getAudioMultipartRoute(req *http.Request) (string, error) { @@ -223,7 +222,8 @@ func getAudioMultipartRoute(req *http.Request) (string, error) { } escapedDeployment := url.PathEscape(string(modelBytes)) - return strings.Replace(req.URL.Path, "/openai/", "/openai/deployments/"+escapedDeployment+"/", 1), nil + // Convert path from /audio/transcriptions to /openai/deployments/{deployment-id}/audio/transcriptions + return "/openai/deployments/" + escapedDeployment + req.URL.Path, nil } } } diff --git a/azure/azure_test.go b/azure/azure_test.go index 4c3e3757..4022e289 100644 --- a/azure/azure_test.go +++ b/azure/azure_test.go @@ -25,7 +25,7 @@ func TestJSONRoute(t *testing.T) { t.Fatal(err) } - req, err := http.NewRequest("POST", "/openai/chat/completions", bytes.NewReader(serializedBytes)) + req, err := http.NewRequest("POST", "/chat/completions", bytes.NewReader(serializedBytes)) if err != nil { t.Fatal(err) @@ -65,7 +65,7 @@ func TestGetAudioMultipartRoute(t *testing.T) { t.Fatal(err) } - req, err := http.NewRequest("POST", "/openai/audio/transcriptions", bytes.NewReader(buff.Bytes())) + req, err := http.NewRequest("POST", "/audio/transcriptions", bytes.NewReader(buff.Bytes())) if err != nil { t.Fatal(err) @@ -115,3 +115,17 @@ func TestNoRouteChangeNeeded(t *testing.T) { t.Fatalf("replacementpath didn't match: %s", replacementPath) } } + +func TestAPIKeyAuthentication(t *testing.T) { + // Test that the API key option is created successfully + apiKeyOption := WithAPIKey("test-api-key") + + // Verify the option is not nil + if apiKeyOption == nil { + t.Fatal("Expected API key option to be created") + } + + // This test verifies the option is created correctly. + // The actual header setting happens in the middleware chain. + t.Log("API key option created successfully") +}