# Chat Completions vs Responses API: cuál usar y cuándo migrar

> Chat Completions sigue soportada, pero Responses API es la opción para proyectos nuevos y la única que combina herramientas y razonamiento desde GPT-5.4.

- Source: https://www.aifreeapi.com/es/posts/chat-completions-vs-responses-api
- Language: es
- Published: 2026-08-25
- Updated: 2026-09-25
- Publisher: AI Free API (https://www.aifreeapi.com)

Si empiezas un proyecto con la API de OpenAI, usa **Responses API**. Si ya tienes una integración estable con **Chat Completions** que genera texto o salida estructurada, puedes dejarla como está: a 25 de septiembre de 2026, OpenAI la mantiene soportada y no ha anunciado fecha de retirada para `/v1/chat/completions`. Lo que sí la vuelve insuficiente es la combinación de herramientas con razonamiento: desde GPT-5.4, Chat Completions solo admite llamadas a herramientas con `reasoning_effort` en `none`, y con GPT-6 Astra no admite function calling en absoluto.

Resumido en una regla: **si tu flujo llama a funciones y quieres que el modelo razone, o si necesitas las herramientas alojadas por OpenAI (búsqueda web, búsqueda en archivos, intérprete de código, MCP remoto), tu destino es Responses**. Para lo demás, migrar es una mejora recomendada que puedes hacer flujo a flujo, no una urgencia.

## Qué API te toca según tu modelo y lo que usas

La [guía de migración de OpenAI](https://developers.openai.com/api/docs/guides/migrate-to-responses) resume la postura oficial en dos frases: Chat Completions «sigue soportada» y Responses se recomienda para todos los proyectos nuevos. Las condiciones concretas que inclinan la balanza dependen del modelo y de las funciones que tengas activas:

![Árbol de decisión en cuatro preguntas: proyecto nuevo, herramientas alojadas, llamadas a funciones y razonamiento o GPT-6 Astra, con el resultado Chat Completions o Responses API en cada rama](https://www.aifreeapi.com/posts/es/chat-completions-vs-responses-api/img/api-decision-tree.webp)

| Tu caso | API adecuada | Por qué |
|---|---|---|
| Proyecto nuevo, cualquier modelo | Responses | Es la recomendación explícita de OpenAI y evita una segunda migración |
| Integración en producción que solo genera texto o JSON con esquema | Chat Completions sirve | Ambas APIs admiten Structured Outputs con JSON Schema estricto; cambia solo la forma de la petición |
| Llamadas a funciones sin razonamiento (`reasoning_effort: "none"`) en GPT-5.4 o posterior | Cualquiera de las dos | Chat Completions admite herramientas solo con `none` |
| Llamadas a funciones con razonamiento en GPT-5.4 o posterior | Responses | Chat Completions no admite herramientas con otro valor de `reasoning_effort` |
| GPT-6 Astra con herramientas | Responses | Chat Completions no admite function calling con Astra, y Astra devuelve HTTP 400 si pides `none` |
| Búsqueda web, búsqueda en archivos, intérprete de código o MCP remoto | Responses | En Chat Completions no puedes usar las herramientas alojadas de forma nativa; tendrías que integrarlas tú |
| Proveedor tercero «compatible con OpenAI» | Lo que exponga el proveedor | Esa etiqueta suele referirse a Chat Completions y no garantiza paridad con Responses |

Las restricciones por modelo están en la [guía de razonamiento](https://developers.openai.com/api/docs/guides/reasoning) y en la de [function calling](https://developers.openai.com/api/docs/guides/function-calling), que indica literalmente que «GPT-6 Astra requires the Responses API for tool calling». La página de [GPT-6 Astra](https://developers.openai.com/api/docs/models/gpt-6-astra) lista los niveles de razonamiento que acepta (`low`, `medium`, `high`, `xhigh`, `max`) y precisa que sus herramientas alojadas están disponibles «when using the Responses API».

Hay una consecuencia práctica que no aparece escrita así en la documentación, pero se deduce de ella: [GPT-5.6 Sol](https://developers.openai.com/api/docs/models/gpt-5.6-sol) (alias `gpt-5.6`) razona en `medium` por defecto. Para enviarle herramientas por Chat Completions tendrías que bajar `reasoning_effort` a `none`, es decir, renunciar al razonamiento en esas peticiones. En Responses mantienes las dos cosas. Lo mismo aplica a GPT-6 Sol y GPT-6 Luna, que aceptan de `none` a `max` y también parten de `medium`.

Si todavía no has elegido modelo, empieza por ahí: la comparativa de [modelos de texto de OpenAI en 2026](/es/posts/openai-text-models-2026) resume IDs y usos, y [GPT-6 Luna vs Sol](/es/posts/gpt-6-luna-vs-sol-price) calcula el coste por tarea de esos dos modelos, que funcionan en ambas APIs.

## ¿Chat Completions está obsoleta?

No. La [página de deprecaciones](https://developers.openai.com/api/docs/deprecations) no incluye ninguna entrada que retire `/v1/chat/completions`, y la guía de migración sigue describiéndola como soportada. OpenAI recomienda migrar «all flows to the Responses API over time» y, precisamente porque Chat Completions sigue en pie, propone hacerlo de un flujo de usuario en uno.

Tres cosas que se confunden con esa supuesta retirada:

- **Assistants API** sí desapareció: se apagó el 26 de agosto de 2026 y su sustituto oficial es Responses junto con la Conversations API. Si todavía tienes código de Assistants, esa migración ya no es opcional.
- **Completions (legacy)**, el endpoint `/v1/completions` de texto sin roles, es otra API más antigua. La etiqueta «legacy» es suya, no de Chat Completions.
- **La CLI de Codex** dejó de aceptar `wire_api = "chat"` en febrero de 2026, según el [anuncio en GitHub](https://github.com/openai/codex/discussions/7782). Es un cliente que abandona el protocolo, no la API que cierra. Si configuras Codex con un proveedor propio, el artículo sobre [conectar Codex a una API de modelos externa](/es/posts/codex-third-party-api) explica cómo hacerlo con Responses.

## Qué cambia de verdad al pasar a Responses

El cambio de URL (`POST /v1/chat/completions` → `POST /v1/responses`) es lo de menos. Chat Completions trabaja con **mensajes**: entran como `messages` y salen dentro de `choices[].message`, con la llamada a herramientas pegada al mensaje del asistente. Responses trabaja con **Items**, elementos tipados e independientes: un mensaje es un tipo de Item, y también lo son `reasoning`, `function_call` y `function_call_output`. De ahí salen casi todas las diferencias:

| Aspecto | Chat Completions | Responses API |
|---|---|---|
| Entrada | `messages` | `input` (string o lista de Items) e `instructions` opcional |
| Salida | `choices[].message` | lista `output` de Items tipados |
| Texto final | `choices[0].message.content` | `output_text`, ayuda del SDK; en REST crudo, recorre `output` |
| Varias respuestas | `n` devuelve varias choices | no existe `n`: una generación por petición |
| Definición de función | `{"type": "function", "function": {"name": ...}}` | `{"type": "function", "name": ...}`, sin el nivel `function` |
| Modo estricto de funciones | no estricto por defecto | si omites `strict`, intenta el modo estricto |
| Llamada a herramienta | `tool_calls` dentro del mensaje del asistente | Item `function_call` con `call_id`, `name` y `arguments` |
| Resultado de herramienta | mensaje `role: "tool"` con `tool_call_id` | Item `function_call_output` con el mismo `call_id` |
| Salida estructurada | `response_format` | `text.format` |
| Límite de salida | `max_completion_tokens` (`max_tokens` está deprecado) | `max_output_tokens`, que incluye los tokens de razonamiento |
| Esfuerzo de razonamiento | `reasoning_effort` | `reasoning.effort` |
| Streaming | fragmentos con campo `delta` | eventos tipados (`response.output_text.delta`, etc.) |
| Estado entre turnos | reenvías el historial | historial manual, `previous_response_id` o Conversations API |
| Qué hace `store` | guarda la salida para destilación y evals | guarda el estado de la aplicación, 30 días por defecto |

Para una llamada de texto de un solo turno, el cambio es pequeño:

```python
from openai import OpenAI

client = OpenAI()

# Chat Completions
completion = client.chat.completions.create(
    model="gpt-5.6",
    messages=[
        {"role": "system", "content": "Clasifica tickets de soporte en una palabra."},
        {"role": "user", "content": "No puedo iniciar sesión desde ayer."},
    ],
)
print(completion.choices[0].message.content)

# Responses API
response = client.responses.create(
    model="gpt-5.6",
    instructions="Clasifica tickets de soporte en una palabra.",
    input="No puedo iniciar sesión desde ayer.",
)
print(response.output_text)
```

Si no usas funciones ni entradas multimodales, también puedes pasar tu lista de mensajes `role`/`content` tal cual como `input`. Eso sirve para una primera prueba, pero no convierte en compatible el código que lee la respuesta: con un modelo de razonamiento, el primer elemento de `output` suele ser un Item `reasoning`, no el mensaje. Si lees `output[0]` esperando texto, te encontrarás con un Item sin el texto de la respuesta. Usa `output_text` para el texto final y recorre `output` por tipo cuando haya herramientas o razonamiento de por medio.

## Llamadas a herramientas: donde más código se rompe

![Ciclo de una llamada a función en tres pasos: en Chat Completions, tool_calls y respuesta role tool con tool_call_id; en Responses API, Item function_call con arguments como string JSON y function_call_output con el mismo call_id](https://www.aifreeapi.com/posts/es/chat-completions-vs-responses-api/img/tool-call-cycle.webp)

En Chat Completions, el modelo devuelve un mensaje del asistente con `tool_calls`, tú ejecutas la función y respondes con un mensaje `role: "tool"` que lleva `tool_call_id`. En Responses, la llamada y su resultado son dos Items separados que se enlazan por `call_id`. Tres detalles provocan la mayoría de fallos:

- `arguments` llega como **string JSON**, no como objeto: hay que parsearlo con `json.loads`.
- El `output` que devuelves en `function_call_output` debe ser normalmente un string (JSON, un código de error o texto plano). Para imágenes o archivos puedes pasar una lista de objetos de imagen o de archivo. Si tu función no devuelve nada, devuelve algo como `"success"`.
- Con modelos de razonamiento, los Items `reasoning` que acompañan a las llamadas tienen que volver en la siguiente petición junto con los resultados. La forma más segura es reenviar todo `response.output`.

Este ejemplo completo sigue el patrón de la [guía de function calling](https://developers.openai.com/api/docs/guides/function-calling); `consultar_pedido` es una función de ejemplo que sustituirías por la tuya:

```python
import json
from openai import OpenAI

client = OpenAI()

tools = [{
    "type": "function",
    "name": "consultar_pedido",
    "description": "Devuelve el estado de un pedido a partir de su ID.",
    "parameters": {
        "type": "object",
        "properties": {"pedido_id": {"type": "string"}},
        "required": ["pedido_id"],
        "additionalProperties": False,
    },
}]

def consultar_pedido(pedido_id: str) -> dict:
    return {"pedido_id": pedido_id, "estado": "enviado"}

entrada = [{"role": "user", "content": "¿En qué estado está el pedido A-1042?"}]
response = client.responses.create(model="gpt-5.6", input=entrada, tools=tools)

entrada += response.output  # conserva reasoning y function_call
for item in response.output:
    if item.type == "function_call":
        args = json.loads(item.arguments)  # arguments es un string JSON
        resultado = consultar_pedido(**args)
        entrada.append({
            "type": "function_call_output",
            "call_id": item.call_id,
            "output": json.dumps(resultado),
        })

final = client.responses.create(model="gpt-5.6", input=entrada, tools=tools)
print(final.output_text)
```

El bucle recorre todos los Items porque el modelo puede pedir varias funciones en el mismo turno; si quieres como máximo una, pon `parallel_tool_calls=False`. El esquema lleva `additionalProperties: False` y todos los campos en `required` porque, al omitir `strict`, Responses intenta el modo estricto. Si el esquema no encaja, vuelve a un modo no estricto y la herramienta resuelta aparece con `strict: false`. Para conservar a propósito el comportamiento que tenías en Chat Completions, declara `"strict": False`.

## Estado de la conversación, `store` y lo que pagas

Chat Completions no guarda la conversación por ti: reenvías el historial en cada petición. Responses ofrece tres caminos, descritos en la guía de [estado de conversación](https://developers.openai.com/api/docs/guides/conversation-state):

1. **Historial manual**: reenvías los Items anteriores tú mismo. Es lo más parecido a lo que ya haces y te deja recortar el contexto.
2. **`previous_response_id`**: encadenas con el ID de la respuesta anterior y OpenAI recupera el contexto.
3. **Conversations API**: un objeto de conversación persistente que sobrevive entre sesiones, dispositivos o trabajos.

`previous_response_id` tiene dos trampas. La primera: no hereda las `instructions` de la respuesta anterior, así que debes reenviarlas en cada turno.

```python
primera = client.responses.create(
    model="gpt-5.6",
    instructions="Responde como analista de incidentes, en frases cortas.",
    input="Agrupa estas alertas por causa probable: ...",
)

siguiente = client.responses.create(
    model="gpt-5.6",
    previous_response_id=primera.id,
    instructions="Responde como analista de incidentes, en frases cortas.",
    input="Quédate solo con el grupo que hay que escalar ya.",
)
```

La segunda: no abarata la conversación. Según OpenAI, todos los tokens de entrada de las respuestas anteriores de la cadena se facturan como entrada. Lo que puede reducir el coste es la caché: OpenAI afirma haber medido en pruebas internas entre un 40 % y un 80 % más de aprovechamiento de caché en Responses frente a Chat Completions, y una mejora del 3 % en SWE-bench con modelos de razonamiento y el mismo prompt. Son cifras del propio proveedor; compruébalas con tu tráfico antes de contar con ellas.

El parámetro `store` no significa lo mismo en las dos APIs, y conviene tenerlo claro si tienes requisitos de privacidad:

- En **Responses**, `store` guarda el estado de la aplicación que usa `previous_response_id`. Según la página de [controles de datos](https://developers.openai.com/api/docs/guides/your-data), ese estado se conserva 30 días por defecto. Con `store: false` no se guarda.
- En **Chat Completions**, la [referencia](https://developers.openai.com/api/reference/resources/chat/subresources/completions/methods/create) define `store` como guardar la salida «for use in our model distillation or evals products». No tiene que ver con el estado de la conversación, pero la guía de migración indica que en cuentas nuevas se guarda por defecto.
- Las organizaciones con **retención de datos cero (ZDR)** tienen `store` forzado a `false`. Para mantener el razonamiento entre turnos sin estado en el servidor, reenvía los Items `reasoning` con su `encrypted_content`.
- Los objetos de la **Conversations API** no caducan a los 30 días: se conservan hasta que los borras y no son elegibles para ZDR.

Si no quieres que se almacene nada, pon `store: false` de forma explícita en cualquiera de las dos APIs y revisa la configuración de tu organización.

## Salida estructurada, límites de tokens y streaming

**Salida estructurada.** El esquema no cambia; cambia dónde va. En Chat Completions iba en `response_format` con un nivel `json_schema`; en Responses va en `text.format` y el nombre sube un nivel:

```python
esquema = {
    "type": "object",
    "properties": {"nombre": {"type": "string"}, "edad": {"type": "integer"}},
    "required": ["nombre", "edad"],
    "additionalProperties": False,
}

response = client.responses.create(
    model="gpt-5.6",
    input="Extrae nombre y edad: Lucía tiene 34 años.",
    text={"format": {"type": "json_schema", "name": "persona", "strict": True, "schema": esquema}},
)
print(response.output_text)  # JSON que cumple el esquema
```

**Límite de salida.** `max_output_tokens` cuenta los tokens visibles **y** los de razonamiento, con un mínimo de 16, según la [referencia de Responses](https://developers.openai.com/api/reference/resources/responses/methods/create). Si copias el `max_tokens` que usabas en Chat Completions, un modelo de razonamiento puede agotar el presupuesto antes de escribir la respuesta y devolverte algo vacío o cortado. Microsoft lo recoge como fallo típico en su [guía de migración para Azure OpenAI](https://learn.microsoft.com/es-es/azure/developer/ai/how-to/azure-openai-to-responses).

**Streaming.** En Chat Completions concatenabas `choices[0].delta.content`. Responses envía eventos con tipo y tienes que ramificar por él. La [guía de streaming](https://developers.openai.com/api/docs/guides/streaming-responses) cita para texto `response.created`, `response.output_text.delta`, `response.completed` y `error`; si hay llamadas a funciones, llegan también `response.function_call_arguments.delta` y `response.function_call_arguments.done`.

```python
stream = client.responses.create(
    model="gpt-5.6",
    input="Resume este ticket en dos frases: ...",
    stream=True,
)
for event in stream:
    if event.type == "response.output_text.delta":
        print(event.delta, end="", flush=True)
    elif event.type == "error":
        raise RuntimeError(str(event))
    elif event.type == "response.completed":
        break
```

Envuelve el bucle en un `try`/`except`: si un error de cuota, autenticación o filtro de contenido llega a mitad del stream y nadie lo captura, el stream simplemente se detiene sin decir nada.

**Parámetros de muestreo.** Esa misma guía de Microsoft recomienda quitar `seed` y comprobar si el modelo de destino acepta `temperature` y `top_p`, sobre todo si es de razonamiento. La guía de OpenAI no menciona `seed`; tómalo como una comprobación más, no como una regla universal.

## Cómo migrar un flujo de Chat Completions a Responses sin perder nada

OpenAI propone migrar de un flujo en uno y comparar antes de mover más tráfico. Ordenado para que cada paso se pueda verificar:

1. **Aísla el acceso a la API.** Si el código de negocio lee `choices[0]` directamente, crea antes una capa propia que devuelva texto, llamadas a herramientas y uso de tokens. Así la migración toca un solo sitio.
2. **Empieza por un flujo de solo texto.** Cambia endpoint, cuerpo de la petición y lectura de la respuesta (`output_text` o recorrido de `output`).
3. **Elige cómo guardas el estado**: historial manual, `previous_response_id` o Conversations API. Si el flujo es sin estado o tu organización usa ZDR, añade `store: false` y reenvía los Items `reasoning` cifrados.
4. **Migra las funciones**: aplana las definiciones, parsea `arguments`, devuelve `function_call_output` con el `call_id` correcto y prueba cada esquema con el modo estricto.
5. **Mueve los esquemas** de `response_format` a `text.format`.
6. **Reescribe el consumidor de streaming** para eventos tipados, con manejo de `error`.
7. **Sustituye integraciones propias** por herramientas alojadas solo donde encajen en el flujo.
8. **Compara antes de ampliar**: comportamiento, latencia, tokens consumidos y errores con peticiones reales. No busques frases idénticas; busca que el JSON se parsee, que no se pierda ninguna llamada a herramienta y que el stream termine.

No des por hecho que una API es más rápida que la otra: la documentación de OpenAI no incluye una comparación de latencia, y su propia lista de comprobación pide medirla con tu tráfico. Actualiza también los mocks y snapshots de los tests: los que simulan `choices` o `choices[0].delta.content` dejan de representar lo que devuelve la API.

## Síntomas típicos al enviar un payload con forma de Chat Completions

Estos son los fallos que aparecen cuando parte del código sigue pensando en mensajes. Los mensajes de error literales proceden de la tabla de solución de problemas de Microsoft para Azure OpenAI; en `api.openai.com` el texto exacto puede variar, pero la causa es la misma.

| Síntoma | Causa probable | Arreglo |
|---|---|---|
| `missing_required_parameter: tools[0].name` | Definición de función anidada al estilo Chat | Quita el nivel `function` y pon `name` arriba |
| `unknown_parameter: input[N].tool_calls` | Reenvías el mensaje del asistente con `tool_calls` o mensajes `role: "tool"` | Reenvía los Items de `output` y un `function_call_output` por llamada |
| `invalid_type: text.format` | Pasas la estructura antigua de `response_format` | Usa `text={"format": {"type": "json_schema", "name": ..., "schema": ...}}` |
| `invalid input content type` | Contenido con tipo `text` o `image_url` | Usa `input_text` e `input_image` |
| `integer below minimum value` en `max_output_tokens` | Valor por debajo de 16 | Sube el límite |
| Respuesta vacía o cortada | `max_output_tokens` pequeño con un modelo de razonamiento | Deja margen para los tokens de razonamiento |
| El código no encuentra el texto | Lees `choices` o tratas `output[0]` como mensaje | Usa `output_text` o filtra `output` por tipo `message` |
| El modelo no «ve» el resultado de la función | `function_call_output` sin el `call_id` correcto | Copia el `call_id` del Item `function_call` |
| El modelo pierde el rol en el segundo turno | `previous_response_id` no hereda `instructions` | Reenvía `instructions` en cada petición |
| La factura no baja con `previous_response_id` | Toda la cadena se factura como entrada | Recorta contexto con historial manual si el coste importa |
| HTTP 400 con GPT-6 Astra | Pides esfuerzo de razonamiento `none` | Usa de `low` a `max` |
| El stream se para sin error visible | Un error a mitad del stream que nadie captura | Maneja el evento `error` y envuelve el bucle en `try`/`except` |

La lista de errores comunes de la guía de OpenAI incluye dos más que conviene revisar a mano: perder Items `reasoning`, `function_call` o `function_call_output` al reconstruir el contexto y reutilizar el manejador de fragmentos de Chat Completions sin adaptarlo a eventos.

## Proveedores «compatibles con OpenAI»

Muchos proveedores y pasarelas que se anuncian como compatibles con OpenAI implementan Chat Completions; el soporte de Responses varía mucho. Algunos exponen una ruta compatible con Responses pero sin estado e ignorando parámetros o herramientas que no soportan. Antes de migrar un flujo que pasa por un tercero, comprueba en su documentación que existe `/v1/responses` y qué hace con `previous_response_id`, `store`, los eventos de streaming y las herramientas alojadas.

En Azure, Microsoft documenta la migración a Responses con el cliente `OpenAI` apuntando a `.../openai/v1/`. Y si llamas a un modelo de otro proveedor con el cliente de OpenAI, como en el caso de [DeepSeek V4 Pro](/es/posts/deepseek-v4-pro), la documentación de ese proveedor es la que dice qué endpoint expone y con qué limitaciones.

## Preguntas frecuentes

### ¿Puedo seguir usando Chat Completions en 2026?

Sí. A 25 de septiembre de 2026 está soportada y sin fecha de retirada. El límite real es funcional: no te sirve para combinar herramientas y razonamiento en GPT-5.4 o posterior, ni para function calling con GPT-6 Astra, ni para herramientas alojadas.

### ¿Responses API es más rápida?

La documentación de OpenAI no incluye una comparación de latencia. Mide con tus propias peticiones antes de sacar conclusiones; las diferencias que publica OpenAI se refieren a calidad con modelos de razonamiento y a aprovechamiento de caché, no a velocidad.

### ¿`previous_response_id` reduce el coste de una conversación larga?

No. Todos los tokens de entrada de la cadena se facturan como entrada en cada turno. Te ahorra reenviar el historial, no pagarlo.

### ¿Tengo que migrar si usaba Assistants API?

Sí, y no es opcional: Assistants API dejó de funcionar el 26 de agosto de 2026. Los sustitutos oficiales son Responses API y la Conversations API.
