Módulo 12: LangSmith y Producción

Rate Limiting y Cost Control

Descripción de la cápsula

En la cápsula anterior aprendiste a trackear cuánto cuestan tus llamadas al modelo — tokens, dólares, desglose por operación. Pero trackear sin actuar es como tener un velocímetro sin frenos. Un solo usuario ejecutando 100 investigaciones en una hora puede consumir tu presupuesto diario completo. Sin rate limiting, tu sistema de AI es un grifo abierto de dinero.

Rate limiting en sistemas de AI no es lo mismo que rate limiting en APIs tradicionales. En una API REST, limitas requests por segundo para proteger el servidor. En un agente de AI, limitas requests para proteger tu presupuesto. Una sola ejecución del Research Assistant puede hacer 5-10 llamadas al modelo — si un usuario dispara 50 ejecuciones, son 250-500 llamadas al modelo en minutos. A $0.035 por ejecución con GPT-4.1, eso son $1.75 en unos minutos. Multiplica por 100 usuarios haciendo lo mismo y tienes una factura de $175 en una hora.

LangChain incluye InMemoryRateLimiter para controlar el ritmo de llamadas al modelo. Combinado con el cost tracking de la cápsula anterior y el model routing del Módulo 4, puedes construir un sistema de control de costos completo: limitar la velocidad de requests, establecer presupuestos por usuario y por proyecto, y degradar automáticamente a modelos más baratos cuando el presupuesto se agota.


InMemoryRateLimiter: control de velocidad

InMemoryRateLimiter es la implementación built-in de LangChain para limitar la tasa de requests al modelo. Funciona con un token bucket algorithm: tienes un "cubo" de tokens disponibles, cada request consume uno, y los tokens se reponen a una tasa fija.

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.rate_limiters import InMemoryRateLimiter
import time

rate_limiter = InMemoryRateLimiter(
    requests_per_second=1,
    check_every_n_seconds=0.1,
    max_bucket_size=2,
)

model = init_chat_model("openai:gpt-4.1-mini")
limited_model = model.with_rate_limiter(rate_limiter)

prompts = [
    "Di 'uno'.",
    "Di 'dos'.",
    "Di 'tres'.",
    "Di 'cuatro'.",
    "Di 'cinco'.",
]

start = time.time()
for i, prompt in enumerate(prompts):
    t = time.time() - start
    response = limited_model.invoke(prompt)
    elapsed = time.time() - start
    print(f"  [{elapsed:.1f}s] Prompt {i+1}: {response.content.strip()}")

total = time.time() - start
print(f"\nTiempo total: {total:.1f}s (sin rate limit sería ~2-3s)")
# Output esperado:
#   [0.5s] Prompt 1: uno
#   [1.0s] Prompt 2: dos
#   [2.0s] Prompt 3: tres
#   [3.0s] Prompt 4: cuatro
#   [4.0s] Prompt 5: cinco
#
# Tiempo total: 4.5s (sin rate limit sería ~2-3s)

Los parámetros de InMemoryRateLimiter:

ParámetroSignificadoEjemplo
requests_per_secondTasa de reposición de tokens1 = un request por segundo
check_every_n_secondsFrecuencia de verificación0.1 = verifica cada 100ms
max_bucket_sizeCapacidad máxima del bucket2 = permite burst de 2 requests

max_bucket_size: permitir bursts controlados

El max_bucket_size permite acumular tokens cuando no hay actividad. Si tu tasa es 1 request/segundo y no hay requests durante 5 segundos, el bucket acumula hasta max_bucket_size tokens, permitiendo un burst cuando vuelve la actividad.

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.rate_limiters import InMemoryRateLimiter
import time

rate_limiter = InMemoryRateLimiter(
    requests_per_second=2,
    check_every_n_seconds=0.1,
    max_bucket_size=5,
)

model = init_chat_model("openai:gpt-4.1-mini")
limited_model = model.with_rate_limiter(rate_limiter)

print("Esperando 3 segundos para acumular tokens en el bucket...")
time.sleep(3)

start = time.time()
for i in range(8):
    response = limited_model.invoke(f"Di '{i+1}'.")
    elapsed = time.time() - start
    print(f"  [{elapsed:.1f}s] Request {i+1}: {response.content.strip()}")

total = time.time() - start
print(f"\nTiempo total: {total:.1f}s")
print("Los primeros 5 fueron rápidos (bucket lleno), luego 2/segundo")
# Output esperado:
# Esperando 3 segundos para acumular tokens en el bucket...
#   [0.3s] Request 1: 1
#   [0.6s] Request 2: 2
#   [0.9s] Request 3: 3
#   [1.2s] Request 4: 4
#   [1.5s] Request 5: 5
#   [2.0s] Request 6: 6
#   [2.5s] Request 7: 7
#   [3.0s] Request 8: 8
#
# Tiempo total: 3.0s
# Los primeros 5 fueron rápidos (bucket lleno), luego 2/segundo

Rate limiting en agentes

Cuando aplicas rate limiting a un agente, cada llamada al modelo (no cada invocación del agente) es limitada. Un agente que hace 3 llamadas al modelo por ejecución se ve afectado 3 veces por el rate limiter.

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain.agents import create_agent
from langchain_core.tools import tool
from langchain_core.rate_limiters import InMemoryRateLimiter
import time

rate_limiter = InMemoryRateLimiter(
    requests_per_second=2,
    check_every_n_seconds=0.1,
    max_bucket_size=3,
)

model = init_chat_model("openai:gpt-4.1-mini")
limited_model = model.with_rate_limiter(rate_limiter)

@tool
def search(query: str) -> str:
    """Busca información sobre un tema."""
    return f"Resultado: {query} es un concepto importante en AI engineering."

agent = create_agent(limited_model, [search])

start = time.time()
result = agent.invoke({"messages": [("user", "Busca qué es LangGraph y resúmelo.")]})
elapsed = time.time() - start

print(f"Respuesta: {result['messages'][-1].content[:100]}...")
print(f"Tiempo de ejecución: {elapsed:.1f}s")
print("(Incluye rate limiting entre cada llamada interna al modelo)")
# Output esperado:
# Respuesta: LangGraph es un framework de código abierto para construir aplicaciones con agentes de AI...
# Tiempo de ejecución: 2.5s
# (Incluye rate limiting entre cada llamada interna al modelo)

Per-user rate limiting: tiers de servicio

En producción, diferentes usuarios tienen diferentes límites. Un usuario free no debería consumir los mismos recursos que un usuario enterprise.

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.rate_limiters import InMemoryRateLimiter
import time

