StackPractices
advanced Por Mathias Paulenko

Guía de la API de OpenAI con ejemplos en Python

Guía de producción de la API de OpenAI: Responses API, chat completions, streaming, function calling, salidas estructuradas, embeddings, fine-tuning, batch jobs, rate limits y control de costes.

Introducción

La API de OpenAI parece sencilla hasta que la ejecutas en producción: el streaming se queda colgado, los rate limits golpean en el peor momento, una integración que “funciona” quema tokens en silencio y la propia API cambia bajo tus pies. El cambio más grande reciente es que OpenAI consolidó todo alrededor de la Responses API, que absorbe chat, llamadas a herramientas y salida estructurada en una sola interfaz — la Assistants API a la que reemplaza está ahora deprecada.

Esta guía cubre ambas superficies con honestidad: la Responses API como opción por defecto para trabajo nuevo, chat completions porque la mayoría del código en producción (y la mayoría de proveedores compatibles) aún lo usan, además de streaming, embeddings, fine-tuning, batch jobs y el manejo de errores que de verdad necesitarás. Los ejemplos son principalmente Python, con variantes en JavaScript donde difieren de forma relevante. Las versiones ejecutables están en el repositorio complementario.

Responses API

La Responses API es la interfaz recomendada actual de OpenAI. Una sola llamada a responses.create maneja chat simple, herramientas integradas, function calling y salida estructurada, y puede mantener estado en el servidor para no reenviar toda la conversación en cada turno.

Respuesta básica

from openai import OpenAI

client = OpenAI()

response = client.responses.create(
    model="gpt-4o",
    input="Explain Python decorators in 2 sentences."
)

print(response.output_text)
print(f"Tokens used: {response.usage.total_tokens}")

output_text aplana la respuesta por ti — sin rebuscar en choices[0].message.content. Para conversaciones multi-turno, encadena respuestas con previous_response_id en lugar de gestionar la lista de mensajes a mano:

first = client.responses.create(
    model="gpt-4o",
    input="You are a Python expert. What is a decorator?"
)

follow_up = client.responses.create(
    model="gpt-4o",
    previous_response_id=first.id,
    input="Show me an example."
)

print(follow_up.output_text)

Salida estructurada con responses.parse

Para salida tipada, responses.parse acepta un modelo Pydantic directamente y valida el resultado:

from pydantic import BaseModel

class Person(BaseModel):
    name: str
    age: int
    email: str
    skills: list[str]

response = client.responses.parse(
    model="gpt-4o",
    input=[
        {"role": "system", "content": "Extract person information."},
        {"role": "user", "content": "Jane Smith, 28, jane@test.com. Skills: Python, SQL, Docker."}
    ],
    text_format=Person,
)

person = response.output_parsed
print(f"Name: {person.name}, Age: {person.age}")

Es el camino más limpio cuando necesitas campos garantizados — sin parsear JSON ni validar esquemas a mano.

Function calling en Responses API

Las function tools tienen aquí una forma aplanada (sin la clave function anidada), y las llamadas vuelven como items en response.output:

tools = [
    {
        "type": "function",
        "name": "get_weather",
        "description": "Get current weather for a location",
        "parameters": {
            "type": "object",
            "properties": {
                "location": {"type": "string", "description": "City name, e.g. 'Madrid, Spain'"},
                "unit": {"type": "string", "enum": ["celsius", "fahrenheit"]}
            },
            "required": ["location"]
        },
        "strict": True
    }
]

response = client.responses.create(
    model="gpt-4o",
    input="What's the weather in Madrid?",
    tools=tools
)

for item in response.output:
    if item.type == "function_call":
        result = get_weather(**json.loads(item.arguments))
        # Devolver el resultado de la herramienta como function_call_output
        follow = client.responses.create(
            model="gpt-4o",
            previous_response_id=response.id,
            input=[{
                "type": "function_call_output",
                "call_id": item.call_id,
                "output": result
            }]
        )
        print(follow.output_text)

Chat Completions

