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

@@ -70,52 +70,39 @@ class ApiKeyController extends Controller
{
$validated = $request->validate([
'key_name' => 'required|string|max:255',
'user_id' => 'required|string|exists:users,user_id',
'user_id' => 'required|string|exists:gateway_users,user_id',
'expires_at' => 'nullable|date|after:now',
'metadata' => 'nullable|json',
]);
try {
// Get master key from config
$masterKey = env('GATEWAY_MASTER_KEY');
if (!$masterKey) {
return back()->with('error', 'Gateway Master Key not configured');
// Generate a unique API token
$token = 'llmg_' . Str::random(48);
// Parse metadata if provided
$metadata = null;
if (!empty($validated['metadata'])) {
$metadata = json_decode($validated['metadata'], true);
if (json_last_error() !== JSON_ERROR_NONE) {
return back()->with('error', 'Invalid JSON in metadata field');
}
}
// Prepare request payload
$payload = [
// Create API key directly in database
$apiKey = ApiKey::create([
'token' => $token,
'user_id' => $validated['user_id'],
'key_name' => $validated['key_name'],
];
'key_alias' => $validated['key_name'], // Use key_name as alias
'expires' => $validated['expires_at'] ?? null,
'metadata' => $metadata,
'permissions' => [], // Default empty permissions
'models' => [], // Default empty models
]);
// Add optional fields only if they have values
if (!empty($validated['expires_at'])) {
$payload['expires_at'] = $validated['expires_at'];
}
if (!empty($validated['metadata'])) {
$payload['metadata'] = json_decode($validated['metadata'], true) ?: new \stdClass();
}
// Create Virtual Key via Any-LLM Gateway API
$response = Http::withHeaders([
'X-AnyLLM-Key' => 'Bearer ' . $masterKey,
'Content-Type' => 'application/json',
])->post(env('GATEWAY_API_URL', 'http://gateway:8000') . '/v1/keys', $payload);
if (!$response->successful()) {
Log::error('Failed to create API key', [
'status' => $response->status(),
'body' => $response->body()
]);
return back()->with('error', 'Failed to create API key: ' . $response->body());
}
$data = $response->json();
// The actual key is only available once - store it in session for display
session()->flash('new_api_key', $data['key'] ?? null);
session()->flash('new_api_key_id', $data['id'] ?? null);
// Store the token in session for one-time display
session()->flash('new_api_key', $token);
session()->flash('new_api_key_id', $apiKey->token);
return redirect()->route('api-keys.index')
->with('success', 'API Key created successfully! Make sure to copy it now - it won\'t be shown again.');
@@ -160,26 +147,8 @@ class ApiKeyController extends Controller
try {
$apiKey = ApiKey::findOrFail($id);
// Get master key from config
$masterKey = env('GATEWAY_MASTER_KEY');
if (!$masterKey) {
return back()->with('error', 'Gateway Master Key not configured');
}
// Revoke via Any-LLM Gateway API
$response = Http::withHeaders([
'X-AnyLLM-Key' => 'Bearer ' . $masterKey,
'Content-Type' => 'application/json',
])->delete(env('GATEWAY_API_URL', 'http://gateway:8000') . '/v1/keys/' . $id);
if (!$response->successful()) {
Log::error('Failed to revoke API key', [
'key_id' => $id,
'status' => $response->status(),
'body' => $response->body()
]);
return back()->with('error', 'Failed to revoke API key: ' . $response->body());
}
// Delete the API key from database
$apiKey->delete();
return redirect()->route('api-keys.index')
->with('success', 'API Key revoked successfully');

View File

@@ -41,19 +41,35 @@ class BudgetController extends Controller
'custom_duration_days' => 'nullable|integer|min:1|required_if:budget_type,custom',
]);
// Calculate budget_duration_sec based on type
$duration = match($validated['budget_type']) {
'daily' => 86400, // 1 day
'weekly' => 604800, // 7 days
'monthly' => 2592000, // 30 days
'custom' => ($validated['custom_duration_days'] ?? 1) * 86400,
'unlimited' => null,
};
// Set monthly and daily limits based on budget type
$monthlyLimit = null;
$dailyLimit = null;
switch($validated['budget_type']) {
case 'daily':
$dailyLimit = $validated['max_budget'];
break;
case 'weekly':
$dailyLimit = $validated['max_budget'] / 7;
break;
case 'monthly':
$monthlyLimit = $validated['max_budget'];
$dailyLimit = $validated['max_budget'] / 30;
break;
case 'custom':
$days = $validated['custom_duration_days'] ?? 1;
$dailyLimit = $validated['max_budget'] / $days;
break;
case 'unlimited':
// No limits
break;
}
$budget = Budget::create([
'budget_id' => 'budget-' . Str::uuid(),
'max_budget' => $validated['max_budget'],
'budget_duration_sec' => $duration,
'name' => $validated['budget_name'],
'monthly_limit' => $monthlyLimit,
'daily_limit' => $dailyLimit,
]);
return redirect()
@@ -106,23 +122,40 @@ class BudgetController extends Controller
$budget = Budget::findOrFail($id);
$validated = $request->validate([
'budget_name' => 'required|string|max:255',
'max_budget' => 'required|numeric|min:0',
'budget_type' => 'required|in:daily,weekly,monthly,custom,unlimited',
'custom_duration_days' => 'nullable|integer|min:1|required_if:budget_type,custom',
]);
// Calculate budget_duration_sec based on type
$duration = match($validated['budget_type']) {
'daily' => 86400,
'weekly' => 604800,
'monthly' => 2592000,
'custom' => ($validated['custom_duration_days'] ?? 1) * 86400,
'unlimited' => null,
};
// Set monthly and daily limits based on budget type
$monthlyLimit = null;
$dailyLimit = null;
switch($validated['budget_type']) {
case 'daily':
$dailyLimit = $validated['max_budget'];
break;
case 'weekly':
$dailyLimit = $validated['max_budget'] / 7;
break;
case 'monthly':
$monthlyLimit = $validated['max_budget'];
$dailyLimit = $validated['max_budget'] / 30;
break;
case 'custom':
$days = $validated['custom_duration_days'] ?? 1;
$dailyLimit = $validated['max_budget'] / $days;
break;
case 'unlimited':
// No limits
break;
}
$budget->update([
'max_budget' => $validated['max_budget'],
'budget_duration_sec' => $duration,
'name' => $validated['budget_name'],
'monthly_limit' => $monthlyLimit,
'daily_limit' => $dailyLimit,
]);
return redirect()

View File

@@ -21,13 +21,28 @@ class DashboardController extends Controller
$topUsers = $this->statsService->getTopUsers(5);
$providerStats = $this->statsService->getUsageByProvider(30);
$modelStats = $this->statsService->getUsageByModel(30);
$costTrends = $this->statsService->getCostTrends(30);
$errorStats = $this->statsService->getErrorStats(30);
return view('dashboard', compact(
'stats',
'dailyUsage',
'topUsers',
'providerStats',
'modelStats'
'modelStats',
'costTrends',
'errorStats'
));
}
/**
* Get real-time stats via AJAX
*/
public function realtimeStats()
{
return response()->json([
'stats' => $this->statsService->getDashboardStats(),
'timestamp' => now()->toIso8601String(),
]);
}
}

