Known limitations
What the Cruxy API does not do today, why each limit matters for your code, and the workaround to use until it changes.
These are the limits you are most likely to hit when you build on the API. Each entry says what happens, why it matters, and what to do instead. When the API changes, the Changelog records it by version.
Kavi's thinking spends max_tokens and isn't returned
- What:
kavireasons by default, atreasoning_efforthigh, and a reasoning tier writes its thinking inside the samemax_tokensbudget as its answer.POST /api/v1/chatandPOST /v1/chat/completionsdo not return the thinking.POST /api/v2/chatreturns it as a separatereasoningstring, but no wire reports a reasoning-token count. - Why it matters: A small
max_tokensonkavican be spent entirely on thinking. The call ends withfinish_reason: "length"and empty content, and on the v1 wire nothing in the response shows where the tokens went. - Workaround: For a short, direct answer, send
reasoning_effort: "none". To keep the reasoning, leavemax_tokensunset, which gives the tier's cap (8192 onkavi). Treatfinish_reason: "length"with empty content as a spent budget, not an empty answer. See Reasoning effort.
# reasoning_effort "none" turns kavi's thinking off, so the whole
# max_tokens budget goes to the answer.
curl https://api.cruxy.ai/api/v1/chat \
-H "Authorization: Bearer $CRUXY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kavi",
"reasoning_effort": "none",
"max_tokens": 256,
"messages": [
{ "role": "user", "content": "In one sentence, what is a monsoon?" }
]
}'const res = await fetch('https://api.cruxy.ai/api/v1/chat', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.CRUXY_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'kavi',
// Thinking off: the whole max_tokens budget goes to the answer.
reasoning_effort: 'none',
max_tokens: 256,
messages: [{ role: 'user', content: 'In one sentence, what is a monsoon?' }],
}),
})
if (!res.ok) throw new Error(`chat failed: ${res.status} ${await res.text()}`)
const completion = await res.json()
if (completion.finish_reason === 'length' && !completion.message.content) {
// The budget ran out before any answer was written.
throw new Error(`no answer within max_tokens (request_id ${completion.cruxy.request_id})`)
}
console.log(completion.message.content)Non-streaming calls have a 90-second ceiling
- What: With
stream: false, the gateway makes one attempt, bounded by the tier's deadline: 90 seconds onkaviandvaani, and 45 seconds onmira. Past it, the call is cancelled and answered504upstream_timeout. It is not billed, and the gateway does not retry it. - Why it matters: A long
kaviturn, with reasoning on and a large output, can need more than 90 seconds. The same request takes as long again on a retry, so retrying it unchanged times out every time. - Workaround: Stream long turns with
stream: true: a stream may run for up to five minutes. Otherwise, ask for less with a lowermax_tokensorreasoning_effort. See Reliability.
Signed-in keys expire in 90 days, and you can't revoke one
- What: A key from device login or native OAuth login is a signed-in
credential. It stops working at its
expires_at, 90 days after issue by default. Use does not extend it, and there is no refresh grant. Its holder can neither rotate nor revoke it: it ends atexpires_at, when its organization membership ends, or when Cruxy support revokes it. - Why it matters: A signed-in key that leaks stays valid until one of those
happens. And a tool that stores one stops working the moment it expires,
with
401unauthorized, the same answer as a revoked key. - Workaround: On a server, use a developer API key, created in the Cruxy
Console: it does not expire, and you rotate or revoke it there. In a person's
own tool, store
expires_atwith the key and prompt for a new login before it passes. If a signed-in key leaks, ask Cruxy support to revoke it. See Get your API key.
Tool-call IDs may not use OpenAI's call_ format
- What: A tool call's
idis a string with no documented format. It is not guaranteed to start with thecall_prefix OpenAI uses, even though the examples in these docs showcall_abc123. - Why it matters: Code that validates, parses, or generates ids on the
call_pattern can reject a real call or fail to match its result. When the model makes several calls in one turn,tool_call_idis the only thing that says which result answers which call. - Workaround: Treat each
idas opaque. Copy it unchanged into thetool_call_idof your result, key any lookup on the whole string, and never build an id yourself. See Handle tool calls.
No per-language list for Indic languages
- What: Cruxy supports 20+ Indic languages, but it publishes no list of
which ones, and the tier catalog at
GET /api/v1/modelshas no language field. - Why it matters: Neither the docs nor the API can confirm that a specific language is supported, or how well each tier handles it.
- Workaround: Before you launch in a language, test it on each tier you plan
to use, with prompts from your own product. An eval suite under
/api/v1/evals/suitesruns the same cases against the tier you name; each case is billed as a chat completion.
Next steps
- Reliability: deadlines, retries, and confirming a stream finished.
- Reasoning effort: control how much a tier thinks.
- Get your API key: choose between a signed-in key and a developer API key.
- Handle tool calls: answer each call by its
tool_call_id. - OpenAI SDK compatibility: what
POST /v1/chat/completionssupports, ignores, and rejects.