Invita y gana

Cómo funcionan las recompensas

Comparte tu enlace. Cuando un amigo se registre con él y recargue saldo, recibirás la recompensa indicada por sus recargas posteriores.

Anthropic Messages, Chat Completions y Responses: cómo elegir, convertir y entender los límites de compatibilidad

Un HTTP 200 no significa que un agente sea compatible. Tres ciclos completos de herramientas muestran las diferencias reales entre Messages, Chat Completions y Responses, con pasos de migración, prueba y diagnóstico.

Índice

Un mismo agente puede seguir recibiendo 200 después de cambiar el endpoint y los nombres de los campos, pero dejar de ejecutar herramientas, devolver JSON que no cumple la Schema o perder de repente el contexto entre turnos. Normalmente no significa que el modelo se haya vuelto «menos capaz», sino que la aplicación ha tratado tres protocolos distintos como si fueran una sola interfaz.

Una migración solo puede considerarse correcta si se comprueba al menos en tres niveles:

  1. El formato se acepta: el servidor puede interpretar la solicitud y devuelve un estado satisfactorio.
  2. El comportamiento es equivalente: se llaman las herramientas, los resultados vuelven correctamente al modelo, el stream termina por completo y el contexto multivuelta se mantiene coherente.
  3. Las capacidades se conservan: la Schema estricta, el estado de reasoning nativo, las herramientas alojadas y las salidas estructuradas no se ignoran ni degradan en silencio.

HTTP 200 solo demuestra el primer nivel. Una conversión sencilla puede bastar para una solicitud que genera un bloque de texto. Cuando intervienen agentes, herramientas, argumentos en streaming, estado multivuelta o modelos de reasoning, hay que validar la cadena completa de interacción.

Conclusión práctica: el protocolo debe seguir al cliente y a las capacidades, no al nombre del modelo

EscenarioPunto de partida más adecuadoMotivo
Una aplicación existente ya usa de forma estable el SDK de OpenAI y messagesChat CompletionsRequiere menos cambios y permite conservar el ciclo actual de mensajes y herramientas
Un agente nuevo de OpenAI necesita herramientas alojadas, Items tipados o continuidad de estado en el servidorResponsesOpenAI lo recomienda actualmente para proyectos nuevos y ofrece un conjunto más amplio de capacidades para agentes
Claude Code, una aplicación nativa de Claude o un flujo que depende de funciones propias de ClaudeAnthropic MessagesLos bloques de contenido, resultados de herramientas, thinking y demás comportamiento siguen el contrato nativo de Anthropic
Un gateway propio o un router multimodeloMantener un adaptador independiente para cada protocolo upstreamUn único «JSON universal» no puede representar todas las capacidades nativas sin pérdidas

OpenAI sigue admitiendo Chat Completions, por lo que una aplicación estable no tiene que reescribirse de inmediato solo porque exista una interfaz más reciente. La migración resulta más razonable para proyectos nuevos o cuando se necesitan capacidades nativas de Responses. Anthropic Messages tampoco es una interfaz de OpenAI con un campo renombrado a messages: sus bloques de contenido, traspaso de herramientas, eventos de streaming y reglas de estado forman un contrato propio.

Diferencias esenciales entre las tres API

En este artículo, Completions se refiere a Chat Completions, no al antiguo endpoint /v1/completions.

DimensiónOpenAI Chat CompletionsOpenAI ResponsesAnthropic Messages
Endpoint/v1/chat/completions/v1/responses/v1/messages
Entrada principalmessagesItems en input; también admite mensajes simplesmessages, normalmente con un system independiente en el nivel superior
Salida principalchoices[].messageItems tipados en output[]Bloques de contenido en content[]
Definición de herramientastools[].functionname y parameters aparecen directamente en tools[]input_schema dentro de tools[]
Argumentos de herramientasfunction.arguments, una cadena JSONarguments, una cadena JSONtool_use.input, un objeto JSON
ID de correlacióntool_calls[].idcall_idtool_use.id
Devolución del resultadorole: "tool" + tool_call_idfunction_call_output + call_idtool_result + tool_use_id dentro de un mensaje user
Estado multivueltaLa aplicación reenvía el historial de mensajesReenvío de Items, previous_response_id o ConversationsLa aplicación reenvía mensajes y bloques de contenido
Salida estructurada finalresponse_formattext.formatoutput_config.format
Streamingchoices[].deltaEventos tipados de ResponsesEventos de message/content block

La tabla puede hacer que todo parezca una cuestión de renombrar campos. Los fallos reales suelen aparecer en la segunda solicitud: después de que el modelo emita una llamada a herramienta, ¿cómo la ejecuta la aplicación, qué ID debe conservar y con qué rol y orden devuelve el resultado? A continuación se recorre la misma tarea sin efectos secundarios en los tres protocolos.

Ejemplo común: consultar un plan de prueba

El usuario pregunta:

Consulta el plan team y dime si admite cobro por uso cuando se supera lo incluido.

La herramienta se llama get_plan_info. Solo lee datos locales fijos y no produce efectos secundarios externos, por lo que resulta apropiada para probar una migración de protocolo.

