Chat
OpenAI-compatible chat completions across the Cruxy tiers, streaming or not.
POST /api/v1/chat runs an OpenAI-compatible chat completion. You pick a
public tier, send a list of messages, and read back either a single JSON
completion or a stream of Server-Sent Events. Here is the smallest
complete call:
curl https://api.cruxy.ai/api/v1/chat \
-H "Authorization: Bearer $CRUXY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kavi",
"stream": false,
"messages": [
{ "role": "user", "content": "Hello" }
]
}'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',
stream: false,
messages: [{ role: 'user', content: 'Hello' }],
}),
})
const completion = await res.json()Tiers, not provider models
model is a public tier, one of:
| Tier | Profile |
|---|---|
kavi | Deep |
vaani | Balanced |
mira | Fast |
The gateway resolves each tier to an upstream provider model on the server. You never send or receive an upstream provider id. Tiers are the stable contract, so your code keeps working when the backing model changes. Choosing a tier compares them; to discover the catalog at runtime rather than hardcoding these strings, see Models.
model also accepts "auto", which hands tier selection to the gateway's
router. It resolves to a concrete tier before billing and the upstream call,
and routed_tier on the response (see The cruxy block)
names the tier that served. Auto routing covers
what the router guarantees.
Request options
Beyond model and messages, the request accepts:
| Field | Type | Example | Purpose |
|---|---|---|---|
stream | boolean | true | Stream SSE chunks. Optional and false by default, which returns one JSON object. |
temperature | number | 0.7 | Sampling temperature. |
max_tokens | integer | 1024 | Cap on tokens generated. |
reasoning_effort | string | "medium" | How much the model thinks before answering: none, low, medium, or high (see Reasoning effort). |
tools | array | Functions the model may call (see Define tools). | |
tool_choice | string or object | "auto" | Whether and which tool the model calls. |
memory | object | { "recall": true } | Memory recall and capture for this request (see Memory). |
rules | boolean | false | Standing-rules injection for this request (see Standing rules). |
Each messages entry has a role and content. Assistant turns that
call tools carry tool_calls, and your replies to those calls carry a
tool_call_id; both are covered in
Handle tool calls.
Streaming vs. not
stream controls the response format.
stream: false
This is what you get when you omit stream: a single JSON object. Cruxy
flattens it: the assistant reply is the
top-level message, alongside finish_reason, usage, and the cruxy
block. There is no OpenAI choices[] array. Read message.content
directly, not choices[0].message.
{
"id": "chatcmpl-8f3a1c2d",
"model": "kavi",
"message": { "role": "assistant", "content": "Namaste." },
"finish_reason": "stop",
"usage": {
"input_tokens": 12,
"output_tokens": 48,
"billable_input_tokens": 12,
"cache_read_tokens": 0,
"cache_write_tokens": 0,
"cost": { "tier": "kavi", "amount": "<amount>", "currency": "<currency>" }
},
"cruxy": {
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890",
"routed_tier": "kavi",
"reasoning_effort": "high"
}
}<amount> and <currency> are placeholders: a real amount is a number. See
Usage and cost for the full cost object.
stream: true
You get an OpenAI-style Server-Sent Events stream: a sequence of
chat.completion.chunk events, each on a data: line, terminated by a
final data: [DONE]. Unlike the non-streaming reply, a chunk uses the
OpenAI choices[] shape: each token arrives in choices[0].delta.content.
curl -N https://api.cruxy.ai/api/v1/chat \
-H "Authorization: Bearer $CRUXY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "kavi",
"stream": true,
"messages": [
{ "role": "user", "content": "Write a haiku about monsoon rain." }
]
}'data: [DONE] is the only signal that the completion finished: a stream that
ends without it failed partway. Streaming covers
the frame order, the chunk and usage shapes, a complete TypeScript reader, and
how to detect a failed stream.
The cruxy block
Cruxy-native fields are additive and namespaced under a cruxy key. They
never alter the OpenAI-shaped fields, so a vanilla OpenAI client parses the
response unchanged and ignores the extra key. On a chat response the block
carries three fields for the turn:
request_id: a UUID for the call, e.g.a1b2c3d4-e5f6-7890-abcd-ef1234567890. Log it; it identifies the call in support requests and pairs with Feedback.routed_tier: the tier that actually served the request. This is how you learn which tier ran when you sendmodel: "auto".reasoning_effort: the effort the call ran at, after any tier default is applied.
When they apply, the block also reports what the turn recalled, captured, or
injected: memory_recall, memory_capture, rules_applied, files_omitted,
and undeclared_tool_replay.
Request IDs and metadata lists
every field, and how to quote request_id when you report a problem.
Cost and token counts are not in this block; they live in usage.
Usage
Every chat response carries a usage object with the token accounting and
the call's cost. The token fields depend on the response mode:
non-streaming uses input_tokens and output_tokens; streaming uses
prompt_tokens, completion_tokens, and total_tokens. Both also carry
billable_input_tokens, cache_read_tokens, and cache_write_tokens.
usage.cost breaks down the price of the call:
| Field | Meaning |
|---|---|
tier | The public tier that served the request. |
amount | Total cost of the call. |
currency | Currency of amount. |
input_unit | Input rate, per million tokens. |
output_unit | Output rate, per million tokens. |
cache_read_unit | input_unit × the cache discount, for cached reads. |
Amounts and rates are specific to your account and shown in your Cruxy Console. Usage and cost covers which tokens are billed and how a stream reports usage.
Errors
Chat returns 400 for a malformed request, 401 unauthorized for a
missing, expired, or revoked key, 429 when you hit a limit, such as
rate_limited or budget_exhausted, and 5xx when the model call fails.
With stream: false the gateway makes one attempt, bounded by the tier's
deadline; an attempt that runs past it is 504 upstream_timeout, which the
gateway does not retry and does not bill (see
Timeouts). See
Errors for the response shape,
Errors and retries for every code and
which ones to retry, and Rate limits and budgets
for how to tell a rate limit from a budget.
Next steps
- Streaming: read the SSE stream and detect a failed one.
- Define tools: let the model call your functions.
- Reasoning effort: keep thinking from spending your
max_tokens. - Known limitations: the limits to plan around, such as the 90-second non-streaming ceiling.
- Streaming chat in the browser: consume the SSE stream in a UI.
- Models: discover tiers at runtime.
- Errors and retries: handle
400,401, and429, and retry safely.