Rename project from any-llm to laravel-llm

- Remove old any-llm related files (Dockerfile, config.yml, web/, setup-laravel.sh)
- Update README.md with new Laravel LLM Gateway documentation
- Keep docker-compose.yml with laravel-llm container names
- Clean project structure for Laravel-only implementation
This commit is contained in:
wtrinkl
2025-11-18 22:05:05 +01:00
parent b1363aeab9
commit bef36c7ca2
33 changed files with 1341 additions and 2930 deletions

View File

@@ -1,45 +0,0 @@
<?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

@@ -2,34 +2,73 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class ApiKey extends Model
{
protected $primaryKey = 'id';
use HasFactory;
protected $table = 'api_keys';
protected $primaryKey = 'token';
public $incrementing = false;
protected $keyType = 'string';
protected $fillable = [
'id',
'key_hash',
'key_name',
'token',
'user_id',
'last_used_at',
'expires_at',
'is_active',
'key_alias',
'key_name',
'permissions',
'models',
'metadata',
'expires',
];
protected function casts(): array
protected $casts = [
'permissions' => 'array',
'models' => 'array',
'metadata' => 'array',
'expires' => 'datetime',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Get masked version of the key
*/
public function getMaskedKeyAttribute(): string
{
return [
'is_active' => 'boolean',
'metadata' => 'array',
'created_at' => 'datetime',
'last_used_at' => 'datetime',
'expires_at' => 'datetime',
];
return substr($this->token, 0, 8) . '...' . substr($this->token, -4);
}
/**
* Check if key is active (not explicitly marked inactive)
*/
public function getIsActiveAttribute(): bool
{
// For now, consider all keys active unless explicitly deleted
return true;
}
/**
* Check if key is expired
*/
public function getIsExpiredAttribute(): bool
{
if (!$this->expires) {
return false;
}
return $this->expires->isPast();
}
/**
* Get last used at timestamp
*/
public function getLastUsedAtAttribute()
{
$latestLog = $this->usageLogs()->latest('timestamp')->first();
return $latestLog ? $latestLog->timestamp : null;
}
public function gatewayUser()
@@ -37,33 +76,14 @@ class ApiKey extends Model
return $this->belongsTo(GatewayUser::class, 'user_id', 'user_id');
}
// Alias for backwards compatibility
public function user()
{
return $this->gatewayUser();
}
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();
return $this->hasMany(UsageLog::class, 'api_key', 'token');
}
}

View File

