Skip to content

Iris Concepts

This page explains the core building blocks of Iris. Understanding these concepts will help you design effective AI applications and use the SDK efficiently.

A Provider implements the core.Provider interface and adapts a specific LLM API to Iris’s unified interface. Providers handle the translation between Iris’s common types and provider-specific API formats.

type Provider interface {
// Identity
ID() string // "openai", "anthropic", etc.
Models() []ModelInfo // Available models with metadata
// Capabilities
Supports(feature Feature) bool // Check if feature is supported
// Core operations
Chat(ctx context.Context, req *ChatRequest) (*ChatResponse, error)
StreamChat(ctx context.Context, req *ChatRequest) (*ChatStream, error)
}

Each provider advertises what it supports via the Supports() method:

// Feature capability flags
const (
FeatureChat Feature = "chat"
FeatureChatStreaming Feature = "chat_streaming"
FeatureToolCalling Feature = "tool_calling"
FeatureReasoning Feature = "reasoning"
FeatureBuiltInTools Feature = "built_in_tools"
FeatureResponseChain Feature = "response_chain"
FeatureEmbeddings Feature = "embeddings"
FeatureContextualizedEmbeddings Feature = "contextualized_embeddings"
FeatureReranking Feature = "reranking"
)
// Check capabilities before using features
if provider.Supports(core.FeatureToolCalling) {
// Safe to use tool calling
}

Each provider package exports a New() function:

// Cloud providers require API keys
openaiProvider := openai.New(os.Getenv("OPENAI_API_KEY"))
anthropicProvider := anthropic.New(os.Getenv("ANTHROPIC_API_KEY"))
geminiProvider := gemini.New(os.Getenv("GEMINI_API_KEY"))
// Local providers require base URLs
ollamaProvider := ollama.New("http://localhost:11434")

Providers accept configuration options:

provider := openai.New(apiKey,
openai.WithBaseURL("https://custom-endpoint.example.com/v1"),
openai.WithOrganization("org-xxx"),
openai.WithHTTPClient(customHTTPClient),
openai.WithTimeout(60 * time.Second),
)

The core.Client wraps a provider with middleware features: retry logic, telemetry hooks, and the fluent builder API. The client is the primary entry point for SDK usage.

// Basic client
client := core.NewClient(provider)
// Client with options
client := core.NewClient(provider,
core.WithRetryPolicy(customPolicy),
core.WithTelemetry(telemetryHook),
core.WithTimeout(30 * time.Second),
)

core.Client is safe for concurrent use. Create one client and share it across goroutines:

// Create once at startup
client := core.NewClient(provider)
// Use from multiple goroutines
var wg sync.WaitGroup
for i := 0; i < 10; i++ {
wg.Add(1)
go func(id int) {
defer wg.Done()
resp, err := client.Chat("gpt-4o").
User(fmt.Sprintf("Query %d", id)).
GetResponse(ctx)
// Handle response...
}(i)
}
wg.Wait()
// Start a chat builder
builder := client.Chat("gpt-4o")
// Access underlying provider
provider := client.Provider()
// For embeddings, use the provider directly (if it supports them)
if embedProvider, ok := provider.(core.EmbeddingProvider); ok {
resp, _ := embedProvider.CreateEmbeddings(ctx, &core.EmbeddingRequest{...})
}

The ChatBuilder constructs chat requests through a fluent API. Each method returns the builder for chaining. Builders are not thread-safe—use Clone() when sharing configurations.

resp, err := client.Chat("gpt-4o").
// Message methods
System("You are a helpful assistant."). // System message
User("What is the capital of France?"). // User message
Assistant("Paris is the capital of France."). // Assistant message (for context)
// Configuration methods
Temperature(0.7). // 0.0-2.0, lower = more deterministic
MaxTokens(1000). // Maximum tokens in response
Timeout(30 * time.Second). // Request timeout (alternative to context timeout)
// Tool methods
Tools(tool1, tool2). // Available tools
// Responses API methods (GPT-4o, GPT-5, etc.)
Instructions("System-level instructions"). // System instructions
ReasoningEffort(core.ReasoningEffortMedium). // Reasoning level: low/medium/high/xhigh
WebSearch(). // Enable web search built-in tool
FileSearch("vs_abc123"). // Enable file search with vector store IDs
CodeInterpreter(). // Enable code interpreter built-in tool
ContinueFrom(previousRespID). // Chain to previous response
Truncation("auto"). // Truncation mode
// Execute
GetResponse(ctx) // Blocking response
// or
Stream(ctx) // Streaming response

