Files
laravel-llm-gateway/laravel-app/app/Http/Controllers/ModelPricingController.php
wtrinkl bef36c7ca2 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
2025-11-18 22:05:05 +01:00

513 lines
16 KiB
PHP

<?php
namespace App\Http\Controllers;
use App\Models\ModelPricing;
use Illuminate\Http\Request;
use Illuminate\Support\Facades\Validator;
class ModelPricingController extends Controller
{
/**
* Display a listing of model pricing
*/
public function index()
{
$modelPricing = ModelPricing::orderBy('provider')
->orderBy('model')
->paginate(20);
return view('model-pricing.index', compact('modelPricing'));
}
/**
* Show the form for creating a new model pricing
*/
public function create()
{
return view('model-pricing.create');
}
/**
* Store a newly created model pricing
*/
public function store(Request $request)
{
$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',
]);
ModelPricing::create($validated);
return redirect()
->route('model-pricing.index')
->with('success', 'Model pricing created successfully!');
}
/**
* Display the specified model pricing
*/
public function show(ModelPricing $modelPricing)
{
return view('model-pricing.show', compact('modelPricing'));
}
/**
* Show the form for editing the specified model pricing
*/
public function edit(ModelPricing $modelPricing)
{
return view('model-pricing.edit', compact('modelPricing'));
}
/**
* Update the specified model pricing
*/
public function update(Request $request, ModelPricing $modelPricing)
{
$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',
]);
$modelPricing->update($validated);
return redirect()
->route('model-pricing.index')
->with('success', 'Model pricing updated successfully!');
}
/**
* Remove the specified model pricing
*/
public function destroy(ModelPricing $modelPricing)
{
$modelPricing->delete();
return redirect()
->route('model-pricing.index')
->with('success', 'Model pricing deleted successfully!');
}
/**
* Show the cost calculator
*/
public function calculator()
{
$models = ModelPricing::where('is_active', true)
->orderBy('provider')
->orderBy('model')
->get();
return view('model-pricing.calculator', compact('models'));
}
/**
* Calculate cost based on input
*/
public function calculate(Request $request)
{
$validated = $request->validate([
'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_pricing_id']);
$cost = $model->calculateCost(
$validated['input_tokens'],
$validated['output_tokens']
);
return response()->json([
'provider' => $model->provider,
'model' => $model->model,
'input_tokens' => $validated['input_tokens'],
'output_tokens' => $validated['output_tokens'],
'total_tokens' => $validated['input_tokens'] + $validated['output_tokens'],
'cost' => $cost,
'cost_formatted' => '$' . number_format($cost, 6),
]);
}
/**
* Show CSV import form
*/
public function importForm()
{
return view('model-pricing.import');
}
/**
* Import model pricing from CSV
*/
public function import(Request $request)
{
$request->validate([
'csv_file' => 'required|file|mimes:csv,txt|max:2048',
]);
$file = $request->file('csv_file');
$handle = fopen($file->getRealPath(), 'r');
$imported = 0;
$updated = 0;
$errors = [];
// Skip header row
fgetcsv($handle);
while (($row = fgetcsv($handle)) !== false) {
if (count($row) < 4) {
continue; // Skip invalid rows
}
$provider = trim($row[0]);
$model = trim($row[1]);
$inputPrice = floatval($row[2]);
$outputPrice = floatval($row[3]);
if (empty($provider) || empty($model) || $inputPrice < 0 || $outputPrice < 0) {
$errors[] = "Invalid data for model: {$provider}/{$model}";
continue;
}
$existing = ModelPricing::where('provider', $provider)
->where('model', $model)
->first();
if ($existing) {
$existing->update([
'input_price_per_million' => $inputPrice,
'output_price_per_million' => $outputPrice,
]);
$updated++;
} else {
ModelPricing::create([
'provider' => $provider,
'model' => $model,
'input_price_per_million' => $inputPrice,
'output_price_per_million' => $outputPrice,
]);
$imported++;
}
}
fclose($handle);
$message = "Import completed! Created: {$imported}, Updated: {$updated}";
if (count($errors) > 0) {
$message .= '. Errors: ' . implode(', ', array_slice($errors, 0, 5));
}
return redirect()
->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;
}
}