OpenAI Go API Library
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 *Catjson:",omitzero,inlineOfDog *Dogjson:",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 stringjson:"name,nullable"Owners intjson:"owners"Age intjson:"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 Personjson:"owner"// From variant [Dog] DogBreed stringjson:"dog_breed"// From variant [Cat] CatBreed stringjson:"cat_breed"// ...json:"-"JSON struct { Owner respjson.Field // ... }
}// 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, andError.Errorexpose 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, }{"hello": "foo"}// 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(
), "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)