Los datos del plan son sintéticos y se usan únicamente con fines didácticos. No representan planes, precios ni derechos reales de OpenAI, Anthropic o BetterToken. Las tres secuencias de solicitudes y respuestas muestran la estructura del protocolo; no son registros de ejecuciones reales de la API.

La herramienta de la aplicación puede implementarse como una función independiente del protocolo:

from __future__ import annotations

import json
from typing import Any


PLAN_FIXTURES: dict[str, dict[str, Any]] = {
    "team": {
        "plan_code": "team",
        "display_name": "Team",
        "billing_mode": "usage_based",
        "included_requests": 10_000,
        "overage_allowed": True,
        "source_version": "fixture-2026-09-01",
    }
}


def execute_tool(name: str, raw_arguments: str | dict[str, Any]) -> str:
    """Ejecuta la herramienta didáctica de solo lectura y devuelve una cadena JSON que puede enviarse directamente al modelo."""
    if isinstance(raw_arguments, str):
        arguments = json.loads(raw_arguments)
    elif isinstance(raw_arguments, dict):
        arguments = raw_arguments
    else:
        raise TypeError("los argumentos de la herramienta deben ser una cadena JSON o un objeto")

    if name != "get_plan_info":
        raise ValueError(f"herramienta desconocida: {name}")

    if set(arguments) != {"plan_code"}:
        raise ValueError("get_plan_info solo acepta plan_code")

    plan_code = arguments["plan_code"]
    if not isinstance(plan_code, str):
        raise TypeError("plan_code debe ser una cadena")

    plan = PLAN_FIXTURES.get(plan_code)
    if plan is None:
        return json.dumps(
            {"ok": False, "error": "plan_not_found", "plan_code": plan_code},
            ensure_ascii=False,
        )

    return json.dumps({"ok": True, "data": plan}, ensure_ascii=False)

Aunque la solicitud active una Schema estricta, la aplicación debe conservar sus propias validaciones de entrada. El modo estricto limita los argumentos que genera el modelo, pero no sustituye la autorización, la validez de enumeraciones, la idempotencia ni las comprobaciones de seguridad de la lógica de negocio.

Chat Completions: ciclo completo de una herramienta

Primera solicitud: pedir al modelo que genere una llamada a herramienta

Los siguientes ejemplos usan el endpoint oficial de OpenAI para mostrar el protocolo. Al conectarse a un servicio compatible, hay que sustituir Base URL, el método de autenticación y el Model ID según la documentación del proveedor.

curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "YOUR_OPENAI_MODEL",
    "messages": [
      {
        "role": "system",
        "content": "Eres un asistente de planes. Responde únicamente con los datos devueltos por la herramienta; no hagas suposiciones."
      },
      {
        "role": "user",
        "content": "Consulta el plan team y dime si admite cobro por uso cuando se supera lo incluido."
      }
    ],
    "tools": [
      {
        "type": "function",
        "function": {
          "name": "get_plan_info",
          "description": "Consultar datos de prueba fijos por código de plan",
          "strict": true,
          "parameters": {
            "type": "object",
            "properties": {
              "plan_code": {
                "type": "string",
                "enum": ["team"]
              }
            },
            "required": ["plan_code"],
            "additionalProperties": false
          }
        }
      }
    ],
    "tool_choice": "required",
    "parallel_tool_calls": false
  }'

La aplicación debe leer tool_calls en el mensaje assistant. La siguiente respuesta conserva únicamente los campos necesarios para continuar:

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_plan_001",
            "type": "function",
            "function": {
              "name": "get_plan_info",
              "arguments": "{\"plan_code\":\"team\"}"
            }
          }
        ]
      },
      "finish_reason": "tool_calls"
    }
  ]
}

Hay dos valores que no pueden perderse:

  • tool_calls[0].id: debe devolverse sin cambios como tool_call_id en la segunda solicitud.
  • function.arguments: es una cadena JSON. Primero hay que analizarla y después aplicar la validación propia de Schema y de negocio.

Ejecuta la herramienta:

tool_result = execute_tool(
    "get_plan_info",
    "{\"plan_code\":\"team\"}",
)

Segunda solicitud: devolver al modelo el resultado de la herramienta

Chat Completions exige conservar el mensaje assistant que contiene la llamada original y añadir después un mensaje de resultado con role: "tool".

curl https://api.openai.com/v1/chat/completions \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "YOUR_OPENAI_MODEL",
    "messages": [
      {
        "role": "system",
        "content": "Eres un asistente de planes. Responde únicamente con los datos devueltos por la herramienta; no hagas suposiciones."
      },
      {
        "role": "user",
        "content": "Consulta el plan team y dime si admite cobro por uso cuando se supera lo incluido."
      },
      {
        "role": "assistant",
        "content": null,
        "tool_calls": [
          {
            "id": "call_plan_001",
            "type": "function",
            "function": {
              "name": "get_plan_info",
              "arguments": "{\"plan_code\":\"team\"}"
            }
          }
        ]
      },
      {
        "role": "tool",
        "tool_call_id": "call_plan_001",
        "content": "{\"ok\":true,\"data\":{\"plan_code\":\"team\",\"display_name\":\"Team\",\"billing_mode\":\"usage_based\",\"included_requests\":10000,\"overage_allowed\":true,\"source_version\":\"fixture-2026-09-01\"}}"
      }
    ],
    "tools": [
      {
        "type": "function",
        "function": {
          "name": "get_plan_info",
          "description": "Consultar datos de prueba fijos por código de plan",
          "strict": true,
          "parameters": {
            "type": "object",
            "properties": {
              "plan_code": {
                "type": "string",
                "enum": ["team"]
              }
            },
            "required": ["plan_code"],
            "additionalProperties": false
          }
        }
      }
    ]
  }'

