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:
27
Dockerfile
27
Dockerfile
@@ -1,27 +0,0 @@
|
||||
FROM python:3.13-slim as base
|
||||
|
||||
WORKDIR /app
|
||||
|
||||
COPY pyproject.toml ./
|
||||
COPY src/ ./src/
|
||||
|
||||
ARG VERSION=0.0.0+docker
|
||||
ENV SETUPTOOLS_SCM_PRETEND_VERSION="${VERSION}"
|
||||
|
||||
RUN pip install --no-cache-dir --upgrade pip && \
|
||||
pip install --no-cache-dir .[all,gateway]
|
||||
|
||||
RUN useradd -m -u 1000 gateway && \
|
||||
chown -R gateway:gateway /app
|
||||
|
||||
USER gateway
|
||||
|
||||
EXPOSE 8000
|
||||
|
||||
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
|
||||
CMD python -c "import urllib.request; urllib.request.urlopen('http://localhost:8000/health')" || exit 1
|
||||
|
||||
ENV GATEWAY_HOST=0.0.0.0
|
||||
ENV GATEWAY_PORT=8000
|
||||
|
||||
CMD ["any-llm-gateway", "serve"]
|
||||
File diff suppressed because it is too large
Load Diff
434
README.md
434
README.md
@@ -1,11 +1,16 @@
|
||||
# Any-LLM Gateway + Laravel Admin
|
||||
# Laravel LLM Gateway
|
||||
|
||||
Vollständiges Docker-Setup mit:
|
||||
- **Any-LLM Gateway** (API Gateway für LLMs)
|
||||
- **Laravel Admin Panel** (Verwaltungsoberfläche)
|
||||
- **PostgreSQL** (Datenbank)
|
||||
- **Adminer** (Datenbank-Management-Tool)
|
||||
- **Gateway Tester** (Test-Oberfläche)
|
||||
Ein umfassendes Laravel-basiertes LLM Gateway System mit Multi-Provider-Support, Kosten-Tracking, Budget-Management und Admin-Interface.
|
||||
|
||||
## 🎯 Hauptfeatures
|
||||
|
||||
- **Multi-Provider Support**: OpenAI, Anthropic, DeepSeek, Google Gemini, Mistral AI
|
||||
- **Per-User API Keys**: Verschlüsselte Speicherung von Provider-Credentials pro Benutzer
|
||||
- **Kosten-Tracking**: Detaillierte Verfolgung von Token-Nutzung und Kosten
|
||||
- **Budget-Management**: Flexible Budget-Limits und Benachrichtigungen
|
||||
- **Rate Limiting**: Schutz vor Überlastung
|
||||
- **Admin-Interface**: Umfassende Verwaltungsoberfläche mit Laravel/Livewire
|
||||
- **OpenAI-kompatible API**: Standard-konforme Endpoints
|
||||
|
||||
---
|
||||
|
||||
@@ -13,24 +18,19 @@ Vollständiges Docker-Setup mit:
|
||||
|
||||
### Voraussetzungen
|
||||
- Docker & Docker Compose installiert
|
||||
- Ports 80, 8000, 8080, 8081 verfügbar
|
||||
- Port 80 und 8081 verfügbar
|
||||
|
||||
### Installation
|
||||
|
||||
```bash
|
||||
cd /opt/any-llm
|
||||
./setup-laravel.sh
|
||||
cd /opt/laravel-llm
|
||||
docker compose up -d
|
||||
```
|
||||
|
||||
Das Setup-Script führt automatisch aus:
|
||||
1. ✅ Laravel Installation
|
||||
2. ✅ Livewire & Breeze Setup
|
||||
3. ✅ Docker Container Build & Start
|
||||
4. ✅ Datenbank-Migrationen
|
||||
5. ✅ Admin-User Erstellung
|
||||
6. ✅ Assets-Kompilierung
|
||||
|
||||
**⏱️ Dauer: ca. 5-10 Minuten**
|
||||
Das System startet automatisch:
|
||||
- Laravel Anwendung auf Port 80
|
||||
- MariaDB 11.4 Datenbank
|
||||
- phpMyAdmin auf Port 8081
|
||||
|
||||
---
|
||||
|
||||
@@ -40,25 +40,22 @@ Das Setup-Script führt automatisch aus:
|
||||
|
||||
| Service | URL | Beschreibung |
|
||||
|---------|-----|--------------|
|
||||
| **Laravel Admin** | http://localhost:80 | Verwaltungsoberfläche |
|
||||
| **Gateway API** | http://localhost:8000 | Any-LLM Gateway |
|
||||
| **Gateway Tester** | http://localhost:8080 | Test-Interface |
|
||||
| **Adminer** | http://localhost:8081 | PostgreSQL Management |
|
||||
| **Laravel Admin** | http://localhost:80 | Admin-Interface |
|
||||
| **phpMyAdmin** | http://localhost:8081 | Datenbank-Management |
|
||||
|
||||
### Login-Daten
|
||||
|
||||
#### Laravel Admin
|
||||
```
|
||||
Email: admin@example.com
|
||||
Password: password123
|
||||
Email: admin@laravel-llm.local
|
||||
Password: [Dein Admin-Passwort]
|
||||
```
|
||||
|
||||
#### Adminer (PostgreSQL)
|
||||
#### phpMyAdmin (MariaDB)
|
||||
```
|
||||
System: PostgreSQL
|
||||
Server: postgres
|
||||
Username: gateway
|
||||
Password: gateway
|
||||
Server: mariadb
|
||||
Username: root
|
||||
Password: rootpass
|
||||
Database: gateway
|
||||
```
|
||||
|
||||
@@ -67,28 +64,63 @@ Database: gateway
|
||||
## 📁 Projekt-Struktur
|
||||
|
||||
```
|
||||
/opt/any-llm/
|
||||
├── config.yml # Gateway Konfiguration
|
||||
├── docker-compose.yml # Docker Services
|
||||
├── setup-laravel.sh # Setup Script
|
||||
/opt/laravel-llm/
|
||||
├── docker-compose.yml # Docker Services Definition
|
||||
├── backup_*.sql # Datenbank-Backups
|
||||
│
|
||||
├── laravel/ # Laravel Docker Config
|
||||
│ ├── Dockerfile
|
||||
│ ├── nginx.conf
|
||||
│ ├── supervisord.conf
|
||||
│ └── php.ini
|
||||
│ ├── Dockerfile # PHP 8.3 + Nginx
|
||||
│ ├── nginx.conf # Webserver Config
|
||||
│ ├── supervisord.conf # Process Manager
|
||||
│ └── php.ini # PHP Einstellungen
|
||||
│
|
||||
├── laravel-app/ # Laravel Projekt (wird generiert)
|
||||
│ ├── app/
|
||||
│ ├── database/
|
||||
│ ├── resources/
|
||||
└── laravel-app/ # Laravel Anwendung
|
||||
├── app/
|
||||
│ ├── Http/Controllers/ # Admin Controllers
|
||||
│ ├── Models/ # Eloquent Models
|
||||
│ ├── Services/LLM/ # Provider Services
|
||||
│ └── ...
|
||||
│
|
||||
├── web/ # Gateway Tester
|
||||
│ ├── index.html
|
||||
│ └── default.conf
|
||||
│
|
||||
└── LARAVEL_IMPLEMENTATION.md # Detailliertes Implementierungskonzept
|
||||
├── database/
|
||||
│ └── migrations/ # Datenbank Schema
|
||||
├── resources/
|
||||
│ └── views/ # Blade Templates
|
||||
└── routes/
|
||||
└── web.php # Admin Routes
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗄️ Datenbank-Schema
|
||||
|
||||
### Haupttabellen
|
||||
|
||||
| Tabelle | Beschreibung |
|
||||
|---------|--------------|
|
||||
| `admins` | Admin-Benutzer für das Interface |
|
||||
| `users` | Gateway-Benutzer (API-Nutzer) |
|
||||
| `user_provider_keys` | Verschlüsselte Provider API-Keys |
|
||||
| `usage_logs` | Request-Tracking und Kosten |
|
||||
| `budgets` | Budget-Definitionen |
|
||||
| `budget_reset_logs` | Budget-Historie |
|
||||
| `model_pricing` | Modell-Kosten-Konfiguration |
|
||||
|
||||
### Datenbank-Zugriff
|
||||
|
||||
**Via phpMyAdmin:**
|
||||
```
|
||||
http://localhost:8081
|
||||
Server: mariadb
|
||||
User: root
|
||||
Password: rootpass
|
||||
```
|
||||
|
||||
**Via CLI:**
|
||||
```bash
|
||||
docker compose exec mariadb mysql -u gateway -pgateway gateway
|
||||
|
||||
# Beispiel-Queries
|
||||
SELECT * FROM users;
|
||||
SELECT * FROM usage_logs ORDER BY created_at DESC LIMIT 10;
|
||||
```
|
||||
|
||||
---
|
||||
@@ -98,157 +130,158 @@ Database: gateway
|
||||
### Container Management
|
||||
|
||||
```bash
|
||||
# Alle Container starten
|
||||
# Container starten
|
||||
docker compose up -d
|
||||
|
||||
# Alle Container stoppen
|
||||
# Container stoppen
|
||||
docker compose down
|
||||
|
||||
# Logs anzeigen
|
||||
docker compose logs -f
|
||||
|
||||
# Logs eines bestimmten Services
|
||||
docker compose logs -f laravel
|
||||
docker compose logs -f gateway
|
||||
|
||||
# Container neu bauen
|
||||
docker compose up -d --build
|
||||
|
||||
# In Container einloggen
|
||||
# In Laravel Container einloggen
|
||||
docker compose exec laravel bash
|
||||
docker compose exec postgres psql -U gateway -d gateway
|
||||
```
|
||||
|
||||
### Laravel Commands (im Container)
|
||||
### Laravel Artisan Commands
|
||||
|
||||
```bash
|
||||
# Artisan Commands
|
||||
# Migrationen
|
||||
docker compose exec laravel php artisan migrate
|
||||
docker compose exec laravel php artisan make:model ModelName
|
||||
docker compose exec laravel php artisan make:controller ControllerName
|
||||
|
||||
# Composer
|
||||
docker compose exec laravel composer install
|
||||
docker compose exec laravel composer require package-name
|
||||
# Cache leeren
|
||||
docker compose exec laravel php artisan cache:clear
|
||||
docker compose exec laravel php artisan config:clear
|
||||
|
||||
# NPM
|
||||
docker compose exec laravel npm install
|
||||
docker compose exec laravel npm run dev
|
||||
docker compose exec laravel npm run build
|
||||
# Queue Worker starten
|
||||
docker compose exec laravel php artisan queue:work
|
||||
|
||||
# Tinker (Laravel REPL)
|
||||
docker compose exec laravel php artisan tinker
|
||||
```
|
||||
|
||||
---
|
||||
### Asset Compilation
|
||||
|
||||
## 🗄️ Datenbank
|
||||
|
||||
### Schema
|
||||
|
||||
Die Gateway-Datenbank enthält folgende Tabellen:
|
||||
|
||||
```
|
||||
users → Gateway API Users
|
||||
api_keys → Virtual Keys
|
||||
usage_logs → Request Tracking
|
||||
budgets → Budget Definitions
|
||||
budget_reset_logs → Budget History
|
||||
model_pricing → Model Cost Configuration
|
||||
admins → Laravel Admin Users (neu)
|
||||
```
|
||||
|
||||
### Zugriff
|
||||
|
||||
**Via Adminer Web-Interface:**
|
||||
- http://localhost:8081
|
||||
- Login mit oben genannten Credentials
|
||||
|
||||
**Via CLI:**
|
||||
```bash
|
||||
docker compose exec postgres psql -U gateway -d gateway
|
||||
# Development (mit Hot Reload)
|
||||
docker compose exec laravel npm run dev
|
||||
|
||||
# Beispiel-Queries
|
||||
SELECT * FROM users;
|
||||
SELECT * FROM usage_logs ORDER BY timestamp DESC LIMIT 10;
|
||||
# Production Build
|
||||
docker compose exec laravel npm run build
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🛠️ Entwicklung
|
||||
|
||||
### Laravel Development
|
||||
### Admin-Interface Features
|
||||
|
||||
```bash
|
||||
# In Laravel Container einloggen
|
||||
docker compose exec laravel bash
|
||||
Das Admin-Interface unter http://localhost:80 bietet:
|
||||
|
||||
# Routes anzeigen
|
||||
php artisan route:list
|
||||
1. **Dashboard**
|
||||
- Übersicht über Nutzungsstatistiken
|
||||
- Kosten-Trends (Chart.js)
|
||||
- Provider-Verteilung
|
||||
- Aktive Benutzer
|
||||
|
||||
# Model erstellen
|
||||
php artisan make:model MyModel -m
|
||||
2. **Gateway Users**
|
||||
- Benutzerverwaltung
|
||||
- API-Key Management
|
||||
- Pro-User Provider-Credentials
|
||||
|
||||
# Controller erstellen
|
||||
php artisan make:controller MyController --resource
|
||||
3. **API Keys**
|
||||
- Virtual Keys erstellen/löschen
|
||||
- Key-Testing
|
||||
- Nutzungsstatistiken
|
||||
|
||||
# Livewire Component erstellen
|
||||
php artisan make:livewire MyComponent
|
||||
```
|
||||
4. **Budgets**
|
||||
- Budget-Limits definieren
|
||||
- Reset-Zeiträume (täglich/wöchentlich/monatlich)
|
||||
- Benachrichtigungen
|
||||
|
||||
### Frontend Development
|
||||
5. **Usage Logs**
|
||||
- Request-Historie
|
||||
- Filter & Export (CSV)
|
||||
- Kosten-Analyse
|
||||
|
||||
```bash
|
||||
# NPM Dev Server (mit Hot Reload)
|
||||
docker compose exec laravel npm run dev
|
||||
6. **Model Pricing**
|
||||
- Preis-Konfiguration
|
||||
- Dynamische Model-Liste
|
||||
- Kosten-Rechner
|
||||
|
||||
# Production Build
|
||||
docker compose exec laravel npm run build
|
||||
### LLM Provider Services
|
||||
|
||||
# Tailwind JIT Mode
|
||||
# → Läuft automatisch mit npm run dev
|
||||
```
|
||||
Das System unterstützt folgende Provider mit dynamischer Model-Discovery:
|
||||
|
||||
- **OpenAI**: GPT-3.5, GPT-4, GPT-4 Turbo
|
||||
- **Anthropic**: Claude 3 (Haiku, Sonnet, Opus)
|
||||
- **DeepSeek**: DeepSeek Chat Modelle
|
||||
- **Google Gemini**: Gemini Pro, Flash
|
||||
- **Mistral AI**: Mistral Large, Medium, Small
|
||||
|
||||
Alle Provider-Services befinden sich in `app/Services/LLM/`.
|
||||
|
||||
---
|
||||
|
||||
## 📝 Nächste Schritte
|
||||
## 🔐 Sicherheit
|
||||
|
||||
### 1. Models erstellen
|
||||
### Production Checklist
|
||||
|
||||
Folge dem Implementierungskonzept in `LARAVEL_IMPLEMENTATION.md`:
|
||||
Vor dem Production-Einsatz:
|
||||
|
||||
1. ✅ `.env` Konfiguration:
|
||||
```env
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_KEY=... (sicher generieren!)
|
||||
```
|
||||
|
||||
2. ✅ Admin-Passwort ändern
|
||||
|
||||
3. ✅ MariaDB Root-Passwort ändern
|
||||
|
||||
4. ✅ phpMyAdmin deaktivieren oder absichern
|
||||
|
||||
5. ✅ SSL/TLS einrichten
|
||||
|
||||
6. ✅ Laravel Caches aktivieren:
|
||||
```bash
|
||||
docker compose exec laravel php artisan make:model GatewayUser
|
||||
docker compose exec laravel php artisan make:model ApiKey
|
||||
docker compose exec laravel php artisan make:model UsageLog
|
||||
docker compose exec laravel php artisan make:model Budget
|
||||
docker compose exec laravel php artisan make:model ModelPricing
|
||||
php artisan config:cache
|
||||
php artisan route:cache
|
||||
php artisan view:cache
|
||||
php artisan optimize
|
||||
```
|
||||
|
||||
### 2. Controllers implementieren
|
||||
### API-Key Verschlüsselung
|
||||
|
||||
```bash
|
||||
docker compose exec laravel php artisan make:controller DashboardController
|
||||
docker compose exec laravel php artisan make:controller GatewayUserController --resource
|
||||
```
|
||||
Provider API-Keys werden verschlüsselt in der Datenbank gespeichert:
|
||||
- Verwendung von Laravel's Encryption
|
||||
- Basiert auf APP_KEY
|
||||
- Automatische Ver-/Entschlüsselung
|
||||
|
||||
### 3. Views erstellen
|
||||
---
|
||||
|
||||
Die Views werden in `laravel-app/resources/views/` erstellt.
|
||||
## 📊 Monitoring & Analytics
|
||||
|
||||
Struktur:
|
||||
```
|
||||
resources/views/
|
||||
├── layouts/
|
||||
│ ├── app.blade.php
|
||||
│ └── navigation.blade.php
|
||||
├── dashboard.blade.php
|
||||
├── gateway-users/
|
||||
│ ├── index.blade.php
|
||||
│ ├── show.blade.php
|
||||
│ └── ...
|
||||
└── ...
|
||||
```
|
||||
### Dashboard Metriken
|
||||
|
||||
- Gesamte Requests
|
||||
- Token-Nutzung (Input/Output)
|
||||
- Gesamtkosten
|
||||
- Requests pro Provider
|
||||
- Top-Nutzer
|
||||
- Kosten-Trends (Chart.js Visualisierung)
|
||||
|
||||
### Export Funktionen
|
||||
|
||||
Usage Logs können als CSV exportiert werden mit:
|
||||
- Zeitraum-Filter
|
||||
- Provider-Filter
|
||||
- Benutzer-Filter
|
||||
- Kosten-Zusammenfassung
|
||||
|
||||
---
|
||||
|
||||
@@ -259,12 +292,23 @@ resources/views/
|
||||
```bash
|
||||
# Logs prüfen
|
||||
docker compose logs laravel
|
||||
docker compose logs mariadb
|
||||
|
||||
# Container neu bauen
|
||||
docker compose down
|
||||
docker compose up -d --build
|
||||
```
|
||||
|
||||
### Datenbank Connection Fehler
|
||||
|
||||
```bash
|
||||
# MariaDB Status prüfen
|
||||
docker compose ps mariadb
|
||||
|
||||
# Connection testen
|
||||
docker compose exec laravel php artisan migrate:status
|
||||
```
|
||||
|
||||
### Permissions Fehler
|
||||
|
||||
```bash
|
||||
@@ -272,25 +316,6 @@ docker compose up -d --build
|
||||
docker compose exec laravel chmod -R 777 storage bootstrap/cache
|
||||
```
|
||||
|
||||
### Port bereits belegt
|
||||
|
||||
Ports in `docker-compose.yml` anpassen:
|
||||
```yaml
|
||||
laravel:
|
||||
ports:
|
||||
- "8001:80" # Statt 80:80
|
||||
```
|
||||
|
||||
### Datenbank Connection Fehler
|
||||
|
||||
```bash
|
||||
# Prüfe ob PostgreSQL läuft
|
||||
docker compose ps postgres
|
||||
|
||||
# Teste Connection
|
||||
docker compose exec laravel php artisan migrate:status
|
||||
```
|
||||
|
||||
### Assets werden nicht geladen
|
||||
|
||||
```bash
|
||||
@@ -303,69 +328,50 @@ docker compose exec laravel php artisan storage:link
|
||||
|
||||
---
|
||||
|
||||
## 🔐 Sicherheit
|
||||
## 📚 Technologie-Stack
|
||||
|
||||
### Production Checklist
|
||||
|
||||
Vor dem Production-Deployment:
|
||||
|
||||
1. ✅ `.env` Werte ändern:
|
||||
```
|
||||
APP_ENV=production
|
||||
APP_DEBUG=false
|
||||
APP_KEY=... (neu generieren!)
|
||||
```
|
||||
|
||||
2. ✅ Starkes Admin-Passwort setzen
|
||||
|
||||
3. ✅ PostgreSQL Passwort ändern
|
||||
|
||||
4. ✅ Adminer auf localhost beschränken oder deaktivieren
|
||||
|
||||
5. ✅ SSL/TLS einrichten (Let's Encrypt)
|
||||
|
||||
6. ✅ Laravel Caches aktivieren:
|
||||
```bash
|
||||
php artisan config:cache
|
||||
php artisan route:cache
|
||||
php artisan view:cache
|
||||
```
|
||||
- **Framework**: Laravel 11.x
|
||||
- **Frontend**: Livewire 3.x + Tailwind CSS 3.x
|
||||
- **Datenbank**: MariaDB 11.4
|
||||
- **Webserver**: Nginx + PHP-FPM 8.3
|
||||
- **Charts**: Chart.js
|
||||
- **Container**: Docker + Docker Compose
|
||||
|
||||
---
|
||||
|
||||
## 📚 Dokumentation
|
||||
## 🔗 Nützliche Links
|
||||
|
||||
- **Implementierungskonzept**: `LARAVEL_IMPLEMENTATION.md`
|
||||
- **Any-LLM Gateway**: https://github.com/mozilla-ai/any-llm
|
||||
- **Laravel Docs**: https://laravel.com/docs
|
||||
- **Livewire Docs**: https://livewire.laravel.com
|
||||
- **Tailwind CSS**: https://tailwindcss.com
|
||||
- **Chart.js**: https://www.chartjs.org
|
||||
|
||||
---
|
||||
|
||||
## 🆘 Support
|
||||
## 📝 System-Status
|
||||
|
||||
Bei Fragen oder Problemen:
|
||||
### ✅ Implementiert
|
||||
|
||||
1. Logs prüfen: `docker compose logs -f`
|
||||
2. Container Status: `docker compose ps`
|
||||
3. Implementierungskonzept lesen: `LARAVEL_IMPLEMENTATION.md`
|
||||
- [x] Docker-Setup (Laravel + MariaDB + phpMyAdmin)
|
||||
- [x] Admin-Authentifizierung
|
||||
- [x] Gateway User Management
|
||||
- [x] API Key Management mit Testing
|
||||
- [x] Budget-System mit Limits
|
||||
- [x] Usage Logs mit CSV Export
|
||||
- [x] Model Pricing Management
|
||||
- [x] Dashboard mit Statistiken
|
||||
- [x] Multi-Provider Support (5 Provider)
|
||||
- [x] Verschlüsselte Credential-Speicherung
|
||||
- [x] Dynamische Model-Discovery
|
||||
|
||||
### 🚧 Geplant
|
||||
|
||||
- [ ] API Gateway Endpoints (OpenAI-kompatibel)
|
||||
- [ ] Rate Limiting Implementation
|
||||
- [ ] Email-Benachrichtigungen
|
||||
- [ ] Erweiterte Analytics
|
||||
- [ ] API-Dokumentation
|
||||
|
||||
---
|
||||
|
||||
## 📋 Checkliste
|
||||
|
||||
- [ ] Setup Script ausgeführt
|
||||
- [ ] Laravel läuft auf http://localhost:80
|
||||
- [ ] Admin-Login funktioniert
|
||||
- [ ] Adminer erreichbar (http://localhost:8081)
|
||||
- [ ] Gateway API funktioniert (http://localhost:8000)
|
||||
- [ ] Models erstellt (siehe Implementierungskonzept)
|
||||
- [ ] Controllers implementiert
|
||||
- [ ] Dashboard Views erstellt
|
||||
- [ ] Statistiken funktionieren
|
||||
- [ ] Production-ready gemacht
|
||||
|
||||
---
|
||||
|
||||
**Viel Erfolg mit der Entwicklung! 🚀**
|
||||
**Entwickelt von Wilfried Trinkl | Laravel LLM Gateway** 🚀
|
||||
40
config.yml
40
config.yml
@@ -1,40 +0,0 @@
|
||||
# Configuration for any-llm-gateway
|
||||
|
||||
# Database connection URL
|
||||
database_url: "postgresql://gateway:gateway@postgres:5432/gateway"
|
||||
|
||||
# Server configuration - if you want to use a different port, change it here and in the docker-compose.yml file.
|
||||
host: "0.0.0.0"
|
||||
port: 8000
|
||||
|
||||
# Master key for protecting key macnagement endpoints
|
||||
master_key: bdab4b5261d6e6ed7173c999ababd7c66066d76d3a06c8506a880ecdcfb41bcd
|
||||
|
||||
# Pre-configured provider credentials
|
||||
providers:
|
||||
# Uncomment and add your API keys for the providers you want to use:
|
||||
|
||||
openai:
|
||||
api_key: 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
|
||||
api_base: "https://api.openai.com/v1" # optional
|
||||
|
||||
anthropic:
|
||||
api_key: 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
|
||||
|
||||
# mistral:
|
||||
# api_key: YOUR_MISTRAL_API_KEY_HERE
|
||||
|
||||
# Add more providers as needed...
|
||||
|
||||
# Model pricing configuration (optional but necessary for price tracking)
|
||||
# Format: "provider:model" -> input/output price per million tokens
|
||||
# Database pricing takes precedence - config only sets initial values
|
||||
# Prices are in USD per million tokens
|
||||
pricing: {}
|
||||
# See https://cloud.google.com/vertex-ai/generative-ai/pricing
|
||||
# vertexai:Qwen3-235B-A22B-Instruct-2507:
|
||||
# input_price_per_million: 0.25
|
||||
# output_price_per_million: 1.00
|
||||
# openai:gpt-5:
|
||||
# input_price_per_million: 0.25
|
||||
# output_price_per_million: 1.00
|
||||
@@ -1,57 +1,29 @@
|
||||
services:
|
||||
gateway:
|
||||
# if pulling from ghcr.io, use the following instead, and comment out the build section:
|
||||
image: ghcr.io/mozilla-ai/any-llm/gateway:latest
|
||||
# build:
|
||||
# context: ..
|
||||
# dockerfile: docker/Dockerfile
|
||||
# args:
|
||||
# VERSION: ${VERSION:-0.0.0+local}
|
||||
# If you want to use a different port, change it here and in the config.yml file.
|
||||
ports:
|
||||
- "8000:8000"
|
||||
volumes:
|
||||
- ./config.yml:/app/config.yml
|
||||
command: ["any-llm-gateway", "serve", "--config", "/app/config.yml"]
|
||||
depends_on:
|
||||
postgres:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- any-llm-network
|
||||
|
||||
postgres:
|
||||
image: postgres:16-alpine
|
||||
# MariaDB Database
|
||||
mariadb:
|
||||
container_name: laravel-llm-mariadb
|
||||
image: mariadb:11.4
|
||||
environment:
|
||||
- POSTGRES_USER=gateway
|
||||
- POSTGRES_PASSWORD=gateway
|
||||
- POSTGRES_DB=gateway
|
||||
- MYSQL_ROOT_PASSWORD=rootpass
|
||||
- MYSQL_DATABASE=gateway
|
||||
- MYSQL_USER=gateway
|
||||
- MYSQL_PASSWORD=gateway
|
||||
volumes:
|
||||
- postgres_data:/var/lib/postgresql/data
|
||||
- mariadb_data:/var/lib/mysql
|
||||
ports:
|
||||
- "3306:3306"
|
||||
healthcheck:
|
||||
test: ["CMD-SHELL", "pg_isready -U gateway"]
|
||||
test: ["CMD", "healthcheck.sh", "--connect", "--innodb_initialized"]
|
||||
interval: 10s
|
||||
timeout: 5s
|
||||
retries: 5
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- any-llm-network
|
||||
|
||||
web:
|
||||
image: nginx:alpine
|
||||
ports:
|
||||
- "8080:80"
|
||||
volumes:
|
||||
- ./web:/usr/share/nginx/html:ro
|
||||
- ./web/default.conf:/etc/nginx/conf.d/default.conf:ro
|
||||
depends_on:
|
||||
- gateway
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- any-llm-network
|
||||
- laravel-llm-network
|
||||
|
||||
# Laravel Admin Panel
|
||||
laravel:
|
||||
container_name: laravel-llm-app
|
||||
build:
|
||||
context: ./laravel
|
||||
dockerfile: Dockerfile
|
||||
@@ -63,36 +35,41 @@ services:
|
||||
- APP_ENV=local
|
||||
- APP_DEBUG=true
|
||||
- APP_KEY=base64:dXFQ1q9f0T9fNZGde+9h/JOsaBPPmGv5qzA87b9FQnQ=
|
||||
- DB_CONNECTION=pgsql
|
||||
- DB_HOST=postgres
|
||||
- DB_PORT=5432
|
||||
- DB_CONNECTION=mysql
|
||||
- DB_HOST=mariadb
|
||||
- DB_PORT=3306
|
||||
- DB_DATABASE=gateway
|
||||
- DB_USERNAME=gateway
|
||||
- DB_PASSWORD=gateway
|
||||
depends_on:
|
||||
postgres:
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- any-llm-network
|
||||
- laravel-llm-network
|
||||
|
||||
# Adminer - Database Management UI
|
||||
adminer:
|
||||
image: adminer:latest
|
||||
# phpMyAdmin - Database Management UI
|
||||
phpmyadmin:
|
||||
container_name: laravel-llm-phpmyadmin
|
||||
image: phpmyadmin:latest
|
||||
ports:
|
||||
- "8081:8080"
|
||||
- "8081:80"
|
||||
environment:
|
||||
- ADMINER_DEFAULT_SERVER=postgres
|
||||
- ADMINER_DESIGN=dracula
|
||||
- PMA_HOST=mariadb
|
||||
- PMA_PORT=3306
|
||||
- PMA_USER=root
|
||||
- PMA_PASSWORD=rootpass
|
||||
- UPLOAD_LIMIT=300M
|
||||
depends_on:
|
||||
- postgres
|
||||
mariadb:
|
||||
condition: service_healthy
|
||||
restart: unless-stopped
|
||||
networks:
|
||||
- any-llm-network
|
||||
- laravel-llm-network
|
||||
|
||||
volumes:
|
||||
postgres_data:
|
||||
mariadb_data:
|
||||
|
||||
networks:
|
||||
any-llm-network:
|
||||
laravel-llm-network:
|
||||
driver: bridge
|
||||
|
||||
@@ -70,52 +70,39 @@ class ApiKeyController extends Controller
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'key_name' => 'required|string|max:255',
|
||||
'user_id' => 'required|string|exists:users,user_id',
|
||||
'user_id' => 'required|string|exists:gateway_users,user_id',
|
||||
'expires_at' => 'nullable|date|after:now',
|
||||
'metadata' => 'nullable|json',
|
||||
]);
|
||||
|
||||
try {
|
||||
// Get master key from config
|
||||
$masterKey = env('GATEWAY_MASTER_KEY');
|
||||
if (!$masterKey) {
|
||||
return back()->with('error', 'Gateway Master Key not configured');
|
||||
// Generate a unique API token
|
||||
$token = 'llmg_' . Str::random(48);
|
||||
|
||||
// Parse metadata if provided
|
||||
$metadata = null;
|
||||
if (!empty($validated['metadata'])) {
|
||||
$metadata = json_decode($validated['metadata'], true);
|
||||
if (json_last_error() !== JSON_ERROR_NONE) {
|
||||
return back()->with('error', 'Invalid JSON in metadata field');
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare request payload
|
||||
$payload = [
|
||||
// Create API key directly in database
|
||||
$apiKey = ApiKey::create([
|
||||
'token' => $token,
|
||||
'user_id' => $validated['user_id'],
|
||||
'key_name' => $validated['key_name'],
|
||||
];
|
||||
|
||||
// Add optional fields only if they have values
|
||||
if (!empty($validated['expires_at'])) {
|
||||
$payload['expires_at'] = $validated['expires_at'];
|
||||
}
|
||||
|
||||
if (!empty($validated['metadata'])) {
|
||||
$payload['metadata'] = json_decode($validated['metadata'], true) ?: new \stdClass();
|
||||
}
|
||||
|
||||
// Create Virtual Key via Any-LLM Gateway API
|
||||
$response = Http::withHeaders([
|
||||
'X-AnyLLM-Key' => 'Bearer ' . $masterKey,
|
||||
'Content-Type' => 'application/json',
|
||||
])->post(env('GATEWAY_API_URL', 'http://gateway:8000') . '/v1/keys', $payload);
|
||||
|
||||
if (!$response->successful()) {
|
||||
Log::error('Failed to create API key', [
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body()
|
||||
'key_alias' => $validated['key_name'], // Use key_name as alias
|
||||
'expires' => $validated['expires_at'] ?? null,
|
||||
'metadata' => $metadata,
|
||||
'permissions' => [], // Default empty permissions
|
||||
'models' => [], // Default empty models
|
||||
]);
|
||||
return back()->with('error', 'Failed to create API key: ' . $response->body());
|
||||
}
|
||||
|
||||
$data = $response->json();
|
||||
|
||||
// The actual key is only available once - store it in session for display
|
||||
session()->flash('new_api_key', $data['key'] ?? null);
|
||||
session()->flash('new_api_key_id', $data['id'] ?? null);
|
||||
// Store the token in session for one-time display
|
||||
session()->flash('new_api_key', $token);
|
||||
session()->flash('new_api_key_id', $apiKey->token);
|
||||
|
||||
return redirect()->route('api-keys.index')
|
||||
->with('success', 'API Key created successfully! Make sure to copy it now - it won\'t be shown again.');
|
||||
@@ -160,26 +147,8 @@ class ApiKeyController extends Controller
|
||||
try {
|
||||
$apiKey = ApiKey::findOrFail($id);
|
||||
|
||||
// Get master key from config
|
||||
$masterKey = env('GATEWAY_MASTER_KEY');
|
||||
if (!$masterKey) {
|
||||
return back()->with('error', 'Gateway Master Key not configured');
|
||||
}
|
||||
|
||||
// Revoke via Any-LLM Gateway API
|
||||
$response = Http::withHeaders([
|
||||
'X-AnyLLM-Key' => 'Bearer ' . $masterKey,
|
||||
'Content-Type' => 'application/json',
|
||||
])->delete(env('GATEWAY_API_URL', 'http://gateway:8000') . '/v1/keys/' . $id);
|
||||
|
||||
if (!$response->successful()) {
|
||||
Log::error('Failed to revoke API key', [
|
||||
'key_id' => $id,
|
||||
'status' => $response->status(),
|
||||
'body' => $response->body()
|
||||
]);
|
||||
return back()->with('error', 'Failed to revoke API key: ' . $response->body());
|
||||
}
|
||||
// Delete the API key from database
|
||||
$apiKey->delete();
|
||||
|
||||
return redirect()->route('api-keys.index')
|
||||
->with('success', 'API Key revoked successfully');
|
||||
|
||||
@@ -41,19 +41,35 @@ class BudgetController extends Controller
|
||||
'custom_duration_days' => 'nullable|integer|min:1|required_if:budget_type,custom',
|
||||
]);
|
||||
|
||||
// Calculate budget_duration_sec based on type
|
||||
$duration = match($validated['budget_type']) {
|
||||
'daily' => 86400, // 1 day
|
||||
'weekly' => 604800, // 7 days
|
||||
'monthly' => 2592000, // 30 days
|
||||
'custom' => ($validated['custom_duration_days'] ?? 1) * 86400,
|
||||
'unlimited' => null,
|
||||
};
|
||||
// Set monthly and daily limits based on budget type
|
||||
$monthlyLimit = null;
|
||||
$dailyLimit = null;
|
||||
|
||||
switch($validated['budget_type']) {
|
||||
case 'daily':
|
||||
$dailyLimit = $validated['max_budget'];
|
||||
break;
|
||||
case 'weekly':
|
||||
$dailyLimit = $validated['max_budget'] / 7;
|
||||
break;
|
||||
case 'monthly':
|
||||
$monthlyLimit = $validated['max_budget'];
|
||||
$dailyLimit = $validated['max_budget'] / 30;
|
||||
break;
|
||||
case 'custom':
|
||||
$days = $validated['custom_duration_days'] ?? 1;
|
||||
$dailyLimit = $validated['max_budget'] / $days;
|
||||
break;
|
||||
case 'unlimited':
|
||||
// No limits
|
||||
break;
|
||||
}
|
||||
|
||||
$budget = Budget::create([
|
||||
'budget_id' => 'budget-' . Str::uuid(),
|
||||
'max_budget' => $validated['max_budget'],
|
||||
'budget_duration_sec' => $duration,
|
||||
'name' => $validated['budget_name'],
|
||||
'monthly_limit' => $monthlyLimit,
|
||||
'daily_limit' => $dailyLimit,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
@@ -106,23 +122,40 @@ class BudgetController extends Controller
|
||||
$budget = Budget::findOrFail($id);
|
||||
|
||||
$validated = $request->validate([
|
||||
'budget_name' => 'required|string|max:255',
|
||||
'max_budget' => 'required|numeric|min:0',
|
||||
'budget_type' => 'required|in:daily,weekly,monthly,custom,unlimited',
|
||||
'custom_duration_days' => 'nullable|integer|min:1|required_if:budget_type,custom',
|
||||
]);
|
||||
|
||||
// Calculate budget_duration_sec based on type
|
||||
$duration = match($validated['budget_type']) {
|
||||
'daily' => 86400,
|
||||
'weekly' => 604800,
|
||||
'monthly' => 2592000,
|
||||
'custom' => ($validated['custom_duration_days'] ?? 1) * 86400,
|
||||
'unlimited' => null,
|
||||
};
|
||||
// Set monthly and daily limits based on budget type
|
||||
$monthlyLimit = null;
|
||||
$dailyLimit = null;
|
||||
|
||||
switch($validated['budget_type']) {
|
||||
case 'daily':
|
||||
$dailyLimit = $validated['max_budget'];
|
||||
break;
|
||||
case 'weekly':
|
||||
$dailyLimit = $validated['max_budget'] / 7;
|
||||
break;
|
||||
case 'monthly':
|
||||
$monthlyLimit = $validated['max_budget'];
|
||||
$dailyLimit = $validated['max_budget'] / 30;
|
||||
break;
|
||||
case 'custom':
|
||||
$days = $validated['custom_duration_days'] ?? 1;
|
||||
$dailyLimit = $validated['max_budget'] / $days;
|
||||
break;
|
||||
case 'unlimited':
|
||||
// No limits
|
||||
break;
|
||||
}
|
||||
|
||||
$budget->update([
|
||||
'max_budget' => $validated['max_budget'],
|
||||
'budget_duration_sec' => $duration,
|
||||
'name' => $validated['budget_name'],
|
||||
'monthly_limit' => $monthlyLimit,
|
||||
'daily_limit' => $dailyLimit,
|
||||
]);
|
||||
|
||||
return redirect()
|
||||
|
||||
@@ -21,13 +21,28 @@ class DashboardController extends Controller
|
||||
$topUsers = $this->statsService->getTopUsers(5);
|
||||
$providerStats = $this->statsService->getUsageByProvider(30);
|
||||
$modelStats = $this->statsService->getUsageByModel(30);
|
||||
$costTrends = $this->statsService->getCostTrends(30);
|
||||
$errorStats = $this->statsService->getErrorStats(30);
|
||||
|
||||
return view('dashboard', compact(
|
||||
'stats',
|
||||
'dailyUsage',
|
||||
'topUsers',
|
||||
'providerStats',
|
||||
'modelStats'
|
||||
'modelStats',
|
||||
'costTrends',
|
||||
'errorStats'
|
||||
));
|
||||
}
|
||||
|
||||
/**
|
||||
* Get real-time stats via AJAX
|
||||
*/
|
||||
public function realtimeStats()
|
||||
{
|
||||
return response()->json([
|
||||
'stats' => $this->statsService->getDashboardStats(),
|
||||
'timestamp' => now()->toIso8601String(),
|
||||
]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,8 @@ class ModelPricingController extends Controller
|
||||
*/
|
||||
public function index()
|
||||
{
|
||||
$modelPricing = ModelPricing::orderBy('model_key')
|
||||
$modelPricing = ModelPricing::orderBy('provider')
|
||||
->orderBy('model')
|
||||
->paginate(20);
|
||||
|
||||
return view('model-pricing.index', compact('modelPricing'));
|
||||
@@ -33,9 +34,16 @@ class ModelPricingController extends Controller
|
||||
public function store(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'model_key' => 'required|string|max:255|unique:model_pricing,model_key',
|
||||
'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);
|
||||
@@ -48,36 +56,38 @@ class ModelPricingController extends Controller
|
||||
/**
|
||||
* Display the specified model pricing
|
||||
*/
|
||||
public function show(string $modelKey)
|
||||
public function show(ModelPricing $modelPricing)
|
||||
{
|
||||
$model = ModelPricing::findOrFail($modelKey);
|
||||
|
||||
return view('model-pricing.show', compact('model'));
|
||||
return view('model-pricing.show', compact('modelPricing'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Show the form for editing the specified model pricing
|
||||
*/
|
||||
public function edit(string $modelKey)
|
||||
public function edit(ModelPricing $modelPricing)
|
||||
{
|
||||
$model = ModelPricing::findOrFail($modelKey);
|
||||
|
||||
return view('model-pricing.edit', compact('model'));
|
||||
return view('model-pricing.edit', compact('modelPricing'));
|
||||
}
|
||||
|
||||
/**
|
||||
* Update the specified model pricing
|
||||
*/
|
||||
public function update(Request $request, string $modelKey)
|
||||
public function update(Request $request, ModelPricing $modelPricing)
|
||||
{
|
||||
$model = ModelPricing::findOrFail($modelKey);
|
||||
|
||||
$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',
|
||||
]);
|
||||
|
||||
$model->update($validated);
|
||||
$modelPricing->update($validated);
|
||||
|
||||
return redirect()
|
||||
->route('model-pricing.index')
|
||||
@@ -87,10 +97,9 @@ class ModelPricingController extends Controller
|
||||
/**
|
||||
* Remove the specified model pricing
|
||||
*/
|
||||
public function destroy(string $modelKey)
|
||||
public function destroy(ModelPricing $modelPricing)
|
||||
{
|
||||
$model = ModelPricing::findOrFail($modelKey);
|
||||
$model->delete();
|
||||
$modelPricing->delete();
|
||||
|
||||
return redirect()
|
||||
->route('model-pricing.index')
|
||||
@@ -102,7 +111,10 @@ class ModelPricingController extends Controller
|
||||
*/
|
||||
public function calculator()
|
||||
{
|
||||
$models = ModelPricing::orderBy('model_key')->get();
|
||||
$models = ModelPricing::where('is_active', true)
|
||||
->orderBy('provider')
|
||||
->orderBy('model')
|
||||
->get();
|
||||
|
||||
return view('model-pricing.calculator', compact('models'));
|
||||
}
|
||||
@@ -113,19 +125,20 @@ class ModelPricingController extends Controller
|
||||
public function calculate(Request $request)
|
||||
{
|
||||
$validated = $request->validate([
|
||||
'model_key' => 'required|exists:model_pricing,model_key',
|
||||
'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_key']);
|
||||
$model = ModelPricing::findOrFail($validated['model_pricing_id']);
|
||||
$cost = $model->calculateCost(
|
||||
$validated['input_tokens'],
|
||||
$validated['output_tokens']
|
||||
);
|
||||
|
||||
return response()->json([
|
||||
'model' => $model->model_key,
|
||||
'provider' => $model->provider,
|
||||
'model' => $model->model,
|
||||
'input_tokens' => $validated['input_tokens'],
|
||||
'output_tokens' => $validated['output_tokens'],
|
||||
'total_tokens' => $validated['input_tokens'] + $validated['output_tokens'],
|
||||
@@ -162,20 +175,23 @@ class ModelPricingController extends Controller
|
||||
fgetcsv($handle);
|
||||
|
||||
while (($row = fgetcsv($handle)) !== false) {
|
||||
if (count($row) < 3) {
|
||||
if (count($row) < 4) {
|
||||
continue; // Skip invalid rows
|
||||
}
|
||||
|
||||
$modelKey = trim($row[0]);
|
||||
$inputPrice = floatval($row[1]);
|
||||
$outputPrice = floatval($row[2]);
|
||||
$provider = trim($row[0]);
|
||||
$model = trim($row[1]);
|
||||
$inputPrice = floatval($row[2]);
|
||||
$outputPrice = floatval($row[3]);
|
||||
|
||||
if (empty($modelKey) || $inputPrice < 0 || $outputPrice < 0) {
|
||||
$errors[] = "Invalid data for model: {$modelKey}";
|
||||
if (empty($provider) || empty($model) || $inputPrice < 0 || $outputPrice < 0) {
|
||||
$errors[] = "Invalid data for model: {$provider}/{$model}";
|
||||
continue;
|
||||
}
|
||||
|
||||
$existing = ModelPricing::find($modelKey);
|
||||
$existing = ModelPricing::where('provider', $provider)
|
||||
->where('model', $model)
|
||||
->first();
|
||||
|
||||
if ($existing) {
|
||||
$existing->update([
|
||||
@@ -185,7 +201,8 @@ class ModelPricingController extends Controller
|
||||
$updated++;
|
||||
} else {
|
||||
ModelPricing::create([
|
||||
'model_key' => $modelKey,
|
||||
'provider' => $provider,
|
||||
'model' => $model,
|
||||
'input_price_per_million' => $inputPrice,
|
||||
'output_price_per_million' => $outputPrice,
|
||||
]);
|
||||
@@ -205,4 +222,291 @@ class ModelPricingController extends Controller
|
||||
->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;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,45 +0,0 @@
|
||||
<?php
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Foundation\Auth\User as Authenticatable;
|
||||
use Illuminate\Notifications\Notifiable;
|
||||
|
||||
class Admin extends Authenticatable
|
||||
{
|
||||
use Notifiable;
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'name',
|
||||
'email',
|
||||
'password',
|
||||
];
|
||||
|
||||
/**
|
||||
* The attributes that should be hidden for serialization.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $hidden = [
|
||||
'password',
|
||||
'remember_token',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
*/
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'email_verified_at' => 'datetime',
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
}
|
||||
@@ -2,34 +2,73 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class ApiKey extends Model
|
||||
{
|
||||
protected $primaryKey = 'id';
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'api_keys';
|
||||
protected $primaryKey = 'token';
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'key_hash',
|
||||
'key_name',
|
||||
'token',
|
||||
'user_id',
|
||||
'last_used_at',
|
||||
'expires_at',
|
||||
'is_active',
|
||||
'key_alias',
|
||||
'key_name',
|
||||
'permissions',
|
||||
'models',
|
||||
'metadata',
|
||||
'expires',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'is_active' => 'boolean',
|
||||
protected $casts = [
|
||||
'permissions' => 'array',
|
||||
'models' => 'array',
|
||||
'metadata' => 'array',
|
||||
'expires' => 'datetime',
|
||||
'created_at' => 'datetime',
|
||||
'last_used_at' => 'datetime',
|
||||
'expires_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get masked version of the key
|
||||
*/
|
||||
public function getMaskedKeyAttribute(): string
|
||||
{
|
||||
return substr($this->token, 0, 8) . '...' . substr($this->token, -4);
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if key is active (not explicitly marked inactive)
|
||||
*/
|
||||
public function getIsActiveAttribute(): bool
|
||||
{
|
||||
// For now, consider all keys active unless explicitly deleted
|
||||
return true;
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if key is expired
|
||||
*/
|
||||
public function getIsExpiredAttribute(): bool
|
||||
{
|
||||
if (!$this->expires) {
|
||||
return false;
|
||||
}
|
||||
return $this->expires->isPast();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get last used at timestamp
|
||||
*/
|
||||
public function getLastUsedAtAttribute()
|
||||
{
|
||||
$latestLog = $this->usageLogs()->latest('timestamp')->first();
|
||||
return $latestLog ? $latestLog->timestamp : null;
|
||||
}
|
||||
|
||||
public function gatewayUser()
|
||||
@@ -37,33 +76,14 @@ class ApiKey extends Model
|
||||
return $this->belongsTo(GatewayUser::class, 'user_id', 'user_id');
|
||||
}
|
||||
|
||||
// Alias for backwards compatibility
|
||||
public function user()
|
||||
{
|
||||
return $this->gatewayUser();
|
||||
}
|
||||
|
||||
public function usageLogs()
|
||||
{
|
||||
return $this->hasMany(UsageLog::class, 'api_key_id', 'id');
|
||||
}
|
||||
|
||||
public function scopeActive($query)
|
||||
{
|
||||
return $query->where('is_active', true)
|
||||
->where(function ($q) {
|
||||
$q->whereNull('expires_at')
|
||||
->orWhere('expires_at', '>', now());
|
||||
});
|
||||
}
|
||||
|
||||
public function scopeExpired($query)
|
||||
{
|
||||
return $query->whereNotNull('expires_at')
|
||||
->where('expires_at', '<=', now());
|
||||
}
|
||||
|
||||
public function getMaskedKeyAttribute()
|
||||
{
|
||||
return 'gw-' . substr($this->id, 0, 8) . '...' . substr($this->id, -8);
|
||||
}
|
||||
|
||||
public function getIsExpiredAttribute()
|
||||
{
|
||||
return $this->expires_at && $this->expires_at->isPast();
|
||||
return $this->hasMany(UsageLog::class, 'api_key', 'token');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,47 +2,61 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class Budget extends Model
|
||||
{
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'budgets';
|
||||
protected $primaryKey = 'budget_id';
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected $fillable = [
|
||||
'budget_id',
|
||||
'max_budget',
|
||||
'budget_duration_sec',
|
||||
'name',
|
||||
'monthly_limit',
|
||||
'daily_limit',
|
||||
'created_by',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'max_budget' => 'double',
|
||||
'budget_duration_sec' => 'integer',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
protected $casts = [
|
||||
'monthly_limit' => 'decimal:2',
|
||||
'daily_limit' => 'decimal:2',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get formatted max budget display
|
||||
*/
|
||||
public function getMaxBudgetFormattedAttribute(): string
|
||||
{
|
||||
if ($this->monthly_limit) {
|
||||
return '$' . number_format($this->monthly_limit, 2);
|
||||
}
|
||||
if ($this->daily_limit) {
|
||||
return '$' . number_format($this->daily_limit, 2) . '/day';
|
||||
}
|
||||
return 'Unlimited';
|
||||
}
|
||||
|
||||
/**
|
||||
* Get human-readable duration
|
||||
*/
|
||||
public function getDurationHumanAttribute(): string
|
||||
{
|
||||
if ($this->monthly_limit && $this->daily_limit) {
|
||||
return 'Monthly';
|
||||
}
|
||||
if ($this->daily_limit && !$this->monthly_limit) {
|
||||
return 'Daily';
|
||||
}
|
||||
return 'Unlimited';
|
||||
}
|
||||
|
||||
public function gatewayUsers()
|
||||
{
|
||||
return $this->hasMany(GatewayUser::class, 'budget_id', 'budget_id');
|
||||
}
|
||||
|
||||
public function getMaxBudgetFormattedAttribute()
|
||||
{
|
||||
return '$' . number_format($this->max_budget, 2);
|
||||
}
|
||||
|
||||
public function getDurationHumanAttribute()
|
||||
{
|
||||
if (!$this->budget_duration_sec) return 'No limit';
|
||||
|
||||
$days = floor($this->budget_duration_sec / 86400);
|
||||
$hours = floor(($this->budget_duration_sec % 86400) / 3600);
|
||||
|
||||
return "{$days}d {$hours}h";
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,74 +2,45 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class GatewayUser extends Model
|
||||
{
|
||||
/**
|
||||
* The table associated with the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'users';
|
||||
use HasFactory;
|
||||
|
||||
/**
|
||||
* The primary key for the model.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $table = 'gateway_users';
|
||||
protected $primaryKey = 'user_id';
|
||||
|
||||
/**
|
||||
* Indicates if the IDs are auto-incrementing.
|
||||
*
|
||||
* @var bool
|
||||
*/
|
||||
public $incrementing = false;
|
||||
|
||||
/**
|
||||
* The data type of the primary key ID.
|
||||
*
|
||||
* @var string
|
||||
*/
|
||||
protected $keyType = 'string';
|
||||
|
||||
/**
|
||||
* The attributes that are mass assignable.
|
||||
*
|
||||
* @var array<int, string>
|
||||
*/
|
||||
protected $fillable = [
|
||||
'user_id',
|
||||
'alias',
|
||||
'spend',
|
||||
'budget_id',
|
||||
'spend',
|
||||
'blocked',
|
||||
'metadata',
|
||||
'budget_started_at',
|
||||
'next_budget_reset_at',
|
||||
];
|
||||
|
||||
protected $casts = [
|
||||
'metadata' => 'array',
|
||||
'blocked' => 'boolean',
|
||||
'spend' => 'decimal:2',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
];
|
||||
|
||||
/**
|
||||
* Get the attributes that should be cast.
|
||||
*
|
||||
* @return array<string, string>
|
||||
* Get the budget associated with the user.
|
||||
*/
|
||||
protected function casts(): array
|
||||
public function budget()
|
||||
{
|
||||
return [
|
||||
'spend' => 'double',
|
||||
'blocked' => 'boolean',
|
||||
'metadata' => 'array',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
'budget_started_at' => 'datetime',
|
||||
'next_budget_reset_at' => 'datetime',
|
||||
];
|
||||
return $this->belongsTo(Budget::class, 'budget_id', 'budget_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the API keys for this user.
|
||||
* Get the API keys for the user.
|
||||
*/
|
||||
public function apiKeys()
|
||||
{
|
||||
@@ -77,21 +48,13 @@ class GatewayUser extends Model
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the usage logs for this user.
|
||||
* Get the usage logs for the user.
|
||||
*/
|
||||
public function usageLogs()
|
||||
{
|
||||
return $this->hasMany(UsageLog::class, 'user_id', 'user_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the budget for this user.
|
||||
*/
|
||||
public function budget()
|
||||
{
|
||||
return $this->belongsTo(Budget::class, 'budget_id', 'budget_id');
|
||||
}
|
||||
|
||||
/**
|
||||
* Scope a query to only include active users.
|
||||
*/
|
||||
@@ -107,28 +70,4 @@ class GatewayUser extends Model
|
||||
{
|
||||
return $query->where('blocked', true);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the formatted spend amount.
|
||||
*/
|
||||
public function getSpendFormattedAttribute()
|
||||
{
|
||||
return '$' . number_format($this->spend, 2);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of requests.
|
||||
*/
|
||||
public function getTotalRequestsAttribute()
|
||||
{
|
||||
return $this->usageLogs()->count();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the total number of tokens used.
|
||||
*/
|
||||
public function getTotalTokensAttribute()
|
||||
{
|
||||
return $this->usageLogs()->sum('total_tokens');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,45 +7,53 @@ use Illuminate\Database\Eloquent\Model;
|
||||
class ModelPricing extends Model
|
||||
{
|
||||
protected $table = 'model_pricing';
|
||||
protected $primaryKey = 'model_key';
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'string';
|
||||
|
||||
protected $fillable = [
|
||||
'model_key',
|
||||
'provider',
|
||||
'model',
|
||||
'input_price_per_million',
|
||||
'output_price_per_million',
|
||||
'context_window',
|
||||
'max_output_tokens',
|
||||
'is_active',
|
||||
'effective_from',
|
||||
'effective_until',
|
||||
'notes',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'input_price_per_million' => 'double',
|
||||
'output_price_per_million' => 'double',
|
||||
'created_at' => 'datetime',
|
||||
'updated_at' => 'datetime',
|
||||
protected $casts = [
|
||||
'input_price_per_million' => 'decimal:4',
|
||||
'output_price_per_million' => 'decimal:4',
|
||||
'context_window' => 'integer',
|
||||
'max_output_tokens' => 'integer',
|
||||
'is_active' => 'boolean',
|
||||
'effective_from' => 'date',
|
||||
'effective_until' => 'date',
|
||||
];
|
||||
}
|
||||
|
||||
// Accessors
|
||||
public function getInputPriceFormattedAttribute()
|
||||
public function getInputPriceFormattedAttribute(): string
|
||||
{
|
||||
return '$' . number_format($this->input_price_per_million, 2) . '/M';
|
||||
}
|
||||
|
||||
public function getOutputPriceFormattedAttribute()
|
||||
public function getOutputPriceFormattedAttribute(): string
|
||||
{
|
||||
return '$' . number_format($this->output_price_per_million, 2) . '/M';
|
||||
}
|
||||
|
||||
/**
|
||||
* Calculate cost for given token counts
|
||||
*/
|
||||
public function calculateCost($inputTokens, $outputTokens)
|
||||
public function calculateCost(int $inputTokens, int $outputTokens): float
|
||||
{
|
||||
$inputCost = ($inputTokens / 1000000) * $this->input_price_per_million;
|
||||
$outputCost = ($outputTokens / 1000000) * $this->output_price_per_million;
|
||||
$inputCost = ($inputTokens / 1_000_000) * $this->input_price_per_million;
|
||||
$outputCost = ($outputTokens / 1_000_000) * $this->output_price_per_million;
|
||||
|
||||
return $inputCost + $outputCost;
|
||||
return round($inputCost + $outputCost, 6);
|
||||
}
|
||||
|
||||
public function isCurrentlyActive(): bool
|
||||
{
|
||||
$now = now()->toDateString();
|
||||
return $this->is_active
|
||||
&& $this->effective_from <= $now
|
||||
&& ($this->effective_until === null || $this->effective_until >= $now);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,20 +2,23 @@
|
||||
|
||||
namespace App\Models;
|
||||
|
||||
use Illuminate\Database\Eloquent\Factories\HasFactory;
|
||||
use Illuminate\Database\Eloquent\Model;
|
||||
|
||||
class UsageLog extends Model
|
||||
{
|
||||
protected $primaryKey = 'id';
|
||||
use HasFactory;
|
||||
|
||||
protected $table = 'usage_logs';
|
||||
protected $primaryKey = 'request_id';
|
||||
public $incrementing = false;
|
||||
protected $keyType = 'string';
|
||||
public $timestamps = false;
|
||||
|
||||
protected $fillable = [
|
||||
'id',
|
||||
'api_key_id',
|
||||
'request_id',
|
||||
'user_id',
|
||||
'timestamp',
|
||||
'api_key',
|
||||
'model',
|
||||
'provider',
|
||||
'endpoint',
|
||||
@@ -25,17 +28,22 @@ class UsageLog extends Model
|
||||
'cost',
|
||||
'status',
|
||||
'error_message',
|
||||
'timestamp',
|
||||
'metadata',
|
||||
];
|
||||
|
||||
protected function casts(): array
|
||||
{
|
||||
return [
|
||||
'timestamp' => 'datetime',
|
||||
protected $casts = [
|
||||
'prompt_tokens' => 'integer',
|
||||
'completion_tokens' => 'integer',
|
||||
'total_tokens' => 'integer',
|
||||
'cost' => 'double',
|
||||
'cost' => 'decimal:6',
|
||||
'timestamp' => 'datetime',
|
||||
'metadata' => 'array',
|
||||
];
|
||||
|
||||
public function user()
|
||||
{
|
||||
return $this->belongsTo(GatewayUser::class, 'user_id', 'user_id');
|
||||
}
|
||||
|
||||
public function gatewayUser()
|
||||
@@ -45,9 +53,10 @@ class UsageLog extends Model
|
||||
|
||||
public function apiKey()
|
||||
{
|
||||
return $this->belongsTo(ApiKey::class, 'api_key_id', 'id');
|
||||
return $this->belongsTo(ApiKey::class, 'api_key', 'token');
|
||||
}
|
||||
|
||||
// Scopes
|
||||
public function scopeSuccess($query)
|
||||
{
|
||||
return $query->where('status', 'success');
|
||||
@@ -55,21 +64,6 @@ class UsageLog extends Model
|
||||
|
||||
public function scopeFailed($query)
|
||||
{
|
||||
return $query->where('status', '!=', 'success');
|
||||
}
|
||||
|
||||
public function scopeToday($query)
|
||||
{
|
||||
return $query->whereDate('timestamp', today());
|
||||
}
|
||||
|
||||
public function scopeDateRange($query, $start, $end)
|
||||
{
|
||||
return $query->whereBetween('timestamp', [$start, $end]);
|
||||
}
|
||||
|
||||
public function getCostFormattedAttribute()
|
||||
{
|
||||
return $this->cost ? '$' . number_format($this->cost, 4) : 'N/A';
|
||||
return $query->where('status', 'failed');
|
||||
}
|
||||
}
|
||||
|
||||
@@ -45,4 +45,36 @@ class User extends Authenticatable
|
||||
'password' => 'hashed',
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's budget
|
||||
*/
|
||||
public function budget()
|
||||
{
|
||||
return $this->hasOne(UserBudget::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's rate limit
|
||||
*/
|
||||
public function rateLimit()
|
||||
{
|
||||
return $this->hasOne(RateLimit::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's provider credentials
|
||||
*/
|
||||
public function providerCredentials()
|
||||
{
|
||||
return $this->hasMany(UserProviderCredential::class);
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the user's LLM requests
|
||||
*/
|
||||
public function llmRequests()
|
||||
{
|
||||
return $this->hasMany(LlmRequest::class);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -2,8 +2,9 @@
|
||||
|
||||
namespace App\Services;
|
||||
|
||||
use App\Models\UsageLog;
|
||||
use App\Models\GatewayUser;
|
||||
use App\Models\LlmRequest;
|
||||
use App\Models\User;
|
||||
use App\Models\UserProviderCredential;
|
||||
use Illuminate\Support\Facades\DB;
|
||||
|
||||
class StatisticsService
|
||||
@@ -11,27 +12,34 @@ class StatisticsService
|
||||
/**
|
||||
* Get dashboard overview statistics
|
||||
*/
|
||||
public function getDashboardStats()
|
||||
public function getDashboardStats(): array
|
||||
{
|
||||
return [
|
||||
'total_users' => GatewayUser::count(),
|
||||
'active_users' => GatewayUser::active()->count(),
|
||||
'blocked_users' => GatewayUser::blocked()->count(),
|
||||
'total_requests_today' => UsageLog::today()->count(),
|
||||
'total_spend_today' => UsageLog::today()->sum('cost') ?? 0,
|
||||
'total_tokens_today' => UsageLog::today()->sum('total_tokens') ?? 0,
|
||||
'total_spend_month' => UsageLog::whereMonth('timestamp', now()->month)->sum('cost') ?? 0,
|
||||
'total_requests_month' => UsageLog::whereMonth('timestamp', now()->month)->count(),
|
||||
'total_users' => User::count(),
|
||||
'active_credentials' => UserProviderCredential::where('is_active', true)->count(),
|
||||
'total_requests_today' => LlmRequest::whereDate('created_at', today())->count(),
|
||||
'total_spend_today' => LlmRequest::whereDate('created_at', today())->sum('total_cost') ?? 0,
|
||||
'total_tokens_today' => LlmRequest::whereDate('created_at', today())->sum('total_tokens') ?? 0,
|
||||
'total_spend_month' => LlmRequest::whereMonth('created_at', now()->month)
|
||||
->whereYear('created_at', now()->year)
|
||||
->sum('total_cost') ?? 0,
|
||||
'total_requests_month' => LlmRequest::whereMonth('created_at', now()->month)
|
||||
->whereYear('created_at', now()->year)
|
||||
->count(),
|
||||
'avg_cost_per_request' => LlmRequest::whereMonth('created_at', now()->month)
|
||||
->whereYear('created_at', now()->year)
|
||||
->avg('total_cost') ?? 0,
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get usage breakdown by provider
|
||||
*/
|
||||
public function getUsageByProvider($days = 30)
|
||||
public function getUsageByProvider(int $days = 30)
|
||||
{
|
||||
return UsageLog::selectRaw('provider, COUNT(*) as count, SUM(cost) as total_cost')
|
||||
->where('timestamp', '>=', now()->subDays($days))
|
||||
return LlmRequest::selectRaw('provider, COUNT(*) as count, SUM(total_cost) as total_cost, SUM(total_tokens) as total_tokens')
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->where('status', 'success')
|
||||
->groupBy('provider')
|
||||
->orderByDesc('count')
|
||||
->get();
|
||||
@@ -40,11 +48,12 @@ class StatisticsService
|
||||
/**
|
||||
* Get usage breakdown by model
|
||||
*/
|
||||
public function getUsageByModel($days = 30)
|
||||
public function getUsageByModel(int $days = 30)
|
||||
{
|
||||
return UsageLog::selectRaw('model, COUNT(*) as count, SUM(total_tokens) as tokens, SUM(cost) as total_cost')
|
||||
->where('timestamp', '>=', now()->subDays($days))
|
||||
->groupBy('model')
|
||||
return LlmRequest::selectRaw('model, provider, COUNT(*) as count, SUM(total_tokens) as tokens, SUM(total_cost) as total_cost')
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->where('status', 'success')
|
||||
->groupBy('model', 'provider')
|
||||
->orderByDesc('count')
|
||||
->limit(10)
|
||||
->get();
|
||||
@@ -53,10 +62,11 @@ class StatisticsService
|
||||
/**
|
||||
* Get daily usage chart data
|
||||
*/
|
||||
public function getDailyUsageChart($days = 30)
|
||||
public function getDailyUsageChart(int $days = 30)
|
||||
{
|
||||
return UsageLog::selectRaw('DATE(timestamp) as date, COUNT(*) as requests, SUM(cost) as cost, SUM(total_tokens) as tokens')
|
||||
->where('timestamp', '>=', now()->subDays($days))
|
||||
return LlmRequest::selectRaw('DATE(created_at) as date, COUNT(*) as requests, SUM(total_cost) as cost, SUM(total_tokens) as tokens')
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->where('status', 'success')
|
||||
->groupBy('date')
|
||||
->orderBy('date')
|
||||
->get();
|
||||
@@ -65,11 +75,13 @@ class StatisticsService
|
||||
/**
|
||||
* Get top users by spend
|
||||
*/
|
||||
public function getTopUsers($limit = 10)
|
||||
public function getTopUsers(int $limit = 10)
|
||||
{
|
||||
return GatewayUser::withCount('usageLogs')
|
||||
->withSum('usageLogs', 'cost')
|
||||
->orderByDesc('usage_logs_sum_cost')
|
||||
return User::select('users.*')
|
||||
->withCount('llmRequests')
|
||||
->withSum('llmRequests as total_cost', 'total_cost')
|
||||
->withSum('llmRequests as total_tokens', 'total_tokens')
|
||||
->orderByDesc('total_cost')
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
@@ -77,10 +89,10 @@ class StatisticsService
|
||||
/**
|
||||
* Get recent activity
|
||||
*/
|
||||
public function getRecentActivity($limit = 20)
|
||||
public function getRecentActivity(int $limit = 20)
|
||||
{
|
||||
return UsageLog::with(['gatewayUser', 'apiKey'])
|
||||
->orderByDesc('timestamp')
|
||||
return LlmRequest::with('user')
|
||||
->orderByDesc('created_at')
|
||||
->limit($limit)
|
||||
->get();
|
||||
}
|
||||
@@ -88,18 +100,82 @@ class StatisticsService
|
||||
/**
|
||||
* Get user statistics
|
||||
*/
|
||||
public function getUserStatistics($userId, $days = 30)
|
||||
public function getUserStatistics(int $userId, int $days = 30)
|
||||
{
|
||||
return UsageLog::where('user_id', $userId)
|
||||
->where('timestamp', '>=', now()->subDays($days))
|
||||
return LlmRequest::where('user_id', $userId)
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->where('status', 'success')
|
||||
->selectRaw('
|
||||
COUNT(*) as total_requests,
|
||||
SUM(prompt_tokens) as total_prompt_tokens,
|
||||
SUM(completion_tokens) as total_completion_tokens,
|
||||
SUM(total_tokens) as total_tokens,
|
||||
SUM(cost) as total_cost,
|
||||
AVG(total_tokens) as avg_tokens_per_request
|
||||
SUM(total_cost) as total_cost,
|
||||
AVG(total_tokens) as avg_tokens_per_request,
|
||||
AVG(total_cost) as avg_cost_per_request,
|
||||
AVG(response_time_ms) as avg_response_time_ms
|
||||
')
|
||||
->first();
|
||||
}
|
||||
|
||||
/**
|
||||
* Get provider usage over time
|
||||
*/
|
||||
public function getProviderUsageOverTime(int $days = 30)
|
||||
{
|
||||
return LlmRequest::selectRaw('DATE(created_at) as date, provider, COUNT(*) as count, SUM(total_cost) as cost')
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->where('status', 'success')
|
||||
->groupBy('date', 'provider')
|
||||
->orderBy('date')
|
||||
->get()
|
||||
->groupBy('provider');
|
||||
}
|
||||
|
||||
/**
|
||||
* Get cost trends
|
||||
*/
|
||||
public function getCostTrends(int $days = 30)
|
||||
{
|
||||
$data = LlmRequest::selectRaw('
|
||||
DATE(created_at) as date,
|
||||
SUM(total_cost) as daily_cost,
|
||||
AVG(total_cost) as avg_request_cost,
|
||||
COUNT(*) as request_count
|
||||
')
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->where('status', 'success')
|
||||
->groupBy('date')
|
||||
->orderBy('date')
|
||||
->get();
|
||||
|
||||
return [
|
||||
'daily_data' => $data,
|
||||
'total_cost' => $data->sum('daily_cost'),
|
||||
'avg_daily_cost' => $data->avg('daily_cost'),
|
||||
'total_requests' => $data->sum('request_count'),
|
||||
];
|
||||
}
|
||||
|
||||
/**
|
||||
* Get error statistics
|
||||
*/
|
||||
public function getErrorStats(int $days = 30)
|
||||
{
|
||||
return [
|
||||
'total_errors' => LlmRequest::where('created_at', '>=', now()->subDays($days))
|
||||
->where('status', '!=', 'success')
|
||||
->count(),
|
||||
'errors_by_status' => LlmRequest::selectRaw('status, COUNT(*) as count')
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->where('status', '!=', 'success')
|
||||
->groupBy('status')
|
||||
->get(),
|
||||
'errors_by_provider' => LlmRequest::selectRaw('provider, COUNT(*) as count')
|
||||
->where('created_at', '>=', now()->subDays($days))
|
||||
->where('status', '!=', 'success')
|
||||
->groupBy('provider')
|
||||
->get(),
|
||||
];
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,11 +7,15 @@ use Illuminate\Foundation\Configuration\Middleware;
|
||||
return Application::configure(basePath: dirname(__DIR__))
|
||||
->withRouting(
|
||||
web: __DIR__.'/../routes/web.php',
|
||||
api: __DIR__.'/../routes/api.php',
|
||||
commands: __DIR__.'/../routes/console.php',
|
||||
health: '/up',
|
||||
)
|
||||
->withMiddleware(function (Middleware $middleware): void {
|
||||
//
|
||||
$middleware->alias([
|
||||
'checkbudget' => \App\Http\Middleware\CheckBudget::class,
|
||||
'checkratelimit' => \App\Http\Middleware\CheckRateLimit::class,
|
||||
]);
|
||||
})
|
||||
->withExceptions(function (Exceptions $exceptions): void {
|
||||
//
|
||||
|
||||
@@ -15,7 +15,7 @@ return [
|
||||
|
||||
'defaults' => [
|
||||
'guard' => env('AUTH_GUARD', 'web'),
|
||||
'passwords' => env('AUTH_PASSWORD_BROKER', 'admins'),
|
||||
'passwords' => env('AUTH_PASSWORD_BROKER', 'users'),
|
||||
],
|
||||
|
||||
/*
|
||||
@@ -38,7 +38,7 @@ return [
|
||||
'guards' => [
|
||||
'web' => [
|
||||
'driver' => 'session',
|
||||
'provider' => 'admins',
|
||||
'provider' => 'users',
|
||||
],
|
||||
],
|
||||
|
||||
|
||||
@@ -1,49 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('users', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index();
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('users');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
@@ -1,26 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('admins', function (Blueprint $table) {
|
||||
$table->id();
|
||||
$table->string('name');
|
||||
$table->string('email')->unique();
|
||||
$table->timestamp('email_verified_at')->nullable();
|
||||
$table->string('password');
|
||||
$table->rememberToken();
|
||||
$table->timestamps();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('admins');
|
||||
}
|
||||
};
|
||||
@@ -1,40 +0,0 @@
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
/**
|
||||
* Run the migrations.
|
||||
*/
|
||||
public function up(): void
|
||||
{
|
||||
// Password Reset Tokens für Admins
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
$table->string('email')->primary();
|
||||
$table->string('token');
|
||||
$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
// Sessions Tabelle (für admins)
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
$table->string('id')->primary();
|
||||
$table->foreignId('user_id')->nullable()->index(); // referenziert admins.id
|
||||
$table->string('ip_address', 45)->nullable();
|
||||
$table->text('user_agent')->nullable();
|
||||
$table->longText('payload');
|
||||
$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Reverse the migrations.
|
||||
*/
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
@@ -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.');">
|
||||
|
||||
@@ -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 + '%)';
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -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 -->
|
||||
|
||||
@@ -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"
|
||||
<select name="provider" id="provider" required
|
||||
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
@error('model_key')
|
||||
<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="e.g., 3.00"
|
||||
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
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>
|
||||
<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="e.g., 15.00"
|
||||
class="w-full rounded-md border-gray-300 shadow-sm focus:border-blue-500 focus:ring-blue-500">
|
||||
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
|
||||
|
||||
@@ -2,7 +2,15 @@
|
||||
|
||||
use Illuminate\Foundation\Inspiring;
|
||||
use Illuminate\Support\Facades\Artisan;
|
||||
use Illuminate\Support\Facades\Schedule;
|
||||
use App\Jobs\ResetDailyBudgets;
|
||||
use App\Jobs\ResetMonthlyBudgets;
|
||||
|
||||
Artisan::command('inspire', function () {
|
||||
$this->comment(Inspiring::quote());
|
||||
})->purpose('Display an inspiring quote');
|
||||
|
||||
// Schedule budget reset jobs
|
||||
Schedule::job(new ResetDailyBudgets)->dailyAt('00:00')->name('reset-daily-budgets');
|
||||
Schedule::job(new ResetMonthlyBudgets)->monthlyOn(1, '00:00')->name('reset-monthly-budgets');
|
||||
|
||||
|
||||
@@ -7,12 +7,16 @@ use App\Http\Controllers\ApiKeyController;
|
||||
use App\Http\Controllers\BudgetController;
|
||||
use App\Http\Controllers\UsageLogController;
|
||||
use App\Http\Controllers\ModelPricingController;
|
||||
use App\Http\Controllers\Admin\CredentialController;
|
||||
use App\Http\Controllers\Admin\UserBudgetController;
|
||||
use App\Http\Controllers\Admin\UserManagementController;
|
||||
|
||||
Route::view('/', 'welcome');
|
||||
|
||||
Route::middleware(['auth', 'verified'])->group(function () {
|
||||
// Dashboard
|
||||
Route::get('dashboard', [DashboardController::class, 'index'])->name('dashboard');
|
||||
Route::get('dashboard/realtime-stats', [DashboardController::class, 'realtimeStats'])->name('dashboard.realtime-stats');
|
||||
|
||||
// Gateway Users Management
|
||||
Route::resource('gateway-users', GatewayUserController::class);
|
||||
@@ -41,6 +45,33 @@ Route::middleware(['auth', 'verified'])->group(function () {
|
||||
Route::post('model-pricing-calculate', [ModelPricingController::class, 'calculate'])->name('model-pricing.calculate');
|
||||
Route::get('model-pricing-import', [ModelPricingController::class, 'importForm'])->name('model-pricing.import-form');
|
||||
Route::post('model-pricing-import', [ModelPricingController::class, 'import'])->name('model-pricing.import');
|
||||
Route::get('api/provider-models/{provider}', [ModelPricingController::class, 'getProviderModels'])->name('api.provider-models');
|
||||
|
||||
// Provider Credentials Management (Admin)
|
||||
Route::prefix('admin')->name('admin.')->group(function () {
|
||||
// User Management
|
||||
Route::get('users', [UserManagementController::class, 'index'])
|
||||
->name('users.index');
|
||||
|
||||
// Credentials
|
||||
Route::resource('credentials', CredentialController::class);
|
||||
Route::post('credentials/{credential}/test', [CredentialController::class, 'test'])
|
||||
->name('credentials.test');
|
||||
Route::post('credentials/{credential}/toggle', [CredentialController::class, 'toggle'])
|
||||
->name('credentials.toggle');
|
||||
|
||||
// User Budget & Rate Limit Management
|
||||
Route::get('users/{user}/budget', [UserBudgetController::class, 'show'])
|
||||
->name('users.budget.show');
|
||||
Route::put('users/{user}/budget', [UserBudgetController::class, 'updateBudget'])
|
||||
->name('users.budget.update');
|
||||
Route::put('users/{user}/rate-limit', [UserBudgetController::class, 'updateRateLimit'])
|
||||
->name('users.rate-limit.update');
|
||||
Route::post('users/{user}/rate-limit/reset', [UserBudgetController::class, 'resetRateLimit'])
|
||||
->name('users.rate-limit.reset');
|
||||
Route::post('users/{user}/budget/reset', [UserBudgetController::class, 'resetBudget'])
|
||||
->name('users.budget.reset');
|
||||
});
|
||||
});
|
||||
|
||||
Route::view('profile', 'profile')
|
||||
|
||||
@@ -21,7 +21,7 @@ RUN curl -fsSL https://deb.nodesource.com/setup_20.x | bash - && \
|
||||
RUN apt-get clean && rm -rf /var/lib/apt/lists/*
|
||||
|
||||
# Install PHP extensions
|
||||
RUN docker-php-ext-install pdo pdo_pgsql pgsql mbstring exif pcntl bcmath gd
|
||||
RUN docker-php-ext-install pdo pdo_mysql pdo_pgsql pgsql mysqli mbstring exif pcntl bcmath gd
|
||||
|
||||
# Get latest Composer
|
||||
COPY --from=composer:latest /usr/bin/composer /usr/bin/composer
|
||||
|
||||
190
setup-laravel.sh
190
setup-laravel.sh
@@ -1,190 +0,0 @@
|
||||
#!/bin/bash
|
||||
|
||||
set -e
|
||||
|
||||
echo "🚀 Any-LLM Laravel Setup"
|
||||
echo "========================"
|
||||
echo ""
|
||||
|
||||
# Farben für Output
|
||||
GREEN='\033[0;32m'
|
||||
BLUE='\033[0;34m'
|
||||
RED='\033[0;31m'
|
||||
NC='\033[0m' # No Color
|
||||
|
||||
# Check if Docker is running
|
||||
if ! docker info > /dev/null 2>&1; then
|
||||
echo -e "${RED}❌ Docker ist nicht gestartet!${NC}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
echo -e "${BLUE}📦 Schritt 1: Laravel Projekt erstellen${NC}"
|
||||
if [ ! -d "laravel-app" ]; then
|
||||
echo "Laravel wird in ./laravel-app installiert..."
|
||||
docker run --rm -v $(pwd):/app composer create-project laravel/laravel laravel-app
|
||||
echo -e "${GREEN}✅ Laravel installiert${NC}"
|
||||
else
|
||||
echo -e "${GREEN}✅ Laravel-App existiert bereits${NC}"
|
||||
fi
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}📦 Schritt 2: Laravel Packages installieren${NC}"
|
||||
cd laravel-app
|
||||
|
||||
# Composer packages installieren
|
||||
echo "Installiere Livewire und Breeze..."
|
||||
docker run --rm -v $(pwd):/app composer require livewire/livewire
|
||||
docker run --rm -v $(pwd):/app composer require laravel/breeze --dev
|
||||
|
||||
echo -e "${GREEN}✅ Packages installiert${NC}"
|
||||
|
||||
# Zurück zum Hauptverzeichnis
|
||||
cd ..
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}📦 Schritt 3: Docker Container starten${NC}"
|
||||
docker compose up -d --build
|
||||
|
||||
echo "Warte auf Container..."
|
||||
sleep 10
|
||||
|
||||
echo -e "${GREEN}✅ Container gestartet${NC}"
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}📦 Schritt 4: Laravel konfigurieren${NC}"
|
||||
|
||||
# Breeze installieren
|
||||
echo "Installiere Laravel Breeze..."
|
||||
docker compose exec laravel php artisan breeze:install livewire --dark
|
||||
|
||||
# APP_KEY generieren
|
||||
echo "Generiere APP_KEY..."
|
||||
docker compose exec laravel php artisan key:generate
|
||||
|
||||
# Storage Link erstellen
|
||||
echo "Erstelle Storage Link..."
|
||||
docker compose exec laravel php artisan storage:link
|
||||
|
||||
echo -e "${GREEN}✅ Laravel konfiguriert${NC}"
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}📦 Schritt 5: Admin Migration erstellen${NC}"
|
||||
|
||||
# Admin Migration erstellen
|
||||
docker compose exec laravel php artisan make:migration create_admins_table
|
||||
|
||||
# Migration File Content erstellen
|
||||
MIGRATION_FILE=$(docker compose exec laravel bash -c "ls -t database/migrations/*create_admins_table.php | head -1" | tr -d '\r')
|
||||
|
||||
docker compose exec laravel bash -c "cat > $MIGRATION_FILE << 'EOF'
|
||||
<?php
|
||||
|
||||
use Illuminate\Database\Migrations\Migration;
|
||||
use Illuminate\Database\Schema\Blueprint;
|
||||
use Illuminate\Support\Facades\Schema;
|
||||
|
||||
return new class extends Migration
|
||||
{
|
||||
public function up(): void
|
||||
{
|
||||
Schema::create('admins', function (Blueprint $table) {
|
||||
\$table->id();
|
||||
\$table->string('name');
|
||||
\$table->string('email')->unique();
|
||||
\$table->timestamp('email_verified_at')->nullable();
|
||||
\$table->string('password');
|
||||
\$table->rememberToken();
|
||||
\$table->timestamps();
|
||||
});
|
||||
|
||||
Schema::create('password_reset_tokens', function (Blueprint $table) {
|
||||
\$table->string('email')->primary();
|
||||
\$table->string('token');
|
||||
\$table->timestamp('created_at')->nullable();
|
||||
});
|
||||
|
||||
Schema::create('sessions', function (Blueprint $table) {
|
||||
\$table->string('id')->primary();
|
||||
\$table->foreignId('user_id')->nullable()->index();
|
||||
\$table->string('ip_address', 45)->nullable();
|
||||
\$table->text('user_agent')->nullable();
|
||||
\$table->longText('payload');
|
||||
\$table->integer('last_activity')->index();
|
||||
});
|
||||
}
|
||||
|
||||
public function down(): void
|
||||
{
|
||||
Schema::dropIfExists('admins');
|
||||
Schema::dropIfExists('password_reset_tokens');
|
||||
Schema::dropIfExists('sessions');
|
||||
}
|
||||
};
|
||||
EOF"
|
||||
|
||||
# Migration ausführen
|
||||
echo "Führe Migrationen aus..."
|
||||
docker compose exec laravel php artisan migrate --force
|
||||
|
||||
echo -e "${GREEN}✅ Migrationen abgeschlossen${NC}"
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}📦 Schritt 6: Admin User erstellen${NC}"
|
||||
|
||||
# Admin User erstellen
|
||||
docker compose exec laravel php artisan tinker --execute="
|
||||
\App\Models\User::create([
|
||||
'name' => 'Admin',
|
||||
'email' => 'admin@example.com',
|
||||
'password' => bcrypt('password123')
|
||||
]);
|
||||
echo 'Admin User erstellt: admin@example.com / password123';
|
||||
"
|
||||
|
||||
echo -e "${GREEN}✅ Admin User erstellt${NC}"
|
||||
|
||||
echo ""
|
||||
echo -e "${BLUE}📦 Schritt 7: Assets kompilieren${NC}"
|
||||
|
||||
# NPM installieren und Assets kompilieren
|
||||
echo "Installiere NPM Packages..."
|
||||
docker compose exec laravel npm install
|
||||
|
||||
echo "Kompiliere Assets..."
|
||||
docker compose exec laravel npm run build
|
||||
|
||||
echo -e "${GREEN}✅ Assets kompiliert${NC}"
|
||||
|
||||
echo ""
|
||||
echo "=========================================="
|
||||
echo -e "${GREEN}✅ Setup abgeschlossen!${NC}"
|
||||
echo "=========================================="
|
||||
echo ""
|
||||
echo "📍 URLs:"
|
||||
echo " - Laravel Admin: http://localhost:80"
|
||||
echo " - Gateway API: http://localhost:8000"
|
||||
echo " - Gateway Tester: http://localhost:8080"
|
||||
echo " - Adminer (DB): http://localhost:8081"
|
||||
echo ""
|
||||
echo "🔐 Admin Login:"
|
||||
echo " Email: admin@example.com"
|
||||
echo " Password: password123"
|
||||
echo ""
|
||||
echo "🗄️ Adminer Login:"
|
||||
echo " System: PostgreSQL"
|
||||
echo " Server: postgres"
|
||||
echo " Username: gateway"
|
||||
echo " Password: gateway"
|
||||
echo " Database: gateway"
|
||||
echo ""
|
||||
echo "📝 Nächste Schritte:"
|
||||
echo " 1. Öffne http://localhost:80 im Browser"
|
||||
echo " 2. Melde dich mit admin@example.com / password123 an"
|
||||
echo " 3. Beginne mit der Entwicklung!"
|
||||
echo ""
|
||||
echo "🛑 Container stoppen:"
|
||||
echo " docker compose down"
|
||||
echo ""
|
||||
echo "🔄 Container neu starten:"
|
||||
echo " docker compose up -d"
|
||||
echo ""
|
||||
@@ -1,34 +0,0 @@
|
||||
server {
|
||||
listen 80;
|
||||
server_name localhost;
|
||||
|
||||
root /usr/share/nginx/html;
|
||||
index index.html;
|
||||
|
||||
location / {
|
||||
try_files $uri $uri/ =404;
|
||||
add_header Cache-Control "no-cache, no-store, must-revalidate";
|
||||
}
|
||||
|
||||
# Proxy für Gateway-Anfragen (um CORS zu vermeiden)
|
||||
location /api/ {
|
||||
proxy_pass http://gateway:8000/;
|
||||
proxy_http_version 1.1;
|
||||
proxy_set_header Upgrade $http_upgrade;
|
||||
proxy_set_header Connection 'upgrade';
|
||||
proxy_set_header Host $host;
|
||||
proxy_cache_bypass $http_upgrade;
|
||||
proxy_set_header X-Real-IP $remote_addr;
|
||||
proxy_set_header X-Forwarded-For $proxy_add_x_forwarded_for;
|
||||
proxy_set_header X-Forwarded-Proto $scheme;
|
||||
|
||||
# CORS headers
|
||||
add_header Access-Control-Allow-Origin * always;
|
||||
add_header Access-Control-Allow-Methods 'GET, POST, PUT, DELETE, OPTIONS' always;
|
||||
add_header Access-Control-Allow-Headers 'Content-Type, Authorization, X-Master-Key' always;
|
||||
|
||||
if ($request_method = 'OPTIONS') {
|
||||
return 204;
|
||||
}
|
||||
}
|
||||
}
|
||||
428
web/index.html
428
web/index.html
@@ -1,428 +0,0 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="de">
|
||||
<head>
|
||||
<meta charset="UTF-8">
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
||||
<title>Any-LLM Gateway Tester</title>
|
||||
<style>
|
||||
* {
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, sans-serif;
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
min-height: 100vh;
|
||||
padding: 20px;
|
||||
}
|
||||
|
||||
.container {
|
||||
max-width: 1200px;
|
||||
margin: 0 auto;
|
||||
}
|
||||
|
||||
h1 {
|
||||
color: white;
|
||||
text-align: center;
|
||||
margin-bottom: 30px;
|
||||
font-size: 2.5em;
|
||||
text-shadow: 2px 2px 4px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.card {
|
||||
background: white;
|
||||
border-radius: 12px;
|
||||
padding: 25px;
|
||||
margin-bottom: 20px;
|
||||
box-shadow: 0 10px 30px rgba(0,0,0,0.2);
|
||||
}
|
||||
|
||||
.form-group {
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
label {
|
||||
display: block;
|
||||
margin-bottom: 8px;
|
||||
font-weight: 600;
|
||||
color: #333;
|
||||
}
|
||||
|
||||
input, select, textarea {
|
||||
width: 100%;
|
||||
padding: 12px;
|
||||
border: 2px solid #e0e0e0;
|
||||
border-radius: 8px;
|
||||
font-size: 14px;
|
||||
transition: border-color 0.3s;
|
||||
}
|
||||
|
||||
input:focus, select:focus, textarea:focus {
|
||||
outline: none;
|
||||
border-color: #667eea;
|
||||
}
|
||||
|
||||
textarea {
|
||||
min-height: 100px;
|
||||
resize: vertical;
|
||||
font-family: inherit;
|
||||
}
|
||||
|
||||
button {
|
||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
||||
color: white;
|
||||
padding: 12px 30px;
|
||||
border: none;
|
||||
border-radius: 8px;
|
||||
font-size: 16px;
|
||||
font-weight: 600;
|
||||
cursor: pointer;
|
||||
transition: transform 0.2s, box-shadow 0.2s;
|
||||
margin-right: 10px;
|
||||
}
|
||||
|
||||
button:hover {
|
||||
transform: translateY(-2px);
|
||||
box-shadow: 0 5px 15px rgba(102, 126, 234, 0.4);
|
||||
}
|
||||
|
||||
button:active {
|
||||
transform: translateY(0);
|
||||
}
|
||||
|
||||
button:disabled {
|
||||
opacity: 0.6;
|
||||
cursor: not-allowed;
|
||||
transform: none;
|
||||
}
|
||||
|
||||
.response-box {
|
||||
background: #f8f9fa;
|
||||
border-left: 4px solid #667eea;
|
||||
padding: 20px;
|
||||
border-radius: 8px;
|
||||
margin-top: 20px;
|
||||
white-space: pre-wrap;
|
||||
word-wrap: break-word;
|
||||
max-height: 400px;
|
||||
overflow-y: auto;
|
||||
}
|
||||
|
||||
.error {
|
||||
background: #fee;
|
||||
border-left-color: #dc3545;
|
||||
color: #721c24;
|
||||
}
|
||||
|
||||
.success {
|
||||
background: #d4edda;
|
||||
border-left-color: #28a745;
|
||||
color: #155724;
|
||||
}
|
||||
|
||||
.loading {
|
||||
text-align: center;
|
||||
padding: 20px;
|
||||
color: #667eea;
|
||||
}
|
||||
|
||||
.info {
|
||||
background: #e7f3ff;
|
||||
border-left: 4px solid #2196F3;
|
||||
padding: 15px;
|
||||
border-radius: 8px;
|
||||
margin-bottom: 20px;
|
||||
}
|
||||
|
||||
.info h3 {
|
||||
margin-bottom: 10px;
|
||||
color: #1976D2;
|
||||
}
|
||||
|
||||
.grid {
|
||||
display: grid;
|
||||
grid-template-columns: repeat(auto-fit, minmax(200px, 1fr));
|
||||
gap: 10px;
|
||||
}
|
||||
|
||||
code {
|
||||
background: #f4f4f4;
|
||||
padding: 2px 6px;
|
||||
border-radius: 4px;
|
||||
font-family: 'Courier New', monospace;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<div class="container">
|
||||
<h1>🚀 Any-LLM Gateway Tester</h1>
|
||||
|
||||
<div class="info">
|
||||
<h3>ℹ️ Gateway Info</h3>
|
||||
<p><strong>Gateway URL:</strong> /api</p>
|
||||
<p><strong>Master Key:</strong> <code>bdab4b...bcd</code> (aus config.yml)</p>
|
||||
<p><strong>Virtual Key für test-user-1:</strong> <code>gw-H9xo...ziAQ</code></p>
|
||||
<p><strong>Authentifizierung:</strong> X-AnyLLM-Key: Bearer KEY</p>
|
||||
<p style="margin-top: 10px; font-size: 14px;"><strong>Hinweis:</strong> Anthropic erfordert Virtual Keys! Master Key funktioniert nur mit OpenAI.</p>
|
||||
</div>
|
||||
|
||||
|
||||
<!-- User Management -->
|
||||
<div class="card">
|
||||
<h2>👤 User Management</h2>
|
||||
<p style="margin-bottom: 15px;">Erstelle zuerst einen User, um Requests zu tracken.</p>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="userId">User ID:</label>
|
||||
<input type="text" id="userId" value="test-user-1" placeholder="z.B. user-123">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="userAlias">Alias (optional):</label>
|
||||
<input type="text" id="userAlias" value="Test User" placeholder="z.B. Bob">
|
||||
</div>
|
||||
|
||||
<button onclick="createUser()" id="createUserBtn">User erstellen</button>
|
||||
<button onclick="getUser()" id="getUserBtn">User abrufen</button>
|
||||
|
||||
<div id="userResponse" style="display: none;"></div>
|
||||
</div>
|
||||
|
||||
<!-- Chat Completion Test -->
|
||||
<div class="card">
|
||||
<h2>💬 Chat Completion Test</h2>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="provider">Provider:</label>
|
||||
<select id="provider">
|
||||
<option value="openai">OpenAI</option>
|
||||
<option value="anthropic">Anthropic</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="model">Modell:</label>
|
||||
<select id="model">
|
||||
<option value="gpt-4o-mini">gpt-4o-mini (OpenAI)</option>
|
||||
<option value="gpt-4o">gpt-4o (OpenAI)</option>
|
||||
<option value="claude-3-5-sonnet-20241022">claude-3-5-sonnet (Anthropic)</option>
|
||||
<option value="claude-3-5-haiku-20241022">claude-3-5-haiku (Anthropic)</option>
|
||||
</select>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="chatUserId">User ID für Request:</label>
|
||||
<input type="text" id="chatUserId" value="test-user-1" placeholder="User ID aus User Management">
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label for="message">Deine Nachricht:</label>
|
||||
<textarea id="message" placeholder="Schreibe eine Nachricht...">Hallo! Kannst du mir in einem Satz sagen, wer du bist?</textarea>
|
||||
</div>
|
||||
|
||||
<div class="form-group">
|
||||
<label>
|
||||
<input type="checkbox" id="streaming"> Streaming aktivieren
|
||||
</label>
|
||||
</div>
|
||||
|
||||
<button onclick="sendMessage()" id="sendBtn">Nachricht senden</button>
|
||||
|
||||
<div id="response" style="display: none;"></div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<script>
|
||||
const GATEWAY_URL = '/api';
|
||||
const MASTER_KEY = 'bdab4b5261d6e6ed7173c999ababd7c66066d76d3a06c8506a880ecdcfb41bcd';
|
||||
const VIRTUAL_KEY = 'gw-H9xoOa9YIPAU50DaRzIz9-aXW1QtnZvZg3m48hLn1F66-QvI_qjMZh12f0fWziAQ'; // Virtual Key für test-user-1
|
||||
|
||||
// User Management Functions
|
||||
async function createUser() {
|
||||
const userId = document.getElementById('userId').value;
|
||||
const userAlias = document.getElementById('userAlias').value;
|
||||
const responseDiv = document.getElementById('userResponse');
|
||||
const createBtn = document.getElementById('createUserBtn');
|
||||
|
||||
if (!userId) {
|
||||
alert('Bitte User ID eingeben!');
|
||||
return;
|
||||
}
|
||||
|
||||
responseDiv.style.display = 'block';
|
||||
responseDiv.className = 'response-box loading';
|
||||
responseDiv.textContent = 'Erstelle User...';
|
||||
createBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${GATEWAY_URL}/v1/users`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-AnyLLM-Key': `Bearer ${MASTER_KEY}`
|
||||
},
|
||||
body: JSON.stringify({
|
||||
user_id: userId,
|
||||
alias: userAlias || undefined
|
||||
})
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
|
||||
const data = await response.json();
|
||||
responseDiv.className = 'response-box success';
|
||||
responseDiv.textContent = `✅ User erstellt!\n\nUser ID: ${data.user_id}\nAlias: ${data.alias || '-'}\nErstellt: ${new Date(data.created_at).toLocaleString('de-DE')}`;
|
||||
} catch (error) {
|
||||
responseDiv.className = 'response-box error';
|
||||
responseDiv.textContent = `Fehler: ${error.message}`;
|
||||
} finally {
|
||||
createBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
async function getUser() {
|
||||
const userId = document.getElementById('userId').value;
|
||||
const responseDiv = document.getElementById('userResponse');
|
||||
const getBtn = document.getElementById('getUserBtn');
|
||||
|
||||
if (!userId) {
|
||||
alert('Bitte User ID eingeben!');
|
||||
return;
|
||||
}
|
||||
|
||||
responseDiv.style.display = 'block';
|
||||
responseDiv.className = 'response-box loading';
|
||||
responseDiv.textContent = 'Lade User-Daten...';
|
||||
getBtn.disabled = true;
|
||||
|
||||
try {
|
||||
const response = await fetch(`${GATEWAY_URL}/v1/users/${userId}`, {
|
||||
method: 'GET',
|
||||
headers: {
|
||||
'X-AnyLLM-Key': `Bearer ${MASTER_KEY}`
|
||||
}
|
||||
});
|
||||
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
const data = await response.json();
|
||||
responseDiv.className = 'response-box success';
|
||||
responseDiv.textContent = `📊 User Info:\n\nUser ID: ${data.user_id}\nAlias: ${data.alias || '-'}\nSpend: $${data.spend}\nBudget: ${data.budget_id || 'Keine Budget-Limits'}\nBlocked: ${data.blocked ? '❌ Ja' : '✅ Nein'}`;
|
||||
} catch (error) {
|
||||
responseDiv.className = 'response-box error';
|
||||
responseDiv.textContent = `Fehler: ${error.message}`;
|
||||
} finally {
|
||||
getBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
|
||||
// Chat Completion Function
|
||||
async function sendMessage() {
|
||||
const provider = document.getElementById('provider').value;
|
||||
const model = document.getElementById('model').value;
|
||||
const message = document.getElementById('message').value;
|
||||
const chatUserId = document.getElementById('chatUserId').value;
|
||||
const streaming = document.getElementById('streaming').checked;
|
||||
const responseDiv = document.getElementById('response');
|
||||
const sendBtn = document.getElementById('sendBtn');
|
||||
|
||||
if (!chatUserId) {
|
||||
alert('Bitte User ID eingeben!');
|
||||
return;
|
||||
}
|
||||
|
||||
|
||||
responseDiv.style.display = 'block';
|
||||
responseDiv.className = 'response-box loading';
|
||||
responseDiv.textContent = 'Sende Anfrage...';
|
||||
sendBtn.disabled = true;
|
||||
|
||||
try {
|
||||
// Wähle den richtigen API Key basierend auf dem Provider
|
||||
// Anthropic erfordert Virtual Keys, da der 'user' Parameter nicht unterstützt wird
|
||||
const apiKey = provider === 'anthropic' ? VIRTUAL_KEY : MASTER_KEY;
|
||||
|
||||
// Anthropic unterstützt den 'user' Parameter nicht
|
||||
const requestBody = {
|
||||
model: `${provider}:${model}`,
|
||||
messages: [
|
||||
{ role: 'user', content: message }
|
||||
],
|
||||
stream: streaming
|
||||
};
|
||||
|
||||
// Nur für OpenAI den user Parameter hinzufügen
|
||||
if (provider === 'openai') {
|
||||
requestBody.user = chatUserId;
|
||||
}
|
||||
|
||||
const response = await fetch(`${GATEWAY_URL}/v1/chat/completions`, {
|
||||
method: 'POST',
|
||||
headers: {
|
||||
'Content-Type': 'application/json',
|
||||
'X-AnyLLM-Key': `Bearer ${apiKey}`
|
||||
},
|
||||
body: JSON.stringify(requestBody)
|
||||
});
|
||||
|
||||
if (!response.ok) {
|
||||
const errorText = await response.text();
|
||||
throw new Error(`HTTP ${response.status}: ${errorText}`);
|
||||
}
|
||||
|
||||
if (streaming) {
|
||||
responseDiv.textContent = '';
|
||||
responseDiv.className = 'response-box';
|
||||
|
||||
const reader = response.body.getReader();
|
||||
const decoder = new TextDecoder();
|
||||
|
||||
while (true) {
|
||||
const { done, value } = await reader.read();
|
||||
if (done) break;
|
||||
|
||||
const chunk = decoder.decode(value);
|
||||
const lines = chunk.split('\n').filter(line => line.trim() !== '');
|
||||
|
||||
for (const line of lines) {
|
||||
if (line.startsWith('data: ')) {
|
||||
const data = line.slice(6);
|
||||
if (data === '[DONE]') continue;
|
||||
|
||||
try {
|
||||
const json = JSON.parse(data);
|
||||
const content = json.choices[0]?.delta?.content || '';
|
||||
responseDiv.textContent += content;
|
||||
} catch (e) {
|
||||
console.error('Parse error:', e);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
responseDiv.className = 'response-box success';
|
||||
} else {
|
||||
const data = await response.json();
|
||||
responseDiv.className = 'response-box success';
|
||||
responseDiv.textContent = data.choices[0].message.content;
|
||||
}
|
||||
} catch (error) {
|
||||
responseDiv.className = 'response-box error';
|
||||
responseDiv.textContent = `Fehler: ${error.message}`;
|
||||
} finally {
|
||||
sendBtn.disabled = false;
|
||||
}
|
||||
}
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
Reference in New Issue
Block a user