Core Features: - Multi-provider support (OpenAI, Anthropic, DeepSeek, Gemini, Mistral) - Provider service architecture with abstract base class - Dynamic model discovery from provider APIs - Encrypted per-user provider credentials storage Admin Interface: - Complete admin panel with Livewire components - User management with CRUD operations - API key management with testing capabilities - Budget system with limits and reset schedules - Usage logs with filtering and CSV export - Model pricing management with cost calculator - Dashboard with Chart.js visualizations Database Schema: - MariaDB migrations for all tables - User provider credentials (encrypted) - LLM request logging - Budget tracking and rate limiting - Model pricing configuration API Implementation: - OpenAI-compatible endpoints - Budget checking middleware - Rate limit enforcement - Request logging jobs - Cost calculation service Testing: - Unit tests for all provider services - Provider factory tests - Cost calculator tests Documentation: - Admin user seeder - Model pricing seeder - Configuration files
241 lines
8.0 KiB
PHP
241 lines
8.0 KiB
PHP
<?php
|
|
|
|
namespace App\Services\RateLimit;
|
|
|
|
use App\Models\User;
|
|
use App\Models\RateLimit;
|
|
use App\Exceptions\RateLimitExceededException;
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class RateLimitChecker
|
|
{
|
|
/**
|
|
* Check if user has exceeded rate limits
|
|
*
|
|
* @param User $user
|
|
* @return bool
|
|
* @throws RateLimitExceededException
|
|
*/
|
|
public function checkRateLimit(User $user): bool
|
|
{
|
|
$rateLimit = $this->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]);
|
|
}
|
|
}
|