Docs / Reference
API reference
LaneZero exposes OpenAI-compatible chat, embeddings, and model routes, a public health check, and a session-authenticated management API.
Send Authorization: Bearer lz-live_... to authenticated /v1 routes. Provider credentials are separate. The health and documentation routes are public.
/v1/chat/completions
Creates a normalized chat completion. The request body is JSON. Set stream to true for Server-Sent Events.
Request fields
| Field | Type | Behavior |
|---|---|---|
model | string | A namespaced provider/model ID. Required unless models is present. |
models | string[] | LaneZero extension. A non-empty ordered fallback chain with at most three distinct models. Duplicates are removed before routing. Its first entry is the primary model. If model is also sent, it must equal the first entry. |
messages required | object[] | Ordered system, user, assistant, and tool messages. Content may be a string or an array of text parts. Image, audio, and other content-part types are not accepted. |
stream | boolean | Only literal true selects SSE. Missing or false returns one JSON response. |
tools | object[] | Function tools with name, optional description, optional JSON Schema parameters, and optional strict. |
tool_choice | string or object | none, auto, required, or one named function choice. |
temperature | number | Forwarded or translated to the selected provider. LaneZero does not impose a range. |
top_p | number | Forwarded or translated to the selected provider. |
max_tokens | integer | Maximum output tokens. Anthropic uses catalog metadata or 4096 when this field is absent. |
stop | string or string[] | One or more stop sequences. |
user | string | Forwarded to OpenAI-compatible providers. Anthropic and Google ignore it. |
| Other top-level fields | any JSON | Preserved and forwarded to OpenAI-compatible providers. Anthropic and Google ignore unmodeled fields. |
LaneZero removes models before calling a provider and sends only that attempt's provider-local model name. For Anthropic and Google, strict is currently not forwarded. Assistant tool-call arguments must contain valid JSON for Anthropic and a JSON object for Google.
Provider-key passthrough headers
| Header | Scope and precedence |
|---|---|
x-provider-key | Credentials for the primary provider only. The primary provider comes from model or the first models entry. |
x-provider-key-<provider> | Credentials for one named provider, such as x-provider-key-openai. This takes precedence over the generic header and also works for fallback entries. |
If neither matching header is present, LaneZero looks for that provider in the user's vault. Passthrough values are used for the current request and are not stored.
Request example
{
"models": [
"anthropic/claude-sonnet-4-6",
"openai/gpt-4.1"
],
"messages": [
{"role": "user", "content": "What is the weather in Liverpool?"}
],
"tools": [
{
"type": "function",
"function": {
"name": "get_weather",
"description": "Read current weather",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
}
],
"tool_choice": "auto",
"stream": false
}
Prompt caching
For Anthropic requests, add a cache_control object to a text content block at each cache breakpoint. The same extension works on text blocks in system messages and ordinary conversation messages. LaneZero translates it to Anthropic's native block format; other providers ignore the marker.
{
"model": "anthropic/claude-sonnet-4-6",
"messages": [
{
"role": "system",
"content": [
{
"type": "text",
"text": "Long-lived instructions and reference material...",
"cache_control": {"type": "ephemeral"}
}
]
},
{
"role": "user",
"content": [
{
"type": "text",
"text": "Stable conversation context...",
"cache_control": {"type": "ephemeral"}
},
{"type": "text", "text": "Answer this question."}
]
}
]
}
OpenAI and DeepSeek cache eligible prompts automatically, so clients do not send a cache marker for those providers. Cached input appears in usage.prompt_tokens_details.cached_tokens for normal responses and the final usage frame of streams.
"usage": {
"prompt_tokens": 2048,
"completion_tokens": 24,
"total_tokens": 2072,
"prompt_tokens_details": {"cached_tokens": 1536},
"x_lanezero": {
"cache_creation_input_tokens": 512,
"cache_read_input_tokens": 1536
}
}
x_lanezero preserves counters that have no direct OpenAI equivalent. Anthropic responses use cache_creation_input_tokens and cache_read_input_tokens; DeepSeek responses use prompt_cache_hit_tokens and prompt_cache_miss_tokens. OpenAI responses need no extension because cached_tokens is already native to its wire format.
Non-streaming response
{
"id": "chatcmpl_example",
"object": "chat.completion",
"created": 1720000000,
"model": "openai/gpt-4.1",
"choices": [
{
"index": 0,
"message": {"role": "assistant", "content": "Use soft tyres."},
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 12,
"completion_tokens": 4,
"total_tokens": 16
}
}
usage and system_fingerprint are omitted when the provider does not supply them. created is 0 for Anthropic and Google because those adapters do not receive a compatible creation timestamp.
Streaming response
The response uses Content-Type: text/event-stream and Cache-Control: no-cache. Each chunk is one data: event followed by a blank line. The final success chunk has empty choices and a usage object. LaneZero writes data: [DONE] immediately after that usage chunk.
data: {"id":"chatcmpl_example","object":"chat.completion.chunk","created":1720000000,"model":"openai/gpt-4.1","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}
data: {"id":"chatcmpl_example","object":"chat.completion.chunk","created":1720000000,"model":"openai/gpt-4.1","choices":[{"index":0,"delta":{"content":"Hello"},"finish_reason":null}]}
data: {"id":"chatcmpl_example","object":"chat.completion.chunk","created":1720000000,"model":"openai/gpt-4.1","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: {"id":"chatcmpl_example","object":"chat.completion.chunk","created":1720000000,"model":"openai/gpt-4.1","choices":[],"usage":{"prompt_tokens":7,"completion_tokens":2,"total_tokens":9}}
data: [DONE]
Tool calls arrive in delta.tool_calls. Function argument text can be split across chunks. Z.ai and Mistral do not receive stream_options; if they omit usage, LaneZero synthesizes a final usage object containing zeros. For the other OpenAI-compatible adapters, LaneZero requests usage but does not synthesize it. If one of those providers ends with [DONE] and no usage chunk, the downstream response currently ends without a final usage chunk or its own [DONE].
A failure after the first downstream chunk cannot switch providers. The HTTP status is already 200. LaneZero sends event: error with the normal error JSON in data:, then sends data: [DONE].
Routing response headers
| Header | Meaning |
|---|---|
x-lz-provider | The provider for the selected model or final routing failure. It is unknown when failure happens before a model can be routed. |
x-lz-attempts | The number of upstream calls made. Missing credentials do not increment it. |
x-lz-fallback | true when the final provider differs from the primary provider. It does not mean simply that attempts are greater than one. |
These three headers are added to every /v1/chat/completions response, including gateway authentication and JSON errors. Early failures use unknown, 0, and false.
/v1/embeddings
Creates one or more floating-point embedding vectors using the OpenAI request and response shape. Gateway and provider-key authentication use the same headers and stored-key fallback as chat completions.
Request example
{
"model": "openai/text-embedding-3-small",
"input": [
"Brake at the 100-metre board.",
"Turn in after releasing the brake."
],
"encoding_format": "float",
"dimensions": 512
}
input may be one string or an array of strings. encoding_format may be omitted or set to float; base64 returns a clear 400 in v1. dimensions is accepted only for models whose provider surface supports configurable vector lengths.
Response example
{
"object": "list",
"data": [
{"object": "embedding", "embedding": [0.012, -0.034], "index": 0},
{"object": "embedding", "embedding": [0.056, 0.078], "index": 1}
],
"model": "openai/text-embedding-3-small",
"usage": {"prompt_tokens": 18, "total_tokens": 18}
}
Provider support
- OpenAI: native
/v1/embeddingspassthrough. Configurable dimensions are supported by catalogedtext-embedding-3models. - Google: translated to
embedContentfor one string orbatchEmbedContentsfor an array.dimensionsmaps tooutputDimensionality. Google responses that omit token metadata report zero token counts rather than an estimate. - Mistral: native OpenAI-wire
/embeddings. LaneZero v1 does not senddimensionsto this surface. - Together: native OpenAI-wire
/embeddingsthrough the Host lane. LaneZero v1 does not senddimensionsto this surface. - Fireworks AI: native OpenAI-wire
/embeddingsthrough the Host lane. LaneZero v1 does not senddimensionsto this surface.
Qwen, DeepSeek, and the other chat-only providers do not expose an embeddings surface through their LaneZero adapters. Requests for them fail at preflight with a 400 that names the provider; LaneZero never invents an upstream endpoint.
Routing and fallback
The models array is evaluated from left to right. LaneZero makes at most three upstream calls.
- Fallback advances after provider
429, provider5xx, a transport or upstream-protocol failure, or a request timeout. - For streaming, fallback is allowed only before the first normalized chunk reaches the client. This includes a timeout waiting for response headers or the first upstream data. Once one chunk exists, later failures become SSE error events.
- Fallback does not advance after provider
400,401,403, or another non-4294xx. - A fallback entry without a passthrough or vaulted credential is skipped. The skip does not consume the three-call budget.
- An upstream
Retry-Afterdelay is honored only when it parses successfully and is no more than two seconds.
The implementation also classifies upstream 3xx responses as retryable upstream errors. Non-streaming timeouts can occur while waiting for the full response body, not only before the first byte.
/v1/models
Returns the embedded chat and embeddings model catalog, sorted by namespaced model ID. A gateway bearer key is required.
{
"object": "list",
"data": [
{
"id": "anthropic/claude-sonnet-4-6",
"object": "model",
"owned_by": "anthropic"
}
]
}
The response does not include prices, context limits, or tool capabilities. See Models and providers for catalog details.
/healthz
Public liveness response. The HTTP status and top-level status remain 200 and ok even when the database check is degraded.
- Database reachable
{"status":"ok","db":"ok"}
- Database check failed
{"status":"ok","db":"degraded"}
- No database configured
{"status":"ok","db":"absent"}
Management API
Management routes use the signed browser session created by OAuth or a magic link. They do not accept a gateway bearer key as a substitute. Every POST, PUT, PATCH, and DELETE also requires X-LaneZero-CSRF: 1. They return 503 when LaneZero runs without DATABASE_URL.
Sensitive mutations require authentication from the last 10 minutes. After that window, connected OAuth accounts sign in again and TOTP-enabled accounts can send X-LaneZero-TOTP; a successful TOTP check rotates the session cookie and refreshes the shared 10-minute window. TOTP attempts are limited per account and per IP address to five per five minutes, with escalating backoff.
For privacy, usage-event records are retained for 90 days by default. Operators can change the window with RACEWAY_USAGE_RETENTION_DAYS; setting it to 0 disables automatic pruning. Account deletion anonymizes usage unless RACEWAY_DELETE_USAGE_ON_ACCOUNT_DELETE=true.
| Route | Purpose |
|---|---|
GET /mgmt/me | Returns the backward-compatible github_login console label; Google-only accounts receive their verified email in that field. |
POST /mgmt/keys | Mints a gateway key from name, optional rate_limit_rps, optional positive rpm_limit and tpm_limit, and optional non-negative daily_budget_usd and monthly_budget_usd. The legacy burst default is 10 requests per second; omitted or null minute limits and budgets are unlimited. The response is the only time the full key is returned. |
GET /mgmt/keys | Lists key metadata, prefixes, rpm_limit, tpm_limit, both budget caps, and current_day_spend_usd/current_month_spend_usd from usage data. Revoked keys remain in the list. Plaintext keys and hashes are never returned. |
PATCH /mgmt/keys/{id}/limits | Changes either per-key minute limit after step-up authentication. Omitted fields stay unchanged, null clears a limit, and zero or negative values return 400. |
PATCH /mgmt/keys/{id}/budgets | Changes either per-key budget after step-up authentication. Omitted fields stay unchanged, null clears a cap, and negative values return 400. |
DELETE /mgmt/keys/{id} | Revokes one active gateway key owned by the session user after step-up authentication. |
POST /mgmt/keys/{id}/rotate | Mints a replacement key and returns its plaintext once. The old key remains usable until its configured grace deadline. |
GET /mgmt/provider-keys | Lists stored provider names, masked key prefixes and four-character hints, and creation times. |
POST /mgmt/provider-keys/test | Tests {"provider":"openai","key":"..."} against that provider's read-only models-list endpoint with a 10-second timeout. Returns a translated success or failure reason. Limited to six tests per minute per browser session. |
PUT /mgmt/provider-keys/{provider} | After step-up authentication, validates, encrypts, and creates or replaces one provider key from {"key":"..."}. Connectivity failures store it with stored_unverified: true; provider 401/403 responses reject it. Set save_anyway to true only to bypass validation when the gateway cannot reach the provider. |
DELETE /mgmt/provider-keys/{provider} | Deletes one stored provider key after step-up authentication. |
POST /mgmt/signout | Clears the current browser session cookie. |
POST /mgmt/signout-all | After step-up authentication, clears the cookie and invalidates every previously issued session for the user. |
POST /mgmt/totp/setup | Starts TOTP enrollment. OAuth-connected accounts require normal step-up; magic-only accounts require a sign-in from the last 10 minutes. |
POST /mgmt/totp/confirm | Confirms enrollment with a current six-digit code. |
POST /mgmt/totp/disable | Disables TOTP after verifying a current six-digit code. |
GET /mgmt/sessions | Returns the current stateless browser session's issue time and session epoch. |
GET /mgmt/usage?window=24h | Returns totals, per-model summaries, and nullable avg_ttft_ms for streaming requests in the selected window. Valid windows are 24h, 7d, and 30d. |
GET /mgmt/usage/recent?limit=20 | Returns recent requests, including nullable ttft_ms. The default limit is 20. The allowed range is 1 through 100. |
GET /mgmt/usage/export?from=YYYY-MM-DD&to=YYYY-MM-DD&format=csv | Streams the session account's usage events in the requested date range as csv or json. |
DELETE /mgmt/account | Revokes and removes gateway keys, crypto-shreds provider keys, disconnects identities, invalidates sessions, and anonymizes usage unless deletion is configured. |
Usage export
/mgmt/usage/export
Downloads only usage rows owned by the signed-in session account. This read-only GET does not require the CSRF header.
Query parameters
| Parameter | Value | Behavior |
|---|---|---|
from required | YYYY-MM-DD | First UTC calendar date included in the export. |
to required | YYYY-MM-DD | Last UTC calendar date included in the export. |
format required | csv or json | Selects an RFC4180 CSV document or a JSON array of objects. |
The inclusive from through to range may cover at most 90 calendar days. A longer range returns HTTP 400 with code usage_export_range_too_large; a from date after to also returns 400.
Output fields
CSV columns and JSON object keys use this exact order:
created_at, model, provider, key_id, prompt_tokens, completion_tokens,
cached_prompt_tokens, est_cost_usd, latency_ms, ttft_ms, usage_estimated, status, attempts
The compatibility field created_at is the request-completion time stored internally as occurred_at; the database's own created_at remains the later ingestion time. usage_estimated is true when provider usage was missing or zero and LaneZero estimated completion tokens from forwarded stream chunks. A client-cancelled stream is retained with status 499; when its final provider counters were not received, its forwarded chunks supply the estimate.
ttft_ms is elapsed time from the successful upstream streaming request start to its first content or tool-call delta. It is null (and an empty CSV field) for non-streaming requests or streams without such a delta. The summary endpoint exposes the server-computed nullable avg_ttft_ms for the selected window.
CSV output includes a header row and uses RFC4180 quoting: fields containing a comma, double quote, carriage return, or newline are enclosed in double quotes, and each embedded double quote is doubled. Records use CRLF line endings. JSON output is an array containing the same fields.
The response is streamed in bounded pages and includes Content-Disposition: attachment; filename=lanezero-usage-<from>-<to>.<ext>, where ext is csv or json.
Rate limits
Rate limits are best-effort and approximately enforced, not hard ceilings. Concurrency, soft token debits, bounded in-memory state, and multiple serving instances can permit overshoot.
Optional per-key RPM limits count authenticated /v1 requests when admission starts. The governor allows the configured requests-per-minute value as the burst and replenishes capacity continuously over one minute. Optional per-key TPM limits count the provider-reported usage.total_tokens from completed chat and embeddings requests in a rolling one-minute window with one-second bucket granularity. A null limit is unlimited.
TPM uses soft-debit semantics: a request is admitted whenever the tokens already recorded in the current window are below the limit. LaneZero does not reserve the request's possible output. After the request completes, its actual total_tokens are debited, even when that debit carries the window over the limit. Concurrent requests can therefore all be admitted below the limit and create a temporary overshoot; later requests wait for recorded tokens to roll out of the window.
Enforcement is intentionally per-instance and uses bounded in-memory state; there is no distributed or cross-instance counter. With two serving machines, effective aggregate RPM and TPM are approximately each configured per-instance limit × 2 (subject to traffic distribution). More generally, each configured value is a per-instance limit, so the effective aggregate limit is approximately per-limit × machine-count.
Response headers
Allowed and rate-limited responses include these headers whenever RPM or TPM applies. Values are decimal integers. On a 429, they describe the exceeded limit. When both limits apply to an allowed request, they describe the limit with the lowest remaining fraction; RPM wins an exact tie.
| Header | Meaning |
|---|---|
x-lz-ratelimit-limit | The configured request or token limit represented by this response. |
x-lz-ratelimit-remaining | Requests or recorded tokens remaining at admission time. Soft-debited tokens from the current request are not reflected in headers already sent. |
x-lz-ratelimit-reset | Whole seconds, rounded up, until the reported minute window resets enough to admit capacity. |
Rate-limit-exceeded response
An exhausted RPM or TPM limit returns HTTP 429 with code rate_limit_exceeded. The message names rpm or tpm; this example shows RPM.
HTTP/1.1 429 Too Many Requests
x-lz-ratelimit-limit: 60
x-lz-ratelimit-remaining: 0
x-lz-ratelimit-reset: 1
content-type: application/json
{
"error": {
"message": "gateway-key rpm rate limit exceeded",
"type": "rate_limit_error",
"code": "rate_limit_exceeded"
}
}
Change a key's minute limits
Send either or both fields. A positive integer sets the limit, null clears it, and an omitted field is unchanged. The signed management session, X-LaneZero-CSRF: 1, and step-up authentication are required.
{
"rpm_limit": 120,
"tpm_limit": null
}
Budgets
Daily and monthly budgets are best-effort, approximately enforced per-gateway-key guardrails—not hard spend limits—based on LaneZero's estimated provider cost in usage_events.est_cost_usd. Concurrent requests, delayed metering, estimation, and fail-open reconciliation can overshoot a configured amount. A null cap is unlimited. Daily windows are UTC calendar days and reset at 00:00:00Z; monthly windows are UTC calendar months and reset at the first instant of the next month.
LaneZero reconciles each budgeted key's exact stored spend on a short cache interval and adds each newly metered estimate to the cached totals between reconciliations. If the database reconciliation fails, the request is allowed and LaneZero logs a warning. This fail-open behavior favors provider availability during a database outage; a later successful reconciliation resumes enforcement.
Budget-exceeded response
An exceeded cap returns HTTP 429, error code budget_exceeded, and x-lz-budget-reset containing the same RFC3339 UTC reset timestamp named in the message. If both caps are exceeded, the daily cap is reported first.
HTTP/1.1 429 Too Many Requests
x-lz-budget-reset: 2026-08-27T00:00:00+00:00
content-type: application/json
{
"error": {
"message": "daily budget exceeded; resets at 2026-08-27T00:00:00+00:00",
"type": "rate_limit_error",
"code": "budget_exceeded"
}
}
HTTP/1.1 429 Too Many Requests
x-lz-budget-reset: 2026-09-01T00:00:00+00:00
content-type: application/json
{
"error": {
"message": "monthly budget exceeded; resets at 2026-09-01T00:00:00+00:00",
"type": "rate_limit_error",
"code": "budget_exceeded"
}
}
Change a key's budgets
The body accepts either or both fields. A number must be non-negative, null clears that cap, and an omitted field is unchanged. The signed management session, X-LaneZero-CSRF: 1, and step-up authentication are required.
{
"daily_budget_usd": 5.00,
"monthly_budget_usd": null
}
{
"id": "7b229b92-8949-4a88-a6ad-df42f6a3fb08",
"daily_budget_usd": 5.0,
"monthly_budget_usd": null
}
Error JSON
Gateway-generated errors use one envelope.
{
"error": {
"message": "model `openai/not-in-the-catalog` is not present in the catalog",
"type": "invalid_request_error",
"code": "model_not_found"
}
}
| Error type | HTTP status | Typical codes |
|---|---|---|
invalid_request_error | 400 or 404 | invalid_json, model_not_found, invalid_request, management validation and not-found codes |
authentication_error | 401 | invalid_api_key, missing_provider_key, invalid_session |
rate_limit_error | 429 | rate_limit_exceeded, budget_exceeded |
upstream_error | 502 | upstream_error, oauth_upstream_error |
timeout_error | 504 | timeout |
service_unavailable_error | 503 | auth_backend_unavailable, account_backend_unavailable, vault_unavailable, database_disabled |
not_implemented_error | 501 | provider_not_implemented |
server_error | 500 | oauth_configuration_error |
See Errors and retries for triggers and caller guidance. Malformed management JSON or query extraction can be rejected by the web framework before this envelope is built.
Legacy burst limits and spend caps
The older rate_limit_rps guard remains enforced per gateway key in addition to RPM and TPM. Its configured requests-per-second value is also its burst size. Exceeding it returns 429 with rate_limit_error.
Spend caps are advisory. They use estimated provider cost for the current calendar month. They never block a request. The current wire header is:
x-lz-spend-cap: true
x-lz-spend-cap appears after an authenticated /v1 request when the asynchronously refreshed estimate is greater than the configured cap.
Structured outputs
Ask any provider for JSON with the OpenAI-compatible response_format field. LaneZero translates it per provider, so one request shape works everywhere.
{
"model": "anthropic/claude-haiku-4-5-20251001",
"messages": [{ "role": "user", "content": "Extract the city and country from: Anfield, Liverpool, England" }],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "location",
"schema": { "type": "object", "properties": { "city": {"type": "string"}, "country": {"type": "string"} }, "required": ["city", "country"] }
}
}
}
{"type": "json_object"} is also supported for schema-free JSON. Both work with "stream": true.
Provider notes: OpenAI-wire providers receive response_format natively. DeepSeek supports json_object but not json_schema; LaneZero rejects json_schema at preflight and suggests json_object. Anthropic is served through a forced tool call and streams the JSON incrementally; caller-supplied tools cannot be combined with JSON mode there, and the schema root must be an object. Google translates your schema to its dialect; a schema keyword it cannot express returns a 400 naming the keyword and path, never a silent downgrade. A provider that cannot honor the requested mode fails fast with a 400 before any upstream call.