---
title: Chat Completions
description: POST /v1/chat/completions — el endpoint de chat compatible con OpenAI de Clipia AI Gateway. Respuestas con streaming y normales, parámetros, roles, herramientas y errores.
icon: MessageSquare
---

`POST /v1/chat/completions` es el endpoint principal de Clipia AI Gateway, compatible con OpenAI Chat Completions. La clave necesita el permiso `generate`, que tienen todas las claves, así que no necesita una clave aparte para el chat. Recibe un array `messages` y devuelve la respuesta del asistente en modo sin streaming o con streaming, con soporte para tool calling y salida estructurada mediante esquema JSON.

<Method name="POST" path="/v1/chat/completions" />

## Respuesta normal

<Tabs items={['Python', 'Node.js', 'cURL']}>
<Tab value="Python">

```python
import os
from openai import OpenAI

client = OpenAI(
    api_key=os.environ["CLIPIA_KEY"],
    base_url="https://api.clipia.ai/v1",
)

resp = client.chat.completions.create(
    model="claude-opus-5-5",
    messages=[
        {"role": "system", "content": "You are a concise assistant."},
        {"role": "user", "content": "Tell me three facts about Mars."},
    ],
    max_tokens=512,
)

print(resp.choices[0].message.content)
print(resp.usage)
```

</Tab>
<Tab value="Node.js">

```ts
import OpenAI from "openai";

const client = new OpenAI({
  apiKey: process.env.CLIPIA_KEY,
  baseURL: "https://api.clipia.ai/v1",
});

const resp = await client.chat.completions.create({
  model: "claude-opus-5-5",
  messages: [
    { role: "system", content: "You are a concise assistant." },
    { role: "user", content: "Tell me three facts about Mars." },
  ],
  max_tokens: 512,
});

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

</Tab>
<Tab value="cURL">

```bash
curl https://api.clipia.ai/v1/chat/completions \
  -H "Authorization: Bearer $CLIPIA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-opus-5-5",
    "messages": [
      { "role": "system", "content": "You are a concise assistant." },
      { "role": "user", "content": "Tell me three facts about Mars." }
    ],
    "max_tokens": 512
  }'
```

</Tab>
</Tabs>

**Respuesta `200`**

```json
{
  "id": "chatcmpl-3f9a1c7e2b41",
  "object": "chat.completion",
  "created": 1782300000,
  "model": "claude-opus-5-5",
  "provider": "Clipia",
  "choices": [
    {
      "index": 0,
      "message": { "role": "assistant", "content": "1. ...\n2. ...\n3. ..." },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 28,
    "completion_tokens": 64,
    "total_tokens": 92,
    "cost": 0.046
  }
}
```

`usage.cost` es el coste de la petición en **créditos**. El `id` de la respuesta tiene la forma `chatcmpl-…`, y `provider` siempre es `"Clipia"`. `finish_reason` se normaliza al enum de OpenAI: `stop`, `length`, `tool_calls`, `content_filter`.

## Respuesta con streaming

Pase `stream: true`: la respuesta llega de forma incremental como Server-Sent Events. Cada evento es un `chat.completion.chunk` con un delta; el stream termina con el literal `data: [DONE]`.

<Tabs items={['Python', 'Node.js', 'cURL']}>
<Tab value="Python">

```python
stream = client.chat.completions.create(
    model="claude-opus-5-5",
    messages=[{"role": "user", "content": "Write a haiku about the sea."}],
    stream=True,
)

for chunk in stream:
    delta = chunk.choices[0].delta
    if delta.content:
        print(delta.content, end="", flush=True)
```

</Tab>
<Tab value="Node.js">

```ts
const stream = await client.chat.completions.create({
  model: "claude-opus-5-5",
  messages: [{ role: "user", content: "Write a haiku about the sea." }],
  stream: true,
});

for await (const chunk of stream) {
  process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}
```

</Tab>
<Tab value="cURL">

```bash
curl -N https://api.clipia.ai/v1/chat/completions \
  -H "Authorization: Bearer $CLIPIA_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "claude-opus-5-5",
    "messages": [{ "role": "user", "content": "Write a haiku about the sea." }],
    "stream": true
  }'
