Lararouter
Getting started

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

PrefixKindEnvironment
lr_sk_live_Secret keyLive. Spends money.
lr_sk_test_Secret keySandbox. Free.
lr_pk_live_ / lr_pk_test_Publishable keyRead-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: REGION

Switching 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_2Wp7Kd91

Sandbox 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 then completed, on a simulated delay, incrementing request_counts as it goes. Your poll-and-backoff loop is genuinely exercised.
  • The output_file_id is a real JSONL file in the region, one result line per input line, with your custom_id echoed 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:

requests.jsonl
{"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.

On this page