TIER_LIMITS = {
    "free": {"requests_per_second": 0.5, "max_bucket_size": 2},
    "pro": {"requests_per_second": 2, "max_bucket_size": 5},
    "enterprise": {"requests_per_second": 10, "max_bucket_size": 20},
}

class UserRateLimitManager:
    def __init__(self):
        self.limiters: dict[str, InMemoryRateLimiter] = {}
        self.models: dict[str, object] = {}
        self.base_model = init_chat_model("openai:gpt-4.1-mini")

    def get_limited_model(self, user_id: str, tier: str):
        """Retorna un modelo con rate limiting apropiado para el tier del usuario."""
        key = f"{user_id}:{tier}"
        if key not in self.limiters:
            limits = TIER_LIMITS[tier]
            self.limiters[key] = InMemoryRateLimiter(
                requests_per_second=limits["requests_per_second"],
                check_every_n_seconds=0.1,
                max_bucket_size=limits["max_bucket_size"],
            )
            self.models[key] = self.base_model.with_rate_limiter(self.limiters[key])
        return self.models[key]


manager = UserRateLimitManager()

users = [
    ("alice", "free"),
    ("bob", "pro"),
    ("corp-acme", "enterprise"),
]

for user_id, tier in users:
    model = manager.get_limited_model(user_id, tier)
    limits = TIER_LIMITS[tier]

    start = time.time()
    for i in range(3):
        response = model.invoke(f"Di '{i+1}'.")
    elapsed = time.time() - start

    print(f"[{tier:>10}] {user_id}: 3 requests en {elapsed:.1f}s "
          f"(límite: {limits['requests_per_second']} req/s)")
# Output esperado:
# [      free] alice: 3 requests en 5.2s (límite: 0.5 req/s)
# [       pro] bob: 3 requests en 1.8s (límite: 2 req/s)
# [enterprise] corp-acme: 3 requests en 0.9s (límite: 10 req/s)

Cost budgets: límites de gasto

Rate limiting controla la velocidad. Cost budgets controlan el gasto total. Necesitas ambos: un usuario puede respetar el rate limit pero ejecutar miles de requests en un día.

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from dataclasses import dataclass, field
from datetime import datetime

PRICING = {
    "gpt-4.1-mini": {"input": 0.40, "output": 1.60},
    "gpt-4.1": {"input": 2.00, "output": 8.00},
}


@dataclass
class CostBudget:
    user_id: str
    daily_limit: float
    monthly_limit: float
    spent_today: float = 0.0
    spent_month: float = 0.0
    requests_today: int = 0

    def check_budget(self, estimated_cost: float) -> tuple[bool, str]:
        """Verifica si hay presupuesto disponible."""
        if self.spent_today + estimated_cost > self.daily_limit:
            return False, f"daily_limit_exceeded (${self.spent_today:.4f}/${self.daily_limit:.2f})"
        if self.spent_month + estimated_cost > self.monthly_limit:
            return False, f"monthly_limit_exceeded (${self.spent_month:.2f}/${self.monthly_limit:.2f})"
        return True, "ok"

    def record(self, cost: float):
        """Registra un gasto."""
        self.spent_today += cost
        self.spent_month += cost
        self.requests_today += 1

    def alert_status(self) -> str:
        """Retorna el estado de alerta."""
        pct = (self.spent_today / self.daily_limit * 100) if self.daily_limit > 0 else 0
        if pct >= 100:
            return "BLOCKED"
        if pct >= 80:
            return "WARNING"
        if pct >= 50:
            return "NOTICE"
        return "OK"


class CostController:
    def __init__(self, model_name: str = "gpt-4.1-mini"):
        self.model_name = model_name
        self.budgets: dict[str, CostBudget] = {}

    def register_user(self, user_id: str, daily: float, monthly: float):
        self.budgets[user_id] = CostBudget(
            user_id=user_id,
            daily_limit=daily,
            monthly_limit=monthly,
        )

    def estimate_cost(self, input_tokens: int = 200, output_tokens: int = 300) -> float:
        """Estima el costo de una llamada típica."""
        pricing = PRICING[self.model_name]
        return (input_tokens / 1_000_000) * pricing["input"] + \
               (output_tokens / 1_000_000) * pricing["output"]

    def can_proceed(self, user_id: str) -> tuple[bool, str]:
        """Verifica si el usuario puede hacer una llamada."""
        if user_id not in self.budgets:
            return False, "user_not_registered"
        estimated = self.estimate_cost()
        return self.budgets[user_id].check_budget(estimated)

    def record_usage(self, user_id: str, usage_metadata: dict):
        """Registra el uso real de una llamada."""
        pricing = PRICING[self.model_name]
        cost = (usage_metadata["input_tokens"] / 1_000_000) * pricing["input"] + \
               (usage_metadata["output_tokens"] / 1_000_000) * pricing["output"]
        self.budgets[user_id].record(cost)
        return cost


controller = CostController("gpt-4.1-mini")
controller.register_user("free-user", daily=0.001, monthly=0.02)
controller.register_user("pro-user", daily=0.01, monthly=0.20)

model = init_chat_model("openai:gpt-4.1-mini")

for i in range(10):
    for user_id in ["free-user", "pro-user"]:
        can, reason = controller.can_proceed(user_id)

        if not can:
            budget = controller.budgets[user_id]
            print(f"  [{user_id:>10}] Request {i+1}: BLOCKED — {reason}")
            continue

        response = model.invoke(f"Concepto {i+1} de AI engineering en 1 oración.")
        cost = controller.record_usage(user_id, response.usage_metadata)
        budget = controller.budgets[user_id]
        alert = budget.alert_status()
        alert_str = f" [{alert}]" if alert != "OK" else ""
        print(f"  [{user_id:>10}] Request {i+1}: ${cost:.6f} | "
              f"Daily: ${budget.spent_today:.6f}/{budget.daily_limit:.4f}{alert_str}")

print(f"\nResumen final:")
for uid, budget in controller.budgets.items():
    pct = (budget.spent_today / budget.daily_limit * 100) if budget.daily_limit > 0 else 0
    print(f"  {uid}: {budget.requests_today} requests, "
          f"${budget.spent_today:.6f} / ${budget.daily_limit:.4f} ({pct:.0f}%)")
