Last updated 2026-05-17

Package llmrouter — Errors

Every public symbol declared in errors.go. Two top-level kinds: a typed wrapper for non-2xx HTTP responses (ErrUpstream) and a sentinel used for configuration validation (ErrInvalidConfig). Other errors (network, context, scanner) flow through unwrapped from the standard library.

ErrUpstream

Wraps a non-2xx response from the upstream provider. Source: errors.go#L9.

type ErrUpstream struct {
Provider string
StatusCode int
Body string
}
Provider
Stable provider id — "openai", "anthropic", …
StatusCode
The HTTP status code that triggered the wrap. Always ≥ 400.
Body
A snippet of the response body. OpenAI captures up to 1 KiB (whitespace trimmed); Anthropic captures up to 8 KiB (verbatim).

ErrUpstream.Error

func (e *ErrUpstream) Error() string

Formatted as: fmt.Sprintf("%s upstream %d: %s", e.Provider, e.StatusCode, e.Body). Source: errors.go#L15.

Sample formatted output:

openai upstream 401: {"error":{"message":"Incorrect API key provided","type":"invalid_request_error","code":"invalid_api_key"}}
anthropic upstream 429: {"type":"error","error":{"type":"rate_limit_error","message":"..."}}

Example: errors.As to inspect

stream, err := provider.CompletionStream(ctx, req)
if err != nil {
var upErr *llmrouter.ErrUpstream
if errors.As(err, &upErr) {
switch upErr.StatusCode {
case 401:
log.Printf("auth failed for %s: %s", upErr.Provider, upErr.Body)
case 429:
log.Printf("%s rate-limited, body=%s", upErr.Provider, upErr.Body)
case 500, 502, 503, 504:
log.Printf("%s upstream %d, retry later", upErr.Provider, upErr.StatusCode)
default:
log.Printf("%s upstream %d: %s", upErr.Provider, upErr.StatusCode, upErr.Body)
}
return
}
log.Fatal(err) // not an upstream error — network, context, etc.
}

ErrInvalidConfig

Sentinel returned (wrapped) by every provider's New when required config is missing or malformed. Source: errors.go#L20.

var ErrInvalidConfig = errors.New("invalid provider config")

Used as the target of errors.Is. Providers wrap it with additional context, for example:

providers/openai/openai.go
return nil, fmt.Errorf("%w: api key required", llmrouter.ErrInvalidConfig)
// providers/anthropic/anthropic.go
return nil, fmt.Errorf("%w: anthropic requires an api key", llmrouter.ErrInvalidConfig)

Example: errors.Is at construction

p, err := openai.New() // missing WithAPIKey
if errors.Is(err, llmrouter.ErrInvalidConfig) {
fmt.Fprintln(os.Stderr, "set OPENAI_API_KEY and try again")
os.Exit(2)
}
if err != nil {
log.Fatal(err)
}
_ = p

Error categories

Every error you can encounter, where it comes from, and how to detect it. Stream errors are returned from Stream.Err(); everything else is returned synchronously from New or CompletionStream.

Category Returned by How to detect Examples
Configuration provider.New, llmrouter.NewConfig, individual With* options errors.Is(err, llmrouter.ErrInvalidConfig) Missing API key, empty base URL, invalid URL syntax, nil HTTP client, non-positive timeout, empty extra key.
Network provider.CompletionStream (synchronous) Plain err != nil; type-assert against *net.OpError, net.Error, *url.Error as needed DNS failure, connection refused, TLS handshake, request-construction errors.
Upstream provider.CompletionStream (synchronous, on HTTP ≥ 400) errors.As(err, &upErr) where upErr is *llmrouter.ErrUpstream 401 auth, 429 rate limit, 400 invalid request body, 500/502/503/504 upstream outage.
Stream Stream.Err() (after channel closes) errors.Is(err, context.Canceled), errors.Is(err, context.DeadlineExceeded), or generic check on the wrapped scanner error Context cancelled mid-stream, deadline exceeded, SSE scanner buffer overflow, network reset mid-read.

Example: handling every category

func runOnce(ctx context.Context) error {
p, err := openai.New(llmrouter.WithAPIKey(os.Getenv("OPENAI_API_KEY")))
if err != nil {
if errors.Is(err, llmrouter.ErrInvalidConfig) {
return fmt.Errorf("configuration: %w", err)
}
return fmt.Errorf("init: %w", err)
}
stream, err := p.CompletionStream(ctx, llmrouter.ChatRequest{
Model: "gpt-4o-mini",
Messages: []llmrouter.Message{llmrouter.TextMessage("user", "hi")},
})
if err != nil {
var upErr *llmrouter.ErrUpstream
switch {
case errors.As(err, &upErr):
return fmt.Errorf("upstream %d (%s): %s", upErr.StatusCode, upErr.Provider, upErr.Body)
default:
return fmt.Errorf("network: %w", err)
}
}
for chunk := range stream.Chunks() {
for _, c := range chunk.Choices {
fmt.Print(c.Delta.Content)
}
}
if err := stream.Err(); err != nil {
switch {
case errors.Is(err, context.Canceled):
return nil // consumer cancelled, treat as success
case errors.Is(err, context.DeadlineExceeded):
return fmt.Errorf("stream: timeout")
default:
return fmt.Errorf("stream: %w", err)
}
}
return nil
}