# Request Logs Source: https://aiand-docs-staging.pages.dev/analytics/logs/ ```http GET /logs?range=&after=&after_id=&errors=&limit= ``` Returns the full per-request log for your organization, ordered newest-first. You can also retrieve and inspect the same logs in the [console](https://console.aiand.com/analytics?tab=logs) under **Analytics → Logs**. Use the console for interactive troubleshooting and the API for programmatic retrieval or export. From a terminal, the [ai& CLI](/cli/) reads the same logs: `aiand logs --range 1h --errors` shows recent failures, and `aiand logs --follow` tails new requests. ## Query parameters | Parameter | Type | Required | Description | | ---------------- | ------- | -------- | ----------------------------------------------------------------------------------------------------------- | | `range` | string | No | `15m`, `1h`, `6h`, `24h`, `7days`, `30days`. Default: `24h`. | | `after` | string | No | Pagination cursor — `next_after` from the previous page. Requires `after_id`. | | `after_id` | string | No | Pagination cursor — `next_after_id` from the previous page. Requires `after`. | | `errors` | boolean | No | If `true`, only non-2xx requests. | | `endpoint` | string | No | Only requests to this `/v1` path, e.g. `/v1/responses`. | | `content_logged` | boolean | No | If `true`, only requests where capture was attempted (see below). The page is then ordered by `request_at`. | | `limit` | integer | No | Page size. Capped at 100. Default: 50. | ## Response ```json { "data": [ { "id": "...", "model": "...", "api_key": "sk-...abcd", "status_code": 200, "ttft_ms": 210, "latency_ms": 420, "input_tokens": 123, "output_tokens": 45, "cached_tokens": 64, "cost": "0.00420000", "currency": "usd", "created_at": "2026-05-18 12:34:56.123456+00", "request_at": "2026-05-18 12:34:55.901234+00", "endpoint": "/v1/chat/completions", "content_logged": true } ], "has_more": true, "next_after": "2026-05-18T12:34:55.654321Z", "next_after_id": "..." } ``` `cost` is denominated in `currency` — the org's billing currency (`usd` or `jpy`); it is never a bare USD figure. When `has_more` is `false`, you've reached the end of the available range. `ttft_ms` is the time to first token: milliseconds from receiving a streamed request to the first generated output you receive, whether text, reasoning or a tool call. A chunk that carries only metadata, such as the opening role announcement, does not count, and neither does output the endpoint does not send you, such as reasoning on `/v1/messages` without `thinking`. It is `null` for a non-streamed request and for a stream that ended before any output. `cached_tokens` is the portion of `input_tokens` billed at the discounted [cached input rate](/models/pricing/#prompt-caching). It is `0` when there was no cache hit and `null` on models without a cached rate. `request_at` is when the request was received; `created_at` is when the row was settled, a moment later. Rows are ordered and paged by `created_at`, or by `request_at` when `content_logged=true`; `next_after` always carries the matching value, so pass it back unchanged. `endpoint` is the `/v1` path, and `content_logged` says whether capture was attempted for this request — not that storage necessarily succeeded. A rare miss surfaces as `unavailable` in the detail response, never as `content_logged: false` (see [Content logging](#content-logging)). ## Pagination Pass `next_after` and `next_after_id` from the previous page as `after` and `after_id`, and stop when `has_more` is `false`. Send both: `after_id` breaks ties between rows that share a timestamp, so either one alone is rejected with `400 invalid_request_error` naming the missing parameter. `next_after` is an opaque cursor, not an RFC 3339 timestamp: pass it back unchanged and don't parse it. ## Auth API key or JWT + `X-Org-ID`. See [Authentication](/authentication/). ## Retrieve one request (with content) ```http GET /logs/{id} ``` Returns one request's metadata plus its captured **content** (the request body and the response), when content logging is enabled for your organization (see [Content logging](#content-logging)). Use the `id` from the list, or the `X-Request-ID` header returned on every `/v1` response. ```json { "id": "...", "state": "ready", "metadata": { "model": "...", "endpoint": "/v1/chat/completions", "api_key": "sk-...abcd", "status_code": 200, "input_tokens": 123, "output_tokens": 45, "cost": "0.00420000", "currency": "usd", "request_at": "2026-05-18 12:34:56.123456+00", "created_at": "2026-05-18 12:34:58.000000+00" }, "content": { "endpoint": "/v1/chat/completions", "streamed": false, "capturedAt": "2026-05-18T12:34:56.123Z", "responseId": "chatcmpl_...", "request": "{ ...the request you sent... }", "response": "{ ...the response... }", "error": null, "truncated": false } } ``` `request` and `response` are stored as **text** — a JSON string for a non-streamed response, and the captured SSE event stream for a streamed one — so you parse them client-side. The stored values retain the endpoint's native shape (`/v1/messages` uses the Anthropic shape, for example), but they are processed for safe storage and are not guaranteed to be byte-for-byte identical to the original wire content. Inline-media placeholders and size truncation are described below. For streamed responses, inline-media replacement is applied only to JSON payloads in the stored copy's `data:` events. The SSE framing and non-media events remain parseable in their original form, and the live stream delivered to your client is never modified by request logging. As a general rule, each captured log record is limited to **10 MiB (approximately 10 MB)** across the request, response, and envelope metadata. Content beyond that limit is truncated rather than rejected: `truncated` is `true`, and the stored text includes a truncation marker. Inline base64 media is replaced with an `omitted:;bytes=` placeholder before the size limit is applied. For a failed request, `response` is the error body you received and `error` carries its `message` plus `status` — the upstream status when there was one, otherwise the status you received. The upstream body itself is never stored. Client-supplied request ids must match `[A-Za-z0-9._:@=+-]{1,36}`. If your `X-Request-ID` is outside that shape, the service replaces it with a generated id; use the `X-Request-ID` returned in the response to retrieve the metadata and content. ### `state` `content` may be absent; `state` says why, so the response is never an error unless the id is unknown: | State | Meaning | | ------------- | ------------------------------------------------------- | | `ready` | Metadata and content are both present. | | `processing` | The request just completed; metadata is still settling. | | `not_logged` | Content logging was off for this request. | | `expired` | Older than 30 days — the content has been deleted. | | `unavailable` | Content was expected but is missing. | A `404` is returned only when neither metadata nor content exists. ## Export stored content ```http GET /logs/export ``` Organization admins only. Downloads the stored request/response bodies of the logged requests in a window as one gzip-compressed [JSON Lines](https://jsonlines.org/) file, `request-logs.jsonl.gz`, newest first: one line per request, each in the shape of `content` in [Retrieve one request](#retrieve-one-request-with-content). It takes the list's `range`, `errors` and `endpoint` parameters, with the same `24h` default. One file holds at most the newest 5,000 requests in the window. For older ones, page through the list with `content_logged=true` (see [Pagination](#pagination)) and retrieve each request. A request whose content has already been deleted is left out of the file. The console offers this export when an admin switches the organization to [zero data retention](/organizations/zero-data-retention/). ## Delete stored content ```http DELETE /logs ``` Organization admins only. Starts deleting every stored request/response body for your organization and returns `202 { "accepted": true }`; the purge finishes in the background within a few minutes. Metadata rows are kept, and affected requests then open as `unavailable`. Deleting does not change the logging switch: while logging is on, requests made after the purge are captured again. Turning logging off first is not a clean cut either, since capture can continue for up to about a minute after the switch (see [Content logging](#content-logging)). ## Content logging Capturing request/response bodies is **opt-in and off by default**. Metadata (the list above) is always recorded; request logs store **content** (the actual prompts and responses) only while content logging is on. Other features store content of their own whether or not it is on, such as inline PDFs and video jobs; [Zero data retention](/organizations/zero-data-retention/) lists them. ```http GET /logs/settings PATCH /logs/settings { "log_content_enabled": true } ``` `GET` returns `{ "log_content_enabled": , "available": , "data_retention": "standard" | "zero" }` and reflects a `PATCH` immediately. When `available` is `false`, hide the content-capture control; deletion remains available. `PATCH` (organization admins only) toggles logging when available. An organization on [zero data retention](/organizations/zero-data-retention/) reports `"data_retention": "zero"`, and where capture is available a `PATCH` that switches logging on returns `403 zero_data_retention`. Organization admins can also turn content logging on or off in the [console](https://console.aiand.com/settings/organizations) under **Settings → Organizations → Request Logging**. The switch is shown only when content capture is available in the current environment; when it is unavailable, the console still allows admins to delete previously stored content. --- # Metrics Source: https://aiand-docs-staging.pages.dev/analytics/metrics/ ```http GET /analytics/metrics?range= ``` Returns time-bucketed metrics for the calling organization. Useful for charts, dashboards, and capacity planning. ## Query parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `range` | string | Yes | One of `1h`, `24h`, `7days`, `30days`, `3months`. The server picks an appropriate bucket size. | ## Response ```json { "range": "24h", "buckets": [ { "ts": "2026-05-18T00:00:00Z", "requests": 1234, "input_tokens": 567890, "output_tokens": 12345, "cost_usd": 1.23, "errors": 5, "p50_latency_ms": 420, "p95_latency_ms": 1850 } ] } ``` ## Caching Responses are cached for **120 seconds** with singleflight deduplication — repeated calls within that window hit the cache and don't re-query the analytics backend. ## Auth API key or JWT + `X-Org-ID`. See [Authentication](/authentication/). --- # Summary Source: https://aiand-docs-staging.pages.dev/analytics/summary/ ```http GET /analytics/summary?range= ``` Returns request counts and input and output token totals for the requested range and for an earlier window, plus a token timeseries for the current range. Use this for "headline number" displays. ## Query parameters | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `range` | string | Yes | One of `1h`, `24h`, `7days`, `30days`, `3months`. | ## Response ```json { "current": { "requests": 87432, "input_tokens": 12345678, "output_tokens": 234567 }, "previous": { "requests": 80110, "input_tokens": 11023456, "output_tokens": 198765 }, "timeseries": [ { "timestamp": "2026-05-11T00:00:00", "tokens": 1804321 }, { "timestamp": "2026-05-11T01:00:00", "tokens": 1756789 } ] } ``` ## Caching Responses are cached for **120 seconds** with singleflight deduplication. ## Auth API key or JWT + `X-Org-ID`. See [Authentication](/authentication/). --- # Chat Completions Source: https://aiand-docs-staging.pages.dev/api/chat-completions/ ```http POST /v1/chat/completions ``` Generate a model response for a given conversation. Fully compatible with the [OpenAI Chat Completions API](https://platform.openai.com/docs/api-reference/chat/create). ## Request body | Parameter | Type | Required | Description | | ----------------------- | ------------------ | -------- | ---------------------------------------------------------------------------------------------------------------------------- | | `model` | string | Yes | Model ID (see [Models](/api/models/)) | | `messages` | array | Yes | Conversation messages (see [Message types](#message-types)) | | `stream` | boolean | No | Stream partial deltas as SSE. Default: `false` | | `stream_options` | object | No | `{ "include_usage": true }` to include token counts in the final stream event | | `temperature` | number | No | Sampling temperature, 0–2. Default: model-dependent | | `top_p` | number | No | Nucleus sampling threshold, greater than 0 and at most 1 | | `n` | integer | No | Number of choices to generate, 1–128 | | `max_tokens` | integer | No | Maximum tokens to generate (deprecated — use `max_completion_tokens`) | | `max_completion_tokens` | integer | No | Upper bound on generated tokens, including reasoning tokens | | `stop` | string \| string[] | No | Up to 4 stop sequences | | `frequency_penalty` | number | No | Frequency penalty, -2 to 2 | | `presence_penalty` | number | No | Presence penalty, -2 to 2 | | `logprobs` | boolean | No | Return log probabilities of output tokens | | `top_logprobs` | integer | No | Most likely tokens per position, 0–20 | | `response_format` | object | No | `{ "type": "text" }`, `{ "type": "json_object" }`, or `{ "type": "json_schema", "json_schema": {...} }` | | `seed` | integer | No | Seed for deterministic sampling | | `tools` | array | No | Function tools the model may call | | `tool_choice` | string \| object | No | `"none"`, `"auto"`, `"required"`, or a specific tool | | `parallel_tool_calls` | boolean | No | Allow parallel function calling | | `reasoning_effort` | string | No | Per-model — read `reasoning_efforts` from [`GET /v1/models`](/models/catalog/). A level the model doesn't list runs at the next listed level up, or the highest, which can cost more than the level you sent, and `X-Reasoning-Effort` reports it; a value outside the effort vocabulary is rejected | | `top_k` | integer | No | Top-k sampling (provider-specific) | | `repetition_penalty` | number | No | Repetition penalty (provider-specific) | | `user` | string | No | End-user identifier for abuse tracking | A model can turn reasoning off only if its levels include `none`. Sending `reasoning_effort` `none` to a model that does not publish it is a 400. The error text is on [Errors](/errors/#turning-reasoning-off). ### Message types #### System message ```json { "role": "system", "content": "You are a helpful assistant." } ``` #### User message ```json { "role": "user", "content": "What is the capital of France?" } ``` User messages also accept multimodal content arrays: ```json { "role": "user", "content": [ { "type": "text", "text": "What's in this image?" }, { "type": "image_url", "image_url": { "url": "https://...", "detail": "auto" } } ] } ``` Supported content types: `text`, `image_url`, `video_url`, `audio_url`, `input_audio`, `file`. URLs in `image_url`, `video_url`, `audio_url` and `file.file_url` must meet the [URL requirements](/capabilities/vision/#url-requirements). For images, prefer uploading via the [Files API](/api/files/) and referencing by `file_id` rather than inline base64 — multi-turn conversations and retries don't re-send bytes from your client: ```json { "role": "user", "content": [ { "type": "text", "text": "What's in this image?" }, { "type": "file", "file": { "file_id": "file-abc123" } } ] } ``` The model must have the matching capability (e.g. `vision`); otherwise the request is rejected with `400 model_capability_mismatch`. PDFs can also be sent inline, in the shape the OpenAI SDK and the Vercel AI SDK use (OpenCode sends PDF attachments this way): ```json { "type": "file", "file": { "filename": "report.pdf", "file_data": "data:application/pdf;base64,JVBERi0x..." } } ``` An inline PDF is stored as a `purpose: "document"` file, as if you had uploaded it to the [Files API](/api/files/): it appears in your file list and expires after 30 days. A later request carrying the same PDF reuses that file, so only the first request that sends it waits for its pages to be rendered. The model needs the `vision` capability, the PDF can have at most 100 pages, and a request can carry at most 16 MiB of PDFs that no earlier request stored; PDFs reused from an earlier request don't count. For anything larger, upload the PDF to the Files API with `purpose: "document"` and reference it by `file_id`. An organization on [zero data retention](/organizations/zero-data-retention/) cannot upload files: its inline PDFs are rendered in memory, nothing is stored, and the limits in [Inline PDFs](/organizations/zero-data-retention/#inline-pdfs) apply instead, though the model still needs the `vision` capability. #### Assistant message ```json { "role": "assistant", "content": "The capital of France is Paris." } ``` #### Tool message ```json { "role": "tool", "tool_call_id": "call_abc123", "content": "{\"result\": 42}" } ``` ## Response ```json { "id": "chatcmpl-abc123", "object": "chat.completion", "created": 1700000000, "model": "google/gemma-4-31b-it", "choices": [ { "index": 0, "message": { "role": "assistant", "content": "Hello! How can I help you today?" }, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 10, "completion_tokens": 9, "total_tokens": 19 } } ``` ### Usage | Field | Type | Description | | --------------------------- | ------- | ---------------------------------------------- | | `prompt_tokens` | integer | Input tokens consumed | | `completion_tokens` | integer | Output tokens generated | | `total_tokens` | integer | Sum of input and output tokens | | `prompt_tokens_details` | object | Optional. `{ cached_tokens, audio_tokens }` | | `completion_tokens_details` | object | Optional. `{ reasoning_tokens, audio_tokens }` | ### Reasoning Reasoning models also return their reasoning in `message.reasoning_content` and `message.reasoning`, or `delta.reasoning_content` and `delta.reasoning` when streaming. Both hold the same text. See [Reasoning](/capabilities/reasoning/). ## Streaming Set `stream: true` to receive partial responses as server-sent events. ```bash curl https://api.aiand.com/v1/chat/completions \ -H "Authorization: Bearer sk-your-api-key" \ -H "Content-Type: application/json" \ -d '{ "model": "google/gemma-4-31b-it", "stream": true, "messages": [{"role": "user", "content": "Count to 5"}] }' ``` Each event contains a `data:` line with a JSON chunk. The stream ends with `data: [DONE]`. To include token usage in the final event: ```json { "stream": true, "stream_options": { "include_usage": true } } ``` ## Tool calling Define tools in the request and the model can choose to call them: ```json { "model": "google/gemma-4-31b-it", "messages": [{ "role": "user", "content": "What's the weather in Tokyo?" }], "tools": [ { "type": "function", "function": { "name": "get_weather", "description": "Get current weather for a location", "parameters": { "type": "object", "properties": { "location": { "type": "string" } }, "required": ["location"] } } } ] } ``` When the model calls a tool, the response includes `tool_calls`: ```json { "choices": [ { "message": { "role": "assistant", "tool_calls": [ { "id": "call_abc123", "type": "function", "function": { "name": "get_weather", "arguments": "{\"location\": \"Tokyo\"}" } } ] }, "finish_reason": "tool_calls" } ] } ``` Send the result back as a `tool` message to continue the conversation. --- # Completions (retired) Source: https://aiand-docs-staging.pages.dev/api/completions/ It was the legacy text-completion endpoint, which OpenAI also lists as legacy in its [Completions API reference](https://platform.openai.com/docs/api-reference/completions). Send the prompt as a user message to [Chat Completions](/api/chat-completions/) instead: ```diff - POST /v1/completions {"model": "...", "prompt": "Say hello"} + POST /v1/chat/completions {"model": "...", "messages": [{"role": "user", "content": "Say hello"}]} ``` [Responses](/api/responses/) and [Messages](/api/messages/) accept the same models if your client speaks those APIs. --- # Lifecycle & Expiry Source: https://aiand-docs-staging.pages.dev/api/file-lifecycle/ Uploaded files have a finite lifetime. ai& cleans up expired files automatically and lets you delete files on demand. ## Default retention Files are retained for **30 days** after upload by default. You can set a shorter expiry on individual uploads where the SDK supports it, or delete files explicitly at any time. ## Manual deletion ```bash curl -X DELETE https://api.aiand.com/v1/files/file-abc123 \ -H "Authorization: Bearer sk-..." ``` The file becomes inaccessible immediately. A subsequent `GET` returns `404`. ## Automatic cleanup Once a file passes its `expires_at`, it is removed automatically — bytes and metadata both. Subsequent reads return `404`. ## What happens to references Once a file is deleted, any chat request that references its `file_id` returns an error. Deletion is not reversible — re-upload the file to use it again. --- # File Purposes Source: https://aiand-docs-staging.pages.dev/api/file-purposes/ Every uploaded file has a `purpose` — a label that tells ai& how the file may be used and which model capabilities are required to reference it. ## Supported purposes | Purpose | MIME types | Single-shot limit | Use with | |---------|------------|-------------------|----------| | `vision` | `image/png`, `image/jpeg`, `image/webp`, `image/gif` | 100 MiB | Vision-capable models — see [Vision](/capabilities/vision/) | | `video` | `video/mp4`, `video/webm`, `video/quicktime` | Use [Uploads API](/api/uploads/) (8 GiB) | Video-capable models — see [Video Understanding](/capabilities/video/) | Video generation does not take GIF: frames and reference images for [`/v1/videos`](/api/videos/) must be PNG, JPEG, or WebP, even though `vision` accepts `image/gif` for chat. ## Inference from MIME The `purpose` field is **optional** on upload. If you omit it, ai& infers the purpose from the file's MIME type. This lets the official `anthropic` SDK — which doesn't carry a purpose field — work drop-in. ```bash curl https://api.aiand.com/v1/files \ -H "Authorization: Bearer sk-..." \ -F "file=@cat.png" # inferred purpose: "vision" ``` ## Capability gating at request time When you reference a `file_id` in a chat message, the file's purpose must match a capability the target model declares: - `purpose: "vision"` → model must have the `vision` capability. - `purpose: "video"` → model must have the `video` capability. Mismatched requests return `400 invalid_request_error` without ever calling the model. --- # OpenAI vs Anthropic shape Source: https://aiand-docs-staging.pages.dev/api/file-shapes/ The ai& Files API is a single endpoint that responds in two shapes — selected by whether your request carries an `anthropic-version` header. This means both the official `openai` SDK and the `anthropic` SDK work against the same URL, with the same API key, without any code changes. ## Header-based shape selection | Header | Response shape | |--------|----------------| | `anthropic-version: ` | Anthropic Files API shape | | (header absent) | OpenAI Files API shape | ## Example: same file, two shapes ```bash curl https://api.aiand.com/v1/files/file-abc123 \ -H "Authorization: Bearer sk-..." ``` ```json { "id": "file-abc123", "object": "file", "bytes": 12345, "created_at": 1716960000, "filename": "cat.png", "purpose": "vision" } ``` ```bash curl https://api.aiand.com/v1/files/file-abc123 \ -H "Authorization: Bearer sk-..." \ -H "anthropic-version: 2023-06-01" ``` ```json { "id": "file-abc123", "type": "file", "size_bytes": 12345, "created_at": "2026-05-18T12:00:00Z", "filename": "cat.png", "mime_type": "image/png" } ``` ## What this enables - The OpenAI Python SDK's `client.files.create(...)` works as-is. - The Anthropic Python SDK's `client.beta.files.upload(...)` works as-is. - You can upload via one SDK and read back via the other — same `file_id` either way. --- # Files Source: https://aiand-docs-staging.pages.dev/api/files/ The Files API lets you upload an asset once and reference it in inference requests via a `file_id`, instead of re-sending the bytes (or base64) on every turn. Fully compatible with the [OpenAI Files API](https://platform.openai.com/docs/api-reference/files) shape, and dual-emits the [Anthropic Files API](https://docs.claude.com/en/docs/build-with-claude/files) shape when the request carries an `anthropic-version` header — so both the official `openai` and `anthropic` SDKs work against it without any extension. ```http POST /v1/files GET /v1/files GET /v1/files/{file_id} GET /v1/files/{file_id}/content DELETE /v1/files/{file_id} ``` ## Limits | Purpose | Single-shot `/v1/files` | Total via [`/v1/uploads`](/api/uploads/) | Allowed mime types | |------------|-------------------------|------------------------------------------|---------------------------------------------------------------------------------------------------------------------------------| | `vision` | 100 MiB | 100 MiB | `image/png`, `image/jpeg`, `image/webp`, `image/gif` | | `video` | 100 MiB | 8 GiB | `video/mp4`, `video/webm`, `video/quicktime` | | `audio` | 100 MiB | 1 GiB | `audio/wav`, `audio/x-wav`, `audio/mpeg`, `audio/mp3`, `audio/flac`, `audio/ogg`, `audio/webm`, `audio/mp4`, `audio/x-m4a` | | `document` | 100 MiB | 1 GiB | `application/pdf` | Video generation does not take GIF: frames and reference images for [`/v1/videos`](/api/videos/) must be PNG, JPEG, or WebP, even though `vision` accepts `image/gif` for chat. Uploads default to a 30-day expiry — `expires_at` is returned on every file object. After expiry, references will fail with `410 file_expired`. Files are also subject to soft delete via `DELETE`. ## Upload a file ```http POST /v1/files ``` Multipart form upload. | Field | Type | Required | Description | |-------|------|----------|-------------| | `file` | file | Yes | The asset bytes | | `purpose` | string | No | One of `vision`, `video`, `audio`, `document`. Determines limits + which models can reference it. If omitted, inferred from the file's MIME type — Anthropic SDK uploads (which don't send `purpose`) work transparently | ### Example ```python from openai import OpenAI client = OpenAI( api_key="sk-your-api-key", base_url="https://api.aiand.com/v1", ) with open("photo.png", "rb") as f: file = client.files.create(file=f, purpose="vision") print(file.id) # → file-abc123... ``` ```javascript import OpenAI, { toFile } from "openai"; import fs from "node:fs"; const client = new OpenAI({ apiKey: "sk-your-api-key", baseURL: "https://api.aiand.com/v1", }); const file = await client.files.create({ file: await toFile(fs.createReadStream("photo.png"), "photo.png", { type: "image/png", }), purpose: "vision", }); console.log(file.id); // → file-abc123... ``` ```bash curl https://api.aiand.com/v1/files \ -H "Authorization: Bearer sk-your-api-key" \ -F "file=@photo.png" \ -F "purpose=vision" ``` The MIME type is taken from the multipart part's `Content-Type`. openai-node sends a bare `fs.ReadStream` as `application/octet-stream`, which every purpose rejects, so wrap it in `toFile` with an explicit `type`. ### Response ```json { "id": "file-abc123def456", "object": "file", "bytes": 482301, "purpose": "vision", "filename": "photo.png", "created_at": 1719450000, "expires_at": 1722042000 } ``` | Field | Type | Description | |-------|------|-------------| | `id` | string | Stable id with `file-` prefix. Use this in chat completion content parts. | | `object` | string | Always `"file"` | | `bytes` | integer | File size in bytes | | `purpose` | string | What you uploaded it for | | `filename` | string \| null | Original filename if provided | | `created_at` | integer | Unix timestamp | | `expires_at` | integer \| null | Unix timestamp; defaults to 30 days from upload | | `page_count` | integer | Only present on `document` uploads — number of pages in the PDF | ## Reference a file in chat completions Use a `type: "file"` content part inside a user message. Your client only ever sends the id; the bytes never leave the server again. ```json { "model": "", "messages": [{ "role": "user", "content": [ { "type": "text", "text": "What's in this image?" }, { "type": "file", "file": { "file_id": "file-abc123def456" } } ] }] } ``` ```python completion = client.chat.completions.create( model="", messages=[{ "role": "user", "content": [ {"type": "text", "text": "What's in this image?"}, {"type": "file", "file": {"file_id": file.id}}, ], }], ) print(completion.choices[0].message.content) ``` ```javascript const completion = await client.chat.completions.create({ model: "", messages: [{ role: "user", content: [ { type: "text", text: "What's in this image?" }, { type: "file", file: { file_id: file.id } }, ], }], }); console.log(completion.choices[0].message.content); ``` ### Video example ```json { "model": "", "messages": [{ "role": "user", "content": [ { "type": "text", "text": "Describe what happens in this clip." }, { "type": "file", "file": { "file_id": "file-vid789..." } } ] }] } ``` The wire shape is identical to images — you always send `type: "file"` and the file's `purpose` determines how the model receives it. ### Audio example ```json { "model": "", "messages": [{ "role": "user", "content": [ { "type": "text", "text": "Transcribe and summarize this clip." }, { "type": "file", "file": { "file_id": "file-aud456..." } } ] }] } ``` Same shape. If you prefer to inline audio rather than upload it first, you can also send an OpenAI-shape `{ "type": "input_audio", "input_audio": { "data": "", "format": "wav" } }` part directly. ### Document (PDF) example ```json { "model": "", "messages": [{ "role": "user", "content": [ { "type": "text", "text": "Summarize this paper." }, { "type": "file", "file": { "file_id": "file-doc456..." } } ] }] } ``` The wire shape is the same `type: "file"` reference. The model receives one image per page, so it must have the `vision` capability. Page count is available as `page_count` on `GET /v1/files/{file_id}`. PDF processing happens at upload time, so inference latency is unaffected by the size of an uploaded document. A PDF sent inline in a request is processed and stored the same way on the first request that carries it, and appears in this list, unless your organization is on [zero data retention](/organizations/zero-data-retention/); see [Chat Completions](/api/chat-completions/). ## List files ```http GET /v1/files ``` | Query param | Type | Description | |-------------|------|-------------| | `purpose` | string | Filter by purpose | | `limit` | integer | Page size, 1–100 (default 20) | | `after` | string | Cursor: pass the `id` of the last item from the previous page | Returns files in reverse-chronological order (newest first). ### Example ```bash curl "https://api.aiand.com/v1/files?purpose=vision&limit=50" \ -H "Authorization: Bearer sk-your-api-key" ``` ### Response ```json { "object": "list", "data": [ { "id": "file-abc...", "object": "file", "bytes": 482301, "purpose": "vision", "filename": "photo.png", "created_at": 1719450000, "expires_at": 1722042000 } ], "has_more": false, "first_id": "file-abc...", "last_id": "file-abc..." } ``` When `has_more: true`, pass `last_id` as the next request's `after`. SDK paging (`client.files.list().auto_paging_iter()`) works automatically. ## Retrieve file metadata ```http GET /v1/files/{file_id} ``` Returns the same shape as the upload response. ```bash curl https://api.aiand.com/v1/files/file-abc123 \ -H "Authorization: Bearer sk-your-api-key" ``` ## Download file content ```http GET /v1/files/{file_id}/content ``` Returns the raw bytes with `Content-Type` and `Content-Disposition` headers set from the upload metadata. ```python content = client.files.content("file-abc123") with open("downloaded.png", "wb") as f: f.write(content.read()) ``` ```bash curl https://api.aiand.com/v1/files/file-abc123/content \ -H "Authorization: Bearer sk-your-api-key" \ -o downloaded.png ``` ## Delete a file ```http DELETE /v1/files/{file_id} ``` Deletes the file. Subsequent inference requests referencing the id will fail. ```bash curl -X DELETE https://api.aiand.com/v1/files/file-abc123 \ -H "Authorization: Bearer sk-your-api-key" ``` ### Response ```json { "id": "file-abc123", "object": "file", "deleted": true } ``` Returns `404` if the file is missing or already deleted (idempotent semantics). ## Errors | Status | `error.code` | When | |--------|--------------|------| | `400` | `invalid_request_error` | Missing `file`/`purpose`, unknown `purpose`, mime not in allowlist, file exceeds size cap | | `400` | `model_capability_mismatch` | Chat completion references a file whose `purpose` requires a capability the model lacks | | `401` | `invalid_api_key` | Missing or invalid `Authorization` header | | `403` | `zero_data_retention` | Your organization is on [zero data retention](/organizations/zero-data-retention/), which refuses uploads | | `404` | `file_not_found` | Unknown id, or id belongs to a different organization | | `410` | `file_expired` | File past its `expires_at` | | `500` | `server_error` | Internal failure | ## Notes - File ids are scoped to the **organization** that uploaded them. Cross-org references return `404`. - For one-off small images you can still inline base64 via the standard `{type: "image_url", image_url: {url: "data:..."}}` content part — but multi-turn conversations and large assets benefit from upload-once + reference. --- # Messages (Anthropic) Source: https://aiand-docs-staging.pages.dev/api/messages/ ```http POST /v1/messages ``` Accepts requests in the [Anthropic Messages API](https://docs.anthropic.com/en/api/messages) format. Requests are translated to the internal OpenAI-compatible format, processed, and the response is translated back to the Anthropic format before returning. This lets you use the Anthropic SDK with ai& without changing your code. ## Usage ```python from anthropic import Anthropic client = Anthropic( api_key="sk-your-api-key", base_url="https://api.aiand.com", ) message = client.messages.create( model="google/gemma-4-31b-it", max_tokens=1024, messages=[{"role": "user", "content": "Hello!"}], ) print(message.content[0].text) ``` ```javascript import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic({ apiKey: "sk-your-api-key", baseURL: "https://api.aiand.com", }); const message = await client.messages.create({ model: "google/gemma-4-31b-it", max_tokens: 1024, messages: [{ role: "user", content: "Hello!" }], }); console.log(message.content[0].text); ``` ## How it works 1. Your request arrives in Anthropic Messages format 2. ai& translates it to OpenAI format and runs inference 3. The response is translated back to Anthropic format Streaming is supported. Authentication uses the same `Authorization: Bearer` header as all other endpoints. ## Supported parameters Standard Anthropic Messages API parameters are accepted. Parameters with no equivalent in the underlying model are silently ignored, with the exceptions below. - **`tools`** — client tools (type `custom`, or no type) are translated to function tools. Anthropic's server-side tools (`web_search_*`, `web_fetch_*`, `code_execution_*`, `tool_search_*`, …) run inside Anthropic's API and cannot be served here. Anthropic-defined client tools (`bash_*`, `text_editor_*`, `computer_*`, …) carry no `input_schema` for a non-Anthropic model to follow; send them as custom tools instead. A request that includes any Anthropic-defined tool type is rejected with `400 invalid_request_error` naming the tool. - **`tool_choice`** — `auto`, `any`, `none` and `tool` are mapped; `disable_parallel_tool_use: true` becomes `parallel_tool_calls: false`. - **`output_config.effort` and `thinking`** — `output_config.effort` sets the reasoning level, and `thinking: { "type": "disabled" }` turns reasoning off on a named reasoning model that lists `none`; see [Reasoning](#reasoning) for the exceptions. - **`tool_result` images** — on vision-capable models, images inside a tool result are handed to the model in a user message that follows the turn's tool results, each labelled with its `tool_use_id`; on text-only models the tool text carries an `[Image omitted…]` placeholder. ## Errors Errors on this endpoint use Anthropic's envelope, `{"type":"error","error":{"type":"…","message":"…"},"request_id":"…"}`, with Anthropic's type names (`invalid_request_error`, `authentication_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `api_error`, …). A request that exceeds the model's context window is a `400` whose message begins `prompt is too long` and carries the `capability_rejected: prompt_too_long` token, which is what Claude Code's automatic compaction looks for. ## Keep-alive A streaming response that stays idle for 15 seconds receives a keep-alive: an SSE comment line before `message_start`, a `ping` event after it. Clients that count bytes to detect a stalled stream (Claude Code aborts after 300 seconds of silence) keep the connection through long reasoning spans. ## Reasoning The `thinking` block does not set the reasoning level — `budget_tokens` names nothing on our effort scale, and neither does `enabled`, so depth is otherwise left to the model's own default. To control the level, send `reasoning_effort` with one of the model's published [`reasoning_efforts`](/models/catalog/), or Anthropic's own `output_config: { "effort": "…" }`, which Claude Code sends for `/effort`. An explicit `reasoning_effort` wins when both are present. Either is checked exactly as on `/v1/chat/completions`: a level the model doesn't list runs at the next listed level up, or the highest, which can cost more than the level you sent, a value outside the effort vocabulary is a `400` naming the field you sent, and the applied level comes back in [`X-Reasoning-Effort`](/reference/response-headers/). `thinking: { "type": "disabled" }` is the one exception, because it does state something the scale can express: no reasoning at all. We apply it as `reasoning_effort: "none"` and return `X-Reasoning-Effort: none` to confirm it. It wins over `output_config.effort`, so a request that disables thinking is not billed at the effort in that field. On a reasoning model that publishes no `none` the request is a `400` naming `thinking`, with the same sentence an explicit `reasoning_effort` `none` gets. The error text is on [Errors](/errors/#turning-reasoning-off). Send `reasoning_effort` as well and that is what applies, leaving the block to control the response shape alone. A model without the `reasoning` capability ignores it, having nothing to suppress. With the reserved [`auto`](/models/auto/) model the block sets no level either way. `auto` accepts no effort at all, so name a model directly if you need reasoning off. Claude Code's `output_config.effort` is ignored under `auto` rather than rejected. What `thinking` does control is whether that reasoning is returned to you. Send `thinking: { "type": "enabled" }` — or `"adaptive"`, which is accepted identically — and the reasoning comes back as a `thinking` content block ahead of the text block, streaming as `thinking_delta` events. Omit it and you get the answer alone. The `thinking` blocks we return carry no `signature` — nothing upstream signs them, so one replayed to Anthropic's own API is rejected. Echoing one back to ai& is safe: a `thinking` block on a request is dropped before it reaches the model. Some reasoning models put their whole answer inside the reasoning and leave the message content empty. Those turns come back as an ordinary `text` block whether or not you asked for `thinking`, so a successful response always carries at least one content block. ## Image input Image content blocks are supported on vision-capable models. All three Anthropic source types work — `base64`, `url`, and `file` (referencing a `file_id` from [`POST /v1/files`](/api/files/)). See [Anthropic's content blocks docs](https://docs.anthropic.com/en/api/messages#content-blocks) for the wire format. Requests against models without the `vision` capability are rejected with `400 invalid_request_error` before inference. A `url` source must meet the [URL requirements](/capabilities/vision/#url-requirements). ## Document input PDF `document` blocks are supported on vision-capable models, with a `base64` source (`media_type: "application/pdf"`) or a `file` source referencing a `purpose: "document"` upload. Each page reaches the model as an image, and a PDF inside a `tool_result` is handled like a tool-result image. A `base64` PDF is stored as a `document` file and reused, with the same limits as an inline PDF on [Chat Completions](/api/chat-completions/), except under [zero data retention](/organizations/zero-data-retention/), where it is rendered in memory, not stored, and held to [its own limits](/organizations/zero-data-retention/#inline-pdfs). `text` and `content` sources are passed to the model as text. A `url` source is not fetched, but it must still meet the [URL requirements](/capabilities/vision/#url-requirements), or the request is rejected. --- # Models Source: https://aiand-docs-staging.pages.dev/api/models/ ```http GET /v1/models ``` Lists models with pricing and metadata. Authentication is optional. With an API key, the list holds only the models your organization has access to, priced in its billing currency. Without one, it lists every model ai& serves, priced in USD. Requests without a key are rate-limited to 600 per minute per IP, one budget shared by every catalog endpoint callable without a key, including [`GET /v1/videos/models`](/api/videos/#list-video-models). ## Example ```bash curl https://api.aiand.com/v1/models ``` With an API key: ```bash curl https://api.aiand.com/v1/models \ -H "Authorization: Bearer sk-your-api-key" ``` ## Response ```json { "object": "list", "data": [ { "id": "google/gemma-4-31b-it", "name": "google/gemma-4-31b-it", "object": "model", "created": 1775474514, "owned_by": "ai&", "provider": "google", "context_window": 262144, "max_output_tokens": 32768, "capabilities": [ "reasoning", "tool_calling", "vision", "video", "document" ], "reasoning_efforts": [ "none", "high" ], "reasoning_effort_default": "none", "description": "Largest Gemma 4 instruction model for open, self-hosted chat and reasoning", "currency": "usd", "input_per_1m": "0.200000", "output_per_1m": "0.500000", "cached_input_per_1m": "0.050000", "deprecation_date": null } ] } ``` ### Model object | Field | Type | Description | |-------|------|-------------| | `id` | string | Model identifier — use this as the `model` parameter in requests | | `object` | string | Always `"model"` | | `created` | integer | Unix timestamp | | `owned_by` | string | Always `"ai&"` | | `provider` | string | Upstream provider name | | `context_window` | integer | Maximum context length in tokens | | `max_output_tokens` | integer \| null | Recorded maximum generation length. `null` when no limit is recorded for the model | | `description` | string | Short model description | | `currency` | string | Your organization's billing currency (`usd` or `jpy`) | | `input_per_1m` | string | Cost per 1M input tokens, in your billing currency. Numeric value returned as a string for precision (e.g. `"0.150000"`) | | `output_per_1m` | string | Cost per 1M output tokens, in your billing currency | | `deprecation_date` | string \| null | When the model stops serving requests, as an ISO 8601 timestamp. `null` when no retirement is scheduled | ## Example ```bash curl https://api.aiand.com/v1/models \ -H "Authorization: Bearer sk-your-api-key" ``` ## Model name resolution The `model` parameter you send on an inference request (`/v1/chat/completions`, `/v1/responses`, `/v1/messages`) must be an **exact** model `id` from `GET /v1/models`. Matching is case-insensitive, but partial names are not accepted: a model family without its version returns a `404` with error code `model_not_found` rather than guessing a version. Use `GET /v1/models` as the source of valid names. A model that exists but your organization does not have access to returns a `404` with error code `model_access_denied`. It is also left out of your organization's `GET /v1/models` list. Usage is always recorded and billed against the model's canonical `id`, regardless of the casing you sent. ## Cost calculation ``` cost = (input_tokens / 1_000_000 * input_per_1m) + (output_tokens / 1_000_000 * output_per_1m) ``` Credits are deducted after each successful request. Failed requests (4xx/5xx) are not billed. --- # Responses Source: https://aiand-docs-staging.pages.dev/api/responses/ ```http POST /v1/responses ``` Generate a model response for a given input. Compatible with the [OpenAI Responses API](https://platform.openai.com/docs/api-reference/responses/create). ## Request body | Parameter | Type | Required | Description | |-----------|------|----------|-------------| | `model` | string | Yes | Model ID | | `input` | string \| array | Yes | Text string (treated as a user message) or array of input items | | `instructions` | string | No | System/developer message prepended to context | | `stream` | boolean | No | Stream as SSE. Default: `false` | | `temperature` | number | No | Sampling temperature, 0–2 | | `top_p` | number | No | Nucleus sampling, greater than 0 and at most 1 | | `max_output_tokens` | integer | No | Upper bound on generated tokens | | `tools` | array | No | Function tools the model may call | | `tool_choice` | string \| object | No | `"none"`, `"auto"`, `"required"`, or a specific tool | | `parallel_tool_calls` | boolean | No | Allow parallel tool calls | | `reasoning` | object | No | `{ "effort": …, "summary": "auto"\|"concise"\|"detailed" }`. `effort` is checked per model against its [`reasoning_efforts`](/models/catalog/): a level the model doesn't list runs at the next listed level up, or the highest, which can cost more than the level you sent, and `X-Reasoning-Effort` reports it. A value outside the effort vocabulary is a 400. | | `truncation` | string | No | `"auto"` (truncate to fit context) or `"disabled"` | | `metadata` | object | No | Key-value pairs for tracking | | `text` | object | No | Text response format config | | `seed` | integer | No | Deterministic sampling seed | | `stop` | string \| string[] | No | Up to 4 stop sequences | | `top_k` | integer | No | Top-k sampling (provider-specific) | | `repetition_penalty` | number | No | Repetition penalty (provider-specific) | A model can turn reasoning off only if its levels include `none`. Setting `reasoning.effort` to `none` on a model that does not publish it is a 400. The error text is on [Errors](/errors/#turning-reasoning-off). ### Input types **Simple string:** ```json { "model": "google/gemma-4-31b-it", "input": "Explain quantum computing in one paragraph." } ``` **Structured input items:** ```json { "model": "google/gemma-4-31b-it", "input": [ { "role": "user", "content": [ { "type": "input_text", "text": "Describe this image" }, { "type": "input_image", "image_url": "https://..." } ] } ] } ``` **Input item types:** | Type | Description | |------|-------------| | Message | `{ "role": "user"\|"assistant"\|"developer"\|"system", "content": string \| content[] }` | | Function call | `{ "type": "function_call", "call_id": "...", "name": "...", "arguments": "..." }` | | Function call output | `{ "type": "function_call_output", "call_id": "...", "output": "..." }` | **Content types:** `input_text`, `input_image`, `input_file` `input_image` and `input_file` accept either an inline URL/base64 (`image_url` on `input_image`; `file_url` or `file_data` on `input_file`) or a `file_id` returned by the [Files API](/api/files/). When you reference a `file_id`, the server resolves the upload to a short-lived presigned URL before forwarding to the model. Inline URLs, and the `server_url` of an MCP tool, must meet the [URL requirements](/capabilities/vision/#url-requirements). ```json { "model": "", "input": [ { "role": "user", "content": [ { "type": "input_text", "text": "Describe this image" }, { "type": "input_image", "file_id": "file-img123" } ] } ] } ``` For PDFs, send `input_file` with the document's `file_id`, or inline as `file_data` (`data:application/pdf;base64,...`), which is stored as a `document` file with the same limits as on [Chat Completions](/api/chat-completions/) (or, under [zero data retention](/organizations/zero-data-retention/), rendered in memory with [its own limits](/organizations/zero-data-retention/#inline-pdfs)); the server rasterizes each page and forwards them as `input_image` parts (one per page). `input_image` only accepts `purpose: "vision"` file_ids. Video and audio uploads aren't yet routable through `/v1/responses` — use `/v1/chat/completions` for those. ## Response ```json { "id": "resp_abc123", "object": "response", "status": "completed", "created_at": 1700000000, "model": "google/gemma-4-31b-it", "output": [ { "type": "message", "role": "assistant", "content": [ { "type": "output_text", "text": "Quantum computing uses quantum bits (qubits)..." } ] } ], "usage": { "input_tokens": 15, "output_tokens": 80, "total_tokens": 95 } } ``` ### Response status | Status | Description | |--------|-------------| | `completed` | Generation finished | | `failed` | Generation failed (see `error` field) | | `in_progress` | Still generating (streaming) | | `incomplete` | Stopped early (see `incomplete_details.reason`) | | `cancelled` | Request was cancelled | | `queued` | Waiting to be processed | ### Usage | Field | Type | Description | |-------|------|-------------| | `input_tokens` | integer | Input tokens consumed | | `output_tokens` | integer | Output tokens generated | | `total_tokens` | integer | Sum of input and output tokens | | `input_tokens_details` | object | `{ cached_tokens }` | | `output_tokens_details` | object | `{ reasoning_tokens }` | ## Multi-turn conversations Response objects are not stored, so send the earlier turns in `input` on every request, including each `function_call` the model made alongside its `function_call_output`. A request with a non-empty `previous_response_id` or an `item_reference` input item, including one sent as just `{ "id": "..." }`, is rejected with `400 unsupported_value`. `store` itself is accepted and ignored, but clients that default to `store: true`, such as the Vercel AI SDK, send earlier turns as `item_reference`, so set `store: false` on them. Content a request carries may still be stored, such as an inline PDF or a logged request body; [Zero data retention](/organizations/zero-data-retention/) lists what is stored and for how long. ```json { "model": "google/gemma-4-31b-it", "input": [ { "role": "user", "content": "Explain quantum computing in one paragraph." }, { "role": "assistant", "content": "Quantum computing uses quantum bits (qubits)..." }, { "role": "user", "content": "Now explain it to a 5-year-old." } ] } ``` --- # Uploads Source: https://aiand-docs-staging.pages.dev/api/uploads/ The Uploads API lets you push large files (multi-hundred-MB videos, multi-GB clips) by splitting them into parts of 5 MiB to 64 MiB. Each part is uploaded against an upload session; calling `complete` assembles them into a regular [File object](/api/files/) you can reference in inference requests via `file_id`. Wire-compatible with the [OpenAI Uploads API](https://platform.openai.com/docs/api-reference/uploads): the official Python `openai` SDK's `client.uploads.upload_file_chunked(...)` helper drives the entire sequence in one call. One difference: parts are assembled in the order they arrive, so upload them one after another and complete in that same order (see [Part sizes and order](#part-sizes-and-order)). ```http POST /v1/uploads POST /v1/uploads/{upload_id}/parts POST /v1/uploads/{upload_id}/complete POST /v1/uploads/{upload_id}/cancel ``` ## Limits | Constraint | Value | | ---------------- | --------------------------------------------------------------------- | | Total file size | 8 GiB | | Per-part size | 5 MiB to 64 MiB. Only the last part may be smaller than 5 MiB. | | Session lifetime | 1 hour from creation | | Allowed purposes | `video` (recommended). `vision` works but rarely needed at this size. | Sessions that aren't completed within 1 hour are automatically cancelled. The completed file inherits the standard 30-day expiry from the Files API. ### Part sizes and order Cut the file into parts of at least 5 MiB (the SDK helper uses 64 MiB); only the last part may be smaller. Parts are numbered in the order they reach the API and assembled in that order. Upload them one after another, and pass `part_ids` to `complete` in the same order. A part under 5 MiB that arrives before any other part is refused with a `400`, unless it is the whole file. If parts arrive out of file order, `complete` is refused with a `400` rather than assembling a file in the wrong order. The sizes of the listed parts must add up exactly to the `bytes` you declared when creating the session. ## Quick start The easiest path is the Python SDK's high-level helper, which handles chunking for you. The Node SDK has no equivalent helper; the JavaScript tab drives the raw endpoints, which is the same sequence in a few more lines. ```python from pathlib import Path from openai import OpenAI client = OpenAI( api_key="sk-your-api-key", base_url="https://api.aiand.com/v1", ) upload = client.uploads.upload_file_chunked( file=Path("clip.mp4"), purpose="video", mime_type="video/mp4", ) print(upload.file.id) # → file-vid789... ``` ```javascript import OpenAI, { toFile } from "openai"; import fs from "node:fs/promises"; const client = new OpenAI({ apiKey: "sk-your-api-key", baseURL: "https://api.aiand.com/v1", }); const PART = 64 * 1024 * 1024; const data = await fs.readFile("clip.mp4"); const session = await client.uploads.create({ filename: "clip.mp4", purpose: "video", bytes: data.byteLength, mime_type: "video/mp4", }); const partIds = []; for (let offset = 0; offset < data.byteLength; offset += PART) { const chunk = data.subarray(offset, Math.min(offset + PART, data.byteLength)); const part = await client.uploads.parts.create(session.id, { data: await toFile(chunk, "part"), }); partIds.push(part.id); } const upload = await client.uploads.complete(session.id, { part_ids: partIds }); console.log(upload.file.id); // → file-vid789... ``` Both paths split the file into uniform parts, open a session, upload the parts one after another, and call `complete` with the part ids in upload order. The returned `upload.file.id` is a regular `file-...` id you can use in chat completions exactly like any other file. ## Manual flow If you're not using the SDK, here's the same flow against the raw HTTP endpoints. ### 1. Create a session ```http POST /v1/uploads ``` | Field | Type | Required | Description | | ----------- | ------- | -------- | -------------------------------------------------- | | `filename` | string | Yes | Original filename | | `purpose` | string | Yes | One of `vision`, `video`, `audio`, or `document` | | `bytes` | integer | Yes | Total declared size of the file | | `mime_type` | string | Yes | Mime type; checked against the purpose's allowlist | ```bash curl https://api.aiand.com/v1/uploads \ -H "Authorization: Bearer sk-your-api-key" \ -H "Content-Type: application/json" \ -d '{ "filename": "clip.mp4", "purpose": "video", "bytes": 524288000, "mime_type": "video/mp4" }' ``` #### Response ```json { "id": "upload-abc123", "object": "upload", "bytes": 524288000, "purpose": "video", "filename": "clip.mp4", "status": "pending", "created_at": 1719450000, "expires_at": 1719453600, "file": null } ``` ### 2. Upload parts ```http POST /v1/uploads/{upload_id}/parts ``` Multipart form upload with a single `data` field. Every part except the last must be at least 5 MiB; no part may exceed 64 MiB. Upload parts one after another: they are numbered as they arrive, and `complete` must list them in that order. A part larger than the declared `bytes`, or a first part under 5 MiB that is not the whole file, is refused with a `400`. ```bash curl https://api.aiand.com/v1/uploads/upload-abc123/parts \ -H "Authorization: Bearer sk-your-api-key" \ -F "data=@clip.part1.bin" ``` #### Response ```json { "id": "part-xyz789", "object": "upload.part", "upload_id": "upload-abc123", "created_at": 1719450010 } ``` Repeat until every chunk of the source file has been submitted, capturing each `part.id` in order. ### 3. Complete the upload ```http POST /v1/uploads/{upload_id}/complete ``` | Field | Type | Required | Description | | ---------- | -------- | -------- | --------------------------------------------------------------------------------------------- | | `part_ids` | string[] | Yes | The `part.id`s from step 2, in upload order. Their sizes must add up to the declared `bytes`. | | `md5` | string | No | Optional MD5 of the assembled file | ```bash curl https://api.aiand.com/v1/uploads/upload-abc123/complete \ -H "Authorization: Bearer sk-your-api-key" \ -H "Content-Type: application/json" \ -d '{ "part_ids": ["part-xyz789", "part-uvw456", "part-rst123"] }' ``` #### Response ```json { "id": "upload-abc123", "object": "upload", "bytes": 524288000, "purpose": "video", "filename": "clip.mp4", "status": "completed", "created_at": 1719450000, "expires_at": 1719453600, "file": { "id": "file-vid789", "object": "file", "bytes": 524288000, "purpose": "video", "filename": "clip.mp4", "created_at": 1719450090, "expires_at": 1722042090 } } ``` The `file.id` is now a regular file you can reference in chat completions. For documents, `file` also carries `page_count`, exactly as `GET /v1/files/{file_id}` does. Completing is idempotent: if the response to a successful `complete` was lost, calling it again on the same session returns the same `file`. ### 4. Cancel (optional) ```http POST /v1/uploads/{upload_id}/cancel ``` Aborts a pending session. Idempotent — already-completed or already-cancelled sessions return `404`. ```bash curl -X POST https://api.aiand.com/v1/uploads/upload-abc123/cancel \ -H "Authorization: Bearer sk-your-api-key" ``` ## Reference the file in chat completions Once `complete` returns a `file.id`, use it like any other file: ```json { "model": "", "messages": [ { "role": "user", "content": [ { "type": "text", "text": "Describe what happens in this clip." }, { "type": "file", "file": { "file_id": "file-vid789" } } ] } ] } ``` See [Files → Reference a file in chat completions](/api/files/#reference-a-file-in-chat-completions) for full examples. ## Errors | Status | `error.code` | When | | ------ | ----------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `400` | `invalid_request_error` | Missing field, unknown `purpose`, mime not in allowlist, declared `bytes` exceeds the per-purpose cap, part body > 64 MiB, `part_ids` empty or contains an id from a different upload | | `400` | `invalid_request_error` | A part that does not fit the session: the first part under 5 MiB when it is not the whole file, or a part larger than the declared `bytes` | | `400` | `invalid_request_error` | `part_ids` out of upload order, listing a part twice, listing a part under 5 MiB anywhere but last, or with sizes that do not add up to the declared `bytes` | | `400` | `invalid_request_error` | Adding a part to, or completing, an upload that's no longer `pending` or whose session has expired | | `401` | `invalid_api_key` | Missing or invalid `Authorization` header | | `403` | `zero_data_retention` | Your organization is on [zero data retention](/organizations/zero-data-retention/), which refuses uploads | | `404` | `not_found` | Unknown `upload_id`, or id belongs to a different organization | | `500` | `server_error` | Internal failure. If the message says to start a new upload session, the session is now `failed` and cannot be retried. | ## Notes - Upload sessions and resulting files are scoped to the **organization** that created them. - Each part is durably stored as soon as it's accepted — if your client crashes mid-upload, you can resume by listing your existing `part_ids` (kept until session expiry) and continuing. - A part re-sent after a lost response (the OpenAI SDKs do this automatically on connection errors and 5xx) becomes a second, unused part. Pass only the ids you want assembled; the duplicate is discarded with the session. - The model must have a matching capability for the file's purpose. `purpose: "video"` requires a `video`-capable model — see [`GET /v1/models`](/api/models/). --- # Videos Source: https://aiand-docs-staging.pages.dev/api/videos/ The Videos API generates a video clip from a text prompt, optionally conditioned on uploaded media — one or two images as first/last frames, or reference images, video clips, and audio clips. Rendering takes minutes rather than seconds, so it is an **asynchronous job resource**: you create a job, poll it until it reaches a terminal status, then download the output. ```http POST /v1/videos GET /v1/videos GET /v1/videos/models GET /v1/videos/acceptance POST /v1/videos/acceptance GET /v1/videos/{video_id} GET /v1/videos/{video_id}/content GET /v1/videos/{video_id}/thumbnail ``` Output is MP4 with audio at 768p. Every request must specify `aspect_ratio`. MiniMax H3 video generation runs on hardware in Japan. Japanese scene descriptions are translated to English before generation. The translator is instructed to leave dialogue inside `…` and text depicted in the scene unchanged. ## Agreement A person must agree to the Video Service Terms in the console playground before they can start a job. After that, an API key used by that same person works. A teammate who has not agreed cannot start a job, even in the same organization. Creating a job never records agreement. The only way to record it is `POST /v1/videos/acceptance` from a console login. An API key cannot agree. `GET /v1/videos/acceptance` returns the current terms version and whether this person, in this organization, has agreed to it. A missing agreement returns `403` with code `agreement_required`. Listing jobs, reading a job, and listing models stay open. ## Create a job ```bash curl https://api.aiand.com/v1/videos \ -H "Authorization: Bearer $AIAND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "minimaxai/minimax-h3", "prompt": "A paper boat drifting down a rain-soaked gutter at dusk", "seconds": 8, "aspect_ratio": "16:9" }' ``` | Field | Required | Notes | | ----------------- | -------- | ---------------------------------------------------------------------------------------------------------------------------- | | `model` | yes | A model id from [`GET /v1/videos/models`](#list-video-models) | | `prompt` | yes | Up to 7000 characters | | `seconds` | yes | Clip duration, 4–30. **16–30 seconds is experimental; quality may vary.** | | `aspect_ratio` | yes | One of `Auto`, `21:9`, `16:9`, `4:3`, `1:1`, `3:4`, `9:16` | | `image_reference` | no | One or two images (`purpose: "vision"`) with unique `first_frame`/`last_frame` roles (image-to-video) | | `references` | no | Reference media to condition on (reference-to-video): `images`, `videos`, `audio`. Cannot be combined with `image_reference` | ### First and last frame (image-to-video) Upload each image first and use the returned `file_id`. Each image must be a PNG, JPEG, or WebP file with `purpose: "vision"`, **at most 30 MiB**. Video generation has a lower input limit than the Files API, which accepts uploads up to 100 MiB. ```bash curl https://api.aiand.com/v1/files \ -H "Authorization: Bearer $AIAND_API_KEY" \ -F "purpose=vision" \ -F "file=@first.jpg" ``` Then submit one or two file references in customer-defined order: ```bash curl https://api.aiand.com/v1/videos \ -H "Authorization: Bearer $AIAND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "minimaxai/minimax-h3", "prompt": "A cinematic scene inspired by the references, natural motion", "seconds": 5, "aspect_ratio": "16:9", "image_reference": [ { "file_id": "file-first", "role": "first_frame" }, { "file_id": "file-last", "role": "last_frame" } ] }' ``` The API validates that every file belongs to your organization and has `purpose: "vision"`. It converts the stored files to short-lived signed URLs immediately before generation; public URLs are never required. Either role may be used alone, so a single `first_frame` does not require a `last_frame`. When both are supplied, their roles must be unique. ### Reference images, video, and audio (reference-to-video) To condition on reference media instead of frames, upload each file with the matching purpose — `vision` for images, `video` for clips, `audio` for sound — and pass the `file_id`s inside a `references` object: ```bash curl https://api.aiand.com/v1/videos \ -H "Authorization: Bearer $AIAND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "minimaxai/minimax-h3", "prompt": "A cinematic scene inspired by the references, natural motion", "seconds": 5, "aspect_ratio": "16:9", "references": { "images": [{ "file_id": "file-img1" }], "videos": [{ "file_id": "file-clip1" }], "audio": [{ "file_id": "file-aud1" }] } }' ``` Counts, per-file sizes, and the total size are validated at request time. A reference video or audio clip longer than the video you request is trimmed: only its first part, as long as the requested `seconds`, is used. For an 8-second video, that is the first 8 seconds of each clip. For best results, use clips of 2–15 seconds. | Reference | Purpose | Max count | Max size | Recommended length | | --------- | -------- | --------- | -------- | ------------------ | | image | `vision` | 9 | 30 MiB | — | | video | `video` | 3 | 50 MiB | 2–15 s | | audio | `audio` | 3 | 15 MiB | 2–15 s | These rules apply: - **Frames and references cannot be mixed.** A request uses `image_reference` **or** `references` — never both. They are separate request shapes. - **Audio needs an anchor.** `references` must include at least one image or video; audio alone is rejected. - **At most 12 references in total.** Within the per-type limits above, `references` may hold no more than 12 files altogether. - **At most 256 MiB in total.** Within the per-file limits above, all input files of one request may add up to no more than 256 MiB. A file used in two slots counts twice. - **Input files need an hour left.** A frame or reference file that expires within the next hour is rejected. Upload it again. - **Each slot accepts certain formats.** First and last frames and reference images: PNG, JPEG, or WebP. Reference videos: MP4, MOV, or WebM. Reference audio: WAV, MP3, FLAC, OGG, M4A, or audio in an MP4 or WebM file. Other formats, such as GIF or HEIC, are rejected. - **Input files must not be empty.** A frame or reference file of 0 bytes is rejected. - **Images must be between 1:4 and 4:1.** A frame or reference image whose long side is more than four times its short side is rejected. - **Images must have a standard, readable header.** JPEGs must be 8-bit baseline or progressive; multi-picture JPEGs (such as some iPhone photos) and CMYK JPEGs are rejected. Ultra HDR photos are accepted. The image header must lie within the first 64 KiB; very large metadata can push it past that. Convert such files to a standard PNG or JPEG. - **Images must be still.** Animated PNG and WebP files are rejected. A request that breaks one of these rules is rejected with `400 invalid_value`. Where possible, `param` names the input at fault. The response is a job object with `status: "moderating"`: ```json { "id": "video_9f2c1b7e4a3d4e8fa1b0c9d8e7f6a5b4", "object": "video", "model": "minimaxai/minimax-h3", "prompt": "A paper boat drifting down a rain-soaked gutter at dusk", "seconds": 8, "status": "moderating", "error": null, "cost": "0.64000000", "currency": "usd", "created_at": 1755302400, "completed_at": null, "thumbnail": false } ``` `status` is one of `moderating`, `translating`, `queued`, `in_progress`, `completed`, `failed`, or `canceled`. No job ends as `canceled` today. Treat any status you do not recognize as still running — new intermediate stages may be added. `error` is populated only when the job failed. ## Input moderation Every job starts as `moderating` while its prompt and reference media are checked against our content policy. This normally takes a few seconds — up to about a minute when references are attached — and allowed jobs move to `translating` while the prompt is translated to English, then to `queued`, and render as usual. A request that violates the policy fails with error code `moderation_blocked` and is never billed: ```json { "status": "failed", "error": { "code": "moderation_blocked", "message": "Your request was rejected by our content moderation system." } } ``` If the moderation system itself is unavailable, the job fails closed with error code `moderation_unavailable` — nothing was judged and nothing is billed, so resubmit later. Likewise, if a Japanese prompt cannot be translated, the job fails closed with error code `translation_unavailable` — nothing is billed, so resubmit later. ## Poll for completion ```bash curl https://api.aiand.com/v1/videos/video_9f2c1b7e4a3d4e8fa1b0c9d8e7f6a5b4 \ -H "Authorization: Bearer $AIAND_API_KEY" ``` A typical clip takes several minutes. Poll every 10–30 seconds until `status` is terminal — polling faster does not make a render finish sooner. ## Download the output Once `status` is `completed`, fetch the bytes. The response is the raw MP4, not JSON: ```bash curl https://api.aiand.com/v1/videos/video_9f2c1b7e4a3d4e8fa1b0c9d8e7f6a5b4/content \ -H "Authorization: Bearer $AIAND_API_KEY" \ -o clip.mp4 ``` Requesting content for a job that is not `completed` returns `400`. ### Thumbnail A completed job usually also has a JPEG still, 480 pixels wide, taken from near the start of the clip. `thumbnail` is `true` when the job has one to download; otherwise this returns `404`, or `400` if the job isn't `completed`: ```bash curl https://api.aiand.com/v1/videos/video_9f2c1b7e4a3d4e8fa1b0c9d8e7f6a5b4/thumbnail \ -H "Authorization: Bearer $AIAND_API_KEY" \ -o clip.jpg ``` The thumbnail is free and is deleted together with the video. ## List your jobs ```bash curl "https://api.aiand.com/v1/videos?limit=20" \ -H "Authorization: Bearer $AIAND_API_KEY" ``` Returns newest first. `limit` defaults to 20 and caps at 100; pass `after=` to page. Jobs from everyone in the organization are listed; add `mine=true` to list only the jobs you (or your API key's owner) created. ## List video models Authentication is optional. With an API key, prices are shown in your organization's billing currency; without one, prices are shown in USD. Requests without a key are rate-limited to 600 per minute per IP, one budget shared by every catalog endpoint callable without a key, including [`GET /v1/models`](/api/models/). ```bash curl https://api.aiand.com/v1/videos/models ``` With an API key: ```bash curl https://api.aiand.com/v1/videos/models \ -H "Authorization: Bearer $AIAND_API_KEY" ``` Each entry carries per-second pricing, one row per output resolution: ```json { "object": "list", "data": [ { "id": "minimaxai/minimax-h3", "object": "video_model", "name": "MiniMaxAI/MiniMax-H3", "description": null, "pricing": [ { "resolution": "768p", "per_second": "0.055000", "currency": "usd" } ] } ] } ``` ## Billing A render is priced as `per_second × seconds` and **charged only when it completes**. A job that fails or times out is never charged. A job cannot be canceled once submitted. Because a render costs dollars and cannot be aborted part-way, its exact cost is fixed when the job is submitted: `per_second × requested seconds`. A job is refused unless your balance covers that quote plus the quoted costs of renders already in flight. This is stricter than the chat endpoints, which admit any request while the balance is above zero. `cost` and `currency` contain that quote from the first response. The balance is deducted only after the render completes successfully. ## Errors | Status | Code | Meaning | | ------ | ---------------------------- | --------------------------------------------------------------------------------------------------------- | | `400` | `invalid_value` | Bad body, out-of-range `seconds`, invalid or oversized references, or content requested before completion | | `403` | `agreement_required` | This person has not agreed to the current Video Service Terms in the console playground | | `403` | `zero_data_retention` | Your organization is on [zero data retention](/organizations/zero-data-retention/), which refuses video generation | | `402` | `insufficient_credits` | Balance does not cover the quoted cost of this render | | `402` | `monthly_budget_reached` | Your organization's remaining monthly budget does not cover this render plus renders already in flight | | `404` | `model_not_found` | Unknown model | | `404` | `model_access_denied` | Your organization does not have access to this model | | `429` | `concurrency_limit_exceeded` | You already have the maximum number of renders in progress for your plan | | `429` | `rate_limit_exceeded` | More than 10 submit attempts in one minute from your organization; retry after `Retry-After` seconds | | `409` | `conflict` | The job is still running, or is under review and cannot be deleted | | `503` | `provider_unavailable` | Video generation is temporarily unavailable for this model | An admitted job can also end `failed`, for example with one of these error codes. Failed jobs are never charged. - `engine_unavailable`: video generation was unavailable. Resubmit later. - `engine_timeout`: the render did not finish within the time limit. Resubmit; contact support if it persists. - `engine_rejected`: the video could not be generated. Retrying with the same inputs will fail the same way; contact support if it persists. ## Retention and deletion A finished job — the generated video and the job record, including its prompt — is deleted automatically **30 days** after it finishes. Download anything you want to keep before then. `DELETE /v1/videos/{video_id}` deletes one immediately: ```bash curl -X DELETE https://api.aiand.com/v1/videos/video_abc123 \ -H "Authorization: Bearer $AIAND_API_KEY" ``` ```json { "id": "video_abc123", "object": "video", "deleted": true } ``` This is permanent and cannot be undone. A job that is still running has to finish first — poll it until its status is terminal, then delete. --- # Authentication Source: https://aiand-docs-staging.pages.dev/authentication/ All API endpoints require authentication. ai& supports two methods: ## API Key (recommended) Pass your API key in the `Authorization` header: ```http Authorization: Bearer sk-your-api-key ``` API keys are scoped to an organization. Each key resolves the associated billing account automatically. ### Key format All API keys start with the `sk-` prefix. ### Key management Create, list, update, and revoke API keys from the [console](https://console.aiand.com) or via the management API: | Action | Endpoint | | ---------- | ------------------------------ | | List keys | `GET /api/v1/keys` | | Create key | `POST /api/v1/keys` | | Update key | `PATCH /api/v1/keys/{key_id}` | | Delete key | `DELETE /api/v1/keys/{key_id}` | All key management endpoints require the `X-Org-ID` header set to your organization ID. From a terminal, `aiand login` from the [ai& CLI](/cli/) signs in through your browser, then creates, stores, and rotates a key for you. ## JWT (session auth) Browser-based clients authenticate via JWT tokens stored in `httpOnly` cookies. This is used by the console web app and is not recommended for programmatic API access. - **Access token**: 15-minute TTL, set as `__Host-access_token` cookie on the API host only - **Refresh token**: 30-day TTL, set as `__Host-refresh_token` cookie on the API host only JWTs are issued via OAuth (Google/GitHub), magic link, or SAML SSO sign-in flows. ### SAML SSO Enterprise tenants can enable SAML SSO (Okta, Entra ID, and other IdPs) instead of Google/magic link sign-in. SAML termination is handled by WorkOS; ai& treats it as an OAuth-style redirect: 1. The console redirects to `GET /auth/login/workos`, which sends the browser to the WorkOS AuthKit sign-in page. No organization or domain is passed in. 2. The user enters their work email address on that page. WorkOS picks the SSO connection from the email domain and sends the user to your IdP. 3. After the IdP redirects back, `GET /auth/callback/workos` exchanges the result for a session, following the same cookie-based flow as the other sign-in methods. Only sign-ins that actually went through an SSO connection are accepted. A sign-in through AuthKit's own password or social options is rejected, even for an email domain that is registered to an organization. An SSO sign-in is also accepted only when the email address is on one of your company's verified domains, so only existing accounts with such an address can be merged through SSO. The first person to sign in from your company creates the organization in ai& and becomes its Admin. Everyone after that joins the same organization as a Member. Removing a member in ai& does not block them from signing in again: while their account is active in your IdP, their next SSO sign-in adds them back. To offboard someone for good, deactivate them in your IdP as well. For email domains that require SSO, Google and GitHub sign-in redirect back to the console with an `sso_required` error, and magic link requests return a `403` — in both cases the user is directed to sign in via SSO instead. Contact your account team to enable SSO for your organization. ## Organization context API keys are bound to an organization. When authenticating with an API key, the organization is resolved automatically from the key. When using JWT auth, pass the `X-Org-ID` header to specify which organization context to use. ## Error responses | Status | Meaning | | ------ | ---------------------------------------------- | | `401` | Missing or invalid credentials | | `402` | Insufficient credits in the billing account | | `403` | Valid credentials but insufficient permissions | See [Error Codes](/errors/) for the full error response format. --- # Balance API Source: https://aiand-docs-staging.pages.dev/billing/balance/ ```http GET /billing/balance ``` Returns the organization's remaining prepaid credit — the same number the console shows on the billing page. Use it from scripts that watch for a low balance and alert before requests start failing with `402 Payment Required`. ## Response ```json { "balance": "42.75000000", "currency": "usd" } ``` | Field | Type | Description | | ---------- | ------ | -------------------------------------------------------------- | | `balance` | string | Remaining credit in the billing currency, as a decimal string. | | `currency` | string | `usd` or `jpy`, your organization's billing currency. | `balance` is a string so no precision is lost. Parse it with a decimal library rather than a float. ## What the number means The balance is the **settled** ledger value, so it can briefly read higher than what is actually spendable: - Chat and completion requests are deducted after they finish, through an asynchronous settlement queue. Expect a lag of a few seconds, longer if settlement is backed up or retrying. - Video generations reserve credit when accepted but deduct it only when the render completes. A queued or running render is not yet reflected here. - In-flight requests can push the balance slightly negative. The next top-up clears that first. For alerting, polling once a minute is plenty. Pick a threshold that leaves room for the lag above. ## Auth API key or JWT + `X-Org-ID`. See [Authentication](/authentication/). With an API key the balance is always that of the organization the key belongs to; `X-Org-ID` is ignored. ## Example ```bash curl https://api.aiand.com/billing/balance \ -H "Authorization: Bearer $AIAND_API_KEY" ``` --- # Coupons Source: https://aiand-docs-staging.pages.dev/billing/coupons/ ai& supports promotional discount codes at checkout. If you have a coupon code, you can enter it during purchase to get a discount on your credit top-up — you still receive the full credit amount you selected. ## How it works When you apply a coupon: - **You pay less** — the coupon reduces the amount charged to your card - **You get full credits** — your balance is credited with the amount you originally selected - **No expiry on credits** — credits don't expire, regardless of how you purchased them Example: You select $100 of credits and apply `WELCOME10` (10% off). Stripe charges your card $90. Your account balance goes up by $100. ## Applying a coupon 1. **Go to the billing page** at [console.aiand.com/settings/billing](https://console.aiand.com/settings/billing). 2. **Click "Purchase Credits"** and enter the amount you want to buy (e.g. $100). 3. **Click "Purchase"** — you'll be redirected to Stripe's secure checkout page. 4. **Click "Add promotion code"** on the Stripe checkout page and enter your code. Stripe checkout with 'Add promotion code' button 5. **Review the discount** — Stripe will show the original amount, the discount, and the final total. Stripe checkout with WELCOME10 applied showing 10% off 6. **Complete payment.** Once processed, your balance is credited with the full amount you selected. ## Viewing applied coupons Your payment history on the [billing page](https://console.aiand.com/settings/billing) shows: - **Credits** — the amount added to your balance - **Amount** — what was actually charged to your card - **Coupon** — the coupon code applied (if any) ## Troubleshooting **"The code you entered is invalid"** — Coupons have usage limits and expiry dates. Double-check the code, or reach out to whoever sent it to confirm it's still active. **"This code has expired"** — The coupon's validity period has ended. Contact the issuer for a new one. **"You've already used this code"** — Some coupons are single-use per customer. ## Questions? If you have a coupon that isn't working or need help, contact [support@aiand.com](mailto:support@aiand.com). --- # Credits & Top-Up Source: https://aiand-docs-staging.pages.dev/billing/credits/ ai& uses a prepaid credit model. You add credits to your organization, and inference requests deduct from that balance as they complete. ## Purchasing credits 1. Go to [console.aiand.com/settings/billing](https://console.aiand.com/settings/billing). 2. Click **Purchase Credits** and choose an amount. 3. Optionally apply a [Coupon](/billing/coupons/). 4. Pay via Stripe Checkout. The balance is credited immediately on successful checkout. Stripe webhooks confirm the payment; if a charge later fails, the corresponding credits are reversed. ## How charges work - Each request is billed when it completes, computed from input + output tokens. - The cost is deducted atomically — concurrent requests can't overdraft. - See [Pricing](/models/pricing/) for per-model rates. ## What happens at zero balance When your balance reaches zero, new inference requests return `402 Payment Required` with a clear error. Existing in-flight requests complete and may push the balance slightly negative; the next top-up zeroes that out before adding new credit. --- # Invoices & Receipts Source: https://aiand-docs-staging.pages.dev/billing/invoices/ Every credit purchase generates an invoice and receipt. Both are accessible from the billing page in the console and emailed to the billing contact. ## Finding invoices Go to [console.aiand.com/settings/billing](https://console.aiand.com/settings/billing) → **Payment History**. Each row links to: - A **receipt** — Stripe-hosted, suitable for personal records. - An **invoice PDF** — itemized, suitable for expense reimbursement and accounting. ## What's on the invoice - Organization name and billing address (if set). - Date of purchase, payment method last-4. - Credit amount purchased and amount charged (after any coupon discount). - Stripe transaction ID for reconciliation. ## Setting a billing address For invoices that need a company name and address (e.g. for reimbursement), set them in **Settings → Billing → Billing Details**. Future invoices use those details; past invoices can be re-issued on request. --- # Payment Methods Source: https://aiand-docs-staging.pages.dev/billing/payment-methods/ ai& uses Stripe for all card processing. Payment methods are attached to your organization's billing account and reused across top-ups. ## Adding a card A payment method is saved automatically the first time you complete a Checkout. To add additional cards or change the default: 1. Go to [console.aiand.com/settings/billing](https://console.aiand.com/settings/billing). 2. Click **Payment Methods**. 3. Add a new card via Stripe's hosted form, or set an existing card as the default. ## Default payment method The default is used for any future automated charges (e.g. when auto top-up is enabled — coming soon). One-off top-ups always show a method picker. ## Supported payment types - Major credit and debit cards (Visa, Mastercard, AmEx, JCB, Discover). - Other Stripe-supported methods may appear at checkout based on your country. ## Removing a card You can remove any non-default card directly from the billing page. To remove the default card, set a different one as default first. --- # Redemption Codes Source: https://aiand-docs-staging.pages.dev/billing/redemption-codes/ Redemption codes are handed out at events, hackathons and workshops. Entering one adds credits to your organization's balance directly — no card, no checkout, nothing to pay. This is different from a [coupon](/billing/coupons), which discounts a purchase you are already making at checkout. A redemption code grants credits on its own. ## Redeeming a code 1. **Go to the billing page** at [console.aiand.com/settings/billing](https://console.aiand.com/settings/billing). 2. **Find the "Redeem a Code" panel** and type your code. Case and spacing don't matter. 3. **Click "Apply Code."** The credits appear in your balance immediately, and the redemption shows up in Billing History as **Code Redemption**. ## Rules Each code carries its own limits, set when the code was created: - **One redemption per person.** Aliases of the same address count as one person — `you+anything@gmail.com` and `y.o.u@gmail.com` are the same account for this purpose. - **One redemption per organization.** - **A total claim limit.** Popular codes run out. - **An expiry.** Event codes are often valid only on the day of the event. - **Sometimes a list of email domains.** A code for a company or university event may only work for addresses at those domains. The match is exact, so an organizer covering both staff and students lists both domains — `u-tokyo.ac.jp` on its own does not admit `s.u-tokyo.ac.jp`. - **Only organization admins can redeem.** Credits land on the shared organization balance, so it takes the same permission as choosing the billing currency. A code can grant a different amount in each billing currency — say ¥3,000 or $20 for the same event. Your organization receives the amount for **its own** billing currency, which is chosen once at signup and fixed afterwards. If a code has no amount set for your currency, it is refused rather than converted at a rate nobody chose. The two figures are set independently, so they are not conversions of one another. ## Troubleshooting | Message | What it means | | ----------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------------------------------------- | | This code wasn't recognised. Please check for errors and try again. | Check for a typo. `0`/`O` and `1`/`I` are never used in generated codes. | | This code has expired and can no longer be redeemed. | The redemption window has closed. | | That code has been fully claimed | The claim limit was reached. | | That code does not grant … credits | The code has no amount set for your organization's billing currency. | | This code has already been redeemed. | One redemption per person and per organization. | | That code is restricted to a different email domain | It only works for addresses at specific domains. If your address is on a sub-domain of one of them, ask the organizer to add it. | | Only organization admins can redeem a code | Ask an admin of your organization to redeem it. | | Rate limit exceeded. Try again in 3600 seconds. | Failed attempts are rate limited. Wait an hour and try again. | | This account is not eligible to redeem codes | The account, the organization or its email domain is blocked. Contact support. | Credits from a code never expire, and behave exactly like purchased credits when you make requests. They are not refundable for cash. --- # Refund Policy Source: https://aiand-docs-staging.pages.dev/billing/refunds/ ## Refund eligibility ai& credits are non-refundable as a default — they're a prepaid spend on inference, not a deposit. Exceptions: - **Charge errors** — incorrect amount, duplicate charge, or unauthorized use. Contact support with the Stripe transaction ID. - **Service-level issues** — if a sustained outage prevented use of credits you'd just purchased, we'll consider partial refunds case-by-case. - **Cancellation within 14 days** — if you haven't used any credits from a top-up, you can request a refund within 14 days of purchase. ## Credit expiry Credits **do not expire**. Balance persists indefinitely on an active organization. If an organization is deleted with a positive balance, the balance is forfeited — contact support before deleting if you have substantial credit. ## Chargebacks Filing a chargeback through your card issuer will result in the disputed amount of credit being clawed back from your balance. If the balance is insufficient, the organization is suspended until the dispute is resolved. --- # Usage Tracking Source: https://aiand-docs-staging.pages.dev/billing/usage/ Usage data is available in two places: the **console dashboards** (human-friendly) and the **Analytics API** (programmatic). ## Console dashboards At [console.aiand.com](https://console.aiand.com), the dashboard shows: - Current credit balance. - Spend over the last 24 hours, 7 days, and 30 days. - Top models by request count and by cost. - Recent requests with status, latency, and cost. ## Analytics API For automated reporting, query the [Analytics](/analytics/metrics/) endpoints: - `GET /analytics/metrics` — time-series of requests and tokens. - `GET /analytics/summary` — aggregated totals over a range. - `GET /logs` — individual request rows with cursor pagination. Each endpoint's page lists the `range` values it accepts. From a terminal, `aiand usage --range 30days` from the [ai& CLI](/cli/) shows requests and tokens for the range, compared with the previous one. `GET /analytics/metrics` and `GET /analytics/summary` return requests and tokens for the range. `GET /analytics/metrics` also returns tokens per second, latency, and time to first token. `GET /logs` rows include each request's cost and currency. For the remaining balance, use the [Balance API](/billing/balance/). ## Per-request cost Send `X-Aiand-Metrics: true` with a request to receive `X-Cost` and `X-Cost-Currency` headers carrying the exact cost of that request in your billing currency. Streaming responses emit the same values in the final trailer event. See [Response Headers](/reference/response-headers/) and [Streaming Events](/reference/streaming-events/). --- # Reasoning Source: https://aiand-docs-staging.pages.dev/capabilities/reasoning/ A reasoning model writes out its reasoning before it answers. These models list `reasoning` in `capabilities` on [`GET /v1/models`](/models/catalog/). Reasoning tokens are billed as output tokens, and usage counts them separately as `reasoning_tokens`. ## Chat Completions The reasoning comes back beside `content` under two names, `reasoning_content` and `reasoning`, which always hold the same text. Engines and client libraries disagree on the name, so every model returns both. Read whichever one your client expects. ```json "message": { "role": "assistant", "content": "391", "reasoning_content": "17 × 23 = 17 × 20 + 17 × 3 = 340 + 51 = 391.", "reasoning": "17 × 23 = 17 × 20 + 17 × 3 = 340 + 51 = 391." } ``` When streaming, the reasoning arrives first as deltas carrying both fields, then the answer as `delta.content`: ```text data: {"choices":[{"index":0,"delta":{"reasoning_content":"17 × 23","reasoning":"17 × 23"},"finish_reason":null}],...} data: {"choices":[{"index":0,"delta":{"content":"391"},"finish_reason":null}],...} ``` ```python stream = client.chat.completions.create( model="moonshotai/kimi-k3", messages=[{"role": "user", "content": "What is 17 × 23?"}], stream=True, ) for chunk in stream: if not chunk.choices: continue delta = chunk.choices[0].delta reasoning = getattr(delta, "reasoning_content", None) if reasoning: print(reasoning, end="") if delta.content: print(delta.content, end="") ``` You don't need to send the reasoning back in later turns. If an assistant turn you send fills both fields, `reasoning_content` is the one used. ## Responses The reasoning is an output item of type `reasoning`, ahead of the message. Its text is in `content[].text` (type `reasoning_text`) when the model returns it; `summary[].text` holds a summary instead, when one is produced. Usage counts it in `output_tokens_details.reasoning_tokens`. ## Messages Ask for the reasoning with `thinking: { "type": "enabled" }`, or `"adaptive"`, which is accepted identically. It comes back as a `thinking` content block ahead of the text; when streaming, it arrives in `content_block_delta` events whose `delta.type` is `thinking_delta`. Without it you get the answer alone, unless the model put its whole reply inside the reasoning; that turn comes back as an ordinary `text` block. See [Messages](/api/messages/). ## Reasoning effort Send a level in `reasoning_effort` on Chat Completions, `reasoning.effort` on Responses, and `reasoning_effort` or `output_config.effort` on Messages. Each model lists the levels it takes in `reasoning_efforts` on [`GET /v1/models`](/models/catalog/). The applied level comes back in [`X-Reasoning-Effort`](/reference/response-headers/). How each endpoint validates the level is described on its own page. --- # Streaming Source: https://aiand-docs-staging.pages.dev/capabilities/streaming/ All generative endpoints support streaming — set `stream: true` on the request and the response is delivered as Server-Sent Events (SSE). ## Enabling streaming ```python stream = client.chat.completions.create( model="...", messages=[{"role": "user", "content": "Hello"}], stream=True, stream_options={"include_usage": True}, ) for chunk in stream: if chunk.choices: print(chunk.choices[0].delta.content or "", end="") ``` ```python with client.messages.stream( model="...", max_tokens=1024, messages=[{"role": "user", "content": "Hello"}], ) as stream: for text in stream.text_stream: print(text, end="") ``` ```bash curl https://api.aiand.com/v1/chat/completions \ -H "Authorization: Bearer sk-..." \ -d '{"model":"...","stream":true,"messages":[{"role":"user","content":"Hello"}]}' ``` ## What you receive - **OpenAI shape**: `data: {...}` events with `choices[].delta` chunks; final `data: [DONE]`. - **Anthropic shape**: typed events — `message_start`, `content_block_delta`, `message_delta`, `message_stop`. - **ai& trailer** (OpenAI shape, opt-in): with the `X-Aiand-Metrics: true` request header, ai& appends a `metrics` event with token counts, cost, and currency after the model's terminal event. See [Streaming Events](/reference/streaming-events/). ## Usage and cost in streams Set `stream_options.include_usage = true` (OpenAI shape) to get the final `usage` block before the trailer event. Anthropic streams always carry usage in `message_delta`. --- # Structured Outputs Source: https://aiand-docs-staging.pages.dev/capabilities/structured-outputs/ Structured outputs make the model emit JSON that conforms to a schema you supply — no parsing failures, no missing fields. ## JSON mode The simplest form. Set `response_format` to `json_object`: ```json { "model": "...", "messages": [...], "response_format": { "type": "json_object" } } ``` The model is guaranteed to return syntactically valid JSON. Shape is up to your prompt. ## Strict JSON Schema For guaranteed shape, pass a schema: ```python response = client.chat.completions.create( model="...", messages=[{"role": "user", "content": "Extract: Jane Doe, jane@example.com"}], response_format={ "type": "json_schema", "json_schema": { "name": "contact", "strict": True, "schema": { "type": "object", "properties": { "name": {"type": "string"}, "email": {"type": "string"}, }, "required": ["name", "email"], "additionalProperties": False, }, }, }, ) ``` When `strict: true`, output is constrained to match the schema exactly — no extra fields, no missing required keys. ## With the OpenAI Python SDK The SDK can derive the schema from a Pydantic model via `client.chat.completions.parse()` (or `responses.parse()`). --- # Tool Calling Source: https://aiand-docs-staging.pages.dev/capabilities/tool-calling/ Tool calling (a.k.a. function calling) lets a model emit structured calls into JSON tool definitions you supply. ai& supports tool calling on both the OpenAI Chat Completions and Anthropic Messages surfaces. ## Defining tools ```python tools = [{ "type": "function", "function": { "name": "get_weather", "description": "Get current weather for a city.", "parameters": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], }, }, }] response = client.chat.completions.create( model="...", messages=[{"role": "user", "content": "Weather in Tokyo?"}], tools=tools, tool_choice="auto", ) ``` ```python tools = [{ "name": "get_weather", "description": "Get current weather for a city.", "input_schema": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], }, }] response = client.messages.create( model="...", max_tokens=1024, tools=tools, messages=[{"role": "user", "content": "Weather in Tokyo?"}], ) ``` ## Parallel tool calls OpenAI-shape requests can set `parallel_tool_calls: true` (the default) to let the model emit multiple tool calls in a single turn. ## tool_choice / forced calls - `"auto"` — model decides whether to call a tool (default when `tools` is set). - `"none"` — disable tool use for this request. - `"required"` — force the model to call a tool. - `{ "type": "function", "function": { "name": "..." } }` — force a specific tool. --- # Video Understanding Source: https://aiand-docs-staging.pages.dev/capabilities/video/ Video-capable models accept video clips alongside text. You can send video two ways: reference an upload by `file_id`, or pass an inline URL. ## Option 1: Upload first, reference by `file_id` (recommended) Best for clips you'll use across multiple requests, and the only path for files you don't already host somewhere reachable. 1. Upload via the [Uploads API](/api/uploads/) (chunked, up to 8 GiB) with `purpose: "video"` — or let the purpose be inferred from MIME. 2. Reference the returned `file-...` id in a chat message: ```json { "role": "user", "content": [ { "type": "text", "text": "Summarize this clip." }, { "type": "file", "file": { "file_id": "file-abc123" } } ] } ``` The bytes are pulled from storage at inference time — your client never re-uploads. ## Option 2: Inline `video_url` If the video is already at a public URL, you can reference it directly: ```json { "role": "user", "content": [ { "type": "text", "text": "Summarize this clip." }, { "type": "video_url", "video_url": { "url": "https://example.com/clip.mp4" } } ] } ``` The URL must be reachable from ai& and serve the bytes with a supported `Content-Type`. It must also meet the [URL requirements](/capabilities/vision/#url-requirements): `https` on port 443, with a public hostname. ## Limits - Supported MIME types: `video/mp4`, `video/webm`, `video/quicktime`. - Uploaded files: up to 8 GiB via the chunked [Uploads API](/api/uploads/). Single-shot `/v1/files` is capped at 100 MiB. ## Capability gating Both options require the target model to declare the `video` capability — otherwise the request returns `400 invalid_request_error`. List models and their capabilities via [Models](/api/models/). --- # Vision Source: https://aiand-docs-staging.pages.dev/capabilities/vision/ Vision-capable models accept image inputs alongside text. ai& supports three input modes; choose the one that fits your use case. ## Three ways to send an image ```json { "role": "user", "content": [ { "type": "text", "text": "What's in this image?" }, { "type": "image_url", "image_url": { "url": "https://example.com/cat.png" } } ] } ``` ```json { "role": "user", "content": [ { "type": "text", "text": "What's in this image?" }, { "type": "image_url", "image_url": { "url": "data:image/png;base64,iVBORw0KG..." } } ] } ``` ```json { "role": "user", "content": [ { "type": "text", "text": "What's in this image?" }, { "type": "file", "file": { "file_id": "file-abc123" } } ] } ``` ## URL requirements ai& checks media URLs before the request reaches a model. The check covers `image_url`, `video_url` and `audio_url` parts, `image_url` on Responses `input_image`, `file_url` on chat `file` parts and Responses `input_file`, Anthropic sources with `type: "url"`, and `server_url` on Responses MCP tools. A URL passes only if it: - is an absolute `https` URL. `http` and other schemes are rejected. - uses the default port, 443. - contains no username or password. - names a host, not an IP address. This applies to IPv4 and IPv6 in any notation. - ends in a public top-level domain, such as `.com` or `.jp`. `localhost`, single-label names, and names under `.local` or `.internal` are rejected. Media sent as a `data:` URL or by `file_id` is not checked. A URL that fails returns `400` with `type: "invalid_request_error"` and `code: "invalid_media_url"`. `param` names the field, for example `messages.0.content.1.image_url.url`. On `/v1/messages` the error uses [Anthropic's envelope](/errors/#error-format). The request does not reach a model, and you are not charged for it. To fix a media URL, host the file at a public `https` URL, upload it with the [Files API](/api/files/) and pass its `file_id`, or send it inline as a base64 `data:` URL. An MCP `server_url` must itself be a public `https` URL. ## The file_id path When you send `{type: "file", file: {file_id}}`, the model receives the bytes you uploaded — no public hosting required. The wire format on your end stays the same regardless of which mode you use. Upload images via [Files](/api/files/) with `purpose: "vision"` — or omit the purpose and let it be inferred from MIME. Supported MIME types: `image/png`, `image/jpeg`, `image/webp`, `image/gif`. Max size: 100 MiB. ## Capability gating Vision requests are rejected if the target model lacks the `vision` capability — both for inline `image_url` parts and for `file_id` references whose purpose maps to vision. List models and their capabilities via [Models](/api/models/). --- # ai& CLI Source: https://aiand-docs-staging.pages.dev/cli/ The ai& CLI, `aiand`, signs you in through your browser and manages your API key for you. It sets up coding agents to run on ai&, sends prompts, and covers the everyday console tasks (model catalog, request logs, usage) from the terminal. It is open source at [github.com/aiandlabs/aiand-cli](https://github.com/aiandlabs/aiand-cli). ```bash npm install -g @aiand/cli aiand code # sign in, set up OpenCode, and open it on ai& aiand run "hello" # or send a single prompt ``` ## Install The CLI requires Node.js 22 or newer. ```bash npm install -g @aiand/cli ``` ```bash curl -fsSL https://raw.githubusercontent.com/aiandlabs/aiand-cli/main/install.sh | bash ``` ```powershell irm https://raw.githubusercontent.com/aiandlabs/aiand-cli/main/install.ps1 | iex ``` The install scripts add `aiand` to your `PATH` by editing your shell profile. Set `AIAND_NO_MODIFY_PATH=1` to skip that change. ### Update and uninstall To update, run the install command again. To uninstall, the install script first turns off every coding agent aiand set up, then removes the CLI. Your profiles and credentials under `~/.config/aiand` are kept. ```bash aiand init --off npm uninstall -g @aiand/cli ``` ```bash bash ~/.aiand/cli/install.sh uninstall ``` ```powershell & "$env:USERPROFILE\.aiand\cli\install.ps1" uninstall ``` ## Sign in ```bash aiand login ``` `aiand login` opens your browser. Approving the sign-in creates an [API key](/organizations/api-keys/) for your organization, named `aiand@` so you can tell machines apart in the console. If browser sign-in can't finish (no browser, a timeout, or no terminal), the CLI switches to a code you approve from any device: ``` Your code BCDF-GHJK Approve at https://api.aiand.com/auth/device?user_code=BCDF-GHJK ``` To use a key you already created in the console instead: ```bash aiand login --paste # masked prompt aiand login --with-token < key.txt # read the key from stdin ``` `aiand whoami` shows the signed-in identity, organization, and key expiry. `aiand status` adds where the key is stored and which coding agents are set up. `aiand logout` offers to revoke a key that `aiand login` created. A pasted key is only removed from this machine; revoke it in the console. ### Where the key is stored The key is stored in the OS keychain when one is available, otherwise in an encrypted file under `~/.config/aiand/`. The encryption key for that file sits next to it, so it protects against a casual look, not against anyone who can read the directory. Set `AIAND_KEY_STORAGE=plaintext` to store the key in a plain owner-only file instead. Keys created by `aiand login` last 30 days. The CLI rotates them automatically during their last 3 days, or immediately if the API rejects one. Pasted keys are never rotated or revoked by the CLI. ## Commands | Command | What it does | | ------------------------------------- | ---------------------------------------------------------------- | | `aiand login` / `aiand logout` | Start or end this machine's session | | `aiand whoami` | Identity, organization, and key expiry | | `aiand status` | Sign-in state and each coding agent's setup | | `aiand code` | Open OpenCode on ai&, setting it up first if needed | | `aiand ` | Open a coding agent on ai& the same way | | `aiand on` / `off` / `status` | Set up a coding agent to use ai&, remove that setup, or check it | | `aiand init` | Detect installed coding agents and set them up in one go | | `aiand run-agent ` | Run a coding agent on ai& for one session, changing no config | | `aiand restore --force` | Put an agent's config back as it was before aiand changed it | | `aiand run ` | Send one prompt and stream the answer to stdout | | `aiand chat` | Interactive conversation | | `aiand models` | Model catalog, priced in your billing currency | | `aiand logs` | Recent requests, with `--follow` to tail new ones | | `aiand usage` | Requests and tokens, compared with the previous period | | `aiand orgs` | Organizations you belong to | | `aiand config` | Profiles and defaults | | `aiand key export` | Print the active key, for piping into another tool | Most commands accept `--json`, and every command accepts `--help`. ## Coding agents The CLI supports three coding agents. `` is one of: | Agent | Command | Guide | | ----------- | ---------------------------------- | ----------------------------------------- | | OpenCode | `aiand opencode` (or `aiand code`) | [OpenCode](/integrations/opencode/) | | Claude Code | `aiand claude` | [Claude Code](/integrations/claude-code/) | | Codex | `aiand codex` | [Codex](/integrations/codex/) | `aiand ` opens the agent on ai&. If you're signed out, it signs you in. If the agent isn't installed, it offers to install it. If the agent isn't set up for ai& yet, it runs `on` first, so the agent's own command uses ai& afterwards too. ```bash aiand claude # open Claude Code on ai&, setting it up first if needed aiand claude on # set up Claude Code to use ai& aiand claude status # check what Claude Code is configured to use aiand claude off # remove only what aiand added aiand run-agent claude # use ai& for this one session, change nothing aiand init --all # set up every installed agent ``` Arguments after the agent name are passed to the agent unchanged, as in `aiand claude -p "explain this repo"`. Put aiand's own flags before the agent name (`aiand --profile work codex`), and put `--` before an argument aiand would read as a verb (`aiand opencode -- status`). Pass `--model ` to `on` to choose the model. `on` snapshots the agent's config before its first change, and `off` removes only what aiand added. To return to the exact config from before aiand, run `aiand restore --force`. What `on` writes for each agent is described in its guide. ## Run prompts ```bash aiand run "why is the sky blue?" cat main.ts | aiand run "review this file" aiand run -m google/gemma-4-31b-it --system "be terse" "summarize CAP theorem" aiand run --no-stream --json "hello" | jq .usage aiand chat ``` Piped input is appended to the prompt. The answer goes to stdout and everything else to stderr, so `aiand run "..." > out.md` saves only the answer. After each answer, a footer shows the model, tokens, cost, time, and request ID; `-q` hides it. Without `-m`, the CLI uses your profile's model, or the recommended default from the catalog. `-m auto` lets ai& choose per request where your account supports it; see [Automatic Selection](/models/auto/). Set a default with `aiand config set model `. In `aiand chat`, type `/help` to list the in-session commands, such as `/model ` to switch models. ## Models, logs, and usage ```bash aiand models --capability vision # filter the catalog aiand logs --range 1h --errors # only failed requests aiand logs --follow # tail new requests aiand usage --range 30days aiand usage --metrics # full breakdown ``` - `aiand models` lists the [catalog](/models/catalog/) with prices per 1M tokens in your billing currency. It also works signed out, priced in USD. Filter with `--search`, `--capability`, and `--sort`. - `aiand logs` reads the [request logs](/analytics/logs/). `--range` takes `15m`, `1h`, `6h`, `24h` (default), `7days`, or `30days`. - `aiand usage` summarizes [usage](/billing/usage/) against the previous period. `--range` takes `1h`, `24h`, `7days` (default), `30days`, or `3months`. Logs and usage cover your whole organization, not just this machine. ## Organizations and profiles The CLI's key is scoped to one organization, chosen at sign-in. `aiand orgs` lists the organizations you belong to. To work in a different organization, change your default organization in the console and run `aiand login --force`. Profiles keep separate sign-ins, so each profile can hold a key for a different organization: ```bash aiand login --profile work aiand --profile work usage aiand config use work # make it the default profile ``` See [Switching Orgs](/organizations/switching/) for how organizations apply to API requests. ## Configuration ```bash aiand config # current settings aiand config set model google/gemma-4-31b-it aiand config profiles # list profiles aiand config path # where the files are ``` Settings live in `~/.config/aiand/config.json` (or under `$XDG_CONFIG_HOME`). Credentials are kept in a separate file, so the config file is safe to share. Flags take precedence over environment variables, which take precedence over the stored profile. | Variable | Effect | | ---------------------- | ------------------------------------------------------------------------------- | | `AIAND_API_KEY` | Use this key; no sign-in, nothing stored | | `AIAND_PROFILE` | Profile to use | | `AIAND_BASE_URL` | API endpoint (default `https://api.aiand.com`) | | `AIAND_AUTH_URL` | Sign-in endpoint, if different from the API | | `AIAND_CONFIG_DIR` | Where config and credentials live | | `AIAND_HOME` | Home directory to look for agent configs in, such as your Windows home from WSL | | `AIAND_KEY_STORAGE` | `keychain`, `file`, or `plaintext` | | `AIAND_NO_BROWSER=1` | Never open a browser; print the sign-in link instead | | `AIAND_UPDATE_CHECK=0` | Turn off the daily update notice | | `NO_COLOR` | Turn off color | ## Exit codes | Code | Meaning | | ----- | ---------------------------------------------------- | | `0` | Success | | `1` | Request or usage error; the message says which | | `2` | Not signed in, or the session could not be refreshed | | `3` | Sign-in denied, or the code expired | | `70` | A bug in the CLI; the stack trace is printed | | `127` | Unknown command, or the coding agent isn't installed | | `130` | Interrupted (Ctrl-C) | `aiand status` exits `0` when signed in, even if ai& can't be reached to check the key, and `1` only when signed out, so scripts that check it keep working during an outage. Report bugs and request features on [GitHub](https://github.com/aiandlabs/aiand-cli/issues). --- # Structured Extraction Source: https://aiand-docs-staging.pages.dev/cookbook/extraction/ Extract structured records from free-form text — invoices, emails, transcripts, scraped pages. Use a strict JSON Schema so the result parses cleanly every time. ## Example: parse contact info ```python from openai import OpenAI client = OpenAI(base_url="https://api.aiand.com/v1", api_key="sk-...") text = """ From: Jane Doe Re: Coffee next week Phone: +1 555-123-4567 """ response = client.chat.completions.create( model="google/gemma-4-31b-it", messages=[ {"role": "system", "content": "Extract contact info from the user's text."}, {"role": "user", "content": text}, ], response_format={ "type": "json_schema", "json_schema": { "name": "contact", "strict": True, "schema": { "type": "object", "properties": { "name": {"type": "string"}, "email": {"type": "string"}, "phone": {"type": ["string", "null"]}, }, "required": ["name", "email", "phone"], "additionalProperties": False, }, }, }, ) import json contact = json.loads(response.choices[0].message.content) print(contact) # {'name': 'Jane Doe', 'email': 'jane@example.com', 'phone': '+1 555-123-4567'} ``` ## With Pydantic (Python) The OpenAI Python SDK can derive the schema from a Pydantic model: ```python from pydantic import BaseModel from typing import Optional class Contact(BaseModel): name: str email: str phone: Optional[str] response = client.chat.completions.parse( model="google/gemma-4-31b-it", messages=[ {"role": "system", "content": "Extract contact info."}, {"role": "user", "content": text}, ], response_format=Contact, ) contact = response.choices[0].message.parsed print(contact.name, contact.email) ``` ## Tips - Make every field `required` and use `["string", "null"]` for optional values — `strict` mode rejects missing keys, not nullable ones. - Set `additionalProperties: false` to prevent the model from sneaking in extra fields. - For nested arrays (multiple contacts), wrap in `{"type": "array", "items": {...}}` inside an outer object. --- # Chat over Documents (RAG) Source: https://aiand-docs-staging.pages.dev/cookbook/rag/ ## Approach A: stuff the whole corpus For small-to-medium corpora — internal handbooks, product FAQs, a few PDFs — the simplest solution is to put everything in the system prompt. No retrieval, no vector store, no embedding model. ```python from openai import OpenAI client = OpenAI(base_url="https://api.aiand.com/v1", api_key="sk-...") with open("handbook.md") as f: handbook = f.read() def answer(question: str) -> str: response = client.chat.completions.create( model="deepseek-ai/deepseek-v4.1-flash", messages=[ { "role": "system", "content": ( "Answer based only on the handbook below. " "If the answer isn't in it, say so.\n\n" f"---\n{handbook}\n---" ), }, {"role": "user", "content": question}, ], ) return response.choices[0].message.content print(answer("What's the policy on remote work?")) ``` Pick a model whose `context_window` holds the whole handbook from the [Catalog](/models/catalog/) — `deepseek-ai/deepseek-v4.1-flash` has the largest window at the lowest price. ## Approach B: retrieval with local embeddings When the corpus is too large to fit, compute embeddings yourself and retrieve the top-K chunks at query time. The example below runs embeddings locally with `sentence-transformers`: ```bash pip install openai sentence-transformers numpy ``` ```python import numpy as np from openai import OpenAI from sentence_transformers import SentenceTransformer client = OpenAI(base_url="https://api.aiand.com/v1", api_key="sk-...") embedder = SentenceTransformer("BAAI/bge-small-en-v1.5") CHUNKS = [ "Refunds are processed within 5 business days of approval.", "All employees are entitled to 20 days of paid annual leave.", "Expense reports must be submitted within 30 days of the expense.", "Remote work is supported for engineering and design roles.", # ... thousands more in practice ] embeds = embedder.encode(CHUNKS, normalize_embeddings=True) def retrieve(query: str, k: int = 3) -> list[str]: q = embedder.encode([query], normalize_embeddings=True)[0] scores = embeds @ q return [CHUNKS[i] for i in np.argsort(-scores)[:k]] def answer(question: str) -> str: context = "\n\n".join(retrieve(question)) response = client.chat.completions.create( model="deepseek-ai/deepseek-v4.1-flash", messages=[ { "role": "system", "content": ( "Answer based only on the snippets below. " "If the answer isn't in them, say so.\n\n" f"{context}" ), }, {"role": "user", "content": question}, ], ) return response.choices[0].message.content print(answer("What's the policy on remote work?")) ``` ## Scaling up When you outgrow a numpy array: - **Vector store**: swap in pgvector, Pinecone, Qdrant, or Weaviate. - **Chunking**: 200–500 tokens per chunk with some overlap. Don't embed entire pages. - **Re-ranking**: pull top-K with dense retrieval, then re-rank with a cross-encoder or an LLM. High-leverage for accuracy. - **Citations**: ask the model to quote chunk IDs alongside the answer. Use [Structured Outputs](/capabilities/structured-outputs/) for reliability. - **Embeddings provider**: if running embeddings locally isn't a fit, call any external embeddings API from the same script. --- # Streaming Chat UI Source: https://aiand-docs-staging.pages.dev/cookbook/streaming-ui/ Streaming UIs feel instant because tokens render as they arrive. The pattern is the same everywhere: server route opens a stream to ai&, pipes the SSE body straight to the browser, and the client renders deltas. ## Server route (Next.js App Router) ```ts // app/api/chat/route.ts import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.aiand.com/v1", apiKey: process.env.AIAND_API_KEY!, }); export async function POST(req: Request) { const { messages } = await req.json(); const stream = await client.chat.completions.create({ model: "google/gemma-4-31b-it", messages, stream: true, stream_options: { include_usage: true }, }); const encoder = new TextEncoder(); const body = new ReadableStream({ async start(controller) { for await (const chunk of stream) { const delta = chunk.choices[0]?.delta?.content ?? ""; if (delta) controller.enqueue(encoder.encode(delta)); } controller.close(); }, }); return new Response(body, { headers: { "Content-Type": "text/plain; charset=utf-8" }, }); } ``` ## Client component ```tsx "use client"; import { useState } from "react"; export default function Chat() { const [input, setInput] = useState(""); const [output, setOutput] = useState(""); async function send() { setOutput(""); const res = await fetch("/api/chat", { method: "POST", headers: { "Content-Type": "application/json" }, body: JSON.stringify({ messages: [{ role: "user", content: input }] }), }); const reader = res.body!.getReader(); const decoder = new TextDecoder(); while (true) { const { value, done } = await reader.read(); if (done) break; setOutput((prev) => prev + decoder.decode(value)); } } return (
setInput(e.target.value)} />
{output}
); } ``` ## Capturing cost and request_id The Node SDK's `for await` loop iterates content chunks but doesn't expose the `metrics` trailer event. To capture cost and request_id: - Read the request ID from the `X-Request-ID` response header. - For cost, send the `X-Aiand-Metrics: true` request header and drop down to `fetch` + SSE parsing to read `event: metrics` after `[DONE]` — its payload carries token counts, `cost`, and `currency`. See [Streaming Events](/reference/streaming-events/). ## Notes - The upstream response body matches OpenAI's format exactly — you can swap base URLs without changing the parser. - Never paste the API key into client code; always route through your server. - For a Vercel AI SDK integration (with `useChat` etc.), see [Vercel AI SDK](/integrations/vercel-ai/). --- # Tool-Calling Agent Source: https://aiand-docs-staging.pages.dev/cookbook/tool-agent/ Build a loop where the model can call your tools, see the results, and keep going until it has an answer. ## The loop 1. Define tools as a JSON-schema list. 2. Send the messages + tools. 3. If the model returns `tool_calls`, execute them locally and append the results. 4. Repeat until the model returns a normal message. ## Full example ```python import json from openai import OpenAI client = OpenAI(base_url="https://api.aiand.com/v1", api_key="sk-...") def get_weather(city: str) -> str: return json.dumps({"city": city, "temp_c": 22, "conditions": "sunny"}) def search_web(query: str) -> str: return json.dumps({"results": [{"title": "Tokyo", "url": "https://en.wikipedia.org/wiki/Tokyo"}]}) TOOLS = [ { "type": "function", "function": { "name": "get_weather", "description": "Get current weather for a city.", "parameters": { "type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"], }, }, }, { "type": "function", "function": { "name": "search_web", "description": "Search the web.", "parameters": { "type": "object", "properties": {"query": {"type": "string"}}, "required": ["query"], }, }, }, ] HANDLERS = {"get_weather": get_weather, "search_web": search_web} def run(question: str) -> str: messages = [{"role": "user", "content": question}] for _ in range(10): response = client.chat.completions.create( model="google/gemma-4-31b-it", messages=messages, tools=TOOLS, ) msg = response.choices[0].message messages.append(msg.model_dump(exclude_none=True)) if not msg.tool_calls: return msg.content for call in msg.tool_calls: args = json.loads(call.function.arguments) result = HANDLERS[call.function.name](**args) messages.append({ "role": "tool", "tool_call_id": call.id, "content": result, }) raise RuntimeError("Agent exceeded max iterations") print(run("What's the weather in Tokyo, and is it famous for anything?")) ``` ## Notes - Cap iterations (10 above) so a misbehaving model can't loop forever. - `tool_choice="auto"` is the default; force a specific tool with `tool_choice={"type": "function", "function": {"name": "..."}}`. - For parallel tool calls in a single turn, leave `parallel_tool_calls: true` (the default) — the loop above already handles multiple `tool_calls` per iteration. --- # Vision Extraction Source: https://aiand-docs-staging.pages.dev/cookbook/vision-extraction/ This recipe pulls structured data out of an image — receipts, invoices, ID cards, business cards. Three pieces: 1. Upload the image via [Files](/api/files/). 2. Reference it in a chat message by `file_id`. 3. Constrain the output with a JSON Schema. ## Full example ```python from openai import OpenAI from pydantic import BaseModel from typing import List client = OpenAI(base_url="https://api.aiand.com/v1", api_key="sk-...") class LineItem(BaseModel): description: str quantity: int unit_price_jpy: float class Receipt(BaseModel): merchant: str date: str items: List[LineItem] total_jpy: float uploaded = client.files.create( file=open("receipt.jpg", "rb"), purpose="vision", ) response = client.chat.completions.parse( model="zai-org/glm-5.3-flash", messages=[ {"role": "system", "content": "Extract structured data from receipts."}, { "role": "user", "content": [ {"type": "text", "text": "Parse this receipt into JSON."}, {"type": "file", "file": {"file_id": uploaded.id}}, ], }, ], response_format=Receipt, ) receipt = response.choices[0].message.parsed for item in receipt.items: print(f"{item.description}: ¥{item.unit_price_jpy}") print(f"Total: ¥{receipt.total_jpy}") ``` ## What's happening - `client.files.create(..., purpose="vision")` uploads the image to ai& storage. The purpose is also inferred from MIME if omitted. - The `file` content part — `{type: "file", file: {file_id}}` — tells ai& to resolve the file. The platform generates a short-lived signed URL and rewrites the part to `image_url` before forwarding to the model. See [Vision](/capabilities/vision/) for details. - `response_format=Receipt` ships the Pydantic schema as a strict JSON Schema. ## Cleanup Files cost storage. Delete them when you're done: ```python client.files.delete(uploaded.id) ``` Or let the [30-day expiry](/api/file-lifecycle/) handle it. --- # Error Codes Source: https://aiand-docs-staging.pages.dev/errors/ ## Error format All errors follow the OpenAI-compatible error format: ```json { "error": { "message": "A human-readable description of the error", "type": "error_type", "param": "field_name", "code": "error_code" } } ``` The `param` and `code` fields are optional and may be `null`. The one exception is [`POST /v1/messages`](/api/messages/#errors), which answers in Anthropic's envelope so that Anthropic SDKs and Claude Code read it natively: ```json { "type": "error", "error": { "type": "invalid_request_error", "message": "A human-readable description of the error" }, "request_id": "…" } ``` `code` and `param` are included inside `error` when set. A request that exceeds the model's context window is a `400` whose message begins `prompt is too long` and carries the `capability_rejected: prompt_too_long` token, which Claude Code's automatic compaction looks for. The status codes below apply to both shapes. ## Status codes | Status | Type | Description | |--------|------|-------------| | `400` | `invalid_request_error` | Request validation failed — malformed JSON, missing required fields, or invalid parameter values. | | `401` | `authentication_error` | Missing or invalid API key / JWT token. | | `402` | `billing_error` | Billing account balance is too low to process the request. Add credits to continue. | | `403` | `authorization_error` | Valid credentials but insufficient permissions for the requested resource. | | `404` | `invalid_request_error` | Resource not found — unknown route, a model your organization cannot use, or referenced resource (e.g. `file_id`) doesn't exist or belongs to a different organization. | | `410` | `invalid_request_error` | Resource gone — a referenced file is past its `expires_at`, or the endpoint has been removed. | | `429` | `rate_limit_error` | Rate limit exceeded. How to retry depends on the bucket — see [Rate Limits](/reference/rate-limits/#429-responses). | | `499` | `client_closed_request` | Client disconnected before the response finished. Returned for aborted streams. | | `500` | `server_error` | Unexpected error while serving the request. | | `502` | `server_error` | Failed to reach the model inference layer. | | `504` | `server_error` | Model did not respond before the request timeout. | `500`, `502` and `504` all carry `type: "server_error"` — read `code` to tell them apart. A `500` carries no `code`. ## Common error codes The `code` field gives a more specific reason than the type alone. Notable ones: | Code | Status | When | |------|--------|------| | `invalid_value` | 400 | A request field has an invalid value — a bad mime type or purpose, an `image_url` / `video_url` / `file_id` needing a capability the chosen model lacks, a content part type the model does not support, a tool result with no matching tool call, a `tool_choice` asking for a tool that is not in `tools`, or a `max_tokens` (`max_completion_tokens` when a chat request sends that, `max_output_tokens` on `/v1/responses`) at least as large as the model's context window. `param` names the field | | `invalid_media_url` | 400 | A media URL does not meet the [URL requirements](/capabilities/vision/#url-requirements), for example an `http` URL, an IP address, or an internal hostname. Use a public `https` URL, a `file_id`, or a base64 `data:` URL. `param` names the field | | `context_length_exceeded` | 400 | Request exceeds the model's max context window. Shorten the prompt or lower the requested output tokens | | `unsupported_value` | 400 | The request uses something that is not available: `previous_response_id` or an `item_reference` input item on `/v1/responses` (response objects are not stored), or an input item type or tool type the model does not support. `param` names the field, and the message names the type when it is a standard one | | `file_not_found` | 404 | Referenced `file_id` doesn't exist or isn't owned by your organization | | `file_expired` | 410 | Referenced `file_id` is past its `expires_at` | | `endpoint_removed` | 410 | The endpoint no longer exists, such as `/v1/completions`. The message names the endpoint to use instead | | `model_not_found` | 404 | The `model` is not an exact model `id`, or the model is not priced in your billing currency. `GET /v1/models` lists chat models and `GET /v1/videos/models` lists video models | | `model_deprecated` | 404 | The model was retired. The message names the model to use instead | | `model_access_denied` | 404 | The model exists, but your organization does not have access to it. Contact support@aiand.com to request access | | `invalid_api_key` | 401 | Missing, malformed, or unknown API key | | `rate_limit_exceeded` | 429 | A rate limit was exceeded. Model-bucket rejects set `X-RateLimit-Policy`; authentication throttles carry only `Retry-After` | | `insufficient_credits` | 402 | Balance too low | | `upstream_error` | 502 | Could not reach the model inference layer, or its response was unreadable | | `timeout` | 504 | Model did not respond before the request timeout | | `client_closed_request` | 499 | Client disconnected before the response finished | ## Turning reasoning off A model can turn reasoning off only if its published levels include `none`. Sending `reasoning_effort` `none`, `reasoning.effort` `none`, or on `/v1/messages` a `thinking` block with `type` `disabled`, to a reasoning model that does not publish `none`, returns `400` `invalid_request_error`: ```text Model '' cannot disable reasoning: suppressing it needs reasoning_effort 'none', which this model does not publish. Supported values: . Use '' for the least reasoning. ``` `` is the lowest of that model's published levels. A model without the `reasoning` capability ignores these fields instead. ## Other reasoning level errors A level other than `none` that a model doesn't list runs at the next listed level up, or the model's highest, instead of failing; an unlisted `none` is the 400 above. A value outside the effort vocabulary, such as `ultra`, gets `Model '' does not support ''. Supported values: .` A reasoning model that lists no levels at all answers any level with `Model '' lists no reasoning levels, so it cannot apply .` ## Handling errors ```python from openai import OpenAI, APIError client = OpenAI( api_key="sk-your-api-key", base_url="https://api.aiand.com/v1", ) try: response = client.chat.completions.create( model="google/gemma-4-31b-it", messages=[{"role": "user", "content": "Hello!"}], ) except APIError as e: print(f"Status: {e.status_code}") print(f"Message: {e.message}") ``` The OpenAI SDK automatically parses the error response and raises typed exceptions. For `429` errors with `X-RateLimit-Policy`, handle the reject by bucket: back off (honoring `Retry-After`) on time-based rejects, but for concurrency rejects let in-flight requests drain instead of retrying. An authentication throttle has no policy and should honor `Retry-After`. See [Rate Limits](/reference/rate-limits/#429-responses). --- # Quick Start Source: https://aiand-docs-staging.pages.dev/getting-started/ This page walks you through getting an API key and making your first call. By the end you'll have streamed a response, and know where to go for tool calling, vision, and structured outputs. ## 1. Get a key and credits 1. Sign in at [console.aiand.com](https://console.aiand.com) — Google, GitHub, or magic link. 2. Create an API key. Keys start with `sk-` and are shown **once** — store it in your secret manager. 3. Add credits on the billing page. Minimum top-up is $1. ## 2. Make your first call ```bash pip install openai ``` ```python from openai import OpenAI client = OpenAI( base_url="https://api.aiand.com/v1", api_key="sk-your-api-key", ) response = client.chat.completions.create( model="google/gemma-4-31b-it", messages=[{"role": "user", "content": "Hello!"}], ) print(response.choices[0].message.content) ``` ```bash npm install openai ``` ```ts import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.aiand.com/v1", apiKey: "sk-your-api-key", }); const response = await client.chat.completions.create({ model: "google/gemma-4-31b-it", messages: [{ role: "user", content: "Hello!" }], }); console.log(response.choices[0].message.content); ``` ```bash curl https://api.aiand.com/v1/chat/completions \ -H "Authorization: Bearer sk-your-api-key" \ -H "Content-Type: application/json" \ -d '{ "model": "google/gemma-4-31b-it", "messages": [{"role": "user", "content": "Hello!"}] }' ``` ## 3. Stream the response Add `stream=True` and iterate. You get tokens as they're produced, plus a final cost and request-id trailer event. ```python stream = client.chat.completions.create( model="google/gemma-4-31b-it", messages=[{"role": "user", "content": "Tell me a haiku about Tokyo."}], stream=True, stream_options={"include_usage": True}, ) for chunk in stream: if chunk.choices: print(chunk.choices[0].delta.content or "", end="", flush=True) ``` Streaming details, including the opt-in `metrics` trailer, are documented at [Streaming](/capabilities/streaming/). ## 4. Pick your next step | You want to… | Go here | |--------------|---------| | Let the model call your functions | [Tool Calling](/capabilities/tool-calling/) | | Get JSON output that matches a schema | [Structured Outputs](/capabilities/structured-outputs/) | | Send an image to the model | [Vision](/capabilities/vision/) | | Build something end-to-end (RAG, agent, extraction…) | [Cookbook](/cookbook/rag/) | | Wire ai& into LangChain / LlamaIndex / Vercel AI SDK | [Integrations](/integrations/langchain/) | | Migrate an existing OpenAI / Anthropic project | [Migrating from OpenAI](/migrating-openai/) · [Migrating from Anthropic](/migrating-anthropic/) | ## Authentication beyond API keys For browser apps and the console, ai& also supports JWT auth with an `X-Org-ID` header. See [Authentication](/authentication/) and [Switching Orgs](/organizations/switching/). --- # ai& API Documentation Source: https://aiand-docs-staging.pages.dev/ ## Overview ai& serves open-weight LLMs from our own infrastructure through OpenAI- and Anthropic-compatible APIs. Point your existing SDK at our base URL — your code runs unchanged against the open-source models we host, with per-token credit billing. ## Base URL ``` https://api.aiand.com ``` All API endpoints are served from this base URL. Authentication is required for all requests. ## Your first call ```bash curl https://api.aiand.com/v1/chat/completions \ -H "Authorization: Bearer sk-your-api-key" \ -H "Content-Type: application/json" \ -d '{ "model": "google/gemma-4-31b-it", "messages": [{"role": "user", "content": "Hello!"}] }' ``` ```python from openai import OpenAI client = OpenAI(base_url="https://api.aiand.com/v1", api_key="sk-...") response = client.chat.completions.create( model="google/gemma-4-31b-it", messages=[{"role": "user", "content": "Hello!"}], ) print(response.choices[0].message.content) ``` ```ts import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.aiand.com/v1", apiKey: "sk-...", }); const response = await client.chat.completions.create({ model: "google/gemma-4-31b-it", messages: [{ role: "user", content: "Hello!" }], }); console.log(response.choices[0].message.content); ``` ## What you get Drop-in replacement for `/v1/chat/completions`. Works with the OpenAI SDK, LangChain, LlamaIndex, and anything that speaks the OpenAI format. Access open source models through a single API key. Same OpenAI-compatible request format across all models. Upload images and video once via the [Files API](/api/files/), then reference by `file_id` in chat completions. OpenAI-canonical wire shape — vanilla SDK works. Pay-per-token with automatic credit deduction. Per-model pricing — you only pay for what you use. Failed requests are not billed. ## Next steps ## For AI agents These docs are published as a plain-text corpus following the [llms.txt convention](https://llmstxt.org/), so an agent can read the whole thing without crawling. --- # Claude Code Source: https://aiand-docs-staging.pages.dev/integrations/claude-code/ [Claude Code](https://claude.com/claude-code) talks to the Anthropic Messages API. ai& exposes an Anthropic-compatible `/v1/messages` endpoint, so any tool-calling model in our [catalog](/models/catalog/) can drive the agent loop. Two ways to wire it up: - **[Session](#session-current-terminal-only)** — `export` the env vars into your current shell. Lost when you close the terminal. Good for a one-off try. - **[Permanent](#permanent-settingsjson)** — drop the same config into `settings.json`. Persists across sessions, can be scoped per project or globally. ## Session (current terminal only) **1. Install Claude Code** Prerequisite: [Node.js 22 or newer](https://nodejs.org/). ```bash npm install -g @anthropic-ai/claude-code ``` Confirm the install — `claude --version` should print a version number: ```bash claude --version ``` **2. Set the env vars in this terminal** ```bash export ANTHROPIC_BASE_URL="https://api.aiand.com" export ANTHROPIC_AUTH_TOKEN="sk-your-aiand-api-key" export ANTHROPIC_API_KEY="" export ANTHROPIC_DEFAULT_OPUS_MODEL="moonshotai/kimi-k3" export ANTHROPIC_DEFAULT_SONNET_MODEL="moonshotai/kimi-k3" export ANTHROPIC_DEFAULT_HAIKU_MODEL="google/gemma-4-31b-it" export CLAUDE_CODE_SUBAGENT_MODEL="moonshotai/kimi-k3" ``` **3. Run Claude Code** ```bash claude ``` **1. Install Claude Code** Prerequisites: - [Node.js 22 or newer](https://nodejs.org/) - [Git for Windows](https://git-scm.com/download/win) ```powershell npm install -g @anthropic-ai/claude-code ``` Confirm the install — `claude --version` should print a version number: ```powershell claude --version ``` **2. Set the env vars in this terminal** ```powershell $env:ANTHROPIC_BASE_URL = "https://api.aiand.com" $env:ANTHROPIC_AUTH_TOKEN = "sk-your-aiand-api-key" $env:ANTHROPIC_API_KEY = "" $env:ANTHROPIC_DEFAULT_OPUS_MODEL = "moonshotai/kimi-k3" $env:ANTHROPIC_DEFAULT_SONNET_MODEL = "moonshotai/kimi-k3" $env:ANTHROPIC_DEFAULT_HAIKU_MODEL = "google/gemma-4-31b-it" $env:CLAUDE_CODE_SUBAGENT_MODEL = "moonshotai/kimi-k3" ``` **3. Run Claude Code** ```powershell claude ``` ## Permanent (settings.json) Claude Code reads its config from a `settings.json` file on startup. Drop the same `env` block in and you never have to re-export: ```json { "env": { "ANTHROPIC_BASE_URL": "https://api.aiand.com", "ANTHROPIC_AUTH_TOKEN": "sk-your-aiand-api-key", "ANTHROPIC_API_KEY": "", "ANTHROPIC_DEFAULT_OPUS_MODEL": "moonshotai/kimi-k3", "ANTHROPIC_DEFAULT_SONNET_MODEL": "moonshotai/kimi-k3", "ANTHROPIC_DEFAULT_HAIKU_MODEL": "google/gemma-4-31b-it", "CLAUDE_CODE_SUBAGENT_MODEL": "moonshotai/kimi-k3" } } ``` Two scopes — pick one: - **Project (`.claude/settings.local.json` in your repo)** — applies only when you run `claude` from this repo. The file holds your API key in plain text, so the command below also adds it to the repo's `.gitignore` (skipped if the line is already there); commit that `.gitignore` change. Recommended for trying ai& without touching your global setup. - **Global (`~/.claude/settings.json`)** — applies machine-wide. Every Claude Code surface on your machine picks up the same ai& backend: the `claude` CLI in any terminal session, the [Claude Code VS Code extension](https://code.claude.com/docs/en/vs-code) (the extension and CLI [share this file](https://code.claude.com/docs/en/vs-code#configure-settings) by design), **and** the Claude Desktop app. Use this once ai& is your default everywhere. The commands below **safely merge** the `env` block into the file — any existing keys you have (`permissions`, `hooks`, etc.) are preserved. They use Node, which is already on your machine since step 1 installed Claude Code via npm. Replace `sk-your-aiand-api-key` with your real key before running. ### Project scope (recommended for first try) Run from the root of your project: ```bash mkdir -p .claude && node -e ' const fs = require("fs"), p = ".claude/settings.local.json"; const d = fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, "utf8")) : {}; d.env = Object.assign(d.env || {}, { ANTHROPIC_BASE_URL: "https://api.aiand.com", ANTHROPIC_AUTH_TOKEN: "sk-your-aiand-api-key", ANTHROPIC_API_KEY: "", ANTHROPIC_DEFAULT_OPUS_MODEL: "moonshotai/kimi-k3", ANTHROPIC_DEFAULT_SONNET_MODEL: "moonshotai/kimi-k3", ANTHROPIC_DEFAULT_HAIKU_MODEL: "google/gemma-4-31b-it", CLAUDE_CODE_SUBAGENT_MODEL: "moonshotai/kimi-k3" }); fs.writeFileSync(p, JSON.stringify(d, null, 2)); const g = ".gitignore", gi = fs.existsSync(g) ? fs.readFileSync(g, "utf8") : ""; if (!gi.split(/\r?\n/).includes(p)) fs.appendFileSync(g, (gi && !gi.endsWith("\n") ? "\n" : "") + p + "\n"); ' ``` Run from the root of your project: ```powershell New-Item -ItemType Directory -Force -Path .claude | Out-Null $script = @' const fs = require("fs"), p = ".claude/settings.local.json"; const d = fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, "utf8")) : {}; d.env = Object.assign(d.env || {}, { ANTHROPIC_BASE_URL: "https://api.aiand.com", ANTHROPIC_AUTH_TOKEN: "sk-your-aiand-api-key", ANTHROPIC_API_KEY: "", ANTHROPIC_DEFAULT_OPUS_MODEL: "moonshotai/kimi-k3", ANTHROPIC_DEFAULT_SONNET_MODEL: "moonshotai/kimi-k3", ANTHROPIC_DEFAULT_HAIKU_MODEL: "google/gemma-4-31b-it", CLAUDE_CODE_SUBAGENT_MODEL: "moonshotai/kimi-k3" }); fs.writeFileSync(p, JSON.stringify(d, null, 2)); const g = ".gitignore", gi = fs.existsSync(g) ? fs.readFileSync(g, "utf8") : ""; if (!gi.split(/\r?\n/).includes(p)) fs.appendFileSync(g, (gi && !gi.endsWith("\n") ? "\n" : "") + p + "\n"); '@ node -e $script ``` ### Global scope (all projects) ```bash mkdir -p ~/.claude && node -e ' const fs = require("fs"), os = require("os"), path = require("path"); const p = path.join(os.homedir(), ".claude", "settings.json"); const d = fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, "utf8")) : {}; d.env = Object.assign(d.env || {}, { ANTHROPIC_BASE_URL: "https://api.aiand.com", ANTHROPIC_AUTH_TOKEN: "sk-your-aiand-api-key", ANTHROPIC_API_KEY: "", ANTHROPIC_DEFAULT_OPUS_MODEL: "moonshotai/kimi-k3", ANTHROPIC_DEFAULT_SONNET_MODEL: "moonshotai/kimi-k3", ANTHROPIC_DEFAULT_HAIKU_MODEL: "google/gemma-4-31b-it", CLAUDE_CODE_SUBAGENT_MODEL: "moonshotai/kimi-k3" }); fs.writeFileSync(p, JSON.stringify(d, null, 2)); ' ``` ```powershell New-Item -ItemType Directory -Force -Path $HOME\.claude | Out-Null $script = @' const fs = require("fs"), os = require("os"), path = require("path"); const p = path.join(os.homedir(), ".claude", "settings.json"); const d = fs.existsSync(p) ? JSON.parse(fs.readFileSync(p, "utf8")) : {}; d.env = Object.assign(d.env || {}, { ANTHROPIC_BASE_URL: "https://api.aiand.com", ANTHROPIC_AUTH_TOKEN: "sk-your-aiand-api-key", ANTHROPIC_API_KEY: "", ANTHROPIC_DEFAULT_OPUS_MODEL: "moonshotai/kimi-k3", ANTHROPIC_DEFAULT_SONNET_MODEL: "moonshotai/kimi-k3", ANTHROPIC_DEFAULT_HAIKU_MODEL: "google/gemma-4-31b-it", CLAUDE_CODE_SUBAGENT_MODEL: "moonshotai/kimi-k3" }); fs.writeFileSync(p, JSON.stringify(d, null, 2)); '@ node -e $script ``` ## Verify Inside Claude Code, run: ```text /status ``` The panel shows the active base URL and model. You should see `https://api.aiand.com` and `moonshotai/kimi-k3`. You can also probe the wire-level path from the shell: ```bash curl -sS https://api.aiand.com/v1/models \ -H "Authorization: Bearer sk-your-aiand-api-key" \ | grep -o 'moonshotai/kimi-k3' ``` ```powershell (Invoke-RestMethod https://api.aiand.com/v1/models ` -Headers @{ Authorization = "Bearer sk-your-aiand-api-key" }).data.id ` | Select-String "moonshotai/kimi-k3" ``` A match means the key authenticates and the model is on your org. ## What to expect - **Effort.** `/effort` and `CLAUDE_CODE_EFFORT_LEVEL` are honoured: Claude Code sends the level as `output_config.effort`, and ai& applies it as the model's [`reasoning_effort`](/models/catalog/). A level the model does not list, other than `none` (a 400), runs at the next listed level up, or its highest, so `/effort xhigh` on a model whose top level is `max` runs at `max` and can cost more; `X-Reasoning-Effort` reports the level applied, and each model lists its levels in `reasoning_efforts`. When no level is sent, ai& forwards none and the model runs at its own default, which can be its most expensive level, so set one deliberately. - **Context limit.** When a conversation outgrows the model's window, ai& answers with Anthropic's own "prompt is too long" wording, so Claude Code compacts and retries on its own. Claude Code assumes a 200K window for model names it does not recognise, so a model with a smaller `context_window` reaches its limit sooner and relies on that compact-and-retry. - **Thinking.** Claude Code asks for adaptive thinking on its main chat requests and sends no `thinking` on background calls; ai& streams the model's reasoning back as `thinking` blocks. The block sets no level except `thinking: { "type": "disabled" }`, which turns reasoning off on a named reasoning model that lists `none` and is a 400 on one that doesn't ([details](/api/messages/#reasoning), [error text](/errors/#turning-reasoning-off)); otherwise the level comes from effort. - **WebSearch and other server-side tools.** Anthropic's server-side tools (web search, web fetch, code execution) run inside Anthropic's API and are not available through ai&. A request that includes one is refused with a 400 naming the tool, so Claude Code's `WebSearch` tool reports an error rather than a made-up result. Use an MCP web-search server instead. - **Screenshots.** Images inside tool results (the `Read` tool on a PNG, for example) reach the model on vision-capable models and are replaced by a short placeholder on text-only ones. --- # Cline Source: https://aiand-docs-staging.pages.dev/integrations/cline/ [Cline](https://github.com/cline/cline) is an AI coding agent for the terminal. ai& is included as a built-in provider, with id `aiand` and base URL `https://api.aiand.com/v1`. ## 1. Install the CLI ```bash npm install -g cline ``` ## 2. Add your API key Create a key in the [ai& console](https://console.aiand.com/api-keys), then set it in your shell: ```bash export AIAND_API_KEY="sk-your-aiand-api-key" ``` ```powershell $env:AIAND_API_KEY = "sk-your-aiand-api-key" ``` ## 3. Select ai& Save the provider and a model. This writes the API key into Cline's local settings: ```bash cline auth --provider aiand --apikey "$AIAND_API_KEY" --modelid moonshotai/kimi-k3 ``` The model id is the catalog id, such as `moonshotai/kimi-k3`. It is not prefixed with `aiand/`. To switch the saved model: ```bash cline auth --provider aiand --apikey "$AIAND_API_KEY" --modelid google/gemma-4-31b-it ``` ## 4. Run Cline Start a task with the saved provider: ```bash cline "inspect this repository and fix the failing test" ``` A prompt runs in act mode with tool auto-approval on. Override the saved model for one run: ```bash cline -P aiand -m moonshotai/kimi-k3 "reply with only: ok" ``` When `AIAND_API_KEY` is set, that one-shot form does not need `cline auth` first. ## Verify ```bash cline "reply with only: ok" ``` Cline prints token counts for the turn, including cached input tokens when the model supports prompt caching. ## Troubleshooting - **Unauthorized request** — set `AIAND_API_KEY` in the same shell, or run `cline auth` again with a key from the [ai& console](https://console.aiand.com/api-keys). - **Model not found** — use an id from the [model catalog](/models/catalog/). Do not prefix it with `aiand/`. - **The agent answers but does not edit files** — pick a model with tool calling, such as `moonshotai/kimi-k3` or `google/gemma-4-31b-it`. --- # Cloudflare AI Gateway Source: https://aiand-docs-staging.pages.dev/integrations/cloudflare-ai-gateway/ [Cloudflare AI Gateway](https://developers.cloudflare.com/ai-gateway/) can proxy `https://api.aiand.com` as a [custom provider](https://developers.cloudflare.com/ai-gateway/configuration/custom-providers/). You get Gateway logs, analytics, cache, and rate limits; ai& still bills the `sk-` key. ai& is not a native Gateway provider. The URL uses `custom-{slug}`, not `/openai` or `/aiand`. ## Setup Create an [API key](https://console.aiand.com) first. 1. In the Cloudflare dashboard, open **AI → AI Gateway → Custom Providers** and add a provider: - Slug: `aiand` - Base URL: `https://api.aiand.com` (no trailing slash, no `/v1`) - Enable it 2. Create a gateway (any name; examples below use `aiand-gateway`). 3. Send the ai& key as `Authorization: Bearer sk-…`. **Authenticated Gateway** is optional on this path. If it is on, also send `cf-aig-authorization` with a Cloudflare token that has **AI Gateway Run**. 4. Or store the key under **Provider Keys** (BYOK) and omit `Authorization`. BYOK needs Authenticated Gateway on, and every request must send `cf-aig-authorization`. That Cloudflare token is not the `sk-` key. The custom-provider form has no key field. That is expected. ## Call through the gateway Replace `ACCOUNT_ID` with your Cloudflare account id. ```bash curl https://gateway.ai.cloudflare.com/v1/ACCOUNT_ID/aiand-gateway/custom-aiand/v1/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $AIAND_API_KEY" \ -H "cf-aig-authorization: Bearer $CF_AIG_TOKEN" \ -d '{ "model": "google/gemma-4-31b-it", "messages": [{"role": "user", "content": "Hello"}] }' ``` The examples include both headers. Drop `cf-aig-authorization` only when you send the `sk-` yourself and Authenticated Gateway is off. Drop `Authorization` only for BYOK, and keep `cf-aig-authorization` in that case. ```ts import OpenAI from "openai"; const client = new OpenAI({ apiKey: process.env.AIAND_API_KEY, baseURL: "https://gateway.ai.cloudflare.com/v1/ACCOUNT_ID/aiand-gateway/custom-aiand/v1", defaultHeaders: { "cf-aig-authorization": `Bearer ${process.env.CF_AIG_TOKEN}`, }, }); const response = await client.chat.completions.create({ model: "google/gemma-4-31b-it", messages: [{ role: "user", content: "Hello" }], }); ``` The SDK appends `/chat/completions`. The gateway then calls `https://api.aiand.com/v1/chat/completions`. The same `baseURL` works for `/v1/responses`, `/v1/messages`, and `/v1/models`. ## Unified `/compat` (optional) ```bash curl https://gateway.ai.cloudflare.com/v1/ACCOUNT_ID/aiand-gateway/compat/chat/completions \ -H "Content-Type: application/json" \ -H "Authorization: Bearer $AIAND_API_KEY" \ -H "cf-aig-authorization: Bearer $CF_AIG_TOKEN" \ -d '{ "model": "custom-aiand/google/gemma-4-31b-it", "messages": [{"role": "user", "content": "Hello"}] }' ``` `/compat` is chat completions only. Prefer the provider-specific `/custom-aiand/v1` path for Responses, Messages, and the catalog. ## Caching Turn on **Cache Responses** in the gateway settings. Identical requests return `cf-aig-cache-status: HIT` and do not reach ai& (and are not billed). A cache miss is `MISS`. The default cache key is an exact match of provider, path, model, auth, and the full JSON body. Bypass with `cf-aig-skip-cache: true`. Streaming is not cached by default. **Cache Responses** covers identical non-streaming text and image requests only. A `stream: true` call still reaches ai& and is billed. ## Errors - Missing or bad `cf-aig-authorization` (when auth is on): Cloudflare `401` (`AiGatewayError`), not an OpenAI error body. - Wrong custom slug: Cloudflare `502` (`The provider did not return a valid response`), not ai&'s `404`. - Unknown model, bad JSON, empty `messages`, invalid `reasoning_effort`, vision on a text-only model: ai&'s usual OpenAI-shaped errors, passed through. ## What this is not Calling `env.AI.run("@cf/…")` is [Workers AI](https://developers.cloudflare.com/workers-ai/), a different product. A Worker can call ai& with the OpenAI SDK and `baseURL: "https://api.aiand.com/v1"` without AI Gateway at all. --- # Codex Source: https://aiand-docs-staging.pages.dev/integrations/codex/ [Codex](https://developers.openai.com/codex) talks to the OpenAI Responses API. ai& exposes an OpenAI-compatible `/v1/responses` endpoint, so any tool-calling model in our [catalog](/models/catalog/) can drive the agent loop. ## 1. Install Codex Prerequisite: [Node.js 18 or newer](https://nodejs.org/). ```bash npm install -g @openai/codex ``` Confirm the install — `codex --version` should print a version number: ```bash codex --version ``` ```powershell npm install -g @openai/codex ``` Confirm the install — `codex --version` should print a version number: ```powershell codex --version ``` ## 2. Set your API key The config below reads your key from the `AIAND_API_KEY` environment variable, so Codex never stores it on disk. ```bash export AIAND_API_KEY="sk-your-aiand-api-key" ``` Add that line to your `~/.zshrc` or `~/.bashrc` to persist it across terminals. ```powershell $env:AIAND_API_KEY = "sk-your-aiand-api-key" ``` To persist it across sessions: `setx AIAND_API_KEY "sk-your-aiand-api-key"` (reopen the terminal afterward). ## 3. Add an ai& profile Codex 0.141+ keeps each profile in its own file, `~/.codex/.config.toml`. Create `~/.codex/aiand.config.toml`: ```toml model = "moonshotai/kimi-k3" model_reasoning_effort = "high" model_provider = "aiand" web_search = "disabled" [model_providers.aiand] name = "ai&" base_url = "https://api.aiand.com/v1" wire_api = "responses" [model_providers.aiand.auth] command = "sh" args = ["-c", "echo $AIAND_API_KEY"] [tools] view_image = false [features] unified_exec = false apps = false browser_use = false browser_use_external = false computer_use = false image_generation = false multi_agent = false in_app_browser = false ``` The `model_reasoning_effort` line matters. Codex's built-in default is `medium`, but supported reasoning levels vary by model, and a level a model does not list in its `reasoning_efforts` is rejected. `moonshotai/kimi-k3` accepts `high`, which makes it a safe, deliberate choice here; check `reasoning_efforts` in [`GET /v1/models`](/api/models/) before switching models. The `[model_providers.aiand.auth]` block is what makes Codex fetch ai&'s model catalog. Codex 0.157 and later only ask a custom provider for its model list when the provider is authenticated through a command; with a plain `env_key = "AIAND_API_KEY"` the request still works, but Codex falls back to its bundled metadata, `/model` shows an "Unknown model" warning, and the reasoning levels each ai& model supports are not offered. The command just prints the key from the environment variable you set in step 2. The `[features]` block matters too. ai& serves standard **function tools** — which is all the coding agent loop needs — but not Codex's hosted/built-in tools (web search, image generation, browser & computer control) or its grouped-tool wrapper. Left on, Codex can send tool types the API rejects (`tool type … not supported`), so the profile turns them off. Save it in one step: ```bash mkdir -p ~/.codex && cat > ~/.codex/aiand.config.toml <<'EOF' model = "moonshotai/kimi-k3" model_reasoning_effort = "high" model_provider = "aiand" web_search = "disabled" [model_providers.aiand] name = "ai&" base_url = "https://api.aiand.com/v1" wire_api = "responses" [model_providers.aiand.auth] command = "sh" args = ["-c", "echo $AIAND_API_KEY"] [tools] view_image = false [features] unified_exec = false apps = false browser_use = false browser_use_external = false computer_use = false image_generation = false multi_agent = false in_app_browser = false EOF ``` ```powershell New-Item -ItemType Directory -Force -Path $HOME\.codex | Out-Null @' model = "moonshotai/kimi-k3" model_reasoning_effort = "high" model_provider = "aiand" web_search = "disabled" [model_providers.aiand] name = "ai&" base_url = "https://api.aiand.com/v1" wire_api = "responses" [model_providers.aiand.auth] command = "cmd" args = ["/c", "echo %AIAND_API_KEY%"] [tools] view_image = false [features] unified_exec = false apps = false browser_use = false browser_use_external = false computer_use = false image_generation = false multi_agent = false in_app_browser = false '@ | Set-Content -Encoding utf8 $HOME\.codex\aiand.config.toml ``` ## 4. Run Codex ```bash codex --profile aiand ``` Or non-interactively — `--sandbox workspace-write` lets the agent edit files in the workspace: ```bash codex exec --profile aiand --sandbox workspace-write "Fix the failing test in src/math.js" ``` ## Verify Probe the wire-level path from the shell — a match means the key authenticates and the model is on your org: ```bash curl -sS https://api.aiand.com/v1/models \ -H "Authorization: Bearer $AIAND_API_KEY" \ | grep -o 'moonshotai/kimi-k3' ``` ```powershell (Invoke-RestMethod https://api.aiand.com/v1/models ` -Headers @{ Authorization = "Bearer $env:AIAND_API_KEY" }).data.id ` | Select-String "moonshotai/kimi-k3" ``` --- # CrewAI Source: https://aiand-docs-staging.pages.dev/integrations/crewai/ [CrewAI](https://docs.crewai.com/) builds role-playing multi-agent systems. It routes OpenAI-compatible providers through LiteLLM, installed with the `litellm` extra — so ai& works by setting two env vars. ## Setup ```bash pip install 'crewai[litellm]' ``` ```bash export OPENAI_API_KEY="sk-your-aiand-api-key" export OPENAI_API_BASE="https://api.aiand.com/v1" ``` ## A two-agent crew ```python from crewai import Agent, Task, Crew, LLM llm = LLM(model="openai/google/gemma-4-31b-it") researcher = Agent( role="Researcher", goal="Find accurate information", backstory="An experienced research analyst.", llm=llm, ) writer = Agent( role="Writer", goal="Write a clear summary", backstory="A concise technical writer.", llm=llm, ) research_task = Task( description="Research the city of Tokyo — population, history, notable features.", expected_output="Bullet points covering the key facts.", agent=researcher, ) write_task = Task( description="Write a 100-word summary based on the research.", expected_output="A 100-word paragraph.", agent=writer, ) crew = Crew(agents=[researcher, writer], tasks=[research_task, write_task]) print(crew.kickoff()) ``` ## With tools ```python from crewai.tools import tool @tool("get_weather") def get_weather(city: str) -> str: """Get current weather for a city.""" return f"Sunny in {city}, 22°C" researcher = Agent( role="Weather Reporter", goal="Answer weather questions", backstory="A meteorologist.", llm=llm, tools=[get_weather], ) ``` --- # Cursor Source: https://aiand-docs-staging.pages.dev/integrations/cursor/ [Cursor](https://cursor.com) supports overriding the OpenAI base URL with your own API key — commonly called BYOK (Bring Your Own Key). ai& exposes an OpenAI-compatible `/v1/chat/completions` endpoint, so any tool-calling model in our [catalog](/models/catalog/) can drive Cursor's Agent mode. ## Prerequisites - [Cursor](https://cursor.com) installed - An [ai& API key](https://console.aiand.com/settings/api-keys) with access to a tool-calling model ## Setup 1. Open Cursor Settings (`Cmd+,` / `Ctrl+,`) 2. Go to **Models** → scroll to **OpenAI API Key** 3. Paste your ai& API key into the **OpenAI API Key** field 4. Enable **Override OpenAI Base URL** and set it to: ``` https://api.aiand.com/v1 ``` The `/v1` suffix is required — Cursor appends `/chat/completions` to whatever you provide. 5. Click **+ Add Model** in the model list and enter a model ID from the [catalog](/models/catalog/) that supports tool calling, e.g.: ``` moonshotai/kimi-k3 ``` Toggle the model on, then click **Verify** — Cursor sends a test request to confirm the key and model work ## Using Agent mode Once verified, switch to Agent mode (the robot icon in the sidebar, or `Cmd+.` / `Ctrl+.`). The agent can: - Read and edit files in your workspace - Run terminal commands - Use multi-turn tool calls to complete tasks ## Verify the connection From the shell, confirm your key authenticates and the model is available: ```bash curl -sS https://api.aiand.com/v1/models \ -H "Authorization: Bearer $AIAND_API_KEY" \ | grep -o 'moonshotai/kimi-k3' ``` ```powershell (Invoke-RestMethod https://api.aiand.com/v1/models ` -Headers @{ Authorization = "Bearer $env:AIAND_API_KEY" }).data.id ` | Select-String "moonshotai/kimi-k3" ``` A match means the key works and the model is on your org. ## Troubleshooting - **"Invalid API key"** — check that you copied the full key from the ai& dashboard, and that it hasn't been revoked. - **"Model not found"** — the model ID must match the [catalog](/models/catalog/) exactly, including the provider prefix (`zai-org/`, `moonshotai/`, etc.). - **Agent mode doesn't use tools** — make sure the model you selected supports tool calling. Not all models in the catalog do. - **Verify fails despite correct key/model** — Cursor sometimes sends the verification request with `model: "default"`. Workaround: disable all other models in the list first, then verify. - **Requests blocked or 403** — if you're behind a corporate proxy or VPN, it may be interfering. Try on a direct connection first. ## Switching back to Cursor-hosted models 1. Go to Settings → Models 2. Disable **Override OpenAI Base URL** (or clear the field) 3. Cursor's hosted models (GPT-4o, Claude, etc.) become available again Your BYOK key is stored locally by Cursor and is not sent anywhere except the overridden endpoint. --- # Kilo Code Source: https://aiand-docs-staging.pages.dev/integrations/kilo-code/ [Kilo Code](https://kilo.ai) is an open-source coding agent for VS Code, JetBrains, and the terminal. Connect ai& as a custom OpenAI-compatible provider to use the same models and configuration on every surface. ## Prerequisites - Kilo Code for [VS Code](https://marketplace.visualstudio.com/items?itemName=kilocode.Kilo-Code), [JetBrains](https://plugins.jetbrains.com/plugin/28350-kilo-code), or the CLI - An [ai& API key](https://console.aiand.com/api-keys) ## VS Code and JetBrains 1. Open Kilo Code settings: - **VS Code:** click the gear icon in the Kilo Code sidebar. - **JetBrains:** open **Settings → Tools → Kilo Code**. 2. Open **Providers**, scroll to the bottom, and select **Custom provider**. 3. Enter: | Setting | Value | | ------------ | -------------------------- | | Provider ID | `aiand` | | Display name | `ai&` | | Provider API | **OpenAI Compatible** | | Base URL | `https://api.aiand.com/v1` | | API key | Your ai& API key | 4. Select models from the fetched list, or add an exact ID from the [model catalog](/models/catalog/). 5. Save the provider and choose `aiand/` in Kilo's model picker. ## CLI Install Kilo: ```bash npm install -g @kilocode/cli ``` Set your API key: ```bash export AIAND_API_KEY="sk-your-aiand-api-key" ``` ```powershell $env:AIAND_API_KEY = "sk-your-aiand-api-key" ``` Create the global config at `~/.config/kilo/kilo.jsonc`: ```jsonc { "$schema": "https://app.kilo.ai/config.json", "model": "openai-compatible/moonshotai/kimi-k3", "provider": { "openai-compatible": { "options": { "apiKey": "{env:AIAND_API_KEY}", "baseURL": "https://api.aiand.com/v1", }, "models": { "moonshotai/kimi-k3": { "name": "moonshotai/kimi-k3", "tool_call": true, "reasoning": true, "limit": { "context": 1048576, "output": 131072, }, }, "google/gemma-4-31b-it": { "name": "google/gemma-4-31b-it", "tool_call": true, "reasoning": true, "limit": { "context": 262144, "output": 32768, }, }, }, }, }, } ``` Run the interactive terminal UI: ```bash kilo ``` Or run one task: ```bash kilo run --auto -m openai-compatible/moonshotai/kimi-k3 "inspect this repository and fix the failing test" ``` ## Verify Confirm Kilo can see the configured provider and model: ```bash kilo models openai-compatible kilo run --auto -m openai-compatible/moonshotai/kimi-k3 "reply with only: ok" ``` ## Troubleshooting - **Models do not load** — confirm the base URL is exactly `https://api.aiand.com/v1` and re-enter the API key. - **Unauthorized request** — create a new key in the [ai& console](https://console.aiand.com/api-keys) and update the provider or `AIAND_API_KEY`. - **The CLI ignores `AIAND_API_KEY`** — keep the `{env:AIAND_API_KEY}` reference in the global config, not a project config. - **The agent answers but cannot edit files** — choose a model with `tool_calling` in the [catalog](/models/catalog/) and allow Kilo's file tools when prompted. - **Context usage is missing or compaction does not run** — add the model's `limit.context` and `limit.output` values from the [catalog](/models/catalog/) to `kilo.jsonc`. --- # LangChain Source: https://aiand-docs-staging.pages.dev/integrations/langchain/ LangChain works against ai& through `ChatOpenAI` — point it at the ai& base URL and your existing chains, agents, and graphs work unchanged. ## Setup ```bash pip install langchain langchain-openai ``` ```python from langchain_openai import ChatOpenAI llm = ChatOpenAI( model="google/gemma-4-31b-it", base_url="https://api.aiand.com/v1", api_key="sk-your-api-key", ) print(llm.invoke("Hello").content) ``` ## In a chain ```python from langchain_core.prompts import ChatPromptTemplate prompt = ChatPromptTemplate.from_messages([ ("system", "Translate the user's input to {language}."), ("user", "{text}"), ]) chain = prompt | llm print(chain.invoke({"language": "Japanese", "text": "Hello, world!"}).content) ``` ## With tools ```python from langchain_core.tools import tool @tool def get_weather(city: str) -> str: """Get current weather for a city.""" return f"Sunny in {city}, 22°C" llm_with_tools = llm.bind_tools([get_weather]) response = llm_with_tools.invoke("What's the weather in Tokyo?") print(response.tool_calls) ``` ## LangGraph LangGraph nodes that wrap `ChatOpenAI` work the same way — there's nothing ai&-specific. See [LangGraph docs](https://langchain-ai.github.io/langgraph/). --- # LlamaIndex Source: https://aiand-docs-staging.pages.dev/integrations/llamaindex/ LlamaIndex works against ai& through its `OpenAILike` LLM — same OpenAI wire protocol, custom base URL. ## Setup ```bash pip install llama-index llama-index-llms-openai-like ``` ```python from llama_index.llms.openai_like import OpenAILike llm = OpenAILike( model="google/gemma-4-31b-it", api_base="https://api.aiand.com/v1", api_key="sk-your-api-key", is_chat_model=True, ) print(llm.complete("Hello").text) ``` ## Indexing + querying documents ```python from llama_index.core import VectorStoreIndex, SimpleDirectoryReader, Settings Settings.llm = llm # Bring your own embeddings — ai& doesn't expose an embeddings endpoint: from llama_index.embeddings.huggingface import HuggingFaceEmbedding Settings.embed_model = HuggingFaceEmbedding(model_name="BAAI/bge-small-en-v1.5") docs = SimpleDirectoryReader("./data").load_data() index = VectorStoreIndex.from_documents(docs) query_engine = index.as_query_engine() print(query_engine.query("What is this corpus about?")) ``` ## With tools (function agent) ```python from llama_index.core.agent.workflow import FunctionAgent from llama_index.core.tools import FunctionTool def get_weather(city: str) -> str: """Get current weather for a city.""" return f"Sunny in {city}, 22°C" agent = FunctionAgent( tools=[FunctionTool.from_defaults(fn=get_weather)], llm=llm, ) print(await agent.run("What's the weather in Tokyo?")) ``` --- # OMP Source: https://aiand-docs-staging.pages.dev/integrations/omp/ [OMP](https://github.com/can1357/oh-my-pi) is an AI coding agent for the terminal. ai& is included as a built-in provider. ## 1. Install OMP ```bash curl -fsSL https://omp.sh/install | sh ``` ```powershell irm https://omp.sh/install.ps1 | iex ``` You can also install OMP with Bun 1.3.14 or newer: ```bash bun install -g @oh-my-pi/pi-coding-agent ``` ## 2. Add your API key Create a key in the [ai& console](https://console.aiand.com/api-keys), then set it in your shell: ```bash export AIAND_API_KEY="sk-your-aiand-api-key" ``` ```powershell $env:AIAND_API_KEY = "sk-your-aiand-api-key" ``` ## 3. Run OMP Start OMP with any ai& model: ```bash omp --model aiand/ ``` The model name must use the `aiand/` format. ## Verify Run a one-shot prompt: ```bash omp --print --model aiand/ "reply with only: ok" ``` If OMP reports an unauthorized request, confirm that `AIAND_API_KEY` is set in the same shell. --- # OpenAI Agents SDK Source: https://aiand-docs-staging.pages.dev/integrations/openai-agents/ The [OpenAI Agents SDK](https://github.com/openai/openai-agents-python) (Python) and its JS counterpart work against ai& by configuring a custom `OpenAI` client. ## Setup ```bash pip install openai-agents ``` ```python from agents import Agent, Runner, ModelSettings from agents.models.openai_chatcompletions import OpenAIChatCompletionsModel from openai import AsyncOpenAI client = AsyncOpenAI( base_url="https://api.aiand.com/v1", api_key="sk-your-api-key", ) model = OpenAIChatCompletionsModel( model="google/gemma-4-31b-it", openai_client=client, ) ``` ## A simple agent ```python agent = Agent( name="Assistant", instructions="You are a helpful assistant. Always reply concisely.", model=model, ) result = await Runner.run(agent, "What is the capital of France?") print(result.final_output) ``` ## Agent with tools ```python from agents import function_tool @function_tool def get_weather(city: str) -> str: """Get current weather for a city.""" return f"Sunny in {city}, 22°C" agent = Agent( name="WeatherBot", instructions="Help the user with weather questions.", model=model, tools=[get_weather], ) result = await Runner.run(agent, "What's the weather in Tokyo?") print(result.final_output) ``` ## Handoffs and multi-agent The Agents SDK's handoff pattern works the same way — pass `handoffs=[other_agent]` on `Agent(...)`. No ai&-specific configuration needed. --- # OpenCode Source: https://aiand-docs-staging.pages.dev/integrations/opencode/ [OpenCode](https://opencode.ai) is an open source AI coding agent available as a terminal interface, CLI, IDE extension, and desktop app. ai& ships as a built-in OpenCode provider: connect with your API key and ai& models appear in the model picker. ## 1. Install OpenCode For the terminal or one-shot CLI: ```bash curl -fsSL https://opencode.ai/install | bash ``` Other supported installers include npm and Homebrew: ```bash npm install -g opencode-ai brew install anomalyco/tap/opencode ``` OpenCode recommends WSL for the terminal experience. You can also install the Windows package: ```powershell choco install opencode ``` or: ```powershell scoop install opencode ``` For the desktop app, download **OpenCode Desktop** from [opencode.ai/download](https://opencode.ai/download), or install it with a package manager: ```bash brew install --cask opencode-desktop ``` ```powershell scoop bucket add extras scoop install extras/opencode-desktop ``` ## 2. Connect ai& Create an API key in the [ai& console](https://console.aiand.com/api-keys), then add it to OpenCode in whichever surface you use. ### Terminal In the OpenCode TUI, run: ```text /connect ``` Choose **ai&** from the provider list and paste your `sk-...` key. OpenCode stores the credential and reuses it in future sessions. Outside the TUI, `opencode auth login` walks through the same provider list from your shell. ### Desktop app Open **Settings → Providers → View all providers**, search for **ai&**, and paste your `sk-...` key. The Providers screen lists only connected and popular providers by default, so ai& does not appear until you open the full list. ### Environment variable For CI, scripts, or non-interactive runs: ```bash export AIAND_API_KEY="sk-your-aiand-api-key" ``` ```powershell $env:AIAND_API_KEY = "sk-your-aiand-api-key" ``` ## 3. Pick a model Open the model picker and choose a model from the **ai&** section: ```text /models ``` Model names use `aiand/`, for example `aiand/moonshotai/kimi-k3`. OpenCode pulls its provider catalog automatically, so new ai& models appear after a restart without reinstalling anything. ## 4. Run OpenCode Interactive terminal UI: ```bash opencode ``` One-shot CLI: ```bash opencode run "say hi in one word" ``` ## Desktop App The Desktop app uses the same built-in ai& provider as the terminal: 1. Install **OpenCode Desktop** from [opencode.ai/download](https://opencode.ai/download). 2. Open **Settings → Providers → View all providers**, search for **ai&**, and paste your API key. 3. Open a project folder. 4. Run `/models` and choose an `aiand/...` model. ## Verify Check that OpenCode can reach ai&: ```bash opencode run -m aiand/moonshotai/kimi-k3 "reply with only: ok" ``` You can also test the key directly against ai&: ```bash curl -sS https://api.aiand.com/v1/models \ -H "Authorization: Bearer $AIAND_API_KEY" ``` ```powershell Invoke-RestMethod https://api.aiand.com/v1/models ` -Headers @{ Authorization = "Bearer $env:AIAND_API_KEY" } ``` ## Troubleshooting - **ai& is missing from the provider list** — update OpenCode to a recent version and restart it so it refreshes the provider catalog. - **A model from the ai& catalog is missing in `/models`** — the models.dev listing can lag behind the [ai& catalog](/models/catalog/). Restart OpenCode; if it is still missing, pass the model id directly with `opencode run -m aiand/`. - **Unauthorized request** — re-enter the key with `/connect` or `opencode auth login` (terminal) or under **Settings → Providers → View all providers → ai&** (Desktop), or confirm `AIAND_API_KEY` is set in the process running OpenCode. - **The Desktop app does not see your key** — add it under **Settings → Providers → View all providers → ai&**, or launch Desktop from a shell where `AIAND_API_KEY` is already exported. --- # PydanticAI Source: https://aiand-docs-staging.pages.dev/integrations/pydantic-ai/ [PydanticAI](https://ai.pydantic.dev/) treats LLMs as typed functions. It works against ai& through its OpenAI-compatible provider. ## Setup ```bash pip install pydantic-ai ``` ```python from pydantic_ai import Agent from pydantic_ai.models.openai import OpenAIChatModel from pydantic_ai.providers.openai import OpenAIProvider model = OpenAIChatModel( "google/gemma-4-31b-it", provider=OpenAIProvider( base_url="https://api.aiand.com/v1", api_key="sk-your-api-key", ), ) ``` ## A typed agent ```python agent = Agent(model, system_prompt="Be concise.") result = await agent.run("What is the capital of France?") print(result.output) ``` ## Structured outputs ```python from pydantic import BaseModel class Contact(BaseModel): name: str email: str agent = Agent(model, output_type=Contact) result = await agent.run("From: Jane Doe ") print(result.output.name, result.output.email) ``` The schema is sent to ai& as the parameters of a strict `final_result` tool, and the tool call's arguments are parsed back into the Pydantic model. ## Tools ```python agent = Agent(model) @agent.tool_plain def get_weather(city: str) -> str: """Get current weather for a city.""" return f"Sunny in {city}, 22°C" result = await agent.run("What's the weather in Tokyo?") print(result.output) ``` --- # Vercel AI SDK Source: https://aiand-docs-staging.pages.dev/integrations/vercel-ai/ The Vercel AI SDK connects to ai& through `@ai-sdk/openai-compatible`. ## Setup ```bash npm install ai @ai-sdk/openai-compatible ``` ```ts import { createOpenAICompatible } from "@ai-sdk/openai-compatible"; export const aiand = createOpenAICompatible({ name: "aiand", baseURL: "https://api.aiand.com/v1", apiKey: process.env.AIAND_API_KEY, includeUsage: true, }); ``` Create an API key in the [ai& console](https://console.aiand.com) and set `AIAND_API_KEY`. ## Generate text ```ts import { generateText } from "ai"; import { aiand } from "./client"; const { text } = await generateText({ model: aiand("google/gemma-4-31b-it"), prompt: "Tell me a haiku about Tokyo.", }); console.log(text); ``` A system prompt belongs in `instructions`, not a `{ role: "system" }` message. ## Stream to the browser (Next.js App Router) ```bash npm install @ai-sdk/react ``` ```ts // app/api/chat/route.ts import { convertToModelMessages, createUIMessageStreamResponse, streamText, toUIMessageStream, type UIMessage } from "ai"; import { aiand } from "@/lib/aiand"; export async function POST(req: Request) { const { messages }: { messages: UIMessage[] } = await req.json(); const result = streamText({ model: aiand("google/gemma-4-31b-it"), messages: await convertToModelMessages(messages), }); return createUIMessageStreamResponse({ stream: toUIMessageStream({ stream: result.stream }), }); } ``` ```tsx // app/page.tsx "use client"; import { useChat } from "@ai-sdk/react"; import { useState } from "react"; export default function Page() { const { messages, sendMessage } = useChat(); const [input, setInput] = useState(""); return (
{ e.preventDefault(); sendMessage({ text: input }); setInput(""); }} > {messages.map((m) => (
{m.role}:{" "} {m.parts.map((part, i) => (part.type === "text" ? {part.text} : null))}
))} setInput(e.target.value)} />
); } ``` ## Tools ```ts import { generateText, tool } from "ai"; import { z } from "zod"; import { aiand } from "./client"; const result = await generateText({ model: aiand("google/gemma-4-31b-it"), tools: { getWeather: tool({ description: "Get current weather", inputSchema: z.object({ city: z.string() }), execute: async ({ city }) => `Sunny in ${city}, 22°C`, }), }, prompt: "What's the weather in Tokyo?", }); ``` ## Reasoning Reasoning models accept a level via `providerOptions`. Supported levels per model are in the `reasoning_efforts` field of the [model catalog](https://api.aiand.com/v1/models). ```ts const { text } = await generateText({ model: aiand("moonshotai/kimi-k3"), prompt: "Solve this step by step: What is 15 * 23?", providerOptions: { aiand: { reasoningEffort: "high", }, }, }); ``` --- # Migrating from Anthropic Source: https://aiand-docs-staging.pages.dev/migrating-anthropic/ The ai& Inference API exposes Anthropic-compatible endpoints alongside its OpenAI surface. If you're using the `anthropic` SDK, you can migrate by changing the **base URL** and the **API key**. ## Change two things ```python from anthropic import Anthropic client = Anthropic( base_url="https://api.aiand.com", api_key="sk-your-aiand-api-key", ) ``` ```ts import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic({ baseURL: "https://api.aiand.com", apiKey: "sk-your-aiand-api-key", }); ``` The `anthropic-version` header the SDK sends automatically tells ai& to respond with Anthropic-shape payloads — including on the dual-shape Files endpoints. ## Supported endpoints | Anthropic endpoint | ai& path | Notes | |--------------------|----------|-------| | `/v1/messages` | [Messages](/api/messages/) | Streaming + tool use + vision | | `/v1/files` | [Files](/api/files/) | Anthropic shape selected by `anthropic-version` header | ## What's different - **API keys** start with `sk-` (not `sk-ant-`) — see [Authentication](/authentication/). - **Model IDs** differ from Anthropic's. List them via the [catalog](/models/catalog/) or `client.models.list()` on the OpenAI surface. --- # Migrating from OpenAI Source: https://aiand-docs-staging.pages.dev/migrating-openai/ The ai& Inference API is wire-compatible with the OpenAI Chat Completions, Responses, and Models APIs. If you have an OpenAI integration already, you can usually migrate by changing the **base URL** and the **API key**. ## Change two things ```python from openai import OpenAI client = OpenAI( base_url="https://api.aiand.com/v1", api_key="sk-your-aiand-api-key", ) ``` ```ts import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.aiand.com/v1", apiKey: "sk-your-aiand-api-key", }); ``` Everything else — `chat.completions.create`, streaming, tool calls, structured outputs — works as-is. ## Supported endpoints | OpenAI endpoint | ai& path | Notes | |-----------------|----------|-------| | `/v1/chat/completions` | [Chat Completions](/api/chat-completions/) | Full parity | | `/v1/models` | [Models](/api/models/) | Public; org-currency prices with a key, USD without | | `/v1/files` | [Files](/api/files/) | Same multipart upload shape | | `/v1/uploads` | [Uploads](/api/uploads/) | Chunked uploads for large files | OpenAI's legacy `/v1/completions` is not available: send the prompt as a user message to `/v1/chat/completions` instead. Requests to it return `410` with the error code [`endpoint_removed`](/errors/#common-error-codes). ## What's different - **API keys** start with `sk-` like OpenAI's but are issued by ai& — see [Authentication](/authentication/). - **Model IDs** differ. List them with `client.models.list()` or see the [catalog](/models/catalog/). - **Org scoping** — pass `X-Org-ID` when using JWT auth. API keys carry their org automatically. --- # Automatic Model Selection Source: https://aiand-docs-staging.pages.dev/models/auto/ `auto` is a reserved model name. Send it in `model` and ai& picks a concrete model for that request, then serves it normally — same request and response shapes, same streaming behaviour, same billing rules as naming the model yourself. It exists so a workload with mixed traffic doesn't have to send every request to its most expensive model. Short lookups resolve to a cheap model; work that needs reasoning resolves to one that can do it. ## Using it ```bash curl -i https://api.aiand.com/v1/chat/completions \ -H "Authorization: Bearer sk-your-api-key" \ -H "Content-Type: application/json" \ -d '{ "model": "auto", "messages": [{"role": "user", "content": "What is the capital of Japan?"}] }' ``` ```python from openai import OpenAI client = OpenAI(base_url="https://api.aiand.com/v1", api_key="sk-...") response = client.chat.completions.with_raw_response.create( model="auto", messages=[{"role": "user", "content": "What is the capital of Japan?"}], ) print(response.headers["x-model"]) print(response.parse().choices[0].message.content) ``` ```ts import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.aiand.com/v1", apiKey: "sk-..." }); const { data, response } = await client.chat.completions .create({ model: "auto", messages: [{ role: "user", content: "What is the capital of Japan?" }], }) .withResponse(); console.log(response.headers.get("x-model")); console.log(data.choices[0].message.content); ``` `auto` works on `/v1/chat/completions`, `/v1/responses`, and `/v1/messages`. ## Knowing which model ran The `X-Model` response header carries the resolved model's catalog name, lowercased — compare it case-insensitively. It is present on streaming responses too — response headers arrive before the first body chunk, so you can read it as soon as the stream opens. ``` X-Model: google/gemma-4-31b-it ``` The header is exposed via CORS, so browser clients can read it. Your [request logs](/analytics/logs/) and usage records also store the resolved name — never `auto` — so cost attribution and analytics work without knowing selection happened. See [Response Headers](/reference/response-headers/). ## How the model is chosen Three steps, in order: 1. **Filter** to models your organization can call for _this_ request. A model is eligible only if it has been added to the automatic-selection pool, your plan entitles you to it, the prompt fits its context window, and it supports every input modality in the request (image, video, audio). 2. **Order** what survives by capability, least capable first. Where two models are equally capable, the cheaper one comes first. 3. **Select** by complexity. The request is graded into one of four tiers, and the tier picks a position in that ordered list. | Tier | Resolves toward | Typical request | | ----------- | -------------------------------- | --------------------------------------------------------------- | | `simple` | the least capable eligible model | greetings, acknowledgements, a factual lookup | | `medium` | lower-middle of the list | everyday requests needing some explanation | | `complex` | upper-middle of the list | non-trivial code, multi-step analysis, domain expertise | | `reasoning` | the most capable eligible model | proofs, derivations, debugging from symptoms, design trade-offs | Capability is ranked from published benchmark results, not from price — the two do not reliably track each other, and a model being expensive is not evidence that it is stronger. A model we have not yet ranked is not selected by `auto` at all; name it directly to use it. **The pool is curated so that capability and price rise together**, which is why "a cheap model" and "the least capable eligible model" describe the same choice above rather than competing ones. A model earns a place only if nothing cheaper is at least as capable, so ordering the pool by capability also orders it by cost. Ranking on benchmarks rather than on price is what keeps that true as the catalog moves: a model that grows more expensive without growing stronger loses its place instead of drifting up the list. At the `reasoning` tier, selection first narrows to models tagged with the `reasoning` capability when your organization has any. **Judging the difficulty usually involves a second model.** Greetings and acknowledgements are recognised locally and resolve to the least capable model in the pool. Everything else is graded by a small model we choose, which is the only difficulty judgement involved — there is no local scoring of hard requests, because a keyword list cannot grade one reliably in every language. Where that model is asked, the text of your request — clipped, with your system instructions labelled separately — is sent to it before the model that answers you runs. It comes from our catalog and need not be a model your organization otherwise calls, so it can be a different provider. Under [zero data retention](/organizations/zero-data-retention/) your text goes only to a grading model ai& serves; if the one we have chosen is not, the request is not graded and takes the middle of the pool. The classification is never billed to you and never appears in your usage. A small random sample of other requests is also classified in the background for calibration. If your data handling requires that a request only reaches the model you named, name it. **`auto` does not accept `reasoning_effort`.** Sending one is a 400 — name a model directly if you want to choose the effort. The ranking above comes from each model's published index, and that index is recorded at one configuration per model, so an effort you picked would move models off the very point the ranking describes. `auto` chooses the model and leaves the effort at the model's own default, which is the setting its ranking was measured at. `reasoning_effort: "none"` is rejected with the rest, and for the sharpest version of that reason: a reasoning model with its thinking suppressed is the furthest it ever gets from the point it was ranked at. The same rule covers `reasoning.effort` on `/v1/responses` — whichever name your endpoint gives the field. Anthropic's `thinking: { "type": "disabled" }` on [`/v1/messages`](/api/messages/) is ignored here rather than rejected: it is not you naming an effort, so the request still routes, and the chosen model keeps its default. Name a model that publishes `none` if you need reasoning off. **With no credit left, selection narrows to free models first**, whatever the tier — choosing a paid model would only produce an insufficient-credit error on a model you didn't name. If nothing in your pool is free, you get that error as usual. The pool is per-organization, so two organizations sending the same prompt can resolve to different models. ## Billing You are charged the resolved model's price, at its normal input and output rates. There is no surcharge for using `auto`, and no separate line item — the charge appears against the model that ran. This means **the cost of an identical request can differ between calls** if the pool changes. Budget against your [usage](/billing/usage/) totals rather than a single model's rate. ## Conversations `auto` decides per request, so a multi-turn conversation is re-decided on every turn. A follow-up that reads as harder than the turn before it can resolve to a different model, and `X-Model` can change during a thread. That has a cost worth knowing about: the [prompt-prefix discount](/models/pricing/) only applies while a conversation keeps going to the same model, so a thread that switches models loses it for that turn. If you need one model for a whole conversation, name it — a named model is never re-routed. ## Other limits - **If the grading model is unavailable, requests take the middle of the pool.** On a timeout or an outage there is no difficulty judgement, so anything that isn't a recognised greeting resolves to a mid-capability eligible model rather than being guessed at. That is deliberately not the weakest one — it means a hard request is never quietly downgraded — but it does mean `X-Model` can differ from what the same request usually resolves to. Name a model directly if you need that fixed. - **`auto` is not listed in `GET /v1/models`.** It's a routing instruction, not a model, so it has no capabilities, context window, or price of its own. - **The pool changes.** Models are added to and removed from it as the catalog evolves. Don't build logic that assumes a given prompt always lands on a given model. - **Capability requirements narrow it, they don't override it.** A request with an image only considers models that accept images. If nothing in your pool does, the request is rejected rather than silently dropping the image. ## Errors | Status | Message | Cause | | ------ | ------------------------------------------------------ | ------------------------------------------------------------------------------------------------------------------------------------------ | | 400 | `Model 'auto' is not supported.` | Automatic selection is not enabled. Name a model directly. | | 400 | `No model available to 'auto' can serve this request.` | Nothing in your pool satisfies the request — commonly a modality or context-window fit. | | 400 | `Model 'auto' does not accept reasoning_effort.` | `auto` chooses the effort along with the model. Name a model directly to set one yourself. | All are the ordinary `invalid_request_error` shape documented under [Errors](/errors/), with `param` set to `model` — or to the effort field you sent, for the last one. --- # Model Catalog Source: https://aiand-docs-staging.pages.dev/models/catalog/ ai& currently serves the open-weight models below. The list is dynamic — each organization sees only what it has access to, so the source of truth is always `GET /v1/models`. ## Quick picks This page computes the picks live: the lowest output price, the models that take images, video or documents, and the largest context window. `GET /v1/models` returns the data they come from. ## Available models This page renders the table live. `GET /v1/models` returns every model with its capabilities, context window and prices. Sorted by output price, low to high. Prices are USD per million tokens — see [Pricing](/models/pricing/) for how the formula computes a per-request cost. To have ai& pick one of these per request instead of choosing yourself, see [Automatic Selection](/models/auto/). ## Capability values | Capability | Meaning | | -------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `reasoning` | Emits internal reasoning tokens (charged as output) and accepts `reasoning_effort`. The values it takes are listed in that model's `reasoning_efforts`, since models don't share one vocabulary. Reasoning can be turned off only if its levels include `none`. | | `tool_calling` | Accepts `tools` / `tool_choice`. See [Tool Calling](/capabilities/tool-calling/). | | `vision` | Accepts image inputs (`image_url` or `file_id`). See [Vision](/capabilities/vision/). | | `video` | Accepts video inputs by `file_id`. See [Video Understanding](/capabilities/video/). | | `document` | Accepts PDF / document inputs. | A capability absent from the list means requests using that feature will be rejected at validation time. ## Listing your models programmatically ```bash curl https://api.aiand.com/v1/models ``` ```python from openai import OpenAI client = OpenAI(base_url="https://api.aiand.com/v1", api_key="sk-...") for m in client.models.list().data: print(m.id, m.context_window, m.capabilities) ``` ```ts import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.aiand.com/v1", apiKey: "sk-..." }); const { data } = await client.models.list(); for (const m of data) console.log(m.id, m.context_window, m.capabilities); ``` ```bash aiand models aiand models --capability vision --sort output ``` Prices are per 1M tokens in your billing currency. See [ai& CLI](/cli/). ## Response shape (OpenAI surface) ```json { "object": "list", "data": [ { "id": "google/gemma-4-31b-it", "name": "google/gemma-4-31b-it", "object": "model", "created": 1775474514, "owned_by": "ai&", "provider": "google", "context_window": 262144, "max_output_tokens": 32768, "capabilities": [ "reasoning", "tool_calling", "vision", "video", "document" ], "reasoning_efforts": [ "none", "high" ], "reasoning_effort_default": "none", "description": "Largest Gemma 4 instruction model for open, self-hosted chat and reasoning", "currency": "usd", "input_per_1m": "0.200000", "output_per_1m": "0.500000", "cached_input_per_1m": "0.050000", "deprecation_date": null } ] } ``` | Field | Type | Meaning | | -------------------------- | ---------------- | ---------------------------------------------------------------------------------------------------------------------------------------------- | | `id` | string | The value to pass as `"model"` in chat / responses / messages. | | `provider` | string | The lab that released the open weights (`openai`, `google`, `qwen`, …). All models are hosted on ai& infrastructure regardless of provider. | | `context_window` | int | Max combined input + output tokens. | | `max_output_tokens` | int \| null | Recorded maximum generation length. `null` when no limit is recorded for the model. The list does not fill in a guess. | | `capabilities` | string[] | Supported features — see the table above. | | `reasoning_efforts` | string[] \| null | The `reasoning_effort` values this model runs. Another known level runs at the next listed level up, or the highest, which can cost more than the level you sent; [`X-Reasoning-Effort`](/reference/response-headers/) reports the one applied. A model can turn reasoning off only if its levels include `none`, and a value outside the effort vocabulary is a 400. `null` only on models without the `reasoning` capability. | | `reasoning_effort_default` | string \| null | The level to default a UI to for this model. Not applied server-side — omitting `reasoning_effort` leaves the upstream's own default in place. | | `currency` | string | Your organization's billing currency (`usd` or `jpy`), set at org creation. `usd` when called without an API key. | | `input_per_1m` | string | Price per **1 million** input tokens, in your billing currency. Numeric stored as a string for precision. | | `output_per_1m` | string | Price per **1 million** output tokens, in your billing currency. | | `deprecation_date` | string \| null | When the model stops serving requests, as an ISO 8601 timestamp. `null` when no retirement is scheduled. | ## Anthropic surface `GET /v1/models` with an `anthropic-version` header returns the Anthropic shape — `data[].display_name`, `created_at`, `has_more`, `first_id`, `last_id`. Pricing and capabilities are only on the OpenAI surface. --- # Pricing Source: https://aiand-docs-staging.pages.dev/models/pricing/ ai& bills inference per token, deducted from your prepaid credit balance after each request completes. Top-up via [Credits & Top-Up](/billing/credits/). ## The formula ``` cost = (input_tokens / 1_000_000) × input_per_1m + (output_tokens / 1_000_000) × output_per_1m ``` Both `input_per_1m` and `output_per_1m` are **prices per 1 million tokens in your organization's billing currency** (USD or JPY, fixed at org creation), served on each model row via `GET /v1/models` together with the `currency` field. Called without an API key, the endpoint returns USD prices. A model is only listed and callable if it has a price in your currency. ## Prompt caching On models with a cached input rate, that rate applies when a request's messages begin with the complete messages of an earlier request from your organization, to the same model with the same `tools`. A single-string `prompt` or `input` qualifies only on an exact repeat; appending text does not. In a conversation, each turn's messages begin with the previous turn's, so that portion is discounted. No `cache_control` markers or code changes are required. When a model offers a cached rate, the cost becomes: ``` cost = (cached_tokens / 1_000_000) × cached_input_per_1m + (input_tokens − cached_tokens) / 1_000_000 × input_per_1m + (output_tokens / 1_000_000) × output_per_1m ``` The cached token count is reported back in the response `usage`, under the field your SDK expects: | API | Field | |-----|-------| | OpenAI Chat Completions | `usage.prompt_tokens_details.cached_tokens` | | OpenAI Responses | `usage.input_tokens_details.cached_tokens` | | Anthropic Messages | `usage.cache_read_input_tokens` | Streaming responses report the same count as `tokens.cached` in the `event: metrics` trailer (with `X-Aiand-Metrics: true`). - Caching applies to prompts of **1024 tokens or more**, counted in **128-token increments**. - Reuse is scoped to your organization and lasts roughly **10 minutes** of inactivity. - Only models with a cached input rate report `cached_tokens`; on other models all input bills at the standard rate and the field is omitted. ## Per-request reporting Send the request header `X-Aiand-Metrics: true` to receive the exact computed cost with each response: - **Non-streaming**: `X-Cost` + `X-Cost-Currency` headers. See [Response Headers](/reference/response-headers/). - **Streaming**: a trailing `event: metrics` SSE event after the model's terminal message. Its payload includes the token counts, `cost`, and a `currency` field telling you what currency the cost is denominated in. See [Streaming Events](/reference/streaming-events/). You can also query usage and cost retrospectively via the [Analytics](/analytics/metrics/) and [Request Logs](/analytics/logs/) endpoints. ## When you're charged | Outcome | Charged? | |---------|----------| | 2xx response with output | Yes — `(input × rate) + (output × rate)` | | 4xx before model dispatch (validation, auth, rate limit, no credit) | No | | 5xx after partial output streamed | Yes for what was billed; the cost reflects what was produced | | Stream cancelled mid-flight | Yes for tokens already produced | Cost is deducted atomically when the request settles — concurrent requests can't overdraw. ## Tiers Token rates are the same across tiers. What differs is throughput — see [Rate Limits](/reference/rate-limits/) for per-tier RPM/TPM caps. --- # Organization API Keys Source: https://aiand-docs-staging.pages.dev/organizations/api-keys/ API keys are the recommended credential for server-to-server use. Each key is scoped to a single organization and automatically resolves that org's billing account on every request — no `X-Org-ID` header needed. ## Key format All keys start with `sk-` and are shown **once** at creation time. Store them in your secret manager — they can't be retrieved again. ## Creating a key ```bash curl -X POST https://api.aiand.com/api/v1/keys \ -H "Authorization: Bearer " \ -H "X-Org-ID: " \ -H "Content-Type: application/json" \ -d '{"name": "production-server"}' ``` Or use the console: Settings → API Keys → Create. On your own machine, `aiand login` from the [ai& CLI](/cli/) creates a key named `aiand@` after you approve the sign-in in your browser, and stores it for you. ## Managing keys | Action | Endpoint | |--------|----------| | List keys | `GET /api/v1/keys` | | Create key | `POST /api/v1/keys` | | Update key (name, status) | `PATCH /api/v1/keys/{key_id}` | | Revoke key | `DELETE /api/v1/keys/{key_id}` | All management endpoints require JWT auth and `X-Org-ID`. ## Rotation Keys don't auto-expire, except those created by `aiand login`: they last 30 days, and the [ai& CLI](/cli/) rotates them automatically. Rotate other keys when: - A team member with key access leaves. - A key is exposed (leaked logs, accidental commit). - Compliance policy requires periodic rotation. Create the new key first, deploy it, then revoke the old one — this avoids downtime. --- # Organizations Source: https://aiand-docs-staging.pages.dev/organizations/concepts/ ai& is multi-tenant by design. Every request runs in the context of an **organization**, and all billing, API keys, rate limits, and request history are scoped to that org. ## The data model - **User** — an individual account, identified by email. Created on first OAuth or magic-link sign-in. - **Organization** — a billing and access boundary. Owns credits, API keys, files, request logs. - **Membership** — connects a user to an organization with a **role** (`owner`, `admin`, `member`). A user can belong to multiple orgs. - **API Key** — issued at the org level. Carries the org context automatically; no `X-Org-ID` needed when authing with an API key. - **Billing Account** — one per org. Holds credit balance, payment methods, invoice history. ## Picking the right org - **API key auth** — the key already encodes the org; everything happens in that org. - **JWT (browser) auth** — pass `X-Org-ID` on each request to select the active org. See [Switching Orgs](/organizations/switching/). ## Per-org scoping Each of these is independent across organizations: - Credit balance and payment methods - API keys - Uploaded files - Request logs and analytics - Rate-limit buckets (see [Rate Limits](/reference/rate-limits/)) --- # Members & Roles Source: https://aiand-docs-staging.pages.dev/organizations/members/ Organizations support multiple members. Each member has a **role** that determines what they can do. ## Roles | Role | Inference | API keys | Billing | Members | Org settings | |------|-----------|----------|---------|---------|--------------| | `owner` | ✓ | manage | manage | manage | manage | | `admin` | ✓ | manage | view | manage | view | | `member` | ✓ | own keys only | view | view | view | - **Owners** can transfer ownership and delete the org. There is always at least one owner. - **Admins** can invite/remove members and rotate keys but cannot change billing ownership. - **Members** can call the API and create API keys for their own use. ## Inviting members Invite from the [console](https://console.aiand.com) — Settings → Members → Invite. The invitee receives an email; once they accept, the membership is created with the role you chose. ## Removing members Removing a member revokes their access immediately. API keys they created **remain valid** until explicitly rotated — see [Organization API Keys](/organizations/api-keys/). --- # Switching Orgs Source: https://aiand-docs-staging.pages.dev/organizations/switching/ A user can belong to multiple organizations. The org used for a given request determines the billing account, rate limits, model access, and visible files. ## With API keys API keys are scoped to a single org at creation time. **The key chooses the org** — no extra header needed. ```bash curl https://api.aiand.com/v1/chat/completions \ -H "Authorization: Bearer sk-..." \ -d '{...}' ``` To act on a different org, use that org's API key. The [ai& CLI](/cli/) works the same way: its key belongs to the org chosen at sign-in. Keep one [profile](/cli/#organizations-and-profiles) per org and pick it with `--profile`, as in `aiand --profile work usage`. ## With JWT (browser / console) JWTs identify the user, not an org. Pass `X-Org-ID` on every request to select the active org: ```bash curl https://api.aiand.com/api/v1/keys \ -H "Authorization: Bearer " \ -H "X-Org-ID: " ``` Requests without `X-Org-ID` on JWT-authed endpoints return `400`. ## Listing your orgs ```bash curl https://api.aiand.com/api/v1/me \ -H "Authorization: Bearer " ``` The response includes a `memberships` array with each org's `id`, `name`, and your `role` in it. From a terminal, `aiand orgs` lists the same organizations. --- # Zero Data Retention Source: https://aiand-docs-staging.pages.dev/organizations/zero-data-retention/ Zero data retention is a setting on an organization. Under it, ai& keeps your prompts, responses and files only in memory, for as long as it takes to answer each request, and writes none of them to storage. Without it, ai& stores content only where a feature needs it: request bodies while [content logging](/analytics/logs/#content-logging) is on, inline PDFs and uploaded files as [files](/api/file-lifecycle/), and video jobs with their prompt and video. Each is deleted within 30 days. It is off until you turn it on. You can turn it on when you set up your account, and after that organization admins turn it on or off in the [console](https://console.aiand.com/settings/organizations) under **Settings → Organizations**. A change applies to every API key and console session of the organization within about a minute. Where an enterprise agreement requires it, ai& locks the setting, and then only your account team can change it. ## What changes | Feature | Under zero data retention | | --- | --- | | Chat Completions, Responses, Messages | Work as usual. `/v1/responses` never stores response objects, for any organization. | | Inline images | Work as usual. They are passed to the model and not stored. | | Inline PDFs | Work, but are rendered in memory instead of being stored as a file. See [Inline PDFs](#inline-pdfs). | | Request logging | Off, and cannot be switched on. `GET /logs/settings` reports `"data_retention": "zero"`, and a `PATCH` that switches it on returns `403`. | | Files and Uploads APIs | Uploading returns `403`. Files stored before the setting was turned on are deleted, as described [below](#turning-it-on-for-an-existing-organization). | | Video generation | Refused with `403`, since a video job stores its prompt and the rendered video. | | Models served by an outside provider | Refused with `403`, since the provider's own retention would apply. [`auto`](/models/auto/) only routes to models ai& serves, and sends your request to no outside provider to classify it. | A refused request returns: ```json { "error": { "message": "File upload stores content, so it is unavailable to an organization on zero data retention.", "type": "authorization_error", "param": null, "code": "zero_data_retention" } } ``` ## Inline PDFs A PDF sent inline (`file_data` on Chat Completions or Responses, or a `base64` `document` block on Messages) is rendered page by page in memory and passed to the model as images. No file is created, and nothing is reused between requests, so a PDF you send again is rendered again. The limits are tighter than for other organizations, because the pages are held in memory for the life of the request: - at most 100 pages per PDF; - at most 16 MiB of PDFs per request; - at most 10 MiB of rendered page images per request. When PDFs in earlier messages take a request over either per-request limit, it is refused like one over the model's context window: `400` with code `context_length_exceeded`, or `prompt is too long` on Messages, so an agent can compact its history. When the newest message's PDFs are over a limit on their own, the request is refused with `400` and code `invalid_value`. ## What is still kept Zero data retention covers the content of your requests. These are kept because billing and operations depend on them, and none of them hold prompt or response text: - **Request metadata**: model, token counts, cost, status and timing for each request, the same rows shown in [Request Logs](/analytics/logs/). Kept for 365 days. - **Prompt caching**: to price cached input tokens, ai& keeps a one-way hash of each prompt prefix with its token count for 10 minutes. The hash is not the text. - **The model's working memory**: while generating, the model keeps intermediate state for your prompt (its KV cache) in GPU and server memory. It is never written to storage and is evicted within a day. - **Operational logs**: request IDs, status codes and timings. They never contain prompts or responses. ## Turning it on for an existing organization Turning it on deletes what the organization has already stored, and turning it off later does not bring it back. The console shows how many files and videos will be deleted before you confirm, and offers stored request logs as a download ([`GET /logs/export`](/analytics/logs/#export-stored-content)). - Files are deleted within 15 minutes, and uploads in progress are cancelled. - Stored request logs and finished videos are deleted within a day. A video still being generated finishes and is deleted within a day of completing. Content we are required to preserve, such as under a legal hold, is kept until the hold is released. Moderation records of past video requests, which include the prompt, are kept for 30 days as evidence against abuse and then deleted. ## Turning it off Request logging stays off until an admin turns it on again under **Settings → Organizations**. Nothing deleted while the setting was on comes back. --- # Rate Limits Source: https://aiand-docs-staging.pages.dev/reference/rate-limits/ ai& enforces rate limits per organization to keep the platform healthy under shared load. Limits depend on your tier and apply to inference endpoints; management APIs have their own (looser) limits. ## Tiers | Tier | Description | |------|-------------| | **Tier 0** | Evaluation tier. Lower per-minute caps. Suitable for development and small projects. New orgs start here. | | **Tier 1** | Production tier. Higher caps and access to higher-throughput models. Orgs are promoted on their first successful payment. | ## What's measured Each request is checked against six buckets — four per-model and two per-org global: - **Per-model RPM** — requests per minute against one specific model. - **Per-model Input TPM** — input tokens per minute (estimated up-front from the request body). - **Per-model Output TPM** — output tokens per minute (charged after the response completes). - **Per-model Concurrency** — max in-flight requests against one model. - **Global RPM** — requests per minute across **all** models you call. - **Global Concurrency** — max in-flight requests across all models. Whichever bucket fills first triggers throttling. ## Headers Every authenticated inference request that reaches model admission reports each bucket that applies to you — including `200` responses, streams, and `429` responses from one of these six buckets. The first two rows use OpenAI's header names and formats unchanged, so tools that already read them keep working; the rest extend the same pattern. | Bucket | Headers | |--------|---------| | Per-model RPM | `x-ratelimit-limit-requests`, `x-ratelimit-remaining-requests`, `x-ratelimit-reset-requests` | | Per-model Input TPM | `x-ratelimit-limit-tokens`, `x-ratelimit-remaining-tokens`, `x-ratelimit-reset-tokens` | | Per-model Output TPM | `x-ratelimit-limit-output-tokens`, `x-ratelimit-remaining-output-tokens`, `x-ratelimit-reset-output-tokens` | | Per-model Concurrency | `x-ratelimit-limit-concurrency`, `x-ratelimit-remaining-concurrency` | | Global RPM | `x-ratelimit-limit-global-requests`, `x-ratelimit-remaining-global-requests`, `x-ratelimit-reset-global-requests` | | Global Concurrency | `x-ratelimit-limit-global-concurrency`, `x-ratelimit-remaining-global-concurrency` | - `limit-*` is the bucket's cap and `remaining-*` what is left in it right now. `remaining-tokens` counts input only — output tokens are charged after the response completes. - `reset-*` is the time until that bucket is full again, as a duration like `45s` or `6m0s`. Concurrency buckets have no reset: they free up as in-flight requests finish. - A bucket your tier has no cap on (an Input/Output TPM or global cap of `0`) is omitted. A `limit-requests` or `limit-concurrency` of `0` means the opposite — that model is closed to your tier, and no wait will open it. On a `429 Too Many Requests` from one of these six model buckets, two more headers are added: | Header | Meaning | |--------|---------| | `X-RateLimit-Policy` | Which bucket denied the request: `rpm`, `global_rpm`, `input_tpm`, `output_tpm`, `concurrency`, or `global_concurrency`. | | `Retry-After` | Seconds until this request fits — shorter than `reset-*`, which waits for the whole bucket. Omitted for `concurrency` and `global_concurrency` rejects — finish or cancel in-flight requests instead. | The earlier `X-RateLimit-Limit` and `X-RateLimit-Remaining` headers remain as deprecated compatibility aliases. New integrations should use the explicit headers above. Authentication throttles happen before model admission and carry `Retry-After` without `X-RateLimit-Policy` or bucket headers. Public catalog endpoints such as `GET /v1/models` are not metered this way and carry none of these headers. See [Response Headers](/reference/response-headers/) for non-rate-limit headers. ## 429 responses When throttled, ai& returns `429 Too Many Requests`. The two kinds of bucket clear differently, so read `X-RateLimit-Policy` and handle the reject by its type — the same "back off and retry" is wrong for a concurrency reject. **Time-based rejects** — `rpm`, `global_rpm`, `input_tpm`, `output_tpm`. These refill as the window advances. `Retry-After` gives the minimum safe delay: wait at least that long, then retry with exponential backoff and jitter. **Concurrency rejects** — `concurrency`, `global_concurrency`. These clear only when in-flight requests finish or are cancelled, not with time, so no `Retry-After` is sent. Let existing requests drain, or lower your concurrency, before sending more. --- # Response Headers Source: https://aiand-docs-staging.pages.dev/reference/response-headers/ ai& exposes request metadata through HTTP headers rather than modifying the OpenAI- or Anthropic-shaped response body. This preserves wire-format compatibility with the official SDKs. ## Identity & tracing | Header | Description | | -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `X-Request-ID` | Unique ID for this request on traced API routes (inference, auth, billing, and files). Include it in support tickets. | | `traceparent` | [OpenTelemetry trace context](https://opentelemetry.io/docs/concepts/context-propagation/) header. On traced inference routes and auth/files/billing HTTP requests, ai& accepts an incoming value or starts a new trace, propagates it downstream where applicable, and returns the server's trace context so you can correlate distributed traces. | | `X-Model` | The model that served the request, as its lowercased catalog name. This is the resolved name, so it can differ in casing from what you sent — and with the reserved `auto` name it is the only place the chosen model is reported. Compare it case-insensitively. | ## Cost metrics (opt-in) Cost and timing headers are **not** sent by default. To receive them, send the request header `X-Aiand-Metrics: true`. They are emitted on non-streaming responses only: | Header | Description | | ----------------- | ---------------------------------------------------------------------- | | `X-Cost` | Cost of this request in your billing currency. | | `X-Cost-Currency` | The currency `X-Cost` is denominated in (`usd` or `jpy`). | | `X-Inference-Ms` | Time the upstream model spent producing the response, in milliseconds. | Token counts are not exposed as headers — read the `usage` block in the response body. ## Reasoning effort | Header | Description | | -------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `X-Reasoning-Effort` | The effort level actually applied, on requests that asked for one — `reasoning_effort` on `/v1/chat/completions` and `/v1/messages`, `reasoning.effort` on `/v1/responses`. On [`/v1/messages`](/api/messages/) a named reasoning model treats `thinking: { "type": "disabled" }` as asking, and reports `none`. The reserved [`auto`](/models/auto/) model ignores that block, so the header is absent. Always one of the model's published levels. Absent if you didn't ask — the model's own default then applies, and we don't substitute one for you. | A level a model doesn't list — see [`reasoning_efforts`](/models/catalog/) — runs at the next listed level up, or the model's highest if none is above it. This header then differs from what you sent, and the reasoning tokens you are billed for are the applied level's, which can cost more than the one you asked for. `none` is never rounded: on a model that doesn't list it the request is a 400, as is a value outside the effort vocabulary. ## Rate limiting See [Rate Limits](/reference/rate-limits/) for the full set of rate-limit headers. ## Streaming Streaming responses carry `X-Request-ID` and the rate-limit headers immediately but never carry cost headers. With `X-Aiand-Metrics: true`, chat-completion streams append a trailing `event: metrics` SSE event with tokens, cost, and currency after the model's terminal message. See [Streaming Events](/reference/streaming-events/). --- # Streaming Events Source: https://aiand-docs-staging.pages.dev/reference/streaming-events/ When you request a streaming response, ai& streams the model's SSE events unchanged, apart from two small repairs on `/v1/responses` described below. If the request carries the opt-in header `X-Aiand-Metrics: true`, ai& appends a single **trailer event** named `metrics` carrying token counts, cost, and timing. ## OpenAI shape ``` data: {"id":"chatcmpl-...","object":"chat.completion.chunk","choices":[{"delta":{"content":"Hel"}}]} data: {"id":"chatcmpl-...","object":"chat.completion.chunk","choices":[{"delta":{"content":"lo"}}]} data: {"id":"chatcmpl-...","object":"chat.completion.chunk","choices":[{"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":7,"completion_tokens":2,"total_tokens":9}} data: [DONE] event: metrics data: {"tokens":{"input":7,"output":2,"total":9,"cached":0},"cost":0.000018,"currency":"usd","ttft_ms":120,"inference_ms":850} ``` The `metrics` event is emitted **after** `[DONE]`. Robust clients should keep the connection open until the stream closes naturally. ## The trailer event | Field | Description | |-------|-------------| | `tokens.input` | Input tokens counted toward billing. | | `tokens.output` | Output tokens counted toward billing. | | `tokens.total` | Sum of input and output tokens. | | `tokens.cached` | Input tokens billed at the model's discounted cached rate. `0` when there is no cache hit. See [Prompt caching](/models/pricing/#prompt-caching). | | `cost` | Final cost of the request, in your billing currency. | | `currency` | The currency `cost` is denominated in (`usd` or `jpy`). | | `ttft_ms` | Time to first token, in milliseconds: from sending the request to the model until its first generated text, reasoning or tool call. Metadata-only chunks do not count. `null` when the response generated no output. | | `inference_ms` | Time the upstream model spent producing the response, in milliseconds. | ## Which endpoints emit it The trailer is emitted on `/v1/chat/completions` and `/v1/responses` streams. Anthropic-shaped `/v1/messages` streams do not carry it — read the native `usage` block, or query [Request Logs](/analytics/logs/) for cost. ## Repairs on `/v1/responses` Some models emit two small defects that break strict clients such as Codex, so ai& repairs them on `/v1/responses`: - A message announced in `response.output_item.added` without `content` gets `"content": []`. - In a tool call's final arguments, whole numbers written as floats become integers: `{"yield_time_ms":10000.0}` becomes `{"yield_time_ms":10000}`. This applies to `response.output_item.done`, `response.function_call_arguments.done`, the `output` of `response.completed` and `response.incomplete`, and non-streaming responses. Streamed argument deltas are left as they are, and numbers inside strings are never touched. Both keep the same meaning in JSON, so clients that parse JSON see no difference. ## Why a trailer event? Apart from those repairs, ai& never modifies the response body emitted by the model. Tokens, cost, and timing are delivered as a named SSE event after the terminal message, so the byte stream above the trailer is identical to what the source API would produce — the official OpenAI and Anthropic SDKs work without modification. --- # ai& SDK Source: https://aiand-docs-staging.pages.dev/sdks/aiand/ The official ai& SDKs are generated from the public OpenAPI spec and ship full type definitions. They cover the same OpenAI-compatible surface as the rest of the platform. ## Setup ```bash pip install aiand ``` ```python import aiand configuration = aiand.Configuration(access_token="sk-your-aiand-api-key") client = aiand.OpenaiApi(aiand.ApiClient(configuration)) models = client.list_models() print(models.data[0].id) ``` ```bash npm install @aiand/sdk ``` ```ts import { Configuration, OpenaiApi } from "@aiand/sdk"; const client = new OpenaiApi( new Configuration({ accessToken: "sk-your-aiand-api-key" }), ); const models = await client.listModels(); console.log(models.data[0].id); ``` ## Chat completion ```python response = client.create_chat_completion( aiand.CreateChatCompletionRequest.from_dict({ "model": "google/gemma-4-31b-it", "messages": [{"role": "user", "content": "Hello"}], }) ) print(response.choices[0].message.content) ``` ```ts const response = await client.createChatCompletion({ createChatCompletionRequest: { model: "google/gemma-4-31b-it", messages: [{ role: "user", content: "Hello" }], }, }); console.log(response.choices[0].message.content); ``` ## Responses ```python response = client.create_response( aiand.CreateResponseRequest( model="google/gemma-4-31b-it", input=aiand.ResponseInput("Hello"), ) ) # output[0] is often a reasoning item message = next((item for item in response.to_dict()["output"] if item["type"] == "message"), None) if message: print(message["content"][0]["text"]) else: print(f"No message in the output (status: {response.status})") ``` ```ts const response = await client.createResponse({ createResponseRequest: { model: "google/gemma-4-31b-it", input: "Hello", }, }); console.log(response.output); ``` ## Other surfaces - `list_models()` / `listModels()` — [Models](/api/models/) - Files, uploads, and more follow the same client — see [Files](/api/files/) --- # Anthropic SDK Source: https://aiand-docs-staging.pages.dev/sdks/anthropic/ ai& exposes Anthropic-compatible endpoints alongside its OpenAI surface. Use the official `anthropic` SDKs by overriding the base URL. ## Setup ```bash pip install anthropic ``` ```python from anthropic import Anthropic client = Anthropic( base_url="https://api.aiand.com", api_key="sk-your-aiand-api-key", ) ``` ```bash npm install @anthropic-ai/sdk ``` ```ts import Anthropic from "@anthropic-ai/sdk"; const client = new Anthropic({ baseURL: "https://api.aiand.com", apiKey: "sk-your-aiand-api-key", }); ``` ## Messages ```python response = client.messages.create( model="...", max_tokens=1024, messages=[{"role": "user", "content": "Hello"}], ) print(response.content[0].text) ``` ## Streaming ```python with client.messages.stream( model="...", max_tokens=1024, messages=[{"role": "user", "content": "Hello"}], ) as stream: for text in stream.text_stream: print(text, end="") ``` ## Files The Anthropic Files API works too — selected by the SDK's `anthropic-version` header: ```python file = client.beta.files.upload(file=open("cat.png", "rb")) ``` See [OpenAI vs Anthropic shape](/api/file-shapes/) for how the dual-emit works. --- # curl Source: https://aiand-docs-staging.pages.dev/sdks/curl/ Every ai& endpoint is just HTTP + JSON. Use `curl` to test from a shell, automate from CI, or sanity-check before reaching for an SDK. ## Authentication ```bash export AIAND_API_KEY="sk-your-aiand-api-key" ``` ## Chat completion ```bash curl https://api.aiand.com/v1/chat/completions \ -H "Authorization: Bearer $AIAND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "...", "messages": [{"role": "user", "content": "Hello"}] }' ``` ## Streaming Add `"stream": true` and use `curl -N` to disable buffering: ```bash curl -N https://api.aiand.com/v1/chat/completions \ -H "Authorization: Bearer $AIAND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "...", "stream": true, "messages": [{"role": "user", "content": "Hello"}] }' ``` ## Vision (file_id) ```bash curl https://api.aiand.com/v1/chat/completions \ -H "Authorization: Bearer $AIAND_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "...", "messages": [{ "role": "user", "content": [ {"type": "text", "text": "What is this?"}, {"type": "file", "file": {"file_id": "file-abc123"}} ] }] }' ``` ## File upload ```bash curl https://api.aiand.com/v1/files \ -H "Authorization: Bearer $AIAND_API_KEY" \ -F "file=@cat.png" \ -F "purpose=vision" ``` ## Anthropic shape Add `-H "anthropic-version: 2023-06-01"` and hit `/v1/messages` instead. See [Messages](/api/messages/). --- # OpenAI SDK Source: https://aiand-docs-staging.pages.dev/sdks/openai/ The ai& Inference API is wire-compatible with OpenAI's. Use the official `openai` SDKs by overriding the base URL. ## Setup ```bash pip install openai ``` ```python from openai import OpenAI client = OpenAI( base_url="https://api.aiand.com/v1", api_key="sk-your-aiand-api-key", ) ``` ```bash npm install openai ``` ```ts import OpenAI from "openai"; const client = new OpenAI({ baseURL: "https://api.aiand.com/v1", apiKey: "sk-your-aiand-api-key", }); ``` ## Chat completion ```python response = client.chat.completions.create( model="...", messages=[{"role": "user", "content": "Hello"}], ) print(response.choices[0].message.content) ``` ## Streaming ```python stream = client.chat.completions.create( model="...", messages=[{"role": "user", "content": "Hello"}], stream=True, stream_options={"include_usage": True}, ) for chunk in stream: if chunk.choices: print(chunk.choices[0].delta.content or "", end="") ``` ## Other surfaces - `client.files.create(...)` — [Files](/api/files/) - `client.models.list()` — [Models](/api/models/)