From bef36c7ca2e376373de3809a99cf9b98ab3413cc Mon Sep 17 00:00:00 2001 From: wtrinkl Date: Tue, 18 Nov 2025 22:05:05 +0100 Subject: [PATCH] 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 --- Dockerfile | 27 - LARAVEL_IMPLEMENTATION.md | 1330 ----------------- README.md | 440 +++--- config.yml | 40 - docker-compose.yml | 91 +- .../app/Http/Controllers/ApiKeyController.php | 81 +- .../app/Http/Controllers/BudgetController.php | 73 +- .../Http/Controllers/DashboardController.php | 17 +- .../Controllers/ModelPricingController.php | 362 ++++- laravel-app/app/Models/Admin.php | 45 - laravel-app/app/Models/ApiKey.php | 102 +- laravel-app/app/Models/Budget.php | 62 +- laravel-app/app/Models/GatewayUser.php | 95 +- laravel-app/app/Models/ModelPricing.php | 56 +- laravel-app/app/Models/UsageLog.php | 52 +- laravel-app/app/Models/User.php | 32 + .../app/Services/StatisticsService.php | 142 +- laravel-app/bootstrap/app.php | 6 +- laravel-app/config/auth.php | 4 +- ..._01_000000_create_users_table.php.disabled | 49 - .../2025_11_15_192931_create_admins_table.php | 26 - ..._15_193000_create_admin_support_tables.php | 40 - .../resources/views/api-keys/index.blade.php | 4 +- .../resources/views/dashboard.blade.php | 161 +- .../livewire/layout/navigation.blade.php | 12 + .../views/model-pricing/create.blade.php | 203 ++- .../views/model-pricing/index.blade.php | 26 +- laravel-app/routes/console.php | 8 + laravel-app/routes/web.php | 31 + laravel/Dockerfile | 2 +- setup-laravel.sh | 190 --- web/default.conf | 34 - web/index.html | 428 ------ 33 files changed, 1341 insertions(+), 2930 deletions(-) delete mode 100644 Dockerfile delete mode 100644 LARAVEL_IMPLEMENTATION.md delete mode 100644 config.yml delete mode 100644 laravel-app/app/Models/Admin.php delete mode 100644 laravel-app/database/migrations/0001_01_01_000000_create_users_table.php.disabled delete mode 100644 laravel-app/database/migrations/2025_11_15_192931_create_admins_table.php delete mode 100644 laravel-app/database/migrations/2025_11_15_193000_create_admin_support_tables.php delete mode 100755 setup-laravel.sh delete mode 100644 web/default.conf delete mode 100644 web/index.html diff --git a/Dockerfile b/Dockerfile deleted file mode 100644 index c2d6b31..0000000 --- a/Dockerfile +++ /dev/null @@ -1,27 +0,0 @@ -FROM python:3.13-slim as base - -WORKDIR /app - -COPY pyproject.toml ./ -COPY src/ ./src/ - -ARG VERSION=0.0.0+docker -ENV SETUPTOOLS_SCM_PRETEND_VERSION="${VERSION}" - -RUN pip install --no-cache-dir --upgrade pip && \ - pip install --no-cache-dir .[all,gateway] - -RUN useradd -m -u 1000 gateway && \ - chown -R gateway:gateway /app - -USER gateway - -EXPOSE 8000 - -HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \ - CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1 - -ENV GATEWAY_HOST=0.0.0.0 -ENV GATEWAY_PORT=8000 - -CMD ["any-llm-gateway", "serve"] diff --git a/LARAVEL_IMPLEMENTATION.md b/LARAVEL_IMPLEMENTATION.md deleted file mode 100644 index f750868..0000000 --- a/LARAVEL_IMPLEMENTATION.md +++ /dev/null @@ -1,1330 +0,0 @@ -# Laravel Verwaltungsoberfläche für Any-LLM Gateway -## Implementierungskonzept - ---- - -## 1. Projekt-Übersicht - -### 1.1 Ziel -Vollständige Laravel-Verwaltungsoberfläche für das Any-LLM Gateway mit: -- Benutzer-Login und Authentifizierung -- Dashboard mit Statistiken und Analysen -- Verwaltung von Users, API Keys, Budgets -- Detaillierte Nutzungsberichte -- Monitoring und Alerts - -### 1.2 Tech Stack -- **Backend**: Laravel 11.x -- **Frontend**: Livewire 3.x + Alpine.js + Tailwind CSS -- **Charts**: Chart.js / ApexCharts -- **Datenbank**: PostgreSQL (existing Gateway DB) -- **Authentication**: Laravel Breeze mit Livewire - ---- - -## 2. Datenbankstruktur (Existing Gateway DB) - -### 2.1 Tabellen-Übersicht - -``` -┌─────────────────┐ -│ users │ ← Gateway Users (API Consumers) -└────────┬────────┘ - │ - ┌────┴────┬─────────────┬──────────────┐ - │ │ │ │ -┌───▼────┐ ┌─▼────────┐ ┌──▼───────┐ ┌──▼──────────┐ -│api_keys│ │usage_logs│ │ budgets │ │budget_reset │ -└────────┘ └──────────┘ └──────────┘ │ _logs │ - └─────────────┘ -``` - -### 2.2 Tabellen-Details - -#### `users` - Gateway API Users -```sql -- user_id (PK) VARCHAR -- alias VARCHAR -- spend DOUBLE -- budget_id (FK) VARCHAR -- blocked BOOLEAN -- created_at TIMESTAMP -- updated_at TIMESTAMP -- metadata JSON -- budget_started_at TIMESTAMP -- next_budget_reset_at TIMESTAMP -``` - -#### `api_keys` - Virtual Keys -```sql -- id (PK) VARCHAR -- key_hash VARCHAR (UNIQUE) -- key_name VARCHAR -- user_id (FK) VARCHAR -- created_at TIMESTAMP -- last_used_at TIMESTAMP -- expires_at TIMESTAMP -- is_active BOOLEAN -- metadata JSON -``` - -#### `usage_logs` - Request Tracking -```sql -- id (PK) VARCHAR -- api_key_id (FK) VARCHAR -- user_id (FK) VARCHAR -- timestamp TIMESTAMP (INDEXED) -- model VARCHAR -- provider VARCHAR -- endpoint VARCHAR -- prompt_tokens INT -- completion_tokens INT -- total_tokens INT -- cost DOUBLE -- status VARCHAR -- error_message VARCHAR -``` - -#### `budgets` - Budget Definitions -```sql -- budget_id (PK) VARCHAR -- max_budget DOUBLE -- created_at TIMESTAMP -- updated_at TIMESTAMP -- budget_duration_sec INT -``` - -#### `model_pricing` - Model Costs -```sql -- model_key (PK) VARCHAR -- input_price_per_million DOUBLE -- output_price_per_million DOUBLE -- created_at TIMESTAMP -- updated_at TIMESTAMP -``` - ---- - -## 3. Laravel Projektstruktur - -### 3.1 Verzeichnisstruktur -``` -any-llm-admin/ -├── app/ -│ ├── Http/ -│ │ ├── Controllers/ -│ │ │ ├── DashboardController.php -│ │ │ ├── GatewayUserController.php -│ │ │ ├── ApiKeyController.php -│ │ │ ├── BudgetController.php -│ │ │ ├── UsageLogController.php -│ │ │ └── ModelPricingController.php -│ │ ├── Livewire/ -│ │ │ ├── Dashboard/ -│ │ │ │ ├── StatsOverview.php -│ │ │ │ ├── UsageChart.php -│ │ │ │ ├── TopUsers.php -│ │ │ │ └── RecentActivity.php -│ │ │ ├── GatewayUsers/ -│ │ │ │ ├── Index.php -│ │ │ │ ├── Create.php -│ │ │ │ ├── Edit.php -│ │ │ │ └── Show.php -│ │ │ ├── ApiKeys/ -│ │ │ │ ├── Index.php -│ │ │ │ ├── Create.php -│ │ │ │ └── Revoke.php -│ │ │ └── Budgets/ -│ │ │ ├── Index.php -│ │ │ ├── Create.php -│ │ │ └── Edit.php -│ │ └── Middleware/ -│ │ └── EnsureAdmin.php -│ ├── Models/ -│ │ ├── Admin.php (Laravel Auth User) -│ │ ├── GatewayUser.php (Gateway users table) -│ │ ├── ApiKey.php -│ │ ├── UsageLog.php -│ │ ├── Budget.php -│ │ ├── BudgetResetLog.php -│ │ └── ModelPricing.php -│ └── Services/ -│ ├── StatisticsService.php -│ ├── BudgetService.php -│ └── GatewayApiService.php -├── database/ -│ ├── migrations/ -│ │ └── 2025_11_15_000001_create_admins_table.php -│ └── seeders/ -│ └── AdminSeeder.php -├── resources/ -│ ├── views/ -│ │ ├── layouts/ -│ │ │ ├── app.blade.php -│ │ │ ├── navigation.blade.php -│ │ │ └── guest.blade.php -│ │ ├── dashboard.blade.php -│ │ ├── gateway-users/ -│ │ ├── api-keys/ -│ │ ├── budgets/ -│ │ ├── usage-logs/ -│ │ └── model-pricing/ -│ └── js/ -│ └── charts.js -└── routes/ - ├── web.php - └── api.php -``` - ---- - -## 4. Laravel Models - -### 4.1 Admin Model (Laravel Auth) -```php - 'datetime', - 'password' => 'hashed', - ]; -} -``` - -### 4.2 GatewayUser Model -```php - 'double', - 'blocked' => 'boolean', - 'metadata' => 'array', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - 'budget_started_at' => 'datetime', - 'next_budget_reset_at' => 'datetime', - ]; - - // Relationships - public function apiKeys() - { - return $this->hasMany(ApiKey::class, 'user_id', 'user_id'); - } - - public function usageLogs() - { - return $this->hasMany(UsageLog::class, 'user_id', 'user_id'); - } - - public function budget() - { - return $this->belongsTo(Budget::class, 'budget_id', 'budget_id'); - } - - public function budgetResetLogs() - { - return $this->hasMany(BudgetResetLog::class, 'user_id', 'user_id'); - } - - // Scopes - public function scopeActive($query) - { - return $query->where('blocked', false); - } - - public function scopeBlocked($query) - { - return $query->where('blocked', true); - } - - // Accessors - public function getSpendFormattedAttribute() - { - return '$' . number_format($this->spend, 2); - } - - public function getTotalRequestsAttribute() - { - return $this->usageLogs()->count(); - } - - public function getTotalTokensAttribute() - { - return $this->usageLogs()->sum('total_tokens'); - } -} -``` - -### 4.3 ApiKey Model -```php - 'boolean', - 'metadata' => 'array', - 'created_at' => 'datetime', - 'last_used_at' => 'datetime', - 'expires_at' => 'datetime', - ]; - - // Relationships - public function gatewayUser() - { - return $this->belongsTo(GatewayUser::class, 'user_id', 'user_id'); - } - - public function usageLogs() - { - return $this->hasMany(UsageLog::class, 'api_key_id', 'id'); - } - - // Scopes - 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()); - } - - // Accessors - 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(); - } -} -``` - -### 4.4 UsageLog Model -```php - 'datetime', - 'prompt_tokens' => 'integer', - 'completion_tokens' => 'integer', - 'total_tokens' => 'integer', - 'cost' => 'double', - ]; - - // Relationships - public function gatewayUser() - { - return $this->belongsTo(GatewayUser::class, 'user_id', 'user_id'); - } - - public function apiKey() - { - return $this->belongsTo(ApiKey::class, 'api_key_id', 'id'); - } - - // Scopes - public function scopeSuccess($query) - { - return $query->where('status', 'success'); - } - - public function scopeFailed($query) - { - return $query->where('status', '!=', 'success'); - } - - public function scopeToday($query) - { - return $query->whereDate('timestamp', today()); - } - - public function scopeDateRange($query, $start, $end) - { - return $query->whereBetween('timestamp', [$start, $end]); - } - - // Accessors - public function getCostFormattedAttribute() - { - return $this->cost ? '$' . number_format($this->cost, 4) : 'N/A'; - } -} -``` - -### 4.5 Budget Model -```php - 'double', - 'budget_duration_sec' => 'integer', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - ]; - - // Relationships - public function gatewayUsers() - { - return $this->hasMany(GatewayUser::class, 'budget_id', 'budget_id'); - } - - public function resetLogs() - { - return $this->hasMany(BudgetResetLog::class, 'budget_id', 'budget_id'); - } - - // Accessors - 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"; - } -} -``` - -### 4.6 ModelPricing Model -```php - 'double', - 'output_price_per_million' => 'double', - 'created_at' => 'datetime', - 'updated_at' => 'datetime', - ]; - - // Accessors - public function getInputPriceFormattedAttribute() - { - return '$' . number_format($this->input_price_per_million, 2) . '/M'; - } - - public function getOutputPriceFormattedAttribute() - { - return '$' . number_format($this->output_price_per_million, 2) . '/M'; - } -} -``` - ---- - -## 5. Services - -### 5.1 StatisticsService -```php - GatewayUser::count(), - 'active_users' => GatewayUser::active()->count(), - 'total_requests_today' => UsageLog::today()->count(), - 'total_spend_today' => UsageLog::today()->sum('cost'), - 'total_tokens_today' => UsageLog::today()->sum('total_tokens'), - ]; - } - - public function getUsageByProvider($days = 30) - { - return UsageLog::selectRaw('provider, COUNT(*) as count, SUM(cost) as total_cost') - ->where('timestamp', '>=', now()->subDays($days)) - ->groupBy('provider') - ->get(); - } - - public function getUsageByModel($days = 30) - { - return UsageLog::selectRaw('model, COUNT(*) as count, SUM(total_tokens) as tokens') - ->where('timestamp', '>=', now()->subDays($days)) - ->groupBy('model') - ->orderByDesc('count') - ->limit(10) - ->get(); - } - - public function getDailyUsageChart($days = 30) - { - return UsageLog::selectRaw('DATE(timestamp) as date, COUNT(*) as requests, SUM(cost) as cost') - ->where('timestamp', '>=', now()->subDays($days)) - ->groupBy('date') - ->orderBy('date') - ->get(); - } - - public function getTopUsers($limit = 10) - { - return GatewayUser::withCount('usageLogs') - ->withSum('usageLogs', 'cost') - ->orderByDesc('usage_logs_sum_cost') - ->limit($limit) - ->get(); - } - - public function getRecentActivity($limit = 20) - { - return UsageLog::with(['gatewayUser', 'apiKey']) - ->orderByDesc('timestamp') - ->limit($limit) - ->get(); - } - - public function getUserStatistics($userId, $days = 30) - { - $stats = UsageLog::where('user_id', $userId) - ->where('timestamp', '>=', now()->subDays($days)) - ->selectRaw(' - COUNT(*) as total_requests, - SUM(prompt_tokens) as total_prompt_tokens, - SUM(completion_tokens) as total_completion_tokens, - SUM(total_tokens) as total_tokens, - SUM(cost) as total_cost, - AVG(total_tokens) as avg_tokens_per_request - ') - ->first(); - - return $stats; - } -} -``` - ---- - -## 6. Controllers - -### 6.1 DashboardController -```php -statsService->getDashboardStats(); - $dailyUsage = $this->statsService->getDailyUsageChart(30); - $topUsers = $this->statsService->getTopUsers(5); - $providerStats = $this->statsService->getUsageByProvider(30); - - return view('dashboard', compact( - 'stats', - 'dailyUsage', - 'topUsers', - 'providerStats' - )); - } -} -``` - -### 6.2 GatewayUserController -```php -withCount('apiKeys') - ->withCount('usageLogs') - ->paginate(20); - - return view('gateway-users.index', compact('users')); - } - - public function show($userId) - { - $user = GatewayUser::with(['apiKeys', 'budget']) - ->findOrFail($userId); - - $stats = $this->statsService->getUserStatistics($userId, 30); - $recentLogs = $user->usageLogs() - ->orderByDesc('timestamp') - ->limit(50) - ->get(); - - return view('gateway-users.show', compact('user', 'stats', 'recentLogs')); - } - - // ... weitere CRUD Methoden -} -``` - ---- - -## 7. Livewire Components - -### 7.1 Dashboard Stats Overview -```php -getDashboardStats(); - - return view('livewire.dashboard.stats-overview', [ - 'stats' => $stats - ]); - } - - public function refresh() - { - // Livewire will automatically re-render - } -} -``` - -### 7.2 Usage Chart Component -```php -getDailyUsageChart($this->days); - - return view('livewire.dashboard.usage-chart', [ - 'chartData' => $data - ]); - } - - public function updatedDays() - { - // Chart will automatically update - } -} -``` - ---- - -## 8. Views Structure - -### 8.1 Dashboard Layout -```blade - - - - - - - Any-LLM Admin - @yield('title') - @vite(['resources/css/app.css', 'resources/js/app.js']) - @livewireStyles - - -
- - @include('layouts.navigation') - - -
-
- @yield('content') -
-
-
- - @livewireScripts - - - -``` - -### 8.2 Dashboard View -```blade - -@extends('layouts.app') - -@section('title', 'Dashboard') - -@section('content') -
- -
-
-
Total Users
-
{{ $stats['total_users'] }}
-
-
-
Requests Today
-
{{ number_format($stats['total_requests_today']) }}
-
-
-
Spend Today
-
${{ number_format($stats['total_spend_today'], 2) }}
-
-
-
Tokens Today
-
{{ number_format($stats['total_tokens_today']) }}
-
-
- - -
-