You can set a timeout on the builder instead of creating a context manually:

// Using Timeout() on builder
resp, err := client.Chat("gpt-4o").
User("Hello").
Timeout(30 * time.Second).
GetResponse(context.Background())
// Equivalent to manual context timeout
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
resp, err := client.Chat("gpt-4o").User("Hello").GetResponse(ctx)

For messages with images or files, use the multimodal builder:

resp, err := client.Chat("gpt-4o").
System("Analyze images in detail.").
UserMultimodal().
Text("What objects are in this image?").
ImageURL("https://example.com/photo.jpg").
ImageURLWithDetail("https://example.com/diagram.png", core.ImageDetailHigh).
ImageFileID("file-abc123"). // From Files API
FileURL("https://example.com/doc.pdf"). // Document URL
FileID("file-xyz789"). // Document from Files API
FileBase64("report.pdf", base64Data). // Base64-encoded file
Done().
GetResponse(ctx)

For common single-image or single-file cases:

// Image from URL
resp, _ := client.Chat("gpt-4o").
UserWithImageURL("Describe this image", "https://example.com/photo.jpg").
GetResponse(ctx)
// Image from Files API
resp, _ := client.Chat("gpt-4o").
UserWithImageFileID("Analyze this screenshot", "file-abc123").
GetResponse(ctx)
// Document from URL
resp, _ := client.Chat("gpt-4o").
UserWithFileURL("Summarize this PDF", "https://example.com/doc.pdf").
GetResponse(ctx)
// Document from Files API
resp, _ := client.Chat("gpt-4o").
UserWithFileID("Extract data from this file", "file-xyz789").
GetResponse(ctx)

Use Clone() to create a copy with the current configuration:

// Base configuration
base := client.Chat("gpt-4o").
System("You are a helpful assistant.").
Temperature(0.7)
// Create specialized builders from base
codeHelper := base.Clone().System("You are a coding expert.")
mathHelper := base.Clone().System("You are a math tutor.")
// Use independently
codeResp, _ := codeHelper.User("How do I reverse a string in Go?").GetResponse(ctx)
mathResp, _ := mathHelper.User("What is the derivative of x^2?").GetResponse(ctx)

Build multi-turn conversations by adding message history:

builder := client.Chat("gpt-4o").
System("You are a helpful assistant.")
// First turn
resp1, _ := builder.Clone().
User("What is Python?").
GetResponse(ctx)
// Second turn with history
resp2, _ := builder.Clone().
User("What is Python?").
Assistant(resp1.Output).
User("How do I install it?").
GetResponse(ctx)

The response from a chat completion contains the generated content and metadata:

type ChatResponse struct {
ID string // Response identifier
Model ModelID // Actual model used
Output string // Generated text content
Usage TokenUsage // Token counts
ToolCalls []ToolCall // Tool invocations (if tools were provided)
// Responses API fields
Reasoning *ReasoningOutput // Reasoning summary (models with reasoning)
Status string // Response status
}
type TokenUsage struct {
PromptTokens int
CompletionTokens int
TotalTokens int
}
type ReasoningOutput struct {
ID string
Summary []string // Reasoning steps
}

ChatResponse provides convenience methods for common checks:

// Check for tool calls
if resp.HasToolCalls() {
tc := resp.FirstToolCall() // Get first tool call (common for single-tool scenarios)
// or iterate all
for _, tc := range resp.ToolCalls {
executeToolCall(tc)
}
}
// Check for reasoning output
if resp.HasReasoning() {
for _, step := range resp.Reasoning.Summary {
fmt.Println("Reasoning:", step)
}
}
resp, err := client.Chat("gpt-4o").
User("Hello!").
GetResponse(ctx)
if err != nil {
// Handle error (see Error Handling section)
return err
}
// Print output
fmt.Println(resp.Output)
// Check for tool calls using helper method
if resp.HasToolCalls() {
for _, tc := range resp.ToolCalls {
result := executeToolCall(tc)
// Continue conversation with tool result (see Tools section)
}
}
// Check token usage
fmt.Printf("Tokens used: %d\n", resp.Usage.TotalTokens)