@@ -2,47 +2,61 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class Budget extends Model
{
use HasFactory;
protected $table = 'budgets';
protected $primaryKey = 'budget_id';
public $incrementing = false;
protected $keyType = 'string';
protected $fillable = [
'budget_id',
'max_budget',
'budget_duration_sec',
'name',
'monthly_limit',
'daily_limit',
'created_by',
];
protected function casts(): array
protected $casts = [
'monthly_limit' => 'decimal:2',
'daily_limit' => 'decimal:2',
];
/**
* Get formatted max budget display
*/
public function getMaxBudgetFormattedAttribute(): string
{
return [
'max_budget' => 'double',
'budget_duration_sec' => 'integer',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
if ($this->monthly_limit) {
return '$' . number_format($this->monthly_limit, 2);
}
if ($this->daily_limit) {
return '$' . number_format($this->daily_limit, 2) . '/day';
}
return 'Unlimited';
}
/**
* Get human-readable duration
*/
public function getDurationHumanAttribute(): string
{
if ($this->monthly_limit && $this->daily_limit) {
return 'Monthly';
}
if ($this->daily_limit && !$this->monthly_limit) {
return 'Daily';
}
return 'Unlimited';
}
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

@@ -2,74 +2,45 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class GatewayUser extends Model
{
/**
* The table associated with the model.
*
* @var string
*/
protected $table = 'users';
use HasFactory;
/**
* The primary key for the model.
*
* @var string
*/
protected $table = 'gateway_users';
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',
'spend',
'blocked',
'metadata',
'budget_started_at',
'next_budget_reset_at',
];
protected $casts = [
'metadata' => 'array',
'blocked' => 'boolean',
'spend' => 'decimal:2',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
/**
* Get the attributes that should be cast.
*
* @return array<string, string>
* Get the budget associated with the user.
*/
protected function casts(): array
public function budget()
{
return [
'spend' => 'double',
'blocked' => 'boolean',
'metadata' => 'array',
'created_at' => 'datetime',
'updated_at' => 'datetime',
'budget_started_at' => 'datetime',
'next_budget_reset_at' => 'datetime',
];
return $this->belongsTo(Budget::class, 'budget_id', 'budget_id');
}
/**
* Get the API keys for this user.
* Get the API keys for the user.
*/
public function apiKeys()
{
@@ -77,21 +48,13 @@ class GatewayUser extends Model
}
/**
* Get the usage logs for this user.
* Get the usage logs for the 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.
*/
@@ -107,28 +70,4 @@ class GatewayUser extends Model
{
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

@@ -7,45 +7,53 @@ 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',
'provider',
'model',
'input_price_per_million',
'output_price_per_million',
'context_window',
'max_output_tokens',
'is_active',
'effective_from',
'effective_until',
'notes',
];
protected function casts(): array
{
return [
'input_price_per_million' => 'double',
'output_price_per_million' => 'double',
'created_at' => 'datetime',
'updated_at' => 'datetime',
];
}
protected $casts = [
'input_price_per_million' => 'decimal:4',
'output_price_per_million' => 'decimal:4',
'context_window' => 'integer',
'max_output_tokens' => 'integer',
'is_active' => 'boolean',
'effective_from' => 'date',
'effective_until' => 'date',
];
// Accessors
public function getInputPriceFormattedAttribute()
public function getInputPriceFormattedAttribute(): string
{
return '$' . number_format($this->input_price_per_million, 2) . '/M';
}
public function getOutputPriceFormattedAttribute()
public function getOutputPriceFormattedAttribute(): string
{
return '$' . number_format($this->output_price_per_million, 2) . '/M';
}
/**
* Calculate cost for given token counts
*/
public function calculateCost($inputTokens, $outputTokens)
public function calculateCost(int $inputTokens, int $outputTokens): float
{
$inputCost = ($inputTokens / 1000000) * $this->input_price_per_million;
$outputCost = ($outputTokens / 1000000) * $this->output_price_per_million;
$inputCost = ($inputTokens / 1_000_000) * $this->input_price_per_million;
$outputCost = ($outputTokens / 1_000_000) * $this->output_price_per_million;
return $inputCost + $outputCost;
return round($inputCost + $outputCost, 6);
}
public function isCurrentlyActive(): bool
{
$now = now()->toDateString();
return $this->is_active
&& $this->effective_from <= $now
&& ($this->effective_until === null || $this->effective_until >= $now);
}
}

View File

@@ -2,20 +2,23 @@
namespace App\Models;
use Illuminate\Database\Eloquent\Factories\HasFactory;
use Illuminate\Database\Eloquent\Model;
class UsageLog extends Model
{
protected $primaryKey = 'id';
use HasFactory;
protected $table = 'usage_logs';
protected $primaryKey = 'request_id';
public $incrementing = false;
protected $keyType = 'string';
public $timestamps = false;
protected $fillable = [
'id',
'api_key_id',
'request_id',
'user_id',
'timestamp',
'api_key',
'model',
'provider',
'endpoint',
@@ -25,17 +28,22 @@ class UsageLog extends Model
'cost',
'status',
'error_message',
'timestamp',
'metadata',
];
protected function casts(): array
protected $casts = [
'prompt_tokens' => 'integer',
'completion_tokens' => 'integer',
'total_tokens' => 'integer',
'cost' => 'decimal:6',
'timestamp' => 'datetime',
'metadata' => 'array',
];
public function user()
{
return [
'timestamp' => 'datetime',
'prompt_tokens' => 'integer',
'completion_tokens' => 'integer',
'total_tokens' => 'integer',
'cost' => 'double',
];
return $this->belongsTo(GatewayUser::class, 'user_id', 'user_id');
}
public function gatewayUser()
@@ -45,9 +53,10 @@ class UsageLog extends Model
public function apiKey()
{
return $this->belongsTo(ApiKey::class, 'api_key_id', 'id');
return $this->belongsTo(ApiKey::class, 'api_key', 'token');
}
// Scopes
public function scopeSuccess($query)
{
return $query->where('status', 'success');
@@ -55,21 +64,6 @@ class UsageLog extends Model
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';
return $query->where('status', 'failed');
}
}

View File

@@ -45,4 +45,36 @@ class User extends Authenticatable
'password' => 'hashed',
];
}
/**
* Get the user's budget
*/
public function budget()
{
return $this->hasOne(UserBudget::class);
}
/**
* Get the user's rate limit
*/
public function rateLimit()
{
return $this->hasOne(RateLimit::class);
}
/**
* Get the user's provider credentials
*/
public function providerCredentials()
{
return $this->hasMany(UserProviderCredential::class);
}
/**
* Get the user's LLM requests
*/
public function llmRequests()
{
return $this->hasMany(LlmRequest::class);
}
}