# Chat completions

> POST /v1/chat/completions: request fields, headers, the response and the cairva metering object.

Source: https://cairva.xyz/docs/chat-completions

Runs a chat completion against your allocation's model and meters it. The request and response follow the OpenAI chat completions format, so existing OpenAI SDK code works once you change the base URL, the key and the model id.

```endpoint
POST /v1/chat/completions
```

Requires the `inference` scope.

## Example

```bash curl
curl https://cairva.xyz/v1/chat/completions \
  -H "Authorization: Bearer $CAIRVA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "x-request-id: ticket-4812" \
  -d '{
    "model": "gemma4:e2b",
    "messages": [
      {"role": "system", "content": "Answer in one sentence."},
      {"role": "user", "content": "What does a metered allowance include?"}
    ],
    "max_tokens": 200
  }'
```

```javascript JavaScript
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.CAIRVA_API_KEY,
  baseURL: "https://cairva.xyz/v1",
});

const completion = await client.chat.completions.create({
  model: "gemma4:e2b",
  messages: [
    { role: "system", content: "Answer in one sentence." },
    { role: "user", content: "What does a metered allowance include?" },
  ],
  max_tokens: 200,
});

console.log(completion.choices[0].message.content);
console.log(completion.cairva);
```

```python Python
import os
from openai import OpenAI

client = OpenAI(api_key=os.environ["CAIRVA_API_KEY"], base_url="https://cairva.xyz/v1")

completion = client.chat.completions.create(
    model="gemma4:e2b",
    messages=[
        {"role": "system", "content": "Answer in one sentence."},
        {"role": "user", "content": "What does a metered allowance include?"},
    ],
    max_tokens=200,
)

print(completion.choices[0].message.content)
print(completion.model_extra["cairva"])
```

## Request body

| Field | Type | Notes |
| --- | --- | --- |
| `model` | string, required | Must equal the model id your allocation covers. Get it from [`GET /v1/models`](https://cairva.xyz/docs/models) |
| `messages` | array, required | 1 to 200 messages. `role` is `system`, `user` or `assistant`. `content` is a string or an array of `{"type": "text", "text": "..."}` parts, which are joined without a separator |
| `max_tokens` | integer | 1 to 32,768. Defaults to 1,024. Lowered to your remaining output allowance if that's smaller |
| `max_completion_tokens` | integer | Same limits. Wins over `max_tokens` when both are set |
| `temperature` | number | 0 to 2. Passed to the provider |
| `top_p` | number | 0 to 1. Passed to the provider |
| `stop` | string or array | Up to 4 stop sequences |
| `stream` | boolean | Stream server-sent events. See [Streaming](https://cairva.xyz/docs/streaming) |
| `user` | string | Up to 200 characters. Accepted for SDK compatibility, not forwarded or stored |

Any other field is dropped before the request reaches the provider. That includes `tools`, `n`, `response_format`, `logprobs` and `seed`. A request with an image part, or a `tool`, `function` or `developer` role, fails with `400 validation_failed`. Details in [OpenAI compatibility](https://cairva.xyz/docs/compatibility).

## Headers

| Header | Direction | Notes |
| --- | --- | --- |
| `Authorization` | Request | `Bearer` and your key |
| `x-request-id` | Request, optional | Your own identifier, truncated to 120 characters. Stored as `client_request_id` and returned in [usage events](https://cairva.xyz/docs/usage), so you can join Cairva's meter to your logs |
| `x-cairva-request-id` | Response | Cairva's id for the request. Sent on non-streaming responses |

## Response

```json
{
  "id": "chatcmpl-7d0c2f4e-6a51-4c43-9f0e-2b8f5a1c9e30",
  "object": "chat.completion",
  "created": 1793520000,
  "model": "gemma4:e2b",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "..." },
      "finish_reason": "stop"
    }
  ],
  "usage": { "prompt_tokens": 31, "completion_tokens": 18, "total_tokens": 49 },
  "cairva": {
    "request_id": "7d0c2f4e-6a51-4c43-9f0e-2b8f5a1c9e30",
    "allocation_id": "c1a9e1d2-0b7f-4f9e-a3c4-58e2d6b0f114",
    "service_mode": "sandbox",
    "usage_source": "provider_reported",
    "remaining": { "input": 999969, "output": 499982 }
  }
}
```

There's always exactly one choice. `finish_reason` is whatever the provider returned, usually `stop` or `length`.

## The cairva object

| Field | Meaning |
| --- | --- |
| `request_id` | Cairva's id for this request. The response `id` is `chatcmpl-` followed by it |
| `allocation_id` | The allocation that was charged, as a UUID |
| `service_mode` | `live` or `sandbox` |
| `usage_source` | `provider_reported` when the provider returned token counts, `sandbox_estimate` on a stand-in sandbox route that calls no model, or `pending` when no counts came back |
| `remaining` | Input and output tokens left after this request, net of other requests still running |

When `usage_source` is `pending`, `usage` is `null`. Cairva never fills in an estimate. The request's hold stays in place until Cairva operations reconcile it from provider metering. See [Rate limits and holds](https://cairva.xyz/docs/rate-limits).

## Output tokens you don't see

Some models reason before they answer, and that reasoning counts as output tokens. The gateway returns only the message `content`, so reasoning isn't included in the response. Ollama, which serves the sandbox route, doesn't return reasoning as content either.

If `max_tokens` runs out while the model is still reasoning, you can get an empty or very short `content` with `finish_reason: "length"`. The provider still reports the output tokens it generated, and they're metered. Give reasoning models enough `max_tokens` for both the reasoning and the answer.

> [!TIP]
> If you see empty answers with `finish_reason: "length"`, raise `max_tokens` before anything else.

## Errors

Admission errors such as `model_not_permitted`, `allocation_exhausted` and `rate_limited` come back as an HTTP error before the provider is called. If the provider fails, you get `502 upstream_error`, the hold is released, and nothing is charged. All codes are in [Errors](https://cairva.xyz/docs/errors).
