Environments
Sandbox and live, and the key prefix that decides which one you are talking to.
Every project has two environments. Sandbox returns mock completions, never touches an accelerator, and never reaches an invoice. Live does the real work and costs real money.
Which one answers is decided by the key you send, not by a header.
The prefix carries the environment
| Prefix | Kind | Environment |
|---|---|---|
lr_sk_live_ | Secret key | Live. Spends money. |
lr_sk_test_ | Secret key | Sandbox. Free. |
lr_pk_live_ / lr_pk_test_ | Publishable key | Read-only, safe to expose. |
There is no environment header
The prefix on the key selects sandbox or live: lr_sk_test_ is sandbox, lr_sk_live_ is live.
Live spend happens when you send a live key, not because a header was omitted. Keys are
region-bound the same way: a key is issued for one region, and
that region's host is the one that accepts it.
Every response echoes which environment answered, next to the region:
GET /v1/me HTTP/1.1
Host: REGION.api.lararouter.com
Authorization: Bearer lr_sk_test_9Hs4TnQ2
HTTP/1.1 200 OK
X-Lararouter-Environment: sandbox
X-Lararouter-Region: REGIONSwitching an integration is a credential swap and nothing else:
# Sandbox
export LARAROUTER_API_KEY=lr_sk_test_9Hs4TnQ2
# Live, once the integration is proven
export LARAROUTER_API_KEY=lr_sk_live_2Wp7Kd91Sandbox mirrors everything except the inference
Only the model call is mocked. Everything before it behaves exactly as it does in live: scope checks,
region binding, model_not_available_in_region with its available_in list, 422 context_length_exceeded, idempotency including all three 409s, rate limiting, and budget
enforcement.
A sandbox request for a model that isn't deployed in that region still returns 404, even though no
accelerator was ever going to be involved. If sandbox skipped any of that, an integration would pass
in sandbox and fail in live, which is worse than having no sandbox at all.
What sandbox returns is a mock completion of a plausible length, and a job that takes time. The delay is the part that matters for integration work:
- A batch passes through
validating,in_progress, and only thencompleted, on a simulated delay, incrementingrequest_countsas it goes. Your poll-and-backoff loop is genuinely exercised. - The
output_file_idis a real JSONL file in the region, one result line per input line, with yourcustom_idechoed back. - Partial failure is reachable, because it is the case most integrations handle worst.
Triggering a specific failure
Errors & retries publishes a canonical retry loop, and sandbox is where you prove yours works. Ask for a documented failure with sandbox-only metadata rather than a magic model ID, so it composes with any model:
{"custom_id":"ticket_4821","method":"POST","url":"/v1/chat/completions","body":{"model":"llama-3.3-70b","messages":[{"role":"user","content":"Summarize this ticket."}]},"metadata":{"sandbox_error":"upstream_unavailable"}}Prop
Type
sandbox_error is rejected by a live key, so a test fixture that leaks into production fails loudly
instead of doing nothing.
Sandbox is measured, but never billed
Not billed does not mean unmeasured. Sandbox computes a simulated cost against the real
rate card and writes it to a separate ledger, so usage,
budgets, and 429 usage_limit_exceeded behave exactly as they will in live.
None of it reaches Stripe.
Everything else is isolated per environment too:
- Files and batches. A sandbox
file_ID can't be used by a live batch. - Audit records. A sandbox request ID
404s in live, and the reverse. - Rate-limit buckets, so a sandbox load test can't throttle paying traffic.
- Budgets, so a simulated overspend can't stop live work.