# KI-Schlüssel (BYOK)

Lalabase folgt dem **Bring-your-own-Key**-Prinzip: Die KI-Features laufen über deinen
eigenen Provider-Schlüssel. So fließen keine Daten über fremde Sammelkonten, und du behältst
die volle Kontrolle und Abrechnung.

## Schlüssel hinterlegen

Trage den Schlüssel deines Providers als Umgebungsvariable ein:

```bash
OPENAI_API_KEY=sk-...
```

Ohne hinterlegten Schlüssel bleiben die KI-Features deaktiviert — der Rest von Lalabase
funktioniert uneingeschränkt weiter.

## Was an den Provider geht

KI-Funktionen senden nur den jeweils nötigen Kontext an den Provider — etwa den Text eines
Tickets, das zusammengefasst werden soll. Es werden keine vollständigen Datenbank-Inhalte
übertragen.

<div class="docs-callout docs-callout--tip">
  <div class="docs-callout__title">Datenhoheit</div>
  <p>Mit einem eigenen Schlüssel bleibt die Vertragsbeziehung zwischen dir und deinem KI-Provider. Lalabase ist nur das Werkzeug dazwischen.</p>
</div>

## EU-Anbieter (DSGVO)

Neben OpenAI und Anthropic lassen sich EU-Anbieter mit Datenverarbeitung in der EU
anbinden — über den Provider **OpenAI-kompatibel** im Admin-Backend. Beim Anlegen eines
neuen Credentials füllt ein **EU-Preset** Endpunkt-URL und Modell direkt vor:

- **Mistral** (Frankreich)
- **IONOS AI Model Hub** (Deutschland)
- **OVHcloud AI Endpoints** (EU)

Die Presets füllen bewusst nur das **Chat**-Modell vor. Für Embeddings sind
768- und 1024-dimensionale EU-Modelle (z. B. `mistral-embed`, `nomic`, `bge-m3`)
inzwischen konfigurierbar — das geschieht aber pro Organisation über die Konsole und
nicht über ein Preset, weil ein Wechsel auf bestehendem Bestand einen vollständigen
Neu-Index auslöst. Details im Abschnitt „Embedding-Dimension wählen" weiter unten.

<div class="docs-callout docs-callout--tip">
  <div class="docs-callout__title">Datensouveränität</div>
  <p>Mit einem EU-Anbieter und eigenem Schlüssel bleiben Vertragsbeziehung und Datenverarbeitung vollständig in der EU.</p>
</div>

## Google Gemini über Vertex AI (EU)

Google Gemini lässt sich **DSGVO-konform ausschließlich über Vertex AI** mit einer
EU-Region anbinden — nicht über Google AI Studio (der einfache API-Key-Weg landet auf
einem globalen Endpunkt ohne garantierte EU-Datenresidenz). Deshalb ist dieser Weg
bewusst als **Betreiber-Einrichtung** gebaut: Du hinterlegst ihn einmal systemweit im
Admin-Backend, danach steht „Google (Gemini)" allen Organisationen zur Verfügung —
niemand muss selbst ein Google-Cloud-Konto anfassen.

Wichtig vorab: Vertex nutzt **keinen API-Key**, sondern einen **Service-Account** mit
JSON-Schlüssel. Nur so lässt sich der regionale EU-Endpunkt erzwingen. Ersetze in den
folgenden Befehlen `DEIN_PROJEKT` durch deine GCP-Projekt-ID.

### Schritt 1 — Vertex AI API aktivieren

Lege ein **GCP-Projekt** an und aktiviere die **Vertex AI API**. In der Cloud Console
heißt dieser Eintrag inzwischen teils **„Agent Platform API"** — der Dienstname darunter
ist unverändert `aiplatform.googleapis.com`, und genau der ist gemeint. Am schnellsten
per Cloud Shell:

```bash
gcloud services enable aiplatform.googleapis.com --project=DEIN_PROJEKT
```

Ein Rechnungskonto muss mit dem Projekt verknüpft sein.

### Schritt 2 — Service-Account mit Rolle anlegen

Erstelle ein **Dienstkonto** und weise ihm die Rolle **Vertex AI User**
(`roles/aiplatform.user`) zu — so eng wie möglich, kein „Owner":

