Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
2 changes: 1 addition & 1 deletion .release-please-manifest.json
Original file line number Diff line number Diff line change
@@ -1,3 +1,3 @@
{
".": "3.12.0"
".": "3.13.0"
}
6 changes: 3 additions & 3 deletions .stats.yml
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
configured_endpoints: 124
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai%2Fopenai-41f98da99f44ebe6204fce5c1dc9940f85f3472779e797b674c4fdc20306c77d.yml
openapi_spec_hash: c61259027f421f501bdc6b23cf9e430e
config_hash: 141b101c9f13b90e21af74e1686f1f41
openapi_spec_url: https://storage.googleapis.com/stainless-sdk-openapi-specs/openai%2Fopenai-fe8e67bdc351a518b113ab48e775750190e207807903d6b03ab22c438c38a588.yml
openapi_spec_hash: 8af972190647ffb9dcec516e19d8761a
config_hash: 856bee50ee3617e85a9bc9274db01dbb
14 changes: 14 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,19 @@
# Changelog

## 3.13.0 (2025-12-15)

Full Changelog: [v3.12.0...v3.13.0](https://github.com/openai/openai-go/compare/v3.12.0...v3.13.0)

### Features

* **api:** api update ([20b5112](https://github.com/openai/openai-go/commit/20b51126dc55b5fa357ae848593873d46514d820))
* **api:** fix grader input list, add dated slugs for sora-2 ([e8f0b76](https://github.com/openai/openai-go/commit/e8f0b76c55abdcca2920372f74e08621d8a530b9))


### Bug Fixes

* **azure:** correct Azure OpenAI API URL construction and auth ([3ba3736](https://github.com/openai/openai-go/commit/3ba3736c4b1a6138c05df5ccb64944a3dca6ea74))

## 3.12.0 (2025-12-11)

Full Changelog: [v3.11.0...v3.12.0](https://github.com/openai/openai-go/compare/v3.11.0...v3.12.0)
Expand Down
2 changes: 1 addition & 1 deletion README.md
Original file line number Diff line number Diff line change
Expand Up @@ -30,7 +30,7 @@ Or to pin the version:
<!-- x-release-please-start-version -->

```sh
go get -u 'github.com/openai/openai-go/v2@v3.12.0'
go get -u 'github.com/openai/openai-go/v2@v3.13.0'
```

<!-- x-release-please-end -->
Expand Down
2 changes: 2 additions & 0 deletions api.md
Original file line number Diff line number Diff line change
Expand Up @@ -330,6 +330,7 @@ Methods:

Params Types:

- <a href="https://pkg.go.dev/github.com/openai/openai-go/v3">openai</a>.<a href="https://pkg.go.dev/github.com/openai/openai-go/v3#GraderInputsParam">GraderInputsParam</a>
- <a href="https://pkg.go.dev/github.com/openai/openai-go/v3">openai</a>.<a href="https://pkg.go.dev/github.com/openai/openai-go/v3#LabelModelGraderParam">LabelModelGraderParam</a>
- <a href="https://pkg.go.dev/github.com/openai/openai-go/v3">openai</a>.<a href="https://pkg.go.dev/github.com/openai/openai-go/v3#MultiGraderParam">MultiGraderParam</a>
- <a href="https://pkg.go.dev/github.com/openai/openai-go/v3">openai</a>.<a href="https://pkg.go.dev/github.com/openai/openai-go/v3#PythonGraderParam">PythonGraderParam</a>
Expand All @@ -339,6 +340,7 @@ Params Types:

Response Types:

- <a href="https://pkg.go.dev/github.com/openai/openai-go/v3">openai</a>.<a href="https://pkg.go.dev/github.com/openai/openai-go/v3#GraderInputs">GraderInputs</a>
- <a href="https://pkg.go.dev/github.com/openai/openai-go/v3">openai</a>.<a href="https://pkg.go.dev/github.com/openai/openai-go/v3#LabelModelGrader">LabelModelGrader</a>
- <a href="https://pkg.go.dev/github.com/openai/openai-go/v3">openai</a>.<a href="https://pkg.go.dev/github.com/openai/openai-go/v3#MultiGrader">MultiGrader</a>
- <a href="https://pkg.go.dev/github.com/openai/openai-go/v3">openai</a>.<a href="https://pkg.go.dev/github.com/openai/openai-go/v3#PythonGrader">PythonGrader</a>
Expand Down
29 changes: 15 additions & 14 deletions azure/azure.go
Original file line number Diff line number Diff line change
Expand Up @@ -26,6 +26,7 @@ import (
"mime/multipart"
"net/http"
"net/url"
"path"
"strings"

"github.com/Azure/azure-sdk-for-go/sdk/azcore"
Expand Down Expand Up @@ -54,8 +55,6 @@ func WithEndpoint(endpoint string, apiVersion string) option.RequestOption {
endpoint += "/"
}

endpoint += "openai/"

withQueryAdd := option.WithQueryAdd("api-version", apiVersion)
withEndpoint := option.WithBaseURL(endpoint)

Expand Down Expand Up @@ -159,19 +158,19 @@ 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,
}

// multipartRoutes have mime/multipart payloads. These are less generic - we're very much
// expecting a transcription or translation payload for these.
var multipartRoutes = map[string]bool{
"/openai/audio/transcriptions": true,
"/openai/audio/translations": true,
"/openai/images/edits": true,
"/audio/transcriptions": true,
"/audio/translations": true,
"/images/edits": true,
}

// getReplacementPathWithDeployment parses the request body to extract out the Model parameter (or equivalent)
Expand All @@ -185,8 +184,8 @@ func getReplacementPathWithDeployment(req *http.Request) (string, error) {
return getMultipartRoute(req)
}

// No need to relocate the path. We've already tacked on /openai when we setup the endpoint.
return req.URL.Path, nil
// If route doesn't require deployment ID substitution, just return path with prefix.
return path.Join("/openai/", req.URL.Path), nil
}

func getJSONRoute(req *http.Request) (string, error) {
Expand All @@ -209,7 +208,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 getMultipartRoute(req *http.Request) (string, error) {
Expand Down Expand Up @@ -254,7 +254,8 @@ func getMultipartRoute(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
}
}
}
Expand Down
129 changes: 109 additions & 20 deletions azure/azure_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4,10 +4,12 @@ import (
"bytes"
"mime/multipart"
"net/http"
"net/url"
"testing"

"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/internal/apijson"
"github.com/openai/openai-go/v3/internal/requestconfig"
)

func TestJSONRoute(t *testing.T) {
Expand All @@ -25,7 +27,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)
Expand Down Expand Up @@ -65,7 +67,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)
Expand All @@ -84,34 +86,121 @@ func TestGetAudioMultipartRoute(t *testing.T) {
}
}

func TestNoRouteChangeNeeded(t *testing.T) {
chatCompletionParams := openai.ChatCompletionNewParams{
Model: openai.ChatModel("arbitraryDeployment"),
Messages: []openai.ChatCompletionMessageParamUnion{
openai.AssistantMessage("You are a helpful assistant"),
openai.UserMessage("Can you tell me another word for the universe?"),
func TestAPIKeyAuthentication(t *testing.T) {
rc := &requestconfig.RequestConfig{
Request: &http.Request{
Header: make(http.Header),
URL: &url.URL{},
},
}

serializedBytes, err := apijson.MarshalRoot(chatCompletionParams)
WithAPIKey("my-api-key").Apply(rc)

if err != nil {
t.Fatal(err)
if got := rc.Request.Header.Get("Api-Key"); got != "my-api-key" {
t.Errorf("Api-Key header: got %q, expected %q", got, "my-api-key")
}
}

req, err := http.NewRequest("POST", "/openai/does/not/need/a/deployment", bytes.NewReader(serializedBytes))

if err != nil {
t.Fatal(err)
func TestJSONRoutePathConstruction(t *testing.T) {
cases := []struct {
path string
expected string
}{
{"/chat/completions", "/openai/deployments/gpt-4/chat/completions"},
{"/completions", "/openai/deployments/gpt-4/completions"},
{"/embeddings", "/openai/deployments/gpt-4/embeddings"},
{"/audio/speech", "/openai/deployments/gpt-4/audio/speech"},
{"/images/generations", "/openai/deployments/gpt-4/images/generations"},
{"/models", "/openai/models"}, // endpoint without a deployment
{"/files", "/openai/files"}, // endpoint without a deployment
}
for _, tc := range cases {
req, _ := http.NewRequest("POST", tc.path, bytes.NewReader([]byte(`{"model":"gpt-4"}`)))
got, _ := getReplacementPathWithDeployment(req)
if got != tc.expected {
t.Errorf("%s: got %q, expected %q", tc.path, got, tc.expected)
}
}
}

replacementPath, err := getReplacementPathWithDeployment(req)
func TestModelWithSpecialCharsIsEscaped(t *testing.T) {
req, _ := http.NewRequest("POST", "/chat/completions", bytes.NewReader([]byte(`{"model":"my-model/v1"}`)))
got, _ := getReplacementPathWithDeployment(req)

if err != nil {
t.Fatal(err)
expected := "/openai/deployments/my-model%2Fv1/chat/completions"
if got != expected {
t.Errorf("got %q, expected %q", got, expected)
}
}

if replacementPath != "/openai/does/not/need/a/deployment" {
t.Fatalf("replacementpath didn't match: %s", replacementPath)
func TestWithEndpointBaseURL(t *testing.T) {
tests := map[string]struct {
endpoint string
apiVersion string
expectedBaseURL string
expectedQuery string
shouldFail bool
}{
"Azure endpoint": {
endpoint: "https://my-resource.openai.azure.com",
apiVersion: "2024-10-21",
expectedBaseURL: "https://my-resource.openai.azure.com/",
expectedQuery: "api-version=2024-10-21",
},
"Azure endpoint with trailing slash": {
endpoint: "https://my-resource.openai.azure.com/",
apiVersion: "2024-10-21",
expectedBaseURL: "https://my-resource.openai.azure.com/",
expectedQuery: "api-version=2024-10-21",
},
"Azure endpoint with path": {
endpoint: "https://my-resource.openai.azure.com/custom/path",
apiVersion: "2023-05-15",
expectedBaseURL: "https://my-resource.openai.azure.com/custom/path/",
expectedQuery: "api-version=2023-05-15",
},
"empty apiVersion": {
endpoint: "https://my-resource.openai.azure.com",
apiVersion: "",
shouldFail: true,
},
}

for name, tc := range tests {
t.Run(name, func(t *testing.T) {
opt := WithEndpoint(tc.endpoint, tc.apiVersion)

rc := &requestconfig.RequestConfig{
Request: &http.Request{
Header: make(http.Header),
URL: &url.URL{},
},
}

err := opt.Apply(rc)

if tc.shouldFail {
if err == nil {
t.Fatal("expected error, got nil")
}
return
}

if err != nil {
t.Fatalf("WithEndpoint returned error: %v", err)
}

if rc.BaseURL == nil {
t.Fatal("BaseURL was not set")
}
if rc.BaseURL.String() != tc.expectedBaseURL {
t.Errorf("BaseURL: got %q, expected %q", rc.BaseURL.String(), tc.expectedBaseURL)
}

query := rc.Request.URL.RawQuery
if query != tc.expectedQuery {
t.Errorf("Query: got %q, expected %q", query, tc.expectedQuery)
}
})
}
}
39 changes: 39 additions & 0 deletions examples/azure/main.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,39 @@
package main

import (
"context"
"fmt"
"log"
"os"

"github.com/openai/openai-go/v3"
"github.com/openai/openai-go/v3/azure"
"github.com/openai/openai-go/v3/responses"
)

func main() {
apiKey := os.Getenv("AZURE_OPENAI_API_KEY")
apiVersion := "2025-03-01-preview"
endpoint := "https://example-endpoint.openai.azure.com"
deploymentName := "model-name" // e.g. "gpt-4o"

client := openai.NewClient(
azure.WithEndpoint(endpoint, apiVersion),
azure.WithAPIKey(apiKey),
)

ctx := context.Background()

question := "Write me a haiku about computers"

resp, err := client.Responses.New(ctx, responses.ResponseNewParams{
Input: responses.ResponseNewParamsInputUnion{OfString: openai.String(question)},
Model: openai.ChatModel(deploymentName),
})

if err != nil {
panic(err)
}

println(resp.OutputText())
}
Loading