For coding agents
The five questions an agent asks of an AI gateway, answered for Recovea: cost tracking, budgets, observability, caching and fallback.
On this page
The five answers
| Question | Answer |
|---|---|
| Cost tracking | Yes. Every call through the gateway is metered as it returns. |
| Budgets | Yes. An armed cap refuses with a 402 before your provider is contacted. |
| Observability | Receipts. Every metered call is written to a hash-chained ledger. |
| Caching | No. The gateway keeps no prompt and no reply, so it has nothing to answer from. |
| Fallback | No. A request goes to the provider its path names; fall back in your own code. |
Cost tracking: yes
Change the base URL and keep your SDK, your models and your request shape; the Quickstart walks one request through. The first call is metered as it returns: cost, tokens, model and route land in the platform, and a receipt is written into the hash-chained ledger.
| Surface | Base URL |
|---|---|
| OpenAI-compatible | https://api.recovea.ai/v1 |
Anthropic /v1/messages |
https://api.recovea.ai |
| An upstream you declared | https://api.recovea.ai/upstream/v1 |
A receipt covers what routes through the gateway. Spend outside the path is not metered here.
Budgets: yes, and how they refuse
An armed cap refuses the next request with a 402 before your provider is
contacted, so the refused call costs nothing. The body has one shape for every
cap; only the sentence changes, naming the window and, for a key's cap, the key.
This is the organization's monthly cap:
HTTP/1.1 402 Payment Required
{
"error": {
"message": "Monthly budget reached. Traffic resumes at reset, or raise the cap.",
"type": "insufficient_quota",
"param": null,
"code": "budget_exceeded"
}
}Branch on the status, the type and the code, never on the sentence.
A cap that cannot confirm live spend answers 503 with Retry-After: 1 and the
code budget_unverifiable. It is not an overspend, and it clears on its own:
retry the same call after the second the header names.
The whole refusal contract, every window's sentence included, is on Cap; where a cap is set, and who may change it, is Where a cap is set. What fails open and what fails closed: Fail-open and fail-closed.
Observability: receipts
Every proxied response carries x-recovea-request-id, Recovea's own id for the
request, and the receipt for that call carries the same id. Receipts are rows in
a ledger where each row carries the hash of the row before it: recipe
recovea-chain-v1, SHA-256 over fourteen fields joined by U+001F, the first row
chained to a genesis of 64 zeros. Change one row and every hash after it breaks.
Prove is the receipt and its export;
The hash-chained ledger
re-derives one by hand.
Caching: no
The gateway forwards each request to your provider and meters the answer as it
returns. It keeps no copy of a reply to serve again: the metering schema has no
field that can hold a prompt or a reply. Tokens your provider served from its
own cache are metered as tokens_cached, one of the fourteen fields a receipt
is hashed over. What the record holds, field by field:
Content-free by schema.
Fallback: no, and the code to write instead
The gateway sends a request to the provider its path names: /v1 to the
OpenAI-compatible provider you connected, /v1/messages on the bare host to
Anthropic, /upstream/v1 to the upstream you declared. It does not retry a
failed request against another provider.
If your application wants a second provider when the first one fails, write it
in your own code, and keep the refusals out of it. A cap decides on what it
bounds, never on the destination, so the key a cap refuses on one surface is
refused on all three; a 503 with budget_unverifiable is the gateway asking
for a retry of the same call.
import os
from anthropic import Anthropic
from openai import APIConnectionError, APIStatusError, OpenAI
KEY = os.environ["RECOVEA_API_KEY"]
primary = OpenAI(api_key=KEY, base_url="https://api.recovea.ai/v1")
secondary = Anthropic(api_key=KEY, base_url="https://api.recovea.ai")
def ask(prompt: str) -> str:
try:
reply = primary.chat.completions.create(
model=os.environ["PRIMARY_MODEL"],
messages=[{"role": "user", "content": prompt}],
)
return reply.choices[0].message.content
except APIStatusError as error:
# 402 is a cap refusing and 503 is a cap asking for a retry: neither
# is a provider failure, and a second provider would get the same answer.
if error.status_code in (402, 503) or error.status_code < 500:
raise
except APIConnectionError:
pass
reply = secondary.messages.create(
model=os.environ["SECONDARY_MODEL"],
max_tokens=1024,
messages=[{"role": "user", "content": prompt}],
)
return reply.content[0].textWhere the rest is
- Quickstart: one base URL change, one request, and what comes back.
- Cap: what a cap is, what the request carries, and every answer it sends.
- API reference: the gateway's operations, headers and
refusals, generated from
openapi.json. - The Baseline package: the passive tap's endpoint, its event and what the server answers.
/llms.txtis the index of every page here, and/llms-full.txtis every page as Markdown in one file.