147 lines
7.7 KiB
HTML
147 lines
7.7 KiB
HTML
{% 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 %}
|