feat: Implementiere umfassende RESTful API für LLM Gateway

Fügt 21 neue API-Endpoints in 4 Phasen hinzu:

Phase 1 - Foundation (Provider & Models):
- GET /api/providers - Liste aller Provider
- GET /api/providers/{provider} - Provider-Details
- GET /api/models - Liste aller Models mit Filtering/Sorting
- GET /api/models/{provider}/{model} - Model-Details

Phase 2 - Core Features (Credentials, Budget, Pricing):
- GET/POST/PUT/DELETE /api/credentials - Credential-Management
- POST /api/credentials/{id}/test - Connection Testing
- GET /api/budget - Budget-Status mit Projektionen
- GET /api/budget/history - Budget-Historie
- GET /api/pricing - Model-Pricing-Listen
- GET /api/pricing/calculator - Kosten-Kalkulator
- GET /api/pricing/compare - Preis-Vergleich

Phase 3 - Analytics (Usage Statistics):
- GET /api/usage/summary - Umfassende Statistiken
- GET /api/usage/requests - Request-History mit Pagination
- GET /api/usage/requests/{id} - Request-Details
- GET /api/usage/charts - Chart-Daten (4 Typen)

Phase 4 - Account (Account Info & Activity):
- GET /api/account - User-Informationen
- GET /api/account/activity - Activity-Log

Features:
- Vollständige Scramble/Swagger-Dokumentation
- Consistent Error-Handling
- API-Key Authentication
- Filtering, Sorting, Pagination
- Budget-Tracking mit Alerts
- Provider-Breakdown
- Performance-Metriken
- Chart-Ready-Data

Controller erstellt:
- ProviderController
- ModelController
- CredentialController
- BudgetController
- PricingController
- UsageController
- AccountController

Dokumentation:
- API_KONZEPT.md - Vollständiges API-Konzept
- API_IMPLEMENTATION_STATUS.txt - Implementation-Tracking
- API_IMPLEMENTATION_SUMMARY.md - Zusammenfassung und Workflows
This commit is contained in:
wtrinkl
2025-11-19 12:33:11 +01:00
parent c149bdbdde
commit b6d75d51e3
12 changed files with 4556 additions and 11 deletions

View File