View File

@@ -13,7 +13,8 @@ class ModelPricingController extends Controller
*/
public function index()
{
$modelPricing = ModelPricing::orderBy('model_key')
$modelPricing = ModelPricing::orderBy('provider')
->orderBy('model')
->paginate(20);
return view('model-pricing.index', compact('modelPricing'));
@@ -33,9 +34,16 @@ class ModelPricingController extends Controller
public function store(Request $request)
{
$validated = $request->validate([
'model_key' => 'required|string|max:255|unique:model_pricing,model_key',
'provider' => 'required|string|max:50',
'model' => 'required|string|max:100',
'input_price_per_million' => 'required|numeric|min:0',
'output_price_per_million' => 'required|numeric|min:0',
'context_window' => 'nullable|integer|min:0',
'max_output_tokens' => 'nullable|integer|min:0',
'is_active' => 'boolean',
'effective_from' => 'nullable|date',
'effective_until' => 'nullable|date',
'notes' => 'nullable|string',
]);
ModelPricing::create($validated);
@@ -48,36 +56,38 @@ class ModelPricingController extends Controller
/**
* Display the specified model pricing
*/
public function show(string $modelKey)
public function show(ModelPricing $modelPricing)
{
$model = ModelPricing::findOrFail($modelKey);
return view('model-pricing.show', compact('model'));
return view('model-pricing.show', compact('modelPricing'));
}
/**
* Show the form for editing the specified model pricing
*/
public function edit(string $modelKey)
public function edit(ModelPricing $modelPricing)
{
$model = ModelPricing::findOrFail($modelKey);
return view('model-pricing.edit', compact('model'));
return view('model-pricing.edit', compact('modelPricing'));
}
/**
* Update the specified model pricing
*/
public function update(Request $request, string $modelKey)
public function update(Request $request, ModelPricing $modelPricing)
{
$model = ModelPricing::findOrFail($modelKey);
$validated = $request->validate([
'provider' => 'required|string|max:50',
'model' => 'required|string|max:100',
'input_price_per_million' => 'required|numeric|min:0',
'output_price_per_million' => 'required|numeric|min:0',
'context_window' => 'nullable|integer|min:0',
'max_output_tokens' => 'nullable|integer|min:0',
'is_active' => 'boolean',
'effective_from' => 'nullable|date',
'effective_until' => 'nullable|date',
'notes' => 'nullable|string',
]);
$model->update($validated);
$modelPricing->update($validated);
return redirect()
->route('model-pricing.index')
@@ -87,10 +97,9 @@ class ModelPricingController extends Controller
/**
* Remove the specified model pricing
*/
public function destroy(string $modelKey)
public function destroy(ModelPricing $modelPricing)
{
$model = ModelPricing::findOrFail($modelKey);
$model->delete();
$modelPricing->delete();
return redirect()
->route('model-pricing.index')
@@ -102,7 +111,10 @@ class ModelPricingController extends Controller
*/
public function calculator()
{
$models = ModelPricing::orderBy('model_key')->get();
$models = ModelPricing::where('is_active', true)
->orderBy('provider')
->orderBy('model')
->get();
return view('model-pricing.calculator', compact('models'));
}
@@ -113,19 +125,20 @@ class ModelPricingController extends Controller
public function calculate(Request $request)
{
$validated = $request->validate([
'model_key' => 'required|exists:model_pricing,model_key',
'model_pricing_id' => 'required|exists:model_pricing,id',
'input_tokens' => 'required|integer|min:0',
'output_tokens' => 'required|integer|min:0',
]);
$model = ModelPricing::findOrFail($validated['model_key']);
$model = ModelPricing::findOrFail($validated['model_pricing_id']);
$cost = $model->calculateCost(
$validated['input_tokens'],
$validated['output_tokens']
);
return response()->json([
'model' => $model->model_key,
'provider' => $model->provider,
'model' => $model->model,
'input_tokens' => $validated['input_tokens'],
'output_tokens' => $validated['output_tokens'],
'total_tokens' => $validated['input_tokens'] + $validated['output_tokens'],
@@ -162,20 +175,23 @@ class ModelPricingController extends Controller
fgetcsv($handle);
while (($row = fgetcsv($handle)) !== false) {
if (count($row) < 3) {
if (count($row) < 4) {
continue; // Skip invalid rows
}
$modelKey = trim($row[0]);
$inputPrice = floatval($row[1]);
$outputPrice = floatval($row[2]);
$provider = trim($row[0]);
$model = trim($row[1]);
$inputPrice = floatval($row[2]);
$outputPrice = floatval($row[3]);
if (empty($modelKey) || $inputPrice < 0 || $outputPrice < 0) {
$errors[] = "Invalid data for model: {$modelKey}";
if (empty($provider) || empty($model) || $inputPrice < 0 || $outputPrice < 0) {
$errors[] = "Invalid data for model: {$provider}/{$model}";
continue;
}
$existing = ModelPricing::find($modelKey);
$existing = ModelPricing::where('provider', $provider)
->where('model', $model)
->first();
if ($existing) {
$existing->update([
@@ -185,7 +201,8 @@ class ModelPricingController extends Controller
$updated++;
} else {
ModelPricing::create([
'model_key' => $modelKey,
'provider' => $provider,
'model' => $model,
'input_price_per_million' => $inputPrice,
'output_price_per_million' => $outputPrice,
]);
@@ -205,4 +222,291 @@ class ModelPricingController extends Controller
->route('model-pricing.index')
->with('success', $message);
}
/**
* Get available models from a provider
*/
public function getProviderModels(string $provider)
{
try {
$models = [];
switch($provider) {
case 'openai':
$models = $this->getOpenAIModels();
break;
case 'anthropic':
$models = $this->getAnthropicModels();
break;
case 'deepseek':
$models = $this->getDeepSeekModels();
break;
case 'google':
$models = $this->getGeminiModels();
break;
case 'cohere':
return response()->json([
'success' => false,
'message' => 'Cohere does not provide a public models API. Please enter model names manually'
]);
case 'mistral':
$models = $this->getMistralModels();
break;
default:
return response()->json([
'success' => false,
'message' => 'Provider not supported for automatic model fetching'
]);
}
return response()->json([
'success' => true,
'models' => $models
]);
} catch (\Exception $e) {
return response()->json([
'success' => false,
'message' => $e->getMessage()
], 500);
}
}
/**
* Fetch OpenAI models
*/
private function getOpenAIModels()
{
// Get API key from credentials
$credential = \App\Models\UserProviderCredential::where('provider', 'openai')
->where('is_active', true)
->first();
if (!$credential || !$credential->api_key) {
throw new \Exception('No active OpenAI credentials found. Please add credentials first.');
}
$response = \Illuminate\Support\Facades\Http::withHeaders([
'Authorization' => 'Bearer ' . $credential->api_key,
])->get('https://api.openai.com/v1/models');
if (!$response->successful()) {
throw new \Exception('Failed to fetch OpenAI models: ' . $response->body());
}
$data = $response->json();
$models = [];
foreach ($data['data'] as $model) {
if (isset($model['id'])) {
$models[] = [
'id' => $model['id'],
'name' => $model['id'],
'created' => $model['created'] ?? null,
];
}
}
// Sort by name
usort($models, fn($a, $b) => strcmp($a['name'], $b['name']));
return $models;
}
/**
* Fetch Anthropic models from API
*/
private function getAnthropicModels()
{
// Get API key from credentials
$credential = \App\Models\UserProviderCredential::where('provider', 'anthropic')
->where('is_active', true)
->first();
if (!$credential || !$credential->api_key) {
throw new \Exception('No active Anthropic credentials found. Please add credentials first.');
}
$response = \Illuminate\Support\Facades\Http::withHeaders([
'x-api-key' => $credential->api_key,
'anthropic-version' => '2023-06-01',
])->get('https://api.anthropic.com/v1/models');
if (!$response->successful()) {
throw new \Exception('Failed to fetch Anthropic models: ' . $response->body());
}
$data = $response->json();
$models = [];
foreach ($data['data'] as $model) {
if (isset($model['id'])) {
$models[] = [
'id' => $model['id'],
'name' => $model['display_name'] ?? $model['id'],
'created' => isset($model['created_at']) ? strtotime($model['created_at']) : null,
];
}
}
// Sort by name
usort($models, fn($a, $b) => strcmp($a['name'], $b['name']));
return $models;
}
/**
* Fetch DeepSeek models from API
*/
private function getDeepSeekModels()
{
// Get API key from credentials
$credential = \App\Models\UserProviderCredential::where('provider', 'deepseek')
->where('is_active', true)
->first();
if (!$credential || !$credential->api_key) {
throw new \Exception('No active DeepSeek credentials found. Please add credentials first.');
}
// DeepSeek uses OpenAI-compatible API
$response = \Illuminate\Support\Facades\Http::withHeaders([
'Authorization' => 'Bearer ' . $credential->api_key,
])->get('https://api.deepseek.com/models');
if (!$response->successful()) {
throw new \Exception('Failed to fetch DeepSeek models: ' . $response->body());
}
$data = $response->json();
$models = [];
foreach ($data['data'] as $model) {
if (isset($model['id'])) {
$models[] = [
'id' => $model['id'],
'name' => $model['id'],
'created' => $model['created'] ?? null,
];
}
}
// Sort by name
usort($models, fn($a, $b) => strcmp($a['name'], $b['name']));
return $models;
}
/**
* Fetch Google Gemini models from API
*/
private function getGeminiModels()
{
// Get API key from credentials
$credential = \App\Models\UserProviderCredential::where('provider', 'gemini')
->where('is_active', true)
->first();
if (!$credential || !$credential->api_key) {
throw new \Exception('No active Google Gemini credentials found. Please add credentials first.');
}
$models = [];
$nextPageToken = null;
// Fetch all pages
do {
$url = 'https://generativelanguage.googleapis.com/v1beta/models?key=' . $credential->api_key;
if ($nextPageToken) {
$url .= '&pageToken=' . $nextPageToken;
}
$response = \Illuminate\Support\Facades\Http::get($url);
if (!$response->successful()) {
throw new \Exception('Failed to fetch Gemini models: ' . $response->body());
}
$data = $response->json();
foreach ($data['models'] as $model) {
// Only include models that support generateContent
if (isset($model['name']) &&
isset($model['supportedGenerationMethods']) &&
in_array('generateContent', $model['supportedGenerationMethods'])) {
// Extract model ID from "models/gemini-xxx" format
$modelId = str_replace('models/', '', $model['name']);
$models[] = [
'id' => $modelId,
'name' => $model['displayName'] ?? $modelId,
'created' => null,
];
}
}
$nextPageToken = $data['nextPageToken'] ?? null;
} while ($nextPageToken);
// Sort by name
usort($models, fn($a, $b) => strcmp($a['name'], $b['name']));
return $models;
}
/**
* Fetch Mistral AI models from API
*/
private function getMistralModels()
{
// Get API key from credentials
$credential = \App\Models\UserProviderCredential::where('provider', 'mistral')
->where('is_active', true)
->first();
if (!$credential || !$credential->api_key) {
throw new \Exception('No active Mistral AI credentials found. Please add credentials first.');
}
$response = \Illuminate\Support\Facades\Http::withHeaders([
'Authorization' => 'Bearer ' . $credential->api_key,
])->get('https://api.mistral.ai/v1/models');
if (!$response->successful()) {
throw new \Exception('Failed to fetch Mistral models: ' . $response->body());
}
$data = $response->json();
$models = [];
$seenIds = []; // Track seen IDs to avoid duplicates (aliases)
foreach ($data['data'] as $model) {
// Skip deprecated models
if (isset($model['deprecation']) && $model['deprecation']) {
continue;
}
// Only include models that support chat completion
if (isset($model['id']) &&
isset($model['capabilities']['completion_chat']) &&
$model['capabilities']['completion_chat'] === true &&
!isset($seenIds[$model['id']])) {
$seenIds[$model['id']] = true;
$models[] = [
'id' => $model['id'],
'name' => $model['name'] ?? $model['id'],
'created' => $model['created'] ?? null,
];
}
}
// Sort by name
usort($models, fn($a, $b) => strcmp($a['name'], $b['name']));
return $models;
}
}