API Reference

DocsDashboard
Developer API

Anathema API Reference

Programmatic access to Lexi, a conversational model with positions of her own, and Ana, a coding model, over an OpenAI-compatible interface. If your code already talks to the OpenAI Chat Completions API, point it at our base URL and swap the key. It just works.

Base URL

https://api.anathema.ai/v1

Endpoint

POST/v1/chat/completions

Auth

Authorization: Bearer an_sk_…

Overview

The API exposes a chat endpoint, POST /v1/chat/completions, matching the OpenAI Chat Completions shape, plus model-discovery endpoints under /v1/models. Chat responses come back as standard JSON, or as a Server-Sent Events stream when you set stream: true. You authenticate with a Bearer API key created in your developer dashboard.

The API is stateless. There is no server-side conversation memory, so send the full message history on every request, exactly as you would with OpenAI.

Quickstart

Replace the placeholder with a secret key from the Keys tab, then run:

Replace <YOUR_API_KEY> with your secret key, found on the Keys tab.

Authentication

Every request needs a Bearer API key. Create, rotate, and revoke keys in your dashboard.

  • Keys are shown once. Copy the key when you create it; we only ever store a hash.
  • Rotate to swap a live key for a new one. The old key stops working immediately.
  • Revoke to kill a key instantly. A revoked or rotated-out key returns 401.
  • Scopes: keys carry a chat scope (today's only scope). Future scopes will gate additional capabilities.
  • IP allowlist (optional): restrict a key to specific IPs or CIDRs from its settings. A request from a non-allowlisted IP is rejected.

You must fund your account before you can create keys (minimum $5). See Pricing.

The models

lexi answers candidly on subjects most models refuse and holds consistent positions rather than hedging. She is a fixed personality, not a blank slate you reshape per request, and she grounds answers with built-in research and a curated library (see Tools & grounding).

ana is a coding model. She is calibrated and direct, and she surfaces a blocker rather than routing around it. She is in beta and served differently from lexi, which is covered under Data handling below.

What either model will and won't do is governed by our Acceptable Use Policy.

Model IDContextMax outputInput $/MCached input $/MOutput $/MVisionMax imagesToolsStatus
ana900,000130,048$5.50$0.50$20.00Yes8YesBeta
lexi900,00064,000$3.00$0.30$15.00Yes8YesActive
Model ID
ana
Context
900,000
Max output
130,048
Input $/M
$5.50
Cached input $/M
$0.50
Output $/M
$20.00
Vision
Yes
Max images
8
Tools
Yes
Status
Beta
Model ID
lexi
Context
900,000
Max output
64,000
Input $/M
$3.00
Cached input $/M
$0.30
Output $/M
$15.00
Vision
Yes
Max images
8
Tools
Yes
Status
Active

Context is the most you can send in one request, counting your own messages. The system prompt we add rides outside it, and outside your bill. Max output is the visible tokens one response can return. Cached input bills the leading tokens of a request the engine has already processed recently. It applies to that repeated prefix only, and the rest of the prompt bills at the input rate, so a bill sits between the two figures rather than at either one.

Data handling

ana runs on Kimi K3, served by third-party infrastructure rather than hardware we operate. Requests you address to ana leave our systems to be answered.

Content sent to ana may be used to train that model. Under the provider's default terms, what you send and what the model returns may both be used to develop and improve their models. The terms name enterprise agreements as the only route to a restriction, so there is no flag, header or parameter on this API that changes it. Treat anything you send to ana as disclosed to that provider. This describes ana alone and says nothing about how requests to other models are handled.

Tools & grounding

Both models have built-in tools they use on their own to ground answers in current, real information. You don't configure or invoke them; the model decides when they help, runs them server-side, and you receive the finished answer. The individual tool calls do not appear in the response.

The tool set differs by model:

  • Web research (lexi and ana): live search plus page reading for current events and facts.
  • Library search (lexi and ana): a curated corpus of primary sources the model can list, draw on, and cite.
  • Firearm-file search (lexi only): the GunCAD Index of 3D-printable firearm files.
  • Code execution (ana only): a sandbox that runs Python, JavaScript, Bash, C, or Rust and returns the real output. Ana is a coding model and calls it herself when a question is better answered by running code. It costs you nothing beyond the tokens of the turn, and there is no per-request switch to enable it.

Not on the API: image generation and long-term memory are features of the consumer app and do not run through the API on either model.

The tools above are what the model has on a request that sends no tools array of your own. Send one and yours take their place. See Function calling.

Function calling

Send your own tools and the model calls them the standard OpenAI way: it returns tool_calls with finish_reason: "tool_calls" instead of a normal answer. You run the call, then send the result back as a role: "tool" message to continue the conversation. Works on both models.

Your tools replace ours. Send a tools array and the model gets exactly those, plus the one reserved name below. The built-in tools under Tools & grounding, research and page fetching among them, are not added beside yours: a coding harness brings its own, and every call you receive is for a function you defined.

A name you define is always yours. Send a tool called research and it is your research, called and returned to you like any other tool of yours. You will never receive a call for a function nobody defined.

"anathema_tools": false asks for a plain completion: send it with no tools array of your own and the model gets none of the built-in tools above either. On a request that does carry your own tools it changes nothing, since ours are already out.

One name is reserved: aup_violation. It is ours on every call, including calls that send "anathema_tools": false, and it is never returned to you to run. A request whose tools array defines a function by that name returns 400 unsupported_feature naming it. Rename the function and send it again.

tool_choice is honored: "auto", "none", "required", or a named function. A named function must be one you defined in this request's tools array.

On ana, response_format and tools combine freely on one request. The model either calls one of your tools or answers in your schema, and both stay available for the whole turn. When it calls one, that ends the turn the same way it would without a schema: you get finish_reason: "tool_calls" and no content, and the schema applies to the answer it gives after you send the tool result back. On lexi the two cannot be combined: a schema constrains her output from the first token, so there is no point in the turn at which she could call one of your tools instead, and the request returns a 400 saying so rather than accepting tools it could never dispatch.

Tool definitions count toward your prompt tokens and toward the model's published input limit. There is no separate cap on how many tools you can send.

Run list_files yourself, then send the result back as a role: "tool" message, with the same tools array resent alongside it:

The API is stateless, so tools must be resent on the follow-up. A request that omits it is not sending your tools, and the model gets our built-in set for that turn instead.

The request

Standard OpenAI Chat Completions fields:

FieldNotes
model"lexi" or "ana". See The models.
messagesArray of { role, content }. Role is system, user, or assistant.
max_tokensOn both models it covers reasoning and the visible answer together, and both reason on every request, so a budget that looks generous for an answer can be spent entirely on reasoning. Your value is honored either way. If reasoning consumes it, you get finish_reason: "length" with little or no content, and usage.completion_tokens_details.reasoning_tokens tells you where it went. Any positive value is accepted, and the maximum is the Max output column in the table under The models; above it the request is rejected with max_tokens_too_large. A small budget is served rather than refused: you get a shorter answer, truncated with finish_reason: "length" if it did not finish. You are billed for the tokens actually produced, and across our own measurements a hard coding task on ana finished in about 8,600. Omit the field and each model applies its own default. It is a ceiling, not a target, so short answers stay short.
streamtrue for SSE streaming.
response_format{type:"json_schema", json_schema:{…}} for JSON matching your schema, or {type:"json_object"} for loose JSON mode (valid JSON, no enforced shape, describe the fields you want in the prompt). Both models constrain generation to the schema, so an off-schema response cannot be produced, and both support stream: true with it.ana also accepts it alongside tools; on lexithat one combination returns a 400. See Structured outputs. Omit the field for normal prose.
stopUp to 4 stop sequences (string or array). Honored.
reasoning_effortControls the model's internal reasoning. Set "low", "medium", or "high" for step-by-step reasoning on harder, open-ended tasks, so a higher tier buys deeper deliberation. A request that uses tools may reason at each step, so its total can exceed one step's budget. Reasoning tokens count toward completion_tokens and bill at the output rate; the reasoning text itself is internal and is not returned. This is a standard OpenAI parameter, so your existing client can set it directly. The floor is per model. On lexi, reasoning always runs: omitting the field, or sending "none", "minimal" or "low", serves the request at low effort, and "medium" and "high" both serve it at high effort. Reasoning shares the max_tokens budget with the answer, and the response reports the exact spend in usage.completion_tokens_details.reasoning_tokens. On ana, reasoning always runs and cannot be switched off: omitting the field, or sending "none" or "minimal", serves the request at "low". Her three tiers map onto the engine's own, so "medium" and "high" reach deeper settings than "low" rather than a token budget we choose. Her responses do report usage.completion_tokens_details.reasoning_tokens, so the spend is itemized on both models.
max_completion_tokensAccepted as an alias for max_tokens, with the same minimum, maximum and errors. Recent OpenAI SDKs send this name instead of max_tokens on reasoning models, so a client that sets only this one is honored. If a request carries both, max_tokens wins.
n, functions, function_calln: 1 is accepted and any higher value returns 400 invalid_request_body, rather than billing for several and returning one. The legacy functions and function_call parameters return 400 unsupported_feature; send the same definitions as tools and use tool_choice in place of function_call.
logprobs, top_logprobsNot returned by either model. logprobs: false is accepted so a client sending the default works unchanged; logprobs: true returns 400 unsupported_feature.
stream_optionsSend {"include_usage": true} to get a final chunk carrying usage for the request, with the same fields the non-streaming response returns. It arrives after the last content chunk and before [DONE], and it is the one chunk whose choices array is empty, which is how to tell it apart. Omit the field and no usage chunk is sent. A request that ends in an error carries no usage chunk, the same way an error response carries no usage body.

The same object takes include_obfuscation, which is on unless you turn it off. Streamed frames carry an obfuscation field of random characters that rounds each frame's size up to a fixed block. Without it, a frame's size is the length of the token inside it, and an observer who cannot read your traffic can still read those lengths. With it, frames whose contents differ by less than the block are identical, and larger differences are coarsened to the block rather than erased. Ignore the field; nothing in it means anything. Send {"include_obfuscation": false} to drop the overhead on a network you trust.
prompt_cache_keyHonored on both models. Requests in your organization that carry the same key are routed together, so a long system prompt or document they share can be processed once and then billed at the cached rate. Use one key per customer, user or session of yours; a single key for all of your traffic puts every request in one group. The key is never grouped with another organization's requests and is never sent anywhere as written. On lexi it guides routing on our primary capacity, and a request served from backup capacity is served without it. Without a key, we group requests by your organization and the opening of each conversation, so conversations that start with the same system prompt and first message are grouped together. The key is 1 to 1,024 characters and cannot be blank; a longer, blank or non-string value returns 400 invalid_request_body, and null is treated as not sent.
safety_identifier, userAccepted and not used, so sending them changes nothing.
seedHonored on ana, ignored on lexi. Send it for a repeatable run.
parallel_tool_callsHonored on ana, ignored on lexi. Send false if your harness can only execute one call per turn.
predictionHonored on ana, ignored on lexi. Send the output you expect and the engine decodes against it, which is faster when most of the answer matches. The case it is built for is an edit: send the current file as the prediction and ask for a change to one part of it. A wrong guess costs speed, never correctness. Sent on the first call of a turn only, since after a tool result the model is answering something you had not seen when you wrote it.
temperature, top_p, penalties, logit_biasAccepted for compatibility but ignored, on both models. Your existing client works as-is; these simply have no effect. On lexi that is our choice: she runs a single tuned profile and the personality is the product. On ana it is the engine's: it accepts exactly one value for each of these and rejects the rest, so there is no setting to pass along. stop is honored on both.

Idempotency: send an Idempotency-Key: <unique-string> header to make retries safe. A repeated key replays the original response instead of billing twice. A streamed response cannot be replayed, so a streamed request holds its key only while it runs: a duplicate sent while it is still running gets 409 idempotency_key_in_use, and a retry sent after it finishes runs again and is billed again.

Conversations

Because the API is stateless, a multi-turn conversation is just the running history resent on each call: append the model's previous reply as an assistant message, add the new user turn, and send the whole array again. This works the same way on both models.

Every token you resend is billed again, so for long sessions trim old turns yourself once they stop being relevant. There is no hidden server-side truncation: if the history exceeds the context window you get context_length_exceeded.

System prompts

Add one or more system messages to steer how the model responds: tone, format, length, focus, output structure. Your instructions are layered on top of its persona. Both models work this way.

You can shape the delivery, but you can't override a model's core identity or positions with a system prompt. Lexi and Ana are both fixed personalities, and instructions that try to make either one someone else are ignored.

Responses

A non-streaming request returns a standard chat.completion object. The answer is in choices[0].message.content, and token counts are in usage:

finish_reason is stop on a complete answer, length when output hit the max_tokens budget (raise max_tokens or continue the conversation to get the rest), and tool_calls when the model is asking you to run one of the functions you supplied. Handling that one is covered under function calling above. usage.prompt_tokens is what you are billed for on the input side. It counts your tokens only: your messages, your system prompt, any image tokens, and any tool definitions you send. The model's own persona rides every request internally but is neither reported nor billed, on either model. prompt_tokens_details.cached_tokens reports how much of your prompt was served from prefix cache: stable content (your system prompt, earlier conversation turns you resend) caches automatically, so keeping your system prompt byte-identical across calls makes repeat requests faster. Both models report cached tokens. On the output side, usage.completion_tokens counts every token the model generates, billed at the output rate, and that includes internal reasoning tokens whenever reasoning ran. The reasoning text itself is never returned. On lexi, reasoning always runs, at low effort unless you ask for more, shares the max_tokens budget with the answer, and is reported exactly in usage.completion_tokens_details.reasoning_tokens. On ana, reasoning runs on every request and responses carry no completion_tokens_details at all, so her reasoning is inside completion_tokens without a separate line for it.

Every response carries headers worth reading:

HeaderMeaning
x-request-idCorrelation id for this request. Include it in support tickets. Also mirrored into error bodies.
x-ratelimit-limit-requestsYour per-minute request ceiling.
x-ratelimit-remaining-requestsRequests left in the current minute.
x-ratelimit-reset-requestsISO 8601 timestamp when the window resets.
retry-afterSeconds to wait before retrying. Sent on 429.

Structured outputs

Send the standard OpenAI response_format with a JSON Schema and the response content is guaranteed to parse as a valid instance of it. JSON.parse(content) cannot throw and cannot return a shape outside your schema. Tools keep working underneath on both models: if the request needs research or a library lookup, the model does that first and the final answer still lands in your schema.

The answer arrives in choices[0].message.content as the JSON string, same key as every other response, nothing new to parse, and any OpenAI SDK handles it unmodified. Rules worth knowing:

  • Schema subset. Supported keywords: type, properties, required, items, enum, const, anyOf, additionalProperties (boolean), internal $ref/$defs, and numeric/length/array bounds. pattern and format are rejected. Anything unsupported returns a 400 naming the keyword and its location.
  • Field description steers the model. description and title on a field are passed through to guide what the model puts there. Use them the way you would on any OpenAI structured-output call. The remaining annotations (examples, default, $comment, and similar) are accepted and ignored.
  • Caps. Schemas up to 32 KiB, 8 levels deep, 256 properties total, 64 entries per enum. Property names are plain identifiers.
  • Both models constrain generation. The inference engine holds the output to your schema as it is produced, so the model cannot choose to answer off-schema and there is no validate-and-retry step to pay for. One consequence worth planning around: a schema that fights the question is answered rather than refused. Give an enum that excludes the true answer and you get a value from the enum, not an error.
  • Check finish_reason before you parse. A constrained response can still be cut off by max_tokens, and a truncated instance is incomplete JSON. That arrives as a normal 200 with finish_reason: "length", the same as any other truncated response. Treat "length" as a failed parse and retry with a larger budget. Reasoning tokens count against max_tokens, so a schema that needs the model to think can be cut off well before the JSON looks long.
  • Streaming works on both models. stream: true with a schema is supported everywhere, and concatenating the content deltas gives you exactly the instance a non-streamed call returns. On ana the JSON arrives as one delta rather than token by token, because a half-written object is not something you could parse anyway.
  • Contradictory bounds are rejected. Bounds no value can satisfy (minLength above maxLength, minimum above maximum, and the same pattern on items and exclusive bounds) return a 400 naming both bounds and the field. Nothing is billed.
  • Reasoning follows the model's own default here too. On lexi it runs at low effort by default; setting "medium" or "high" raises it, and the response still parses as valid JSON. On ana reasoning runs regardless, and the response still parses as valid JSON.
  • Reuse the schema byte-identically across calls. The schema rides in the prompt, so a stable schema prefix-caches and bills at the cached rate; a schema that changes every call loses those hits.
  • Billing is unchanged. Output tokens are the JSON itself; tool steps bill exactly as without a schema.

Loose JSON mode. If you only need valid JSON without a fixed shape, send {"type":"json_object"} instead of a schema. The response content is guaranteed to parse as JSON, and the model fills it from your prompt, so describe the fields you want in the message. When you know the shape, prefer json_schema: it is strictly stronger (the keys and types are enforced, not just the JSON-ness). json_object follows the same per-model streaming rule as json_schema, and like it, reasoning runs at low effort by default and higher if you set reasoning_effort.

Give the schema a way to say "I don't know." A structured request is always answered in your schema, even when a search turns up nothing, so the model cannot fall back to a prose "I couldn't find that." If your use case depends on research or retrieval, add a field that can express the absence: a found boolean, a nullable result, or a confidence number. Without one, a populated-looking answer can mask a search that came up empty. The JSON will be valid either way, so the uncertainty has to live in the schema, not in the prose you no longer get.

Streaming

Set "stream": true to receive a text/event-stream of chat.completion.chunk events terminated by data: [DONE], exactly like OpenAI:

With the OpenAI SDK it's the same iterator you already use:

Vision

Both models can see images. Send them as content parts in the standard OpenAI vision format: a user message whose content is an array of text and image_url parts.

Two ways to supply an image:

  • A public https URL. We fetch it for you.
  • An inline base64 data URL, like data:image/png;base64,…. The request body is capped at 12 MB, which comfortably fits a max-size (8 MB) image plus your prompt. An image too large to fit inline can be passed as a hosted URL instead; those are fetched server-side and don't count against the body limit. An oversize body returns 413 payload_too_large.
  • Formats: JPEG, PNG, WebP, GIF.
  • Max size: 8 MB per image (hosted URLs).
  • Images per request: per model, in the Max images column of the table under The models. GET /v1/models reports the same number as max_images_per_request.

Images attach only to user (and assistant) turns, not system. An image the fetcher read and rejected returns image_input_invalid (400); a failure inside our own fetch path returns image_service_unavailable (503) or image_service_error (502) instead, since there is nothing in the request to change. A content part that isn't text or an image returns multimodal_not_supported.

URL fetching

Both models can read the content of links in your request. When URL fetching is enabled for your organization, we fetch the text of any URLs in the latest user message, then give the model the page content so it answers from the page rather than from the link alone. The setting is per organization and applies to every model.

URL fetching is off by default. An organization owner turns it on under Developers → Settings → URL fetching. The setting applies to every request the organization makes. There is no per-request override, so an OpenAI client works without modification.

  • Up to 5 URLs per request are fetched, taken from the latest user message. Additional URLs are ignored.
  • You are billed for the fetched content as input tokens at the standard rate. A large page can raise the cost of a request.
  • A fetch that fails does not fail your request. The model receives an inline note such as [Could not fetch <url>: timeout] and answers from the rest of the message, so it never invents page content.
  • Fetched content is treated as untrusted. The model reads it as quoted source material and ignores any instructions embedded in a page.

If fetched content pushes the request past the model context window, the request returns 400 context_length_exceeded. Shorten the message or fetch fewer pages.

Model discovery

GET /v1/models lists available models and GET /v1/models/{id} retrieves one, both authenticated with the same Bearer key (no balance or rate-limit cost). The objects use the standard OpenAI shape plus capability fields, so a client that probes /v1/models on init works unchanged.

Limits

The limits that are the same on every model. Context window, output ceiling and images per request vary by model and are in the table under The models. Each limit is explained in the section it belongs to.

LimitValue
max_tokens (reasoning and answer)Any positive value (the maximum is per model)
Request body size12 MB
Image size8 MB per image
Image formatsJPEG, PNG, WebP, GIF
Stop sequencesUp to 4
Rate limit10 requests/sec, 300 requests/min
Minimum funding$5

We do not silently truncate. A prompt over the context window returns 400 context_length_exceeded so you can split the work rather than get an answer about a partial input. Need higher rate limits? See Rate limits.

Pricing

Pay-as-you-go, billed per token. No subscription.

ModelInputCached inputOutput
ana$5.50$0.50$20.00
lexi$3.00$0.30$15.00

Per million tokens. Minimum first purchase $5.

Heads-up on token counts: your prompt tokens do not include our system prompt. Each model's persona rides every request but is excluded from prompt_tokens and from your bill. Tool definitions you send are your own prompt tokens, so they are counted and billed like the rest of your request. A one-line question with no tools reports a one-line-sized count. Conversations follow the standard OpenAI pattern (resend the full message array each turn), so input grows with conversation length; prefix caching keeps the repeat content fast and cheap.

Caching is automatic and free to use. When a request starts with content the engine recently processed (your system prompt, earlier turns of a conversation you resend, a document you keep asking about), those tokens bill at the cached rate and the response reports them in usage.prompt_tokens_details.cached_tokens. There is nothing to enable and no cache-write fee. To maximize hits: keep your system prompt byte-identical across calls, append to conversations instead of editing earlier messages, and reuse stable context verbatim. Caching is best-effort (a cold start or long idle resets it), so the first request in a while bills at the standard rate.

  • Spend cap: set a monthly usage cap in your dashboard; requests pause when you hit it (it resets each period; it is not a charge ceiling).
  • Auto-recharge: opt in to top up your balance automatically when it runs low.

Cost estimator

Plan API spend for your expected input and output. Estimates use the selected model’s rates.

Standard input $5.50/M · Output $20.00/M. Cache discounts and additional tool-loop input are excluded.

Input tokens: 1,000

0

32,000

Output tokens: 500

0

32,000

$0.0155

per request

At 1K req

$15.50

At 10K req

$155.00

At 100K req

$1,550

At 1M req

$15,500

Rate limits

Defaults per organization:

Requests / second10
Requests / minute300

Pace yourself with the x-ratelimit-* response headers (see Responses). Over-limit requests return 429 with a retry-after header. Need more? Request a raise from your dashboard, or contact [email protected] for higher limits.

Errors

OpenAI-compatible error envelope. Every error carries a request_id; include it in support requests.

HTTPCodeMeaning
401invalid_api_keyKey is missing, malformed, revoked, blocked by your IP allowlist, or lacks the scope this endpoint requires
402insufficient_balanceOut of credits; top up your balance
402spend_cap_exceededMonthly spend cap reached
404model_not_foundA model id we do not serve, either unknown or retired. The message lists what is available, and GET /v1/models returns the same set
429rate_limit_exceededToo many requests; back off and retry
400invalid_content_typeThe Content-Type header is missing or does not say application/json, so the body was never read. Send Content-Type: application/json and retry the request unchanged
400invalid_jsonThe body did not parse as JSON. Nothing else in the request was read, so no field is named. Check what your client actually put on the wire
400invalid_request_bodyThe body is JSON but does not match the schema, for example no model, an empty messages array, a role we do not accept, or n above 1. The message quotes the first field that failed, and that is the one to fix
400unsupported_featureA parameter is shaped correctly but its value is not implemented here, such as logprobs, a tool_choice other than auto, or json_schema with stream true. The message names the parameter. Drop it or send a supported value
400tool_choice_unsupportedNamed tool_choice is not supported on ana. Use auto, required or none; named choices on lexi are unchanged
400max_tokens_too_smallmax_tokens below the 512 minimum; raise it or omit it
400max_tokens_too_largemax_tokens above the output ceiling (16,384, minus the active reasoning budget); lower it or omit it
400invalid_response_format_schemaresponse_format.json_schema failed validation; the message names the violating keyword or cap and its location
400aup_refusalRequest refused under the acceptable use policy
403org_suspendedOrganization's API access is suspended; contact support
502no_responseThe model produced no output (transient). Retry; not billed.
400image_input_invalidAn image was unreadable, too large, the wrong type, or could not be fetched
503image_service_unavailableWe could not load a remote image URL. Nothing to change in the request; retry it
502image_service_errorWe could not load a remote image URL and a retry will not clear it. Send the image inline as a base64 data: URL, or contact support
400multimodal_not_supportedA content part type other than text or image_url was sent
400tool_role_unsupportedA role:"tool" message was sent on a request with no tools array. A role:"tool" message answers one of your own tool calls, so it only makes sense on a request that also sends tools
413payload_too_largeRequest body exceeds 12 MB; shrink an inline image or pass a hosted image URL
400context_length_exceededPrompt exceeds the model's context window; shorten it or split across requests
400idempotency_key_invalidThe Idempotency-Key header arrived empty or longer than 256 characters. Send a key of 1 to 256 characters, or leave the header off
409idempotency_key_conflictThis Idempotency-Key was already used with a different request body, inside the 24 hour window the key lives for. Mint a fresh key per distinct request and reuse one only to retry the request that created it
409idempotency_key_in_useA request holding this Idempotency-Key is still running, so this one was refused instead of running the same work twice. Retry once the first request finishes, or send a new key
401invalid_api_key
Key is missing, malformed, revoked, blocked by your IP allowlist, or lacks the scope this endpoint requires
402insufficient_balance
Out of credits; top up your balance
402spend_cap_exceeded
Monthly spend cap reached
404model_not_found
A model id we do not serve, either unknown or retired. The message lists what is available, and GET /v1/models returns the same set
429rate_limit_exceeded
Too many requests; back off and retry
400invalid_content_type
The Content-Type header is missing or does not say application/json, so the body was never read. Send Content-Type: application/json and retry the request unchanged
400invalid_json
The body did not parse as JSON. Nothing else in the request was read, so no field is named. Check what your client actually put on the wire
400invalid_request_body
The body is JSON but does not match the schema, for example no model, an empty messages array, a role we do not accept, or n above 1. The message quotes the first field that failed, and that is the one to fix
400unsupported_feature
A parameter is shaped correctly but its value is not implemented here, such as logprobs, a tool_choice other than auto, or json_schema with stream true. The message names the parameter. Drop it or send a supported value
400tool_choice_unsupported
Named tool_choice is not supported on ana. Use auto, required or none; named choices on lexi are unchanged
400max_tokens_too_small
max_tokens below the 512 minimum; raise it or omit it
400max_tokens_too_large
max_tokens above the output ceiling (16,384, minus the active reasoning budget); lower it or omit it
400invalid_response_format_schema
response_format.json_schema failed validation; the message names the violating keyword or cap and its location
400aup_refusal
Request refused under the acceptable use policy
403org_suspended
Organization's API access is suspended; contact support
502no_response
The model produced no output (transient). Retry; not billed.
400image_input_invalid
An image was unreadable, too large, the wrong type, or could not be fetched
503image_service_unavailable
We could not load a remote image URL. Nothing to change in the request; retry it
502image_service_error
We could not load a remote image URL and a retry will not clear it. Send the image inline as a base64 data: URL, or contact support
400multimodal_not_supported
A content part type other than text or image_url was sent
400tool_role_unsupported
A role:"tool" message was sent on a request with no tools array. A role:"tool" message answers one of your own tool calls, so it only makes sense on a request that also sends tools
413payload_too_large
Request body exceeds 12 MB; shrink an inline image or pass a hosted image URL
400context_length_exceeded
Prompt exceeds the model's context window; shorten it or split across requests
400idempotency_key_invalid
The Idempotency-Key header arrived empty or longer than 256 characters. Send a key of 1 to 256 characters, or leave the header off
409idempotency_key_conflict
This Idempotency-Key was already used with a different request body, inside the 24 hour window the key lives for. Mint a fresh key per distinct request and reuse one only to retry the request that created it
409idempotency_key_in_use
A request holding this Idempotency-Key is still running, so this one was refused instead of running the same work twice. Retry once the first request finishes, or send a new key

Best practice: retry 429 and transient 5xx with exponential backoff; reuse an Idempotency-Key so the retry of a non-streamed request can't double-bill. On a streamed request the key refuses a concurrent duplicate rather than replaying, so back off before retrying. Not every 5xx is transient, and the Meaning column says which: image_service_error returns the same result on every attempt until we fix it. Both image_service_* responses also carry an x-should-retry header, true on the 503 and false on the 502; the official OpenAI client reads it instead of the status.

Best practices

  • max_tokens is a ceiling, not a target. On both models it includes reasoning. On lexi the default is 32,000 at low effort and 64,000 at medium or high. A small budget is served, and truncated with finish_reason: "length" if the answer does not fit. Omit it to use the model's default. The maximum is per model, in the table under The models.
  • Don't tune temperature or top_p. They are accepted but ignored on both models, each of which runs at a fixed, tuned profile.
  • On lexi, reasoning runs at low effort by default. Responses stay direct, so JSON, classification, and extraction stay clean and parse-ready, with no markdown fences and no preamble. For harder, open-ended tasks, set reasoning_effort to "medium" or "high". ana always reasons, so size max_tokens with that in mind.
  • Use an Idempotency-Key on anything you might retry.
  • Check finish_reason. length means the answer was cut off; raise max_tokens or continue the conversation.
  • High max_tokens values suit input-anchored work. Translation, extraction, transformation, and annotation of long inputs decode cleanly at length, because the source material anchors the output. Open-ended generation asked to run very long tends to thin out and repeat well before the ceiling; for that, several focused requests beat one enormous one.
  • Stream for interactive UX; non-streaming for batch.
  • Handle 429 and 5xx with exponential backoff, pacing off the x-ratelimit-* headers.
  • Trim old turns in long conversations; you pay for the full history on every call.
  • Store keys server-side, never in client code. Rotate on suspicion of exposure.

Versioning

The API is versioned in the path (/v1). Within a version we make only backwards-compatible changes: new fields on responses, new endpoints, new models, new capabilities. We will not remove fields or change the shape of an existing response under /v1, so code written against it today keeps working.

A genuinely breaking change would ship under a new version path, and we would give notice before retiring an old one. Treat unknown response fields leniently (ignore what you don't recognize) so additive changes never break your client.

Acceptable use

Both models are far more permissive than mainstream ones, but not unbounded, and the same policy governs each. What is and isn't allowed is spelled out in our Acceptable Use Policy. Requests that cross those lines are refused with aup_refusal, and repeated or severe violations can result in suspension of your API access (you'll see org_suspended). Build accordingly: handle refusals gracefully and don't route prohibited content through the API.

Changelog

2026-10-12

  • prompt_cache_key is now honored on both models. Until today a value you sent was replaced by a key we derived from your organization and the opening of each conversation, so you could neither group conversations that open differently nor separate ones that open the same. Requests in your organization that carry the same key are now routed together, and requests without one behave exactly as before. The key is never grouped with another organization's requests and is never sent anywhere as written. On lexi it guides routing on our primary capacity, and a request served from backup capacity is served without it. The key must be 1 to 1,024 characters and not blank: a longer, blank or non-string value now returns 400 invalid_request_body, where until today any value was accepted and ignored, and null is still treated as not sent. If you send one longer than 1,024 characters, shorten it, for example by hashing it.
  • Corrected, not changed: the request table listed prediction as accepted and not used. It is honored on ana and ignored on lexi, as its own row already said and as the 2026-08-16 entry here announced.

2026-10-07

  • Lexi now runs on a newer, more capable open-weight model, and her published context window is 900,000 tokens, up from 80,000. Ana is unchanged at 900,000. If you hardcoded 80,000 for lexi, raise it to 900,000, or read the figure from GET /v1/models.

2026-10-05

  • Ana's input limit and billing now count Kimi tokens exactly. The advertised input window remains 900,000 tokens. Over-limit errors quote the full prompt count in Kimi tokens, including our system prompt, tools and an allowance for images.
  • Named tool_choice on ana now returns a local 400 tool_choice_unsupported. Previously it was forwarded to the engine, which refused it while reasoning was enabled. Use auto, required or none on ana. Named choices on lexi are unchanged.
  • A request whose connection closes before the response finishes is billed for the tokens the model generated up to that point, on both models. Until today the pass that was running when the connection closed was not billed. On lexi the count stops where the connection closed. On ana the running pass completes upstream, because ana reports usage once at the end of a pass, and it is billed in full; no tool runs and no further pass starts after the connection closes. These requests appear in your request log with status 499 and the tokens they were billed for.

2026-08-29

  • The published context window on lexi is now 80,000, down from 85,000. Ana is unchanged at 900,000. Nothing about what we accept has moved: the request that was served yesterday is served today, and the gate refuses at the same point it always did. What changed is the size of the gap we keep between the two. Our own system prompt is counted against the window and you never send it, so the published figure is the enforced ceiling minus that prompt minus a reserve, and the reserve is what absorbs our prompt growing between measurements. It had worn down to under one ordinary edit, which is how a published number becomes wrong without anyone touching it. If you hardcoded 85,000, lower it to 80,000. If you size prompts against GET /v1/models, you already have the new figure.

2026-08-17

  • Structured output on lexi now works where it used to fail. A request with response_format: {"type":"json_schema"} could return 503 model_unavailable on ordinary prompts, and how likely that was depended on the prompt rather than on your schema. The cause was on our side: her schema was enforced by asking the model to answer through a reserved function call, and the engine did not always honor that, sometimes answering in the message content instead. Her schema is now enforced by the engine directly, so an off-schema answer cannot be produced rather than being caught after the fact. Nothing about your request changes. If you built a retry around that 503, you can keep it; it should stop firing.
  • response_format now works with stream: true on lexi. It previously returned 400 unsupported_feature, because a schema we validated after the fact could not be checked once the first frame had gone out. There is nothing left to check when an off-schema token cannot be generated, so the refusal is gone. Concatenating the content deltas gives you the same instance a non-streamed call returns. This also applies to json_object mode. Nothing changes on ana, which already supported it.
  • Unchanged, and worth stating because the reason moved: response_format still cannot be combined with tools on lexi, and still returns 400 unsupported_feature. It is no longer a contention over the tool channel. A schema constrains her output from the first token, so there is no point in the turn at which she could call one of your tools, and accepting both would leave you waiting on a call that cannot arrive. Send them on separate requests, or use ana, which serves both together.

2026-08-16

  • max_completion_tokens is now accepted as an alias for max_tokens, with the same minimum, maximum and error codes. Recent OpenAI SDKs send this name instead of max_tokens on reasoning models, and until today it parsed and was read by nobody, so a client that set only this field silently received the model default rather than the budget it asked for. If a request carries both, max_tokens wins.
  • Corrected, not changed: three things the request table said about max_tokens and reasoning were wrong for ana. It is not a visible-answer budget on her, it covers reasoning and the answer together. Values between 512 and her maximum are raised to her maximum rather than honored, because a smaller budget on a reasoning model returns an empty answer instead of a short one, so the field cannot lower your ceiling on ana today. And her responses do report usage.completion_tokens_details.reasoning_tokens, where the table said they carry no breakdown. Behavior is unchanged; the description was wrong.
  • Streamed responses are now padded, and this one is on unless you turn it off. Each frame carries an obfuscation field of random characters that rounds each frame up to a fixed block size, so frames whose contents differ by less than the block are identical and larger differences are coarsened rather than erased. Ignore it; nothing in it means anything. The reason it is there: we send one frame per token, so before today the size of each frame was the length of the token inside it, and encryption hides the content of a frame rather than its size. Anyone on the network path could read that sequence of lengths. Published work reconstructs a meaningful share of a response from lengths alone, and classifies what a prompt was about. The cost is a small number of extra bytes per frame. Send stream_options: {"include_obfuscation": false} to drop it on a network you trust.
  • Streaming responses can now return token counts. Send stream_options: {"include_usage": true} and the stream carries one final chunk with a usage object, the same fields the non-streaming response returns, after the last content chunk and before [DONE]. It is the only chunk whose choices array is empty, which is how to identify it. Omit the field and nothing changes, because a client that reads choices[0] without checking would fail on that shape. A request that ends in an error carries no usage chunk, matching an error response, which carries no usage body. Until today this parameter was accepted and ignored, and a streamed request reported no tokens at all.
  • Newly documented, unchanged: n above 1 returns 400 invalid_request_body, and logprobs: true returns 400 unsupported_feature.
  • seed, parallel_tool_calls and prediction are now honored on ana. Send seed for a repeatable run, and parallel_tool_calls: false if your harness can only execute one call per turn; prediction takes the output you expect and lets the engine decode against it, which is faster when most of the answer matches: send the current file and ask for one part of it to change. A wrong guess costs speed, never correctness, and it is sent on the first call of a turn only. All three are passed to the engine rather than dropped. They still have no effect on lexi, which runs a fixed sampling profile.
  • The legacy functions and function_call parameters now return 400 unsupported_feature instead of being accepted and ignored. Being ignored was worse than being refused: the model received no tools at all, so a request that looked accepted came back as a plain answer while your client waited for a function_call that could never arrive. Send the same definitions as tools, and use tool_choice in place of function_call.
  • Clarified, not changed: temperature, top_p, the penalties and logit_bias are still accepted and ignored on both models, and on ana the reason is the engine rather than a policy of ours. It accepts exactly one value for each and rejects everything else, so there is no setting for us to pass along.
  • max_tokens is now honored on ana. It was briefly clamped up to her ceiling, so any value you sent had no effect; it now caps the turn like it does everywhere else. On a reasoning model the budget covers reasoning and the visible answer together, so a small value can be spent entirely on reasoning: you get finish_reason "length" with little or no content, and usage.completion_tokens_details.reasoning_tokens tells you where it went. Nothing about billing changes, since you are charged for tokens produced rather than for the cap.
  • finish_reason no longer says "tool_calls" on a response that carries none. It is an instruction to dispatch, and a client that branches on it was being handed the instruction with nothing behind it: the engine reports "tool_calls" for a turn whose only call we then dropped, which happens when a call arrives truncated mid-arguments, without a name, or naming a function nobody defined. Such a turn now ends with "stop" and its text is the answer. A turn that does carry calls is unchanged, and "length" still means the output hit the budget whether or not calls went with it.
  • max_tokens no longer has a minimum of 512. Any positive value is accepted, and a small budget is served rather than refused: you get a shorter answer, truncated with finish_reason "length" if it did not finish. The old floor returned 400 max_tokens_too_small, which refused requests OpenAI accepts and hit classifiers, title generators and routing calls on their first request. Measured before changing it: lexi answers completely at 16 tokens and truncates honestly below that, with no empty responses at any value tried. Nothing changes for ana, whose output budget is raised to her ceiling regardless.
  • One tool name is reserved: aup_violation. It is ours on every call and is never returned to you to run. A request whose tools array defines a function by that name returns 400 unsupported_feature naming it; rename the function and send it again. No other name is reserved.

2026-08-15

  • Client-side function calling, on both models. Send a tools array of JSON Schema function definitions and the response comes back with finish_reason "tool_calls" and a tool_calls array on the message, where function.arguments is a JSON string. You execute them and continue the conversation by appending the assistant turn you received and one message per result with role "tool" and the matching tool_call_id. Both of those shapes used to be rejected or dropped: a role "tool" message returned 400, and an assistant tool_calls field was parsed and thrown away, so the second turn of a function-calling loop could not be sent. The API is stateless, so resend the tools array on every request in the exchange.
  • Your tools replace ours. Send a tools array and the model gets exactly those, plus one reserved name (see the aup_violation entry). Our own tools, research and fetch_url among them, are not added beside yours: a coding harness brings its own, and a name you define is always yours. A call comes back to you with finish_reason "tool_calls" for you to run. Send no tools array and you get our full set as before. Send "anathema_tools": false with no tools array of your own and you get a plain completion, with none of our capabilities in play.
  • tool_choice is honored on a request that carries tools: "auto", "none", "required", or {"type":"function","function":{"name":"..."}}. It used to return 400 unsupported_feature for everything except "auto". A named function must appear in that request tools array, or the response is 400. On a request with no tools array the old behavior is unchanged, and anything other than "auto" is still 400.
  • response_format and tools combine on ana and cannot be combined on lexi, where sending both returns 400 unsupported_feature. The difference is where the schema is enforced. Ana's upstream takes the schema as its own field and leaves the tool array alone, so both stay available for the whole turn. On lexi we serve structured output as a forced tool call, so the two features want the same channel and no resolution honors both. Send one or the other there.
  • Changed limit: tool definitions now count toward your prompt tokens and toward the published input limit for the model. This is the part that can turn a 200 into a 400 context_length_exceeded. The limit itself has not moved, and neither has what we accept, but the count that gates it used to skip the tools array entirely. The tools we send on your behalf were never counted either, which is about 2,500 tokens on lexi and about 2,800 on ana, so a request that sat inside that band of the published ceiling passed before and can fail now. On a request that carries your own tools we send only yours plus the one reserved name, so that overhead does not apply to it. Published ceilings are unchanged at 85,000 for lexi and 900,000 for ana. If you take a 400, the limit named in the error is the one we enforce, and shortening the prompt or sending fewer tool definitions brings the request back under it. There is no separate cap on how many tools you may send.
  • prompt_tokens billing scope, stated correctly. Our system prompt is still excluded from prompt_tokens and from your bill. Tool definitions you send are your own prompt tokens, so they are counted and billed like the rest of your request. The previous wording implied everything outside your messages was excluded, which was never true of anything you supplied.

2026-08-14

  • The published context window is now the number you can actually fill: 85,000 tokens for lexi and 900,000 for ana, where the table and GET /v1/models used to say 102,400 and 1,019,904. Nothing we accept has changed, so a request that worked yesterday works today. The old figures counted room the system prompt occupies, and on ana they were counted with a different tokenizer than the one you count with, so a request sized to the published number could come back 400 context_length_exceeded quoting a limit above your own count. The new figures leave both of those out. If you do go over, the limit named in the error is the internal one we enforce, which is larger than the published figure.
  • New model id: ana, in beta. She is a coding model, addressed the same way lexi is (send "model": "ana"). Her published context window, output ceiling, per-token prices, image limit and capabilities are in the table under The models, alongside lexi's. Read Data handling before you send anything: ana is served by third-party infrastructure, and under that provider's default terms the content you send and the content she returns may both be used to train and improve their models. There is no flag, header or parameter on this API that changes that.
  • Two new error codes for a hosted image URL we could not load, for reasons that are ours rather than yours. Both cases used to come back as a 400 image_input_invalid, which pointed at an image that was fine. image_service_unavailable (503) is the failure that clears on its own, so retry the request as sent. image_service_error (502) is the failure that does not, so the same request returns the same result; send the image inline as a base64 data: URL, or contact [email protected].
  • Breaking change for structured requests that also enable reasoning. A request that sends response_format (json_schema or json_object) and a reasoning_effort of "low", "medium", or "high" and a max_tokens above the ceiling below now returns 400 max_tokens_too_large where it returned 200. This reaches lexi: the exemption structured requests had on models that can turn reasoning off is gone. The ceiling for those requests used to be the full 16,384. It is now 15,360 at reasoning_effort "low", 14,336 at "medium", and 13,288 at "high", because the reasoning that reasoning_effort funds is spent out of the same output budget. The old 200 was the defect rather than the new ceiling: reasoning could take up to 3,096 tokens you never saw, so JSON we called schema-valid could come back truncated at finish_reason "length". To get a 200 again, lower max_tokens to the number the max_tokens_too_large message names, choose a smaller reasoning_effort, or omit max_tokens and take the default. Structured requests that send no reasoning_effort are unchanged, and so is any reasoning_effort sent without response_format.

2026-06-15

  • reasoning_effort now applies to structured output too. Previously it was ignored when you sent a json_schema or json_object response_format (thinking was always off for structured requests). Now "low", "medium", or "high" enables reasoning even with structured output, and the response stays valid JSON. Default is still off (direct, clean, cheapest).

2026-06-11

  • reasoning_effort parameter: reasoning is now off by default, so responses are direct and structured output (JSON, classification, extraction) stays clean and parse-ready. Set "low", "medium", or "high" to enable step-by-step reasoning for harder tasks.
  • URL fetching (opt-in per organization): when enabled, Lexi reads the text of up to 5 URLs in your latest message and answers from the page content. Turn it on under Developers, Settings, URL fetching.

2026-06-07

  • Vision: image_url content parts (hosted URLs and inline base64 data URLs).
  • Model discovery: GET /v1/models and GET /v1/models/{id}.
  • finish_reason now reports "length" when output hits the token budget.
  • Request body limit raised to 12 MB to accommodate inline images.

Support

Questions, limit raises, or a key you can't explain: email [email protected] with the request_id from the response.

Ready to build? Create an API key.