OpenAI SDK compatibility

What POST /v1/chat/completions supports, ignores, and rejects from an OpenAI request, and how its streams, tool calls, and errors behave in the openai SDK.

POST /v1/chat/completions is the route the official openai npm package calls when you set baseURL to https://api.cruxy.ai/v1. It runs your messages on the tier you name and nothing else: no memory, no standing rules, and no cruxy block, so a run is reproducible. This page goes field by field through what it does with an OpenAI request. To move a client over first, see Migrate from OpenAI.

Request parameters

ParameterStatusBehavior
modelSupportedkavi, vaani, mira, or auto. Required.
messagesSupportedRequired. Each message has a role, one of system, user, assistant, or tool, and a string content. An assistant message can replay tool_calls, and a tool message needs tool_call_id.
max_tokensSupported, clampedUnset, or above the tier's cap, is clamped to the cap: 8192 on kavi, 4096 on vaani, 2048 on mira. Thinking is generated inside it.
temperatureSupportedForwarded upstream unchanged; the gateway adds no bound of its own. Omit it for the model's default.
top_pSupportedForwarded upstream unchanged. 0 is a real value, so sending it is not the same as omitting it. Omit it for the model's default.
streamSupportedfalse by default.
stream_optionsSupportedinclude_usage: true adds the usage frame to a stream. Ignored when stream is false.
reasoning_effortSupportednone, low, medium, or high. Omit it for the tier's default.
toolsSupportedFunctions the model may call: type: "function", with function.name, description, and a JSON Schema parameters.
tool_choiceSupported"none", "auto", "required", or {"type": "function", "function": {"name": "…"}} to force one function.
response_formatSupported{"type": "text"}, {"type": "json_object"}, or {"type": "json_schema", "json_schema": {"name", "schema", "strict"}}.
n, stop, seed, presence_penalty, frequency_penalty, logit_bias, logprobs, userIgnoredAccepted and not applied, so a request that sends them still runs.
Any other OpenAI fieldIgnoredThe route ignores the OpenAI fields it does not implement. That includes max_completion_tokens: cap the output with max_tokens.
file_idsRejectedThe file attachments of POST /api/v1/chat are refused here.

messages takes the four roles above; OpenAI's developer role is not one of them, so send your instructions as a system message.

Requests the route rejects

These requests fail with a 4xx and the code shown:

RequestStatus and code
model other than kavi, vaani, mira, or auto400 invalid_request
reasoning_effort other than none, low, medium, or high, OpenAI's minimal included400 invalid_request
tool_choice names a function that is not in tools, or is "required" with no tools400 invalid_request
A tool's parameters schema exceeds a bound on bytes, depth, or nodes400 invalid_tool_schema
A response_format JSON schema is malformed, oversized, too deep, or uses an unsupported keyword400 invalid_schema
The input exceeds the tier's context window400 context_overflow
The server-side input moderation gate, when it is enabled, refuses the request's text400 moderation_rejected, not billed
response_format on a tier that does not do constrained decoding422 structured_output_unsupported
An Idempotency-Key reused with a different body409 idempotency_conflict

One more 422 comes after the model call: structured_output_unsatisfied means the output did not satisfy your json_schema. That request is billed, and it may pass on a retry.

Response fields

With stream: false, the response is a standard chat.completion object:

FieldValue
idThe completion id, such as chatcmpl-8f3a1c2d.
objectchat.completion.
createdUnix seconds at which the completion was produced.
modelThe tier that served the request. With auto, the tier the router chose.
choices[].messageThe assistant message, with tool_calls when the model called tools.
choices[].finish_reasonstop, tool_calls, length, or content_filter.
choices[].logprobsAlways null. The key is there because OpenAI sends it.
usageprompt_tokens, completion_tokens, and total_tokens, plus prompt_tokens_details.cached_tokens when the prompt cache was involved.

The response has no cruxy block, no request_id, and no cost.

Cruxy extensions in the SDK

