The functional options pattern for flexible client configuration.
Wormhole uses the functional options pattern for client configuration. This pattern provides:
- Zero default configuration - clients work without any options
- Composability - combine options in any order
- Extensibility - add custom options without breaking existing code
- Type safety - compile-time validation of configuration
- Future-proof - add new options without changing the API
In practice, this makes client setup both composable and extensible as your application grows.
An Option is a function that modifies a configuration struct:
// Option is a function that configures a Wormhole client
type Option func(*Config)
// Example option implementation
func WithDefaultProvider(name string) Option {
return func(c *Config) {
c.DefaultProvider = name
}
}Options are passed to New() and applied in order:
client := wormhole.New(
wormhole.WithOpenAI(apiKey),
wormhole.WithAnthropic(apiKey),
wormhole.WithDefaultProvider("openai"),
wormhole.WithDebugLogging(),
)Benefits over struct-based configuration:
| Struct Config | Options Pattern |
|---|---|
Config{Provider: "openai", Timeout: 30} |
WithOpenAI(key), WithTimeout(30) |
| Must know all fields | Discover options via autocomplete |
| Hard to add defaults | Sensible defaults built-in |
| Breaking changes add fields | New options are non-breaking |
Configure the OpenAI provider:
client := wormhole.New(
wormhole.WithOpenAI("sk-..."),
)With additional configuration:
client := wormhole.New(
wormhole.WithOpenAI("sk-...", types.ProviderConfig{
BaseURL: "https://api.openai.com/v1",
Timeout: 60 * time.Second,
}),
)Configure the OpenAI provider to use the Responses API for text generation:
client := wormhole.New(
wormhole.WithDefaultProvider("openai"),
wormhole.WithOpenAIResponses("sk-..."),
)WithOpenAIResponses sets types.ProviderConfig.UseResponsesAPI to true.
The default remains Chat Completions when you use WithOpenAI.
Responses-specific provider options:
| Option | Type | Default | Source | Effect |
|---|---|---|---|---|
UseResponsesAPI |
bool |
false |
types/provider.go |
Route OpenAI Text() and Stream() through /responses |
ResponsesPath |
string |
/responses |
providers/openai/openai.go |
Override the path appended to BaseURL |
ChatPath |
string |
/chat/completions |
providers/openai/openai.go |
Override the Chat Completions path when Responses mode is off |
Use ProviderOptions() on a text request for advanced Responses fields that do
not have first-class builder methods:
resp, err := client.Text().
Model("gpt-5").
Prompt("Summarize this").
ProviderOptions(map[string]any{
"store": false,
"reasoning": map[string]any{"effort": "low"},
}).
Generate(ctx)Configure the Anthropic provider:
client := wormhole.New(
wormhole.WithAnthropic("sk-ant-..."),
)Configure the Google Gemini provider:
client := wormhole.New(
wormhole.WithGemini("AIza..."),
)Configure a generic OpenAI-compatible provider:
client := wormhole.New(
wormhole.WithOpenAICompatible("custom", "https://api.example.com/v1", types.ProviderConfig{
APIKey: "key",
}),
)ProviderConfig.APIKey takes precedence when set. Otherwise Wormhole uses
APIKeys[0] consistently for initial runtime authentication, configuration
validation, and model discovery; remaining entries stay available for key
rotation.
Common OpenAI-compatible providers:
- Groq:
WithGroq(apiKey) - Mistral:
WithMistral(config) - OpenRouter:
WithOpenAICompatible("openrouter", "https://openrouter.ai/api/v1", config)
Configure Groq using its OpenAI-compatible API:
client := wormhole.New(
wormhole.WithGroq("gsk_..."),
)Configure Mistral using the built-in OpenAI-compatible adapter:
client := wormhole.New(
wormhole.WithMistral(types.ProviderConfig{APIKey: "mistral-key"}),
)Configure a local Ollama server:
client := wormhole.New(
wormhole.WithOllama(types.ProviderConfig{BaseURL: "http://localhost:11434"}),
)Configure LM Studio as a local OpenAI-compatible provider:
client := wormhole.New(
wormhole.WithLMStudio(types.ProviderConfig{BaseURL: "http://localhost:1234/v1"}),
)Configure a no-auth local OpenAI-compatible provider named local:
client := wormhole.New(
wormhole.WithLocalOpenAI("http://localhost:8000/v1"),
)WithLocalOpenAI enables dynamic models and disables automatic retries by
default for faster local diagnostics. Pass the OpenAI-compatible API root;
Wormhole appends /chat/completions, so most local servers should use
http://host:port/v1. If the server requires bearer auth, use
WithOpenAICompatible("local", baseURL, types.ProviderConfig{APIKey: key}).
Configure a vLLM deployment:
client := wormhole.New(
wormhole.WithVLLM(types.ProviderConfig{BaseURL: "http://localhost:8000/v1"}),
)Configure Ollama through its OpenAI-compatible endpoint:
client := wormhole.New(
wormhole.WithOllamaOpenAI(types.ProviderConfig{BaseURL: "http://localhost:11434/v1"}),
)Register a custom provider with a factory function:
client := wormhole.New(
wormhole.WithCustomProvider("myprovider", func(cfg types.ProviderConfig) (types.Provider, error) {
return &MyProvider{config: cfg}, nil
}),
wormhole.WithProviderConfig("myprovider", types.ProviderConfig{
APIKey: "key",
}),
)Set the default provider for requests:
client := wormhole.New(
wormhole.WithOpenAI(openAIKey),
wormhole.WithAnthropic(anthropicKey),
wormhole.WithDefaultProvider("openai"),
)
// Uses "openai" by default
resp, _ := client.Text().Model("gpt-5.2").Prompt("Hello").Generate(ctx)
// Override for specific request
resp, _ := client.Text().Using("anthropic").Model("claude-sonnet-4-5").Prompt("Hello").Generate(ctx)Set the default timeout for all requests:
client := wormhole.New(
wormhole.WithOpenAI(apiKey),
wormhole.WithTimeout(60 * time.Second),
)Disable timeouts for long-running operations:
client := wormhole.New(
wormhole.WithOpenAI(apiKey),
wormhole.WithUnlimitedTimeout(),
)Use case: Heavy text processing that may take 3+ minutes.
Enable debug logging:
client := wormhole.New(
wormhole.WithOpenAI(apiKey),
wormhole.WithDebugLogging(),
)With custom logger:
logger := slog.New(slog.NewTextHandler(os.Stderr, &slog.HandlerOptions{
Level: slog.LevelDebug,
}))
client := wormhole.New(
wormhole.WithOpenAI(apiKey),
wormhole.WithDebugLogging(logger),
)Set a custom logger without enabling debug mode:
client := wormhole.New(
wormhole.WithOpenAI(apiKey),
wormhole.WithLogger(myLogger),
)Add type-safe middleware to the execution chain:
import "github.com/garyblankenship/wormhole/v2/middleware"
client := wormhole.New(
wormhole.WithOpenAI(apiKey),
wormhole.WithProviderMiddleware(
middleware.NewCircuitBreaker(5, 30*time.Second),
middleware.RateLimitMiddleware(10),
),
)Legacy middleware (automatically converted to type-safe):
// Deprecated: Use WithProviderMiddleware instead
client := wormhole.New(
wormhole.WithOpenAI(apiKey),
wormhole.WithMiddleware(legacyMiddleware...),
)Configure dynamic model discovery:
import "github.com/garyblankenship/wormhole/v2/discovery"
client := wormhole.New(
wormhole.WithOpenAI(apiKey),
wormhole.WithDiscoveryConfig(discovery.DiscoveryConfig{
CacheTTL: 12 * time.Hour, // Cache for 12 hours
RefreshInterval: 6 * time.Hour, // Refresh every 6 hours
OfflineMode: false, // Allow network fetching
}),
)WithDiscoveryConfig merges partial values with the default discovery config.
Use DisableFileCache: true or DisableBackgroundRefresh: true for explicit
opt-outs.
Enable or disable discovery:
client := wormhole.New(
wormhole.WithOpenAI(apiKey),
wormhole.WithDiscovery(false), // Disable discovery, use fallback models
)Enable offline mode (no network fetching):
client := wormhole.New(
wormhole.WithOpenAI(apiKey),
wormhole.WithOfflineMode(true),
)Enable or disable model validation:
client := wormhole.New(
wormhole.WithOpenAI(apiKey),
wormhole.WithModelValidation(true), // Enable validation
)Configure a provider from environment variables:
// Set environment variables:
// export OPENAI_API_KEY=sk-...
// export ANTHROPIC_API_KEY=sk-ant-...
client := wormhole.New(
wormhole.WithProviderFromEnv("openai"),
wormhole.WithProviderFromEnv("anthropic"),
wormhole.WithDefaultProvider("openai"),
)Supported providers:
| Provider | API Key Env Var | Base URL Env Var |
|---|---|---|
openai |
OPENAI_API_KEY |
OPENAI_BASE_URL |
anthropic |
ANTHROPIC_API_KEY |
ANTHROPIC_BASE_URL |
gemini |
GEMINI_API_KEY |
GEMINI_BASE_URL |
groq |
GROQ_API_KEY |
- |
openrouter |
OPENROUTER_API_KEY |
- |
Silent skip: If the environment variable is not set, the option does nothing (no error).
Configure all available providers from environment:
// All providers with API keys in env will be configured
client := wormhole.New(
wormhole.WithAllProvidersFromEnv(),
wormhole.WithDefaultProvider("openai"),
)Add idempotency key for duplicate operation prevention:
client := wormhole.New(
wormhole.WithOpenAI(apiKey),
wormhole.WithIdempotencyKey("req-123", 1*time.Hour),
)How it works: The SDK caches responses keyed by the idempotency key. If the same key is used again, the cached response is returned instead of making a new request.
Use cases:
- Preventing duplicate charge operations during retries
- Ensuring exactly-once processing in distributed systems
- Avoiding duplicate API calls on client retry
Options are applied in order, allowing overwriting:
client := wormhole.New(
wormhole.WithDefaultProvider("openai"),
wormhole.WithDefaultProvider("anthropic"), // This overwrites the previous
)
// Result: default provider is "anthropic"Use conditionals to build option lists:
opts := []wormhole.Option{
wormhole.WithOpenAI(openAIKey),
}
if enableDebug {
opts = append(opts, wormhole.WithDebugLogging())
}
if anthropicKey != "" {
opts = append(opts, wormhole.WithAnthropic(anthropicKey))
}
client := wormhole.New(opts...)Create reusable option groups:
// Production configuration
var ProductionOpts = []wormhole.Option{
wormhole.WithProviderFromEnv("openai"),
wormhole.WithProviderFromEnv("anthropic"),
wormhole.WithTimeout(60 * time.Second),
wormhole.WithProviderMiddleware(
middleware.NewCircuitBreaker(5, 30*time.Second),
),
}
// Test configuration
var TestOpts = []wormhole.Option{
wormhole.WithOpenAI("test-key"),
wormhole.WithDebugLogging(),
wormhole.WithTimeout(5 * time.Second),
}
// Usage
client := wormhole.New(ProductionOpts...)Load providers dynamically:
func NewClientFromConfig(cfg Config) (*wormhole.Wormhole, error) {
opts := []wormhole.Option{}
if cfg.OpenAIKey != "" {
opts = append(opts, wormhole.WithOpenAI(cfg.OpenAIKey))
}
if cfg.AnthropicKey != "" {
opts = append(opts, wormhole.WithAnthropic(cfg.AnthropicKey))
}
if cfg.DefaultProvider != "" {
opts = append(opts, wormhole.WithDefaultProvider(cfg.DefaultProvider))
}
if cfg.Debug {
opts = append(opts, wormhole.WithDebugLogging())
}
return wormhole.New(opts...), nil
}Wormhole uses two different patterns for configuration:
Applied at client creation, affect all requests:
client := wormhole.New(
wormhole.WithOpenAI(apiKey),
wormhole.WithTimeout(30 * time.Second),
)Applied per-request, override client defaults:
resp, err := client.Text().
Using("openai"). // Override provider
Model("gpt-5.2"). // Set model
Temperature(0.7). // Set temperature
FrequencyPenalty(0.2). // Penalize repeated tokens
PresencePenalty(0.1). // Encourage new tokens
Seed(42). // Request repeatable sampling
ParallelToolCalls(false). // Request at most one tool call
Prompt("Hello"). // Set prompt
Generate(ctx)Frequency and presence penalties accept values from -2.0 to 2.0. Provider
support differs: Anthropic rejects both penalties and seed; Gemini and Ollama
reject ParallelToolCalls; OpenAI Responses rejects both penalties and seed.
Wormhole returns a validation error for these unsupported combinations rather
than dropping the fields.
Embedding requests can choose their response representation:
resp, err := client.Embeddings().
Model("text-embedding-3-small").
Input("hello").
EncodingFormat(types.EmbeddingEncodingBase64).
Generate(ctx)
encoded := resp.Embeddings[0].Base64The default format is types.EmbeddingEncodingFloat, exposed through
Embedding.Embedding. Base64 format exposes little-endian float32 bytes through
Embedding.Base64 and leaves the numeric slice empty.
Key differences:
| Aspect | Client Options | Request Options |
|---|---|---|
| Pattern | Functional options | Builder pattern |
| Scope | All requests | Single request |
| Override | Later options win | Builder methods chain |
| Examples | WithOpenAI(), WithTimeout() |
.Model(), .Temperature(), .EncodingFormat() |
You can create custom options for your application:
// Custom option for your application
func WithMyAppConfig(cfg MyAppConfig) wormhole.Option {
return func(c *wormhole.Config) {
// Modify the config based on your app settings
if cfg.Debug {
c.DebugLogging = true
}
if cfg.Timeout > 0 {
c.DefaultTimeout = cfg.Timeout
}
if cfg.DefaultProvider != "" {
c.DefaultProvider = cfg.DefaultProvider
}
}
}
// Usage
client := wormhole.New(
wormhole.WithOpenAI(apiKey),
WithMyAppConfig(myConfig),
)Create options for provider-specific settings:
// Option for OpenAI-specific settings
func WithOpenAIOrganization(org string) wormhole.Option {
return func(c *wormhole.Config) {
if c.Providers == nil {
c.Providers = make(map[string]types.ProviderConfig)
}
cfg := c.Providers["openai"]
cfg.Organization = org
c.Providers["openai"] = cfg
}
}
// Usage
client := wormhole.New(
wormhole.WithOpenAI(apiKey),
WithOpenAIOrganization("org-123"),
)- Use environment variables for sensitive data (API keys)
- Group related options in reusable slices
- Enable debug logging during development
- Set appropriate timeouts based on your use case
- Use WithProviderFromEnv for flexible configuration
- Hardcode API keys in source code
- Mix provider configuration in a single option
- Overwrite options unintentionally (order matters)
- Forget timeout on production deployments
- Enable debug logging in production (performance impact)
func NewProductionClient() (*wormhole.Wormhole, error) {
return wormhole.New(
// Providers from environment
wormhole.WithAllProvidersFromEnv(),
wormhole.WithDefaultProvider("openai"),
// Production timeout
wormhole.WithTimeout(60 * time.Second),
// Production middleware
wormhole.WithProviderMiddleware(
middleware.NewCircuitBreaker(5, 30*time.Second),
middleware.RateLimitMiddleware(10),
),
// Discovery settings
wormhole.WithDiscoveryConfig(discovery.DiscoveryConfig{
CacheTTL: 12 * time.Hour,
RefreshInterval: 6 * time.Hour,
OfflineMode: false,
}),
), nil
}func NewTestClient(apiKey string) *wormhole.Wormhole {
return wormhole.New(
wormhole.WithOpenAI(apiKey),
wormhole.WithDebugLogging(),
wormhole.WithTimeout(5 * time.Second),
wormhole.WithDiscovery(false), // Use fallback models only
)
}- Client Setup - Create and configure a client
- Errors - Error handling and types