
## Create a chat completion

> **The response arrives in raw OpenAI format.**
>
> The `{success, data}` wrapper used by the other Vibecode endpoints — `/v1/deals`, `/v1/tasks` and others — is absent here.
>
> This is done for compatibility with the OpenAI SDK. If you have a single client that checks `if (!response.success)`, add an exception for the AI Router.

`POST /v1/chat/completions`

Generates an AI model's response to an array of messages. The request and response format is compatible with `POST /v1/chat/completions` from the OpenAI API.

The endpoint's capabilities are documented on separate pages: [streaming](./streaming.md), [guaranteed JSON response](./json.md), [function calling](./tools.md), [image analysis](./vision.md) and [rate limits](./rate-limits.md).

## Request fields (body)

| Field | Type | Required | Default | Description |
|------|-----|:-----:|-----------|----------|
| `model` | string | no | `auto` | Model ID or alias. See the "Model aliases" section below. If the field is omitted or set to `auto`, the request runs on the Bitrix24 account's default model. The list of available models — [`GET /v1/models`](/docs/ai/models/list) |
| `messages` | array | yes | — | Array of conversation messages. Minimum 1, maximum 256 |
| `messages[].role` | string | yes | — | Role: `system`, `user`, `assistant`, `tool` |
| `messages[].content` | string \| array \| null | yes | — | Message text. Maximum 500,000 characters in a single message or 64 elements in the `content` array. For requests with [images](./vision.md) — an array with `type: "text"` and `type: "image_url"`. An `assistant` message with `tool_calls` may have `null` content |
| `messages[].name` | string | no | — | Sender name (multi-agent scenarios) |
| `messages[].tool_calls` | array | no | — | Function calls from the assistant — present in the model's response when `finish_reason: "tool_calls"` |
| `messages[].reasoning_content` | string \| null | no | — | The model's reasoning from an earlier response, sent back into the conversation history. Honored only for the `assistant` role and ignored on messages with other roles. A `null` value is the same as omitting the field, and an empty string is accepted. A non-string value or one longer than 4,000,000 characters is rejected with `400 invalid_request`. The field reaches the model when its `owned_by` in [`GET /v1/models`](/docs/ai/models/list) is `bitrix`, `openrouter` or `vibecode`, and it is not sent to models from OpenAI, Anthropic or other OpenAI-compatible providers. A model that reads the reasoning counts it as input tokens. See [function calling](./tools.md) for how it is used |
| `messages[].tool_call_id` | string | no | — | Function call ID — required in a message with `role: "tool"` |
| `temperature` | number | no | per model | Generation temperature, range `0..2`. Lower values make the output more precise and deterministic; higher values make it more creative |
| `max_tokens` | number | no | per model | Maximum tokens in the response |
| `top_p` | number | no | — | Nucleus sampling, range `0..1` |
| `reasoning_effort` | string | no | per model | OpenAI-compatible reasoning level: `none`, `minimal`, `low`, `medium`, `high`, `xhigh`, `max`. Normalized to a platform step — see [Reasoning control](#reasoning-control) |
| `reasoning` | object | no | — | The canonical object. `effort` — a level from the same vocabulary. `max_tokens` — reasoning token budget, honored only on models with `budgetTokens: true`. `enabled` — `false` disables reasoning. `exclude` — `true` removes `reasoning_content` from the response; reasoning tokens are still billed. Takes precedence over `reasoning_effort` |
| `chat_template_kwargs` | object | no | — | Vendor format `{"thinking": boolean, "reasoning_effort": string}`. Accepted for compatibility, resolved to the same step and never forwarded to the model raw. Unknown keys inside are ignored. Lowest precedence of the three fields |
| `stop` | string \| array | no | — | Stop sequences (up to 4, each up to 64 characters) |
| `stream` | boolean | no | `false` | If `true`, the response arrives as a [stream](./streaming.md) of `Server-Sent Events` |
| `response_format` | object | no | — | Controls the [response format](./json.md): `{"type": "text"}` — the default, `{"type": "json_object"}` — valid JSON, `{"type": "json_schema", "json_schema": {...}}` — strict JSON Schema, requires model support |
| `repetition_guard` | string | no | `"off"` | `"off"` or `"abort"`. Explicit early refusal on repeated JSON. Requires `json_object`/`json_schema`, no declared tools and a streaming-capable model even with `stream: false`. Restrictions, false-positive risk and SDK example: [JSON response](./json.md) |
| `tools` | array | no | — | Definitions of [functions](./tools.md) the model may call |
| `tool_choice` | string \| object | no | — | `auto` (the model decides on its own), `none` (forbid calls) or `{"type": "function", "function": {"name": "..."}}` (force a specific one) |

## Reasoning control

Reasoning models think before they answer. Reasoning tokens count towards `usage.completion_tokens` and **are billed as output tokens at the same rate** — the price lever is volume, not the rate. Vibecode describes that volume with five steps: `none` — answer immediately, `low` — a brief sketch, `medium` — the model's standard reasoning when it is enabled and no level is given, `high` — stronger than standard, `max` — everything the model can do.

The `reasoning` field in [`GET /v1/models`](/docs/ai/models/list) shows what a particular model supports. Its `map` maps the steps onto the model's native modes; two steps sharing one native value are a declared collapse. `default` is the default step, and `budgetTokens` indicates support for a reasoning token budget. On a model with `reasoning: null`, control is unavailable: the parameter is ignored with the `REASONING_NOT_SUPPORTED` warning, and nothing is added to the model call.

Input normalization: `minimal` → `low`, `xhigh` → `max`. `reasoning.enabled: true` or `chat_template_kwargs.thinking: true` without a level gives `medium`. `reasoning.max_tokens` without `effort` gives `medium` with a budget. An empty `reasoning: {}` or `chat_template_kwargs: {}` is the same as no parameter. `null` in any of the three fields is equivalent to omitting it. When several fields arrive, the highest-precedence one wins: `reasoning` first, then `reasoning_effort`, then `chat_template_kwargs`. A value outside the vocabulary, a non-positive `reasoning.max_tokens` or a non-object `chat_template_kwargs` is rejected with `400 invalid_request`.

If the model lacks the requested step, the nearest step **below** among the reasoning steps is applied, and `warnings` carries `REASONING_EFFORT_ADJUSTED` with the `requested`, `applied` and `direction` fields. The `none` step is never chosen by rounding — reasoning can only be disabled explicitly. If there is no step below, the nearest step above is taken. Asking for `none` on a model where reasoning cannot be disabled gives the lowest step and `REASONING_CANNOT_BE_DISABLED`. A budget on a model without `budgetTokens` is ignored with `REASONING_BUDGET_NOT_SUPPORTED`.

**When the request omits the parameter, the platform explicitly sends the `default` step to a model with a reasoning declaration.** The mode is not left to the model provider, so a model update on the provider's side does not change the behavior of requests without the parameter. The response's `reasoning` field reports that step.

The response carries the `reasoning` field with `requested`, `applied` and `native`, plus warnings in `warnings`. In streaming mode the body carries no warnings — the state travels in the `X-Reasoning-Applied`, `X-Reasoning-Native` and `X-Reasoning-Warnings` headers, the last one carrying comma-separated codes. On a synchronous response the same headers duplicate the body. A header is absent when there is nothing to report. When the model is substituted — a DISABLED redirect or a fallback to the backup model — the step is resolved against the model that ran the request, and the `reasoning` field and headers describe that model. `reasoning.exclude` is honored on every model, including ones without a reasoning declaration.

## Model aliases

The `model` field accepts three values that resolve to the Bitrix24 account's default model (currently `bitrix/bitrixgpt-5.5`): `auto`, `bitrix/free` and an empty string.

The models `bitrix/bitrixgpt-5` and `bitrix/bitrixgpt-5-vl` are marked as deprecated and work until July 31, 2026. After that date, requests to them are transparently redirected to `bitrix/bitrixgpt-5.5` with the `X-Model-Replacement` header — see [model lifecycle](/docs/ai/models/lifecycle) for details.

In addition, `model` accepts a model identifier without the provider prefix: `bitrixgpt-5.5` resolves to `bitrix/bitrixgpt-5.5`. The short name must be an exact, case-sensitive match for an identifier from [`GET /v1/models`](/docs/ai/models/list) with its first segment dropped. There is no partial matching: a request with `bitrixgpt` gets `404 ai_model_not_found`, not the first model whose identifier contains that word. A request with a short name that matches several models available to your key also gets `404` — pass the full identifier. A model found by its short name is flagged in the response with the `X-Model-Resolved` header carrying its full identifier.

## Examples

### curl — personal key

```bash
curl -X POST https://vibecode.bitrix24.com/v1/chat/completions \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "bitrix/bitrixgpt-5.5",
    "messages": [
      {"role": "system", "content": "You are a sales expert. Classify leads by quality."},
      {"role": "user", "content": "Acme LLC, 50 users, budget 500,000 per month."}
    ],
    "temperature": 0.3,
    "max_tokens": 300
  }'
```

### curl — OAuth application

```bash
curl -X POST https://vibecode.bitrix24.com/v1/chat/completions \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "bitrix/bitrixgpt-5.5",
    "messages": [
      {"role": "system", "content": "You are a sales expert. Classify leads by quality."},
      {"role": "user", "content": "Acme LLC, 50 users, budget 500,000 per month."}
    ],
    "temperature": 0.3,
    "max_tokens": 300
  }'
```

### JavaScript — personal key

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/chat/completions', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    model: 'bitrix/bitrixgpt-5.5',
    messages: [
      { role: 'system', content: 'You are a sales expert. Classify leads by quality.' },
      { role: 'user', content: 'Acme LLC, 50 users, budget 500,000 per month.' },
    ],
    temperature: 0.3,
    max_tokens: 300,
  }),
})