# Output esperado:
#   [ free-user] Request 1: $0.000120 | Daily: $0.000120/0.0010
#   [  pro-user] Request 1: $0.000118 | Daily: $0.000118/0.0100
#   [ free-user] Request 2: $0.000115 | Daily: $0.000235/0.0010
#   [  pro-user] Request 2: $0.000122 | Daily: $0.000240/0.0100
#   ...
#   [ free-user] Request 8: $0.000119 | Daily: $0.000930/0.0010 [WARNING]
#   [  pro-user] Request 8: $0.000118 | Daily: $0.000960/0.0100
#   [ free-user] Request 9: BLOCKED — daily_limit_exceeded ($0.000930/0.00)
#   [  pro-user] Request 9: $0.000115 | Daily: $0.001075/0.0100
#   [ free-user] Request 10: BLOCKED — daily_limit_exceeded ($0.000930/0.00)
#   [  pro-user] Request 10: $0.000120 | Daily: $0.001195/0.0100
#
# Resumen final:
#   free-user: 8 requests, $0.000930 / $0.0010 (93%)
#   pro-user: 10 requests, $0.001195 / $0.0100 (12%)

Circuit breaker: pausa automática ante costos anómalos

Un circuit breaker detecta cuando la tasa de gasto excede lo normal y pausa el sistema antes de que el daño sea mayor. Si tu gasto promedio es $0.05/hora y de repente sube a $0.50/hora, algo anda mal — un loop infinito, un prompt inyectado que causa respuestas enormes, o un ataque de un usuario malicioso.

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from dataclasses import dataclass, field
from datetime import datetime
import time

PRICING = {"gpt-4.1-mini": {"input": 0.40, "output": 1.60}}


@dataclass
class CostCircuitBreaker:
    """Circuit breaker que se activa si el gasto por minuto excede el threshold."""
    cost_per_minute_threshold: float
    window_seconds: int = 60
    state: str = "closed"
    cost_history: list = field(default_factory=list)
    tripped_at: str = ""

    def record_cost(self, cost: float) -> str:
        """Registra un costo y verifica si el circuit breaker debe activarse."""
        now = time.time()
        self.cost_history.append((now, cost))

        cutoff = now - self.window_seconds
        self.cost_history = [(t, c) for t, c in self.cost_history if t > cutoff]

        window_cost = sum(c for _, c in self.cost_history)

        if window_cost > self.cost_per_minute_threshold:
            self.state = "open"
            self.tripped_at = datetime.now().isoformat()
            return "TRIPPED"

        self.state = "closed"
        return "OK"

    def can_proceed(self) -> tuple[bool, str]:
        if self.state == "open":
            return False, f"Circuit breaker OPEN (tripped at {self.tripped_at})"
        return True, "OK"

    def reset(self):
        self.state = "closed"
        self.cost_history = []
        self.tripped_at = ""


breaker = CostCircuitBreaker(cost_per_minute_threshold=0.001)
model = init_chat_model("openai:gpt-4.1-mini")

for i in range(12):
    can, reason = breaker.can_proceed()
    if not can:
        print(f"  Request {i+1}: BLOCKED — {reason}")
        continue

    response = model.invoke(f"Explica concepto #{i+1} de producción de AI en 3 oraciones detalladas.")
    usage = response.usage_metadata
    cost = (usage["input_tokens"] / 1_000_000) * PRICING["gpt-4.1-mini"]["input"] + \
           (usage["output_tokens"] / 1_000_000) * PRICING["gpt-4.1-mini"]["output"]

    status = breaker.record_cost(cost)
    window_cost = sum(c for _, c in breaker.cost_history)
    print(f"  Request {i+1}: ${cost:.6f} | Window: ${window_cost:.6f} | Status: {status}")
# Output esperado:
#   Request 1: $0.000180 | Window: $0.000180 | Status: OK
#   Request 2: $0.000175 | Window: $0.000355 | Status: OK
#   Request 3: $0.000190 | Window: $0.000545 | Status: OK
#   Request 4: $0.000185 | Window: $0.000730 | Status: OK
#   Request 5: $0.000178 | Window: $0.000908 | Status: OK
#   Request 6: $0.000182 | Window: $0.001090 | Status: TRIPPED
#   Request 7: BLOCKED — Circuit breaker OPEN (tripped at 2026-03-08T...)
#   Request 8: BLOCKED — Circuit breaker OPEN (tripped at 2026-03-08T...)
#   ...

Auto-degradación: modelo más barato cuando el presupuesto aprieta

En vez de bloquear al usuario cuando se acerca al límite, puedes degradar automáticamente a un modelo más barato. El usuario sigue recibiendo respuestas, pero con un modelo más económico (y potencialmente menos capaz).

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model

PRICING = {
    "gpt-4.1": {"input": 2.00, "output": 8.00},
    "gpt-4.1-mini": {"input": 0.40, "output": 1.60},
    "gpt-4.1-nano": {"input": 0.10, "output": 0.40},
}

MODEL_TIERS = [
    ("gpt-4.1", "openai:gpt-4.1"),
    ("gpt-4.1-mini", "openai:gpt-4.1-mini"),
    ("gpt-4.1-nano", "openai:gpt-4.1-nano"),
]


class AutoDegradeController:
    def __init__(self, daily_budget: float):
        self.daily_budget = daily_budget
        self.spent = 0.0
        self.models = {name: init_chat_model(model_id) for name, model_id in MODEL_TIERS}
        self.degradation_log = []

    def select_model(self) -> tuple[str, object]:
        """Selecciona el modelo basado en el presupuesto restante."""
        remaining_pct = 1.0 - (self.spent / self.daily_budget) if self.daily_budget > 0 else 0

        if remaining_pct > 0.5:
            name = "gpt-4.1"
        elif remaining_pct > 0.2:
            name = "gpt-4.1-mini"
        else:
            name = "gpt-4.1-nano"

        return name, self.models[name]

    def invoke(self, prompt: str) -> tuple[str, str]:
        """Invoca el modelo apropiado y registra el costo."""
        model_name, model = self.select_model()
        response = model.invoke(prompt)
        usage = response.usage_metadata
        pricing = PRICING[model_name]
        cost = (usage["input_tokens"] / 1_000_000) * pricing["input"] + \
               (usage["output_tokens"] / 1_000_000) * pricing["output"]

        self.spent += cost
        remaining_pct = (1 - self.spent / self.daily_budget) * 100
        self.degradation_log.append({
            "model": model_name, "cost": cost, "remaining_pct": remaining_pct,
        })
        return model_name, response.content


controller = AutoDegradeController(daily_budget=0.002)

prompts = [
    "Explica qué es observabilidad en AI en 3 oraciones.",
    "¿Qué es tracing en LangSmith? 2 oraciones.",
    "¿Qué es evaluation en AI? 2 oraciones.",
    "¿Qué es rate limiting? 1 oración.",
    "¿Qué es un circuit breaker? 1 oración.",
    "Define monitoring. 1 oración.",
    "Define deployment. 1 oración.",
    "Define scaling. 1 oración.",
    "Define resilience. 1 oración.",
    "Define caching. 1 oración.",
]

