Source: https://aircodezero.com/docs/ai-gateway

# AI gateway

Use the AI accounts connected to your own AirCode Ø instance from your personal
applications. Supply your instance's **Base URL**, a separate **application
token**, and an exact **model ID**. The instance invokes its connected provider
CLI and returns the answer through a text-only Chat Completions API.

```text
Your application's backend
  → your instance URL + application token
    → a connected native provider account
      → validated answer + available usage counters
```

The website hosts documentation only. API requests go directly to your instance;
aircodezero.com does not receive your instance address, token or prompts.
Use this service only for your own applications and accounts. Provider terms,
subscription access and quotas still apply.

## Set up the instance

1. Connect a compatible native provider account in **AI Usage**. The gateway
   lists the account models supported by the installed instance; API-key and
   managed-credit connections are excluded.
2. Open **Settings → AI gateway** on desktop/web, or **More → AI gateway** on
   native mobile, then turn on **Enable AI gateway**.
3. Copy the displayed **Base URL**. It ends in `/api/ai-gateway/v1`.
4. In **Application tokens**, enter your application's name, select **Create
   token**, and save the token before dismissing its one-time reveal.
5. Configure these server-side environment variables in your application:

| Variable | Value |
| --- | --- |
| `AIRCODE_AI_BASE_URL` | The full Base URL copied from the instance, without an extra `/v1` |
| `AIRCODE_AI_TOKEN` | The application token you just created |
| `AIRCODE_AI_MODEL` | An exact `id` from **Compatible models** or `GET /models` |

Keep tokens in your backend's secret configuration, not in browser code, a
mobile app bundle, a repository, a URL or a shared conversation. The examples
read environment variables instead of embedding credentials. Never use the
instance password or an owner login token in the API-key field.

The instance must remain running and reachable from your application's backend.
`localhost` means the computer running that backend. Use the instance's HTTPS
address for remote access, or your trusted local/private-network connection.
The gateway does not create a public origin. A provider not listed by `/models`
cannot be selected by inventing an ID.

## Authentication and model discovery

Send `Authorization: Bearer YOUR_APPLICATION_TOKEN` on each call. Send JSON with
`Content-Type: application/json`. No query parameters or cookie authentication
are supported. Cross-origin browser calls are not supported; use your backend.

`GET {BASE_URL}/models` returns an object with `object: "list"` and a `data`
array. Each model provides an `id`, display `label`, `engine`, `engineLabel`,
`provider`, `connectionLabel` and `efforts`. Pass the returned `id` unchanged;
it may already contain percent encoding. The list can change when accounts,
provider access or installed models change.

For an OpenAI-compatible application's configuration, select **Chat
Completions**, use the full Base URL, put the application token in its **API
key** field, and copy one of the returned model IDs. An application that
requires Responses, tools, images or unsupported sampling parameters is not
compatible with the current gateway.

## Generate a response

`POST {BASE_URL}/chat/completions` accepts the following fields. Unknown fields
are refused rather than silently ignored.

| Field | Supported value |
| --- | --- |
| `model` | Required exact model ID returned by `/models` |
| `messages` | Required array of 1–100 messages; at least one non-empty user message |
| `messages[].role` | `system`, `developer`, `user` or `assistant` |
| `messages[].content` | A string, or an array of `{ "type": "text", "text": "..." }` blocks |
| `reasoning_effort` | Optional value from the selected model's `efforts`; omit it for the default |
| `stream` | Optional boolean; defaults to `false` |
| `stream_options` | Only `{ "include_usage": true }` or `false` for that flag, with `stream: true` |
| `n` | Omit it or use `1` |
| `store` | Omit it or use `false` |

Messages contain only `role` and `content`. Text blocks are joined with newlines.
Null characters are refused. The normalized message array must fit within
64 KiB of UTF-8 JSON; the full HTTP body is limited to 80 KiB. The validated
answer is limited to 49,152 JavaScript string code units. These are gateway
limits, not promises about a provider's context window or output capacity.

Supply the complete relevant conversation on every call. To continue a
conversation, append the previous assistant answer and the next user message.
The gateway does not create or retain a conversation, session or completion
history. A token grants no access to instance projects, files, terminal,
sessions or owner settings.

A normal response has `object: "chat.completion"`; read the text from
`choices[0].message.content`. It also includes `id`, `created`, the requested
`model`, and `finish_reason: "stop"`. Native counters appear in `usage` when
complete enough to report. The `aircode_usage` extension preserves accounting
details and incomplete estimates. Missing counters or prices are unknown,
never zero. API-equivalent estimates are not extra subscription charges.

## Streaming

Set `stream: true` to receive `Content-Type: text/event-stream`. The current
implementation uses **buffered streaming**: it sends an initial waiting comment
and keepalive comments while the provider runs, then sends the complete answer
after validation. It does **not** stream words or tokens as they are generated.

A successful stream contains:

1. SSE comments beginning with `:` while waiting; ignore them.
2. A `data:` JSON `chat.completion.chunk` with the assistant role and complete
   text in `choices[0].delta.content`.
3. A chunk with an empty delta and `finish_reason: "stop"`.
4. An optional chunk with `choices: []`, `usage` (possibly `null`) and
   `aircode_usage` when `stream_options.include_usage` is true.
5. `data: [DONE]`.

After a stream starts, failures arrive as an SSE `data:` event containing
`error`; do not treat HTTP 200 alone as success. A failed stream closes without
the success marker. SSE frames may be split across transport chunks; use an
SDK or an SSE parser rather than parsing each network chunk as JSON.

Closing a connection cancels its active request. Revoking a token, disabling
the gateway or stopping the instance also cancels matching work. There is no
automatic replay or resume. Long-running requests need an appropriate client
and reverse-proxy timeout; keepalives cannot override a network's absolute
request-duration limit.

