PerdurancePerdurance

Usage and storage

What a namespace has spent per key and per model, what it is holding, and why accepted submissions and provider calls are different numbers.

Two routes, both scoped to a namespace and both needing a key that can manage it.

GET /usage

What the namespace has spent, in total and attributed per key.

curl "$PERDURANCE_URL/usage" -H "Authorization: Bearer $PERDURANCE_KEY"
{
  "namespace_id": "…",
  "itemised_since": 1787900000000,
  "accepted_submissions": 1841,
  "provider_calls": 1792,
  "upstream_sent_bytes": 4192048,
  "upstream_received_bytes": 19402881,
  "api_keys": [
    {
      "api_key_id": "…",
      "ledger_events": 1204,
      "provider_calls": 1180,
      "prompt_tokens": 884102,
      "completion_tokens": 219884,
      "total_tokens": 1103986
    }
  ]
}

accepted_submissions and provider_calls are deliberately different numbers. A submission that landed on an existing execution inside the idempotency window is counted once as a submission and not at all as a provider call. The gap between them is what deduplication saved you.

itemised_since is the oldest event the per-key breakdown covers. The totals above it are all-time; the api_keys list is only as deep as retention has kept.

A token count is null where no answer under that key reported one, which is not the same as a provider reporting zero. Treat null as "not known", not as "none".

Attribution

Every figure in api_keys is per key, which is what makes a key the unit of cost accounting. Issue one per service, per environment, or per customer, and this route tells you what each of them spent without a second system.

GET /storage

What the namespace is currently holding, and where it went.

curl "$PERDURANCE_URL/storage" -H "Authorization: Bearer $PERDURANCE_KEY"
{
  "namespace_id": "…",
  "stored_bytes": 20418302,
  "request_bytes": 4192048,
  "response_bytes": 2884102,
  "chunk_bytes": 13342152
}

stored_bytes is the sum of the other three. The split matters because they grow for different reasons: request_bytes tracks how much you send, response_bytes how much comes back whole, and chunk_bytes how much comes back streamed — which is usually the largest, because a streamed answer is stored as its individual frames so it can be replayed and resumed.

Keeping it down

Retention is a namespace setting, in minutes, and it is unset by default — which means every record is kept until you say otherwise. Set it in the console under Namespaces, or with PATCH /{tenancy}/v1/namespaces/{namespace}. A namespace that names no horizon takes its tenancy's; a namespace that names 0 keeps everything even where its tenancy sweeps.

What a sweep takes is the record: the request body, the answer, and the chunks. What it leaves is everything you bill and report from — the status, the model, the token counts, the cost, the timings, and the fee ledger. Those have their own, much longer horizons, set for the whole deployment rather than per namespace.

So shortening a namespace's retention bounds your disk without costing you the ability to reconstruct an invoice. Retrieving a swept record answers 410 Gone rather than 404 Not Found, because the request did happen and everything about what it did is still there.

Lapsed idempotency claims are swept whatever retention says, because a claim is not a record.

Per-request sizes are also in the listing: GET /requests gives stored_bytes per row, which is how you find the handful of requests holding most of it.

On this page