```

</Tab>
</Tabs>

**Flujo de eventos**

```text
data: {"id":"chatcmpl-...","object":"chat.completion.chunk","created":1782300000,"model":"claude-opus-5-5","choices":[{"index":0,"delta":{"role":"assistant"},"finish_reason":null}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","model":"claude-opus-5-5","choices":[{"index":0,"delta":{"content":"Waves "},"finish_reason":null}]}

data: {"id":"chatcmpl-...","object":"chat.completion.chunk","model":"claude-opus-5-5","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}

data: [DONE]
```

<Callout type="info" title="Uso final en un stream">
Para recibir `usage` durante el streaming, pase `stream_options: { "include_usage": true }`. En ese caso se envía un chunk adicional con `usage` completo y `choices: []` vacío justo antes de `data: [DONE]`.
</Callout>

## Parámetros de la petición

<TypeTable
  type={{
    model: { type: 'string', description: 'Identificador del modelo, p. ej. claude-opus-5-5 (consulte el catálogo GET /v1/models). Obligatorio.' },
    messages: { type: 'object[]', description: 'Historial de la conversación, no vacío. content es una cadena o un array de partes. Consulte los roles más abajo.' },
    temperature: { type: 'number', description: 'Aleatoriedad, normalmente 0–2. Por defecto, el valor predeterminado del modelo.' },
    top_p: { type: 'number', description: 'Nucleus sampling, 0–1. Una alternativa a temperature.' },
    max_completion_tokens: { type: 'integer', description: 'Máximo de tokens en la respuesta. Prevalece si se envían ambos límites.' },
    max_tokens: { type: 'integer', description: 'El nombre antiguo del mismo límite.' },
    stream: { type: 'boolean', description: 'true — respuesta con streaming (SSE). Por defecto, false.' },
    stream_options: { type: 'object', description: '{ include_usage: true } — añade al stream un chunk final con usage.' },
    stop: { type: 'string | string[]', description: 'Secuencias de parada.' },
    seed: { type: 'integer', description: 'Solicita determinismo cuando el modelo lo admite.' },
    presence_penalty: { type: 'number', description: 'Penalización por repetir temas, −2…2.' },
    frequency_penalty: { type: 'number', description: 'Penalización por repetir tokens, −2…2.' },
    reasoning_effort: { type: 'string', description: 'Profundidad del razonamiento: minimal, none y low equivalen a fast; medium, a balanced; high y xhigh, a deep. También puede enviarse como reasoning: { effort }.' },
    response_format: { type: 'object', description: '{ type: "json_object" } o { type: "json_schema", json_schema: {...} } — salida estructurada.' },
    tools: { type: 'object[]', description: 'Descripciones de las funciones que el modelo puede llamar.' },
    tool_choice: { type: 'string | object', description: 'auto | none | required | { type: "function", function: { name } }.' },
    user: { type: 'string', description: 'Identificador estable del usuario final.' },
  }}
/>

<Callout type="info" title="Compatibilidad de parámetros">
Los campos `n`, `parallel_tool_calls`, `logprobs`, `top_logprobs`, `logit_bias`, `service_tier` y `metadata` se aceptan, pero no afectan a la respuesta: siempre recibe una sola opción. Los campos desconocidos de nivel superior tampoco provocan un `400`, de modo que el código escrito para OpenAI funciona sin cambios.

La compatibilidad de los parámetros depende del modelo: consulte `supported_parameters` en `GET /v1/models`. Los modelos actuales pueden ignorar `temperature`, `top_p` y las penalizaciones por repetición. DeepSeek V4.1 Flash no llama a herramientas. Para una salida estricta por esquema JSON, elija un modelo con `response_format`, como GPT-6.1 Sol. Claude 5.5 sigue las instrucciones de salida estructurada sin garantizar una conformidad estricta con el esquema.
</Callout>

## Roles de los mensajes [#roles]

| Rol | Cómo se trata |
| --- | --- |
| `system` | instrucción de sistema |
| `developer` | se trata como `system` |
| `user` | mensaje del usuario |
| `assistant` | una respuesta anterior del modelo, incluida una con `tool_calls` |
| `tool` | el resultado de una llamada a herramienta; requiere `tool_call_id` |
| cualquier otro | se trata como `user` |

Un array `messages` vacío, un mensaje sin `role` de tipo cadena o una petición sin `model` devuelven `400 invalid_request_error`.

<Callout type="info" title="Reintentos de peticiones">
Este endpoint no admite la cabecera `Idempotency-Key`: un reintento tras un fallo de red ejecuta y cobra la petición de nuevo. El chat está sujeto a los [límites de frecuencia y de concurrencia](/es/docs/api-reference/rate-limits) comunes.
</Callout>

## Llamadas a herramientas [#tool-calling]

Ciclo completo: usted describe las herramientas en `tools`, el modelo devuelve `tool_calls`, usted ejecuta la función por su parte y devuelve el resultado como un mensaje con `role: "tool"`, tras lo cual el modelo genera la respuesta final.

```python
import json

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Current weather in a city",
            "parameters": {
                "type": "object",
                "properties": {
                    "city": {"type": "string", "description": "City name"},
                },
                "required": ["city"],
            },
        },
    }
]

messages = [{"role": "user", "content": "What's the weather in Moscow right now?"}]

# 1) The model decides to call a tool
resp = client.chat.completions.create(
    model="claude-opus-5-5", messages=messages, tools=tools,
)
msg = resp.choices[0].message
# resp.choices[0].finish_reason == "tool_calls"

# 2) Run the function on your side
call = msg.tool_calls[0]
args = json.loads(call.function.arguments)   # {"city": "Moscow"}
result = {"temp_c": 14, "condition": "cloudy"}

