update-1.6.2
ci / test (services/knowledge) (push) Successful in 47s
ci / test (services/agent) (push) Successful in 55s
ci / test (platform/neuroforge) (push) Successful in 56s
ci / docker-build (push) Failing after 1m10s
ci / test (services/control) (push) Successful in 1m26s
release-tag / release-image (push) Successful in 6m43s

This commit is contained in:
2026-09-09 12:55:35 +02:00
parent e0bf42bf32
commit d18405ed2e
48 changed files with 8062 additions and 1111 deletions
+3 -1
View File
@@ -290,6 +290,8 @@ OLLAMA_REQUIRE_EMBEDDING_MODEL=true
# OLLAMA_MODEL=qwen3:8b
# Embedding-Modell für RAG.
OLLAMA_EMBEDDING_MODEL=embeddinggemma
# Optional: Bearer token sent as Authorization: Bearer <token> to Ollama-compatible endpoints.
OLLAMA_API_KEY=
# Modellspezifisches Retrieval-Prompting.
# auto = Modell automatisch erkennen; für embeddinggemma empfohlen.
@@ -799,4 +801,4 @@ QUEUE_SIZE=256
# Ticketpipelines gleichzeitig verteilen, wie Worker aktiv sind. Für drei
# gleichartige Nodes ist WORKERS=3 ein sinnvoller Lasttest; jeder Node bleibt
# zusätzlich durch OLLAMA_NODE_MAX_INFLIGHT begrenzt.
WORKERS=2
WORKERS=2
+1 -1
View File
@@ -62,7 +62,7 @@ func main() {
nodes = append(nodes, ollama.NodeConfig{Name: name, URL: nodeURL, Weight: weight})
}
o, err := ollama.NewPool(ollama.PoolConfig{
Nodes: nodes, RoutingMode: cfg.OllamaRoutingMode, NodeMaxInflight: cfg.OllamaNodeMaxInflight,
APIKey: cfg.OllamaAPIKey, Nodes: nodes, RoutingMode: cfg.OllamaRoutingMode, NodeMaxInflight: cfg.OllamaNodeMaxInflight,
HealthInterval: cfg.OllamaNodeHealthInterval, FailureCooldown: cfg.OllamaNodeFailureCooldown,
NodeRequestTimeout: cfg.OllamaNodeRequestTimeout, FailoverEnabled: cfg.OllamaFailoverEnabled,
FailoverAttempts: cfg.OllamaFailoverAttempts, RequireSameModelDigest: cfg.OllamaRequireSameDigest,
@@ -8,6 +8,7 @@ services:
DATA_DIR: /app/data
KNOWLEDGE_DIR: /app/knowledge
OLLAMA_URL: ${OLLAMA_URL:-http://ollama:11434}
OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
OLLAMA_TIMEOUT: ${OLLAMA_TIMEOUT:-10m}
OLLAMA_NUM_PREDICT: ${OLLAMA_NUM_PREDICT:-768}
OLLAMA_JSON_RETRIES: ${OLLAMA_JSON_RETRIES:-1}
+1
View File
@@ -24,6 +24,7 @@ services:
DATA_DIR: /app/data
KNOWLEDGE_DIR: /app/knowledge
OLLAMA_URL: ${OLLAMA_URL:-http://ollama:11434}
OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
# Local CPU inference can take several minutes on the first request.
OLLAMA_TIMEOUT: ${OLLAMA_TIMEOUT:-10m}
OLLAMA_NUM_PREDICT: ${OLLAMA_NUM_PREDICT:-768}
+2
View File
@@ -39,6 +39,7 @@ type Config struct {
GLPIAllowedStatusIDs []int64
OllamaURL string // legacy single-node value
OllamaAPIKey string // optional Bearer token sent to every Ollama node
OllamaURLs []string
OllamaNodeNames []string
OllamaNodeWeights []int
@@ -246,6 +247,7 @@ func Load() (Config, error) {
GLPIAllowInsecureHTTP: envBool("GLPI_ALLOW_INSECURE_HTTP", false),
GLPIAllowedStatusIDs: envInt64List("GLPI_ALLOWED_STATUS_IDS", "1"),
OllamaURL: strings.TrimRight(env("OLLAMA_URL", "http://ollama:11434"), "/"),
OllamaAPIKey: strings.TrimSpace(os.Getenv("OLLAMA_API_KEY")),
OllamaURLs: envStringListPreserveCase("OLLAMA_URLS", ""),
OllamaNodeNames: envStringListPreserveCase("OLLAMA_NODE_NAMES", ""),
OllamaNodeWeights: envIntListAllowEmpty("OLLAMA_NODE_WEIGHTS"),
+10
View File
@@ -29,6 +29,7 @@ type NodeConfig struct {
}
type PoolConfig struct {
APIKey string
Nodes []NodeConfig
RoutingMode string
NodeMaxInflight int
@@ -390,6 +391,7 @@ func (p *Pool) checkNode(ctx context.Context, n *poolNode) (string, string, erro
if err != nil {
return "", "", err
}
setBearer(req, p.cfg.APIKey)
resp, err := p.http.Do(req)
if err != nil {
return "", "", err
@@ -559,6 +561,7 @@ func (p *Pool) doPost(ctx context.Context, n *poolNode, path string, body []byte
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
setBearer(req, p.cfg.APIKey)
resp, err := p.http.Do(req)
if err != nil {
return 0, nil, err
@@ -574,6 +577,13 @@ func (p *Pool) doPost(ctx context.Context, n *poolNode, path string, body []byte
return resp.StatusCode, raw, nil
}
func setBearer(req *http.Request, token string) {
token = strings.TrimSpace(token)
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
}
func (p *Pool) selectNode(ctx context.Context, excluded map[string]struct{}, stage string) (*poolNode, int64, error) {
for {
nodes := p.orderedCandidates(excluded, stage)
@@ -388,3 +388,41 @@ func TestPoolFastestRecentProbesUnmeasuredNodes(t *testing.T) {
t.Fatalf("fastest_recent must measure both nodes before preferring one: a=%d b=%d", callsA.Load(), callsB.Load())
}
}
func TestPoolSendsBearerToHealthAndInference(t *testing.T) {
const token = "ollama-secret"
var tagsAuth, embedAuth string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/tags":
tagsAuth = r.Header.Get("Authorization")
tagsResponse(w, "chat-digest", "embed-digest")
case "/api/embed":
embedAuth = r.Header.Get("Authorization")
_ = json.NewEncoder(w).Encode(map[string]any{"embeddings": [][]float64{{1, 0}}})
default:
http.NotFound(w, r)
}
}))
defer srv.Close()
c, err := NewPool(PoolConfig{
APIKey: token, Nodes: []NodeConfig{{Name: "secure", URL: srv.URL, Weight: 1}}, RoutingMode: "least_inflight",
NodeMaxInflight: 1, HealthInterval: time.Minute, FailureCooldown: time.Second, NodeRequestTimeout: time.Second,
FailoverEnabled: false, FailoverAttempts: 1, RequireSameModelDigest: true, RequireEmbeddingModel: true, Model: "m", EmbeddingModel: "e",
}, "m", "e", "de-DE", "formal", 128, time.Minute, false, 0)
if err != nil {
t.Fatal(err)
}
if err := c.Ping(context.Background()); err != nil {
t.Fatal(err)
}
if _, err := c.Embed(context.Background(), []string{"x"}); err != nil {
t.Fatal(err)
}
if tagsAuth != "Bearer "+token {
t.Fatalf("tags authorization=%q", tagsAuth)
}
if embedAuth != "Bearer "+token {
t.Fatalf("embed authorization=%q", embedAuth)
}
}