Streaming delivers response tokens as they’re generated, enabling real-time UI updates. The ChatStream type provides channels for incremental processing.

type ChatStream struct {
Ch <-chan ChatChunk // Incremental content chunks
Err <-chan error // Error channel (receives at most one)
Final <-chan *ChatResponse // Aggregated final response
}
type ChatChunk struct {
Delta string // Incremental text content
}
stream, err := client.Chat("gpt-4o").
User("Tell me a story.").
Stream(ctx)
if err != nil {
return err
}
// Process chunks as they arrive
for chunk := range stream.Ch {
fmt.Print(chunk.Delta) // Real-time output
}
// Check for errors
if err := <-stream.Err; err != nil {
return err
}
// Get final aggregated response
final := <-stream.Final
fmt.Printf("\nTotal tokens: %d\n", final.Usage.TotalTokens)
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
stream, err := client.Chat("gpt-4o").
User("Write a long essay.").
Stream(ctx)
if err != nil {
return err
}
for {
select {
case chunk, ok := <-stream.Ch:
if !ok {
// Stream finished
break
}
fmt.Print(chunk.Delta)
case <-ctx.Done():
// Timeout or cancellation
return ctx.Err()
}
}

Use core.DrainStream to collect a stream into a full response:

stream, err := client.Chat("gpt-4o").
User("Hello").
Stream(ctx)
if err != nil {
return err
}
// Drain stream to get full response
resp, err := core.DrainStream(ctx, stream)
if err != nil {
return err
}
// Now you have a regular ChatResponse
fmt.Println(resp.Output)

When streaming with tools, consume the text stream normally and get complete tool calls from the Final response:

stream, err := client.Chat("gpt-4o").
User("What's the weather in Tokyo?").
Tools(weatherTool).
Stream(ctx)
if err != nil {
return err
}
// Consume text chunks
for chunk := range stream.Ch {
fmt.Print(chunk.Delta)
}
// Check for errors
if err := <-stream.Err; err != nil {
return err
}
// Get complete tool calls from final response
final := <-stream.Final
if final != nil && final.HasToolCalls() {
for _, tc := range final.ToolCalls {
executeToolCall(tc)
}
}

Tools enable models to invoke external functions with structured arguments. This is the foundation for building agents that can take actions in the real world.

Tools implement the core.Tool interface:

type Tool interface {
Name() string // Unique identifier
Description() string // What the tool does (used by model)
}

Tool implementations typically include JSON Schema for parameters. See the tools package for helper types and builders.

When a model decides to use a tool, it returns tool calls instead of text:

type ToolCall struct {
ID string // Unique call identifier
Name string // Tool name
Arguments json.RawMessage // JSON-encoded arguments
}

After executing tools, send results back using the immutable ToolResults API:

type ToolResult struct {
CallID string // Must match ToolCall.ID from the response
Content any // Result data (will be JSON marshaled)
IsError bool // True if this represents an error
}

Complete tool calling requires multiple turns:

// Turn 1: Initial request with tools
resp, err := client.Chat("gpt-4o").
System("You are a helpful assistant with access to weather data.").
User("What's the weather in New York?").
Tools(weatherTool).
GetResponse(ctx)
if err != nil {
return err
}
// Check if model wants to call a tool
if resp.HasToolCalls() {
tc := resp.FirstToolCall()
// Parse arguments
var args struct {
Location string `json:"location"`
Unit string `json:"unit"`
}
json.Unmarshal(tc.Arguments, &args)
// Execute the tool (your implementation)
weatherResult := getWeather(args.Location, args.Unit)
// Turn 2: Send tool result back
// Note: ToolResult returns a NEW builder (immutable)
finalResp, err := client.Chat("gpt-4o").
System("You are a helpful assistant with access to weather data.").
User("What's the weather in New York?").
ToolResult(resp, tc.ID, weatherResult). // Pass original response + result
Tools(weatherTool).
GetResponse(ctx)
if err != nil {
return err
}
fmt.Println(finalResp.Output)
// "The current weather in New York is 72°F with partly cloudy skies."
}

