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:
@@ -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;
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user