
## Add a model manually

`POST /v1/ai/credentials/:id/models`

Connects a model to a key from any provider. Supply one `modelId` or a batch using `modelIds` (1 to 100 strings). For keys of all providers except the Custom provider, metadata priority is: request body → [live list](./available-models.md) → defaults. Manual connection by model ID works if the list is unavailable. Single requests to the Custom provider keep their existing contract: any string up to 200 characters that is nonblank after trimming surrounding whitespace, with body metadata taking priority.

Requires the `vibe:ai` scope. The public API accepts only your own USER key. A key owned by another user, a missing key or a PORTAL key returns `404 not_found`.

## Parameters

| Parameter | Type | Required | Description |
|----------|-----|:-----:|----------|
| `id` (path) | string | yes | Key ID from [`GET /v1/ai/credentials`](./list.md) |

## Request fields (body)

| Field | Type | Required | Default | Description |
|------|-----|:-----:|-----------|----------|
| `modelId` | string | single only | — | The model ID at the provider without the `<slug>/` prefix. The response adds the prefix; use the returned `modelId` as the `model` in requests to [`/v1/chat/completions`](/docs/ai/chat/completions) |
| `modelIds` | string[] | batch only | — | 1 to 100 provider IDs; cannot be combined with `modelId` |
| `name` | string | no | value of `modelId` | Display name of the model. `null` uses the default value |
| `contextLength` | integer | no | `4096` | Maximum context size in tokens. An integer from `-2147483648` to `2147483647`; `null` uses the default value |
| `maxOutputTokens` | integer | no | `null` | Maximum tokens in the response. An integer from `-2147483648` to `2147483647`, or `null` |

## Examples

### curl — personal key

```bash
curl -X POST https://vibecode.bitrix24.com/v1/ai/credentials/cred_custom_xyz/models \
  -H "X-Api-Key: YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "modelId": "abab6.5-chat",
    "name": "MiniMax abab6.5-chat",
    "contextLength": 32768,
    "maxOutputTokens": 8192
  }'
```

### curl — OAuth application

```bash
curl -X POST https://vibecode.bitrix24.com/v1/ai/credentials/cred_custom_xyz/models \
  -H "X-Api-Key: YOUR_APP_KEY" \
  -H "Authorization: Bearer USER_SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "modelId": "abab6.5-chat",
    "name": "MiniMax abab6.5-chat",
    "contextLength": 32768,
    "maxOutputTokens": 8192
  }'
```

### JavaScript — personal key

```javascript
const id = 'cred_custom_xyz'
const res = await fetch(`https://vibecode.bitrix24.com/v1/ai/credentials/${id}/models`, {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_API_KEY',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    modelId: 'abab6.5-chat',
    name: 'MiniMax abab6.5-chat',
    contextLength: 32768,
    maxOutputTokens: 8192,
  }),
})