The ChatBuilder provides three convenience methods for tool results:

// Single successful result
newBuilder := builder.ToolResult(resp, callID, content)
// Single error result
newBuilder := builder.ToolError(resp, callID, err)
// Multiple results at once
newBuilder := builder.ToolResults(resp, []core.ToolResult{
{CallID: call1.ID, Content: result1, IsError: false},
{CallID: call2.ID, Content: result2, IsError: false},
})

When a model returns multiple tool calls, execute them and return all results:

if resp.HasToolCalls() {
// Execute all tool calls
results := make([]core.ToolResult, 0, len(resp.ToolCalls))
for _, tc := range resp.ToolCalls {
result, err := executeToolCall(tc)
if err != nil {
results = append(results, core.ToolResult{
CallID: tc.ID,
Content: err.Error(),
IsError: true,
})
} else {
results = append(results, core.ToolResult{
CallID: tc.ID,
Content: result,
IsError: false,
})
}
}
// Send all results back
finalResp, err := client.Chat("gpt-4o").
User("What's the weather in New York and London?").
ToolResults(resp, results).
Tools(weatherTool).
GetResponse(ctx)
}

Some models (GPT-5, Claude with extended thinking) provide reasoning summaries explaining their thought process. Additionally, certain models have built-in tools like web search.

type Reasoning struct {
Summary string // High-level explanation
Steps []string // Step-by-step reasoning (if available)
}

Access reasoning in responses:

resp, err := client.Chat("gpt-5").
User("Solve: If a train leaves Chicago at 9am going 60mph...").
GetResponse(ctx)
if resp.Reasoning != nil {
fmt.Println("Model reasoning:")
fmt.Println(resp.Reasoning.Summary)
for i, step := range resp.Reasoning.Steps {
fmt.Printf("%d. %s\n", i+1, step)
}
}
fmt.Println("\nAnswer:", resp.Output)

GPT-5+ models support built-in tools that the model can invoke internally:

resp, err := client.Chat("gpt-5").
User("Search the web for the latest news about AI.").
BuiltInTools(
core.BuiltInToolWebSearch, // Web search
core.BuiltInToolFileSearch, // File search in uploaded files
).
GetResponse(ctx)

Vision-capable models can process images alongside text. Iris provides a consistent interface across providers.

// From URL
resp, err := client.Chat("gpt-4o").
UserMultimodal().
Text("Describe this image.").
ImageURL("https://example.com/photo.jpg").
Done().
GetResponse(ctx)
// From base64
imageData, _ := os.ReadFile("photo.png")
base64Data := base64.StdEncoding.EncodeToString(imageData)
resp, err := client.Chat("gpt-4o").
UserMultimodal().
Text("What's in this image?").
ImageBase64(base64Data, "image/png").
Done().
GetResponse(ctx)
// Multiple images
resp, err := client.Chat("gpt-4o").
UserMultimodal().
Text("Compare these two images.").
ImageURL("https://example.com/before.jpg").
ImageURL("https://example.com/after.jpg").
Done().
GetResponse(ctx)

Control the detail level for image analysis:

resp, err := client.Chat("gpt-4o").
UserMultimodal().
Text("Analyze this diagram in detail.").
ImageURL("https://example.com/diagram.png", core.ImageDetailHigh).
Done().
GetResponse(ctx)
// Detail levels:
// - core.ImageDetailAuto (default) - Model decides
// - core.ImageDetailLow - Faster, lower token usage
// - core.ImageDetailHigh - More detailed analysis
FormatOpenAIAnthropicGeminiOllama
PNG
JPEG
GIF-
WebP-

Embeddings convert text into dense vectors for semantic search, clustering, and RAG pipelines. Providers that support embeddings implement the EmbeddingProvider interface.

Embeddings are created directly on providers that support them:

