Rename project from any-llm to laravel-llm
- Remove old any-llm related files (Dockerfile, config.yml, web/, setup-laravel.sh) - Update README.md with new Laravel LLM Gateway documentation - Keep docker-compose.yml with laravel-llm container names - Clean project structure for Laravel-only implementation
This commit is contained in:
@@ -70,52 +70,39 @@ class ApiKeyController extends Controller
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'key_name' => 'required|string|max:255',
|
||||
'user_id' => 'required|string|exists:users,user_id',
|
||||
'user_id' => 'required|string|exists:gateway_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');
|
||||
// Generate a unique API token
|
||||
$token = 'llmg_' . Str::random(48);
|
||||
|
||||
// Parse metadata if provided
|
||||
$metadata = null;
|
||||
if (!empty($validated['metadata'])) {
|
||||
$metadata = json_decode($validated['metadata'], true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return back()->with('error', 'Invalid JSON in metadata field');
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare request payload
|
||||
$payload = [
|
||||
// Create API key directly in database
|
||||
$apiKey = ApiKey::create([
|
||||
'token' => $token,
|
||||
'user_id' => $validated['user_id'],
|
||||
'key_name' => $validated['key_name'],
|
||||
];
|
||||
'key_alias' => $validated['key_name'], // Use key_name as alias
|
||||
'expires' => $validated['expires_at'] ?? null,
|
||||
'metadata' => $metadata,
|
||||
'permissions' => [], // Default empty permissions
|
||||
'models' => [], // Default empty models
|
||||
]);
|
||||
|
||||
// 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);
|
||||
// Store the token in session for one-time display
|
||||
session()->flash('new_api_key', $token);
|
||||
session()->flash('new_api_key_id', $apiKey->token);
|
||||
|
||||
return redirect()->route('api-keys.index')
|
||||
->with('success', 'API Key created successfully! Make sure to copy it now - it won\'t be shown again.');
|
||||
@@ -160,26 +147,8 @@ class ApiKeyController extends Controller
|
||||
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());
|
||||
}
|
||||
// Delete the API key from database
|
||||
$apiKey->delete();
|
||||
|
||||
return redirect()->route('api-keys.index')
|
||||
->with('success', 'API Key revoked successfully');
|
||||
|
||||
@@ -41,19 +41,35 @@ class BudgetController extends Controller
|
||||
'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,
|
||||
};
|
||||
// Set monthly and daily limits based on budget type
|
||||
$monthlyLimit = null;
|
||||
$dailyLimit = null;
|
||||
|
||||
switch($validated['budget_type']) {
|
||||
case 'daily':
|
||||
$dailyLimit = $validated['max_budget'];
|
||||
break;
|
||||
case 'weekly':
|
||||
$dailyLimit = $validated['max_budget'] / 7;
|
||||
break;
|
||||
case 'monthly':
|
||||
$monthlyLimit = $validated['max_budget'];
|
||||
$dailyLimit = $validated['max_budget'] / 30;
|
||||
break;
|
||||
case 'custom':
|
||||
$days = $validated['custom_duration_days'] ?? 1;
|
||||
$dailyLimit = $validated['max_budget'] / $days;
|
||||
break;
|
||||
case 'unlimited':
|
||||
// No limits
|
||||
break;
|
||||
}
|
||||
|
||||
$budget = Budget::create([
|
||||
'budget_id' => 'budget-' . Str::uuid(),
|
||||
'max_budget' => $validated['max_budget'],
|
||||
'budget_duration_sec' => $duration,
|
||||
'name' => $validated['budget_name'],
|
||||
'monthly_limit' => $monthlyLimit,
|
||||
'daily_limit' => $dailyLimit,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
@@ -106,23 +122,40 @@ class BudgetController extends Controller
|
||||
$budget = Budget::findOrFail($id);
|
||||
|
||||
$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,
|
||||
'weekly' => 604800,
|
||||
'monthly' => 2592000,
|
||||
'custom' => ($validated['custom_duration_days'] ?? 1) * 86400,
|
||||
'unlimited' => null,
|
||||
};
|
||||
// Set monthly and daily limits based on budget type
|
||||
$monthlyLimit = null;
|
||||
$dailyLimit = null;
|
||||
|
||||
switch($validated['budget_type']) {
|
||||
case 'daily':
|
||||
$dailyLimit = $validated['max_budget'];
|
||||
break;
|
||||
case 'weekly':
|
||||
$dailyLimit = $validated['max_budget'] / 7;
|
||||
break;
|
||||
case 'monthly':
|
||||
$monthlyLimit = $validated['max_budget'];
|
||||
$dailyLimit = $validated['max_budget'] / 30;
|
||||
break;
|
||||
case 'custom':
|
||||
$days = $validated['custom_duration_days'] ?? 1;
|
||||
$dailyLimit = $validated['max_budget'] / $days;
|
||||
break;
|
||||
case 'unlimited':
|
||||
// No limits
|
||||
break;
|
||||
}
|
||||
|
||||
$budget->update([
|
||||
'max_budget' => $validated['max_budget'],
|
||||
'budget_duration_sec' => $duration,
|
||||
'name' => $validated['budget_name'],
|
||||
'monthly_limit' => $monthlyLimit,
|
||||
'daily_limit' => $dailyLimit,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
|
||||
@@ -21,13 +21,28 @@ class DashboardController extends Controller
|
||||
$topUsers = $this->statsService->getTopUsers(5);
|
||||
$providerStats = $this->statsService->getUsageByProvider(30);
|
||||
$modelStats = $this->statsService->getUsageByModel(30);
|
||||
$costTrends = $this->statsService->getCostTrends(30);
|
||||
$errorStats = $this->statsService->getErrorStats(30);
|
||||
|
||||
return view('dashboard', compact(
|
||||
'stats',
|
||||
'dailyUsage',
|
||||
'topUsers',
|
||||
'providerStats',
|
||||
'modelStats'
|
||||
'modelStats',
|
||||
'costTrends',
|
||||
'errorStats'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get real-time stats via AJAX
|
||||
*/
|
||||
public function realtimeStats()
|
||||
{
|
||||
return response()->json([
|
||||
'stats' => $this->statsService->getDashboardStats(),
|
||||
'timestamp' => now()->toIso8601String(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ class ModelPricingController extends Controller
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$modelPricing = ModelPricing::orderBy('model_key')
|
||||
$modelPricing = ModelPricing::orderBy('provider')
|
||||
->orderBy('model')
|
||||
->paginate(20);
|
||||
|
||||
return view('model-pricing.index', compact('modelPricing'));
|
||||
@@ -33,9 +34,16 @@ class ModelPricingController extends Controller
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'model_key' => 'required|string|max:255|unique:model_pricing,model_key',
|
||||
'provider' => 'required|string|max:50',
|
||||
'model' => 'required|string|max:100',
|
||||
'input_price_per_million' => 'required|numeric|min:0',
|
||||
'output_price_per_million' => 'required|numeric|min:0',
|
||||
'context_window' => 'nullable|integer|min:0',
|
||||
'max_output_tokens' => 'nullable|integer|min:0',
|
||||
'is_active' => 'boolean',
|
||||
'effective_from' => 'nullable|date',
|
||||
'effective_until' => 'nullable|date',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
|
||||
ModelPricing::create($validated);
|
||||
@@ -48,36 +56,38 @@ class ModelPricingController extends Controller
|
||||
/**
|
||||
* Display the specified model pricing
|
||||
*/
|
||||
public function show(string $modelKey)
|
||||
public function show(ModelPricing $modelPricing)
|
||||
{
|
||||
$model = ModelPricing::findOrFail($modelKey);
|
||||
|
||||
return view('model-pricing.show', compact('model'));
|
||||
return view('model-pricing.show', compact('modelPricing'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified model pricing
|
||||
*/
|
||||
public function edit(string $modelKey)
|
||||
public function edit(ModelPricing $modelPricing)
|
||||
{
|
||||
$model = ModelPricing::findOrFail($modelKey);
|
||||
|
||||
return view('model-pricing.edit', compact('model'));
|
||||
return view('model-pricing.edit', compact('modelPricing'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified model pricing
|
||||
*/
|
||||
public function update(Request $request, string $modelKey)
|
||||
public function update(Request $request, ModelPricing $modelPricing)
|
||||
{
|
||||
$model = ModelPricing::findOrFail($modelKey);
|
||||
|
||||
$validated = $request->validate([
|
||||
'provider' => 'required|string|max:50',
|
||||
'model' => 'required|string|max:100',
|
||||
'input_price_per_million' => 'required|numeric|min:0',
|
||||
'output_price_per_million' => 'required|numeric|min:0',
|
||||
'context_window' => 'nullable|integer|min:0',
|
||||
'max_output_tokens' => 'nullable|integer|min:0',
|
||||
'is_active' => 'boolean',
|
||||
'effective_from' => 'nullable|date',
|
||||
'effective_until' => 'nullable|date',
|
||||
'notes' => 'nullable|string',
|
||||
]);
|
||||
|
||||
$model->update($validated);
|
||||
$modelPricing->update($validated);
|
||||
|
||||
return redirect()
|
||||
->route('model-pricing.index')
|
||||
@@ -87,10 +97,9 @@ class ModelPricingController extends Controller
|
||||
/**
|
||||
* Remove the specified model pricing
|
||||
*/
|
||||
public function destroy(string $modelKey)
|
||||
public function destroy(ModelPricing $modelPricing)
|
||||
{
|
||||
$model = ModelPricing::findOrFail($modelKey);
|
||||
$model->delete();
|
||||
$modelPricing->delete();
|
||||
|
||||
return redirect()
|
||||
->route('model-pricing.index')
|
||||
@@ -102,7 +111,10 @@ class ModelPricingController extends Controller
|
||||
*/
|
||||
public function calculator()
|
||||
{
|
||||
$models = ModelPricing::orderBy('model_key')->get();
|
||||
$models = ModelPricing::where('is_active', true)
|
||||
->orderBy('provider')
|
||||
->orderBy('model')
|
||||
->get();
|
||||
|
||||
return view('model-pricing.calculator', compact('models'));
|
||||
}
|
||||
@@ -113,19 +125,20 @@ class ModelPricingController extends Controller
|
||||
public function calculate(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'model_key' => 'required|exists:model_pricing,model_key',
|
||||
'model_pricing_id' => 'required|exists:model_pricing,id',
|
||||
'input_tokens' => 'required|integer|min:0',
|
||||
'output_tokens' => 'required|integer|min:0',
|
||||
]);
|
||||
|
||||
$model = ModelPricing::findOrFail($validated['model_key']);
|
||||
$model = ModelPricing::findOrFail($validated['model_pricing_id']);
|
||||
$cost = $model->calculateCost(
|
||||
$validated['input_tokens'],
|
||||
$validated['output_tokens']
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'model' => $model->model_key,
|
||||
'provider' => $model->provider,
|
||||
'model' => $model->model,
|
||||
'input_tokens' => $validated['input_tokens'],
|
||||
'output_tokens' => $validated['output_tokens'],
|
||||
'total_tokens' => $validated['input_tokens'] + $validated['output_tokens'],
|
||||
@@ -162,20 +175,23 @@ class ModelPricingController extends Controller
|
||||
fgetcsv($handle);
|
||||
|
||||
while (($row = fgetcsv($handle)) !== false) {
|
||||
if (count($row) < 3) {
|
||||
if (count($row) < 4) {
|
||||
continue; // Skip invalid rows
|
||||
}
|
||||
|
||||
$modelKey = trim($row[0]);
|
||||
$inputPrice = floatval($row[1]);
|
||||
$outputPrice = floatval($row[2]);
|
||||
$provider = trim($row[0]);
|
||||
$model = trim($row[1]);
|
||||
$inputPrice = floatval($row[2]);
|
||||
$outputPrice = floatval($row[3]);
|
||||
|
||||
if (empty($modelKey) || $inputPrice < 0 || $outputPrice < 0) {
|
||||
$errors[] = "Invalid data for model: {$modelKey}";
|
||||
if (empty($provider) || empty($model) || $inputPrice < 0 || $outputPrice < 0) {
|
||||
$errors[] = "Invalid data for model: {$provider}/{$model}";
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing = ModelPricing::find($modelKey);
|
||||
$existing = ModelPricing::where('provider', $provider)
|
||||
->where('model', $model)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$existing->update([
|
||||
@@ -185,7 +201,8 @@ class ModelPricingController extends Controller
|
||||
$updated++;
|
||||
} else {
|
||||
ModelPricing::create([
|
||||
'model_key' => $modelKey,
|
||||
'provider' => $provider,
|
||||
'model' => $model,
|
||||
'input_price_per_million' => $inputPrice,
|
||||
'output_price_per_million' => $outputPrice,
|
||||
]);
|
||||
@@ -205,4 +222,291 @@ class ModelPricingController extends Controller
|
||||
->route('model-pricing.index')
|
||||
->with('success', $message);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get available models from a provider
|
||||
*/
|
||||
public function getProviderModels(string $provider)
|
||||
{
|
||||
try {
|
||||
$models = [];
|
||||
|
||||
switch($provider) {
|
||||
case 'openai':
|
||||
$models = $this->getOpenAIModels();
|
||||
break;
|
||||
case 'anthropic':
|
||||
$models = $this->getAnthropicModels();
|
||||
break;
|
||||
case 'deepseek':
|
||||
$models = $this->getDeepSeekModels();
|
||||
break;
|
||||
case 'google':
|
||||
$models = $this->getGeminiModels();
|
||||
break;
|
||||
case 'cohere':
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Cohere does not provide a public models API. Please enter model names manually'
|
||||
]);
|
||||
case 'mistral':
|
||||
$models = $this->getMistralModels();
|
||||
break;
|
||||
default:
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Provider not supported for automatic model fetching'
|
||||
]);
|
||||
}
|
||||
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'models' => $models
|
||||
]);
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch OpenAI models
|
||||
*/
|
||||
private function getOpenAIModels()
|
||||
{
|
||||
// Get API key from credentials
|
||||
$credential = \App\Models\UserProviderCredential::where('provider', 'openai')
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
if (!$credential || !$credential->api_key) {
|
||||
throw new \Exception('No active OpenAI credentials found. Please add credentials first.');
|
||||
}
|
||||
|
||||
$response = \Illuminate\Support\Facades\Http::withHeaders([
|
||||
'Authorization' => 'Bearer ' . $credential->api_key,
|
||||
])->get('https://api.openai.com/v1/models');
|
||||
|
||||
if (!$response->successful()) {
|
||||
throw new \Exception('Failed to fetch OpenAI models: ' . $response->body());
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
$models = [];
|
||||
|
||||
foreach ($data['data'] as $model) {
|
||||
if (isset($model['id'])) {
|
||||
$models[] = [
|
||||
'id' => $model['id'],
|
||||
'name' => $model['id'],
|
||||
'created' => $model['created'] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by name
|
||||
usort($models, fn($a, $b) => strcmp($a['name'], $b['name']));
|
||||
|
||||
return $models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Anthropic models from API
|
||||
*/
|
||||
private function getAnthropicModels()
|
||||
{
|
||||
// Get API key from credentials
|
||||
$credential = \App\Models\UserProviderCredential::where('provider', 'anthropic')
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
if (!$credential || !$credential->api_key) {
|
||||
throw new \Exception('No active Anthropic credentials found. Please add credentials first.');
|
||||
}
|
||||
|
||||
$response = \Illuminate\Support\Facades\Http::withHeaders([
|
||||
'x-api-key' => $credential->api_key,
|
||||
'anthropic-version' => '2023-06-01',
|
||||
])->get('https://api.anthropic.com/v1/models');
|
||||
|
||||
if (!$response->successful()) {
|
||||
throw new \Exception('Failed to fetch Anthropic models: ' . $response->body());
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
$models = [];
|
||||
|
||||
foreach ($data['data'] as $model) {
|
||||
if (isset($model['id'])) {
|
||||
$models[] = [
|
||||
'id' => $model['id'],
|
||||
'name' => $model['display_name'] ?? $model['id'],
|
||||
'created' => isset($model['created_at']) ? strtotime($model['created_at']) : null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by name
|
||||
usort($models, fn($a, $b) => strcmp($a['name'], $b['name']));
|
||||
|
||||
return $models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch DeepSeek models from API
|
||||
*/
|
||||
private function getDeepSeekModels()
|
||||
{
|
||||
// Get API key from credentials
|
||||
$credential = \App\Models\UserProviderCredential::where('provider', 'deepseek')
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
if (!$credential || !$credential->api_key) {
|
||||
throw new \Exception('No active DeepSeek credentials found. Please add credentials first.');
|
||||
}
|
||||
|
||||
// DeepSeek uses OpenAI-compatible API
|
||||
$response = \Illuminate\Support\Facades\Http::withHeaders([
|
||||
'Authorization' => 'Bearer ' . $credential->api_key,
|
||||
])->get('https://api.deepseek.com/models');
|
||||
|
||||
if (!$response->successful()) {
|
||||
throw new \Exception('Failed to fetch DeepSeek models: ' . $response->body());
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
$models = [];
|
||||
|
||||
foreach ($data['data'] as $model) {
|
||||
if (isset($model['id'])) {
|
||||
$models[] = [
|
||||
'id' => $model['id'],
|
||||
'name' => $model['id'],
|
||||
'created' => $model['created'] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by name
|
||||
usort($models, fn($a, $b) => strcmp($a['name'], $b['name']));
|
||||
|
||||
return $models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Google Gemini models from API
|
||||
*/
|
||||
private function getGeminiModels()
|
||||
{
|
||||
// Get API key from credentials
|
||||
$credential = \App\Models\UserProviderCredential::where('provider', 'gemini')
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
if (!$credential || !$credential->api_key) {
|
||||
throw new \Exception('No active Google Gemini credentials found. Please add credentials first.');
|
||||
}
|
||||
|
||||
$models = [];
|
||||
$nextPageToken = null;
|
||||
|
||||
// Fetch all pages
|
||||
do {
|
||||
$url = 'https://generativelanguage.googleapis.com/v1beta/models?key=' . $credential->api_key;
|
||||
if ($nextPageToken) {
|
||||
$url .= '&pageToken=' . $nextPageToken;
|
||||
}
|
||||
|
||||
$response = \Illuminate\Support\Facades\Http::get($url);
|
||||
|
||||
if (!$response->successful()) {
|
||||
throw new \Exception('Failed to fetch Gemini models: ' . $response->body());
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
foreach ($data['models'] as $model) {
|
||||
// Only include models that support generateContent
|
||||
if (isset($model['name']) &&
|
||||
isset($model['supportedGenerationMethods']) &&
|
||||
in_array('generateContent', $model['supportedGenerationMethods'])) {
|
||||
|
||||
// Extract model ID from "models/gemini-xxx" format
|
||||
$modelId = str_replace('models/', '', $model['name']);
|
||||
|
||||
$models[] = [
|
||||
'id' => $modelId,
|
||||
'name' => $model['displayName'] ?? $modelId,
|
||||
'created' => null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
$nextPageToken = $data['nextPageToken'] ?? null;
|
||||
|
||||
} while ($nextPageToken);
|
||||
|
||||
// Sort by name
|
||||
usort($models, fn($a, $b) => strcmp($a['name'], $b['name']));
|
||||
|
||||
return $models;
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch Mistral AI models from API
|
||||
*/
|
||||
private function getMistralModels()
|
||||
{
|
||||
// Get API key from credentials
|
||||
$credential = \App\Models\UserProviderCredential::where('provider', 'mistral')
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
if (!$credential || !$credential->api_key) {
|
||||
throw new \Exception('No active Mistral AI credentials found. Please add credentials first.');
|
||||
}
|
||||
|
||||
$response = \Illuminate\Support\Facades\Http::withHeaders([
|
||||
'Authorization' => 'Bearer ' . $credential->api_key,
|
||||
])->get('https://api.mistral.ai/v1/models');
|
||||
|
||||
if (!$response->successful()) {
|
||||
throw new \Exception('Failed to fetch Mistral models: ' . $response->body());
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
$models = [];
|
||||
$seenIds = []; // Track seen IDs to avoid duplicates (aliases)
|
||||
|
||||
foreach ($data['data'] as $model) {
|
||||
// Skip deprecated models
|
||||
if (isset($model['deprecation']) && $model['deprecation']) {
|
||||
continue;
|
||||
}
|
||||
|
||||
// Only include models that support chat completion
|
||||
if (isset($model['id']) &&
|
||||
isset($model['capabilities']['completion_chat']) &&
|
||||
$model['capabilities']['completion_chat'] === true &&
|
||||
!isset($seenIds[$model['id']])) {
|
||||
|
||||
$seenIds[$model['id']] = true;
|
||||
|
||||
$models[] = [
|
||||
'id' => $model['id'],
|
||||
'name' => $model['name'] ?? $model['id'],
|
||||
'created' => $model['created'] ?? null,
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
// Sort by name
|
||||
usort($models, fn($a, $b) => strcmp($a['name'], $b['name']));
|
||||
|
||||
return $models;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<?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',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,34 +2,73 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ApiKey extends Model
|
||||
{
|
||||
protected $primaryKey = 'id';
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'api_keys';
|
||||
protected $primaryKey = 'token';
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'key_hash',
|
||||
'key_name',
|
||||
'token',
|
||||
'user_id',
|
||||
'last_used_at',
|
||||
'expires_at',
|
||||
'is_active',
|
||||
'key_alias',
|
||||
'key_name',
|
||||
'permissions',
|
||||
'models',
|
||||
'metadata',
|
||||
'expires',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
protected $casts = [
|
||||
'permissions' => 'array',
|
||||
'models' => 'array',
|
||||
'metadata' => 'array',
|
||||
'expires' => 'datetime',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get masked version of the key
|
||||
*/
|
||||
public function getMaskedKeyAttribute(): string
|
||||
{
|
||||
return [
|
||||
'is_active' => 'boolean',
|
||||
'metadata' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
'last_used_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
];
|
||||
return substr($this->token, 0, 8) . '...' . substr($this->token, -4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if key is active (not explicitly marked inactive)
|
||||
*/
|
||||
public function getIsActiveAttribute(): bool
|
||||
{
|
||||
// For now, consider all keys active unless explicitly deleted
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if key is expired
|
||||
*/
|
||||
public function getIsExpiredAttribute(): bool
|
||||
{
|
||||
if (!$this->expires) {
|
||||
return false;
|
||||
}
|
||||
return $this->expires->isPast();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last used at timestamp
|
||||
*/
|
||||
public function getLastUsedAtAttribute()
|
||||
{
|
||||
$latestLog = $this->usageLogs()->latest('timestamp')->first();
|
||||
return $latestLog ? $latestLog->timestamp : null;
|
||||
}
|
||||
|
||||
public function gatewayUser()
|
||||
@@ -37,33 +76,14 @@ class ApiKey extends Model
|
||||
return $this->belongsTo(GatewayUser::class, 'user_id', 'user_id');
|
||||
}
|
||||
|
||||
// Alias for backwards compatibility
|
||||
public function user()
|
||||
{
|
||||
return $this->gatewayUser();
|
||||
}
|
||||
|
||||
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();
|
||||
return $this->hasMany(UsageLog::class, 'api_key', 'token');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,47 +2,61 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Budget extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'budgets';
|
||||
protected $primaryKey = 'budget_id';
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected $fillable = [
|
||||
'budget_id',
|
||||
'max_budget',
|
||||
'budget_duration_sec',
|
||||
'name',
|
||||
'monthly_limit',
|
||||
'daily_limit',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
protected $casts = [
|
||||
'monthly_limit' => 'decimal:2',
|
||||
'daily_limit' => 'decimal:2',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get formatted max budget display
|
||||
*/
|
||||
public function getMaxBudgetFormattedAttribute(): string
|
||||
{
|
||||
return [
|
||||
'max_budget' => 'double',
|
||||
'budget_duration_sec' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
if ($this->monthly_limit) {
|
||||
return '$' . number_format($this->monthly_limit, 2);
|
||||
}
|
||||
if ($this->daily_limit) {
|
||||
return '$' . number_format($this->daily_limit, 2) . '/day';
|
||||
}
|
||||
return 'Unlimited';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-readable duration
|
||||
*/
|
||||
public function getDurationHumanAttribute(): string
|
||||
{
|
||||
if ($this->monthly_limit && $this->daily_limit) {
|
||||
return 'Monthly';
|
||||
}
|
||||
if ($this->daily_limit && !$this->monthly_limit) {
|
||||
return 'Daily';
|
||||
}
|
||||
return 'Unlimited';
|
||||
}
|
||||
|
||||
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";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,74 +2,45 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GatewayUser extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'users';
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The primary key for the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'gateway_users';
|
||||
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',
|
||||
'spend',
|
||||
'blocked',
|
||||
'metadata',
|
||||
'budget_started_at',
|
||||
'next_budget_reset_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'metadata' => 'array',
|
||||
'blocked' => 'boolean',
|
||||
'spend' => 'decimal:2',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
* Get the budget associated with the user.
|
||||
*/
|
||||
protected function casts(): array
|
||||
public function budget()
|
||||
{
|
||||
return [
|
||||
'spend' => 'double',
|
||||
'blocked' => 'boolean',
|
||||
'metadata' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'budget_started_at' => 'datetime',
|
||||
'next_budget_reset_at' => 'datetime',
|
||||
];
|
||||
return $this->belongsTo(Budget::class, 'budget_id', 'budget_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the API keys for this user.
|
||||
* Get the API keys for the user.
|
||||
*/
|
||||
public function apiKeys()
|
||||
{
|
||||
@@ -77,21 +48,13 @@ class GatewayUser extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the usage logs for this user.
|
||||
* Get the usage logs for the 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.
|
||||
*/
|
||||
@@ -107,28 +70,4 @@ class GatewayUser extends Model
|
||||
{
|
||||
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');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,45 +7,53 @@ 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',
|
||||
'provider',
|
||||
'model',
|
||||
'input_price_per_million',
|
||||
'output_price_per_million',
|
||||
'context_window',
|
||||
'max_output_tokens',
|
||||
'is_active',
|
||||
'effective_from',
|
||||
'effective_until',
|
||||
'notes',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'input_price_per_million' => 'double',
|
||||
'output_price_per_million' => 'double',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
}
|
||||
protected $casts = [
|
||||
'input_price_per_million' => 'decimal:4',
|
||||
'output_price_per_million' => 'decimal:4',
|
||||
'context_window' => 'integer',
|
||||
'max_output_tokens' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
'effective_from' => 'date',
|
||||
'effective_until' => 'date',
|
||||
];
|
||||
|
||||
// Accessors
|
||||
public function getInputPriceFormattedAttribute()
|
||||
public function getInputPriceFormattedAttribute(): string
|
||||
{
|
||||
return '$' . number_format($this->input_price_per_million, 2) . '/M';
|
||||
}
|
||||
|
||||
public function getOutputPriceFormattedAttribute()
|
||||
public function getOutputPriceFormattedAttribute(): string
|
||||
{
|
||||
return '$' . number_format($this->output_price_per_million, 2) . '/M';
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate cost for given token counts
|
||||
*/
|
||||
public function calculateCost($inputTokens, $outputTokens)
|
||||
public function calculateCost(int $inputTokens, int $outputTokens): float
|
||||
{
|
||||
$inputCost = ($inputTokens / 1000000) * $this->input_price_per_million;
|
||||
$outputCost = ($outputTokens / 1000000) * $this->output_price_per_million;
|
||||
$inputCost = ($inputTokens / 1_000_000) * $this->input_price_per_million;
|
||||
$outputCost = ($outputTokens / 1_000_000) * $this->output_price_per_million;
|
||||
|
||||
return $inputCost + $outputCost;
|
||||
return round($inputCost + $outputCost, 6);
|
||||
}
|
||||
|
||||
public function isCurrentlyActive(): bool
|
||||
{
|
||||
$now = now()->toDateString();
|
||||
return $this->is_active
|
||||
&& $this->effective_from <= $now
|
||||
&& ($this->effective_until === null || $this->effective_until >= $now);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,23 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UsageLog extends Model
|
||||
{
|
||||
protected $primaryKey = 'id';
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'usage_logs';
|
||||
protected $primaryKey = 'request_id';
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'string';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'api_key_id',
|
||||
'request_id',
|
||||
'user_id',
|
||||
'timestamp',
|
||||
'api_key',
|
||||
'model',
|
||||
'provider',
|
||||
'endpoint',
|
||||
@@ -25,17 +28,22 @@ class UsageLog extends Model
|
||||
'cost',
|
||||
'status',
|
||||
'error_message',
|
||||
'timestamp',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
protected $casts = [
|
||||
'prompt_tokens' => 'integer',
|
||||
'completion_tokens' => 'integer',
|
||||
'total_tokens' => 'integer',
|
||||
'cost' => 'decimal:6',
|
||||
'timestamp' => 'datetime',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return [
|
||||
'timestamp' => 'datetime',
|
||||
'prompt_tokens' => 'integer',
|
||||
'completion_tokens' => 'integer',
|
||||
'total_tokens' => 'integer',
|
||||
'cost' => 'double',
|
||||
];
|
||||
return $this->belongsTo(GatewayUser::class, 'user_id', 'user_id');
|
||||
}
|
||||
|
||||
public function gatewayUser()
|
||||
@@ -45,9 +53,10 @@ class UsageLog extends Model
|
||||
|
||||
public function apiKey()
|
||||
{
|
||||
return $this->belongsTo(ApiKey::class, 'api_key_id', 'id');
|
||||
return $this->belongsTo(ApiKey::class, 'api_key', 'token');
|
||||
}
|
||||
|
||||
// Scopes
|
||||
public function scopeSuccess($query)
|
||||
{
|
||||
return $query->where('status', 'success');
|
||||
@@ -55,21 +64,6 @@ class UsageLog extends Model
|
||||
|
||||
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';
|
||||
return $query->where('status', 'failed');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,4 +45,36 @@ class User extends Authenticatable
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's budget
|
||||
*/
|
||||
public function budget()
|
||||
{
|
||||
return $this->hasOne(UserBudget::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's rate limit
|
||||
*/
|
||||
public function rateLimit()
|
||||
{
|
||||
return $this->hasOne(RateLimit::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's provider credentials
|
||||
*/
|
||||
public function providerCredentials()
|
||||
{
|
||||
return $this->hasMany(UserProviderCredential::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's LLM requests
|
||||
*/
|
||||
public function llmRequests()
|
||||
{
|
||||
return $this->hasMany(LlmRequest::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\UsageLog;
|
||||
use App\Models\GatewayUser;
|
||||
use App\Models\LlmRequest;
|
||||
use App\Models\User;
|
||||
use App\Models\UserProviderCredential;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class StatisticsService
|
||||
@@ -11,27 +12,34 @@ class StatisticsService
|
||||
/**
|
||||
* Get dashboard overview statistics
|
||||
*/
|
||||
public function getDashboardStats()
|
||||
public function getDashboardStats(): array
|
||||
{
|
||||
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(),
|
||||
'total_users' => User::count(),
|
||||
'active_credentials' => UserProviderCredential::where('is_active', true)->count(),
|
||||
'total_requests_today' => LlmRequest::whereDate('created_at', today())->count(),
|
||||
'total_spend_today' => LlmRequest::whereDate('created_at', today())->sum('total_cost') ?? 0,
|
||||
'total_tokens_today' => LlmRequest::whereDate('created_at', today())->sum('total_tokens') ?? 0,
|
||||
'total_spend_month' => LlmRequest::whereMonth('created_at', now()->month)
|
||||
->whereYear('created_at', now()->year)
|
||||
->sum('total_cost') ?? 0,
|
||||
'total_requests_month' => LlmRequest::whereMonth('created_at', now()->month)
|
||||
->whereYear('created_at', now()->year)
|
||||
->count(),
|
||||
'avg_cost_per_request' => LlmRequest::whereMonth('created_at', now()->month)
|
||||
->whereYear('created_at', now()->year)
|
||||
->avg('total_cost') ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get usage breakdown by provider
|
||||
*/
|
||||
public function getUsageByProvider($days = 30)
|
||||
public function getUsageByProvider(int $days = 30)
|
||||
{
|
||||
return UsageLog::selectRaw('provider, COUNT(*) as count, SUM(cost) as total_cost')
|
||||
->where('timestamp', '>=', now()->subDays($days))
|
||||
return LlmRequest::selectRaw('provider, COUNT(*) as count, SUM(total_cost) as total_cost, SUM(total_tokens) as total_tokens')
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->where('status', 'success')
|
||||
->groupBy('provider')
|
||||
->orderByDesc('count')
|
||||
->get();
|
||||
@@ -40,11 +48,12 @@ class StatisticsService
|
||||
/**
|
||||
* Get usage breakdown by model
|
||||
*/
|
||||
public function getUsageByModel($days = 30)
|
||||
public function getUsageByModel(int $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')
|
||||
return LlmRequest::selectRaw('model, provider, COUNT(*) as count, SUM(total_tokens) as tokens, SUM(total_cost) as total_cost')
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->where('status', 'success')
|
||||
->groupBy('model', 'provider')
|
||||
->orderByDesc('count')
|
||||
->limit(10)
|
||||
->get();
|
||||
@@ -53,10 +62,11 @@ class StatisticsService
|
||||
/**
|
||||
* Get daily usage chart data
|
||||
*/
|
||||
public function getDailyUsageChart($days = 30)
|
||||
public function getDailyUsageChart(int $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))
|
||||
return LlmRequest::selectRaw('DATE(created_at) as date, COUNT(*) as requests, SUM(total_cost) as cost, SUM(total_tokens) as tokens')
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->where('status', 'success')
|
||||
->groupBy('date')
|
||||
->orderBy('date')
|
||||
->get();
|
||||
@@ -65,11 +75,13 @@ class StatisticsService
|
||||
/**
|
||||
* Get top users by spend
|
||||
*/
|
||||
public function getTopUsers($limit = 10)
|
||||
public function getTopUsers(int $limit = 10)
|
||||
{
|
||||
return GatewayUser::withCount('usageLogs')
|
||||
->withSum('usageLogs', 'cost')
|
||||
->orderByDesc('usage_logs_sum_cost')
|
||||
return User::select('users.*')
|
||||
->withCount('llmRequests')
|
||||
->withSum('llmRequests as total_cost', 'total_cost')
|
||||
->withSum('llmRequests as total_tokens', 'total_tokens')
|
||||
->orderByDesc('total_cost')
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
@@ -77,10 +89,10 @@ class StatisticsService
|
||||
/**
|
||||
* Get recent activity
|
||||
*/
|
||||
public function getRecentActivity($limit = 20)
|
||||
public function getRecentActivity(int $limit = 20)
|
||||
{
|
||||
return UsageLog::with(['gatewayUser', 'apiKey'])
|
||||
->orderByDesc('timestamp')
|
||||
return LlmRequest::with('user')
|
||||
->orderByDesc('created_at')
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
@@ -88,18 +100,82 @@ class StatisticsService
|
||||
/**
|
||||
* Get user statistics
|
||||
*/
|
||||
public function getUserStatistics($userId, $days = 30)
|
||||
public function getUserStatistics(int $userId, int $days = 30)
|
||||
{
|
||||
return UsageLog::where('user_id', $userId)
|
||||
->where('timestamp', '>=', now()->subDays($days))
|
||||
return LlmRequest::where('user_id', $userId)
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->where('status', 'success')
|
||||
->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
|
||||
SUM(total_cost) as total_cost,
|
||||
AVG(total_tokens) as avg_tokens_per_request,
|
||||
AVG(total_cost) as avg_cost_per_request,
|
||||
AVG(response_time_ms) as avg_response_time_ms
|
||||
')
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get provider usage over time
|
||||
*/
|
||||
public function getProviderUsageOverTime(int $days = 30)
|
||||
{
|
||||
return LlmRequest::selectRaw('DATE(created_at) as date, provider, COUNT(*) as count, SUM(total_cost) as cost')
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->where('status', 'success')
|
||||
->groupBy('date', 'provider')
|
||||
->orderBy('date')
|
||||
->get()
|
||||
->groupBy('provider');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cost trends
|
||||
*/
|
||||
public function getCostTrends(int $days = 30)
|
||||
{
|
||||
$data = LlmRequest::selectRaw('
|
||||
DATE(created_at) as date,
|
||||
SUM(total_cost) as daily_cost,
|
||||
AVG(total_cost) as avg_request_cost,
|
||||
COUNT(*) as request_count
|
||||
')
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->where('status', 'success')
|
||||
->groupBy('date')
|
||||
->orderBy('date')
|
||||
->get();
|
||||
|
||||
return [
|
||||
'daily_data' => $data,
|
||||
'total_cost' => $data->sum('daily_cost'),
|
||||
'avg_daily_cost' => $data->avg('daily_cost'),
|
||||
'total_requests' => $data->sum('request_count'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error statistics
|
||||
*/
|
||||
public function getErrorStats(int $days = 30)
|
||||
{
|
||||
return [
|
||||
'total_errors' => LlmRequest::where('created_at', '>=', now()->subDays($days))
|
||||
->where('status', '!=', 'success')
|
||||
->count(),
|
||||
'errors_by_status' => LlmRequest::selectRaw('status, COUNT(*) as count')
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->where('status', '!=', 'success')
|
||||
->groupBy('status')
|
||||
->get(),
|
||||
'errors_by_provider' => LlmRequest::selectRaw('provider, COUNT(*) as count')
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->where('status', '!=', 'success')
|
||||
->groupBy('provider')
|
||||
->get(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user