Chat completions no va a desaparecer: es lo que usa la mayoría del código existente, lo que implementan los proveedores compatibles con OpenAI, y sigue perfectamente soportado. Si mantienes una integración existente no hay urgencia por migrar — para proyectos nuevos, prefiere la Responses API de arriba.

Completion básico

from openai import OpenAI

client = OpenAI()

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "You are a helpful assistant."},
        {"role": "user", "content": "Explain Python decorators in 2 sentences."}
    ],
    temperature=0.7,
    max_tokens=200
)

print(response.choices[0].message.content)
print(f"Tokens used: {response.usage.total_tokens}")

Conversación multi-turno

A diferencia de la Responses API, chat completions no tiene estado — envías todo el historial en cada llamada. Mantener la conversación en una lista e ir añadiendo ambos lados es el patrón estándar:

conversation = [
    {"role": "system", "content": "You are a Python expert."},
    {"role": "user", "content": "What is a decorator?"}
]

response = client.chat.completions.create(
    model="gpt-4o",
    messages=conversation
)

assistant_msg = response.choices[0].message.content
conversation.append({"role": "assistant", "content": assistant_msg})

conversation.append({"role": "user", "content": "Show me an example."})
response = client.chat.completions.create(model="gpt-4o", messages=conversation)
print(response.choices[0].message.content)

Ojo con el coste en tokens: las conversaciones largas reenvían todo el historial en cada llamada, así que truncar o resumir el historial se vuelve obligatorio pasadas unas decenas de turnos.

Selección de modelo

Los precios se mueven, así que toma las cifras como orientativas y consulta la página de precios antes de presupuestar:

ModeloMejor paraEntrada ($/1M)Salida ($/1M)
gpt-4oGeneral, multimodal, tareas complejas~2.50~10.00
gpt-4o-miniTareas simples, alto volumen~0.15~0.60
o1Razonamiento multi-paso~15.00~60.00
o1-miniRazonamiento de código/mates, más barato~3.00~12.00
text-embedding-3-smallEmbeddings más baratos~0.02—
text-embedding-3-largeEmbeddings de máxima calidad, 3072 dims~0.13—

Una regla práctica: enruta todo a gpt-4o-mini y sube a gpt-4o solo donde las evaluaciones demuestren que importa. Para profundizar, consulta la guía de optimización de costes de LLM.

Streaming

El streaming no hace el modelo más rápido — la latencia total es la misma — pero el tiempo hasta el primer token baja a unos pocos cientos de milisegundos, que es lo que el usuario percibe. Haz streaming siempre que un humano esté esperando la salida; sáltalo en trabajos en segundo plano donde solo añade complejidad.

Streaming con Server-Sent Events

from openai import OpenAI

client = OpenAI()

stream = client.chat.completions.create(
    model="gpt-4o",
    messages=[{"role": "user", "content": "Write a poem about Python."}],
    stream=True
)

full_text = ""
for chunk in stream:
    if chunk.choices[0].delta.content is not None:
        token = chunk.choices[0].delta.content
        full_text += token
        print(token, end="", flush=True)

print(f"\n\nFull text length: {len(full_text)}")

Streaming en JavaScript

El SDK de Node expone el mismo patrón como un iterador asíncrono:

import OpenAI from "openai";

const client = new OpenAI();

const stream = await client.chat.completions.create({
  model: "gpt-4o",
  messages: [{ role: "user", content: "Explain async programming." }],
  stream: true,
});

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

Streaming con FastAPI

El patrón de producción: envolver el stream del SDK en un generador y devolverlo como StreamingResponse. Ten en cuenta que el endpoint emite chunks con formato SSE, así que el cliente debe parsear las líneas data: — los eventos del SDK de OpenAI no son texto plano.

from fastapi import FastAPI
from fastapi.responses import StreamingResponse
from openai import AsyncOpenAI
import json

app = FastAPI()
client = AsyncOpenAI()

