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:
return nil, fmt.Errorf("%w: api key required", llmrouter.ErrInvalidConfig)
// providers/anthropic/anthropic.goreturn nil, fmt.Errorf("%w: anthropic requires an api key", llmrouter.ErrInvalidConfig)Example: errors.Is at construction
p, err := openai.New() // missing WithAPIKeyif 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)}_ = pError 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}