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
173 lines
5.4 KiB
PHP
173 lines
5.4 KiB
PHP
<?php
|
|
|
|
namespace App\Services\LLM;
|
|
|
|
use App\Models\User;
|
|
use App\Models\UserProviderCredential;
|
|
use App\Exceptions\{ProviderException, InsufficientBudgetException, RateLimitExceededException};
|
|
use Illuminate\Support\Facades\Log;
|
|
|
|
class GatewayService
|
|
{
|
|
public function __construct(
|
|
private CostCalculator $costCalculator,
|
|
private RequestLogger $requestLogger,
|
|
) {}
|
|
|
|
/**
|
|
* Process a chat completion request through the gateway
|
|
*
|
|
* @param User $user
|
|
* @param string $provider
|
|
* @param string $model
|
|
* @param array $messages
|
|
* @param array $options
|
|
* @param string|null $ipAddress
|
|
* @param string|null $userAgent
|
|
* @return array
|
|
* @throws ProviderException
|
|
* @throws InsufficientBudgetException
|
|
*/
|
|
public function chatCompletion(
|
|
User $user,
|
|
string $provider,
|
|
string $model,
|
|
array $messages,
|
|
array $options = [],
|
|
?string $ipAddress = null,
|
|
?string $userAgent = null
|
|
): array {
|
|
$startTime = microtime(true);
|
|
|
|
// 1. Get user's API credentials
|
|
$credential = $this->getUserCredential($user, $provider);
|
|
|
|
// 2. Create provider instance
|
|
$providerInstance = ProviderFactory::create($provider, $credential->api_key);
|
|
|
|
// 3. Build request payload
|
|
$requestPayload = [
|
|
'provider' => $provider,
|
|
'model' => $model,
|
|
'messages' => $messages,
|
|
'options' => $options,
|
|
];
|
|
|
|
try {
|
|
// 4. Make the API request
|
|
$response = $providerInstance->chatCompletion($messages, array_merge($options, ['model' => $model]));
|
|
|
|
// 5. Normalize response
|
|
$normalized = $providerInstance->normalizeResponse($response);
|
|
|
|
// 6. Calculate response time
|
|
$responseTimeMs = (int) round((microtime(true) - $startTime) * 1000);
|
|
|
|
// 7. Calculate costs
|
|
$costs = $this->costCalculator->calculate(
|
|
$provider,
|
|
$normalized['model'],
|
|
$normalized['usage']['prompt_tokens'],
|
|
$normalized['usage']['completion_tokens']
|
|
);
|
|
|
|
// 8. Log request asynchronously
|
|
$requestId = $this->requestLogger->logSuccess(
|
|
$user->id,
|
|
$provider,
|
|
$normalized['model'],
|
|
$requestPayload,
|
|
$normalized,
|
|
$costs,
|
|
$responseTimeMs,
|
|
$ipAddress,
|
|
$userAgent
|
|
);
|
|
|
|
// 9. Update user budget (synchronously for accuracy)
|
|
$this->updateUserBudget($user, $costs['total_cost']);
|
|
|
|
// 10. Return response with metadata
|
|
return [
|
|
'success' => true,
|
|
'request_id' => $requestId,
|
|
'provider' => $provider,
|
|
'model' => $normalized['model'],
|
|
'content' => $normalized['content'],
|
|
'role' => $normalized['role'],
|
|
'finish_reason' => $normalized['finish_reason'],
|
|
'usage' => $normalized['usage'],
|
|
'cost' => $costs,
|
|
'response_time_ms' => $responseTimeMs,
|
|
];
|
|
|
|
} catch (ProviderException $e) {
|
|
// Log failure
|
|
$this->requestLogger->logFailure(
|
|
$user->id,
|
|
$provider,
|
|
$model,
|
|
$requestPayload,
|
|
$e->getMessage(),
|
|
$e->getCode(),
|
|
$ipAddress,
|
|
$userAgent
|
|
);
|
|
|
|
throw $e;
|
|
}
|
|
}
|
|
|
|
/**
|
|
* Get user's credential for a provider
|
|
*/
|
|
private function getUserCredential(User $user, string $provider): UserProviderCredential
|
|
{
|
|
$credential = UserProviderCredential::where('user_id', $user->id)
|
|
->where('provider', $provider)
|
|
->where('is_active', true)
|
|
->first();
|
|
|
|
if (!$credential) {
|
|
throw new ProviderException(
|
|
"No active API credentials found for provider: {$provider}",
|
|
400
|
|
);
|
|
}
|
|
|
|
// Update last used timestamp
|
|
$credential->update(['last_used_at' => now()]);
|
|
|
|
return $credential;
|
|
}
|
|
|
|
/**
|
|
* Update user's budget with spending
|
|
*/
|
|
private function updateUserBudget(User $user, float $cost): void
|
|
{
|
|
$budget = $user->budget;
|
|
|
|
if (!$budget) {
|
|
return; // No budget configured
|
|
}
|
|
|
|
$budget->increment('current_month_spending', $cost);
|
|
$budget->increment('current_day_spending', $cost);
|
|
|
|
// Check if budget exceeded
|
|
if ($budget->current_month_spending >= $budget->monthly_limit) {
|
|
$budget->update(['is_budget_exceeded' => true]);
|
|
}
|
|
|
|
// Check alert threshold
|
|
if ($budget->alert_threshold_percentage) {
|
|
$threshold = $budget->monthly_limit * ($budget->alert_threshold_percentage / 100);
|
|
if ($budget->current_month_spending >= $threshold && !$budget->last_alert_sent_at) {
|
|
// TODO: Dispatch alert notification
|
|
$budget->update(['last_alert_sent_at' => now()]);
|
|
}
|
|
}
|
|
}
|
|
}
|