Un mensaje final representativo sería:

{
  "choices": [
    {
      "message": {
        "role": "assistant",
        "content": "El plan Team admite cobro por uso una vez superado lo incluido. Los datos de prueba incluyen 10.000 solicitudes y overage_allowed es true."
      },
      "finish_reason": "stop"
    }
  ]
}

Si el adaptador solo convierte el primer turno del usuario pero no conserva los tool_calls del assistant, o si introduce un ID incorrecto en tool_call_id, la segunda solicitud ya no continúa la misma llamada a herramienta.

Responses: ciclo completo de una herramienta

Responses representa mensajes, reasoning, llamadas y resultados como distintos tipos de Item. No se debe asumir que output[0] siempre contiene el texto final; la lógica debe ramificarse según el type de cada Item.

Primera solicitud: pedir al modelo un Item function_call

curl https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "YOUR_OPENAI_MODEL",
    "instructions": "Eres un asistente de planes. Responde únicamente con los datos devueltos por la herramienta; no hagas suposiciones.",
    "input": "Consulta el plan team y dime si admite cobro por uso cuando se supera lo incluido.",
    "tools": [
      {
        "type": "function",
        "name": "get_plan_info",
        "description": "Consultar datos de prueba fijos por código de plan",
        "strict": true,
        "parameters": {
          "type": "object",
          "properties": {
            "plan_code": {
              "type": "string",
              "enum": ["team"]
            }
          },
          "required": ["plan_code"],
          "additionalProperties": false
        }
      }
    ],
    "tool_choice": "required",
    "parallel_tool_calls": false,
    "store": false
  }'

Un Item representativo de llamada a herramienta sería:

{
  "id": "resp_plan_001",
  "object": "response",
  "output": [
    {
      "type": "function_call",
      "id": "fc_plan_001",
      "call_id": "call_plan_001",
      "name": "get_plan_info",
      "arguments": "{\"plan_code\":\"team\"}",
      "status": "completed"
    }
  ]
}

El resultado debe correlacionarse con call_id. id: "fc_plan_001" identifica al Item en sí y no debe sustituir a call_id.

Ejecuta la herramienta:

tool_result = execute_tool(
    "get_plan_info",
    "{\"plan_code\":\"team\"}",
)

Segunda solicitud: devolver un function_call_output

El ejemplo siguiente usa reproducción manual sin estado, por lo que vuelve a enviar instructions, la entrada original del usuario, la llamada a herramienta y su resultado.

curl https://api.openai.com/v1/responses \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "YOUR_OPENAI_MODEL",
    "instructions": "Eres un asistente de planes. Responde únicamente con los datos devueltos por la herramienta; no hagas suposiciones.",
    "input": [
      {
        "role": "user",
        "content": "Consulta el plan team y dime si admite cobro por uso cuando se supera lo incluido."
      },
      {
        "type": "function_call",
        "call_id": "call_plan_001",
        "name": "get_plan_info",
        "arguments": "{\"plan_code\":\"team\"}"
      },
      {
        "type": "function_call_output",
        "call_id": "call_plan_001",
        "output": "{\"ok\":true,\"data\":{\"plan_code\":\"team\",\"display_name\":\"Team\",\"billing_mode\":\"usage_based\",\"included_requests\":10000,\"overage_allowed\":true,\"source_version\":\"fixture-2026-09-01\"}}"
      }
    ],
    "tools": [
      {
        "type": "function",
        "name": "get_plan_info",
        "description": "Consultar datos de prueba fijos por código de plan",
        "strict": true,
        "parameters": {
          "type": "object",
          "properties": {
            "plan_code": {
              "type": "string",
              "enum": ["team"]
            }
          },
          "required": ["plan_code"],
          "additionalProperties": false
        }
      }
    ],
    "store": false
  }'

Un Item final de salida representativo sería:

{
  "id": "resp_plan_002",
  "object": "response",
  "output": [
    {
      "type": "message",
      "role": "assistant",
      "content": [
        {
          "type": "output_text",
          "text": "El plan Team admite cobro por uso una vez superado lo incluido. Los datos de prueba incluyen 10.000 solicitudes y overage_allowed es true."
        }
      ]
    }
  ]
}

Si se utiliza continuidad de estado en el servidor, se puede permitir que la primera respuesta se guarde y usar el siguiente formato en la segunda solicitud:

{
  "model": "YOUR_OPENAI_MODEL",
  "previous_response_id": "resp_plan_001",
  "input": [
    {
      "type": "function_call_output",
      "call_id": "call_plan_001",
      "output": "{\"ok\":true,\"data\":{\"plan_code\":\"team\",\"overage_allowed\":true}}"
    }
  ]
}

