Files
laravel-llm-gateway/web/index.html
wtrinkl b1363aeab9 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
2025-11-16 12:38:05 +01:00

429 lines
15 KiB
HTML
Raw Blame History

This file contains invisible Unicode characters
This file contains invisible Unicode characters that are indistinguishable to humans but may be processed differently by a computer. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
<!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>