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:
@@ -9,50 +9,129 @@
|
||||
<div class="max-w-2xl mx-auto sm:px-6 lg:px-8">
|
||||
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
|
||||
<div class="p-6">
|
||||
<form method="POST" action="{{ route('model-pricing.store') }}">
|
||||
<form method="POST" action="{{ route('model-pricing.store') }}" id="pricingForm">
|
||||
@csrf
|
||||
|
||||
<!-- Provider Selection -->
|
||||
<div class="mb-4">
|
||||
<label for="model_key" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Model Key *
|
||||
<label for="provider" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Provider *
|
||||
</label>
|
||||
<input type="text" name="model_key" id="model_key"
|
||||
value="{{ old('model_key') }}" required
|
||||
placeholder="e.g., gpt-4, claude-3-opus-20240229"
|
||||
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
@error('model_key')
|
||||
<select name="provider" id="provider" required
|
||||
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select a provider...</option>
|
||||
<option value="openai" {{ old('provider') == 'openai' ? 'selected' : '' }}>OpenAI (models will be loaded from API)</option>
|
||||
<option value="anthropic" {{ old('provider') == 'anthropic' ? 'selected' : '' }}>Anthropic (models will be loaded from API)</option>
|
||||
<option value="deepseek" {{ old('provider') == 'deepseek' ? 'selected' : '' }}>DeepSeek (models will be loaded from API)</option>
|
||||
<option value="google" {{ old('provider') == 'google' ? 'selected' : '' }}>Google Gemini (models will be loaded from API)</option>
|
||||
<option value="mistral" {{ old('provider') == 'mistral' ? 'selected' : '' }}>Mistral AI (models will be loaded from API)</option>
|
||||
<option value="cohere" {{ old('provider') == 'cohere' ? 'selected' : '' }}>Cohere (manual entry)</option>
|
||||
<option value="other" {{ old('provider') == 'other' ? 'selected' : '' }}>Other</option>
|
||||
</select>
|
||||
@error('provider')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Model Selection (Dropdown for API providers) -->
|
||||
<div class="mb-4" id="modelSelectContainer">
|
||||
<label for="modelSelect" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Model *
|
||||
</label>
|
||||
<div class="relative">
|
||||
<select name="model" id="modelSelect" required
|
||||
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<option value="">Select provider first...</option>
|
||||
</select>
|
||||
<div id="modelLoading" class="hidden absolute right-10 top-2">
|
||||
<svg class="animate-spin h-5 w-5 text-blue-600" xmlns="http://www.w3.org/2000/svg" fill="none" viewBox="0 0 24 24">
|
||||
<circle class="opacity-25" cx="12" cy="12" r="10" stroke="currentColor" stroke-width="4"></circle>
|
||||
<path class="opacity-75" fill="currentColor" d="M4 12a8 8 0 018-8V0C5.373 0 0 5.373 0 12h4zm2 5.291A7.962 7.962 0 014 12H0c0 3.042 1.135 5.824 3 7.938l3-2.647z"></path>
|
||||
</svg>
|
||||
</div>
|
||||
</div>
|
||||
@error('model')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="mt-1 text-xs text-gray-500" id="modelHint">Select a provider to load available models</p>
|
||||
</div>
|
||||
|
||||
<!-- Model Input (Text for manual entry) -->
|
||||
<div class="mb-4 hidden" id="modelInputContainer">
|
||||
<label for="modelInput" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Model Name *
|
||||
</label>
|
||||
<input type="text" name="model" id="modelInput"
|
||||
value="{{ old('model') }}"
|
||||
placeholder="e.g., claude-3-5-sonnet-20241022"
|
||||
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<p class="mt-1 text-xs text-gray-500" id="modelInputHint">Enter the exact model name</p>
|
||||
</div>
|
||||
|
||||
<!-- Input Price -->
|
||||
<div class="mb-4">
|
||||
<label for="input_price_per_million" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Input Price per Million Tokens *
|
||||
</label>
|
||||
<input type="number" name="input_price_per_million" id="input_price_per_million"
|
||||
value="{{ old('input_price_per_million') }}" step="0.01" min="0" required
|
||||
placeholder="e.g., 3.00"
|
||||
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<div class="relative">
|
||||
<span class="absolute left-3 top-2 text-gray-500">$</span>
|
||||
<input type="number" name="input_price_per_million" id="input_price_per_million"
|
||||
value="{{ old('input_price_per_million') }}" step="0.01" min="0" required
|
||||
placeholder="3.00"
|
||||
class="w-full pl-7 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
@error('input_price_per_million')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="mt-1 text-xs text-gray-500">Price in USD per 1 million input tokens</p>
|
||||
</div>
|
||||
|
||||
<div class="mb-6">
|
||||
<!-- Output Price -->
|
||||
<div class="mb-4">
|
||||
<label for="output_price_per_million" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Output Price per Million Tokens *
|
||||
</label>
|
||||
<input type="number" name="output_price_per_million" id="output_price_per_million"
|
||||
value="{{ old('output_price_per_million') }}" step="0.01" min="0" required
|
||||
placeholder="e.g., 15.00"
|
||||
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
<div class="relative">
|
||||
<span class="absolute left-3 top-2 text-gray-500">$</span>
|
||||
<input type="number" name="output_price_per_million" id="output_price_per_million"
|
||||
value="{{ old('output_price_per_million') }}" step="0.01" min="0" required
|
||||
placeholder="15.00"
|
||||
class="w-full pl-7 rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
</div>
|
||||
@error('output_price_per_million')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
<p class="mt-1 text-xs text-gray-500">Price in USD per 1 million output tokens</p>
|
||||
</div>
|
||||
|
||||
<!-- Context Window (Optional) -->
|
||||
<div class="mb-4">
|
||||
<label for="context_window" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Context Window (tokens)
|
||||
</label>
|
||||
<input type="number" name="context_window" id="context_window"
|
||||
value="{{ old('context_window') }}" min="0"
|
||||
placeholder="e.g., 128000"
|
||||
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
@error('context_window')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<!-- Max Output Tokens (Optional) -->
|
||||
<div class="mb-6">
|
||||
<label for="max_output_tokens" class="block text-sm font-medium text-gray-700 mb-1">
|
||||
Max Output Tokens
|
||||
</label>
|
||||
<input type="number" name="max_output_tokens" id="max_output_tokens"
|
||||
value="{{ old('max_output_tokens') }}" min="0"
|
||||
placeholder="e.g., 4096"
|
||||
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
@error('max_output_tokens')
|
||||
<p class="mt-1 text-sm text-red-600">{{ $message }}</p>
|
||||
@enderror
|
||||
</div>
|
||||
|
||||
<div class="flex justify-end space-x-3">
|
||||
<a href="{{ route('model-pricing.index') }}"
|
||||
class="bg-gray-300 hover:bg-gray-400 text-gray-800 font-bold py-2 px-4 rounded">
|
||||
@@ -68,4 +147,94 @@
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@push('scripts')
|
||||
<script>
|
||||
const providerSelect = document.getElementById('provider');
|
||||
const modelSelectContainer = document.getElementById('modelSelectContainer');
|
||||
const modelInputContainer = document.getElementById('modelInputContainer');
|
||||
const modelSelect = document.getElementById('modelSelect');
|
||||
const modelInput = document.getElementById('modelInput');
|
||||
const modelHint = document.getElementById('modelHint');
|
||||
const modelInputHint = document.getElementById('modelInputHint');
|
||||
const modelLoading = document.getElementById('modelLoading');
|
||||
|
||||
// Fetch models when provider changes
|
||||
providerSelect.addEventListener('change', async function() {
|
||||
const provider = this.value;
|
||||
|
||||
// Reset
|
||||
modelSelect.innerHTML = '<option value="">Select a model...</option>';
|
||||
modelInput.value = '';
|
||||
|
||||
if (!provider) {
|
||||
showModelSelect();
|
||||
modelSelect.disabled = true;
|
||||
modelHint.textContent = 'Select a provider to load available models';
|
||||
return;
|
||||
}
|
||||
|
||||
if (provider === 'other') {
|
||||
showModelInput();
|
||||
modelInputHint.textContent = 'Enter custom model name';
|
||||
return;
|
||||
}
|
||||
|
||||
// Show loading indicator
|
||||
showModelSelect();
|
||||
modelLoading.classList.remove('hidden');
|
||||
modelSelect.disabled = true;
|
||||
modelHint.textContent = 'Loading models from ' + provider + '...';
|
||||
|
||||
try {
|
||||
const response = await fetch(`/api/provider-models/${provider}`);
|
||||
const data = await response.json();
|
||||
|
||||
if (data.success && data.models) {
|
||||
// Provider has API - populate dropdown
|
||||
data.models.forEach(model => {
|
||||
const option = document.createElement('option');
|
||||
option.value = model.id;
|
||||
option.textContent = model.name;
|
||||
modelSelect.appendChild(option);
|
||||
});
|
||||
|
||||
modelSelect.disabled = false;
|
||||
modelHint.textContent = `✓ ${data.models.length} models loaded from ${provider} API`;
|
||||
} else {
|
||||
// Provider doesn't have API - switch to text input
|
||||
showModelInput();
|
||||
modelInputHint.innerHTML = `<span class="text-yellow-600">${data.message}</span>`;
|
||||
}
|
||||
} catch (error) {
|
||||
showModelInput();
|
||||
modelInputHint.innerHTML = `<span class="text-red-600">Error loading models: ${error.message}. Please enter manually.</span>`;
|
||||
} finally {
|
||||
modelLoading.classList.add('hidden');
|
||||
}
|
||||
});
|
||||
|
||||
function showModelSelect() {
|
||||
modelSelectContainer.classList.remove('hidden');
|
||||
modelInputContainer.classList.add('hidden');
|
||||
modelSelect.required = true;
|
||||
modelInput.required = false;
|
||||
modelInput.disabled = true;
|
||||
}
|
||||
|
||||
function showModelInput() {
|
||||
modelSelectContainer.classList.add('hidden');
|
||||
modelInputContainer.classList.remove('hidden');
|
||||
modelSelect.required = false;
|
||||
modelSelect.disabled = true;
|
||||
modelInput.required = true;
|
||||
modelInput.disabled = false;
|
||||
}
|
||||
|
||||
// Trigger change event if provider is pre-selected
|
||||
if (providerSelect.value) {
|
||||
providerSelect.dispatchEvent(new Event('change'));
|
||||
}
|
||||
</script>
|
||||
@endpush
|
||||
</x-app-layout>
|
||||
|
||||
@@ -35,6 +35,9 @@
|
||||
<table class="min-w-full divide-y divide-gray-200">
|
||||
<thead class="bg-gray-50">
|
||||
<tr>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Provider
|
||||
</th>
|
||||
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Model
|
||||
</th>
|
||||
@@ -44,6 +47,9 @@
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Output Price
|
||||
</th>
|
||||
<th class="px-6 py-3 text-center text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Status
|
||||
</th>
|
||||
<th class="px-6 py-3 text-right text-xs font-medium text-gray-500 uppercase tracking-wider">
|
||||
Actions
|
||||
</th>
|
||||
@@ -53,7 +59,10 @@
|
||||
@foreach($modelPricing as $model)
|
||||
<tr>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
|
||||
{{ $model->model_key }}
|
||||
{{ $model->provider }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-900">
|
||||
{{ $model->model }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-right text-blue-600 font-semibold">
|
||||
{{ $model->input_price_formatted }}
|
||||
@@ -61,10 +70,21 @@
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-right text-green-600 font-semibold">
|
||||
{{ $model->output_price_formatted }}
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-sm text-center">
|
||||
@if($model->is_active)
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-green-100 text-green-800">
|
||||
Active
|
||||
</span>
|
||||
@else
|
||||
<span class="px-2 inline-flex text-xs leading-5 font-semibold rounded-full bg-gray-100 text-gray-800">
|
||||
Inactive
|
||||
</span>
|
||||
@endif
|
||||
</td>
|
||||
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
|
||||
<a href="{{ route('model-pricing.edit', $model->model_key) }}"
|
||||
<a href="{{ route('model-pricing.edit', $model->id) }}"
|
||||
class="text-indigo-600 hover:text-indigo-900 mr-3">Edit</a>
|
||||
<form action="{{ route('model-pricing.destroy', $model->model_key) }}"
|
||||
<form action="{{ route('model-pricing.destroy', $model->id) }}"
|
||||
method="POST" class="inline"
|
||||
onsubmit="return confirm('Are you sure?');">
|
||||
@csrf
|
||||
|
||||
Reference in New Issue
Block a user