AI API — /v1/ai
A metered, escrowed passthrough to a chat-completion provider (OpenRouter). The app holds no model key; the end user pays their own token cost, escrowed per request so spend can never exceed the caller's balance or the app's cap.
POST /v1/ai/chat#
OpenAI-compatible chat completions. Requires authentication.
Body (a standard chat-completions payload). Only model, messages, and
max_tokens are inspected — everything else (temperature, tools, response_format,
provider routing, reasoning, prompt caching via cache_control, …) is forwarded to
OpenRouter untouched. The gateway is a metering layer on top of OpenRouter's full
surface, not a restricted subset of it.
{
"model": "openai/gpt-5.5",
"messages": [{ "role": "user", "content": "Hello" }],
"max_tokens": 1024,
"stream": false
}| Field | Notes |
|---|---|
model | Required. Provider model id. See GET /v1/ai/models for the catalog. |
messages | Required, non-empty. Serialized prompt is capped at 2,000,000 chars (prompt_too_large). |
max_tokens | Optional; defaults to 4096. Clamped to the model's own max output when known (unknown models pass through unclamped — the escrow hold, not this clamp, bounds spend). |
stream | Optional. true streams SSE. |
The gateway forces usage accounting on, so the response always carries a cost to settle against.
GET /v1/ai/models#
Lists the OpenRouter model catalog the gateway prices against — the same table used to compute escrow holds, refreshed hourly. Unmetered read.
{
"models": [
{
"id": "openai/gpt-4o",
"prompt_micro_usd_per_token": 3,
"completion_micro_usd_per_token": 15,
"max_completion_tokens": 16384,
"context_length": 128000
}
]
}max_completion_tokens / context_length are null for a model OpenRouter doesn't
report limits for. 503 if the catalog has never been fetched successfully.
Response#
- Non-streaming → the provider's JSON completion, passed through unchanged
(
{ choices: [...], usage: {...}, … }). - Streaming (
stream: true) →text/event-stream; SSE bytes are forwarded untouched while the gateway scans for the final usage chunk to settle against.
Billing#
- Before calling upstream, the gateway places an escrow hold bounding the cost
(prompt size +
max_tokensat the model's rate). If the wallet or the app's cap can't cover it, the request fails 402 (insufficient_balance/spend_cap_exceeded) with no spend. - After completion it captures the actual cost (provider-reported cost → reported token counts → the hold, in that order). A stream that dies before usage arrives is settled from observed characters, capped at the hold.
- If upstream fails, the hold is released — you owe nothing (
502 upstream error). - Streaming only — a well-behaved upstream can't exceed the hold (it covers
max_tokenswith margin), but if a provider ignoresmax_tokensand keeps emitting, the gateway tracks accrued cost per chunk and cuts the stream once it would exceed the hold. The client sees a final SSE data event{"error":"cost_limit_reached"}before[DONE]; settlement is capped at the hold either way, so the wallet can never be overdrawn.
Errors#
| Status | error | Meaning |
|---|---|---|
| 400 | invalid_request / prompt_too_large | Bad body, empty messages, oversized prompt. |
| 402 | insufficient_balance / spend_cap_exceeded | Can't cover the hold. |
| 502 | upstream error | Provider failed; no charge. |
| 503 | OPENROUTER_API_KEY … not configured | Server missing the provider key. |
SDK#
const res = await osgarden.ai.chat({
model: "openai/gpt-4o",
messages: [{ role: "user", content: "Hello" }],
});
const reply = res.choices?.[0]?.message?.content;
const { models } = await osgarden.ai.models(); // enumerate ids, rates, output limits