El tope que también se come el razonamiento
Un tope puesto sin cuidado provoca un fallo más sutil que una factura grande.
En modelos que razonan antes de responder, el límite de salida cubre tanto el razonamiento como la respuesta visible. Un límite que parece generoso frente a las respuestas que esperas puede consumirse casi entero pensando, dejando que la respuesta visible se corte a media frase.
Eso es exactamente lo que pasó en el chat. El límite se había fijado cuando el modelo no pensaba antes de responder, y sobraba. Tras un cambio de modelo, las respuestas empezaron a terminar a mitad de idea —y, como nada comprobaba por qué se había detenido la generación, se mostraban como si estuvieran completas.
Dos arreglos, y necesitas los dos. Sube el tope para tener en cuenta el razonamiento, y comprueba la razón de parada, para que cuando se alcance el tope se le diga al usuario en lugar de enseñarle un fragmento seguro de sí mismo:
if ($stopReason === 'max_tokens') {
report(new \RuntimeException('Response truncated on max_tokens'));
$state['messages'][] = [
'role' => 'bot',
'content' => __('messages.errors.ai_truncated'),
'is_error' => true,
];
$state['hasError'] = true;
break;
}
Un tope cuyo impacto no puedes observar es un tope que degrada tu producto en silencio. El capítulo 8 cubre el conjunto completo de razones de parada; esta va aquí porque es la consecuencia directa de un número que elegiste tú.
Dos límites, no uno
Limitar la frecuencia y presupuestar se parecen y responden a preguntas completamente distintas.
Un límite de frecuencia responde: ¿esto va demasiado rápido? Protege la cuota del proveedor, tus workers y la cola. Se reinicia constantemente —por minuto, cada pocos segundos— y va en la ruta, donde Laravel ya lo hace bien.
Un presupuesto responde: ¿esto es demasiado? Protege el dinero. Se reinicia a diario o mensualmente, y va en el manejador, donde sabes quién paga.
Necesitas ambos, y ninguno sustituye al otro. Un límite por minuto permite una factura mensual enorme y perfectamente repartida. Un techo diario no hace nada contra una ráfaga que satura todos tus workers en diez segundos.
Incrementa primero, pregunta después
Aquí está el presupuesto, y el orden es todo:
$perDay = config('site.ai_editable.per_day');
$cacheKey = 'ai-editable:'.$site->id.':'.now()->format('Y-m-d');
Cache::add($cacheKey, 0, now()->endOfDay());
$used = (int) Cache::increment($cacheKey);
if ($used > $perDay) {
return new ErrorResponse(
'Daily AI edit limit reached for this site.',
Response::HTTP_TOO_MANY_REQUESTS,
['Retry-After' => (string) now()->diffInSeconds(now()->endOfDay())],
);
}
La versión tentadora lee el contador, lo compara e incrementa si hay hueco. Esa versión tiene fugas. Bajo concurrencia, veinte peticiones leen el mismo valor, todas deciden que hay hueco y todas siguen adelante —y lo único que un límite de gasto nunca debe hacer es fallar abriendo.
Incrementar primero hace que el contador sea autoritativo. Toda petición que llega hasta aquí ya está contada; la comparación se hace contra un número que se incluye a sí misma. Dos peticiones compitiendo en el límite ven valores distintos y exactamente una es rechazada. El coste es que una petición rechazada consume igualmente un hueco del contador, que es la dirección correcta en la que equivocarse.
Cache::add antes de increment siembra la clave sólo si falta, en una operación atómica, y fija la caducidad para que el contador desaparezca a medianoche en vez de depender de que se ejecute un código que hay que acordarse de escribir.