Envelope¶
The record Brama logs beside every failure, in the fleet's wisent-errors
shape. Brama speaks two error vocabularies to two audiences, and the split
is deliberate: what a client reads is the HTTP error body — a stable type
and code that predate the fleet vocabulary and do not change — and what an
operator reads is the envelope. Where the two readings differ, both are on
the line; neither stands in for the other.
Fields¶
Observed shape (captured from a 0.2.38 dispatch_refused log line):
{
"failure_point": "brama.dispatch.credential-selection",
"error_code": "rate_limit",
"service": "brama",
"impact": "one model request",
"severity": "warning",
"retryable": true,
"outage": false,
"detail": "no active 'openai' credential for agent",
"context": {"model": "openai/stub-ok"}
}
serviceis alwaysbrama.failure_point— exactly one of the nine seams (failure-point).error_code— the fleet's code, derived as below.impact— what this failure costs its caller.detail— the reason the layer below gave, verbatim. Nothing is paraphrased; the provider's text is data.severity,retryable,outage— always derived by thewisent-errorscrate, never decided at a call site.
How the fleet code is derived¶
Derivation is centralised in src/core/failure.rs. Where an upstream HTTP
status exists, the code is classified from that status by the crate's
catalogue — the catalogue is finer than Brama's kinds, and being finer is
the point. Otherwise Brama's own kind is translated exactly once:
| Brama kind | Fleet code |
|---|---|
provider_authentication, unauthenticated, credential_unauthorized, subscription_reauthorization_required |
Auth |
provider_rate_limited, subscription_unavailable |
RateLimit |
dependency_unavailable |
InfraDown |
provider_quota_exhausted |
Config — a spent quota is not a busy provider; no wait repairs it, and the account it names is ours |
invalid_request |
NotFound — the route, selector, or evidence asked for does not exist |
context_length_exceeded |
Refused — the prompt is longer than the model takes (the provider answered 413, or its error object's code or type is context_length_exceeded); HTTP 400, retryable: false. A client promotes the route or compacts; no wait or other credential repairs it |
provider_failure, internal_error |
Unknown |
Every arm preserves what the client contract already said about retrying: retryable kinds map to retryable codes and permanent ones do not. The kind is the refusal's class, stated where the refusal was built and carried with it to the HTTP edge, so the envelope's code and the client's answer are read from the same class and never from the words of the message.
Brama's provider layer still writes the kind as a message prefix — the
captured provider-authentication failure reads
provider_authentication: Incorrect API key provided: sk-… — for the
reader of the sentence; no answer to a model request is decided from it.
Rendering stays local¶
The envelope travels beside the shapes readers already parse, never inside them:
- the HTTP error body keeps its exact client contract (errors) — a new key there would be a wire change;
- log fields keep their names (
event,model, …) with the envelope as one structured field; - the usage ledger keeps its own schema; a definitive refresh refusal lands
there as
credential.state: needs_reauthorizationwith the provider's sentence ascause(subscription).
Where envelopes are not allowed¶
Logs and envelopes never include bearer values, HMAC signatures, capability
IDs, raw credentials, request bodies, prompt text, raw provider payloads, or
donated secrets. /readyz reports a refusal in the words of whatever
refused it — provider name and verdict only, no secret in the body.
Not to be confused with¶
- The client error body. That is the wire contract in errors; the envelope is the operator's record.
- The journal.
journal.jsonlrecords decisions (retirement, refresh verdicts, quality checks); the envelope records failures.