skyl classifies every provider failure onto one of eight sentinels and wraps it
in an *Error carrying enough context to act on.
You branch on the classification, never on message text — providers reword their
messages, and string matching breaks silently when they do.
The sentinels#
| Sentinel | Message | Retried? | Meaning |
|---|---|---|---|
ErrAuth | skyl: authentication failed | Never | The credential was missing, malformed, or rejected. The same key will fail again. |
ErrRateLimit | skyl: rate limited | Yes | The provider is throttling. Retried with backoff, honouring Retry-After. |
ErrNotFound | skyl: not found | Never | The model or endpoint does not exist for this account. Because model IDs pass through unvalidated, a typo arrives here rather than failing locally. |
ErrBadRequest | skyl: invalid request | Never | The request was malformed. Often produced locally by Request.Validate. |
ErrServer | skyl: provider server error | Yes | The provider failed on its side. Retried with backoff. |
ErrUnsupported | skyl: unsupported by this provider | Never | This provider cannot express part of the request. Returned instead of silently dropping data, because a quietly discarded image looks like a model that ignored the question. |
ErrRefusal | skyl: model declined the request | Never | The model or its safety classifiers declined. The same prompt gets the same answer. |
ErrStreamClosed | skyl: stream is closed | Never | The stream was used after being closed. |
Caveats
- Branch with
errors.Is. The sentinels are stable; provider messages are not. Unwrapreturns a slice — the sentinel and the underlying cause — soerrors.Is(err, context.DeadlineExceeded)works on the same value that matcheserrors.Is(err, skyl.ErrServer).- An
Errornever contains credentials.Bodyis the provider's payload truncated to 2 KB. - Local validation failures are
ErrBadRequesttoo, so one branch catches both yours and the provider's.
The API#
| Symbol | Purpose |
|---|---|
| Sentinel errors | The eight values you branch on |
| Error | The type carrying provider, status, message and retry hint |
| Error.Retryable | Whether retrying could plausibly succeed |
| Error.Unwrap | Returns the sentinel and the cause |
| NewError | Builds a classified provider error |
| ClassifyStatus | Maps an HTTP status onto a sentinel |
| ParseRetryAfter | Interprets a Retry-After header |
| Unsupportedf | Builds an ErrUnsupported naming what failed |
The last four are exported so that an adapter in your repository produces errors indistinguishable from an in-tree one.
Usage#
Branching
resp, err := client.Complete(ctx, req)
switch {
case err == nil:
case errors.Is(err, skyl.ErrRateLimit):
// Already retried with backoff; it kept failing. Shed load.
case errors.Is(err, skyl.ErrAuth):
return fmt.Errorf("credential rejected: %w", err)
case errors.Is(err, skyl.ErrRefusal):
// Never retried — the same prompt gets the same answer.
default:
return err
}resp, err := client.Complete(ctx, req)
switch {
case err == nil:
case errors.Is(err, skyl.ErrRateLimit):
// Already retried with backoff; it kept failing. Shed load.
case errors.Is(err, skyl.ErrAuth):
return fmt.Errorf("credential rejected: %w", err)
case errors.Is(err, skyl.ErrRefusal):
// Never retried — the same prompt gets the same answer.
default:
return err
}Recovering the detail
var e *skyl.Error
if errors.As(err, &e) {
log.Printf("%s returned %d: %s (retryable=%v)",
e.Provider, e.StatusCode, e.Message, e.Retryable())
}var e *skyl.Error
if errors.As(err, &e) {
log.Printf("%s returned %d: %s (retryable=%v)",
e.Provider, e.StatusCode, e.Message, e.Retryable())
}Troubleshooting#
My error handling broke after a provider changed its wording
You were matching on message text. Branch on the sentinels with errors.Is;
that is what they are for.
I cannot tell a timeout from a DNS failure
Both arrive with StatusCode == 0 and no sentinel — but the cause survives:
if errors.Is(err, context.DeadlineExceeded) { /* we ran out of time */ }
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() { /* a transport timeout */ }if errors.Is(err, context.DeadlineExceeded) { /* we ran out of time */ }
var netErr net.Error
if errors.As(err, &netErr) && netErr.Timeout() { /* a transport timeout */ }