for i, prompt in enumerate(prompts):
    model_name, content = controller.invoke(prompt)
    remaining = (1 - controller.spent / controller.daily_budget) * 100
    print(f"  [{i+1:>2}] {model_name:<14} | Budget: {remaining:>5.1f}% | {content[:60]}...")

print(f"\nGasto total: ${controller.spent:.6f} / ${controller.daily_budget:.4f}")
print(f"Degradaciones: {len(set(d['model'] for d in controller.degradation_log))} modelos usados")
# Output esperado:
#   [ 1] gpt-4.1        | Budget:  75.0% | La observabilidad en AI se refiere a la capacidad de entende...
#   [ 2] gpt-4.1        | Budget:  55.0% | LangSmith es una plataforma de observabilidad que permite tr...
#   [ 3] gpt-4.1        | Budget:  40.0% | La evaluación en AI es el proceso sistemático de medir la ca...
#   [ 4] gpt-4.1-mini   | Budget:  36.0% | Rate limiting es una técnica que controla la cantidad de sol...
#   [ 5] gpt-4.1-mini   | Budget:  32.0% | Un circuit breaker es un patrón de diseño que detecta fallo...
#   [ 6] gpt-4.1-mini   | Budget:  28.0% | Monitoring es la práctica de observar continuamente el rendi...
#   [ 7] gpt-4.1-mini   | Budget:  24.0% | Deployment es el proceso de poner una aplicación en producc...
#   [ 8] gpt-4.1-nano   | Budget:  22.0% | Scaling es ajustar recursos según demanda....
#   [ 9] gpt-4.1-nano   | Budget:  20.0% | Resilience es la capacidad de recuperarse de fallos....
#   [10] gpt-4.1-nano   | Budget:  18.0% | Caching es almacenar datos para acceso rápido....
#
# Gasto total: $0.001640 / $0.0020
# Degradaciones: 3 modelos usados

El sistema empieza con GPT-4.1 (mejor calidad) y degrada a mini y luego a nano conforme el presupuesto se agota. El usuario no se bloquea — la calidad baja gradualmente.


Combinando rate limiting + cost control + model routing

En producción necesitas las tres capas juntas. Rate limiting controla la velocidad, cost control controla el gasto total, y model routing optimiza la relación costo-calidad.

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.rate_limiters import InMemoryRateLimiter
from dataclasses import dataclass

PRICING = {
    "gpt-4.1-mini": {"input": 0.40, "output": 1.60},
    "gpt-4.1-nano": {"input": 0.10, "output": 0.40},
}

TIER_CONFIG = {
    "free": {
        "rate": {"requests_per_second": 0.5, "max_bucket_size": 2},
        "budget_daily": 0.001,
        "default_model": "gpt-4.1-nano",
    },
    "pro": {
        "rate": {"requests_per_second": 2, "max_bucket_size": 5},
        "budget_daily": 0.01,
        "default_model": "gpt-4.1-mini",
    },
}


@dataclass
class UserSession:
    user_id: str
    tier: str
    spent: float = 0.0
    requests: int = 0
    blocked_requests: int = 0


class ProductionController:
    def __init__(self):
        self.sessions: dict[str, UserSession] = {}
        self.rate_limiters: dict[str, InMemoryRateLimiter] = {}
        self.models = {
            "gpt-4.1-mini": init_chat_model("openai:gpt-4.1-mini"),
            "gpt-4.1-nano": init_chat_model("openai:gpt-4.1-nano"),
        }

    def register_user(self, user_id: str, tier: str):
        self.sessions[user_id] = UserSession(user_id=user_id, tier=tier)
        config = TIER_CONFIG[tier]
        self.rate_limiters[user_id] = InMemoryRateLimiter(
            requests_per_second=config["rate"]["requests_per_second"],
            check_every_n_seconds=0.1,
            max_bucket_size=config["rate"]["max_bucket_size"],
        )

    def get_model_for_user(self, user_id: str):
        """Selecciona modelo: degrada si el presupuesto se agota."""
        session = self.sessions[user_id]
        config = TIER_CONFIG[session.tier]
        budget = config["budget_daily"]
        remaining_pct = 1.0 - (session.spent / budget) if budget > 0 else 0

        if remaining_pct < 0:
            return None, None

        if remaining_pct < 0.2:
            model_name = "gpt-4.1-nano"
        else:
            model_name = config["default_model"]

        limiter = self.rate_limiters[user_id]
        model = self.models[model_name].with_rate_limiter(limiter)
        return model_name, model

    def invoke(self, user_id: str, prompt: str) -> tuple[bool, str, str]:
        """Invoca con todas las protecciones: rate limit + budget + model routing."""
        session = self.sessions[user_id]
        model_name, model = self.get_model_for_user(user_id)

        if model is None:
            session.blocked_requests += 1
            return False, "BUDGET_EXCEEDED", ""

        response = model.invoke(prompt)
        usage = response.usage_metadata
        pricing = PRICING[model_name]
        cost = (usage["input_tokens"] / 1_000_000) * pricing["input"] + \
               (usage["output_tokens"] / 1_000_000) * pricing["output"]

        session.spent += cost
        session.requests += 1
        return True, model_name, response.content


ctrl = ProductionController()
ctrl.register_user("alice", "free")
ctrl.register_user("bob", "pro")

for i in range(8):
    for user_id in ["alice", "bob"]:
        ok, info, content = ctrl.invoke(user_id, f"Concepto {i+1} de AI. 1 oración.")
        session = ctrl.sessions[user_id]
        budget = TIER_CONFIG[session.tier]["budget_daily"]
        pct = (session.spent / budget * 100) if budget > 0 else 0

        if ok:
            print(f"  [{user_id:>6}] #{i+1}: {info:<14} ${session.spent:.6f}/{budget:.4f} ({pct:.0f}%)")
        else:
            print(f"  [{user_id:>6}] #{i+1}: BLOCKED — {info}")

print(f"\nResumen:")
for uid, session in ctrl.sessions.items():
    print(f"  {uid}: {session.requests} OK, {session.blocked_requests} blocked, ${session.spent:.6f} spent")