Un previous_response_id pertenece al servicio upstream que creó esa respuesta. No puede entregarse a otro proveedor para continuarla. Tampoco hace gratuita la entrada histórica: la documentación actual de OpenAI indica que los token de entrada anteriores de la cadena siguen facturándose como input.

Cuando la respuesta contiene un reasoning Item, la reproducción sin estado también debe conservar el Item correspondiente tal como exige la documentación. No se puede eliminar para crear un «formato uniforme» y afirmar después que el contexto de reasoning sigue siendo equivalente.

Anthropic Messages: ciclo completo de una herramienta

Messages representa la llamada como un bloque tool_use dentro del contenido assistant y devuelve el resultado como un bloque tool_result en el siguiente mensaje user. Los argumentos ya son un objeto, no una cadena JSON pendiente de analizar.

Primera solicitud: pedir a Claude que devuelva tool_use

curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "YOUR_CLAUDE_MODEL",
    "max_tokens": 512,
    "system": "Eres un asistente de planes. Responde únicamente con los datos devueltos por la herramienta; no hagas suposiciones.",
    "messages": [
      {
        "role": "user",
        "content": "Consulta el plan team y dime si admite cobro por uso cuando se supera lo incluido."
      }
    ],
    "tools": [
      {
        "name": "get_plan_info",
        "description": "Consultar datos de prueba fijos por código de plan",
        "strict": true,
        "input_schema": {
          "type": "object",
          "properties": {
            "plan_code": {
              "type": "string",
              "enum": ["team"]
            }
          },
          "required": ["plan_code"],
          "additionalProperties": false
        }
      }
    ],
    "tool_choice": {
      "type": "tool",
      "name": "get_plan_info"
    }
  }'

Forzar una herramienta concreta depende de que el modelo y la configuración seleccionados lo admitan. Si el modelo de destino no lo permite, usa auto y comprueba en la aplicación que realmente se haya devuelto una llamada a herramienta.

Una respuesta representativa sería:

{
  "id": "msg_plan_001",
  "type": "message",
  "role": "assistant",
  "content": [
    {
      "type": "tool_use",
      "id": "toolu_plan_001",
      "name": "get_plan_info",
      "input": {
        "plan_code": "team"
      }
    }
  ],
  "stop_reason": "tool_use"
}

El objeto input puede pasarse directamente al ejecutor de la herramienta:

tool_result = execute_tool(
    "get_plan_info",
    {"plan_code": "team"},
)

Segunda solicitud: colocar tool_result en el mensaje user inmediatamente posterior

curl https://api.anthropic.com/v1/messages \
  -H "x-api-key: $ANTHROPIC_API_KEY" \
  -H "anthropic-version: 2023-06-01" \
  -H "content-type: application/json" \
  -d '{
    "model": "YOUR_CLAUDE_MODEL",
    "max_tokens": 512,
    "system": "Eres un asistente de planes. Responde únicamente con los datos devueltos por la herramienta; no hagas suposiciones.",
    "messages": [
      {
        "role": "user",
        "content": "Consulta el plan team y dime si admite cobro por uso cuando se supera lo incluido."
      },
      {
        "role": "assistant",
        "content": [
          {
            "type": "tool_use",
            "id": "toolu_plan_001",
            "name": "get_plan_info",
            "input": {
              "plan_code": "team"
            }
          }
        ]
      },
      {
        "role": "user",
        "content": [
          {
            "type": "tool_result",
            "tool_use_id": "toolu_plan_001",
            "content": "{\"ok\":true,\"data\":{\"plan_code\":\"team\",\"display_name\":\"Team\",\"billing_mode\":\"usage_based\",\"included_requests\":10000,\"overage_allowed\":true,\"source_version\":\"fixture-2026-09-01\"}}"
          }
        ]
      }
    ],
    "tools": [
      {
        "name": "get_plan_info",
        "description": "Consultar datos de prueba fijos por código de plan",
        "strict": true,
        "input_schema": {
          "type": "object",
          "properties": {
            "plan_code": {
              "type": "string",
              "enum": ["team"]
            }
          },
          "required": ["plan_code"],
          "additionalProperties": false
        }
      }
    ]
  }'

Una respuesta final representativa sería:

{
  "id": "msg_plan_002",
  "type": "message",
  "role": "assistant",
  "content": [
    {
      "type": "text",
      "text": "El plan Team admite cobro por uso una vez superado lo incluido. Los datos de prueba incluyen 10.000 solicitudes y overage_allowed es true."
    }
  ],
  "stop_reason": "end_turn"
}

Messages impone un orden claro: tool_result debe aparecer inmediatamente después del mensaje assistant que contiene el tool_use correspondiente. Si un mismo turno assistant genera varias llamadas a herramientas del cliente, todos los bloques de resultados deben volver en el siguiente mensaje user y correlacionarse uno a uno mediante tool_use_id. Si ese mensaje user también incluye texto normal, los bloques de resultados deben ir antes del texto.

Qué puede mapearse directamente y qué conversiones son inevitablemente con pérdida