Every Cruxy extension this route honors is either a typed SDK parameter or a header, so you never need an untyped body field:

  • reasoning_effort is OpenAI's own parameter, typed in chat.completions.create, with Cruxy's values: send only none, low, medium, or high, although the SDK's type allows more. kavi thinks at high by default, vaani at low, and mira not at all. See Reasoning effort.
  • Idempotency-Key is a request header; pass it in the SDK's per-request headers option. The first success under a key is stored for 24 hours, and a retry with the same key and body replays it without calling the model or billing again. Only a success is stored, and only with stream: false. The SDK does not send this header for you. See Reliability.
  • X-Request-Id is on every response, and it is the only request ID this route returns. The SDK exposes it as _request_id on a result and as requestID on an error. Send your own, up to 128 characters of [A-Za-z0-9_-], and the gateway reuses it. See Request IDs and metadata.
# -i prints the response headers, including X-Request-Id.
curl -i https://api.cruxy.ai/v1/chat/completions \
  -H "Authorization: Bearer $CRUXY_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: $(uuidgen)" \
  -d '{
    "model": "kavi",
    "reasoning_effort": "none",
    "max_tokens": 256,
    "messages": [
      { "role": "user", "content": "In one sentence, what is a monsoon?" }
    ]
  }'

Memory and standing rules

This route does no memory recall, no memory capture, and no standing-rules injection, and it takes no vault_id. There is nothing to pass through the SDK to turn them on. To use them, call POST /api/v1/chat or POST /api/v2/chat with fetch: the SDK's chat.completions methods call /chat/completions under the base URL, so they cannot reach either route. See Memory and Standing rules.

Tool calls

Tool calling keeps OpenAI's shapes. Declare tools, and when finish_reason is tool_calls, run each call in message.tool_calls, append the assistant message and one tool message per call, and call again.

A tool call's id is a string with no documented format. It may not start with call_, even though examples show call_abc123. Copy it unchanged into tool_call_id, and never parse, validate, or build one; see Tool-call IDs may not use OpenAI's call_ format.

import OpenAI from 'openai'
 
const client = new OpenAI({
  apiKey: process.env.CRUXY_API_KEY,
  baseURL: 'https://api.cruxy.ai/v1',
})
 
const tools: OpenAI.ChatCompletionTool[] = [
  {
    type: 'function',
    function: {
      name: 'get_weather',
      description: 'Get the current weather for a city',
      parameters: {
        type: 'object',
        properties: { city: { type: 'string' } },
        required: ['city'],
      },
    },
  },
]
const messages: OpenAI.ChatCompletionMessageParam[] = [
  { role: 'user', content: 'Is it raining in Pune?' },
]
 
let completion = await client.chat.completions.create({ model: 'vaani', tools, messages })
while (completion.choices[0].finish_reason === 'tool_calls') {
  const message = completion.choices[0].message
  messages.push(message)
  for (const call of message.tool_calls ?? []) {
    if (call.type !== 'function') continue
    const args = JSON.parse(call.function.arguments)
    // Echo the id exactly as it arrived; do not assume a call_ prefix.
    messages.push({
      role: 'tool',
      tool_call_id: call.id,
      content: JSON.stringify({ city: args.city, raining: true }),
    })
  }
  completion = await client.chat.completions.create({ model: 'vaani', tools, messages })
}
 
console.log(completion.choices[0].message.content)

Streaming

With stream: true, the route speaks OpenAI's Server-Sent Events protocol, in this order:

  1. A role primer: the first chat.completion.chunk.
  2. Content deltas in choices[0].delta.content, or tool-call fragments in choices[0].delta.tool_calls, which you assemble per index.
  3. A frame that carries finish_reason.
  4. With stream_options: {"include_usage": true} only, a usage frame: choices is an empty array and usage is complete. Check choices before you index it.
  5. data: [DONE].

If the model fails after the stream has opened, the stream ends with a data: {"error": {...}} frame instead of [DONE], so a cut-off answer never looks finished. The SDK throws that frame as an APIError from the loop: catch it and discard the partial text. The gateway retries a failing model only while the stream opens, never after a byte has reached you, and a stream may run for up to five minutes.

