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
+6
View File
@@ -74,6 +74,12 @@ OPENAI_API_KEY=
# qwen3:8b statt gemma3 setzen. Das Modell muss vorher in Ollama vorhanden sein.
OLLAMA_MODEL=gemma3
OLLAMA_EMBEDDING_MODEL=embeddinggemma
# Optional: Bearer token sent as Authorization: Bearer <token> to Ollama-compatible endpoints.
OLLAMA_API_KEY=
# Optional NeuroForge GPU-worker override; falls back to OLLAMA_API_KEY.
NEUROFORGE_WORKER_OLLAMA_API_KEY=
# Optional NeuroForge-only override; takes precedence over OLLAMA_API_KEY.
NEUROFORGE_OLLAMA_API_KEY=
OLLAMA_TIMEOUT=10m
OLLAMA_MAX_CONCURRENT=1
OLLAMA_NUM_PREDICT=768
+7 -2
View File
@@ -1,5 +1,5 @@
# Remote NeuroForge subagent host (v1.6.1)
IMAGE_TAG=1.6.1
# Remote NeuroForge subagent host (v1.6.2)
IMAGE_TAG=1.6.2
NEUROFORGE_MASTER_URL=https://neuroforge.internal.example
NEUROFORGE_WORKER_TOKEN=CHANGE_ME_WORKER_TOKEN_SHARED_WITH_MASTER
NEUROFORGE_WORKER_HEARTBEAT_INTERVAL=15s
@@ -16,5 +16,10 @@ NEUROFORGE_GPU_WORKER_CONCURRENCY=1
NEUROFORGE_WORKER_OLLAMA_URL=http://host.docker.internal:11434
OLLAMA_MODEL=gemma4
OLLAMA_EMBEDDING_MODEL=embeddinggemma
# Optional: Bearer token sent as Authorization: Bearer <token> to Ollama-compatible endpoints.
OLLAMA_API_KEY=
NEUROFORGE_OLLAMA_NUM_CTX=8192
OLLAMA_KEEP_ALIVE=10m
# Optional GPU-worker-specific override; falls back to OLLAMA_API_KEY.
NEUROFORGE_WORKER_OLLAMA_API_KEY=
+8 -55
View File
@@ -1,4 +1,4 @@
name: mega-ci
name: ci
on:
push:
@@ -7,7 +7,7 @@ on:
branches: [main]
jobs:
go-quality:
test:
runs-on: ubuntu-latest
strategy:
fail-fast: false
@@ -18,69 +18,22 @@ jobs:
- services/knowledge
- services/control
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
with:
go-version-file: ${{ matrix.module }}/go.mod
cache-dependency-path: ${{ matrix.module }}/go.mod
cache: false
- name: Test
working-directory: ${{ matrix.module }}
run: go test ./...
- name: Vet
working-directory: ${{ matrix.module }}
run: go vet ./...
- name: Build
working-directory: ${{ matrix.module }}
run: go build ./...
static-release-gates:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Install PyYAML
run: python3 -m pip install --user PyYAML
- name: Static production preflight
run: ./scripts/preflight.sh --static
- name: Compose environment isolation
run: ./scripts/check-compose-env.py
- name: Secret scan
run: ./scripts/secret-scan.sh
- name: YAML syntax check
shell: bash
run: |
python3 - <<'PY'
from pathlib import Path
import yaml
files = list(Path('.').rglob('*.yml')) + list(Path('.').rglob('*.yaml'))
for p in files:
if '.git' in p.parts:
continue
with p.open(encoding='utf-8') as f:
yaml.safe_load(f)
print(f'parsed {len(files)} YAML files')
PY
docker-build:
runs-on: ubuntu-latest
needs: [go-quality, static-release-gates]
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Build all project images
- uses: actions/checkout@v4
- uses: docker/setup-buildx-action@v3
- name: Build all release images
run: docker buildx bake --set '*.output=type=cacheonly'
+16 -119
View File
@@ -1,4 +1,4 @@
name: mega-release
name: release
on:
push:
@@ -10,136 +10,33 @@ jobs:
runs-on: ubuntu-latest
env:
REGISTRY: git.send.nrw/sendnrw
RUNNER_TOOL_CACHE: /toolcache
steps:
- name: Checkout
uses: actions/checkout@v4
- uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Validate immutable release version
id: meta
shell: bash
run: |
set -euo pipefail
REF_NAME="${GITEA_REF_NAME:-${GITHUB_REF_NAME:-}}"
VERSION="${REF_NAME#v}"
test -n "$VERSION"
FILE_VERSION="$(tr -d '\r\n' < VERSION)"
test "$VERSION" = "$FILE_VERSION" || {
echo "Tag version $VERSION does not match VERSION=$FILE_VERSION" >&2
exit 1
}
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- uses: docker/setup-qemu-action@v3
- uses: docker/setup-buildx-action@v3
with:
config-inline: |
[registry."git.send.nrw"]
http = true
insecure = true
- name: Login to registry
uses: docker/login-action@v3
- uses: docker/login-action@v3
with:
registry: git.send.nrw
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
- name: Push NeuroForge server
uses: docker/build-push-action@v6
with:
context: ./platform/neuroforge
file: ./platform/neuroforge/Dockerfile
target: server
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/glpi-neuroforge-mega-neuroforge:${{ steps.meta.outputs.version }}
- name: Push NeuroForge worker
uses: docker/build-push-action@v6
with:
context: ./platform/neuroforge
file: ./platform/neuroforge/Dockerfile
target: worker
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/glpi-neuroforge-mega-neuroforge-worker:${{ steps.meta.outputs.version }}
- name: Push Agent
uses: docker/build-push-action@v6
with:
context: ./services/agent
file: ./services/agent/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/glpi-neuroforge-mega-agent:${{ steps.meta.outputs.version }}
- name: Push Agent data-init
uses: docker/build-push-action@v6
with:
context: ./services/agent
file: ./services/agent/Dockerfile
target: data-init
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/glpi-neuroforge-mega-agent-data-init:${{ steps.meta.outputs.version }}
- name: Push Knowledge
uses: docker/build-push-action@v6
with:
context: ./services/knowledge
file: ./services/knowledge/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/glpi-neuroforge-mega-knowledge:${{ steps.meta.outputs.version }}
- name: Push Control
uses: docker/build-push-action@v6
with:
context: ./services/control
file: ./services/control/Dockerfile
platforms: linux/amd64
push: true
tags: ${{ env.REGISTRY }}/glpi-neuroforge-mega-control:${{ steps.meta.outputs.version }}
release-archive:
needs: release-images
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
with:
fetch-depth: 0
- name: Validate and package
- name: Resolve immutable release version
id: meta
shell: bash
run: |
set -euo pipefail
REF_NAME="${GITEA_REF_NAME:-${GITHUB_REF_NAME:-}}"
VERSION="${REF_NAME#v}"
test "$VERSION" = "$(tr -d '\r\n' < VERSION)"
./scripts/secret-scan.sh
find . -type f ! -path './.git/*' ! -name MANIFEST.sha256 -print0 \
| sort -z \
| xargs -0 sha256sum > MANIFEST.sha256
ROOT_DIR="$(pwd)"
PKG_DIR="../glpi-neuroforge-mega-v${VERSION}-gitea"
rm -rf "$PKG_DIR"
mkdir -p "$PKG_DIR"
tar --exclude=.git -cf - . | (cd "$PKG_DIR" && tar -xf -)
cd ..
zip -qr "glpi-neuroforge-mega-v${VERSION}-gitea.zip" "glpi-neuroforge-mega-v${VERSION}-gitea"
sha256sum "glpi-neuroforge-mega-v${VERSION}-gitea.zip" > "glpi-neuroforge-mega-v${VERSION}-gitea.zip.sha256"
- name: Upload release archive
uses: actions/upload-artifact@v4
with:
name: glpi-neuroforge-mega-${{ gitea.ref_name }}
path: |
../glpi-neuroforge-mega-*.zip
../glpi-neuroforge-mega-*.zip.sha256
VERSION="$(git describe --tags --exact-match HEAD | sed 's/^v//')"
test -n "$VERSION"
test "$(cat VERSION)" = "$VERSION"
echo "version=$VERSION" >> "$GITHUB_OUTPUT"
- name: Build and push all release images
env:
IMAGE_TAG: ${{ steps.meta.outputs.version }}
run: docker buildx bake --push
+18 -29
View File
@@ -1,10 +1,8 @@
# Build & Release — GLPI NeuroForge Mega v1.6.2
# Build and Release
The repository has one canonical image/release pipeline. Nested service workflows are intentionally not used.
## Repository image set
## Project images
`docker buildx bake` builds all six immutable project images:
The repository publishes six immutable project images:
- `glpi-neuroforge-mega-neuroforge`
- `glpi-neuroforge-mega-neuroforge-worker`
@@ -13,42 +11,33 @@ The repository has one canonical image/release pipeline. Nested service workflow
- `glpi-neuroforge-mega-knowledge`
- `glpi-neuroforge-mega-control`
Default registry/tag:
The default registry is `git.send.nrw/sendnrw`.
```text
git.send.nrw/sendnrw/<image>:1.6.2
```
## Local build
Override without editing the bake file:
From repository root:
```bash
IMAGE_TAG=1.6.2 REGISTRY=git.send.nrw/sendnrw docker buildx bake
docker buildx bake
```
## Gitea Actions
Override registry/tag when required:
`.gitea/workflows/ci.yml` runs test/vet/build for all four Go modules, static release checks and Docker builds.
`.gitea/workflows/release.yml` runs only on immutable `v*` tags. The tag must exactly match the root `VERSION` file. It pushes all six project images with the version tag only; no production dependency on `latest` is introduced.
Required registry secrets:
```text
DOCKER_USERNAME
DOCKER_PASSWORD
```bash
REGISTRY=registry.example/org IMAGE_TAG=1.6.2 docker buildx bake
```
Release example:
## CI
`.gitea/workflows/ci.yml` runs `go test ./...` and `go vet ./...` for NeuroForge, Agent, Knowledge and Control and then performs a Buildx Bake build of the full image set.
## Release
Set `VERSION` and all compose/deployment tags to the desired immutable version, then push a matching tag:
```bash
git tag v1.6.2
git push origin v1.6.2
```
## Local source gate
```bash
./scripts/release-gate.sh
```
This executes static production checks, Compose environment isolation, secret scanning, graph reproducibility, test/vet/build for every module, and targeted race checks.
`.gitea/workflows/release.yml` verifies that the git tag matches `VERSION` and pushes all six images with tag `1.6.2`. Project production compose files do not require `latest`.
+3 -11
View File
@@ -1,13 +1,5 @@
# Distributed Deployment — v1.6.2
# v1.6.2 Distributed Deployment Bundle
The distributed deployment is part of the canonical release, not a separate code fork.
This is the unchanged v1.6.2 application release plus ready-to-edit deployment kits under `deployments/`. No application code or persisted data format was changed.
- `deployments/master`: authoritative NeuroForge Master, GLPI Agent, Knowledge, Control, optional Research, Prometheus and Grafana.
- `deployments/cpu-subagent`: remote CPU execution node for graph/relink work.
- `deployments/gpu-subagent`: remote GPU execution node plus Ollama for chat/embedding work.
All three roles ship with complete `.env` templates containing `CHANGE_ME_...` placeholders. The same real `NEUROFORGE_WORKER_TOKEN` must be configured on Master and both worker roles.
Recommended startup order: GPU subagent, CPU subagent, then Master. Keep the NeuroForge data volume authoritative on the Master only.
The v1.6.1 recovery/OOM fixes are included unchanged in v1.6.2. Do not delete the existing `neuroforge-data` volume when upgrading.
See `deployments/README.md`.
+645 -640
View File
File diff suppressed because it is too large Load Diff
+16 -22
View File
@@ -1,15 +1,13 @@
# GLPI NeuroForge Mega v1.6.2
# GLPI NeuroForge Mega v1.6.1
> Release: **v1.6.2** · kanonischer Vollrelease mit Full-Mega-, Distributed- und Standalone-Betriebsmodi. Die v1.6.1 Recovery/OOM-Härtung ist vollständig enthalten.
v1.6.2 konsolidiert die zuvor getrennten Pakete wieder in **ein vollständiges Monorepo**: NeuroForge Master, CPU/GPU-Subagents, GLPI Agent, Knowledge, Control, Ollama, Research sowie eigenständig betreibbare Agent-/Knowledge-/Ollama-Core-Kits und Prometheus/Grafana-Beispiele.
> Release: **v1.6.1** · Crash-/Recovery-Hardening für große Knowledge-Korpora auf Basis der v1.6.0 Master/Subagent- und n:m-Graph-Architektur.
Ein kontrolliertes Monorepo aus **GLPI AI Agent**, **GLPI AI Knowledgebase** und **NeuroForge + SQAR**. Ziel ist nicht ein untrennbarer Monolith, sondern eine gemeinsame Plattform mit klaren Zuständigkeiten, getrennten Credentials und nachvollziehbaren Failure-Modi.
## Crash-/Recovery-Hardening (seit v1.6.1, in v1.6.2 enthalten)
## Crash-/Recovery-Hardening (v1.6.1)
Die in v1.6.1 eingeführte Härtung behebt einen RAM-/Recovery-Fehler, der bei großen Graph-Backfills nach mehreren Stunden Laufzeit auftreten konnte. Erfolgreiche `vector.relink`-Jobs verwerfen ihre großen transienten Vector-Payloads unmittelbar nach dem autoritativen Master-Apply; ein Queue-Payload-Budget verhindert neue ungebremste Speicherbelegung. Beim ersten Start migriert NeuroForge alte v1.6.0-Checkpoints streaming, bevor `state.json` vollständig in den RAM geladen wird.
v1.6.1 behebt einen RAM-/Recovery-Fehler, der bei großen Graph-Backfills nach mehreren Stunden Laufzeit auftreten konnte. Erfolgreiche `vector.relink`-Jobs verwerfen ihre großen transienten Vector-Payloads unmittelbar nach dem autoritativen Master-Apply; ein Queue-Payload-Budget verhindert neue ungebremste Speicherbelegung. Beim ersten Start migriert NeuroForge alte v1.6.0-Checkpoints streaming, bevor `state.json` vollständig in den RAM geladen wird.
HNSW-Deltas werden nicht mehr über einen vollständigen Deep-Copy aller Vektoren erzeugt. Checkpoints persistieren den Index vor `state.json`, sodass ein Crash während der Indexpersistenz den autoritativen Checkpoint nicht vor den verwendbaren Index ziehen kann. Bei Containerstarts mit explizitem `-listen` ist sofort eine Bootstrap-Liveness-/Startup-Seite erreichbar; Logs zeigen die aktuelle Recovery-Phase (`checkpoint.precompact`, `memory-segments.scan`, `wal.replay`, `hnsw.snapshot.load`, `hnsw.rebuild`, ...).
@@ -34,22 +32,6 @@ Lokale Standardrollen:
Zusätzliche Hosts können mit `docker-compose.subagent.yml` angebunden werden. Das Control Center bleibt read-only und zeigt Master-, Worker-, Queue- und Graphzustand. Details: [`docs/MASTER-SUBAGENT-ORCHESTRATOR.md`](docs/MASTER-SUBAGENT-ORCHESTRATOR.md).
## Deployment-Modi
Das Repository enthält bewusst mehrere, voneinander entkoppelte Betriebsformen:
- `docker-compose.yml`: vollständiger Mega-Stack mit lokalem CPU- und GPU-Worker.
- `deployments/master`: autoritativer Master ohne lokale Worker, für getrennte CPU-/GPU-Hosts.
- `deployments/cpu-subagent`: abgesetzter CPU-Worker für `cpu,vector.relink`.
- `deployments/gpu-subagent`: abgesetzter GPU-Worker plus Ollama für `gpu,model.chat,model.embed`.
- `deployments/agent`: GLPI Agent standalone mit lokalem Vector-Backend, ohne NeuroForge-Zwang.
- `deployments/knowledge`: Knowledge standalone.
- `deployments/ollama`: Ollama standalone.
- `deployments/combined`: Agent + Knowledge + Ollama ohne NeuroForge.
Details: [`deployments/README.md`](deployments/README.md).
## Unified Graph Explorer (v1.4.0)
Das read-only Control Center visualisiert Runtime/Trust, Ticket-Evidence, Learning-Lineage, Research-Provenance, einen redigierten NeuroForge-Brain-Graph sowie einen reproduzierbaren Engineering-Graph aus Go-AST und Compose. Für Dateien/Symbole/Routen gibt es zusätzlich eine statische Change-Impact-/Blast-Radius-Sicht. 2D ist der operative Default; 3D ist ein optionaler, gebundener Explorer.
@@ -270,3 +252,15 @@ NEUROFORGE_GOAL_LEARNING_ENABLED=true
```
Damit bleibt rohes Chat-/Assistant-Lernen weiterhin deaktiviert.
## v1.6.2: vollständige Deployment-Rollen und Ollama Bearer Auth
Das Repository enthält neben dem vollständigen Mega-Compose eigenständige Kits unter `deployments/` für `master`, `cpu-subagent`, `gpu-subagent`, `agent`, `knowledge`, `ollama` und `combined`. Damit können der produktive GLPI-Agent/Knowledge-Core und die NeuroForge-Orchestrierung getrennt oder gemeinsam betrieben werden.
Für Ollama-kompatible Endpunkte hinter einem Bearer-geschützten Gateway kann optional gesetzt werden:
```env
OLLAMA_API_KEY=
```
Agent, Knowledge, NeuroForge und modellfähige Subagents senden den Wert dann als `Authorization: Bearer ...`. NeuroForge kennt zusätzlich `NEUROFORGE_OLLAMA_API_KEY` und `NEUROFORGE_WORKER_OLLAMA_API_KEY` als rollenbezogene Overrides. Details: `docs/OLLAMA-BEARER-AUTH.md`.
+49 -28
View File
@@ -1,38 +1,59 @@
# GLPI NeuroForge Mega v1.6.2
## Consolidated production release
v1.6.2 is the consolidated production release that reunifies the full Mega repository after the stripped-core packaging experiment and adds authenticated Ollama-compatible endpoints.
v1.6.2 is the canonical full repository release. It keeps the v1.6.1 runtime/data format and recovery/OOM hotfixes, and consolidates all previously split deployment variants into one coherent tree.
## Complete deployment set
### Included deployment modes
The repository now ships all supported operating modes together:
- Full Mega stack with local CPU/GPU workers.
- Distributed Master with remote CPU and GPU subagents.
- Standalone GLPI Agent using a local vector backend.
- Standalone Knowledge service.
- Standalone Ollama.
- Combined Agent + Knowledge + Ollama core stack without NeuroForge.
- Prometheus/Grafana example monitoring on the distributed Master, including payload/queue/graph metrics and alerts.
- full single-host Mega stack,
- distributed Master,
- remote CPU subagent,
- remote GPU subagent with Ollama,
- standalone GLPI Agent,
- standalone Knowledge service,
- standalone Ollama,
- combined Agent + Knowledge + Ollama core,
- Prometheus/Grafana example monitoring on the Master.
### Packaging / CI corrections
The v1.6.1 recovery/OOM hardening remains included, including startup pre-compaction of legacy relink payloads, bounded durable-job payload retention, lower graph-backfill pressure, visible startup phases, stricter corrupted-state handling and HNSW checkpoint memory reductions.
- Restores the complete v1.6.1 NeuroForge recovery hardening and graph/orchestrator source tree.
- Includes complete `.env` files with safe placeholders for Master, CPU subagent, GPU subagent and standalone roles.
- Removes embedded real credentials from deployment examples.
- Replaces conflicting release workflows with one root CI workflow and one immutable tag release workflow.
- CI covers NeuroForge, Agent, Knowledge and Control plus all six project images.
- `docker-bake.hcl` builds NeuroForge server/worker, Agent, Agent data-init, Knowledge and Control.
- Removes nested/legacy workflow copies and stale duplicate compose files.
- Regenerates a single repository manifest for the exact final archive.
## Ollama Bearer authentication
### Runtime safety inherited from v1.6.1
A shared optional variable is now supported:
- Streaming compaction of legacy terminal `vector.relink` payloads before normal state loading.
- Completed relink payload/result blobs are discarded after successful Master apply and during WAL recovery.
- Bounded pending durable job payload bytes.
- HNSW delta checkpoints avoid full graph/vector deep copies.
- Index-first checkpoint ordering and fail-closed authoritative JSON recovery.
- `/livez` and a bootstrap `/admin` page are available while store recovery is still running.
- Conservative graph backfill and job-retention defaults for large Knowledge corpora.
```env
OLLAMA_API_KEY=
```
There is no intentional persisted-data-format break from v1.6.1 to v1.6.2.
When non-empty, clients send:
```http
Authorization: Bearer <OLLAMA_API_KEY>
```
for Ollama-compatible `/api/tags`, `/api/chat` and `/api/embed` requests.
Supported clients:
- GLPI Agent, including every node in its Ollama pool,
- Knowledge AI fallback,
- NeuroForge provider and readiness/provider-health checks,
- NeuroForge GPU/model subagents.
NeuroForge also supports role-specific overrides:
```env
NEUROFORGE_OLLAMA_API_KEY=
NEUROFORGE_WORKER_OLLAMA_API_KEY=
```
The role-specific value takes precedence over `OLLAMA_API_KEY` for the corresponding NeuroForge component.
The key is stored as a NeuroForge secret when configured through `NEUROFORGE_OLLAMA_API_KEY`; admin APIs expose only configured/masked status unless secret reveal is explicitly enabled.
Native Ollama does not itself become authenticated merely by setting this variable. The option is intended for an authenticated Ollama-compatible gateway/reverse proxy or another Ollama-compatible endpoint that validates Bearer tokens. Direct native Ollama deployments should normally leave it empty.
## CI/release consolidation
Gitea workflows now live only at repository root and cover all four Go modules. The release workflow builds and pushes the six immutable project images. `docker-bake.hcl` contains the same complete image set. Production compose files remain pinned through `IMAGE_TAG`; no `latest` project image is required.
+11 -24
View File
@@ -1,29 +1,16 @@
# Deployment Kits — GLPI NeuroForge Mega v1.6.2
# Distributed Deployment Kits (v1.6.2)
This repository intentionally supports both the complete NeuroForge platform and stripped standalone operation.
This directory contains three independent deployment kits:
## Complete / distributed platform
- `master/` - authoritative NeuroForge Master plus Agent, Knowledge, Control, optional SearXNG and optional Prometheus/Grafana.
- `cpu-subagent/` - CPU worker for `vector.relink` / graph convergence.
- `gpu-subagent/` - GPU worker plus local Ollama for `model.chat` and `model.embed`.
- `master/` — authoritative NeuroForge Master plus GLPI Agent, Knowledge, Control, optional SearXNG, Prometheus and Grafana. Remote CPU/GPU workers connect to this node.
- `cpu-subagent/` — disposable CPU worker with `cpu,vector.relink` capabilities.
- `gpu-subagent/` — disposable GPU worker plus Ollama with `gpu,model.chat,model.embed` capabilities.
The generated `NEUROFORGE_WORKER_TOKEN` is identical in all three `.env` files. Replace the RFC 5737 example IP addresses (`192.0.2.x`) with real reachable addresses before starting.
The Master is the only authoritative owner of NeuroForge state. CPU/GPU workers use leases, heartbeats and fenced job completion.
Recommended order:
1. GPU subagent: `docker compose --profile monitoring up -d`
2. CPU subagent: `docker compose --profile monitoring up -d`
3. Master: fill GLPI credentials, then `docker compose --profile research --profile monitoring up -d`
## Standalone core operation
- `agent/` — GLPI Agent with local Knowledge vector backend; no NeuroForge, Control or Research dependency.
- `knowledge/` — standalone Knowledge editor/service.
- `ollama/` — standalone Ollama runtime.
- `combined/` — Agent + Knowledge + Ollama on one host, still without NeuroForge.
The standalone Agent and Knowledge kits share `runtime/knowledge` by default. The Agent mounts it read-only; Knowledge mounts it read-write.
## Release rules
- Project images are pinned by `IMAGE_TAG=1.6.2`; production compose files do not require `latest`.
- Replace every `CHANGE_ME_...` placeholder before startup.
- Never use `docker compose down -v` during an in-place upgrade unless loss of persistent state is intended.
- For a v1.6.0/v1.6.1 NeuroForge data volume, keep the volume: v1.6.2 includes the v1.6.1 recovery/OOM hotfixes and startup compaction path.
See each role's README/preflight and the root `README.md` for startup order.
Only the Master holds authoritative NeuroForge state. Workers are disposable execution nodes.
+1
View File
@@ -24,6 +24,7 @@ services:
DATA_DIR: /app/data
KNOWLEDGE_DIR: /app/knowledge
KNOWLEDGE_VECTOR_BACKEND: local
OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
OUTCOME_LEARNING_ENABLED: "false"
OUTCOME_RETRIEVAL_ENABLED: "false"
BRAIN_ACTIVITY_URL: ""
+1
View File
@@ -33,6 +33,7 @@ services:
DATA_DIR: /app/data
KNOWLEDGE_DIR: /app/knowledge
OLLAMA_URL: http://ollama:11434
OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
OLLAMA_URLS: http://ollama:11434
KNOWLEDGE_VECTOR_BACKEND: local
OUTCOME_LEARNING_ENABLED: "false"
@@ -39,6 +39,8 @@ services:
NEUROFORGE_WORKER_MAX_CONCURRENCY: ${NEUROFORGE_GPU_WORKER_CONCURRENCY:-1}
NEUROFORGE_WORKER_HEARTBEAT_INTERVAL: ${NEUROFORGE_WORKER_HEARTBEAT_INTERVAL:-15s}
NEUROFORGE_WORKER_OLLAMA_URL: http://ollama:11434
NEUROFORGE_WORKER_OLLAMA_API_KEY: ${NEUROFORGE_WORKER_OLLAMA_API_KEY:-}
OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
NEUROFORGE_WORKER_OLLAMA_CHAT_MODEL: ${OLLAMA_MODEL:-gemma4}
NEUROFORGE_WORKER_OLLAMA_EMBEDDING_MODEL: ${OLLAMA_EMBEDDING_MODEL:-embeddinggemma}
NEUROFORGE_WORKER_OLLAMA_NUM_CTX: ${NEUROFORGE_OLLAMA_NUM_CTX:-8192}
+1
View File
@@ -11,6 +11,7 @@ services:
BACKUP_DIR: /data/backups
STAGING_DIR: /data/staging
LISTEN_ADDR: :8080
OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
BRAIN_ACTIVITY_URL: ""
BRAIN_ACTIVITY_API_KEY: ""
ports:
+4
View File
@@ -47,6 +47,8 @@ services:
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
NEUROFORGE_OLLAMA_URL: ${OLLAMA_BASE_URL:?Set Ollama URL reachable from master,
normally GPU subagent host}
NEUROFORGE_OLLAMA_API_KEY: ${NEUROFORGE_OLLAMA_API_KEY:-}
OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
NEUROFORGE_OLLAMA_CHAT_MODEL: ${OLLAMA_MODEL:-gemma3}
NEUROFORGE_OLLAMA_EMBEDDING_MODEL: ${OLLAMA_EMBEDDING_MODEL:-embeddinggemma}
NEUROFORGE_OLLAMA_NUM_CTX: ${NEUROFORGE_OLLAMA_NUM_CTX:-8192}
@@ -317,6 +319,7 @@ services:
OLLAMA_THINK: ${OLLAMA_THINK:-}
OLLAMA_TIMEOUT: ${OLLAMA_TIMEOUT:-}
OLLAMA_URL: ${OLLAMA_BASE_URL:?Set Ollama URL reachable from master}
OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
OLLAMA_URLS: ${OLLAMA_URLS:?Set one or more Ollama URLs reachable from master}
OUTCOME_LEARNING_ENABLED: ${OUTCOME_LEARNING_ENABLED:-true}
OUTCOME_LEARNING_FAIL_OPEN: ${OUTCOME_LEARNING_FAIL_OPEN:-false}
@@ -389,6 +392,7 @@ services:
BASIC_AUTH_PASSWORD: ${BASIC_AUTH_PASSWORD:?Set the Knowledge web password}
AI_FALLBACK_ENABLED: ${AI_FALLBACK_ENABLED:-false}
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:?Set Ollama URL reachable from master}
OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
OLLAMA_MODEL: ${OLLAMA_MODEL:-gemma3}
OLLAMA_TIMEOUT: ${OLLAMA_TIMEOUT:-10m}
OLLAMA_MAX_CONCURRENT: ${OLLAMA_MAX_CONCURRENT:-2}
@@ -63,7 +63,7 @@ groups:
labels: {severity: warning}
annotations:
summary: "NeuroForge pending job payload memory is high"
description: "Pending durable job payload/result bytes exceed 100 MiB for 5 minutes; backfill should remain below the v1.6.2 safety budget."
description: "Pending durable job payload/result bytes exceed 100 MiB for 5 minutes; backfill should remain below the v1.6.1 safety budget."
- alert: NeuroForgeTerminalPayloadHigh
expr: neuroforge_job_payload_bytes{state="terminal"} > 67108864
@@ -71,4 +71,4 @@ groups:
labels: {severity: warning}
annotations:
summary: "NeuroForge terminal job payload retention is high"
description: "Terminal job payload/result bytes exceed 64 MiB. Completed vector.relink jobs should be compacted automatically in v1.6.2."
description: "Terminal job payload/result bytes exceed 64 MiB. Completed vector.relink jobs should be compacted automatically in v1.6.1."
+22 -23
View File
@@ -7,51 +7,50 @@ variable "REGISTRY" {
}
group "default" {
targets = [
"neuroforge",
"neuroforge-worker",
"agent",
"agent-data-init",
"knowledge",
"control",
]
targets = ["neuroforge", "neuroforge-worker", "agent", "agent-data-init", "knowledge", "control"]
}
target "neuroforge" {
context = "./platform/neuroforge"
context = "./platform/neuroforge"
dockerfile = "Dockerfile"
target = "server"
tags = ["${REGISTRY}/glpi-neuroforge-mega-neuroforge:${IMAGE_TAG}"]
target = "server"
tags = ["${REGISTRY}/glpi-neuroforge-mega-neuroforge:${IMAGE_TAG}"]
platforms = ["linux/amd64"]
}
target "neuroforge-worker" {
context = "./platform/neuroforge"
context = "./platform/neuroforge"
dockerfile = "Dockerfile"
target = "worker"
tags = ["${REGISTRY}/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG}"]
target = "worker"
tags = ["${REGISTRY}/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG}"]
platforms = ["linux/amd64"]
}
target "agent" {
context = "./services/agent"
context = "./services/agent"
dockerfile = "Dockerfile"
tags = ["${REGISTRY}/glpi-neuroforge-mega-agent:${IMAGE_TAG}"]
tags = ["${REGISTRY}/glpi-neuroforge-mega-agent:${IMAGE_TAG}"]
platforms = ["linux/amd64"]
}
target "agent-data-init" {
context = "./services/agent"
context = "./services/agent"
dockerfile = "Dockerfile"
target = "data-init"
tags = ["${REGISTRY}/glpi-neuroforge-mega-agent-data-init:${IMAGE_TAG}"]
target = "data-init"
tags = ["${REGISTRY}/glpi-neuroforge-mega-agent-data-init:${IMAGE_TAG}"]
platforms = ["linux/amd64"]
}
target "knowledge" {
context = "./services/knowledge"
context = "./services/knowledge"
dockerfile = "Dockerfile"
tags = ["${REGISTRY}/glpi-neuroforge-mega-knowledge:${IMAGE_TAG}"]
tags = ["${REGISTRY}/glpi-neuroforge-mega-knowledge:${IMAGE_TAG}"]
platforms = ["linux/amd64"]
}
target "control" {
context = "./services/control"
context = "./services/control"
dockerfile = "Dockerfile"
tags = ["${REGISTRY}/glpi-neuroforge-mega-control:${IMAGE_TAG}"]
tags = ["${REGISTRY}/glpi-neuroforge-mega-control:${IMAGE_TAG}"]
platforms = ["linux/amd64"]
}
+4 -2
View File
@@ -7,7 +7,7 @@ name: neuroforge-subagents
services:
cpu-subagent:
profiles: [cpu]
image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set IMAGE_TAG, e.g. 1.6.1}
image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set IMAGE_TAG, e.g. 1.6.2}
command:
- -server
- ${NEUROFORGE_MASTER_URL:?Set the reachable NeuroForge master URL}
@@ -29,7 +29,7 @@ services:
gpu-subagent:
profiles: [gpu]
image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set IMAGE_TAG, e.g. 1.6.1}
image: git.send.nrw/sendnrw/glpi-neuroforge-mega-neuroforge-worker:${IMAGE_TAG:?Set IMAGE_TAG, e.g. 1.6.2}
command:
- -server
- ${NEUROFORGE_MASTER_URL:?Set the reachable NeuroForge master URL}
@@ -43,6 +43,8 @@ services:
NEUROFORGE_WORKER_MAX_CONCURRENCY: ${NEUROFORGE_GPU_WORKER_CONCURRENCY:-1}
NEUROFORGE_WORKER_HEARTBEAT_INTERVAL: ${NEUROFORGE_WORKER_HEARTBEAT_INTERVAL:-15s}
NEUROFORGE_WORKER_OLLAMA_URL: ${NEUROFORGE_WORKER_OLLAMA_URL:?Set Ollama URL reachable from the GPU subagent}
NEUROFORGE_WORKER_OLLAMA_API_KEY: ${NEUROFORGE_WORKER_OLLAMA_API_KEY:-}
OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
NEUROFORGE_WORKER_OLLAMA_CHAT_MODEL: ${OLLAMA_MODEL:-gemma3}
NEUROFORGE_WORKER_OLLAMA_EMBEDDING_MODEL: ${OLLAMA_EMBEDDING_MODEL:-embeddinggemma}
NEUROFORGE_WORKER_OLLAMA_NUM_CTX: ${NEUROFORGE_OLLAMA_NUM_CTX:-8192}
+6
View File
@@ -50,6 +50,8 @@ services:
NEUROFORGE_CLUSTER_TOKEN: ${NEUROFORGE_CLUSTER_TOKEN:-}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
NEUROFORGE_OLLAMA_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
NEUROFORGE_OLLAMA_API_KEY: ${NEUROFORGE_OLLAMA_API_KEY:-}
OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
NEUROFORGE_OLLAMA_CHAT_MODEL: ${OLLAMA_MODEL:-gemma3}
NEUROFORGE_OLLAMA_EMBEDDING_MODEL: ${OLLAMA_EMBEDDING_MODEL:-embeddinggemma}
NEUROFORGE_OLLAMA_NUM_CTX: ${NEUROFORGE_OLLAMA_NUM_CTX:-8192}
@@ -182,6 +184,8 @@ services:
NEUROFORGE_WORKER_MAX_CONCURRENCY: ${NEUROFORGE_GPU_WORKER_CONCURRENCY:-1}
NEUROFORGE_WORKER_HEARTBEAT_INTERVAL: ${NEUROFORGE_WORKER_HEARTBEAT_INTERVAL:-15s}
NEUROFORGE_WORKER_OLLAMA_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
NEUROFORGE_WORKER_OLLAMA_API_KEY: ${NEUROFORGE_WORKER_OLLAMA_API_KEY:-}
OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
NEUROFORGE_WORKER_OLLAMA_CHAT_MODEL: ${OLLAMA_MODEL:-gemma3}
NEUROFORGE_WORKER_OLLAMA_EMBEDDING_MODEL: ${OLLAMA_EMBEDDING_MODEL:-embeddinggemma}
NEUROFORGE_WORKER_OLLAMA_NUM_CTX: ${NEUROFORGE_OLLAMA_NUM_CTX:-8192}
@@ -376,6 +380,7 @@ services:
OLLAMA_THINK: ${OLLAMA_THINK:-}
OLLAMA_TIMEOUT: ${OLLAMA_TIMEOUT:-}
OLLAMA_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
OLLAMA_URLS: ${OLLAMA_URLS:-http://ollama:11434}
OUTCOME_LEARNING_ENABLED: ${OUTCOME_LEARNING_ENABLED:-true}
OUTCOME_LEARNING_FAIL_OPEN: ${OUTCOME_LEARNING_FAIL_OPEN:-false}
@@ -449,6 +454,7 @@ services:
BASIC_AUTH_PASSWORD: ${BASIC_AUTH_PASSWORD:?Set the Knowledge web password}
AI_FALLBACK_ENABLED: ${AI_FALLBACK_ENABLED:-false}
OLLAMA_BASE_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
OLLAMA_MODEL: ${OLLAMA_MODEL:-gemma3}
OLLAMA_TIMEOUT: ${OLLAMA_TIMEOUT:-10m}
OLLAMA_MAX_CONCURRENT: ${OLLAMA_MAX_CONCURRENT:-2}
+20 -8
View File
@@ -1,12 +1,24 @@
# Migration v1.6.1 -> v1.6.2
v1.6.2 is primarily a packaging/deployment consolidation release. It retains the v1.6.1 NeuroForge recovery and persisted-state behavior.
No data-volume migration is required. Keep `neuroforge-data`, Agent data and Knowledge directories intact.
1. Back up the persistent data volumes/directories.
2. Set `IMAGE_TAG=1.6.2` in the selected deployment `.env`.
3. Keep existing `neuroforge-data`, Agent data and Knowledge files; do not delete volumes.
4. Replace deployment examples with the v1.6.2 variants, preserving real secrets locally.
5. Recreate project containers so changed environment/configuration is applied.
6. Check `/livez`, `/readyz`, Agent `/api/status`, worker status, graph convergence and Prometheus alerts.
1. Set `IMAGE_TAG=1.6.2`.
2. Copy the new optional Ollama variables into your `.env`:
The distributed roles now live alongside the standalone Agent/Knowledge/Ollama roles in the same canonical repository.
```env
OLLAMA_API_KEY=
NEUROFORGE_OLLAMA_API_KEY=
NEUROFORGE_WORKER_OLLAMA_API_KEY=
```
Leave them empty for an unauthenticated native Ollama endpoint.
3. If Ollama is protected by a Bearer-aware proxy, normally set only `OLLAMA_API_KEY`. Use the two NeuroForge overrides only when the Master and model worker need different credentials.
4. Pull/recreate services without deleting volumes:
```bash
docker compose --profile research pull
docker compose --profile research up -d --force-recreate --remove-orphans
```
5. For distributed deployments, use the complete `.env` shipped under `deployments/master`, `deployments/cpu-subagent`, and `deployments/gpu-subagent` and preserve the same `NEUROFORGE_WORKER_TOKEN` on Master and subagents.
The v1.6.1 OOM/recovery fixes are part of v1.6.2 and must not be removed when merging older deployment files.
+36
View File
@@ -0,0 +1,36 @@
# Ollama Bearer authentication
v1.6.2 can call Ollama-compatible endpoints protected by an HTTP Bearer token.
## Shared configuration
```env
OLLAMA_API_KEY=CHANGE_ME
```
Every supported Ollama request then carries:
```http
Authorization: Bearer CHANGE_ME
```
This applies to health/model discovery (`/api/tags`), chat (`/api/chat`) and embeddings (`/api/embed`).
## NeuroForge overrides
```env
NEUROFORGE_OLLAMA_API_KEY=
NEUROFORGE_WORKER_OLLAMA_API_KEY=
```
`NEUROFORGE_OLLAMA_API_KEY` overrides the shared key for the NeuroForge Master/provider. `NEUROFORGE_WORKER_OLLAMA_API_KEY` overrides it for model-capable NeuroForge workers. If the override is empty, workers fall back to `OLLAMA_API_KEY`; the Master also accepts `OLLAMA_API_KEY` as a shared alias when no NeuroForge-specific key is supplied.
## Agent pools
The GLPI Agent uses one `OLLAMA_API_KEY` for all URLs in `OLLAMA_URLS`. This is intentional for a pool behind one common authentication boundary. Deploy separate Agent instances or a common gateway if individual nodes require unrelated credentials.
## Native Ollama
The environment variable configures the **clients**, not the bundled native Ollama server. Native Ollama does not gain access control from this setting alone. To require authentication, place Ollama behind an authentication-capable reverse proxy/gateway and point `OLLAMA_URL`, `OLLAMA_URLS` or `OLLAMA_BASE_URL` at that endpoint.
Do not put credentials into Ollama URLs. Keep URLs credential-free and use the Bearer variable.
+3 -2
View File
@@ -20,7 +20,8 @@
"worker_to_neuroforge": "worker_token",
"research_to_kb_staging": "kb_integration_token",
"control_to_agent_graph": "control_read_token",
"control_to_neuroforge_graph": "app_api_key_read_only_endpoints"
"control_to_neuroforge_graph": "app_api_key_read_only_endpoints",
"ollama": "optional bearer token via OLLAMA_API_KEY; role-specific NeuroForge overrides supported"
},
"knowledge_export": {
"format": "Obsidian Markdown + YAML frontmatter + Wikilinks",
@@ -28,7 +29,7 @@
"schema": "Wiki/Schema.md",
"glpi_relations": "KnowbaseItem_Item when exposed by GLPI OpenAPI"
},
"version": "1.5.6",
"version": "1.6.2",
"controlled_learning": {
"raw_chat_auto_learning": false,
"validated_outcomes": [
+1
View File
@@ -21,3 +21,4 @@ b031ea42356a022d7bb03e7e2bda23c4ceddb66ccc59a5d1f0ebe787a790b829 v1.5.1-to-v1.5
24bb0da9bf77a0e8b692989b05c00b623ed9a558b9b5050c24e396e1da41d0f8 v1.5.8-to-v1.5.9.diff
e579d88ed2846cf5bc306cad45502af3edc61bd1c5bb5a2272979feff23dca62 v1.5.9-to-v1.6.0.diff
0cdce94c11a931e11c3c908f2fa3dfa733c5c602ac00f7f89e41b8c301574766 v1.6.0-to-v1.6.1.diff
0b09d93f115608f4ab6ee47e0a085f254eaeb8914fe1415fe73dccea69e5d166 v1.6.1-to-v1.6.2.diff
File diff suppressed because it is too large Load Diff
+7
View File
@@ -1,4 +1,8 @@
# Generate strong random values, e.g. openssl rand -hex 32
# Optional: Bearer token sent as Authorization: Bearer <token> to Ollama-compatible endpoints.
OLLAMA_API_KEY=
# Optional NeuroForge GPU-worker override; falls back to OLLAMA_API_KEY.
NEUROFORGE_WORKER_OLLAMA_API_KEY=
NEUROFORGE_ADMIN_TOKEN=replace-with-random-admin-token
NEUROFORGE_APP_API_KEY=replace-with-random-app-key
NEUROFORGE_WORKER_TOKEN=replace-with-random-worker-token
@@ -7,3 +11,6 @@ NEUROFORGE_CLUSTER_TOKEN=replace-with-shared-random-cluster-token
# Optional. You can also set this through the web interface.
OPENAI_API_KEY=
# Optional NeuroForge-specific override; takes precedence over OLLAMA_API_KEY.
NEUROFORGE_OLLAMA_API_KEY=
+9
View File
@@ -234,6 +234,7 @@ func run() (retErr error) {
changed := false
for name, dst := range map[string]*string{
"OPENAI_API_KEY": &sec.OpenAIAPIKey,
"NEUROFORGE_OLLAMA_API_KEY": &sec.OllamaAPIKey,
"NEUROFORGE_ADMIN_TOKEN": &sec.AdminToken,
"NEUROFORGE_APP_API_KEY": &sec.AppAPIKey,
"NEUROFORGE_INTEGRATION_TOKEN": &sec.IntegrationToken,
@@ -247,6 +248,14 @@ func run() (retErr error) {
changed = true
}
}
// OLLAMA_API_KEY is a convenient shared alias. The NeuroForge-specific
// variable wins when both are set.
if strings.TrimSpace(os.Getenv("NEUROFORGE_OLLAMA_API_KEY")) == "" {
if v := strings.TrimSpace(os.Getenv("OLLAMA_API_KEY")); v != "" {
sec.OllamaAPIKey = v
changed = true
}
}
if changed {
if err := s.UpdateSecrets(sec); err != nil {
return err
+8 -3
View File
@@ -86,6 +86,7 @@ type workerConfig struct {
MaxConcurrency int
Hostname string
OllamaURL string
OllamaAPIKey string
OllamaChatModel string
OllamaEmbedModel string
OllamaNumCtx int
@@ -145,6 +146,7 @@ func main() {
Heartbeat: heartbeat, ResourceClass: resource, Capabilities: caps,
MaxConcurrency: maxConcurrency, Hostname: hostname(),
OllamaURL: strings.TrimRight(firstNonEmpty(os.Getenv("NEUROFORGE_WORKER_OLLAMA_URL"), os.Getenv("OLLAMA_BASE_URL"), os.Getenv("OLLAMA_URL")), "/"),
OllamaAPIKey: firstNonEmpty(os.Getenv("NEUROFORGE_WORKER_OLLAMA_API_KEY"), os.Getenv("OLLAMA_API_KEY")),
OllamaChatModel: firstNonEmpty(os.Getenv("NEUROFORGE_WORKER_OLLAMA_CHAT_MODEL"), os.Getenv("OLLAMA_MODEL")),
OllamaEmbedModel: firstNonEmpty(os.Getenv("NEUROFORGE_WORKER_OLLAMA_EMBEDDING_MODEL"), os.Getenv("OLLAMA_EMBEDDING_MODEL")),
OllamaNumCtx: envInt("NEUROFORGE_WORKER_OLLAMA_NUM_CTX", 8192),
@@ -342,7 +344,7 @@ func ollamaEmbed(ctx context.Context, c *http.Client, cfg workerConfig, p modelE
Embeddings [][]float32 `json:"embeddings"`
PromptEvalCount int64 `json:"prompt_eval_count"`
}
if err := postOllama(ctx, c, cfg.OllamaURL+"/api/embed", body, &resp); err != nil {
if err := postOllama(ctx, c, cfg.OllamaURL+"/api/embed", cfg.OllamaAPIKey, body, &resp); err != nil {
return modelResult{}, err
}
if len(resp.Embeddings) == 0 || len(resp.Embeddings[0]) == 0 {
@@ -385,19 +387,22 @@ func ollamaChat(ctx context.Context, c *http.Client, cfg workerConfig, p modelCh
PromptEvalCount int64 `json:"prompt_eval_count"`
EvalCount int64 `json:"eval_count"`
}
if err := postOllama(ctx, c, cfg.OllamaURL+"/api/chat", body, &resp); err != nil {
if err := postOllama(ctx, c, cfg.OllamaURL+"/api/chat", cfg.OllamaAPIKey, body, &resp); err != nil {
return modelResult{}, err
}
return modelResult{Text: strings.TrimSpace(resp.Message.Content), Usage: modelUsage{InputTokens: resp.PromptEvalCount, OutputTokens: resp.EvalCount}, Provider: "ollama", Model: model, NodeID: cfg.ID}, nil
}
func postOllama(ctx context.Context, c *http.Client, url string, body any, out any) error {
func postOllama(ctx context.Context, c *http.Client, url, apiKey string, body any, out any) error {
raw, _ := json.Marshal(body)
req, err := http.NewRequestWithContext(ctx, http.MethodPost, url, bytes.NewReader(raw))
if err != nil {
return err
}
req.Header.Set("Content-Type", "application/json")
if token := strings.TrimSpace(apiKey); token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := c.Do(req)
if err != nil {
return err
@@ -0,0 +1,26 @@
package main
import (
"context"
"encoding/json"
"net/http"
"net/http/httptest"
"testing"
)
func TestPostOllamaSendsBearerToken(t *testing.T) {
const token = "ollama-secret"
var auth string
srv := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth = r.Header.Get("Authorization")
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true})
}))
defer srv.Close()
var out map[string]any
if err := postOllama(context.Background(), srv.Client(), srv.URL, token, map[string]any{"x": 1}, &out); err != nil {
t.Fatal(err)
}
if auth != "Bearer "+token {
t.Fatalf("authorization=%q", auth)
}
}
+4
View File
@@ -12,6 +12,8 @@ services:
NEUROFORGE_METRICS_TOKEN: ${NEUROFORGE_METRICS_TOKEN}
NEUROFORGE_CLUSTER_TOKEN: ${NEUROFORGE_CLUSTER_TOKEN:-}
OPENAI_API_KEY: ${OPENAI_API_KEY:-}
NEUROFORGE_OLLAMA_API_KEY: ${NEUROFORGE_OLLAMA_API_KEY:-}
OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
volumes:
- neuroforge-data:/app/data
restart: unless-stopped
@@ -59,6 +61,8 @@ services:
NEUROFORGE_WORKER_CAPABILITIES: gpu,model.chat,model.embed
NEUROFORGE_WORKER_MAX_CONCURRENCY: ${NEUROFORGE_GPU_WORKER_CONCURRENCY:-1}
NEUROFORGE_WORKER_OLLAMA_URL: ${OLLAMA_BASE_URL:-http://ollama:11434}
NEUROFORGE_WORKER_OLLAMA_API_KEY: ${NEUROFORGE_WORKER_OLLAMA_API_KEY:-}
OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
NEUROFORGE_WORKER_OLLAMA_CHAT_MODEL: ${OLLAMA_MODEL:-gemma3}
NEUROFORGE_WORKER_OLLAMA_EMBEDDING_MODEL: ${OLLAMA_EMBEDDING_MODEL:-embeddinggemma}
depends_on:
@@ -419,6 +419,7 @@ type Config struct {
type Secrets struct {
OpenAIAPIKey string `json:"openai_api_key"`
OllamaAPIKey string `json:"ollama_api_key,omitempty"`
AppAPIKey string `json:"app_api_key"`
IntegrationToken string `json:"integration_token,omitempty"`
ControlReadToken string `json:"control_read_token,omitempty"`
@@ -693,7 +693,7 @@ func (s *Server) adminPutModelRouting(w http.ResponseWriter, r *http.Request) {
func (s *Server) adminSecretsStatus(w http.ResponseWriter, r *http.Request) {
sec := s.store.Secrets()
s.json(w, 200, map[string]any{"openai_configured": sec.OpenAIAPIKey != "", "app_key_configured": sec.AppAPIKey != "", "integration_token_configured": sec.IntegrationToken != "", "control_read_token_configured": sec.ControlReadToken != "", "worker_token_configured": sec.WorkerToken != "", "metrics_token_configured": sec.MetricsToken != "", "shard_tokens": len(sec.ShardAPIToken), "cluster_token_configured": sec.ClusterToken != ""})
s.json(w, 200, map[string]any{"openai_configured": sec.OpenAIAPIKey != "", "ollama_api_key_configured": sec.OllamaAPIKey != "", "app_key_configured": sec.AppAPIKey != "", "integration_token_configured": sec.IntegrationToken != "", "control_read_token_configured": sec.ControlReadToken != "", "worker_token_configured": sec.WorkerToken != "", "metrics_token_configured": sec.MetricsToken != "", "shard_tokens": len(sec.ShardAPIToken), "cluster_token_configured": sec.ClusterToken != ""})
}
func maskedSecret(v string) string {
if v == "" {
@@ -708,18 +708,19 @@ func (s *Server) adminGetSecrets(w http.ResponseWriter, r *http.Request) {
sec := s.store.Secrets()
reveal := r.URL.Query().Get("reveal") == "1" && s.store.Config().Security.AllowSecretReveal
if reveal {
s.json(w, 200, map[string]any{"revealed": true, "app_api_key": sec.AppAPIKey, "integration_token": sec.IntegrationToken, "control_read_token": sec.ControlReadToken, "worker_token": sec.WorkerToken, "metrics_token": sec.MetricsToken, "shard_api_tokens": sec.ShardAPIToken, "cluster_token": sec.ClusterToken})
s.json(w, 200, map[string]any{"revealed": true, "ollama_api_key": sec.OllamaAPIKey, "app_api_key": sec.AppAPIKey, "integration_token": sec.IntegrationToken, "control_read_token": sec.ControlReadToken, "worker_token": sec.WorkerToken, "metrics_token": sec.MetricsToken, "shard_api_tokens": sec.ShardAPIToken, "cluster_token": sec.ClusterToken})
return
}
maskedShards := map[string]string{}
for k, v := range sec.ShardAPIToken {
maskedShards[k] = maskedSecret(v)
}
s.json(w, 200, map[string]any{"revealed": false, "reveal_allowed": s.store.Config().Security.AllowSecretReveal, "app_api_key": maskedSecret(sec.AppAPIKey), "integration_token": maskedSecret(sec.IntegrationToken), "control_read_token": maskedSecret(sec.ControlReadToken), "worker_token": maskedSecret(sec.WorkerToken), "metrics_token": maskedSecret(sec.MetricsToken), "shard_api_tokens": maskedShards, "cluster_token": maskedSecret(sec.ClusterToken)})
s.json(w, 200, map[string]any{"revealed": false, "reveal_allowed": s.store.Config().Security.AllowSecretReveal, "ollama_api_key": maskedSecret(sec.OllamaAPIKey), "app_api_key": maskedSecret(sec.AppAPIKey), "integration_token": maskedSecret(sec.IntegrationToken), "control_read_token": maskedSecret(sec.ControlReadToken), "worker_token": maskedSecret(sec.WorkerToken), "metrics_token": maskedSecret(sec.MetricsToken), "shard_api_tokens": maskedShards, "cluster_token": maskedSecret(sec.ClusterToken)})
}
func (s *Server) adminPutSecrets(w http.ResponseWriter, r *http.Request) {
var q struct {
OpenAIAPIKey string `json:"openai_api_key,omitempty"`
OllamaAPIKey string `json:"ollama_api_key,omitempty"`
AppAPIKey string `json:"app_api_key,omitempty"`
IntegrationToken string `json:"integration_token,omitempty"`
ControlReadToken string `json:"control_read_token,omitempty"`
@@ -737,8 +738,13 @@ func (s *Server) adminPutSecrets(w http.ResponseWriter, r *http.Request) {
_, ok := os.LookupEnv(name)
return ok && strings.TrimSpace(os.Getenv(name)) != ""
}
if q.OllamaAPIKey != "" && (envLocked("NEUROFORGE_OLLAMA_API_KEY") || envLocked("OLLAMA_API_KEY")) {
s.err(w, http.StatusConflict, fmt.Errorf("Ollama API key is environment-managed and cannot be changed through the admin API"))
return
}
for name, value := range map[string]string{
"OPENAI_API_KEY": q.OpenAIAPIKey,
"NEUROFORGE_OLLAMA_API_KEY": q.OllamaAPIKey,
"NEUROFORGE_APP_API_KEY": q.AppAPIKey,
"NEUROFORGE_INTEGRATION_TOKEN": q.IntegrationToken,
"NEUROFORGE_CONTROL_READ_TOKEN": q.ControlReadToken,
@@ -754,6 +760,9 @@ func (s *Server) adminPutSecrets(w http.ResponseWriter, r *http.Request) {
if q.OpenAIAPIKey != "" {
sec.OpenAIAPIKey = q.OpenAIAPIKey
}
if q.OllamaAPIKey != "" {
sec.OllamaAPIKey = q.OllamaAPIKey
}
if q.AppAPIKey != "" {
sec.AppAPIKey = q.AppAPIKey
}
@@ -899,7 +908,7 @@ func configuredModelAvailable(models map[string]bool, configured string) bool {
return false
}
func checkConfiguredOllamaModels(ctx context.Context, cfg core.Config) (bool, any) {
func checkConfiguredOllamaModels(ctx context.Context, cfg core.Config, apiKey string) (bool, any) {
type tagsResponse struct {
Models []struct {
Name string `json:"name"`
@@ -917,6 +926,9 @@ func checkConfiguredOllamaModels(ctx context.Context, cfg core.Config) (bool, an
details[node.ID] = err.Error()
continue
}
if token := strings.TrimSpace(apiKey); token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := http.DefaultClient.Do(req)
if err != nil {
details[node.ID] = err.Error()
@@ -977,7 +989,7 @@ func (s *Server) readyz(w http.ResponseWriter, r *http.Request) {
if s.readinessOllamaLive {
ctx, cancel := context.WithTimeout(r.Context(), 4*time.Second)
var detail any
ollamaLiveReady, detail = checkConfiguredOllamaModels(ctx, cfg)
ollamaLiveReady, detail = checkConfiguredOllamaModels(ctx, cfg, s.store.Secrets().OllamaAPIKey)
cancel()
components["ollama_live_models"] = detail
}
@@ -307,7 +307,7 @@ func (r *Router) chatOllama(ctx context.Context, o core.OllamaServer, model, ins
}
requestCtx, cancel := optionalTimeout(ctx, o.RequestTimeoutSeconds)
defer cancel()
if err := r.doJSON(requestCtx, "POST", cleanBase(o.BaseURL)+"/api/chat", "", body, &out); err != nil {
if err := r.doJSON(requestCtx, "POST", cleanBase(o.BaseURL)+"/api/chat", r.store.Secrets().OllamaAPIKey, body, &out); err != nil {
return ChatResult{}, fmt.Errorf("ollama %s: %w", o.Name, err)
}
if strings.TrimSpace(out.Message.Content) == "" {
@@ -327,7 +327,7 @@ func (r *Router) embedOllama(ctx context.Context, o core.OllamaServer, model, te
}
requestCtx, cancel := optionalTimeout(ctx, o.RequestTimeoutSeconds)
defer cancel()
if err := r.doJSON(requestCtx, "POST", cleanBase(o.BaseURL)+"/api/embed", "", body, &out); err != nil {
if err := r.doJSON(requestCtx, "POST", cleanBase(o.BaseURL)+"/api/embed", r.store.Secrets().OllamaAPIKey, body, &out); err != nil {
return EmbedResult{}, fmt.Errorf("ollama %s: %w", o.Name, err)
}
if len(out.Embeddings) == 0 || len(out.Embeddings[0]) == 0 {
@@ -459,6 +459,9 @@ func (r *Router) Health(ctx context.Context) []map[string]any {
}
healthCtx, cancel := context.WithTimeout(ctx, 5*time.Second)
req, _ := http.NewRequestWithContext(healthCtx, "GET", cleanBase(o.BaseURL)+"/api/tags", nil)
if token := strings.TrimSpace(r.store.Secrets().OllamaAPIKey); token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := r.http.Do(req)
if err != nil {
cancel()
@@ -111,3 +111,48 @@ func TestOllamaExplicitRequestTimeoutStillWorks(t *testing.T) {
t.Fatalf("configured timeout was not enforced promptly: %v", time.Since(start))
}
}
func TestOllamaBearerTokenIsSentToChatAndHealth(t *testing.T) {
const token = "ollama-secret"
var chatAuth, tagsAuth string
fake := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
switch r.URL.Path {
case "/api/chat":
chatAuth = r.Header.Get("Authorization")
_ = json.NewEncoder(w).Encode(map[string]any{"message": map[string]any{"content": "ok"}})
case "/api/tags":
tagsAuth = r.Header.Get("Authorization")
_ = json.NewEncoder(w).Encode(map[string]any{"models": []map[string]any{{"name": "chat"}, {"name": "embed"}}})
default:
http.NotFound(w, r)
}
}))
defer fake.Close()
s, err := store.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
defer s.Close()
cfg := s.Config()
cfg.Ollama = []core.OllamaServer{{ID: "secure", Name: "secure", BaseURL: fake.URL, ChatModel: "chat", EmbeddingModel: "embed", Weight: 1, Enabled: true}}
cfg.Routing.ChatProvider = "ollama"
if err := s.UpdateConfig(cfg); err != nil {
t.Fatal(err)
}
sec := s.Secrets()
sec.OllamaAPIKey = token
if err := s.UpdateSecrets(sec); err != nil {
t.Fatal(err)
}
r := NewRouter(s)
if _, err := r.Chat(context.Background(), "ollama", "chat", "", "hello", 32); err != nil {
t.Fatal(err)
}
_ = r.Health(context.Background())
if chatAuth != "Bearer "+token {
t.Fatalf("chat authorization=%q", chatAuth)
}
if tagsAuth != "Bearer "+token {
t.Fatalf("tags authorization=%q", tagsAuth)
}
}
+19 -28
View File
@@ -23,14 +23,29 @@ fi
# Private keys and common live-token shapes must not be committed. Placeholders in
# templates/docs are intentionally allowed.
# Distributed deployment bundles intentionally contain three complete `.env`
# templates. They must remain placeholder-only; every other private env/key file
# is still forbidden.
if grep -E '(^|/)\.env$|\.pem$|\.p12$|\.pfx$|(^|/)id_rsa$|(^|/)id_ed25519$' "$FILES" \
| grep -Ev '^deployments/(master|cpu-subagent|gpu-subagent|agent|knowledge|ollama|combined)/\.env$' >/dev/null; then
| grep -Ev '^deployments/[^/]+/\.env$' >/dev/null; then
bad "private environment/key material found"
fi
# Deployment role .env files are intentionally shipped as complete templates.
# Reject them if a sensitive field is populated with anything other than an
# empty value or an explicit CHANGE_ME placeholder.
for f in deployments/*/.env; do
[ -f "$f" ] || continue
awk -F= '
/^[[:space:]]*#/ || NF < 2 { next }
$1 ~ /(PASSWORD|SECRET|TOKEN|API_KEY|CLIENT_SECRET)$/ {
v=$0; sub(/^[^=]*=/,"",v)
if (v != "" && v !~ /^CHANGE_ME/) {
print FILENAME ": unsafe populated secret field " $1 > "/dev/stderr"
bad=1
}
}
END { exit bad ? 1 : 0 }
' "$f" || bad "deployment .env contains a populated secret: $f"
done
if [ "$HAVE_GIT" = true ]; then
if git grep -nE -- '-----BEGIN (RSA |EC |OPENSSH |DSA )?PRIVATE KEY-----' -- ':!*.example' ':!*.md' >"$KEYS" 2>/dev/null; then
cat "$KEYS" >&2; bad "private key material found"
@@ -51,30 +66,6 @@ else
fi
fi
# Checked-in deployment .env files are templates, never live configuration.
for envf in \
deployments/master/.env \
deployments/cpu-subagent/.env \
deployments/gpu-subagent/.env \
deployments/agent/.env \
deployments/knowledge/.env \
deployments/ollama/.env \
deployments/combined/.env; do
[ -f "$envf" ] || continue
if awk -F= '
/^[[:space:]]*#/ || NF < 2 { next }
{
key=$1; sub(/^[[:space:]]+/, "", key); sub(/[[:space:]]+$/, "", key)
val=$0; sub(/^[^=]*=/, "", val)
if (key ~ /(TOKEN|PASSWORD|SECRET|CLIENT_ID|CLIENT_SECRET|API_KEY)$/ && val != "" && val !~ /^CHANGE_ME/) {
print FILENAME ":" NR ": live-looking secret in " key > "/dev/stderr";
bad=1
}
}
END { exit bad ? 1 : 0 }
' "$envf"; then :; else bad "deployment template contains a non-placeholder secret: $envf"; fi
done
# Reject accidental binary blobs outside explicitly expected assets.
while IFS= read -r f; do
[ -f "$f" ] || continue
+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)
}
}
+148 -104
View File
@@ -2464,7 +2464,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": true,
"line": 1063,
"line": 1065,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2476,7 +2476,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": true,
"line": 440,
"line": 442,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2488,7 +2488,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": true,
"line": 224,
"line": 225,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2500,7 +2500,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
"line": 1037,
"line": 1039,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2512,7 +2512,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
"line": 1208,
"line": 1210,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2524,7 +2524,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
"line": 1252,
"line": 1254,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2536,7 +2536,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
"line": 1241,
"line": 1243,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2548,7 +2548,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
"line": 1219,
"line": 1221,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2560,7 +2560,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
"line": 1230,
"line": 1232,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2572,7 +2572,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
"line": 1086,
"line": 1088,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2584,7 +2584,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
"line": 1108,
"line": 1110,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2596,7 +2596,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
"line": 1191,
"line": 1193,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2608,7 +2608,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
"line": 1052,
"line": 1054,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2620,7 +2620,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
"line": 1183,
"line": 1185,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2632,7 +2632,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
"line": 1130,
"line": 1132,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2644,7 +2644,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
"line": 1156,
"line": 1158,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2656,7 +2656,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
"line": 1046,
"line": 1048,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2668,7 +2668,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
"line": 1082,
"line": 1084,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2680,7 +2680,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
"line": 1023,
"line": 1025,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2692,7 +2692,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
"line": 1018,
"line": 1020,
"path": "services/agent/internal/config/config.go"
}
},
@@ -2704,7 +2704,7 @@
"community": "github.com/example/glpi-ai-agent/internal/config",
"meta": {
"exported": false,
"line": 993,
"line": 995,
"path": "services/agent/internal/config/config.go"
}
},
@@ -5716,7 +5716,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": true,
"line": 272,
"line": 273,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5728,7 +5728,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": true,
"line": 262,
"line": 263,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5740,7 +5740,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": true,
"line": 243,
"line": 244,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5752,7 +5752,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 667,
"line": 677,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5764,7 +5764,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 388,
"line": 389,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5776,7 +5776,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 549,
"line": 551,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5788,7 +5788,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 596,
"line": 606,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5800,7 +5800,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 453,
"line": 455,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5812,7 +5812,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 280,
"line": 281,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5824,7 +5824,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 577,
"line": 587,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5836,7 +5836,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 680,
"line": 690,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5848,7 +5848,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": true,
"line": 774,
"line": 784,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5860,7 +5860,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": true,
"line": 731,
"line": 741,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5872,7 +5872,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 369,
"line": 370,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5896,7 +5896,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 709,
"line": 719,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5908,7 +5908,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 693,
"line": 703,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5920,7 +5920,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 763,
"line": 773,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5932,7 +5932,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 715,
"line": 725,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5944,7 +5944,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 438,
"line": 440,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5956,7 +5956,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 161,
"line": 162,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -5992,7 +5992,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 703,
"line": 713,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -6004,7 +6004,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 78,
"line": 79,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -6016,7 +6016,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 540,
"line": 542,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -6028,7 +6028,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 92,
"line": 93,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -6040,7 +6040,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 104,
"line": 105,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -6052,7 +6052,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 87,
"line": 88,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -6064,7 +6064,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 135,
"line": 136,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -6076,7 +6076,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 745,
"line": 755,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -6088,7 +6088,19 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 740,
"line": 750,
"path": "services/agent/internal/ollama/pool.go"
}
},
{
"id": "function:github.com/example/glpi-ai-agent/internal/ollama:setBearer",
"kind": "function",
"label": "setBearer",
"group": "engineering",
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 580,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -6112,7 +6124,7 @@
"community": "github.com/example/glpi-ai-agent/internal/ollama",
"meta": {
"exported": false,
"line": 736,
"line": 746,
"path": "services/agent/internal/ollama/pool.go"
}
},
@@ -7756,7 +7768,7 @@
"community": "kb-editor/cmd/server",
"meta": {
"exported": false,
"line": 292,
"line": 293,
"path": "services/knowledge/cmd/server/main.go"
}
},
@@ -7768,7 +7780,7 @@
"community": "kb-editor/cmd/server",
"meta": {
"exported": false,
"line": 304,
"line": 305,
"path": "services/knowledge/cmd/server/main.go"
}
},
@@ -7804,7 +7816,7 @@
"community": "kb-editor/cmd/server",
"meta": {
"exported": false,
"line": 345,
"line": 346,
"path": "services/knowledge/cmd/server/main.go"
}
},
@@ -7828,7 +7840,7 @@
"community": "kb-editor/cmd/server",
"meta": {
"exported": false,
"line": 311,
"line": 312,
"path": "services/knowledge/cmd/server/main.go"
}
},
@@ -7840,7 +7852,7 @@
"community": "kb-editor/cmd/server",
"meta": {
"exported": false,
"line": 284,
"line": 285,
"path": "services/knowledge/cmd/server/main.go"
}
},
@@ -7864,7 +7876,7 @@
"community": "kb-editor/cmd/server",
"meta": {
"exported": false,
"line": 337,
"line": 338,
"path": "services/knowledge/cmd/server/main.go"
}
},
@@ -7948,7 +7960,7 @@
"community": "kb-editor/internal/aifallback",
"meta": {
"exported": true,
"line": 40,
"line": 41,
"path": "services/knowledge/internal/aifallback/ollama.go"
}
},
@@ -7960,7 +7972,7 @@
"community": "kb-editor/internal/aifallback",
"meta": {
"exported": true,
"line": 76,
"line": 77,
"path": "services/knowledge/internal/aifallback/ollama.go"
}
},
@@ -7972,7 +7984,7 @@
"community": "kb-editor/internal/aifallback",
"meta": {
"exported": true,
"line": 106,
"line": 107,
"path": "services/knowledge/internal/aifallback/ollama.go"
}
},
@@ -7984,7 +7996,7 @@
"community": "kb-editor/internal/aifallback",
"meta": {
"exported": true,
"line": 73,
"line": 74,
"path": "services/knowledge/internal/aifallback/ollama.go"
}
},
@@ -7996,7 +8008,7 @@
"community": "kb-editor/internal/aifallback",
"meta": {
"exported": true,
"line": 74,
"line": 75,
"path": "services/knowledge/internal/aifallback/ollama.go"
}
},
@@ -8008,7 +8020,7 @@
"community": "kb-editor/internal/aifallback",
"meta": {
"exported": true,
"line": 72,
"line": 73,
"path": "services/knowledge/internal/aifallback/ollama.go"
}
},
@@ -8020,7 +8032,7 @@
"community": "kb-editor/internal/aifallback",
"meta": {
"exported": false,
"line": 110,
"line": 111,
"path": "services/knowledge/internal/aifallback/ollama.go"
}
},
@@ -9832,7 +9844,7 @@
"community": "neuroforge/cmd/server",
"meta": {
"exported": false,
"line": 623,
"line": 632,
"path": "platform/neuroforge/cmd/server/main.go"
}
},
@@ -9916,7 +9928,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 255,
"line": 257,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -9928,7 +9940,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 416,
"line": 421,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -9940,7 +9952,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 454,
"line": 459,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -9952,7 +9964,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 507,
"line": 512,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -9964,7 +9976,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 495,
"line": 500,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -9976,7 +9988,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 486,
"line": 491,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -9988,7 +10000,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 468,
"line": 473,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10000,7 +10012,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 239,
"line": 241,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10012,7 +10024,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 446,
"line": 451,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10024,7 +10036,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 100,
"line": 101,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10036,7 +10048,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 105,
"line": 106,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10048,7 +10060,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 110,
"line": 111,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10060,7 +10072,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 478,
"line": 483,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10072,7 +10084,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 120,
"line": 121,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10084,7 +10096,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 354,
"line": 356,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10096,7 +10108,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 332,
"line": 334,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10108,7 +10120,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 189,
"line": 191,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10120,7 +10132,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 421,
"line": 426,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10132,7 +10144,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 394,
"line": 396,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10144,7 +10156,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 235,
"line": 237,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10156,7 +10168,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 279,
"line": 281,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10168,7 +10180,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 178,
"line": 180,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -10180,7 +10192,7 @@
"community": "neuroforge/cmd/worker",
"meta": {
"exported": false,
"line": 227,
"line": 229,
"path": "platform/neuroforge/cmd/worker/main.go"
}
},
@@ -12124,7 +12136,7 @@
"community": "neuroforge/internal/core",
"meta": {
"exported": true,
"line": 741,
"line": 742,
"path": "platform/neuroforge/internal/core/types.go"
}
},
@@ -12340,7 +12352,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
"line": 827,
"line": 836,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -12352,7 +12364,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
"line": 801,
"line": 810,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -12388,7 +12400,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
"line": 823,
"line": 832,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -12544,7 +12556,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
"line": 790,
"line": 799,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -12616,7 +12628,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
"line": 785,
"line": 794,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -12784,7 +12796,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
"line": 813,
"line": 822,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -12808,7 +12820,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
"line": 816,
"line": 825,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -13348,7 +13360,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
"line": 884,
"line": 893,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -13384,7 +13396,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
"line": 949,
"line": 961,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -13396,7 +13408,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
"line": 859,
"line": 868,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -13456,7 +13468,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
"line": 837,
"line": 846,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -13600,7 +13612,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
"line": 902,
"line": 911,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -13612,7 +13624,7 @@
"community": "neuroforge/internal/httpapi",
"meta": {
"exported": false,
"line": 888,
"line": 897,
"path": "platform/neuroforge/internal/httpapi/httpapi.go"
}
},
@@ -32967,6 +32979,12 @@
"to": "function:github.com/example/glpi-ai-agent/internal/ollama:requestStage",
"kind": "defines"
},
{
"id": "file:services/agent/internal/ollama/pool.go-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:setBearer:defines",
"from": "file:services/agent/internal/ollama/pool.go",
"to": "function:github.com/example/glpi-ai-agent/internal/ollama:setBearer",
"kind": "defines"
},
{
"id": "file:services/agent/internal/ollama/pool.go-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:withStage:defines",
"from": "file:services/agent/internal/ollama/pool.go",
@@ -43734,6 +43752,12 @@
"to": "function:github.com/example/glpi-ai-agent/internal/ollama:modelNameMatches",
"kind": "calls"
},
{
"id": "function:github.com/example/glpi-ai-agent/internal/ollama:Pool.checkNode-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:setBearer:calls",
"from": "function:github.com/example/glpi-ai-agent/internal/ollama:Pool.checkNode",
"to": "function:github.com/example/glpi-ai-agent/internal/ollama:setBearer",
"kind": "calls"
},
{
"id": "function:github.com/example/glpi-ai-agent/internal/ollama:Pool.checkNode-\u003epackage:encoding/json:calls_package",
"from": "function:github.com/example/glpi-ai-agent/internal/ollama:Pool.checkNode",
@@ -43769,6 +43793,12 @@
"kind": "calls_package",
"label": "TrimSpace"
},
{
"id": "function:github.com/example/glpi-ai-agent/internal/ollama:Pool.doPost-\u003efunction:github.com/example/glpi-ai-agent/internal/ollama:setBearer:calls",
"from": "function:github.com/example/glpi-ai-agent/internal/ollama:Pool.doPost",
"to": "function:github.com/example/glpi-ai-agent/internal/ollama:setBearer",
"kind": "calls"
},
{
"id": "function:github.com/example/glpi-ai-agent/internal/ollama:Pool.doPost-\u003epackage:bytes:calls_package",
"from": "function:github.com/example/glpi-ai-agent/internal/ollama:Pool.doPost",
@@ -44018,6 +44048,13 @@
"kind": "calls_package",
"label": "Now"
},
{
"id": "function:github.com/example/glpi-ai-agent/internal/ollama:setBearer-\u003epackage:strings:calls_package",
"from": "function:github.com/example/glpi-ai-agent/internal/ollama:setBearer",
"to": "package:strings",
"kind": "calls_package",
"label": "TrimSpace"
},
{
"id": "function:github.com/example/glpi-ai-agent/internal/ollama:uniqueStrings-\u003epackage:strings:calls_package",
"from": "function:github.com/example/glpi-ai-agent/internal/ollama:uniqueStrings",
@@ -56620,6 +56657,13 @@
"kind": "calls_package",
"label": "NewRequestWithContext"
},
{
"id": "function:neuroforge/internal/provider:Router.Health-\u003epackage:strings:calls_package",
"from": "function:neuroforge/internal/provider:Router.Health",
"to": "package:strings",
"kind": "calls_package",
"label": "TrimSpace"
},
{
"id": "function:neuroforge/internal/provider:Router.chatOllama-\u003efunction:neuroforge/internal/provider:Router.doJSON:calls",
"from": "function:neuroforge/internal/provider:Router.chatOllama",
@@ -66863,10 +66907,10 @@
}
],
"meta": {
"edges": 7273,
"edges": 7278,
"format_version": 1,
"generator": "go-ast+compose",
"modules": 4,
"nodes": 1845
"nodes": 1846
}
}
+2
View File
@@ -1,4 +1,6 @@
# Betriebsmodus desselben Images:
# Optional: Bearer token sent as Authorization: Bearer <token> to Ollama-compatible endpoints.
OLLAMA_API_KEY=
# editor = vollständiger Einzel-/Masseneditor mit Schreibzugriff
# google = schreibgeschützte Helpdesk-Suchmaschine mit Artikel-Viewer
APP_MODE=editor
+1
View File
@@ -269,6 +269,7 @@ func aiServiceFromEnv(mode string, st *staging.Store) (*aifallback.Service, time
}
svc, err := aifallback.New(aifallback.Config{
BaseURL: envOr("OLLAMA_BASE_URL", "http://ollama:11434"),
APIKey: strings.TrimSpace(os.Getenv("OLLAMA_API_KEY")),
Model: strings.TrimSpace(os.Getenv("OLLAMA_MODEL")),
Timeout: timeout,
MaxConcurrent: maxConcurrent,
@@ -49,6 +49,7 @@ services:
BASIC_AUTH_PASSWORD: "${SEARCH_AUTH_PASSWORD:-}"
AI_FALLBACK_ENABLED: "${AI_FALLBACK_ENABLED:-false}"
OLLAMA_BASE_URL: "${OLLAMA_BASE_URL:-http://ollama:11434}"
OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
OLLAMA_MODEL: "${OLLAMA_MODEL:-}"
OLLAMA_TIMEOUT: "${OLLAMA_TIMEOUT:-10m}"
OLLAMA_MAX_CONCURRENT: "${OLLAMA_MAX_CONCURRENT:-1}"
+1
View File
@@ -18,6 +18,7 @@ services:
BASIC_AUTH_PASSWORD: "${BASIC_AUTH_PASSWORD:-}"
AI_FALLBACK_ENABLED: "${AI_FALLBACK_ENABLED:-false}"
OLLAMA_BASE_URL: "${OLLAMA_BASE_URL:-http://ollama:11434}"
OLLAMA_API_KEY: ${OLLAMA_API_KEY:-}
OLLAMA_MODEL: "${OLLAMA_MODEL:-}"
OLLAMA_TIMEOUT: "${OLLAMA_TIMEOUT:-10m}"
OLLAMA_MAX_CONCURRENT: "${OLLAMA_MAX_CONCURRENT:-1}"
@@ -18,6 +18,7 @@ import (
type Config struct {
BaseURL string
Model string
APIKey string
Timeout time.Duration
MaxConcurrent int
AutoReply bool
@@ -148,6 +149,9 @@ func (s *Service) askOllama(ctx context.Context, query string) (staging.Draft, e
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("Accept", "application/json")
if token := strings.TrimSpace(s.cfg.APIKey); token != "" {
req.Header.Set("Authorization", "Bearer "+token)
}
resp, err := s.client.Do(req)
if err != nil {
if errors.Is(err, context.DeadlineExceeded) || errors.Is(ctx.Err(), context.DeadlineExceeded) {
@@ -50,3 +50,27 @@ func TestGenerateUsesStructuredChatAndStoresResult(t *testing.T) {
t.Fatal(err)
}
}
func TestGenerateSendsOllamaBearerToken(t *testing.T) {
const token = "ollama-secret"
var auth string
server := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
auth = r.Header.Get("Authorization")
_ = json.NewEncoder(w).Encode(map[string]any{"message": map[string]any{"content": `{"title":"T","text":"X","answer":"A","categories":[],"keywords":[]}`}})
}))
defer server.Close()
st, err := staging.New(t.TempDir())
if err != nil {
t.Fatal(err)
}
svc, err := New(Config{BaseURL: server.URL, Model: "test", APIKey: token, Timeout: time.Second, MaxConcurrent: 1, MinScore: 0.78}, st)
if err != nil {
t.Fatal(err)
}
if _, err := svc.Generate(context.Background(), "secure ollama request"); err != nil {
t.Fatal(err)
}
if auth != "Bearer "+token {
t.Fatalf("authorization=%q", auth)
}
}