Documentation
Send your first request.
The API speaks the OpenAI chat-completions format. Point your base URL here and the common path works unchanged — but not every parameter is supported, and the ones that are not come back as a 400 rather than being ignored.
Get a key
Create an account, confirm your email, then generate a key under API keys. Keys start with clst_live_ and are shown once — we only ever store a hash.
In your terminal
One file, no dependencies. It reads and writes files in the directory you start it in and runs commands there — asking before every write and every command.
curl -fsSL https://calestai.com/install.sh | sh calest logincd ~/your-projectcalest "find out why the tests fail and fix it"Needs Node 18 or newer. Then calest login, and describe what you want done.
Base URL
https://calestai.com
Use calest-auto as the model name and the router picks the tier per request. Which tier ran comes back under calest.tier. To fix the tier yourself, use calest-fast, calest-balanced or calest-deep; GET /models lists them with prices.
What the API takes
Not every OpenAI parameter. CALEST compiles the request in stages and runs every stage at temperature 0, so the knobs that would change sampling have nothing to act on. They are rejected with a 400, not quietly ignored — a call that looks like it worked and didn’t is the more expensive failure.
- messagesRequired. system, user and assistant.
- contentA string, or an array of text and image_url parts.
- modelcalest-auto, a fixed tier, or calest-direct.
- streamServer-sent events, same shape as OpenAI.
- max_tokensCompiled: lowers a tier's budget only. Direct: up to 32000.
- max_completion_tokensSame thing, newer name.
- userPassed through, used for nothing.
- toolsFunction calling, OpenAI shape. Up to 64 per request.
- tool_choiceauto, none, required, or a named function.
- temperature, top_p, seed, stopCompiled: rejected, every stage runs at 0. Direct: honoured.
- frequency_penalty, presence_penaltySame — rejected compiled, honoured direct.
- logprobs, logit_biasRejected in both. The output never comes back.
- nMust be 1. CALEST returns a single answer.
- functions, function_callRejected — the superseded form. Use tools.
- response_formatRejected. No JSON mode; use a tool instead.
A system message is honoured: it is placed in front of the task, so standing instructions apply.
Images go in content as image_url parts — see across. One rule to know up front: an image without text comes back as a 400. CALEST compiles a request, and an attachment on its own is not one; inventing “describe this image” on your behalf would be exactly the made-up requirement the rest of this thing exists to prevent.
What a request costs
Every request carries a floor of roughly 1 400 prompt tokens, however short the question. That is the compiler reading the task before anything is answered, and it is what the price per million does not tell you: “Capital of France?” costs about half a cent here, not a thousandth of one. The floor is worth it on real work and absurd on one-liners — send those straight to a model.
For the same reason usage reports the whole pipeline, not the final answer: a six-character reply can show 235 completion tokens. That is what you are billed for and what calest.cost_eur is computed from — do not read it as the length of the answer.
Limits
Requests are billed by usage, not per call, and every response carries what it cost. The rate limit is 120 requests per minute per IP address; a 429 comes with a Retry-After header and means back off, not that something is broken.
A complete request
curl https://calestai.com/chat/completions \ -H "Authorization: Bearer clst_live_..." \ -H "Content-Type: application/json" \ -d '{ "model": "calest-auto", "messages": [{"role": "user", "content": "Summarise this quarter."}] }'Streaming and balance
curl https://calestai.com/v1/chat/completions \ -H "Authorization: Bearer clst_live_..." \ -d '{"model":"calest-auto","messages":[...],"stream":true}' curl https://calestai.com/v1/credits \ -H "Authorization: Bearer clst_live_..."# {"balance_eur": 9.70, "balance_units": 96957, ...}stream: true sends server-sent events in the OpenAI chunk shape, ending with [DONE]. The final chunk carries usage and calest.
GET /v1/credits returns what is left on the account, so a batch job can check before it starts instead of hitting a 402 halfway through. Every successful response also carries X-RateLimit-Remaining.
Coding agents (Cline, Roo, Continue)
API Provider OpenAI CompatibleBase URL https://calestai.comAPI Key clst_live_...Model ID calest-direct Model config (where the client asks for it) Context window 200000 Max output tokens 32000 Supports images yes Supports browser use noUse calest-direct for these, not calest-auto. An agent like Cline writes its own prompt — tens of thousands of characters defining its own tool protocol — and there is nothing left to compile. Worse, CALEST’s answer prompt (“lead with the result”, “headings for longer texts”) actively fights that protocol, the 24 000-character limit rejects the first call outright, and temperature comes back as a 400.
calest-direct does none of that. Your messages go to the model unchanged, system messages stay system messages, sampling parameters are honoured, max_tokens works up to 32 000, and a conversation can run to 700 000 characters. One call per request instead of two to four, so a step costs less than a compiled one.
Said plainly, because it is the honest half: in this mode CALEST does not compile anything. What you get is one key instead of several, per-request billing in euros, a usage list, your own provider key if you want it — and the same endpoint, so switching a client between compiled and direct is one string. The reason CALEST exists is in the other mode.
Tools
# 1. Werkzeug anbieten{"model":"calest-auto", "messages":[{"role":"user","content":"What's the weather in Kiel?"}], "tools":[{"type":"function","function":{ "name":"get_weather", "description":"Current weather for a place", "parameters":{"type":"object", "properties":{"city":{"type":"string"}}, "required":["city"]}}}]} # → finish_reason: "tool_calls"# tool_calls: [{"id":"call_1","type":"function",# "function":{"name":"get_weather",# "arguments":"{\"city\":\"Kiel\"}"}}] # 2. Ergebnis zurueckgeben — Aufruf und Antwort anhaengen{"model":"calest-auto", "messages":[ {"role":"user","content":"What's the weather in Kiel?"}, {"role":"assistant","content":null,"tool_calls":[ … ]}, {"role":"tool","tool_call_id":"call_1","content":"12 °C, rain"}], "tools":[ … ]} # → finish_reason: "stop", der Text.tools and tool_choice in the OpenAI shape, so an existing agent loop points its base URL here and works. tool_choice takes auto, none, required or a named function. Up to 64 tools per request; a duplicate name is a 400 rather than a coin flip you never see.
One trap, and it is not ours: required forces a call on every turn you send it, so a loop that keeps it set never reaches the answer — it just keeps calling, and each round is a billed request. Set it on the first call and switch to auto afterwards.
Two things are different here, and both follow from what CALEST is for. The answer prompt forbids invented arguments: a schema with required pushes a model to fill the field whatever happens, and a wrong figure in a sentence gets read and doubted while a wrong argument in a call gets executed. If something is missing, it answers in text and names what it needs.
And a tool call is not checked for coverage — it is not an answer, it is the announcement of going to fetch one. The completeness check and the repair step run on the turn where the model stops calling and writes. That is also why a tool step is cheaper than a full request.
Worth knowing before you build on it: every step still pays the compile floor, because CALEST reads the task before each answer. That is amortised over a step that reads a file and writes one, and absurd over fifty micro-steps. Build the loop with few, large steps. response_format stays unsupported — for a structured result, describe it as a tool and get validated JSON instead of prose shaped like JSON.
Images
curl https://calestai.com/v1/chat/completions \ -H "Authorization: Bearer clst_live_..." \ -H "Content-Type: application/json" \ -d '{ "model": "calest-auto", "messages": [{"role": "user", "content": [ {"type": "text", "text": "What is the total on this receipt, and is it dated?"}, {"type": "image_url", "image_url": { "url": "data:image/png;base64,iVBORw0KGgo..."}} ]}] }'content can be an array of text and image_url parts, the same shape OpenAI uses — a client that already sends images needs no change. PNG, JPEG, WebP and GIF, either as an https: URL or a base64 data: URL. Up to 8 per request, 5 MB each, 12 MB together.
An image goes to the stage that writes the answer, not to all four — the stage that reads your request is told an image is attached and works from your text, which keeps the compiler from costing you the image twice.
Images you leave in earlier messages are sent too, exactly as you supplied them. That is the expensive option and it is deliberately yours: a conversation that keeps resending the same picture pays for it every round, and dropping it from messages yourself is one line. The 8-image limit counts the whole request, not each message.
We did try the cheaper route — replacing an old image with a line saying one had been there. Asked a follow-up about it, the model answered with an invented figure instead of saying it could not see the picture. On both tiers, with and without a rule forbidding it. So the line is gone and the image goes.
What compilation does not do for the picture itself: every requirement CALEST treats as binding has to quote a span of your own text, and you cannot quote a span of a photograph. So the question about the image is compiled and the answer is checked against it — but nothing verifies that what the model reports seeing is really there. The answer prompt requires it to quote what it reads and to flag anything blurred or cut off, which is a rule, not a guarantee.
Check an answer you already have
curl https://calestai.com/v1/check \ -H "Authorization: Bearer clst_live_..." \ -d '{"task": "...", "answer": "..."}' # {"ok": false,# "requirements": {"total": 3, "complete": 2, "missing": 1},# "coverage": [{"requirement": "battery life",# "status": "missing", "gap": "not mentioned"}]}POST /v1/check takes a task and an answer and reports, requirement by requirement, what was covered and what was not. It generates nothing.
Built for agent loops. In a single answer a missed requirement is a weak paragraph; over twenty steps it is twenty steps in the wrong direction, and it only shows at the end. Two model calls at most — often one, because a local screen skips the second when the answer plainly covers everything.
ok means one thing only: no binding requirement was left open. It is not a judgement of quality, and not_assessed says when nothing could be judged — there is no silent “looks fine”.
Errors
Every failure returns the same shape: a status code, a stable code, a sentence you can show a user, and a request id to quote at us.
- 400bad_requestSomething in the body is missing or malformed.
- 401unauthorizedNo key, or a key that has been revoked.
- 402insufficient_creditsOut of credit. Top up and retry.
- 403forbiddenThe account exists but isn't allowed to do this yet.
- 429rate_limitedToo many requests. Back off and retry.
- 503upstreamThe model provider failed or is out of capacity. Safe to retry.
- 500internalOur side. Safe to retry, and worth quoting the request id.
Something missing here? Tell us what you were trying to build and we'll write it up. More about the API.