Client & errors

Configuration, environment variables, retries, error handling and listing models.

Creating a client

The quick start reads everything from the environment with .environ_map = init.environ_map. You can also set every option explicitly:

var client: tai.Client = try .init(gpa, io, .{
    .api_key = api_key, // default: TYPESAFE_API_KEY from environ_map
    .base_url = "https://api.typesafe.ai", // default: TYPESAFE_BASE_URL, then this
    .model = "jev-latest", // default: TYPESAFE_DEFAULT_MODEL, then this
    .retry = .{ .max_retries = 3, .backoff_max_ms = 10_000 },
    .extra_headers = &.{.{ .name = "x-request-source", .value = "docs" }},
});
defer client.deinit();

Explicit options take precedence over environment variables, and environment variables take precedence over the defaults. Empty or whitespace-only environment values are ignored. init returns error.MissingApiKey if no key is found for TypeSafe’s own API.

VariableDefault
TYPESAFE_API_KEYnone; required for https://api.typesafe.ai
TYPESAFE_BASE_URLhttps://api.typesafe.ai
TYPESAFE_DEFAULT_MODELjev-latest

Other System One servers

Any server that implements the System One API works through base_url, such as Kev running on your own machine. A key is optional for any server other than TypeSafe’s, and without one the client sends no Authorization header:

var client: tai.Client = try .init(gpa, io, .{
    .base_url = "http://127.0.0.1:8009",
    .model = "kev-latest",
});
defer client.deinit();

The client wraps a std.http.Client, so connections are pooled and reused. Requests can be issued from multiple threads.

Retries

Failed requests are retried with exponential backoff and jitter. The defaults match the official Python and JavaScript SDKs:

FieldDefaultMeaning
max_retries2Retries after the first attempt; 0 disables
backoff_initial_ms500First delay, doubled each retry
backoff_max_ms5000Cap on the backoff delay
backoff_jitter0.25Fraction of each delay randomly subtracted
respect_retry_aftertrueHonor Retry-After / retry-after-ms
max_retry_after_ms60000Longer server delays fall back to backoff
retry_connection_errorstrueRetry when the server can’t be reached
retry_status408, 429, 5xxFunction deciding which statuses to retry

429 Too Many Requests and 529 Overloaded are retried by default. Set the policy on the client, as above, or per call with .retry (see per-call options). Use tai.RetryPolicy.none to disable retries.

Errors

Zig errors can’t carry a payload, so tai returns a precise error and puts the details in an optional Diagnostics value. This example is tested with an invalid API key:

var diag: tai.Client.Diagnostics = .{};
defer diag.deinit();

const outcome = client.ask("Is this spam?", .{ .spam = tai.noul("Is this spam?") }, .{ .diagnostics = &diag });
const problem: []const u8 = if (outcome) |_| "" else |err| switch (err) {
    // diag.status is 401; diag.body is the server's JSON explanation.
    error.Unauthorized => diag.body,
    error.RateLimited, error.Overloaded => "busy, try again later",
    else => return err,
};

ErrorCause
BadRequestHTTP 400
UnauthorizedHTTP 401: missing or invalid API key
PermissionDeniedHTTP 403
NotFoundHTTP 404
RequestTimeoutHTTP 408, after retries
UnprocessableEntityHTTP 422: the body failed validation
RateLimitedHTTP 429, after retries
OverloadedHTTP 529, after retries
ServerErrorOther 5xx, after retries
UnexpectedStatusAny other non-2xx status
ConnectionFailedCouldn’t connect, or the connection dropped
InvalidBaseUrlbase_url isn’t an http(s) URL with a host
InvalidResponseThe body didn’t match the documented shape, or an answer didn’t match its question
NoQuestions, DuplicateQuestionName, …Client-side validation for runtime-built requests

Diagnostics records:

  • status: the last HTTP status, if a response arrived.
  • body: the last non-2xx body, usually JSON such as {"detail": {...}}.
  • attempts: how many attempts were made, including the first.
  • transport_error: the underlying error behind ConnectionFailed.

Call deinit on it when you’re done, since it owns a copy of the body.

Listing models

listModels calls GET /v1/models, which lists the aliases your account can use. Versioned ids such as jev-1.13.0 are accepted in model whether or not they’re listed.

var models = try client.listModels(.{});
defer models.deinit();

var has_latest = false;
for (models.value.models) |m| {
    if (std.mem.eql(u8, m.name, "jev-latest")) has_latest = true;
}

Timeouts

std.http.Client doesn’t support per-request timeouts yet. Because every call takes a std.Io, you can cancel it from outside. For example, run the call with io.concurrent and call cancel on the future after a deadline; the call then returns error.Canceled.