cv.cm

IA · IA para desarrolladores

¿Cómo se llama a una API de LLM?

10 min de lectura

Respuesta corta

Envía una petición HTTPS POST con tu clave de API en una cabecera y un cuerpo JSON que indique el modelo y la lista de mensajes. La respuesta es un JSON con el texto generado y el recuento de tokens. Muchos proveedores aceptan el formato de chat completions al estilo de OpenAI, mientras que Anthropic y Gemini tienen además sus propios formatos. Guarda la clave en tu servidor y paga por token.

Abrir Formateador JSON

Qué ocurre en una llamada a la API

Una API de LLM es un servicio web. Tu programa envía texto, los servidores del proveedor ejecutan el modelo y el resultado vuelve como JSON. No hay memoria entre llamadas: cada petición debe incluir toda la conversación que el modelo debe ver.

Necesitas tres cosas: una cuenta con una clave de API, la URL del endpoint y un ID de modelo de la lista de modelos del proveedor. En los ejemplos de abajo, MODEL_ID es un marcador de posición. Los nombres de los modelos cambian a menudo, así que copia el ID actual de la documentación del proveedor y no de una entrada de blog.

Una petición mínima en curl, Python y JavaScript

El formato de chat completions, publicado por primera vez por OpenAI, es la estructura de petición más copiada. Muchos otros proveedores y servidores locales lo aceptan, a veces solo con una URL base distinta. Esta es la petición útil más pequeña:

curl https://api.openai.com/v1/chat/completions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer $OPENAI_API_KEY" \
  -d '{
    "model": "MODEL_ID",
    "messages": [
      {"role": "system", "content": "You answer in one short paragraph."},
      {"role": "user", "content": "What is a context window?"}
    ]
  }'

La lista messages contiene la conversación. Cada mensaje tiene un role y un content. El mensaje system fija las reglas de la respuesta (los modelos más nuevos de OpenAI también aceptan el nombre developer para este rol). Los mensajes user son las peticiones y los mensajes assistant son respuestas anteriores, que reenvías para continuar una conversación. Una respuesta abreviada tiene este aspecto:

{
  "id": "chatcmpl-...",
  "object": "chat.completion",
  "model": "MODEL_ID",
  "choices": [
    {
      "index": 0,
      "message": {"role": "assistant", "content": "A context window is ..."},
      "finish_reason": "stop"
    }
  ],
  "usage": {"prompt_tokens": 27, "completion_tokens": 64, "total_tokens": 91}
}

El texto está en choices[0].message.content. El objeto usage cuenta los tokens que se te facturan. finish_reason indica por qué se detuvo el modelo: stop significa que terminó, mientras que length significa que alcanzó tu límite de tokens y la respuesta quedó cortada.

En Python, la misma llamada con tiempo de espera y reintentos tiene este aspecto. Usa la biblioteca común requests. Los SDK oficiales de cada proveedor hacen el mismo trabajo y añaden los reintentos por ti.

import os, random, time
import requests

URL = "https://api.openai.com/v1/chat/completions"
API_KEY = os.environ["OPENAI_API_KEY"]   # read from the server environment

def chat(messages, model="MODEL_ID", max_attempts=5):
    body = {"model": model, "messages": messages,
            "max_completion_tokens": 500}   # many compatible APIs: "max_tokens"
    headers = {"Authorization": f"Bearer {API_KEY}"}
    for attempt in range(max_attempts):
        last = attempt == max_attempts - 1
        try:
            r = requests.post(URL, headers=headers, json=body, timeout=60)
        except (requests.Timeout, requests.ConnectionError):
            if last:
                raise
            time.sleep(2 ** attempt + random.random())   # network error: retry
            continue
        if r.status_code == 429 or r.status_code >= 500:
            if last:
                break
            backoff = 2 ** attempt + random.random()      # exponential + jitter
            try:
                wait = max(float(r.headers.get("Retry-After", 0)), backoff)
            except ValueError:
                wait = backoff
            time.sleep(wait)
            continue
        r.raise_for_status()                # 400, 401, 403: fix the request, do not retry
        data = r.json()
        return data["choices"][0]["message"]["content"], data["usage"]
    raise RuntimeError("API still failing after retries")

text, usage = chat([{"role": "user", "content": "What is a context window?"}])
print(text)
print(usage["prompt_tokens"], "in /", usage["completion_tokens"], "out")

Los proveedores difieren: compatible con OpenAI, Anthropic y Gemini

La estructura de chat completions es común, pero no universal. La Messages API de Anthropic y la API de Gemini de Google tienen sus propios formatos nativos. Ambos proveedores publican también opciones de compatibilidad, pero sus API nativas exponen primero todas las funciones. Las diferencias principales:

ElementoChat completions al estilo OpenAIMessages API de AnthropicAPI de Gemini (nativa)
EndpointPOST /v1/chat/completionsPOST /v1/messagesPOST /v1beta/models/{model}:generateContent
Cabecera de la clave de APIAuthorization: Bearer <key>x-api-key más una cabecera obligatoria de fecha anthropic-versionx-goog-api-key
Instrucciones del sistemaUn mensaje system o developerUn campo system de nivel superiorUn campo systemInstruction de nivel superior
Campos obligatoriosmodel, messagesmodel, max_tokens, messagescontents (el modelo va en la URL)
Texto de la respuestachoices[0].message.contentBloques de texto en el array contentcandidates[0].content.parts[].text
Streaming"stream": true, eventos enviados por el servidor que terminan con [DONE]"stream": true, eventos enviados por el servidor con nombreEl método streamGenerateContent con alt=sse

Gemini ofrece una URL base compatible con OpenAI, y muchos otros proveedores, entre ellos DeepSeek y varios alojadores chinos y de modelos abiertos, aceptan el formato de chat completions. La compatibilidad suele ser parcial: funciones avanzadas como la salida estructurada, la llamada a herramientas o la entrada de imágenes pueden diferir. Lee la página de compatibilidad de cada proveedor antes de cambiar solo con modificar la URL.

Guarda la clave de API en el servidor

Cualquiera que tenga tu clave de API puede gastar tu dinero. No la pongas nunca en JavaScript del navegador, en una aplicación móvil, en un repositorio público ni en un archivo de entorno del front-end que se incluya en la página. Todo eso puede leerlo cualquier usuario.

  1. Guarda la clave en una variable de entorno o en un gestor de secretos de tu servidor.
  2. Haz que tu aplicación web o móvil llame a un endpoint de tu propio backend.
  3. En el backend, comprueba al usuario (inicio de sesión, límite de peticiones por usuario, longitud de la entrada) y llama entonces a la API del LLM con la clave.
  4. Devuelve al cliente solo el resultado.
  5. Fija un límite de gasto mensual en el panel del proveedor, usa claves distintas para cada proyecto y rota una clave de inmediato si se filtra.

Streaming: muestra la respuesta a medida que se escribe

Una respuesta larga puede tardar muchos segundos. Con "stream": true, el servidor envía pequeños fragmentos como eventos enviados por el servidor, de modo que tu interfaz puede mostrar el texto según va apareciendo. Cada línea de evento empieza con data: y contiene un fragmento con un delta. El formato de OpenAI termina con data: [DONE]. Este ejemplo se ejecuta en un servidor Node.js:

// Node.js 18+ (server side). Prints the reply as it arrives.
const res = await fetch("https://api.openai.com/v1/chat/completions", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: `Bearer ${process.env.OPENAI_API_KEY}`,
  },
  body: JSON.stringify({
    model: "MODEL_ID",
    stream: true,
    messages: [{ role: "user", content: "Write a haiku about rain." }],
  }),
});
if (!res.ok) throw new Error(`HTTP ${res.status}: ${await res.text()}`);

const decoder = new TextDecoder();
let buffer = "";
for await (const chunk of res.body) {
  buffer += decoder.decode(chunk, { stream: true });
  const lines = buffer.split("\n");
  buffer = lines.pop();                       // keep a partial line for later
  for (const line of lines) {
    if (!line.startsWith("data: ")) continue;
    const data = line.slice(6);
    if (data === "[DONE]") continue;          // end of stream
    const delta = JSON.parse(data).choices[0]?.delta?.content;
    if (delta) process.stdout.write(delta);
  }
}

Para mostrar el stream en un navegador, reenvía los fragmentos desde tu servidor a la página. No llames al proveedor desde el navegador con tu clave. En el formato de OpenAI, el uso no se incluye en los fragmentos transmitidos a menos que establezcas stream_options con include_usage, así que actívalo si necesitas el recuento de tokens para la facturación.

Obtener JSON en lugar de prosa

Cuando el código vaya a leer la respuesta, pide JSON. Hay dos niveles. El modo JSON ("response_format": {"type": "json_object"}) promete un JSON válido, pero no qué campos tendrá. La salida estructurada con un esquema JSON (JSON Schema) hace que el modelo siga tus campos y tipos exactos, en los modelos que la admiten:

{
  "model": "MODEL_ID",
  "messages": [
    {"role": "user", "content": "Extract the total and currency: 'Amount due: 1.250,00 EUR'"}
  ],
  "response_format": {
    "type": "json_schema",
    "json_schema": {
      "name": "invoice_total",
      "strict": true,
      "schema": {
        "type": "object",
        "properties": {
          "total": {"type": "number"},
          "currency": {"type": "string"}
        },
        "required": ["total", "currency"],
        "additionalProperties": false
      }
    }
  }
}

