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}
This commit is contained in:
wtrinkl
2025-11-19 19:36:58 +01:00
parent c65643ac1f
commit cb495e18e3
38 changed files with 1045 additions and 823 deletions

View File

@@ -164,7 +164,7 @@
<div class="flex justify-between items-center mb-4">
<h2 class="text-lg font-semibold text-gray-900">API Keys</h2>
{{-- TODO: Enable when API Keys Management is implemented --}}
{{-- <a href="{{ route('api-keys.create', ['user_id' => $user->user_id]) }}"
{{-- <a href="{{ route('keys.create', ['user_id' => $user->user_id]) }}"
class="text-sm text-indigo-600 hover:text-indigo-900">
+ Create Key
</a> --}}

View File

@@ -4,7 +4,7 @@
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('Create New API Key') }}
</h2>
<a href="{{ route('api-keys.index') }}"
<a href="{{ route('keys.index') }}"
class="inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
@@ -64,7 +64,7 @@
<!-- Create Form -->
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg">
<div class="p-6 text-gray-900">
<form method="POST" action="{{ route('api-keys.store') }}" class="space-y-6">
<form method="POST" action="{{ route('keys.store') }}" class="space-y-6">
@csrf
<!-- Key Name -->
@@ -167,7 +167,7 @@
<!-- Submit Buttons -->
<div class="flex items-center justify-end space-x-4 pt-4">
<a href="{{ route('api-keys.index') }}"
<a href="{{ route('keys.index') }}"
class="inline-flex justify-center py-2 px-4 border border-gray-300 shadow-sm text-sm font-medium rounded-md text-gray-700 bg-white hover:bg-gray-50 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
Cancel
</a>

View File

@@ -4,7 +4,7 @@
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('API Keys Management') }}
</h2>
<a href="{{ route('api-keys.create') }}"
<a href="{{ route('keys.create') }}"
class="inline-flex items-center px-4 py-2 bg-blue-600 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-blue-700 focus:bg-blue-700 active:bg-blue-900 focus:outline-none focus:ring-2 focus:ring-blue-500 focus:ring-offset-2 transition ease-in-out duration-150">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
@@ -44,7 +44,7 @@
<p>This is the only time you'll see this key. Copy it now:</p>
<div class="mt-2 flex items-center">
<code id="new-api-key" class="bg-white px-4 py-2 rounded border border-yellow-300 font-mono text-sm">{{ session('new_api_key') }}</code>
<button onclick="copyToClipboard('new-api-key')"
<button onclick="copyToClipboard('new-api-key', event)"
class="ml-2 px-3 py-2 bg-yellow-500 text-white rounded hover:bg-yellow-600">
Copy
</button>
@@ -58,7 +58,7 @@
<!-- Filters -->
<div class="bg-white overflow-hidden shadow-sm sm:rounded-lg mb-6">
<div class="p-6">
<form method="GET" action="{{ route('api-keys.index') }}" class="grid grid-cols-1 md:grid-cols-4 gap-4">
<form method="GET" action="{{ route('keys.index') }}" class="grid grid-cols-1 md:grid-cols-4 gap-4">
<!-- Search -->
<div>
<label for="search" class="block text-sm font-medium text-gray-700 mb-1">Search</label>
@@ -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->token) }}"
<a href="{{ route('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->token) }}"
<form action="{{ route('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.');">
@@ -215,7 +215,7 @@
<h3 class="mt-2 text-sm font-medium text-gray-900">No API keys found</h3>
<p class="mt-1 text-sm text-gray-500">Get started by creating a new API key.</p>
<div class="mt-6">
<a href="{{ route('api-keys.create') }}"
<a href="{{ route('keys.create') }}"
class="inline-flex items-center px-4 py-2 border border-transparent shadow-sm text-sm font-medium rounded-md text-white bg-blue-600 hover:bg-blue-700 focus:outline-none focus:ring-2 focus:ring-offset-2 focus:ring-blue-500">
<svg class="-ml-1 mr-2 h-5 w-5" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M12 4v16m8-8H4"/>
@@ -232,7 +232,7 @@
@push('scripts')
<script>
function copyToClipboard(elementId) {
function copyToClipboard(elementId, event) {
const element = document.getElementById(elementId);
const text = element.textContent;

View File

@@ -4,7 +4,7 @@
<h2 class="font-semibold text-xl text-gray-800 leading-tight">
{{ __('API Key Details') }}
</h2>
<a href="{{ route('api-keys.index') }}"
<a href="{{ route('keys.index') }}"
class="inline-flex items-center px-4 py-2 bg-gray-800 border border-transparent rounded-md font-semibold text-xs text-white uppercase tracking-widest hover:bg-gray-700">
<svg class="w-4 h-4 mr-2" fill="none" stroke="currentColor" viewBox="0 0 24 24">
<path stroke-linecap="round" stroke-linejoin="round" stroke-width="2" d="M10 19l-7-7m0 0l7-7m-7 7h18"/>
@@ -103,7 +103,7 @@
<!-- Action Buttons -->
@if($apiKey->is_active && !$apiKey->is_expired)
<div class="mt-6 pt-6 border-t border-gray-200">
<form action="{{ route('api-keys.revoke', $apiKey->id) }}"
<form action="{{ route('keys.revoke', $apiKey->id) }}"
method="POST"
onsubmit="return confirm('Are you sure you want to revoke this API key? This action cannot be undone and all requests using this key will be rejected immediately.');">
@csrf

View File

@@ -36,7 +36,7 @@ new class extends Component
<x-nav-link :href="route('gateway-users.index')" :active="request()->routeIs('gateway-users.*')" wire:navigate>
{{ __('Gateway Users') }}
</x-nav-link>
<x-nav-link :href="route('api-keys.index')" :active="request()->routeIs('api-keys.*')" wire:navigate>
<x-nav-link :href="route('keys.index')" :active="request()->routeIs('keys.*')" wire:navigate>
{{ __('API Keys') }}
</x-nav-link>
<x-nav-link :href="route('budgets.index')" :active="request()->routeIs('budgets.*')" wire:navigate>
@@ -108,7 +108,7 @@ new class extends Component
<x-responsive-nav-link :href="route('gateway-users.index')" :active="request()->routeIs('gateway-users.*')" wire:navigate>
{{ __('Gateway Users') }}
</x-responsive-nav-link>
<x-responsive-nav-link :href="route('api-keys.index')" :active="request()->routeIs('api-keys.*')" wire:navigate>
<x-responsive-nav-link :href="route('keys.index')" :active="request()->routeIs('keys.*')" wire:navigate>
{{ __('API Keys') }}
</x-responsive-nav-link>
<x-responsive-nav-link :href="route('budgets.index')" :active="request()->routeIs('budgets.*')" wire:navigate>

View File

@@ -187,7 +187,7 @@
modelHint.textContent = 'Loading models from ' + provider + '...';
try {
const response = await fetch(`/api/provider-models/${provider}`);
const response = await fetch(`/admin/provider-models/${provider}`);
const data = await response.json();
if (data.success && data.models) {