DGX LLM Chat Gateway

System-Prompts (per-tenant, 3-Level-Hierarchie)

Per-Tenant versionierter System-Prompt-Stack mit Tenant / Scope / User Levels, Rollback, Audit-Trail und automatischer Inject-Pipeline in /v1/chat/completions und /c1/chat. Stack-Owner können ihren globalen Default selbst pflegen (Cut 2.35).

Use-Case 1 (häufig): "Wir wollen unseren tenant-globalen Prompt (z.B. Brand-Voice, Compliance, Persona) einmal hinterlegen und in jedem Chat-Call automatisch wirksam haben." → tenant-level POST.

Use-Case 2: "Wir wollen pro Application/Bereich einen anderen Prompt (z.B. 'support' vs 'sales')." → scope-level POST + Header.

Use-Case 3 (selten): "Wir wollen pro Endnutzer einen Prompt-Override (Personalisierung)." → user-level POST.

3-Level-Hierarchie

LevelScopeLookup-KeyRequired Headers
tenantgilt für alle Calls des Tenantslevel='tenant'
scopegilt pro scope_id (z.B. App-Name oder Use-Case)level='scope', scope_id=…X-Scope-Id
usergilt pro (scope_id, user_id)level='user', scope_id=…, user_id=…X-Scope-Id, X-User-Id

Komposition: tenant + scope + user werden bei jedem Chat-Call als XML-Stack zusammengefügt (von <tenant_default> über <scope_default> bis <user_prompt>) und als system-Message vor jeder anderen Message eingefügt. Leere Levels werden übersprungen.

Permission-Modell

6 Scopes, granular delegierbar pro Token in tokens.yaml:

system_prompt:read:tenant
system_prompt:read:scope
system_prompt:read:user
system_prompt:write:tenant
system_prompt:write:scope
system_prompt:write:user

write impliziert automatisch read auf gleichem Level (siehe expand_writes() in auth/scopes.rs).

Default-Mapping pro Role:

Roletenant wscope wuser wtenant rscope ruser r
tenant_admin
tenant_user
audit_reader

Custom-Tokens können zusätzliche Scopes via extra_scopes bekommen. Beispiel godelmann-gocreate-{test,prod} (Cut 2.35): tenant_user + extra_scopes: [system_prompt:write:tenant, system_prompt:write:scope].

Endpoints

Alle 8 Endpoints sind im OpenAPI-Schema (/openapi.json) ab Cut 2.35 und können mit TypeScript-Codegen (openapi-generator) integriert werden.

MethodPathBeschreibung
POST/v1/system-prompts/{level}Neue Version anlegen, wird automatisch is_current=1
GET/v1/system-prompts/{level}Current Version lesen (null wenn keine)
GET/v1/system-prompts/{level}/versionsAlle Versionen auflisten (für History-UI)
GET/v1/system-prompts/{level}/v/{n}Spezifische Version lesen
PUT/v1/system-prompts/{level}/current/{n}Rollback: alte Version aktivieren
DELETE/v1/system-prompts/{level}Soft-delete der current-Version (is_current=0)
DELETE/v1/system-prompts/{level}/v/{n}Hard-delete einer archivierten Version (409 wenn current)
GET/v1/system-prompts/effectiveComposed XML-Stack für aktuelle Token+Scope+User-Kombination

Beispiele — globalen tenant-Prompt setzen

BEARER="$YOUR_TENANT_BEARER"

# 1) Tenant-globalen Prompt setzen
curl -X POST https://dgx.spass.fun/v1/system-prompts/tenant \
  -H "Authorization: Bearer $BEARER" \
  -H "Content-Type: application/json" \
  -d '{
    "content": "Du bist ein präziser, höflicher Assistent. Antworte auf Deutsch...",
    "comment": "Brand-Voice v1.0, 2026-05-16"
  }'
# → { "id": 42, "level": "tenant", "version": 1, "is_current": true, ... }

# 2) Aktuelle Version lesen
curl https://dgx.spass.fun/v1/system-prompts/tenant \
  -H "Authorization: Bearer $BEARER"

# 3) Versions-Historie
curl https://dgx.spass.fun/v1/system-prompts/tenant/versions \
  -H "Authorization: Bearer $BEARER"

# 4) Effective-Stack verifizieren
curl https://dgx.spass.fun/v1/system-prompts/effective \
  -H "Authorization: Bearer $BEARER"
# → { "tenant": {"version": 1, "content": "...", "is_current": true},
#     "scope": null, "user": null,
#     "effective": "<tenant_default>...</tenant_default>",
#     "byte_size_visible": 234 }

Auto-Inject Pipeline (Cut 2.20+)

Wichtigster Fakt: sobald ein tenant-Prompt gesetzt ist, wird er automatisch bei jedem Call zu /v1/chat/completions und /c1/chat in messages[] injiziert. Kein system_prompt_ref-Feld nötig, kein Frontend-Code-Inject — der Stack-Builder läuft per-request server-side.

# Nach POST /v1/system-prompts/tenant ist KEIN client-side prefix mehr nötig:
curl -X POST https://dgx.spass.fun/c1/chat \
  -H "Authorization: Bearer $BEARER" -H "SPASS-User-Id: $USER_ID" \
  -d '{"model":"...", "message":"Hallo"}'
# Response-Header zeigt was injiziert wurde:
# SPASS-Augment-Applied: system-prompt=1items, server-tools=..., memory=...

Deaktivieren per-call mit SPASS-Augment: system-prompt=off (z.B. für admin-Diagnose-Calls). Default ist on.

Versions-Management & Rollback

Jede POST legt eine neue Version mit auto-incrementiertem version an und macht sie is_current=1. Alle vorherigen Versionen bleiben in der DB (append-only-history für audit).

# v2 anlegen
curl -X POST https://dgx.spass.fun/v1/system-prompts/tenant \
  -H "Authorization: Bearer $BEARER" -d '{"content":"v2 prompt..."}'

# Falls v2 schlecht: zurück auf v1
curl -X PUT https://dgx.spass.fun/v1/system-prompts/tenant/current/1 \
  -H "Authorization: Bearer $BEARER"
# → audit-event: system_prompt_rollback

# v2 endgültig löschen (NUR wenn nicht current!)
curl -X DELETE https://dgx.spass.fun/v1/system-prompts/tenant/v/2 \
  -H "Authorization: Bearer $BEARER"
# → 409 wenn v2 noch current ist

Audit-Trail

Jede POST/PUT/DELETE schreibt ein Event in audit.jsonl mit:

Operator-Filter:

jq 'select(.action | startswith("system_prompt_"))' \
  /home/dietmar/dgx-llm/data/audit/audit.jsonl.$(date +%F)

Plus die system_prompts-Row selbst speichert created_by (= token_hash) und created_at (unix-ts). Über GET /v1/system-prompts/{level}/versions sieht man die ganze Historie inkl. wer-wann-was.

Effective-View Redaction

GET /v1/system-prompts/effective zeigt die ganze Stack-Komposition, aber redactet Levels die der Token nicht lesen darf. Beispiel mit einem Token der nur system_prompt:read:scope hat:

{
  "tenant": {"redacted": true},
  "scope":  {"version": 5, "content": "...", ...},
  "user":   {"redacted": true},
  "effective": "<scope_default scope=\"...\">...</scope_default>",
  "byte_size_visible": 482
}

Das ist LLM07-Compliance ("show only what the token has scope to see"). Das Modell-im-Call sieht aber alle Levels — die Redaction wirkt nur auf die Operator-Introspection.

Limits

Cross-References