## Quotas, token lifecycle and errors

The gateway allows two active calls instance-wide, one active call per token,
and 30 admitted generation requests per token per minute. Calls are not queued.
These local limits do not replace provider quotas. Tokens expire after 90 days;
create a replacement, update your application, then revoke the previous token.
Disabling the gateway retains tokens but suspends their use. Restoring instance
state disables the gateway and clears token authority; create fresh tokens.

Errors use `error.message`, `error.type`, `error.code` and `error.param`. Read the
code as well as the HTTP status. Do not automatically retry a generation whose
outcome is unknown: it may already have consumed provider quota. The examples
disable SDK retries.

| Status/code | What to do |
| --- | --- |
| `401`, `AI_GATEWAY_AUTH_REQUIRED` | Check the token, expiry, revocation and target instance |
| `403`, `AI_GATEWAY_DISABLED` | Enable the gateway on the target instance |
| `403`, `AI_GATEWAY_ORIGIN_FORBIDDEN` | Call from your backend, not a foreign browser origin |
| `AI_GATEWAY_ACCESS_REFUSED` | Check the instance's access/subscription requirements |
| `400`, unsupported parameter or effort | Remove unsupported fields; use the selected model's advertised efforts |
| `404`/`409`, `AI_GATEWAY_MODEL_UNAVAILABLE` | Refresh `/models` and explicitly select an available ID |
| `413`, `AI_GATEWAY_INPUT_TOO_LARGE` | Reduce the supplied history; an oversized HTTP body is also refused |
| `429`, `AI_GATEWAY_BUSY` | Wait for the active call to finish before trying again |
| `429`, `AI_GATEWAY_RATE_LIMIT` | Stop immediate retries and wait for the local minute window |
| `SAFE_STRUCTURED_AI_QUOTA_EXHAUSTED` | Wait for provider quota recovery; `error.resets_at` may be `null` |
| `409`, cancellation | Treat the call as cancelled; do not resume automatically |
| `502`/`503`, provider/unavailable error | Check the account connection and instance health before an explicit retry |

An unknown provider reset time stays unknown. Never derive one from a model
name or an unrelated provider's quota schedule.

## Using another instance from the application

An external application can call another **own** instance with that instance's
Base URL and a token created there. Each token belongs to its issuing instance.
For interactive coding sessions, create a token with **Allow remote sessions**
and follow [Remote sessions](/docs/remote-sessions). Save that connection in
Settings, then choose it under **New session → Run on**. Agents work on the
remote instance's authorized project and send live transcript/terminal events.
Ordinary tokens remain text-only. Neither token signs the client in as the
remote owner; Responses, Anthropic Messages and Chat Completions tool calling
remain outside the text API's supported subset.

For AI-assisted integration, see [Documentation for AI](/docs/ai-integrations),
the [raw Markdown guide](/docs/ai-gateway.md), the
[OpenAPI schema](/docs/ai-gateway/openapi.json), and the
[integration skill](/skills/aircode-ai-gateway/SKILL.md).

## Code examples

Set AIRCODE_AI_BASE_URL to the base URL above, AIRCODE_AI_TOKEN to your application token, and AIRCODE_AI_MODEL to an ID copied from Compatible models. Run these examples in your application’s backend or terminal.

### cURL

Bash, curl and jq. List models first, then use the exact model ID you selected.

```bash
set -euo pipefail
# The token is read from standard input, not a command-line argument.
curl --fail-with-body --silent --show-error --location --max-redirs 0 \
  "$AIRCODE_AI_BASE_URL/models" --header @- <<EOF
Authorization: Bearer $AIRCODE_AI_TOKEN
EOF

body=$(jq -n --arg model "$AIRCODE_AI_MODEL" \
  '{model: $model, messages: [{role: "user", content: "Hello!"}]}')
curl --fail-with-body --silent --show-error --location --max-redirs 0 \
  "$AIRCODE_AI_BASE_URL/chat/completions" \
  --header 'Content-Type: application/json' \
  --header @- --data "$body" <<EOF
Authorization: Bearer $AIRCODE_AI_TOKEN
EOF
```

### Python

Install the OpenAI Python SDK 3 with python -m pip install "openai>=3,<4".

```python
import os
from openai import OpenAI, DefaultHttpx2Client

client = OpenAI(
    base_url=os.environ["AIRCODE_AI_BASE_URL"],
    api_key=os.environ["AIRCODE_AI_TOKEN"],
    max_retries=0,
    http_client=DefaultHttpx2Client(follow_redirects=False),
)

for model in client.models.list():
    print(model.id)

response = client.chat.completions.create(
    model=os.environ["AIRCODE_AI_MODEL"],
    messages=[{"role": "user", "content": "Hello!"}],
)
print(response.choices[0].message.content)
```

### JavaScript streaming

Node.js and the OpenAI SDK: npm install openai. This is buffered SSE: the complete validated answer arrives at the end.

```javascript
import OpenAI from "openai";

function required(name) {
  if (!process.env[name]) throw new Error("Set " + name + " first.");
  return process.env[name];
}

const client = new OpenAI({
  baseURL: required("AIRCODE_AI_BASE_URL"),
  apiKey: required("AIRCODE_AI_TOKEN"),
  maxRetries: 0,
  fetchOptions: { redirect: "error" },
});

const stream = await client.chat.completions.create({
  model: required("AIRCODE_AI_MODEL"),
  messages: [{ role: "user", content: "Hello!" }],
  stream: true,
  stream_options: { include_usage: true },
});

try {
  for await (const chunk of stream) {
    process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
    if (chunk.usage) console.error("Tokens:", chunk.usage.total_tokens);
  }
  process.stdout.write("\n");
} finally {
  stream.controller.abort();
}
```