```bash
gcloud iam service-accounts create lalabase-vertex --project=DEIN_PROJEKT
gcloud projects add-iam-policy-binding DEIN_PROJEKT \
  --member="serviceAccount:lalabase-vertex@DEIN_PROJEKT.iam.gserviceaccount.com" \
  --role="roles/aiplatform.user"
```

### Schritt 3 — JSON-Schlüssel erzeugen

Dienstkonto → **Schlüssel → Schlüssel hinzufügen → JSON**. Die Datei wird einmalig
heruntergeladen (nicht erneut abrufbar) — wie ein Passwort behandeln.

<div class="docs-callout docs-callout--warning">
  <div class="docs-callout__title">Falle: „Erstellen von Dienstkontoschlüsseln ist deaktiviert"</div>
  <p>Neue Google-Cloud-Organisationen erzwingen per Voreinstellung die Richtlinie
  <code>iam.disableServiceAccountKeyCreation</code> und blockieren den Schlüssel-Download.
  Zum Aufheben braucht es die Rolle <strong>Organisationsrichtlinienadministrator</strong>
  (<code>roles/orgpolicy.policyAdmin</code>) — „Owner" und „Organization Administrator"
  reichen dafür <em>nicht</em>. Nach dem Zuweisen die Richtlinie fürs Projekt zurücksetzen:</p>
</div>

```bash
gcloud org-policies reset iam.disableServiceAccountKeyCreation --project=DEIN_PROJEKT
```

### Schritt 4 — Region und Modell wählen

Die Modell-Verfügbarkeit **unterscheidet sich je EU-Region und ändert sich laufend** —
prüfe sie, bevor du live gehst. Enumeriere die Modelle in deiner Region direkt
(Statuscode `200` = verfügbar, `404` = nicht):

```bash
TOKEN=$(gcloud auth print-access-token); PROJECT=DEIN_PROJEKT; LOC=europe-west3
for MODEL in gemini-2.5-flash gemini-2.5-pro gemini-embedding-001; do
  CODE=$(curl -s -o /dev/null -w "%{http_code}" -X POST \
    -H "Authorization: Bearer $TOKEN" -H "Content-Type: application/json" \
    "https://${LOC}-aiplatform.googleapis.com/v1/projects/${PROJECT}/locations/${LOC}/publishers/google/models/${MODEL}:generateContent" \
    -d '{"contents":[{"role":"user","parts":[{"text":"ping"}]}]}')
  echo "${MODEL} -> ${CODE}"
done
```

Bewährte Kombination (Stand 2026-07): **`europe-west3` (Frankfurt) + `gemini-2.5-flash`**
für den Chat. Ältere Modelle wie `gemini-2.0-flash` sind zurückgezogen; die neuesten
Generationen (3.x) liefert Google in der EU aktuell nur über den Multi-Region-Endpunkt,
den Lalabase aus Residenz-Gründen bewusst nicht anspricht.

### Schritt 5 — Im Admin-Backend hinterlegen

Neues AI-Credential, Provider **„Google (Gemini)"**:

- **Service-Account-JSON**: den kompletten Schlüssel einfügen (wird verschlüsselt gespeichert; kein API-Key).
- **GCP-Projekt-ID**: die ID deines Projekts.
- **Vertex-AI-Region**: die geprüfte EU-Region, z. B. `europe-west3`. `global` wird abgelehnt, weil es die EU-Datenresidenz aushebeln würde.
- **Modell**: das in Schritt 4 als verfügbar bestätigte Modell, z. B. `gemini-2.5-flash`.

Anschließend die Credential **als Standard setzen** (Stern-Button — sie bedient dann alle Organisationen, die den Provider ohne eigene Schlüssel nutzen) und die Organisation auf den Provider „Google" umstellen.

### Schritt 6 — Prüfen

Sende eine Chat-Nachricht. Schlägt sie fehl, nennt das Server-Log die Ursache
(`grep "ChatResponseJob: provider" log/production.log`):

