An egress proxy for LLM traffic. Apps keep their stock OpenAI or Anthropic SDK
and change only the base URL. The gateway authenticates the caller, swaps in a
provider key it holds, relays the request and streamed response, and writes one
priced billing row (ai.usage) per request.
It is stateless: no database, no session store. Identity is a signed token it verifies without a lookup; revocation and quota arrive as small sets from NATS.
One base URL change, and the hard parts of LLM egress are done.
| The fact | The proof | |
|---|---|---|
| Stateless, so it scales by adding replicas | No database and no session store. A bai_v1 virtual key is an Ed25519-signed token verified in about 28 µs with no lookup, and a forged-key flood is rejected in tens of nanoseconds before that cost is paid. |
Verification cost, Benchmarking |
| Your SDKs, unchanged | OpenAI and Anthropic clients keep working, and a request is translated between Chat Completions, Messages and Responses when the chosen model lives on a different wire. | Routing, verify/ |
| Name a model, get failover | /auto and managed /v1 walk a catalog: the gateway picks the provider, fails over, and opens a per-provider circuit breaker after 20 failures in 10 s. |
Routing, Model routing |
| Every request priced at cost | One ai.usage row per request, priced pass-through at the serving host's card from rates generated out of the vendors' own pricing sources and re-checked daily. A stream cut short is billed an estimate, flagged. |
What it records, rates-drift |
| Keys never reach the app | Apps hold a virtual key; the provider key stays in the gateway and is swapped in per request. Revocation and quota arrive as small sets over NATS. | Two kinds of caller |
| Checked against real clients | Every claim the gateway makes is a registered claim with tests that carry its id, run against real SDKs and providers; every defect found is in a ledger and settled. Rerun it yourself with mise run verify:status. |
verify/, Verification |
| Key | What the gateway does |
|---|---|
Managed: bai_v1…, bai_v2…, bai_v3… |
Verifies the Ed25519 signature, checks the deny-set and allowance-set, swaps in a pool key, bills a row to the tenant. |
| BYO: any other token | Forwards the request unchanged to the provider the key belongs to. No swap, no deny-set, no billing row. |
Managed keys reach only POST generation endpoints: chat/completions,
messages, responses, embeddings, and the count_tokens and compact
sub-resources. Files, batches, stored responses and WebSocket upgrades are
refused. A managed key also gets the model catalog: it names a model and the
gateway picks the provider, fails over, and translates between wires.
BYO only: no config, no NATS. A binary and an upstream key are enough:
cargo run -p beyond-gateway --release # listens on :8080 (data) and :9090 (admin)
curl localhost:8080/v1/chat/completions \
-H "Authorization: Bearer $OPENAI_API_KEY" -H 'Content-Type: application/json' \
-d '{"model":"gpt-5-mini","messages":[{"role":"user","content":"hi"}]}'An image is published as ghcr.io/beyondoss/ai (latest and by commit SHA,
linux/amd64 and linux/arm64); its entrypoint is gateway. Configuration
comes from ./config.toml (-c/AI_CONFIG_PATH) and AI_-prefixed
environment variables.
Managed keys. Managed traffic needs a signing key, a pool key, an id-signing
secret and NATS (nats_url, default nats://localhost:4222). With a dev
keypair:
cargo run -p beyond-gateway --example mint_dev_key # prints SIGNING_PUBKEY_B64, KID, DEV_BAI_V1 (dev only)# config.toml
[signing_keys]
1 = "<SIGNING_PUBKEY_B64>"AI_POOL_KEY_OPENAI=sk-... AI_ID_SIGNING_KEY_1=$(openssl rand -base64 32) \
cargo run -p beyond-gateway --releaseThen use DEV_BAI_V1 as the API key against http://localhost:8080/v1. Until
the allowance-set has been read from NATS (or a snapshot_path snapshot),
managed requests get 503 allowance unavailable and /readyz reports
not_ready; BYO traffic is unaffected. gateway doctor checks NATS, signing
keys, pool keys and provider DNS, and exits non-zero on a failure.
Point an SDK at the gateway:
from openai import OpenAI
client = OpenAI(base_url="http://ai.internal/v1", api_key="bai_v1...") # or your own provider key| Path | Goes to |
|---|---|
/v1/… |
BYO: OpenAI, or Anthropic for /v1/messages and sk-ant- keys. Managed: the model catalog. |
/auto/v1/… |
Managed only: the model catalog. |
/{provider}/… |
That provider, with the rest of the path forwarded verbatim. Unknown provider: 404. |
GET /v1/models |
Managed: the catalog rows this deployment has a pool key for, with list prices. BYO: the provider's own list. |
The provider is the first path segment, never a header:
OpenAI(base_url="http://ai.internal/groq/openai/v1", api_key="bai_v1...")Twelve providers are built in: openai, anthropic, openrouter,
fireworks, groq, deepseek, together, cerebras, mistral, xai,
bedrock and openai-codex (BYO only). Their hosts, wires and auth headers
are rows in providers. [provider_authorities],
[provider_dialects] and [provider_auth_schemes] in config.toml add more,
for example Azure OpenAI.
A managed /v1 or /auto request names a model in the x-beyond-model header
or the body's model. The gateway looks it up in the catalog
(providers), tries the row's providers
in order, rewrites model per attempt, and translates between Chat
Completions, Messages and Responses when the inbound endpoint and the serving
candidate's differ. A name not in the catalog is a 404. The default order keeps
one caller on one provider, so provider prompt caches stay warm.
Per-request control, managed only, stripped before the upstream and never a 4xx:
| Header | Effect |
|---|---|
x-beyond-order |
Try these providers first, then the rest of the row. |
x-beyond-only |
Drop every provider not named. |
x-beyond-split |
name=weight pairs: a weighted primary. |
x-beyond-metadata |
A flat JSON object (at most 16 keys, 1 KB) tagged onto the billing row. |
x-beyond-capture |
on or off: log this request's bodies, or not. |
x-beyond-cache |
off: skip the response cache. |
Every response carries x-beyond-request-id (the billing row's request_id),
x-beyond-provider, and on catalog walks x-beyond-upstream-model.
- Billing. Each managed request writes an
ai.usagerow with token counts and a price fromproviders::pricing: pass-through at the serving host's card, so price equals cost. Rows go to stdout, or to hourlyai-usage-YYYYMMDDTHH.jsonlfiles whenusage_diris set. A stream cut short is billed an estimate flaggedusage_estimated. - Streaming. SSE is relayed event by event and never held whole; a 64 KB tail is read for usage. Request bodies are buffered only where routing needs them (catalog walks, bodies over 64 KB).
- Deny-set and allowance-set. NATS carries which tenants and keys are denied (spend, fraud) or out of quota. A denied caller gets 402 or 403. The deny-set fails open during a NATS outage; the allowance-set fails closed until first read.
- Guardrails.
rate_limit_rpsper credential (default 100), a sharedbyo_rate_limit_rpsfor BYO (default 1000), optionaltenant_max_in_flight, and a per-provider circuit breaker (opens on 20 failures in 10 s, 5xx or connect errors, probes after 30 s). - Payload capture. Off for every tenant until the control plane writes an
aicapture.{tenant}entry, or a caller sendsx-beyond-capture: on. - Response cache. Off by default.
cache_ttl_secs > 0replays identical managed catalog requests from a per-process table.
| Port | Serves |
|---|---|
| 8080 | Client traffic (listen); tls_listen adds a TLS listener. |
| 9090 | /metrics (Prometheus), /livez, /readyz (metrics_listen). |
On SIGTERM the gateway stops accepting, lets in-flight requests finish for up
to shutdown_grace_period_secs (default 600), and exits as soon as none remain.
12 providers, zero config: openai, anthropic, openrouter, fireworks, groq, deepseek, together, cerebras, mistral, xai, bedrock and openai-codex (BYO only). Add more in config.toml under [provider_authorities].
config.example.toml documents every key and its default. Scalar keys take an
AI_ prefix, and pool and signing keys come from AI_POOL_KEY_<NAME> and
AI_SIGNING_KEY_<KID>. Map keys (provider_authorities.*, provider_dialects.*,
provider_auth_schemes.*) are config-file only.
Required to serve managed traffic:
| Key | Source | Purpose |
|---|---|---|
signing_keys |
config.toml |
Ed25519 public keys by kid, verifies bai_v1 tokens |
AI_POOL_KEY_<NAME> |
env (SSM) | Provider key swapped in for managed requests |
Optional:
| Key | Default | Purpose |
|---|---|---|
snapshot_path |
unset | On-disk deny-set snapshot; set on durable nodes, leave unset on Fargate |
rate_limit_rps |
100 |
Per-key request ceiling; 0 disables |
[provider_authorities] |
built-ins | Override or add upstream hosts |
createOpenAI({ baseURL, apiKey }) works. Its default model uses the Responses
API and sends earlier assistant turns as item_reference ids, which only
OpenAI stores. On a managed key and a non-OpenAI model, a continued
conversation is a 400 naming item_reference; tool loops are unaffected. Send
history in full with providerOptions: { openai: { store: false } }, or use
@ai-sdk/anthropic or @ai-sdk/openai-compatible
(Stored-response references).
ARCHITECTURE.md: request flow, key and billing contracts, failure modes, metrics.config.example.toml: every configuration key.verify/README.md: the claims the gateway makes and the tests that prove each.mise run bench: unit micro-benchmarks and end-to-end throughput.