CapacidadEvaluación de la conversiónTratamiento correcto
Texto ordinario del usuarioSuele mapearse directamenteConservar texto, orden y tipos multimodales, no solo las cadenas visibles
Schema básica de funciónPuede cambiarse de formaConvertir entre function.parameters, parameters de Responses e input_schema de Messages, y volver a validar el subconjunto de JSON Schema compatible
Argumentos de herramientasEs necesario convertir el tipoLas dos interfaces de OpenAI suelen devolver cadenas JSON; Messages devuelve un objeto. Normalizar, analizar y validar antes de entrar en la lógica de negocio
ID de llamadaHay que conservar su significado, pero no reutilizar namespacesMantener un canonical call ID interno junto con el ID original del upstream y devolver el campo propio del protocolo
Llamadas paralelasSe pueden admitir, pero nunca correlacionar por posición del arrayAsociar cada resultado mediante tool_call_id, call_id o tool_use_id
Instrucciones system/developerPueden sufrir pérdidasDiferenciar alcance global, de etapa de conversación y de un solo turno; degradar o rechazar explícitamente si el protocolo de destino no puede expresar el alcance original
Salida estructurada finalLos campos no son intercambiables de forma mecánicaChat usa response_format, Responses usa text.format y Messages usa output_config.format
Argumentos en streamingRequieren un parser específico del protocoloAcumular fragmentos por evento e ID de llamada y analizar JSON solo después del evento de finalización
Estado multivuelta del servidorNo existe un equivalente universalLos ID de estado como previous_response_id quedan ligados a su upstream; entre upstreams, reenviar el contexto visible o mantener sticky routing
Estado thinking/reasoningNormalmente no puede convertirse sin pérdidasConservar Items opacos, thinking blocks, firmas o contenido cifrado exactamente como exija el protocolo nativo; nunca inventarlos
Herramientas alojadasA menudo no tienen equivalente directoDeclarar por separado compatibilidad y alternativas para web search, file search, computer use, server tools y funciones similares
Generación de varios candidatosPuede no tener equivalenteNo suponer que n de Chat Completions se mapea directamente a Responses; realizar varias solicitudes en la aplicación o cambiar el comportamiento del producto

Por tanto, la abstracción interna más fiable para un gateway no es un objeto enorme que contenga todos los campos posibles. Conviene modelar por separado mensajes, alcance de instrucciones, definiciones de herramientas, llamadas, resultados, identificadores de estado, eventos de streaming y estado nativo opaco. Si una capacidad no puede expresarse, se debe devolver un estado explícito de «no compatible» o «conversión con pérdida», en lugar de eliminar el campo en silencio.

strict, response_format y text.format resuelven problemas distintos

Un error habitual en una migración es tratar «argumentos válidos para una herramienta» y «respuesta final con una forma JSON determinada» como si fueran una sola función.

ObjetivoChat CompletionsResponsesAnthropic Messages
Restringir argumentos de una llamadatools[].function.stricttools[].stricttools[].strict
Restringir la salida final del modeloresponse_formattext.formatoutput_config.format

El strict de una herramienta limita cómo llama el modelo a la función. La salida estructurada final limita el contenido entregado al usuario. Un agente puede necesitar ambas cosas a la vez: primero llamar a una herramienta con argumentos estrictos y después devolver el resultado final bajo una JSON Schema fija.

La documentación actual de OpenAI incluye además una diferencia de valores por defecto fácil de pasar por alto:

  • Las llamadas de función en Chat Completions no son estrictas por defecto.
  • Cuando se omite strict en Responses, el servicio intenta normalizar la Schema al modo estricto. Si no es compatible, puede volver al modo no estricto y mostrar strict: false en la definición analizada.

Para dejar clara la intención y no depender de valores por defecto distintos, las solicitudes de producción deben indicar deliberadamente strict: true o strict: false. Una Schema estricta también debe cumplir los requisitos correspondientes, como prohibir propiedades adicionales y enumerar todos los campos obligatorios.

Más importante aún: una capa de compatibilidad puede aceptar un campo sin aplicar la restricción. La documentación oficial de Anthropic sobre compatibilidad con el SDK de OpenAI indica que, en esa capa concreta, se ignoran campos como function strict, response_format y reasoning_effort, y que la mayoría de los campos no compatibles no producen errores. Por ello, una solicitud puede devolver 200 aunque la Schema o la configuración de reasoning no se hayan aplicado.

Esto no significa que Anthropic Messages nativo carezca de capacidades equivalentes. Messages nativo admite entrada estricta para herramientas y usa output_config.format para el JSON final. El diagnóstico debe empezar respondiendo a una pregunta: ¿se está llamando a Messages nativo o a una capa compatible con OpenAI?

El alcance de system, developer e instrucciones no se conserva concatenando cadenas

Las interfaces de estilo OpenAI admiten distintos roles en el historial y Responses añade instructions. Anthropic Messages ha usado tradicionalmente un system en el nivel superior. En septiembre de 2026, algunos modelos actuales también admiten role: "system" en mitad de una conversación, pero no todos, y existen restricciones de posición y de orden respecto a las herramientas.