# Output esperado:
#   [ alice] #1: gpt-4.1-nano   $0.000015/0.0010 (2%)
#   [   bob] #1: gpt-4.1-mini   $0.000120/0.0100 (1%)
#   [ alice] #2: gpt-4.1-nano   $0.000028/0.0010 (3%)
#   [   bob] #2: gpt-4.1-mini   $0.000238/0.0100 (2%)
#   ...
#   [ alice] #7: gpt-4.1-nano   $0.000098/0.0010 (10%)
#   [   bob] #7: gpt-4.1-mini   $0.000840/0.0100 (8%)
#   [ alice] #8: gpt-4.1-nano   $0.000112/0.0010 (11%)
#   [   bob] #8: gpt-4.1-mini   $0.000960/0.0100 (10%)
#
# Resumen:
#   alice: 8 OK, 0 blocked, $0.000112 spent
#   bob: 8 OK, 0 blocked, $0.000960 spent

Rate limiting en multi-agent (conecta con M10)

En un sistema multi-agente, múltiples agentes comparten el mismo presupuesto. El researcher, analyst, y writer del Research Assistant hacen llamadas independientes, pero todas cuentan contra el mismo presupuesto del usuario.

from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.rate_limiters import InMemoryRateLimiter

shared_limiter = InMemoryRateLimiter(
    requests_per_second=3,
    check_every_n_seconds=0.1,
    max_bucket_size=5,
)

base_model = init_chat_model("openai:gpt-4.1-mini")
shared_model = base_model.with_rate_limiter(shared_limiter)

shared_budget = {"spent": 0.0, "limit": 0.005}
PRICING = {"gpt-4.1-mini": {"input": 0.40, "output": 1.60}}


def agent_call(agent_name: str, prompt: str) -> tuple[bool, str]:
    """Simula una llamada de un agente con presupuesto compartido."""
    if shared_budget["spent"] >= shared_budget["limit"]:
        return False, f"[{agent_name}] Budget exceeded"

    response = shared_model.invoke(prompt)
    usage = response.usage_metadata
    cost = (usage["input_tokens"] / 1_000_000) * PRICING["gpt-4.1-mini"]["input"] + \
           (usage["output_tokens"] / 1_000_000) * PRICING["gpt-4.1-mini"]["output"]
    shared_budget["spent"] += cost

    return True, f"[{agent_name}] ${cost:.6f} | Total: ${shared_budget['spent']:.6f}"


agents_flow = [
    ("researcher", "Busca información sobre AI en educación. 2 oraciones."),
    ("researcher", "Busca tendencias en edtech. 2 oraciones."),
    ("analyst", "Analiza cómo AI transforma la educación. 3 hallazgos en 3 oraciones."),
    ("writer", "Escribe un resumen de 3 oraciones sobre AI en educación."),
]

print("Multi-agent execution con presupuesto compartido:")
print(f"Budget: ${shared_budget['limit']:.4f}\n")

for agent_name, prompt in agents_flow:
    ok, msg = agent_call(agent_name, prompt)
    status = "OK" if ok else "BLOCKED"
    print(f"  [{status}] {msg}")

remaining = shared_budget["limit"] - shared_budget["spent"]
print(f"\nPresupuesto restante: ${remaining:.6f}")
# Output esperado:
# Multi-agent execution con presupuesto compartido:
# Budget: $0.0050
#
#   [OK] [researcher] $0.000120 | Total: $0.000120
#   [OK] [researcher] $0.000115 | Total: $0.000235
#   [OK] [analyst] $0.000220 | Total: $0.000455
#   [OK] [writer] $0.000160 | Total: $0.000615
#
# Presupuesto restante: $0.004385

El rate limiter compartido asegura que ningún agente monopolice las llamadas al modelo. El presupuesto compartido asegura que la ejecución completa no exceda el límite del usuario.


Cost alerts: notificaciones de gasto

En producción, las alertas tempranas previenen sorpresas en la factura. Configura alertas progresivas: noticia al 50%, advertencia al 80%, bloqueo al 100%.

from dotenv import load_dotenv
load_dotenv()

from dataclasses import dataclass
from datetime import datetime


@dataclass
class CostAlert:
    level: str
    message: str
    timestamp: str
    user_id: str
    spent: float
    limit: float


class AlertSystem:
    def __init__(self):
        self.alerts: list[CostAlert] = []
        self.notified: dict[str, set] = {}

    def check(self, user_id: str, spent: float, limit: float) -> list[CostAlert]:
        """Verifica y genera alertas si aplica."""
        if user_id not in self.notified:
            self.notified[user_id] = set()

        pct = (spent / limit * 100) if limit > 0 else 0
        new_alerts = []

        thresholds = [
            (50, "NOTICE", "Budget at 50% — consider optimizing prompts or switching models"),
            (80, "WARNING", "Budget at 80% — auto-degradation will activate at 80%"),
            (95, "CRITICAL", "Budget at 95% — service will be limited shortly"),
            (100, "BLOCKED", "Budget exceeded — requests blocked until next period"),
        ]

        for threshold, level, msg_template in thresholds:
            if pct >= threshold and threshold not in self.notified[user_id]:
                alert = CostAlert(
                    level=level,
                    message=f"[{user_id}] {msg_template} ({pct:.0f}%)",
                    timestamp=datetime.now().isoformat(),
                    user_id=user_id,
                    spent=spent,
                    limit=limit,
                )
                self.alerts.append(alert)
                new_alerts.append(alert)
                self.notified[user_id].add(threshold)

        return new_alerts


alerts = AlertSystem()

simulated_spending = [
    ("user-001", 0.003, 0.01),
    ("user-001", 0.005, 0.01),
    ("user-001", 0.008, 0.01),
    ("user-001", 0.0095, 0.01),
    ("user-001", 0.011, 0.01),
    ("user-002", 0.04, 0.10),
    ("user-002", 0.085, 0.10),
]

for user_id, spent, limit in simulated_spending:
    new_alerts = alerts.check(user_id, spent, limit)
    for alert in new_alerts:
        icon = {"NOTICE": "ℹ️", "WARNING": "⚠️", "CRITICAL": "🔴", "BLOCKED": "🚫"}.get(alert.level, "")
        print(f"  {icon} [{alert.level:>8}] {alert.message}")

print(f"\nTotal alerts generated: {len(alerts.alerts)}")
# Output esperado:
#   ℹ️ [  NOTICE] [user-001] Budget at 50% — consider optimizing prompts or switching models (50%)
#   ⚠️ [ WARNING] [user-001] Budget at 80% — auto-degradation will activate at 80% (80%)
#   🔴 [CRITICAL] [user-001] Budget at 95% — service will be limited shortly (95%)
#   🚫 [ BLOCKED] [user-001] Budget exceeded — requests blocked until next period (110%)
#   ℹ️ [  NOTICE] [user-002] Budget at 50% — consider optimizing prompts or switching models (85%)
#   ⚠️ [ WARNING] [user-002] Budget at 80% — auto-degradation will activate at 80% (85%)
#
# Total alerts generated: 6

