From c149bdbdde3b994afec8615c0d045e76f4651d53 Mon Sep 17 00:00:00 2001 From: wtrinkl Date: Tue, 18 Nov 2025 23:42:29 +0100 Subject: [PATCH] Architektur-Analyse und Korrektur-Konzept MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Umfassende Analyse der aktuellen Implementierung durchgeführt - Identifiziert: Zwei getrennte User-Systeme (users vs gateway_users) - Problem: API verwendet falsche Tabelle (users statt gateway_users) - Lösung: Kompletter Implementierungsplan für korrekte Architektur - Dokument: ARCHITEKTUR.md mit 6-Tage-Umsetzungsplan erstellt - Enthält: Custom API-Key Guard, Gateway-User-Credentials, Budget-System - Swagger/Scramble Paket hinzugefügt (für spätere API-Dokumentation) Status: Bereit für Implementierung (Start: Tag 1 - Datenbank & Models) --- ARCHITEKTUR.md | 1167 +++++++++++++++++ .../Api/ChatCompletionController.php | 14 +- laravel-app/composer.json | 1 + laravel-app/composer.lock | 310 ++++- laravel-app/config/scramble.php | 136 ++ 5 files changed, 1565 insertions(+), 63 deletions(-) create mode 100644 ARCHITEKTUR.md create mode 100644 laravel-app/config/scramble.php diff --git a/ARCHITEKTUR.md b/ARCHITEKTUR.md new file mode 100644 index 0000000..6a574ac --- /dev/null +++ b/ARCHITEKTUR.md @@ -0,0 +1,1167 @@ +# Laravel LLM Gateway - Korrekte Architektur-Umsetzung + +**Datum:** 2025-11-18 +**Status:** 🎯 Klarstellung der Architektur-Vision + +--- + +## 🏗️ Architektur-Konzept (KORREKT) + +### Zwei getrennte User-Typen: + +#### 1. **Admins** (`users` Tabelle) +- **Zweck:** Verwaltung des Gateway-Systems +- **Zugriff:** Laravel Web-Interface (Session-basiert) +- **Funktionen:** + - Gateway-Users verwalten + - Budgets konfigurieren + - Usage-Logs einsehen + - Model-Pricing pflegen + - Provider-Credentials für Gateway-Users konfigurieren + +#### 2. **Gateway-Users** (`gateway_users` Tabelle) +- **Zweck:** API-Clients (externe Anwendungen) +- **Zugriff:** REST API via API-Keys +- **Funktionen:** + - Chat-Completions anfragen + - Antworten von LLM-Providern erhalten + - Innerhalb ihres Budgets arbeiten + +**Das ist eine saubere Trennung - so sollte es sein!** ✅ + +--- + +## 🔴 Aktuelles Problem + +Die API-Implementation verwendet die **falschen Tabellen**: + +### Was der Code aktuell macht: +```php +// ChatCompletionController.php +Route::middleware('auth:sanctum')->group(function () { + Route::post('/chat/completions', ...); +}); + +// Verwendet: +$user = $request->user(); // ← Aus 'users' Tabelle (Admins!) +$credential = UserProviderCredential::where('user_id', $user->id)->first(); +``` + +### Was der Code machen sollte: +```php +// API-Key Authentication für gateway_users +Route::middleware('auth:api')->group(function () { + Route::post('/chat/completions', ...); +}); + +// Sollte verwenden: +$gatewayUser = $request->user(); // ← Aus 'gateway_users' Tabelle +$credential = GatewayUserCredential::where('gateway_user_id', $gatewayUser->id)->first(); +``` + +--- + + +## 📋 Umsetzungsplan + +### Phase 1: Datenbank-Struktur anpassen + +#### 1.1 Neue Migration: Provider-Credentials für Gateway-Users + +**Aktuell:** `user_provider_credentials` hat Foreign Key zu `users` + +**Neu:** Credential-Tabelle für `gateway_users` erstellen + +```php +// Migration: create_gateway_user_credentials_table.php +Schema::create('gateway_user_credentials', function (Blueprint $table) { + $table->id(); + $table->string('gateway_user_id'); // ← Foreign Key zu gateway_users + $table->string('provider'); // openai, anthropic, google, deepseek, mistral + $table->text('api_key'); // Verschlüsselt + $table->string('organization_id')->nullable(); + $table->boolean('is_active')->default(true); + $table->timestamp('last_used_at')->nullable(); + $table->timestamp('last_tested_at')->nullable(); + $table->string('test_status')->nullable(); // success, failed + $table->text('test_error')->nullable(); + $table->timestamps(); + + $table->foreign('gateway_user_id') + ->references('user_id') + ->on('gateway_users') + ->onDelete('cascade'); + + $table->unique(['gateway_user_id', 'provider']); + $table->index('is_active'); +}); +``` + +#### 1.2 Budget-Tabelle vereinheitlichen + +**Entscheidung:** Welche Tabelle behalten? + +**Option A:** `budgets` Tabelle behalten (aktuell für gateway_users) +```sql +-- budgets Tabelle hat bereits: +- id, name, monthly_limit, current_spending, alert_threshold +- Keine direkte User-Zuordnung (wird über gateway_users.budget_id verknüpft) +``` + +**Option B:** Direkte Budget-Felder in `gateway_users` +```sql +ALTER TABLE gateway_users ADD COLUMN monthly_budget_limit DECIMAL(10,2); +ALTER TABLE gateway_users ADD COLUMN current_month_spending DECIMAL(10,2); +ALTER TABLE gateway_users ADD COLUMN budget_alert_threshold INT; +``` + +**Empfehlung:** Option B - Vereinfachung +- Direkter Zugriff auf Budget pro User +- Weniger Joins +- Einfacheres Datenmodell + +#### 1.3 Usage-Logging konsolidieren + +**Aktuell:** Zwei Tabellen: +- `llm_requests` (von API-Code genutzt) +- `usage_logs` (im Admin-Interface angezeigt) + +**Neu:** Nur `usage_logs` verwenden, aber anpassen: +```php +Schema::table('usage_logs', function (Blueprint $table) { + // Stelle sicher dass alle Felder vorhanden sind: + // user_id → gateway_user_id umbenennen + $table->renameColumn('user_id', 'gateway_user_id'); + + // Falls fehlt: + if (!Schema::hasColumn('usage_logs', 'request_payload')) { + $table->json('request_payload')->nullable(); + } + if (!Schema::hasColumn('usage_logs', 'response_payload')) { + $table->json('response_payload')->nullable(); + } + if (!Schema::hasColumn('usage_logs', 'ip_address')) { + $table->string('ip_address')->nullable(); + } + if (!Schema::hasColumn('usage_logs', 'user_agent')) { + $table->string('user_agent')->nullable(); + } +}); +``` + +--- + +### Phase 2: API-Key Authentication System + +#### 2.1 Custom Guard für API-Keys + +Laravel kann mit Custom Guards arbeiten. Wir erstellen einen `api-key` Guard: + +**config/auth.php:** +```php +'guards' => [ + 'web' => [ + 'driver' => 'session', + 'provider' => 'users', // ← Für Admins + ], + + 'api' => [ + 'driver' => 'api-key', // ← Custom Guard + 'provider' => 'gateway_users', + ], +], + +'providers' => [ + 'users' => [ + 'driver' => 'eloquent', + 'model' => App\Models\User::class, + ], + + 'gateway_users' => [ + 'driver' => 'eloquent', + 'model' => App\Models\GatewayUser::class, + ], +], +``` + + +#### 2.2 API-Key Guard Implementation + +**Neue Datei: `app/Auth/ApiKeyGuard.php`** +```php +provider = $provider; + $this->request = $request; + } + + public function user() + { + if ($this->user !== null) { + return $this->user; + } + + // Get API key from header: Authorization: Bearer sk-xxx + $apiKey = $this->request->bearerToken(); + + if (!$apiKey) { + return null; + } + + // Find API key in database + $keyRecord = ApiKey::where('key_prefix', substr($apiKey, 0, 10)) + ->where('is_active', true) + ->first(); + + if (!$keyRecord) { + return null; + } + + // Verify full key hash + if (!Hash::check($apiKey, $keyRecord->key_hash)) { + return null; + } + + // Update last used timestamp + $keyRecord->update(['last_used_at' => now()]); + + // Return the gateway user + $this->user = GatewayUser::find($keyRecord->gateway_user_id); + + return $this->user; + } + + public function validate(array $credentials = []) + { + return $this->user() !== null; + } +} +``` + +#### 2.3 Service Provider registrieren + +**app/Providers/AuthServiceProvider.php:** +```php +use App\Auth\ApiKeyGuard; +use Illuminate\Support\Facades\Auth; + +public function boot(): void +{ + Auth::extend('api-key', function ($app, $name, array $config) { + return new ApiKeyGuard( + Auth::createUserProvider($config['provider']), + $app['request'] + ); + }); +} +``` + +--- + +### Phase 3: API-Code anpassen + +#### 3.1 Routes aktualisieren + +**routes/api.php:** +```php +use App\Http\Controllers\Api\ChatCompletionController; + +// Gateway API - verwendet 'api' guard (API-Keys für gateway_users) +Route::middleware('auth:api')->group(function () { + Route::post('/chat/completions', [ChatCompletionController::class, 'create']) + ->middleware(['checkbudget', 'checkratelimit']); + + Route::get('/user', function (Request $request) { + return $request->user(); // Gibt GatewayUser zurück + }); +}); +``` + + +#### 3.2 GatewayUser Model erweitern + +**app/Models/GatewayUser.php:** +```php + 'boolean', + 'metadata' => 'json', + 'monthly_budget_limit' => 'decimal:2', + 'current_month_spending' => 'decimal:2', + ]; + + // Relations + public function apiKeys() + { + return $this->hasMany(ApiKey::class, 'gateway_user_id', 'user_id'); + } + + public function credentials() + { + return $this->hasMany(GatewayUserCredential::class, 'gateway_user_id', 'user_id'); + } + + public function usageLogs() + { + return $this->hasMany(UsageLog::class, 'gateway_user_id', 'user_id'); + } + + // Helper methods + public function isBlocked(): bool + { + return $this->blocked; + } + + public function hasExceededBudget(): bool + { + if (!$this->monthly_budget_limit) { + return false; + } + return $this->current_month_spending >= $this->monthly_budget_limit; + } + + public function incrementSpending(float $amount): void + { + $this->increment('current_month_spending', $amount); + } + + public function resetMonthlySpending(): void + { + $this->update(['current_month_spending' => 0]); + } +} +``` + +#### 3.3 Neues Model: GatewayUserCredential + +**app/Models/GatewayUserCredential.php:** +```php + 'boolean', + 'last_used_at' => 'datetime', + 'last_tested_at' => 'datetime', + ]; + + // Automatic encryption + public function setApiKeyAttribute($value): void + { + $this->attributes['api_key'] = Crypt::encryptString($value); + } + + public function getApiKeyAttribute($value): string + { + return Crypt::decryptString($value); + } + + public function gatewayUser() + { + return $this->belongsTo(GatewayUser::class, 'gateway_user_id', 'user_id'); + } +} +``` + + +#### 3.4 GatewayService anpassen + +**app/Services/LLM/GatewayService.php - Änderungen:** +```php +user_id) + ->where('provider', $provider) + ->where('is_active', true) + ->first(); + + if (!$credential) { + throw new ProviderException( + "No active API credentials found for provider: {$provider}", + 400 + ); + } + + $credential->update(['last_used_at' => now()]); + + return $credential; + } + + private function updateUserBudget(GatewayUser $user, float $cost): void + { + // Budget direkt in gateway_users Tabelle + $user->incrementSpending($cost); + + // Check if budget exceeded + if ($user->hasExceededBudget()) { + // TODO: Dispatch alert notification + } + } +} +``` + +#### 3.5 RequestLogger anpassen + +**app/Services/LLM/RequestLogger.php:** +```php +use App\Models\UsageLog; // ← Statt LlmRequest + +public function logSuccess( + string $gatewayUserId, // ← Statt int $userId + string $provider, + string $model, + array $requestPayload, + array $response, + array $costs, + int $responseTimeMs, + ?string $ipAddress = null, + ?string $userAgent = null +): int { + $log = UsageLog::create([ + 'gateway_user_id' => $gatewayUserId, // ← Geändert + 'provider' => $provider, + 'model' => $model, + 'prompt_tokens' => $response['usage']['prompt_tokens'], + 'completion_tokens' => $response['usage']['completion_tokens'], + 'total_tokens' => $response['usage']['total_tokens'], + 'cost' => $costs['total_cost'], + 'request_payload' => $requestPayload, + 'response_payload' => $response, + 'response_time_ms' => $responseTimeMs, + 'ip_address' => $ipAddress, + 'user_agent' => $userAgent, + 'status' => 'success', + ]); + + return $log->id; +} +``` + + +#### 3.6 Middleware anpassen + +**app/Http/Middleware/CheckBudget.php:** +```php +user(); // GatewayUser + + if ($user && $user->hasExceededBudget()) { + return response()->json([ + 'error' => [ + 'message' => 'Budget exceeded. Please contact your administrator.', + 'type' => 'budget_exceeded', + 'code' => 429, + 'budget_limit' => $user->monthly_budget_limit, + 'current_spending' => $user->current_month_spending, + ] + ], 429); + } + + if ($user && $user->isBlocked()) { + return response()->json([ + 'error' => [ + 'message' => 'User is blocked. Please contact your administrator.', + 'type' => 'user_blocked', + 'code' => 403, + ] + ], 403); + } + + return $next($request); + } +} +``` + +**app/Http/Middleware/CheckRateLimit.php:** +```php +user(); // GatewayUser + + if (!$user || !$user->rate_limit_per_hour) { + return $next($request); + } + + $key = 'rate_limit:' . $user->user_id; + $requests = Cache::get($key, 0); + + if ($requests >= $user->rate_limit_per_hour) { + $ttl = Cache::get($key . ':ttl', 3600); + + return response()->json([ + 'error' => [ + 'message' => 'Rate limit exceeded. Please try again later.', + 'type' => 'rate_limit_exceeded', + 'code' => 429, + 'limit' => $user->rate_limit_per_hour, + 'current' => $requests, + 'retry_after' => $ttl, + ] + ], 429); + } + + // Increment counter + Cache::put($key, $requests + 1, 3600); + if ($requests == 0) { + Cache::put($key . ':ttl', 3600, 3600); + } + + return $next($request); + } +} +``` + +--- + +### Phase 4: Admin-Interface für Gateway-User Credentials + +#### 4.1 Neuer Controller: GatewayUserCredentialController + +**app/Http/Controllers/Admin/GatewayUserCredentialController.php:** +```php +credentials()->get(); + + return view('admin.gateway-user-credentials.index', compact('user', 'credentials')); + } + + public function create($gatewayUserId) + { + $user = GatewayUser::findOrFail($gatewayUserId); + $providers = ['openai', 'anthropic', 'google', 'deepseek', 'mistral']; + + return view('admin.gateway-user-credentials.create', compact('user', 'providers')); + } + + public function store(Request $request, $gatewayUserId) + { + $validated = $request->validate([ + 'provider' => 'required|in:openai,anthropic,google,deepseek,mistral', + 'api_key' => 'required|string', + 'organization_id' => 'nullable|string', + ]); + + GatewayUserCredential::create([ + 'gateway_user_id' => $gatewayUserId, + 'provider' => $validated['provider'], + 'api_key' => $validated['api_key'], + 'organization_id' => $validated['organization_id'] ?? null, + 'is_active' => true, + ]); + + return redirect() + ->route('admin.gateway-users.credentials.index', $gatewayUserId) + ->with('success', 'Credential added successfully'); + } + + // Test, Toggle, Delete methods... +} +``` + + +#### 4.2 Routes erweitern + +**routes/web.php - Hinzufügen:** +```php +Route::middleware(['auth', 'verified'])->group(function () { + // ... existierende Routes ... + + // Gateway User Credentials Management + Route::prefix('admin/gateway-users/{gatewayUser}')->name('admin.gateway-users.')->group(function () { + Route::get('credentials', [GatewayUserCredentialController::class, 'index']) + ->name('credentials.index'); + Route::get('credentials/create', [GatewayUserCredentialController::class, 'create']) + ->name('credentials.create'); + Route::post('credentials', [GatewayUserCredentialController::class, 'store']) + ->name('credentials.store'); + Route::post('credentials/{credential}/test', [GatewayUserCredentialController::class, 'test']) + ->name('credentials.test'); + Route::post('credentials/{credential}/toggle', [GatewayUserCredentialController::class, 'toggle']) + ->name('credentials.toggle'); + Route::delete('credentials/{credential}', [GatewayUserCredentialController::class, 'destroy']) + ->name('credentials.destroy'); + }); +}); +``` + +--- + +## 🎯 Migrations-Reihenfolge + +### Migration 1: Budget-Felder zu gateway_users hinzufügen +```bash +php artisan make:migration add_budget_fields_to_gateway_users +``` + +```php +Schema::table('gateway_users', function (Blueprint $table) { + $table->decimal('monthly_budget_limit', 10, 2)->nullable()->after('alias'); + $table->decimal('current_month_spending', 10, 2)->default(0)->after('monthly_budget_limit'); + $table->integer('budget_alert_threshold')->nullable()->after('current_month_spending'); + $table->integer('rate_limit_per_hour')->default(60)->after('budget_alert_threshold'); + $table->dropColumn('spend'); // Alte Spalte entfernen + $table->dropColumn('budget_id'); // Nicht mehr gebraucht +}); +``` + +### Migration 2: Gateway User Credentials erstellen +```bash +php artisan make:migration create_gateway_user_credentials_table +``` + +(Wie oben bereits dokumentiert) + +### Migration 3: Usage Logs anpassen +```bash +php artisan make:migration update_usage_logs_for_gateway_users +``` + +```php +Schema::table('usage_logs', function (Blueprint $table) { + // Spalte umbenennen + $table->renameColumn('user_id', 'gateway_user_id'); + + // Neue Spalten hinzufügen falls nicht vorhanden + if (!Schema::hasColumn('usage_logs', 'request_payload')) { + $table->json('request_payload')->nullable(); + } + if (!Schema::hasColumn('usage_logs', 'response_payload')) { + $table->json('response_payload')->nullable(); + } + if (!Schema::hasColumn('usage_logs', 'response_time_ms')) { + $table->integer('response_time_ms')->nullable(); + } + if (!Schema::hasColumn('usage_logs', 'ip_address')) { + $table->string('ip_address', 45)->nullable(); + } + if (!Schema::hasColumn('usage_logs', 'user_agent')) { + $table->string('user_agent')->nullable(); + } +}); +``` + +### Migration 4: Alte Tabellen optional behalten/entfernen +```bash +php artisan make:migration cleanup_old_tables +``` + +```php +// Optional: Diese Tabellen könnten gelöscht werden wenn nicht mehr gebraucht: +// - user_provider_credentials (alte Struktur) +// - user_budgets (für users, nicht gateway_users) +// - llm_requests (wenn usage_logs konsolidiert ist) +// - budgets (wenn Budget jetzt in gateway_users ist) + +// Oder umbenennen für Archiv: +Schema::rename('user_provider_credentials', 'old_user_provider_credentials'); +Schema::rename('user_budgets', 'old_user_budgets'); +Schema::rename('llm_requests', 'old_llm_requests'); +Schema::rename('budgets', 'old_budgets'); +``` + +--- + + +## ⏱️ Implementierungs-Zeitplan + +### Tag 1-2: Datenbank & Models +- ✅ Migration 1: Budget-Felder zu gateway_users +- ✅ Migration 2: Gateway User Credentials Tabelle +- ✅ Migration 3: Usage Logs anpassen +- ✅ GatewayUser Model erweitern +- ✅ GatewayUserCredential Model erstellen +- ✅ ApiKey Model anpassen (falls nötig) + +### Tag 3: Authentication System +- ✅ ApiKeyGuard implementieren +- ✅ AuthServiceProvider erweitern +- ✅ config/auth.php anpassen +- ✅ Middleware anpassen (CheckBudget, CheckRateLimit) +- ✅ Testing: API-Key Authentication + +### Tag 4: API Services anpassen +- ✅ GatewayService umbauen +- ✅ RequestLogger anpassen +- ✅ routes/api.php anpassen +- ✅ ChatCompletionController anpassen +- ✅ Testing: Chat Completions Endpoint + +### Tag 5: Admin-Interface +- ✅ GatewayUserCredentialController erstellen +- ✅ Views für Credential-Management +- ✅ Routes hinzufügen +- ✅ GatewayUserController anpassen (Budget-Felder) +- ✅ Testing: Admin-Interface + +### Tag 6: Testing & Cleanup +- ✅ Integration Tests schreiben +- ✅ Ende-zu-Ende Testing +- ✅ Alte Tabellen archivieren/entfernen +- ✅ Dokumentation finalisieren + +**Geschätzter Aufwand:** 6 Arbeitstage + +--- + +## 📝 Checkliste + +### Datenbank +- [ ] Migration: Budget-Felder zu gateway_users +- [ ] Migration: Gateway User Credentials Tabelle erstellen +- [ ] Migration: Usage Logs anpassen +- [ ] Migration: Alte Tabellen archivieren +- [ ] php artisan migrate + +### Models +- [ ] GatewayUser Model erweitern (Authenticatable) +- [ ] GatewayUserCredential Model erstellen +- [ ] UsageLog Model anpassen (gateway_user_id) + +### Authentication +- [ ] ApiKeyGuard implementieren +- [ ] AuthServiceProvider registrieren +- [ ] config/auth.php anpassen + +### API +- [ ] routes/api.php: auth:api statt auth:sanctum +- [ ] GatewayService: GatewayUser statt User +- [ ] GatewayService: GatewayUserCredential verwenden +- [ ] RequestLogger: gateway_user_id verwenden +- [ ] Middleware: CheckBudget anpassen +- [ ] Middleware: CheckRateLimit anpassen + +### Admin-Interface +- [ ] GatewayUserCredentialController erstellen +- [ ] Routes für Credential-Management hinzufügen +- [ ] Views erstellen für Credential-Management +- [ ] GatewayUserController: Budget-Felder anpassen + +### Testing +- [ ] API-Key Authentication testen +- [ ] Chat Completions Endpoint testen +- [ ] Budget-System testen +- [ ] Rate-Limiting testen +- [ ] Admin-Interface testen + +--- + + +## 📊 Finale Architektur (Übersicht) + +### User-Typen & Zugriff + +``` +┌─────────────────────────────────────────────────────────────┐ +│ Laravel LLM Gateway │ +├─────────────────────────────────────────────────────────────┤ +│ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ Admin Users │ │ Gateway Users │ │ +│ │ (users table) │ │ (gateway_users) │ │ +│ ├──────────────────┤ ├──────────────────┤ │ +│ │ • Web-Interface │ │ • API-Zugriff │ │ +│ │ • Session Auth │ │ • API-Key Auth │ │ +│ │ • Management │ │ • Chat Requests │ │ +│ └──────────────────┘ └──────────────────┘ │ +│ │ │ │ +│ │ │ │ +│ ▼ ▼ │ +│ ┌──────────────────┐ ┌──────────────────┐ │ +│ │ Web-Interface │ │ REST API │ │ +│ │ /dashboard │ │ /api/chat/... │ │ +│ │ /gateway-users │ │ │ │ +│ │ /budgets │ │ Auth: API-Key │ │ +│ │ /usage-logs │ │ Guard: 'api' │ │ +│ └──────────────────┘ └──────────────────┘ │ +│ │ +└─────────────────────────────────────────────────────────────┘ +``` + +### Datenfluss: API-Request + +``` +1. Externe App sendet Request: + POST /api/chat/completions + Authorization: Bearer sk-gw-abc123xyz... + +2. ApiKeyGuard validiert API-Key + → Findet GatewayUser in gateway_users + +3. Middleware: CheckBudget + → Prüft gateway_user.monthly_budget_limit + → Prüft gateway_user.current_month_spending + → Prüft gateway_user.blocked + +4. Middleware: CheckRateLimit + → Prüft gateway_user.rate_limit_per_hour + +5. ChatCompletionController + → Empfängt GatewayUser + +6. GatewayService + → Holt GatewayUserCredential (Provider API-Key) + → Ruft LLM-Provider auf + → Berechnet Kosten + +7. RequestLogger + → Speichert in usage_logs (gateway_user_id) + +8. Budget Update + → Erhöht gateway_user.current_month_spending + +9. Response zurück an externe App +``` + +--- + +## 🔧 Verwendungs-Beispiele + +### Admin erstellt Gateway-User + +**Im Admin-Interface:** +``` +1. Login als Admin (users Tabelle) +2. Navigate zu: /gateway-users/create +3. Erstelle Gateway-User: + - User ID: "app-customer-123" + - Alias: "Customer App" + - Budget: 100.00 EUR + - Rate Limit: 60 requests/hour +4. Gateway-User wird in gateway_users gespeichert +``` + +### Admin konfiguriert Provider-Credentials für Gateway-User + +**Im Admin-Interface:** +``` +1. Navigate zu: /admin/gateway-users/app-customer-123/credentials +2. Klick auf "Add Credential" +3. Wähle Provider: "OpenAI" +4. API-Key: "sk-proj-..." +5. Speichern +6. GatewayUserCredential wird erstellt (verschlüsselt) +``` + +### Admin generiert API-Key für Gateway-User + +**Im Admin-Interface:** +``` +1. Navigate zu: /api-keys +2. Klick auf "Create API Key" +3. Wähle Gateway-User: "app-customer-123" +4. Name: "Production Key" +5. System generiert: + - Full Key: "sk-gw-1234567890abcdef..." + - Prefix: "sk-gw-1234" + - Hash wird gespeichert +6. WICHTIG: Zeige Full Key nur EINMAL an! +``` + +### Externe App verwendet die API + +**Request:** +```bash +curl -X POST http://localhost/api/chat/completions \ + -H "Authorization: Bearer sk-gw-1234567890abcdef..." \ + -H "Content-Type: application/json" \ + -d '{ + "provider": "openai", + "model": "gpt-4", + "messages": [ + {"role": "user", "content": "Hello, how are you?"} + ] + }' +``` + +**Response:** +```json +{ + "success": true, + "request_id": 12345, + "provider": "openai", + "model": "gpt-4", + "content": "I'm doing well, thank you for asking! ...", + "role": "assistant", + "finish_reason": "stop", + "usage": { + "prompt_tokens": 15, + "completion_tokens": 20, + "total_tokens": 35 + }, + "cost": { + "prompt_cost": 0.00045, + "completion_cost": 0.0012, + "total_cost": 0.00165 + }, + "response_time_ms": 1234 +} +``` + +--- + + +## ✅ Vorteile dieser Architektur + +1. **Klare Trennung** + - Admins (users) vs. API-Clients (gateway_users) + - Verschiedene Auth-Mechanismen (Session vs. API-Key) + - Verschiedene Use-Cases + +2. **Sicherheit** + - API-Keys sind verschlüsselt und gehashed + - Provider-Credentials sind verschlüsselt + - Budget-Limits pro Gateway-User + - Rate-Limiting pro Gateway-User + +3. **Skalierbarkeit** + - Jeder Gateway-User kann eigene Provider-Credentials haben + - Oder zentrale Admin-Credentials nutzen + - Flexible Budget-Verwaltung + +4. **Management** + - Admins haben volle Kontrolle + - Können Gateway-Users blockieren + - Können Budgets anpassen + - Sehen alle Usage-Logs + +5. **Standard-Laravel** + - Nutzt Laravel Guards (Standard-Mechanismus) + - Nutzt Eloquent Models + - Nutzt Laravel Migrations + - Gut dokumentiert und wartbar + +--- + +## ⚠️ Potenzielle Herausforderungen + +1. **Custom Guard** + - Etwas mehr Code als Sanctum + - Braucht sorgfältiges Testing + + **Mitigation:** Guard ist einfach und klar strukturiert + +2. **API-Key Management** + - API-Keys müssen sicher gespeichert werden + - Nur einmal anzeigbar + + **Mitigation:** Standard-Pattern (wie GitHub, AWS, etc.) + +3. **Migration von existierendem Code** + - Mehrere Dateien müssen angepasst werden + - Testing ist wichtig + + **Mitigation:** Systematischer Plan (6 Tage) + +--- + +## 🎯 Zusammenfassung + +### Problem erkannt: +- ❌ API-Code verwendet `users` (für Admins gedacht) +- ❌ Admin-Interface verwaltet `gateway_users` (für API-Clients) +- ❌ Keine Integration zwischen beiden + +### Lösung: +- ✅ API-Code auf `gateway_users` umstellen +- ✅ Custom Guard für API-Key Authentication +- ✅ Admin-Interface für Gateway-User Credentials +- ✅ Klare Architektur: Admins vs. API-Clients + +### Architektur: +``` +users (Admins) + └── Web-Interface (Session Auth) + └── Verwalten gateway_users + +gateway_users (API-Clients) + ├── API-Key Authentication + ├── gateway_user_credentials (Provider API-Keys) + ├── Budget & Rate Limits + └── usage_logs +``` + +### Aufwand: +- **6 Arbeitstage** für vollständige Umsetzung +- Systematischer Plan vorhanden +- Klare Checkliste + +### Ergebnis: +- Funktionierende API +- Saubere Architektur +- Standard Laravel-Patterns +- Wartbar und erweiterbar + +--- + +## 🚀 Nächste Schritte + +**Option A: Sofort starten (Empfohlen)** +1. Migrations erstellen und ausführen +2. Models anpassen +3. ApiKeyGuard implementieren +4. API-Code anpassen +5. Admin-Interface erweitern +6. Testing + +**Option B: Erst Prototyp** +1. Nur Migrations und Models +2. Minimal-Implementierung für Testing +3. Dann entscheiden ob weitermachen + +**Option C: Review und Anpassungen** +1. Plan reviewen +2. Anpassungen vornehmen +3. Dann starten + +--- + +## 📎 Wichtige Dateien (Neu/Geändert) + +### Neu erstellen: +- `app/Auth/ApiKeyGuard.php` +- `app/Models/GatewayUserCredential.php` +- `app/Http/Controllers/Admin/GatewayUserCredentialController.php` +- `database/migrations/*_add_budget_fields_to_gateway_users.php` +- `database/migrations/*_create_gateway_user_credentials_table.php` +- `database/migrations/*_update_usage_logs_for_gateway_users.php` + +### Anpassen: +- `app/Models/GatewayUser.php` (Authenticatable implementieren) +- `app/Services/LLM/GatewayService.php` (GatewayUser statt User) +- `app/Services/LLM/RequestLogger.php` (gateway_user_id) +- `app/Http/Middleware/CheckBudget.php` +- `app/Http/Middleware/CheckRateLimit.php` +- `app/Providers/AuthServiceProvider.php` +- `config/auth.php` +- `routes/api.php` +- `routes/web.php` + +--- + +**Dokument-Ende** + +*Erstellt am: 2025-11-18* +*Status: BEREIT FÜR IMPLEMENTIERUNG* +*Geschätzter Aufwand: 6 Arbeitstage* diff --git a/laravel-app/app/Http/Controllers/Api/ChatCompletionController.php b/laravel-app/app/Http/Controllers/Api/ChatCompletionController.php index e74cdc3..a2d1d61 100644 --- a/laravel-app/app/Http/Controllers/Api/ChatCompletionController.php +++ b/laravel-app/app/Http/Controllers/Api/ChatCompletionController.php @@ -16,8 +16,18 @@ class ChatCompletionController extends Controller ) {} /** - * Handle chat completion request - * + * Create a chat completion + * + * Accepts OpenAI-compatible chat completion requests and routes them to the appropriate + * LLM provider (OpenAI, Anthropic, DeepSeek, Google Gemini, or Mistral AI). + * + * The request uses the authenticated user's API keys for the specified provider. + * Cost tracking, budget checking, and rate limiting are applied automatically. + * + * Returns an OpenAI-compatible response with usage statistics and cost information. + * + * @tags Chat + * * @param ChatCompletionRequest $request * @return JsonResponse */ diff --git a/laravel-app/composer.json b/laravel-app/composer.json index 693ef77..79c2b9c 100644 --- a/laravel-app/composer.json +++ b/laravel-app/composer.json @@ -7,6 +7,7 @@ "license": "MIT", "require": { "php": "^8.2", + "dedoc/scramble": "^0.13.4", "laravel/framework": "^12.0", "laravel/tinker": "^2.10.1", "livewire/livewire": "^3.6.4", diff --git a/laravel-app/composer.lock b/laravel-app/composer.lock index eab5be0..c25c800 100644 --- a/laravel-app/composer.lock +++ b/laravel-app/composer.lock @@ -4,7 +4,7 @@ "Read more about it at https://getcomposer.org/doc/01-basic-usage.md#installing-dependencies", "This file is @generated automatically" ], - "content-hash": "48bc009539e7af0a89770c8e30f8e9a3", + "content-hash": "f90d326460fd22f5fbd0e5a7a7456c1a", "packages": [ { "name": "brick/math", @@ -135,6 +135,86 @@ ], "time": "2024-02-09T16:56:22+00:00" }, + { + "name": "dedoc/scramble", + "version": "v0.13.4", + "source": { + "type": "git", + "url": "https://github.com/dedoc/scramble.git", + "reference": "773f9d41b68a9bd52120648e55068bfbe9be567e" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/dedoc/scramble/zipball/773f9d41b68a9bd52120648e55068bfbe9be567e", + "reference": "773f9d41b68a9bd52120648e55068bfbe9be567e", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^10.0|^11.0|^12.0", + "myclabs/deep-copy": "^1.12", + "nikic/php-parser": "^5.0", + "php": "^8.1", + "phpstan/phpdoc-parser": "^1.0|^2.0", + "spatie/laravel-package-tools": "^1.9.2" + }, + "require-dev": { + "larastan/larastan": "^3.3", + "laravel/pint": "^v1.1.0", + "nunomaduro/collision": "^7.0|^8.0", + "orchestra/testbench": "^8.0|^9.0|^10.0", + "pestphp/pest": "^2.34|^3.7", + "pestphp/pest-plugin-laravel": "^2.3|^3.1", + "phpstan/extension-installer": "^1.4", + "phpstan/phpstan-deprecation-rules": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpunit/phpunit": "^10.5|^11.5.3", + "spatie/laravel-permission": "^6.10", + "spatie/pest-plugin-snapshots": "^2.1" + }, + "type": "library", + "extra": { + "laravel": { + "providers": [ + "Dedoc\\Scramble\\ScrambleServiceProvider" + ] + } + }, + "autoload": { + "psr-4": { + "Dedoc\\Scramble\\": "src", + "Dedoc\\Scramble\\Database\\Factories\\": "database/factories" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Roman Lytvynenko", + "email": "litvinenko95@gmail.com", + "role": "Developer" + } + ], + "description": "Automatic generation of API documentation for Laravel applications.", + "homepage": "https://github.com/dedoc/scramble", + "keywords": [ + "documentation", + "laravel", + "openapi" + ], + "support": { + "issues": "https://github.com/dedoc/scramble/issues", + "source": "https://github.com/dedoc/scramble/tree/v0.13.4" + }, + "funding": [ + { + "url": "https://github.com/romalytvynenko", + "type": "github" + } + ], + "time": "2025-11-16T07:10:35+00:00" + }, { "name": "dflydev/dot-access-data", "version": "v3.0.3", @@ -2259,6 +2339,66 @@ ], "time": "2025-03-24T10:02:05+00:00" }, + { + "name": "myclabs/deep-copy", + "version": "1.13.4", + "source": { + "type": "git", + "url": "https://github.com/myclabs/DeepCopy.git", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", + "shasum": "" + }, + "require": { + "php": "^7.1 || ^8.0" + }, + "conflict": { + "doctrine/collections": "<1.6.8", + "doctrine/common": "<2.13.3 || >=3 <3.2.2" + }, + "require-dev": { + "doctrine/collections": "^1.6.8", + "doctrine/common": "^2.13.3 || ^3.2.2", + "phpspec/prophecy": "^1.10", + "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" + }, + "type": "library", + "autoload": { + "files": [ + "src/DeepCopy/deep_copy.php" + ], + "psr-4": { + "DeepCopy\\": "src/DeepCopy/" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "Create deep copies (clones) of your objects", + "keywords": [ + "clone", + "copy", + "duplicate", + "object", + "object graph" + ], + "support": { + "issues": "https://github.com/myclabs/DeepCopy/issues", + "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" + }, + "funding": [ + { + "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", + "type": "tidelift" + } + ], + "time": "2025-08-01T08:46:24+00:00" + }, { "name": "nesbot/carbon", "version": "3.10.3", @@ -2738,6 +2878,53 @@ ], "time": "2025-08-21T11:53:16+00:00" }, + { + "name": "phpstan/phpdoc-parser", + "version": "2.3.0", + "source": { + "type": "git", + "url": "https://github.com/phpstan/phpdoc-parser.git", + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/phpstan/phpdoc-parser/zipball/1e0cd5370df5dd2e556a36b9c62f62e555870495", + "reference": "1e0cd5370df5dd2e556a36b9c62f62e555870495", + "shasum": "" + }, + "require": { + "php": "^7.4 || ^8.0" + }, + "require-dev": { + "doctrine/annotations": "^2.0", + "nikic/php-parser": "^5.3.0", + "php-parallel-lint/php-parallel-lint": "^1.2", + "phpstan/extension-installer": "^1.0", + "phpstan/phpstan": "^2.0", + "phpstan/phpstan-phpunit": "^2.0", + "phpstan/phpstan-strict-rules": "^2.0", + "phpunit/phpunit": "^9.6", + "symfony/process": "^5.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "PHPStan\\PhpDocParser\\": [ + "src/" + ] + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "description": "PHPDoc parser with support for nullable, intersection and generic types", + "support": { + "issues": "https://github.com/phpstan/phpdoc-parser/issues", + "source": "https://github.com/phpstan/phpdoc-parser/tree/2.3.0" + }, + "time": "2025-08-30T15:50:23+00:00" + }, { "name": "psr/clock", "version": "1.0.0", @@ -3427,6 +3614,67 @@ }, "time": "2025-09-04T20:59:21+00:00" }, + { + "name": "spatie/laravel-package-tools", + "version": "1.92.7", + "source": { + "type": "git", + "url": "https://github.com/spatie/laravel-package-tools.git", + "reference": "f09a799850b1ed765103a4f0b4355006360c49a5" + }, + "dist": { + "type": "zip", + "url": "https://api.github.com/repos/spatie/laravel-package-tools/zipball/f09a799850b1ed765103a4f0b4355006360c49a5", + "reference": "f09a799850b1ed765103a4f0b4355006360c49a5", + "shasum": "" + }, + "require": { + "illuminate/contracts": "^9.28|^10.0|^11.0|^12.0", + "php": "^8.0" + }, + "require-dev": { + "mockery/mockery": "^1.5", + "orchestra/testbench": "^7.7|^8.0|^9.0|^10.0", + "pestphp/pest": "^1.23|^2.1|^3.1", + "phpunit/php-code-coverage": "^9.0|^10.0|^11.0", + "phpunit/phpunit": "^9.5.24|^10.5|^11.5", + "spatie/pest-plugin-test-time": "^1.1|^2.2" + }, + "type": "library", + "autoload": { + "psr-4": { + "Spatie\\LaravelPackageTools\\": "src" + } + }, + "notification-url": "https://packagist.org/downloads/", + "license": [ + "MIT" + ], + "authors": [ + { + "name": "Freek Van der Herten", + "email": "freek@spatie.be", + "role": "Developer" + } + ], + "description": "Tools for creating Laravel packages", + "homepage": "https://github.com/spatie/laravel-package-tools", + "keywords": [ + "laravel-package-tools", + "spatie" + ], + "support": { + "issues": "https://github.com/spatie/laravel-package-tools/issues", + "source": "https://github.com/spatie/laravel-package-tools/tree/1.92.7" + }, + "funding": [ + { + "url": "https://github.com/spatie", + "type": "github" + } + ], + "time": "2025-07-17T15:46:43+00:00" + }, { "name": "symfony/clock", "version": "v7.3.0", @@ -6668,66 +6916,6 @@ }, "time": "2024-05-16T03:13:13+00:00" }, - { - "name": "myclabs/deep-copy", - "version": "1.13.4", - "source": { - "type": "git", - "url": "https://github.com/myclabs/DeepCopy.git", - "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a" - }, - "dist": { - "type": "zip", - "url": "https://api.github.com/repos/myclabs/DeepCopy/zipball/07d290f0c47959fd5eed98c95ee5602db07e0b6a", - "reference": "07d290f0c47959fd5eed98c95ee5602db07e0b6a", - "shasum": "" - }, - "require": { - "php": "^7.1 || ^8.0" - }, - "conflict": { - "doctrine/collections": "<1.6.8", - "doctrine/common": "<2.13.3 || >=3 <3.2.2" - }, - "require-dev": { - "doctrine/collections": "^1.6.8", - "doctrine/common": "^2.13.3 || ^3.2.2", - "phpspec/prophecy": "^1.10", - "phpunit/phpunit": "^7.5.20 || ^8.5.23 || ^9.5.13" - }, - "type": "library", - "autoload": { - "files": [ - "src/DeepCopy/deep_copy.php" - ], - "psr-4": { - "DeepCopy\\": "src/DeepCopy/" - } - }, - "notification-url": "https://packagist.org/downloads/", - "license": [ - "MIT" - ], - "description": "Create deep copies (clones) of your objects", - "keywords": [ - "clone", - "copy", - "duplicate", - "object", - "object graph" - ], - "support": { - "issues": "https://github.com/myclabs/DeepCopy/issues", - "source": "https://github.com/myclabs/DeepCopy/tree/1.13.4" - }, - "funding": [ - { - "url": "https://tidelift.com/funding/github/packagist/myclabs/deep-copy", - "type": "tidelift" - } - ], - "time": "2025-08-01T08:46:24+00:00" - }, { "name": "nunomaduro/collision", "version": "v8.8.2", diff --git a/laravel-app/config/scramble.php b/laravel-app/config/scramble.php new file mode 100644 index 0000000..87a9002 --- /dev/null +++ b/laravel-app/config/scramble.php @@ -0,0 +1,136 @@ + 'api', + + /* + * Your API domain. By default, app domain is used. This is also a part of the default API routes + * matcher, so when implementing your own, make sure you use this config if needed. + */ + 'api_domain' => null, + + /* + * The path where your OpenAPI specification will be exported. + */ + 'export_path' => 'api.json', + + 'info' => [ + /* + * API version. + */ + 'version' => env('API_VERSION', '1.0.0'), + + /* + * Description rendered on the home page of the API documentation (`/docs/api`). + */ + 'description' => '', + ], + + /* + * Customize Stoplight Elements UI + */ + 'ui' => [ + /* + * Define the title of the documentation's website. App name is used when this config is `null`. + */ + 'title' => null, + + /* + * Define the theme of the documentation. Available options are `light`, `dark`, and `system`. + */ + 'theme' => 'light', + + /* + * Hide the `Try It` feature. Enabled by default. + */ + 'hide_try_it' => false, + + /* + * Hide the schemas in the Table of Contents. Enabled by default. + */ + 'hide_schemas' => false, + + /* + * URL to an image that displays as a small square logo next to the title, above the table of contents. + */ + 'logo' => '', + + /* + * Use to fetch the credential policy for the Try It feature. Options are: omit, include (default), and same-origin + */ + 'try_it_credentials_policy' => 'include', + + /* + * There are three layouts for Elements: + * - sidebar - (Elements default) Three-column design with a sidebar that can be resized. + * - responsive - Like sidebar, except at small screen sizes it collapses the sidebar into a drawer that can be toggled open. + * - stacked - Everything in a single column, making integrations with existing websites that have their own sidebar or other columns already. + */ + 'layout' => 'responsive', + ], + + /* + * The list of servers of the API. By default, when `null`, server URL will be created from + * `scramble.api_path` and `scramble.api_domain` config variables. When providing an array, you + * will need to specify the local server URL manually (if needed). + * + * Example of non-default config (final URLs are generated using Laravel `url` helper): + * + * ```php + * 'servers' => [ + * 'Live' => 'api', + * 'Prod' => 'https://scramble.dedoc.co/api', + * ], + * ``` + */ + 'servers' => null, + + /** + * Determines how Scramble stores the descriptions of enum cases. + * Available options: + * - 'description' – Case descriptions are stored as the enum schema's description using table formatting. + * - 'extension' – Case descriptions are stored in the `x-enumDescriptions` enum schema extension. + * + * @see https://redocly.com/docs-legacy/api-reference-docs/specification-extensions/x-enum-descriptions + * - false - Case descriptions are ignored. + */ + 'enum_cases_description_strategy' => 'description', + + /** + * Determines how Scramble stores the names of enum cases. + * Available options: + * - 'names' – Case names are stored in the `x-enumNames` enum schema extension. + * - 'varnames' - Case names are stored in the `x-enum-varnames` enum schema extension. + * - false - Case names are not stored. + */ + 'enum_cases_names_strategy' => false, + + /** + * When Scramble encounters deep objects in query parameters, it flattens the parameters so the generated + * OpenAPI document correctly describes the API. Flattening deep query parameters is relevant until + * OpenAPI 3.2 is released and query string structure can be described properly. + * + * For example, this nested validation rule describes the object with `bar` property: + * `['foo.bar' => ['required', 'int']]`. + * + * When `flatten_deep_query_parameters` is `true`, Scramble will document the parameter like so: + * `{"name":"foo[bar]", "schema":{"type":"int"}, "required":true}`. + * + * When `flatten_deep_query_parameters` is `false`, Scramble will document the parameter like so: + * `{"name":"foo", "schema": {"type":"object", "properties":{"bar":{"type": "int"}}, "required": ["bar"]}, "required":true}`. + */ + 'flatten_deep_query_parameters' => true, + + 'middleware' => [ + 'web', + RestrictedDocsAccess::class, + ], + + 'extensions' => [], +];