Profile
Back to NewsBack
GitHub Trending 34 min
Reader Mode
openai/openai-go: The official Go library for the OpenAI API

openai/openai-go: The official Go library for the OpenAI API

8 hours ago

OpenAI Go API Library

Go Reference

The OpenAI Go library provides convenient access to the OpenAI REST API from applications written in Go.

[!WARNING]
The latest version of this package has small and limited breaking changes.
See the changelog for details.

Installation

import (
	"github.com/openai/openai-go/v3" // imported as openai
)

Or to pin an SDK version (see the Go compatibility note below):

go get -u 'github.com/openai/openai-go/[email protected]'

Requirements

SDK v3.45.0 and later require Go 1.25 or later. If your application must remain on Go 1.22–1.24, pin SDK v3.44.0, the final compatible release. Older SDK releases receive no guaranteed fixes or security backports.

See the Go version support policy for the supported release window and upgrade guidance.

Usage

The full API of this library can be found in api.md.

The primary API for interacting with OpenAI models is the Responses API. You can generate text from the model with the code below.

package main

import ( "context"

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

func main() { ctx := context.Background() client := openai.NewClient( option.WithAPIKey("My API Key"), // defaults to os.LookupEnv("OPENAI_API_KEY") )

question := "Write me a haiku about computers"

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

if err != nil { panic(err) }

println(resp.OutputText()) }

Multi-turn Responses

response, err := client.Responses.New(ctx, responses.ResponseNewParams{
	Model: openai.ChatModelGPT5_2,
	Input: responses.ResponseNewParamsInputUnion{
		OfString: openai.String("What is the capital of France?"),
	},
})
if err != nil {
	panic(err)
}
fmt.Println("First response:", response.OutputText())

// Use PreviousResponseID to continue the conversation response, err = client.Responses.New(ctx, responses.ResponseNewParams{ Model: openai.ChatModelGPT5_2, PreviousResponseID: openai.String(response.ID), Input: responses.ResponseNewParamsInputUnion{ OfString: openai.String("And what is the population of that city?"), }, }) if err != nil { panic(err) } fmt.Println("Second response:", response.OutputText())

Conversations

conv, err := client.Conversations.New(ctx, conversations.ConversationNewParams{})
if err != nil {
	panic(err)
}
fmt.Println("Created conversation:", conv.ID)

response, err := client.Responses.New(ctx, responses.ResponseNewParams{ Model: openai.ChatModelGPT5_2, Input: responses.ResponseNewParamsInputUnion{ OfString: openai.String("Hello! Remember that my favorite color is blue."), }, Conversation: responses.ResponseNewParamsConversationUnion{ OfConversationObject: &responses.ResponseConversationParam{ ID: conv.ID, }, }, }) if err != nil { panic(err) } fmt.Println("First response:", response.OutputText())

// Continue the conversation response, err = client.Responses.New(ctx, responses.ResponseNewParams{ Model: openai.ChatModelGPT5_2, Input: responses.ResponseNewParamsInputUnion{ OfString: openai.String("What is my favorite color?"), }, Conversation: responses.ResponseNewParamsConversationUnion{ OfConversationObject: &responses.ResponseConversationParam{ ID: conv.ID, }, }, }) if err != nil { panic(err) } fmt.Println("Second response:", response.OutputText())

items, err := client.Conversations.Items.List(ctx, conv.ID, conversations.ItemListParams{}) if err != nil { panic(err) } fmt.Println("Conversation has", len(items.Data), "items")

Streaming responses

ctx := context.Background()

stream := client.Responses.NewStreaming(ctx, responses.ResponseNewParams{ Model: openai.ChatModelGPT5_2, Input: responses.ResponseNewParamsInputUnion{ OfString: openai.String("Write a haiku about programming"), }, }) defer func() { _ = stream.Close() }()

for stream.Next() { event := stream.Current() print(event.Delta) }

if stream.Err() != nil { panic(stream.Err()) }

See the full streaming example

Tool calling

ctx := context.Background()

params := responses.ResponseNewParams{ Model: openai.ChatModelGPT5_2, Input: responses.ResponseNewParamsInputUnion{ OfString: openai.String("What is the weather in New York City?"), }, Tools: []responses.ToolUnionParam{{ OfFunction: &responses.FunctionToolParam{ Name: "get_weather", Description: openai.String("Get weather at the given location"), Parameters: map[string]any{ "type": "object", "properties": map[string]any{ "location": map[string]string{ "type": "string", }, }, "required": []string{"location"}, }, }, }}, }

response, _ := client.Responses.New(ctx, params)

// Check for function calls in the response output for _, item := range response.Output { if item.Type == "function_call" { toolCall := item.AsFunctionCall() if toolCall.Name == "get_weather" { // Extract arguments and call your function var args map[string]any json.Unmarshal([]byte(toolCall.Arguments), &args) location := args["location"].(string)

// Simulate getting weather data weatherData := getWeather(location) fmt.Printf("Weather in %s: %s\n", location, weatherData)

// Continue conversation with function result response, _ = client.Responses.New(ctx, responses.ResponseNewParams{ Model: openai.ChatModelGPT5_2, PreviousResponseID: openai.String(response.ID), Input: responses.ResponseNewParamsInputUnion{ OfInputItemList: []responses.ResponseInputItemUnionParam{{ OfFunctionCallOutput: &responses.ResponseInputItemFunctionCallOutputParam{ CallID: toolCall.CallID, Output: responses.ResponseInputItemFunctionCallOutputOutputUnionParam{ OfString: openai.String(weatherData), }, }, }}, }, }) } } }

Structured outputs

import (
	"encoding/json"
	"github.com/invopop/jsonschema"
	// ...
)

// A struct that will be converted to a Structured Outputs response schema type HistoricalComputer struct { Origin Origin json:"origin" jsonschema_description:"The origin of the computer" Name string json:"full_name" jsonschema_description:"The name of the device model" Legacy string json:"legacy" jsonschema:"enum=positive,enum=neutral,enum=negative" jsonschema_description:"Its influence on the field of computing" NotableFacts []string json:"notable_facts" jsonschema_description:"A few key facts about the computer" }

type Origin struct { YearBuilt int64 json:"year_of_construction" jsonschema_description:"The year it was made" Organization string json:"organization" jsonschema_description:"The organization that was in charge of its development" }

// Structured Outputs uses a subset of JSON schema // These flags are necessary to comply with the subset func GenerateSchema[T any]() (map[string]any, error) { reflector := jsonschema.Reflector{ AllowAdditionalProperties: false, DoNotReference: true, } var v T schema := reflector.Reflect(v)

data, err := json.Marshal(schema) if err != nil { return nil, err } // Preserve schema values as raw JSON so integer constraints are not rounded // through float64 before the SDK serializes the request. var rawSchema map[string]json.RawMessage if err := json.Unmarshal(data, &rawSchema); err != nil { return nil, err } result := make(map[string]any, len(rawSchema)) for key, value := range rawSchema { result[key] = value } return result, nil }

func main() { client := openai.NewClient() ctx := context.Background() schema, err := GenerateSchema[HistoricalComputer]() if err != nil { panic(err) }

response, err := client.Responses.New(ctx, responses.ResponseNewParams{ Model: openai.ChatModelGPT5_2, Input: responses.ResponseNewParamsInputUnion{ OfString: openai.String("What computer ran the first neural network?"), }, Text: responses.ResponseTextConfigParam{ Format: responses.ResponseFormatTextConfigUnionParam{ OfJSONSchema: &responses.ResponseFormatTextJSONSchemaConfigParam{ Name: "historical_computer", Description: openai.String("Notable information about a computer"), Schema: schema, Strict: openai.Bool(true), }, }, }, }) if err != nil { panic(err) }

// extract into a well-typed struct var historicalComputer HistoricalComputer if err := json.Unmarshal([]byte(response.OutputText()), &historicalComputer); err != nil { panic(err) }

historicalComputer.Name historicalComputer.Origin.YearBuilt historicalComputer.Origin.Organization for i, fact := range historicalComputer.NotableFacts { // ... } }

See the full structured outputs example

Responses WebSockets

Use client.Responses.Connect(ctx, responses.ResponseConnectionOptions{}, opts...) to open a reusable Responses connection. Client and per-connection request options, including option.WithHeader, apply to the opening handshake. See the WebSocket example. option.WithResponseInto exposes the opening HTTP response. A rejected workload-identity handshake can refresh its token within the configured retry budget; application messages are never replayed. These connections require native HTTP transport support and are unavailable in js/wasm; other SDK APIs remain usable there.

The default options do not cap message size or the receive buffer. Set MaxMessageBytes, MaxBufferedBytes or MaxBufferedEvents to a positive value to opt into a receive limit. Connections default to 16 pending sends and 64 distinct lane IDs over the connection's lifetime, including closed lanes. Reuse an open lane for sequential responses. Exceeding an explicit receive bound fails the connection without silently dropping events. Consume incoming events promptly or configure a buffer limit appropriate for the application. CloseTimeout defaults to five seconds and tears down an unresponsive connection after that interval. Always call Close or Abort when finished.

Turns, lanes, and recovery

Responses WebSockets use response.create and the normal Responses events, not Realtime sessions. Leave stream and background unset. Use Create followed by FinalResponse for a complete terminal snapshot, including tool calls; failed and incomplete responses retain their status. Recv exposes individual typed events and their RawJSON. A terminal response ends a turn, not the socket.

For opt-in incremental text and tool-input snapshots, feed the events you read to a responses.ResponseAccumulator. It consumes no events itself:

var acc responses.ResponseAccumulator
for {
    event, err := connection.Recv(ctx) // Or lane.Recv(ctx) for a named lane.
    if err != nil { return err }
    // The original typed event and event.RawJSON() remain available here.
    if err := acc.AddEvent(event); err != nil { return err }
    if event.Type == "response.completed" || event.Type == "response.failed" || event.Type == "response.incomplete" {
        snapshot := acc.Snapshot()
        // Inspect TerminalEvent: completed, failed and incomplete all end a turn.
        // Function arguments and custom-tool input in snapshot.Output are data;
        // the application decides whether and when to act on them.
        fmt.Println(snapshot.OutputText())
        break
    }
}
acc.Reset() // Ready for another turn; does not close the connection.

Use a separate accumulator for each lane. Snapshots remain unchanged as more events arrive or after Reset. Text is grouped by output and content index; function arguments and custom input retain their item and call IDs. Finalized fields replace earlier deltas. OutputText() is the entire current projection, not a delta: read a snapshot at a terminal or on explicit demand. Use the original typed events for a live UI; later done events can shorten or correct earlier text. A supplied final output, even an empty array, overrides prior items; if final output is absent or null, the opt-in projection retains collected fields. An item with a different nonempty ID starts fresh at its output index; it cannot inherit text or tool inputs from an earlier item. This projection is not a server Response. A missing or invalid terminal response, an API error, or EOF never becomes a completed result. FinalResponse keeps its original behavior and returns the server snapshot.

Use acc.DetailedSnapshot() for a mutable copy of all observed response metadata, item, part and annotation fields, beyond the fields selected by Snapshot(). Response is the last observed lifecycle response metadata, without output (nil until one arrives). Output entries are sorted by OutputIndex; Content and Annotations use sparse int64 content and annotation indices. Fields are json.RawMessage: they preserve exact numbers, omitted versus null, and fields from unknown variants, rather than manufacturing a typed complete Response. Message content and list-valued annotations are extracted to those indexed maps; null annotations remain null in the part fields. Unknown item fields, including non-message content, remain in Item.

details := acc.DetailedSnapshot() // At a terminal, or when explicitly requested.
for _, item := range details.Output {
    for contentIndex, part := range item.Content {
        text := part["text"]                   // nil if never received
        logprobs := part["logprobs"]           // provisional streamed form
        annotations := item.Annotations[contentIndex]
        _, _, _ = text, logprobs, annotations // Use in application-owned UI/state.
    }
}

Streamed logprobs accumulate with deltas. Supplied text-done logprobs replace them (including empty or null); whole part/item replacements discard prior fields and citations at those positions. Annotations and non-text parts enrich a matching known item; an annotation-only event never binds a lane, starts a turn or replaces an unrelated item. Standalone unsupported events and parts without a known item remain available on the original received event. Tools and unknown/partial data are never executed or promoted into final validated models. Every returned map and raw JSON byte slice is a separate caller-owned copy. Reading either snapshot joins/copies full accumulated output; reading one after every delta would repeatedly rebuild growing prefixes.

Continue with PreviousResponseID: openai.String(previous.ID) and only new input. For example, return a function result using its original call ID, then add a new user message. The application executes tools and retains their results:

output := responses.ResponseInputItemParamOfFunctionCallOutput("tool result")
output.OfFunctionCallOutput.CallID = openai.String(callID)
input := responses.ResponseInputParam{
    output,
    responses.ResponseInputItemParamOfMessage("Explain the result.", responses.EasyInputMessageRoleUser),
}
err := connection.Create(ctx, responses.ResponsesClientEventResponseCreateParam{
    Model: "gpt-4o-mini",
    PreviousResponseID: openai.String(previous.ID),
    Input: responses.ResponsesClientEventResponseCreateInputUnionParam{OfResponse: &input},
})

For an optional warmup, add generate: false to a create request using request.SetExtraFields(map[string]any{"generate": false}). Receive its terminal response and use its ID as the next PreviousResponseID; warmup produces no model output. Clear that extra field with request.SetExtraFields(nil) if reusing the request for a generated response. The runnable example shows a normal two-turn exchange on one connection.

StreamID controls routing and server FIFO execution; PreviousResponseID controls lineage. Reusing a lane without a parent ID starts a new chain. Register connection.Lane("fork") before sending its create; Go lanes receive events, so set StreamID on commands sent through the connection. One physical reader routes interleaved events to their lanes. Sequential creates on one lane execute FIFO at the service; different lanes may run concurrently. Keep consuming the connection's unclaimed/default stream for connection-scoped errors too.

To fork a store=false/ZDR response, use its parent ID on a different lane and wait for that lane's response.in_progress before advancing the source lane:

fork, err := connection.Lane("fork")
if err != nil { return err }
defer fork.Close()
if err := connection.Create(ctx, responses.ResponsesClientEventResponseCreateParam{
    Model: "gpt-4o-mini", Store: openai.Bool(false),
    StreamID: openai.String("fork"), PreviousResponseID: openai.String(previous.ID),
    Input: responses.ResponsesClientEventResponseCreateInputUnionParam{OfString: openai.String("Explore another approach.")},
}); err != nil { return err }
for {
    event, err := fork.Recv(ctx)
    if err != nil { return err }
    if event.Type == "error" { return &responses.ResponseProtocolError{Event: event} }
    if event.Type == "response.in_progress" { break }
    if event.Type == "response.completed" || event.Type == "response.failed" || event.Type == "response.incomplete" {
        return fmt.Errorf("fork ended before becoming ready")
    }
}
// The source lane may now advance; continue draining both lanes.

The service keeps recent parents in a connection-local cache. With store=true, older IDs may be hydrated from persisted state; with store=false or ZDR an uncached ID fails with previous_response_not_found. A same-lane continuation returning 4xx/5xx evicts its referenced parent; a failed cross-lane fork preserves the shared parent for the source lane. The SDK does not maintain this server cache.

Compaction and service limits

Server compaction uses ContextManagement entries with Type: "compaction" and CompactThreshold: openai.Int(20000). Continue normally with the latest response ID and new input. Standalone client.Responses.Compact instead returns a compacted input window. Preserve all its output items, including unknown fields, and start a new chain with PreviousResponseID omitted or null. Its compaction resource ID is not a continuation response ID. Using encoding/json and packages/param:

var input responses.ResponseInputParam
for _, item := range compacted.Output {
    input = append(input, param.Overrideresponses.ResponseInputItemUnionParam)))
}
err := connection.Create(ctx, responses.ResponsesClientEventResponseCreateParam{
    Model: "gpt-4o-mini",
    Input: responses.ResponsesClientEventResponseCreateInputUnionParam{OfResponse: &input},
})

The WebSocket mode guide defines service limits separately from SDK buffering: up to 16 active responses (additional creates queue), 32 distinct named streams per connection (the default lane does not count), and a 60-minute connection lifetime. Stream IDs contain 1–256 letters, digits, underscores, hyphens or periods. Omit the ID for the default lane; an empty string is invalid. Closing a local lane does not free a distinct stream ID at the service. SDK MaxLanes bounds distinct lane IDs over the connection lifetime, including closed lanes; MaxPendingSends bounds local writes, not active server responses.

Recv returns protocol errors as events; FinalResponse returns a *responses.ResponseProtocolError retaining the event. Inspect the nested code: previous_response_not_found requires a usable persisted parent or a fresh chain with saved full context; invalid_stream_id requires correcting the ID; websocket_stream_limit_reached requires reusing an existing stream or explicitly opening a new connection; websocket_connection_limit_reached requires a new connection. Named request errors route to their lane; no-ID errors route to the connection. Errors do not by themselves imply a successful turn.

Reconnect/Recover are explicit and never replay old commands or ambiguous writes. Every lane's server cache is lost, even if the old ID was recently used. On the replacement, register needed lanes again and resume from a persisted ID when available, or send saved full context as a new chain without a parent ID. Retain tool results so recovery does not execute tools twice. A Restore callback may run for each explicitly configured recovery attempt; it owns any commands it sends and their replay/idempotency decisions. Do not automatically retry a websocket.DeliveryError whose MayHaveBeenSent is true.

Chat Completions API

The previous standard (supported indefinitely) for generating text is the Chat Completions API. You can use that API to generate text from the model with the code below.

package main

import ( "context"

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

func main() { client := openai.NewClient()

chatCompletion, err := client.Chat.Completions.New(context.TODO(), openai.ChatCompletionNewParams{ Messages: []openai.ChatCompletionMessageParamUnion{ openai.DeveloperMessage("You are a coding assistant that talks like a pirate."), openai.UserMessage("How do I check if a slice is empty in Go?"), }, Model: openai.ChatModelGPT5_2, }) if err != nil { panic(err) }

println(chatCompletion.Choices[0].Message.Content) }

Request fields

The openai library uses the omitzero semantics from the Go 1.24+ encoding/json release for request fields.

Required primitive fields (int64, string, etc.) feature the tag ` api:"required" . These fields are always serialized, even their zero values.

Optional primitive types are wrapped in a param.Opt[T]. These fields can be set with the provided constructors, openai.String(string), openai.Int(int64), etc.

Any param.Opt[T], map, slice, struct or string enum uses the tag json:"...,omitzero" . Its zero value is considered omitted.

The param.IsOmitted(any) function can confirm the presence of any omitzero field.

p := openai.ExampleParams{
	ID:   "id_xxx",             // required property
	Name: openai.String("..."), // optional property

Point: openai.Point{ X: 0, // required field will serialize as 0 Y: openai.Int(1), // optional field will serialize as 1 // ... omitted non-required fields will not be serialized },

Origin: openai.Origin{}, // the zero value of [Origin] is considered omitted }

To send null instead of a param.Opt[T], use param.Null[T](). To send null instead of a struct T, use param.NullStruct[T]().

p.Name = param.Null[string]()       // 'null' instead of string
p.Point = param.NullStruct[Point]() // 'null' instead of struct

param.IsNull(p.Name) // true param.IsNull(p.Point) // true

Request structs contain a .SetExtraFields(map[string]any) method which can send non-conforming fields in the request body. Extra fields overwrite any struct fields with a matching key. For security reasons, only use SetExtraFields with trusted data.

To send a custom value instead of a struct, use param.OverrideT.

// In cases where the API specifies a given type,
// but you want to send something else, use [SetExtraFields]:
p.SetExtraFields(map[string]any{
	"x": 0.01, // send "x" as a float instead of int
})

// Send a number instead of an object custom := param.Overrideopenai.FooParams

Request unions

Unions are represented as a struct with fields prefixed by "Of" for each of its variants, only one field can be non-zero. The non-zero field will be serialized.

Sub-properties of the union can be accessed via methods on the union struct. These methods return a mutable pointer to the underlying data, if present.

// Only one field can be non-zero, use param.IsOmitted() to check if a field is set
type AnimalUnionParam struct {
	OfCat *Cat json:",omitzero,inline
	OfDog *Dog json:",omitzero,inline
}

animal := AnimalUnionParam{ OfCat: &Cat{ Name: "Whiskers", Owner: PersonParam{ Address: AddressParam{Street: "3333 Coyote Hill Rd", Zip: 0}, }, }, }

// Mutating a field if address := animal.GetOwner().GetAddress(); address != nil { address.ZipCode = 94304 }

Response objects

All fields in response structs are ordinary value types (not pointers or wrappers). Response structs also include a special JSON field containing metadata about each property.

type Animal struct {
	Name   string json:"name,nullable"
	Owners int    json:"owners"
	Age    int    json:"age"
	JSON   struct {
		Name        respjson.Field
		Owner       respjson.Field
		Age         respjson.Field
		ExtraFields map[string]respjson.Field
	} json:"-"
}

To handle optional data, use the .Valid() method on the JSON field. .Valid() returns true if a field is not null, not present, or couldn't be marshaled.

If .Valid() is false, the corresponding field will simply be its zero value.

raw := {"owners": 1, "name": null}

var res Animal json.Unmarshal([]byte(raw), &res)

// Accessing regular fields

res.Owners // 1 res.Name // "" res.Age // 0

// Optional field checks

res.JSON.Owners.Valid() // true res.JSON.Name.Valid() // false res.JSON.Age.Valid() // false

// Raw JSON values

res.JSON.Owners.Raw() // "1" res.JSON.Name.Raw() == "null" // true res.JSON.Name.Raw() == respjson.Null // true res.JSON.Age.Raw() == "" // true res.JSON.Age.Raw() == respjson.Omitted // true

These .JSON structs also include an ExtraFields map containing any properties in the json response that were not specified in the struct. This can be useful for API features not yet present in the SDK.

body := res.JSON.ExtraFields["my_unexpected_field"].Raw()

Response Unions

In responses, unions are represented by a flattened struct containing all possible fields from each of the object variants. To convert it to a variant use the .AsFooVariant() method or the .AsAny() method if present.

If a response value union contains primitive values, primitive fields will be alongside the properties but prefixed with Of and feature the tag json:"...,inline".

type AnimalUnion struct {
	// From variants [Dog], [Cat]
	Owner Person json:"owner"
	// From variant [Dog]
	DogBreed string json:"dog_breed"
	// From variant [Cat]
	CatBreed string json:"cat_breed"
	// ...

JSON struct { Owner respjson.Field // ... } json:"-" }

// If animal variant if animal.Owner.Address.ZipCode == "" { panic("missing zip code") }

// Switch on the variant switch variant := animal.AsAny().(type) { case Dog: case Cat: default: panic("unexpected type") }

RequestOptions

This library uses the functional options pattern. Functions defined in the option package return a RequestOption, which is a closure that mutates a RequestConfig. These options can be supplied to the client or at individual requests. For example:

client := openai.NewClient(
	// Adds a header to every request made by the client
	option.WithHeader("X-Some-Header", "custom_header_info"),
)

client.Responses.New(context.TODO(), responses.ResponseNewParams{...}, // Override the header option.WithHeader("X-Some-Header", "some_other_custom_header_info"), // Add an undocumented field to the request body, using sjson syntax option.WithJSONSet("some.json.path", map[string]string{"my": "object"}), )

The request option option.WithDebugLog(nil) may be helpful while debugging.

See the full list of request options.

Pagination

This library provides some conveniences for working with paginated list endpoints.

You can use .ListAutoPaging() methods to iterate through items across all pages:

iter := client.FineTuning.Jobs.ListAutoPaging(context.TODO(), openai.FineTuningJobListParams{
	Limit: openai.Int(20),
})
// Automatically fetches more pages as needed.
for iter.Next() {
	fineTuningJob := iter.Current()
	fmt.Printf("%+v\n", fineTuningJob)
}
if err := iter.Err(); err != nil {
	panic(err.Error())
}

Or you can use simple .List() methods to fetch a single page and receive a standard response object with additional helper methods like .GetNextPage(), e.g.:

page, err := client.FineTuning.Jobs.List(context.TODO(), openai.FineTuningJobListParams{
	Limit: openai.Int(20),
})
for page != nil {
	for _, job := range page.Data {
		fmt.Printf("%+v\n", job)
	}
	page, err = page.GetNextPage()
}
if err != nil {
	panic(err.Error())
}

Errors

When the API returns a non-success status code, we return an error with type openai.Error. This contains the StatusCode, http.Request, and *http.Response values of the request, as well as the JSON of the error body (much like other response objects in the SDK).

To handle errors, we recommend that you use the errors.As pattern:

[!WARNING]
Error.DumpRequest, Error.DumpResponse, and Error.Error expose raw
diagnostics that may include authorization headers, credentials in URLs, and
sensitive request or response bodies. The dump body option does not redact
headers. Sanitize this output before logging, sharing, or storing it.
_, err := client.FineTuning.Jobs.New(context.TODO(), openai.FineTuningJobNewParams{
	Model:        openai.FineTuningJobNewParamsModel("gpt-4o"),
	TrainingFile: "file-abc123",
})
if err != nil {
	var apierr *openai.Error
	if errors.As(err, &apierr) {
		fmt.Printf("OpenAI API error (status: %d)\n", apierr.StatusCode)
	}
	panic("OpenAI request failed")
}

When other errors occur, they are returned unwrapped; for example, if HTTP transport fails, you might receive url.Error wrapping net.OpError.

Timeouts

Requests do not time out by default; use context to configure a timeout for a request lifecycle.

Note that if a request is retried, the context timeout does not start over. To set a per-retry timeout, use option.WithRequestTimeout().

// This sets the timeout for the request, including all the retries.
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Minute)
defer cancel()
client.Responses.New(
	ctx,
	responses.ResponseNewParams{
		Model: openai.ChatModelGPT5_2,
		Input: responses.ResponseNewParamsInputUnion{
			OfString: openai.String("How can I list all files in a directory using Python?"),
		},
	},
	// This sets the per-retry timeout
	option.WithRequestTimeout(20*time.Second),
)

File uploads

Request parameters that correspond to file uploads in multipart requests are typed as io.Reader. The contents of the io.Reader will by default be sent as a multipart form part with the file name of "anonymous_file" and content-type of "application/octet-stream".

The file name and content-type can be customized by implementing Name() string or ContentType() string on the run-time type of io.Reader. Note that os.File implements Name() string, so a file returned by os.Open will be sent with the file name on disk.

We also provide a helper openai.File(reader io.Reader, filename string, contentType string) which can be used to wrap any io.Reader with the appropriate file name and content type.

// A file from the file system
file, err := os.Open("input.jsonl")
openai.FileNewParams{
	File:    file,
	Purpose: openai.FilePurposeFineTune,
}

// A file from a string openai.FileNewParams{ File: strings.NewReader("my file contents"), Purpose: openai.FilePurposeFineTune, }

// With a custom filename and contentType openai.FileNewParams{ File: openai.File(strings.NewReader({"hello": "foo"}), "file.go", "application/json"), Purpose: openai.FilePurposeFineTune, }

Webhook Verification

Verifying webhook signatures is _optional but encouraged_.

For more information about webhooks, see the API docs.

Parsing webhook payloads

For most use cases, you will likely want to verify the webhook and parse the payload at the same time. To achieve this, we provide the method client.Webhooks.Unwrap(), which parses a webhook request and verifies that it was sent by OpenAI. This method will return an error if the signature is invalid.

Note that the body parameter should be the raw JSON bytes sent from the server (do not parse it first). The Unwrap() method will parse this JSON for you into an event object after verifying the webhook was sent from OpenAI.

Webhook payloads are unauthenticated until signature verification succeeds. The example below limits each request to 1 MiB before buffering it and configures server read timeouts. Enforce the same or a smaller limit at your reverse proxy as defense in depth.

package main

import ( "errors" "io" "log" "net/http" "os" "time"

"github.com/gin-gonic/gin" "github.com/openai/openai-go/v3" "github.com/openai/openai-go/v3/option" "github.com/openai/openai-go/v3/webhooks" )

const maxWebhookBodySize = 1 << 20 // 1 MiB

func main() { client := openai.NewClient( option.WithWebhookSecret(os.Getenv("OPENAI_WEBHOOK_SECRET")), // env var used by default; explicit here. )

r := gin.Default()

r.POST("/webhook", func(c *gin.Context) { c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxWebhookBodySize) defer func() { _ = c.Request.Body.Close() }()

body, err := io.ReadAll(c.Request.Body) if err != nil { var maxBytesError *http.MaxBytesError if errors.As(err, &maxBytesError) { c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "request body too large"}) return } c.JSON(http.StatusBadRequest, gin.H{"error": "error reading request body"}) return }

webhookEvent, err := client.Webhooks.Unwrap(body, c.Request.Header) if err != nil { log.Printf("Invalid webhook signature: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "invalid signature"}) return }

switch event := webhookEvent.AsAny().(type) { case webhooks.ResponseCompletedWebhookEvent: log.Printf("Response completed: %+v", event.Data) case webhooks.ResponseFailedWebhookEvent: log.Printf("Response failed: %+v", event.Data) default: log.Printf("Unhandled event type: %T", event) }

c.JSON(http.StatusOK, gin.H{"message": "ok"}) })

server := &http.Server{ Addr: ":8000", Handler: r, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 60 * time.Second, } log.Fatal(server.ListenAndServe()) }

Verifying webhook payloads directly

In some cases, you may want to verify the webhook separately from parsing the payload. If you prefer to handle these steps separately, we provide the method client.Webhooks.VerifySignature() to _only verify_ the signature of a webhook request. Like Unwrap(), this method will return an error if the signature is invalid.

Note that the body parameter should be the raw JSON bytes sent from the server (do not parse it first). You will then need to parse the body after verifying the signature.

As above, bound the unauthenticated request body before reading it and configure server read timeouts. This example also uses a 1 MiB maximum.

package main

import ( "errors" "io" "log" "net/http" "os" "time"

"github.com/gin-gonic/gin" "github.com/openai/openai-go/v3" "github.com/openai/openai-go/v3/option" )

const maxWebhookBodySize = 1 << 20 // 1 MiB

func main() { client := openai.NewClient( option.WithWebhookSecret(os.Getenv("OPENAI_WEBHOOK_SECRET")), // env var used by default; explicit here. )

r := gin.Default()

r.POST("/webhook", func(c *gin.Context) { c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxWebhookBodySize) defer func() { _ = c.Request.Body.Close() }()

body, err := io.ReadAll(c.Request.Body) if err != nil { var maxBytesError *http.MaxBytesError if errors.As(err, &maxBytesError) { c.JSON(http.StatusRequestEntityTooLarge, gin.H{"error": "request body too large"}) return } c.JSON(http.StatusBadRequest, gin.H{"error": "error reading request body"}) return }

err = client.Webhooks.VerifySignature(body, c.Request.Header) if err != nil { log.Printf("Invalid webhook signature: %v", err) c.JSON(http.StatusBadRequest, gin.H{"error": "invalid signature"}) return }

c.JSON(http.StatusOK, gin.H{"message": "ok"}) })

server := &http.Server{ Addr: ":8000", Handler: r, ReadHeaderTimeout: 5 * time.Second, ReadTimeout: 10 * time.Second, WriteTimeout: 10 * time.Second, IdleTimeout: 60 * time.Second, } log.Fatal(server.ListenAndServe()) }

Retries

Certain errors will be automatically retried 2 times by default, with a short exponential backoff. We retry by default all connection errors, 408 Request Timeout, 409 Conflict, 429 Rate Limit, and >=500 Internal errors.

You can use the WithMaxRetries option to configure or disable this:

// Configure the default for all requests:
client := openai.NewClient(
	option.WithMaxRetries(0), // default is 2
)

// Override per-request: client.Responses.New( context.TODO(), responses.ResponseNewParams{ Model: openai.ChatModelGPT5_2, Input: responses.ResponseNewParamsInputUnion{ OfString: openai.String("How can I get the name of the current day in JavaScript?"), }, }, option.WithMaxRetries(5), )

Accessing raw response data (e.g. response headers)

You can access the raw HTTP response data by using the option.WithResponseInto() request option. This is useful when you need to examine response headers, status codes, or other details.

// Create a variable to store the HTTP response
var httpResp *http.Response
response, err := client.Responses.New(
	context.TODO(),
	responses.ResponseNewParams{
		Model: openai.ChatModelGPT5_2,
		Input: responses.ResponseNewParamsInputUnion{
			OfString: openai.String("Say this is a test"),
		},
	},
	option.WithResponseInto(&httpResp),
)
if err != nil {
	// handle error
}
fmt.Printf("%+v\n", response)

fmt.Printf("Status Code: %d\n", httpResp.StatusCode) fmt.Printf("Headers: %+#v\n", httpResp.Header)

Making custom/undocumented requests

This library is typed for convenient access to the documented API. If you need to access undocumented endpoints, params, or response properties, the library can still be used.

Undocumented endpoints

To make requests to undocumented endpoints, you can use client.Get, client.Post, and other HTTP verbs. RequestOptions on the client, such as retries, will be respected when making these requests.

var (
    // params can be an io.Reader, a []byte, an encoding/json serializable object,
    // or a "…Params" struct defined in this library.
    params map[string]any

// result can be an []byte, *http.Response, a encoding/json deserializable object, // or a model defined in this library. result *http.Response ) err := client.Post(context.Background(), "/unspecified", params, &result) if err != nil { … }

Undocumented request params

To make requests using undocumented parameters, you may use either the option.WithQuerySet() or the option.WithJSONSet() methods.

params := FooNewParams{
    ID:   "id_xxxx",
    Data: FooNewParamsData{
        FirstName: openai.String("John"),
    },
}
client.Foo.New(context.Background(), params, option.WithJSONSet("data.last_name", "Doe"))

Undocumented response properties

To access undocumented response properties, you may either access the raw JSON of the response as a string with result.JSON.RawJSON(), or get the raw JSON of a particular field on the result with result.JSON.Foo.Raw().

Any fields that are not present on the response struct will be saved and can be accessed by result.JSON.ExtraFields() which returns the extra fields as a map[string]Field.

Middleware

We provide option.WithMiddleware which applies the given middleware to requests.

func Logger(req http.Request, next option.MiddlewareNext) (res http.Response, err error) {
	// Before the request
	start := time.Now()
	LogReq(req)

// Forward the request to the next handler res, err = next(req)

// Handle stuff after the request end := time.Now() LogRes(res, err, start - end)

return res, err }

client := openai.NewClient( option.WithMiddleware(Logger), )

When multiple middlewares are provided as variadic arguments, the middlewares are applied left to right. If option.WithMiddleware is given multiple times, for example first in the client then the method, the middleware in the client will run first and the middleware given in the method will run next.

You may also replace the default http.Client with option.WithHTTPClient(client). Only one http client is accepted (this overwrites any previous client) and receives requests after any middleware has been applied.

When client is a native *http.Client, the SDK keeps its credential-origin checks in the redirect path, including when the client uses a custom http.RoundTripper. A bespoke implementation of option.HTTPClient owns any redirects it performs inside Do and must keep credentialed requests on the configured origin. Prefer a native *http.Client with a custom transport when possible.

Local HTTP development

Authenticated OpenAI requests require HTTPS, including endpoints selected by OPENAI_BASE_URL or option.WithBaseURL. A remote HTTP endpoint returns an error before credentials are sent or workload tokens are acquired. Static credentials are checked after middleware, so middleware can remove authentication before dispatch. Use an HTTPS endpoint for remote servers.

For local development, explicitly allow plaintext connections to localhost or a literal loopback IP address:

client := openai.NewClient(
    option.WithBaseURL("http://127.0.0.1:8080/v1"),
    option.WithUnsafeAllowHTTP(),
    option.WithAPIKey("local-test-key"),
)

The exception never permits remote HTTP. These local requests use a dedicated direct transport, bypassing proxies, custom transports, dialers, and HTTP doers. Use HTTPS when a test needs a custom transport. HTTPS requests and workload token exchanges retain their configured HTTP clients. Azure and Bedrock retain their provider-specific transport policies.

Mutual TLS with a custom HTTP client

For API-key authenticated HTTP requests that require mutual TLS, configure a native Go *http.Client and pass it through option.WithHTTPClient. The certificate file must contain the client leaf followed by every required intermediate. Presenting intermediates requires certificate-chain support to be enabled for your organization; otherwise, the client certificate must be signed directly by an active uploaded certificate. See the OpenAI mTLS setup requirements:

certificate, err := tls.LoadX509KeyPair(
	"/secrets/openai/client-chain.pem",
	"/secrets/openai/client.key",
)
if err != nil {
	return err
}

defaultTransport, ok := http.DefaultTransport.(*http.Transport) if !ok { return errors.New("http.DefaultTransport is not an *http.Transport") } transport := defaultTransport.Clone() transport.Proxy = nil transport.DialTLS = nil transport.DialTLSContext = nil transport.ResponseHeaderTimeout = 10 * time.Minute transport.TLSClientConfig = &tls.Config{ Certificates: []tls.Certificate{certificate}, GetClientCertificate: func(tls.CertificateRequestInfo) (tls.Certificate, error) { return &certificate, nil }, }

httpClient := &http.Client{ Transport: transport, CheckRedirect: func(http.Request, []http.Request) error { return http.ErrUseLastResponse }, }

client := openai.NewClient( option.WithBaseURL("https://mtls.api.openai.com/v1"), option.WithHTTPClient(httpClient), )

if _, err := client.Models.List(context.Background()); err != nil { return err }

The SDK does not select an mTLS endpoint automatically when a custom HTTP client is used. The explicit option.WithBaseURL above overrides OPENAI_BASE_URL; replace it with https://mtls-eu.api.openai.com/v1 for the EU endpoint, or remove it to use OPENAI_BASE_URL. Keep server trust separate by configuring RootCAs on the fresh tls.Config when custom roots are required.

tls.LoadX509KeyPair fails for unreadable files and for malformed or mismatched leaf/key material. It loads later CERTIFICATE blocks into the presented chain without validating those intermediates. Certificate validity, intermediate parsing, chain trust, and OpenAI product policy remain TLS-handshake/server checks. Rebuild the transport and OpenAI client after rotating a certificate because existing TLS connections cannot renegotiate client authentication. When overriding the HTTP client, the application also owns redirect, proxy, and timeout policy. This dedicated client bypasses proxies, retains the SDK's 10-minute response-header timeout, replaces inherited client-certificate callbacks, TLS dial hooks, and TLS session state with a fresh TLS config, and disables redirects so the client certificate is only offered to the configured API endpoint. Its callback always returns the configured certificate because Go's automatic selection can otherwise suppress it when a server's acceptable-CA hint does not match the local chain. If a proxy is required, use a transport that keeps the proxy TLS configuration separate from the origin client certificate.

The complete tested recipe is in examples/mutual-tls.

Workload Identity Authentication

For cloud workloads (Kubernetes, Azure, Google Cloud Platform), you can use workload identity authentication instead of API keys. This provides short-lived tokens that are automatically refreshed.

Kubernetes

import (
	"github.com/openai/openai-go/v3"
	"github.com/openai/openai-go/v3/auth"
	"github.com/openai/openai-go/v3/option"
)

client := openai.NewClient( option.WithWorkloadIdentity(auth.WorkloadIdentity{ IdentityProviderID: "idp-123", ServiceAccountID: "sa-456", Provider: auth.K8sServiceAccountTokenProvider(""), }), )

Azure Managed Identity

client := openai.NewClient(
	option.WithWorkloadIdentity(auth.WorkloadIdentity{
		IdentityProviderID: "idp-123",
		ServiceAccountID:   "sa-456",
		Provider:           auth.AzureManagedIdentityTokenProvider(nil),
	}),
)

Google Cloud Compute Engine

client := openai.NewClient(
	option.WithWorkloadIdentity(auth.WorkloadIdentity{
		IdentityProviderID: "idp-123",
		ServiceAccountID:   "sa-456",
		Provider:           auth.GCPIDTokenProvider(nil),
	}),
)

Custom Subject Token Provider

You can implement your own subject token provider:

import (
	"context"

"github.com/openai/openai-go/v3" "github.com/openai/openai-go/v3/auth" "github.com/openai/openai-go/v3/option" )

type customTokenProvider struct{}

func (p *customTokenProvider) TokenType() auth.SubjectTokenType { return auth.SubjectTokenTypeJWT }

func (p *customTokenProvider) GetToken(ctx context.Context, httpClient auth.HTTPDoer) (string, error) { return "your-token", nil }

client := openai.NewClient( option.WithWorkloadIdentity(auth.WorkloadIdentity{ IdentityProviderID: "idp-123", ServiceAccountID: "sa-456", Provider: &customTokenProvider{}, }), )

Customizing Refresh Buffer

By default, tokens are refreshed 20 minutes (1200 seconds) before expiry. You can customize this:

client := openai.NewClient(
	option.WithWorkloadIdentity(auth.WorkloadIdentity{
		IdentityProviderID:   "idp-123",
		ServiceAccountID:     "sa-456",
		Provider:             auth.K8sServiceAccountTokenProvider(""),
		RefreshBufferSeconds: 600,
	}),
)

X.509 Workload Identity

An enrolled workload certificate can also be exchanged directly for a short-lived OpenAI bearer token. Load and own the certificate and private key in your application, configure one static certificate on a native transport, and attest that transport before creating the client:

``go import ( "context" "crypto/tls" "net/http" "time"

"github.com/openai/openai-go/v3" "github.com/openai/openai-go/v3/auth" "github.com/openai/openai-go/v3/option" )

certificate, err := tls.LoadX509KeyPair("workload.crt", "workload.key") if err != nil { return err }

transport, err := auth.NewX509Transport(&http.Transport{ TLSClientConfig: &tls.Config{ MinVersion: tls.VersionTLS12, Certificates: []tls.Certificate{certificate}, }, }) if err != nil { return err } defer transport.Close()

client := openai.NewClient(option.WithX509WorkloadIdentity(auth.X509WorkloadIdentity{ IdentityProviderID: "idp-123", ServiceAccountID: "sa-456", RefreshBuffer: 5 * time.Minute, // Optional; five minutes is the default. Transport: transport,

... (README truncated for length)

Chat with me