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

@@ -183,10 +183,10 @@
{{ $key->created_at->format('Y-m-d H:i') }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-right text-sm font-medium">
<a href="{{ route('api-keys.show', $key->id) }}"
<a href="{{ route('api-keys.show', $key->token) }}"
class="text-blue-600 hover:text-blue-900 mr-3">View</a>
@if($key->is_active && !$key->is_expired)
<form action="{{ route('api-keys.revoke', $key->id) }}"
<form action="{{ route('api-keys.revoke', $key->token) }}"
method="POST"
class="inline"
onsubmit="return confirm('Are you sure you want to revoke this API key? This action cannot be undone.');">

View File

@@ -1,7 +1,7 @@
<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 dark:text-gray-200 leading-tight">
{{ __('Dashboard') }} - Any-LLM Gateway
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Dashboard') }} - LLM Gateway
</h2>
</x-slot>
@@ -11,16 +11,16 @@
<!-- Stats Cards -->
<div class="grid grid-cols-1 md:grid-cols-2 lg:grid-cols-4 gap-6">
<!-- Total Users -->
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">Total Users</p>
<p class="text-3xl font-bold text-gray-900 dark:text-gray-100">
<p class="text-sm text-gray-600">Total Users</p>
<p class="text-3xl font-bold text-gray-900">
{{ number_format($stats['total_users']) }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
{{ $stats['active_users'] }} active, {{ $stats['blocked_users'] }} blocked
<p class="text-xs text-gray-500 mt-1">
{{ $stats['active_credentials'] }} active credentials
</p>
</div>
<div class="text-blue-500">
@@ -33,15 +33,15 @@
</div>
<!-- Requests Today -->
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">Requests Today</p>
<p class="text-3xl font-bold text-blue-600 dark:text-blue-400">
<p class="text-sm text-gray-600">Requests Today</p>
<p class="text-3xl font-bold text-blue-600">
{{ number_format($stats['total_requests_today']) }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
<p class="text-xs text-gray-500 mt-1">
{{ number_format($stats['total_requests_month']) }} this month
</p>
</div>
@@ -55,15 +55,15 @@
</div>
<!-- Spend Today -->
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">Spend Today</p>
<p class="text-3xl font-bold text-green-600 dark:text-green-400">
<p class="text-sm text-gray-600">Spend Today</p>
<p class="text-3xl font-bold text-green-600">
${{ number_format($stats['total_spend_today'], 2) }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
<p class="text-xs text-gray-500 mt-1">
${{ number_format($stats['total_spend_month'], 2) }} this month
</p>
</div>
@@ -77,16 +77,16 @@
</div>
<!-- Tokens Today -->
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<div class="flex items-center justify-between">
<div>
<p class="text-sm text-gray-600 dark:text-gray-400">Tokens Today</p>
<p class="text-3xl font-bold text-purple-600 dark:text-purple-400">
<p class="text-sm text-gray-600">Tokens Today</p>
<p class="text-3xl font-bold text-purple-600">
{{ number_format($stats['total_tokens_today']) }}
</p>
<p class="text-xs text-gray-500 dark:text-gray-400 mt-1">
Prompt + Completion
<p class="text-xs text-gray-500 mt-1">
Avg: ${{ number_format($stats['avg_cost_per_request'], 4) }}/req
</p>
</div>
<div class="text-purple-500">
@@ -100,9 +100,9 @@
</div>
<!-- Usage Trend Chart -->
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
<h3 class="text-lg font-semibold text-gray-900 mb-4">
Usage Trend (Last 30 Days)
</h3>
<canvas id="usageChart" height="80"></canvas>
@@ -112,9 +112,9 @@
<!-- Provider Stats & Top Users -->
<div class="grid grid-cols-1 lg:grid-cols-2 gap-6">
<!-- Provider Breakdown -->
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
<h3 class="text-lg font-semibold text-gray-900 mb-4">
Usage by Provider
</h3>
<canvas id="providerChart" height="250"></canvas>
@@ -122,30 +122,33 @@
</div>
<!-- Top Users -->
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
<h3 class="text-lg font-semibold text-gray-900 mb-4">
Top Users by Spend
</h3>
<div class="space-y-4">
@forelse($topUsers as $user)
<div class="flex items-center justify-between border-b border-gray-200 dark:border-gray-700 pb-3">
<div class="flex items-center justify-between border-b border-gray-200 pb-3">
<div class="flex-1">
<p class="font-medium text-gray-900 dark:text-gray-100">
{{ $user->alias ?? $user->user_id }}
<p class="font-medium text-gray-900">
{{ $user->name }}
</p>
<p class="text-sm text-gray-500 dark:text-gray-400">
{{ number_format($user->usage_logs_count ?? 0) }} requests
<p class="text-sm text-gray-500">
{{ number_format($user->llm_requests_count ?? 0) }} requests
</p>
</div>
<div class="text-right">
<p class="font-semibold text-green-600 dark:text-green-400">
${{ number_format($user->usage_logs_sum_cost ?? 0, 2) }}
<p class="font-semibold text-green-600">
${{ number_format($user->total_cost ?? 0, 2) }}
</p>
<p class="text-xs text-gray-500">
{{ number_format($user->total_tokens ?? 0) }} tokens
</p>
</div>
</div>
@empty
<p class="text-gray-500 dark:text-gray-400 text-center py-4">
<p class="text-gray-500 text-center py-4">
No usage data yet
</p>
@endforelse
@@ -155,40 +158,52 @@
</div>
<!-- Model Stats -->
<div class="bg-white dark:bg-gray-800 overflow-hidden shadow-sm sm:rounded-lg">
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6">
<h3 class="text-lg font-semibold text-gray-900 dark:text-gray-100 mb-4">
<h3 class="text-lg font-semibold text-gray-900 mb-4">
Most Used Models
</h3>
<div class="overflow-x-auto">
<table class="min-w-full divide-y divide-gray-200 dark:divide-gray-700">
<thead class="bg-gray-50 dark:bg-gray-900">
<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 dark:text-gray-400 uppercase tracking-wider">Model</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Requests</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Tokens</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 dark:text-gray-400 uppercase tracking-wider">Cost</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Model</th>
<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">Requests</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Tokens</th>
<th class="px-6 py-3 text-left text-xs font-medium text-gray-500 uppercase tracking-wider">Cost</th>
</tr>
</thead>
<tbody class="bg-white dark:bg-gray-800 divide-y divide-gray-200 dark:divide-gray-700">
<tbody class="bg-white divide-y divide-gray-200">
@forelse($modelStats as $model)
<tr>
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900 dark:text-gray-100">
<td class="px-6 py-4 whitespace-nowrap text-sm font-medium text-gray-900">
{{ $model->model }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
<span class="inline-flex items-center px-2 py-1 rounded-full text-xs font-medium
@if($model->provider == 'openai') bg-green-100 text-green-800
@elseif($model->provider == 'anthropic') bg-purple-100 text-purple-800
@elseif($model->provider == 'mistral') bg-blue-100 text-blue-800
@elseif($model->provider == 'gemini') bg-yellow-100 text-yellow-800
@else bg-gray-100 text-gray-800
@endif">
{{ ucfirst($model->provider) }}
</span>
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ number_format($model->count) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
{{ number_format($model->tokens ?? 0) }}
</td>
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500 dark:text-gray-400">
<td class="px-6 py-4 whitespace-nowrap text-sm text-gray-500">
${{ number_format($model->total_cost ?? 0, 4) }}
</td>
</tr>
@empty
<tr>
<td colspan="4" class="px-6 py-4 text-center text-gray-500 dark:text-gray-400">
<td colspan="5" class="px-6 py-4 text-center text-gray-500">
No usage data yet
</td>
</tr>
@@ -245,12 +260,6 @@
title: {
display: true,
text: 'Requests'
},
ticks: {
color: getComputedStyle(document.documentElement).getPropertyValue('--tw-text-opacity') ? '#9CA3AF' : '#6B7280'
},
grid: {
color: 'rgba(156, 163, 175, 0.1)'
}
},
y1: {
@@ -261,27 +270,9 @@
display: true,
text: 'Cost ($)'
},
ticks: {
color: getComputedStyle(document.documentElement).getPropertyValue('--tw-text-opacity') ? '#9CA3AF' : '#6B7280'
},
grid: {
drawOnChartArea: false,
},
},
x: {
ticks: {
color: getComputedStyle(document.documentElement).getPropertyValue('--tw-text-opacity') ? '#9CA3AF' : '#6B7280'
},
grid: {
color: 'rgba(156, 163, 175, 0.1)'
}
}
},
plugins: {
legend: {
labels: {
color: getComputedStyle(document.documentElement).getPropertyValue('--tw-text-opacity') ? '#9CA3AF' : '#6B7280'
}
}
}
}
@@ -292,19 +283,19 @@
new Chart(providerCtx, {
type: 'doughnut',
data: {
labels: @json($providerStats->pluck('provider')),
labels: @json($providerStats->pluck('provider')->map(fn($p) => ucfirst($p))),
datasets: [{
data: @json($providerStats->pluck('count')),
backgroundColor: [
'rgba(59, 130, 246, 0.8)',
'rgba(16, 185, 129, 0.8)',
'rgba(249, 115, 22, 0.8)',
'rgba(168, 85, 247, 0.8)',
'rgba(236, 72, 153, 0.8)',
'rgba(245, 158, 11, 0.8)',
'rgba(34, 197, 94, 0.8)', // Green - OpenAI
'rgba(168, 85, 247, 0.8)', // Purple - Anthropic
'rgba(59, 130, 246, 0.8)', // Blue - Mistral
'rgba(251, 191, 36, 0.8)', // Yellow - Gemini
'rgba(236, 72, 153, 0.8)', // Pink - DeepSeek
'rgba(249, 115, 22, 0.8)', // Orange
],
borderWidth: 2,
borderColor: '#1f2937'
borderColor: '#fff'
}]
},
options: {
@@ -314,7 +305,6 @@
legend: {
position: 'bottom',
labels: {
color: getComputedStyle(document.documentElement).getPropertyValue('--tw-text-opacity') ? '#9CA3AF' : '#6B7280',
padding: 15
}
},
@@ -322,11 +312,10 @@
callbacks: {
label: function(context) {
let label = context.label || '';
if (label) {
label += ': ';
}
label += context.parsed + ' requests';
return label;
let value = context.parsed || 0;
let total = context.dataset.data.reduce((a, b) => a + b, 0);
let percentage = ((value / total) * 100).toFixed(1);
return label + ': ' + value + ' requests (' + percentage + '%)';
}
}
}

View File

@@ -48,6 +48,12 @@ new class extends Component
<x-nav-link :href="route('model-pricing.index')" :active="request()->routeIs('model-pricing.*')" wire:navigate>
{{ __('Pricing') }}
</x-nav-link>
<x-nav-link :href="route('admin.credentials.index')" :active="request()->routeIs('admin.credentials.*')" wire:navigate>
{{ __('Credentials') }}
</x-nav-link>
<x-nav-link :href="route('admin.users.index')" :active="request()->routeIs('admin.users.*')" wire:navigate>
{{ __('User Budgets') }}
</x-nav-link>
</div>
</div>
@@ -114,6 +120,12 @@ new class extends Component
<x-responsive-nav-link :href="route('model-pricing.index')" :active="request()->routeIs('model-pricing.*')" wire:navigate>
{{ __('Pricing') }}
</x-responsive-nav-link>
<x-responsive-nav-link :href="route('admin.credentials.index')" :active="request()->routeIs('admin.credentials.*')" wire:navigate>
{{ __('Credentials') }}
</x-responsive-nav-link>
<x-responsive-nav-link :href="route('admin.users.index')" :active="request()->routeIs('admin.users.*')" wire:navigate>
{{ __('User Budgets') }}
</x-responsive-nav-link>
</div>
<!-- Responsive Settings Options -->

View File

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

View File

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