NeoAI API · v1
API reference
Everything you need to call the NeoAI API: one OpenAI-compatible endpoint for every model, and Neo.
Introduction
The NeoAI API gives your product the leading models from every lab through one endpoint, one key and one bill. It follows the OpenAI Chat Completions format, so the official OpenAI SDKs work as they are: change the base URL and the key.
Base URL https://neoolympus.ai/v1
Auth Authorization: Bearer neo_live_…Send "model": "neo" to use Neo, the layer that unifies every model, or any model id from the model list. Every answer says what it cost.
Keep keys on your server. Never put a key in a web page or a mobile app. The API sends no CORS headers, so browsers refuse it anyway.
Authentication
Every request carries your key in the Authorization header.
Authorization: Bearer neo_live_3f9c…- Make keys in the console. A key is shown once: NeoAI keeps only a fingerprint of it, so it cannot show it again.
- Rotate a key without downtime: the new key works at once and the old one keeps working for 24 hours.
- Revoke a key and it stops at once. An account can have up to 20 working keys.
- Live keys start with
neo_live_. Accounts on a monthly invoice also get test keys (neo_test_) with a small budget of their own. - In the console you can limit an account to a list of IP addresses.
Quickstart
Create your account in the console, add credits, make a key and export it as NEO_API_KEY. Then:
import os
from openai import OpenAI
client = OpenAI(
base_url="https://neoolympus.ai/v1",
api_key=os.environ["NEO_API_KEY"],
)
r = client.chat.completions.create(
model="neo-terra", # neo-luna, neo-sol or any model id from /v1/models
messages=[{"role": "user", "content": "Explain quantum computing in two sentences."}],
)
print(r.choices[0].message.content)import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://neoolympus.ai/v1",
apiKey: process.env.NEO_API_KEY,
});
const r = await client.chat.completions.create({
model: "neo-terra", // neo-luna, neo-sol or any model id from /v1/models
messages: [{ role: "user", content: "Explain quantum computing in two sentences." }],
});
console.log(r.choices[0].message.content);curl https://neoolympus.ai/v1/chat/completions \
-H "Authorization: Bearer $NEO_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "neo-terra",
"messages": [{"role": "user", "content": "Explain quantum computing in two sentences."}]
}'Models
GET/v1/models
The models your account may use: neo first, then every chat model, newest first. Prices are what you pay, per million tokens.
{
"object": "list",
"data": [
{
"id": "neo-terra", "object": "model", "owned_by": "neoai", "name": "Neo Terra",
"neo": {"level": "terra", "price_tier": "$$", "pricing": {"input_usd_per_mtok": 0.91, "output_usd_per_mtok": 4.55}, "tool_calling": true, "zdr": true}
},
{
"id": "anthropic/claude-sonnet-5.5", "object": "model", "owned_by": "anthropic", "context_length": 1000000,
"neo": {
"input_modalities": ["text", "image"], "output_modalities": ["text"], "price_tier": "$$",
"pricing": {"input_usd_per_mtok": 3.6455, "output_usd_per_mtok": 18.2275},
"max_output_tokens": 64000, "tool_calling": true, "zdr": true
}
}
]
}| Field | What it is |
|---|---|
neo.kind | text, image, video, speech or transcription; any for Neo, which does them all. |
neo.price_tier | $, $$ or $$$: how expensive the model is, from its real price. |
neo.pricing | US dollars: per million input and output tokens; for pictures usd_per_image, video usd_per_second, voice usd_per_mchar (per million characters), transcription usd_per_minute. Neo models have them all. |
neo.tool_calling | true: passed NeoAI's tool-calling test. false: failed it or has no tools. null: not tested yet. |
neo.zdr | true if a provider with zero data retention serves it. |
Chat completions
POST/v1/chat/completions
The OpenAI request body, with a few optional fields of NeoAI's.
Request
| Field | Type | What it does |
|---|---|---|
model | string, required | "neo-luna", "neo-terra" or "neo-sol" (Neo), "neo" (Neo at your account's default level), or a model id from GET /v1/models. |
messages | array, required | The conversation, in the OpenAI format: roles system, user, assistant, tool (or developer). |
stream | boolean | Send the answer as server-sent events. See Streaming. |
tools | array | Functions the model may call. See Tool calling. |
tool_choice | string or object | As in OpenAI: auto, none, required or one function. |
max_tokens | integer | Most tokens the answer may use (max_completion_tokens works too). Without it, the model's own limit applies. |
user | string | Your own customer's id, up to 200 characters. Optional but recommended; required for accounts on a monthly invoice. Also accepted as the X-Neo-End-User header. |
temperature, top_p, stop, seed | Sent to the model as they are. | |
response_format | object | JSON mode or a JSON schema, for the models that support it. |
presence_penalty, frequency_penalty, logit_bias, parallel_tool_calls | Sent to the model as they are. | |
reasoning | object | Reasoning effort for the models that think, e.g. {"effort": "high"}. |
NeoAI fields
| Field | Type | What it does |
|---|---|---|
neo_level | "luna" · "terra" · "sol" | With model neo: which level. Default: your account's (terra unless you change it in the console). Not needed with neo-luna, neo-terra or neo-sol. |
neo_zdr | boolean | Only providers with zero data retention answer. See Zero data retention. |
neo_max_cost_usd | number | The most this one request may cost, in US dollars. A request that would go over is refused before it runs. |
neo_fallback | boolean | Default true. With false, only the first model is tried. |
With the OpenAI SDKs, send NeoAI's fields in extra_body (Python) or in the request object (JavaScript).
Response
A chat.completion object as in OpenAI, plus a neo object.
{
"id": "chatcmpl-7f3a…",
"object": "chat.completion",
"created": 1793145600,
"model": "neo-terra",
"choices": [{"index": 0, "message": {"role": "assistant", "content": "…"}, "finish_reason": "stop"}],
"usage": {"prompt_tokens": 412, "completion_tokens": 96, "total_tokens": 508},
"neo": {
"request_id": "req_7f3a…",
"model": "neo-terra",
"requested": "neo-terra",
"level": "terra",
"end_user": "customer-123",
"cost_usd": 0.000812,
"estimated": false
}
}| neo. | What it is |
|---|---|
request_id | The id of this request in your usage and in the console. Quote it when you write to us. |
model | The model that answered, or neo when you asked for neo. |
requested | The model you asked for. |
level | With neo: the level used. |
fallback | With a model id: true if another model answered because yours failed. |
zdr | true when zero data retention was on. |
end_user | The user you sent. |
cost_usd | What this request cost you, in US dollars. |
estimated | true if the provider had not reported the exact cost yet: it is settled within minutes, and usage shows the final figure. |
Usage
GET/v1/usage?from=YYYY-MM-DD&to=YYYY-MM-DD&user=…
What your requests cost in a period, in total, by day, by model and by your customers. Dates are UTC; both are optional (the default is this month). The console has the same figures, charts and a CSV of every request.
curl "https://neoolympus.ai/v1/usage?from=2026-10-01&to=2026-10-31" -H "Authorization: Bearer $NEO_API_KEY"{
"object": "usage", "env": "live",
"requests": 1284, "cost_usd": 3.41827, "tokens_in": 2210431, "tokens_out": 391204,
"by_day": [{"day": "2026-10-01", "requests": 41, "cost_usd": 0.10233}, …],
"by_model": [{"model": "neo-terra", "requests": 1102, "cost_usd": 2.90311, …}, …],
"by_end_user": [{"end_user": "customer-123", "requests": 77, "cost_usd": 0.21455, …}, …],
"balance": {"available_usd": 6.5817, "held_usd": 0.0}
}Neo and levels
Neo is one layer over every model: send it text, code or images, with or without tools, at the level you choose. If a model fails or is too slow, another one answers. Neo comes in three models, each with one fixed price per million tokens:
neo-luna, Neo Luna: fast and cheap. Short answers, extraction, classification.neo-terra, Neo Terra (the default): writing, analysis and everyday code.neo-sol, Neo Sol: the strongest models, for hard reasoning, long documents and complex code.
"model": "neo" is Neo at your account's default level (Neo Terra unless you change it in the console); neo_level picks another.
The response shows the Neo model you asked for and its level, not the model behind it. You pay the level's fixed price, whichever model answered; the API page and GET /v1/models show it. To pin one model, use its id instead.
Streaming
With "stream": true the answer arrives as server-sent events, in the OpenAI format. The last chunk has no choices and carries usage and neo (with the cost). While a model thinks, the stream sends a comment line every 10 seconds so no proxy closes it.
stream = client.chat.completions.create(
model="neo-terra",
messages=[{"role": "user", "content": "Write a haiku about the sea."}],
stream=True,
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)
if not chunk.choices: # the last chunk: usage and cost
print("\n", chunk.usage, chunk.model_extra["neo"]["cost_usd"])data: {"object": "chat.completion.chunk", "model": "neo-terra", "choices": [{"index": 0, "delta": {"role": "assistant", "content": ""}}]}
data: {"object": "chat.completion.chunk", "model": "neo-terra", "choices": [{"index": 0, "delta": {"content": "Salt wind"}}]}
: ping
data: {"object": "chat.completion.chunk", "model": "neo-terra", "choices": [{"index": 0, "delta": {}, "finish_reason": "stop"}]}
data: {"object": "chat.completion.chunk", "model": "neo-terra", "choices": [], "usage": {…}, "neo": {"cost_usd": 0.00002, …}}
data: [DONE]Tool calling
Send tools in the OpenAI format. With neo, Neo only routes to models that passed NeoAI's tool-calling test; with a model id, the model must support tools.
tools = [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Current weather for a city",
"parameters": {"type": "object", "properties": {"city": {"type": "string"}}, "required": ["city"]},
},
}]
r = client.chat.completions.create(model="neo-terra", tools=tools,
messages=[{"role": "user", "content": "Is it raining in Barcelona?"}])
call = r.choices[0].message.tool_calls[0]
print(call.function.name, call.function.arguments) # get_weather {"city": "Barcelona"}Send the result back as a message with role tool and the same tool_call_id, as in OpenAI.
Pictures and audio in
Send pictures as content parts, by URL or as base64 data. With Neo, a model that reads pictures answers; a model id must read them too (see input_modalities in the model list). Audio goes in input_audio parts ({"data": base64, "format": "mp3"}): when the model can't hear it, and always with Neo, it is transcribed first and the transcript takes its place (its price is in neo.transcribed_usd). PDFs and other files go in file parts, as in OpenAI's format.
{
"model": "neo-terra",
"messages": [{
"role": "user",
"content": [
{"type": "text", "text": "What does this receipt total?"},
{"type": "image_url", "image_url": {"url": "data:image/jpeg;base64,/9j/4AAQ…"}}
]
}]
}Every kind in the chat
Neo answers with whatever the message asks for, in /v1/chat/completions:
- A picture ("draw…", "make an image of…", or
modalities: ["image"]):message.images, a list of{"type": "image_url", "image_url": {"url": "data:image/png;base64,…"}}. A picture in the message is edited or used as the start. - Something read aloud ("read this aloud: …"):
message.audiowithdata(base64),formatandtranscript. - A video: the job in
neo.video; the message says where to fetch it. A picture in the message becomes the first frame. modalities: ["text", "audio"]: the text answer, also read aloud inmessage.audio(audio: {"voice", "format"}as in OpenAI).- With a model id that draws (
imagein itsoutput_modalities),modalities: ["image", "text"]brings its pictures inmessage.images.
Streamed, the pictures or the audio come in one chunk before the finish. The price is in neo.cost_usd and neo.kind says what was made.
Pictures
POST /v1/images/generations, in OpenAI's format. With a Neo model it costs that level's price per picture; with the id of a picture model (kind image in the model list), what that model costs.
curl https://neoolympus.ai/v1/images/generations \
-H "Authorization: Bearer $NEO_API_KEY" -H "Content-Type: application/json" \
-d '{"model": "neo-terra", "prompt": "A red bicycle by the sea, morning light", "size": "1024x1536"}'
{
"created": 1793145600,
"data": [{"b64_json": "iVBORw0KGgo…"}],
"size": "1024x1536",
"neo": {"model": "neo-terra", "level": "terra", "cost_usd": 0.056}
}| Field | Type | What it does |
|---|---|---|
model | string, required | neo-luna, neo-terra, neo-sol, neo, or a picture model's id. |
prompt | string, required | What the picture shows, or how to change it. |
n | integer | 1 to 4 pictures. Default 1. You pay for the ones made. |
size | string | Exact pixels, such as 1024x1536 (the picture is cut to them), or auto. |
aspect_ratio | string | Such as 3:4 or 16:9, instead of a size. |
response_format | string | b64_json (default), or url to also get a data URL. |
user | string | Your own customer's id, as in chat completions. |
POST /v1/images/edits takes the same fields plus image: the picture to change or start from. Send it as a file (multipart, as the OpenAI SDKs do), a data URL or an https URL; up to 16.
Voice
POST /v1/audio/speech reads text aloud. The answer is the audio itself; the headers X-Neo-Request-Id, X-Neo-Model, X-Neo-Level and X-Neo-Cost-Usd say what it was and what it cost. With Neo, the price is per million characters.
r = client.audio.speech.create(model="neo-terra", voice="alloy", input="Your order has shipped.")
r.write_to_file("order.mp3")| Field | Type | What it does |
|---|---|---|
input | string, required | The text, up to 20,000 characters. |
voice | string | A voice the model has (its voices in the model list), one of OpenAI's names (alloy, nova, onyx…) or plain words such as deep male. Otherwise the model's first voice. |
response_format | string | mp3 (default), wav, opus, aac, flac or pcm. |
Transcription
POST /v1/audio/transcriptions turns a recording into text: a multipart file (mp3, wav, m4a, mp4, ogg, webm, flac or aac, up to 25 MB) or, in JSON, file as a data URL. With Neo, the price is per minute of audio.
r = client.audio.transcriptions.create(model="neo-luna", file=open("call.m4a", "rb"), response_format="srt")| Field | Type | What it does |
|---|---|---|
language | string | The language spoken (ISO 639-1, such as ca), when you know it. |
response_format | string | json (default: text, seconds and price), text, verbose_json (with segments), srt or vtt. |
Video
A video takes minutes, so POST /v1/videos starts a job and answers at once. Ask GET /v1/videos/{id} for its status (queued, in_progress, completed, failed) and download it from GET /v1/videos/{id}/content (an MP4, kept 7 days). GET /v1/videos lists your latest. It is charged when it is made, per second; a failed job costs nothing. Neo makes video up to 720p.
{"model": "neo-terra", "prompt": "A drone shot over a vineyard at dawn", "seconds": 5, "size": "1280x720"}
{"id": "video_8c1e…", "object": "video", "status": "queued", "model": "neo-terra", "seconds": "5"}| Field | Type | What it does |
|---|---|---|
seconds | integer | 1 to 20. Default 5; a model that can't make that length makes the nearest one. |
size / resolution | string | 1280x720 or 720p. The orientation of a size sets the aspect ratio. |
aspect_ratio | string | 16:9 (default), 9:16, 1:1… |
input_reference | picture | The first frame: a file (multipart), a data URL or an https URL. |
Zero data retention
Send "neo_zdr": true and only providers that keep nothing of the request (no logging, no training) answer it. With neo, only those models answer; a model id without such a provider is refused with zdr_not_available. The console can turn it on for every request of your account.
NeoAI itself never keeps the content of API requests: it records only what billing needs (time, model, tokens and cost) and never trains on your data.
Your customers
Send your own customer's id in user (or the X-Neo-End-User header) and your usage is broken down by customer, in the console and in GET /v1/usage. Use an id of your own, not an email. Providers receive only a one-way fingerprint of it, never the id itself.
Credits and billing
- The API is prepaid: add credits in the console, from $10, by card. Credits never expire.
- Each request costs the tokens it uses at the model's price, as in the model list. Prices exclude tax; VAT or sales tax is added at checkout where it applies.
- When your credits run out, requests are refused with
insufficient_credits(HTTP 402). Nothing is charged automatically. - Set a monthly spend cap in the console;
neo_max_cost_usdcaps a single request. - Every top-up has an invoice in the console. Accounts with a monthly invoice are set up with us: info@neoolympus.ai.
Limits
| Limit | Default |
|---|---|
| Requests per minute, per key | 120 |
| Requests at a time, per account | 4 |
| Request body | 32 MB |
| Messages per request | 4,000 |
Over a limit, the API answers 429 with a Retry-After header. Need more? Write to info@neoolympus.ai.
Errors
Errors use the OpenAI format, with an HTTP status and a code you can match on.
{"error": {"message": "This account has no credits left for this request. Add credits in the console: https://neoolympus.ai/console/billing", "type": "insufficient_credits", "code": "insufficient_credits"}}| Status | Code | What it means |
|---|---|---|
| 400 | invalid_request_error, invalid_json | The body is not valid: the message says what to fix. |
| 400 | tools_not_supported, images_not_supported | The model you asked for cannot do this. Use neo, or another model. |
| 400 | zdr_not_available | The model has no zero-data-retention provider (pictures, voice, transcription and video have none yet). |
| 400 | no_model_available | No model can do this request with your account's settings. |
| 400 | missing_end_user | This account must send user with every request. |
| 400 | content_policy | Every model refused the picture or video (often real, recognisable people). |
| 400 | resolution_not_supported | Neo makes video up to 720p; for more, pick a model id. |
| 409 | video_not_ready, video_failed | The video is still being made, or could not be made (nothing was charged). |
| 410 | video_expired | The file was deleted 7 days after it was made. |
| 401 | invalid_api_key | The key is missing, wrong, revoked or expired. |
| 402 | insufficient_credits | Not enough credits for this request. Add credits in the console. |
| 402 | spend_limit_reached | Your monthly spend cap, or neo_max_cost_usd, would be exceeded. |
| 403 | model_not_allowed, ip_not_allowed, account_inactive | The account may not use this model, from this address, or is switched off. |
| 404 | model_not_found, not_found | Unknown model or endpoint. |
| 413 | request_too_large | The body is over 32 MB. |
| 429 | rate_limited, concurrency_limit | Too many requests. Wait the seconds in Retry-After and try again. |
| 502 | upstream_error | Every model failed. You pay only for any part of an answer already produced; try again. |
Versions
This is version 1. Within /v1 we only add things (new fields, new models, new endpoints); anything that would break your code would come as /v2, announced in advance. Model ids follow the providers: when a provider retires a model, it leaves the model list.
The OpenAPI description of this API: openapi-v1.yaml.


