Modelos de LLM y APIs
Qué es un modelo de lenguaje grande (LLM)
Cómo los modelos de lenguaje grandes convierten texto en tokens, predicen el siguiente y por qué suenan seguros cuando se equivocan.
Cómo es una solicitud a una API de chat: modelo, mensajes, tokens, streaming, claves de API, límites y errores HTTP, con ejemplos anotados.
Una API de LLM es un endpoint web que recibe texto y devuelve la respuesta de un modelo. Tu programa envía una solicitud HTTP con el nombre del modelo, una lista de mensajes y un límite de tokens. El servicio devuelve el texto generado, el motivo por el que se detuvo y el recuento de tokens que leyó y escribió. Una aplicación de chat hace el mismo trabajo detrás de un cuadro de texto. La API te entrega las piezas en bruto, así que tu código decide qué entra y qué se hace con la respuesta.
En una aplicación de chat, el modelo parece un interlocutor. Por debajo, cada mensaje que envías es una solicitud HTTP completa. La aplicación reúne los mensajes anteriores, añade el nuevo, envía todo a un servidor y muestra el texto que regresa. El modelo no guarda memoria de tu último mensaje, salvo que la aplicación vuelva a enviar el historial.
La API se salta la interfaz. Tu código arma la lista de mensajes, define el límite de tokens y procesa la respuesta. El servicio no distingue si quien llama es un navegador, un script o una tarea programada. Solo verifica el formato de la solicitud y la clave.
Tres términos importan desde el principio:
Una ventana de chat sirve para que una persona haga preguntas. El código necesita entradas repetibles, salidas que el programa pueda interpretar y errores que pueda capturar. Cuando un script llama a un modelo, la clave, la factura y los modos de fallo pasan a ser tu responsabilidad. La mayoría de los problemas de una primera integración vienen de un cuerpo mal formado, de una clave ausente o incorrecta, de un límite que nadie revisó o de una respuesta tratada como si siempre hubiera salido bien.
Casi todas las API de LLM que vas a encontrar son API web sobre HTTP. La solicitud tiene método, URL, encabezados y, normalmente, un cuerpo JSON. La llamada al modelo casi siempre es un POST. La RFC 9110, la norma vigente de semántica de HTTP, define qué significa cada parte y cómo debe responder el servidor. Los códigos de estado y los encabezados que verás más adelante vienen de ella.
El cuerpo es un objeto JSON. Como mínimo, nombra el modelo y envía la conversación hasta ese punto. Este es el formato de mensajes, uno de los dos estilos de solicitud comparados más abajo. Los valores son marcadores, y aquí no se ejecuta nada.
{
"model": "<MODEL_ID_FROM_CURRENT_CATALOG>",
"max_tokens": 300,
"system": "You explain security concepts in plain English. Keep answers under 120 words.",
"messages": [
{
"role": "user",
"content": "What is the difference between hashing and encryption?"
}
],
"stream": false
}Anotaciones:
model es el identificador del modelo que genera la respuesta. Cópialo de la lista actual del proveedor, no de un tutorial viejo.max_tokens limita cuántos tokens puede escribir el modelo. No limita tu prompt.system guarda las instrucciones permanentes. En este formato va fuera de la lista de mensajes.messages guarda la conversación. Aquí hay un único turno de usuario.stream está en false, así que el servidor espera a tener la respuesta completa antes de contestar.Cada mensaje tiene un rol que indica quién habla:
user: el texto de tu usuario final o de tu programa.assistant: una respuesta que el modelo escribió antes. La devuelves para continuar la conversación.system o developer: instrucciones permanentes. El nombre depende del formato.tool: el resultado de una llamada a una herramienta, tema de un artículo posterior.Para continuar una conversación, añade la última respuesta del modelo como mensaje assistant y luego el nuevo mensaje user. Si olvidas los turnos anteriores, el modelo responde a la pregunta nueva sin contexto, y nada avisa del error. Es uno de los bugs más comunes.
max_tokens (o su equivalente) limita cuánto escribe el modelo. Si la respuesta llega a ese límite, el texto se corta a mitad de una frase y la respuesta lo indica. Revisa el motivo de parada, descrito en el paso 6, en lugar de asumir que cada respuesta está completa. El límite no reduce lo que pagas por el prompt, porque los tokens de entrada siguen contando para el uso.
Con "stream": true, el servidor envía la respuesta en fragmentos a medida que la genera, mediante eventos enviados por el servidor (server-sent events). El usuario ve las palabras aparecer una tras otra. Tu código lee los eventos y une los fragmentos, y los datos de uso y el motivo de parada llegan en los eventos finales. Usa streaming cuando una persona está viendo la salida. Para un proceso por lotes que guarda la respuesta, una solicitud normal es más sencilla de escribir y de probar.
Una respuesta típica contiene:
choices.Lee el motivo de parada antes de confiar en el texto. Una respuesta que se detuvo en el límite de tokens está incompleta. Una respuesta que termina en una llamada a herramienta no es la respuesta final, y tu código debe ejecutar la herramienta y devolver el resultado.
El objeto de uso determina tu factura. Registra los conteos, el ID del modelo, la hora, el motivo de parada y el código de estado. No registres prompts ni respuestas por defecto, porque pueden contener datos de clientes, código privado o secretos. Los metadatos bastan para detectar picos de costo y fallos.
Dos estilos de solicitud se usan mucho en la documentación pública. Muchos servicios copian uno de ellos para que el código cliente existente siga funcionando con otra URL base. Conocer ambos te ayuda a leer ejemplos de cualquier fuente.
El primero es el formato de mensajes, documentado en la primera referencia de Fuentes. Recibe system como campo de primer nivel, exige max_tokens, devuelve bloques de contenido con stop_reason e informa input_tokens y output_tokens.
El segundo es el formato de chat completions, documentado en la segunda referencia de Fuentes. Pone las instrucciones de sistema o de developer dentro de messages, acepta max_completion_tokens y devuelve un arreglo choices, en el que cada elemento trae un finish_reason y un message. El objeto de uso informa prompt_tokens, completion_tokens y total_tokens.
| Tema | Formato de mensajes | Formato de chat completions |
|---|---|---|
| Instrucciones permanentes | Campo system de primer nivel |
Mensaje system o developer dentro de messages |
| Campos obligatorios | model, messages, max_tokens |
model, messages |
| Límite de salida | max_tokens (obligatorio) |
max_completion_tokens (opcional) |
| Dónde está la respuesta | Lista content de bloques |
choices[0].message |
| Por qué se detuvo | stop_reason |
choices[0].finish_reason |
| Conteo de tokens | usage.input_tokens, usage.output_tokens |
usage.prompt_tokens, usage.completion_tokens, usage.total_tokens |
| Streaming | stream: true, eventos enviados por el servidor |
stream: true, fragmentos en eventos enviados por el servidor |
Las diferencias están en la forma, no en el significado. Ambos envían modelo, conversación y límite, y ambos devuelven texto, motivo y conteos. Cuando un ejemplo use el otro formato, relaciona los campos por el papel que cumplen en esta tabla, en lugar de copiar el JSON.
La misma pregunta en el formato de chat completions:
{
"model": "<MODEL_ID_FROM_CURRENT_CATALOG>",
"max_completion_tokens": 300,
"messages": [
{
"role": "developer",
"content": "You explain security concepts in plain English. Keep answers under 120 words."
},
{
"role": "user",
"content": "What is the difference between hashing and encryption?"
}
],
"stream": false
}Una respuesta en este formato tiene la siguiente forma. Los valores son marcadores, no una respuesta observada:
{
"id": "<RESPONSE_ID>",
"object": "chat.completion",
"model": "<MODEL_ID_FROM_CURRENT_CATALOG>",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "<GENERATED_TEXT>" },
"finish_reason": "stop"
}
],
"usage": {
"prompt_tokens": 0,
"completion_tokens": 0,
"total_tokens": 0
}
}Tu código lee choices[0].message.content, choices[0].finish_reason y después el objeto de uso. El formato de mensajes guarda la misma información en content, stop_reason y usage, con otros nombres.
Una clave de API identifica la cuenta que paga la llamada y define qué modelos y límites se aplican. Los dos formatos la envían de forma distinta. El formato de chat completions usa un encabezado Authorization con el esquema Bearer. El formato de mensajes usa un encabezado propio para la clave y además exige un encabezado de versión de la API, para que el proveedor mantenga estable el comportamiento de versiones anteriores.
Los nombres de los encabezados cambian según el proveedor y la versión, así que cópialos de la documentación actual. Las formas se ven así:
# Placeholder shape only. Do not run this with a real key in a shared shell history.
curl https://<API_HOST>/<MESSAGES_PATH> \
-H "x-api-key: <YOUR_API_KEY>" \
-H "anthropic-version: <VERSION_FROM_CURRENT_DOCS>" \
-H "content-type: application/json" \
-d '{"model":"<MODEL_ID>","max_tokens":300,"messages":[{"role":"user","content":"Hello"}]}'# Placeholder shape only. Same caution as above.
curl https://<API_HOST>/<CHAT_COMPLETIONS_PATH> \
-H "Authorization: Bearer <YOUR_API_KEY>" \
-H "Content-Type: application/json" \
-d '{"model":"<MODEL_ID>","messages":[{"role":"user","content":"Hello"}]}'Anotaciones:
Authorization: Bearer es el esquema estándar de HTTP para tokens. La RFC 9110 define el encabezado Authorization en su sección de autenticación.Aquí empieza la mayor parte de los costos y las filtraciones evitables. Las reglas siguientes siguen la OWASP Secrets Management Cheat Sheet.
Mantén las claves fuera del código fuente. Una clave escrita en un archivo Python existe en cada copia de ese archivo: tu portátil, las máquinas del equipo, el sistema de CI y cada commit que lo tocó. La OWASP señala los secretos fijos en el código y en los archivos de configuración como una práctica arriesgada.
Nunca hagas commit de una clave. Git conserva el historial. Borrar la clave en un commit posterior no la quita de la versión anterior. La OWASP indica que quitar un secreto del historial puede exigir reescribirlo, y eso tiene efectos secundarios. Trata una clave que llegó a un repositorio compartido como expuesta. Revócala, crea una nueva y revisa la cuenta por si hay uso que no hiciste.
Mantén las claves fuera de logs, capturas de pantalla y código del navegador. Cualquier cosa en una línea de log, en un mensaje de error o en un script de página puede ser leída por otros. Las llamadas que necesitan clave deben ejecutarse en un servidor que controles.
No incrustes claves en imágenes de contenedor. La OWASP advierte contra los secretos en instrucciones ENV o ARG de Docker, porque el valor queda en los metadatos de la imagen y en el historial de la construcción.
Prefiere un almacén de secretos. La OWASP dice que las variables de entorno suelen ser accesibles para todos los procesos y pueden aparecer en logs o volcados, así que las recomienda solo cuando no hay mejor opción. En un portátil sirve un gestor de contraseñas o el almacén de credenciales del sistema operativo. En servidores, usa un servicio gestionado de secretos o archivos montados con permisos restringidos.
Limita y rota. Da a cada clave solo el acceso que necesita y reemplázala periódicamente. La OWASP dice que los secretos deben tener el mínimo privilegio necesario y recomienda la rotación automatizada cuando sea posible.
Un patrón local seguro mantiene el valor en el entorno y lo lee por nombre:
# Read the key from the environment. Fail early if it is missing.
import os
api_key = os.environ.get("LLM_API_KEY")
if not api_key:
raise SystemExit("LLM_API_KEY is not set. Set it in your shell or secret store.")Añade los archivos de configuración local a .gitignore antes de crearlos. Haz commit de una plantilla, como .env.example, que liste los nombres de las variables con valores vacíos, y nunca del archivo real.
Los proveedores limitan cuántas solicitudes y cuántos tokens puede usar una cuenta en una ventana de tiempo. Estos límites suelen llamarse límites de tasa (rate limits), y algunas cuentas también tienen un tope de gasto. Las cifras varían según la cuenta, el modelo y el momento, así que consulta la consola del proveedor y su documentación vigente. Este artículo no da ninguna cifra.
Al llegar a un límite, el servidor normalmente responde con 429 Too Many Requests. La RFC 6585 define ese código, que extiende el conjunto básico de HTTP. La respuesta puede traer Retry-After, que la RFC 9110 define en su sección de campos de respuesta y que indica cuánto esperar.
Tu código debe leer el estado antes que el cuerpo, esperar antes de reintentar (usando Retry-After cuando esté presente, o un retardo creciente con algo de variación aleatoria) y detenerse tras unos cuantos intentos, en lugar de reintentar sin fin. Cuenta los tokens antes de enviar prompts muy grandes, porque una solicitud excesiva falla aunque tengas saldo.
El primer dígito de un código de estado indica la clase: un problema del lado del cliente, un fallo del servidor u otra cosa. La RFC 9110 define las clases y los códigos individuales. Estos son los errores que más encuentran las integraciones nuevas.
| Estado | Nombre | Qué suele significar | Qué revisar |
|---|---|---|---|
| 400 | Bad Request | El cuerpo está mal formado o un campo no es válido | Sintaxis JSON, nombres de campos, campos obligatorios, rangos de valores |
| 401 | Unauthorized | La clave falta, está mal formada o fue revocada | Nombre del encabezado, prefijo Bearer, valor de la clave en el entorno |
| 403 | Forbidden | La clave es válida, pero la cuenta puede no tener acceso a ese recurso | Acceso al modelo, permisos de la cuenta, estado de la facturación |
| 404 | Not Found | La ruta está mal, o el ID del modelo no existe para tu cuenta | URL base, ruta, ID exacto del modelo en el catálogo vigente |
| 429 | Too Many Requests | Se alcanzó el límite de tasa o la cuota | Retry-After, página de uso, volumen de solicitudes |
| 500 | Internal Server Error | El servidor falló de su lado | Reintentar con espera creciente y luego revisar la página de estado del proveedor |
| 503 | Service Unavailable | El servicio está saturado o en mantenimiento | Reintentar con espera creciente y un número máximo de intentos |
Un error 4xx significa que la solicitud necesita un cambio, así que reenviar la misma no ayuda. Un 5xx o un 429 pueden funcionar más tarde, por eso tiene sentido esperar. Registra el código de estado, el ID del modelo y el ID de la solicitud. Nunca registres la clave ni el prompt.
Confiar en la primera respuesta. Una respuesta puede cortarse por el límite de tokens o detenerse por otro motivo. Revisa el motivo de parada siempre.
Olvidar los turnos anteriores. Una API sin estado no sabe nada de la solicitud anterior, a menos que la vuelvas a enviar. Construye la lista de mensajes desde tu propio almacenamiento.
Fijar el ID del modelo en el código. Los IDs de modelo se retiran y se renombran. Guárdalos en la configuración, para que el cambio sea editar una línea.
Mezclar los dos formatos. Enviar un cuerpo de chat completions a un endpoint de mensajes, o al revés, produce errores 400. Confirma qué formato documenta el endpoint antes de copiar un ejemplo.
Lee la referencia de un proveedor de principio a fin. Las secciones de solicitud, respuesta y errores son breves y responden a la mayoría de las dudas de la primera semana. Después, lee sobre streaming y ventanas de contexto, que deciden cuánta conversación puedes enviar antes de que una solicitud falle.
El gateway de NotHoly documenta sus propios campos y códigos de error. Compáralos con las dos formas de arriba, y usa su documentación como referencia de su comportamiento.
Si quieres que el modelo llame a tu código, el siguiente paso es el uso de herramientas: un turno del asistente cuyo motivo de parada pide a tu programa ejecutar algo y devolver el resultado. El siguiente artículo cubre ese ciclo.
Retry-After.429 Too Many Requests.Modelos de LLM y APIs
Cómo los modelos de lenguaje grandes convierten texto en tokens, predicen el siguiente y por qué suenan seguros cuando se equivocan.