record('prompt_block', $tenantId, $eventId, $scope); } /** * @return array{type:string,count:int,threshold:int,escalated:bool,reason_code:?string} */ public function recordOutputBlock(int $tenantId, int $eventId, string $scope): array { return $this->record('output_block', $tenantId, $eventId, $scope); } /** * @return array{type:string,count:int,threshold:int,escalated:bool,reason_code:?string} */ private function record(string $type, int $tenantId, int $eventId, string $scope): array { $threshold = max(1, (int) config('ai-editing.abuse.escalation_threshold_per_hour', 25)); $cooldownMinutes = max(1, (int) config('ai-editing.abuse.escalation_cooldown_minutes', 30)); $bucket = now()->format('YmdH'); $counterKey = sprintf('ai-editing:abuse:%s:tenant:%d:event:%d:hour:%s', $type, $tenantId, $eventId, $bucket); Cache::add($counterKey, 0, now()->addHours(2)); $count = (int) Cache::increment($counterKey); $escalated = $count >= $threshold; if ($escalated) { $cooldownKey = sprintf('ai-editing:abuse:%s:tenant:%d:event:%d:cooldown', $type, $tenantId, $eventId); if (Cache::add($cooldownKey, 1, now()->addMinutes($cooldownMinutes))) { Log::warning('AI abuse escalation threshold reached', [ 'tenant_id' => $tenantId, 'event_id' => $eventId, 'type' => $type, 'count' => $count, 'threshold' => $threshold, 'scope_hash' => hash('sha256', $scope), ]); } } return [ 'type' => $type, 'count' => $count, 'threshold' => $threshold, 'escalated' => $escalated, 'reason_code' => $escalated ? self::REASON_CODE : null, ]; } }