# 3) Send the result back and get the final answer
messages.append(msg)  # assistant echo with tool_calls
messages.append({
    "role": "tool",
    "tool_call_id": call.id,
    "content": json.dumps(result),
})

final = client.chat.completions.create(
    model="claude-opus-5-5", messages=messages, tools=tools,
)
print(final.choices[0].message.content)
```

Con streaming, los fragmentos de `tool_calls` llegan agrupados por `index`: el nombre de la función y el `id` llegan en el primer delta de ese índice y, después, los fragmentos de `function.arguments`, que usted concatena hasta formar un JSON válido. Una llamada a herramienta completa se marca con `finish_reason: "tool_calls"`.

## Salidas estructuradas

Para obligar a que la respuesta se ajuste a un esquema JSON, pase `response_format` con `type: "json_schema"` y `strict: true`.

```python
resp = client.chat.completions.create(
    model="gpt-6.1-sol",
    messages=[{"role": "user", "content": "Extract name and age: Anna is 30."}],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "person",
            "strict": True,
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "age": {"type": "integer"},
                },
                "required": ["name", "age"],
                "additionalProperties": False,
            },
        },
    },
)

print(resp.choices[0].message.content)  # {"name": "Anna", "age": 30}
```

<Callout type="info" title="json_object frente a json_schema">
`response_format: { "type": "json_object" }` garantiza un JSON válido, pero sin un esquema concreto, y exige que la palabra «json» aparezca en `messages`. Para una conformidad estructural estricta, utilice `json_schema` con `strict: true`.
</Callout>

## Errores [#errors]

Los errores usan el sobre de OpenAI `{ "error": { "message", "type", "param", "code" } }`, siempre con `Content-Type: application/json` y el estado HTTP correspondiente.

| Estado | `code` | `type` | Cuándo |
|--------|--------|--------|------|
| `400` | `invalid_request_error` | `invalid_request_error` | cuerpo mal formado: sin `model`, `messages` vacío, esquema JSON no válido en `response_format` |
| `400` | `context_length_exceeded` | `invalid_request_error` | la entrada supera el contexto del modelo; se comprueba antes de cobrar nada |
| `401` | `invalid_api_key` | `invalid_request_error` | la clave falta, es incorrecta o está revocada |
| `402` | `insufficient_quota` | `insufficient_quota` | no hay créditos suficientes en el saldo |
| `402` | `subscription_required` | `insufficient_quota` | el modelo requiere una suscripción activa |
| `402` | `insufficient_quota` | `insufficient_quota` | se ha agotado el presupuesto de créditos de la clave para el periodo: consulte [Límites de frecuencia](/es/docs/api-reference/rate-limits) |
| `403` | `insufficient_scope` o `permission_error` | `invalid_request_error` | a la clave le falta un permiso o la petición no está permitida |
| `404` | `model_not_found` | `invalid_request_error` | `model` desconocido o no disponible |
| `422` | `model_input_invalid` | `invalid_request_error` | el modelo rechazó la petición por su contenido; reformule la entrada |
| `429` | `rate_limit_exceeded` o `concurrency_limit_exceeded` | `rate_limit_error` | se ha superado el límite de peticiones, de tokens o de concurrencia |
| `500`, `502`, `503`, `504` | `null` | `api_error` | fallo temporal del modelo; reintente más tarde |

```json
{
  "error": {
    "message": "Insufficient credits. Please top up your balance.",
    "type": "insufficient_quota",
    "param": null,
    "code": "insufficient_quota"
  }
}
```

En los errores `5xx` el mensaje siempre es genérico: "The upstream model is temporarily unavailable. Please retry." para `502`, `503` y `504`, y "An internal error occurred. Please retry or contact support." para `500`.

### Errores dentro de un stream [#stream-errors]

Si el fallo se produce cuando el stream ya ha empezado, el estado HTTP ya es `200`. En ese caso, el último chunk antes de `data: [DONE]` incluye `finish_reason: "error"` y un objeto `error`:

```json
{
  "id": "chatcmpl-...",
  "object": "chat.completion.chunk",
  "choices": [{ "index": 0, "delta": {}, "finish_reason": "error" }],
  "error": { "message": "The model timed out. Please retry.", "code": "upstream_timeout", "type": "api_error" }
}
```

| `code` | Qué ha ocurrido |
| --- | --- |
| `upstream_error` | el modelo no respondió |
| `upstream_timeout` | se agotó el tiempo de espera del modelo |
| `service_unavailable` | el modelo no está disponible temporalmente |
| `empty_response` | el modelo no devolvió ninguna salida utilizable |
| `incomplete_stream` | el stream del modelo terminó antes de tiempo |
| `invalid_structured_output` | la salida no se ajusta al `response_format` solicitado |
| `model_input_invalid` | el modelo rechazó la petición por su contenido |

Compruebe `finish_reason` en el último chunk: los clientes del SDK de OpenAI no siempre convierten ese chunk en una excepción. Los errores que se producen antes de que empiece el stream, como `402` o `404`, llegan como JSON normal con un estado HTTP.