Incluso con un esquema, valida en tu código el resultado analizado. Los valores pueden seguir siendo erróneos, por ejemplo un número mal leído, aunque la forma sea correcta. Anthropic y Gemini admiten esquemas y definiciones de herramientas en sus propios formatos. Cuando depures respuestas JSON largas, un formateador como la herramienta json de cv.cm facilita la lectura de la salida anidada.

Límites de tasa, reintentos y errores

Los proveedores limitan tanto las peticiones por minuto (RPM) como los tokens por minuto (TPM), y se aplica el que alcances primero. Cuando superas un límite, recibes el estado HTTP 429. Trata los errores según su tipo:

EstadoSignificadoQué hacer
400Petición incorrecta: campo erróneo, demasiados tokensCorrige la petición. No reintentes.
401 / 403Clave ausente, incorrecta o sin autorizaciónComprueba la clave y los permisos. No reintentes.
429Límite de tasa o, a veces, cuota agotadaEspera y reintenta con backoff. Si menciona cuota o facturación, detente y arregla la cuenta.
500 / 503Error del servidor o sobrecargaReintenta con backoff.
TimeoutNo hay respuesta a tiempoReintenta una o dos veces. Usa streaming para respuestas largas.

Reintenta con backoff exponencial: espera aproximadamente 1 segundo, luego 2, 4, 8, más un pequeño retardo aleatorio (jitter) para que muchos clientes no reintenten en el mismo instante. Si la respuesta incluye una cabecera Retry-After, espera al menos ese tiempo. Limita el número de intentos. Las peticiones fallidas también pueden contar para tu límite, así que reintentar al instante en un bucle cerrado empeora las cosas. Si usas un SDK oficial, recuerda que ya reintenta algunos errores.

El coste se cuenta en tokens

Pagas por los tokens de entrada (todo lo que envías, incluidos el prompt del sistema y todo el historial de la conversación) y por los tokens de salida (lo que escribe el modelo). Los tokens de salida suelen tener un precio mayor que los de entrada. La fórmula es sencilla:

cost = input_tokens  / 1,000,000 * input_price_per_million
     + output_tokens / 1,000,000 * output_price_per_million

Example with made-up prices (2.00 in, 8.00 out per million tokens):
  27 input + 64 output tokens
  = 0.000054 + 0.000512 = 0.000566 per call
  x 100,000 calls a month = about 56.60
  • Lee los recuentos reales de usage en cada respuesta y regístralos. No los estimes a partir del número de palabras.
  • Limita la longitud de cada respuesta: max_completion_tokens en OpenAI (el antiguo max_tokens está obsoleto allí), max_tokens en Anthropic y en muchas API compatibles.
  • El historial del chat crece con cada turno, así que el mensaje número 20 cuesta más que el primero. Recorta o resume los turnos antiguos.
  • Usa un modelo más pequeño para tareas sencillas como la clasificación, y uno más grande solo donde la calidad lo requiera.
  • Algunos proveedores aplican descuento a los prefijos de prompt repetidos (prompt caching) u ofrecen un procesamiento por lotes más barato para trabajos que pueden esperar.

Preguntas frecuentes

¿Puedo llamar a una API de LLM directamente desde el navegador?

No de forma segura con tu propia clave, porque cualquier visitante puede leerla en la página. Llama a tu propio servidor, que añade la clave. La excepción es una clave que el usuario introduce para su propia cuenta, o un modelo que se ejecuta íntegramente en el navegador.

¿Por qué la API olvida lo que dije antes?

La API no guarda estado. Cada petición debe incluir los mensajes anteriores que quieres que vea el modelo. Las aplicaciones de chat almacenan el historial y lo reenvían cada vez.

¿Qué hace la temperatura y debería configurarla?

La temperatura controla lo aleatoria que es la elección de palabras: más baja es más predecible, más alta es más variada. Algunos proveedores y modelos nuevos restringen o eliminan los parámetros de muestreo, así que consulta la referencia de la API antes de configurarla.

¿Por qué mi respuesta se corta a la mitad?

La respuesta alcanzó el límite de tokens que fijaste o la salida máxima del modelo. Comprueba finish_reason (o stop_reason en Anthropic). Sube el límite o pide una respuesta más corta.

¿Existe una API de LLM gratuita?

Varios proveedores ofrecen planes gratuitos o créditos de prueba con límites de tasa bajos, y los modelos abiertos pueden ejecutarse en tu propio equipo detrás de un servidor local compatible con OpenAI. Los planes gratuitos y sus condiciones cambian a menudo, así que consulta la página actual del proveedor.

Todos los tutoriales de IA