PerdurancePerdurance

The sar CLI

Everything the console does, from a shell — and the one thing it does that the console cannot.

sar drives the same public API the console does. Nothing in it reaches around the API into the database, which is what makes it usable against a deployment you do not operate.

Connecting

Three global options, each with an environment variable behind it, so a shell can be configured once and every command inherits it.

OptionVariableDefault
--urlROUTER_URLhttp://127.0.0.1:8787
--tenancyROUTER_TENANCYThe one the session key opens
ROUTER_API_KEY
ROUTER_SESSION_KEY
ROUTER_REGISTRATION_TOKEN
export ROUTER_URL=https://api.perdurance.dev
export ROUTER_TENANCY=acme
export ROUTER_API_KEY='sar_ab12cd34_…'

The CTX_* variables belong to the service and ROUTER_* to this client. The two lists never mix, so it is always clear which side of the connection a setting is about.

Which credential a command wants

This is the part that surprises people. Commands do not all take the same kind of credential.

WantsCommands
A registration tokenregister
Nothinglogin
A sessionpassword, whoami, logout
An API keysubmit
Either a session or an API keyeverything else

submit is the one place a session is refused by kind rather than by role: sending a request is something a machine credential does, and holding a browser session should not be enough to spend your provider budget.

Getting started from a shell

# once per organisation, with a registration token from the deployment.
# `register` creates the tenancy, so it has to be told what to call it.
sar register --tenancy acme --email you@example.com

# thereafter
sar login --email you@example.com
sar namespaces create --slug prod --name Production
sar backends create --namespace prod --name openai --kind openai_compat
sar rules create --namespace prod --priority 100 --pattern '*' --backend <BACKEND_ID>
sar keys create --name my-service

A provider key is never an argument

backends create prompts for the credential, or reads it from standard input with --credential -. There is no way to pass one inline, deliberately: an argument sits in the shell's history and is visible in ps to everyone on the machine.

pass show openai/prod | sar backends create --namespace prod \
  --name openai --kind openai_compat --credential -

Sending a request

sar submit --namespace prod --dialect openai --file body.json

The body travels exactly as writtensar does not reformat it. That matters more than it looks: the router hashes the bytes it was given, so a re-sent file deduplicates only if it is byte-identical. See Idempotency.

--file - reads from standard input:

jq -n '{model:"gpt-4.1",messages:[{role:"user",content:"Say hello."}]}' \
  | sar submit --namespace prod --file -

--dialect is openai (the default) or anthropic, and it decides which vendor route the body is sent to rather than being sniffed from the body.

Which shape the answer takes is read off what came back rather than worked out from the body, so a request that asked for a stream and was refused before reaching the provider still prints as one document.

Working with stored requests

# fire-and-forget: prints the id and returns
sar requests submit --namespace prod --file body.json

# read one back, whatever it has reached so far
sar requests get 01J8F2ZK9QX3M4NBVWT7 --namespace prod

sar submit holds the connection and gives you the provider's answer. sar requests submit walks away with an id. Same body, same deduplication.

The id is positional; the namespace is not, because a request id is only meaningful inside one.

Replaying a stream

# replay from the first chunk, then follow it live if it is still running
sar requests get $ID --namespace prod --stream

# pick up after the last chunk you already have
sar requests get $ID --namespace prod --stream --from 25

--from takes the sequence number of the last chunk in hand and is the CLI's Last-Event-ID. It requires --stream, since there is nothing to resume in a whole document.

Everything else

CommandDoes
registerCreate a tenancy and its owner from a registration token
loginSign in, opening a session or taking an API key
passwordReplace the password this account signs in with
whoamiShow what the credential in hand stands for
logoutEnd the session this key opened
keysMint, list and end API keys
usersInvite people into the tenancy and see what they hold
ownershipMove ownership of the tenancy
namespacesList and create namespaces
grantsRead and write what a member may do in a namespace
backendsConfigure the providers a namespace sends to
rulesDecide which backend a model name reaches
submitSend a body and take the provider's answer on the connection
requestssubmit without holding a connection, and get one back
usageReport what a namespace has spent
storageReport what a namespace is holding

sar <command> --help has the arguments for each.

whoami first

When something is refused and you are not sure why, sar whoami says which tenancy the credential opens and what it holds. Most 403s are answered by it.

On this page