const data = await res.json()
console.log(data.choices[0].message.content)
console.log('Tokens:', data.usage.total_tokens)
```

### JavaScript — OAuth application

```javascript
const res = await fetch('https://vibecode.bitrix24.com/v1/chat/completions', {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    model: 'bitrix/bitrixgpt-5.5',
    messages: [
      { role: 'system', content: 'You are a sales expert. Classify leads by quality.' },
      { role: 'user', content: 'Acme LLC, 50 users, budget 500,000 per month.' },
    ],
    temperature: 0.3,
    max_tokens: 300,
  }),
})

const data = await res.json()
console.log(data.choices[0].message.content)
```

## Response fields

| Field | Type | Description |
|------|-----|----------|
| `id` | string | Unique completion ID for tracking |
| `object` | string | Always `chat.completion` for a synchronous response |
| `created` | number | Unix timestamp of when the completion was created |
| `model` | string | The model actually used. It differs from the request's `model` when the model was found by its short name, on automatic fallback to a backup model and on a DISABLED redirect |
| `choices` | array | The model's response choices. Without the `n` parameter the array has a single element |
| `choices[].index` | number | Index of the choice |
| `choices[].finish_reason` | string | Reason the generation finished: `stop`, `length`, `tool_calls`, `content_filter` |
| `choices[].message` | object | The generated message |
| `choices[].message.role` | string | Always `assistant` |
| `choices[].message.content` | string \| null | Response text. `null` when `finish_reason: "tool_calls"` — the content is in `tool_calls` |
| `choices[].message.tool_calls` | array | List of function calls (if the model decided to call them) |
| `warnings` | array | Warnings about what the platform changed in the request or what to account for in the response. Every element carries at least `code` and `message`. Some warnings add more fields. Known codes: `MAX_TOKENS_RAISED`, `COWORK_QUOTA_FALLBACK` (also carries `tier`, `nextTier`, `resetAt`), `THINKING_TRUNCATED`, `TOOL_CALL_MISSING`, `REASONING_EFFORT_ADJUSTED` (carries `requested`, `applied`, `direction`), `REASONING_NOT_SUPPORTED`, `REASONING_CANNOT_BE_DISABLED`, `REASONING_BUDGET_NOT_SUPPORTED`, `TEMPERATURE_OVERRIDDEN_BY_REASONING`. The field is absent when there are no warnings |
| `reasoning` | object | Present on models with a reasoning declaration. `requested` — the normalized step from the request, `null` when no parameter was sent. `applied` — the step actually applied. `native` — the model's native mode for that step. See [Reasoning control](#reasoning-control) |
| `usage.prompt_tokens` | number | Input tokens |
| `usage.completion_tokens` | number | Response tokens |
| `usage.total_tokens` | number | Sum of tokens in the request and response |

If a request with `tools` ends without a usable answer or function call, `content` may gain an English explanation and `warnings` may carry `{ code: "TOOL_CALL_MISSING", message: "..." }` with the same text. Any existing text is kept, followed by a blank line and the explanation. HTTP 200, `finish_reason: "stop"` and `usage` are preserved; the explanation does not affect billing. It is not added for `tool_choice: "none"`, structured output or reasoning-only turns ending with `stop`. In streams it is a separate `delta.content` event before the finish, without a `warnings` array.

## Response example

```json
{
  "id": "chatcmpl-a1a73c6eb3f180fd",
  "object": "chat.completion",
  "created": 1777289339,
  "model": "bitrix/bitrixgpt-5.5",
  "choices": [
    {
      "index": 0,
      "finish_reason": "stop",
      "message": {
        "role": "assistant",
        "content": "Quality: HIGH\n\nRationale:\n- Legal entity (LLC) — B2B client\n- Budget 500,000 per month — above average\n- Specific volume (50 users) — a deliberate need\n\nRecommendation: schedule a call within 24 hours."
      }
    }
  ],
  "usage": {
    "prompt_tokens": 42,
    "completion_tokens": 85,
    "total_tokens": 127
  }
}
```

## Error response example

`404 ai_model_not_found` — model not found or the key has no access to it:

```json
{
  "error": {
    "message": "Model \"anthropic/claude-imaginary-x\" not found or disabled.",
    "type": "invalid_request_error",
    "code": "ai_model_not_found"
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `invalid_request` | Empty `messages` array, invalid role, schema violation or invalid `repetition_guard`. An incompatible `"abort"` returns `param: "repetition_guard"` before any reservation or model call |
| 400 | `invalid_image_payload` | Invalid `image_url` — the error message contains the `content` element index and the reason. See [image analysis](./vision.md) |
| 400 | `no_default_model` | The portal has no models available to call |
| 402 | `ai_credentials_not_configured` | No provider credentials for the model — connect [BYOK](/docs/ai/credentials/create) |
| 402 | `insufficient_balance` | Insufficient funds for a paid model |
| 402 | `account_frozen` | The call is blocked because the billing account is frozen due to a negative balance. Top up that account; the response message explains why this call was blocked. Calls covered by the plan's AI quota can keep working; see the [account freeze scope](/docs/errors) |
| 402 | `ai_quota_exhausted` | The portal's monthly [AI quota](/docs/ai/consumption/quota) is exhausted. The `reason` field clarifies the cause |
| 402 | `cowork_subscription_inactive` | A key carrying the `vibe:cowork` scope has no active Cowork subscription. Resume the subscription in the Cowork section of your Vibecode account; the key itself stays valid |
| 402 | `cowork_quota_exhausted` | One of the Cowork subscription quota windows is exhausted. The body carries `window` (`5h`/`week`/`month`), `resetAt` and `nextTier`; the `Retry-After` header holds the number of seconds until the window resets |
| 402 | `company_budget_exhausted` | The monthly company spend budget set by the portal administrator is exhausted. The `scope` field names the budget that was hit: `USER` — the caller's own budget, `PORTAL` — the budget of the whole portal. The `canRequest` field indicates whether an increase can be requested: `true` for a personal budget, `false` for the portal one, which only an administrator raises. The rejection arrives only on calls that draw on the portal balance. Calls inside the plan quota, on a Cowork subscription and with your own provider key (BYOK) keep working |
| 403 | `scope_missing` | The API key is missing the `vibe:ai` scope. For a key issued to a third-party application through [Partner Connect](/docs/partner-connect), the scope is added by requesting it on the consent page rather than by re-issuing the key — see [Platform scopes](/docs/partner-connect#platform-scopes) |
| 403 | `CONNECT_AI_NOT_PERMITTED` | The key belongs to a third-party application from [Partner Connect](/docs/partner-connect), and the account administrator has not allowed AI for the application or has revoked the permission. Returned when the platform has enabled AI approval by account administrators. The body is in the general API envelope `{ success, error: { code, message } }`, not in the OpenAI format |
| 404 | `ai_model_not_found` | Model not found or disabled |
| 404 | `ai_model_not_found` | The model's short name matches several available models. Pass the full identifier |
| 409 | `agent_turn_awaiting_confirmation` | The [AI agent](/docs/agents) is waiting for the user to confirm or cancel an operation. Until the user answers, its key's requests to this endpoint are refused in any conversation. The request is not sent to the model and nothing is charged. With `stream: true` the refusal arrives as a JSON response. The wait ends when the user answers, or expires on its own: the agent sets the limit, at most 30 minutes. Requests made with other keys are not affected |
| 409 | `agent_turn_loop_detected` | The agent turn is stuck in a loop: after the last message with the `user` role, `messages` holds 20 or more `assistant` replies without `tool_calls`. All such replies count, not only consecutive ones; replies with `tool_calls` do not count. The request is not sent to the model and nothing is charged. With `stream: true` the refusal arrives as a JSON response and no event stream is opened. To continue, add a new message with the `user` role to `messages` — the count starts over |
| 413 | `ai_payload_too_large` | The request body for a `bitrix/*` model exceeds the model cluster limit, and a body with images does so even after they are downscaled. The request is not sent to the model and nothing is charged. The message states the request size and the limit in effect. The same code arrives when the model cluster itself, or the provider of a model on your own key, refused the request because of its size: the body then carries `providerStatusCode: 413`. Downscale the images — [image analysis](./vision.md) — or shorten the message history |
| 422 | `structured_output_truncated` | A request with `response_format` did not produce a complete JSON document: generation was cut off with `finish_reason: "length"` without detected repetition, or the `content` field is empty and there are no `tool_calls`, whatever the finish reason. The `json_object` mode has an exception — a complete JSON document that landed in the internal `reasoning_content` channel is recovered and the response stays `200`. See [guaranteed JSON response](./json.md) |
| 422 | `structured_output_invalid_json` | A request with `response_format` produced a non-empty response that does not parse as a JSON object or array — prose around the object, an answer wrapped in a markdown code fence, a bare value such as `42`. The body carries no `param` and no `suggestedMaxTokens`: raising `max_tokens` does not change the result. The `json_object` mode has the same exception — a complete JSON document that landed in the internal `reasoning_content` channel is recovered and the response stays `200`. See [guaranteed JSON response](./json.md) |
| 422 | `reasoning_repetition` | The model got stuck in its reasoning: it kept repeating the same sentences and never started the answer. In a synchronous response it replaces `structured_output_truncated` on requests with `response_format`; in a stream it arrives as an event before `data: [DONE]` on any request. Send the request again with `reasoning_effort: "none"`; a larger `max_tokens` will not help. See [guaranteed JSON response](./json.md) |
| 422 | `reasoning_repetition` | By default (`repetition_guard` omitted or `"off"`), repetition was detected in non-empty incomplete JSON with `json_object` or `json_schema` after `finish_reason: "length"`. The check is terminal. This is a heuristic diagnosis: it can also trigger on a legitimate repetitive array that exceeded the budget. Check whether repetition is expected; if so, check the token budget, otherwise revise the request or use another model. The response carries no hint to disable reasoning |
| 422 | `reasoning_repetition` | With an explicit `repetition_guard: "abort"`, repeated JSON was detected before completion. A synchronous request returns an ordinary JSON `422`; a stream returns one `error` event and one `data: [DONE]` with HTTP `200`. Discard the partial response. The response carries no `retryable`/`retryAfter` fields and no advice to disable reasoning. Conditions and fixed text: [JSON response](./json.md) |
| 429 | `rate_limit_exceeded` | The [rate limit](./rate-limits.md) is exceeded. The `X-RateLimit-Scope` header indicates the level — `per-key` or `per-user`. The same code arrives when the model cluster itself throttled the request: the body then carries the `providerStatusCode` field, there is no `scope` field or `X-RateLimit-Scope` header, and the pause comes from `Retry-After` |
| 429 | `ai_congested` | The AI cluster pool is overloaded. The request did not run and nothing was charged — retry it after the delay in the `Retry-After` header. The response carries the `X-AI-Admission: shed` header, not `X-RateLimit-Scope` |
| 429 | `ai_pacing_limited` | A daily or weekly quota pacing window is exceeded. This is not quota exhaustion — retry the request after the delay in the `Retry-After` header. See [Pacing (day/week smoothing)](/docs/ai/consumption/quota#pacing-day-week-smoothing) |
| 429 | `ai_provider_cooldown` | The model cluster is temporarily unavailable and the platform backs off so retries do not pile up on it. The request was not executed and nothing was charged — retry it after the number of seconds in `Retry-After`. This response carries neither `X-RateLimit-Scope` nor `X-AI-Admission` |
| 429 | `ai_image_resize_busy` | The platform is already downscaling many images at once, and the request body for a `bitrix/*` model does not fit the limit without downscaling. The request was not executed and nothing was charged — retry it after the number of seconds in `Retry-After`, or downscale the images yourself |
| 400 | `ai_provider_rejected` | The model rejected the request itself (for example, an unsupported parameter). Retrying it unchanged will not help. Returned when the provider responded with `400` or `422` |
| 502 | `ai_provider_unavailable` | The external provider answered with an error (`401`/`403`/`5xx`). The original status arrives in the `providerStatusCode` field whenever the provider answered with an HTTP status — including when the stream to the client is already open and the provider refused to establish the upstream one. The field is absent where there was no status: the error arrived as a frame from the body of an already open stream (the platform accepts only the `429` status from the body, and such a frame arrives as `rate_limit_exceeded`) or the failure happened while processing the provider's response |
| 502 | `ai_provider_network` | The platform could not connect to the provider, or the connection dropped before a response. This response carries no `providerStatusCode`: the provider never answered. Retry the request |
| 503 | `model_unavailable` | The model is disabled and no successor is assigned for it. See [model lifecycle](/docs/ai/models/lifecycle) |
| 503 | `pool_exhausted` | The platform is temporarily overloaded. Retry the request after the number of seconds in `Retry-After` — 3-7 seconds with random jitter |

The full list of common API errors — [Errors](/docs/errors).

## Known specifics

**Automatic fallback to a backup model on a provider failure.** If a paid model returns a `5xx` error or times out, the synchronous request is retried with the default model. The `X-Model-Fallback: <original modelId>` header appears in the response. In streaming mode there is no such fallback — the client receives an error in the last event before `data: [DONE]`.

**Exhausted Cowork quota: the response announces the limit instead of doing the work.** This is a separate state, unrelated to `X-Model-Fallback` above: that header marks a model substitution, whereas here the subscription quota is exhausted. When a quota window is exhausted and a backup model is configured on the platform, the request returns `200`, but no work is done for it: the `tools`, `tool_choice` and `response_format` fields are stripped, the request's own system messages have no effect, and the model states that the limit is reached and when it resets. The markers of this state are the `X-Cowork-Fallback: true` header and the `COWORK_QUOTA_FALLBACK` warning in the `warnings` array. In streaming mode there is no warning in the body; the state is visible from the header. Nothing is charged for such a response and no quota is spent. Do not expect `tool_calls` or schema-shaped JSON here: even with `response_format` in the request you get plain text. When no backup model is configured — or the configured one fails to answer — `402 cowork_quota_exhausted` arrives instead. The response code does not tell those two cases apart, so always handle `402` on this endpoint.

**The Bitrix24 account's monthly AI quota.** On Bitrix24 accounts with quota control enabled, a request may return `402 ai_quota_exhausted`. The `reason` field distinguishes three cases: `breaker` — the hourly over-quota spending breaker tripped, `wallet_empty` — the quota is exhausted and the account balance has no funds, `wallet_off` — over-quota spend is not available for the account. The `resetAt` field holds the moment when requests will pass again. For `wallet_off` it may be absent. In the `wallet_empty` case the response may additionally carry a `hint` string and a `topupUrl` link to top up the balance — both fields appear when enforced quota control and the top-up hint are enabled on the platform, so treat them as optional. The `hint` field in this response is a string. When the Bitrix24 account has no paid AI access, a `reason: "wallet_off"` response additionally carries `details.learnMoreUrl` — a page that describes the paid Bitrix24 plan that includes AI — and the `message` field names that plan. For other `wallet_off` causes `details.learnMoreUrl` is absent. While usage stays within the quota, the endpoint's behavior is unchanged. Beyond the quota, if such spend is allowed for the account, requests are charged to the account's money balance at the model's base catalog price.

**The processing budget for a synchronous request is about 850 seconds.** Once the budget runs out, `503 ai_provider_timeout` is returned. This response deliberately carries no `Retry-After` header: repeating the same request would hit the same budget. Reduce the request size or switch to [streaming](./streaming.md), where an idle timeout between events applies instead of a single budget for the whole call.

**Request body size limit.** For `bitrix/*` models, the limit is set by the model cluster and is lower for a body with images than for text. The message of a `413` response states the limit in effect. The platform downscales `data:` images in a body over the limit, and a body still over it gets `413 ai_payload_too_large`. For models called with your own provider key, the limit is 30 MiB, enough for a single image of up to 20 MiB. How the platform downscales images — [image analysis](./vision.md).

**Passing `content` as an array.** For text models, pass `content` as a string. An array with a single `text` element also works but is redundant. An array is required only for requests with images.

**Finishing a Cowork turn near the five-hour limit.** A successful subscription model step may include the `X-Cowork-Completion-Mode` header: `landing-finish` means the server asked an ongoing turn to finish before the window runs out; `reserve` means the step was admitted from the completion allowance after the window ran out. It is accompanied by `X-Cowork-Completion-Reset-At` with the window reset time in ISO format. The headers are available on JSON responses and at the start of an SSE stream. Their absence does not prove that a turn is complete; use the normal model response termination to determine that.

## See also

- [Streaming](./streaming.md)
- [Guaranteed JSON response](./json.md)
- [Function calling](./tools.md)
- [Image analysis](./vision.md)
- [Rate limits](./rate-limits.md)
- [Model lifecycle](/docs/ai/models/lifecycle)
- [List of models](/docs/ai/models/list)
- [AI Router](/docs/ai)
- [Assistant bot for deals and workflows](/docs/recipes/crm-assistant-bot)