# -N turns off buffering so frames print as they arrive.
curl -N https://api.cruxy.ai/v1/chat/completions \
  -H "Authorization: Bearer $CRUXY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "vaani",
    "stream": true,
    "stream_options": { "include_usage": true },
    "messages": [
      { "role": "user", "content": "Write a haiku about monsoon rain." }
    ]
  }'

A stream stores and replays nothing under Idempotency-Key. A retry with the same key and body streams a new generation; when both attempts complete, the request is billed once. Streaming covers the stream of POST /api/v1/chat, which differs: it sends no error frame.

Errors

Errors on POST /v1/chat/completions and GET /v1/models use OpenAI's envelope:

{
  "error": {
    "code": "unauthorized",
    "message": "unauthorized",
    "param": null,
    "type": "authentication_error"
  }
}
  • code is the stable Cruxy error code, the same vocabulary as the rest of the API. Branch on it.
  • type is a coarse category derived from the HTTP status: invalid_request_error, authentication_error, permission_error, not_found_error, rate_limit_error, or server_error.
  • param is always null: no error on this route names the field at fault.

The /api/v1 routes use Cruxy's own envelope instead. The two differ in where each value sits:

Item/v1/chat/completions and /v1/models/api/v1 routes
Envelope{"error": {"message", "type", "param", "code"}}{"code", "error", "request_id", ...}
The code to branch onerror.codecode
The messageerror.messageerror
The request IDThe X-Request-Id header onlyrequest_id in the body
Detail fields such as reset_at, mira_available, or limitNot carriedIn the body

Because this envelope has no place for the detail fields, read the headers instead: every 429 carries Retry-After, and the X-RateLimit-* headers describe the per-minute request window. Errors and retries lists every code and which ones to retry.

The SDK throws a non-2xx response as an APIError, with the status in status, the Cruxy code in code, and X-Request-Id in requestID:

# OpenAI's "minimal" is not a Cruxy reasoning_effort, so this is a 400.
curl -i https://api.cruxy.ai/v1/chat/completions \
  -H "Authorization: Bearer $CRUXY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "kavi",
    "reasoning_effort": "minimal",
    "messages": [
      { "role": "user", "content": "Hello" }
    ]
  }'

Retries in the SDK

The openai package retries a failed request twice by default: after a connection error, and on 408, 409, 429, and any 5xx. It waits for Retry-After when that is 60 seconds or less. Against Cruxy, three of those retries are unlikely to help:

  • 504 upstream_timeout. The gateway made one attempt and does not retry it, and the same request usually takes as long again. Stream long turns, or lower max_tokens or reasoning_effort, rather than rely on the retry.
  • 429 spend and budget codes, such as budget_exhausted. They clear only at their reset, and their Retry-After can be days away, so a retry sooner fails the same way. Branch on code and stop retrying them.
  • 409 idempotency_conflict. The key was reused with a different body, and the SDK resends that same body. Send the new body under a new key.

A 504 or a 429 is not billed. To make the SDK's retries after a dropped connection safe, send an Idempotency-Key, as the extensions sample does.

Known differences

These are the places where this route behaves unlike OpenAI's, collected in one list:

  • model takes a tier or auto, and the response's model is the tier that served, never an upstream model id.
  • Fields the route does not implement are ignored without an error, so a stop sequence or n has no effect.
  • Thinking spends max_tokens and is not returned; see Kavi's thinking spends max_tokens and isn't returned.
  • A non-streaming call ends at the tier's deadline with 504 upstream_timeout; see Non-streaming calls have a 90-second ceiling.
  • A tool call's id may not use the call_ format; see Tool-call IDs may not use OpenAI's call_ format.
  • error.code is a Cruxy code, param is always null, and the detail fields of a Cruxy error do not travel.
  • The response carries no request ID in its body and no cost in its usage.
  • No memory, standing rules, vaults, or file attachments.

Known limitations lists the limits that apply across the API, each with a workaround.

Next steps

Was this page helpful?