API guide for agents

A dense, single-page reference for calling the Cruxy API correctly: auth, tiers, chat, tool calling, embeddings, rerank, moderations, and errors.

One page with everything a model needs to call the API correctly, and nothing else. The base URL is https://api.cruxy.ai. Every path below is under it. For the exhaustive per-field spec, see the API Reference; for the whole corpus as text, fetch /llms-full.txt.

Auth

Every endpoint except device login needs a Bearer key in the Authorization header. Keys carry the cxy_live_ prefix. A missing or invalid key is 401.

curl https://api.cruxy.ai/api/v1/me \
  -H "Authorization: Bearer $CRUXY_API_KEY"

GET /api/v1/me echoes { api_key_id, user_id }; use it to confirm a key works.

Device login

Get a key with an RFC 8628 device flow: no key required, since its job is to mint one.

  1. POST /api/v1/device/code (no body, no auth) returns { device_code, user_code, verification_uri, verification_uri_complete, expires_in, interval }.
  2. Send the user to verification_uri_complete to approve before expires_in seconds elapse.
  3. POST /api/v1/device/token with { device_code }, no more often than every interval seconds. While unresolved it returns 400 with a code: only authorization_pending means keep polling; access_denied, expired_token, and invalid_grant are terminal. On approval it returns 200 with { access_token, token_type, key_id, user_id }.

access_token is the cxy_live_ key, returned once; persist it immediately.

Tiers

model is a public tier: kavi, vaani, or mira. It also accepts auto, which the server-side router resolves to a tier per request, before billing and the upstream call; an auto request may be budget-downgraded to mira, so read cruxy.routed_tier on the response for the tier that served the call (see Auto routing). Never send an upstream provider id; tiers are the stable contract. Discover the catalog at runtime instead of hardcoding:

curl https://api.cruxy.ai/api/v1/models \
  -H "Authorization: Bearer $CRUXY_API_KEY"

GET /api/v1/models returns { object: "list", data: [...] }, each entry { id, label, profile, context_window, max_output, latency_target_ms, reasoning, supports_tools, supports_vision, pricing }. Read context_window, max_output, and supports_tools from the response rather than assuming them. Choosing a tier compares the tiers.

Chat

POST /api/v1/chat. The request is OpenAI-shaped:

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" }]
  }'

Request fields beyond model and messages:

FieldTypeNotes
messagesarrayEach { role, content }; role is system, user, assistant, or tool.
streambooleantrue for SSE, false for one JSON object. Optional; false by default.
temperaturenumber0–2, forwarded upstream unchanged. Omit for the model default.
max_tokensintegerUpper bound on generated tokens. Clamped to the tier cap: kavi 8192, vaani 4096, mira 2048.
reasoning_effortstringnone, low, medium, or high; none disables reasoning. Default per tier: kavi high, vaani low, mira none. Thinking spends max_tokens, so a small max_tokens on kavi can end with finish_reason: "length" and empty content; send none for short answers. See Reasoning effort.
toolsarrayFunction tools; see below.
tool_choicestring or objectnone, auto, required, or { type: "function", function: { name } }.
memoryobject{ recall, capture }, each a boolean. Off by default for a developer key; true needs the memory scope. See Memory.
rulesbooleanStanding-rules injection. Off by default for a developer key; true needs the rules scope. See Standing rules.

Response shape

The non-streaming and streaming responses differ in shape; read the one that matches your stream value.

stream: false returns a single object with the reply at the top level (not an OpenAI choices[] array):

{
  "id": "chatcmpl-8f3a1c2d",
  "model": "kavi",
  "message": { "role": "assistant", "content": "Hi." },
  "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": "…", "routed_tier": "kavi", "reasoning_effort": "high" }
}

<amount> and <currency> are placeholders; a real amount is a number. Amounts are specific to your account and shown in your Cruxy Console.

stream: true is an OpenAI-style Server-Sent Events stream: each event is a chat.completion.chunk on a data: line, terminated by data: [DONE]. Chunks use the choices[].delta shape:

data: {"object":"chat.completion.chunk","choices":[{"index":0,"delta":{"content":"Hi"}}]}
 