const { data } = await res.json()
console.log('Registered model with id:', data.id)
```

### JavaScript — OAuth application

```javascript
const id = 'cred_custom_xyz'
const res = await fetch(`https://vibecode.bitrix24.com/v1/ai/credentials/${id}/models`, {
  method: 'POST',
  headers: {
    'X-Api-Key': 'YOUR_APP_KEY',
    'Authorization': 'Bearer USER_SESSION_TOKEN',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({
    modelId: 'abab6.5-chat',
    name: 'MiniMax abab6.5-chat',
    contextLength: 32768,
    maxOutputTokens: 8192,
  }),
})

const { data } = await res.json()
console.log('Registered model with id:', data.id)
```

## Response fields

| Field | Type | Description |
|------|-----|----------|
| `success` | boolean | Always `true` on success |
| `data.id` | string | Unique ID of the `AiModel` record (needed for [deletion](./models-delete.md)) |
| `data.modelId` | string | The registered `modelId` with the provider `<slug>/` prefix, for example `custom-openai-compat/` |
| `data.name` | string | Model name |
| `data.contextLength` | integer | The saved context size |
| `data.created[]` | array | Batch `modelIds` request only, response `200`: connected models, each `{ modelId, rowId }` |
| `data.skipped[]` | array | Batch `modelIds` request only, response `200`: skipped models, each `{ modelId, reason }`, where `reason` is `already_connected` |

## Response example

Single `modelId` request, HTTP status `201 Created`:

```json
{
  "success": true,
  "data": {
    "id": "aimodel_xyz789",
    "modelId": "custom-openai-compat/abab6.5-chat",
    "name": "MiniMax abab6.5-chat",
    "contextLength": 32768
  }
}
```

Batch request `{ "modelIds": ["gpt-4o-mini", "gpt-4o"] }`, HTTP status `200 OK`. The `gpt-4o` model was already connected to the key:

```json
{
  "success": true,
  "data": {
    "created": [
      { "modelId": "gpt-4o-mini", "rowId": "aimodel_xyz789" }
    ],
    "skipped": [
      { "modelId": "gpt-4o", "reason": "already_connected" }
    ]
  }
}
```

## Error response example

`404 not_found` — the key was not found or belongs to another user:

```json
{
  "success": false,
  "error": {
    "code": "not_found",
    "message": "Credential not found"
  }
}
```

## Errors

| HTTP | Code | Description |
|------|-----|----------|
| 400 | `invalid_request` | `modelId` is missing, is not a string, is empty after `trim`, or the original string is longer than 200 characters |
| 400 | `invalid_request` | `name` is provided and is neither a string nor `null`; the message identifies the `name` field |
| 400 | `invalid_request` | `contextLength` or `maxOutputTokens` is provided and is neither an integer from `-2147483648` to `2147483647` nor `null`; the message identifies the invalid field |
| 400 | `invalid_request` | Both `modelId` and `modelIds` are provided |
| 400 | `invalid_request` | `modelIds` is not an array, is empty, or contains more than 100 IDs |
| 400 | `invalid_request` | An ID for a provider other than the Custom provider contains characters outside `[A-Za-z0-9._:/@-]` |
| 400 | `no_key` | The stored key has no API key value |
| 400 | `base_url_redirect` | The provider answered the model list request with a forbidden redirect |
| 403 | `scope_missing` | The API key lacks the `vibe:ai` scope |
| 404 | `not_found` | A key with this `id` was not found or does not belong to you |
| 409 | `key_models_limit` | The key would exceed 1000 model rows. Nothing from the batch is saved |
| 409 | `already_exists` | A model with the same `modelId` is already registered for this key |
| 429 | `model_discovery_busy` | Another model list request is already running |
| 429 | `model_discovery_rate_limit` | The shared limit of 10 model list requests per minute is reached |
| 500 | `decryption_error` | The stored credential cannot be decrypted |

For the full list of common API errors, see [Errors](/docs/errors).

Model registration limit: 30 requests per minute. The current limit is returned in the `x-ratelimit-limit` header.

## Known specifics

**Price is zero.** Key models are always registered with a zero price (`inputPrice = 0`, `outputPrice = 0`). The platform does not bill such records — you pay the provider directly.

**Batch connection.** Supply `{ "modelIds": ["gpt-4o-mini", "gpt-4o"] }` without `modelId`. The entire batch is validated before writing. IDs for providers other than the Custom provider use only `[A-Za-z0-9._:/@-]`, up to 200 characters. One snapshot of the list is used for the whole batch. Response `200`: `created` contains `{ modelId, rowId }`, `skipped` contains `{ modelId, reason: "already_connected" }`. In a batch response, `modelId` is the raw provider ID. Duplicates and concurrent requests for the same model appear in `skipped`.

A key holds at most 1000 rows. Exceeding this returns `409 key_models_limit` without partial batch writes. A single duplicate returns `409 already_exists`, and a successful single request still returns `201`. A new row inherits `capabilities` and `reasoningControl` from the platform model with the same ID. If there is no such row, capabilities from the provider list are used.

**Model list request limits.** They are shared by `available-models`, `fetch-models` and connection: 20 seconds including all retries and pages, up to 5 MiB per response before JSON parsing, and at most 1000 models and 10 pages. A repeated cursor stops pagination. Only one such request runs per account at a time, and at most 10 run per minute. Incomplete lists never change absent models to `DEPRECATED`.

## See also

- [Preview available models](./available-models.md)
- [Fetch the model catalog](./fetch-models.md)
- [Key model list](./models-list.md)
- [Delete a key model](./models-delete.md)
- [Create a chat completion](/docs/ai/chat/completions)
