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
214 lines
7.0 KiB
PHP
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,
|
|
];
|
|
}
|
|
}
|