Troubleshooting

Problema 1: El rate limiter bloquea demasiado tiempo

Causa: requests_per_second es muy bajo o max_bucket_size es 1, lo que significa que no hay capacidad de burst.

Solución: Ajusta los parámetros según tu caso de uso:

# Para API interna con uso moderado
limiter = InMemoryRateLimiter(
    requests_per_second=5,
    check_every_n_seconds=0.1,
    max_bucket_size=10,
)

# Para usuario free que debe ir lento
limiter = InMemoryRateLimiter(
    requests_per_second=0.5,
    check_every_n_seconds=0.5,
    max_bucket_size=2,
)

Problema 2: El presupuesto se agota sin generar alertas

Causa: Las alertas se verifican después de cada gasto. Si un solo gasto es muy grande (ej: un prompt con 50K tokens), puede saltar del 40% al 120% sin pasar por los umbrales intermedios.

Solución: Verifica todos los umbrales en orden en cada check, no solo el siguiente:

for threshold, level, msg in thresholds:
    if pct >= threshold and threshold not in notified:
        # Genera alerta para CADA umbral alcanzado
        notified.add(threshold)

Problema 3: El rate limiter no se comparte entre agentes

Causa: Cada agente crea su propio rate limiter en vez de compartir uno.

Solución: Crea el rate limiter una vez y pásalo a todos los agentes:

shared_limiter = InMemoryRateLimiter(requests_per_second=5, ...)
researcher_model = base_model.with_rate_limiter(shared_limiter)
analyst_model = base_model.with_rate_limiter(shared_limiter)
writer_model = base_model.with_rate_limiter(shared_limiter)

Problema 4: InMemoryRateLimiter no persiste entre reinicios

Causa: Es in-memory — se reinicia cuando el proceso termina.

Solución: Para producción, usa un rate limiter basado en Redis u otro store persistente. InMemoryRateLimiter es suficiente para un solo proceso, pero no para sistemas distribuidos.

Problema 5: La auto-degradación cambia de modelo en medio de una conversación

Causa: El presupuesto cruza un umbral durante la ejecución del agente, haciendo que las primeras llamadas usen GPT-4.1 y las últimas GPT-4.1-nano.

Solución: Fija el modelo al inicio de cada ejecución completa, no entre llamadas:

model_name = select_model_for_budget(user_budget)
# Usa este modelo para TODA la ejecución del agente

Ejercicios

Ejercicio 1: Rate limiter básico con medición de throughput (Fácil)

Crea un rate limiter de 2 requests/segundo y mide cuánto tardan 6 requests secuenciales. Compara con el tiempo teórico.

Ver solución
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.rate_limiters import InMemoryRateLimiter
import time

limiter = InMemoryRateLimiter(
    requests_per_second=2,
    check_every_n_seconds=0.1,
    max_bucket_size=2,
)

model = init_chat_model("openai:gpt-4.1-mini")
limited = model.with_rate_limiter(limiter)

start = time.time()
for i in range(6):
    response = limited.invoke(f"Di '{i+1}'.")
    elapsed = time.time() - start
    print(f"  [{elapsed:.1f}s] Request {i+1}: {response.content.strip()}")

total = time.time() - start
theoretical = (6 - 2) / 2
print(f"\nTiempo real: {total:.1f}s")
print(f"Tiempo teórico mínimo: ~{theoretical:.1f}s + latencia del modelo")
# Output esperado:
#   [0.3s] Request 1: 1
#   [0.6s] Request 2: 2
#   [1.1s] Request 3: 3
#   [1.6s] Request 4: 4
#   [2.1s] Request 5: 5
#   [2.6s] Request 6: 6
#
# Tiempo real: 2.6s
# Tiempo teórico mínimo: ~2.0s + latencia del modelo

Explicación: Con max_bucket_size=2, los primeros 2 requests salen inmediatos. Los siguientes 4 esperan 0.5s cada uno (2 req/s). El tiempo real es teórico + latencia de la API.

Ejercicio 2: Cost controller con bloqueo al 100% (Fácil)

Crea un cost controller con presupuesto de $0.001. Ejecuta requests hasta que se bloquee y reporta cuántos requests pasaron.

Ver solución
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model

PRICING = {"gpt-4.1-mini": {"input": 0.40, "output": 1.60}}

class SimpleCostController:
    def __init__(self, budget: float):
        self.budget = budget
        self.spent = 0.0
        self.passed = 0
        self.blocked = 0

    def try_charge(self, usage: dict) -> bool:
        cost = (usage["input_tokens"] / 1_000_000) * PRICING["gpt-4.1-mini"]["input"] + \
               (usage["output_tokens"] / 1_000_000) * PRICING["gpt-4.1-mini"]["output"]
        if self.spent + cost > self.budget:
            self.blocked += 1
            return False
        self.spent += cost
        self.passed += 1
        return True

ctrl = SimpleCostController(budget=0.001)
model = init_chat_model("openai:gpt-4.1-mini")

for i in range(20):
    response = model.invoke(f"Explica concepto #{i+1} de AI en 2 oraciones.")
    if ctrl.try_charge(response.usage_metadata):
        pct = ctrl.spent / ctrl.budget * 100
        print(f"  #{i+1}: OK | Spent: ${ctrl.spent:.6f} ({pct:.0f}%)")
    else:
        print(f"  #{i+1}: BLOCKED | Spent: ${ctrl.spent:.6f} / ${ctrl.budget:.4f}")
        break

print(f"\nResultado: {ctrl.passed} pasaron, {ctrl.blocked} bloqueados")
# Output esperado:
#   #1: OK | Spent: $0.000125 (13%)
#   #2: OK | Spent: $0.000248 (25%)
#   ...
#   #7: OK | Spent: $0.000880 (88%)
#   #8: BLOCKED | Spent: $0.000880 / $0.0010
#
# Resultado: 7 pasaron, 1 bloqueados

Explicación: El controller acumula costos y bloquea cuando el siguiente gasto excedería el presupuesto. Simple y efectivo para proteger el presupuesto.

Ejercicio 3: Auto-degradación por presupuesto (Medio)

Implementa un sistema que use GPT-4.1-mini cuando hay >50% de presupuesto y GPT-4.1-nano cuando hay <50%. Muestra el cambio de modelo durante la ejecución.

Ver solución
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model

PRICING = {
    "gpt-4.1-mini": {"input": 0.40, "output": 1.60},
    "gpt-4.1-nano": {"input": 0.10, "output": 0.40},
}