@app.post("/chat/stream")
async def chat_stream(request: dict):
    async def generate():
        stream = await client.chat.completions.create(
            model="gpt-4o",
            messages=[
                {"role": "system", "content": "You are a helpful assistant."},
                {"role": "user", "content": request["message"]}
            ],
            stream=True
        )

        async for chunk in stream:
            if chunk.choices[0].delta.content:
                data = json.dumps({"token": chunk.choices[0].delta.content})
                yield f"data: {data}\n\n"

        yield "data: [DONE]\n\n"

    return StreamingResponse(generate(), media_type="text/event-stream")

Para una implementación SSE completa incluyendo el consumo desde el cliente, consulta la receta sobre streaming de salida de LLM con SSE.

Function Calling

El function calling convierte al modelo en un enrutador: declaras qué funciones existen, el modelo decide cuándo llamarlas y con qué argumentos, y tu código ejecuta el trabajo real. El modelo nunca ejecuta tu código — solo propone llamadas. El ciclo es este:

flowchart diagram: Prompt + defs de tools

Definir funciones en chat completions

from openai import OpenAI
import json

client = OpenAI()

tools = [
    {
        "type": "function",
        "function": {
            "name": "get_weather",
            "description": "Get current weather for a location",
            "parameters": {
                "type": "object",
                "properties": {
                    "location": {
                        "type": "string",
                        "description": "City name, e.g. 'Madrid, Spain'"
                    },
                    "unit": {
                        "type": "string",
                        "enum": ["celsius", "fahrenheit"],
                        "description": "Temperature unit"
                    }
                },
                "required": ["location"]
            }
        }
    }
]

def get_weather(location: str, unit: str = "celsius") -> str:
    # Llamada a API simulada
    return f"Weather in {location}: 22°{unit[0].upper()}, sunny"

function_map = {"get_weather": get_weather}

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

response = client.chat.completions.create(
    model="gpt-4o",
    messages=messages,
    tools=tools
)

tool_calls = response.choices[0].message.tool_calls

if tool_calls:
    messages.append(response.choices[0].message)

    for call in tool_calls:
        func_args = json.loads(call.function.arguments)
        result = function_map[call.function.name](**func_args)

        messages.append({
            "role": "tool",
            "tool_call_id": call.id,
            "content": result
        })

    # Respuesta final con los resultados de las funciones
    final = client.chat.completions.create(
        model="gpt-4o",
        messages=messages,
        tools=tools
    )
    print(final.choices[0].message.content)

Dos detalles de producción que la gente se salta: valida siempre los argumentos que devuelve el modelo (es una sugerencia, no una garantía), y acota el bucle — un modelo que sigue proponiendo tool calls puede quemar tokens indefinidamente. La receta de function calling con salida estructurada cubre setups multi-tool en profundidad.

Salidas estructuradas

Existen dos niveles de garantía. El JSON mode fuerza JSON sintácticamente válido pero no dice nada sobre la forma. Las salidas estructuradas con json_schema imponen tu esquema exacto — nombres de campos, tipos, claves requeridas — que es lo que quieres cuando el código posterior consume el resultado.

JSON Mode

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "You are a data extractor. Return valid JSON only."},
        {"role": "user", "content": "Extract name, age, and email from: John Doe, 30, john@example.com"}
    ],
    response_format={"type": "json_object"}
)

import json
data = json.loads(response.choices[0].message.content)
print(data)  # {"name": "John Doe", "age": 30, "email": "john@example.com"}

Salidas estructuradas con esquema

from pydantic import BaseModel

class Person(BaseModel):
    name: str
    age: int
    email: str
    skills: list[str]

response = client.chat.completions.create(
    model="gpt-4o",
    messages=[
        {"role": "system", "content": "Extract person information."},
        {"role": "user", "content": "Jane Smith, 28, jane@test.com. Skills: Python, SQL, Docker."}
    ],
    response_format={
        "type": "json_schema",
        "json_schema": {
            "name": "person",
            "schema": {
                "type": "object",
                "properties": {
                    "name": {"type": "string"},
                    "age": {"type": "integer"},
                    "email": {"type": "string"},
                    "skills": {"type": "array", "items": {"type": "string"}}
                },
                "required": ["name", "age", "email", "skills"],
                "additionalProperties": False
            },
            "strict": True
        }
    }
)