// Cast provider to EmbeddingProvider
embedProvider, ok := provider.(core.EmbeddingProvider)
if !ok {
return errors.New("provider does not support embeddings")
}
// Single text embedding
resp, err := embedProvider.CreateEmbeddings(ctx, &core.EmbeddingRequest{
Model: "text-embedding-3-small",
Input: []core.EmbeddingInput{
{Text: "The quick brown fox jumps over the lazy dog."},
},
})
if err != nil {
return err
}
fmt.Printf("Dimensions: %d\n", len(resp.Vectors[0].Vector))
// Dimensions: 1536
// Batch embedding
resp, err := embedProvider.CreateEmbeddings(ctx, &core.EmbeddingRequest{
Model: "text-embedding-3-small",
Input: []core.EmbeddingInput{
{Text: "First document"},
{Text: "Second document"},
{Text: "Third document"},
},
})
for i, vec := range resp.Vectors {
fmt.Printf("Document %d: %d dimensions\n", i, len(vec.Vector))
}
dimensions := 512
resp, err := embedProvider.CreateEmbeddings(ctx, &core.EmbeddingRequest{
Model: "text-embedding-3-small",
Input: []core.EmbeddingInput{{Text: "Query text"}},
Dimensions: &dimensions, // Reduce dimensions (if supported)
EncodingFormat: core.EncodingFormatFloat, // "float" or "base64"
InputType: core.InputTypeQuery, // Optimize for queries vs documents
})
// Generate query embedding
resp, _ := embedProvider.CreateEmbeddings(ctx, &core.EmbeddingRequest{
Model: "text-embedding-3-small",
Input: []core.EmbeddingInput{{Text: "What is machine learning?"}},
InputType: core.InputTypeQuery,
})
queryVector := resp.Vectors[0].Vector
// Search vector store (Qdrant example)
results, err := qdrantClient.Search(&qdrant.SearchRequest{
Vector: queryVector,
Limit: 5,
})
// Use results in RAG prompt
var ragContext strings.Builder
for _, r := range results {
ragContext.WriteString(r.Payload["text"].(string))
ragContext.WriteString("\n---\n")
}
chatResp, _ := client.Chat("gpt-4o").
System("Answer based on the provided context.").
User(fmt.Sprintf("Context:\n%s\n\nQuestion: What is machine learning?", ragContext.String())).
GetResponse(ctx)

Telemetry hooks let you instrument Iris requests for observability, debugging, and cost tracking. The telemetry system is designed to never include sensitive data like API keys, prompts, or responses.

type TelemetryHook interface {
OnRequestStart(e RequestStartEvent)
OnRequestEnd(e RequestEndEvent)
}
type RequestStartEvent struct {
Provider string // Provider identifier (e.g., "openai", "anthropic")
Model ModelID // Model being called
Start time.Time // When the request started
}
type RequestEndEvent struct {
Provider string // Provider identifier
Model ModelID // Model that was called
Start time.Time // When the request started
End time.Time // When the request completed
Usage TokenUsage // Token consumption
Err error // Error if request failed, nil on success
}
// RequestEndEvent has a convenience method
func (e RequestEndEvent) Duration() time.Duration
type LoggingHook struct {
logger *log.Logger
}
func (h *LoggingHook) OnRequestStart(e core.RequestStartEvent) {
h.logger.Printf("Starting request to %s/%s", e.Provider, e.Model)
}
func (h *LoggingHook) OnRequestEnd(e core.RequestEndEvent) {
if e.Err != nil {
h.logger.Printf("Request failed after %v: %v", e.Duration(), e.Err)
return
}
h.logger.Printf("Request completed in %v, tokens: %d", e.Duration(), e.Usage.TotalTokens)
}
// Use the hook
client := core.NewClient(provider,
core.WithTelemetry(&LoggingHook{logger: log.Default()}),
)

For cases where no telemetry is needed, use the built-in no-op implementation:

var _ core.TelemetryHook = core.NoopTelemetryHook{}
type CostTracker struct {
mu sync.Mutex
costs map[string]float64
}
func (t *CostTracker) OnRequestStart(e core.RequestStartEvent) {}
func (t *CostTracker) OnRequestEnd(e core.RequestEndEvent) {
if e.Err != nil {
return
}
cost := calculateCost(string(e.Model), e.Usage.PromptTokens, e.Usage.CompletionTokens)
t.mu.Lock()
t.costs[string(e.Model)] += cost
t.mu.Unlock()
}
func calculateCost(model string, promptTokens, outputTokens int) float64 {
// Model-specific pricing
prices := map[string]struct{ prompt, output float64 }{
"gpt-4o": {0.005, 0.015}, // per 1K tokens
"gpt-4o-mini": {0.00015, 0.0006},
}
p, ok := prices[model]
if !ok {
return 0
}
return (float64(promptTokens)/1000)*p.prompt + (float64(outputTokens)/1000)*p.output
}
import (
"go.opentelemetry.io/otel"
"go.opentelemetry.io/otel/attribute"
"go.opentelemetry.io/otel/trace"
)
type OTelHook struct {
tracer trace.Tracer
spans sync.Map // Track spans by request
}
func (h *OTelHook) OnRequestStart(e core.RequestStartEvent) {
ctx, span := h.tracer.Start(context.Background(), "llm.request",
trace.WithAttributes(
attribute.String("llm.provider", e.Provider),
attribute.String("llm.model", string(e.Model)),
),
)
// Store span for matching OnRequestEnd
h.spans.Store(e.Start.UnixNano(), span)
}
func (h *OTelHook) OnRequestEnd(e core.RequestEndEvent) {
spanVal, ok := h.spans.LoadAndDelete(e.Start.UnixNano())
if !ok {
return
}
span := spanVal.(trace.Span)
span.SetAttributes(
attribute.Int("llm.tokens.total", e.Usage.TotalTokens),
attribute.Int64("llm.duration_ms", e.Duration().Milliseconds()),
)
if e.Err != nil {
span.RecordError(e.Err)
}
span.End()
}

Retry policies handle transient failures with configurable exponential backoff.

type RetryPolicy interface {
// NextDelay returns the delay before the next retry attempt and whether to retry.
// If ok is false, no more retries should be attempted.
// attempt starts at 0 for the first retry after the initial failure.
NextDelay(attempt int, err error) (delay time.Duration, ok bool)
}
// Iris includes a sensible default: exponential backoff with jitter,
// max 3 retries, 30s max delay
client := core.NewClient(provider) // Uses DefaultRetryPolicy()
// Or explicitly
client := core.NewClient(provider,
core.WithRetryPolicy(core.DefaultRetryPolicy()),
)

Use RetryConfig to create a policy with custom settings:

type RetryConfig struct {
MaxRetries int // Maximum number of retry attempts (default: 3)
BaseDelay time.Duration // Initial delay before first retry (default: 1s)
MaxDelay time.Duration // Maximum delay cap (default: 30s)
Jitter float64 // Jitter factor 0.0-1.0 (default: 0.2)
}
policy := core.NewRetryPolicy(core.RetryConfig{
MaxRetries: 5,
BaseDelay: 500 * time.Millisecond,
MaxDelay: 60 * time.Second,
Jitter: 0.3,
})
client := core.NewClient(provider,
core.WithRetryPolicy(policy),
)

The default retry policy automatically classifies errors:

Error TypeRetryableReason
ErrRateLimited (429)Temporary, will resolve
ErrServer (500+)Transient infrastructure issue
ErrNetworkNetwork glitch
ErrUnauthorized (401)Invalid credentials won’t change
ErrBadRequest (400)Request itself is malformed
ErrDecodeResponse parsing failure
context.CanceledExplicit cancellation
context.DeadlineExceededTimeout by caller

Implement the RetryPolicy interface for full control:

type MyRetryPolicy struct {
maxAttempts int
}
func (p *MyRetryPolicy) NextDelay(attempt int, err error) (time.Duration, bool) {
if attempt >= p.maxAttempts {
return 0, false
}
// Only retry rate limits
if !errors.Is(err, core.ErrRateLimited) {
return 0, false
}
// Fixed delay
return 5 * time.Second, true
}
client := core.NewClient(provider,
core.WithRetryPolicy(&MyRetryPolicy{maxAttempts: 3}),
)
// Pass nil to disable retries entirely
client := core.NewClient(provider,
core.WithRetryPolicy(nil),
)

