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
114 lines
3.6 KiB
PHP
114 lines
3.6 KiB
PHP
<?php
|
|
|
|
namespace App\Services\LLM\Providers;
|
|
|
|
use App\Models\ModelPricing;
|
|
use Illuminate\Support\Facades\Cache;
|
|
|
|
class AnthropicProvider extends AbstractProvider
|
|
{
|
|
protected string $baseUrl = 'https://api.anthropic.com/v1';
|
|
private string $apiVersion = '2023-06-01';
|
|
|
|
protected function buildRequest(array $messages, array $options): array
|
|
{
|
|
// Anthropic requires system message separate
|
|
$systemMessage = null;
|
|
$formattedMessages = [];
|
|
|
|
foreach ($messages as $message) {
|
|
if ($message['role'] === 'system') {
|
|
$systemMessage = $message['content'];
|
|
} else {
|
|
$formattedMessages[] = $message;
|
|
}
|
|
}
|
|
|
|
$request = array_filter([
|
|
'model' => $options['model'] ?? 'claude-sonnet-4',
|
|
'max_tokens' => $options['max_tokens'] ?? 4096,
|
|
'messages' => $formattedMessages,
|
|
'system' => $systemMessage,
|
|
'temperature' => $options['temperature'] ?? null,
|
|
'top_p' => $options['top_p'] ?? null,
|
|
'stop_sequences' => $options['stop'] ?? null,
|
|
], fn($value) => $value !== null);
|
|
|
|
return $request;
|
|
}
|
|
|
|
protected function getAuthHeaders(): array
|
|
{
|
|
return [
|
|
'x-api-key' => $this->apiKey,
|
|
'anthropic-version' => $this->apiVersion,
|
|
'Content-Type' => 'application/json',
|
|
];
|
|
}
|
|
|
|
public function chatCompletion(array $messages, array $options = []): array
|
|
{
|
|
$data = $this->buildRequest($messages, $options);
|
|
return $this->makeRequest('/messages', $data);
|
|
}
|
|
|
|
public function normalizeResponse(array $response): array
|
|
{
|
|
$content = '';
|
|
if (isset($response['content']) && is_array($response['content'])) {
|
|
foreach ($response['content'] as $block) {
|
|
if ($block['type'] === 'text') {
|
|
$content .= $block['text'];
|
|
}
|
|
}
|
|
}
|
|
|
|
return [
|
|
'id' => $response['id'] ?? null,
|
|
'model' => $response['model'] ?? null,
|
|
'content' => $content,
|
|
'role' => $response['role'] ?? 'assistant',
|
|
'finish_reason' => $response['stop_reason'] ?? null,
|
|
'usage' => [
|
|
'prompt_tokens' => $response['usage']['input_tokens'] ?? 0,
|
|
'completion_tokens' => $response['usage']['output_tokens'] ?? 0,
|
|
'total_tokens' => ($response['usage']['input_tokens'] ?? 0) + ($response['usage']['output_tokens'] ?? 0),
|
|
],
|
|
'raw_response' => $response,
|
|
];
|
|
}
|
|
|
|
public function calculateCost(int $promptTokens, int $completionTokens, string $model): float
|
|
{
|
|
$cacheKey = "pricing:anthropic:{$model}";
|
|
|
|
$pricing = Cache::remember($cacheKey, 3600, function () use ($model) {
|
|
return ModelPricing::where('provider', 'anthropic')
|
|
->where('model', $model)
|
|
->where('is_active', true)
|
|
->first();
|
|
});
|
|
|
|
if (!$pricing) {
|
|
return 0.0;
|
|
}
|
|
|
|
$promptCost = ($promptTokens / 1_000_000) * $pricing->input_price_per_million;
|
|
$completionCost = ($completionTokens / 1_000_000) * $pricing->output_price_per_million;
|
|
|
|
return round($promptCost + $completionCost, 6);
|
|
}
|
|
|
|
public function getSupportedModels(): array
|
|
{
|
|
return [
|
|
'claude-opus-4',
|
|
'claude-sonnet-4',
|
|
'claude-haiku-4',
|
|
'claude-3-opus',
|
|
'claude-3-sonnet',
|
|
'claude-3-haiku',
|
|
];
|
|
}
|
|
}
|