bifroest/app/templates/admin_models.html
2026-10-27 08:34:17 +01:00

147 lines
7.7 KiB
HTML
Raw Blame History

This file contains ambiguous Unicode characters

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.

{% extends "base.html" %}
{% block title %}Modelle und Zugänge{% endblock %}
{% macro csrf() %}<input type="hidden" name="_csrf" value="{{ p.csrf }}">{% endmacro %}
{% block content %}
<div class="stack stack-sm">
<h1>Modelle und Zugänge</h1>
<p class="text-muted">Gilt für die ganze Plattform. Workspaces nutzen das Standardmodell oder wählen ein anderes aktives Modell; der Key kommt vom Team des Workspace.</p>
</div>
{% if error %}<div class="alert alert-danger" role="alert"><div class="alert-content"><p>{{ error }}</p></div></div>{% endif %}
{% if notice %}<div class="alert alert-success" role="status"><div class="alert-content"><p>{{ notice }}</p></div></div>{% endif %}
{% if not vault_ready %}
<div class="alert alert-warning" role="alert"><div class="alert-content">
<p>Keys können nicht gespeichert werden: <code>AIW_SECRET_KEY</code> ist nicht gesetzt. Einen Key erzeugen mit <code>uv run aiw gen-key</code> und in die <code>.env</code> eintragen.</p>
</div></div>
{% endif %}
<section class="card">
<div class="card-header"><h2 class="text-lg">1. Endpunkte</h2></div>
<div class="card-body stack">
<p class="text-sm text-muted">OpenAI-kompatibel mit Responses API: in der Entwicklung <code>https://api.openai.com/v1</code>, später das AI Gateway.</p>
{% if providers %}
<div class="table-wrap"><table>
<thead><tr><th>Name</th><th>Basis-URL</th><th>Status</th><th></th></tr></thead>
<tbody>
{% for pr in providers %}
<tr>
<td>{{ pr.name }}</td>
<td class="text-sm aiw-break">{{ pr.base_url }}</td>
<td><span class="badge {{ 'badge-success' if pr.active else 'badge-default' }}">{{ 'aktiv' if pr.active else 'inaktiv' }}</span></td>
<td class="stack stack-sm">
<button class="btn btn-ghost btn-sm" hx-post="/admin/providers/{{ pr.id }}/test" hx-target="#test-{{ pr.id }}">Verbindung testen</button>
<details>
<summary class="text-sm">Bearbeiten</summary>
<form method="post" action="/admin/providers" class="stack stack-sm">
{{ csrf() }}<input type="hidden" name="provider_id" value="{{ pr.id }}">
<input name="name" value="{{ pr.name }}" required aria-label="Name">
<input name="base_url" value="{{ pr.base_url }}" required aria-label="Basis-URL">
<label class="cluster"><input type="checkbox" name="active" value="1" {% if pr.active %}checked{% endif %}> aktiv</label>
<button class="btn btn-secondary btn-sm" type="submit">Speichern</button>
</form>
<form method="post" action="/admin/providers/{{ pr.id }}/delete" data-confirm="Endpunkt {{ pr.name }} mit allen API-Keys löschen?">
{{ csrf() }}<button class="btn btn-ghost btn-sm" type="submit">Endpunkt löschen</button>
</form>
</details>
</td>
</tr>
<tr><td colspan="4" id="test-{{ pr.id }}"></td></tr>
{% endfor %}
</tbody>
</table></div>
{% endif %}
<form method="post" action="/admin/providers" class="cluster">
{{ csrf() }}<input type="hidden" name="active" value="1">
<input name="name" placeholder="Name, z. B. OpenAI" required aria-label="Name">
<input name="base_url" placeholder="https://api.openai.com/v1" required aria-label="Basis-URL">
<button class="btn btn-primary btn-sm" type="submit">Endpunkt anlegen</button>
</form>
</div>
</section>
<section class="card">
<div class="card-header"><h2 class="text-lg">2. API-Keys</h2></div>
<div class="card-body stack">
<p class="text-sm text-muted">Ein Key für <em>alle Teams</em> (z. B. OpenAI in der Entwicklung) oder je Team (Gateway). Der Team-Key hat Vorrang. Keys werden verschlüsselt gespeichert und nie wieder angezeigt, nur ersetzt.</p>
{% if keys %}
<div class="table-wrap"><table>
<thead><tr><th>Endpunkt</th><th>Gilt für</th><th>Key</th><th>Geändert</th><th></th></tr></thead>
<tbody>
{% for row in keys %}
<tr>
<td>{{ row.provider.name }}</td>
<td>{{ row.team.name if row.team else 'alle Teams' }}</td>
<td><code>…{{ row.key.hint }}</code></td>
<td class="text-sm text-muted">{{ row.key.updated.strftime('%d.%m.%Y %H:%M') }}</td>
<td>
<form method="post" action="/admin/keys/{{ row.key.id }}/delete" data-confirm="Key wirklich löschen? Betroffene Teams können dann nicht mehr chatten.">
{{ csrf() }}<button class="btn btn-ghost btn-sm" type="submit">Löschen</button>
</form>
</td>
</tr>
{% endfor %}
</tbody>
</table></div>
{% endif %}
{% if providers %}
<form method="post" action="/admin/keys" class="cluster">
{{ csrf() }}
<select name="provider_id" aria-label="Endpunkt">{% for pr in providers %}<option value="{{ pr.id }}">{{ pr.name }}</option>{% endfor %}</select>
<select name="team_id" aria-label="Team">
<option value="">alle Teams</option>
{% for t in teams %}<option value="{{ t.id }}">{{ t.name }}</option>{% endfor %}
</select>
<input name="value" type="password" autocomplete="off" placeholder="API-Key" required aria-label="API-Key" {% if not vault_ready %}disabled{% endif %}>
<button class="btn btn-primary btn-sm" type="submit" {% if not vault_ready %}disabled{% endif %}>Key speichern</button>
</form>
{% else %}<p class="text-muted">Erst einen Endpunkt anlegen.</p>{% endif %}
</div>
</section>
<section class="card">
<div class="card-header"><h2 class="text-lg">3. Modelle</h2></div>
<div class="card-body stack">
{% if models %}
<div class="table-wrap"><table>
<thead><tr><th>Anzeige</th><th>Modell</th><th>Endpunkt</th><th>Klasse</th><th>Kontext</th><th>Preis je 1 Mio.</th><th>Status</th><th></th></tr></thead>
<tbody>
{% for m in models %}
<tr>
<td>{{ m.label }}</td>
<td><code>{{ m.name }}</code></td>
<td>{{ provider_names[m.provider_id] }}</td>
<td>{{ m.model_class }}</td>
<td class="text-sm">{{ '{:,}'.format(m.context_window).replace(',', '.') }}</td>
<td class="text-sm">{% if m.input_price is not none %}{{ '{:f}'.format(m.input_price.normalize()).replace('.', ',') }} / {{ '{:f}'.format(m.output_price.normalize()).replace('.', ',') }} {{ currency }}{% else %}<span class="text-muted">–</span>{% endif %}</td>
<td>
{% if m.is_default %}<span class="badge badge-primary">Standard</span>{% endif %}
<span class="badge {{ 'badge-success' if m.active else 'badge-default' }}">{{ 'aktiv' if m.active else 'inaktiv' }}</span>
</td>
<td class="stack stack-sm">
{% if m.active and not m.is_default %}
<form method="post" action="/admin/models/{{ m.id }}/default">{{ csrf() }}<button class="btn btn-ghost btn-sm" type="submit">Als Standard</button></form>
{% endif %}
<details>
<summary class="text-sm">Bearbeiten</summary>
{% with model = m %}{% include "_model_form.html" %}{% endwith %}
{% if not m.is_default %}
<form method="post" action="/admin/models/{{ m.id }}/delete" data-confirm="Modell {{ m.label }} löschen? Workspaces, die es gewählt haben, nutzen danach das Standardmodell.">
{{ csrf() }}<button class="btn btn-ghost btn-sm" type="submit">Modell löschen</button>
</form>
{% endif %}
</details>
</td>
</tr>
{% endfor %}
</tbody>
</table></div>
{% endif %}
{% if providers %}
<details {% if not models %}open{% endif %}>
<summary>Modell hinzufügen</summary>
{% with model = None %}{% include "_model_form.html" %}{% endwith %}
</details>
{% endif %}
</div>
</section>
{% endblock %}