Initial commit: Any-LLM Gateway with Laravel Admin Interface

- Any-LLM Gateway setup with Docker Compose
- Laravel 11 admin interface with Livewire
- Dashboard with usage statistics and charts
- Gateway Users management with budget tracking
- API Keys management with revocation
- Budget templates with assignment
- Usage Logs with filtering and CSV export
- Model Pricing management with calculator
- PostgreSQL database integration
- Complete authentication system for admins
This commit is contained in:
wtrinkl
2025-11-16 12:38:05 +01:00
commit b1363aeab9
148 changed files with 23995 additions and 0 deletions

View File

@@ -0,0 +1,45 @@
<?php
namespace App\Models;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class Admin extends Authenticatable
{
use Notifiable;
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var array<int, string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}

View File

@@ -0,0 +1,69 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ApiKey extends Model
{
protected $primaryKey = 'id';
public $incrementing = false;
protected $keyType = 'string';
protected $fillable = [
'id',
'key_hash',
'key_name',
'user_id',
'last_used_at',
'expires_at',
'is_active',
'metadata',
];
protected function casts(): array
{
return [
'is_active' => 'boolean',
'metadata' => 'array',
'created_at' => 'datetime',
'last_used_at' => 'datetime',
'expires_at' => 'datetime',
];
}
public function gatewayUser()
{
return $this->belongsTo(GatewayUser::class, 'user_id', 'user_id');
}
public function usageLogs()
{
return $this->hasMany(UsageLog::class, 'api_key_id', 'id');
}
public function scopeActive($query)
{
return $query->where('is_active', true)
->where(function ($q) {
$q->whereNull('expires_at')
->orWhere('expires_at', '>', now());
});
}
public function scopeExpired($query)
{
return $query->whereNotNull('expires_at')
->where('expires_at', '<=', now());
}
public function getMaskedKeyAttribute()
{
return 'gw-' . substr($this->id, 0, 8) . '...' . substr($this->id, -8);
}
public function getIsExpiredAttribute()
{
return $this->expires_at && $this->expires_at->isPast();
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class Budget extends Model
{
protected $primaryKey = 'budget_id';
public $incrementing = false;
protected $keyType = 'string';
protected $fillable = [
'budget_id',
'max_budget',
'budget_duration_sec',
];
protected function casts(): array
{
return [
'max_budget' => 'double',
'budget_duration_sec' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
}
public function gatewayUsers()
{
return $this->hasMany(GatewayUser::class, 'budget_id', 'budget_id');
}
public function getMaxBudgetFormattedAttribute()
{
return '$' . number_format($this->max_budget, 2);
}
public function getDurationHumanAttribute()
{
if (!$this->budget_duration_sec) return 'No limit';
$days = floor($this->budget_duration_sec / 86400);
$hours = floor(($this->budget_duration_sec % 86400) / 3600);
return "{$days}d {$hours}h";
}
}

View File

@@ -0,0 +1,134 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class GatewayUser extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'users';
/**
* The primary key for the model.
*
* @var string
*/
protected $primaryKey = 'user_id';
/**
* Indicates if the IDs are auto-incrementing.
*
* @var bool
*/
public $incrementing = false;
/**
* The data type of the primary key ID.
*
* @var string
*/
protected $keyType = 'string';
/**
* The attributes that are mass assignable.
*
* @var array<int, string>
*/
protected $fillable = [
'user_id',
'alias',
'spend',
'budget_id',
'blocked',
'metadata',
'budget_started_at',
'next_budget_reset_at',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'spend' => 'double',
'blocked' => 'boolean',
'metadata' => 'array',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'budget_started_at' => 'datetime',
'next_budget_reset_at' => 'datetime',
];
}
/**
* Get the API keys for this user.
*/
public function apiKeys()
{
return $this->hasMany(ApiKey::class, 'user_id', 'user_id');
}
/**
* Get the usage logs for this user.
*/
public function usageLogs()
{
return $this->hasMany(UsageLog::class, 'user_id', 'user_id');
}
/**
* Get the budget for this user.
*/
public function budget()
{
return $this->belongsTo(Budget::class, 'budget_id', 'budget_id');
}
/**
* Scope a query to only include active users.
*/
public function scopeActive($query)
{
return $query->where('blocked', false);
}
/**
* Scope a query to only include blocked users.
*/
public function scopeBlocked($query)
{
return $query->where('blocked', true);
}
/**
* Get the formatted spend amount.
*/
public function getSpendFormattedAttribute()
{
return '$' . number_format($this->spend, 2);
}
/**
* Get the total number of requests.
*/
public function getTotalRequestsAttribute()
{
return $this->usageLogs()->count();
}
/**
* Get the total number of tokens used.
*/
public function getTotalTokensAttribute()
{
return $this->usageLogs()->sum('total_tokens');
}
}

View File

@@ -0,0 +1,51 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class ModelPricing extends Model
{
protected $table = 'model_pricing';
protected $primaryKey = 'model_key';
public $incrementing = false;
protected $keyType = 'string';
protected $fillable = [
'model_key',
'input_price_per_million',
'output_price_per_million',
];
protected function casts(): array
{
return [
'input_price_per_million' => 'double',
'output_price_per_million' => 'double',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
}
// Accessors
public function getInputPriceFormattedAttribute()
{
return '$' . number_format($this->input_price_per_million, 2) . '/M';
}
public function getOutputPriceFormattedAttribute()
{
return '$' . number_format($this->output_price_per_million, 2) . '/M';
}
/**
* Calculate cost for given token counts
*/
public function calculateCost($inputTokens, $outputTokens)
{
$inputCost = ($inputTokens / 1000000) * $this->input_price_per_million;
$outputCost = ($outputTokens / 1000000) * $this->output_price_per_million;
return $inputCost + $outputCost;
}
}

View File

@@ -0,0 +1,75 @@
<?php
namespace App\Models;
use Illuminate\Database\Eloquent\Model;
class UsageLog extends Model
{
protected $primaryKey = 'id';
public $incrementing = false;
protected $keyType = 'string';
public $timestamps = false;
protected $fillable = [
'id',
'api_key_id',
'user_id',
'timestamp',
'model',
'provider',
'endpoint',
'prompt_tokens',
'completion_tokens',
'total_tokens',
'cost',
'status',
'error_message',
];
protected function casts(): array
{
return [
'timestamp' => 'datetime',
'prompt_tokens' => 'integer',
'completion_tokens' => 'integer',
'total_tokens' => 'integer',
'cost' => 'double',
];
}
public function gatewayUser()
{
return $this->belongsTo(GatewayUser::class, 'user_id', 'user_id');
}
public function apiKey()
{
return $this->belongsTo(ApiKey::class, 'api_key_id', 'id');
}
public function scopeSuccess($query)
{
return $query->where('status', 'success');
}
public function scopeFailed($query)
{
return $query->where('status', '!=', 'success');
}
public function scopeToday($query)
{
return $query->whereDate('timestamp', today());
}
public function scopeDateRange($query, $start, $end)
{
return $query->whereBetween('timestamp', [$start, $end]);
}
public function getCostFormattedAttribute()
{
return $this->cost ? '$' . number_format($this->cost, 4) : 'N/A';
}
}

View File

@@ -0,0 +1,48 @@
<?php
namespace App\Models;
// use Illuminate\Contracts\Auth\MustVerifyEmail;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Foundation\Auth\User as Authenticatable;
use Illuminate\Notifications\Notifiable;
class User extends Authenticatable
{
/** @use HasFactory<\Database\Factories\UserFactory> */
use HasFactory, Notifiable;
/**
* The attributes that are mass assignable.
*
* @var list<string>
*/
protected $fillable = [
'name',
'email',
'password',
];
/**
* The attributes that should be hidden for serialization.
*
* @var list<string>
*/
protected $hidden = [
'password',
'remember_token',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
*/
protected function casts(): array
{
return [
'email_verified_at' => 'datetime',
'password' => 'hashed',
];
}
}