Errors and Reliability

Errors

Every error that the SDK throws is a SmarterError, so one instanceof test catches them all. The classes, their names and their hierarchy are the Python SDK’s, which follows the platform’s own rule (ADR-012).

SmarterError
├── SmarterConfigurationError        the client has no api key or base url
├── SmarterValueError                an argument is invalid, before any request is sent
├── ResourceNotFoundError            a resource was not found, by the API or by find()
├── SmarterPromptError               a streamed prompt failed
└── APIError
    ├── APIConnectionError           the request got no response
    │   └── APITimeoutError
    ├── APIResponseValidationError   the response doesn't match the SDK's schema of it
    └── APIStatusError               the API returned a 4xx or 5xx status
        ├── BadRequestError          400
        ├── AuthenticationError      401
        ├── PermissionDeniedError    403
        ├── NotFoundError            404, also an instanceof ResourceNotFoundError
        ├── ConflictError            409
        ├── UnprocessableEntityError 422
        ├── RateLimitError           429
        └── InternalServerError      5xx

An APIStatusError carries the response’s statusCode, a readable message, the parsed body, the headers, and, when the platform sent a journaled error, its error, whose errorClass (also on the error itself) is the name of the platform’s exception, e.g. SAMBrokerErrorNotFound.

import { NotFoundError, SmarterError } from "@smarter.sh/api";

try {
  await client.manifests.describe("LLMClient", "no_such_client");
} catch (err) {
  if (err instanceof NotFoundError) console.log(err.statusCode, err.message, err.errorClass);
  else if (err instanceof SmarterError) console.log("something else went wrong:", err);
  else throw err;
}

Every method that sends a request is async, so its errors are rejections, also those of its arguments, e.g. an invalid id. An APIResponseValidationError usually means that the SDK and the platform are of incompatible versions. Compare (await client.version()).api.version with the SDK’s 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.

const client = new Smarter({ maxRetries: 0, timeout: 120 });
await client.llmClients.prompt(llmclient.id, "Hello", { timeout: 300 }); // for this request only

The timeout is in seconds. 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 maxRetries: 0 where that matters more than resilience.

Cancellation

Every method takes an AbortSignal. Aborting it cancels the request, and the promise rejects with the signal’s reason. A stream is cancelled when you break out of its loop.

const controller = new AbortController();
setTimeout(() => controller.abort(), 5000);
await client.llmClients.prompt(llmclient.id, "Hello", { signal: controller.signal });

Logging

As a library should, the SDK logs nothing unless you ask it to. Pass a logger (console, or a pino, winston or bunyan logger), and set SMARTER_LOG=info (or the logLevel option) to log each request’s method, url, status and duration, or debug to log the bodies too. Without a logger, a level logs to console.

import pino from "pino";

const client = new Smarter({ logger: pino(), logLevel: "info" });

API keys are never logged: they are masked to their last four characters, also when you console.log() or JSON.stringify() the client.