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:
@@ -0,0 +1,49 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,32 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_provider_credentials', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
|
||||
$table->string('provider', 50)->comment('openai, anthropic, mistral, gemini, deepseek');
|
||||
$table->text('api_key')->comment('Encrypted API key');
|
||||
$table->string('organization_id', 255)->nullable()->comment('Optional organization ID');
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->timestamp('last_used_at')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['user_id', 'provider']);
|
||||
$table->index('user_id');
|
||||
$table->index('provider');
|
||||
$table->index('is_active');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_provider_credentials');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,57 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('llm_requests', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
|
||||
$table->string('provider', 50);
|
||||
$table->string('model', 100);
|
||||
|
||||
// Request Details
|
||||
$table->json('request_payload')->comment('Original request');
|
||||
$table->json('response_payload')->nullable()->comment('Provider response');
|
||||
|
||||
// Tokens & Timing
|
||||
$table->unsignedInteger('prompt_tokens')->default(0);
|
||||
$table->unsignedInteger('completion_tokens')->default(0);
|
||||
$table->unsignedInteger('total_tokens')->default(0);
|
||||
$table->unsignedInteger('response_time_ms')->nullable()->comment('Response time in milliseconds');
|
||||
|
||||
// Cost Calculation
|
||||
$table->decimal('prompt_cost', 10, 6)->default(0)->comment('Cost in USD');
|
||||
$table->decimal('completion_cost', 10, 6)->default(0)->comment('Cost in USD');
|
||||
$table->decimal('total_cost', 10, 6)->default(0)->comment('Cost in USD');
|
||||
|
||||
// Status & Error Handling
|
||||
$table->string('status', 20)->default('pending')->comment('pending, success, failed, rate_limited');
|
||||
$table->text('error_message')->nullable();
|
||||
$table->unsignedInteger('http_status')->nullable();
|
||||
|
||||
// Metadata
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->string('user_agent', 500)->nullable();
|
||||
$table->string('request_id', 100)->nullable()->comment('Unique request identifier');
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('user_id');
|
||||
$table->index(['provider', 'model']);
|
||||
$table->index('status');
|
||||
$table->index('created_at');
|
||||
$table->index('request_id');
|
||||
$table->index(['user_id', 'created_at', 'total_cost']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('llm_requests');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,44 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('model_pricing', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('provider', 50);
|
||||
$table->string('model', 100);
|
||||
|
||||
// Pricing (per 1M tokens)
|
||||
$table->decimal('input_price_per_million', 10, 4)->comment('Cost per 1M input tokens in USD');
|
||||
$table->decimal('output_price_per_million', 10, 4)->comment('Cost per 1M output tokens in USD');
|
||||
|
||||
// Model Information
|
||||
$table->unsignedInteger('context_window')->nullable()->comment('Maximum context size');
|
||||
$table->unsignedInteger('max_output_tokens')->nullable()->comment('Maximum output tokens');
|
||||
|
||||
// Metadata
|
||||
$table->boolean('is_active')->default(true);
|
||||
$table->date('effective_from')->default(DB::raw('CURRENT_DATE'));
|
||||
$table->date('effective_until')->nullable();
|
||||
$table->text('notes')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique(['provider', 'model', 'effective_from']);
|
||||
$table->index('provider');
|
||||
$table->index('model');
|
||||
$table->index('is_active');
|
||||
$table->index(['effective_from', 'effective_until']);
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('model_pricing');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,48 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('user_budgets', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
|
||||
|
||||
// Budget Configuration
|
||||
$table->decimal('monthly_limit', 10, 2)->default(0)->comment('Monthly spending limit in USD');
|
||||
$table->decimal('daily_limit', 10, 2)->nullable()->comment('Optional daily limit');
|
||||
|
||||
// Current Period Tracking
|
||||
$table->decimal('current_month_spending', 10, 2)->default(0);
|
||||
$table->decimal('current_day_spending', 10, 2)->default(0);
|
||||
|
||||
// Period Timestamps
|
||||
$table->date('month_started_at');
|
||||
$table->date('day_started_at');
|
||||
|
||||
// Alert Thresholds
|
||||
$table->unsignedInteger('alert_threshold_percentage')->default(80)->comment('Alert at X% of limit');
|
||||
$table->timestamp('last_alert_sent_at')->nullable();
|
||||
|
||||
// Status
|
||||
$table->boolean('is_budget_exceeded')->default(false);
|
||||
$table->boolean('is_active')->default(true);
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique('user_id');
|
||||
$table->index('is_active');
|
||||
$table->index('is_budget_exceeded');
|
||||
$table->index('current_month_spending');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('user_budgets');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,46 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('rate_limits', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->foreignId('user_id')->constrained('users')->onDelete('cascade');
|
||||
|
||||
// Rate Limit Configuration
|
||||
$table->unsignedInteger('requests_per_minute')->default(60);
|
||||
$table->unsignedInteger('requests_per_hour')->default(1000);
|
||||
$table->unsignedInteger('requests_per_day')->default(10000);
|
||||
|
||||
// Current Period Counters
|
||||
$table->unsignedInteger('current_minute_count')->default(0);
|
||||
$table->unsignedInteger('current_hour_count')->default(0);
|
||||
$table->unsignedInteger('current_day_count')->default(0);
|
||||
|
||||
// Period Timestamps
|
||||
$table->timestamp('minute_started_at')->useCurrent();
|
||||
$table->timestamp('hour_started_at')->useCurrent();
|
||||
$table->timestamp('day_started_at')->useCurrent();
|
||||
|
||||
// Status
|
||||
$table->boolean('is_rate_limited')->default(false);
|
||||
$table->timestamp('rate_limit_expires_at')->nullable();
|
||||
|
||||
$table->timestamps();
|
||||
|
||||
$table->unique('user_id');
|
||||
$table->index('is_rate_limited');
|
||||
$table->index('rate_limit_expires_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('rate_limits');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,159 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
$now = now();
|
||||
|
||||
DB::table('model_pricing')->insert([
|
||||
// Mistral AI Models
|
||||
[
|
||||
'provider' => 'mistral',
|
||||
'model' => 'mistral-large-latest',
|
||||
'input_price_per_million' => 2.00,
|
||||
'output_price_per_million' => 6.00,
|
||||
'context_window' => 128000,
|
||||
'max_output_tokens' => 4096,
|
||||
'is_active' => true,
|
||||
'effective_from' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
[
|
||||
'provider' => 'mistral',
|
||||
'model' => 'mistral-medium-latest',
|
||||
'input_price_per_million' => 2.70,
|
||||
'output_price_per_million' => 8.10,
|
||||
'context_window' => 32000,
|
||||
'max_output_tokens' => 4096,
|
||||
'is_active' => true,
|
||||
'effective_from' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
[
|
||||
'provider' => 'mistral',
|
||||
'model' => 'mistral-small-latest',
|
||||
'input_price_per_million' => 0.20,
|
||||
'output_price_per_million' => 0.60,
|
||||
'context_window' => 32000,
|
||||
'max_output_tokens' => 4096,
|
||||
'is_active' => true,
|
||||
'effective_from' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
[
|
||||
'provider' => 'mistral',
|
||||
'model' => 'open-mistral-7b',
|
||||
'input_price_per_million' => 0.25,
|
||||
'output_price_per_million' => 0.25,
|
||||
'context_window' => 32000,
|
||||
'max_output_tokens' => 4096,
|
||||
'is_active' => true,
|
||||
'effective_from' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
[
|
||||
'provider' => 'mistral',
|
||||
'model' => 'open-mixtral-8x7b',
|
||||
'input_price_per_million' => 0.70,
|
||||
'output_price_per_million' => 0.70,
|
||||
'context_window' => 32000,
|
||||
'max_output_tokens' => 4096,
|
||||
'is_active' => true,
|
||||
'effective_from' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
|
||||
// Google Gemini Models
|
||||
[
|
||||
'provider' => 'gemini',
|
||||
'model' => 'gemini-pro',
|
||||
'input_price_per_million' => 0.50,
|
||||
'output_price_per_million' => 1.50,
|
||||
'context_window' => 32760,
|
||||
'max_output_tokens' => 2048,
|
||||
'is_active' => true,
|
||||
'effective_from' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
[
|
||||
'provider' => 'gemini',
|
||||
'model' => 'gemini-1.5-pro',
|
||||
'input_price_per_million' => 3.50,
|
||||
'output_price_per_million' => 10.50,
|
||||
'context_window' => 2097152,
|
||||
'max_output_tokens' => 8192,
|
||||
'is_active' => true,
|
||||
'effective_from' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
[
|
||||
'provider' => 'gemini',
|
||||
'model' => 'gemini-1.5-flash',
|
||||
'input_price_per_million' => 0.35,
|
||||
'output_price_per_million' => 1.05,
|
||||
'context_window' => 1048576,
|
||||
'max_output_tokens' => 8192,
|
||||
'is_active' => true,
|
||||
'effective_from' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
|
||||
// DeepSeek Models
|
||||
[
|
||||
'provider' => 'deepseek',
|
||||
'model' => 'deepseek-chat',
|
||||
'input_price_per_million' => 0.14,
|
||||
'output_price_per_million' => 0.28,
|
||||
'context_window' => 64000,
|
||||
'max_output_tokens' => 4096,
|
||||
'is_active' => true,
|
||||
'effective_from' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
[
|
||||
'provider' => 'deepseek',
|
||||
'model' => 'deepseek-coder',
|
||||
'input_price_per_million' => 0.14,
|
||||
'output_price_per_million' => 0.28,
|
||||
'context_window' => 128000,
|
||||
'max_output_tokens' => 4096,
|
||||
'is_active' => true,
|
||||
'effective_from' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
[
|
||||
'provider' => 'deepseek',
|
||||
'model' => 'deepseek-reasoner',
|
||||
'input_price_per_million' => 0.55,
|
||||
'output_price_per_million' => 2.19,
|
||||
'context_window' => 64000,
|
||||
'max_output_tokens' => 8192,
|
||||
'is_active' => true,
|
||||
'effective_from' => $now,
|
||||
'created_at' => $now,
|
||||
'updated_at' => $now,
|
||||
],
|
||||
]);
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
DB::table('model_pricing')
|
||||
->whereIn('provider', ['mistral', 'gemini', 'deepseek'])
|
||||
->delete();
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,29 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('gateway_users', function (Blueprint $table) {
|
||||
$table->string('user_id')->primary();
|
||||
$table->string('alias')->nullable();
|
||||
$table->string('budget_id')->nullable();
|
||||
$table->decimal('spend', 10, 2)->default(0);
|
||||
$table->boolean('blocked')->default(false);
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('blocked');
|
||||
$table->index('created_at');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('gateway_users');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,27 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('budgets', function (Blueprint $table) {
|
||||
$table->string('budget_id')->primary();
|
||||
$table->string('name');
|
||||
$table->decimal('monthly_limit', 10, 2)->nullable();
|
||||
$table->decimal('daily_limit', 10, 2)->nullable();
|
||||
$table->string('created_by')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('name');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('budgets');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,36 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('api_keys', function (Blueprint $table) {
|
||||
$table->string('token')->primary();
|
||||
$table->string('user_id');
|
||||
$table->string('key_alias')->nullable();
|
||||
$table->string('key_name')->nullable();
|
||||
$table->json('permissions')->nullable();
|
||||
$table->json('models')->nullable();
|
||||
$table->json('metadata')->nullable();
|
||||
$table->timestamp('expires')->nullable();
|
||||
$table->timestamps();
|
||||
|
||||
$table->index('user_id');
|
||||
$table->index('expires');
|
||||
|
||||
$table->foreign('user_id')
|
||||
->references('user_id')
|
||||
->on('gateway_users')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('api_keys');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,45 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('usage_logs', function (Blueprint $table) {
|
||||
$table->string('request_id')->primary();
|
||||
$table->string('user_id');
|
||||
$table->string('api_key');
|
||||
$table->string('model');
|
||||
$table->string('provider')->nullable();
|
||||
$table->integer('prompt_tokens')->default(0);
|
||||
$table->integer('completion_tokens')->default(0);
|
||||
$table->integer('total_tokens')->default(0);
|
||||
$table->decimal('cost', 10, 6)->default(0);
|
||||
$table->timestamp('timestamp')->useCurrent();
|
||||
$table->json('metadata')->nullable();
|
||||
|
||||
$table->index('user_id');
|
||||
$table->index('api_key');
|
||||
$table->index('model');
|
||||
$table->index('timestamp');
|
||||
|
||||
$table->foreign('user_id')
|
||||
->references('user_id')
|
||||
->on('gateway_users')
|
||||
->onDelete('cascade');
|
||||
|
||||
$table->foreign('api_key')
|
||||
->references('token')
|
||||
->on('api_keys')
|
||||
->onDelete('cascade');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('usage_logs');
|
||||
}
|
||||
};
|
||||
@@ -0,0 +1,28 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::table('usage_logs', function (Blueprint $table) {
|
||||
$table->string('status')->default('success')->after('cost');
|
||||
$table->string('endpoint')->nullable()->after('provider');
|
||||
$table->text('error_message')->nullable()->after('status');
|
||||
|
||||
// Add index for status for better query performance
|
||||
$table->index('status');
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::table('usage_logs', function (Blueprint $table) {
|
||||
$table->dropIndex(['status']);
|
||||
$table->dropColumn(['status', 'endpoint', 'error_message']);
|
||||
});
|
||||
}
|
||||
};
|
||||
Reference in New Issue
Block a user