// 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) }