Files
wtrinkl 6573e15ba4 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
2025-11-18 22:18:36 +01:00

199 lines
10 KiB
PHP
Raw Permalink Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<x-app-layout>
<x-slot name="header">
<div class="flex justify-between items-center">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Add Provider Credentials') }}
</h2>
<a href="{{ route('admin.credentials.index') }}" class="bg-gray-500 hover:bg-gray-700 text-white font-bold py-2 px-4 rounded">
Back to List
</a>
</div>
</x-slot>
<div class="py-12">
<div class="max-w-3xl mx-auto sm:px-6 lg:px-8">
@if(session('error'))
<div class="bg-red-100 border border-red-400 text-red-700 px-4 py-3 rounded mb-4">
{{ session('error') }}
</div>
@endif
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<form method="POST" action="{{ route('admin.credentials.store') }}" class="space-y-6">
@csrf
<!-- User Selection -->
<div>
<label for="user_id" class="block text-sm font-medium text-gray-700 mb-2">
User <span class="text-red-500">*</span>
</label>
<select
name="user_id"
id="user_id"
required
class="w-full rounded-md border-gray-300 @error('user_id') border-red-500 @enderror"
>
<option value="">Select a user</option>
@foreach($users as $user)
<option value="{{ $user->id }}" {{ old('user_id') == $user->id ? 'selected' : '' }}>
{{ $user->name }} ({{ $user->email }})
</option>
@endforeach
</select>
@error('user_id')
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
@enderror
</div>
<!-- Provider Selection -->
<div>
<label for="provider" class="block text-sm font-medium text-gray-700 mb-2">
AI Provider <span class="text-red-500">*</span>
</label>
<select
name="provider"
id="provider"
required
class="w-full rounded-md border-gray-300 @error('provider') border-red-500 @enderror"
onchange="updateProviderHelp()"
>
<option value="">Select a provider</option>
@foreach($providers as $key => $label)
<option value="{{ $key }}" {{ old('provider') == $key ? 'selected' : '' }}>
{{ $label }}
</option>
@endforeach
</select>
@error('provider')
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
@enderror
<!-- Provider-specific help text -->
<div id="provider-help" class="mt-2 text-sm text-gray-600">
<p class="hidden" data-provider="openai">
📝 Get your API key from: <a href="https://platform.openai.com/api-keys" target="_blank" class="text-blue-600 hover:underline">OpenAI Dashboard</a>
</p>
<p class="hidden" data-provider="anthropic">
📝 Get your API key from: <a href="https://console.anthropic.com/settings/keys" target="_blank" class="text-blue-600 hover:underline">Anthropic Console</a>
</p>
<p class="hidden" data-provider="mistral">
📝 Get your API key from: <a href="https://console.mistral.ai/api-keys" target="_blank" class="text-blue-600 hover:underline">Mistral Console</a>
</p>
<p class="hidden" data-provider="gemini">
📝 Get your API key from: <a href="https://makersuite.google.com/app/apikey" target="_blank" class="text-blue-600 hover:underline">Google AI Studio</a>
</p>
<p class="hidden" data-provider="deepseek">
📝 Get your API key from: <a href="https://platform.deepseek.com/api_keys" target="_blank" class="text-blue-600 hover:underline">DeepSeek Platform</a>
</p>
</div>
</div>
<!-- API Key -->
<div>
<label for="api_key" class="block text-sm font-medium text-gray-700 mb-2">
API Key <span class="text-red-500">*</span>
</label>
<input
type="password"
name="api_key"
id="api_key"
required
placeholder="sk-..."
class="w-full rounded-md border-gray-300 @error('api_key') border-red-500 @enderror"
value="{{ old('api_key') }}"
>
@error('api_key')
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
@enderror
<p class="text-xs text-gray-500 mt-1">
🔒 The API key will be encrypted before storage
</p>
</div>
<!-- Organization ID (Optional) -->
<div>
<label for="organization_id" class="block text-sm font-medium text-gray-700 mb-2">
Organization ID <span class="text-gray-400">(optional)</span>
</label>
<input
type="text"
name="organization_id"
id="organization_id"
placeholder="org-..."
class="w-full rounded-md border-gray-300 @error('organization_id') border-red-500 @enderror"
value="{{ old('organization_id') }}"
>
@error('organization_id')
<p class="text-red-500 text-xs mt-1">{{ $message }}</p>
@enderror
<p class="text-xs text-gray-500 mt-1">
Required for some OpenAI enterprise accounts
</p>
</div>
<!-- Active Status -->
<div class="flex items-center">
<input
type="checkbox"
name="is_active"
id="is_active"
value="1"
checked
class="rounded border-gray-300 text-blue-600 shadow-sm focus:border-blue-300 focus:ring focus:ring-blue-200 focus:ring-opacity-50"
>
<label for="is_active" class="ml-2 block text-sm text-gray-900">
Active (enable for immediate use)
</label>
</div>
<!-- Submit Button -->
<div class="flex items-center justify-end space-x-3">
<a href="{{ route('admin.credentials.index') }}" class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded">
Cancel
</a>
<button type="submit" class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Add Credentials
</button>
</div>
</form>
</div>
</div>
<!-- Info Box -->
<div class="mt-6 bg-blue-50 border border-blue-200 rounded-lg p-4">
<h3 class="font-semibold text-blue-900 mb-2"> Important Information</h3>
<ul class="text-sm text-blue-800 space-y-1 list-disc list-inside">
<li>Each user can only have one set of credentials per provider</li>
<li>API keys are encrypted using Laravel's encryption (AES-256-CBC)</li>
<li>You can test credentials after creation to verify they work</li>
<li>Usage and costs will be tracked per user and provider</li>
</ul>
</div>
</div>
</div>
@push('scripts')
<script>
function updateProviderHelp() {
const provider = document.getElementById('provider').value;
const helpTexts = document.querySelectorAll('#provider-help p');
helpTexts.forEach(text => text.classList.add('hidden'));
if (provider) {
const selectedHelp = document.querySelector(`#provider-help p[data-provider="${provider}"]`);
if (selectedHelp) {
selectedHelp.classList.remove('hidden');
}
}
}
// Initialize on page load if provider is already selected
document.addEventListener('DOMContentLoaded', function() {
updateProviderHelp();
});
</script>
@endpush
</x-app-layout>