getOrCreateRateLimit($user); // If currently rate limited, check if ban has expired if ($rateLimit->is_rate_limited) { if ($rateLimit->rate_limit_expires_at && now()->greaterThan($rateLimit->rate_limit_expires_at)) { // Rate limit expired, reset $rateLimit->is_rate_limited = false; $rateLimit->rate_limit_expires_at = null; $rateLimit->save(); } else { // Still rate limited $expiresIn = $rateLimit->rate_limit_expires_at ? $rateLimit->rate_limit_expires_at->diffInSeconds(now()) : 60; throw new RateLimitExceededException( "Rate limit exceeded. Please try again in " . $expiresIn . " seconds." ); } } // Reset counters if periods have passed $this->resetPeriodsIfNeeded($rateLimit); // Check minute limit if ($rateLimit->requests_per_minute > 0) { if ($rateLimit->current_minute_count >= $rateLimit->requests_per_minute) { $this->setRateLimited($rateLimit, 60); throw new RateLimitExceededException( "Minute rate limit exceeded ({$rateLimit->requests_per_minute} requests/min). Try again in 60 seconds." ); } } // Check hour limit if ($rateLimit->requests_per_hour > 0) { if ($rateLimit->current_hour_count >= $rateLimit->requests_per_hour) { $this->setRateLimited($rateLimit, 3600); throw new RateLimitExceededException( "Hourly rate limit exceeded ({$rateLimit->requests_per_hour} requests/hour). Try again in 1 hour." ); } } // Check day limit if ($rateLimit->requests_per_day > 0) { if ($rateLimit->current_day_count >= $rateLimit->requests_per_day) { $secondsUntilMidnight = now()->endOfDay()->diffInSeconds(now()); $this->setRateLimited($rateLimit, $secondsUntilMidnight); throw new RateLimitExceededException( "Daily rate limit exceeded ({$rateLimit->requests_per_day} requests/day). Try again tomorrow." ); } } return true; } /** * Increment rate limit counters after a request * * @param User $user * @return void */ public function incrementCounter(User $user): void { $rateLimit = $this->getOrCreateRateLimit($user); // Reset periods if needed $this->resetPeriodsIfNeeded($rateLimit); // Increment counters $rateLimit->current_minute_count++; $rateLimit->current_hour_count++; $rateLimit->current_day_count++; $rateLimit->save(); Log::debug('Rate limit counter incremented', [ 'user_id' => $user->id, 'minute_count' => $rateLimit->current_minute_count, 'hour_count' => $rateLimit->current_hour_count, 'day_count' => $rateLimit->current_day_count ]); } /** * Get or create rate limit for user * * @param User $user * @return RateLimit */ private function getOrCreateRateLimit(User $user): RateLimit { $rateLimit = $user->rateLimit; if (!$rateLimit) { $rateLimit = RateLimit::create([ 'user_id' => $user->id, 'requests_per_minute' => config('llm.rate_limit.requests_per_minute', 60), 'requests_per_hour' => config('llm.rate_limit.requests_per_hour', 1000), 'requests_per_day' => config('llm.rate_limit.requests_per_day', 10000), 'minute_started_at' => now(), 'hour_started_at' => now(), 'day_started_at' => now()->startOfDay(), ]); Log::info('Rate limit created for user', ['user_id' => $user->id]); } return $rateLimit; } /** * Reset rate limit periods if needed * * @param RateLimit $rateLimit * @return void */ private function resetPeriodsIfNeeded(RateLimit $rateLimit): void { $now = now(); $changed = false; // Reset minute counter if a minute has passed if ($now->diffInSeconds($rateLimit->minute_started_at) >= 60) { $rateLimit->current_minute_count = 0; $rateLimit->minute_started_at = $now; $changed = true; } // Reset hour counter if an hour has passed if ($now->diffInSeconds($rateLimit->hour_started_at) >= 3600) { $rateLimit->current_hour_count = 0; $rateLimit->hour_started_at = $now; $changed = true; } // Reset day counter if a new day has started if ($now->startOfDay()->greaterThan($rateLimit->day_started_at)) { $rateLimit->current_day_count = 0; $rateLimit->day_started_at = $now->startOfDay(); $changed = true; } if ($changed) { $rateLimit->save(); } } /** * Set user as rate limited * * @param RateLimit $rateLimit * @param int $durationSeconds * @return void */ private function setRateLimited(RateLimit $rateLimit, int $durationSeconds): void { $rateLimit->is_rate_limited = true; $rateLimit->rate_limit_expires_at = now()->addSeconds($durationSeconds); $rateLimit->save(); Log::warning('User rate limited', [ 'user_id' => $rateLimit->user_id, 'expires_at' => $rateLimit->rate_limit_expires_at, 'duration_seconds' => $durationSeconds ]); } /** * Get rate limit status for user * * @param User $user * @return array */ public function getRateLimitStatus(User $user): array { $rateLimit = $this->getOrCreateRateLimit($user); return [ 'requests_per_minute' => $rateLimit->requests_per_minute, 'requests_per_hour' => $rateLimit->requests_per_hour, 'requests_per_day' => $rateLimit->requests_per_day, 'current_minute_count' => $rateLimit->current_minute_count, 'current_hour_count' => $rateLimit->current_hour_count, 'current_day_count' => $rateLimit->current_day_count, 'minute_remaining' => max(0, $rateLimit->requests_per_minute - $rateLimit->current_minute_count), 'hour_remaining' => max(0, $rateLimit->requests_per_hour - $rateLimit->current_hour_count), 'day_remaining' => max(0, $rateLimit->requests_per_day - $rateLimit->current_day_count), 'is_rate_limited' => $rateLimit->is_rate_limited, 'rate_limit_expires_at' => $rateLimit->rate_limit_expires_at, ]; } /** * Manually reset rate limit for user (admin function) * * @param User $user * @return void */ public function resetRateLimit(User $user): void { $rateLimit = $this->getOrCreateRateLimit($user); $rateLimit->current_minute_count = 0; $rateLimit->current_hour_count = 0; $rateLimit->current_day_count = 0; $rateLimit->is_rate_limited = false; $rateLimit->rate_limit_expires_at = null; $rateLimit->minute_started_at = now(); $rateLimit->hour_started_at = now(); $rateLimit->day_started_at = now()->startOfDay(); $rateLimit->save(); Log::info('Rate limit manually reset', ['user_id' => $user->id]); } }