person = Person(**json.loads(response.choices[0].message.content))
print(f"Name: {person.name}, Age: {person.age}, Skills: {person.skills}")

En la Responses API, responses.parse con text_format (mostrado arriba) te da la misma garantía con menos boilerplate. Uses lo que uses, additionalProperties: false y strict: true son lo que realmente impone el esquema — omítelos y el modelo puede añadir campos que no pediste.

Embeddings

Los embeddings convierten texto en vectores para comparar significado numéricamente — búsqueda, retrieval para RAG, deduplicación y clustering se construyen sobre ellos. La lección barata que la mayoría de equipos aprende tarde: los embeddings son casi gratis por llamada, pero se vuelven caros a escala de corpus, así que las dimensiones y el tamaño de batch importan.

Generar embeddings

response = client.embeddings.create(
    model="text-embedding-3-small",
    input=["Python is a programming language", "Java is also a programming language"]
)

embeddings = [d.embedding for d in response.data]
print(f"Embedding dimensions: {len(embeddings[0])}")  # 1536

# Similitud coseno
import numpy as np
sim = np.dot(embeddings[0], embeddings[1]) / (
    np.linalg.norm(embeddings[0]) * np.linalg.norm(embeddings[1])
)
print(f"Similarity: {sim:.4f}")

Reducción de dimensionalidad

El parámetro dimensions cambia un poco de recall por ahorro real en almacenamiento y tamaño del índice — útil cuando embebes millones de documentos:

# Reducir de 3072 a 256 dimensiones (ahorro de almacenamiento)
response = client.embeddings.create(
    model="text-embedding-3-large",
    input="Python programming language",
    dimensions=256
)

print(f"Reduced dimensions: {len(response.data[0].embedding)}")  # 256

Para patrones de búsqueda por similitud y construcción de índices, consulta embeddings de OpenAI con similitud coseno.

Fine-Tuning

El fine-tuning es la última herramienta, no la primera. El prompt engineering y los ejemplos few-shot te llevan la mayor parte del camino; haz fine-tuning cuando necesites formato/tono consistente a escala, cuando llegues al límite de contexto con ejemplos, o cuando quieras correr un modelo más pequeño (más barato y rápido) con tu dominio incorporado. El coste real no es el job de entrenamiento — es curar los datos.

Preparar los datos de entrenamiento

import json

training_data = [
    {
        "messages": [
            {"role": "system", "content": "You are a customer support agent for TechCorp."},
            {"role": "user", "content": "How do I reset my password?"},
            {"role": "assistant", "content": "To reset your password: 1) Go to techcorp.com/reset 2) Enter your email 3) Click the link in the email 4) Set a new password."}
        ]
    },
    {
        "messages": [
            {"role": "system", "content": "You are a customer support agent for TechCorp."},
            {"role": "user", "content": "What are your business hours?"},
            {"role": "assistant", "content": "Our business hours are Monday-Friday, 9 AM to 6 PM CET. We also offer 24/7 email support."}
        ]
    }
]

# Guardar como JSONL
with open("training_data.jsonl", "w") as f:
    for item in training_data:
        f.write(json.dumps(item) + "\n")

Cincuenta ejemplos excelentes superan a quinientos mediocres — los malos ejemplos enseñan al modelo tus malos hábitos. Planifica la evaluación antes de entrenar, no después.

Crear un modelo fine-tuned

from openai import OpenAI

client = OpenAI()

# Subir el archivo de entrenamiento
file = client.files.create(
    file=open("training_data.jsonl", "rb"),
    purpose="fine-tune"
)

