Errors and Reliability

Errors

Every error that the SDK returns matches smarter.ErrSmarter with errors.Is. The Python SDK’s exception hierarchy, which follows the platform’s own rule (ADR-012), becomes Go’s sentinel errors and error types:

Check

Means

errors.Is(err, smarter.ErrSmarter)

Any error of the SDK.

errors.As(err, &configurationError)

*smarter.ConfigurationError: the client has no api key, or a value is invalid.

errors.As(err, &valueError)

*smarter.ValueError: an argument is invalid, before any request is sent.

errors.Is(err, smarter.ErrResourceNotFound)

A resource was not found, by the API (a 404) or by Find.

errors.As(err, &connectionError)

*smarter.APIConnectionError: the request got no response. Timeout() reports a timeout.

errors.As(err, &validationError)

*smarter.APIResponseValidationError: the response doesn’t decode into the SDK’s struct.

errors.As(err, &statusError)

*smarter.APIStatusError: the API returned a 4xx or 5xx status.

errors.Is(err, smarter.ErrBadRequest), ErrAuthentication, ErrPermissionDenied, ErrNotFound, ErrConflict, ErrUnprocessableEntity, ErrRateLimit, ErrInternalServer

The status: 400, 401, 403, 404, 409, 422, 429, and 5xx.

errors.As(err, &promptError)

*smarter.PromptError: a streamed prompt failed.

An *APIStatusError carries the response’s StatusCode, a readable Message, the Body, the Header, and, when the platform sent a journaled error, its JournalError, whose ErrorClass() is the name of the platform’s exception, e.g. SAMBrokerErrorNotFound. The checks keep working through your own fmt.Errorf("...: %w", err).

_, err := client.Manifests.Describe(ctx, "LLMClient", "no_such_client")
var status *smarter.APIStatusError
switch {
case errors.Is(err, smarter.ErrNotFound) && errors.As(err, &status):
    fmt.Println(status.StatusCode, status.Message, status.ErrorClass())
case errors.Is(err, smarter.ErrSmarter):
    fmt.Println("something else went wrong:", err)
}

An *APIResponseValidationError usually means that the SDK and the platform are of incompatible versions. Compare the platform’s client.Version(ctx) with the SDK’s smarter.Version.

Retries and timeouts

A request that failed for a transient reason is retried, by default twice, with exponential backoff and jitter, honoring the response’s Retry-After. Transient means: no response, a timeout, or a 408, 429, 502 or 504 status. A 503 is retried only when it didn’t come from the platform itself, e.g. from a load balancer, because the platform answers 503 for a resource that isn’t ready, which a retry won’t change.

client, err := smarter.NewClient(smarter.WithMaxRetries(0), smarter.WithTimeout(2*time.Minute))

The timeout applies to each attempt. For a stream, it applies to each event, so a long prompt that reports its progress doesn’t time out. Retrying a prompt can bill a prompt twice, if the first attempt reached the LLM. Set WithMaxRetries(0) where that matters more than resilience.

Cancellation

Every method takes a context.Context. Cancelling it, or its deadline, ends the request at once, also while the SDK waits to retry, and the error wraps the context’s error:

ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
_, err := client.LLMClients.Prompt(ctx, llmclient.Ref(), smarter.Text("Hello"), nil)
if errors.Is(err, context.DeadlineExceeded) {
    fmt.Println("gave up")
}

Logging

As a library should, the SDK logs nothing unless you ask it to. Give it your *slog.Logger with smarter.WithLogger: it logs each request’s method, url, status and duration at the info level, and the bodies at the debug level, with the attribute component=smarter. Without a logger, SMARTER_LOG=info or SMARTER_LOG=debug logs to stderr.

client, err := smarter.NewClient(smarter.WithLogger(slog.Default()))

API keys are never logged: they are masked to their last four characters, also when you print or slog the client.