route_record_verify.py, written from the extension's text, verifies the router's signature with WAI's strict Ed25519 rule and the hop receipts with the OER/2 reference verifier, recomputes every candidate, removal, rank, order, decision and attempt, gives every vector its outcome, requires every mutated copy of the corpus to fail and catches each of its own checks dropped. The document kinds are wai-route-request, -pricebook, -policy and -record, registered as wire identifiers; wai.route.record is a registered record (SPEC §5, the export regenerated). CI runs the module's tests, tests/route_record.rs and the verifier, and checks the feature alone. The catalogue gains route.verify (wai route verify) and package.build and package.verify (wai package build|verify), which run the reference packager wai-pack as itself; wai-pack.yml packs the golden package through the door and verifies it, and a copy with an edited receipt is refused. CHANGELOG entry, with the meters' Send bound acknowledged as an API break. Co-Authored-By: Claude Opus 5.5 <[email protected]>
28 KiB
WAI Extension: Route Record
Status: Draft. Registers the record
wai.route.record: what a router considered for one media job and why it chose. A job names an operation of WAI's catalogue, a capability and a fallback (SPEC §4), the quantities it expects, hard limits that remove candidates and soft preferences that reorder the rest. A router reads its candidates from a price book, removes those a hard limit removes, ranks the survivors under a named and versioned routing policy, tries them in that order, and signs a route record: every candidate with the figures the price book stated for it and the limits that removed it, the order, the decision and why, and every attempt with its hop's OER/2 receipt and whether it was billed. A verifier given the record, the request, the price book and the policy recomputes the candidates, every removal, the ranking, the order and the decision, holds the attempts to the order and the billing rules, and, given the hops' receipts, holds each attempt to its receipt; it refuses a record those inputs do not give. No transport, no payment and no account: a route record is the recomputable account of a routing decision. The reference implementation is theroute_recordmodule ofwai-rs(featureroute_record); the corpus islive-conformance/routes/, and its second implementation islive-conformance/route_record_verify.py. Keywords MUST, MUST NOT, SHOULD, MAY are RFC 2119/8174.Texts read for this revision: the WAI texts this one cites; RFC 8259; RFC 8785; RFC 8032; ISO 4217 (the alphabetic currency code); FIPS 180-4.
1. Scope
In scope: the three documents a routing decision rests on, the request, the price book and the policy (§3); how a router computes its candidates, their removals, their ranking and its decision from them (§4); the record, its attempts and its signature (§5); how an attempt is held to its hop's OER/2 receipt (§6); verification (§7); and the reference implementation and corpus (§10).
Out of scope: a transport; how a router learns its prices, which the price book states and this extension takes as stated; accounts, credit and payment; the job's output and its own receipts, which their extensions define; and the energy of a hop, which its OER/2 receipt states (oer-receipts). An invoice that bills a job from its route record is the receipt ledger's (receipt-ledger §5).
Relationship to the core spec: wai.route.record is a record (SPEC §5): a
signed record a WAI tool emits and verifies. It names no payload format and is not
dispatchable.
2. Terms and encodings
- Router: an Ed25519 key (RFC 8032) that signs route records. Signer: the record's
signer. - Time: microseconds since 1970-01-01T00:00:00Z, an integer.
- Amount: an integer of millionths of a currency's minor unit (ISO 4217), as the
receipt ledger's amounts are (receipt-ledger §2). A price is an amount per
divisorunits of a unit. - Document: a JSON text (RFC 8259) read strictly: no member name repeated within an
object, no string holding an unpaired surrogate, arrays and objects nested at most 64
deep (the document counting as one), and every number an integer from 0 to 2⁵³ − 1
written in plain decimal (no sign, fraction or exponent). A text that is not such a
document is
malformed. Every value of a document is therefore in the subset RFC 8785 canonicalises without a choice (the subsetwai::jcswrites), and its canonical form is RFC 8785's. - Digest: SHA-256 (FIPS 180-4) over a document's canonical form, written as 64 lowercase hexadecimal digits.
- Token: a string of 1 to 128 bytes, each an ASCII letter or digit or one of
.,_,-,:and/. Endpoints, providers, regions, residencies and a policy's name are tokens. Tokens are ordered by their bytes. - Unit: a string of 1 to 64 bytes, a lowercase ASCII letter and then lowercase letters,
digits and
_(subpixels,seconds,packages): the name of what a quantity counts. - Capability: a string of 1 to 128 bytes of lowercase ASCII letters, digits,
_and., beginningwai.: a capability of WAI's registry (SPEC §5), which a router SHOULD name only when it is registered. - Operation: the id of an operation of WAI's catalogue (
wai/catalogue/ops.json): two words joined by., each a lowercase ASCII letter and then lowercase letters and digits (media.encode). - Key and signature: 64 and 128 lowercase hexadecimal digits.
- Class: an acquisition class of energy-measurement §2, by its
label:
CalibratedInstrument,OnChipCounter,ModelBasedorEstimator. - ⌊x⌋ is x rounded down; products are computed in integers wide enough not to overflow.
3. The documents
Each document is an object of exactly the members its table lists, each of its form; a
document that breaks a rule of this section is refused with its own reason
(request_invalid, pricebook_invalid, policy_invalid).
3.1 The request
What the job asked for, as the router received it.
| member | form |
|---|---|
kind |
"wai-route-request" |
job |
a token: the job's id |
operation |
an operation |
capability |
a capability |
fallback |
a capability other than capability, or null |
quantities |
1 to 16 objects of exactly unit (a unit) and quantity (an integer at least 1), in strictly ascending order of unit: what the job is expected to need, unit by unit, before anything ran |
limits |
an object of exactly the members below |
prefer |
an object of exactly sort (null or one of price, ttfb, energy, quality), ttfb_ms (null or an integer: a time to first byte, in milliseconds) and quality (null or an integer 0 to 1000: a quality floor) |
deadline_us |
null or an integer at least 1: the time after acceptance by which the job's output must be served |
limits, the hard limits:
| member | form |
|---|---|
regions |
null, or 1 to 64 tokens in strictly ascending order: the regions an endpoint may run in |
residency |
null, or 1 to 64 tokens in strictly ascending order: the places the job's data may rest |
no_training |
a boolean: no endpoint that trains on its inputs |
zero_retention |
a boolean: no endpoint that keeps the job's data |
ga_only |
a boolean: only endpoints generally available, not previews |
consent_on_file |
a boolean: whether the job's owner holds consent for what an endpoint that needs it does (a cloned voice) |
max_price |
0 to 16 objects of exactly unit, price and divisor (an integer at least 1), in strictly ascending order of unit: the most the job pays per divisor units of the unit |
spend_ceiling |
null, or an object of exactly currency (three capitals A–Z) and amount: the most the job may cost |
3.2 The price book
What each endpoint offers and costs, as the router read it.
| member | form |
|---|---|
kind |
"wai-route-pricebook" |
currency |
three capitals A–Z |
issued |
a time: when the prices were stated |
entries |
1 to 1024 objects of exactly the members below, in strictly ascending order of endpoint |
An entry:
| member | form |
|---|---|
endpoint |
a token: the endpoint's id |
provider |
a token: who runs it |
capability |
a capability: what it produces |
region, residency |
a token, or null when not known |
trains_on_inputs |
a boolean, or null when not known |
retention_days |
an integer, or null when not known: how long it keeps a job's data (0: not at all) |
ga |
a boolean, or null when not known: generally available |
needs_consent |
a boolean, or null when not known: whether what it does needs the consent of the person it imitates |
unit |
a unit: what it is priced by |
divisor |
an integer at least 1 |
price |
an integer: its price per divisor units |
ttfb_ms |
an integer, or null when not known: its time to first byte, in milliseconds |
energy_uj |
an integer, or null when not stated: the microjoules it states per divisor units |
energy_class |
the class of that figure, or null exactly when energy_uj is null |
quality |
an integer 0 to 1000, or null when not known |
A price book states each figure as its endpoint's provider gave it; a value that is not known
is null, and §4.3 reads every null limit as the most restrictive value.
3.3 The policy
How survivors are ranked and how many are tried.
| member | form |
|---|---|
kind |
"wai-route-policy" |
name |
a token |
version |
an integer at least 1 |
keys |
the four ranking keys price, ttfb, energy and quality, each once, in the policy's order |
max_attempts |
an integer 1 to 16: the most survivors tried |
exclude |
0 to 64 provider tokens in strictly ascending order: providers the policy never routes to |
A policy is named and versioned so that a deployment pins it: the record states its name, version and digest, and a policy whose rules change takes a new version.
4. Routing
A router computes the following from the request, the price book and the policy, and a verifier computes the same. Nothing in it depends on the order of a document's members, on the time, or on anything the three documents do not state.
4.1 Candidates
The candidates are the price book's entries whose capability is the request's
capability, which are direct (via_fallback false), and, when the request names a
fallback, those whose capability is the fallback (via_fallback true), in the price
book's order. An entry of any other capability is not a candidate, and the record does not
list it.
4.2 Estimates
For a candidate whose unit the request states a quantity q for:
- its cost is ⌊
q×price/divisor⌋; - its energy is ⌊
q×energy_uj/divisor⌋ whenenergy_ujis stated.
An estimate is null when the request states no quantity in the candidate's unit, when
energy_uj is null (for the energy), and when it exceeds 2⁵³ − 1, which no document can
state. Units are never converted: a candidate priced in seconds is estimated from the
request's seconds and from nothing else. An estimate is the router's, from the request's
own quantities; what a job costs is what its invoice bills from its receipts.
4.3 Hard limits
Each candidate is held to every limit in this order, and its removals list each limit
that holds, in this order, each with the candidate's value the limit read (null when the
price book does not know it):
| limit | removes a candidate when | value |
|---|---|---|
policy |
its provider is in the policy's exclude |
provider |
unit |
the request states no quantity in its unit |
unit |
region |
the request's regions is not null, and its region is null or not in it |
region |
residency |
the request's residency is not null, and its residency is null or not in it |
residency |
training |
no_training is true and its trains_on_inputs is not false |
trains_on_inputs |
retention |
zero_retention is true and its retention_days is not 0 |
retention_days |
ga |
ga_only is true and its ga is not true |
ga |
consent |
consent_on_file is false and its needs_consent is not false |
needs_consent |
unit_price |
max_price has an entry for its unit, and price × that entry's divisor exceeds that entry's price × divisor |
price |
spend |
spend_ceiling is not null, and its currency is not the price book's, or the candidate's cost is null or exceeds its amount |
the cost |
So a value the price book does not know counts as the most restrictive one: an endpoint that
does not say it never trains on its inputs is removed by no_training, one with no stated
retention by zero_retention, and a cost that cannot be estimated, or a ceiling in another
currency, by spend. A candidate with no removal is a survivor.
4.4 Ranking
The survivors are ordered by a list of keys, the key list:
targets, when the request'sprefer.ttfb_msorprefer.qualityis notnull: a survivor meets the targets when, for each target stated, itsttfb_msis stated and at mostprefer.ttfb_ms, and itsqualityis stated and at leastprefer.quality;- the request's
prefer.sort, when it is notnull; - the policy's
keys, in its order, withoutprefer.sort; endpoint.
Two survivors compare by the first key of the list on which they differ, and the one that comes first under that key ranks first:
| key | first |
|---|---|
targets |
the one that meets the targets |
price |
the smaller cost |
ttfb |
the smaller ttfb_ms |
energy |
the smaller energy |
quality |
the larger quality |
endpoint |
the smaller endpoint, by its bytes |
A null figure comes after every stated one, and two nulls do not differ. Endpoints are
distinct, so the order is total. The order is the survivors' endpoints in it, and each
survivor's rank is its position there, from 1. The energy compared is each figure as its
provider states it, whatever its class: the record states every class beside its figure, so
a reader sees what was compared.
4.5 The decision
decided_by is none when there is no survivor, only_survivor when there is one, and
otherwise the first key of the key list on which the first two survivors of the order
differ: the reason the first was tried before the second. runner_up is the second
survivor's endpoint, or null when there are fewer than two.
The number of survivors the router may try is the smaller of the survivors' number and the
policy's max_attempts, the allowed attempts.
5. The record
5.1 Members
A route record is a document of exactly these members:
| member | form |
|---|---|
kind |
"wai-route-record" |
version |
1 in this revision |
job, operation, capability, fallback, quantities |
the request's |
request |
the request's digest |
policy |
an object of exactly name, version and digest: the policy's |
pricebook |
an object of exactly digest, currency and issued: the price book's |
candidates |
the candidates (§4.1), in their order, each an object of exactly the members below |
order |
the order (§4.4): the survivors' endpoints |
decision |
an object of exactly decided_by and runner_up (§4.5) |
accepted |
a time: when the router accepted the job |
deadline |
null when the request's deadline_us is null; otherwise accepted + deadline_us |
attempts |
0 to 16 objects of exactly the members of §5.2, in the order they were made |
signer |
the router's key |
sig |
§5.3 |
A candidate:
| member | form |
|---|---|
endpoint, provider, capability, region, residency, trains_on_inputs, retention_days, ga, needs_consent, unit, divisor, price, ttfb_ms, energy_uj, energy_class, quality |
the price book's entry's, as it stated them |
via_fallback |
a boolean (§4.1) |
cost, energy |
its estimates (§4.2) |
removed |
its removals (§4.3): objects of exactly limit and value, in §4.3's order; empty for a survivor |
rank |
its rank (§4.4), or null for a candidate a limit removed |
5.2 Attempts
An attempt is one hop the router sent the job to:
| member | form |
|---|---|
hop |
its position among the attempts, from 1 |
endpoint |
the endpoint it was sent to |
outcome |
served (the hop returned the job's output) or failed |
error |
null when served; when failed, one of timeout, server_error, rate_limited, refused and unavailable |
start, end |
times: when it was sent and when it ended |
billed |
a boolean: whether the job is billed for it |
not_billed |
null when billed; otherwise failed or deadline_missed |
receipt |
the receipt_id of the hop's OER/2 receipt (oer-receipts §5): sha256: or blake3: and 64 lowercase hexadecimal digits |
The attempts MUST follow the order: the attempt at position i names the i-th survivor
of the order, and there are at most the allowed attempts (§4.5). So no attempt names a
removed candidate, and no survivor is tried before one ranked above it. Only the last attempt
may be served; a job whose last attempt failed was tried on every allowed survivor before
the router gave up. The times are in order: accepted is at most the first attempt's
start, each attempt's start is at most its end, and each attempt starts no earlier than
the one before it ended.
Billing:
- a failed attempt is not billed,
not_billedfailed: a hop that returned no output costs the job nothing, whatever the provider charged the router; - a served attempt that ended after the
deadlineis not billed,not_billeddeadline_missed: a job that missed the deadline it declared is not billed; - every other served attempt is billed.
So at most one attempt of a record is billed, and a reader finds the hop that served, whether it was billed and why not, and the receipt of every hop, failed ones too.
5.3 Signature and digest
The record's sig is Ed25519 by signer over the 17 bytes wai:route-record (ASCII) and
0x01, followed by the canonical form of the record without sig. It is verified strictly, as
SPEC §7 Signatures requires: a key that fails step 1 or 2 is bad_key, and a
signature that fails a later step is bad_signature. The record's digest, which names it
(receipt-ledger §5.2), is the digest of the whole record, sig included.
6. Hop receipts
Every hop is receipted under oer-receipts: a job receipt whose
capability is oer.job. followed by the record's operation (oer-receipts §3), whose
work.status is 1 for a hop that served and -1 for one that failed, and whose figure is
the hop's energy, or the unmetered marker where nobody measured it (oer-receipts §3.1). When
a verifier is given the receipts and the keys they are published under (oer-receipts §5), it
holds each attempt to its receipt:
- a receipt whose
receipt_idis the attempt'sreceiptis given: otherwisereceipt_missing; - it verifies under oer-receipts on its own, with no reason (§7 there): otherwise
receipt_invalid; - it is a
jobreceipt of the record's operation whosework.statusis the attempt's outcome's: otherwisereceipt_mismatch.
A receipt states the hop's energy with its grade, and, in minor version 1, its class or the unmetered marker; the record states no figure of its own. A reader of the hops' energy reads it from their receipts, and an invoice bills a job from them (receipt-ledger §5).
7. Verification
7.1 Inputs
A verifier is given a verification document: a JSON object of exactly request,
pricebook, policy and record, each a string holding that document's text, receipts,
null or a list of strings each holding an OER/2 receipt's text, and keys, null or an
object of a receipt's chain.signer names to keys, as oer-receipts §5 publishes them. The
texts are carried as strings so that each is read as written. A verification document that is
not such an object, or whose receipts is a list while its keys is null, is malformed.
7.2 Steps
A verifier runs these steps and stops at the first refusal, which it reports with its reason
and where (at, a list: the member, then indexes or members within it):
- The request (§3.1), at
request:malformed,request_invalid. - The price book (§3.2), at
pricebook:malformed,pricebook_invalid. - The policy (§3.3), at
policy:malformed,policy_invalid. - The record (§5), at
record: a text that is not a document, or whosekindis notwai-route-record, ismalformed; aversionthat is an integer other than1isunknown_version, atrecord,version; one that is not of §5.1's and §5.2's members, each of the form its table gives it (a restated member of a candidate, the form §3.2 gives it), isrecord_invalid(an attempt whosehopis not its position, or whoseerroris not as itsoutcomerequires, among them; the relations between a candidate's members, such as an energy figure and its class, are step 6's); then its signature (§5.3):bad_key,bad_signature, atrecord,sig. - The binding, at
recordand the member: itsjob,operation,capability,fallback,quantities,requestanddeadlineare not the request's (request_mismatch); itspolicyis not the policy's name, version and digest (policy_mismatch); itspricebookis not the price book's digest, currency andissued(pricebook_mismatch). - The candidates, at
record,candidatesand the index, recomputed by §4, each in turn and for each these three in this order: a candidate missing, added or out of order, or with a member other thanremovedandranknot what §4 gives it (candidate_mismatch); itsremovednot its removals (removal_mismatch); itsranknot its rank (rank_mismatch). A list of the wrong length is acandidate_mismatchat the first index past the shorter. Then, atrecord,order, the order (rank_mismatch); and atrecord,decision, the decision (decision_mismatch). - The attempts, at
record,attemptsand the index, each in turn, by §5.2: an attempt past the allowed number (attempts_exceeded); one that names an endpoint that is not a survivor (attempt_removed); one that names a survivor not at its position in the order (attempt_out_of_order); a served attempt that is not the last (served_not_last); times out of order (times_invalid);billedandnot_billednot what §5.2 gives (billing_invalid). Then, atrecord,attempts: a job whose last attempt failed, or that made none while a survivor was allowed, with fewer attempts than allowed (attempts_incomplete). - The hop receipts (§6), when
receiptsis notnull: first each receipt's text, atreceiptsand its index, which a receipt that is not a document under oer-receipts §4 fails (receipt_invalid); then each attempt in turn, atrecord,attempts, its index andreceipt:receipt_missing,receipt_invalid,receipt_mismatch, the first receipt of the list whosereceipt_idis the attempt's being its receipt.
7.3 Outcome
- refuse: the reason and where.
- accept: the record's digest; the job and its operation; the served endpoint and the hop
that served it, or
nulls for a job no hop served; the billed hop, ornull; the deadline,nonewhen none was declared and otherwisemetormissedby the served hop, ornonetoo when no hop served; the order; the decision; each removed candidate's endpoint with the limits that removed it; whether the hop receipts werecheckedornot_checked; and, when checked, the hops whose receipts state the unmetered marker (oer-receipts §3.1), which a reader MUST NOT present as hops of zero joules.
8. Reasons
The codes this extension adds, each with where it is raised:
| reason | raised at |
|---|---|
request_invalid |
§3.1, §7.2 step 1 |
pricebook_invalid |
§3.2, §7.2 step 2 |
policy_invalid |
§3.3, §7.2 step 3 |
record_invalid |
§5.1, §5.2, §7.2 step 4 |
request_mismatch |
§7.2 step 5 |
policy_mismatch |
§7.2 step 5 |
pricebook_mismatch |
§7.2 step 5 |
candidate_mismatch |
§4.1, §4.2, §7.2 step 6 |
removal_mismatch |
§4.3, §7.2 step 6 |
rank_mismatch |
§4.4, §7.2 step 6 |
decision_mismatch |
§4.5, §7.2 step 6 |
attempts_exceeded |
§5.2, §7.2 step 7 |
attempt_removed |
§5.2, §7.2 step 7 |
attempt_out_of_order |
§5.2, §7.2 step 7 |
served_not_last |
§5.2, §7.2 step 7 |
times_invalid |
§5.2, §7.2 step 7 |
billing_invalid |
§5.2, §7.2 step 7 |
attempts_incomplete |
§5.2, §7.2 step 7 |
receipt_missing |
§6, §7.2 step 8 |
receipt_invalid |
§6, §7.2 step 8 |
receipt_mismatch |
§6, §7.2 step 8 |
A verifier of this extension also raises malformed for a text that is not a document (§2)
or a verification document that is not §7.1's, unknown_version for a record of another
version, and bad_key and bad_signature for its signature (SPEC §7).
9. Operation
The catalogue's operation route.verify (wai route verify <document>) verifies a
verification document (§7.1) and prints its outcome (§7.3), offline; it exits 0 when the
record is accepted and 1 when it is refused.
10. Conformance
live-conformance/routes/ holds the corpus, in one file,
vectors.json, written by wai_route_record_vectors gen from fixed seeds: a router's key and
a hop's, and the OER/2 receipts of minor version 1 the hops sign. Each vector is a
verification document with the outcome §7.3 gives it:
- Accepted: a job served by its first survivor, billed; a failover after a server error,
the failed hop unmetered and not billed; a served hop past the deadline, not billed; a job
every allowed survivor failed, with the policy's
max_attemptsbelow the survivors' number; a job no candidate survives; a ranking decided by the targets, by the request's sort, by a key of the policy and by the endpoint; an unknown energy and time to first byte ranked last; every limit of §4.3 removing a candidate, anullvalue among them, and a candidate removed by several; a ceiling in another currency; the fallback's candidates; and a record verified without its receipts. - Refused: each reason of §8 at least once, each vector a control of one defect, its record signed again over the defect where the defect is not in the signature; and each code §8 reuses.
wai_route_record_vectors verify and the test tests/route_record.rs check that the file
regenerates byte for byte, that wai-rs gives every vector its outcome, that every reason of
§8 refuses a vector, and that §8 lists exactly the reference's codes.
live-conformance/route_record_verify.py is the second implementation, written from this
text: it shares no code with wai-rs, verifies the router's signature with WAI's strict rule
written from RFC 8032 (claim-conformance/_ed25519.py), verifies the hop receipts with the
OER/2 reference verifier (oer-conformance/oer2_verify.py), recomputes every candidate,
removal, rank, order, decision and attempt itself, gives every vector the same outcome,
requires every mutated copy of the corpus to fail, and requires the corpus to catch the
verifier with each of its checks dropped.
11. Security and privacy considerations
- What the record proves. That the router, holding these three documents, chose by their
rules, and tried and billed as it says. It does not prove that the price book states what
the providers charge, or that an endpoint's stated retention or training is true: the price
book is the router's statement, pinned by digest, and a buyer who holds the published price
book of that digest compares it. A record whose candidates differ from its price book's
entries is refused (
candidate_mismatch), so a router cannot list a price it did not state. - Removed means never tried. An attempt that names a removed candidate is refused
(
attempt_removed): a job whose limits removed an endpoint was not sent there, by this router's own signed account. Whether a hop's provider kept the data it was sent is outside any record; the limit that removed a provider that says it keeps data is not. - Failed hops are not billed. A record that bills a failed attempt, or a served one past
the declared deadline, is refused (
billing_invalid); the receipt ledger bills a job from the billed attempt alone. - Unknown is restrictive. Every
nulla limit reads removes the candidate (§4.3), so a price book that omits what a buyer cares about removes the endpoint rather than admitting it. - What a record reveals. Its job id, its operation and its quantities, every endpoint and provider that could have served the job with their prices, and when each hop ran. It travels between the router, the job's owner and an auditor they choose.