Initial commit: Any-LLM Gateway with Laravel Admin Interface

- Any-LLM Gateway setup with Docker Compose
- Laravel 11 admin interface with Livewire
- Dashboard with usage statistics and charts
- Gateway Users management with budget tracking
- API Keys management with revocation
- Budget templates with assignment
- Usage Logs with filtering and CSV export
- Model Pricing management with calculator
- PostgreSQL database integration
- Complete authentication system for admins
This commit is contained in:
wtrinkl
2025-11-16 12:38:05 +01:00
commit b1363aeab9
148 changed files with 23995 additions and 0 deletions

428
web/index.html Normal file
View File

@@ -0,0 +1,428 @@
<!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>