Al mismo tiempo, la capa de compatibilidad de Anthropic con el SDK de OpenAI recopila los mensajes system/developer, los concatena con saltos de línea y los eleva a un único prompt de sistema al principio. Eso permite procesar la solicitud, pero altera su momento y alcance originales. Una instrucción developer prevista solo a partir del octavo turno puede cambiar la semántica de los siete primeros al trasladarse al inicio.

Un adaptador más seguro distingue primero tres niveles dentro de la aplicación:

  • Instrucciones globales: se aplican a toda la conversación.
  • Instrucciones de etapa: empiezan a aplicarse desde un turno determinado.
  • Instrucciones de un turno: controlan únicamente la tarea actual.

Solo debe mapearse una instrucción cuando el protocolo de destino pueda expresar el mismo alcance. Si no puede, hay que elegir una estrategia explícita: mantener la solicitud en un modelo que admita la capacidad, degradar la instrucción y registrar la diferencia, o rechazar la migración. La concatenación silenciosa requiere poco código, pero suele producir «la solicitud fue correcta, el comportamiento cambió».

El streaming requiere una máquina de estados, no concatenar token de texto

Las tres interfaces ofrecen streaming, pero sus eventos no son equivalentes:

  • Chat Completions suele acumular texto y fragmentos de tool_calls desde choices[].delta.
  • Responses emite eventos tipados como response.output_text.delta, response.function_call_arguments.delta, response.function_call_arguments.done, response.completed y error.
  • Messages usa message_start, content_block_start, content_block_delta, content_block_stop, message_delta y message_stop; los argumentos llegan fragmentados mediante input_json_delta.partial_json.

Los argumentos pueden dividirse así:

{"plan_
code":"te
am"}

Ninguno de estos fragmentos es JSON válido por sí solo. Hay que acumularlos por ID de llamada o índice de bloque y analizarlos únicamente al recibir el evento de finalización correspondiente:

from __future__ import annotations

import json
from collections import defaultdict
from typing import Any


class ToolArgumentAssembler:
    def __init__(self) -> None:
        self._buffers: dict[str, list[str]] = defaultdict(list)

    def add_delta(self, call_id: str, fragment: str) -> None:
        self._buffers[call_id].append(fragment)

    def finish(self, call_id: str) -> dict[str, Any]:
        if call_id not in self._buffers:
            raise KeyError(f"call_id desconocido: {call_id}")

        raw = "".join(self._buffers.pop(call_id))
        value = json.loads(raw)
        if not isinstance(value, dict):
            raise TypeError("los argumentos de la herramienta deben decodificarse como un objeto")
        return value

    def discard(self, call_id: str) -> None:
        self._buffers.pop(call_id, None)

El adaptador también debe registrar un estado terminal explícito:

created -> receiving -> completed
                   \-> failed
                   \-> disconnected

disconnected no equivale a completed. Anthropic Messages puede emitir un event: error dentro del stream después de que la conexión HTTP ya se haya establecido correctamente; Responses también dispone de eventos de error independientes. Mirar solo el estado HTTP inicial o tratar el cierre de la conexión como una finalización natural puede truncar argumentos o la respuesta final.

El parser debe tolerar tipos de evento desconocidos: registrarlos y omitir los que no afecten a la capacidad actual, en vez de bloquear todo el cliente cada vez que el servidor añada un evento.

El estado multivuelta y el estado de reasoning no pueden inventarse

Chat Completions y los flujos tradicionales de Messages suelen depender de que la aplicación reenvíe el historial. Responses también puede mantener estado del servidor mediante previous_response_id o Conversations. Su concepto de «turno anterior» no es intercambiable.

Cuando un gateway recibe un ID de estado, solo tiene tres estrategias válidas:

  1. Sticky routing: enviar las solicitudes posteriores al mismo upstream que creó el estado.
  2. Reproducción completa: reenviar todos los mensajes, llamadas, resultados y elementos de estado nativo que se puedan reproducir legalmente.
  3. Rechazo explícito: si el upstream de destino no puede continuar, devolver un error diagnosticable y permitir que el cliente reinicie la conversación.

No se debe pasar un previous_response_id de OpenAI a Anthropic ni presentar un ID interno de conversación del gateway como si otro proveedor pudiera entenderlo.

El estado de reasoning tampoco se resuelve renombrando campos:

  • En configuraciones sin estado o con determinadas políticas de retención, Responses puede devolver reasoning Items cifrados que deben reenviarse en la siguiente solicitud.
  • Los flujos de thinking de Anthropic pueden incluir thinking blocks, firmas u otro estado opaco. Al usar herramientas y conversaciones multivuelta deben conservarse según la documentación nativa.
  • En septiembre de 2026, el uso manual de Anthropic thinking.type: "enabled" con budget_tokens está obsoleto en modelos de generación 4.6 y se rechaza en 4.7 y posteriores; los modelos más nuevos usan adaptive thinking y el control de effort correspondiente.

Por eso no puede establecerse una regla permanente que equipare OpenAI reasoning_effort con Anthropic budget_tokens. Una descripción correcta de capacidades debe incluir el modelo de destino, su modo thinking actual y la estrategia de degradación cuando no esté disponible.

Herramientas paralelas: correlacionar por ID, nunca por posición

Un modelo puede solicitar varias herramientas en un turno. Los tiempos de ejecución varían y los resultados pueden llegar en otro orden. El adaptador necesita mantener una relación como esta:

canonical_call_id
  -> provider
  -> provider_call_id
  -> tool_name
  -> validated_arguments
  -> execution_status
  -> result

Al devolver resultados:

  • Chat Completions crea un mensaje role: "tool" por resultado e incluye el tool_call_id correspondiente.
  • Responses crea un Item function_call_output por resultado e incluye el call_id correspondiente.
  • Messages coloca los bloques tool_result en el turno user inmediatamente posterior e incluye en cada uno su tool_use_id.

Durante las pruebas de migración conviene empezar con parallel_tool_calls: false, completar el flujo de una sola herramienta y después activar el paralelismo. En producción, las herramientas con efectos secundarios —enviar correo, cobrar o crear recursos— también necesitan claves de idempotencia. Los reintentos de red, una desconexión del stream o la repetición por el upstream pueden entregar la misma llamada semántica otra vez; el texto generado por el modelo no basta para saber si ya se ejecutó.

Por qué HTTP 200 no basta para validar compatibilidad

Una prueba útil de migración debe cubrir, como mínimo, estos caminos:

PruebaCriterio de aprobación
Texto normalEl contenido es legible y el alcance system/developer se comporta como se espera
Una llamada a herramientaNombre, argumentos, ID, resultado y respuesta final forman un ciclo completo
Llamadas paralelasCada resultado se asocia por ID sin cruces ni pérdidas
Argumentos en streamingLos fragmentos se ensamblan por completo y el JSON se analiza al finalizar
Schema estricta de herramientaLos campos y tipos no válidos se rechazan como se espera o se degradan de forma explícita
Salida estructurada finalLa respuesta cumple la Schema indicada, no solo «parece JSON»
Error de ejecuciónEl modelo recibe un error estructurado y no entra en bucle ni inventa éxito
Continuidad multivueltaEl segundo turno puede citar hechos del primero y las reglas de estado están claras
reasoning/thinkingEl modo declarado funciona y el estado nativo no se elimina ni se fabrica
Errores dentro del stream y desconexionesEl cliente distingue finalización, fallo e interrupción
Error controlado de APITipo de error, request ID y política de reintento siguen siendo diagnosticables

Usa entradas y fixtures fijos y registra por separado, para cada protocolo:

  • si el resultado de negocio final es equivalente;
  • si la llamada y la devolución del resultado están completas;
  • latencia P50 y P95;
  • usage de input, output y cache;
  • tipo de error, request ID y estado terminal;
  • qué capacidades se han degradado explícitamente.

No registres API Keys, prompts sensibles completos ni salidas privadas. Como mínimo, los logs de error deben conservar HTTP status, upstream error type/code, un mensaje breve, request ID, endpoint, protocolo, Model ID y estado terminal del stream. De lo contrario, model_not_found, falta de permisos y rutas incompatibles pueden convertirse en un único 400 imposible de diagnosticar.

Diagnóstico por síntomas: dónde se rompió el agente

SíntomaCausa habitualDiagnóstico y solución
Devuelve 200, pero el modelo nunca llama a una herramientaNo se envió la definición, se ignoró tool_choice, el modelo no admite herramientas o el prompt no es suficienteImprimir la solicitud final; comprobar el modelo y la capa compatible; dejar una única herramienta de solo lectura y forzar o pedir explícitamente su uso
El modelo devuelve una llamada, pero la aplicación no la ejecutaLa aplicación sigue leyendo un campo antiguo, como solo message.contentLeer tool_calls, el Item function_call o el bloque tool_use según el protocolo
Falla el análisis del JSON de argumentosSe trató un fragmento de streaming como JSON completo o se intentó analizar de nuevo un objeto como cadenaEsperar al evento de finalización; determinar primero si el valor es cadena u objeto
Aparecen campos adicionales pese a strictLa capa compatible lo ignora, la Schema no cumple el modo estricto o no se está llamando al endpoint nativoVerificar endpoint y documentación; establecer strict explícitamente; añadir una prueba de regresión que infrinja a propósito la Schema
La segunda solicitud indica que falta el resultadoEl ID no coincide o no se conservó el primer Item assistant/toolGuardar la llamada upstream y su ID sin cambios; devolver el resultado justo donde lo exige el protocolo
Messages devuelve tool_use ids ... without tool_resulttool_result no sigue inmediatamente a la llamada o se insertó texto antesColocar todos los tool_result correspondientes en el siguiente mensaje user y antes del texto opcional
El streaming se detiene o solo entrega media lista de argumentosEl cliente espera únicamente un marcador de fin de texto y no trata estados terminales de argumentos o erroresImplementar una máquina de eventos separada para cada protocolo y distinguir completed, failed, error y disconnected
El segundo turno olvida el primeroSe omitieron historial, llamada o result Items; o previous_response_id pertenece a otro upstreamReenviar el contexto visible completo o mantener sticky routing; no transferir ID de estado entre proveedores
Una instrucción system empieza a aplicarse demasiado pronto al cambiar de interfazLa capa compatible elevó un system/developer intermedio al principioModelar el alcance; degradar de forma visible o mantener el protocolo nativo cuando el mapeo sin pérdidas sea imposible
La herramienta se ejecuta dos vecesReintento de solicitud, repetición tras desconexión o falta de idempotenciaUsar herramientas de solo lectura en pruebas; generar la clave de idempotencia de herramientas con efectos secundarios a partir de un canonical call ID
La salida final es JSON, pero a veces faltan camposEl prompt solo pide «devuelve JSON» y no activa salida estructuradaUsar response_format, text.format u output_config.format según la interfaz y volver a validar en la aplicación

Secuencia de migración más segura

  1. Identifica el protocolo que el cliente envía realmente. No lo deduzcas del nombre del modelo. Registra el endpoint completo, el método del SDK, los campos superiores y los tipos de eventos del stream.
  2. Enumera los comportamientos que deben mantenerse. Como mínimo: herramientas, llamadas paralelas, Schema estricta, salida estructurada final, estado multivuelta, streaming y thinking/reasoning.
  3. Prioriza el protocolo nativo. Si una capacidad puede implementarse directamente con Messages o Responses nativos, evita una capa compatible adicional.
  4. Crea una matriz de capacidades de conversión. Marca cada función como compatible por completo, compatible con pérdidas o no compatible, y haz visible el resultado al caller.
  5. Ejecuta un ciclo completo de dos solicitudes con un fixture sin efectos secundarios. No basta con comprobar que una solicitud devuelve texto; ejecuta la herramienta y devuelve su resultado.
  6. Después prueba paralelismo, streaming y rutas de error. Solo cuando pase el flujo normal deben activarse herramientas con efectos secundarios y tráfico real.
  7. Aumenta el tráfico gradualmente y compara métricas. Observa corrección, latencia, usage, errores y ejecuciones duplicadas, no solo la tasa de HTTP satisfactorio.

Cómo elegir el punto de entrada de BetterToken

BetterToken ofrece rutas distintas para clientes distintos. El protocolo sigue determinado por el wire contract que use realmente el cliente:

  • Chat Completions: la URL completa es https://www.bettertoken.ai/v1/chat/completions. Para SDK o herramientas que añaden la ruta automáticamente, Base URL suele ser https://www.bettertoken.ai/v1. Consulta la referencia de Chat Completions API.
  • Codex / Responses: la documentación actual de Codex usa base_url = "https://www.bettertoken.ai/v1" y wire_api = "responses"; Codex añade /responses. Consulta la guía de Codex.
  • Claude Code / Messages: la documentación actual usa ANTHROPIC_BASE_URL=https://bettertoken.ai, sin /v1 al final de Base URL; el cliente añade /v1/messages. Consulta la guía de Claude Code.

Usar el mismo Dashboard, API Key o nombre de modelo no convierte tres protocolos en un solo formato. Para una herramienta existente, elige el protocolo que espera. Para un agente propio, valida las capacidades necesarias con los ciclos completos y la matriz de aceptación de este artículo.

Preguntas frecuentes

¿OpenAI-compatible equivale a una réplica completa de OpenAI API?

No. Normalmente significa que ciertos endpoints y estructuras pueden utilizarse desde clientes de estilo OpenAI. Modelos, parámetros, eventos de streaming, herramientas, salida estructurada, herramientas alojadas y semántica de errores deben comprobarse por separado.

¿Basta con sustituir Base URL y API Key?

A veces, para solicitudes de texto simples que ya usan el mismo wire contract. Un agente con herramientas aún exige validar definiciones, devolución de resultados en la segunda solicitud, eventos de streaming, Schema estricta, estado y errores. Si el cliente espera Responses, /chat/completions no basta; si espera Messages, un endpoint de estilo OpenAI no se adapta automáticamente.

¿Puede un adaptador universal convertir los tres protocolos?

Puede cubrir texto ordinario y parte del ciclo de funciones, pero no debe prometer compatibilidad completa sin pérdidas. El estado gestionado por el proveedor, las herramientas alojadas, el estado opaco de thinking/reasoning, determinados alcances system y funciones específicas del modelo suelen carecer de equivalente universal. El adaptador debe exponer una matriz de capacidades y la información de degradación.

¿Por qué pasan los unit tests pero falla el agente real?

Muchas pruebas solo simulan la primera respuesta. No validan la devolución del resultado en la segunda solicitud, las llamadas paralelas, los fragmentos en streaming ni la continuidad de estado. Amplía la prueba a «solicitud del usuario → llamada del modelo → ejecución en la aplicación → devolución del resultado → respuesta final» para descubrir fallos reales del protocolo.

¿Conviene migrar primero Chat Completions o Responses?

Una aplicación estable en Chat Completions puede seguir funcionando y migrar capacidad por capacidad según el valor de negocio. Un agente nuevo de OpenAI, o uno que necesite Items tipados, herramientas alojadas o estado de Responses, es mejor construirlo directamente sobre Responses. Los criterios son las capacidades y el coste de migración, no si el nombre de la interfaz parece más reciente.

¿Quieres optimizar tu flujo de trabajo con LLM?

Conecta modelos mediante una API, gestiona claves y controla el gasto en IA.

Empezar gratis