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

@@ -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;
}
}