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:
147
laravel-app/tests/Unit/Services/GeminiProviderTest.php
Normal file
147
laravel-app/tests/Unit/Services/GeminiProviderTest.php
Normal file
@@ -0,0 +1,147 @@
|
||||
<?php
|
||||
|
||||
namespace Tests\Unit\Services;
|
||||
|
||||
use Tests\TestCase;
|
||||
use App\Services\LLM\Providers\GeminiProvider;
|
||||
use App\Models\ModelPricing;
|
||||
use Illuminate\Support\Facades\Http;
|
||||
use Illuminate\Support\Facades\Cache;
|
||||
use Illuminate\Foundation\Testing\RefreshDatabase;
|
||||
|
||||
class GeminiProviderTest extends TestCase
|
||||
{
|
||||
use RefreshDatabase;
|
||||
|
||||
private GeminiProvider $provider;
|
||||
|
||||
protected function setUp(): void
|
||||
{
|
||||
parent::setUp();
|
||||
$this->provider = new GeminiProvider('test-api-key');
|
||||
}
|
||||
|
||||
public function test_builds_request_correctly(): void
|
||||
{
|
||||
$messages = [
|
||||
['role' => 'user', 'content' => 'Hello, Gemini!']
|
||||
];
|
||||
|
||||
$options = [
|
||||
'model' => 'gemini-pro',
|
||||
'temperature' => 0.9,
|
||||
'max_tokens' => 2000
|
||||
];
|
||||
|
||||
$reflection = new \ReflectionClass($this->provider);
|
||||
$method = $reflection->getMethod('buildRequest');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$result = $method->invoke($this->provider, $messages, $options);
|
||||
|
||||
$this->assertArrayHasKey('contents', $result);
|
||||
$this->assertCount(1, $result['contents']);
|
||||
$this->assertEquals('user', $result['contents'][0]['role']);
|
||||
$this->assertEquals('Hello, Gemini!', $result['contents'][0]['parts'][0]['text']);
|
||||
|
||||
$this->assertArrayHasKey('generationConfig', $result);
|
||||
$this->assertEquals(0.9, $result['generationConfig']['temperature']);
|
||||
$this->assertEquals(2000, $result['generationConfig']['maxOutputTokens']);
|
||||
}
|
||||
|
||||
public function test_converts_system_messages_to_user(): void
|
||||
{
|
||||
$messages = [
|
||||
['role' => 'system', 'content' => 'You are helpful'],
|
||||
['role' => 'user', 'content' => 'Hello']
|
||||
];
|
||||
|
||||
$reflection = new \ReflectionClass($this->provider);
|
||||
$method = $reflection->getMethod('buildRequest');
|
||||
$method->setAccessible(true);
|
||||
|
||||
$result = $method->invoke($this->provider, $messages, []);
|
||||
|
||||
$this->assertEquals('user', $result['contents'][0]['role']);
|
||||
$this->assertEquals('user', $result['contents'][1]['role']);
|
||||
}
|
||||
|
||||
public function test_normalizes_response_correctly(): void
|
||||
{
|
||||
$rawResponse = [
|
||||
'candidates' => [
|
||||
[
|
||||
'content' => [
|
||||
'parts' => [
|
||||
['text' => 'Hello! How can I help you today?']
|
||||
]
|
||||
],
|
||||
'finishReason' => 'STOP'
|
||||
]
|
||||
],
|
||||
'usageMetadata' => [
|
||||
'promptTokenCount' => 8,
|
||||
'candidatesTokenCount' => 15,
|
||||
'totalTokenCount' => 23
|
||||
],
|
||||
'modelVersion' => 'gemini-pro'
|
||||
];
|
||||
|
||||
$normalized = $this->provider->normalizeResponse($rawResponse);
|
||||
|
||||
$this->assertEquals('gemini-pro', $normalized['model']);
|
||||
$this->assertEquals('Hello! How can I help you today?', $normalized['content']);
|
||||
$this->assertEquals('assistant', $normalized['role']);
|
||||
$this->assertEquals('STOP', $normalized['finish_reason']);
|
||||
$this->assertEquals(8, $normalized['usage']['prompt_tokens']);
|
||||
$this->assertEquals(15, $normalized['usage']['completion_tokens']);
|
||||
$this->assertEquals(23, $normalized['usage']['total_tokens']);
|
||||
}
|
||||
|
||||
public function test_calculates_cost_correctly(): void
|
||||
{
|
||||
ModelPricing::updateOrCreate(
|
||||
[
|
||||
'provider' => 'gemini',
|
||||
'model' => 'gemini-pro',
|
||||
'effective_from' => now()->toDateString(),
|
||||
],
|
||||
[
|
||||
'input_price_per_million' => 0.50,
|
||||
'output_price_per_million' => 1.50,
|
||||
'is_active' => true,
|
||||
]
|
||||
);
|
||||
|
||||
Cache::flush();
|
||||
|
||||
$cost = $this->provider->calculateCost(1000, 500, 'gemini-pro');
|
||||
|
||||
// Expected: (1000/1M * 0.50) + (500/1M * 1.50) = 0.0005 + 0.00075 = 0.00125
|
||||
$this->assertEquals(0.00125, $cost);
|
||||
}
|
||||
|
||||
public function test_handles_api_errors(): void
|
||||
{
|
||||
Http::fake([
|
||||
'https://generativelanguage.googleapis.com/*' => Http::response(['error' => 'Invalid API key'], 401)
|
||||
]);
|
||||
|
||||
$this->expectException(\App\Exceptions\ProviderException::class);
|
||||
$this->expectExceptionMessage('Invalid API key');
|
||||
|
||||
$this->provider->chatCompletion([
|
||||
['role' => 'user', 'content' => 'test']
|
||||
]);
|
||||
}
|
||||
|
||||
public function test_get_supported_models(): void
|
||||
{
|
||||
$models = $this->provider->getSupportedModels();
|
||||
|
||||
$this->assertIsArray($models);
|
||||
$this->assertContains('gemini-pro', $models);
|
||||
$this->assertContains('gemini-1.5-pro', $models);
|
||||
$this->assertContains('gemini-1.5-flash', $models);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user