# Crear el job de fine-tuning
job = client.fine_tuning.jobs.create(
    training_file=file.id,
    model="gpt-4o-mini",
    hyperparameters={
        "n_epochs": 3,
        "batch_size": 4,
        "learning_rate_multiplier": 0.5
    }
)

print(f"Fine-tune job ID: {job.id}")
print(f"Status: {job.status}")

# Consultar el estado
import time
while True:
    job = client.fine_tuning.jobs.retrieve(job.id)
    print(f"Status: {job.status}")
    if job.status in ["succeeded", "failed"]:
        break
    time.sleep(30)

if job.status == "succeeded":
    print(f"Fine-tuned model: {job.fine_tuned_model}")

    # Usar el modelo fine-tuned
    response = client.chat.completions.create(
        model=job.fine_tuned_model,
        messages=[
            {"role": "system", "content": "You are a customer support agent for TechCorp."},
            {"role": "user", "content": "How do I cancel my subscription?"}
        ]
    )
    print(response.choices[0].message.content)

Para el flujo completo incluyendo validation sets y evaluaciones, consulta la receta de fine-tuning de LLM.

Batch API

La Batch API cambia latencia por precio: los jobs corren en una ventana de 24 horas aproximadamente a mitad de coste estándar. Encaja en todo lo que no necesita respuesta inmediata — clasificación masiva, evaluaciones offline, enriquecimiento nocturno. La parte incómoda es el flujo por archivos: escribes JSONL, lo subes, haces polling y descargas resultados.

Enviar un batch

import json

# Preparar las peticiones del batch
requests = [
    {
        "custom_id": f"request-{i}",
        "method": "POST",
        "url": "/v1/chat/completions",
        "body": {
            "model": "gpt-4o-mini",
            "messages": [
                {"role": "system", "content": "Classify the sentiment as positive, negative, or neutral."},
                {"role": "user", "content": f"Review: {review}"}
            ]
        }
    }
    for i, review in enumerate(reviews)
]

# Guardar como JSONL
with open("batch_requests.jsonl", "w") as f:
    for req in requests:
        f.write(json.dumps(req) + "\n")

# Subir el archivo
batch_file = client.files.create(
    file=open("batch_requests.jsonl", "rb"),
    purpose="batch"
)

# Crear el batch
batch = client.batches.create(
    input_file_id=batch_file.id,
    endpoint="/v1/chat/completions",
    completion_window="24h"
)

print(f"Batch ID: {batch.id}")
print(f"Status: {batch.status}")

# Consultar estado (el batch tarda minutos u horas)
batch = client.batches.retrieve(batch.id)
if batch.status == "completed":
    # Descargar resultados
    result_file = client.files.content(batch.output_file_id)
    results = result_file.text.strip().split("\n")

    for line in results:
        result = json.loads(line)
        if result.get("error"):
            print(f"Error: {result['error']}")
        else:
            content = result["response"]["body"]["choices"][0]["message"]["content"]
            print(f"{result['custom_id']}: {content}")

custom_id es lo único que enlaza los resultados con tu entrada — pon ahí tus claves de base de datos, no solo números de secuencia.

Assistants API (deprecada)

La Assistants API — threads, runs y client.beta.assistants — está deprecada y programada para retirada; OpenAI dirige las integraciones nuevas a la Responses API, que cubre los mismos casos de uso (estado persistente, tools, file search) con un modelo más simple. Si mantienes un Assistant existente, la ruta de migración es: los threads se convierten en cadenas de previous_response_id o estado en servidor, el cableado de tools pasa a function tools, y el code interpreter se convierte en herramienta integrada de la Responses API.

Para código legado que aún no migras, el patrón de abajo sigue funcionando — pero no construyas nada nuevo sobre él:

# Patrón legado — migra a Responses API para trabajo nuevo
assistant = client.beta.assistants.create(
    name="Code Reviewer",
    model="gpt-4o",
    instructions="You are a senior code reviewer.",
    tools=[{"type": "code_interpreter"}]
)
thread = client.beta.threads.create()
client.beta.threads.messages.create(
    thread_id=thread.id, role="user", content="Review: def add(a,b): return a+b"
)
run = client.beta.threads.runs.create(thread_id=thread.id, assistant_id=assistant.id)