Usage Trend (Last 30 Days)

- -
- - -
-
-

Usage by Provider

- -
- -
-

Top Users

-
- @foreach($topUsers as $user) -
-
-
{{ $user->alias ?? $user->user_id }}
-
{{ number_format($user->usage_logs_count) }} requests
-
-
-
${{ number_format($user->usage_logs_sum_cost ?? 0, 2) }}
-
-
- @endforeach -
-
-
-
- -@push('scripts') - -@endpush -@endsection -``` - ---- - -## 9. Routes - -### 9.1 Web Routes -```php -group(function () { - // Dashboard - Route::get('/dashboard', [DashboardController::class, 'index'])->name('dashboard'); - - // Gateway Users - Route::resource('gateway-users', GatewayUserController::class); - Route::post('gateway-users/{id}/toggle-block', [GatewayUserController::class, 'toggleBlock']) - ->name('gateway-users.toggle-block'); - - // API Keys - Route::resource('api-keys', ApiKeyController::class)->except(['edit', 'update']); - Route::post('api-keys/{id}/revoke', [ApiKeyController::class, 'revoke']) - ->name('api-keys.revoke'); - - // Budgets - Route::resource('budgets', BudgetController::class'); - - // Usage Logs - Route::get('usage-logs', [UsageLogController::class, 'index'])->name('usage-logs.index'); - Route::get('usage-logs/export', [UsageLogController::class, 'export'])->name('usage-logs.export'); - - // Model Pricing - Route::resource('model-pricing', ModelPricingController::class); - - // API for Charts (AJAX) - Route::prefix('api')->group(function () { - Route::get('stats/daily-usage', [DashboardController::class, 'dailyUsage']); - Route::get('stats/provider-breakdown', [DashboardController::class, 'providerBreakdown']); - Route::get('stats/model-usage', [DashboardController::class, 'modelUsage']); - }); -}); -``` - ---- - -## 10. Installation & Setup - -### 10.1 Voraussetzungen -- PHP 8.2+ -- Composer -- Node.js & NPM -- PostgreSQL Client - -### 10.2 Installation Steps - -```bash -# 1. Laravel Projekt erstellen -composer create-project laravel/laravel any-llm-admin -cd any-llm-admin - -# 2. Zusätzliche Packages installieren -composer require livewire/livewire -composer require laravel/breeze --dev - -# 3. Breeze mit Livewire installieren -php artisan breeze:install livewire -npm install && npm run build - -# 4. .env konfigurieren -cat > .env << 'EOF' -APP_NAME="Any-LLM Admin" -APP_URL=http://localhost:8001 - -DB_CONNECTION=pgsql -DB_HOST=localhost -DB_PORT=5432 -DB_DATABASE=gateway -DB_USERNAME=gateway -DB_PASSWORD=gateway -EOF - -# 5. Admin Migration erstellen -php artisan make:migration create_admins_table - -# 6. Models erstellen -php artisan make:model GatewayUser -php artisan make:model ApiKey -php artisan make:model UsageLog -php artisan make:model Budget -php artisan make:model BudgetResetLog -php artisan make:model ModelPricing - -# 7. Services erstellen -php artisan make:class Services/StatisticsService - -# 8. Controllers erstellen -php artisan make:controller DashboardController -php artisan make:controller GatewayUserController --resource -php artisan make:controller ApiKeyController --resource -php artisan make:controller BudgetController --resource -php artisan make:controller UsageLogController -php artisan make:controller ModelPricingController --resource - -# 9. Livewire Components erstellen -php artisan make:livewire Dashboard/StatsOverview -php artisan make:livewire Dashboard/UsageChart -php artisan make:livewire GatewayUsers/Index -php artisan make:livewire ApiKeys/Index - -# 10. Migration für Admins ausführen -php artisan migrate - -# 11. Admin User erstellen -php artisan tinker ->>> \App\Models\Admin::create([ -... 'name' => 'Admin', -... 'email' => 'admin@example.com', -... 'password' => bcrypt('password123') -... ]); - -# 12. Development Server starten -php artisan serve --port=8001 -``` - -### 10.3 Admin Migration -```php -id(); - $table->string('name'); - $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); - $table->rememberToken(); - $table->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('admins'); - } -}; -``` - ---- - -## 11. Features & Funktionalität - -### 11.1 Dashboard -✅ **Übersicht-Statistiken** -- Total Users / Active Users -- Requests Today -- Spend Today / This Month -- Tokens Today - -✅ **Visualisierungen** -- Daily Usage Chart (Line Chart) -- Provider Breakdown (Doughnut Chart) -- Model Usage (Bar Chart) -- Cost Trends - -✅ **Quick Actions** -- Top Users Widget -- Recent Activity Feed -- Alerts (Budget Warnings) - -### 11.2 Gateway Users Management -✅ **Liste aller Users** -- Sortierbar nach Spend, Requests, Created Date -- Filter: Active / Blocked / All -- Suche nach User ID / Alias - -✅ **User Detail Page** -- Übersicht (Spend, Requests, Tokens) -- Associated API Keys -- Budget Information -- 30-Day Usage Chart -- Recent Activity Log - -✅ **User Actions** -- Create New User -- Edit User (Alias, Budget) -- Block / Unblock User -- Delete User (mit Confirmation) - -### 11.3 API Keys Management -✅ **Liste aller Keys** -- Masked Keys anzeigen -- Status: Active / Expired / Revoked -- Last Used Date -- Associated User - -✅ **Key Actions** -- Create Virtual Key -- Revoke Key -- Set Expiration Date -- View Key Details - -### 11.4 Budgets Management -✅ **Budget Templates** -- Daily / Weekly / Monthly Budgets -- Custom Duration -- No Limit Option - -✅ **Budget Assignment** -- Assign to Users -- Bulk Assignment -- Auto-Reset Configuration - -### 11.5 Usage Logs -✅ **Comprehensive Logging** -- Filter by Date Range -- Filter by User / Provider / Model -- Export to CSV / Excel -- Real-time Updates (Livewire) - -✅ **Log Details** -- Request Metadata -- Token Counts -- Cost Calculation -- Error Messages (if failed) - -### 11.6 Model Pricing -✅ **Pricing Management** -- Add New Model Pricing -- Update Existing Prices -- Bulk Import from CSV -- Cost Calculator Tool - ---- - -## 12. Security & Best Practices - -### 12.1 Authentication -```php -// auth.php guard configuration -'guards' => [ - 'web' => [ - 'driver' => 'session', - 'provider' => 'admins', - ], -], - -'providers' => [ - 'admins' => [ - 'driver' => 'eloquent', - 'model' => App\Models\Admin::class, - ], -], -``` - -### 12.2 Middleware -- `auth` - Alle Admin Routes -- `throttle:60,1` - Rate Limiting -- CSRF Protection (automatisch) - -### 12.3 Database Security -- Read-only Connection für Statistics -- Prepared Statements (Eloquent default) -- Input Validation & Sanitization - ---- - -## 13. Testing Strategy - -### 13.1 Feature Tests -```php -// tests/Feature/DashboardTest.php -public function test_admin_can_view_dashboard() -{ - $admin = Admin::factory()->create(); - - $response = $this->actingAs($admin) - ->get('/dashboard'); - - $response->assertStatus(200); - $response->assertSee('Total Users'); -} -``` - -### 13.2 Unit Tests -```php -// tests/Unit/StatisticsServiceTest.php -public function test_dashboard_stats_calculation() -{ - $stats = app(StatisticsService::class)->getDashboardStats(); - - $this->assertIsArray($stats); - $this->assertArrayHasKey('total_users', $stats); -} -``` - ---- - -## 14. Deployment Checklist - -### 14.1 Production Setup -```bash -# 1. Umgebung vorbereiten -composer install --optimize-autoloader --no-dev -npm ci && npm run build - -# 2. .env für Production -APP_ENV=production -APP_DEBUG=false -APP_KEY=[generieren mit php artisan key:generate] - -# 3. Optimierungen -php artisan config:cache -php artisan route:cache -php artisan view:cache -php artisan event:cache - -# 4. Queue Worker (optional) -php artisan queue:work --tries=3 -``` - -### 14.2 Server Requirements -- PHP 8.2+ (php-fpm) -- Nginx / Apache -- PostgreSQL Client -- Supervisor (für Queue Workers) -- SSL Certificate (Let's Encrypt) - ---- - -## 15. Erweiterungsmöglichkeiten - -### 15.1 Geplante Features -- [ ] Email Notifications (Budget Alerts) -- [ ] API Rate Limiting per User -- [ ] Advanced Analytics Dashboard -- [ ] Multi-Admin with Roles -- [ ] Audit Log -- [ ] Webhook Support -- [ ] Slack Integration -- [ ] Cost Forecasting - -### 15.2 Integration Options -- Stripe für Billing -- Sentry für Error Tracking -- DataDog für Monitoring -- Grafana für Advanced Charts - ---- - -## 16. Maintenance - -### 16.1 Backup Strategy -```bash -# Database Backup -pg_dump -U gateway gateway > backup_$(date +%Y%m%d).sql - -# Automated Backup -0 2 * * * /usr/bin/pg_dump -U gateway gateway > /backups/gateway_$(date +\%Y\%m\%d).sql -``` - -### 16.2 Log Rotation -```bash -# Laravel Log Rotation -php artisan log:clear --keep=30 - -# Usage Logs Archival (älter als 90 Tage) -DELETE FROM usage_logs WHERE timestamp < NOW() - INTERVAL '90 days'; -``` - ---- - -## 17. Support & Documentation - -### 17.1 Admin Handbuch -- Benutzer-Onboarding Guide -- Troubleshooting Common Issues -- API Key Best Practices -- Budget Configuration Examples - -### 17.2 Developer Documentation -- Code Style Guide (PSR-12) -- Component Structure -- Adding New Features -- Testing Guidelines - ---- - -**Ende des Implementierungskonzepts** - -Nächste Schritte: -1. Repository aufsetzen -2. Models & Migrations implementieren -3. Dashboard UI aufbauen -4. Testing durchführen -5. Production Deployment diff --git a/README.md b/README.md index ceb5795..4a51874 100644 --- a/README.md +++ b/README.md @@ -1,11 +1,16 @@ -# Any-LLM Gateway + Laravel Admin +# Laravel LLM Gateway -Vollständiges Docker-Setup mit: -- **Any-LLM Gateway** (API Gateway für LLMs) -- **Laravel Admin Panel** (Verwaltungsoberfläche) -- **PostgreSQL** (Datenbank) -- **Adminer** (Datenbank-Management-Tool) -- **Gateway Tester** (Test-Oberfläche) +Ein umfassendes Laravel-basiertes LLM Gateway System mit Multi-Provider-Support, Kosten-Tracking, Budget-Management und Admin-Interface. + +## 🎯 Hauptfeatures + +- **Multi-Provider Support**: OpenAI, Anthropic, DeepSeek, Google Gemini, Mistral AI +- **Per-User API Keys**: Verschlüsselte Speicherung von Provider-Credentials pro Benutzer +- **Kosten-Tracking**: Detaillierte Verfolgung von Token-Nutzung und Kosten +- **Budget-Management**: Flexible Budget-Limits und Benachrichtigungen +- **Rate Limiting**: Schutz vor Überlastung +- **Admin-Interface**: Umfassende Verwaltungsoberfläche mit Laravel/Livewire +- **OpenAI-kompatible API**: Standard-konforme Endpoints --- @@ -13,24 +18,19 @@ Vollständiges Docker-Setup mit: ### Voraussetzungen - Docker & Docker Compose installiert -- Ports 80, 8000, 8080, 8081 verfügbar +- Port 80 und 8081 verfügbar ### Installation ```bash -cd /opt/any-llm -./setup-laravel.sh +cd /opt/laravel-llm +docker compose up -d ``` -Das Setup-Script führt automatisch aus: -1. ✅ Laravel Installation -2. ✅ Livewire & Breeze Setup -3. ✅ Docker Container Build & Start -4. ✅ Datenbank-Migrationen -5. ✅ Admin-User Erstellung -6. ✅ Assets-Kompilierung - -**⏱️ Dauer: ca. 5-10 Minuten** +Das System startet automatisch: +- Laravel Anwendung auf Port 80 +- MariaDB 11.4 Datenbank +- phpMyAdmin auf Port 8081 --- @@ -40,25 +40,22 @@ Das Setup-Script führt automatisch aus: | Service | URL | Beschreibung | |---------|-----|--------------| -| **Laravel Admin** | http://localhost:80 | Verwaltungsoberfläche | -| **Gateway API** | http://localhost:8000 | Any-LLM Gateway | -| **Gateway Tester** | http://localhost:8080 | Test-Interface | -| **Adminer** | http://localhost:8081 | PostgreSQL Management | +| **Laravel Admin** | http://localhost:80 | Admin-Interface | +| **phpMyAdmin** | http://localhost:8081 | Datenbank-Management | ### Login-Daten #### Laravel Admin ``` -Email: admin@example.com -Password: password123 +Email: admin@laravel-llm.local +Password: [Dein Admin-Passwort] ``` -#### Adminer (PostgreSQL) +#### phpMyAdmin (MariaDB) ``` -System: PostgreSQL -Server: postgres -Username: gateway -Password: gateway +Server: mariadb +Username: root +Password: rootpass Database: gateway ``` @@ -67,28 +64,63 @@ Database: gateway ## 📁 Projekt-Struktur ``` -/opt/any-llm/ -├── config.yml # Gateway Konfiguration -├── docker-compose.yml # Docker Services -├── setup-laravel.sh # Setup Script +/opt/laravel-llm/ +├── docker-compose.yml # Docker Services Definition +├── backup_*.sql # Datenbank-Backups │ ├── laravel/ # Laravel Docker Config -│ ├── Dockerfile -│ ├── nginx.conf -│ ├── supervisord.conf -│ └── php.ini +│ ├── Dockerfile # PHP 8.3 + Nginx +│ ├── nginx.conf # Webserver Config +│ ├── supervisord.conf # Process Manager +│ └── php.ini # PHP Einstellungen │ -├── laravel-app/ # Laravel Projekt (wird generiert) -│ ├── app/ -│ ├── database/ -│ ├── resources/ -│ └── ... -│ -├── web/ # Gateway Tester -│ ├── index.html -│ └── default.conf -│ -└── LARAVEL_IMPLEMENTATION.md # Detailliertes Implementierungskonzept +└── laravel-app/ # Laravel Anwendung + ├── app/ + │ ├── Http/Controllers/ # Admin Controllers + │ ├── Models/ # Eloquent Models + │ ├── Services/LLM/ # Provider Services + │ └── ... + ├── database/ + │ └── migrations/ # Datenbank Schema + ├── resources/ + │ └── views/ # Blade Templates + └── routes/ + └── web.php # Admin Routes +``` + +--- + +## 🗄️ Datenbank-Schema + +### Haupttabellen + +| Tabelle | Beschreibung | +|---------|--------------| +| `admins` | Admin-Benutzer für das Interface | +| `users` | Gateway-Benutzer (API-Nutzer) | +| `user_provider_keys` | Verschlüsselte Provider API-Keys | +| `usage_logs` | Request-Tracking und Kosten | +| `budgets` | Budget-Definitionen | +| `budget_reset_logs` | Budget-Historie | +| `model_pricing` | Modell-Kosten-Konfiguration | + +### Datenbank-Zugriff + +**Via phpMyAdmin:** +``` +http://localhost:8081 +Server: mariadb +User: root +Password: rootpass +``` + +**Via CLI:** +```bash +docker compose exec mariadb mysql -u gateway -pgateway gateway + +# Beispiel-Queries +SELECT * FROM users; +SELECT * FROM usage_logs ORDER BY created_at DESC LIMIT 10; ``` --- @@ -98,157 +130,158 @@ Database: gateway ### Container Management ```bash -# Alle Container starten +# Container starten docker compose up -d -# Alle Container stoppen +# Container stoppen docker compose down # Logs anzeigen -docker compose logs -f - -# Logs eines bestimmten Services docker compose logs -f laravel -docker compose logs -f gateway # Container neu bauen docker compose up -d --build -# In Container einloggen +# In Laravel Container einloggen docker compose exec laravel bash -docker compose exec postgres psql -U gateway -d gateway ``` -### Laravel Commands (im Container) +### Laravel Artisan Commands ```bash -# Artisan Commands +# Migrationen docker compose exec laravel php artisan migrate -docker compose exec laravel php artisan make:model ModelName -docker compose exec laravel php artisan make:controller ControllerName -# Composer -docker compose exec laravel composer install -docker compose exec laravel composer require package-name +# Cache leeren +docker compose exec laravel php artisan cache:clear +docker compose exec laravel php artisan config:clear -# NPM -docker compose exec laravel npm install -docker compose exec laravel npm run dev -docker compose exec laravel npm run build +# Queue Worker starten +docker compose exec laravel php artisan queue:work # Tinker (Laravel REPL) docker compose exec laravel php artisan tinker ``` ---- +### Asset Compilation -## 🗄️ Datenbank - -### Schema - -Die Gateway-Datenbank enthält folgende Tabellen: - -``` -users → Gateway API Users -api_keys → Virtual Keys -usage_logs → Request Tracking -budgets → Budget Definitions -budget_reset_logs → Budget History -model_pricing → Model Cost Configuration -admins → Laravel Admin Users (neu) -``` - -### Zugriff - -**Via Adminer Web-Interface:** -- http://localhost:8081 -- Login mit oben genannten Credentials - -**Via CLI:** ```bash -docker compose exec postgres psql -U gateway -d gateway +# Development (mit Hot Reload) +docker compose exec laravel npm run dev -# Beispiel-Queries -SELECT * FROM users; -SELECT * FROM usage_logs ORDER BY timestamp DESC LIMIT 10; +# Production Build +docker compose exec laravel npm run build ``` --- ## 🛠️ Entwicklung -### Laravel Development +### Admin-Interface Features -```bash -# In Laravel Container einloggen -docker compose exec laravel bash +Das Admin-Interface unter http://localhost:80 bietet: -# Routes anzeigen -php artisan route:list +1. **Dashboard** + - Übersicht über Nutzungsstatistiken + - Kosten-Trends (Chart.js) + - Provider-Verteilung + - Aktive Benutzer -# Model erstellen -php artisan make:model MyModel -m +2. **Gateway Users** + - Benutzerverwaltung + - API-Key Management + - Pro-User Provider-Credentials -# Controller erstellen -php artisan make:controller MyController --resource +3. **API Keys** + - Virtual Keys erstellen/löschen + - Key-Testing + - Nutzungsstatistiken -# Livewire Component erstellen -php artisan make:livewire MyComponent -``` +4. **Budgets** + - Budget-Limits definieren + - Reset-Zeiträume (täglich/wöchentlich/monatlich) + - Benachrichtigungen -### Frontend Development +5. **Usage Logs** + - Request-Historie + - Filter & Export (CSV) + - Kosten-Analyse -```bash -# NPM Dev Server (mit Hot Reload) -docker compose exec laravel npm run dev +6. **Model Pricing** + - Preis-Konfiguration + - Dynamische Model-Liste + - Kosten-Rechner -# Production Build -docker compose exec laravel npm run build +### LLM Provider Services -# Tailwind JIT Mode -# → Läuft automatisch mit npm run dev -``` +Das System unterstützt folgende Provider mit dynamischer Model-Discovery: + +- **OpenAI**: GPT-3.5, GPT-4, GPT-4 Turbo +- **Anthropic**: Claude 3 (Haiku, Sonnet, Opus) +- **DeepSeek**: DeepSeek Chat Modelle +- **Google Gemini**: Gemini Pro, Flash +- **Mistral AI**: Mistral Large, Medium, Small + +Alle Provider-Services befinden sich in `app/Services/LLM/`. --- -## 📝 Nächste Schritte +## 🔐 Sicherheit -### 1. Models erstellen +### Production Checklist -Folge dem Implementierungskonzept in `LARAVEL_IMPLEMENTATION.md`: +Vor dem Production-Einsatz: -```bash -docker compose exec laravel php artisan make:model GatewayUser -docker compose exec laravel php artisan make:model ApiKey -docker compose exec laravel php artisan make:model UsageLog -docker compose exec laravel php artisan make:model Budget -docker compose exec laravel php artisan make:model ModelPricing -``` +1. ✅ `.env` Konfiguration: + ```env + APP_ENV=production + APP_DEBUG=false + APP_KEY=... (sicher generieren!) + ``` -### 2. Controllers implementieren +2. ✅ Admin-Passwort ändern -```bash -docker compose exec laravel php artisan make:controller DashboardController -docker compose exec laravel php artisan make:controller GatewayUserController --resource -``` +3. ✅ MariaDB Root-Passwort ändern -### 3. Views erstellen +4. ✅ phpMyAdmin deaktivieren oder absichern -Die Views werden in `laravel-app/resources/views/` erstellt. +5. ✅ SSL/TLS einrichten -Struktur: -``` -resources/views/ -├── layouts/ -│ ├── app.blade.php -│ └── navigation.blade.php -├── dashboard.blade.php -├── gateway-users/ -│ ├── index.blade.php -│ ├── show.blade.php -│ └── ... -└── ... -``` +6. ✅ Laravel Caches aktivieren: + ```bash + php artisan config:cache + php artisan route:cache + php artisan view:cache + php artisan optimize + ``` + +### API-Key Verschlüsselung + +Provider API-Keys werden verschlüsselt in der Datenbank gespeichert: +- Verwendung von Laravel's Encryption +- Basiert auf APP_KEY +- Automatische Ver-/Entschlüsselung + +--- + +## 📊 Monitoring & Analytics + +### Dashboard Metriken + +- Gesamte Requests +- Token-Nutzung (Input/Output) +- Gesamtkosten +- Requests pro Provider +- Top-Nutzer +- Kosten-Trends (Chart.js Visualisierung) + +### Export Funktionen + +Usage Logs können als CSV exportiert werden mit: +- Zeitraum-Filter +- Provider-Filter +- Benutzer-Filter +- Kosten-Zusammenfassung --- @@ -259,12 +292,23 @@ resources/views/ ```bash # Logs prüfen docker compose logs laravel +docker compose logs mariadb # Container neu bauen docker compose down docker compose up -d --build ``` +### Datenbank Connection Fehler + +```bash +# MariaDB Status prüfen +docker compose ps mariadb + +# Connection testen +docker compose exec laravel php artisan migrate:status +``` + ### Permissions Fehler ```bash @@ -272,25 +316,6 @@ docker compose up -d --build docker compose exec laravel chmod -R 777 storage bootstrap/cache ``` -### Port bereits belegt - -Ports in `docker-compose.yml` anpassen: -```yaml -laravel: - ports: - - "8001:80" # Statt 80:80 -``` - -### Datenbank Connection Fehler - -```bash -# Prüfe ob PostgreSQL läuft -docker compose ps postgres - -# Teste Connection -docker compose exec laravel php artisan migrate:status -``` - ### Assets werden nicht geladen ```bash @@ -303,69 +328,50 @@ docker compose exec laravel php artisan storage:link --- -## 🔐 Sicherheit +## 📚 Technologie-Stack -### Production Checklist - -Vor dem Production-Deployment: - -1. ✅ `.env` Werte ändern: - ``` - APP_ENV=production - APP_DEBUG=false - APP_KEY=... (neu generieren!) - ``` - -2. ✅ Starkes Admin-Passwort setzen - -3. ✅ PostgreSQL Passwort ändern - -4. ✅ Adminer auf localhost beschränken oder deaktivieren - -5. ✅ SSL/TLS einrichten (Let's Encrypt) - -6. ✅ Laravel Caches aktivieren: - ```bash - php artisan config:cache - php artisan route:cache - php artisan view:cache - ``` +- **Framework**: Laravel 11.x +- **Frontend**: Livewire 3.x + Tailwind CSS 3.x +- **Datenbank**: MariaDB 11.4 +- **Webserver**: Nginx + PHP-FPM 8.3 +- **Charts**: Chart.js +- **Container**: Docker + Docker Compose --- -## 📚 Dokumentation +## 🔗 Nützliche Links -- **Implementierungskonzept**: `LARAVEL_IMPLEMENTATION.md` -- **Any-LLM Gateway**: https://github.com/mozilla-ai/any-llm - **Laravel Docs**: https://laravel.com/docs - **Livewire Docs**: https://livewire.laravel.com - **Tailwind CSS**: https://tailwindcss.com +- **Chart.js**: https://www.chartjs.org --- -## 🆘 Support +## 📝 System-Status -Bei Fragen oder Problemen: +### ✅ Implementiert -1. Logs prüfen: `docker compose logs -f` -2. Container Status: `docker compose ps` -3. Implementierungskonzept lesen: `LARAVEL_IMPLEMENTATION.md` +- [x] Docker-Setup (Laravel + MariaDB + phpMyAdmin) +- [x] Admin-Authentifizierung +- [x] Gateway User Management +- [x] API Key Management mit Testing +- [x] Budget-System mit Limits +- [x] Usage Logs mit CSV Export +- [x] Model Pricing Management +- [x] Dashboard mit Statistiken +- [x] Multi-Provider Support (5 Provider) +- [x] Verschlüsselte Credential-Speicherung +- [x] Dynamische Model-Discovery + +### 🚧 Geplant + +- [ ] API Gateway Endpoints (OpenAI-kompatibel) +- [ ] Rate Limiting Implementation +- [ ] Email-Benachrichtigungen +- [ ] Erweiterte Analytics +- [ ] API-Dokumentation --- -## 📋 Checkliste - -- [ ] Setup Script ausgeführt -- [ ] Laravel läuft auf http://localhost:80 -- [ ] Admin-Login funktioniert -- [ ] Adminer erreichbar (http://localhost:8081) -- [ ] Gateway API funktioniert (http://localhost:8000) -- [ ] Models erstellt (siehe Implementierungskonzept) -- [ ] Controllers implementiert -- [ ] Dashboard Views erstellt -- [ ] Statistiken funktionieren -- [ ] Production-ready gemacht - ---- - -**Viel Erfolg mit der Entwicklung! 🚀** +**Entwickelt von Wilfried Trinkl | Laravel LLM Gateway** 🚀 \ No newline at end of file diff --git a/config.yml b/config.yml deleted file mode 100644 index 1271afe..0000000 --- a/config.yml +++ /dev/null @@ -1,40 +0,0 @@ -# Configuration for any-llm-gateway - -# Database connection URL -database_url: "postgresql://gateway:gateway@postgres:5432/gateway" - -# Server configuration - if you want to use a different port, change it here and in the docker-compose.yml file. -host: "0.0.0.0" -port: 8000 - -# Master key for protecting key macnagement endpoints -master_key: bdab4b5261d6e6ed7173c999ababd7c66066d76d3a06c8506a880ecdcfb41bcd - -# Pre-configured provider credentials -providers: - # Uncomment and add your API keys for the providers you want to use: - - openai: - api_key: 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 - api_base: "https://api.openai.com/v1" # optional - - anthropic: - api_key: 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 - - # mistral: - # api_key: YOUR_MISTRAL_API_KEY_HERE - - # Add more providers as needed... - -# Model pricing configuration (optional but necessary for price tracking) -# Format: "provider:model" -> input/output price per million tokens -# Database pricing takes precedence - config only sets initial values -# Prices are in USD per million tokens -pricing: {} - # See https://cloud.google.com/vertex-ai/generative-ai/pricing - # vertexai:Qwen3-235B-A22B-Instruct-2507: - # input_price_per_million: 0.25 - # output_price_per_million: 1.00 - # openai:gpt-5: - # input_price_per_million: 0.25 - # output_price_per_million: 1.00 diff --git a/docker-compose.yml b/docker-compose.yml index 165d776..bbd13c7 100644 --- a/docker-compose.yml +++ b/docker-compose.yml @@ -1,57 +1,29 @@ services: - gateway: - # if pulling from ghcr.io, use the following instead, and comment out the build section: - image: ghcr.io/mozilla-ai/any-llm/gateway:latest - # build: - # context: .. - # dockerfile: docker/Dockerfile - # args: - # VERSION: ${VERSION:-0.0.0+local} - # If you want to use a different port, change it here and in the config.yml file. - ports: - - "8000:8000" - volumes: - - ./config.yml:/app/config.yml - command: ["any-llm-gateway", "serve", "--config", "/app/config.yml"] - depends_on: - postgres: - condition: service_healthy - restart: unless-stopped - networks: - - any-llm-network - - postgres: - image: postgres:16-alpine + # MariaDB Database + mariadb: + container_name: laravel-llm-mariadb + image: mariadb:11.4 environment: - - POSTGRES_USER=gateway - - POSTGRES_PASSWORD=gateway - - POSTGRES_DB=gateway + - MYSQL_ROOT_PASSWORD=rootpass + - MYSQL_DATABASE=gateway + - MYSQL_USER=gateway + - MYSQL_PASSWORD=gateway volumes: - - postgres_data:/var/lib/postgresql/data + - mariadb_data:/var/lib/mysql + ports: + - "3306:3306" healthcheck: - test: ["CMD-SHELL", "pg_isready -U gateway"] + test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"] interval: 10s timeout: 5s retries: 5 restart: unless-stopped networks: - - any-llm-network - - web: - image: nginx:alpine - ports: - - "8080:80" - volumes: - - ./web:/usr/share/nginx/html:ro - - ./web/default.conf:/etc/nginx/conf.d/default.conf:ro - depends_on: - - gateway - restart: unless-stopped - networks: - - any-llm-network + - laravel-llm-network # Laravel Admin Panel laravel: + container_name: laravel-llm-app build: context: ./laravel dockerfile: Dockerfile @@ -63,36 +35,41 @@ services: - APP_ENV=local - APP_DEBUG=true - APP_KEY=base64:dXFQ1q9f0T9fNZGde+9h/JOsaBPPmGv5qzA87b9FQnQ= - - DB_CONNECTION=pgsql - - DB_HOST=postgres - - DB_PORT=5432 + - DB_CONNECTION=mysql + - DB_HOST=mariadb + - DB_PORT=3306 - DB_DATABASE=gateway - DB_USERNAME=gateway - DB_PASSWORD=gateway depends_on: - postgres: + mariadb: condition: service_healthy restart: unless-stopped networks: - - any-llm-network + - laravel-llm-network - # Adminer - Database Management UI - adminer: - image: adminer:latest + # phpMyAdmin - Database Management UI + phpmyadmin: + container_name: laravel-llm-phpmyadmin + image: phpmyadmin:latest ports: - - "8081:8080" + - "8081:80" environment: - - ADMINER_DEFAULT_SERVER=postgres - - ADMINER_DESIGN=dracula + - PMA_HOST=mariadb + - PMA_PORT=3306 + - PMA_USER=root + - PMA_PASSWORD=rootpass + - UPLOAD_LIMIT=300M depends_on: - - postgres + mariadb: + condition: service_healthy restart: unless-stopped networks: - - any-llm-network + - laravel-llm-network volumes: - postgres_data: + mariadb_data: networks: - any-llm-network: + laravel-llm-network: driver: bridge diff --git a/laravel-app/app/Http/Controllers/ApiKeyController.php b/laravel-app/app/Http/Controllers/ApiKeyController.php index ef3152c..652440f 100644 --- a/laravel-app/app/Http/Controllers/ApiKeyController.php +++ b/laravel-app/app/Http/Controllers/ApiKeyController.php @@ -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'); diff --git a/laravel-app/app/Http/Controllers/BudgetController.php b/laravel-app/app/Http/Controllers/BudgetController.php index f67c7ff..c3e1051 100644 --- a/laravel-app/app/Http/Controllers/BudgetController.php +++ b/laravel-app/app/Http/Controllers/BudgetController.php @@ -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() diff --git a/laravel-app/app/Http/Controllers/DashboardController.php b/laravel-app/app/Http/Controllers/DashboardController.php index 4220e33..30cca09 100644 --- a/laravel-app/app/Http/Controllers/DashboardController.php +++ b/laravel-app/app/Http/Controllers/DashboardController.php @@ -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(), + ]); + } } diff --git a/laravel-app/app/Http/Controllers/ModelPricingController.php b/laravel-app/app/Http/Controllers/ModelPricingController.php index 298bc70..466d730 100644 --- a/laravel-app/app/Http/Controllers/ModelPricingController.php +++ b/laravel-app/app/Http/Controllers/ModelPricingController.php @@ -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; + } } diff --git a/laravel-app/app/Models/Admin.php b/laravel-app/app/Models/Admin.php deleted file mode 100644 index e301f72..0000000 --- a/laravel-app/app/Models/Admin.php +++ /dev/null @@ -1,45 +0,0 @@ - - */ - protected $fillable = [ - 'name', - 'email', - 'password', - ]; - - /** - * The attributes that should be hidden for serialization. - * - * @var array - */ - protected $hidden = [ - 'password', - 'remember_token', - ]; - - /** - * Get the attributes that should be cast. - * - * @return array - */ - protected function casts(): array - { - return [ - 'email_verified_at' => 'datetime', - 'password' => 'hashed', - ]; - } -} diff --git a/laravel-app/app/Models/ApiKey.php b/laravel-app/app/Models/ApiKey.php index eda70f2..927df6b 100644 --- a/laravel-app/app/Models/ApiKey.php +++ b/laravel-app/app/Models/ApiKey.php @@ -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'); } } diff --git a/laravel-app/app/Models/Budget.php b/laravel-app/app/Models/Budget.php index b9e0808..f635553 100644 --- a/laravel-app/app/Models/Budget.php +++ b/laravel-app/app/Models/Budget.php @@ -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"; - } } diff --git a/laravel-app/app/Models/GatewayUser.php b/laravel-app/app/Models/GatewayUser.php index b665628..0464110 100644 --- a/laravel-app/app/Models/GatewayUser.php +++ b/laravel-app/app/Models/GatewayUser.php @@ -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 - */ 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 + * 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'); - } } diff --git a/laravel-app/app/Models/ModelPricing.php b/laravel-app/app/Models/ModelPricing.php index a9e4df9..7142c72 100644 --- a/laravel-app/app/Models/ModelPricing.php +++ b/laravel-app/app/Models/ModelPricing.php @@ -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); } } diff --git a/laravel-app/app/Models/UsageLog.php b/laravel-app/app/Models/UsageLog.php index fc3420e..e7da8bc 100644 --- a/laravel-app/app/Models/UsageLog.php +++ b/laravel-app/app/Models/UsageLog.php @@ -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'); } } diff --git a/laravel-app/app/Models/User.php b/laravel-app/app/Models/User.php index 749c7b7..541cebc 100644 --- a/laravel-app/app/Models/User.php +++ b/laravel-app/app/Models/User.php @@ -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); + } } diff --git a/laravel-app/app/Services/StatisticsService.php b/laravel-app/app/Services/StatisticsService.php index b3e72bf..2841c78 100644 --- a/laravel-app/app/Services/StatisticsService.php +++ b/laravel-app/app/Services/StatisticsService.php @@ -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(), + ]; + } } diff --git a/laravel-app/bootstrap/app.php b/laravel-app/bootstrap/app.php index c183276..187f0d4 100644 --- a/laravel-app/bootstrap/app.php +++ b/laravel-app/bootstrap/app.php @@ -7,11 +7,15 @@ use Illuminate\Foundation\Configuration\Middleware; return Application::configure(basePath: dirname(__DIR__)) ->withRouting( web: __DIR__.'/../routes/web.php', + api: __DIR__.'/../routes/api.php', commands: __DIR__.'/../routes/console.php', health: '/up', ) ->withMiddleware(function (Middleware $middleware): void { - // + $middleware->alias([ + 'checkbudget' => \App\Http\Middleware\CheckBudget::class, + 'checkratelimit' => \App\Http\Middleware\CheckRateLimit::class, + ]); }) ->withExceptions(function (Exceptions $exceptions): void { // diff --git a/laravel-app/config/auth.php b/laravel-app/config/auth.php index 4b74b79..a0837e3 100644 --- a/laravel-app/config/auth.php +++ b/laravel-app/config/auth.php @@ -15,7 +15,7 @@ return [ 'defaults' => [ 'guard' => env('AUTH_GUARD', 'web'), - 'passwords' => env('AUTH_PASSWORD_BROKER', 'admins'), + 'passwords' => env('AUTH_PASSWORD_BROKER', 'users'), ], /* @@ -38,7 +38,7 @@ return [ 'guards' => [ 'web' => [ 'driver' => 'session', - 'provider' => 'admins', + 'provider' => 'users', ], ], diff --git a/laravel-app/database/migrations/0001_01_01_000000_create_users_table.php.disabled b/laravel-app/database/migrations/0001_01_01_000000_create_users_table.php.disabled deleted file mode 100644 index 05fb5d9..0000000 --- a/laravel-app/database/migrations/0001_01_01_000000_create_users_table.php.disabled +++ /dev/null @@ -1,49 +0,0 @@ -id(); - $table->string('name'); - $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); - $table->rememberToken(); - $table->timestamps(); - }); - - Schema::create('password_reset_tokens', function (Blueprint $table) { - $table->string('email')->primary(); - $table->string('token'); - $table->timestamp('created_at')->nullable(); - }); - - Schema::create('sessions', function (Blueprint $table) { - $table->string('id')->primary(); - $table->foreignId('user_id')->nullable()->index(); - $table->string('ip_address', 45)->nullable(); - $table->text('user_agent')->nullable(); - $table->longText('payload'); - $table->integer('last_activity')->index(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('users'); - Schema::dropIfExists('password_reset_tokens'); - Schema::dropIfExists('sessions'); - } -}; diff --git a/laravel-app/database/migrations/2025_11_15_192931_create_admins_table.php b/laravel-app/database/migrations/2025_11_15_192931_create_admins_table.php deleted file mode 100644 index 950905e..0000000 --- a/laravel-app/database/migrations/2025_11_15_192931_create_admins_table.php +++ /dev/null @@ -1,26 +0,0 @@ -id(); - $table->string('name'); - $table->string('email')->unique(); - $table->timestamp('email_verified_at')->nullable(); - $table->string('password'); - $table->rememberToken(); - $table->timestamps(); - }); - } - - public function down(): void - { - Schema::dropIfExists('admins'); - } -}; diff --git a/laravel-app/database/migrations/2025_11_15_193000_create_admin_support_tables.php b/laravel-app/database/migrations/2025_11_15_193000_create_admin_support_tables.php deleted file mode 100644 index ea39a6c..0000000 --- a/laravel-app/database/migrations/2025_11_15_193000_create_admin_support_tables.php +++ /dev/null @@ -1,40 +0,0 @@ -string('email')->primary(); - $table->string('token'); - $table->timestamp('created_at')->nullable(); - }); - - // Sessions Tabelle (für admins) - Schema::create('sessions', function (Blueprint $table) { - $table->string('id')->primary(); - $table->foreignId('user_id')->nullable()->index(); // referenziert admins.id - $table->string('ip_address', 45)->nullable(); - $table->text('user_agent')->nullable(); - $table->longText('payload'); - $table->integer('last_activity')->index(); - }); - } - - /** - * Reverse the migrations. - */ - public function down(): void - { - Schema::dropIfExists('password_reset_tokens'); - Schema::dropIfExists('sessions'); - } -}; diff --git a/laravel-app/resources/views/api-keys/index.blade.php b/laravel-app/resources/views/api-keys/index.blade.php index 6f13a04..4ef6e15 100644 --- a/laravel-app/resources/views/api-keys/index.blade.php +++ b/laravel-app/resources/views/api-keys/index.blade.php @@ -183,10 +183,10 @@ {{ $key->created_at->format('Y-m-d H:i') }} - View @if($key->is_active && !$key->is_expired) -
diff --git a/laravel-app/resources/views/dashboard.blade.php b/laravel-app/resources/views/dashboard.blade.php index 139bbc3..18ae2e3 100644 --- a/laravel-app/resources/views/dashboard.blade.php +++ b/laravel-app/resources/views/dashboard.blade.php @@ -1,7 +1,7 @@ -

- {{ __('Dashboard') }} - Any-LLM Gateway +

+ {{ __('Dashboard') }} - LLM Gateway

@@ -11,16 +11,16 @@
-
+
-

Total Users

-

+

Total Users

+

{{ number_format($stats['total_users']) }}

-

- {{ $stats['active_users'] }} active, {{ $stats['blocked_users'] }} blocked +

+ {{ $stats['active_credentials'] }} active credentials

@@ -33,15 +33,15 @@
-
+
-

Requests Today

-

+

Requests Today

+

{{ number_format($stats['total_requests_today']) }}

-

+

{{ number_format($stats['total_requests_month']) }} this month

@@ -55,15 +55,15 @@
-
+
-

Spend Today

-

+

Spend Today

+

${{ number_format($stats['total_spend_today'], 2) }}

-

+

${{ number_format($stats['total_spend_month'], 2) }} this month

@@ -77,16 +77,16 @@
-
+
-

Tokens Today

-

+

Tokens Today

+

{{ number_format($stats['total_tokens_today']) }}

-

- Prompt + Completion +

+ Avg: ${{ number_format($stats['avg_cost_per_request'], 4) }}/req

@@ -100,9 +100,9 @@
-
+
-

+

Usage Trend (Last 30 Days)

@@ -112,9 +112,9 @@
-
+
-

+

Usage by Provider

@@ -122,30 +122,33 @@
-
+
-

+

Top Users by Spend

@forelse($topUsers as $user) -
+
-

- {{ $user->alias ?? $user->user_id }} +

+ {{ $user->name }}

-

- {{ number_format($user->usage_logs_count ?? 0) }} requests +

+ {{ number_format($user->llm_requests_count ?? 0) }} requests

-

- ${{ number_format($user->usage_logs_sum_cost ?? 0, 2) }} +

+ ${{ number_format($user->total_cost ?? 0, 2) }} +

+

+ {{ number_format($user->total_tokens ?? 0) }} tokens

@empty -

+

No usage data yet

@endforelse @@ -155,40 +158,52 @@
-
+
-

+

Most Used Models

- - +
+ - - - - + + + + + - + @forelse($modelStats as $model) - - + - - @empty - @@ -245,12 +260,6 @@ title: { display: true, text: 'Requests' - }, - ticks: { - color: getComputedStyle(document.documentElement).getPropertyValue('--tw-text-opacity') ? '#9CA3AF' : '#6B7280' - }, - grid: { - color: 'rgba(156, 163, 175, 0.1)' } }, y1: { @@ -261,27 +270,9 @@ display: true, text: 'Cost ($)' }, - ticks: { - color: getComputedStyle(document.documentElement).getPropertyValue('--tw-text-opacity') ? '#9CA3AF' : '#6B7280' - }, grid: { drawOnChartArea: false, }, - }, - x: { - ticks: { - color: getComputedStyle(document.documentElement).getPropertyValue('--tw-text-opacity') ? '#9CA3AF' : '#6B7280' - }, - grid: { - color: 'rgba(156, 163, 175, 0.1)' - } - } - }, - plugins: { - legend: { - labels: { - color: getComputedStyle(document.documentElement).getPropertyValue('--tw-text-opacity') ? '#9CA3AF' : '#6B7280' - } } } } @@ -292,19 +283,19 @@ new Chart(providerCtx, { type: 'doughnut', data: { - labels: @json($providerStats->pluck('provider')), + labels: @json($providerStats->pluck('provider')->map(fn($p) => ucfirst($p))), datasets: [{ data: @json($providerStats->pluck('count')), backgroundColor: [ - 'rgba(59, 130, 246, 0.8)', - 'rgba(16, 185, 129, 0.8)', - 'rgba(249, 115, 22, 0.8)', - 'rgba(168, 85, 247, 0.8)', - 'rgba(236, 72, 153, 0.8)', - 'rgba(245, 158, 11, 0.8)', + 'rgba(34, 197, 94, 0.8)', // Green - OpenAI + 'rgba(168, 85, 247, 0.8)', // Purple - Anthropic + 'rgba(59, 130, 246, 0.8)', // Blue - Mistral + 'rgba(251, 191, 36, 0.8)', // Yellow - Gemini + 'rgba(236, 72, 153, 0.8)', // Pink - DeepSeek + 'rgba(249, 115, 22, 0.8)', // Orange ], borderWidth: 2, - borderColor: '#1f2937' + borderColor: '#fff' }] }, options: { @@ -314,7 +305,6 @@ legend: { position: 'bottom', labels: { - color: getComputedStyle(document.documentElement).getPropertyValue('--tw-text-opacity') ? '#9CA3AF' : '#6B7280', padding: 15 } }, @@ -322,11 +312,10 @@ callbacks: { label: function(context) { let label = context.label || ''; - if (label) { - label += ': '; - } - label += context.parsed + ' requests'; - return label; + let value = context.parsed || 0; + let total = context.dataset.data.reduce((a, b) => a + b, 0); + let percentage = ((value / total) * 100).toFixed(1); + return label + ': ' + value + ' requests (' + percentage + '%)'; } } } diff --git a/laravel-app/resources/views/livewire/layout/navigation.blade.php b/laravel-app/resources/views/livewire/layout/navigation.blade.php index bd89fbd..192db4e 100644 --- a/laravel-app/resources/views/livewire/layout/navigation.blade.php +++ b/laravel-app/resources/views/livewire/layout/navigation.blade.php @@ -48,6 +48,12 @@ new class extends Component {{ __('Pricing') }} + + {{ __('Credentials') }} + + + {{ __('User Budgets') }} + @@ -114,6 +120,12 @@ new class extends Component {{ __('Pricing') }} + + {{ __('Credentials') }} + + + {{ __('User Budgets') }} + diff --git a/laravel-app/resources/views/model-pricing/create.blade.php b/laravel-app/resources/views/model-pricing/create.blade.php index 3e45843..f597440 100644 --- a/laravel-app/resources/views/model-pricing/create.blade.php +++ b/laravel-app/resources/views/model-pricing/create.blade.php @@ -9,50 +9,129 @@
- + @csrf +
-
+ +
+ +
+ + +
+ @error('model') +

{{ $message }}

+ @enderror +

Select a provider to load available models

+
+ + + + +
- +
+ $ + +
@error('input_price_per_million')

{{ $message }}

@enderror

Price in USD per 1 million input tokens

-
+ +
- +
+ $ + +
@error('output_price_per_million')

{{ $message }}

@enderror

Price in USD per 1 million output tokens

+ +
+ + + @error('context_window') +

{{ $message }}

+ @enderror +
+ + +
+ + + @error('max_output_tokens') +

{{ $message }}

+ @enderror +
+
+ + @push('scripts') + + @endpush diff --git a/laravel-app/resources/views/model-pricing/index.blade.php b/laravel-app/resources/views/model-pricing/index.blade.php index 97ec264..e048a4b 100644 --- a/laravel-app/resources/views/model-pricing/index.blade.php +++ b/laravel-app/resources/views/model-pricing/index.blade.php @@ -35,6 +35,9 @@
ModelRequestsTokensCostModelProviderRequestsTokensCost
+ {{ $model->model }} + + + {{ ucfirst($model->provider) }} + + {{ number_format($model->count) }} + {{ number_format($model->tokens ?? 0) }} + ${{ number_format($model->total_cost ?? 0, 4) }}
+ No usage data yet
+ @@ -44,6 +47,9 @@ + @@ -53,7 +59,10 @@ @foreach($modelPricing as $model) + +
+ Provider + Model Output Price + Status + Actions
- {{ $model->model_key }} + {{ $model->provider }} + + {{ $model->model }} {{ $model->input_price_formatted }} @@ -61,10 +70,21 @@ {{ $model->output_price_formatted }} + @if($model->is_active) + + Active + + @else + + Inactive + + @endif + - Edit - @csrf diff --git a/laravel-app/routes/console.php b/laravel-app/routes/console.php index 3c9adf1..67d4428 100644 --- a/laravel-app/routes/console.php +++ b/laravel-app/routes/console.php @@ -2,7 +2,15 @@ use Illuminate\Foundation\Inspiring; use Illuminate\Support\Facades\Artisan; +use Illuminate\Support\Facades\Schedule; +use App\Jobs\ResetDailyBudgets; +use App\Jobs\ResetMonthlyBudgets; Artisan::command('inspire', function () { $this->comment(Inspiring::quote()); })->purpose('Display an inspiring quote'); + +// Schedule budget reset jobs +Schedule::job(new ResetDailyBudgets)->dailyAt('00:00')->name('reset-daily-budgets'); +Schedule::job(new ResetMonthlyBudgets)->monthlyOn(1, '00:00')->name('reset-monthly-budgets'); + diff --git a/laravel-app/routes/web.php b/laravel-app/routes/web.php index eb75f79..a72d31f 100644 --- a/laravel-app/routes/web.php +++ b/laravel-app/routes/web.php @@ -7,12 +7,16 @@ use App\Http\Controllers\ApiKeyController; use App\Http\Controllers\BudgetController; use App\Http\Controllers\UsageLogController; use App\Http\Controllers\ModelPricingController; +use App\Http\Controllers\Admin\CredentialController; +use App\Http\Controllers\Admin\UserBudgetController; +use App\Http\Controllers\Admin\UserManagementController; Route::view('/', 'welcome'); Route::middleware(['auth', 'verified'])->group(function () { // Dashboard Route::get('dashboard', [DashboardController::class, 'index'])->name('dashboard'); + Route::get('dashboard/realtime-stats', [DashboardController::class, 'realtimeStats'])->name('dashboard.realtime-stats'); // Gateway Users Management Route::resource('gateway-users', GatewayUserController::class); @@ -41,6 +45,33 @@ Route::middleware(['auth', 'verified'])->group(function () { Route::post('model-pricing-calculate', [ModelPricingController::class, 'calculate'])->name('model-pricing.calculate'); Route::get('model-pricing-import', [ModelPricingController::class, 'importForm'])->name('model-pricing.import-form'); Route::post('model-pricing-import', [ModelPricingController::class, 'import'])->name('model-pricing.import'); + Route::get('api/provider-models/{provider}', [ModelPricingController::class, 'getProviderModels'])->name('api.provider-models'); + + // Provider Credentials Management (Admin) + Route::prefix('admin')->name('admin.')->group(function () { + // User Management + Route::get('users', [UserManagementController::class, 'index']) + ->name('users.index'); + + // Credentials + Route::resource('credentials', CredentialController::class); + Route::post('credentials/{credential}/test', [CredentialController::class, 'test']) + ->name('credentials.test'); + Route::post('credentials/{credential}/toggle', [CredentialController::class, 'toggle']) + ->name('credentials.toggle'); + + // User Budget & Rate Limit Management + Route::get('users/{user}/budget', [UserBudgetController::class, 'show']) + ->name('users.budget.show'); + Route::put('users/{user}/budget', [UserBudgetController::class, 'updateBudget']) + ->name('users.budget.update'); + Route::put('users/{user}/rate-limit', [UserBudgetController::class, 'updateRateLimit']) + ->name('users.rate-limit.update'); + Route::post('users/{user}/rate-limit/reset', [UserBudgetController::class, 'resetRateLimit']) + ->name('users.rate-limit.reset'); + Route::post('users/{user}/budget/reset', [UserBudgetController::class, 'resetBudget']) + ->name('users.budget.reset'); + }); }); Route::view('profile', 'profile') diff --git a/laravel/Dockerfile b/laravel/Dockerfile index a24a9eb..c207e49 100644 --- a/laravel/Dockerfile +++ b/laravel/Dockerfile @@ -21,7 +21,7 @@ RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \ RUN apt-get clean && rm -rf /var/lib/apt/lists/* # Install PHP extensions -RUN docker-php-ext-install pdo pdo_pgsql pgsql mbstring exif pcntl bcmath gd +RUN docker-php-ext-install pdo pdo_mysql pdo_pgsql pgsql mysqli mbstring exif pcntl bcmath gd # Get latest Composer COPY --from=composer:latest /usr/bin/composer /usr/bin/composer diff --git a/setup-laravel.sh b/setup-laravel.sh deleted file mode 100755 index 81dae8d..0000000 --- a/setup-laravel.sh +++ /dev/null @@ -1,190 +0,0 @@ -#!/bin/bash - -set -e - -echo "🚀 Any-LLM Laravel Setup" -echo "========================" -echo "" - -# Farben für Output -GREEN='\033[0;32m' -BLUE='\033[0;34m' -RED='\033[0;31m' -NC='\033[0m' # No Color - -# Check if Docker is running -if ! docker info > /dev/null 2>&1; then - echo -e "${RED}❌ Docker ist nicht gestartet!${NC}" - exit 1 -fi - -echo -e "${BLUE}📦 Schritt 1: Laravel Projekt erstellen${NC}" -if [ ! -d "laravel-app" ]; then - echo "Laravel wird in ./laravel-app installiert..." - docker run --rm -v $(pwd):/app composer create-project laravel/laravel laravel-app - echo -e "${GREEN}✅ Laravel installiert${NC}" -else - echo -e "${GREEN}✅ Laravel-App existiert bereits${NC}" -fi - -echo "" -echo -e "${BLUE}📦 Schritt 2: Laravel Packages installieren${NC}" -cd laravel-app - -# Composer packages installieren -echo "Installiere Livewire und Breeze..." -docker run --rm -v $(pwd):/app composer require livewire/livewire -docker run --rm -v $(pwd):/app composer require laravel/breeze --dev - -echo -e "${GREEN}✅ Packages installiert${NC}" - -# Zurück zum Hauptverzeichnis -cd .. - -echo "" -echo -e "${BLUE}📦 Schritt 3: Docker Container starten${NC}" -docker compose up -d --build - -echo "Warte auf Container..." -sleep 10 - -echo -e "${GREEN}✅ Container gestartet${NC}" - -echo "" -echo -e "${BLUE}📦 Schritt 4: Laravel konfigurieren${NC}" - -# Breeze installieren -echo "Installiere Laravel Breeze..." -docker compose exec laravel php artisan breeze:install livewire --dark - -# APP_KEY generieren -echo "Generiere APP_KEY..." -docker compose exec laravel php artisan key:generate - -# Storage Link erstellen -echo "Erstelle Storage Link..." -docker compose exec laravel php artisan storage:link - -echo -e "${GREEN}✅ Laravel konfiguriert${NC}" - -echo "" -echo -e "${BLUE}📦 Schritt 5: Admin Migration erstellen${NC}" - -# Admin Migration erstellen -docker compose exec laravel php artisan make:migration create_admins_table - -# Migration File Content erstellen -MIGRATION_FILE=$(docker compose exec laravel bash -c "ls -t database/migrations/*create_admins_table.php | head -1" | tr -d '\r') - -docker compose exec laravel bash -c "cat > $MIGRATION_FILE << 'EOF' -id(); - \$table->string('name'); - \$table->string('email')->unique(); - \$table->timestamp('email_verified_at')->nullable(); - \$table->string('password'); - \$table->rememberToken(); - \$table->timestamps(); - }); - - Schema::create('password_reset_tokens', function (Blueprint $table) { - \$table->string('email')->primary(); - \$table->string('token'); - \$table->timestamp('created_at')->nullable(); - }); - - Schema::create('sessions', function (Blueprint $table) { - \$table->string('id')->primary(); - \$table->foreignId('user_id')->nullable()->index(); - \$table->string('ip_address', 45)->nullable(); - \$table->text('user_agent')->nullable(); - \$table->longText('payload'); - \$table->integer('last_activity')->index(); - }); - } - - public function down(): void - { - Schema::dropIfExists('admins'); - Schema::dropIfExists('password_reset_tokens'); - Schema::dropIfExists('sessions'); - } -}; -EOF" - -# Migration ausführen -echo "Führe Migrationen aus..." -docker compose exec laravel php artisan migrate --force - -echo -e "${GREEN}✅ Migrationen abgeschlossen${NC}" - -echo "" -echo -e "${BLUE}📦 Schritt 6: Admin User erstellen${NC}" - -# Admin User erstellen -docker compose exec laravel php artisan tinker --execute=" -\App\Models\User::create([ - 'name' => 'Admin', - 'email' => 'admin@example.com', - 'password' => bcrypt('password123') -]); -echo 'Admin User erstellt: admin@example.com / password123'; -" - -echo -e "${GREEN}✅ Admin User erstellt${NC}" - -echo "" -echo -e "${BLUE}📦 Schritt 7: Assets kompilieren${NC}" - -# NPM installieren und Assets kompilieren -echo "Installiere NPM Packages..." -docker compose exec laravel npm install - -echo "Kompiliere Assets..." -docker compose exec laravel npm run build - -echo -e "${GREEN}✅ Assets kompiliert${NC}" - -echo "" -echo "==========================================" -echo -e "${GREEN}✅ Setup abgeschlossen!${NC}" -echo "==========================================" -echo "" -echo "📍 URLs:" -echo " - Laravel Admin: http://localhost:80" -echo " - Gateway API: http://localhost:8000" -echo " - Gateway Tester: http://localhost:8080" -echo " - Adminer (DB): http://localhost:8081" -echo "" -echo "🔐 Admin Login:" -echo " Email: admin@example.com" -echo " Password: password123" -echo "" -echo "🗄️ Adminer Login:" -echo " System: PostgreSQL" -echo " Server: postgres" -echo " Username: gateway" -echo " Password: gateway" -echo " Database: gateway" -echo "" -echo "📝 Nächste Schritte:" -echo " 1. Öffne http://localhost:80 im Browser" -echo " 2. Melde dich mit admin@example.com / password123 an" -echo " 3. Beginne mit der Entwicklung!" -echo "" -echo "🛑 Container stoppen:" -echo " docker compose down" -echo "" -echo "🔄 Container neu starten:" -echo " docker compose up -d" -echo "" diff --git a/web/default.conf b/web/default.conf deleted file mode 100644 index be89b5d..0000000 --- a/web/default.conf +++ /dev/null @@ -1,34 +0,0 @@ -server { - listen 80; - server_name localhost; - - root /usr/share/nginx/html; - index index.html; - - location / { - try_files $uri $uri/ =404; - add_header Cache-Control "no-cache, no-store, must-revalidate"; - } - - # Proxy für Gateway-Anfragen (um CORS zu vermeiden) - location /api/ { - proxy_pass http://gateway:8000/; - proxy_http_version 1.1; - proxy_set_header Upgrade $http_upgrade; - proxy_set_header Connection 'upgrade'; - proxy_set_header Host $host; - proxy_cache_bypass $http_upgrade; - proxy_set_header X-Real-IP $remote_addr; - proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for; - proxy_set_header X-Forwarded-Proto $scheme; - - # CORS headers - add_header Access-Control-Allow-Origin * always; - add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS' always; - add_header Access-Control-Allow-Headers 'Content-Type, Authorization, X-Master-Key' always; - - if ($request_method = 'OPTIONS') { - return 204; - } - } -} diff --git a/web/index.html b/web/index.html deleted file mode 100644 index 38b0864..0000000 --- a/web/index.html +++ /dev/null @@ -1,428 +0,0 @@ - - - - - - Any-LLM Gateway Tester - - - -
-

🚀 Any-LLM Gateway Tester

- -
-

ℹ️ Gateway Info

-

Gateway URL: /api

-

Master Key: bdab4b...bcd (aus config.yml)

-

Virtual Key für test-user-1: gw-H9xo...ziAQ

-

Authentifizierung: X-AnyLLM-Key: Bearer KEY

-

Hinweis: Anthropic erfordert Virtual Keys! Master Key funktioniert nur mit OpenAI.

-
- - - -
-

👤 User Management

-

Erstelle zuerst einen User, um Requests zu tracken.

- -
- - -
- -
- - -
- - - - - -
- - -
-

💬 Chat Completion Test

- -
- - -
- -
- - -
- -
- - -
- -
- - -
- -
- -
- - - - -
-
- - - - -