models = {
    "gpt-4.1-mini": init_chat_model("openai:gpt-4.1-mini"),
    "gpt-4.1-nano": init_chat_model("openai:gpt-4.1-nano"),
}

budget = 0.001
spent = 0.0

for i in range(12):
    remaining_pct = (1 - spent / budget) * 100 if budget > 0 else 0
    model_name = "gpt-4.1-mini" if remaining_pct > 50 else "gpt-4.1-nano"

    if spent >= budget:
        print(f"  #{i+1}: STOPPED — budget exhausted")
        break

    response = models[model_name].invoke(f"Concepto {i+1} de AI. 1 oración corta.")
    usage = response.usage_metadata
    pricing = PRICING[model_name]
    cost = (usage["input_tokens"] / 1_000_000) * pricing["input"] + \
           (usage["output_tokens"] / 1_000_000) * pricing["output"]

    spent += cost
    remaining_pct = (1 - spent / budget) * 100
    print(f"  #{i+1}: {model_name:<14} | ${cost:.6f} | Budget: {remaining_pct:.0f}%")

print(f"\nTotal: ${spent:.6f} / ${budget:.4f}")
# Output esperado:
#   #1: gpt-4.1-mini   | $0.000120 | Budget: 88%
#   #2: gpt-4.1-mini   | $0.000115 | Budget: 76%
#   #3: gpt-4.1-mini   | $0.000118 | Budget: 64%
#   #4: gpt-4.1-mini   | $0.000122 | Budget: 52%
#   #5: gpt-4.1-nano   | $0.000025 | Budget: 49%
#   #6: gpt-4.1-nano   | $0.000022 | Budget: 47%
#   ...

Explicación: Cuando el presupuesto cruza el 50%, el sistema degrada a nano automáticamente. Nota cómo el costo por request baja dramáticamente, extendiendo la vida del presupuesto.

Ejercicio 4: Sistema de alertas con 3 niveles (Medio)

Crea un sistema de alertas que notifique al 50%, 80%, y 100% del presupuesto. Simula gasto progresivo y muestra las alertas generadas.

Ver solución
from dotenv import load_dotenv
load_dotenv()

from datetime import datetime

class BudgetAlerts:
    def __init__(self, budget: float):
        self.budget = budget
        self.spent = 0.0
        self.triggered = set()
        self.log = []

    def spend(self, amount: float) -> list[str]:
        self.spent += amount
        pct = (self.spent / self.budget * 100) if self.budget > 0 else 0
        alerts = []

        levels = [(50, "NOTICE"), (80, "WARNING"), (100, "CRITICAL")]
        for threshold, level in levels:
            if pct >= threshold and threshold not in self.triggered:
                self.triggered.add(threshold)
                msg = f"[{level}] Budget at {pct:.0f}% — ${self.spent:.6f}/${self.budget:.4f}"
                alerts.append(msg)
                self.log.append({"level": level, "pct": pct, "time": datetime.now().isoformat()})

        return alerts


system = BudgetAlerts(budget=0.001)

increments = [0.0001, 0.0001, 0.0001, 0.0001, 0.0001,
              0.0001, 0.0001, 0.0001, 0.0001, 0.0002]

for i, amount in enumerate(increments):
    alerts = system.spend(amount)
    pct = system.spent / system.budget * 100
    print(f"  Spend #{i+1}: +${amount:.4f} | Total: {pct:.0f}%")
    for alert in alerts:
        print(f"    → {alert}")

print(f"\nAlerts triggered: {len(system.log)}")
# Output esperado:
#   Spend #1: +$0.0001 | Total: 10%
#   Spend #2: +$0.0001 | Total: 20%
#   Spend #3: +$0.0001 | Total: 30%
#   Spend #4: +$0.0001 | Total: 40%
#   Spend #5: +$0.0001 | Total: 50%
#     → [NOTICE] Budget at 50% — $0.000500/$0.0010
#   Spend #6: +$0.0001 | Total: 60%
#   Spend #7: +$0.0001 | Total: 70%
#   Spend #8: +$0.0001 | Total: 80%
#     → [WARNING] Budget at 80% — $0.000800/$0.0010
#   Spend #9: +$0.0001 | Total: 90%
#   Spend #10: +$0.0002 | Total: 110%
#     → [CRITICAL] Budget at 110% — $0.001100/$0.0010
#
# Alerts triggered: 3

Explicación: Las alertas se disparan una sola vez por umbral (el triggered set previene duplicados). En producción, cada nivel enviaría la notificación por el canal apropiado (log, email, Slack, PagerDuty).

Ejercicio 5: Circuit breaker con auto-reset (Difícil)

Implementa un circuit breaker que se activa cuando el gasto en 30 segundos excede un threshold, pero se auto-resetea después de 10 segundos de pausa.

Ver solución
from dotenv import load_dotenv
load_dotenv()

import time

class AutoResetCircuitBreaker:
    def __init__(self, cost_threshold: float, window_s: int = 30, cooldown_s: int = 10):
        self.cost_threshold = cost_threshold
        self.window_s = window_s
        self.cooldown_s = cooldown_s
        self.history: list[tuple[float, float]] = []
        self.state = "closed"
        self.opened_at: float = 0

    def record(self, cost: float) -> str:
        now = time.time()

        if self.state == "open":
            if now - self.opened_at > self.cooldown_s:
                self.state = "closed"
                self.history = []
                print(f"    [CIRCUIT] Auto-reset after {self.cooldown_s}s cooldown")
            else:
                return "OPEN"

        self.history.append((now, cost))
        cutoff = now - self.window_s
        self.history = [(t, c) for t, c in self.history if t > cutoff]

        window_cost = sum(c for _, c in self.history)
        if window_cost > self.cost_threshold:
            self.state = "open"
            self.opened_at = now
            return "TRIPPED"

        return "OK"

    def can_proceed(self) -> bool:
        if self.state == "open":
            if time.time() - self.opened_at > self.cooldown_s:
                self.state = "closed"
                self.history = []
                return True
            return False
        return True


breaker = AutoResetCircuitBreaker(cost_threshold=0.0005, window_s=30, cooldown_s=3)

costs = [0.0001, 0.0001, 0.00015, 0.00012, 0.0001, 0.0001, 0.0001, 0.0001]