The core.Secret type wraps sensitive values to prevent accidental logging or exposure.

type Secret struct {
value string
}
func NewSecret(value string) Secret {
return Secret{value: value}
}
func (s Secret) String() string {
return "[REDACTED]" // Never exposes actual value
}
func (s Secret) Expose() string {
return s.value // Explicit access required
}
func (s Secret) IsEmpty() bool {
return s.value == ""
}
// Create a secret
apiKey := core.NewSecret("sk-actual-api-key-here")
// Safe to log
log.Printf("Using API key: %s", apiKey)
// Output: Using API key: [REDACTED]
// Explicit access for actual use
provider := openai.New(apiKey.Expose())

The encrypted keystore returns secrets:

keystore, err := core.LoadKeystore()
if err != nil {
return err
}
// Returns core.Secret, not string
apiKey, err := keystore.Get("openai")
if err != nil {
return err
}
// Safe to pass around - won't leak in logs
provider := openai.New(apiKey.Expose())

Iris provides structured errors for provider failures and sentinel errors for classification.

The primary error type for API failures with full context:

type ProviderError struct {
Provider string // Provider identifier (e.g., "openai")
Status int // HTTP status code
RequestID string // Provider request ID (for support)
Code string // Provider error code
Message string // Error description
Err error // Underlying/wrapped error
}
// Error implements the error interface
func (e *ProviderError) Error() string
// Unwrap returns the underlying error for error chaining
func (e *ProviderError) Unwrap() error

Use sentinel errors for error classification with errors.Is():

var (
ErrUnauthorized = errors.New("unauthorized") // Invalid API key (401)
ErrRateLimited = errors.New("rate limited") // Too many requests (429)
ErrBadRequest = errors.New("bad request") // Invalid request (400)
ErrNotFound = errors.New("not found") // Resource not found (404)
ErrServer = errors.New("server error") // Provider issue (5xx)
ErrNetwork = errors.New("network error") // Connection failure
ErrDecode = errors.New("decode error") // Response parsing failed
ErrNotSupported = errors.New("operation not supported")
)
// Validation errors with guidance
var (
ErrModelRequired = errors.New("model required: pass a model ID to Client.Chat()")
ErrNoMessages = errors.New("no messages: add at least one message using .User()")
)
resp, err := client.Chat("gpt-4o").User("Hello").GetResponse(ctx)
if err != nil {
// Check for sentinel errors (most common)
switch {
case errors.Is(err, core.ErrRateLimited):
// Retry with backoff (handled automatically if retry policy enabled)
log.Println("Rate limited, waiting...")
time.Sleep(time.Second)
return retry(ctx)
case errors.Is(err, core.ErrUnauthorized):
// Invalid credentials - don't retry
return fmt.Errorf("check API key: %w", err)
case errors.Is(err, core.ErrBadRequest):
// Request is malformed - don't retry
return fmt.Errorf("invalid request: %w", err)
case errors.Is(err, core.ErrServer):
// Provider issue - may retry
log.Println("Server error, retrying...")
return retry(ctx)
case errors.Is(err, core.ErrNetwork):
// Connection issue - may retry
log.Println("Network error, retrying...")
return retry(ctx)
}
// Get full error details if available
var pe *core.ProviderError
if errors.As(err, &pe) {
log.Printf("Provider %s error: status=%d code=%s request_id=%s",
pe.Provider, pe.Status, pe.Code, pe.RequestID)
}
return fmt.Errorf("request failed: %w", err)
}

Handle context cancellation and timeouts:

resp, err := client.Chat("gpt-4o").User("Hello").GetResponse(ctx)
if err != nil {
if errors.Is(err, context.Canceled) {
return fmt.Errorf("request canceled by caller")
}
if errors.Is(err, context.DeadlineExceeded) {
return fmt.Errorf("request timed out")
}
return err
}

Tools Guide

Build tool-augmented agents. Tools →