@@ -0,0 +1,511 @@
<?php
namespace App\Http\Controllers\Api;
use App\Http\Controllers\Controller;
use App\Models\GatewayUserCredential;
use Illuminate\Http\JsonResponse;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
use Illuminate\Support\Facades\Log;
use Illuminate\Support\Str;
class CredentialController extends Controller
{
/**
* Get list of all provider credentials for the authenticated user
*
* Returns a list of all configured provider credentials, including status,
* last usage information, and test results.
*
* ## Example Response
*
* ```json
* {
* "data": [
* {
* "id": 1,
* "provider": "openai",
* "provider_name": "OpenAI",
* "api_key_preview": "sk-proj-...xyz",
* "organization_id": null,
* "is_active": true,
* "status": "verified",
* "last_used": "2025-11-19T11:45:00Z",
* "last_tested": "2025-11-19T10:30:00Z",
* "test_result": {
* "status": "success",
* "message": "Connection successful",
* "tested_at": "2025-11-19T10:30:00Z"
* },
* "created_at": "2025-11-10T08:00:00Z",
* "updated_at": "2025-11-19T10:30:00Z"
* }
* ]
* }
* ```
*
* @tags Credentials
*
* @param Request $request
* @return JsonResponse
*/
public function index(Request $request): JsonResponse
{
$user = $request->user();
$credentials = GatewayUserCredential::where('gateway_user_id', $user->user_id)
->orderBy('provider')
->get()
->map(function ($credential) {
return [
'id' => $credential->id,
'provider' => $credential->provider,
'provider_name' => $this->getProviderName($credential->provider),
'api_key_preview' => $this->maskApiKey($credential->api_key),
'organization_id' => $credential->organization_id,
'is_active' => $credential->is_active,
'status' => $credential->test_status ?? 'not_tested',
'last_used' => $credential->last_used_at?->toIso8601String(),
'last_tested' => $credential->last_tested_at?->toIso8601String(),
'test_result' => $credential->last_tested_at ? [
'status' => $credential->test_status ?? 'unknown',
'message' => $credential->test_error ?: 'Connection successful',
'tested_at' => $credential->last_tested_at->toIso8601String(),
] : null,
'created_at' => $credential->created_at->toIso8601String(),
'updated_at' => $credential->updated_at->toIso8601String(),
];
});
return response()->json([
'data' => $credentials,
]);
}
/**
* Add new provider credentials
*
* Create new credentials for a specific provider. Optionally test the
* connection before saving.
*
* ## Request Body
*
* ```json
* {
* "provider": "openai",
* "api_key": "sk-proj-abc123...",
* "organization_id": null,
* "test_connection": true
* }
* ```
*
* ## Example Response
*
* ```json
* {
* "data": {
* "id": 3,
* "provider": "openai",
* "provider_name": "OpenAI",
* "api_key_preview": "sk-proj-...xyz",
* "organization_id": null,
* "is_active": true,
* "status": "verified",
* "test_result": {
* "status": "success",
* "message": "Connection successful",
* "model_tested": "gpt-3.5-turbo"
* },
* "created_at": "2025-11-19T12:00:00Z"
* },
* "message": "Credentials successfully added and verified"
* }
* ```
*
* @tags Credentials
*
* @param Request $request
* @return JsonResponse
*/
public function store(Request $request): JsonResponse
{
$validator = Validator::make($request->all(), [
'provider' => 'required|string|in:openai,anthropic,gemini,deepseek,mistral',
'api_key' => 'required|string|min:10',
'organization_id' => 'nullable|string',
'test_connection' => 'sometimes|boolean',
]);
if ($validator->fails()) {
return response()->json([
'error' => [
'code' => 'validation_error',
'message' => 'Invalid request data',
'status' => 422,
'details' => $validator->errors(),
],
], 422);
}
$user = $request->user();
// Check if credentials already exist for this provider
$existing = GatewayUserCredential::where('gateway_user_id', $user->user_id)
->where('provider', $request->input('provider'))
->first();
if ($existing) {
return response()->json([
'error' => [
'code' => 'already_exists',
'message' => "Credentials for provider '{$request->input('provider')}' already exist. Use PUT to update.",
'status' => 409,
],
], 409);
}
// Create credentials
$credential = new GatewayUserCredential();
$credential->gateway_user_id = $user->user_id;
$credential->provider = $request->input('provider');
$credential->api_key = $request->input('api_key'); // Will be encrypted by model
$credential->organization_id = $request->input('organization_id');
$credential->is_active = true;
// Test connection if requested
if ($request->input('test_connection', true)) {
$testResult = $this->testCredentials($credential);
$credential->test_status = $testResult['status'];
$credential->test_error = $testResult['error'] ?? null;
$credential->last_tested_at = now();
if ($testResult['status'] !== 'success') {
return response()->json([
'error' => [
'code' => 'test_failed',
'message' => 'Credential test failed',
'status' => 400,
'details' => [
'test_error' => $testResult['error'],
],
],
], 400);
}
}
$credential->save();
return response()->json([
'data' => [
'id' => $credential->id,
'provider' => $credential->provider,
'provider_name' => $this->getProviderName($credential->provider),
'api_key_preview' => $this->maskApiKey($credential->api_key),
'organization_id' => $credential->organization_id,
'is_active' => $credential->is_active,
'status' => $credential->test_status ?? 'not_tested',
'test_result' => $credential->last_tested_at ? [
'status' => $credential->test_status,
'message' => $credential->test_error ?: 'Connection successful',
] : null,
'created_at' => $credential->created_at->toIso8601String(),
],
'message' => $request->input('test_connection', true)
? 'Credentials successfully added and verified'
: 'Credentials successfully added',
], 201);
}
/**
* Update existing credentials
*
* Update credentials for an existing provider. Can update API key,
* organization ID, or active status.
*
* ## Request Body
*
* ```json
* {
* "api_key": "sk-proj-new-key-...",
* "organization_id": "org-123",
* "is_active": true,
* "test_connection": true
* }
* ```
*
* @tags Credentials
*
* @param Request $request
* @param int $id
* @return JsonResponse
*/
public function update(Request $request, int $id): JsonResponse
{
$validator = Validator::make($request->all(), [
'api_key' => 'sometimes|string|min:10',
'organization_id' => 'nullable|string',
'is_active' => 'sometimes|boolean',
'test_connection' => 'sometimes|boolean',
]);
if ($validator->fails()) {
return response()->json([
'error' => [
'code' => 'validation_error',
'message' => 'Invalid request data',
'status' => 422,
'details' => $validator->errors(),
],
], 422);
}
$user = $request->user();
$credential = GatewayUserCredential::where('id', $id)
->where('gateway_user_id', $user->user_id)
->first();
if (!$credential) {
return response()->json([
'error' => [
'code' => 'not_found',
'message' => 'Credentials not found',
'status' => 404,
],
], 404);
}
// Update fields
if ($request->has('api_key')) {
$credential->api_key = $request->input('api_key');
}
if ($request->has('organization_id')) {
$credential->organization_id = $request->input('organization_id');
}
if ($request->has('is_active')) {
$credential->is_active = $request->input('is_active');
}
// Test connection if requested or if API key changed
if ($request->input('test_connection', $request->has('api_key'))) {
$testResult = $this->testCredentials($credential);
$credential->test_status = $testResult['status'];
$credential->test_error = $testResult['error'] ?? null;
$credential->last_tested_at = now();
if ($testResult['status'] !== 'success') {
return response()->json([
'error' => [
'code' => 'test_failed',
'message' => 'Credential test failed',
'status' => 400,
'details' => [
'test_error' => $testResult['error'],
],
],
], 400);
}
}
$credential->save();
return response()->json([
'data' => [
'id' => $credential->id,
'provider' => $credential->provider,
'provider_name' => $this->getProviderName($credential->provider),
'api_key_preview' => $this->maskApiKey($credential->api_key),
'organization_id' => $credential->organization_id,
'is_active' => $credential->is_active,
'status' => $credential->test_status ?? 'not_tested',
'test_result' => $credential->last_tested_at ? [
'status' => $credential->test_status,
'message' => $credential->test_error ?: 'Connection successful',
] : null,
'updated_at' => $credential->updated_at->toIso8601String(),
],
'message' => 'Credentials successfully updated',
]);
}
/**
* Delete credentials
*
* Remove credentials for a specific provider. This will prevent any further
* requests to this provider.
*
* @tags Credentials
*
* @param Request $request
* @param int $id
* @return JsonResponse
*/
public function destroy(Request $request, int $id): JsonResponse
{
$user = $request->user();
$credential = GatewayUserCredential::where('id', $id)
->where('gateway_user_id', $user->user_id)
->first();
if (!$credential) {
return response()->json([
'error' => [
'code' => 'not_found',
'message' => 'Credentials not found',
'status' => 404,
],
], 404);
}
$provider = $credential->provider;
$credential->delete();
return response()->json([
'message' => "Credentials for provider '{$provider}' successfully deleted",
]);
}
/**
* Test credentials without saving changes
*
* Test if credentials are valid by making a test request to the provider.
* Does not modify the stored credentials.
*
* ## Example Response
*
* ```json
* {
* "status": "success",
* "message": "Connection successful",
* "details": {
* "provider": "openai",
* "model_tested": "gpt-3.5-turbo",
* "response_time_ms": 245,
* "tested_at": "2025-11-19T12:05:00Z"
* }
* }
* ```
*
* @tags Credentials
*
* @param Request $request
* @param int $id
* @return JsonResponse
*/
public function test(Request $request, int $id): JsonResponse
{
$user = $request->user();
$credential = GatewayUserCredential::where('id', $id)
->where('gateway_user_id', $user->user_id)
->first();
if (!$credential) {
return response()->json([
'error' => [
'code' => 'not_found',
'message' => 'Credentials not found',
'status' => 404,
],
], 404);
}
$testResult = $this->testCredentials($credential);
// Update test results
$credential->test_status = $testResult['status'];
$credential->test_error = $testResult['error'] ?? null;
$credential->last_tested_at = now();
$credential->save();
if ($testResult['status'] !== 'success') {
return response()->json([
'status' => 'failed',
'message' => 'Connection test failed',
'error' => $testResult['error'],
'tested_at' => now()->toIso8601String(),
], 400);
}
return response()->json([
'status' => 'success',
'message' => 'Connection successful',
'details' => [
'provider' => $credential->provider,
'tested_at' => now()->toIso8601String(),
],
]);
}
/**
* Test credentials by making a simple API call
*/
private function testCredentials(GatewayUserCredential $credential): array
{
try {
$provider = \App\Services\LLM\ProviderFactory::create(
$credential->provider,
$credential->api_key
);
// Try to get models list as a simple test
$models = $provider->getAvailableModels();
if (empty($models)) {
return [
'status' => 'failed',
'error' => 'No models returned from provider',
];
}
return [
'status' => 'success',
];
} catch (\Exception $e) {
Log::error('Credential test failed', [
'provider' => $credential->provider,
'error' => $e->getMessage(),
]);
return [
'status' => 'failed',
'error' => $e->getMessage(),
];
}
}
/**
* Mask API key for display
*/
private function maskApiKey(string $apiKey): string
{
$length = strlen($apiKey);
if ($length <= 8) {
return str_repeat('*', $length);
}
// Show first 4 and last 4 characters
return substr($apiKey, 0, 4) . '...' . substr($apiKey, -4);
}
/**
* Get human-readable provider name
*/
private function getProviderName(string $provider): string
{
return match ($provider) {
'openai' => 'OpenAI',
'anthropic' => 'Anthropic',
'gemini' => 'Google Gemini',
'deepseek' => 'DeepSeek',
'mistral' => 'Mistral AI',
default => ucfirst($provider),
};
}
}