| Log-Zeile | Ursache | Lösung |
|---|---|---|
| `provider unauthorized: … HTTP 403` | Rolle fehlt am Dienstkonto | `roles/aiplatform.user` zuweisen (Schritt 2) |
| `provider unauthorized: google token mint failed` | Service-Account-JSON beschädigt | JSON neu einfügen (`private_key` mit intakten Zeilenumbrüchen) |
| `provider error: … HTTP 404` | Modell in der Region nicht verfügbar | Region/Modell nach Schritt 4 korrigieren |

Welche Modelle in welcher EU-Region tatsächlich erreichbar sind, zeigt die Karte
**„Modell-Verfügbarkeit (Vertex AI)"** auf der Credential-Seite im Admin-Backend: Ein
Klick prüft kostenlos per `countTokens` alle Kombinationen aus Region und Modell — die
Verfügbarkeit unterscheidet sich je EU-Region und ändert sich laufend.

Gemini ist auch für **Embeddings** ein EU-tauglicher Weg: Sein Embedding-Modell liefert
per Kürzung (Matryoshka) sauber 1536 Dimensionen. Seit der Multidim-Unterstützung ist es
nicht mehr der einzige — 768- und 1024-dimensionale EU-Modelle anderer Anbieter lassen
sich ebenfalls konfigurieren (siehe unten). Ein Wechsel des Embedding-Raums auf einem
bestehenden Bestand erfordert in jedem Fall einen vollständigen Neu-Index der
Organisation.

<div class="docs-callout docs-callout--tip">
  <div class="docs-callout__title">Betreiber-Einrichtung, kein Endnutzer-Schritt</div>
  <p>Der Vertex-Zugang wird einmal vom Betreiber der Instanz hinterlegt. Endnutzer wählen ihn nicht selbst aus und benötigen kein Google-Cloud-Konto.</p>
</div>

## Embeddings

Für die semantische Suche und den KI-Assistenten erzeugt Lalabase Embeddings, die in der
`vector`-Spalte von PostgreSQL gespeichert werden. Auch diese laufen über deinen Schlüssel.

### Embedding-Dimension wählen

Ein Embedding-Raum ist das Tripel aus **Provider, Modell und Dimension**. Unterstützt
werden **768**, **1024**, **1536** und **3072** Dimensionen — damit sind EU-Modelle wie
`mistral-embed` (1024), `nomic` (768) und `bge-m3` (1024) nutzbar, ebenso die großen
Modelle `text-embedding-3-large` (3072) und `gemini-embedding-001` (3072).

**3072 setzt pgvector 0.7+ voraus.** Der Suchindex nutzt für diese Größe einen
`halfvec`-Cast, weil pgvector reguläre Vektor-Indexe nur bis 2000 Dimensionen zulässt.
Der gespeicherte Vektor bleibt voll präzise; halbiert wird nur die Genauigkeit im
Index, was die Trefferreihenfolge praktisch nicht verändert. Läuft die Extension unter
0.7, lässt sich das Schema gar nicht erst einspielen — und sollte eine Organisation
dennoch auf 3072 stehen, **schlägt die Suche mit einem Fehler fehl** statt langsam zu
werden: die Abfrage nutzt denselben `halfvec`-Cast wie der Index, und den Typ gibt es
dort nicht.

Für diese Konfiguration gibt es bewusst **keine Oberfläche**: Sie greift sofort und
ohne Zwischenstufe, deshalb gehört sie in die Konsole und in die Hand des Betreibers.
Eine Organisation ohne Konfiguration bleibt unverändert bei 1536.

<div class="docs-callout docs-callout--info">
  <div class="docs-callout__title">Der Wechsel läuft in Stufen — die Suche bleibt währenddessen erreichbar</div>
  <p>Lalabase trennt <strong>Konfiguration</strong> (wohin neue Embeddings geschrieben werden) von <strong>Bedienung</strong> (welcher Raum die Suche beantwortet). Eine Umkonfiguration allein verschiebt die Bedienung <em>nicht</em>: Der bisherige Raum antwortet weiter, während der neue im Hintergrund aufgebaut wird. Erst das Umschalten (<code>embeddings:flip</code>) wechselt die Bedienung — in einem einzigen Schritt.</p>
</div>

Ablauf für einen Wechsel, am Beispiel Mistral mit 1024 Dimensionen:

