Add complete Laravel LLM Gateway implementation
Core Features: - Multi-provider support (OpenAI, Anthropic, DeepSeek, Gemini, Mistral) - Provider service architecture with abstract base class - Dynamic model discovery from provider APIs - Encrypted per-user provider credentials storage Admin Interface: - Complete admin panel with Livewire components - User management with CRUD operations - API key management with testing capabilities - Budget system with limits and reset schedules - Usage logs with filtering and CSV export - Model pricing management with cost calculator - Dashboard with Chart.js visualizations Database Schema: - MariaDB migrations for all tables - User provider credentials (encrypted) - LLM request logging - Budget tracking and rate limiting - Model pricing configuration API Implementation: - OpenAI-compatible endpoints - Budget checking middleware - Rate limit enforcement - Request logging jobs - Cost calculation service Testing: - Unit tests for all provider services - Provider factory tests - Cost calculator tests Documentation: - Admin user seeder - Model pricing seeder - Configuration files
This commit is contained in:
478
laravel-app/app/Http/Controllers/Admin/CredentialController.php
Normal file
478
laravel-app/app/Http/Controllers/Admin/CredentialController.php
Normal file
@@ -0,0 +1,478 @@
|
||||
<?php
|
||||
|
||||
namespace App\Http\Controllers\Admin;
|
||||
|
||||
use App\Http\Controllers\Controller;
|
||||
use App\Models\UserProviderCredential;
|
||||
use App\Models\User;
|
||||
use Illuminate\Http\Request;
|
||||
use Illuminate\Support\Facades\Log;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
|
||||
class CredentialController extends Controller
|
||||
{
|
||||
/**
|
||||
* Display a listing of provider credentials.
|
||||
*/
|
||||
public function index(Request $request)
|
||||
{
|
||||
$query = UserProviderCredential::with('user');
|
||||
|
||||
// Filter by provider
|
||||
if ($request->has('provider') && $request->provider) {
|
||||
$query->where('provider', $request->provider);
|
||||
}
|
||||
|
||||
// Filter by user
|
||||
if ($request->has('user_id') && $request->user_id) {
|
||||
$query->where('user_id', $request->user_id);
|
||||
}
|
||||
|
||||
// Filter by status
|
||||
if ($request->has('status')) {
|
||||
switch ($request->status) {
|
||||
case 'active':
|
||||
$query->where('is_active', true);
|
||||
break;
|
||||
case 'inactive':
|
||||
$query->where('is_active', false);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Search
|
||||
if ($request->has('search') && $request->search) {
|
||||
$query->whereHas('user', function ($q) use ($request) {
|
||||
$q->where('name', 'like', '%' . $request->search . '%')
|
||||
->orWhere('email', 'like', '%' . $request->search . '%');
|
||||
});
|
||||
}
|
||||
|
||||
// Sort
|
||||
$sortBy = $request->get('sort_by', 'created_at');
|
||||
$sortOrder = $request->get('sort_order', 'desc');
|
||||
$query->orderBy($sortBy, $sortOrder);
|
||||
|
||||
$credentials = $query->paginate(20)->withQueryString();
|
||||
|
||||
// Get all users and providers for filters
|
||||
$users = User::orderBy('name')->get();
|
||||
$providers = ['openai', 'anthropic', 'mistral', 'gemini', 'deepseek'];
|
||||
|
||||
return view('admin.credentials.index', compact('credentials', 'users', 'providers'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for creating a new credential.
|
||||
*/
|
||||
public function create()
|
||||
{
|
||||
$users = User::orderBy('name')->get();
|
||||
$providers = [
|
||||
'openai' => 'OpenAI',
|
||||
'anthropic' => 'Anthropic (Claude)',
|
||||
'mistral' => 'Mistral AI',
|
||||
'gemini' => 'Google Gemini',
|
||||
'deepseek' => 'DeepSeek'
|
||||
];
|
||||
|
||||
return view('admin.credentials.create', compact('users', 'providers'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Store a newly created credential.
|
||||
*/
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'user_id' => 'required|exists:users,id',
|
||||
'provider' => 'required|in:openai,anthropic,mistral,gemini,deepseek',
|
||||
'api_key' => 'required|string|min:10',
|
||||
'organization_id' => 'nullable|string|max:255',
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
try {
|
||||
// Check for duplicate
|
||||
$existing = UserProviderCredential::where('user_id', $validated['user_id'])
|
||||
->where('provider', $validated['provider'])
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
return back()
|
||||
->withInput()
|
||||
->with('error', 'This user already has credentials for ' . ucfirst($validated['provider']) . '. Please edit the existing credentials instead.');
|
||||
}
|
||||
|
||||
// Create credential (encryption happens automatically in model)
|
||||
$credential = UserProviderCredential::create([
|
||||
'user_id' => $validated['user_id'],
|
||||
'provider' => $validated['provider'],
|
||||
'api_key' => $validated['api_key'],
|
||||
'organization_id' => $validated['organization_id'] ?? null,
|
||||
'is_active' => $validated['is_active'] ?? true,
|
||||
]);
|
||||
|
||||
return redirect()->route('admin.credentials.index')
|
||||
->with('success', 'Provider credentials added successfully!');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to create provider credential', [
|
||||
'error' => $e->getMessage(),
|
||||
'user_id' => $validated['user_id'],
|
||||
'provider' => $validated['provider']
|
||||
]);
|
||||
|
||||
return back()
|
||||
->withInput()
|
||||
->with('error', 'Failed to add credentials: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Display the specified credential.
|
||||
*/
|
||||
public function show(UserProviderCredential $credential)
|
||||
{
|
||||
$credential->load('user');
|
||||
|
||||
// Get usage statistics
|
||||
$stats = [
|
||||
'total_requests' => $credential->user->llmRequests()
|
||||
->where('provider', $credential->provider)
|
||||
->count(),
|
||||
'total_cost' => $credential->user->llmRequests()
|
||||
->where('provider', $credential->provider)
|
||||
->sum('total_cost'),
|
||||
'total_tokens' => $credential->user->llmRequests()
|
||||
->where('provider', $credential->provider)
|
||||
->sum('total_tokens'),
|
||||
'last_30_days_requests' => $credential->user->llmRequests()
|
||||
->where('provider', $credential->provider)
|
||||
->where('created_at', '>=', now()->subDays(30))
|
||||
->count(),
|
||||
];
|
||||
|
||||
return view('admin.credentials.show', compact('credential', 'stats'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified credential.
|
||||
*/
|
||||
public function edit(UserProviderCredential $credential)
|
||||
{
|
||||
$credential->load('user');
|
||||
|
||||
$providers = [
|
||||
'openai' => 'OpenAI',
|
||||
'anthropic' => 'Anthropic (Claude)',
|
||||
'mistral' => 'Mistral AI',
|
||||
'gemini' => 'Google Gemini',
|
||||
'deepseek' => 'DeepSeek'
|
||||
];
|
||||
|
||||
return view('admin.credentials.edit', compact('credential', 'providers'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified credential.
|
||||
*/
|
||||
public function update(Request $request, UserProviderCredential $credential)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'api_key' => 'nullable|string|min:10',
|
||||
'organization_id' => 'nullable|string|max:255',
|
||||
'is_active' => 'boolean',
|
||||
]);
|
||||
|
||||
try {
|
||||
// Only update API key if provided
|
||||
if (!empty($validated['api_key'])) {
|
||||
$credential->api_key = $validated['api_key'];
|
||||
}
|
||||
|
||||
$credential->organization_id = $validated['organization_id'] ?? null;
|
||||
$credential->is_active = $validated['is_active'] ?? true;
|
||||
$credential->save();
|
||||
|
||||
return redirect()->route('admin.credentials.index')
|
||||
->with('success', 'Provider credentials updated successfully!');
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to update provider credential', [
|
||||
'error' => $e->getMessage(),
|
||||
'credential_id' => $credential->id
|
||||
]);
|
||||
|
||||
return back()
|
||||
->withInput()
|
||||
->with('error', 'Failed to update credentials: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove the specified credential.
|
||||
*/
|
||||
public function destroy(UserProviderCredential $credential)
|
||||
{
|
||||
try {
|
||||
$provider = ucfirst($credential->provider);
|
||||
$userName = $credential->user->name;
|
||||
|
||||
$credential->delete();
|
||||
|
||||
return redirect()->route('admin.credentials.index')
|
||||
->with('success', "Provider credentials for {$provider} (User: {$userName}) deleted successfully!");
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to delete provider credential', [
|
||||
'error' => $e->getMessage(),
|
||||
'credential_id' => $credential->id
|
||||
]);
|
||||
|
||||
return back()
|
||||
->with('error', 'Failed to delete credentials: ' . $e->getMessage());
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test the API key validity.
|
||||
*/
|
||||
public function test(UserProviderCredential $credential)
|
||||
{
|
||||
try {
|
||||
$result = $this->testProviderApiKey($credential->provider, $credential->api_key);
|
||||
|
||||
if ($result['success']) {
|
||||
return response()->json([
|
||||
'success' => true,
|
||||
'message' => 'API key is valid and working!',
|
||||
'details' => $result['details'] ?? null
|
||||
]);
|
||||
} else {
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => $result['message'] ?? 'API key validation failed',
|
||||
'error' => $result['error'] ?? null
|
||||
], 400);
|
||||
}
|
||||
|
||||
} catch (\Exception $e) {
|
||||
Log::error('Failed to test provider credential', [
|
||||
'error' => $e->getMessage(),
|
||||
'credential_id' => $credential->id
|
||||
]);
|
||||
|
||||
return response()->json([
|
||||
'success' => false,
|
||||
'message' => 'Test failed: ' . $e->getMessage()
|
||||
], 500);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Test provider API key validity
|
||||
*/
|
||||
private function testProviderApiKey(string $provider, string $apiKey): array
|
||||
{
|
||||
switch ($provider) {
|
||||
case 'openai':
|
||||
return $this->testOpenAI($apiKey);
|
||||
|
||||
case 'anthropic':
|
||||
return $this->testAnthropic($apiKey);
|
||||
|
||||
case 'mistral':
|
||||
return $this->testMistral($apiKey);
|
||||
|
||||
case 'gemini':
|
||||
return $this->testGemini($apiKey);
|
||||
|
||||
case 'deepseek':
|
||||
return $this->testDeepSeek($apiKey);
|
||||
|
||||
default:
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Unsupported provider'
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function testOpenAI(string $apiKey): array
|
||||
{
|
||||
try {
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => 'Bearer ' . $apiKey,
|
||||
'Content-Type' => 'application/json',
|
||||
])->timeout(10)->post('https://api.openai.com/v1/chat/completions', [
|
||||
'model' => 'gpt-3.5-turbo',
|
||||
'messages' => [
|
||||
['role' => 'user', 'content' => 'test']
|
||||
],
|
||||
'max_tokens' => 5
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
return [
|
||||
'success' => true,
|
||||
'details' => 'Model: gpt-3.5-turbo accessible'
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Invalid API key or insufficient permissions',
|
||||
'error' => $response->body()
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Connection failed',
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function testAnthropic(string $apiKey): array
|
||||
{
|
||||
try {
|
||||
$response = Http::withHeaders([
|
||||
'x-api-key' => $apiKey,
|
||||
'anthropic-version' => '2023-06-01',
|
||||
'Content-Type' => 'application/json',
|
||||
])->timeout(10)->post('https://api.anthropic.com/v1/messages', [
|
||||
'model' => 'claude-3-haiku-20240307',
|
||||
'max_tokens' => 10,
|
||||
'messages' => [
|
||||
['role' => 'user', 'content' => 'test']
|
||||
]
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
return [
|
||||
'success' => true,
|
||||
'details' => 'Model: Claude 3 Haiku accessible'
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Invalid API key or insufficient permissions',
|
||||
'error' => $response->body()
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Connection failed',
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function testMistral(string $apiKey): array
|
||||
{
|
||||
try {
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => 'Bearer ' . $apiKey,
|
||||
'Content-Type' => 'application/json',
|
||||
])->timeout(10)->post('https://api.mistral.ai/v1/chat/completions', [
|
||||
'model' => 'mistral-tiny',
|
||||
'messages' => [
|
||||
['role' => 'user', 'content' => 'test']
|
||||
],
|
||||
'max_tokens' => 5
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
return [
|
||||
'success' => true,
|
||||
'details' => 'Model: Mistral Tiny accessible'
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Invalid API key or insufficient permissions',
|
||||
'error' => $response->body()
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Connection failed',
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function testGemini(string $apiKey): array
|
||||
{
|
||||
try {
|
||||
$response = Http::timeout(10)
|
||||
->post("https://generativelanguage.googleapis.com/v1beta/models/gemini-pro:generateContent?key={$apiKey}", [
|
||||
'contents' => [
|
||||
['parts' => [['text' => 'test']]]
|
||||
]
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
return [
|
||||
'success' => true,
|
||||
'details' => 'Model: Gemini Pro accessible'
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Invalid API key or insufficient permissions',
|
||||
'error' => $response->body()
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Connection failed',
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
private function testDeepSeek(string $apiKey): array
|
||||
{
|
||||
try {
|
||||
$response = Http::withHeaders([
|
||||
'Authorization' => 'Bearer ' . $apiKey,
|
||||
'Content-Type' => 'application/json',
|
||||
])->timeout(10)->post('https://api.deepseek.com/v1/chat/completions', [
|
||||
'model' => 'deepseek-chat',
|
||||
'messages' => [
|
||||
['role' => 'user', 'content' => 'test']
|
||||
],
|
||||
'max_tokens' => 5
|
||||
]);
|
||||
|
||||
if ($response->successful()) {
|
||||
return [
|
||||
'success' => true,
|
||||
'details' => 'Model: DeepSeek Chat accessible'
|
||||
];
|
||||
}
|
||||
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Invalid API key or insufficient permissions',
|
||||
'error' => $response->body()
|
||||
];
|
||||
|
||||
} catch (\Exception $e) {
|
||||
return [
|
||||
'success' => false,
|
||||
'message' => 'Connection failed',
|
||||
'error' => $e->getMessage()
|
||||
];
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user