skyl.New wraps a Provider with the behaviour
every production caller needs: request validation, retry with jittered backoff,
per-attempt timeouts, and observability hooks.
Reference#
Parameters
Returns
A *Client, ready to use. It is safe for concurrent use by multiple
goroutines.
Caveats
- It panics if
pis nil. A nil provider is a programmer error that would otherwise surface as a confusing nil dereference on the first request, far from the line that caused it. - Options are applied in order, so
New(p, WithMaxRetries(5), WithMaxRetries(2))gives you two retries. - Options with invalid values are ignored, not rejected: a negative retry count, or a non-positive delay, leaves the default in place.
Newperforms no I/O and never fails. A wrong credential is not detected here — it surfaces asErrAuthon the first request.
Usage#
Wrapping a provider
client := skyl.New(openai.New(os.Getenv("OPENAI_API_KEY")))client := skyl.New(openai.New(os.Getenv("OPENAI_API_KEY")))Configuring for production
client := skyl.New(
anthropic.New(os.Getenv("ANTHROPIC_API_KEY")),
skyl.WithMaxRetries(5),
skyl.WithRetryDelay(time.Second, time.Minute),
skyl.WithTimeout(2*time.Minute),
skyl.WithHook(func(_ context.Context, ev skyl.HookEvent) {
metrics.Record(ev.Provider, ev.ResponseModel, ev.Duration, ev.Err)
}),
)client := skyl.New(
anthropic.New(os.Getenv("ANTHROPIC_API_KEY")),
skyl.WithMaxRetries(5),
skyl.WithRetryDelay(time.Second, time.Minute),
skyl.WithTimeout(2*time.Minute),
skyl.WithHook(func(_ context.Context, ev skyl.HookEvent) {
metrics.Record(ev.Provider, ev.ResponseModel, ev.Duration, ev.Err)
}),
)Sharing options across several clients
// Build the options once so every client in the process behaves identically.
opts := []skyl.Option{
skyl.WithMaxRetries(4),
skyl.WithTimeout(90 * time.Second),
skyl.WithHook(telemetry.Hook),
}
fast := skyl.New(gemini.New(geminiKey), opts...)
smart := skyl.New(anthropic.New(anthropicKey), opts...)// Build the options once so every client in the process behaves identically.
opts := []skyl.Option{
skyl.WithMaxRetries(4),
skyl.WithTimeout(90 * time.Second),
skyl.WithHook(telemetry.Hook),
}
fast := skyl.New(gemini.New(geminiKey), opts...)
smart := skyl.New(anthropic.New(anthropicKey), opts...)Construct once, reuse everywhere
// A Client holds no per-request state and is concurrency-safe, so one per
// process is correct. Building one per request throws away connection pooling.
type Service struct {
ai *skyl.Client
}
func NewService(key string) *Service {
return &Service{ai: skyl.New(openai.New(key))}
}// A Client holds no per-request state and is concurrency-safe, so one per
// process is correct. Building one per request throws away connection pooling.
type Service struct {
ai *skyl.Client
}
func NewService(key string) *Service {
return &Service{ai: skyl.New(openai.New(key))}
}Troubleshooting#
My program panics with “skyl: New called with a nil Provider”
You passed a nil provider — most often from a constructor that returned an error
you did not check, or from a switch with a default branch returning nil.
p, err := pick(name)
if err != nil {
return err // ← without this, p is nil and New panics
}
client := skyl.New(p)p, err := pick(name)
if err != nil {
return err // ← without this, p is nil and New panics
}
client := skyl.New(p)My option seems to have no effect
Two likely causes.
The value was invalid and was ignored. WithMaxRetries(-1) and
WithRetryDelay(0, 0) leave the defaults in place rather than erroring.
A later option overrode it. Options apply in order; check for a duplicate further down the list.
I set WithHTTPClient on New and it did not compile
The HTTP client is a provider option, not a client option — the adapter is what performs the request:
client := skyl.New(openai.New(key, openai.WithHTTPClient(hc)))client := skyl.New(openai.New(key, openai.WithHTTPClient(hc)))The general rule: behaviour that is the same for every vendor lives on Client;
behaviour about how one vendor is reached lives on the provider.