Files
archivdms/internal/api/ollama_config_handlers.go
patrick 9a24ea29e1 FDN-01: repository & projektgerüst
Git-Repository für bestehenden archivdms-Code initialisiert, Branch-/Commit-Konvention (feature/<ticket>-<slug>-Branches, Ticket-Prefix in Commit-Nachricht) etabliert.
2026-08-11 21:27:53 +02:00

135 lines
4.6 KiB
Go

// Per-tenant configuration for an EXTERNAL, already-running Ollama server
// (never installed on the archivdms host — the base URL/port comes from the
// tenant admin). Gates the optional 'ollama' metadata-suggestion provider.
//
// GET /api/ollama-config
// PUT /api/ollama-config
//
// Both are admin-only (domain_admin manages its own tenant; superadmin must
// pass ?tenant_id=), mirroring the LDAP-config and tenant-settings handlers.
// The base URL is an internal network URL, not a secret, and is returned as-is.
package api
import (
"encoding/json"
"net/http"
"time"
"archivdms/internal/audit"
"archivdms/internal/llm"
)
// handleGetOllamaConfig returns the tenant's Ollama connection config. A tenant
// that has never configured Ollama gets a zero/default (disabled) config, not a
// 404 — the frontend always renders an editable form.
func (s *Server) handleGetOllamaConfig(w http.ResponseWriter, r *http.Request) {
tenantID, ok := s.resolveTenantSettingsTenant(w, r)
if !ok {
return
}
cfg, err := s.store.GetOllamaConfig(r.Context(), tenantID)
if err != nil {
writeError(w, http.StatusInternalServerError, "load ollama config failed")
return
}
writeJSON(w, http.StatusOK, cfg)
}
// handleListOllamaModels queries an Ollama server for the models actually
// installed there, so the frontend can offer a picklist instead of a free-text
// field. Live call, no caching. Prefers the not-yet-saved ?base_url= query
// param (lets the admin test a URL before hitting "Speichern"); falls back to
// the persisted config's base_url when the param is absent. 400 if neither is
// set. When the external Ollama server is unreachable the failure is the
// external dependency's, not ours → 502 Bad Gateway, not 500.
func (s *Server) handleListOllamaModels(w http.ResponseWriter, r *http.Request) {
tenantID, ok := s.resolveTenantSettingsTenant(w, r)
if !ok {
return
}
cfg, err := s.store.GetOllamaConfig(r.Context(), tenantID)
if err != nil {
writeError(w, http.StatusInternalServerError, "load ollama config failed")
return
}
baseURL := r.URL.Query().Get("base_url")
if baseURL == "" {
baseURL = cfg.BaseURL
}
if baseURL == "" {
writeError(w, http.StatusBadRequest, "Server-URL muss zuerst eingetragen werden")
return
}
// Short, listing-specific timeout — independent of the (possibly long)
// generate timeout. Cap the stored value so a large generate timeout does
// not make the picklist request hang for minutes.
timeout := 10 * time.Second
if cfg.TimeoutSeconds > 0 && cfg.TimeoutSeconds < 10 {
timeout = time.Duration(cfg.TimeoutSeconds) * time.Second
}
models, err := llm.ListModels(r.Context(), baseURL, timeout)
if err != nil {
writeError(w, http.StatusBadGateway, "ollama nicht erreichbar: "+err.Error())
return
}
writeJSON(w, http.StatusOK, map[string][]string{"models": models})
}
type upsertOllamaConfigRequest struct {
Enabled bool `json:"enabled"`
BaseURL string `json:"base_url"`
Model string `json:"model"`
TimeoutSeconds int `json:"timeout_seconds"`
}
// handleUpsertOllamaConfig creates or updates the tenant's Ollama connection
// config. Validation (enabled requires base_url+model, http(s) prefix, timeout
// range) lives in the store. Every attempt — success or failure — is
// audit-logged (GoBD).
func (s *Server) handleUpsertOllamaConfig(w http.ResponseWriter, r *http.Request) {
sess := sessionFromCtx(r.Context())
tenantID, ok := s.resolveTenantSettingsTenant(w, r)
if !ok {
return
}
logFail := func(detail string) {
tid := tenantID
s.audlog.Log(audit.Entry{
EventType: audit.EventOllamaConfigUpdate, Username: sess.Username,
TenantID: &tid, Success: false, Detail: detail,
})
}
var req upsertOllamaConfigRequest
if err := json.NewDecoder(r.Body).Decode(&req); err != nil {
logFail("ollama_config invalid_body")
writeError(w, http.StatusBadRequest, "invalid request body")
return
}
if err := s.store.UpsertOllamaConfig(r.Context(), tenantID, req.Enabled, req.BaseURL, req.Model, req.TimeoutSeconds); err != nil {
logFail("ollama_config upsert_failed:" + err.Error())
writeError(w, http.StatusBadRequest, err.Error())
return
}
tid := tenantID
s.audlog.Log(audit.Entry{
EventType: audit.EventOllamaConfigUpdate, Username: sess.Username,
TenantID: &tid, Success: true, Detail: "ollama_config_saved",
})
// Reload so the response reflects the persisted (normalised) state.
cfg, err := s.store.GetOllamaConfig(r.Context(), tenantID)
if err != nil {
writeError(w, http.StatusInternalServerError, "load ollama config failed")
return
}
writeJSON(w, http.StatusOK, cfg)
}