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:
wtrinkl
2025-11-18 22:18:36 +01:00
parent bef36c7ca2
commit 6573e15ba4
60 changed files with 5991 additions and 0 deletions

View 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]);
}
}