for i, cost in enumerate(costs):
    if not breaker.can_proceed():
        print(f"  #{i+1}: BLOCKED (circuit open, waiting for cooldown...)")
        time.sleep(1)
        if breaker.can_proceed():
            print(f"  #{i+1}: Circuit reset! Proceeding...")
            status = breaker.record(cost)
            print(f"  #{i+1}: ${cost:.5f} | Status: {status}")
        else:
            continue
    else:
        status = breaker.record(cost)
        print(f"  #{i+1}: ${cost:.5f} | Status: {status}")

    if status == "TRIPPED":
        print(f"    Circuit breaker TRIPPED! Pausing for {breaker.cooldown_s}s...")
        time.sleep(breaker.cooldown_s + 0.5)
# Output esperado:
#   #1: $0.00010 | Status: OK
#   #2: $0.00010 | Status: OK
#   #3: $0.00015 | Status: OK
#   #4: $0.00012 | Status: OK
#   #5: $0.00010 | Status: TRIPPED
#     Circuit breaker TRIPPED! Pausing for 3s...
#     [CIRCUIT] Auto-reset after 3s cooldown
#   #6: $0.00010 | Status: OK
#   #7: $0.00010 | Status: OK
#   #8: $0.00010 | Status: OK

Explicación: El circuit breaker se abre cuando el gasto acumulado en la ventana excede el threshold. Después del cooldown, se auto-resetea con historial limpio. Esto previene bloqueos permanentes mientras protege contra picos de gasto.

Ejercicio 6: Controller completo con rate limit + budget + degradación (Difícil)

Construye un controller que combine rate limiting (2 req/s), presupuesto ($0.002), y auto-degradación (GPT-4.1-mini → nano al 60%). Ejecuta 15 prompts y muestra la transición completa.

Ver solución
from dotenv import load_dotenv
load_dotenv()

from langchain.chat_models import init_chat_model
from langchain_core.rate_limiters import InMemoryRateLimiter
import time

PRICING = {
    "gpt-4.1-mini": {"input": 0.40, "output": 1.60},
    "gpt-4.1-nano": {"input": 0.10, "output": 0.40},
}

limiter = InMemoryRateLimiter(
    requests_per_second=2,
    check_every_n_seconds=0.1,
    max_bucket_size=3,
)

models = {
    "gpt-4.1-mini": init_chat_model("openai:gpt-4.1-mini").with_rate_limiter(limiter),
    "gpt-4.1-nano": init_chat_model("openai:gpt-4.1-nano").with_rate_limiter(limiter),
}

budget = 0.002
spent = 0.0
degradation_threshold = 0.60

start = time.time()
for i in range(15):
    if spent >= budget:
        print(f"  #{i+1}: STOPPED — budget exhausted (${spent:.6f})")
        break

    remaining_pct = 1 - (spent / budget) if budget > 0 else 0
    model_name = "gpt-4.1-mini" if remaining_pct > (1 - degradation_threshold) else "gpt-4.1-nano"

    response = models[model_name].invoke(f"Concepto {i+1} de producción AI. 1 oración.")
    usage = response.usage_metadata
    pricing = PRICING[model_name]
    cost = (usage["input_tokens"] / 1_000_000) * pricing["input"] + \
           (usage["output_tokens"] / 1_000_000) * pricing["output"]

    spent += cost
    elapsed = time.time() - start
    remaining_pct_display = (1 - spent / budget) * 100
    print(f"  #{i+1:>2} [{elapsed:>5.1f}s] {model_name:<14} ${cost:.6f} | "
          f"Budget: {remaining_pct_display:>5.1f}% | Total: ${spent:.6f}")

print(f"\nFinal: ${spent:.6f} / ${budget:.4f} en {time.time() - start:.1f}s")
# Output esperado:
#   # 1 [  0.5s] gpt-4.1-mini   $0.000120 | Budget:  94.0% | Total: $0.000120
#   # 2 [  1.0s] gpt-4.1-mini   $0.000118 | Budget:  88.1% | Total: $0.000238
#   # 3 [  1.5s] gpt-4.1-mini   $0.000115 | Budget:  82.4% | Total: $0.000353
#   # 4 [  2.0s] gpt-4.1-mini   $0.000122 | Budget:  76.3% | Total: $0.000475
#   # 5 [  2.5s] gpt-4.1-mini   $0.000119 | Budget:  70.4% | Total: $0.000594
#   # 6 [  3.0s] gpt-4.1-mini   $0.000125 | Budget:  64.2% | Total: $0.000719
#   # 7 [  3.5s] gpt-4.1-nano   $0.000025 | Budget:  62.9% | Total: $0.000744
#   # 8 [  4.0s] gpt-4.1-nano   $0.000022 | Budget:  61.8% | Total: $0.000766
#   ...
#
# Final: $0.000900 / $0.0020 en 7.5s

Explicación: Las tres capas trabajan juntas: el rate limiter espacía los requests (visible en los timestamps), el presupuesto bloquea al agotar, y la auto-degradación cambia de mini a nano al cruzar el 60%. Nota cómo el costo por request baja dramáticamente después de la degradación.


Resumen

En esta cápsula aprendiste:

  • InMemoryRateLimiter controla la velocidad de requests al modelo usando un token bucket algorithm con requests_per_second, check_every_n_seconds, y max_bucket_size
  • .with_rate_limiter() aplica el limiter a cualquier modelo de forma transparente — el caller no necesita saber que hay rate limiting
  • Per-user rate limiting asigna diferentes velocidades según el tier del usuario (free/pro/enterprise)
  • Cost budgets establecen límites de gasto diario y mensual por usuario, con verificación antes de cada llamada
  • Circuit breaker detecta tasas de gasto anómalas y pausa el sistema automáticamente para prevenir descontrol
  • Auto-degradación cambia a modelos más baratos conforme el presupuesto se agota, en vez de bloquear al usuario
  • Rate limit + cost control + model routing combinados crean un sistema de control de costos completo para producción
  • Presupuesto compartido en multi-agent asegura que todos los agentes (researcher, analyst, writer) cuentan contra el mismo límite del usuario
  • Cost alerts con notificaciones progresivas (50%, 80%, 100%) previenen sorpresas en la factura

Próxima cápsula: Production Checklist y Deployment — la checklist completa para poner tu agente de AI en producción.


Recursos adicionales

  1. LangChain Rate Limiting — Guía oficial de rate limiting en modelos
  2. InMemoryRateLimiter API — Referencia de la API
  3. Token Bucket Algorithm — Teoría detrás del rate limiting
  4. OpenAI Rate Limits — Límites de la API de OpenAI
  5. Circuit Breaker Pattern — Martin Fowler sobre circuit breakers
  6. LangSmith Usage Dashboard — Monitoreo de uso y costos en LangSmith

Módulo 12 — LangChain & LangGraph: From Chains to Agents