What it is#
POST /api/v1/inference makes one typed model call and returns the answer as one JSON body. With stream: true it returns a text/event-stream instead. You name a capability_tier label. You never name a provider, a model, a base URL or a provider key, and you never hold a provider key.
The endpoint uses the same controls as the rest of LumenFlow Cloud: a credential, limits, residency checks, a receipt for every call, and typed refusals. A refusal is never a fallback. A call never moves to a cheaper or less governed route.
Credentials#
Call the endpoint with an inference-only workspace key. Its prefix is lf_inf_ and it carries exactly one scope, inference:invoke. A primary workspace key, an org service token and an app key are refused with 403 insufficient_scope. Issue and revoke inference keys as described in Credentials and scopes.
curl -X POST https://<your-cloud-host>/api/v1/inference \
-H "Authorization: Bearer lf_inf_..." \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 7f3c2a10-5b1e-4d8a-9c2e-1a2b3c4d5e6f" \
-d '{
"capability_tier": "standard",
"messages": [
{ "role": "system", "content": "You summarise support tickets in one sentence." },
{ "role": "user", "content": "The export button stopped working after the 3 October release." }
],
"max_output_tokens": 200,
"app_id": "support-desk",
"caller_reference": "ticket-10482"
}'
Request fields#
The request body is a closed schema. Any field not listed here is refused with 400 invalid_request. A field that names a provider, model, base URL or key is listed in error.details.fields.
| Field | Required | Meaning |
|---|---|---|
capability_tier | Yes | A label of 1 to 64 letters, digits, ., _, : or -. It must match a tier your workspace has a route for. |
messages | Yes | One or more messages, up to 4096 per request. Each has role (system, user or assistant) and content. |
max_output_tokens | Yes | A positive integer. It counts, with the estimated input, against the per-call token limit. |
max_cost_usd | No | A positive USD amount with at most six fractional digits. It is a pre-call admission cap on the provider-estimated worst-case charge. It is not a promise that the final cost stays below the cap. |
tools | No | One to 64 tool definitions. See Tool calling below. |
tool_choice | No | Needs tools. One of auto, none, required, or { "name": "<declared tool>" }. |
stream | No | true returns a text/event-stream. See Streaming below. |
app_id | No | An opaque id of 1 to 200 printable ASCII characters. It enables per-app limits and attributes the receipt to the app. |
end_user_id | No | An opaque id. It is recorded on the receipt for attribution only. |
caller_reference | No | An opaque reference of 1 to 128 characters. It is echoed in the response and is not part of the idempotency hash. |
Send Idempotency-Key as a header to make a retry safe. The same key with the same payload returns the stored result with the header Idempotent-Replayed: true and makes no second model call. The same key with a different payload returns 409 idempotency_conflict. A call that is still running returns 409 idempotency_in_progress. A stored result is kept for the retention limit, which is 24 hours by default, and is then cleared.
Capability tiers and routes#
A tier label is resolved to a route before any provider call. The route is the workspace's own model registration that declares that tier. A workspace with no registration of its own for a tier can use a managed route for that tier where the platform offers one. Whether managed routes are enabled for a given workspace is not visible from this documentation. Check the routes listing (below) for the tiers your key can reach.
Messages, images and cache hints#
Image parts are accepted in user messages. Content is either a string or an array of parts. A part is { "type": "text", "text": "..." } or { "type": "image", "media_type": "image/png", "data": "<base64>" }.
- Media types are
image/png,image/jpegandimage/webp. datais canonical base64 with no line breaks.- A request carries at most 8 images. Each image decodes to at most 5 MiB and each side is at most 4096 pixels. The dimensions are read from the image header, not from the request.
- A message carries at most 64 parts.
Cache hints let a long, repeated prefix, such as a system prompt and a tool set, be cached by the route when the route supports it. Mark a message or a tool definition with "cache": "ephemeral". The marker covers everything before it. A request carries at most 4 markers across messages and tools together. More than 4 is refused with 400 invalid_request.
A route that cannot cache accepts the markers, ignores them and serves the call normally. When the request has a marker, the response includes cache_hint_applied (true or false). Without markers the field is absent. Usage reports cached_input_tokens and cache_write_tokens, which are null when the route reports neither.
Tool calling#
The endpoint carries the model side of an agent loop. Cloud never runs a tool.
- Declare.
toolsis an array of{ "name", "description", "input_schema" }. A name is 1 to 64 letters, digits,_or-, starts with a letter, and is unique.input_schemais a JSON Schema object with"type": "object". - Respond. When the model asks for tools, the
200response carriestool_calls(each{ id, name, arguments }) andfinish_reason: "tool_calls".textmay be empty. Run the tool in your own code. - Send the result. Send the next request with the assistant message's
tool_callsand atoolmessage per call:{ "role": "tool", "tool_call_id": "...", "content": "...", "is_error": false }. Every result must answer exactly one earlier call. An orphan, a duplicate or a result that comes before its call is refused with400 invalid_request. - Invalid calls. A model call with an unknown name, unparseable arguments or a bad id is never returned as a tool call. It is counted in
invalid_tool_call_count. If no valid call remains the response hasfinish_reason: "tool_calls_invalid"and notool_calls. - Bounds.
input_schemaandargumentsare limited to 32 levels of nesting, 2,000 keys and 1,000 enum entries.
Tool names, schemas, arguments and results are never written to a receipt. The receipt records only counts.
Streaming#
With stream: true the response is text/event-stream (Content-Type: text/event-stream, Cache-Control: no-store, no-transform). It is the same governed call as a single response: the same refusal order, one model call, one hold, one receipt and one settlement.
- Before the first byte. Every refusal that is known before the response opens is an ordinary JSON error with its status code.
- Frames. Each frame is
event: <name>, onedata: <json>line and a blank line. The events are:deltawith{ "text": "..." }, zero or more times.tool_callwith{ "id", "name", "arguments" }, one whole assembled call. Partial argument text is never sent.- exactly one terminal event, either
usage(the call completed, carrying the response fields other than the content, includingreceipt) orerror(the call failed after the first byte, withcode,messageandrequest_id).
- Heartbeat. While the model is silent for 15 seconds the stream sends a comment line
: keepalive. Ignore it. - Idle and total limits. A stream ends with an
errorevent if it runs longer than the stream duration limit or sits silent past the idle limit. Output of any kind, including a tool argument being written, resets the idle clock. The routes listing reports both limits for your key. - Disconnect. If your client closes the connection, the upstream call stops and is charged for the output produced, estimated from the text sent. The receipt is marked
stream.termination: "client_closed"andstream.usage_estimated: true. - Replay. A retry with the same
Idempotency-Keyafter a stream has ended replays the stored result, with no model call and no charge. A retry while the first stream is still running returns409 idempotency_in_progress.
Response#
A single 200 response carries:
textandfinish_reason.usagewithinput_tokens,output_tokens,total_tokens,cached_input_tokensandcache_write_tokens.routewithcapability_tier,provider_kindandprovider_family.provider_kindis a label for the kind of route: a hosted route, or a customer-addressed endpoint. No provider or model name is returned.costwithamount(a six-decimal string),currency(USD) andbasis.basisismanagedfor a wallet-debited call,byok_declaredorbyok_estimatefor a call on your own route where the provider bills you, ornullwhen the route has no known price.receiptwithid,recordedandsigned.caller_reference, when you sent one.
A receipt is written for every admitted, refused or replayed call. receipt.recorded is false when a call on your own route went ahead but its receipt could not be written. Such a gap is flagged, not hidden. A call whose cost is debited from a managed wallet is refused with 503 receipt_unavailable instead, and nothing is charged. receipt.signed is true only when the stored signature is present.
Typed refusals#
Every refusal has the body { "request_id", "error": { "code", "message", "details?" } }.
| Code | Status | Meaning |
|---|---|---|
invalid_request | 400 | A schema violation, a provider-shaped field, or a bad Idempotency-Key. |
insufficient_scope | 403 | The key is not an inference-only key. |
managed_not_enabled | 403 | No registration of your own for the tier, and managed routes are not available to this workspace. |
residency_violation | 403 | The workspace is pinned to a region and the route is in another region, or the pin cannot be checked. |
no_route | 422 | No registration declares the requested tier. |
routing_policy_refused | 422 | The routing policy refused the route. |
insufficient_credit | 402 | The inference wallet cannot cover the call's hold. Refused before any model call. |
limit_exceeded | 429 | A per-key, per-app or per-workspace limit was reached. details.limit and details.scope say which. Retry-After is set. |
provider_rate_limited | 429 | The upstream provider returned HTTP 429 before any output. Retry-After is set only when a valid delay was sent. |
idempotency_conflict | 409 | The same key was used with a different payload, or the stored result has expired. |
idempotency_in_progress | 409 | The first call with this key is still running. |
provider_error | 502 | The provider call failed. The body does not echo provider detail. |
platform_ceiling_reached | 503 | The service is at capacity for this route. Retry later. |
route_unavailable | 503 | The route could not be read or prepared. |
receipt_unavailable | 503 | A wallet-debited call whose receipt could not be written. The result is withheld and nothing is charged. |
internal_error | 500 | An unexpected failure. No detail is returned. |
Each provider call makes one generation attempt. The SDK does not retry it, and a provider failure never triggers another route.
Routes listing#
GET /api/v1/inference/routes tells a client what it can reach before it calls. Authenticate with an inference key, or with a primary workspace key holding read. An app key and an org service token are refused.
The reply has three parts:
tiers: one entry per capability tier withcapability_tier,reachable,source_kind,priced,input_modalitiesandmax_output_tokens.limits: the effective limits for the calling key:tokens_per_call,tokens_per_day,spend_usd_per_day,requests_per_minute,concurrent_calls,stream_max_duration_secondsandstream_idle_timeout_seconds.ceilings: the most the key or its workspace may set the two stream limits to.
It never returns a provider, a model, an endpoint, a region or a credential. Errors are 503 route_unavailable with a retry message.
Keys and limits#
Inference keys belong to a workspace. A workspace owner or admin session can manage them. A primary workspace key with the write scope can also mint and revoke keys, but only for its own workspace and only the keys it minted.
| Action | Route | Who can call it |
|---|---|---|
| Mint a key | POST /api/v1/workspaces/{workspaceId}/inference-keys with { "name", "scopes"? } | Owner or admin session, or the primary key with write. |
| List keys | GET /api/v1/workspaces/{workspaceId}/inference-keys | Owner or admin session. Returns display metadata only, never a key or a hash. |
| Revoke a key | DELETE /api/v1/workspaces/{workspaceId}/inference-keys/{keyId} | Owner or admin session, or the primary key for the keys it minted. |
| Read a key's limits | GET /api/v1/workspaces/{workspaceId}/inference-keys/{keyId}/limits | read or an owner or admin session. |
| Set a key's limits | PUT /api/v1/workspaces/{workspaceId}/inference-keys/{keyId}/limits | limits:write, or an owner or admin session. |
The plaintext key is returned once, at mint, with a notice that it cannot be retrieved again. Only its hash is stored. A primary workspace key that mints is refused with 409 inference_key_cap_reached when the workspace already holds its limit of active keys. The reply names the limit.
Limits resolve in order: the key, then the workspace, then the organisation, then the tier and the platform. Each value comes with its source and its ceiling.
| Action | Route |
|---|---|
| Workspace limits | GET and PUT /api/v1/workspaces/{workspaceId}/inference-limits |
| Per-subject budgets | GET, PUT and DELETE /api/v1/workspaces/{workspaceId}/inference-limits/subjects/{subject} |
A PUT body is { "limits": { "<key>": <value> }, "reason": "..." }. The reason is up to 500 characters. A value above its ceiling is clamped, and the reply lists every clamp. Each change is audited with the actor and the old and new values.
The self-service limit keys are inference.tokens_per_call, inference.message_chars, inference.request_body_bytes, inference.tokens_per_day, inference.spend_usd_per_day, inference.stream_max_duration_seconds, inference.stream_idle_timeout_seconds and inference.max_output_tokens. Other limits are set by the platform operator.
A per-subject budget applies to one consumer-asserted subject. It limits inference.tenant.tokens_per_day and inference.tenant.spend_usd_per_day for that subject. The subject is never used to decide who may call the endpoint. A subject with no budget of its own reads its source as default.
Receipts and cost#
Every call writes one receipt. A receipt holds the calling key's id (never the key), the resolved labels, token counts, latency, the gate outcome (permit, refuse or replay), the refusal code if there was one, the routing decision, app_id and end_user_id when you sent them, and a cost block for admitted calls. Your prompt, your completion, your credentials and any provider key are never written to a receipt, a log or an error body.
Cost is reported per call on the response (cost). The receipts are the record of spend. Spend is grouped by day or by caller_reference through the costs endpoint.