Add complete Laravel LLM Gateway implementation
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
This commit is contained in:
240
laravel-app/app/Services/RateLimit/RateLimitChecker.php
Normal file
240
laravel-app/app/Services/RateLimit/RateLimitChecker.php
Normal file
@@ -0,0 +1,240 @@
|
||||
<?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]);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user