Rate Limits y manejo de errores

Los rate limits son por modelo y se dividen en peticiones por minuto y tokens por minuto — puedes reventar el límite de tokens sin acercarte al de peticiones. Toda integración en producción necesita dos cosas: un limitador en el cliente que se mantenga por debajo de tu nivel, y backoff con jitter para cuando lo excedas igualmente.

Manejo de rate limits

import asyncio
import time
from openai import AsyncOpenAI, RateLimitError

client = AsyncOpenAI()

async def call_with_backoff(messages, max_retries=5):
    for attempt in range(max_retries):
        try:
            response = await client.chat.completions.create(
                model="gpt-4o",
                messages=messages
            )
            return response

        except RateLimitError as e:
            # Parsear la cabecera retry-after
            retry_after = getattr(e, "retry_after", 2 ** attempt)
            print(f"Rate limited. Retrying in {retry_after}s...")
            await asyncio.sleep(retry_after)

    raise RuntimeError("Max retries exceeded")

# Rate limiter tipo token bucket
class RateLimiter:
    def __init__(self, requests_per_minute: int):
        self.interval = 60.0 / requests_per_minute
        self.last_request = 0.0

    async def wait(self):
        now = time.time()
        elapsed = now - self.last_request
        if elapsed < self.interval:
            await asyncio.sleep(self.interval - elapsed)
        self.last_request = time.time()

limiter = RateLimiter(requests_per_minute=50)  # Límite RPM

async def rate_limited_call(messages):
    await limiter.wait()
    return await client.chat.completions.create(model="gpt-4o", messages=messages)

Tipos de error

Mapea cada excepción a una reacción distinta: AuthenticationError es callejón sin salida (alerta, no reintentes), RateLimitError espera, APITimeoutError/APIConnectionError reintenta, y BadRequestError significa que tu petición está rota — reintentar solo vuelve a fallar más despacio.

from openai import (
    APIError,
    RateLimitError,
    APIConnectionError,
    APITimeoutError,
    AuthenticationError,
    BadRequestError
)

async def robust_call(messages):
    try:
        return await client.chat.completions.create(model="gpt-4o", messages=messages)

    except AuthenticationError:
        # API key inválida
        raise RuntimeError("Invalid API key. Check OPENAI_API_KEY.")

    except BadRequestError as e:
        # Petición inválida (modelo o params incorrectos)
        raise RuntimeError(f"Bad request: {e}")

    except RateLimitError:
        # Rate limit alcanzado
        await asyncio.sleep(60)
        return await robust_call(messages)

    except APITimeoutError:
        # La petición agotó el timeout
        return await robust_call(messages)

    except APIConnectionError:
        # Error de red
        await asyncio.sleep(5)
        return await robust_call(messages)

    except APIError as e:
        # Error genérico de la API
        raise RuntimeError(f"OpenAI API error: {e}")

Optimización de costes

La mayoría de problemas de coste se reducen a tres cosas: el modelo equivocado para la tarea, reenviar contexto que no necesitas, y max_tokens sin techo. Antes de cualquier optimización sofisticada, pon un tracker delante del cliente para ver qué llamadas cuestan dinero de verdad — lo que no mides se optimiza a ciegas.

