Initial commit: Any-LLM Gateway with Laravel Admin Interface
- Any-LLM Gateway setup with Docker Compose - Laravel 11 admin interface with Livewire - Dashboard with usage statistics and charts - Gateway Users management with budget tracking - API Keys management with revocation - Budget templates with assignment - Usage Logs with filtering and CSV export - Model Pricing management with calculator - PostgreSQL database integration - Complete authentication system for admins
This commit is contained in:
201
laravel-app/app/Http/Controllers/ApiKeyController.php
Normal file
201
laravel-app/app/Http/Controllers/ApiKeyController.php
Normal file
@@ -0,0 +1,201 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\ApiKey;
|
||||
use App\Models\GatewayUser;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class ApiKeyController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the API keys.
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = ApiKey::with('gatewayUser');
|
||||
|
||||
// Filter by status
|
||||
if ($request->has('status')) {
|
||||
switch ($request->status) {
|
||||
case 'active':
|
||||
$query->active();
|
||||
break;
|
||||
case 'expired':
|
||||
$query->expired();
|
||||
break;
|
||||
case 'inactive':
|
||||
$query->where('is_active', false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Filter by user
|
||||
if ($request->has('user_id') && $request->user_id) {
|
||||
$query->where('user_id', $request->user_id);
|
||||
}
|
||||
|
||||
// Search by key name
|
||||
if ($request->has('search') && $request->search) {
|
||||
$query->where('key_name', 'like', '%' . $request->search . '%');
|
||||
}
|
||||
|
||||
// Sort
|
||||
$sortBy = $request->get('sort_by', 'created_at');
|
||||
$sortOrder = $request->get('sort_order', 'desc');
|
||||
$query->orderBy($sortBy, $sortOrder);
|
||||
|
||||
$apiKeys = $query->paginate(20)->withQueryString();
|
||||
$gatewayUsers = GatewayUser::orderBy('alias')->get();
|
||||
|
||||
return view('api-keys.index', compact('apiKeys', 'gatewayUsers'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new API key.
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$gatewayUsers = GatewayUser::orderBy('alias')->get();
|
||||
return view('api-keys.create', compact('gatewayUsers'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created API key.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'key_name' => 'required|string|max:255',
|
||||
'user_id' => 'required|string|exists:users,user_id',
|
||||
'expires_at' => 'nullable|date|after:now',
|
||||
'metadata' => 'nullable|json',
|
||||
]);
|
||||
|
||||
try {
|
||||
// Get master key from config
|
||||
$masterKey = env('GATEWAY_MASTER_KEY');
|
||||
if (!$masterKey) {
|
||||
return back()->with('error', 'Gateway Master Key not configured');
|
||||
}
|
||||
|
||||
// Prepare request payload
|
||||
$payload = [
|
||||
'user_id' => $validated['user_id'],
|
||||
'key_name' => $validated['key_name'],
|
||||
];
|
||||
|
||||
// Add optional fields only if they have values
|
||||
if (!empty($validated['expires_at'])) {
|
||||
$payload['expires_at'] = $validated['expires_at'];
|
||||
}
|
||||
|
||||
if (!empty($validated['metadata'])) {
|
||||
$payload['metadata'] = json_decode($validated['metadata'], true) ?: new \stdClass();
|
||||
}
|
||||
|
||||
// Create Virtual Key via Any-LLM Gateway API
|
||||
$response = Http::withHeaders([
|
||||
'X-AnyLLM-Key' => 'Bearer ' . $masterKey,
|
||||
'Content-Type' => 'application/json',
|
||||
])->post(env('GATEWAY_API_URL', 'http://gateway:8000') . '/v1/keys', $payload);
|
||||
|
||||
if (!$response->successful()) {
|
||||
Log::error('Failed to create API key', [
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body()
|
||||
]);
|
||||
return back()->with('error', 'Failed to create API key: ' . $response->body());
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
// The actual key is only available once - store it in session for display
|
||||
session()->flash('new_api_key', $data['key'] ?? null);
|
||||
session()->flash('new_api_key_id', $data['id'] ?? null);
|
||||
|
||||
return redirect()->route('api-keys.index')
|
||||
->with('success', 'API Key created successfully! Make sure to copy it now - it won\'t be shown again.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Exception creating API key', ['error' => $e->getMessage()]);
|
||||
return back()->with('error', 'Failed to create API key: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified API key.
|
||||
*/
|
||||
public function show(string $id)
|
||||
{
|
||||
$apiKey = ApiKey::with(['gatewayUser', 'usageLogs'])->findOrFail($id);
|
||||
|
||||
// Get usage statistics
|
||||
$stats = [
|
||||
'total_requests' => $apiKey->usageLogs()->count(),
|
||||
'total_cost' => $apiKey->usageLogs()->sum('cost'),
|
||||
'total_tokens' => $apiKey->usageLogs()->sum('total_tokens'),
|
||||
'last_30_days_requests' => $apiKey->usageLogs()
|
||||
->where('timestamp', '>=', now()->subDays(30))
|
||||
->count(),
|
||||
];
|
||||
|
||||
// Get recent activity
|
||||
$recentLogs = $apiKey->usageLogs()
|
||||
->orderByDesc('timestamp')
|
||||
->limit(20)
|
||||
->get();
|
||||
|
||||
return view('api-keys.show', compact('apiKey', 'stats', 'recentLogs'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Revoke the specified API key.
|
||||
*/
|
||||
public function revoke(string $id)
|
||||
{
|
||||
try {
|
||||
$apiKey = ApiKey::findOrFail($id);
|
||||
|
||||
// Get master key from config
|
||||
$masterKey = env('GATEWAY_MASTER_KEY');
|
||||
if (!$masterKey) {
|
||||
return back()->with('error', 'Gateway Master Key not configured');
|
||||
}
|
||||
|
||||
// Revoke via Any-LLM Gateway API
|
||||
$response = Http::withHeaders([
|
||||
'X-AnyLLM-Key' => 'Bearer ' . $masterKey,
|
||||
'Content-Type' => 'application/json',
|
||||
])->delete(env('GATEWAY_API_URL', 'http://gateway:8000') . '/v1/keys/' . $id);
|
||||
|
||||
if (!$response->successful()) {
|
||||
Log::error('Failed to revoke API key', [
|
||||
'key_id' => $id,
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body()
|
||||
]);
|
||||
return back()->with('error', 'Failed to revoke API key: ' . $response->body());
|
||||
}
|
||||
|
||||
return redirect()->route('api-keys.index')
|
||||
->with('success', 'API Key revoked successfully');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Exception revoking API key', ['error' => $e->getMessage()]);
|
||||
return back()->with('error', 'Failed to revoke API key: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified API key.
|
||||
*/
|
||||
public function destroy(string $id)
|
||||
{
|
||||
// This is an alias for revoke
|
||||
return $this->revoke($id);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Auth;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use Illuminate\Auth\Events\Verified;
|
||||
use Illuminate\Foundation\Auth\EmailVerificationRequest;
|
||||
use Illuminate\Http\RedirectResponse;
|
||||
|
||||
class VerifyEmailController extends Controller
|
||||
{
|
||||
/**
|
||||
* Mark the authenticated user's email address as verified.
|
||||
*/
|
||||
public function __invoke(EmailVerificationRequest $request): RedirectResponse
|
||||
{
|
||||
if ($request->user()->hasVerifiedEmail()) {
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
|
||||
if ($request->user()->markEmailAsVerified()) {
|
||||
event(new Verified($request->user()));
|
||||
}
|
||||
|
||||
return redirect()->intended(route('dashboard', absolute: false).'?verified=1');
|
||||
}
|
||||
}
|
||||
179
laravel-app/app/Http/Controllers/BudgetController.php
Normal file
179
laravel-app/app/Http/Controllers/BudgetController.php
Normal file
@@ -0,0 +1,179 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\Budget;
|
||||
use App\Models\GatewayUser;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class BudgetController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of budgets
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$budgets = Budget::withCount('gatewayUsers')
|
||||
->orderBy('created_at', 'desc')
|
||||
->paginate(20);
|
||||
|
||||
return view('budgets.index', compact('budgets'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new budget
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
return view('budgets.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created budget
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'budget_name' => 'required|string|max:255',
|
||||
'max_budget' => 'required|numeric|min:0',
|
||||
'budget_type' => 'required|in:daily,weekly,monthly,custom,unlimited',
|
||||
'custom_duration_days' => 'nullable|integer|min:1|required_if:budget_type,custom',
|
||||
]);
|
||||
|
||||
// Calculate budget_duration_sec based on type
|
||||
$duration = match($validated['budget_type']) {
|
||||
'daily' => 86400, // 1 day
|
||||
'weekly' => 604800, // 7 days
|
||||
'monthly' => 2592000, // 30 days
|
||||
'custom' => ($validated['custom_duration_days'] ?? 1) * 86400,
|
||||
'unlimited' => null,
|
||||
};
|
||||
|
||||
$budget = Budget::create([
|
||||
'budget_id' => 'budget-' . Str::uuid(),
|
||||
'max_budget' => $validated['max_budget'],
|
||||
'budget_duration_sec' => $duration,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('budgets.index')
|
||||
->with('success', 'Budget template created successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified budget
|
||||
*/
|
||||
public function show(string $id)
|
||||
{
|
||||
$budget = Budget::with('gatewayUsers')->findOrFail($id);
|
||||
|
||||
// Get users without budget for potential assignment
|
||||
$availableUsers = GatewayUser::whereNull('budget_id')
|
||||
->orWhere('budget_id', '')
|
||||
->get();
|
||||
|
||||
return view('budgets.show', compact('budget', 'availableUsers'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified budget
|
||||
*/
|
||||
public function edit(string $id)
|
||||
{
|
||||
$budget = Budget::findOrFail($id);
|
||||
|
||||
// Determine budget type from duration
|
||||
$budgetType = 'unlimited';
|
||||
if ($budget->budget_duration_sec) {
|
||||
$days = $budget->budget_duration_sec / 86400;
|
||||
$budgetType = match(true) {
|
||||
$days == 1 => 'daily',
|
||||
$days == 7 => 'weekly',
|
||||
$days == 30 => 'monthly',
|
||||
default => 'custom'
|
||||
};
|
||||
}
|
||||
|
||||
return view('budgets.edit', compact('budget', 'budgetType'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified budget
|
||||
*/
|
||||
public function update(Request $request, string $id)
|
||||
{
|
||||
$budget = Budget::findOrFail($id);
|
||||
|
||||
$validated = $request->validate([
|
||||
'max_budget' => 'required|numeric|min:0',
|
||||
'budget_type' => 'required|in:daily,weekly,monthly,custom,unlimited',
|
||||
'custom_duration_days' => 'nullable|integer|min:1|required_if:budget_type,custom',
|
||||
]);
|
||||
|
||||
// Calculate budget_duration_sec based on type
|
||||
$duration = match($validated['budget_type']) {
|
||||
'daily' => 86400,
|
||||
'weekly' => 604800,
|
||||
'monthly' => 2592000,
|
||||
'custom' => ($validated['custom_duration_days'] ?? 1) * 86400,
|
||||
'unlimited' => null,
|
||||
};
|
||||
|
||||
$budget->update([
|
||||
'max_budget' => $validated['max_budget'],
|
||||
'budget_duration_sec' => $duration,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('budgets.show', $budget->budget_id)
|
||||
->with('success', 'Budget updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified budget
|
||||
*/
|
||||
public function destroy(string $id)
|
||||
{
|
||||
$budget = Budget::findOrFail($id);
|
||||
|
||||
// Check if budget is assigned to users
|
||||
if ($budget->gatewayUsers()->count() > 0) {
|
||||
return redirect()
|
||||
->route('budgets.index')
|
||||
->with('error', 'Cannot delete budget that is assigned to users. Please reassign users first.');
|
||||
}
|
||||
|
||||
$budget->delete();
|
||||
|
||||
return redirect()
|
||||
->route('budgets.index')
|
||||
->with('success', 'Budget deleted successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Assign budget to users (bulk)
|
||||
*/
|
||||
public function assignUsers(Request $request, string $id)
|
||||
{
|
||||
$budget = Budget::findOrFail($id);
|
||||
|
||||
$validated = $request->validate([
|
||||
'user_ids' => 'required|array',
|
||||
'user_ids.*' => 'exists:users,user_id',
|
||||
]);
|
||||
|
||||
GatewayUser::whereIn('user_id', $validated['user_ids'])
|
||||
->update([
|
||||
'budget_id' => $budget->budget_id,
|
||||
'budget_started_at' => now(),
|
||||
'next_budget_reset_at' => $budget->budget_duration_sec
|
||||
? now()->addSeconds($budget->budget_duration_sec)
|
||||
: null,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('budgets.show', $budget->budget_id)
|
||||
->with('success', count($validated['user_ids']) . ' user(s) assigned to budget successfully!');
|
||||
}
|
||||
}
|
||||
8
laravel-app/app/Http/Controllers/Controller.php
Normal file
8
laravel-app/app/Http/Controllers/Controller.php
Normal file
@@ -0,0 +1,8 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
abstract class Controller
|
||||
{
|
||||
//
|
||||
}
|
||||
33
laravel-app/app/Http/Controllers/DashboardController.php
Normal file
33
laravel-app/app/Http/Controllers/DashboardController.php
Normal file
@@ -0,0 +1,33 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Services\StatisticsService;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class DashboardController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private StatisticsService $statsService
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display the dashboard
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$stats = $this->statsService->getDashboardStats();
|
||||
$dailyUsage = $this->statsService->getDailyUsageChart(30);
|
||||
$topUsers = $this->statsService->getTopUsers(5);
|
||||
$providerStats = $this->statsService->getUsageByProvider(30);
|
||||
$modelStats = $this->statsService->getUsageByModel(30);
|
||||
|
||||
return view('dashboard', compact(
|
||||
'stats',
|
||||
'dailyUsage',
|
||||
'topUsers',
|
||||
'providerStats',
|
||||
'modelStats'
|
||||
));
|
||||
}
|
||||
}
|
||||
242
laravel-app/app/Http/Controllers/GatewayUserController.php
Normal file
242
laravel-app/app/Http/Controllers/GatewayUserController.php
Normal file
@@ -0,0 +1,242 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\GatewayUser;
|
||||
use App\Models\Budget;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Str;
|
||||
|
||||
class GatewayUserController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of the gateway users.
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = GatewayUser::with('budget')
|
||||
->withCount(['apiKeys', 'usageLogs']);
|
||||
|
||||
// Search
|
||||
if ($request->filled('search')) {
|
||||
$search = $request->search;
|
||||
$query->where(function ($q) use ($search) {
|
||||
$q->where('user_id', 'like', "%{$search}%")
|
||||
->orWhere('alias', 'like', "%{$search}%");
|
||||
});
|
||||
}
|
||||
|
||||
// Filter by status
|
||||
if ($request->filled('status')) {
|
||||
if ($request->status === 'active') {
|
||||
$query->active();
|
||||
} elseif ($request->status === 'blocked') {
|
||||
$query->blocked();
|
||||
}
|
||||
}
|
||||
|
||||
// Sort
|
||||
$sortField = $request->get('sort', 'created_at');
|
||||
$sortDirection = $request->get('direction', 'desc');
|
||||
$query->orderBy($sortField, $sortDirection);
|
||||
|
||||
$users = $query->paginate(20)->withQueryString();
|
||||
|
||||
return view('gateway-users.index', compact('users'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new gateway user.
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$budgets = Budget::all();
|
||||
return view('gateway-users.create', compact('budgets'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created gateway user in storage.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'alias' => 'nullable|string|max:255',
|
||||
'budget_id' => 'nullable|exists:budgets,budget_id',
|
||||
'metadata' => 'nullable|array',
|
||||
]);
|
||||
|
||||
// Generate unique user_id
|
||||
$userId = 'user_' . Str::random(16);
|
||||
|
||||
$user = GatewayUser::create([
|
||||
'user_id' => $userId,
|
||||
'alias' => $validated['alias'] ?? null,
|
||||
'budget_id' => $validated['budget_id'] ?? null,
|
||||
'spend' => 0,
|
||||
'blocked' => false,
|
||||
'metadata' => $validated['metadata'] ?? [],
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
->route('gateway-users.show', $user->user_id)
|
||||
->with('success', 'Gateway User created successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified gateway user.
|
||||
*/
|
||||
public function show(string $userId)
|
||||
{
|
||||
$user = GatewayUser::with(['apiKeys', 'budget'])
|
||||
->findOrFail($userId);
|
||||
|
||||
// Get usage statistics for last 30 days
|
||||
$stats = $this->getUserStatistics($userId, 30);
|
||||
|
||||
// Get recent logs
|
||||
$recentLogs = $user->usageLogs()
|
||||
->with('apiKey')
|
||||
->orderByDesc('timestamp')
|
||||
->limit(50)
|
||||
->get();
|
||||
|
||||
// Get daily usage for chart (last 30 days)
|
||||
$dailyUsage = $user->usageLogs()
|
||||
->selectRaw('DATE(timestamp) as date, COUNT(*) as requests, SUM(cost) as cost, SUM(total_tokens) as tokens')
|
||||
->where('timestamp', '>=', now()->subDays(30))
|
||||
->groupBy('date')
|
||||
->orderBy('date')
|
||||
->get();
|
||||
|
||||
return view('gateway-users.show', compact('user', 'stats', 'recentLogs', 'dailyUsage'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified gateway user.
|
||||
*/
|
||||
public function edit(string $userId)
|
||||
{
|
||||
$user = GatewayUser::findOrFail($userId);
|
||||
$budgets = Budget::all();
|
||||
|
||||
return view('gateway-users.edit', compact('user', 'budgets'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified gateway user in storage.
|
||||
*/
|
||||
public function update(Request $request, string $userId)
|
||||
{
|
||||
$user = GatewayUser::findOrFail($userId);
|
||||
|
||||
$validated = $request->validate([
|
||||
'alias' => 'nullable|string|max:255',
|
||||
'budget_id' => 'nullable|exists:budgets,budget_id',
|
||||
'metadata' => 'nullable|array',
|
||||
]);
|
||||
|
||||
$user->update($validated);
|
||||
|
||||
return redirect()
|
||||
->route('gateway-users.show', $user->user_id)
|
||||
->with('success', 'Gateway User updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified gateway user from storage.
|
||||
*/
|
||||
public function destroy(string $userId)
|
||||
{
|
||||
$user = GatewayUser::findOrFail($userId);
|
||||
|
||||
// Delete associated API keys and usage logs
|
||||
$user->apiKeys()->delete();
|
||||
$user->usageLogs()->delete();
|
||||
$user->delete();
|
||||
|
||||
return redirect()
|
||||
->route('gateway-users.index')
|
||||
->with('success', 'Gateway User deleted successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Toggle block status of a gateway user.
|
||||
*/
|
||||
public function toggleBlock(string $userId)
|
||||
{
|
||||
$user = GatewayUser::findOrFail($userId);
|
||||
$user->blocked = !$user->blocked;
|
||||
$user->save();
|
||||
|
||||
$status = $user->blocked ? 'blocked' : 'unblocked';
|
||||
|
||||
return redirect()
|
||||
->back()
|
||||
->with('success', "User has been {$status} successfully!");
|
||||
}
|
||||
|
||||
/**
|
||||
* Bulk actions for gateway users.
|
||||
*/
|
||||
public function bulkAction(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'action' => 'required|in:block,unblock,delete',
|
||||
'user_ids' => 'required|array|min:1',
|
||||
'user_ids.*' => 'exists:users,user_id',
|
||||
]);
|
||||
|
||||
$count = 0;
|
||||
|
||||
switch ($validated['action']) {
|
||||
case 'block':
|
||||
GatewayUser::whereIn('user_id', $validated['user_ids'])
|
||||
->update(['blocked' => true]);
|
||||
$count = count($validated['user_ids']);
|
||||
$message = "{$count} user(s) have been blocked successfully!";
|
||||
break;
|
||||
|
||||
case 'unblock':
|
||||
GatewayUser::whereIn('user_id', $validated['user_ids'])
|
||||
->update(['blocked' => false]);
|
||||
$count = count($validated['user_ids']);
|
||||
$message = "{$count} user(s) have been unblocked successfully!";
|
||||
break;
|
||||
|
||||
case 'delete':
|
||||
foreach ($validated['user_ids'] as $userId) {
|
||||
$user = GatewayUser::find($userId);
|
||||
if ($user) {
|
||||
$user->apiKeys()->delete();
|
||||
$user->usageLogs()->delete();
|
||||
$user->delete();
|
||||
$count++;
|
||||
}
|
||||
}
|
||||
$message = "{$count} user(s) have been deleted successfully!";
|
||||
break;
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('gateway-users.index')
|
||||
->with('success', $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user statistics for a given time period.
|
||||
*/
|
||||
private function getUserStatistics(string $userId, int $days)
|
||||
{
|
||||
return \App\Models\UsageLog::where('user_id', $userId)
|
||||
->where('timestamp', '>=', now()->subDays($days))
|
||||
->selectRaw('
|
||||
COUNT(*) as total_requests,
|
||||
SUM(prompt_tokens) as total_prompt_tokens,
|
||||
SUM(completion_tokens) as total_completion_tokens,
|
||||
SUM(total_tokens) as total_tokens,
|
||||
SUM(cost) as total_cost,
|
||||
AVG(total_tokens) as avg_tokens_per_request
|
||||
')
|
||||
->first();
|
||||
}
|
||||
}
|
||||
208
laravel-app/app/Http/Controllers/ModelPricingController.php
Normal file
208
laravel-app/app/Http/Controllers/ModelPricingController.php
Normal file
@@ -0,0 +1,208 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\ModelPricing;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
|
||||
class ModelPricingController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of model pricing
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$modelPricing = ModelPricing::orderBy('model_key')
|
||||
->paginate(20);
|
||||
|
||||
return view('model-pricing.index', compact('modelPricing'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new model pricing
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
return view('model-pricing.create');
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created model pricing
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'model_key' => 'required|string|max:255|unique:model_pricing,model_key',
|
||||
'input_price_per_million' => 'required|numeric|min:0',
|
||||
'output_price_per_million' => 'required|numeric|min:0',
|
||||
]);
|
||||
|
||||
ModelPricing::create($validated);
|
||||
|
||||
return redirect()
|
||||
->route('model-pricing.index')
|
||||
->with('success', 'Model pricing created successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified model pricing
|
||||
*/
|
||||
public function show(string $modelKey)
|
||||
{
|
||||
$model = ModelPricing::findOrFail($modelKey);
|
||||
|
||||
return view('model-pricing.show', compact('model'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified model pricing
|
||||
*/
|
||||
public function edit(string $modelKey)
|
||||
{
|
||||
$model = ModelPricing::findOrFail($modelKey);
|
||||
|
||||
return view('model-pricing.edit', compact('model'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified model pricing
|
||||
*/
|
||||
public function update(Request $request, string $modelKey)
|
||||
{
|
||||
$model = ModelPricing::findOrFail($modelKey);
|
||||
|
||||
$validated = $request->validate([
|
||||
'input_price_per_million' => 'required|numeric|min:0',
|
||||
'output_price_per_million' => 'required|numeric|min:0',
|
||||
]);
|
||||
|
||||
$model->update($validated);
|
||||
|
||||
return redirect()
|
||||
->route('model-pricing.index')
|
||||
->with('success', 'Model pricing updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified model pricing
|
||||
*/
|
||||
public function destroy(string $modelKey)
|
||||
{
|
||||
$model = ModelPricing::findOrFail($modelKey);
|
||||
$model->delete();
|
||||
|
||||
return redirect()
|
||||
->route('model-pricing.index')
|
||||
->with('success', 'Model pricing deleted successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the cost calculator
|
||||
*/
|
||||
public function calculator()
|
||||
{
|
||||
$models = ModelPricing::orderBy('model_key')->get();
|
||||
|
||||
return view('model-pricing.calculator', compact('models'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate cost based on input
|
||||
*/
|
||||
public function calculate(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'model_key' => 'required|exists:model_pricing,model_key',
|
||||
'input_tokens' => 'required|integer|min:0',
|
||||
'output_tokens' => 'required|integer|min:0',
|
||||
]);
|
||||
|
||||
$model = ModelPricing::findOrFail($validated['model_key']);
|
||||
$cost = $model->calculateCost(
|
||||
$validated['input_tokens'],
|
||||
$validated['output_tokens']
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'model' => $model->model_key,
|
||||
'input_tokens' => $validated['input_tokens'],
|
||||
'output_tokens' => $validated['output_tokens'],
|
||||
'total_tokens' => $validated['input_tokens'] + $validated['output_tokens'],
|
||||
'cost' => $cost,
|
||||
'cost_formatted' => '$' . number_format($cost, 6),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Show CSV import form
|
||||
*/
|
||||
public function importForm()
|
||||
{
|
||||
return view('model-pricing.import');
|
||||
}
|
||||
|
||||
/**
|
||||
* Import model pricing from CSV
|
||||
*/
|
||||
public function import(Request $request)
|
||||
{
|
||||
$request->validate([
|
||||
'csv_file' => 'required|file|mimes:csv,txt|max:2048',
|
||||
]);
|
||||
|
||||
$file = $request->file('csv_file');
|
||||
$handle = fopen($file->getRealPath(), 'r');
|
||||
|
||||
$imported = 0;
|
||||
$updated = 0;
|
||||
$errors = [];
|
||||
|
||||
// Skip header row
|
||||
fgetcsv($handle);
|
||||
|
||||
while (($row = fgetcsv($handle)) !== false) {
|
||||
if (count($row) < 3) {
|
||||
continue; // Skip invalid rows
|
||||
}
|
||||
|
||||
$modelKey = trim($row[0]);
|
||||
$inputPrice = floatval($row[1]);
|
||||
$outputPrice = floatval($row[2]);
|
||||
|
||||
if (empty($modelKey) || $inputPrice < 0 || $outputPrice < 0) {
|
||||
$errors[] = "Invalid data for model: {$modelKey}";
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing = ModelPricing::find($modelKey);
|
||||
|
||||
if ($existing) {
|
||||
$existing->update([
|
||||
'input_price_per_million' => $inputPrice,
|
||||
'output_price_per_million' => $outputPrice,
|
||||
]);
|
||||
$updated++;
|
||||
} else {
|
||||
ModelPricing::create([
|
||||
'model_key' => $modelKey,
|
||||
'input_price_per_million' => $inputPrice,
|
||||
'output_price_per_million' => $outputPrice,
|
||||
]);
|
||||
$imported++;
|
||||
}
|
||||
}
|
||||
|
||||
fclose($handle);
|
||||
|
||||
$message = "Import completed! Created: {$imported}, Updated: {$updated}";
|
||||
|
||||
if (count($errors) > 0) {
|
||||
$message .= '. Errors: ' . implode(', ', array_slice($errors, 0, 5));
|
||||
}
|
||||
|
||||
return redirect()
|
||||
->route('model-pricing.index')
|
||||
->with('success', $message);
|
||||
}
|
||||
}
|
||||
187
laravel-app/app/Http/Controllers/UsageLogController.php
Normal file
187
laravel-app/app/Http/Controllers/UsageLogController.php
Normal file
@@ -0,0 +1,187 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers;
|
||||
|
||||
use App\Models\UsageLog;
|
||||
use App\Models\GatewayUser;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class UsageLogController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of usage logs with filters
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = UsageLog::with(['gatewayUser', 'apiKey']);
|
||||
|
||||
// Date Range Filter
|
||||
if ($request->filled('date_from')) {
|
||||
$query->where('timestamp', '>=', $request->date_from . ' 00:00:00');
|
||||
}
|
||||
|
||||
if ($request->filled('date_to')) {
|
||||
$query->where('timestamp', '<=', $request->date_to . ' 23:59:59');
|
||||
}
|
||||
|
||||
// User Filter
|
||||
if ($request->filled('user_id')) {
|
||||
$query->where('user_id', $request->user_id);
|
||||
}
|
||||
|
||||
// Provider Filter
|
||||
if ($request->filled('provider')) {
|
||||
$query->where('provider', $request->provider);
|
||||
}
|
||||
|
||||
// Model Filter
|
||||
if ($request->filled('model')) {
|
||||
$query->where('model', $request->model);
|
||||
}
|
||||
|
||||
// Status Filter
|
||||
if ($request->filled('status')) {
|
||||
if ($request->status === 'success') {
|
||||
$query->success();
|
||||
} elseif ($request->status === 'failed') {
|
||||
$query->failed();
|
||||
}
|
||||
}
|
||||
|
||||
// Get filter options for dropdowns
|
||||
$users = GatewayUser::select('user_id', 'alias')
|
||||
->orderBy('alias')
|
||||
->get();
|
||||
|
||||
$providers = UsageLog::select('provider')
|
||||
->distinct()
|
||||
->whereNotNull('provider')
|
||||
->orderBy('provider')
|
||||
->pluck('provider');
|
||||
|
||||
$models = UsageLog::select('model')
|
||||
->distinct()
|
||||
->whereNotNull('model')
|
||||
->orderBy('model')
|
||||
->pluck('model');
|
||||
|
||||
// Get summary statistics for current filter
|
||||
$summary = $query->clone()
|
||||
->selectRaw('
|
||||
COUNT(*) as total_requests,
|
||||
SUM(CASE WHEN status = \'success\' THEN 1 ELSE 0 END) as successful_requests,
|
||||
SUM(prompt_tokens) as total_prompt_tokens,
|
||||
SUM(completion_tokens) as total_completion_tokens,
|
||||
SUM(total_tokens) as total_tokens,
|
||||
SUM(cost) as total_cost
|
||||
')
|
||||
->first();
|
||||
|
||||
// Paginate results
|
||||
$logs = $query->orderByDesc('timestamp')
|
||||
->paginate(50)
|
||||
->withQueryString(); // Preserve query parameters
|
||||
|
||||
return view('usage-logs.index', compact(
|
||||
'logs',
|
||||
'users',
|
||||
'providers',
|
||||
'models',
|
||||
'summary'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Export usage logs to CSV
|
||||
*/
|
||||
public function export(Request $request)
|
||||
{
|
||||
$query = UsageLog::with(['gatewayUser', 'apiKey']);
|
||||
|
||||
// Apply same filters as index
|
||||
if ($request->filled('date_from')) {
|
||||
$query->where('timestamp', '>=', $request->date_from . ' 00:00:00');
|
||||
}
|
||||
|
||||
if ($request->filled('date_to')) {
|
||||
$query->where('timestamp', '<=', $request->date_to . ' 23:59:59');
|
||||
}
|
||||
|
||||
if ($request->filled('user_id')) {
|
||||
$query->where('user_id', $request->user_id);
|
||||
}
|
||||
|
||||
if ($request->filled('provider')) {
|
||||
$query->where('provider', $request->provider);
|
||||
}
|
||||
|
||||
if ($request->filled('model')) {
|
||||
$query->where('model', $request->model);
|
||||
}
|
||||
|
||||
if ($request->filled('status')) {
|
||||
if ($request->status === 'success') {
|
||||
$query->success();
|
||||
} elseif ($request->status === 'failed') {
|
||||
$query->failed();
|
||||
}
|
||||
}
|
||||
|
||||
// Limit export to 10,000 records for performance
|
||||
$logs = $query->orderByDesc('timestamp')
|
||||
->limit(10000)
|
||||
->get();
|
||||
|
||||
$filename = 'usage-logs-' . now()->format('Y-m-d-His') . '.csv';
|
||||
|
||||
$headers = [
|
||||
'Content-Type' => 'text/csv',
|
||||
'Content-Disposition' => "attachment; filename=\"{$filename}\"",
|
||||
];
|
||||
|
||||
$callback = function() use ($logs) {
|
||||
$file = fopen('php://output', 'w');
|
||||
|
||||
// CSV Header
|
||||
fputcsv($file, [
|
||||
'Timestamp',
|
||||
'User ID',
|
||||
'User Alias',
|
||||
'API Key',
|
||||
'Provider',
|
||||
'Model',
|
||||
'Endpoint',
|
||||
'Prompt Tokens',
|
||||
'Completion Tokens',
|
||||
'Total Tokens',
|
||||
'Cost',
|
||||
'Status',
|
||||
'Error Message'
|
||||
]);
|
||||
|
||||
// CSV Rows
|
||||
foreach ($logs as $log) {
|
||||
fputcsv($file, [
|
||||
$log->timestamp->format('Y-m-d H:i:s'),
|
||||
$log->user_id,
|
||||
$log->gatewayUser?->alias ?? 'N/A',
|
||||
$log->api_key_id,
|
||||
$log->provider,
|
||||
$log->model,
|
||||
$log->endpoint,
|
||||
$log->prompt_tokens,
|
||||
$log->completion_tokens,
|
||||
$log->total_tokens,
|
||||
$log->cost,
|
||||
$log->status,
|
||||
$log->error_message ?? ''
|
||||
]);
|
||||
}
|
||||
|
||||
fclose($file);
|
||||
};
|
||||
|
||||
return response()->stream($callback, 200, $headers);
|
||||
}
|
||||
}
|
||||
20
laravel-app/app/Livewire/Actions/Logout.php
Normal file
20
laravel-app/app/Livewire/Actions/Logout.php
Normal file
@@ -0,0 +1,20 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Actions;
|
||||
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\Session;
|
||||
|
||||
class Logout
|
||||
{
|
||||
/**
|
||||
* Log the current user out of the application.
|
||||
*/
|
||||
public function __invoke(): void
|
||||
{
|
||||
Auth::guard('web')->logout();
|
||||
|
||||
Session::invalidate();
|
||||
Session::regenerateToken();
|
||||
}
|
||||
}
|
||||
72
laravel-app/app/Livewire/Forms/LoginForm.php
Normal file
72
laravel-app/app/Livewire/Forms/LoginForm.php
Normal file
@@ -0,0 +1,72 @@
|
||||
<?php
|
||||
|
||||
namespace App\Livewire\Forms;
|
||||
|
||||
use Illuminate\Auth\Events\Lockout;
|
||||
use Illuminate\Support\Facades\Auth;
|
||||
use Illuminate\Support\Facades\RateLimiter;
|
||||
use Illuminate\Support\Str;
|
||||
use Illuminate\Validation\ValidationException;
|
||||
use Livewire\Attributes\Validate;
|
||||
use Livewire\Form;
|
||||
|
||||
class LoginForm extends Form
|
||||
{
|
||||
#[Validate('required|string|email')]
|
||||
public string $email = '';
|
||||
|
||||
#[Validate('required|string')]
|
||||
public string $password = '';
|
||||
|
||||
#[Validate('boolean')]
|
||||
public bool $remember = false;
|
||||
|
||||
/**
|
||||
* Attempt to authenticate the request's credentials.
|
||||
*
|
||||
* @throws \Illuminate\Validation\ValidationException
|
||||
*/
|
||||
public function authenticate(): void
|
||||
{
|
||||
$this->ensureIsNotRateLimited();
|
||||
|
||||
if (! Auth::attempt($this->only(['email', 'password']), $this->remember)) {
|
||||
RateLimiter::hit($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'form.email' => trans('auth.failed'),
|
||||
]);
|
||||
}
|
||||
|
||||
RateLimiter::clear($this->throttleKey());
|
||||
}
|
||||
|
||||
/**
|
||||
* Ensure the authentication request is not rate limited.
|
||||
*/
|
||||
protected function ensureIsNotRateLimited(): void
|
||||
{
|
||||
if (! RateLimiter::tooManyAttempts($this->throttleKey(), 5)) {
|
||||
return;
|
||||
}
|
||||
|
||||
event(new Lockout(request()));
|
||||
|
||||
$seconds = RateLimiter::availableIn($this->throttleKey());
|
||||
|
||||
throw ValidationException::withMessages([
|
||||
'form.email' => trans('auth.throttle', [
|
||||
'seconds' => $seconds,
|
||||
'minutes' => ceil($seconds / 60),
|
||||
]),
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the authentication rate limiting throttle key.
|
||||
*/
|
||||
protected function throttleKey(): string
|
||||
{
|
||||
return Str::transliterate(Str::lower($this->email).'|'.request()->ip());
|
||||
}
|
||||
}
|
||||
45
laravel-app/app/Models/Admin.php
Normal file
45
laravel-app/app/Models/Admin.php
Normal file
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class Admin extends Authenticatable
|
||||
{
|
||||
use Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
}
|
||||
69
laravel-app/app/Models/ApiKey.php
Normal file
69
laravel-app/app/Models/ApiKey.php
Normal file
@@ -0,0 +1,69 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ApiKey extends Model
|
||||
{
|
||||
protected $primaryKey = 'id';
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'key_hash',
|
||||
'key_name',
|
||||
'user_id',
|
||||
'last_used_at',
|
||||
'expires_at',
|
||||
'is_active',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_active' => 'boolean',
|
||||
'metadata' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
'last_used_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function gatewayUser()
|
||||
{
|
||||
return $this->belongsTo(GatewayUser::class, 'user_id', 'user_id');
|
||||
}
|
||||
|
||||
public function usageLogs()
|
||||
{
|
||||
return $this->hasMany(UsageLog::class, 'api_key_id', 'id');
|
||||
}
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true)
|
||||
->where(function ($q) {
|
||||
$q->whereNull('expires_at')
|
||||
->orWhere('expires_at', '>', now());
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeExpired($query)
|
||||
{
|
||||
return $query->whereNotNull('expires_at')
|
||||
->where('expires_at', '<=', now());
|
||||
}
|
||||
|
||||
public function getMaskedKeyAttribute()
|
||||
{
|
||||
return 'gw-' . substr($this->id, 0, 8) . '...' . substr($this->id, -8);
|
||||
}
|
||||
|
||||
public function getIsExpiredAttribute()
|
||||
{
|
||||
return $this->expires_at && $this->expires_at->isPast();
|
||||
}
|
||||
}
|
||||
48
laravel-app/app/Models/Budget.php
Normal file
48
laravel-app/app/Models/Budget.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Budget extends Model
|
||||
{
|
||||
protected $primaryKey = 'budget_id';
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected $fillable = [
|
||||
'budget_id',
|
||||
'max_budget',
|
||||
'budget_duration_sec',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'max_budget' => 'double',
|
||||
'budget_duration_sec' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
public function gatewayUsers()
|
||||
{
|
||||
return $this->hasMany(GatewayUser::class, 'budget_id', 'budget_id');
|
||||
}
|
||||
|
||||
public function getMaxBudgetFormattedAttribute()
|
||||
{
|
||||
return '$' . number_format($this->max_budget, 2);
|
||||
}
|
||||
|
||||
public function getDurationHumanAttribute()
|
||||
{
|
||||
if (!$this->budget_duration_sec) return 'No limit';
|
||||
|
||||
$days = floor($this->budget_duration_sec / 86400);
|
||||
$hours = floor(($this->budget_duration_sec % 86400) / 3600);
|
||||
|
||||
return "{$days}d {$hours}h";
|
||||
}
|
||||
}
|
||||
134
laravel-app/app/Models/GatewayUser.php
Normal file
134
laravel-app/app/Models/GatewayUser.php
Normal file
@@ -0,0 +1,134 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GatewayUser extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'users';
|
||||
|
||||
/**
|
||||
* The primary key for the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $primaryKey = 'user_id';
|
||||
|
||||
/**
|
||||
* Indicates if the IDs are auto-incrementing.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $incrementing = false;
|
||||
|
||||
/**
|
||||
* The data type of the primary key ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $keyType = 'string';
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'alias',
|
||||
'spend',
|
||||
'budget_id',
|
||||
'blocked',
|
||||
'metadata',
|
||||
'budget_started_at',
|
||||
'next_budget_reset_at',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'spend' => 'double',
|
||||
'blocked' => 'boolean',
|
||||
'metadata' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'budget_started_at' => 'datetime',
|
||||
'next_budget_reset_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the API keys for this user.
|
||||
*/
|
||||
public function apiKeys()
|
||||
{
|
||||
return $this->hasMany(ApiKey::class, 'user_id', 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the usage logs for this user.
|
||||
*/
|
||||
public function usageLogs()
|
||||
{
|
||||
return $this->hasMany(UsageLog::class, 'user_id', 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the budget for this user.
|
||||
*/
|
||||
public function budget()
|
||||
{
|
||||
return $this->belongsTo(Budget::class, 'budget_id', 'budget_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include active users.
|
||||
*/
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('blocked', false);
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include blocked users.
|
||||
*/
|
||||
public function scopeBlocked($query)
|
||||
{
|
||||
return $query->where('blocked', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the formatted spend amount.
|
||||
*/
|
||||
public function getSpendFormattedAttribute()
|
||||
{
|
||||
return '$' . number_format($this->spend, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of requests.
|
||||
*/
|
||||
public function getTotalRequestsAttribute()
|
||||
{
|
||||
return $this->usageLogs()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of tokens used.
|
||||
*/
|
||||
public function getTotalTokensAttribute()
|
||||
{
|
||||
return $this->usageLogs()->sum('total_tokens');
|
||||
}
|
||||
}
|
||||
51
laravel-app/app/Models/ModelPricing.php
Normal file
51
laravel-app/app/Models/ModelPricing.php
Normal file
@@ -0,0 +1,51 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ModelPricing extends Model
|
||||
{
|
||||
protected $table = 'model_pricing';
|
||||
protected $primaryKey = 'model_key';
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected $fillable = [
|
||||
'model_key',
|
||||
'input_price_per_million',
|
||||
'output_price_per_million',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'input_price_per_million' => 'double',
|
||||
'output_price_per_million' => 'double',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
|
||||
// Accessors
|
||||
public function getInputPriceFormattedAttribute()
|
||||
{
|
||||
return '$' . number_format($this->input_price_per_million, 2) . '/M';
|
||||
}
|
||||
|
||||
public function getOutputPriceFormattedAttribute()
|
||||
{
|
||||
return '$' . number_format($this->output_price_per_million, 2) . '/M';
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate cost for given token counts
|
||||
*/
|
||||
public function calculateCost($inputTokens, $outputTokens)
|
||||
{
|
||||
$inputCost = ($inputTokens / 1000000) * $this->input_price_per_million;
|
||||
$outputCost = ($outputTokens / 1000000) * $this->output_price_per_million;
|
||||
|
||||
return $inputCost + $outputCost;
|
||||
}
|
||||
}
|
||||
75
laravel-app/app/Models/UsageLog.php
Normal file
75
laravel-app/app/Models/UsageLog.php
Normal file
@@ -0,0 +1,75 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UsageLog extends Model
|
||||
{
|
||||
protected $primaryKey = 'id';
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'string';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'api_key_id',
|
||||
'user_id',
|
||||
'timestamp',
|
||||
'model',
|
||||
'provider',
|
||||
'endpoint',
|
||||
'prompt_tokens',
|
||||
'completion_tokens',
|
||||
'total_tokens',
|
||||
'cost',
|
||||
'status',
|
||||
'error_message',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'timestamp' => 'datetime',
|
||||
'prompt_tokens' => 'integer',
|
||||
'completion_tokens' => 'integer',
|
||||
'total_tokens' => 'integer',
|
||||
'cost' => 'double',
|
||||
];
|
||||
}
|
||||
|
||||
public function gatewayUser()
|
||||
{
|
||||
return $this->belongsTo(GatewayUser::class, 'user_id', 'user_id');
|
||||
}
|
||||
|
||||
public function apiKey()
|
||||
{
|
||||
return $this->belongsTo(ApiKey::class, 'api_key_id', 'id');
|
||||
}
|
||||
|
||||
public function scopeSuccess($query)
|
||||
{
|
||||
return $query->where('status', 'success');
|
||||
}
|
||||
|
||||
public function scopeFailed($query)
|
||||
{
|
||||
return $query->where('status', '!=', 'success');
|
||||
}
|
||||
|
||||
public function scopeToday($query)
|
||||
{
|
||||
return $query->whereDate('timestamp', today());
|
||||
}
|
||||
|
||||
public function scopeDateRange($query, $start, $end)
|
||||
{
|
||||
return $query->whereBetween('timestamp', [$start, $end]);
|
||||
}
|
||||
|
||||
public function getCostFormattedAttribute()
|
||||
{
|
||||
return $this->cost ? '$' . number_format($this->cost, 4) : 'N/A';
|
||||
}
|
||||
}
|
||||
48
laravel-app/app/Models/User.php
Normal file
48
laravel-app/app/Models/User.php
Normal file
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
// use Illuminate\Contracts\Auth\MustVerifyEmail;
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class User extends Authenticatable
|
||||
{
|
||||
/** @use HasFactory<\Database\Factories\UserFactory> */
|
||||
use HasFactory, Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var list<string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
}
|
||||
24
laravel-app/app/Providers/AppServiceProvider.php
Normal file
24
laravel-app/app/Providers/AppServiceProvider.php
Normal file
@@ -0,0 +1,24 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
|
||||
class AppServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register any application services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap any application services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
}
|
||||
28
laravel-app/app/Providers/VoltServiceProvider.php
Normal file
28
laravel-app/app/Providers/VoltServiceProvider.php
Normal file
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
namespace App\Providers;
|
||||
|
||||
use Illuminate\Support\ServiceProvider;
|
||||
use Livewire\Volt\Volt;
|
||||
|
||||
class VoltServiceProvider extends ServiceProvider
|
||||
{
|
||||
/**
|
||||
* Register services.
|
||||
*/
|
||||
public function register(): void
|
||||
{
|
||||
//
|
||||
}
|
||||
|
||||
/**
|
||||
* Bootstrap services.
|
||||
*/
|
||||
public function boot(): void
|
||||
{
|
||||
Volt::mount([
|
||||
config('livewire.view_path', resource_path('views/livewire')),
|
||||
resource_path('views/pages'),
|
||||
]);
|
||||
}
|
||||
}
|
||||
105
laravel-app/app/Services/StatisticsService.php
Normal file
105
laravel-app/app/Services/StatisticsService.php
Normal file
@@ -0,0 +1,105 @@
|
||||
<?php
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\UsageLog;
|
||||
use App\Models\GatewayUser;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class StatisticsService
|
||||
{
|
||||
/**
|
||||
* Get dashboard overview statistics
|
||||
*/
|
||||
public function getDashboardStats()
|
||||
{
|
||||
return [
|
||||
'total_users' => GatewayUser::count(),
|
||||
'active_users' => GatewayUser::active()->count(),
|
||||
'blocked_users' => GatewayUser::blocked()->count(),
|
||||
'total_requests_today' => UsageLog::today()->count(),
|
||||
'total_spend_today' => UsageLog::today()->sum('cost') ?? 0,
|
||||
'total_tokens_today' => UsageLog::today()->sum('total_tokens') ?? 0,
|
||||
'total_spend_month' => UsageLog::whereMonth('timestamp', now()->month)->sum('cost') ?? 0,
|
||||
'total_requests_month' => UsageLog::whereMonth('timestamp', now()->month)->count(),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get usage breakdown by provider
|
||||
*/
|
||||
public function getUsageByProvider($days = 30)
|
||||
{
|
||||
return UsageLog::selectRaw('provider, COUNT(*) as count, SUM(cost) as total_cost')
|
||||
->where('timestamp', '>=', now()->subDays($days))
|
||||
->groupBy('provider')
|
||||
->orderByDesc('count')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get usage breakdown by model
|
||||
*/
|
||||
public function getUsageByModel($days = 30)
|
||||
{
|
||||
return UsageLog::selectRaw('model, COUNT(*) as count, SUM(total_tokens) as tokens, SUM(cost) as total_cost')
|
||||
->where('timestamp', '>=', now()->subDays($days))
|
||||
->groupBy('model')
|
||||
->orderByDesc('count')
|
||||
->limit(10)
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get daily usage chart data
|
||||
*/
|
||||
public function getDailyUsageChart($days = 30)
|
||||
{
|
||||
return UsageLog::selectRaw('DATE(timestamp) as date, COUNT(*) as requests, SUM(cost) as cost, SUM(total_tokens) as tokens')
|
||||
->where('timestamp', '>=', now()->subDays($days))
|
||||
->groupBy('date')
|
||||
->orderBy('date')
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get top users by spend
|
||||
*/
|
||||
public function getTopUsers($limit = 10)
|
||||
{
|
||||
return GatewayUser::withCount('usageLogs')
|
||||
->withSum('usageLogs', 'cost')
|
||||
->orderByDesc('usage_logs_sum_cost')
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get recent activity
|
||||
*/
|
||||
public function getRecentActivity($limit = 20)
|
||||
{
|
||||
return UsageLog::with(['gatewayUser', 'apiKey'])
|
||||
->orderByDesc('timestamp')
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get user statistics
|
||||
*/
|
||||
public function getUserStatistics($userId, $days = 30)
|
||||
{
|
||||
return UsageLog::where('user_id', $userId)
|
||||
->where('timestamp', '>=', now()->subDays($days))
|
||||
->selectRaw('
|
||||
COUNT(*) as total_requests,
|
||||
SUM(prompt_tokens) as total_prompt_tokens,
|
||||
SUM(completion_tokens) as total_completion_tokens,
|
||||
SUM(total_tokens) as total_tokens,
|
||||
SUM(cost) as total_cost,
|
||||
AVG(total_tokens) as avg_tokens_per_request
|
||||
')
|
||||
->first();
|
||||
}
|
||||
}
|
||||
17
laravel-app/app/View/Components/AppLayout.php
Normal file
17
laravel-app/app/View/Components/AppLayout.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class AppLayout extends Component
|
||||
{
|
||||
/**
|
||||
* Get the view / contents that represents the component.
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
return view('layouts.app');
|
||||
}
|
||||
}
|
||||
17
laravel-app/app/View/Components/GuestLayout.php
Normal file
17
laravel-app/app/View/Components/GuestLayout.php
Normal file
@@ -0,0 +1,17 @@
|
||||
<?php
|
||||
|
||||
namespace App\View\Components;
|
||||
|
||||
use Illuminate\View\Component;
|
||||
use Illuminate\View\View;
|
||||
|
||||
class GuestLayout extends Component
|
||||
{
|
||||
/**
|
||||
* Get the view / contents that represents the component.
|
||||
*/
|
||||
public function render(): View
|
||||
{
|
||||
return view('layouts.guest');
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user