Fix API controllers to use correct database column names
- Fix model_pricing table references (model_id -> model, display_name -> model)
- Fix price columns (output_price_per_1k -> output_price_per_million)
- Add price conversion (per_million / 1000 = per_1k) in all API responses
- Add whereNotNull('model') filters to exclude invalid entries
- Add getModelDisplayName() helper method to all controllers
- Fix AccountController to use gateway_users budget fields directly
- Remove Budget model dependencies from AccountController
- Add custom Scramble server URL configuration for API docs
- Create ScrambleServiceProvider to set correct /api prefix
- Add migration to rename user_id to gateway_user_id in llm_requests
- Add custom ApiGuard for gateway_users authentication
- Update all API controllers: AccountController, ModelController, PricingController, ProviderController
All API endpoints now working correctly:
- GET /api/account
- GET /api/models
- GET /api/pricing
- GET /api/providers/{provider}
This commit is contained in:
@@ -1,95 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\User;
|
||||
use App\Models\UserBudget;
|
||||
use App\Services\Budget\BudgetChecker;
|
||||
use App\Services\RateLimit\RateLimitChecker;
|
||||
use Illuminate\Http\Request;
|
||||
|
||||
class UserBudgetController extends Controller
|
||||
{
|
||||
public function __construct(
|
||||
private BudgetChecker $budgetChecker,
|
||||
private RateLimitChecker $rateLimitChecker
|
||||
) {}
|
||||
|
||||
/**
|
||||
* Display budget and rate limit status for a user
|
||||
*/
|
||||
public function show(User $user)
|
||||
{
|
||||
$budgetStatus = $this->budgetChecker->getBudgetStatus($user);
|
||||
$rateLimitStatus = $this->rateLimitChecker->getRateLimitStatus($user);
|
||||
|
||||
return view('admin.user-budget.show', compact('user', 'budgetStatus', 'rateLimitStatus'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update budget limits for a user
|
||||
*/
|
||||
public function updateBudget(Request $request, User $user)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'monthly_limit' => 'required|numeric|min:0',
|
||||
'daily_limit' => 'nullable|numeric|min:0',
|
||||
'alert_threshold_percentage' => 'required|integer|min:0|max:100',
|
||||
]);
|
||||
|
||||
$budget = $user->budget ?? new UserBudget(['user_id' => $user->id]);
|
||||
$budget->fill($validated);
|
||||
$budget->save();
|
||||
|
||||
return back()->with('success', 'Budget limits updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Update rate limits for a user
|
||||
*/
|
||||
public function updateRateLimit(Request $request, User $user)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'requests_per_minute' => 'required|integer|min:0',
|
||||
'requests_per_hour' => 'required|integer|min:0',
|
||||
'requests_per_day' => 'required|integer|min:0',
|
||||
]);
|
||||
|
||||
$rateLimit = $user->rateLimit ?? new \App\Models\RateLimit(['user_id' => $user->id]);
|
||||
$rateLimit->fill($validated);
|
||||
$rateLimit->save();
|
||||
|
||||
return back()->with('success', 'Rate limits updated successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset rate limit for a user
|
||||
*/
|
||||
public function resetRateLimit(User $user)
|
||||
{
|
||||
$this->rateLimitChecker->resetRateLimit($user);
|
||||
|
||||
return back()->with('success', 'Rate limit reset successfully!');
|
||||
}
|
||||
|
||||
/**
|
||||
* Reset budget for a user (admin action)
|
||||
*/
|
||||
public function resetBudget(User $user)
|
||||
{
|
||||
$budget = $user->budget;
|
||||
|
||||
if ($budget) {
|
||||
$budget->current_month_spending = 0.0;
|
||||
$budget->current_day_spending = 0.0;
|
||||
$budget->is_budget_exceeded = false;
|
||||
$budget->last_alert_sent_at = null;
|
||||
$budget->month_started_at = now()->startOfMonth();
|
||||
$budget->day_started_at = now()->startOfDay();
|
||||
$budget->save();
|
||||
}
|
||||
|
||||
return back()->with('success', 'Budget reset successfully!');
|
||||
}
|
||||
}
|
||||
@@ -3,7 +3,7 @@
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\{GatewayUser, ApiKey, GatewayUserCredential, Budget, LlmRequest};
|
||||
use App\Models\{GatewayUser, ApiKey, GatewayUserCredential, LlmRequest};
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
@@ -51,12 +51,11 @@ class AccountController extends Controller
|
||||
->get()
|
||||
->map(function ($key) {
|
||||
return [
|
||||
'id' => $key->id,
|
||||
'name' => $key->name ?? 'Default Key',
|
||||
'key_preview' => substr($key->api_key, 0, 8) . '...' . substr($key->api_key, -4),
|
||||
'token_preview' => substr($key->token, 0, 8) . '...' . substr($key->token, -4),
|
||||
'name' => $key->key_name ?? $key->key_alias ?? 'Default Key',
|
||||
'alias' => $key->key_alias,
|
||||
'created_at' => $key->created_at->toIso8601String(),
|
||||
'last_used' => $key->last_used_at?->toIso8601String(),
|
||||
'expires_at' => $key->expires_at?->toIso8601String(),
|
||||
'expires_at' => $key->expires?->toIso8601String(),
|
||||
];
|
||||
});
|
||||
|
||||
@@ -65,20 +64,19 @@ class AccountController extends Controller
|
||||
->where('is_active', true)
|
||||
->count();
|
||||
|
||||
// Get budget info
|
||||
$budget = Budget::where('gateway_user_id', $user->user_id)->first();
|
||||
$monthlySpending = LlmRequest::where('gateway_user_id', $user->user_id)
|
||||
->whereYear('created_at', now()->year)
|
||||
->whereMonth('created_at', now()->month)
|
||||
->where('status', 'success')
|
||||
->sum('total_cost') ?? 0;
|
||||
|
||||
$budgetInfo = $budget ? [
|
||||
'total' => round($budget->monthly_limit, 2),
|
||||
'used' => round($monthlySpending, 4),
|
||||
'remaining' => round($budget->monthly_limit - $monthlySpending, 4),
|
||||
'currency' => 'USD',
|
||||
] : null;
|
||||
// Get budget info directly from gateway_user
|
||||
// The gateway_users table has budget fields: monthly_budget_limit, current_month_spending
|
||||
$budgetInfo = null;
|
||||
if ($user->monthly_budget_limit !== null) {
|
||||
$budgetInfo = [
|
||||
'total' => round($user->monthly_budget_limit, 2),
|
||||
'used' => round($user->current_month_spending, 4),
|
||||
'remaining' => round($user->monthly_budget_limit - $user->current_month_spending, 4),
|
||||
'currency' => 'USD',
|
||||
'alert_threshold' => $user->budget_alert_threshold,
|
||||
];
|
||||
}
|
||||
|
||||
|
||||
// Get statistics
|
||||
$stats = LlmRequest::where('gateway_user_id', $user->user_id)
|
||||
@@ -111,7 +109,7 @@ class AccountController extends Controller
|
||||
'rate_limits' => [
|
||||
'requests_per_minute' => 100, // TODO: Get from rate_limits table
|
||||
'tokens_per_request' => 10000,
|
||||
'daily_budget_limit' => $budget ? round($budget->monthly_limit / 30, 2) : null,
|
||||
'daily_budget_limit' => $user->monthly_budget_limit ? round($user->monthly_budget_limit / 30, 2) : null,
|
||||
],
|
||||
],
|
||||
]);
|
||||
|
||||
@@ -1,298 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Api;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\{Budget, LlmRequest};
|
||||
use Illuminate\Http\JsonResponse;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Validator;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class BudgetController extends Controller
|
||||
{
|
||||
/**
|
||||
* Get current budget status
|
||||
*
|
||||
* Returns comprehensive budget information including total budget, used budget,
|
||||
* remaining budget, projections, and breakdowns by provider.
|
||||
*
|
||||
* ## Example Response
|
||||
*
|
||||
* ```json
|
||||
* {
|
||||
* "data": {
|
||||
* "total_budget": 100.00,
|
||||
* "used_budget": 45.67,
|
||||
* "remaining_budget": 54.33,
|
||||
* "budget_percentage": 45.67,
|
||||
* "currency": "USD",
|
||||
* "period": "monthly",
|
||||
* "period_start": "2025-11-01T00:00:00Z",
|
||||
* "period_end": "2025-11-30T23:59:59Z",
|
||||
* "days_remaining": 11,
|
||||
* "projected_spend": 95.34,
|
||||
* "projected_overspend": false,
|
||||
* "limits": {
|
||||
* "daily_limit": 10.00,
|
||||
* "daily_used": 2.45,
|
||||
* "daily_remaining": 7.55
|
||||
* },
|
||||
* "alerts": {
|
||||
* "threshold_50_percent": false,
|
||||
* "threshold_75_percent": false,
|
||||
* "threshold_90_percent": false
|
||||
* },
|
||||
* "breakdown_by_provider": [
|
||||
* {
|
||||
* "provider": "openai",
|
||||
* "provider_name": "OpenAI",
|
||||
* "spent": 25.30,
|
||||
* "percentage": 55.4,
|
||||
* "requests": 850
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @tags Budget
|
||||
*
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function index(Request $request): JsonResponse
|
||||
{
|
||||
$user = $request->user();
|
||||
|
||||
// Get budget configuration
|
||||
$budget = Budget::where('gateway_user_id', $user->user_id)->first();
|
||||
|
||||
if (!$budget) {
|
||||
return response()->json([
|
||||
'error' => [
|
||||
'code' => 'not_found',
|
||||
'message' => 'No budget configured for this user',
|
||||
'status' => 404,
|
||||
],
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Calculate period dates
|
||||
$now = now();
|
||||
$periodStart = $now->copy()->startOfMonth();
|
||||
$periodEnd = $now->copy()->endOfMonth();
|
||||
$daysRemaining = $now->diffInDays($periodEnd, false);
|
||||
$daysInMonth = $periodStart->daysInMonth;
|
||||
$daysElapsed = $now->diffInDays($periodStart);
|
||||
|
||||
// Get current month's spending
|
||||
$monthlySpending = LlmRequest::where('gateway_user_id', $user->user_id)
|
||||
->whereYear('created_at', $now->year)
|
||||
->whereMonth('created_at', $now->month)
|
||||
->where('status', 'success')
|
||||
->sum('total_cost') ?? 0;
|
||||
|
||||
// Get today's spending
|
||||
$dailySpending = LlmRequest::where('gateway_user_id', $user->user_id)
|
||||
->whereDate('created_at', $now->toDateString())
|
||||
->where('status', 'success')
|
||||
->sum('total_cost') ?? 0;
|
||||
|
||||
// Calculate projections
|
||||
$dailyAverage = $daysElapsed > 0 ? ($monthlySpending / $daysElapsed) : 0;
|
||||
$projectedSpend = $dailyAverage * $daysInMonth;
|
||||
|
||||
// Calculate remaining budget
|
||||
$remainingBudget = $budget->monthly_limit - $monthlySpending;
|
||||
$budgetPercentage = $budget->monthly_limit > 0
|
||||
? ($monthlySpending / $budget->monthly_limit) * 100
|
||||
: 0;
|
||||
|
||||
// Get breakdown by provider
|
||||
$providerBreakdown = LlmRequest::where('gateway_user_id', $user->user_id)
|
||||
->whereYear('created_at', $now->year)
|
||||
->whereMonth('created_at', $now->month)
|
||||
->where('status', 'success')
|
||||
->select('provider', DB::raw('SUM(total_cost) as spent'), DB::raw('COUNT(*) as requests'))
|
||||
->groupBy('provider')
|
||||
->orderByDesc('spent')
|
||||
->get()
|
||||
->map(function ($item) use ($monthlySpending) {
|
||||
$percentage = $monthlySpending > 0 ? ($item->spent / $monthlySpending) * 100 : 0;
|
||||
return [
|
||||
'provider' => $item->provider,
|
||||
'provider_name' => $this->getProviderName($item->provider),
|
||||
'spent' => round($item->spent, 4),
|
||||
'percentage' => round($percentage, 1),
|
||||
'requests' => $item->requests,
|
||||
];
|
||||
});
|
||||
|
||||
// Calculate daily limit (monthly limit / days in month)
|
||||
$dailyLimit = $budget->monthly_limit / $daysInMonth;
|
||||
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'total_budget' => round($budget->monthly_limit, 2),
|
||||
'used_budget' => round($monthlySpending, 4),
|
||||
'remaining_budget' => round($remainingBudget, 4),
|
||||
'budget_percentage' => round($budgetPercentage, 2),
|
||||
'currency' => 'USD',
|
||||
'period' => 'monthly',
|
||||
'period_start' => $periodStart->toIso8601String(),
|
||||
'period_end' => $periodEnd->toIso8601String(),
|
||||
'days_remaining' => max(0, $daysRemaining),
|
||||
'projected_spend' => round($projectedSpend, 2),
|
||||
'projected_overspend' => $projectedSpend > $budget->monthly_limit,
|
||||
'limits' => [
|
||||
'daily_limit' => round($dailyLimit, 2),
|
||||
'daily_used' => round($dailySpending, 4),
|
||||
'daily_remaining' => round($dailyLimit - $dailySpending, 4),
|
||||
],
|
||||
'alerts' => [
|
||||
'threshold_50_percent' => $budgetPercentage >= 50,
|
||||
'threshold_75_percent' => $budgetPercentage >= 75,
|
||||
'threshold_90_percent' => $budgetPercentage >= 90,
|
||||
'approaching_daily_limit' => ($dailySpending / $dailyLimit) >= 0.8,
|
||||
],
|
||||
'breakdown_by_provider' => $providerBreakdown,
|
||||
'updated_at' => now()->toIso8601String(),
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get budget history over time
|
||||
*
|
||||
* Returns historical budget data with daily, weekly, or monthly aggregation.
|
||||
*
|
||||
* ## Query Parameters
|
||||
*
|
||||
* - `period` (optional) - Aggregation period: daily, weekly, monthly (default: daily)
|
||||
* - `days` (optional) - Number of days to look back (default: 30)
|
||||
*
|
||||
* ## Example Response
|
||||
*
|
||||
* ```json
|
||||
* {
|
||||
* "data": [
|
||||
* {
|
||||
* "date": "2025-11-19",
|
||||
* "total_cost": 2.45,
|
||||
* "total_requests": 45,
|
||||
* "total_tokens": 45000,
|
||||
* "breakdown": [
|
||||
* {
|
||||
* "provider": "openai",
|
||||
* "cost": 1.35,
|
||||
* "requests": 25
|
||||
* }
|
||||
* ]
|
||||
* }
|
||||
* ],
|
||||
* "meta": {
|
||||
* "period": "daily",
|
||||
* "days": 30,
|
||||
* "total_cost": 45.67,
|
||||
* "total_requests": 1250,
|
||||
* "avg_daily_cost": 1.52,
|
||||
* "avg_daily_requests": 41.7
|
||||
* }
|
||||
* }
|
||||
* ```
|
||||
*
|
||||
* @tags Budget
|
||||
*
|
||||
* @param Request $request
|
||||
* @return JsonResponse
|
||||
*/
|
||||
public function history(Request $request): JsonResponse
|
||||
{
|
||||
$validator = Validator::make($request->all(), [
|
||||
'period' => 'sometimes|string|in:daily,weekly,monthly',
|
||||
'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();
|
||||
$period = $request->input('period', 'daily');
|
||||
$days = $request->input('days', 30);
|
||||
|
||||
$startDate = now()->subDays($days)->startOfDay();
|
||||
|
||||
// Get daily aggregated data
|
||||
$dailyData = LlmRequest::where('gateway_user_id', $user->user_id)
|
||||
->where('created_at', '>=', $startDate)
|
||||
->where('status', 'success')
|
||||
->select(
|
||||
DB::raw('DATE(created_at) as date'),
|
||||
'provider',
|
||||
DB::raw('SUM(total_cost) as cost'),
|
||||
DB::raw('COUNT(*) as requests'),
|
||||
DB::raw('SUM(total_tokens) as tokens')
|
||||
)
|
||||
->groupBy('date', 'provider')
|
||||
->orderBy('date')
|
||||
->get();
|
||||
|
||||
// Group by date
|
||||
$groupedByDate = $dailyData->groupBy('date')->map(function ($items, $date) {
|
||||
return [
|
||||
'date' => $date,
|
||||
'total_cost' => round($items->sum('cost'), 4),
|
||||
'total_requests' => $items->sum('requests'),
|
||||
'total_tokens' => $items->sum('tokens'),
|
||||
'breakdown' => $items->map(function ($item) {
|
||||
return [
|
||||
'provider' => $item->provider,
|
||||
'cost' => round($item->cost, 4),
|
||||
'requests' => $item->requests,
|
||||
];
|
||||
})->values(),
|
||||
];
|
||||
})->values();
|
||||
|
||||
// Calculate meta statistics
|
||||
$totalCost = $groupedByDate->sum('total_cost');
|
||||
$totalRequests = $groupedByDate->sum('total_requests');
|
||||
$dataPoints = $groupedByDate->count();
|
||||
|
||||
return response()->json([
|
||||
'data' => $groupedByDate,
|
||||
'meta' => [
|
||||
'period' => $period,
|
||||
'days' => $days,
|
||||
'total_cost' => round($totalCost, 4),
|
||||
'total_requests' => $totalRequests,
|
||||
'avg_daily_cost' => $dataPoints > 0 ? round($totalCost / $dataPoints, 4) : 0,
|
||||
'avg_daily_requests' => $dataPoints > 0 ? round($totalRequests / $dataPoints, 1) : 0,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -87,7 +87,8 @@ class ModelController extends Controller
|
||||
], 422);
|
||||
}
|
||||
|
||||
$query = ModelPricing::where('is_active', true);
|
||||
$query = ModelPricing::where('is_active', true)
|
||||
->whereNotNull('model');
|
||||
|
||||
// Apply filters
|
||||
if ($request->has('provider')) {
|
||||
@@ -95,7 +96,9 @@ class ModelController extends Controller
|
||||
}
|
||||
|
||||
if ($request->has('max_price')) {
|
||||
$query->where('output_price_per_1k', '<=', $request->input('max_price'));
|
||||
// Convert per-1k price to per-million for comparison
|
||||
$maxPricePerMillion = $request->input('max_price') * 1000;
|
||||
$query->where('output_price_per_million', '<=', $maxPricePerMillion);
|
||||
}
|
||||
|
||||
if ($request->has('min_context')) {
|
||||
@@ -106,7 +109,7 @@ class ModelController extends Controller
|
||||
$sort = $request->input('sort', 'name');
|
||||
switch ($sort) {
|
||||
case 'price':
|
||||
$query->orderBy('output_price_per_1k');
|
||||
$query->orderBy('output_price_per_million');
|
||||
break;
|
||||
case 'context':
|
||||
$query->orderByDesc('context_window');
|
||||
@@ -122,7 +125,7 @@ class ModelController extends Controller
|
||||
->orderByDesc('usage_count');
|
||||
break;
|
||||
default:
|
||||
$query->orderBy('display_name');
|
||||
$query->orderBy('model');
|
||||
}
|
||||
|
||||
$totalCount = ModelPricing::where('is_active', true)->count();
|
||||
@@ -130,10 +133,10 @@ class ModelController extends Controller
|
||||
|
||||
$data = $models->map(function ($model) {
|
||||
return [
|
||||
'id' => $model->model_id,
|
||||
'id' => $model->model,
|
||||
'provider' => $model->provider,
|
||||
'provider_name' => $this->getProviderName($model->provider),
|
||||
'name' => $model->display_name,
|
||||
'name' => $this->getModelDisplayName($model->model),
|
||||
'description' => $this->getModelDescription($model),
|
||||
'context_window' => $model->context_window,
|
||||
'max_output_tokens' => $model->max_output_tokens,
|
||||
@@ -141,8 +144,8 @@ class ModelController extends Controller
|
||||
'supports_function_calling' => in_array($model->provider, ['openai', 'anthropic']),
|
||||
'supports_vision' => $this->supportsVision($model->model_id),
|
||||
'pricing' => [
|
||||
'input_per_1k_tokens' => $model->input_price_per_1k,
|
||||
'output_per_1k_tokens' => $model->output_price_per_1k,
|
||||
'input_per_1k_tokens' => round($model->input_price_per_million / 1000, 6),
|
||||
'output_per_1k_tokens' => round($model->output_price_per_million / 1000, 6),
|
||||
'currency' => 'USD',
|
||||
],
|
||||
'availability' => 'available',
|
||||
@@ -225,7 +228,7 @@ class ModelController extends Controller
|
||||
{
|
||||
// Find the model
|
||||
$modelData = ModelPricing::where('provider', $provider)
|
||||
->where('model_id', $model)
|
||||
->where('model', $model)
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
@@ -277,11 +280,11 @@ class ModelController extends Controller
|
||||
|
||||
$response = [
|
||||
'data' => [
|
||||
'id' => $modelData->model_id,
|
||||
'id' => $modelData->model,
|
||||
'provider' => $modelData->provider,
|
||||
'provider_name' => $this->getProviderName($modelData->provider),
|
||||
'name' => $modelData->display_name,
|
||||
'full_name' => $this->getProviderName($modelData->provider) . ' ' . $modelData->display_name,
|
||||
'name' => $this->getModelDisplayName($modelData->model),
|
||||
'full_name' => $this->getProviderName($modelData->provider) . ' ' . $this->getModelDisplayName($modelData->model),
|
||||
'description' => $this->getModelDescription($modelData),
|
||||
'status' => 'active',
|
||||
'capabilities' => [
|
||||
@@ -293,8 +296,8 @@ class ModelController extends Controller
|
||||
'supports_json_mode' => in_array($modelData->provider, ['openai', 'anthropic']),
|
||||
],
|
||||
'pricing' => [
|
||||
'input_per_1k_tokens' => $modelData->input_price_per_1k,
|
||||
'output_per_1k_tokens' => $modelData->output_price_per_1k,
|
||||
'input_per_1k_tokens' => round($modelData->input_price_per_million / 1000, 6),
|
||||
'output_per_1k_tokens' => round($modelData->output_price_per_million / 1000, 6),
|
||||
'currency' => 'USD',
|
||||
'last_updated' => $modelData->updated_at->toIso8601String(),
|
||||
],
|
||||
@@ -333,13 +336,23 @@ class ModelController extends Controller
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get model display name from model ID
|
||||
*/
|
||||
private function getModelDisplayName(string $modelId): string
|
||||
{
|
||||
// Convert model ID to a readable display name
|
||||
// e.g., "gpt-4-turbo" -> "GPT-4 Turbo"
|
||||
return ucwords(str_replace(['-', '_'], ' ', $modelId));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get model description
|
||||
*/
|
||||
private function getModelDescription(ModelPricing $model): string
|
||||
{
|
||||
// Extract description from model name or provide generic one
|
||||
$modelId = strtolower($model->model_id);
|
||||
$modelId = strtolower($model->model);
|
||||
|
||||
if (str_contains($modelId, 'gpt-4')) {
|
||||
return 'Most capable GPT-4 model with improved instruction following';
|
||||
@@ -361,14 +374,18 @@ class ModelController extends Controller
|
||||
return 'Open-source model with strong performance';
|
||||
}
|
||||
|
||||
return $model->display_name;
|
||||
return $this->getModelDisplayName($model->model);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if model supports vision
|
||||
*/
|
||||
private function supportsVision(string $modelId): bool
|
||||
private function supportsVision(?string $modelId): bool
|
||||
{
|
||||
if ($modelId === null) {
|
||||
return false;
|
||||
}
|
||||
|
||||
$visionModels = [
|
||||
'gpt-4-vision-preview',
|
||||
'gpt-4-turbo',
|
||||
|
||||
@@ -70,7 +70,8 @@ class PricingController extends Controller
|
||||
], 422);
|
||||
}
|
||||
|
||||
$query = ModelPricing::where('is_active', true);
|
||||
$query = ModelPricing::where('is_active', true)
|
||||
->whereNotNull('model');
|
||||
|
||||
// Apply filters
|
||||
if ($request->has('provider')) {
|
||||
@@ -81,13 +82,13 @@ class PricingController extends Controller
|
||||
$sort = $request->input('sort', 'name');
|
||||
switch ($sort) {
|
||||
case 'price':
|
||||
$query->orderBy('output_price_per_1k');
|
||||
$query->orderBy('output_price_per_million');
|
||||
break;
|
||||
case 'provider':
|
||||
$query->orderBy('provider')->orderBy('display_name');
|
||||
$query->orderBy('provider')->orderBy('model');
|
||||
break;
|
||||
default:
|
||||
$query->orderBy('display_name');
|
||||
$query->orderBy('model');
|
||||
}
|
||||
|
||||
$models = $query->get();
|
||||
@@ -96,11 +97,11 @@ class PricingController extends Controller
|
||||
return [
|
||||
'provider' => $model->provider,
|
||||
'provider_name' => $this->getProviderName($model->provider),
|
||||
'model' => $model->model_id,
|
||||
'model_name' => $model->display_name,
|
||||
'model' => $model->model,
|
||||
'model_name' => $this->getModelDisplayName($model->model),
|
||||
'pricing' => [
|
||||
'input_per_1k_tokens' => $model->input_price_per_1k,
|
||||
'output_per_1k_tokens' => $model->output_price_per_1k,
|
||||
'input_per_1k_tokens' => round($model->input_price_per_million / 1000, 6),
|
||||
'output_per_1k_tokens' => round($model->output_price_per_million / 1000, 6),
|
||||
'currency' => 'USD',
|
||||
],
|
||||
'last_updated' => $model->updated_at->toIso8601String(),
|
||||
@@ -196,7 +197,7 @@ class PricingController extends Controller
|
||||
$outputTokens = $request->input('output_tokens');
|
||||
|
||||
// Find the model
|
||||
$model = ModelPricing::where('model_id', $modelId)
|
||||
$model = ModelPricing::where('model', $modelId)
|
||||
->where('is_active', true)
|
||||
->first();
|
||||
|
||||
@@ -210,9 +211,12 @@ class PricingController extends Controller
|
||||
], 404);
|
||||
}
|
||||
|
||||
// Calculate costs
|
||||
$inputCost = ($inputTokens / 1000) * $model->input_price_per_1k;
|
||||
$outputCost = ($outputTokens / 1000) * $model->output_price_per_1k;
|
||||
// Calculate costs (convert from per-million to per-1k)
|
||||
$inputPricePer1k = $model->input_price_per_million / 1000;
|
||||
$outputPricePer1k = $model->output_price_per_million / 1000;
|
||||
|
||||
$inputCost = ($inputTokens / 1000) * $inputPricePer1k;
|
||||
$outputCost = ($outputTokens / 1000) * $outputPricePer1k;
|
||||
$totalCost = $inputCost + $outputCost;
|
||||
|
||||
// Calculate examples for different request volumes
|
||||
@@ -225,13 +229,13 @@ class PricingController extends Controller
|
||||
|
||||
return response()->json([
|
||||
'data' => [
|
||||
'model' => $model->model_id,
|
||||
'model' => $model->model,
|
||||
'provider' => $model->provider,
|
||||
'input_tokens' => $inputTokens,
|
||||
'output_tokens' => $outputTokens,
|
||||
'pricing' => [
|
||||
'input_per_1k' => $model->input_price_per_1k,
|
||||
'output_per_1k' => $model->output_price_per_1k,
|
||||
'input_per_1k' => round($inputPricePer1k, 6),
|
||||
'output_per_1k' => round($outputPricePer1k, 6),
|
||||
'currency' => 'USD',
|
||||
],
|
||||
'calculation' => [
|
||||
@@ -296,7 +300,7 @@ class PricingController extends Controller
|
||||
$outputTokens = $request->input('output_tokens');
|
||||
|
||||
// Get all models
|
||||
$models = ModelPricing::whereIn('model_id', $modelIds)
|
||||
$models = ModelPricing::whereIn('model', $modelIds)
|
||||
->where('is_active', true)
|
||||
->get();
|
||||
|
||||
@@ -312,13 +316,16 @@ class PricingController extends Controller
|
||||
|
||||
// Calculate costs for each model
|
||||
$comparisons = $models->map(function ($model) use ($inputTokens, $outputTokens) {
|
||||
$inputCost = ($inputTokens / 1000) * $model->input_price_per_1k;
|
||||
$outputCost = ($outputTokens / 1000) * $model->output_price_per_1k;
|
||||
$inputPricePer1k = $model->input_price_per_million / 1000;
|
||||
$outputPricePer1k = $model->output_price_per_million / 1000;
|
||||
|
||||
$inputCost = ($inputTokens / 1000) * $inputPricePer1k;
|
||||
$outputCost = ($outputTokens / 1000) * $outputPricePer1k;
|
||||
$totalCost = $inputCost + $outputCost;
|
||||
|
||||
return [
|
||||
'model' => $model->model_id,
|
||||
'model_name' => $model->display_name,
|
||||
'model' => $model->model,
|
||||
'model_name' => $this->getModelDisplayName($model->model),
|
||||
'provider' => $model->provider,
|
||||
'provider_name' => $this->getProviderName($model->provider),
|
||||
'costs' => [
|
||||
@@ -327,8 +334,8 @@ class PricingController extends Controller
|
||||
'total_cost' => round($totalCost, 6),
|
||||
],
|
||||
'pricing' => [
|
||||
'input_per_1k' => $model->input_price_per_1k,
|
||||
'output_per_1k' => $model->output_price_per_1k,
|
||||
'input_per_1k' => round($inputPricePer1k, 6),
|
||||
'output_per_1k' => round($outputPricePer1k, 6),
|
||||
],
|
||||
];
|
||||
})->sortBy('costs.total_cost')->values();
|
||||
@@ -372,4 +379,14 @@ class PricingController extends Controller
|
||||
default => ucfirst($provider),
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get model display name from model ID
|
||||
*/
|
||||
private function getModelDisplayName(string $modelId): string
|
||||
{
|
||||
// Convert model ID to a readable display name
|
||||
// e.g., "gpt-4-turbo" -> "GPT-4 Turbo"
|
||||
return ucwords(str_replace(['-', '_'], ' ', $modelId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -68,6 +68,7 @@ class ProviderController extends Controller
|
||||
// Get model count for this provider
|
||||
$modelsCount = ModelPricing::where('provider', $providerId)
|
||||
->where('is_active', true)
|
||||
->whereNotNull('model')
|
||||
->count();
|
||||
|
||||
$providerData[] = [
|
||||
@@ -172,19 +173,20 @@ class ProviderController extends Controller
|
||||
// Get models for this provider
|
||||
$models = ModelPricing::where('provider', $provider)
|
||||
->where('is_active', true)
|
||||
->orderBy('display_name')
|
||||
->whereNotNull('model')
|
||||
->orderBy('model')
|
||||
->get()
|
||||
->map(function ($model) {
|
||||
return [
|
||||
'id' => $model->model_id,
|
||||
'name' => $model->display_name,
|
||||
'id' => $model->model,
|
||||
'name' => $this->getModelDisplayName($model->model),
|
||||
'context_window' => $model->context_window,
|
||||
'max_output_tokens' => $model->max_output_tokens,
|
||||
'supports_streaming' => true, // Default to true for now
|
||||
'supports_function_calling' => in_array($model->provider, ['openai', 'anthropic']),
|
||||
'pricing' => [
|
||||
'input_per_1k' => $model->input_price_per_1k,
|
||||
'output_per_1k' => $model->output_price_per_1k,
|
||||
'input_per_1k' => round($model->input_price_per_million / 1000, 6),
|
||||
'output_per_1k' => round($model->output_price_per_million / 1000, 6),
|
||||
'currency' => 'USD',
|
||||
],
|
||||
];
|
||||
@@ -308,4 +310,14 @@ class ProviderController extends Controller
|
||||
default => '#',
|
||||
};
|
||||
}
|
||||
|
||||
/**
|
||||
* Get model display name from model ID
|
||||
*/
|
||||
private function getModelDisplayName(string $modelId): string
|
||||
{
|
||||
// Convert model ID to a readable display name
|
||||
// e.g., "gpt-4-turbo" -> "GPT-4 Turbo"
|
||||
return ucwords(str_replace(['-', '_'], ' ', $modelId));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -51,7 +51,7 @@ class ApiKeyController extends Controller
|
||||
$apiKeys = $query->paginate(20)->withQueryString();
|
||||
$gatewayUsers = GatewayUser::orderBy('alias')->get();
|
||||
|
||||
return view('api-keys.index', compact('apiKeys', 'gatewayUsers'));
|
||||
return view('keys.index', compact('apiKeys', 'gatewayUsers'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -60,7 +60,7 @@ class ApiKeyController extends Controller
|
||||
public function create()
|
||||
{
|
||||
$gatewayUsers = GatewayUser::orderBy('alias')->get();
|
||||
return view('api-keys.create', compact('gatewayUsers'));
|
||||
return view('keys.create', compact('gatewayUsers'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -104,7 +104,7 @@ class ApiKeyController extends Controller
|
||||
session()->flash('new_api_key', $token);
|
||||
session()->flash('new_api_key_id', $apiKey->token);
|
||||
|
||||
return redirect()->route('api-keys.index')
|
||||
return redirect()->route('keys.index')
|
||||
->with('success', 'API Key created successfully! Make sure to copy it now - it won\'t be shown again.');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
@@ -136,7 +136,7 @@ class ApiKeyController extends Controller
|
||||
->limit(20)
|
||||
->get();
|
||||
|
||||
return view('api-keys.show', compact('apiKey', 'stats', 'recentLogs'));
|
||||
return view('keys.show', compact('apiKey', 'stats', 'recentLogs'));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -150,7 +150,7 @@ class ApiKeyController extends Controller
|
||||
// Delete the API key from database
|
||||
$apiKey->delete();
|
||||
|
||||
return redirect()->route('api-keys.index')
|
||||
return redirect()->route('keys.index')
|
||||
->with('success', 'API Key revoked successfully');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
|
||||
@@ -1,212 +0,0 @@
|
||||
<?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',
|
||||
]);
|
||||
|
||||
// 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(),
|
||||
'name' => $validated['budget_name'],
|
||||
'monthly_limit' => $monthlyLimit,
|
||||
'daily_limit' => $dailyLimit,
|
||||
]);
|
||||
|
||||
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([
|
||||
'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',
|
||||
]);
|
||||
|
||||
// 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([
|
||||
'name' => $validated['budget_name'],
|
||||
'monthly_limit' => $monthlyLimit,
|
||||
'daily_limit' => $dailyLimit,
|
||||
]);
|
||||
|
||||
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!');
|
||||
}
|
||||
}
|
||||
@@ -14,8 +14,7 @@ class GatewayUserController extends Controller
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = GatewayUser::with('budget')
|
||||
->withCount(['apiKeys', 'usageLogs']);
|
||||
$query = GatewayUser::withCount(['apiKeys', 'usageLogs']);
|
||||
|
||||
// Search
|
||||
if ($request->filled('search')) {
|
||||
@@ -87,7 +86,7 @@ class GatewayUserController extends Controller
|
||||
*/
|
||||
public function show(string $userId)
|
||||
{
|
||||
$user = GatewayUser::with(['apiKeys', 'budget'])
|
||||
$user = GatewayUser::with(['apiKeys'])
|
||||
->findOrFail($userId);
|
||||
|
||||
// Get usage statistics for last 30 days
|
||||
|
||||
Reference in New Issue
Block a user