Files
wtrinkl cb495e18e3 Fix API controllers to use correct database column names
- Fix model_pricing table references (model_id -> model, display_name -> model)
- Fix price columns (output_price_per_1k -> output_price_per_million)
- Add price conversion (per_million / 1000 = per_1k) in all API responses
- Add whereNotNull('model') filters to exclude invalid entries
- Add getModelDisplayName() helper method to all controllers
- Fix AccountController to use gateway_users budget fields directly
- Remove Budget model dependencies from AccountController
- Add custom Scramble server URL configuration for API docs
- Create ScrambleServiceProvider to set correct /api prefix
- Add migration to rename user_id to gateway_user_id in llm_requests
- Add custom ApiGuard for gateway_users authentication
- Update all API controllers: AccountController, ModelController, PricingController, ProviderController

All API endpoints now working correctly:
- GET /api/account
- GET /api/models
- GET /api/pricing
- GET /api/providers/{provider}
2025-11-19 19:36:58 +01:00

241 lines
13 KiB
PHP

<x-app-layout>
<x-slot name="header">
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
Add New Model Pricing
</h2>
</x-slot>
<div class="py-12">
<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') }}" id="pricingForm">
@csrf
<!-- Provider Selection -->
<div class="mb-4">
<label for="provider" class="block text-sm font-medium text-gray-700 mb-1">
Provider *
</label>
<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>
<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>
<!-- 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>
<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">
Cancel
</a>
<button type="submit"
class="bg-blue-500 hover:bg-blue-700 text-white font-bold py-2 px-4 rounded">
Create Model Pricing
</button>
</div>
</form>
</div>
</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(`/admin/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>