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:
511
laravel-app/app/Http/Controllers/Api/CredentialController.php
Normal file
511
laravel-app/app/Http/Controllers/Api/CredentialController.php
Normal 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),
|
||||
};
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user