all(), [ 'period' => 'sometimes|string|in:today,week,month,all', 'provider' => 'sometimes|string|in:openai,anthropic,gemini,deepseek,mistral', ]); if ($validator->fails()) { return response()->json([ 'error' => [ 'code' => 'validation_error', 'message' => 'Invalid query parameters', 'status' => 422, 'details' => $validator->errors(), ], ], 422); } $user = $request->user(); $period = $request->input('period', 'month'); // Calculate date range $dateRange = $this->getDateRange($period); // Base query $query = LlmRequest::where('gateway_user_id', $user->user_id) ->where('created_at', '>=', $dateRange['start']); if ($dateRange['end']) { $query->where('created_at', '<=', $dateRange['end']); } // Apply provider filter if ($request->has('provider')) { $query->where('provider', $request->input('provider')); } // Get summary statistics $summary = $query->selectRaw(' COUNT(*) as total_requests, SUM(CASE WHEN status = "success" THEN 1 ELSE 0 END) as successful_requests, SUM(CASE WHEN status != "success" THEN 1 ELSE 0 END) as failed_requests, SUM(prompt_tokens) as prompt_tokens, SUM(completion_tokens) as completion_tokens, SUM(total_tokens) as total_tokens, 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(); $successRate = $summary->total_requests > 0 ? ($summary->successful_requests / $summary->total_requests) * 100 : 0; // Get breakdown by provider $byProvider = LlmRequest::where('gateway_user_id', $user->user_id) ->where('created_at', '>=', $dateRange['start']) ->where('status', 'success') ->select( 'provider', DB::raw('COUNT(*) as requests'), DB::raw('SUM(total_tokens) as tokens'), DB::raw('SUM(total_cost) as cost'), DB::raw('AVG(response_time_ms) as avg_response_time_ms') ) ->groupBy('provider') ->orderByDesc('requests') ->get() ->map(function ($item) use ($summary) { $successRate = LlmRequest::where('gateway_user_id', request()->user()->user_id) ->where('provider', $item->provider) ->where('created_at', '>=', $this->getDateRange(request()->input('period', 'month'))['start']) ->selectRaw(' COUNT(*) as total, SUM(CASE WHEN status = "success" THEN 1 ELSE 0 END) as successful ') ->first(); $rate = $successRate->total > 0 ? ($successRate->successful / $successRate->total) * 100 : 0; return [ 'provider' => $item->provider, 'provider_name' => $this->getProviderName($item->provider), 'requests' => $item->requests, 'tokens' => $item->tokens, 'cost' => round($item->cost, 4), 'avg_response_time_ms' => round($item->avg_response_time_ms), 'success_rate' => round($rate, 1), ]; }); // Get breakdown by model (top 10) $byModel = LlmRequest::where('gateway_user_id', $user->user_id) ->where('created_at', '>=', $dateRange['start']) ->where('status', 'success') ->select( 'model', 'provider', DB::raw('COUNT(*) as requests'), DB::raw('SUM(total_tokens) as tokens'), DB::raw('SUM(total_cost) as cost'), DB::raw('AVG(total_tokens) as avg_tokens_per_request') ) ->groupBy('model', 'provider') ->orderByDesc('requests') ->limit(10) ->get() ->map(function ($item) { return [ 'model' => $item->model, 'provider' => $item->provider, 'requests' => $item->requests, 'tokens' => $item->tokens, 'cost' => round($item->cost, 4), 'avg_tokens_per_request' => round($item->avg_tokens_per_request), ]; }); // Get top hours $topHours = LlmRequest::where('gateway_user_id', $user->user_id) ->where('created_at', '>=', $dateRange['start']) ->where('status', 'success') ->select( DB::raw('HOUR(created_at) as hour'), DB::raw('COUNT(*) as requests'), DB::raw('SUM(total_cost) as cost') ) ->groupBy('hour') ->orderByDesc('requests') ->limit(5) ->get() ->map(function ($item) { return [ 'hour' => $item->hour, 'requests' => $item->requests, 'cost' => round($item->cost, 4), ]; }); return response()->json([ 'data' => [ 'period' => $period, 'period_start' => $dateRange['start']->toIso8601String(), 'period_end' => $dateRange['end']?->toIso8601String() ?? now()->toIso8601String(), 'summary' => [ 'total_requests' => $summary->total_requests ?? 0, 'successful_requests' => $summary->successful_requests ?? 0, 'failed_requests' => $summary->failed_requests ?? 0, 'success_rate' => round($successRate, 1), 'total_tokens' => $summary->total_tokens ?? 0, 'prompt_tokens' => $summary->prompt_tokens ?? 0, 'completion_tokens' => $summary->completion_tokens ?? 0, 'total_cost' => round($summary->total_cost ?? 0, 4), 'avg_cost_per_request' => round($summary->avg_cost_per_request ?? 0, 6), 'avg_tokens_per_request' => round($summary->avg_tokens_per_request ?? 0), 'avg_response_time_ms' => round($summary->avg_response_time_ms ?? 0), ], 'by_provider' => $byProvider, 'by_model' => $byModel, 'top_hours' => $topHours, ], ]); } /** * Get list of individual requests * * Returns paginated list of requests with filtering and sorting options. * * ## Query Parameters * * - `page` (optional) - Page number (default: 1) * - `per_page` (optional) - Items per page (default: 20, max: 100) * - `provider` (optional) - Filter by provider * - `model` (optional) - Filter by model * - `status` (optional) - Filter by status: success, failed, all (default: all) * - `date_from` (optional) - From date (ISO 8601) * - `date_to` (optional) - To date (ISO 8601) * - `sort` (optional) - Sort field: created_at, cost, tokens, response_time (default: -created_at) * * @tags Usage * * @param Request $request * @return JsonResponse */ public function requests(Request $request): JsonResponse { $validator = Validator::make($request->all(), [ 'page' => 'sometimes|integer|min:1', 'per_page' => 'sometimes|integer|min:1|max:100', 'provider' => 'sometimes|string|in:openai,anthropic,gemini,deepseek,mistral', 'model' => 'sometimes|string', 'status' => 'sometimes|string|in:success,failed,all', 'date_from' => 'sometimes|date', 'date_to' => 'sometimes|date', 'sort' => 'sometimes|string|in:created_at,-created_at,cost,-cost,tokens,-tokens,response_time,-response_time', ]); if ($validator->fails()) { return response()->json([ 'error' => [ 'code' => 'validation_error', 'message' => 'Invalid query parameters', 'status' => 422, 'details' => $validator->errors(), ], ], 422); } $user = $request->user(); $perPage = $request->input('per_page', 20); // Build query $query = LlmRequest::where('gateway_user_id', $user->user_id); // Apply filters if ($request->has('provider')) { $query->where('provider', $request->input('provider')); } if ($request->has('model')) { $query->where('model', $request->input('model')); } $status = $request->input('status', 'all'); if ($status === 'success') { $query->where('status', 'success'); } elseif ($status === 'failed') { $query->where('status', '!=', 'success'); } if ($request->has('date_from')) { $query->where('created_at', '>=', $request->input('date_from')); } if ($request->has('date_to')) { $query->where('created_at', '<=', $request->input('date_to')); } // Apply sorting $sort = $request->input('sort', '-created_at'); $sortField = ltrim($sort, '-'); $sortDirection = str_starts_with($sort, '-') ? 'desc' : 'asc'; $query->orderBy($sortField, $sortDirection); // Get summary for filtered results $summary = $query->clone()->selectRaw(' SUM(total_cost) as total_cost, SUM(total_tokens) as total_tokens, AVG(response_time_ms) as avg_response_time_ms ')->first(); // Paginate $paginated = $query->paginate($perPage); $data = $paginated->map(function ($request) { return [ 'id' => $request->request_id, 'provider' => $request->provider, 'model' => $request->model, 'status' => $request->status, 'prompt_tokens' => $request->prompt_tokens, 'completion_tokens' => $request->completion_tokens, 'total_tokens' => $request->total_tokens, 'input_cost' => round($request->prompt_tokens * ($request->input_price_per_token ?? 0), 6), 'output_cost' => round($request->completion_tokens * ($request->output_price_per_token ?? 0), 6), 'total_cost' => round($request->total_cost, 6), 'response_time_ms' => $request->response_time_ms, 'created_at' => $request->created_at->toIso8601String(), ]; }); return response()->json([ 'data' => $data, 'meta' => [ 'current_page' => $paginated->currentPage(), 'per_page' => $paginated->perPage(), 'total' => $paginated->total(), 'total_pages' => $paginated->lastPage(), 'has_more' => $paginated->hasMorePages(), ], 'links' => [ 'first' => $paginated->url(1), 'last' => $paginated->url($paginated->lastPage()), 'prev' => $paginated->previousPageUrl(), 'next' => $paginated->nextPageUrl(), ], 'summary' => [ 'total_cost' => round($summary->total_cost ?? 0, 4), 'total_tokens' => $summary->total_tokens ?? 0, 'avg_response_time_ms' => round($summary->avg_response_time_ms ?? 0), ], ]); } /** * Get details of a specific request * * Returns complete information about a single request including * full request and response data. * * @tags Usage * * @param Request $request * @param string $id * @return JsonResponse */ public function show(Request $request, string $id): JsonResponse { $user = $request->user(); $llmRequest = LlmRequest::where('gateway_user_id', $user->user_id) ->where('request_id', $id) ->first(); if (!$llmRequest) { return response()->json([ 'error' => [ 'code' => 'not_found', 'message' => 'Request not found', 'status' => 404, ], ], 404); } return response()->json([ 'data' => [ 'id' => $llmRequest->request_id, 'gateway_user_id' => $llmRequest->gateway_user_id, 'provider' => $llmRequest->provider, 'provider_name' => $this->getProviderName($llmRequest->provider), 'model' => $llmRequest->model, 'status' => $llmRequest->status, 'request' => $llmRequest->request_data, 'response' => $llmRequest->response_data, 'usage' => [ 'prompt_tokens' => $llmRequest->prompt_tokens, 'completion_tokens' => $llmRequest->completion_tokens, 'total_tokens' => $llmRequest->total_tokens, ], 'cost' => [ 'input_cost' => round($llmRequest->prompt_tokens * ($llmRequest->input_price_per_token ?? 0), 6), 'output_cost' => round($llmRequest->completion_tokens * ($llmRequest->output_price_per_token ?? 0), 6), 'total_cost' => round($llmRequest->total_cost, 6), 'currency' => 'USD', ], 'performance' => [ 'response_time_ms' => $llmRequest->response_time_ms, ], 'metadata' => [ 'ip_address' => $llmRequest->ip_address, 'user_agent' => $llmRequest->user_agent, ], 'created_at' => $llmRequest->created_at->toIso8601String(), 'completed_at' => $llmRequest->created_at->addMilliseconds($llmRequest->response_time_ms)->toIso8601String(), ], ]); } /** * Get chart data for visualizations * * Returns data formatted for chart visualizations. * * ## Query Parameters * * - `type` (required) - Chart type: daily_cost, provider_distribution, model_usage, hourly_pattern * - `days` (optional) - Number of days to look back (default: 30) * * @tags Usage * * @param Request $request * @return JsonResponse */ public function charts(Request $request): JsonResponse { $validator = Validator::make($request->all(), [ 'type' => 'required|string|in:daily_cost,provider_distribution,model_usage,hourly_pattern', 'days' => 'sometimes|integer|min:1|max:365', ]); if ($validator->fails()) { return response()->json([ 'error' => [ 'code' => 'validation_error', 'message' => 'Invalid query parameters', 'status' => 422, 'details' => $validator->errors(), ], ], 422); } $user = $request->user(); $type = $request->input('type'); $days = $request->input('days', 30); $startDate = now()->subDays($days); $chartData = match ($type) { 'daily_cost' => $this->getDailyCostChart($user, $startDate), 'provider_distribution' => $this->getProviderDistributionChart($user, $startDate), 'model_usage' => $this->getModelUsageChart($user, $startDate), 'hourly_pattern' => $this->getHourlyPatternChart($user, $startDate), }; return response()->json([ 'data' => $chartData, ]); } /** * Calculate date range for period */ private function getDateRange(string $period): array { $now = now(); return match ($period) { 'today' => [ 'start' => $now->copy()->startOfDay(), 'end' => $now->copy()->endOfDay(), ], 'week' => [ 'start' => $now->copy()->startOfWeek(), 'end' => $now->copy()->endOfWeek(), ], 'month' => [ 'start' => $now->copy()->startOfMonth(), 'end' => $now->copy()->endOfMonth(), ], 'all' => [ 'start' => $now->copy()->subYears(10), // 10 years back 'end' => null, ], }; } /** * Get daily cost chart data */ private function getDailyCostChart($user, $startDate): array { $dailyData = LlmRequest::where('gateway_user_id', $user->user_id) ->where('created_at', '>=', $startDate) ->where('status', 'success') ->select( DB::raw('DATE(created_at) as date'), DB::raw('SUM(total_cost) as cost'), DB::raw('COUNT(*) as requests') ) ->groupBy('date') ->orderBy('date') ->get(); return [ 'type' => 'daily_cost', 'labels' => $dailyData->pluck('date')->toArray(), 'datasets' => [ [ 'label' => 'Daily Cost', 'data' => $dailyData->pluck('cost')->map(fn($v) => round($v, 4))->toArray(), 'backgroundColor' => 'rgba(59, 130, 246, 0.5)', 'borderColor' => 'rgba(59, 130, 246, 1)', ], ], ]; } /** * Get provider distribution chart data */ private function getProviderDistributionChart($user, $startDate): array { $providerData = LlmRequest::where('gateway_user_id', $user->user_id) ->where('created_at', '>=', $startDate) ->where('status', 'success') ->select('provider', DB::raw('SUM(total_cost) as cost')) ->groupBy('provider') ->orderByDesc('cost') ->get(); return [ 'type' => 'provider_distribution', 'labels' => $providerData->pluck('provider')->map(fn($p) => $this->getProviderName($p))->toArray(), 'datasets' => [ [ 'label' => 'Cost by Provider', 'data' => $providerData->pluck('cost')->map(fn($v) => round($v, 4))->toArray(), 'backgroundColor' => [ 'rgba(59, 130, 246, 0.8)', 'rgba(239, 68, 68, 0.8)', 'rgba(34, 197, 94, 0.8)', 'rgba(251, 146, 60, 0.8)', 'rgba(168, 85, 247, 0.8)', ], ], ], ]; } /** * Get model usage chart data */ private function getModelUsageChart($user, $startDate): array { $modelData = LlmRequest::where('gateway_user_id', $user->user_id) ->where('created_at', '>=', $startDate) ->where('status', 'success') ->select('model', DB::raw('COUNT(*) as requests')) ->groupBy('model') ->orderByDesc('requests') ->limit(10) ->get(); return [ 'type' => 'model_usage', 'labels' => $modelData->pluck('model')->toArray(), 'datasets' => [ [ 'label' => 'Requests by Model', 'data' => $modelData->pluck('requests')->toArray(), 'backgroundColor' => 'rgba(59, 130, 246, 0.5)', 'borderColor' => 'rgba(59, 130, 246, 1)', ], ], ]; } /** * Get hourly pattern chart data */ private function getHourlyPatternChart($user, $startDate): array { $hourlyData = LlmRequest::where('gateway_user_id', $user->user_id) ->where('created_at', '>=', $startDate) ->where('status', 'success') ->select( DB::raw('HOUR(created_at) as hour'), DB::raw('COUNT(*) as requests') ) ->groupBy('hour') ->orderBy('hour') ->get(); // Fill missing hours with 0 $allHours = collect(range(0, 23))->map(function ($hour) use ($hourlyData) { $data = $hourlyData->firstWhere('hour', $hour); return $data?->requests ?? 0; }); return [ 'type' => 'hourly_pattern', 'labels' => range(0, 23), 'datasets' => [ [ 'label' => 'Requests by Hour', 'data' => $allHours->toArray(), 'backgroundColor' => 'rgba(59, 130, 246, 0.5)', 'borderColor' => 'rgba(59, 130, 246, 1)', ], ], ]; } /** * Get human-readable provider name */ private function getProviderName(string $provider): string { return match ($provider) { 'openai' => 'OpenAI', 'anthropic' => 'Anthropic', 'gemini' => 'Google Gemini', 'deepseek' => 'DeepSeek', 'mistral' => 'Mistral AI', default => ucfirst($provider), }; } }