```bash
# 0. Ausgangslage prüfen — muss grün sein
bin/rails "embeddings:check_alignment[ORG_ID]"

# 1. Umkonfigurieren (Rails-Konsole)
#    org.update!(embedding_provider: 'openai_compatible',
#                embedding_api_base: 'https://api.mistral.ai/v1',
#                embedding_access_token: '…',
#                embedding_model: 'mistral-embed',
#                embedding_dims: 1024)
#    → die Suche läuft unverändert weiter, im BISHERIGEN Raum

# 2. Ziel-Raum im Hintergrund aufbauen (kein Umschalten).
#    Idempotent und wiederanlaufbar; läuft durchs KI-Kontingent.
bin/rails "embeddings:build_target[ORG_ID]"

# 3. Fortschritt prüfen — zeigt aktiven Raum, Ziel-Raum und die Restlücke
bin/rails "embeddings:status[ORG_ID]"

# 4. Umschalten. Verweigert, solange der Ziel-Raum unvollständig ist.
bin/rails "embeddings:flip[ORG_ID]"

# 5. Gegenprüfen — muss grün sein
bin/rails "embeddings:check_alignment[ORG_ID]"
```

Nach dem Umschalten werden die Vektoren des alten Raums aus dem Suchindex genommen
(im Hintergrund), bleiben aber als Rückweg liegen. Im `check_alignment` erscheinen sie
als `stale` — das ist der Normalzustand und kein Befund.

**Zurückrollen**, falls die Qualität des neuen Modells nicht überzeugt:

```bash
# SPACE_ID aus `embeddings:status`. Reaktiviert den alten Raum, holt die
# Änderungen seit dem Wechsel nach und schaltet zurück.
bin/rails "embeddings:rollback[ORG_ID,SPACE_ID]"
```

Der Rückweg funktioniert auch mit eigenen Zugangsdaten: Zu jedem gehaltenen Raum sind
Endpunkt und Token gespeichert, sie müssen also nicht neu eingegeben werden — auch dann
nicht, wenn die Organisations-Konfiguration inzwischen auf einen anderen Endpunkt zeigt.

<div class="docs-callout docs-callout--warning">
  <div class="docs-callout__title">Zwei Stolpersteine</div>
  <p><strong>KI muss aktiv sein:</strong> Ist die KI für die Organisation deaktiviert, bricht der Aufbau ab. Erst aktivieren, dann aufbauen.</p>
  <p><strong>Neue Inhalte während des Aufbaus:</strong> Sie werden bereits mit der Ziel-Konfiguration eingebettet und sind deshalb erst nach dem Umschalten auffindbar. Ein kurzes Frische-Fenster für die jüngsten Inhalte, keine Lücke im Bestand.</p>
</div>

<div class="docs-callout docs-callout--warning">
  <div class="docs-callout__title">Rückweg für Code-Symbole gilt nur bis zum nächsten Push</div>
  <p>Anders als Dokumente und Notizen werden Code-Symbole bei jedem Voll-Index gelöscht und neu angelegt. Die aufbewahrten Vektoren des alten Raums verlieren damit ihren Bezug: Für Code ist ein Rückweg nach dem nächsten Push faktisch ein vollständiger Neu-Index. Für alle anderen Inhalte bleibt er billig.</p>
</div>

## KI-Pakete: derselbe Mechanismus, vom Kunden ausgelöst

Alles oben beschreibt den Weg über die Konsole. Daneben gibt es **kuratierte KI-Pakete** —
vom Betreiber im Adminbereich gepflegte Bündel aus einem Embedding-Anbieter und einer
Staffel von Chat-Stufen, jeweils mit einer Rechtsraum-Einordnung. Wählt eine Organisation
ein Paket, entscheidet dessen Embedding-Slot über ihren Vektorraum — **nicht mehr die
Organisations-Konfiguration**.

Wichtig für den Betrieb: Bewegt ein Paketwechsel den Vektorraum, läuft **genau die
gestaffelte Umstellung von oben** ab — Zielraum aufbauen, alte Bedienung beibehalten,
automatisch umschalten, sobald der neue Raum vollständig ist. Der Unterschied ist nur, wer
sie auslöst: Das macht seit KI-Pakete P4 die Organisation selbst, über einen
Konsequenzen-Dialog, ohne Konsole und ohne dein Zutun. Ein Watcher übernimmt das
Umschalten; `embeddings:flip` von Hand ist dafür nicht nötig.

