> ## Documentation index
> Fetch the complete documentation index at: https://arize.com/llms.txt
> Ask it with GET https://arize.com/ask?q=<keywords or question>. Short keywords rank best.
> Use these to discover all available pages before exploring further.

# OpenAI Go SDK

> Instrument and observe OpenAI calls in Go

This module provides instrumentation for the [OpenAI Go SDK (`openai/openai-go`)](https://github.com/openai/openai-go) using the [`openinference-instrumentation-openai-go`](https://github.com/Arize-ai/openinference/tree/main/go/openinference-instrumentation-openai-go) Go package, exporting OpenInference spans to Phoenix.

## Install

```bash
go get github.com/openai/openai-go
go get github.com/Arize-ai/openinference/go/openinference-instrumentation
go get github.com/Arize-ai/openinference/go/openinference-instrumentation-openai-go
go get go.opentelemetry.io/otel
go get go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp
go get go.opentelemetry.io/otel/sdk
```

Requires Go 1.25 or newer.

## Setup

Configure an OTLP/HTTP exporter pointed at Phoenix. Create `tracing.go`:

```go expandable
package main

import (
	"context"
	"os"
	"strings"

	"go.opentelemetry.io/otel"
	"go.opentelemetry.io/otel/exporters/otlp/otlptrace/otlptracehttp"
	"go.opentelemetry.io/otel/sdk/resource"
	sdktrace "go.opentelemetry.io/otel/sdk/trace"
	semconv "go.opentelemetry.io/otel/semconv/v1.26.0"
)

func phoenixTraceEndpoint(endpoint string) string {
	endpoint = strings.TrimRight(endpoint, "/")
	if strings.HasSuffix(endpoint, "/v1/traces") {
		return endpoint
	}
	return endpoint + "/v1/traces"
}

func initTracer(ctx context.Context) (*sdktrace.TracerProvider, error) {
	// Default: self-hosted Phoenix on localhost.
	// For a remote deployment, set PHOENIX_COLLECTOR_ENDPOINT and PHOENIX_API_KEY.
	opts := []otlptracehttp.Option{
		otlptracehttp.WithEndpoint("localhost:6006"),
		otlptracehttp.WithURLPath("/v1/traces"),
		otlptracehttp.WithInsecure(),
	}
	if endpoint := os.Getenv("PHOENIX_COLLECTOR_ENDPOINT"); endpoint != "" {
		opts = []otlptracehttp.Option{
			otlptracehttp.WithEndpointURL(phoenixTraceEndpoint(endpoint)),
			otlptracehttp.WithHeaders(map[string]string{
				"Authorization": "Bearer " + os.Getenv("PHOENIX_API_KEY"),
			}),
		}
	}

	exporter, err := otlptracehttp.New(ctx, opts...)
	if err != nil {
		return nil, err
	}
	res, _ := resource.New(ctx, resource.WithAttributes(
		semconv.ServiceName("openai-go-app"),
	))
	tp := sdktrace.NewTracerProvider(
		sdktrace.WithBatcher(exporter),
		sdktrace.WithResource(res),
	)
	otel.SetTracerProvider(tp)
	return tp, nil
}
```

## Run OpenAI

Pass `openaiotel.Middleware` to the client via `option.WithMiddleware`. Session and user identifiers are stored on the Go context and applied to every LLM span descended from it.

```go expandable
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/openai/openai-go"
	"github.com/openai/openai-go/option"
	"github.com/openai/openai-go/shared"
	"go.opentelemetry.io/otel"

	"github.com/Arize-ai/openinference/go/openinference-instrumentation"
	openaiotel "github.com/Arize-ai/openinference/go/openinference-instrumentation-openai-go"
)

func main() {
	ctx := context.Background()
	tp, _ := initTracer(ctx)
	defer tp.Shutdown(ctx)

	client := openai.NewClient(
		option.WithAPIKey(os.Getenv("OPENAI_API_KEY")),
		option.WithMiddleware(openaiotel.Middleware(otel.Tracer("openai-go-app"))),
	)

	ctx = instrumentation.WithSession(ctx, "session-abc")
	ctx = instrumentation.WithUser(ctx, "user-xyz")

	resp, _ := client.Chat.Completions.New(ctx, openai.ChatCompletionNewParams{
		Model: shared.ChatModelGPT4o,
		Messages: []openai.ChatCompletionMessageParamUnion{
			openai.UserMessage("Write a haiku."),
		},
	})
	fmt.Println(resp.Choices[0].Message.Content)
}
```

To redact sensitive data, set `OPENINFERENCE_HIDE_INPUTS=true` or `OPENINFERENCE_HIDE_OUTPUTS=true`. See the [openinference Go README](https://github.com/Arize-ai/openinference/tree/main/go/openinference-instrumentation-openai-go) for the full env-var matrix and the in-code `WithTraceConfig` override.

## Observe

After setting up instrumentation and running your OpenAI application, traces will appear in the Phoenix UI for visualization and analysis. Chat completions (`openai.chat.completions.create`) and the Responses API (`openai.responses.create`) are traced as LLM spans, and the Decisions API (`openai.decisions.create`) as a DECISION span. Embeddings, completions, and image endpoints pass through to the next middleware without a span.

Streaming responses pass through unchanged, and the span ends when your code closes or fully reads the body, so its duration reflects time-to-last-token. Streamed Responses calls also record their output and token counts; streamed chat completions do not yet.

## Decisions API

The [OpenAI Decisions API](https://developers.openai.com/api/docs/guides/decisions) (`client.Decisions.New`) asks a decision model a fixed set of typed questions about text or images. It returns one typed, probabilistic answer per question instead of generated text, which makes it a good fit for routing, classification, and scoring inside your application.

Requires `openinference-instrumentation-openai-go >= v0.1.1`. The typed Decisions client arrived in `github.com/openai/openai-go/v3` v3.73.0, so install that major:

```bash
go get github.com/openai/openai-go/v3
```

`option.Middleware` is a type alias in both major versions, so the same `openaiotel.Middleware(...)` value passes to a v3 client unchanged. This program replaces the `main.go` above and reuses the `initTracer` helper from [Setup](#setup):

```go expandable
package main

import (
	"context"
	"fmt"
	"os"

	"github.com/openai/openai-go/v3"
	"github.com/openai/openai-go/v3/option"
	"go.opentelemetry.io/otel"

	openaiotel "github.com/Arize-ai/openinference/go/openinference-instrumentation-openai-go"
)

func main() {
	ctx := context.Background()
	tp, _ := initTracer(ctx)
	defer tp.Shutdown(ctx)

	client := openai.NewClient(
		option.WithAPIKey(os.Getenv("OPENAI_API_KEY")),
		option.WithMiddleware(openaiotel.Middleware(otel.Tracer("openai-go-app"))),
	)

	decision, err := client.Decisions.New(ctx, openai.DecisionNewParams{
		Model: "gpt-6-luna",
		Input: openai.DecisionNewParamsInputUnion{
			OfString: openai.String("I was charged twice for my order."),
		},
		Questions: []openai.DecisionNewParamsQuestionUnion{
			{OfPredicate: &openai.DecisionNewParamsQuestionPredicate{
				Name:         openai.String("is_refund"),
				Instructions: "Is this a refund request?",
			}},
			{OfChoice: &openai.DecisionNewParamsQuestionChoice{
				Name:         openai.String("department"),
				Instructions: "Which department should handle this complaint?",
				Choices: []openai.DecisionNewParamsQuestionChoiceChoice{
					{
						Value:       openai.DecisionNewParamsQuestionChoiceChoiceValueUnion{OfString: openai.String("billing")},
						Description: openai.String("Payments, invoices, and refunds."),
					},
					{
						Value:       openai.DecisionNewParamsQuestionChoiceChoiceValueUnion{OfString: openai.String("technical")},
						Description: openai.String("Problems using the product."),
					},
					{
						Value:       openai.DecisionNewParamsQuestionChoiceChoiceValueUnion{OfString: openai.String("shipping")},
						Description: openai.String("Delivery and tracking."),
					},
				},
			}},
			{OfScore: &openai.DecisionNewParamsQuestionScore{
				Name:         openai.String("urgency"),
				Instructions: "How urgent is this message?",
				Levels: []openai.DecisionNewParamsQuestionScoreLevel{
					{Label: "low", Description: openai.String("Can wait a week.")},
					{Label: "medium"},
					{Label: "high", Description: openai.String("Needs a reply today.")},
				},
			}},
		},
	})
	if err != nil {
		fmt.Println("openai:", err)
		return
	}

	for _, answer := range decision.Answers {
		fmt.Println(answer.RawJSON())
	}
}
```

Each call shows up in Phoenix as a **[DECISION span](/docs/phoenix/tracing/concepts-tracing/otel-openinference/span-kinds#decision)** named `openai.decisions.create`, rather than an LLM span, containing:

- `input.value`: the request body (`model`, `input`, `questions`) as JSON
- `output.value`: the response body (`answers`, `model`, `usage`) as JSON
- `decision.system`: `openai`
- `decision.provider`: inferred from the client's host, so `openai`, or `azure` for Azure OpenAI
- `decision.request.model_name`, `decision.response.model_name`, and `decision.model_name`
- `decision.token_count.input` and `decision.token_count.output`, from the response's `usage`

Phoenix names the decision model in the span's input card. The model and its usage are recorded under `decision.*` rather than `llm.*`, so decision-model calls stay out of Phoenix's LLM token and cost reporting. A non-2xx response sets the span status to `Error` and records only the request attributes. The Decisions API has no streaming mode.

The questions and answers live only in `input.value` and `output.value`, so `OPENINFERENCE_HIDE_INPUTS=true` and `OPENINFERENCE_HIDE_OUTPUTS=true` redact them completely. `OPENINFERENCE_HIDE_INPUT_IMAGES=true` replaces the `image_url` of each inline input image with `__REDACTED__`.

### Staying on openai-go v1

`openai-go` v1 has no Decisions client. Send the request with the generic `client.Post`, which goes through the same middleware and produces the same span:

```go
var res struct {
	Answers []map[string]any `json:"answers"`
}
err := client.Post(ctx, "decisions", map[string]any{
	"model": "gpt-6-luna",
	"input": "I was charged twice for my order.",
	"questions": []map[string]any{
		{"type": "predicate", "name": "is_refund", "instructions": "Is this a refund request?"},
	},
}, &res)
```

## Resources

* [OpenInference Go package for OpenAI](https://github.com/Arize-ai/openinference/tree/main/go/openinference-instrumentation-openai-go)

* [OpenAI Go SDK](https://github.com/openai/openai-go)

* [Go decisions example](https://github.com/Arize-ai/openinference/tree/main/go/openinference-instrumentation-openai-go/examples/decisions)

* [Decision span specification](https://github.com/Arize-ai/openinference/blob/main/spec/decision_spans.md)