data: [DONE]
const reader = res.body!.getReader()
const decoder = new TextDecoder()
let buffer = ''
while (true) {
  const { value, done } = await reader.read()
  if (done) break
  buffer += decoder.decode(value, { stream: true })
  const events = buffer.split('\n\n')
  buffer = events.pop() ?? ''
  for (const event of events) {
    const line = event.replace(/^data: /, '').trim()
    if (line === '[DONE]') break
    const chunk = JSON.parse(line) // choices[0].delta.content is the token
  }
}

The final chunk carries usage as { prompt_tokens, completion_tokens, total_tokens, cost, … }.

Tool calling

Pass tools, each { type: "function", function: { name, description, parameters } } where parameters is a JSON Schema object. When the model calls one, the turn ends with finish_reason: "tool_calls" and the assistant message carries tool_calls:

{ "id": "call_abc123", "type": "function",
  "function": { "name": "get_weather", "arguments": "{\"city\":\"Paris\"}" } }

arguments is a JSON-encoded string; parse it. Run the function, then continue the conversation with the assistant message that made the call, kept whole, followed by one tool result per call, tied back by tool_call_id, and send the same tools again:

messages.push(
  message, // the assistant turn as returned, tool_calls included
  { role: 'tool', tool_call_id: 'call_abc123', content: JSON.stringify(result) },
)

Repeat while finish_reason is tool_calls; the turn with finish_reason: "stop" carries the answer. Handle tool calls has the full loop.

Embeddings

POST /api/v1/embeddings with model: "cruxy-embed". input is a string or an array of strings; one embedding is returned per input, in order.

curl https://api.cruxy.ai/api/v1/embeddings \
  -H "Authorization: Bearer $CRUXY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "model": "cruxy-embed", "input": ["hello", "world"] }'

Returns { object: "list", model, data: [{ object: "embedding", index, embedding: [...] }], usage: { prompt_tokens, total_tokens }, cruxy }.

Rerank

POST /api/v1/rerank with model: "cruxy-rerank", a query, and a documents array of candidate strings. Optional top_n caps how many results come back.

curl https://api.cruxy.ai/api/v1/rerank \
  -H "Authorization: Bearer $CRUXY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "cruxy-rerank",
    "query": "What is the capital of France?",
    "documents": ["Paris is the capital of France.", "Berlin is in Germany."],
    "top_n": 3
  }'

Returns { model, results: [{ index, relevance_score }], usage, cruxy }, ranked by relevance_score descending. Each result's index points back into the documents you sent.

Moderations

POST /api/v1/moderations with model: "cruxy-guard". input is a string or an array; one result is returned per input.

curl https://api.cruxy.ai/api/v1/moderations \
  -H "Authorization: Bearer $CRUXY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "model": "cruxy-guard", "input": "text to classify" }'

Returns { id, model, results: [{ flagged, categories, category_scores }] }. flagged is the overall boolean; categories is a per-label boolean map over harassment, hate, self-harm, sexual, and violence (with their /-suffixed subtypes). category_scores mirrors it as numbers, but the guard model emits labels, so those are binary 1.0/0.0, not calibrated probabilities. This endpoint returns no cruxy block and no usage.

Errors

Every failure is { code, error }. Branch on code: it is stable; error is human text that may change.

StatuscodeMeaning
400invalid_requestMalformed request. Fix it; a retry fails the same way.
400context_overflowChat input exceeded the tier's context window.
401unauthorizedBearer key missing, malformed, unknown, expired, or revoked.
429rate_limitedRequests too fast. Back off, then retry.
429budget_exhaustedSpend cap hit. A retry won't clear it until the window resets.

The device-login endpoints use OAuth-style 400 codes (authorization_pending, access_denied, expired_token, invalid_grant) instead of this shape.

The cruxy block

Cruxy-native fields are additive and namespaced under a cruxy key, so a vanilla OpenAI client ignores them and keeps parsing. What the block carries differs by endpoint:

  • Chat: { request_id, routed_tier, reasoning_effort }. Token counts and cost live in the sibling usage object, not here.
  • Embeddings and rerank: { request_id, cost }.
  • Moderations: no top-level block. The request id is the response's id, formatted modr-<request_id>, and a flagged result carries results[].cruxy.hazard_codes.

request_id is a UUID for the call. Log it; it identifies the call in support requests. See Request IDs and metadata.

Was this page helpful?