Files
laravel-llm-gateway/laravel-app/app/Services/Budget/BudgetChecker.php
wtrinkl 6573e15ba4 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
2025-11-18 22:18:36 +01:00

214 lines
7.0 KiB
PHP

<?php
namespace App\Services\Budget;
use App\Models\User;
use App\Models\UserBudget;
use App\Exceptions\InsufficientBudgetException;
use Illuminate\Support\Facades\Cache;
use Illuminate\Support\Facades\Log;
class BudgetChecker
{
/**
* Check if user has sufficient budget for a request
*
* @param User $user
* @param float $estimatedCost
* @return bool
* @throws InsufficientBudgetException
*/
public function checkBudget(User $user, float $estimatedCost = 0.0): bool
{
$budget = $this->getOrCreateBudget($user);
// If budget is already exceeded, deny immediately
if ($budget->is_budget_exceeded) {
throw new InsufficientBudgetException(
"Budget limit exceeded. Current spending: $" . number_format($budget->current_month_spending, 2) .
" / Monthly limit: $" . number_format($budget->monthly_limit, 2)
);
}
// Check daily limit if set
if ($budget->daily_limit > 0) {
$projectedDailySpending = $budget->current_day_spending + $estimatedCost;
if ($projectedDailySpending > $budget->daily_limit) {
throw new InsufficientBudgetException(
"Daily budget limit would be exceeded. Current: $" . number_format($budget->current_day_spending, 2) .
" / Daily limit: $" . number_format($budget->daily_limit, 2)
);
}
}
// Check monthly limit
if ($budget->monthly_limit > 0) {
$projectedMonthlySpending = $budget->current_month_spending + $estimatedCost;
if ($projectedMonthlySpending > $budget->monthly_limit) {
throw new InsufficientBudgetException(
"Monthly budget limit would be exceeded. Current: $" . number_format($budget->current_month_spending, 2) .
" / Monthly limit: $" . number_format($budget->monthly_limit, 2)
);
}
// Check alert threshold
$usagePercentage = ($projectedMonthlySpending / $budget->monthly_limit) * 100;
if ($usagePercentage >= $budget->alert_threshold_percentage) {
$this->sendBudgetAlert($user, $budget, $usagePercentage);
}
}
return true;
}
/**
* Update user budget after a request
*
* @param User $user
* @param float $actualCost
* @return void
*/
public function updateBudget(User $user, float $actualCost): void
{
$budget = $this->getOrCreateBudget($user);
// Reset periods if needed
$this->checkAndResetPeriods($budget);
// Update spending
$budget->current_month_spending += $actualCost;
$budget->current_day_spending += $actualCost;
// Check if budget is now exceeded
if ($budget->monthly_limit > 0 && $budget->current_month_spending >= $budget->monthly_limit) {
$budget->is_budget_exceeded = true;
}
$budget->save();
// Invalidate cache
Cache::forget("user_budget:{$user->id}");
Log::info('Budget updated', [
'user_id' => $user->id,
'cost' => $actualCost,
'monthly_spending' => $budget->current_month_spending,
'daily_spending' => $budget->current_day_spending
]);
}
/**
* Get or create user budget
*
* @param User $user
* @return UserBudget
*/
private function getOrCreateBudget(User $user): UserBudget
{
$budget = $user->budget;
if (!$budget) {
$budget = UserBudget::create([
'user_id' => $user->id,
'monthly_limit' => config('llm.default_monthly_budget', 100.00),
'daily_limit' => config('llm.default_daily_budget', 10.00),
'month_started_at' => now()->startOfMonth(),
'day_started_at' => now()->startOfDay(),
'alert_threshold_percentage' => 80,
]);
Log::info('Budget created for user', ['user_id' => $user->id]);
}
return $budget;
}
/**
* Check and reset budget periods if needed
*
* @param UserBudget $budget
* @return void
*/
private function checkAndResetPeriods(UserBudget $budget): void
{
$now = now();
// Reset monthly budget if new month
if ($now->startOfMonth()->greaterThan($budget->month_started_at)) {
$budget->current_month_spending = 0.0;
$budget->month_started_at = $now->startOfMonth();
$budget->is_budget_exceeded = false;
$budget->last_alert_sent_at = null;
Log::info('Monthly budget reset', ['user_id' => $budget->user_id]);
}
// Reset daily budget if new day
if ($now->startOfDay()->greaterThan($budget->day_started_at)) {
$budget->current_day_spending = 0.0;
$budget->day_started_at = $now->startOfDay();
Log::info('Daily budget reset', ['user_id' => $budget->user_id]);
}
}
/**
* Send budget alert to user
*
* @param User $user
* @param UserBudget $budget
* @param float $usagePercentage
* @return void
*/
private function sendBudgetAlert(User $user, UserBudget $budget, float $usagePercentage): void
{
// Only send alert once per day
if ($budget->last_alert_sent_at && $budget->last_alert_sent_at->isToday()) {
return;
}
Log::warning('Budget threshold reached', [
'user_id' => $user->id,
'user_email' => $user->email,
'usage_percentage' => round($usagePercentage, 2),
'current_spending' => $budget->current_month_spending,
'monthly_limit' => $budget->monthly_limit
]);
// TODO: Send email notification
// Mail::to($user->email)->send(new BudgetAlertMail($budget, $usagePercentage));
$budget->last_alert_sent_at = now();
$budget->save();
}
/**
* Get budget status for user
*
* @param User $user
* @return array
*/
public function getBudgetStatus(User $user): array
{
$budget = $this->getOrCreateBudget($user);
return [
'monthly_limit' => $budget->monthly_limit,
'daily_limit' => $budget->daily_limit,
'current_month_spending' => $budget->current_month_spending,
'current_day_spending' => $budget->current_day_spending,
'monthly_remaining' => max(0, $budget->monthly_limit - $budget->current_month_spending),
'daily_remaining' => max(0, $budget->daily_limit - $budget->current_day_spending),
'monthly_usage_percentage' => $budget->monthly_limit > 0
? ($budget->current_month_spending / $budget->monthly_limit) * 100
: 0,
'is_exceeded' => $budget->is_budget_exceeded,
'month_started_at' => $budget->month_started_at,
'day_started_at' => $budget->day_started_at,
];
}
}