ProviderError
Go · package github.com/muthuishere/toolnexus/golang · SPEC §8
type ProviderError struct { Status int // the HTTP status of the failed response Body string // whole body, already redacted; NOT capped (Error() caps it) RetryAfter string // the raw Retry-After header, empty when absent}
func (e *ProviderError) Error() string // "LLM <status>: <redacted+capped body>"
// RedactProviderBody(status int, body string) string — the standalone policy fn,// for a host that logs its own provider calls and wants the same redaction.const RedactedMarker = "«redacted»" // U+00AB / U+00BBA non-2xx response from the model endpoint raises a typed *ProviderError carrying the status
code, a redacted+capped body, and the retry-after signal — never a bare unstructured exception a
host has to string-match. It is built by the internal newProviderError(status, body, retryAfter)
at every place a request fails against the provider, so the shape is the same whether the failure
came from OpenAI, Anthropic or a proxy in front of either.
When to use it
Section titled “When to use it”Reach for *ProviderError any time you need to branch on why an LLM call failed rather than
just knowing that it did: rate limiting (429) with RetryAfter, a billing failure (402), an
account-suspended body worth surfacing to an operator, or simply deciding whether a failure is
retryable. Match it with errors.As on whatever error your Client.Run/Ask/Stream call
returned — it is a plain Go error value, not a panic and not a sentinel string.
var pe *toolnexus.ProviderErrorif errors.As(err, &pe) && pe.Status == 429 { // pe.RetryAfter is the raw header value, e.g. "20" or an HTTP-date}Why this and not the alternative
Section titled “Why this and not the alternative”A *ProviderError is what you get back from Run/Ask/Stream after retries are exhausted.
While a call is still being retried, Resilience’s OnError(info)
sees the same Status as a plain int on ErrorInfo, not yet wrapped — the two features classify
the identical failure at two different points (mid-retry vs. final return), rather than each
inventing its own status representation.
Examples
Section titled “Examples”1. The smallest useful call — match the type and read Status
Section titled “1. The smallest useful call — match the type and read Status”package main
import ( "context" "errors" "fmt" "log" "net/http" "net/http/httptest"
toolnexus "github.com/muthuishere/toolnexus/golang")
func main() { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(http.StatusTooManyRequests) _, _ = w.Write([]byte(`{"error":"rate limited"}`)) })) defer srv.Close()
tk, err := toolnexus.CreateToolkit(context.Background(), toolnexus.Options{Builtins: false}) if err != nil { log.Fatal(err) } defer tk.Close()
client := toolnexus.CreateClient(toolnexus.ClientOptions{ BaseURL: srv.URL, Style: toolnexus.StyleOpenAI, Model: "stub", APIKey: "k", Retries: -1, RetryBaseMs: 1, }) _, err = client.Run(context.Background(), "hi", tk)
var pe *toolnexus.ProviderError if !errors.As(err, &pe) { log.Fatalf("expected a *ProviderError, got %T: %v", err, err) } if pe.Status != 429 { log.Fatalf("Status = %d, want 429", pe.Status) }
fmt.Println("ok: status", pe.Status)}2. The realistic case — reading RetryAfter to back off
Section titled “2. The realistic case — reading RetryAfter to back off”package main
import ( "context" "errors" "fmt" "log" "net/http" "net/http/httptest"
toolnexus "github.com/muthuishere/toolnexus/golang")
func main() { srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Header().Set("Retry-After", "20") w.WriteHeader(http.StatusTooManyRequests) _, _ = w.Write([]byte(`{"error":"rate limited"}`)) })) defer srv.Close()
tk, err := toolnexus.CreateToolkit(context.Background(), toolnexus.Options{Builtins: false}) if err != nil { log.Fatal(err) } defer tk.Close()
client := toolnexus.CreateClient(toolnexus.ClientOptions{ BaseURL: srv.URL, Style: toolnexus.StyleOpenAI, Model: "stub", APIKey: "k", Retries: -1, RetryBaseMs: 1, }) _, err = client.Run(context.Background(), "hi", tk)
var pe *toolnexus.ProviderError if !errors.As(err, &pe) { log.Fatal("expected a *ProviderError") } if pe.RetryAfter != "20" { log.Fatalf("RetryAfter = %q, want %q", pe.RetryAfter, "20") }
fmt.Println("ok: retry after", pe.RetryAfter, "seconds")}3. The full surface — an account-identifier body gets redacted, an auth body gets dropped
Section titled “3. The full surface — an account-identifier body gets redacted, an auth body gets dropped”package main
import ( "context" "errors" "fmt" "log" "net/http" "net/http/httptest" "strings"
toolnexus "github.com/muthuishere/toolnexus/golang")
func main() { turn := 0 srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { turn++ if turn == 1 { // A 500 with an account_id in the body: redacted, not dropped. w.WriteHeader(http.StatusInternalServerError) _, _ = w.Write([]byte(`{"error":"internal","account_id":"acct_12345"}`)) return } // A 401: the whole body is dropped, since it routinely echoes the credential. w.WriteHeader(http.StatusUnauthorized) _, _ = w.Write([]byte(`{"error":"invalid api key sk-abc123"}`)) })) defer srv.Close()
client := toolnexus.CreateClient(toolnexus.ClientOptions{ BaseURL: srv.URL, Style: toolnexus.StyleOpenAI, Model: "stub", APIKey: "k", Retries: -1, RetryBaseMs: 1, }) tk, err := toolnexus.CreateToolkit(context.Background(), toolnexus.Options{Builtins: false}) if err != nil { log.Fatal(err) } defer tk.Close()
_, err = client.Run(context.Background(), "hi", tk) var pe *toolnexus.ProviderError if !errors.As(err, &pe) { log.Fatal("expected a *ProviderError") } if strings.Contains(pe.Body, "acct_12345") || !strings.Contains(pe.Body, toolnexus.RedactedMarker) { log.Fatalf("account_id must be redacted, got: %s", pe.Body) }
_, err = client.Run(context.Background(), "hi", tk) if !errors.As(err, &pe) { log.Fatal("expected a *ProviderError") } if pe.Body != "" { log.Fatalf("a 401 body must be dropped in full, got: %q", pe.Body) }
fmt.Println("ok: redacted, and auth body dropped")}See also
Section titled “See also”CreateClient— The unified client: system prompt, skills injection, parallel and chained tool calls, retries, memory.Client.Run— Send a prompt, let the loop call tools until the model stops, get a RunResult.Client.Stream— The streaming loop: text deltas, tool-call events, and suspension events as they happen.Hooks— Intercept before/after model calls and tool calls: audit, redact, veto, or rewrite.Resilience— The retry/fail policy that classifies the same status mid-retry, before it would ever become a*ProviderError.