# Account and organizations
Source: https://docs.naga.ac/account/account-management
The dashboard sections, organizations and roles, sign-in options and closing an account.
## Dashboard
The dashboard at [naga.ac/dashboard](https://naga.ac/dashboard) has these sections:
| Section | What it holds |
| - | - |
| Credits | Balance, **Top Up Balance**, top-up history and low balance alerts |
| API Keys | Keys that call models: create, rename, set a spending limit or expiry, disable, delete |
| Provisioning Keys | Keys for the account API: create, rename, disable, delete |
| Metrics | Requests, cost, error rate and latency over time, by key and model |
| Logs | Every request of the last 31 days, with export |
| Referrals | Your referral link and earnings |
| Organization | Organization settings, members and invitations |
| Privacy | Training and data logging settings |
| Account | Profile, linked sign-in methods, password, two-factor authentication, sessions |
## Organizations
An organization has its own balance, API keys and logs, separate from your personal account. Use the workspace switcher at the top of the dashboard to move between your personal account and organizations.
You can create up to 3 organizations, and an organization can have up to 100 members. Invite members from **Organization → Invitations**.
| Role | Can use |
| - | - |
| Admin | Every dashboard section of the organization |
| Member | API Keys, Organization and Account. Members cannot see credits, provisioning keys, metrics, logs or privacy settings |
## Sign-in
You can sign in with email and password, a magic link sent by email, Google, GitHub or Discord. Two-factor authentication is under **Account → Security**. If you lose the second factor, sign in with a backup code at [Recover account](https://naga.ac/auth/recover-account).
## Referrals
Share your referral link from the Referrals page. You earn 10% of what referred users top up, and 15% once 20 of them have paid. A referred user gets 25% extra credits on their first top-up.
## Closing the account
**Account → Security → Close Account** signs you out everywhere and disables the keys of your personal account. NagaAI keeps payment records. You cannot close an account that still owns an organization.
# API keys
Source: https://docs.naga.ac/account/api-keys
Manage keys in the dashboard or through the account API, with spending limits, expiry and a default routing strategy.
## Key settings
| Setting | Effect |
| - | - |
| Name | A label, 1 to 255 characters |
| Credit limit | The most this key may spend, in credits. Requests past it fail with `403 credit_limit_exceeded` |
| Expiry | After this time the key fails with `401 expired_api_key` |
| Enabled | A disabled key fails with `401 disabled_api_key` until you enable it again |
| Default routing strategy | The [routing strategy](/build/routing) for requests that do not name one. Only through the API |
There is no rotate action. To rotate a key, create a new one, move your clients to it, then delete the old one.
## In the dashboard
On [API Keys](https://naga.ac/dashboard/api-keys), **Create API Key** asks for a name, an optional credit limit and an expiry. The expiry is never, 7, 30 or 90 days, or a date you pick. The new key is shown once. The table lists each key's usage against its limit, and the row menu renames, sets the limit or expiry, disables, enables or deletes it.
## Through the API
The account API needs a [provisioning key](/get-started/authentication).
| Operation | Request |
| - | - |
| List keys, newest first, including disabled and expired ones | `GET /v1/account/keys` |
| Create a key | `POST /v1/account/keys` |
| Get one key | `GET /v1/account/keys/{key_id}` |
| Change a key | `PATCH /v1/account/keys/{key_id}` |
| Delete a key | `DELETE /v1/account/keys/{key_id}`, returns `204` |
### Create a key
```python Python theme={null}
import os
import requests
response = requests.post(
"https://api.naga.ac/v1/account/keys",
headers={"Authorization": f"Bearer {os.environ['NAGAAI_PROVISIONING_KEY']}"},
json={
"name": "ci-worker",
"credit_limit": 25,
"expires_at": "2026-12-31T00:00:00Z",
"default_routing_strategy": "reliability",
},
)
response.raise_for_status()
created = response.json()
print(created["id"], created["key"])
```
```javascript Node.js theme={null}
const response = await fetch("https://api.naga.ac/v1/account/keys", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.NAGAAI_PROVISIONING_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
name: "ci-worker",
credit_limit: 25,
expires_at: "2026-12-31T00:00:00Z",
default_routing_strategy: "reliability",
}),
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const created = await response.json();
console.log(created.id, created.key);
```
```bash cURL theme={null}
curl https://api.naga.ac/v1/account/keys \
-H "Authorization: Bearer $NAGAAI_PROVISIONING_KEY" \
-H "Content-Type: application/json" \
-d '{"name": "ci-worker", "credit_limit": 25, "expires_at": "2026-12-31T00:00:00Z", "default_routing_strategy": "reliability"}'
```
Only `name` is required. The response carries the secret in `key`, and no later response shows it again:
```json theme={null}
{
"id": 38933,
"name": "ci-worker",
"key": "ng-...",
"created_at": "2026-09-26T08:21:55",
"expires_at": "2026-12-31T00:00:00",
"is_enabled": true,
"credit_limit": 25.0,
"default_routing_strategy": "reliability",
"message": "Store this key securely. It will not be shown again."
}
```
Times are UTC without an offset. `expires_at` must be in the future.
### Change a key
`PATCH` changes only the fields you send. Send `null` to clear `credit_limit`, `expires_at` or `default_routing_strategy`. `name` and `is_enabled` cannot be `null`.
```bash theme={null}
curl -X PATCH https://api.naga.ac/v1/account/keys/38933 \
-H "Authorization: Bearer $NAGAAI_PROVISIONING_KEY" \
-H "Content-Type: application/json" \
-d '{"is_enabled": false, "credit_limit": null}'
```
A `key_id` that does not exist on the account returns `400` with `API key not found`.
# Balance, metrics and logs
Source: https://docs.naga.ac/account/balance-and-activity
Where to see the balance, spend and individual requests, in the dashboard and through the account API.
## In the dashboard
**Credits** shows the balance, the history of top-ups and the low balance alert setting.
**Metrics** charts requests, cost, error rate and latency for a period from 15 minutes to 30 days. You can filter by API key and model, and see cost by model publisher, the top keys and the top models.
**Logs** lists every request of the last 31 days with its time, model, endpoint, status, input and output tokens, processing time and cost. Select a row to see its details. With [data logging](/account/privacy-and-logging) turned on, the details include the request and response bodies. **Export** downloads up to 10,000 rows as CSV or JSON.
Each log row matches the `x-request-id` header of the response it describes, so you can find a request from your own logs.
## Through the API
Both endpoints need a [provisioning key](/get-started/authentication).
### Balance
```bash theme={null}
curl https://api.naga.ac/v1/account/balance \
-H "Authorization: Bearer $NAGAAI_PROVISIONING_KEY"
```
```json theme={null}
{"balance": "22.2782894050"}
```
The balance is a decimal string in credits, so no digits are lost to floating point.
### Activity
`GET /v1/account/activity?days=N` summarizes the last `N` days, from 1 to 30. The default is 30, and other values fail with `400`.
```python Python theme={null}
import os
import requests
response = requests.get(
"https://api.naga.ac/v1/account/activity",
params={"days": 7},
headers={"Authorization": f"Bearer {os.environ['NAGAAI_PROVISIONING_KEY']}"},
)
response.raise_for_status()
activity = response.json()
print(activity["total_stats"]["total_cost"])
```
```javascript Node.js theme={null}
const response = await fetch("https://api.naga.ac/v1/account/activity?days=7", {
headers: { Authorization: `Bearer ${process.env.NAGAAI_PROVISIONING_KEY}` },
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const activity = await response.json();
console.log(activity.total_stats.total_cost);
```
```bash cURL theme={null}
curl "https://api.naga.ac/v1/account/activity?days=7" \
-H "Authorization: Bearer $NAGAAI_PROVISIONING_KEY"
```
```json theme={null}
{
"period_days": 7,
"total_stats": {"total_requests": 4714, "total_cost": "0.0231609992", "total_input_tokens": 301686, "total_output_tokens": 152654},
"daily_stats": [{"date": "2026-09-26", "total_requests": 203, "total_cost": "0.0015422437", "total_input_tokens": 10166, "total_output_tokens": 6385}],
"top_models": [{"model_name": "gpt-oss-120b", "request_count": 4680, "total_cost": "0.0223454175", "total_input_tokens": 301533, "total_output_tokens": 152453}],
"api_key_usage": [{"api_key_id": 6370, "api_key_name": "status-page", "request_count": 3731, "total_cost": "0.0210395830", "total_input_tokens": 247828, "total_output_tokens": 150353}]
}
```
`top_models` holds up to 10 models. Costs are decimal strings. NagaAI caches the report for up to 5 minutes, so the latest requests can take that long to appear.
# Privacy and logging
Source: https://docs.naga.ac/account/privacy-and-logging
The privacy settings in the dashboard and the request data NagaAI keeps.
The legal terms are in the [Privacy Policy](https://naga.ac/legal/privacy).
## Settings
The [Privacy](https://naga.ac/dashboard/privacy) page has three settings. Each change is saved as soon as you flip it and applies to requests sent after that.
| Setting | Default | Effect |
| - | - | - |
| Training on paid models | Off | When on, prompts and outputs of paid-model requests may be used to train and improve models |
| Training on free models | Always on | Prompts and outputs of `:free` models may be used by NagaAI and its partners for training. Use a paid model if you do not want this |
| Data logging (beta) | Off | When on, NagaAI stores the full request and response bodies and shows them in [Logs](/account/balance-and-activity) |
In an organization, only admins can change these settings.
## What NagaAI always keeps
For every request NagaAI records metadata: time, API key, model, endpoint, status, token counts, latency and cost. The Logs page shows the last 31 days of it, and NagaAI keeps it for one year. Request and response bodies appear in Logs only with data logging on.
Security and operational logs, such as IP addresses used for rate limiting and error diagnostics, are covered by the Privacy Policy.
## Providers
NagaAI sends each request to a model provider, which processes it under its own terms. Which provider served a request is not shown and can change between requests, see [Routing](/build/routing).
# Get account activity
Source: https://docs.naga.ac/api-reference/account/get-account-activity
/openapi/naga-api.json get /v1/account/activity
Returns the account's usage over a recent window, aggregated four ways. Requires a provisioning key.
# Get the account balance
Source: https://docs.naga.ac/api-reference/account/get-the-account-balance
/openapi/naga-api.json get /v1/account/balance
Returns the account's credit balance. Requires a provisioning key.
# Create an API key
Source: https://docs.naga.ac/api-reference/api-keys/create-an-api-key
/openapi/naga-api.json post /v1/account/keys
Creates an API key on the account and returns its secret this once. Requires a provisioning key.
# Delete an API key
Source: https://docs.naga.ac/api-reference/api-keys/delete-an-api-key
/openapi/naga-api.json delete /v1/account/keys/{key_id}
Deletes one API key of the account, immediately and past undoing. Requires a provisioning key.
# Get an API key
Source: https://docs.naga.ac/api-reference/api-keys/get-an-api-key
/openapi/naga-api.json get /v1/account/keys/{key_id}
Returns one API key of the account, without its secret. Requires a provisioning key.
# List API keys
Source: https://docs.naga.ac/api-reference/api-keys/list-api-keys
/openapi/naga-api.json get /v1/account/keys
Returns every API key on the account, disabled and expired ones included. Requires a provisioning key.
# Update an API key
Source: https://docs.naga.ac/api-reference/api-keys/update-an-api-key
/openapi/naga-api.json patch /v1/account/keys/{key_id}
Updates one API key of the account in place, changing only the fields sent. Requires a provisioning key.
# Create a transcription
Source: https://docs.naga.ac/api-reference/audio/create-a-transcription
/openapi/naga-api.json post /v1/audio/transcriptions
Converts uploaded audio into text in the language that was spoken and charges the account for what it counted. Requires an API key. `/v1/audio/translations` answers in English instead.
# Create a translation
Source: https://docs.naga.ac/api-reference/audio/create-a-translation
/openapi/naga-api.json post /v1/audio/translations
Converts uploaded audio into English text and charges the account for what it counted. Requires an API key. `/v1/audio/transcriptions` answers in the language that was spoken.
# Create speech
Source: https://docs.naga.ac/api-reference/audio/create-speech
/openapi/naga-api.json post /v1/audio/speech
Converts text into spoken audio and charges the account for what it counted. Requires an API key.
# Create a chat completion
Source: https://docs.naga.ac/api-reference/chat-completions/create-a-chat-completion
/openapi/naga-api.json post /v1/chat/completions
Creates a model response in the OpenAI Chat Completions dialect and charges the account for the tokens it used. Requires an API key. `/v1/responses` and `/v1/messages` serve the same models in the other two dialects.
# Create embeddings
Source: https://docs.naga.ac/api-reference/embeddings/create-embeddings
/openapi/naga-api.json post /v1/embeddings
Creates an embedding vector for the input and charges the account for the tokens it counted. Requires an API key.
# Create an image
Source: https://docs.naga.ac/api-reference/images/create-an-image
/openapi/naga-api.json post /v1/images/generations
Creates images from a text prompt and charges the account for what it counted. Requires an API key. `/v1/images/edits` changes images that were uploaded instead.
# Edit an image
Source: https://docs.naga.ac/api-reference/images/edit-an-image
/openapi/naga-api.json post /v1/images/edits
Edits uploaded images against a text prompt and charges the account for what it counted. Requires an API key. `/v1/images/generations` draws from a prompt alone.
# Create a message
Source: https://docs.naga.ac/api-reference/messages/create-a-message
/openapi/naga-api.json post /v1/messages
Creates a model response in the Anthropic Messages dialect and charges the account for the tokens it used. Requires an API key. `/v1/chat/completions` and `/v1/responses` serve the same models in the other two dialects.
# List models
Source: https://docs.naga.ac/api-reference/models/list-models
/openapi/naga-api.json get /v1/models
Returns the models NagaAI serves, whole and unpaged. Requires no key; presenting one narrows the catalog to what the account's balance can reach.
# Classify content
Source: https://docs.naga.ac/api-reference/moderations/classify-content
/openapi/naga-api.json post /v1/moderations
Classifies text and images against a moderation model's categories and charges the account for the tokens it counted. Requires an API key.
# API reference
Source: https://docs.naga.ac/api-reference/overview
Base URLs, authentication, headers and error shapes shared by every NagaAI endpoint.
The reference pages come from the OpenAPI document that NagaAI serves at [`https://api.naga.ac/openapi.json`](https://api.naga.ac/openapi.json). Each operation page has a playground that sends real requests with your key.
## Base URLs
| Client | Base URL |
| - | - |
| OpenAI SDKs and raw HTTP | `https://api.naga.ac/v1` |
| Anthropic SDK | `https://api.naga.ac` |
## Authentication
| Endpoints | Key | Header |
| - | - | - |
| Chat, images, audio, embeddings, moderations | API key | `Authorization: Bearer` or `x-api-key` |
| `/v1/account/*` | Provisioning key | `Authorization: Bearer` |
| `GET /v1/models`, `GET /v1/startups` | None | |
See [Authentication](/get-started/authentication).
## Headers
| Header | Direction | Meaning |
| - | - | - |
| `x-request-id` | Response | The id of the request. Quote it to support, and find it on the Logs page |
| `x-naga-routing-strategy` | Request and response | On the chat endpoints: the routing strategy to use, and the one that was used. See [Routing](/build/routing) |
| `Retry-After` | Response | Seconds to wait, on `429` and some `503` responses |
## Errors
Every endpoint except `/v1/messages` returns `{"error": {"type", "message"}}`, often with `code` and `param`. `/v1/messages` returns `{"type": "error", "error": {"type", "message"}}`. See [Errors](/build/error-handling).
## Versioning
NagaAI has one API version. Changes that affect clients are listed in the [Changelog](/changelog).
# Create a response
Source: https://docs.naga.ac/api-reference/responses/create-a-response
/openapi/naga-api.json post /v1/responses
Creates a model response in the OpenAI Responses dialect and charges the account for the tokens it used. Requires an API key. `/v1/chat/completions` and `/v1/messages` serve the same models in the other two dialects.
# List startups
Source: https://docs.naga.ac/api-reference/startups/list-startups
/openapi/naga-api.json get /v1/startups
Returns the organizations whose models NagaAI serves. Requires no key, and presenting one changes nothing.
# Audio
Source: https://docs.naga.ac/api/audio
Text to speech, transcription and translation to English, in the OpenAI Audio format.
| Task | Endpoint | Example models |
| - | - | - |
| Text to speech | `POST /v1/audio/speech` | `gpt-4o-mini-tts`, `eleven-v3` |
| Speech to text | `POST /v1/audio/transcriptions` | `gpt-4o-transcribe`, `whisper-large-v3-turbo` |
| Speech to English text | `POST /v1/audio/translations` | `whisper-large-v3-turbo`, `whisper-large-v3` |
A model's `supported_endpoints` in [`/v1/models`](/get-started/models) says which of the three it serves.
* [Text to speech](/api/audio/text-to-speech)
* [Speech to text and translation](/api/audio/speech-to-text)
Chat models that accept audio, such as `gemini-3.7-flash`, can also take audio inside a chat request. See [Multimodal content](/api/chat-completions/multimodal-content#audio).
# Speech to text
Source: https://docs.naga.ac/api/audio/speech-to-text
Transcribe audio, or translate it to English text, with /v1/audio/transcriptions and /v1/audio/translations.
Both endpoints take a multipart form and return `{"text": "..."}`.
```python Python theme={null}
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.naga.ac/v1", api_key=os.environ["NAGAAI_API_KEY"])
with open("speech.mp3", "rb") as audio:
transcript = client.audio.transcriptions.create(model="gpt-4o-transcribe", file=audio)
print(transcript.text)
```
```javascript Node.js theme={null}
import fs from "node:fs";
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.naga.ac/v1", apiKey: process.env.NAGAAI_API_KEY });
const transcript = await client.audio.transcriptions.create({
model: "gpt-4o-transcribe",
file: fs.createReadStream("speech.mp3"),
});
console.log(transcript.text);
```
```bash cURL theme={null}
curl https://api.naga.ac/v1/audio/transcriptions \
-H "Authorization: Bearer $NAGAAI_API_KEY" \
-F model=gpt-4o-transcribe \
-F file=@speech.mp3
```
For English text from speech in another language, call `client.audio.translations.create` with `whisper-large-v3-turbo` or `whisper-large-v3`, or `POST /v1/audio/translations` with the same form.
| Field | Notes |
| - | - |
| `file` | mp3, wav, m4a, ogg, flac, webm and other common audio formats. NagaAI converts the file when the model needs another format. The whole request can be up to 32 MiB |
| `language` | Optional ISO 639-1 code in lower case, such as `en` or `de`. Other forms fail with `400` |
| `prompt` | Optional text that guides spelling and style |
The answer is always JSON with `text`. NagaAI ignores `response_format`, `timestamp_granularities` and `temperature`, so there are no timestamps, SRT or VTT output.
# Text to speech
Source: https://docs.naga.ac/api/audio/text-to-speech
Turn text into audio with POST /v1/audio/speech.
```python Python theme={null}
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.naga.ac/v1", api_key=os.environ["NAGAAI_API_KEY"])
with client.audio.speech.with_streaming_response.create(
model="gpt-4o-mini-tts",
voice="alloy",
input="NagaAI serves models from many providers behind one API.",
) as response:
response.stream_to_file("speech.mp3")
```
```javascript Node.js theme={null}
import fs from "node:fs";
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.naga.ac/v1", apiKey: process.env.NAGAAI_API_KEY });
const response = await client.audio.speech.create({
model: "gpt-4o-mini-tts",
voice: "alloy",
input: "NagaAI serves models from many providers behind one API.",
});
fs.writeFileSync("speech.mp3", Buffer.from(await response.arrayBuffer()));
```
```bash cURL theme={null}
curl https://api.naga.ac/v1/audio/speech \
-H "Authorization: Bearer $NAGAAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "gpt-4o-mini-tts", "voice": "alloy", "input": "NagaAI serves models from many providers behind one API."}' \
--output speech.mp3
```
The body of the response is the audio file itself. [Listen to the result](/examples/speech.mp3).
| Field | Notes |
| - | - |
| `input` | Up to 32,768 characters |
| `voice` | Depends on the model. OpenAI voice names such as `alloy` and `nova` also work on ElevenLabs models, where NagaAI maps them to similar voices |
| `response_format` | `mp3` (default), `opus`, `aac`, `flac`, `wav` or `pcm`. A format the model cannot produce fails with `400`: ElevenLabs models, for example, have no `aac` or `flac` |
| `speed` | Greater than 0 |
| `instructions` | Style directions, on models that support them |
# Chat Completions
Source: https://docs.naga.ac/api/chat-completions
POST /v1/chat/completions in the OpenAI Chat Completions format, with the fields NagaAI adds and ignores.
`POST https://api.naga.ac/v1/chat/completions` accepts the OpenAI Chat Completions format and serves every chat model in the catalog. Point the OpenAI SDK at `https://api.naga.ac/v1`.
```python Python theme={null}
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.naga.ac/v1", api_key=os.environ["NAGAAI_API_KEY"])
completion = client.chat.completions.create(
model="deepseek-v4.1-flash",
messages=[
{"role": "system", "content": "Answer in one sentence."},
{"role": "user", "content": "Why do HTTP clients retry with backoff?"},
],
)
print(completion.choices[0].message.content)
print(completion.usage)
```
```javascript Node.js theme={null}
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.naga.ac/v1", apiKey: process.env.NAGAAI_API_KEY });
const completion = await client.chat.completions.create({
model: "deepseek-v4.1-flash",
messages: [
{ role: "system", content: "Answer in one sentence." },
{ role: "user", content: "Why do HTTP clients retry with backoff?" },
],
});
console.log(completion.choices[0].message.content);
console.log(completion.usage);
```
```bash cURL theme={null}
curl https://api.naga.ac/v1/chat/completions \
-H "Authorization: Bearer $NAGAAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4.1-flash",
"messages": [
{"role": "system", "content": "Answer in one sentence."},
{"role": "user", "content": "Why do HTTP clients retry with backoff?"}
]
}'
```
## Response
The response has the OpenAI shape. `id` starts with `chatcmpl-`, `model` is the catalog id even if you sent an alias, and `choices` always has one entry.
## What NagaAI adds
* `routing` in the body and the `x-naga-routing-strategy` header, see [Routing](/build/routing).
* `message.reasoning_content` and `message.reasoning_details` with the model's reasoning, see [Reasoning](/api/chat-completions/reasoning).
* `message.images` with images from models that draw.
* `file.file_url` and more `input_audio` formats, see [Multimodal content](/api/chat-completions/multimodal-content).
* `max_tokens` works as an alias of `max_completion_tokens`.
## What NagaAI ignores
NagaAI accepts and drops `n`, `seed`, `logprobs`, `top_logprobs`, `logit_bias`, `user`, `store`, `metadata`, `service_tier`, `prediction`, `audio`, `modalities`, `prompt_cache_key` and `web_search_options`. `functions` and the `function` role fail with `400`: use `tools`. More differences are in [Differences from OpenAI and Anthropic](/build/compatibility).
# Multimodal content
Source: https://docs.naga.ac/api/chat-completions/multimodal-content
Send images, PDFs and audio as content parts in Chat Completions messages.
A user message can hold a list of content parts instead of a string. Check `architecture.input_modalities` of the model in [`/v1/models`](/get-started/models) first: a part the model cannot read fails with `400 unsupported_modality`.
## Images
`image_url.url` takes an `https` URL or a data URI. `detail` is `low`, `high` or `auto`.
```python Python theme={null}
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.naga.ac/v1", api_key=os.environ["NAGAAI_API_KEY"])
completion = client.chat.completions.create(
model="deepseek-v4.1-flash",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "What is in this image? One sentence."},
{"type": "image_url", "image_url": {"url": "https://media.naga.ac/outputs/6ab3444c-f19e-4944-80dc-d0b49c49c144.png"}},
],
}],
)
print(completion.choices[0].message.content)
```
```javascript Node.js theme={null}
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.naga.ac/v1", apiKey: process.env.NAGAAI_API_KEY });
const completion = await client.chat.completions.create({
model: "deepseek-v4.1-flash",
messages: [{
role: "user",
content: [
{ type: "text", text: "What is in this image? One sentence." },
{ type: "image_url", image_url: { url: "https://media.naga.ac/outputs/6ab3444c-f19e-4944-80dc-d0b49c49c144.png" } },
],
}],
});
console.log(completion.choices[0].message.content);
```
```bash cURL theme={null}
curl https://api.naga.ac/v1/chat/completions \
-H "Authorization: Bearer $NAGAAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4.1-flash",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "What is in this image? One sentence."},
{"type": "image_url", "image_url": {"url": "https://media.naga.ac/outputs/6ab3444c-f19e-4944-80dc-d0b49c49c144.png"}}
]
}]
}'
```
## Files and PDFs
Send the file as a data URI in `file_data`, or as an `https` link in `file_url`, a NagaAI extension. NagaAI has no Files API, so a `file_id` fails with `400 file_id_unsupported`.
```python Python theme={null}
import base64
with open("report.pdf", "rb") as f:
pdf = base64.b64encode(f.read()).decode()
completion = client.chat.completions.create(
model="gpt-oss-120b",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Summarize this document in two sentences."},
{"type": "file", "file": {"filename": "report.pdf", "file_data": f"data:application/pdf;base64,{pdf}"}},
],
}],
)
```
## Audio
`input_audio.data` is base64. A `data:` prefix fails with `400`. `format` is `wav`, `mp3`, `aiff`, `aac`, `ogg`, `flac`, `m4a`, `pcm16` or `pcm24`; the official API accepts only the first two.
```python Python theme={null}
with open("question.mp3", "rb") as f:
audio = base64.b64encode(f.read()).decode()
completion = client.chat.completions.create(
model="gemini-3.7-flash",
messages=[{
"role": "user",
"content": [
{"type": "text", "text": "Transcribe this and answer the question."},
{"type": "input_audio", "input_audio": {"data": audio, "format": "mp3"}},
],
}],
)
```
One inline part can be up to 20 MiB and the whole request up to 32 MiB.
## Images in the answer
Models that draw return images in `message.images`, a list of `{"type": "image_url", "image_url": {"url": "..."}}` entries next to `content`. The `url` is a `data:` URI or an `https` link, so handle both. When you continue the conversation, send the assistant message back with its `images` so the model can edit the picture instead of drawing a new one.
# Reasoning
Source: https://docs.naga.ac/api/chat-completions/reasoning
Set reasoning effort and read the model's reasoning from Chat Completions responses.
`reasoning_effort` sets how much the model thinks before it answers: `none`, `minimal`, `low`, `medium`, `high` or `xhigh`. NagaAI moves the value to the nearest level the model supports.
```python Python theme={null}
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.naga.ac/v1", api_key=os.environ["NAGAAI_API_KEY"])
completion = client.chat.completions.create(
model="deepseek-v4.1-flash",
messages=[{"role": "user", "content": "A bat and a ball cost 1.10 in total. The bat costs 1.00 more than the ball. What does the ball cost?"}],
reasoning_effort="high",
)
message = completion.choices[0].message
print("Reasoning:", getattr(message, "reasoning_content", None))
print("Answer:", message.content)
print("Reasoning tokens:", completion.usage.completion_tokens_details.reasoning_tokens)
```
```javascript Node.js theme={null}
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.naga.ac/v1", apiKey: process.env.NAGAAI_API_KEY });
const completion = await client.chat.completions.create({
model: "deepseek-v4.1-flash",
messages: [{ role: "user", content: "A bat and a ball cost 1.10 in total. The bat costs 1.00 more than the ball. What does the ball cost?" }],
reasoning_effort: "high",
});
const message = completion.choices[0].message;
console.log("Reasoning:", message.reasoning_content);
console.log("Answer:", message.content);
console.log("Reasoning tokens:", completion.usage.completion_tokens_details?.reasoning_tokens);
```
```bash cURL theme={null}
curl https://api.naga.ac/v1/chat/completions \
-H "Authorization: Bearer $NAGAAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4.1-flash",
"messages": [{"role": "user", "content": "A bat and a ball cost 1.10 in total. The bat costs 1.00 more than the ball. What does the ball cost?"}],
"reasoning_effort": "high"
}'
```
## Where the reasoning is
The official Chat Completions API returns no reasoning. NagaAI adds two fields to the assistant message and to stream deltas:
| Field | Content |
| - | - |
| `reasoning_content` | A plain-text view of the reasoning, often its summary. On input NagaAI also accepts `reasoning` as an alias |
| `reasoning_details` | The reasoning as typed entries: `reasoning.text` (full text, with a `signature` when the model signs it), `reasoning.summary`, or `reasoning.encrypted` (sealed reasoning in `data`) |
Models that hide their reasoning return only a summary or an encrypted entry. NagaAI bills the tokens as output either way and counts them in `usage.completion_tokens_details.reasoning_tokens`.
## Keep the reasoning in multi-turn chats
When you continue a conversation, send each assistant message back with its `reasoning_details`. In the Python SDK, append `message.model_dump(exclude_none=True)`, in Node.js the message object itself. Models that sign their reasoning need the signature to continue a chain of tool calls. NagaAI removes signatures issued by a different provider, so switching providers mid-conversation does not fail.
# Streaming
Source: https://docs.naga.ac/api/chat-completions/streaming
Stream Chat Completions chunks, including tool arguments, reasoning and final usage.
```python Python theme={null}
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.naga.ac/v1", api_key=os.environ["NAGAAI_API_KEY"])
stream = client.chat.completions.create(
model="deepseek-v4.1-flash",
messages=[{"role": "user", "content": "Explain backpressure in three sentences."}],
stream=True,
stream_options={"include_usage": True},
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
if chunk.usage:
print("\n", chunk.usage)
```
```javascript Node.js theme={null}
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.naga.ac/v1", apiKey: process.env.NAGAAI_API_KEY });
const stream = await client.chat.completions.create({
model: "deepseek-v4.1-flash",
messages: [{ role: "user", content: "Explain backpressure in three sentences." }],
stream: true,
stream_options: { include_usage: true },
});
for await (const chunk of stream) {
const text = chunk.choices[0]?.delta?.content;
if (text) process.stdout.write(text);
if (chunk.usage) console.log("\n", chunk.usage);
}
```
```bash cURL theme={null}
curl -N https://api.naga.ac/v1/chat/completions \
-H "Authorization: Bearer $NAGAAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4.1-flash",
"messages": [{"role": "user", "content": "Explain backpressure in three sentences."}],
"stream": true,
"stream_options": {"include_usage": true}
}'
```
## Chunks
Each `data:` line holds a `chat.completion.chunk`. All chunks of one answer share one `id`.
| In `choices[0].delta` | Carries |
| - | - |
| `role` | `"assistant"`, in the first chunk |
| `content` | The next piece of the answer text |
| `reasoning_content` | The next piece of the model's reasoning, on reasoning models |
| `tool_calls` | Tool call pieces: the first piece has `id` and `function.name`, later pieces add to `function.arguments` by `index` |
The last content chunk has `finish_reason`: `stop`, `length`, `tool_calls` or `content_filter`. With `include_usage`, one more chunk follows with an empty `choices` array and the `usage` object. The stream ends with `data: [DONE]`.
If a provider fails after the stream has started, a chunk with a top-level `error` object arrives, followed by `data: [DONE]`. See [Errors](/build/error-handling#errors-in-a-stream).
# Structured outputs
Source: https://docs.naga.ac/api/chat-completions/structured-outputs
Ask for JSON that matches a schema with response_format.
```python Python theme={null}
import json
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.naga.ac/v1", api_key=os.environ["NAGAAI_API_KEY"])
completion = client.chat.completions.create(
model="deepseek-v4.1-flash",
messages=[{"role": "user", "content": "Name one European capital and its country."}],
response_format={
"type": "json_schema",
"json_schema": {
"name": "capital",
"strict": True,
"schema": {
"type": "object",
"properties": {"city": {"type": "string"}, "country": {"type": "string"}},
"required": ["city", "country"],
"additionalProperties": False,
},
},
},
)
print(json.loads(completion.choices[0].message.content))
```
```javascript Node.js theme={null}
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.naga.ac/v1", apiKey: process.env.NAGAAI_API_KEY });
const completion = await client.chat.completions.create({
model: "deepseek-v4.1-flash",
messages: [{ role: "user", content: "Name one European capital and its country." }],
response_format: {
type: "json_schema",
json_schema: {
name: "capital",
strict: true,
schema: {
type: "object",
properties: { city: { type: "string" }, country: { type: "string" } },
required: ["city", "country"],
additionalProperties: false,
},
},
},
});
console.log(JSON.parse(completion.choices[0].message.content));
```
```bash cURL theme={null}
curl https://api.naga.ac/v1/chat/completions \
-H "Authorization: Bearer $NAGAAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4.1-flash",
"messages": [{"role": "user", "content": "Name one European capital and its country."}],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "capital",
"strict": true,
"schema": {
"type": "object",
"properties": {"city": {"type": "string"}, "country": {"type": "string"}},
"required": ["city", "country"],
"additionalProperties": false
}
}
}
}'
```
`{"type": "json_object"}` asks for any JSON object without a schema. Mention JSON in the prompt when you use it.
The SDK helper `client.chat.completions.parse()` with a Pydantic or Zod model builds the same `response_format`, and it works against NagaAI too.
On models without schema support NagaAI falls back to JSON mode plus the schema in the prompt, so validate the result. See [Structured outputs](/build/structured-outputs).
# Tool calling
Source: https://docs.naga.ac/api/chat-completions/tool-calling
A complete tool loop with /v1/chat/completions.
The model asks for a tool call, your code runs the function, and you send the result back in a `tool` message. Repeat until `finish_reason` is `stop`.
```python Python theme={null}
import json
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.naga.ac/v1", api_key=os.environ["NAGAAI_API_KEY"])
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city.",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
},
}]
def get_weather(city):
return {"city": city, "temperature_c": 18, "sky": "cloudy"}
messages = [{"role": "user", "content": "What is the weather in Prague?"}]
while True:
completion = client.chat.completions.create(
model="deepseek-v4.1-flash", messages=messages, tools=tools
)
message = completion.choices[0].message
messages.append(message.model_dump(exclude_none=True))
if not message.tool_calls:
break
for call in message.tool_calls:
result = get_weather(**json.loads(call.function.arguments))
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
print(message.content)
```
```javascript Node.js theme={null}
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.naga.ac/v1", apiKey: process.env.NAGAAI_API_KEY });
const tools = [{
type: "function",
function: {
name: "get_weather",
description: "Current weather for a city.",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
},
}];
const getWeather = ({ city }) => ({ city, temperature_c: 18, sky: "cloudy" });
const messages = [{ role: "user", content: "What is the weather in Prague?" }];
let message;
while (true) {
const completion = await client.chat.completions.create({ model: "deepseek-v4.1-flash", messages, tools });
message = completion.choices[0].message;
messages.push(message);
if (!message.tool_calls?.length) break;
for (const call of message.tool_calls) {
const result = getWeather(JSON.parse(call.function.arguments));
messages.push({ role: "tool", tool_call_id: call.id, content: JSON.stringify(result) });
}
}
console.log(message.content);
```
```bash cURL theme={null}
curl https://api.naga.ac/v1/chat/completions \
-H "Authorization: Bearer $NAGAAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4.1-flash",
"messages": [{"role": "user", "content": "What is the weather in Prague?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city.",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]}
}
}]
}'
```
The assistant message goes back into `messages` unchanged, with its `tool_calls` and any `reasoning_details`. Some models lose their reasoning between tool calls if you rebuild the message from its fields.
`tool_choice` takes `auto`, `none`, `required` or `{"type": "function", "function": {"name": "..."}}`. `parallel_tool_calls: false` asks for one call per turn. The rules NagaAI enforces, such as a result for every call, are in [Tool calling](/build/tool-calling).
# Embeddings
Source: https://docs.naga.ac/api/embeddings
Turn text into vectors with POST /v1/embeddings in the OpenAI format.
```python Python theme={null}
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.naga.ac/v1", api_key=os.environ["NAGAAI_API_KEY"])
result = client.embeddings.create(
model="text-embedding-3-small",
input=["How do I reset my password?", "Where is my invoice?"],
)
for item in result.data:
print(item.index, len(item.embedding))
```
```javascript Node.js theme={null}
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.naga.ac/v1", apiKey: process.env.NAGAAI_API_KEY });
const result = await client.embeddings.create({
model: "text-embedding-3-small",
input: ["How do I reset my password?", "Where is my invoice?"],
});
for (const item of result.data) console.log(item.index, item.embedding.length);
```
```bash cURL theme={null}
curl https://api.naga.ac/v1/embeddings \
-H "Authorization: Bearer $NAGAAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "text-embedding-3-small", "input": ["How do I reset my password?", "Where is my invoice?"]}'
```
`input` is one string or a list of strings. Match each vector to its input by `data[].index`.
| Field | Notes |
| - | - |
| `encoding_format` | `float` (default) or `base64`. Base64 holds little-endian float32 values: smaller on the wire, same numbers as `float` rounded to float32 |
| `dimensions` | Shorter vectors, on models that support it. Providers that do not support it ignore the field and return the full length |
Vector lengths differ by model: 1536 for `text-embedding-3-small`, 3072 for `gemini-embedding-001`, 4096 for `qwen3-embedding-8b`. Compare vectors only from the same model and the same `dimensions`.
NagaAI charges embeddings per input token at `pricing.per_token`.
# Images
Source: https://docs.naga.ac/api/images
Generate images with POST /v1/images/generations in the OpenAI Images format.
`POST https://api.naga.ac/v1/images/generations` creates images from a prompt. Models are listed at [naga.ac/models](https://naga.ac/models) with the `image` output type.
```python Python theme={null}
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.naga.ac/v1", api_key=os.environ["NAGAAI_API_KEY"])
result = client.images.generate(
model="gpt-image-2",
prompt="A red paper boat on a calm lake, flat illustration",
size="1024x1024",
n=1,
)
print(result.data[0].url)
```
```javascript Node.js theme={null}
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.naga.ac/v1", apiKey: process.env.NAGAAI_API_KEY });
const result = await client.images.generate({
model: "gpt-image-2",
prompt: "A red paper boat on a calm lake, flat illustration",
size: "1024x1024",
n: 1,
});
console.log(result.data[0].url);
```
```bash cURL theme={null}
curl https://api.naga.ac/v1/images/generations \
-H "Authorization: Bearer $NAGAAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "gpt-image-2", "prompt": "A red paper boat on a calm lake, flat illustration", "size": "1024x1024", "n": 1}'
```
## Parameters
| Field | Notes |
| - | - |
| `prompt` | 1 to 8,192 characters |
| `n` | Up to the model's `max_output_images` in `/v1/models`, usually 4 |
| `size` | `WIDTHxHEIGHT`, for example `1536x1024`. Other forms fail with `400`. There is no separate aspect ratio field: set the ratio through `size` |
| `quality` | Passed to the provider as is |
| `response_format` | `url` (default) or `b64_json` |
NagaAI ignores `style`, `output_format` and other fields.
## Response
With `url`, each entry of `data` holds a link on `media.naga.ac` to a copy NagaAI keeps. Download the file if you need it long term. With `b64_json`, the image bytes come inline.
`usage` reports tokens. When a provider does not report them, NagaAI counts 2,048 output tokens per image. The price per image token is in `pricing.per_output_image_token`.
NagaAI checks prompts with its own moderation first. A blocked prompt fails with `403 inappropriate_content`.
To edit an existing image, see [Image edits](/api/images/image-edits). Chat models that draw also return images through the chat endpoints, see [Multimodal content](/api/chat-completions/multimodal-content#images-in-the-answer).
# Image edits
Source: https://docs.naga.ac/api/images/image-edits
Change existing images with a prompt and an optional mask through POST /v1/images/edits.
`POST /v1/images/edits` takes a multipart form with one or more images, a prompt and an optional mask.
```python Python theme={null}
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.naga.ac/v1", api_key=os.environ["NAGAAI_API_KEY"])
with open("boat.png", "rb") as image:
result = client.images.edit(
model="gpt-image-2",
image=image,
prompt="Make the boat blue and add a small flag",
size="1024x1024",
)
print(result.data[0].url)
```
```javascript Node.js theme={null}
import fs from "node:fs";
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.naga.ac/v1", apiKey: process.env.NAGAAI_API_KEY });
const result = await client.images.edit({
model: "gpt-image-2",
image: fs.createReadStream("boat.png"),
prompt: "Make the boat blue and add a small flag",
size: "1024x1024",
});
console.log(result.data[0].url);
```
```bash cURL theme={null}
curl https://api.naga.ac/v1/images/edits \
-H "Authorization: Bearer $NAGAAI_API_KEY" \
-F model=gpt-image-2 \
-F image=@boat.png \
-F prompt="Make the boat blue and add a small flag" \
-F size=1024x1024
```
| Field | Notes |
| - | - |
| `image` | The image to change. Models that combine images take several |
| `mask` | A PNG whose transparent area marks what to change |
| `prompt` | Up to 32,000 characters |
| `background`, `quality`, `n`, `size`, `response_format` | As in [Images](/api/images) |
The model must list `images.edits` in `supported_endpoints`. The response has the same shape as a generation.
# Messages
Source: https://docs.naga.ac/api/messages
POST /v1/messages in the Anthropic Messages format, for the Anthropic SDK and Claude Code.
`POST https://api.naga.ac/v1/messages` accepts the Anthropic Messages format and serves every chat model in the catalog, not only Claude. Point the Anthropic SDK at `https://api.naga.ac`, without `/v1`. The key works as `x-api-key`, the SDK default, or as `Authorization: Bearer`.
```python Python theme={null}
import os
from anthropic import Anthropic
client = Anthropic(base_url="https://api.naga.ac", api_key=os.environ["NAGAAI_API_KEY"])
message = client.messages.create(
model="claude-sonnet-5",
max_tokens=512,
system="Answer in one sentence.",
messages=[{"role": "user", "content": "Why do HTTP clients retry with backoff?"}],
)
print(message.content[0].text)
print(message.usage)
```
```typescript TypeScript theme={null}
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ baseURL: "https://api.naga.ac", apiKey: process.env.NAGAAI_API_KEY });
const message = await client.messages.create({
model: "claude-sonnet-5",
max_tokens: 512,
system: "Answer in one sentence.",
messages: [{ role: "user", content: "Why do HTTP clients retry with backoff?" }],
});
for (const block of message.content) {
if (block.type === "text") console.log(block.text);
}
console.log(message.usage);
```
```bash cURL theme={null}
curl https://api.naga.ac/v1/messages \
-H "x-api-key: $NAGAAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-5",
"max_tokens": 512,
"system": "Answer in one sentence.",
"messages": [{"role": "user", "content": "Why do HTTP clients retry with backoff?"}]
}'
```
`max_tokens` is required, as in the official API. NagaAI accepts mid-conversation `system` messages with text content only; any other block in them fails with `400`. NagaAI errors use Anthropic's error types by status: `authentication_error` for `401`, `billing_error` for `402`, `not_found_error` for `404`, `rate_limit_error` for `429`. NagaAI ignores the `anthropic-version` and `anthropic-beta` headers, so you can leave them out.
## Structured output
`output_config.format` with `{"type": "json_schema", "schema": {...}}` asks for JSON that matches the schema. The JSON comes back in the first `text` block. See [Structured outputs](/build/structured-outputs).
## What NagaAI adds
* `routing` in the body and the `x-naga-routing-strategy` header, see [Routing](/build/routing).
* `image` blocks in the response from models that draw.
* `search_context_size` and `external_web_access` on the web search tool, see [Web search](/build/web-search).
## What NagaAI ignores or refuses
* `cache_control`, at the top level and on blocks. There are no explicit cache breakpoints.
* `service_tier`, `container`, `mcp_servers`, `context_management`, `inference_geo` and `thinking.display`.
* `is_error` on `tool_result` blocks. Say that the tool failed in the result text.
* Audio blocks fail with `400`.
* `POST /v1/messages/count_tokens` and the batches API return `404`.
More differences are in [Differences from OpenAI and Anthropic](/build/compatibility).
# Documents and PDFs
Source: https://docs.naga.ac/api/messages/documents-and-pdfs
Send PDFs, text documents and images as blocks in /v1/messages.
The model must accept files: check that `architecture.input_modalities` lists `file` in [`/v1/models`](/get-started/models).
```python Python theme={null}
import base64
import os
from anthropic import Anthropic
client = Anthropic(base_url="https://api.naga.ac", api_key=os.environ["NAGAAI_API_KEY"])
with open("report.pdf", "rb") as f:
pdf = base64.b64encode(f.read()).decode()
message = client.messages.create(
model="claude-haiku-4.5",
max_tokens=1024,
messages=[{
"role": "user",
"content": [
{"type": "document", "source": {"type": "base64", "media_type": "application/pdf", "data": pdf}},
{"type": "text", "text": "Summarize this document in two sentences."},
],
}],
)
print(message.content[0].text)
```
```typescript TypeScript theme={null}
import fs from "node:fs";
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ baseURL: "https://api.naga.ac", apiKey: process.env.NAGAAI_API_KEY });
const pdf = fs.readFileSync("report.pdf").toString("base64");
const message = await client.messages.create({
model: "claude-haiku-4.5",
max_tokens: 1024,
messages: [{
role: "user",
content: [
{ type: "document", source: { type: "base64", media_type: "application/pdf", data: pdf } },
{ type: "text", text: "Summarize this document in two sentences." },
],
}],
});
for (const block of message.content) {
if (block.type === "text") console.log(block.text);
}
```
## Sources
| `source.type` | Use |
| - | - |
| `base64` | A PDF, with `media_type: "application/pdf"` |
| `url` | A link to a PDF |
| `text` | Plain text, with `media_type: "text/plain"` |
| `content` | Not supported. NagaAI drops the whole document block |
Images use the `image` block with a `base64` or `url` source. One inline block can be up to 20 MiB and the whole request up to 32 MiB.
# Streaming
Source: https://docs.naga.ac/api/messages/streaming
Stream Messages events with the Anthropic SDK or raw SSE.
```python Python theme={null}
import os
from anthropic import Anthropic
client = Anthropic(base_url="https://api.naga.ac", api_key=os.environ["NAGAAI_API_KEY"])
with client.messages.stream(
model="claude-sonnet-5",
max_tokens=512,
messages=[{"role": "user", "content": "Explain backpressure in three sentences."}],
) as stream:
for text in stream.text_stream:
print(text, end="", flush=True)
print("\n", stream.get_final_message().usage)
```
```typescript TypeScript theme={null}
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ baseURL: "https://api.naga.ac", apiKey: process.env.NAGAAI_API_KEY });
const stream = client.messages.stream({
model: "claude-sonnet-5",
max_tokens: 512,
messages: [{ role: "user", content: "Explain backpressure in three sentences." }],
});
stream.on("text", (text) => process.stdout.write(text));
const final = await stream.finalMessage();
console.log("\n", final.usage);
```
```bash cURL theme={null}
curl -N https://api.naga.ac/v1/messages \
-H "x-api-key: $NAGAAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-5",
"max_tokens": 512,
"messages": [{"role": "user", "content": "Explain backpressure in three sentences."}],
"stream": true
}'
```
## Events
| Event | Carries |
| - | - |
| `message_start` | The message shell with `id` and `model` |
| `content_block_start` | A new block: `text`, `thinking`, `tool_use` and others |
| `content_block_delta` | `text_delta`, `thinking_delta`, `signature_delta` or `input_json_delta` for tool input |
| `content_block_stop` | The end of a block |
| `message_delta` | `stop_reason` and the final `usage` |
| `message_stop` | The end of the stream |
If a provider fails after the stream has started, an `event: error` arrives with the Anthropic error shape, and the connection closes.
# Thinking
Source: https://docs.naga.ac/api/messages/thinking-blocks
Turn on extended thinking in /v1/messages and keep thinking blocks across turns.
Turn thinking on with `thinking` or with `output_config.effort`.
```python Python theme={null}
import os
from anthropic import Anthropic
client = Anthropic(base_url="https://api.naga.ac", api_key=os.environ["NAGAAI_API_KEY"])
message = client.messages.create(
model="deepseek-v4.1-flash",
max_tokens=4096,
thinking={"type": "enabled", "budget_tokens": 2048},
messages=[{"role": "user", "content": "A bat and a ball cost 1.10 in total. The bat costs 1.00 more than the ball. What does the ball cost?"}],
)
for block in message.content:
if block.type == "thinking":
print("Thinking:", block.thinking)
elif block.type == "text":
print("Answer:", block.text)
```
```typescript TypeScript theme={null}
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ baseURL: "https://api.naga.ac", apiKey: process.env.NAGAAI_API_KEY });
const message = await client.messages.create({
model: "deepseek-v4.1-flash",
max_tokens: 4096,
thinking: { type: "enabled", budget_tokens: 2048 },
messages: [{ role: "user", content: "A bat and a ball cost 1.10 in total. The bat costs 1.00 more than the ball. What does the ball cost?" }],
});
for (const block of message.content) {
if (block.type === "thinking") console.log("Thinking:", block.thinking);
if (block.type === "text") console.log("Answer:", block.text);
}
```
## How NagaAI reads the settings
NagaAI turns thinking settings into one effort level and translates it for the provider serving the model.
| You send | Effort |
| - | - |
| `thinking.budget_tokens` 1024 to 8191 | `low` |
| `thinking.budget_tokens` 8192 to 32767 | `medium` |
| `thinking.budget_tokens` 32768 to 65535 | `high` |
| `thinking.budget_tokens` 65536 or more | `xhigh` |
| `thinking.budget_tokens` under 1024 | `400` error |
| `thinking: {"type": "adaptive"}` | No level: the model uses its own default |
| `output_config.effort`: `low` to `xhigh` | The same level. `max` runs as `xhigh` |
`output_config.effort` wins when you send both. The budget does not cap the thinking exactly, and thinking tokens are part of `usage.output_tokens`.
## Keep thinking blocks across turns
`thinking` blocks carry a `signature`, and `redacted_thinking` blocks carry sealed data. In a tool loop, send the assistant `content` back unchanged, with these blocks in their place. Some models need them to continue. NagaAI removes signatures issued by another provider, so a conversation can move between providers without errors.
# Tool use
Source: https://docs.naga.ac/api/messages/tool-use
Run client tools with /v1/messages and return tool_result blocks.
The model returns `tool_use` blocks and `stop_reason: "tool_use"`. Your code runs the tools and answers in a `user` message with one `tool_result` block per call.
```python Python theme={null}
import json
import os
from anthropic import Anthropic
client = Anthropic(base_url="https://api.naga.ac", api_key=os.environ["NAGAAI_API_KEY"])
tools = [{
"name": "get_weather",
"description": "Current weather for a city.",
"input_schema": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}]
def get_weather(city):
return {"city": city, "temperature_c": 18, "sky": "cloudy"}
messages = [{"role": "user", "content": "What is the weather in Prague?"}]
while True:
message = client.messages.create(model="claude-sonnet-5", max_tokens=1024, tools=tools, messages=messages)
messages.append({"role": "assistant", "content": message.content})
if message.stop_reason != "tool_use":
break
results = [
{"type": "tool_result", "tool_use_id": block.id, "content": json.dumps(get_weather(**block.input))}
for block in message.content
if block.type == "tool_use"
]
messages.append({"role": "user", "content": results})
print(next(block.text for block in message.content if block.type == "text"))
```
```typescript TypeScript theme={null}
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ baseURL: "https://api.naga.ac", apiKey: process.env.NAGAAI_API_KEY });
const tools: Anthropic.Tool[] = [{
name: "get_weather",
description: "Current weather for a city.",
input_schema: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
}];
const getWeather = ({ city }: { city: string }) => ({ city, temperature_c: 18, sky: "cloudy" });
const messages: Anthropic.MessageParam[] = [{ role: "user", content: "What is the weather in Prague?" }];
let message: Anthropic.Message;
while (true) {
message = await client.messages.create({ model: "claude-sonnet-5", max_tokens: 1024, tools, messages });
messages.push({ role: "assistant", content: message.content });
if (message.stop_reason !== "tool_use") break;
const results: Anthropic.ToolResultBlockParam[] = message.content
.filter((block): block is Anthropic.ToolUseBlock => block.type === "tool_use")
.map((block) => ({
type: "tool_result",
tool_use_id: block.id,
content: JSON.stringify(getWeather(block.input as { city: string })),
}));
messages.push({ role: "user", content: results });
}
for (const block of message.content) {
if (block.type === "text") console.log(block.text);
}
```
Send the assistant `content` back as it came, including any `thinking` blocks. NagaAI ignores `is_error` on `tool_result`, so describe a failure in the result text. `tool_choice` takes `{"type": "auto"}`, `{"type": "any"}`, `{"type": "tool", "name": "..."}` or `{"type": "none"}`. See [Tool calling](/build/tool-calling) for the rules shared by all formats.
# Moderations
Source: https://docs.naga.ac/api/moderations
Classify text and images for harmful content with POST /v1/moderations.
```python Python theme={null}
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.naga.ac/v1", api_key=os.environ["NAGAAI_API_KEY"])
result = client.moderations.create(
model="omni-moderation-latest",
input=[
{"type": "text", "text": "I will hurt you"},
{"type": "image_url", "image_url": {"url": "https://media.naga.ac/outputs/6ab3444c-f19e-4944-80dc-d0b49c49c144.png"}},
],
)
outcome = result.results[0]
print(outcome.flagged, outcome.categories)
```
```javascript Node.js theme={null}
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.naga.ac/v1", apiKey: process.env.NAGAAI_API_KEY });
const result = await client.moderations.create({
model: "omni-moderation-latest",
input: [
{ type: "text", text: "I will hurt you" },
{ type: "image_url", image_url: { url: "https://media.naga.ac/outputs/6ab3444c-f19e-4944-80dc-d0b49c49c144.png" } },
],
});
console.log(result.results[0].flagged, result.results[0].categories);
```
```bash cURL theme={null}
curl https://api.naga.ac/v1/moderations \
-H "Authorization: Bearer $NAGAAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "omni-moderation-latest", "input": "I will hurt you"}'
```
`model` is required. `input` is a string, a list of strings, or a list of `text` and `image_url` parts that are judged together.
Each result has `flagged`, `categories` and `category_scores`. `omni-moderation-latest` also returns `category_applied_input_types`, which says whether text, image or both triggered each category. Other models, such as `mistral-moderation`, use their own category set.
NagaAI charges each `image_url` part as 250 input tokens.
# Responses
Source: https://docs.naga.ac/api/responses
POST /v1/responses in the OpenAI Responses format. NagaAI keeps no state between calls.
`POST https://api.naga.ac/v1/responses` accepts the OpenAI Responses format and serves every chat model in the catalog.
```python Python theme={null}
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.naga.ac/v1", api_key=os.environ["NAGAAI_API_KEY"])
response = client.responses.create(
model="deepseek-v4.1-flash",
instructions="Answer in one sentence.",
input="Why do HTTP clients retry with backoff?",
)
print(response.output_text)
```
```javascript Node.js theme={null}
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.naga.ac/v1", apiKey: process.env.NAGAAI_API_KEY });
const response = await client.responses.create({
model: "deepseek-v4.1-flash",
instructions: "Answer in one sentence.",
input: "Why do HTTP clients retry with backoff?",
});
console.log(response.output_text);
```
```bash cURL theme={null}
curl https://api.naga.ac/v1/responses \
-H "Authorization: Bearer $NAGAAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4.1-flash",
"instructions": "Answer in one sentence.",
"input": "Why do HTTP clients retry with backoff?"
}'
```
`output_text` is a helper of the SDK. In raw JSON the text is in the `output` array, in the `message` item's `output_text` part. `output` can also hold `reasoning`, `function_call`, `web_search_call` and `image_generation_call` items, so find the item by `type` rather than by position.
## No state between calls
NagaAI stores nothing. It ignores `previous_response_id`, `store`, `conversation`, `background` and `prompt`, and there is no `GET /v1/responses/{id}`. Send the full conversation in `input` on every call. See [Conversation state](/api/responses/conversation-state).
## What NagaAI adds
* `routing` in the body and the `x-naga-routing-strategy` header, see [Routing](/build/routing).
* `max_uses`, `allowed_domains` and `blocked_domains` on the `web_search` tool, see [Web search](/build/web-search).
* NagaAI accepts chat-style tools, `{"type": "function", "function": {...}}`, next to the flat Responses shape.
## What NagaAI ignores
NagaAI accepts and drops `include`, `metadata`, `user`, `service_tier`, `prompt_cache_key`, `top_logprobs`, `max_tool_calls` and `stream_options`. It also drops server-side tools other than web search. More differences are in [Differences from OpenAI and Anthropic](/build/compatibility).
# Conversation state
Source: https://docs.naga.ac/api/responses/conversation-state
Carry a multi-turn conversation through /v1/responses, which stores nothing between calls.
NagaAI keeps no conversation on its side. A call with `previous_response_id` gets an answer without that context, and no error tells you so. Keep the history in your application and send it in `input` on each turn.
Append the whole `output` of each response to your input list, then add the next user message:
```python Python theme={null}
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.naga.ac/v1", api_key=os.environ["NAGAAI_API_KEY"])
history = [{"role": "user", "content": "My name is Alice. Say hello."}]
response = client.responses.create(model="deepseek-v4.1-flash", input=history)
print(response.output_text)
history += [item.model_dump(exclude_none=True) for item in response.output]
history.append({"role": "user", "content": "What is my name?"})
response = client.responses.create(model="deepseek-v4.1-flash", input=history)
print(response.output_text)
```
```javascript Node.js theme={null}
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.naga.ac/v1", apiKey: process.env.NAGAAI_API_KEY });
const history = [{ role: "user", content: "My name is Alice. Say hello." }];
let response = await client.responses.create({ model: "deepseek-v4.1-flash", input: history });
console.log(response.output_text);
history.push(...response.output);
history.push({ role: "user", content: "What is my name?" });
response = await client.responses.create({ model: "deepseek-v4.1-flash", input: history });
console.log(response.output_text);
```
Sending the output items back, rather than only the text, keeps `reasoning` items and `function_call` items in place. Reasoning models use them to continue, and a `function_call_output` without its `function_call` fails with `400 tool_output_unmatched`.
## Clients that store by default
Some clients replace earlier turns with `item_reference` entries when `store` is on, expecting the server to fill them in. NagaAI refuses such requests with `400` and names the input item to send in full. Turn storing off in the client. In the Vercel AI SDK that is `providerOptions: { openai: { store: false } }`. Codex CLI and OpenCode already send `store: false`.
# Multimodal inputs
Source: https://docs.naga.ac/api/responses/multimodal-inputs
Send images, files and audio as input parts to /v1/responses.
A message in `input` can hold a list of parts. Check `architecture.input_modalities` of the model in [`/v1/models`](/get-started/models): a part the model cannot read fails with `400 unsupported_modality`.
```python Python theme={null}
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.naga.ac/v1", api_key=os.environ["NAGAAI_API_KEY"])
response = client.responses.create(
model="deepseek-v4.1-flash",
input=[{
"role": "user",
"content": [
{"type": "input_text", "text": "What is in this image? One sentence."},
{"type": "input_image", "image_url": "https://media.naga.ac/outputs/6ab3444c-f19e-4944-80dc-d0b49c49c144.png"},
],
}],
)
print(response.output_text)
```
```javascript Node.js theme={null}
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.naga.ac/v1", apiKey: process.env.NAGAAI_API_KEY });
const response = await client.responses.create({
model: "deepseek-v4.1-flash",
input: [{
role: "user",
content: [
{ type: "input_text", text: "What is in this image? One sentence." },
{ type: "input_image", image_url: "https://media.naga.ac/outputs/6ab3444c-f19e-4944-80dc-d0b49c49c144.png" },
],
}],
});
console.log(response.output_text);
```
| Part | Fields |
| - | - |
| `input_image` | `image_url`: an `https` URL or a data URI. `detail`: `low`, `high` or `auto` |
| `input_file` | `file_data` as a data URI with `filename`, or `file_url`. A `file_id` fails with `400 file_id_unsupported`, because NagaAI has no Files API |
| `input_audio` | `input_audio.data` as base64 and `input_audio.format`, as in [Chat Completions](/api/chat-completions/multimodal-content#audio) |
One inline part can be up to 20 MiB and the whole request up to 32 MiB.
Models that draw return an `image_generation_call` output item with the image in `result` and its type in `mime_type`. To edit that image on the next turn, send the item back in `input`.
# Reasoning
Source: https://docs.naga.ac/api/responses/reasoning
Set reasoning effort and read reasoning items in /v1/responses.
`reasoning.effort` takes `none`, `minimal`, `low`, `medium`, `high` or `xhigh`. `reasoning.summary` asks for a summary of the reasoning: `auto`, `concise` or `detailed`.
```python Python theme={null}
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.naga.ac/v1", api_key=os.environ["NAGAAI_API_KEY"])
response = client.responses.create(
model="deepseek-v4.1-flash",
input="A bat and a ball cost 1.10 in total. The bat costs 1.00 more than the ball. What does the ball cost?",
reasoning={"effort": "high", "summary": "auto"},
)
for item in response.output:
if item.type == "reasoning":
for part in item.summary:
print("Reasoning:", part.text)
print("Answer:", response.output_text)
print("Reasoning tokens:", response.usage.output_tokens_details.reasoning_tokens)
```
```javascript Node.js theme={null}
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.naga.ac/v1", apiKey: process.env.NAGAAI_API_KEY });
const response = await client.responses.create({
model: "deepseek-v4.1-flash",
input: "A bat and a ball cost 1.10 in total. The bat costs 1.00 more than the ball. What does the ball cost?",
reasoning: { effort: "high", summary: "auto" },
});
for (const item of response.output) {
if (item.type === "reasoning") item.summary.forEach((part) => console.log("Reasoning:", part.text));
}
console.log("Answer:", response.output_text);
console.log("Reasoning tokens:", response.usage.output_tokens_details.reasoning_tokens);
```
## Reasoning items
A `reasoning` item has three parts. `summary` is a list of `summary_text` parts. `content` holds the full reasoning as `reasoning_text` parts, when the model returns it. `encrypted_content` is an opaque value, when the model seals its reasoning. NagaAI always returns `encrypted_content` when there is one, so `include: ["reasoning.encrypted_content"]` is not needed.
Send reasoning items back in `input` on the next turn, as [Conversation state](/api/responses/conversation-state) shows. NagaAI removes encrypted reasoning that another provider issued, so a conversation can move between providers.
In a stream, reasoning arrives as `response.reasoning_summary_text.delta` and `response.reasoning_text.delta` events.
# Streaming
Source: https://docs.naga.ac/api/responses/streaming
Stream Responses events and rebuild the final response from them.
```python Python theme={null}
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.naga.ac/v1", api_key=os.environ["NAGAAI_API_KEY"])
stream = client.responses.create(
model="deepseek-v4.1-flash",
input="Explain backpressure in three sentences.",
stream=True,
)
for event in stream:
if event.type == "response.output_text.delta":
print(event.delta, end="", flush=True)
elif event.type == "response.completed":
print("\n", event.response.usage)
```
```javascript Node.js theme={null}
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.naga.ac/v1", apiKey: process.env.NAGAAI_API_KEY });
const stream = await client.responses.create({
model: "deepseek-v4.1-flash",
input: "Explain backpressure in three sentences.",
stream: true,
});
for await (const event of stream) {
if (event.type === "response.output_text.delta") process.stdout.write(event.delta);
if (event.type === "response.completed") console.log("\n", event.response.usage);
}
```
```bash cURL theme={null}
curl -N https://api.naga.ac/v1/responses \
-H "Authorization: Bearer $NAGAAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{"model": "deepseek-v4.1-flash", "input": "Explain backpressure in three sentences.", "stream": true}'
```
## Events
Every event has an `event:` line with its type and a `data:` line with a JSON object that repeats the type and carries a `sequence_number`. A text answer produces:
1. `response.created`
2. `response.output_item.added`, then `response.content_part.added`
3. `response.output_text.delta`, many times
4. `response.output_text.done`, `response.content_part.done`, `response.output_item.done`
5. `response.completed`, with the full response and its `usage`
Other items add their own events: `response.function_call_arguments.delta` and `.done` for tool calls, `response.reasoning_summary_text.delta` and `response.reasoning_text.delta` for reasoning, and `response.web_search_call.in_progress`, `.searching` and `.completed` for search.
The stream ends with `response.completed`, `response.incomplete` (for example at `max_output_tokens`) or `response.failed`. There is no `data: [DONE]` line. If a provider fails after the stream has started, an `error` event arrives before `response.failed`.
# Structured outputs
Source: https://docs.naga.ac/api/responses/structured-outputs
Ask /v1/responses for JSON that matches a schema with text.format.
```python Python theme={null}
import json
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.naga.ac/v1", api_key=os.environ["NAGAAI_API_KEY"])
response = client.responses.create(
model="deepseek-v4.1-flash",
input="Name one European capital and its country.",
text={
"format": {
"type": "json_schema",
"name": "capital",
"strict": True,
"schema": {
"type": "object",
"properties": {"city": {"type": "string"}, "country": {"type": "string"}},
"required": ["city", "country"],
"additionalProperties": False,
},
}
},
)
print(json.loads(response.output_text))
```
```javascript Node.js theme={null}
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.naga.ac/v1", apiKey: process.env.NAGAAI_API_KEY });
const response = await client.responses.create({
model: "deepseek-v4.1-flash",
input: "Name one European capital and its country.",
text: {
format: {
type: "json_schema",
name: "capital",
strict: true,
schema: {
type: "object",
properties: { city: { type: "string" }, country: { type: "string" } },
required: ["city", "country"],
additionalProperties: false,
},
},
},
});
console.log(JSON.parse(response.output_text));
```
```bash cURL theme={null}
curl https://api.naga.ac/v1/responses \
-H "Authorization: Bearer $NAGAAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "deepseek-v4.1-flash",
"input": "Name one European capital and its country.",
"text": {
"format": {
"type": "json_schema",
"name": "capital",
"strict": true,
"schema": {
"type": "object",
"properties": {"city": {"type": "string"}, "country": {"type": "string"}},
"required": ["city", "country"],
"additionalProperties": false
}
}
}
}'
```
`{"type": "json_object"}` in `text.format` asks for any JSON object. The SDK helper `client.responses.parse()` builds the same request from a Pydantic or Zod model. On models without schema support NagaAI falls back to JSON mode plus the schema in the prompt, see [Structured outputs](/build/structured-outputs).
# Tool calling
Source: https://docs.naga.ac/api/responses/tool-calling
Run function tools with /v1/responses and send the results back with the full history.
The model returns `function_call` items. Your code runs them and sends `function_call_output` items back. Because NagaAI keeps no state, the next request carries the earlier `function_call` items too.
```python Python theme={null}
import json
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.naga.ac/v1", api_key=os.environ["NAGAAI_API_KEY"])
tools = [{
"type": "function",
"name": "get_weather",
"description": "Current weather for a city.",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"],
},
}]
def get_weather(city):
return {"city": city, "temperature_c": 18, "sky": "cloudy"}
items = [{"role": "user", "content": "What is the weather in Prague?"}]
while True:
response = client.responses.create(model="deepseek-v4.1-flash", input=items, tools=tools)
items += [item.model_dump(exclude_none=True) for item in response.output]
calls = [item for item in response.output if item.type == "function_call"]
if not calls:
break
for call in calls:
result = get_weather(**json.loads(call.arguments))
items.append({"type": "function_call_output", "call_id": call.call_id, "output": json.dumps(result)})
print(response.output_text)
```
```javascript Node.js theme={null}
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.naga.ac/v1", apiKey: process.env.NAGAAI_API_KEY });
const tools = [{
type: "function",
name: "get_weather",
description: "Current weather for a city.",
parameters: {
type: "object",
properties: { city: { type: "string" } },
required: ["city"],
},
}];
const getWeather = ({ city }) => ({ city, temperature_c: 18, sky: "cloudy" });
const items = [{ role: "user", content: "What is the weather in Prague?" }];
let response;
while (true) {
response = await client.responses.create({ model: "deepseek-v4.1-flash", input: items, tools });
items.push(...response.output);
const calls = response.output.filter((item) => item.type === "function_call");
if (!calls.length) break;
for (const call of calls) {
const result = getWeather(JSON.parse(call.arguments));
items.push({ type: "function_call_output", call_id: call.call_id, output: JSON.stringify(result) });
}
}
console.log(response.output_text);
```
`tool_choice` takes `auto`, `none`, `required` or `{"type": "function", "name": "..."}`. NagaAI also accepts the Chat Completions tool shape, `{"type": "function", "function": {...}}`. See [Tool calling](/build/tool-calling) for the rules shared by all formats.
# Billing
Source: https://docs.naga.ac/build/billing
NagaAI bills prepaid credits per request, at the model's published price.
## Credits
NagaAI bills in credits. One credit costs one US dollar. You buy credits in advance and each request spends them. There is no subscription, and credits do not expire.
## What a request costs
Each model has prices per unit in `pricing` of [`/v1/models`](/get-started/models) and on its page at [naga.ac/models](https://naga.ac/models). Most chat models charge per token:
| Usage | Price field | Notes |
| - | - | - |
| Input tokens | `per_input_token` | Everything you send: messages, system prompt, tool definitions |
| Cached input tokens | `per_cached_input_token` | Input the provider served from its prompt cache |
| Output tokens | `per_output_token` | Includes reasoning tokens, even when the reasoning text is hidden |
Writing to a provider's prompt cache costs nothing extra. Image, audio and speech models also have prices per image token, per audio token, per hour of audio or per character. The fields for each model are in its `pricing` object.
For example, `claude-sonnet-5` costs `1e-06` credits per input token and `5e-06` per output token. A request with 2,000 input tokens and 500 output tokens costs 2,000 × 0.000001 + 500 × 0.000005 = 0.0045 credits.
Token counts for a request are in its `usage` object. The response does not include the cost. Spend per day, model and key is in the dashboard and in [`/v1/account/activity`](/account/balance-and-activity).
## When NagaAI charges
* NagaAI charges a request after its answer reaches you, at the price of the model you requested.
* NagaAI does not charge a request that fails, nor attempts at providers that failed before another one answered.
* NagaAI does not charge a stream that fails midway.
* If your client disconnects, NagaAI still finishes the work and charges the full request. This holds for streams and for image, audio, embedding and moderation requests.
* An answer cut short by `max_tokens` costs the tokens it used.
* Models whose id ends in `:free` cost nothing.
## Paying
Top up on the [Credits](https://naga.ac/dashboard/credits) page with **Top Up Balance**. One top-up is between $5 and $5,000.
| Method | Notes |
| - | - |
| Card | Processed by Stripe |
| PayPal | Processed by Stripe |
| Crypto | BTC, ETH, USDC, SOL and other coins through CoinGate. USDT is not accepted here |
| Crypto (legacy) | USDT and 200+ coins through NOWPayments. Shown only to accounts that have paid \$1,000 or more in total |
| Alipay, WeChat Pay | |
NagaAI adds the credits when the payment completes. A refund or chargeback on a card, PayPal or CoinGate payment removes the credits it paid for, which can take the balance below zero.
**Low balance alerts.** On the Credits page you can set a threshold. NagaAI emails you when the balance falls below it.
**Organizations.** Each organization has its own balance and keys. Credits bought in an organization are spent only by its keys. See [Account](/account/account-management).
**Refunds.** Support reviews requests to refund unused credits case by case, within 14 days of the purchase.
# Differences from OpenAI and Anthropic
Source: https://docs.naga.ac/build/compatibility
What changes when you point an OpenAI or Anthropic SDK at NagaAI.
Code written for the official OpenAI or Anthropic API works against NagaAI after you change the base URL, the key and the model id. NagaAI ids come from [`/v1/models`](/get-started/models) and use dots where Anthropic uses hyphens: `claude-opus-5.5`, not `claude-opus-5-5`.
## Not supported
**Responses keeps no state.** NagaAI ignores `previous_response_id`, `store`, `conversation`, `background` and `prompt`. Send the whole conversation in `input` on every call, including the output items of earlier turns. A request that relies on `previous_response_id` gets an answer without that context and no error.
**No stored objects.** There are no Files, Batches, Assistants, Realtime, fine-tuning or legacy `/v1/completions` endpoints, and no `GET` by id for responses, completions or models. These paths return `404`, and so does `POST /v1/messages/count_tokens`.
**Send files inline or by URL.** There is no Files API, so a `file_id` fails with `400 file_id_unsupported`. Send the file as base64 in `file_data`, or as a link in `file_url`.
**Web search is the only server-side tool.** `web_search` works in Responses and `web_search_*` in Messages, when the model can search. NagaAI drops `file_search`, `code_interpreter`, computer use, `mcp`, `shell` and Anthropic's `code_execution`, `web_fetch`, `bash`, `text_editor` and `memory`. A tool type NagaAI does not know fails with `400`. Chat Completions has no web search: `web_search_options` has no effect.
**No explicit prompt caching.** NagaAI drops `cache_control` in Messages and `prompt_cache_key` in the OpenAI formats. Models with automatic caching still report cache hits in `usage`, billed at `per_cached_input_token`. The [`cache` routing strategy](/build/routing) prefers providers that return such hits.
**NagaAI ignores vendor headers:** `anthropic-beta`, `anthropic-version`, `OpenAI-Organization` and `OpenAI-Project`.
## Works differently
**NagaAI drops unknown fields instead of refusing them.** A request with `n: 2`, `seed`, `logprobs`, `logit_bias`, `user`, `metadata`, `service_tier` or `prediction` gets `200`, and NagaAI ignores those fields. You always get one choice, and `logprobs` is `null`. NagaAI ignores a misspelled field name the same way.
**A parameter can be dropped per request.** `supported_parameters` in [`/v1/models`](/get-started/models) lists what at least one provider of the model accepts. When the provider serving your request does not accept `temperature`, `tools`, `response_format` or `reasoning_effort`, NagaAI removes that field for this request.
**The provider can change between requests.** Latency, cache hits and exact wording can differ between two identical requests. See [Routing](/build/routing).
**The response names the catalog model.** `model` holds the NagaAI id, even if you called an alias. NagaAI generates the `id` (`chatcmpl-…`, `resp_…`, `msg_…`), and you cannot fetch a response by it later.
**Cost is not in the response.** `usage` has token counts. Prices are in `pricing` of `/v1/models`, and spend per day, model and key is in [`/v1/account/activity`](/account/balance-and-activity).
**Some status codes differ.**
| Situation | NagaAI | OpenAI |
| - | - | - |
| Balance too low | `402 insufficient_quota` | `429 insufficient_quota` |
| Unknown model | `400 model_not_found` | `404 model_not_found` |
A retired model returns `410 model_deprecated`, and a request no provider could serve returns `503` without provider details. In `/v1/messages` NagaAI uses Anthropic's error types, such as `billing_error` for `402`. See [Errors](/build/error-handling).
**Thinking budgets become effort levels.** In Messages, the model does not stop exactly at `thinking.budget_tokens`, and a budget under 1024 fails with `400`. See [Thinking](/api/messages/thinking-blocks).
**Images return a URL by default.** `/v1/images/generations` and `/v1/images/edits` return `data[].url`, a link to a copy NagaAI keeps. Pass `response_format: "b64_json"` to get base64.
**Transcriptions return text only.** `/v1/audio/transcriptions` and `/v1/audio/translations` return `{"text": "..."}`. NagaAI ignores `response_format`, `timestamp_granularities` and `temperature`.
## Requests NagaAI changes
NagaAI changes these requests instead of returning an error:
* It removes a trailing assistant message (prefill) for models that cannot continue one. The model answers from scratch.
* It lowers a `max_tokens` that does not fit in the model's context window together with the input. The answer can then end with `finish_reason: "length"`.
* It moves `reasoning_effort` to the nearest level the model supports.
* On some models without schema support, it turns `json_schema` into JSON mode and adds the schema to the prompt. Validate the JSON in your code.
## NagaAI extensions
* Every chat model works on all three chat endpoints: a Claude model through Chat Completions, a GPT model through Messages.
* [`routing.strategy`](/build/routing) and the `x-naga-routing-strategy` header.
* Chat Completions returns the model's reasoning in `reasoning_content` and `reasoning_details`, in responses and in stream deltas. See [Reasoning](/api/chat-completions/reasoning).
* Chat Completions and Messages return images from models that draw: `message.images` and `image` blocks.
* Chat Completions accepts more audio formats than the official API. See [Multimodal content](/api/chat-completions/multimodal-content#audio).
# Errors
Source: https://docs.naga.ac/build/error-handling
The error body of each endpoint, the meaning of every status code NagaAI returns, and which ones to retry.
## Error shape
Chat Completions, Responses and every non-chat endpoint return the OpenAI shape:
```json theme={null}
{"error": {"type": "invalid_request_error", "message": "body.messages: ...", "code": null, "param": null}}
```
`/v1/messages` returns the Anthropic shape:
```json theme={null}
{"type": "error", "error": {"type": "invalid_request_error", "message": "..."}}
```
`message` is text for people, and NagaAI can change it. Decide in code by the HTTP status first, then by `error.type` or `error.code`. `code` and `param` are missing or `null` on many errors. On `/v1/messages`, `error.type` uses Anthropic's names by status, such as `authentication_error`, `billing_error` and `rate_limit_error`, and there is no `code`.
Every response from an API endpoint, error or not, carries an `x-request-id` header. Include it when you contact support.
## Where an error comes from
**Your request.** NagaAI checks the request before it picks a provider. A missing field, a wrong type or an image sent to a text-only model fails here with `400` and a message such as `body.messages.0.content: ...`. Fix the request and send it again.
**A provider that could not serve it.** On a timeout, a `5xx`, a rate limit or an exhausted provider quota, NagaAI moves the request to another provider, up to three attempts. If every attempt fails you get `503` with a generic message, and NagaAI does not charge the request.
**A provider's content filter.** You get `400` with the message `Request rejected by upstream content moderation.` and, in the OpenAI formats, `code: "content_moderation"`. Change the content, or try another model.
**A provider that rejected a request NagaAI accepted.** You get `400` with the provider's text after `Upstream returned an error: `. This is a bug in NagaAI's conversion, not in your request. Report it with the `x-request-id`.
## Status codes
| Status | `type` or `code` | Meaning | Retry |
| - | - | - | - |
| `400` | `invalid_request_error` | The request failed validation | No, fix it |
| `400` | `unsupported_modality` | The model does not accept this input type, see `architecture.input_modalities` | No |
| `400` | `model_not_found` | No model has this id or alias, or it does not serve this endpoint | No |
| `400` | `content_moderation` | A provider's content filter rejected the request | No |
| `401` | `invalid_api_key` and others | Missing or wrong key, see [Authentication](/get-started/authentication) | No |
| `402` | `insufficient_quota` | The balance is too low for a paid model | After a top-up |
| `403` | `credit_limit_exceeded` | The key reached its own spending limit | After raising the limit |
| `403` | `inappropriate_content` | NagaAI's moderation blocked an image prompt | No |
| `404` | | The path does not exist. For an unknown path the body is plain text, not JSON | No |
| `405` | | The path exists but not with this HTTP method | No |
| `408` | | The request body stopped arriving for 60 seconds, or took longer than 15 minutes | Once |
| `410` | `model_deprecated` | The model was retired | No, switch models |
| `413` | | The body is over 32 MiB, or one inline part of a chat request is over 20 MiB | No |
| `422` | `billing_metering_incomplete` | The model has no price for an input or output in the request, such as audio, so NagaAI did not serve it | No, change the model or the input |
| `429` | `rate_limit_exceeded` | The free-model rate limit, see [Limits](/build/rate-limits) | After `Retry-After` |
| `500` | `server_error` | A bug in NagaAI | Once, then report |
| `503` | `server_error` | No provider could take the request right now, or NagaAI is shedding load | After `Retry-After`, or with backoff |
`Retry-After` holds seconds. It comes with `429` and with some `503` responses.
## Errors in a stream
NagaAI holds back the HTTP status until the first piece of content is ready. If every provider fails before that point, you get a normal error response with one of the status codes listed under Status codes.
After content has started, the status is already `200`, so an error arrives inside the stream:
| Endpoint | What the stream sends |
| - | - |
| Chat Completions | `data: {"error": {...}}`, then `data: [DONE]` |
| Responses | An `error` event with `code`, `message` and `param`, then `response.failed` |
| Messages | `event: error` with the Anthropic error shape, then the connection closes |
NagaAI does not charge a stream that fails midway, so a retry costs only the new request.
## Retry pattern
```python Python theme={null}
import random
import time
import openai
RETRYABLE = {408, 429, 500, 503}
def create_with_retry(client, **kwargs):
for attempt in range(5):
try:
return client.chat.completions.create(**kwargs)
except openai.APIStatusError as error:
if error.status_code not in RETRYABLE or attempt == 4:
raise
wait = error.response.headers.get("retry-after")
time.sleep(float(wait) if wait else 2**attempt + random.random())
```
```javascript Node.js theme={null}
import OpenAI from "openai";
const RETRYABLE = new Set([408, 429, 500, 503]);
async function createWithRetry(client, params) {
for (let attempt = 0; ; attempt++) {
try {
return await client.chat.completions.create(params);
} catch (error) {
if (!(error instanceof OpenAI.APIError) || !RETRYABLE.has(error.status) || attempt === 4) throw error;
const wait = Number(error.headers?.get("retry-after")) || 2 ** attempt + Math.random();
await new Promise((resolve) => setTimeout(resolve, wait * 1000));
}
}
}
```
The OpenAI and Anthropic SDKs already retry `408`, `409`, `429` and `5xx` twice by default and read `Retry-After`. Raise `max_retries` (Python) or `maxRetries` (Node.js) instead of writing a loop, unless you need custom backoff.
A request your client gave up on can still finish at NagaAI and be charged. Retrying it after a client timeout can charge twice, so set the client timeout above your longest expected answer.
# Multimodal inputs
Source: https://docs.naga.ac/build/multimodal-inputs
Image, file and audio parts in the three chat formats side by side.
| | Chat Completions | Responses | Messages |
| - | - | - | - |
| Image | `{"type": "image_url", "image_url": {"url"}}` with an `https` URL or a data URI | `{"type": "input_image", "image_url"}` | `{"type": "image", "source"}` with `base64` or `url` |
| File or PDF | `{"type": "file", "file": {"filename", "file_data"}}` with a data URI, or `{"file_url"}` | `{"type": "input_file", "filename", "file_data"}` or `file_url` | `{"type": "document", "source"}` with `base64`, `url` or `text` |
| Audio | `{"type": "input_audio", "input_audio": {"data", "format"}}` | `{"type": "input_audio", ...}` | Not accepted |
Code for each endpoint: [Chat Completions](/api/chat-completions/multimodal-content), [Responses](/api/responses/multimodal-inputs), [Messages](/api/messages/documents-and-pdfs).
## Rules
* **Check what the model accepts.** `architecture.input_modalities` in [`/v1/models`](/get-started/models) lists `text`, `image`, `audio` and `file`. Sending another type fails with `400 unsupported_modality` before the request reaches a provider.
* **Size.** One inline image, file or audio part can be up to 20 MiB, and the whole request up to 32 MiB.
* **Files by value or by link.** Send files as base64 or as an `https` link. NagaAI has no Files API, so a `file_id` fails with `400 file_id_unsupported`. Models without native file input receive the text of the file.
* **Audio formats.** `input_audio.data` is raw base64. A `data:` prefix fails with `400`. `format` is one of `wav`, `mp3`, `aiff`, `aac`, `ogg`, `flac`, `m4a`, `pcm16`, `pcm24`.
* **Messages documents.** NagaAI drops a `document` with a `content` source. Use `base64`, `url` or `text`.
# Production checklist
Source: https://docs.naga.ac/build/production-checklist
What to set up before sending production traffic to NagaAI.
**Keys**
* One API key per environment and service, each with a `credit_limit`, so a runaway loop cannot drain the balance. See [API keys](/account/api-keys).
* Keys live in server-side secrets, never in browser or mobile code.
**Balance**
* A low balance alert on the [Credits](https://naga.ac/dashboard/credits) page, set above a day of spend.
* Enough balance for your peak concurrency: each running paid request reserves 0.05 credits. See [Limits](/build/rate-limits).
**Models**
* Model ids copied from the catalog, with their `supported_parameters` and `input_modalities` checked. NagaAI drops parameters a model does not support without an error.
* A fallback model for your main one, used when a request fails with `503`. NagaAI already retries across providers of the same model, but it does not switch models for you.
**Requests**
* A client timeout that fits your longest answers. Long reasoning requests can run for minutes.
* For Responses, the full conversation in every call. NagaAI keeps no state.
* A [routing strategy](/build/routing) if latency or cache hits matter to you.
**Errors**
* Retries with backoff on `408`, `429` and `503`, honoring `Retry-After`. See [Errors](/build/error-handling).
* Stream handlers that expect an error event after content has started.
* The `x-request-id` header of every response in your logs. Support uses it to find the request.
# Limits
Source: https://docs.naga.ac/build/rate-limits
Rate limits for free models, how balance limits concurrent paid requests, key spending limits and request size limits.
## Free models
Models whose id ends in `:free` share one limit per account. Every key and every free model on the account counts toward it.
| Window | Requests |
| - | - |
| Per minute | 10 |
| Per day | 100 |
The limit applies at any balance. Over the limit you get `429 rate_limit_exceeded` with a `Retry-After` header in seconds.
## Paid models
Paid models have no requests-per-minute or requests-per-day limit. Two things bound them instead.
**Balance.** While a paid request runs, NagaAI reserves 0.05 credits of your balance and releases the reserve when it charges the real cost. The number of paid requests that can run at the same time is your balance divided by 0.05, rounded down. With $2.00 you can run 40 at once. Below $0.05 a paid request fails with `402 insufficient_quota`.
**Key spending limit.** A key can carry its own `credit_limit`, set on the [API Keys](https://naga.ac/dashboard/api-keys) page or through the [API](/account/api-keys). When the key's spend reaches the limit, its requests fail with `403 credit_limit_exceeded`. NagaAI checks the limit before a request starts, so the last request can take the spend slightly past it.
## Request size and time
| Limit | Value | Error |
| - | - | - |
| Request body | 32 MiB | `413` |
| One inline file, image or audio part | 20 MiB | `413` |
| Pause while the body is uploading | 60 seconds | `408` |
| Total time to upload the body | 15 minutes | `408` |
## Traffic from one IP address
More than 300 requests in 10 seconds from one IP address to `api.naga.ac` are blocked for 10 seconds at the network edge, before they reach NagaAI.
## Overload
When NagaAI is shedding load it returns `503` with `Retry-After: 1`.
NagaAI sends no `x-ratelimit-*` headers.
# Reasoning
Source: https://docs.naga.ac/build/reasoning
How to set reasoning effort and where the model's reasoning appears in each chat format.
| | Chat Completions | Responses | Messages |
| - | - | - | - |
| Set the effort | `reasoning_effort` | `reasoning: {"effort"}` | `output_config: {"effort"}` or `thinking` |
| Effort values | `none`, `minimal`, `low`, `medium`, `high`, `xhigh` | same as Chat Completions | `low`, `medium`, `high`, `xhigh`, `max` |
| Reasoning in the answer | `message.reasoning_content` (text) and `message.reasoning_details` (structured) | Output item `reasoning` with `summary` and `content` | `thinking` and `redacted_thinking` blocks |
| Reasoning tokens | `usage.completion_tokens_details.reasoning_tokens` | `usage.output_tokens_details.reasoning_tokens` | Included in `usage.output_tokens` |
Code for each endpoint: [Chat Completions](/api/chat-completions/reasoning), [Responses](/api/responses/reasoning), [Messages](/api/messages/thinking-blocks).
## How NagaAI handles effort
* **One scale for every model.** NagaAI converts the level for each model. If a model does not support the level you asked for, NagaAI uses the nearest level it does support.
* **Budgets become levels.** In Messages, `thinking.budget_tokens` maps to a level: 1024 to 8191 is `low`, up to 32767 is `medium`, up to 65535 is `high`, and 65536 or more is `xhigh`. `output_config.effort` wins over `thinking` when you send both. `max` runs as `xhigh`.
* **Models without reasoning ignore it.** If `reasoning_effort` is missing from a model's `supported_parameters`, NagaAI removes it from the request.
* **Reasoning costs the output price.** NagaAI bills reasoning tokens as output, even when the provider hides the reasoning text.
## Multi-turn conversations
Some models need their earlier reasoning back to keep it across tool calls. Send the previous assistant turn back unchanged: the whole Chat Completions message with `reasoning_details`, the Responses `reasoning` items, or the Messages `thinking` blocks with their `signature`. NagaAI checks where a signature came from and removes signatures that another provider issued, so a conversation can move between providers without errors.
# Routing
Source: https://docs.naga.ac/build/routing
NagaAI picks a provider for every chat request and moves it to another on failure; a routing strategy sets the order.
## What NagaAI does with each request
Most models in the catalog are served by several providers. For every chat request NagaAI:
1. Takes the providers that serve the requested model and are healthy right now.
2. Orders them by the routing strategy.
3. Sends the request to the first one. If that provider fails with a timeout, a `5xx`, a rate limit or an exhausted quota, NagaAI tries the next one, up to three attempts. NagaAI does not retry a `400` or `422` from a provider.
You get one answer or one error. The response never names the provider, and NagaAI does not charge failed attempts. The `model` field of the response is the NagaAI catalog id, whichever provider answered.
Because the provider can change from one request to the next, two identical requests can land on different providers. Latency, cache hits and exact wording can differ between them.
## Strategies
A strategy changes the order in which NagaAI tries providers. It never removes a provider and never makes a request fail.
| Strategy | Prefers |
| - | - |
| `reliability` | The provider with the best recent success rate |
| `balanced` | Speed, among providers whose success rate is close to the best |
| `latency` | The shortest time to the first token. Without streaming, the fastest complete answer |
| `throughput` | The most output tokens per second |
| `cache` | Providers that return prompt-cache reads for this model, which lowers the price of long repeated prompts |
If you do not choose a strategy, NagaAI chooses one, and the response header below tells you which.
Strategies apply to `/v1/chat/completions`, `/v1/responses` and `/v1/messages`. Images, audio, embeddings and moderation ignore them.
## Choosing a strategy
There are three places to set one. When more than one is present, the first in this list wins.
1. The request body: a top-level `routing` object.
2. The `x-naga-routing-strategy` request header.
3. The API key's default, set through the [API keys API](/account/api-keys).
### In the request body
```json theme={null}
{
"model": "claude-sonnet-5",
"messages": [{"role": "user", "content": "Hello"}],
"routing": {"strategy": "latency"}
}
```
The same `routing` object works in all three chat formats. The value is case-sensitive. An unknown strategy or an unknown key inside `routing` fails with `400 invalid_request_error`:
```json theme={null}
{"error": {"type": "invalid_request_error", "message": "body.routing.strategy: unknown variant `fast`, expected one of `reliability`, `balanced`, `latency`, `throughput`, `cache` at line 1 column 114"}}
```
SDKs pass extra body fields through their own option:
```python Python theme={null}
completion = client.chat.completions.create(
model="claude-sonnet-5",
messages=[{"role": "user", "content": "Hello"}],
extra_body={"routing": {"strategy": "latency"}},
)
```
```javascript Node.js theme={null}
const completion = await client.chat.completions.create({
model: "claude-sonnet-5",
messages: [{ role: "user", content: "Hello" }],
routing: { strategy: "latency" },
});
```
### In a header
Use the header when you cannot change the body, for example in a tool that only lets you add headers.
```http theme={null}
x-naga-routing-strategy: throughput
```
The header is case-insensitive. NagaAI ignores an unknown value in the header instead of failing the request.
### As a key default
Set `default_routing_strategy` when you create or update a key through the account API. Every request made with that key uses the strategy unless the body or the header names another. Send `null` in an update to go back to letting NagaAI choose.
```bash theme={null}
curl -X PATCH https://api.naga.ac/v1/account/keys/KEY_ID \
-H "Authorization: Bearer $NAGAAI_PROVISIONING_KEY" \
-H "Content-Type: application/json" \
-d '{"default_routing_strategy": "cache"}'
```
## Checking which strategy ran
Every successful chat response carries the strategy NagaAI used:
```http theme={null}
x-naga-routing-strategy: latency
```
Errors returned before NagaAI reaches a provider do not carry this header.
## What you cannot choose
NagaAI does not let a request name a provider, exclude one, or list fallback models. To use another model after a failure, catch the error and send a new request with that model.
# Streaming
Source: https://docs.naga.ac/build/streaming
The stream events of Chat Completions, Responses and Messages side by side.
Set `stream: true` on any chat endpoint. The response is `text/event-stream`.
| | Chat Completions | Responses | Messages |
| - | - | - | - |
| Text | `choices[0].delta.content` | `response.output_text.delta` event | `content_block_delta` with `text_delta` |
| Tool arguments | `choices[0].delta.tool_calls[].function.arguments` | `response.function_call_arguments.delta` | `content_block_delta` with `input_json_delta` |
| Reasoning | `choices[0].delta.reasoning_content` | `response.reasoning_summary_text.delta`, `response.reasoning_text.delta` | `content_block_delta` with `thinking_delta` |
| Usage | Last chunk, when you send `stream_options: {"include_usage": true}` | In `response.completed` | In `message_delta` |
| End of stream | A chunk with `finish_reason`, then `data: [DONE]` | `response.completed`, `response.incomplete` or `response.failed`. No `[DONE]` | `message_stop` |
Code for each endpoint: [Chat Completions](/api/chat-completions/streaming), [Responses](/api/responses/streaming), [Messages](/api/messages/streaming).
## What NagaAI adds
* **The HTTP status waits for content.** NagaAI sends the `200` only when the first content is ready. If the chosen provider fails before that, NagaAI tries the next one. If all fail, you get a normal error status instead of an error inside a stream.
* **Errors after the start arrive as events.** See [Errors in a stream](/build/error-handling#errors-in-a-stream).
* **A disconnect does not stop generation.** If your client closes the connection, the model finishes and NagaAI charges the full request.
* **Failures before content rotate.** If a provider fails inside its stream before sending content, NagaAI moves the request to another provider.
# Structured outputs
Source: https://docs.naga.ac/build/structured-outputs
How to ask for JSON that matches a schema in each chat format, and what happens on models without schema support.
| | Chat Completions | Responses | Messages |
| - | - | - | - |
| JSON with a schema | `response_format: {"type": "json_schema", "json_schema": {"name", "schema", "strict"}}` | `text: {"format": {"type": "json_schema", "name", "schema", "strict"}}` | `output_config: {"format": {"type": "json_schema", "schema"}}` |
| Any JSON object | `response_format: {"type": "json_object"}` | `text: {"format": {"type": "json_object"}}` | Not available |
| Where the JSON is | `choices[0].message.content` | The `output_text` item | The first `text` block |
Code for each endpoint: [Chat Completions](/api/chat-completions/structured-outputs), [Responses](/api/responses/structured-outputs).
## Models without schema support
Check `supported_parameters` in [`/v1/models`](/get-started/models) for `response_format`. The list covers every provider of the model, so the provider serving a request can still lack schema support. Then NagaAI sends JSON mode and adds your schema to the prompt, and schema compliance is not guaranteed. Parse the result and validate it against the schema in your code.
# Tokens and usage
Source: https://docs.naga.ac/build/tokens-and-usage
The usage fields of the three chat formats side by side, and how they map to price.
| | Chat Completions | Responses | Messages |
| - | - | - | - |
| Input | `prompt_tokens`, cached tokens included | `input_tokens`, cached tokens included | `input_tokens`, cached tokens not included |
| Cached input | `prompt_tokens_details.cached_tokens` | `input_tokens_details.cached_tokens` | `cache_read_input_tokens` |
| Output | `completion_tokens` | `output_tokens` | `output_tokens` |
| Reasoning | `completion_tokens_details.reasoning_tokens` | `output_tokens_details.reasoning_tokens` | Inside `output_tokens` |
| Audio and image tokens | `prompt_tokens_details.audio_tokens`, `completion_tokens_details.image_tokens` | `input_tokens_details.audio_tokens`, `output_tokens_details.image_tokens` | |
NagaAI returns the audio and image fields only when the provider reports them. `total_tokens` can differ from input plus output.
## How usage maps to cost
* NagaAI charges uncached input at `per_input_token` and cached input at `per_cached_input_token`. In the OpenAI formats uncached input is input minus cached input; in Messages it is `input_tokens` as reported.
* NagaAI charges output, including reasoning, at `per_output_token`.
* `cache_creation_input_tokens` in Messages shows tokens written to the provider's cache. Writing to the cache costs nothing extra.
* The response has no cost field. See [Billing](/build/billing) for prices and [Balance, metrics and logs](/account/balance-and-activity) for spend.
In a stream, usage arrives at the end: in the last chunk of Chat Completions when you set `stream_options: {"include_usage": true}`, in `response.completed` in Responses, and in `message_delta` in Messages.
# Tool calling
Source: https://docs.naga.ac/build/tool-calling
Function tools in the three chat formats side by side, with the rules NagaAI enforces.
| | Chat Completions | Responses | Messages |
| - | - | - | - |
| Declare | `tools: [{"type": "function", "function": {"name", "description", "parameters"}}]` | `tools: [{"type": "function", "name", "description", "parameters"}]` | `tools: [{"name", "description", "input_schema"}]` |
| The model calls | `message.tool_calls[]` with `id` and `function.arguments` as a JSON string | Output item `function_call` with `call_id` and `arguments` | Content block `tool_use` with `id` and `input` as an object |
| The turn stops with | `finish_reason: "tool_calls"` | The response ends after the `function_call` items | `stop_reason: "tool_use"` |
| You answer with | A `role: "tool"` message with `tool_call_id` | An input item `function_call_output` with `call_id` | A `user` message with a `tool_result` block and `tool_use_id` |
Code for each endpoint: [Chat Completions](/api/chat-completions/tool-calling), [Responses](/api/responses/tool-calling), [Messages](/api/messages/tool-use).
## Rules
* **Every call needs its result, and every result its call.** A tool call without a matching result, or a result without its call (`tool_output_unmatched`), fails with `400` before the request reaches a provider.
* **Responses keeps no state.** Send the earlier `function_call` items back together with their `function_call_output` items. See [Conversation state](/api/responses/conversation-state).
* **Models without tool support lose the tools.** If `tools` is missing from a model's `supported_parameters`, NagaAI removes the tools from the request and the model answers in text.
* **Some models accept only `tool_choice: "auto"`.** For `claude-opus-5.5`, `claude-fable-5.1` and Mythos 5.1, NagaAI turns `required` or a named tool into `auto`, so the model may answer in text.
* **NagaAI refuses legacy fields.** `functions`, the `function` role and `function_call` on a message fail with `400`, and NagaAI drops a top-level `function_call`. Use `tools`, `tool_choice`, `tool_calls` and the `tool` role.
* **Function tools and web search only.** NagaAI supports your own function tools and web search. NagaAI drops other server-side tools, see [Differences](/build/compatibility).
# Web search
Source: https://docs.naga.ac/build/web-search
How to ask a model to search the web in Responses and Messages, and how to tell whether a search ran.
| | Chat Completions | Responses | Messages |
| - | - | - | - |
| Turn it on | Not available | `tools: [{"type": "web_search"}]` | `tools: [{"type": "web_search_20250305", "name": "web_search"}]` |
| Proof that it ran | | A `web_search_call` output item | `server_tool_use` and `web_search_tool_result` blocks |
| Sources | | `url_citation` annotations on the output text | Inside `web_search_tool_result`. Text blocks carry `citations: null` |
NagaAI drops `web_search_options` in Chat Completions. Use Responses or Messages for search.
## Search depends on the provider
NagaAI passes the search tool to the provider that serves the request. If that provider cannot search, NagaAI removes the tool, and the model answers from what it already knows, without an error. Check the answer for a `web_search_call` item or a `web_search_tool_result` block before you rely on it being current.
## Options
Responses accepts `web_search` with `search_context_size`, `user_location`, `filters.allowed_domains`, and the NagaAI additions `max_uses`, `blocked_domains` and `external_web_access`. The older `web_search_preview` type takes only `search_context_size` and `user_location`. Sending both allowed and blocked domains fails with `400`.
Messages accepts `web_search_20250305` and `web_search_20260209` with `max_uses`, `allowed_domains`, `blocked_domains` and `user_location`, plus the NagaAI additions `search_context_size` and `external_web_access`.
Options the serving model cannot use have no effect, and no error is raised.
# Changelog
Source: https://docs.naga.ac/changelog
Changes to the NagaAI API, billing and dashboard that affect your code or your bill.
* `GET /v1/models` accepts an `x-naga-supported-endpoint` header. With `chat.completions` it lists only the models that serve Chat Completions, and a name no model serves returns an empty list. Without the header the list is unchanged. The [LibreChat guide](/integrations/chat-apps/librechat) uses it to load chat models automatically.
* `GET /v1/models` without a key and `GET /v1/startups` may be cached for 60 seconds and served for 60 more while the cache refreshes. A catalog change shows within two minutes. A request with a key is never answered from cache: an invalid key gets `401`, and an account with no balance sees only `:free` models.
* Tool calling works across turns on Gemini 2.5 models. Before, the request that carried a tool result failed with `400`.
* Gemini 2.5 Flash Lite accepts every `reasoning_effort`. It reasons only at `high`, so `low` and above run as `high`, and `none` and `minimal` run without reasoning. Gemini 2.5 Flash runs `medium` as `high`. Both failed with `400` before.
* A provider that no longer serves a model moves the request to another provider. Before, some of these returned `400` with `Upstream returned an error:`.
* Fewer `400` errors on Claude models: NagaAI removes trailing assistant messages and empty user messages where the provider rejects them.
* `max_tokens` works on `gpt-6-*` models in Chat Completions.
* PDFs and text files reach every model that accepts files. Models without native file input receive the text of the file.
* `file_url` works on every model in Chat Completions and Responses, and `url` sources work for documents in Messages.
* A `file_id` fails with `400 file_id_unsupported`, and an `item_reference` in Responses fails with `400`; NagaAI used to drop it.
* Text to speech returns `400` for a `response_format` the model cannot produce.
* `/v1/messages` accepts mid-conversation `role: "system"` messages with text content. Claude Code sends them on its first request.
* A provider failure inside a stream, before any content, moves the request to another provider. If every attempt fails, you get the generic `503`.
* A provider's rejection inside a stream arrives as `400 invalid_request_error` with `Upstream returned an error:` in front of its text.
* NagaAI no longer returns `502`. A request no provider could serve returns `503`.
* Image, audio, embedding and moderation endpoints report provider errors like the chat endpoints: a generic `503`, `400` with `Upstream returned an error:`, or `400` with `Request rejected by upstream content moderation.` (was `403 inappropriate_content`).
* `/v1/messages` uses Anthropic error types by status: `authentication_error`, `billing_error`, `not_found_error`, `rate_limit_error`.
* A `function_call_output` without its `function_call` fails with `400 tool_output_unmatched`. A `language` that is not an ISO 639-1 code fails with `400` on transcriptions. `days` outside 1 to 30 on `/v1/account/activity` fails with `400` instead of `500`.
* `/v1/models` no longer lists `web_search_options`, `functions`, `function_call`, `logit_bias`, `logprobs` or `prediction` in `supported_parameters`: NagaAI does not pass them to any provider.
* `json_object` works without the word "json" in the prompt.
* Responses output lists reasoning before the answer.
* `/v1/responses` carries Codex CLI's tool formats: freeform tools such as `apply_patch`, MCP tools grouped by namespace, and the compact tool list of `gpt-6-*` and `gpt-5.6-*`.
* The API reference documents the `x-naga-routing-strategy` request and response header.
* An unknown `default_routing_strategy` in `POST` or `PATCH /v1/account/keys` now fails with `400` instead of `500`. The response to `POST /v1/account/keys` includes `default_routing_strategy`.
* This documentation was rewritten against the current API. New pages: [Routing](/build/routing), [Differences from OpenAI and Anthropic](/build/compatibility), [Limits](/build/rate-limits).
* Crypto top-ups go through CoinGate: BTC, ETH, USDC, SOL and other coins. USDT is not accepted there.
* The legacy crypto method with USDT and 200+ coins stays available to accounts that have paid \$1,000 or more in total.
* Card and PayPal payments add credits only when the payment completes. A refund or chargeback removes the credits it paid for.
* Bank debit methods such as SEPA are no longer offered at checkout.
* Routing strategies: `reliability`, `balanced`, `latency`, `throughput` and `cache`. Set one in the `routing` body field, the `x-naga-routing-strategy` header or as a key default. The `x-naga-routing-strategy` response header names the strategy NagaAI used. See [Routing](/build/routing).
* NagaAI removes reasoning signatures issued by one provider before a request goes to another. Conversations with reasoning models no longer fail when the provider changes.
* Errors come in three kinds. A provider failure moves the request to another provider, and if all fail you get a generic `503` or `502`. A provider's content filter returns `400` with `code: "content_moderation"`. A provider's refusal of a valid request returns `400` with `Upstream returned an error:` in front of its text.
* NagaAI drops parameters a model does not list in `supported_parameters` before the request reaches a provider.
* A conversation with a tool call and no matching tool result fails with `400`.
* An input the model cannot read, such as an image for a text-only model, fails with `400 unsupported_modality` before it reaches a provider.
* `pricing` in `/v1/models` has new units: `per_input_audio_token`, `per_output_audio_token`, `per_input_audio_hour`, `per_output_audio_hour`, `per_output_text_token`, `per_input_text_character` and `per_output_text_character`. `per_audio_token` is gone.
* NagaAI charges cached input tokens at `per_cached_input_token` on every model that has the price. Before, some models charged them at zero.
* Every response from an API endpoint carries an `x-request-id` header. The same id identifies the request on the Logs page.
* Response ids have the prefixes `chatcmpl-`, `resp_` and `msg_`, and `model` always holds the catalog id.
* A request body that stops arriving for 60 seconds, or takes longer than 15 minutes, fails with `408`.
* `dall-e-3` and `dall-e-3:free` left the catalog after OpenAI retired them.
* Request bodies are limited to 32 MiB and single inline files to 20 MiB. Larger requests fail with `413`.
* Validation errors return `400 invalid_request_error` with a text `message` such as `body.messages: ...`.
* A model with no available provider returns `503` with `No upstream available.`
* NagaAI does not charge a request that fails before its answer reaches you.
* A running paid request reserves 0.05 credits of the balance. A paid request with less than 0.05 credits left fails with `402`.
* `max_tokens` in Chat Completions works as an alias of `max_completion_tokens`.
* Chat models that draw return images in all three chat formats: `message.images` in Chat Completions, `image_generation_call` in Responses and `image` blocks in Messages.
* Chat Completions and Responses accept audio input on models that support it.
* `reasoning_effort` is moved to the nearest level a model supports instead of failing.
* Data logging (beta) on the Privacy page stores request and response bodies for 30 days and shows them in Logs.
* `/v1/models` no longer has `supported_parameters_by_endpoint`. Use `supported_parameters`.
# Authentication
Source: https://docs.naga.ac/get-started/authentication
API keys call models and provisioning keys manage the account. Headers for each, and the 401 errors.
NagaAI has two kinds of keys. Both look the same, `ng-` followed by 32 characters, and neither works in place of the other.
| Key | Calls | Created on |
| - | - | - |
| API key | Every model endpoint: chat, responses, messages, images, audio, embeddings, moderations | [API Keys](https://naga.ac/dashboard/api-keys) |
| Provisioning key | `/v1/account/*`: balance, activity and API key management | [Provisioning Keys](https://naga.ac/dashboard/provisioning-keys) |
`GET /v1/models` and `GET /v1/startups` need no key.
## Model endpoints
Send the API key in either header. If a request carries a `Bearer` token and `x-api-key`, NagaAI uses the `Bearer` token.
```http theme={null}
Authorization: Bearer YOUR_API_KEY
x-api-key: YOUR_API_KEY
```
OpenAI SDKs send `Authorization: Bearer`. The Anthropic SDK sends `x-api-key`. Both work on every model endpoint, including `/v1/messages`.
## Account endpoints
`/v1/account/*` accepts only a provisioning key, and only as `Authorization: Bearer`. The OpenAI and Anthropic SDKs have no methods for these endpoints, so call them over HTTP.
```python Python theme={null}
import os
import requests
response = requests.get(
"https://api.naga.ac/v1/account/balance",
headers={"Authorization": f"Bearer {os.environ['NAGAAI_PROVISIONING_KEY']}"},
)
response.raise_for_status()
print(response.json()["balance"])
```
```javascript Node.js theme={null}
const response = await fetch("https://api.naga.ac/v1/account/balance", {
headers: { Authorization: `Bearer ${process.env.NAGAAI_PROVISIONING_KEY}` },
});
if (!response.ok) throw new Error(`HTTP ${response.status}`);
const { balance } = await response.json();
console.log(balance);
```
```bash cURL theme={null}
curl https://api.naga.ac/v1/account/balance \
-H "Authorization: Bearer $NAGAAI_PROVISIONING_KEY"
```
The balance is a decimal string, for example `"22.2782894050"`, so it keeps every digit.
## 401 errors
The error name is in `error.type`. On `/v1/messages` every row below arrives as `error.type: "authentication_error"`, and the message tells them apart.
| `error.type` | Meaning |
| - | - |
| `authentication_required` | No key, a header scheme other than `Bearer`, or `x-api-key` on an account endpoint |
| `invalid_api_key` | The key does not exist, or the request sent a provisioning key to a model endpoint |
| `disabled_api_key` | The key was disabled on the API Keys page or through the API |
| `expired_api_key` | The key passed its `expires_at` |
| `invalid_provisioning_key` | The provisioning key does not exist, or the request sent an API key to an account endpoint |
| `disabled_provisioning_key` | The provisioning key was disabled |
| `account_suspended`, `account_disabled`, `account_closed` | The account itself is restricted. Contact support. |
Keep keys on your server. A key in browser or mobile code can be copied by anyone who opens the app.
# Choose an API
Source: https://docs.naga.ac/get-started/choose-an-api
Which endpoint to call for chat, images, audio, embeddings and moderation.
## Chat and agents
The three chat endpoints reach the same models. Pick the one your SDK or tool already speaks.
| If your code uses | Call | Guide |
| - | - | - |
| `client.chat.completions` from the OpenAI SDK, or a tool that asks for an "OpenAI-compatible" base URL | `POST /v1/chat/completions` | [Chat Completions](/api/chat-completions) |
| `client.responses` from the OpenAI SDK, Codex CLI, OpenAI Agents SDK | `POST /v1/responses` | [Responses](/api/responses) |
| The Anthropic SDK, Claude Code | `POST /v1/messages` | [Messages](/api/messages) |
For new code, choose by the features you need:
* **Chat Completions** is the format most third-party tools expect. NagaAI adds the model's reasoning text to it as `reasoning_content`.
* **Responses** returns typed output items and streams named events. NagaAI keeps no state between Responses calls, so you send the whole conversation each time.
* **Messages** uses Anthropic content blocks and requires `max_tokens`.
## Other tasks
| Task | Call | Guide |
| - | - | - |
| Generate or edit images | `POST /v1/images/generations`, `POST /v1/images/edits` | [Images](/api/images) |
| Text to speech | `POST /v1/audio/speech` | [Audio](/api/audio) |
| Speech to text, translation to English | `POST /v1/audio/transcriptions`, `POST /v1/audio/translations` | [Audio](/api/audio) |
| Vectors for search and retrieval | `POST /v1/embeddings` | [Embeddings](/api/embeddings) |
| Classify text and images for safety | `POST /v1/moderations` | [Moderations](/api/moderations) |
Field-by-field contracts for every endpoint are in the [API reference](/api-reference/overview).
# Models
Source: https://docs.naga.ac/get-started/models
The catalog of models in the dashboard and in GET /v1/models, field by field.
## Browse the catalog
[naga.ac/models](https://naga.ac/models) lists every model with its price, context window and inputs. You can filter by input and output type, by supported parameter and by publisher, and sort by price, latency, throughput or popularity. Each model page has a copy button for its id and a **Try in Playground** link.
## Free and paid models
A model id that ends in `:free` costs nothing. Free models share one rate limit per account, listed in [Limits](/build/rate-limits). Every other model needs a balance of at least 0.05 credits, see [Limits](/build/rate-limits).
## The catalog in code
`GET /v1/models` returns the same catalog. It works without a key, for example with cURL. The SDKs always send a key, so pass a real one: an invalid key fails with `401`. With a key whose account has no balance, the list shrinks to the `:free` models.
```python Python theme={null}
import os
from openai import OpenAI
client = OpenAI(base_url="https://api.naga.ac/v1", api_key=os.environ["NAGAAI_API_KEY"])
for model in client.models.list():
print(model.id)
```
```javascript Node.js theme={null}
import OpenAI from "openai";
const client = new OpenAI({ baseURL: "https://api.naga.ac/v1", apiKey: process.env.NAGAAI_API_KEY });
for await (const model of client.models.list()) {
console.log(model.id);
}
```
```bash cURL theme={null}
curl https://api.naga.ac/v1/models
```
One entry, shortened:
```json theme={null}
{
"id": "claude-sonnet-5",
"object": "model",
"owned_by": "anthropic",
"display_name": "Claude Sonnet 5",
"context_window": 1000000,
"supported_endpoints": ["chat.completions"],
"pricing": {
"per_input_token": 1e-06,
"per_cached_input_token": 1e-07,
"per_output_token": 5e-06
},
"architecture": {
"input_modalities": ["text", "image"],
"output_modalities": ["text"]
},
"supported_parameters": ["max_completion_tokens", "reasoning_effort", "response_format", "temperature", "tools"]
}
```
| Field | Meaning |
| - | - |
| `supported_endpoints` | Which endpoint family serves the model. `chat.completions` means all three chat endpoints: `/v1/chat/completions`, `/v1/responses` and `/v1/messages`. Other values are `embeddings`, `moderations`, `images.generations`, `images.edits`, `audio.speech`, `audio.transcriptions` and `audio.translations`. |
| `architecture.input_modalities` | What the model accepts: `text`, `image`, `audio`, `file`. A request with another input type fails with `400` before it reaches a provider. |
| `supported_parameters` | Optional request parameters that at least one provider of the model accepts. The provider serving a request may still drop some of them, see [Differences from OpenAI and Anthropic](/build/compatibility). |
| `pricing` | Credits per unit, where one credit is one US dollar. `1e-06` per input token is \$1 per million tokens. Units other than tokens appear for images, audio and characters. |
| `aliases` | Other names the model answers to. |
| `max_output_images` | How many images one request can return, on image models. |
| `created`, `description`, `owned_by` | When the model joined the catalog, a short description, and the publisher (or `null`). |
| `deprecates_at` | Unix time after which the model returns `410 model_deprecated`. |
To list one family, send its `supported_endpoints` value in the `x-naga-supported-endpoint` header. A value no model serves returns an empty list.
```bash theme={null}
curl https://api.naga.ac/v1/models -H "x-naga-supported-endpoint: chat.completions"
```
## Aliases and dated ids
Some models have a dated id and a short alias, for example `gpt-image-2-2026-04-21` and `gpt-image-2`. A request may use either. The `model` field of the response always holds the catalog id, so pin the id you see there if you need the exact version.
# How NagaAI works
Source: https://docs.naga.ac/get-started/overview
The parts of NagaAI a developer touches: request formats, models, providers, credits and keys.
## One key, three request formats
NagaAI accepts the three chat formats that most SDKs and tools already speak. Every chat model in the catalog works on all three endpoints. Pick the format your code uses, not the one the model's vendor uses.
| Endpoint | Format | SDK |
| - | - | - |
| `POST /v1/chat/completions` | OpenAI Chat Completions | `openai` for Python and Node.js, most OpenAI-compatible tools |
| `POST /v1/responses` | OpenAI Responses | `openai`, Codex CLI, OpenAI Agents SDK |
| `POST /v1/messages` | Anthropic Messages | `anthropic`, Claude Code |
Images, audio, embeddings and moderation have their own endpoints in the OpenAI format. See [Choose an API](/get-started/choose-an-api).
## Models
`GET /v1/models` lists every model with its id, price, context window and input types. A request names a model by its id or by one of its aliases. Model ids that end in `:free` cost nothing and share one rate limit per account. See [Models](/get-started/models).
## Providers and routing
Most models are served by more than one provider. For each request NagaAI chooses a provider, and if that provider fails it moves the request to another one. You never see which provider answered. You can ask NagaAI to prefer speed, time to first token or cache hits with a routing strategy. See [Routing](/build/routing).
## Credits
NagaAI bills in credits, and one credit costs one US dollar. NagaAI charges each request for the tokens it used, at the model's published price. A request that fails before the answer reaches you costs nothing. See [Billing](/build/billing).
## Keys
An API key calls models. A provisioning key manages the account through the `/v1/account/*` endpoints and cannot call models. See [Authentication](/get-started/authentication).
# Quickstart
Source: https://docs.naga.ac/get-started/quickstart
Create an API key and send your first request with the OpenAI or Anthropic SDK.
Open [API Keys](https://naga.ac/dashboard/api-keys) in the dashboard and select **Create API Key**. The dashboard shows the key once, so copy it before you close the window.
Paid models need a balance of at least 0.05 credits. Top up on the [Credits](https://naga.ac/dashboard/credits) page, or start with a model whose id ends in `:free`.
```bash Python theme={null}
pip install openai
```
```bash Node.js theme={null}
npm install openai
```
If your code already uses the Anthropic SDK, keep it. The last step shows the Messages call.
Point the SDK at `https://api.naga.ac/v1` and pass your key.
```python Python theme={null}
import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.naga.ac/v1",
api_key=os.environ["NAGAAI_API_KEY"],
)
completion = client.chat.completions.create(
model="claude-sonnet-5",
messages=[{"role": "user", "content": "What is the capital of France? Answer in one sentence."}],
)
print(completion.choices[0].message.content)
```
```javascript Node.js theme={null}
import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.naga.ac/v1",
apiKey: process.env.NAGAAI_API_KEY,
});
const completion = await client.chat.completions.create({
model: "claude-sonnet-5",
messages: [{ role: "user", content: "What is the capital of France? Answer in one sentence." }],
});
console.log(completion.choices[0].message.content);
```
```bash cURL theme={null}
curl https://api.naga.ac/v1/chat/completions \
-H "Authorization: Bearer $NAGAAI_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "claude-sonnet-5",
"messages": [{"role": "user", "content": "What is the capital of France? Answer in one sentence."}]
}'
```
The answer is in `choices[0].message.content`.
The Anthropic SDK takes the host without `/v1`.
```python Python theme={null}
import os
from anthropic import Anthropic
client = Anthropic(
base_url="https://api.naga.ac",
api_key=os.environ["NAGAAI_API_KEY"],
)
message = client.messages.create(
model="claude-sonnet-5",
max_tokens=256,
messages=[{"role": "user", "content": "What is the capital of France? Answer in one sentence."}],
)
print(message.content[0].text)
```
```typescript TypeScript theme={null}
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({
baseURL: "https://api.naga.ac",
apiKey: process.env.NAGAAI_API_KEY,
});
const message = await client.messages.create({
model: "claude-sonnet-5",
max_tokens: 256,
messages: [{ role: "user", content: "What is the capital of France? Answer in one sentence." }],
});
console.log(message.content[0].type === "text" ? message.content[0].text : "");
```
## If the request fails
| Status | Cause | Fix |
| - | - | - |
| `401` | The key is wrong, disabled or expired, or it is a provisioning key | Create an API key on the API Keys page |
| `402` | The balance is too low for a paid model | Top up, or use a `:free` model |
| `400` with `model_not_found` | The model id is not in the catalog | Copy the id from [naga.ac/models](https://naga.ac/models) |
Other statuses are covered in [Errors](/build/error-handling).
# Claude Code
Source: https://docs.naga.ac/integrations/agents/claude-code
Run Claude Code on a NagaAI API key through the Messages endpoint.
Claude Code talks to NagaAI through `/v1/messages`. Install it from the [official guide](https://code.claude.com/docs/en/overview), then add the settings below. Setup follows the Claude Code [LLM gateway guide](https://code.claude.com/docs/en/llm-gateway-connect).
## Configure
Put the variables in `~/.claude/settings.json`. Values in `env` of a settings file take precedence over the shell. A project `.claude/settings.local.json` works too, after you accept the trust prompt for that folder.
```json theme={null}
{
"env": {
"ANTHROPIC_BASE_URL": "https://api.naga.ac",
"ANTHROPIC_AUTH_TOKEN": "YOUR_NAGAAI_API_KEY",
"ANTHROPIC_DEFAULT_OPUS_MODEL": "claude-opus-5.5",
"ANTHROPIC_DEFAULT_SONNET_MODEL": "claude-sonnet-5",
"ANTHROPIC_DEFAULT_HAIKU_MODEL": "claude-haiku-4.5",
"ANTHROPIC_DEFAULT_FABLE_MODEL": "claude-fable-5.1",
"CLAUDE_CODE_SUBAGENT_MODEL": "claude-sonnet-5",
"CLAUDE_CODE_ENABLE_GATEWAY_MODEL_DISCOVERY": "1"
}
}
```
`ANTHROPIC_AUTH_TOKEN` sends the key as `Authorization: Bearer`. Set all four `ANTHROPIC_DEFAULT_*_MODEL` variables: Claude Code's built-in aliases expand to ids such as `claude-opus-5-5`, while NagaAI ids use a dot. Any chat model from the catalog works in these slots, not only Claude models, as long as it supports tools.
With model discovery on, `/model` lists the Claude models from `/v1/models`.
Start `claude` and run `/status` to see the base URL and model in use.
## Limits through NagaAI
* **No prompt caching.** NagaAI drops `cache_control`, so Claude models get no cache hits and every turn costs full input price. Long sessions cost more than on a direct Anthropic account with caching.
* **No token counting.** `count_tokens` returns `404`. Claude Code then estimates context size, so `/context` shows approximate numbers.
* **Beta features.** NagaAI ignores `anthropic-beta` headers, so features that depend on them are off.
# Cline
Source: https://docs.naga.ac/integrations/agents/cline
Use Cline in VS Code with a NagaAI API key.
In Cline open the settings and choose the **OpenAI Compatible** provider, as described in the [Cline docs](https://docs.cline.bot/provider-config/openai-compatible):
| Field | Value |
| - | - |
| Base URL | `https://api.naga.ac/v1` |
| API Key | Your NagaAI API key |
| Model ID | A chat model id, such as `claude-sonnet-5` or `deepseek-v4.1-flash` |
Under **Model Configuration**, set the context window from `context_window` in [`/v1/models`](/get-started/models). Turn on image support if the model accepts images, and enter prices if you want Cline to show costs.
Use Cline 4.1.13 or later. Older versions could strip tools from requests to custom OpenAI-compatible models.
Cline can also use the **Anthropic** provider with the custom base URL `https://api.naga.ac`. Prompt caching does not work through NagaAI, so Claude models get no cache hits.
Roo Code users: Roo Code was shut down on May 15, 2026, and its authors point to Cline.
# Codex CLI
Source: https://docs.naga.ac/integrations/agents/codex-cli
Run OpenAI Codex CLI on a NagaAI API key through the Responses endpoint.
Codex CLI talks to NagaAI through `/v1/responses`. Install it from the [official repository](https://github.com/openai/codex), then add a custom provider. The keys below follow the [Codex configuration reference](https://learn.chatgpt.com/docs/config-file/config-reference).
## Configure
Codex reads custom providers only from `~/.codex/config.toml`, not from a project `.codex/config.toml`.
```toml theme={null}
model_provider = "nagaai"
model = "gpt-5.3-codex"
model_context_window = 400000
[model_providers.nagaai]
name = "NagaAI"
base_url = "https://api.naga.ac/v1"
env_key = "NAGAAI_API_KEY"
wire_api = "responses"
```
Then export the key and start Codex:
```bash theme={null}
export NAGAAI_API_KEY="YOUR_NAGAAI_API_KEY"
codex
```
## How Codex works through NagaAI
Codex sends its own tool formats: `apply_patch` as a freeform tool, MCP tools grouped by server, and a compact tool list for `gpt-6-*` and `gpt-5.6-*`. NagaAI passes all of them to the model and returns the calls in Codex's item types, so file editing and MCP tools work. Remote compaction is off for custom providers, and Codex compacts the conversation locally.
`model_context_window` tells Codex the window of a model it does not know. Take the value from `context_window` in [`/v1/models`](/get-started/models).
# Continue
Source: https://docs.naga.ac/integrations/agents/continue
Use Continue in VS Code or JetBrains with a NagaAI API key.
Add NagaAI as an OpenAI provider in Continue's `config.yaml`, following the [Continue docs](https://docs.continue.dev/customize/model-providers/top-level/openai):
```yaml theme={null}
name: NagaAI
version: 1.0.0
schema: v1
models:
- name: Claude Sonnet 5 (NagaAI)
provider: openai
model: claude-sonnet-5
apiBase: https://api.naga.ac/v1
apiKey: ${{ secrets.NAGAAI_API_KEY }}
capabilities:
- tool_use
- image_input
roles:
- chat
- edit
- apply
```
Put the key in `~/.continue/.env` as `NAGAAI_API_KEY=...`. The IDE extensions do not read shell variables.
* `capabilities: [tool_use]` is required for agent mode. Continue does not know the capabilities of model ids outside its list.
* NagaAI has no legacy `/v1/completions` endpoint, so autocomplete setups that use it do not work.
# Kilo Code
Source: https://docs.naga.ac/integrations/agents/kilo-code
Add NagaAI as a custom provider in Kilo Code.
In Kilo Code open **Settings → Providers → Custom provider** and fill in the fields, as described in the [Kilo Code docs](https://kilo.ai/docs/ai-providers/openai-compatible):
| Field | Value |
| - | - |
| Provider ID | `nagaai` |
| Display name | `NagaAI` |
| Provider API | `OpenAI Compatible` for Chat Completions, `OpenAI Responses` or `Anthropic Messages` for the other formats |
| Base URL | `https://api.naga.ac/v1` |
| API key | Your NagaAI API key |
Kilo Code loads the model list from `/v1/models`. The list includes image, audio and embedding models, so search for the chat model you want, such as `claude-sonnet-5`.
Set the context limit of each model you use. Without it Kilo Code assumes 0 and cannot manage the context. Take the value from `context_window` in [`/v1/models`](/get-started/models).
The same provider in a config file, `~/.config/kilo/kilo.json`:
```json theme={null}
{
"provider": {
"nagaai": {
"npm": "@ai-sdk/openai-compatible",
"env": ["NAGAAI_API_KEY"],
"options": { "baseURL": "https://api.naga.ac/v1" },
"models": {
"claude-sonnet-5": { "name": "Claude Sonnet 5", "limit": { "context": 1000000, "output": 64000 } }
}
}
},
"model": "nagaai/claude-sonnet-5"
}
```
# OpenClaw
Source: https://docs.naga.ac/integrations/agents/openclaw
Add NagaAI as a custom model provider in OpenClaw.
Add a provider to `~/.openclaw/openclaw.json`, following the [OpenClaw custom provider docs](https://docs.openclaw.ai/gateway/config-tools/custom-providers):
```json5 theme={null}
{
models: {
mode: "merge",
providers: {
nagaai: {
baseUrl: "https://api.naga.ac/v1",
apiKey: "${NAGAAI_API_KEY}",
api: "openai-completions",
models: [
{ id: "claude-sonnet-5", name: "Claude Sonnet 5", input: ["text", "image"], contextWindow: 1000000, maxTokens: 64000 },
],
},
},
},
agents: { defaults: { model: { primary: "nagaai/claude-sonnet-5" } } },
}
```
The OpenClaw gateway watches this file and applies changes to `models` and `agents` without a restart. You can also apply a patch file with `openclaw config patch --file ./nagaai.json5`. Check the result with `openclaw models status`.
* `api` is `openai-completions` for Chat Completions, `openai-responses` for Responses.
* Set `contextWindow` and `maxTokens`. Without them OpenClaw assumes a 200,000-token window and sends no output limit.
# OpenCode
Source: https://docs.naga.ac/integrations/agents/opencode
Add NagaAI as a custom provider in OpenCode.
OpenCode connects to NagaAI as a [custom provider](https://opencode.ai/docs/providers/). Put this in `~/.config/opencode/opencode.json`, or in `opencode.json` in a project:
```json theme={null}
{
"$schema": "https://opencode.ai/config.json",
"model": "nagaai/claude-sonnet-5",
"provider": {
"nagaai": {
"npm": "@ai-sdk/openai-compatible",
"name": "NagaAI",
"options": {
"baseURL": "https://api.naga.ac/v1",
"apiKey": "{env:NAGAAI_API_KEY}"
},
"models": {
"claude-sonnet-5": { "name": "Claude Sonnet 5", "limit": { "context": 1000000, "output": 64000 } },
"deepseek-v4.1-flash": { "name": "DeepSeek V4.1 Flash", "limit": { "context": 1048576, "output": 32000 } }
}
}
}
}
```
Export `NAGAAI_API_KEY` before you start `opencode`, or run `/connect`, choose **Other**, enter `nagaai` as the provider id and paste the key.
* `@ai-sdk/openai-compatible` uses Chat Completions. To use Responses for one model, add `"provider": { "npm": "@ai-sdk/openai" }` inside that model's entry.
* Every model needs `limit`: OpenCode has no metadata for custom providers. Take `context` from `context_window` in [`/v1/models`](/get-started/models). The catalog has no output limit, so choose a value the model accepts.
* Only models listed under `models` appear in the model picker.
# Other tools
Source: https://docs.naga.ac/integrations/agents/others
Connect any tool that accepts a custom OpenAI or Anthropic base URL, plus notes on Cursor and Gemini CLI.
A tool that lets you set a base URL, a key and a model name can use NagaAI.
| The tool speaks | Base URL | Key |
| - | - | - |
| OpenAI, "OpenAI-compatible" | `https://api.naga.ac/v1` | Your NagaAI API key |
| OpenAI Responses | `https://api.naga.ac/v1` | Your NagaAI API key |
| Anthropic | `https://api.naga.ac` | Your NagaAI API key, as `x-api-key` or `Authorization: Bearer` |
Use a model id from [naga.ac/models](https://naga.ac/models). Agents need a model with `tools` in `supported_parameters`. Tools that read `/v1/models` will list every model, including image, audio and embedding models.
NagaAI drops unknown fields instead of failing, while `functions` and unknown tool types fail with `400`. See [Differences from OpenAI and Anthropic](/build/compatibility).
## Cursor
Cursor accepts an OpenAI key and a custom base URL under **Settings → Models**, with **Override OpenAI Base URL** set to `https://api.naga.ac/v1`. Per [Cursor's documentation](https://cursor.com/help/models-and-usage/api-keys), custom keys work only for chat models, and Tab completion keeps using Cursor's own models. Cursor staff have confirmed [on the forum](https://forum.cursor.com/t/custom-openai-override-and-cursor-models-cannot-both-stay-enabled/172795) that the override skips Claude and Gemini model names. For GPT model names Cursor sends a request format that NagaAI rejects. Add a model id from another publisher, such as `deepseek-v4.1-flash`, and use that.
## Gemini CLI
Gemini CLI has no official way to use an OpenAI-compatible or Anthropic-compatible endpoint. It talks only to Google's own API format, which NagaAI does not serve.
# LibreChat
Source: https://docs.naga.ac/integrations/chat-apps/librechat
Add NagaAI as a custom endpoint in a self-hosted LibreChat.
Add NagaAI to `librechat.yaml`, following the [LibreChat custom endpoint reference](https://www.librechat.ai/docs/configuration/librechat_yaml/object_structure/custom_endpoint):
```yaml theme={null}
endpoints:
custom:
- name: "NagaAI"
apiKey: "${NAGAAI_API_KEY}"
baseURL: "https://api.naga.ac/v1"
headers:
x-naga-supported-endpoint: "chat.completions"
models:
default: ["claude-sonnet-5", "deepseek-v4.1-flash"]
fetch: true
titleConvo: true
titleModel: "deepseek-v4.1-flash"
modelDisplayLabel: "NagaAI"
customParams:
reasoningFormat: reasoning_effort
```
Set `NAGAAI_API_KEY` in LibreChat's `.env`, or use `apiKey: "user_provided"` to let each user enter their own key.
* `fetch: true` loads the model list from `/v1/models`, so new models appear without editing the file. LibreChat sends `headers` with that request, and `x-naga-supported-endpoint: chat.completions` limits the list to chat models. Without the header the list includes image, audio and embedding models. `default` is the list LibreChat shows when the fetch fails.
* `reasoningFormat: reasoning_effort` makes LibreChat send `reasoning_effort`, the field NagaAI reads.
NagaAI runs a hosted LibreChat at [chat.naga.ac](/product/chat-interface).
# Open WebUI
Source: https://docs.naga.ac/integrations/chat-apps/open-webui
Connect Open WebUI to NagaAI as an OpenAI-compatible connection.
In Open WebUI open the admin settings, go to **Connections**, add an OpenAI connection and fill in, as described in the [Open WebUI docs](https://docs.openwebui.com/getting-started/quick-start/connect-a-provider/starting-with-openai-compatible):
| Field | Value |
| - | - |
| URL | `https://api.naga.ac/v1` |
| API Key | Your NagaAI API key |
| Model IDs | The models to show, for example `claude-sonnet-5` and `deepseek-v4.1-flash` |
Fill in **Model IDs**. Without it Open WebUI shows every model in the catalog, including image, audio and embedding models.
The same connection through environment variables:
```bash theme={null}
OPENAI_API_BASE_URL=https://api.naga.ac/v1
OPENAI_API_KEY=YOUR_NAGAAI_API_KEY
```
Open WebUI's settings for image generation, speech and RAG embeddings also accept an OpenAI base URL. NagaAI can serve those with the same key.
# LangChain
Source: https://docs.naga.ac/integrations/frameworks/langchain
Use NagaAI from LangChain for Python with ChatOpenAI or ChatAnthropic.
`ChatOpenAI` and `ChatAnthropic` take a custom base URL. See the LangChain docs for [ChatOpenAI](https://docs.langchain.com/oss/python/integrations/chat/openai) and [ChatAnthropic](https://docs.langchain.com/oss/python/integrations/chat/anthropic).
```python theme={null}
import os
from langchain_anthropic import ChatAnthropic
from langchain_openai import ChatOpenAI
chat = ChatOpenAI(
model="deepseek-v4.1-flash",
base_url="https://api.naga.ac/v1",
api_key=os.environ["NAGAAI_API_KEY"],
stream_usage=True,
)
print(chat.invoke("Why do HTTP clients retry with backoff? One sentence.").content)
claude = ChatAnthropic(
model="claude-sonnet-5",
base_url="https://api.naga.ac",
api_key=os.environ["NAGAAI_API_KEY"],
)
print(claude.invoke("Why do HTTP clients retry with backoff? One sentence.").content)
```
* `stream_usage=True` is needed for token usage in streams: `ChatOpenAI` turns it off for custom base URLs.
* `ChatOpenAI` reads only standard OpenAI fields, so it does not expose NagaAI's `reasoning_content`.
* `ChatAnthropic.get_num_tokens_from_messages` calls `count_tokens`, which returns `404` on NagaAI.
* With `use_responses_api=True`, send the full history each turn. NagaAI ignores `previous_response_id`.
# Vercel AI SDK
Source: https://docs.naga.ac/integrations/frameworks/vercel-ai-sdk
Use NagaAI from the Vercel AI SDK through the OpenAI, OpenAI-compatible or Anthropic provider.
The AI SDK reaches NagaAI through any of its three providers. See the [AI SDK provider docs](https://ai-sdk.dev/providers/ai-sdk-providers/openai).
```typescript theme={null}
import { generateText } from "ai";
import { createOpenAI } from "@ai-sdk/openai";
import { createAnthropic } from "@ai-sdk/anthropic";
const naga = createOpenAI({
baseURL: "https://api.naga.ac/v1",
apiKey: process.env.NAGAAI_API_KEY,
});
// Responses API. store: false is required, see below.
const { text } = await generateText({
model: naga("deepseek-v4.1-flash"),
prompt: "Why do HTTP clients retry with backoff? One sentence.",
providerOptions: { openai: { store: false } },
});
console.log(text);
// Chat Completions
const chat = await generateText({
model: naga.chat("deepseek-v4.1-flash"),
prompt: "Why do HTTP clients retry with backoff? One sentence.",
});
console.log(chat.text);
// Messages
const claude = createAnthropic({ baseURL: "https://api.naga.ac/v1", apiKey: process.env.NAGAAI_API_KEY });
const message = await generateText({
model: claude("claude-sonnet-5"),
prompt: "Why do HTTP clients retry with backoff? One sentence.",
});
console.log(message.text);
```
The OpenAI provider stores responses by default and sends earlier turns as references to them. NagaAI keeps no state, so without `store: false` the second turn fails with `400`.
`createOpenAICompatible` from `@ai-sdk/openai-compatible` also works with `baseURL: "https://api.naga.ac/v1"`. Set `includeUsage: true` to get token usage in streams.
The Anthropic provider adds `/messages` to its base URL, so its `baseURL` ends in `/v1`.
# Chat
Source: https://docs.naga.ac/product/chat-interface
chat.naga.ac, a browser chat app that runs on your NagaAI API key.
[chat.naga.ac](https://chat.naga.ac) is a hosted [LibreChat](https://www.librechat.ai) app connected to NagaAI. It keeps your conversation history and lets you switch models between messages.
The chat has its own sign-in, separate from naga.ac: register with email, Google or Discord. When the chat asks for a key, paste a NagaAI API key, not a provisioning key. NagaAI charges requests to the account that owns the key.
To run LibreChat on your own server with NagaAI, see [LibreChat](/integrations/chat-apps/librechat).
# Playground
Source: https://docs.naga.ac/product/playground
Try chat, image generation, text to speech and speech to text in the browser before writing code.
[naga.ac/playground](https://naga.ac/playground) runs requests against the same models as the API, from the browser. You need to be signed in to naga.ac. The sidebar keeps each conversation so you can return to it.
| Mode | What you do |
| - | - |
| Chat with LLM | Send messages to any chat model and read the answers |
| Generate Images | Create an image from a prompt, or change an image you upload |
| Text to Speech | Turn text into speech and play it back |
| Speech to Text | Upload audio and get a transcript |
Pick a mode on the start screen, then pick a model. Model pages on [naga.ac/models](https://naga.ac/models) also have a **Try in Playground** link.
NagaAI charges Playground requests to your balance like API requests. They appear in [Logs](/account/balance-and-activity) under the key name `Playground`.