class CostTracker:
    PRICING = {
        "gpt-4o": {"input": 2.50, "output": 10.00},
        "gpt-4o-mini": {"input": 0.15, "output": 0.60},
        "text-embedding-3-small": {"input": 0.02, "output": 0.0},
    }

    def __init__(self):
        self.costs: list[dict] = []

    def track(self, model: str, input_tokens: int, output_tokens: int):
        if model not in self.PRICING:
            return

        pricing = self.PRICING[model]
        cost = (input_tokens / 1_000_000 * pricing["input"]) + \
               (output_tokens / 1_000_000 * pricing["output"])

        self.costs.append({
            "model": model,
            "input_tokens": input_tokens,
            "output_tokens": output_tokens,
            "cost": cost
        })

    def total_cost(self) -> float:
        return sum(c["cost"] for c in self.costs)

    def summary(self) -> dict:
        by_model = {}
        for c in self.costs:
            if c["model"] not in by_model:
                by_model[c["model"]] = {"calls": 0, "cost": 0.0, "tokens": 0}
            by_model[c["model"]]["calls"] += 1
            by_model[c["model"]]["cost"] += c["cost"]
            by_model[c["model"]]["tokens"] += c["input_tokens"] + c["output_tokens"]

        return {
            "total_cost": self.total_cost(),
            "by_model": by_model,
            "total_calls": len(self.costs)
        }

# Uso
tracker = CostTracker()
response = client.chat.completions.create(model="gpt-4o", messages=[...])
tracker.track("gpt-4o", response.usage.prompt_tokens, response.usage.completion_tokens)
print(f"Total cost: ${tracker.total_cost():.4f}")

Errores comunes

  • Reintentar BadRequestError en bucle — una petición malformada falla para siempre; arregla la petición, no la política de reintentos.
  • Sin techo en los bucles de tool calls — acota las iteraciones; un modelo confundido propone llamadas indefinidamente.
  • Reenviar todo el historial en cada turno — trunca o resume pasados ~20 turnos, o usa previous_response_id.
  • API key en código de cliente — las claves en el JS que se distribuye se scrapean en horas; pasa las llamadas por tu backend.
  • Fine-tuning antes que prompting — tunea solo cuando el prompt engineering se estanque en evaluaciones, no por intuición.
  • Contar peticiones, no tokens — los límites TPM saltan por tokens; unos pocos prompts gigantes pueden generar 429 en una app de bajo tráfico.

See Also

Lecturas Recomendadas

Preguntas frecuentes

¿Qué modelo debería usar?

Empieza con gpt-4o-mini para tareas simples (clasificación, resúmenes, extracción) y gpt-4o donde importe la calidad o la multimodalidad. Usa o1/o1-mini solo para razonamiento genuinamente multi-paso — son más lentos y mucho más caros. En caso de duda, mide con tus propias evaluaciones en lugar de fiarte de benchmarks.

¿Debería usar la Responses API o chat completions?

Para código nuevo, la Responses API — es la superficie recomendada actual y maneja estado, tools y salida estructurada en un solo lugar. Quédate con chat completions si mantienes una integración existente o apuntas a proveedores compatibles con OpenAI que aún no han adoptado Responses.

¿Cómo reduzco los costes?

Enruta las consultas simples a gpt-4o-mini. Cachea las consultas repetidas. Usa la Batch API para trabajo no urgente (~50 % más barato). Acota max_tokens para frenar la sobre-generación. Mantén los system prompts cortos — cuestan tokens en cada llamada. Y registra el uso por feature para saber de dónde viene la factura.

¿Cuál es la diferencia entre JSON mode y salidas estructuradas?

El JSON mode garantiza JSON sintácticamente válido, nada más. Las salidas estructuradas (json_schema con strict: true) garantizan que la respuesta cumple tu esquema — campos correctos, tipos correctos, nada extra. Si el código posterior parsea el resultado, usa salidas estructuradas.

¿Cómo manejo contextos largos?

gpt-4o soporta una ventana de 128K tokens, pero llenarla sale caro y degrada la calidad del retrieval. Prefiere RAG antes que empaquetar documentos enteros, resume los turnos antiguos de la conversación, cuenta tokens con tiktoken antes de enviar, y acota siempre max_tokens en la salida.

¿Puedo usar la API de OpenAI en Europa bajo el RGPD?

Sí, con matices: firma el Data Processing Agreement de OpenAI, consigue base legal antes de enviar datos personales y aplica minimización de datos. Para cargas sensibles, comprueba si una opción de retención cero o residencia regional encaja antes de enviar datos de usuarios.