Eight package-level values. Branch on them with errors.Is rather than
inspecting message text — providers reword their messages, and string matching
breaks silently when they do.
Reference#
var (
ErrAuth = errors.New("skyl: authentication failed")
ErrRateLimit = errors.New("skyl: rate limited")
ErrNotFound = errors.New("skyl: not found")
ErrBadRequest = errors.New("skyl: invalid request")
ErrServer = errors.New("skyl: provider server error")
ErrUnsupported = errors.New("skyl: unsupported by this provider")
ErrRefusal = errors.New("skyl: model declined the request")
ErrStreamClosed = errors.New("skyl: stream is closed")
)
| 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
- Only
ErrRateLimit,ErrServerand unclassified transport failures are retried. The rest are hopeless by construction. ErrBadRequestis also produced locally byRequest.Validate, so one branch catches both your malformed request and the provider's rejection.ErrUnsupportedis usually produced before any network call, when an adapter cannot represent part of the request.- A refusal with text arrives as a normal response with
StopRefusal; one without text isErrRefusal. Handle both. - They match through wrapping: the "giving up after 4 attempts" wrapper does not hide the sentinel.
Usage#
Branching
switch {
case errors.Is(err, skyl.ErrAuth):
return fmt.Errorf("credential rejected: %w", err)
case errors.Is(err, skyl.ErrNotFound):
return fmt.Errorf("no such model: %w", err)
case errors.Is(err, skyl.ErrRateLimit):
return fmt.Errorf("rate limited after retries: %w", err)
case errors.Is(err, skyl.ErrRefusal):
return fmt.Errorf("the model declined: %w", err)
}switch {
case errors.Is(err, skyl.ErrAuth):
return fmt.Errorf("credential rejected: %w", err)
case errors.Is(err, skyl.ErrNotFound):
return fmt.Errorf("no such model: %w", err)
case errors.Is(err, skyl.ErrRateLimit):
return fmt.Errorf("rate limited after retries: %w", err)
case errors.Is(err, skyl.ErrRefusal):
return fmt.Errorf("the model declined: %w", err)
}Separating permanent from transient
var e *skyl.Error
if errors.As(err, &e) && !e.Retryable() {
// Will never succeed on its own. Alert rather than backing off.
alerts.Page("permanent failure", "provider", e.Provider)
}var e *skyl.Error
if errors.As(err, &e) && !e.Retryable() {
// Will never succeed on its own. Alert rather than backing off.
alerts.Page("permanent failure", "provider", e.Provider)
}Troubleshooting#
ErrNotFound on a model I am sure exists
Because model IDs pass through unvalidated, a typo arrives here rather than failing locally. Check the spelling — and check that your account has access, since the list is scoped per account.
ErrUnsupported with no network call
Correct. An adapter that cannot represent a part of your request says so before sending anything — an image URL on Gemini, or a tool call on a user message on OpenAI.
ErrStreamClosed
You used a stream after closing it. Close is idempotent, but Next and
Event after it are not meaningful.