Ir al contenido principal
Laravel, thinking fast.

La clave merece tanta reflexión como el límite:

'ai-editable:'.$site->id.':'.now()->format('Y-m-d')

La elección obvia es la credencial con la que llegó la petición. Está mal. Esa credencial de edición es efímera por diseño y rota varias veces por hora —y un presupuesto atado a ella se reinicia en cada rotación. El límite estaría perfectamente implementado y sería completamente inútil, que es la peor combinación, porque parece que funciona.

Átalo a la cosa duradera que se corresponde con el dinero: el sitio, el espacio de trabajo, la cuenta. Entonces el techo aguanta entre sesiones, credenciales y dispositivos, por muchas sesiones de edición que se abran.

La fecha en la clave es lo que hace que caduque sola. Sin reinicio programado, sin tarea de limpieza, sin posibilidad de que una tarea de reinicio falle en silencio y deje a un cliente bloqueado para siempre.

Di que no como es debido

Una negativa es una respuesta, y merece el mismo cuidado que un acierto.

Usa 429, no 403. Esto no es un fallo de permisos. El llamante puede volver a intentarlo, y el código de estado debería decirlo.

Envía Retry-After. Calculado hasta cuando el presupuesto realmente se reinicia —el final del día, aquí— no una constante adivinada. Un cliente que lo respeta deja de machacarte, y uno que no, al menos recibe la verdad.

Di qué límite se alcanzó. «Has alcanzado el límite diario de ediciones con IA para este sitio» le dice al comerciante qué pasó y que no es permanente. «Demasiadas peticiones» no le dice nada y genera un ticket de soporte.

La misma regla que en todo el libro: el camino del fallo es parte de la función.

Lee el bloque de uso

No puedes gestionar lo que nunca miras, y los números de uso vuelven en cada respuesta —gratis, ya pagados y casi universalmente descartados.

Acumúlalos a lo largo de un turno, porque un turno son muchas llamadas:

private function accumulateUsage(array $response): void
{
    $usage = $response['usage'] ?? null;

    if (! is_array($usage)) {
        return;
    }

    foreach (['input_tokens', 'output_tokens', 'cache_read_input_tokens', 'cache_creation_input_tokens'] as $key) {
        $this->usage[$key] = ($this->usage[$key] ?? 0) + (int) ($usage[$key] ?? 0);
    }
}

Luego registra el total una vez, con el recuento de iteraciones al lado:

Log::info('ai_widget.turn', [
    'iterations' => $iterations,
    'input_tokens' => $this->usage['input_tokens'] ?? 0,
    'output_tokens' => $this->usage['output_tokens'] ?? 0,
    'cache_read_input_tokens' => $this->usage['cache_read_input_tokens'] ?? 0,
    'cache_creation_input_tokens' => $this->usage['cache_creation_input_tokens'] ?? 0,
]);

Cada número responde a una pregunta que acabarán haciéndote:

  • Entrada que sube durante un turno — el historial crece, como debe; la cuestión es si crece de forma lineal o cuadrática.
  • Salida cerca del tope — o tu tope es bajo o tu prompt pide demasiado.
  • Muchas iteraciones — el modelo recurre a herramientas una y otra vez, lo cual es un problema de prompt o de descripciones, no de presupuesto.
  • Lecturas de caché a cero — la siguiente sección.

Antes de que esto existiera, el gasto era a la vez ilimitado e invisible. Lo peligroso es lo ilimitado; lo invisible es por qué nadie se dio cuenta.