<div class="docs-callout docs-callout--warning">
  <div class="docs-callout__title">Den Embedding-Slot eines veröffentlichten Pakets zu ändern, erzwingt Reindexe</div>
  <p>Anbieter, Modell oder Dimension im Embedding-Slot eines Pakets zu bearbeiten, verschiebt den Vektorraum <em>jeder</em> Organisation, die auf diesem Paket sitzt. Jede von ihnen baut daraufhin ihren gesamten Korpus neu auf — und weil Reindexe seit P4a das Monatskontingent belasten, ist das eine Kostenentscheidung für fremde Konten, nicht bloß eine Konfigurationsänderung. Willst du ein anderes Embedding-Modell anbieten, lege ein <strong>neues Paket</strong> an und ziehe das alte zurück, statt das bestehende umzubauen.</p>
</div>

Ein zurückgezogenes Paket (`active: false`) verschwindet aus dem Katalog, läuft für
Organisationen darauf aber unverändert weiter. Sie sehen ihre Karte weiterhin, können die
Leistungsstufe darin jedoch nicht mehr wechseln — der Weg hinaus ist ein anderes Paket oder
„Eigene Konfiguration". Wer ein Paket zurückzieht, sollte deshalb einen Nachfolger
veröffentlicht haben.

**Wer wann gewechselt hat**, wird in zwei Formen festgehalten: Die Organisation trägt
Zeitpunkt und Person der *letzten* Wahl in eigenen Spalten, und jede Wahl schreibt zusätzlich
eine `Activity` mit beiden Rechtsraum-Einordnungen (vorher/nachher) und der Angabe, ob dabei
ein Neuaufbau der Suche angestoßen wurde. Diese Einträge lösen bewusst **keine**
Benachrichtigungen aus.

Dasselbe gilt seit dem KI-Bereichs-Umbau für den **Ein- und Ausschalter**: Jedes Aktivieren und
jedes Deaktivieren schreibt eine eigene `Activity` (`ai_activated` / `ai_deactivated`) mit
Zeitpunkt und Person. Auch hier halten die Spalten der Organisation nur die *letzte*
Aktivierung — wer die Kette braucht, findet sie ausschließlich in diesen Einträgen.

Die Historie hat seit dem KI-Bereichs-Umbau eine **Oberfläche**: Im Bereich **KI** der
Organisationsverwaltung listet die Seite **Protokoll** alle vier Verben in zeitlicher
Reihenfolge auf, mit Zeitpunkt, Person und, wo das Paket den Rechtsraum bewegt hat, beiden
Einordnungen. Sie ist Administratoren der Organisation vorbehalten und rein lesend.

Der Konsolenweg bleibt daneben nützlich, wenn du über Organisationen hinweg oder nach
Rohwerten aus dem Payload suchst:

```ruby
Activity.where(organisation_id: ORG_ID,
               action: %w[ai_activated ai_deactivated ai_package_selected ai_tier_selected])
        .order(:created_at)
        .pluck(:created_at, :action, :actor_id, :parameters)
```

<div class="docs-callout docs-callout--warning">
  <div class="docs-callout__title">Was der Verlauf (noch) nicht enthält</div>
  <p>Zwei Einschränkungen, damit Seite und Abfrage nicht mehr versprechen, als sie halten. <strong>Erstens</strong> schreibt das Formular „Eigene Konfiguration" bislang nichts mit: Ein Wechsel des Anbieters oder des hinterlegten Tokens taucht im Verlauf nicht auf, obwohl er den Auftragsverarbeiter ändert. <strong>Zweitens</strong> ist das Schreiben der Schalter-Einträge bewusst fehlertolerant: Scheitert es, wird die Aktivierung trotzdem ausgeführt und der Fehlschlag nur ins Log geschrieben, denn der Schalter darf an einer Protokollzeile nicht scheitern. Die Paketwahl verhält sich umgekehrt und committet ohne ihren Eintrag gar nicht erst.</p>
</div>

