From 75f183aa2594d04f0be0b558572a88a5e2c7e45c Mon Sep 17 00:00:00 2001 From: Michael Mayer Date: Wed, 10 Dec 2025 10:52:26 +0100 Subject: [PATCH] AI: Add support for OLLAMA_BASE_URL env expansion in vision.yml #5361 Signed-off-by: Michael Mayer --- docker/ddns/Dockerfile | 2 +- docker/develop/armv7/Dockerfile | 1 + docker/develop/bookworm-slim/Dockerfile | 5 +- docker/develop/bookworm/Dockerfile | 3 +- docker/develop/bullseye-slim/Dockerfile | 5 +- docker/develop/bullseye/Dockerfile | 3 +- docker/develop/buster/Dockerfile | 3 +- docker/develop/impish/Dockerfile | 3 +- docker/develop/jammy-slim/Dockerfile | 1 + docker/develop/jammy/Dockerfile | 1 + docker/develop/lunar-slim/Dockerfile | 3 +- docker/develop/lunar/Dockerfile | 3 +- docker/develop/mantic-slim/Dockerfile | 3 +- docker/develop/mantic/Dockerfile | 3 +- docker/develop/noble-slim/Dockerfile | 3 +- docker/develop/noble/Dockerfile | 3 +- docker/develop/oracular-slim/Dockerfile | 1 + docker/develop/oracular/Dockerfile | 1 + docker/develop/plucky-slim/Dockerfile | 1 + docker/develop/plucky/Dockerfile | 1 + docker/develop/questing-slim/Dockerfile | 1 + docker/develop/questing/Dockerfile | 1 + docker/goproxy/Dockerfile | 2 +- docker/photoprism/armv7/Dockerfile | 1 + docker/photoprism/bookworm/Dockerfile | 1 + docker/photoprism/bullseye/Dockerfile | 1 + docker/photoprism/buster/Dockerfile | 1 + docker/photoprism/impish/Dockerfile | 1 + docker/photoprism/jammy/Dockerfile | 1 + docker/photoprism/lunar/Dockerfile | 1 + docker/photoprism/mantic/Dockerfile | 1 + docker/photoprism/noble/Dockerfile | 1 + docker/photoprism/oracular/Dockerfile | 1 + docker/photoprism/plucky/Dockerfile | 1 + docker/photoprism/questing/Dockerfile | 1 + internal/ai/vision/README.md | 28 +++++------ internal/ai/vision/engine_ollama.go | 22 ++++----- internal/ai/vision/engine_ollama_test.go | 36 ++++++++++---- internal/ai/vision/ollama/README.md | 11 +++-- internal/ai/vision/ollama/const.go | 12 +++-- internal/ai/vision/service.go | 8 +++- internal/ai/vision/service_test.go | 16 +++++++ internal/ai/vision/testdata/vision.yml | 2 +- internal/ai/vision/vision.go | 42 ---------------- internal/ai/vision/vision_env.go | 61 ++++++++++++++++++++++++ internal/ai/vision/vision_env_test.go | 25 ++++++++++ 46 files changed, 226 insertions(+), 102 deletions(-) create mode 100644 internal/ai/vision/vision_env.go diff --git a/docker/ddns/Dockerfile b/docker/ddns/Dockerfile index b99b80640..43977a310 100644 --- a/docker/ddns/Dockerfile +++ b/docker/ddns/Dockerfile @@ -5,4 +5,4 @@ FROM golang:1.22-alpine RUN go install github.com/skibish/ddns@latest -CMD ["ddns", "-conf-file", "/config/ddns.yml"] \ No newline at end of file +CMD ["ddns", "-conf-file", "/config/ddns.yml"] diff --git a/docker/develop/armv7/Dockerfile b/docker/develop/armv7/Dockerfile index cee210c11..b23e74f5f 100644 --- a/docker/develop/armv7/Dockerfile +++ b/docker/develop/armv7/Dockerfile @@ -38,6 +38,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ GO111MODULE="on" \ CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \ PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" \ S6_KEEP_ENV=1 \ S6_VERBOSITY=0 \ S6_LOGGING=0 diff --git a/docker/develop/bookworm-slim/Dockerfile b/docker/develop/bookworm-slim/Dockerfile index 4f2445d23..7c6c5161f 100644 --- a/docker/develop/bookworm-slim/Dockerfile +++ b/docker/develop/bookworm-slim/Dockerfile @@ -32,7 +32,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ TF_CPP_MIN_LOG_LEVEL=4 \ TF_ENABLE_ONEDNN_OPTS=1 \ MALLOC_ARENA_MAX=2 \ - PROG="photoprism" + PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" # Copy scripts and package sources config. COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/ @@ -79,4 +80,4 @@ WORKDIR /photoprism EXPOSE 2342 2442 2443 # Keep container running. -CMD ["tail", "-f", "/dev/null"] \ No newline at end of file +CMD ["tail", "-f", "/dev/null"] diff --git a/docker/develop/bookworm/Dockerfile b/docker/develop/bookworm/Dockerfile index 740132594..183d7b76d 100644 --- a/docker/develop/bookworm/Dockerfile +++ b/docker/develop/bookworm/Dockerfile @@ -37,7 +37,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ GOBIN="/usr/local/bin" \ GO111MODULE="on" \ CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \ - PROG="photoprism" + PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" # Copy scripts and package sources config. COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/ diff --git a/docker/develop/bullseye-slim/Dockerfile b/docker/develop/bullseye-slim/Dockerfile index d367802eb..3b41504c1 100644 --- a/docker/develop/bullseye-slim/Dockerfile +++ b/docker/develop/bullseye-slim/Dockerfile @@ -32,7 +32,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ TF_CPP_MIN_LOG_LEVEL=4 \ TF_ENABLE_ONEDNN_OPTS=1 \ MALLOC_ARENA_MAX=2 \ - PROG="photoprism" + PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" # copy scripts and debian backports sources list COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/ @@ -99,4 +100,4 @@ WORKDIR /photoprism EXPOSE 2342 2443 # keep container running -CMD ["tail", "-f", "/dev/null"] \ No newline at end of file +CMD ["tail", "-f", "/dev/null"] diff --git a/docker/develop/bullseye/Dockerfile b/docker/develop/bullseye/Dockerfile index 437e7dcf0..ff6206f3c 100644 --- a/docker/develop/bullseye/Dockerfile +++ b/docker/develop/bullseye/Dockerfile @@ -37,7 +37,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ GOBIN="/usr/local/bin" \ GO111MODULE="on" \ CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \ - PROG="photoprism" + PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" # copy scripts and debian backports sources list COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/ diff --git a/docker/develop/buster/Dockerfile b/docker/develop/buster/Dockerfile index 82f9ac231..20315a6d2 100644 --- a/docker/develop/buster/Dockerfile +++ b/docker/develop/buster/Dockerfile @@ -37,7 +37,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ GOBIN="/usr/local/bin" \ GO111MODULE="on" \ CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \ - PROG="photoprism" + PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" # copy scripts and debian backports sources list COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/ diff --git a/docker/develop/impish/Dockerfile b/docker/develop/impish/Dockerfile index edcc86254..e70f53094 100644 --- a/docker/develop/impish/Dockerfile +++ b/docker/develop/impish/Dockerfile @@ -37,7 +37,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ GOBIN="/usr/local/bin" \ GO111MODULE="on" \ CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \ - PROG="photoprism" + PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" # copy scripts and debian backports sources list COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/ diff --git a/docker/develop/jammy-slim/Dockerfile b/docker/develop/jammy-slim/Dockerfile index 6788d8cc8..f37fa12d3 100644 --- a/docker/develop/jammy-slim/Dockerfile +++ b/docker/develop/jammy-slim/Dockerfile @@ -33,6 +33,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ TF_ENABLE_ONEDNN_OPTS=1 \ MALLOC_ARENA_MAX=2 \ PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" \ S6_KEEP_ENV=0 \ S6_VERBOSITY=0 \ S6_LOGGING=0 diff --git a/docker/develop/jammy/Dockerfile b/docker/develop/jammy/Dockerfile index f825155dc..8c19fb8cc 100644 --- a/docker/develop/jammy/Dockerfile +++ b/docker/develop/jammy/Dockerfile @@ -38,6 +38,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ GO111MODULE="on" \ CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \ PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" \ S6_KEEP_ENV=1 \ S6_VERBOSITY=0 \ S6_LOGGING=0 diff --git a/docker/develop/lunar-slim/Dockerfile b/docker/develop/lunar-slim/Dockerfile index 2a165ed4f..f1d0e1954 100644 --- a/docker/develop/lunar-slim/Dockerfile +++ b/docker/develop/lunar-slim/Dockerfile @@ -32,7 +32,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ TF_CPP_MIN_LOG_LEVEL=4 \ TF_ENABLE_ONEDNN_OPTS=1 \ MALLOC_ARENA_MAX=2 \ - PROG="photoprism" + PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" # Copy scripts and package sources config. COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/ diff --git a/docker/develop/lunar/Dockerfile b/docker/develop/lunar/Dockerfile index 439bbdcd8..f093e6677 100644 --- a/docker/develop/lunar/Dockerfile +++ b/docker/develop/lunar/Dockerfile @@ -37,7 +37,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ GOBIN="/usr/local/bin" \ GO111MODULE="on" \ CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \ - PROG="photoprism" + PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" # Copy scripts and package sources config. COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/ diff --git a/docker/develop/mantic-slim/Dockerfile b/docker/develop/mantic-slim/Dockerfile index 8fb3b74de..3f86b2b5a 100644 --- a/docker/develop/mantic-slim/Dockerfile +++ b/docker/develop/mantic-slim/Dockerfile @@ -32,7 +32,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ TF_CPP_MIN_LOG_LEVEL=4 \ TF_ENABLE_ONEDNN_OPTS=1 \ MALLOC_ARENA_MAX=2 \ - PROG="photoprism" + PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" # Copy scripts and package sources config. COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/ diff --git a/docker/develop/mantic/Dockerfile b/docker/develop/mantic/Dockerfile index 44f07ba03..ae0c3494c 100644 --- a/docker/develop/mantic/Dockerfile +++ b/docker/develop/mantic/Dockerfile @@ -37,7 +37,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ GOBIN="/usr/local/bin" \ GO111MODULE="on" \ CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \ - PROG="photoprism" + PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" # Copy scripts and package sources config. COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/ diff --git a/docker/develop/noble-slim/Dockerfile b/docker/develop/noble-slim/Dockerfile index fd76c91c2..4a88f454e 100644 --- a/docker/develop/noble-slim/Dockerfile +++ b/docker/develop/noble-slim/Dockerfile @@ -32,7 +32,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ TF_CPP_MIN_LOG_LEVEL=4 \ TF_ENABLE_ONEDNN_OPTS=1 \ MALLOC_ARENA_MAX=2 \ - PROG="photoprism" + PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" # Copy scripts and package sources config. COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/ diff --git a/docker/develop/noble/Dockerfile b/docker/develop/noble/Dockerfile index 4588f4dd9..cc1741cec 100644 --- a/docker/develop/noble/Dockerfile +++ b/docker/develop/noble/Dockerfile @@ -37,7 +37,8 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ GOBIN="/usr/local/bin" \ GO111MODULE="on" \ CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \ - PROG="photoprism" + PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" # Copy scripts and package sources config. COPY --chown=root:root --chmod=755 /scripts/dist/ /scripts/ diff --git a/docker/develop/oracular-slim/Dockerfile b/docker/develop/oracular-slim/Dockerfile index 343703bad..0bd6661e3 100644 --- a/docker/develop/oracular-slim/Dockerfile +++ b/docker/develop/oracular-slim/Dockerfile @@ -33,6 +33,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ TF_ENABLE_ONEDNN_OPTS=1 \ MALLOC_ARENA_MAX=2 \ PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" \ S6_KEEP_ENV=0 \ S6_VERBOSITY=0 \ S6_LOGGING=0 diff --git a/docker/develop/oracular/Dockerfile b/docker/develop/oracular/Dockerfile index eb4a548f7..36f0a67b1 100644 --- a/docker/develop/oracular/Dockerfile +++ b/docker/develop/oracular/Dockerfile @@ -38,6 +38,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ GO111MODULE="on" \ CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \ PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" \ S6_KEEP_ENV=1 \ S6_VERBOSITY=0 \ S6_LOGGING=0 diff --git a/docker/develop/plucky-slim/Dockerfile b/docker/develop/plucky-slim/Dockerfile index 0b9b9a582..6ea2ea13f 100644 --- a/docker/develop/plucky-slim/Dockerfile +++ b/docker/develop/plucky-slim/Dockerfile @@ -33,6 +33,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ TF_ENABLE_ONEDNN_OPTS=1 \ MALLOC_ARENA_MAX=2 \ PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" \ S6_KEEP_ENV=0 \ S6_VERBOSITY=0 \ S6_LOGGING=0 diff --git a/docker/develop/plucky/Dockerfile b/docker/develop/plucky/Dockerfile index 69d50b18c..73aaef3d8 100644 --- a/docker/develop/plucky/Dockerfile +++ b/docker/develop/plucky/Dockerfile @@ -38,6 +38,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ GO111MODULE="on" \ CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \ PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" \ S6_KEEP_ENV=1 \ S6_VERBOSITY=0 \ S6_LOGGING=0 diff --git a/docker/develop/questing-slim/Dockerfile b/docker/develop/questing-slim/Dockerfile index be78570da..806efd4cf 100644 --- a/docker/develop/questing-slim/Dockerfile +++ b/docker/develop/questing-slim/Dockerfile @@ -33,6 +33,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ TF_ENABLE_ONEDNN_OPTS=1 \ MALLOC_ARENA_MAX=2 \ PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" S6_KEEP_ENV=0 \ S6_VERBOSITY=0 \ S6_LOGGING=0 diff --git a/docker/develop/questing/Dockerfile b/docker/develop/questing/Dockerfile index d8f47d964..3547f84ae 100644 --- a/docker/develop/questing/Dockerfile +++ b/docker/develop/questing/Dockerfile @@ -38,6 +38,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ GO111MODULE="on" \ CGO_CFLAGS="-g -O2 -Wno-return-local-addr" \ PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" S6_KEEP_ENV=1 \ S6_VERBOSITY=0 \ S6_LOGGING=0 diff --git a/docker/goproxy/Dockerfile b/docker/goproxy/Dockerfile index f6da905e8..ce349ef45 100644 --- a/docker/goproxy/Dockerfile +++ b/docker/goproxy/Dockerfile @@ -23,4 +23,4 @@ VOLUME "/go" EXPOSE 8888 ENTRYPOINT ["/usr/bin/tini", "--"] -CMD ["/goproxy", "-listen", "0.0.0.0:8888"] \ No newline at end of file +CMD ["/goproxy", "-listen", "0.0.0.0:8888"] diff --git a/docker/photoprism/armv7/Dockerfile b/docker/photoprism/armv7/Dockerfile index 222d4d6f1..ea29bd956 100644 --- a/docker/photoprism/armv7/Dockerfile +++ b/docker/photoprism/armv7/Dockerfile @@ -50,6 +50,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ TF_CPP_MIN_LOG_LEVEL=4 \ MALLOC_ARENA_MAX=2 \ PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \ PHOTOPRISM_IMPORT_PATH="/photoprism/import" \ PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \ diff --git a/docker/photoprism/bookworm/Dockerfile b/docker/photoprism/bookworm/Dockerfile index b0aaadd88..ea860d2cd 100644 --- a/docker/photoprism/bookworm/Dockerfile +++ b/docker/photoprism/bookworm/Dockerfile @@ -45,6 +45,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ TF_ENABLE_ONEDNN_OPTS=1 \ MALLOC_ARENA_MAX=2 \ PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" \ PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \ PHOTOPRISM_IMPORT_PATH="/photoprism/import" \ PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \ diff --git a/docker/photoprism/bullseye/Dockerfile b/docker/photoprism/bullseye/Dockerfile index 99c31dd0d..3b1997933 100644 --- a/docker/photoprism/bullseye/Dockerfile +++ b/docker/photoprism/bullseye/Dockerfile @@ -45,6 +45,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ TF_ENABLE_ONEDNN_OPTS=1 \ MALLOC_ARENA_MAX=2 \ PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" \ PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \ PHOTOPRISM_IMPORT_PATH="/photoprism/import" \ PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \ diff --git a/docker/photoprism/buster/Dockerfile b/docker/photoprism/buster/Dockerfile index 75d978c98..42ad3597a 100644 --- a/docker/photoprism/buster/Dockerfile +++ b/docker/photoprism/buster/Dockerfile @@ -48,6 +48,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ TF_ENABLE_ONEDNN_OPTS=1 \ MALLOC_ARENA_MAX=2 \ PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" \ PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \ PHOTOPRISM_IMPORT_PATH="/photoprism/import" \ PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \ diff --git a/docker/photoprism/impish/Dockerfile b/docker/photoprism/impish/Dockerfile index 6e32e1b3d..3f8d1fe58 100644 --- a/docker/photoprism/impish/Dockerfile +++ b/docker/photoprism/impish/Dockerfile @@ -48,6 +48,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ TF_ENABLE_ONEDNN_OPTS=1 \ MALLOC_ARENA_MAX=2 \ PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" \ PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \ PHOTOPRISM_IMPORT_PATH="/photoprism/import" \ PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \ diff --git a/docker/photoprism/jammy/Dockerfile b/docker/photoprism/jammy/Dockerfile index 153dc6632..dd376282e 100644 --- a/docker/photoprism/jammy/Dockerfile +++ b/docker/photoprism/jammy/Dockerfile @@ -46,6 +46,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ TF_ENABLE_ONEDNN_OPTS=1 \ MALLOC_ARENA_MAX=2 \ PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" \ PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \ PHOTOPRISM_IMPORT_PATH="/photoprism/import" \ PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \ diff --git a/docker/photoprism/lunar/Dockerfile b/docker/photoprism/lunar/Dockerfile index 7a5fc1436..ba7e7dd39 100644 --- a/docker/photoprism/lunar/Dockerfile +++ b/docker/photoprism/lunar/Dockerfile @@ -46,6 +46,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ TF_ENABLE_ONEDNN_OPTS=1 \ MALLOC_ARENA_MAX=2 \ PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" \ PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \ PHOTOPRISM_IMPORT_PATH="/photoprism/import" \ PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \ diff --git a/docker/photoprism/mantic/Dockerfile b/docker/photoprism/mantic/Dockerfile index f77f121f1..cdc7b0372 100644 --- a/docker/photoprism/mantic/Dockerfile +++ b/docker/photoprism/mantic/Dockerfile @@ -46,6 +46,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ TF_ENABLE_ONEDNN_OPTS=1 \ MALLOC_ARENA_MAX=2 \ PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" \ PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \ PHOTOPRISM_IMPORT_PATH="/photoprism/import" \ PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \ diff --git a/docker/photoprism/noble/Dockerfile b/docker/photoprism/noble/Dockerfile index aeabdcbb3..18bd330e4 100644 --- a/docker/photoprism/noble/Dockerfile +++ b/docker/photoprism/noble/Dockerfile @@ -46,6 +46,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ TF_ENABLE_ONEDNN_OPTS=1 \ MALLOC_ARENA_MAX=2 \ PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" \ PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \ PHOTOPRISM_IMPORT_PATH="/photoprism/import" \ PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \ diff --git a/docker/photoprism/oracular/Dockerfile b/docker/photoprism/oracular/Dockerfile index 299e73bff..3406d3cb8 100644 --- a/docker/photoprism/oracular/Dockerfile +++ b/docker/photoprism/oracular/Dockerfile @@ -46,6 +46,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ TF_ENABLE_ONEDNN_OPTS=1 \ MALLOC_ARENA_MAX=2 \ PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" \ PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \ PHOTOPRISM_IMPORT_PATH="/photoprism/import" \ PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \ diff --git a/docker/photoprism/plucky/Dockerfile b/docker/photoprism/plucky/Dockerfile index 0cd39fa2d..bc59b6355 100644 --- a/docker/photoprism/plucky/Dockerfile +++ b/docker/photoprism/plucky/Dockerfile @@ -46,6 +46,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ TF_ENABLE_ONEDNN_OPTS=1 \ MALLOC_ARENA_MAX=2 \ PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" \ PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \ PHOTOPRISM_IMPORT_PATH="/photoprism/import" \ PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \ diff --git a/docker/photoprism/questing/Dockerfile b/docker/photoprism/questing/Dockerfile index 3e9059fe6..f0247324f 100644 --- a/docker/photoprism/questing/Dockerfile +++ b/docker/photoprism/questing/Dockerfile @@ -46,6 +46,7 @@ ENV PHOTOPRISM_ARCH=$TARGETARCH \ TF_ENABLE_ONEDNN_OPTS=1 \ MALLOC_ARENA_MAX=2 \ PROG="photoprism" \ + OLLAMA_BASE_URL="http://ollama:11434" \ PHOTOPRISM_ASSETS_PATH="/opt/photoprism/assets" \ PHOTOPRISM_IMPORT_PATH="/photoprism/import" \ PHOTOPRISM_ORIGINALS_PATH="/photoprism/originals" \ diff --git a/internal/ai/vision/README.md b/internal/ai/vision/README.md index 38324d11d..818962860 100644 --- a/internal/ai/vision/README.md +++ b/internal/ai/vision/README.md @@ -1,13 +1,13 @@ ## PhotoPrism — Vision Package -**Last Updated:** December 2, 2025 +**Last Updated:** December 10, 2025 ### Overview `internal/ai/vision` provides the shared model registry, request builders, and parsers that power PhotoPrism’s caption, label, face, NSFW, and future generate workflows. It reads `vision.yml`, normalizes models, and dispatches calls to one of three engines: - **TensorFlow (built‑in)** — default Nasnet / NSFW / Facenet models, no remote service required. -- **Ollama** — local or proxied multimodal LLMs. See [`ollama/README.md`](ollama/README.md) for tuning and schema details. +- **Ollama** — local or proxied multimodal LLMs. See [`ollama/README.md`](ollama/README.md) for tuning and schema details. The engine defaults to `${OLLAMA_BASE_URL:-http://ollama:11434}/api/generate`, trimming any trailing slash on the base URL; set `OLLAMA_BASE_URL=https://ollama.com` to opt into cloud defaults. - **OpenAI** — cloud Responses API. See [`openai/README.md`](openai/README.md) for prompts, schema variants, and header requirements. ### Configuration @@ -93,17 +93,17 @@ The model `Options` adjust model parameters such as temperature, top-p, and sche Configures the endpoint URL, method, format, and authentication for [Ollama](ollama/README.md), [OpenAI](openai/README.md), and other engines that perform remote HTTP requests: -| Field | Default | Notes | -|------------------------------------|------------------------------------------|------------------------------------------------------------------------------------------| -| `Uri` | required for remote | Endpoint base. Empty keeps model local (TensorFlow). | -| `Method` | `POST` | Override verb if provider needs it. | -| `Key` | `""` | Bearer token; prefer env expansion (OpenAI: `OPENAI_API_KEY`, Ollama: `OLLAMA_API_KEY`). | -| `Username` / `Password` | `""` | Injected as basic auth when URI lacks userinfo. | -| `Model` | `""` | Endpoint-specific override; wins over model/name. | -| `Org` / `Project` | `""` | OpenAI headers (org/proj IDs) | -| `RequestFormat` / `ResponseFormat` | set by engine alias | Explicit values win over alias defaults. | -| `FileScheme` | set by engine alias (`data` or `base64`) | Controls image transport. | -| `Disabled` | `false` | Disable the endpoint without removing the model. | +| Field | Default | Notes | +|------------------------------------|------------------------------------------|-------------------------------------------------------------------------------------------------------------------------------------------------| +| `Uri` | required for remote | Endpoint base. Empty keeps model local (TensorFlow). Ollama alias fills `${OLLAMA_BASE_URL}/api/generate`, defaulting to `http://ollama:11434`. | +| `Method` | `POST` | Override verb if provider needs it. | +| `Key` | `""` | Bearer token; prefer env expansion (OpenAI: `OPENAI_API_KEY`, Ollama: `OLLAMA_API_KEY`). | +| `Username` / `Password` | `""` | Injected as basic auth when URI lacks userinfo. | +| `Model` | `""` | Endpoint-specific override; wins over model/name. | +| `Org` / `Project` | `""` | OpenAI headers (org/proj IDs) | +| `RequestFormat` / `ResponseFormat` | set by engine alias | Explicit values win over alias defaults. | +| `FileScheme` | set by engine alias (`data` or `base64`) | Controls image transport. | +| `Disabled` | `false` | Disable the endpoint without removing the model. | > **Authentication:** All credentials and identifiers support `${ENV_VAR}` expansion. `Service.Key` sets `Authorization: Bearer `; `Username`/`Password` injects HTTP basic authentication into the service URI when it is not already present. When `Service.Key` is empty, PhotoPrism defaults to `OPENAI_API_KEY` (OpenAI engine) or `OLLAMA_API_KEY` (Ollama engine), also honoring their `_FILE` counterparts. @@ -142,7 +142,7 @@ Models: Engine: ollama Run: newly-indexed Service: - Uri: http://ollama:11434/api/generate + Uri: ${OLLAMA_BASE_URL}/api/generate ``` More Ollama guidance: [`internal/ai/vision/ollama/README.md`](ollama/README.md). diff --git a/internal/ai/vision/engine_ollama.go b/internal/ai/vision/engine_ollama.go index 49772e04f..ac6f07440 100644 --- a/internal/ai/vision/engine_ollama.go +++ b/internal/ai/vision/engine_ollama.go @@ -28,25 +28,25 @@ func init() { } // registerOllamaEngineDefaults selects the default Ollama endpoint based on the -// available credentials and registers the engine alias accordingly. When an -// API key is configured, we default to the hosted Cloud endpoint; otherwise we -// assume a self-hosted instance reachable via the docker-compose default. -// This keeps the zero-config path fast for local dev while automatically using -// the cloud service when credentials are present. +// configured base URL and registers the engine alias accordingly. When +// OLLAMA_BASE_URL points at the cloud host we only switch the default model to +// the cloud preset; the actual base URL continues to come from +// OLLAMA_BASE_URL (or falls back to the local compose default) so we don't +// accidentally talk to the hosted service without an explicit endpoint. func registerOllamaEngineDefaults() { - defaultModel := ollama.DefaultModel - defaultUri := ollama.DefaultUri + ensureEnv() - // Detect Ollama cloud API key. - if key := os.Getenv(ollama.APIKeyEnv); len(key) > 50 && strings.Contains(key, ".") { + defaultModel := ollama.DefaultModel + + // Use different default model for the Ollama cloud service. + if baseUrl := os.Getenv(ollama.BaseUrlEnv); baseUrl == ollama.CloudBaseUrl { defaultModel = ollama.CloudModel - defaultUri = ollama.CloudUri } // Register the human-friendly engine name so configuration can simply use // `Engine: "ollama"` and inherit adapter defaults. RegisterEngineAlias(ollama.EngineName, EngineInfo{ - Uri: defaultUri, + Uri: ollama.DefaultUri, RequestFormat: ApiFormatOllama, ResponseFormat: ApiFormatOllama, FileScheme: scheme.Base64, diff --git a/internal/ai/vision/engine_ollama_test.go b/internal/ai/vision/engine_ollama_test.go index abe82a63e..c2f768482 100644 --- a/internal/ai/vision/engine_ollama_test.go +++ b/internal/ai/vision/engine_ollama_test.go @@ -4,6 +4,7 @@ import ( "context" "encoding/json" "os" + "sync" "testing" "github.com/photoprism/photoprism/internal/ai/vision/ollama" @@ -29,6 +30,7 @@ func TestRegisterOllamaEngineDefaults(t *testing.T) { }) t.Run("SelfHosted", func(t *testing.T) { + ensureEnvOnce = sync.Once{} CaptionModel = testCaptionModel.Clone() _ = os.Unsetenv(ollama.APIKeyEnv) @@ -56,8 +58,9 @@ func TestRegisterOllamaEngineDefaults(t *testing.T) { } }) t.Run("Cloud", func(t *testing.T) { + ensureEnvOnce = sync.Once{} CaptionModel = testCaptionModel.Clone() - t.Setenv(ollama.APIKeyEnv, cloudToken) + t.Setenv(ollama.BaseUrlEnv, ollama.CloudBaseUrl+"/") registerOllamaEngineDefaults() @@ -66,8 +69,8 @@ func TestRegisterOllamaEngineDefaults(t *testing.T) { t.Fatalf("expected engine info for %s", ollama.EngineName) } - if info.Uri != ollama.CloudUri { - t.Fatalf("expected cloud uri %s, got %s", ollama.CloudUri, info.Uri) + if info.Uri != ollama.DefaultUri { + t.Fatalf("expected default uri %s, got %s", ollama.DefaultUri, info.Uri) } if info.DefaultModel != ollama.CloudModel { @@ -78,14 +81,31 @@ func TestRegisterOllamaEngineDefaults(t *testing.T) { t.Fatalf("expected caption model %s, got %s", ollama.CloudModel, CaptionModel.Model) } - if CaptionModel.Service.Uri != ollama.CloudUri { - t.Fatalf("expected caption model uri %s, got %s", ollama.CloudUri, CaptionModel.Service.Uri) + if CaptionModel.Service.Uri != ollama.DefaultUri { + t.Fatalf("expected caption model uri %s, got %s", ollama.DefaultUri, CaptionModel.Service.Uri) + } + }) + t.Run("ApiKeyAloneKeepsLocalDefaults", func(t *testing.T) { + ensureEnvOnce = sync.Once{} + CaptionModel = testCaptionModel.Clone() + t.Setenv(ollama.APIKeyEnv, cloudToken) + + registerOllamaEngineDefaults() + + info, ok := EngineInfoFor(ollama.EngineName) + if !ok { + t.Fatalf("expected engine info for %s", ollama.EngineName) + } + + if info.DefaultModel != ollama.DefaultModel { + t.Fatalf("expected default model %s, got %s", ollama.DefaultModel, info.DefaultModel) } }) t.Run("NewModels", func(t *testing.T) { + ensureEnvOnce = sync.Once{} CaptionModel = testCaptionModel.Clone() - t.Setenv(ollama.APIKeyEnv, cloudToken) + t.Setenv(ollama.BaseUrlEnv, ollama.CloudBaseUrl) registerOllamaEngineDefaults() model := &Model{Type: ModelTypeCaption, Engine: ollama.EngineName} @@ -95,8 +115,8 @@ func TestRegisterOllamaEngineDefaults(t *testing.T) { t.Fatalf("expected model %s, got %s", ollama.CloudModel, model.Model) } - if model.Service.Uri != ollama.CloudUri { - t.Fatalf("expected service uri %s, got %s", ollama.CloudUri, model.Service.Uri) + if model.Service.Uri != ollama.DefaultUri { + t.Fatalf("expected service uri %s, got %s", ollama.DefaultUri, model.Service.Uri) } if model.Service.RequestFormat != ApiFormatOllama || model.Service.ResponseFormat != ApiFormatOllama { diff --git a/internal/ai/vision/ollama/README.md b/internal/ai/vision/ollama/README.md index 8fe30c398..c2592a760 100644 --- a/internal/ai/vision/ollama/README.md +++ b/internal/ai/vision/ollama/README.md @@ -1,14 +1,14 @@ ## PhotoPrism — Ollama Engine Integration -**Last Updated:** November 14, 2025 +**Last Updated:** December 10, 2025 ### Overview -This package provides PhotoPrism’s native adapter for Ollama-compatible multimodal models. It lets Caption, Labels, and future Generate workflows call locally hosted models without changing worker logic, reusing the shared API client (`internal/ai/vision/api_client.go`) and result types (`LabelResult`, `CaptionResult`). Requests stay inside your infrastructure, rely on base64 thumbnails, and honor the same ACL, timeout, and logging hooks as the default TensorFlow engines. +This package provides PhotoPrism’s native adapter for Ollama-compatible multimodal models. It lets Caption, Labels, and future Generate workflows call locally hosted models without changing worker logic, reusing the shared API client (`internal/ai/vision/api_client.go`) and result types (`LabelResult`, `CaptionResult`). Requests stay inside your infrastructure, rely on base64 thumbnails, and honor the same ACL, timeout, and logging hooks as the default TensorFlow engines. The adapter resolves `${OLLAMA_BASE_URL}/api/generate`, trimming trailing slashes and defaulting to `http://ollama:11434`; set `OLLAMA_BASE_URL=https://ollama.com` to opt into cloud defaults. #### Context & Constraints -- Engine defaults live in `internal/ai/vision/ollama` and are applied whenever a model sets `Engine: ollama`. Aliases map to `ApiFormatOllama`, `scheme.Base64`, and a default 720 px thumbnail. +- Engine defaults live in `internal/ai/vision/ollama` and are applied whenever a model sets `Engine: ollama`. Aliases map to `ApiFormatOllama`, `scheme.Base64`, and a default 720 px thumbnail. Cloud defaults are only selected when `OLLAMA_BASE_URL` equals `https://ollama.com`. - Responses may arrive as newline-delimited JSON chunks. `decodeOllamaResponse` keeps the most recent chunk, while `parseOllamaLabels` replays plain JSON strings found in `response`. - Structured JSON is optional for captions but enforced for labels when `Format: json` (default for label models targeting the Ollama engine). - The adapter never overwrites TensorFlow defaults. If an Ollama call fails, downstream code still has Nasnet, NSFW, and Face models available. @@ -73,6 +73,7 @@ This package provides PhotoPrism’s native adapter for Ollama-compatible multim - `PHOTOPRISM_VISION_YAML` — Custom `vision.yml` path. Keep it synced in Git if you automate deployments. - `OLLAMA_HOST`, `OLLAMA_MODELS`, `OLLAMA_MAX_QUEUE`, `OLLAMA_NUM_PARALLEL`, etc. — Provided in `compose*.yaml` to tune the Ollama daemon. Adjust `OLLAMA_KEEP_ALIVE` if you want models to stay loaded between worker batches. - `OLLAMA_API_KEY` / `OLLAMA_API_KEY_FILE` — Default bearer token picked up when `Service.Key` is empty; useful for hosted Ollama services (e.g., Ollama Cloud). +- `OLLAMA_BASE_URL` — Base URL for the Ollama API; defaults to `http://ollama:11434`, trailing slashes are trimmed. Set to `https://ollama.com` to enable cloud defaults. - `PHOTOPRISM_LOG_LEVEL=trace` — Enables verbose request/response previews (truncated to avoid leaking images). Use temporarily when debugging parsing issues. #### `vision.yml` Example @@ -90,7 +91,7 @@ Models: Stop: ["\n\n"] ForceJson: true Service: - Uri: http://ollama:11434/api/generate + Uri: ${OLLAMA_BASE_URL}/api/generate RequestFormat: ollama ResponseFormat: ollama FileScheme: base64 @@ -102,7 +103,7 @@ Models: Options: Temperature: 0.2 Service: - Uri: http://ollama:11434/api/generate + Uri: ${OLLAMA_BASE_URL}/api/generate ``` Guidelines: diff --git a/internal/ai/vision/ollama/const.go b/internal/ai/vision/ollama/const.go index c6093414e..1d9538c49 100644 --- a/internal/ai/vision/ollama/const.go +++ b/internal/ai/vision/ollama/const.go @@ -11,10 +11,16 @@ const ( APIKeyFileEnv = "OLLAMA_API_KEY_FILE" //nolint:gosec // environment variable name, not a secret // APIKeyPlaceholder is the `${VAR}` form injected when no explicit key is provided. APIKeyPlaceholder = "${" + APIKeyEnv + "}" + // BaseUrlEnv defines the environment variable used for the Ollama base URL e.g. "https://ollama.com" or "http://ollama:11434". + BaseUrlEnv = "OLLAMA_BASE_URL" //nolint:gosec // environment variable name, not a secret + // BaseUrlPlaceholder is the `${VAR}` form injected when no explicit URL is provided. + BaseUrlPlaceholder = "${" + BaseUrlEnv + "}" + // DefaultBaseUrl is the local Ollama endpoint used when the environment variable is unset. + DefaultBaseUrl = "http://ollama:11434" + // CloudBaseUrl is the base URL for the Ollama Cloud service. + CloudBaseUrl = "https://ollama.com" // DefaultUri is the default service URI for self-hosted Ollama instances. - DefaultUri = "http://ollama:11434/api/generate" - // CloudUri is the Ollama cloud service URI - CloudUri = "https://ollama.com/api/generate" + DefaultUri = BaseUrlPlaceholder + "/api/generate" // DefaultModel names the default caption model bundled with our adapter defaults. DefaultModel = "gemma3:latest" // CloudModel names the default caption for the Ollama cloud service, see https://ollama.com/cloud. diff --git a/internal/ai/vision/service.go b/internal/ai/vision/service.go index fa01ec2a3..1810feeaf 100644 --- a/internal/ai/vision/service.go +++ b/internal/ai/vision/service.go @@ -31,14 +31,18 @@ func (m *Service) Endpoint() (uri, method string) { return "", "" } + ensureEnv() + + if uri = strings.TrimSpace(os.ExpandEnv(m.Uri)); strings.Contains(uri, "${") { + uri = "" + } + if m.Method != "" { method = m.Method } else { method = ServiceMethod } - uri = strings.TrimSpace(m.Uri) - if username, password := m.BasicAuth(); username != "" || password != "" { if parsed, err := url.Parse(uri); err == nil { if parsed.User == nil { diff --git a/internal/ai/vision/service_test.go b/internal/ai/vision/service_test.go index 846fc7594..da2531d4e 100644 --- a/internal/ai/vision/service_test.go +++ b/internal/ai/vision/service_test.go @@ -33,10 +33,26 @@ func TestServiceEndpoint(t *testing.T) { wantURI: "https://keep:me@vision.example.com", wantMethod: ServiceMethod, }, + { + name: "ExpandsBaseUrlEnv", + svc: Service{Uri: "${OLLAMA_BASE_URL}/api/generate"}, + wantURI: "http://custom:11434/api/generate", + wantMethod: ServiceMethod, + }, + { + name: "FallbacksWhenEnvMissing", + svc: Service{Uri: "${OLLAMA_BASE_URL}/api/generate"}, + wantURI: "http://ollama:11434/api/generate", + wantMethod: ServiceMethod, + }, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { + if tt.name == "ExpandsBaseUrlEnv" { + t.Setenv("OLLAMA_BASE_URL", "http://custom:11434") + } + uri, method := tt.svc.Endpoint() if uri != tt.wantURI { t.Fatalf("uri: got %q want %q", uri, tt.wantURI) diff --git a/internal/ai/vision/testdata/vision.yml b/internal/ai/vision/testdata/vision.yml index 753cf4913..91b0d8058 100644 --- a/internal/ai/vision/testdata/vision.yml +++ b/internal/ai/vision/testdata/vision.yml @@ -70,7 +70,7 @@ Models: Run: manual Resolution: 720 Service: - Uri: http://ollama:11434/api/generate + Uri: ${OLLAMA_BASE_URL}/api/generate Key: ${OLLAMA_API_KEY} FileScheme: base64 RequestFormat: ollama diff --git a/internal/ai/vision/vision.go b/internal/ai/vision/vision.go index 613bd1b12..d1caadecf 100644 --- a/internal/ai/vision/vision.go +++ b/internal/ai/vision/vision.go @@ -25,49 +25,7 @@ Additional information can be found in our Developer Guide: package vision import ( - "os" - "strings" - "sync" - - "github.com/photoprism/photoprism/internal/ai/vision/ollama" - "github.com/photoprism/photoprism/internal/ai/vision/openai" "github.com/photoprism/photoprism/internal/event" - "github.com/photoprism/photoprism/pkg/clean" - "github.com/photoprism/photoprism/pkg/fs" ) var log = event.Log - -var ensureEnvOnce sync.Once - -// ensureEnv loads environment-backed credentials once so adapters can look up -// OPENAI_API_KEY / OLLAMA_API_KEY even when operators rely on *_FILE fallbacks. -// Future engine integrations can reuse this hook to normalise additional -// secrets. -func ensureEnv() { - ensureEnvOnce.Do(func() { - loadEnvKeyFromFile(openai.APIKeyEnv, openai.APIKeyFileEnv) - loadEnvKeyFromFile(ollama.APIKeyEnv, ollama.APIKeyFileEnv) - }) -} - -// loadEnvKeyFromFile populates envVar from fileVar when the environment value -// is empty and the referenced file exists and is non-empty. -func loadEnvKeyFromFile(envVar, fileVar string) { - if os.Getenv(envVar) != "" { - return - } - - filePath := strings.TrimSpace(os.Getenv(fileVar)) - - if !fs.FileExistsNotEmpty(filePath) { - return - } - - // #nosec G304 path provided via env - if data, err := os.ReadFile(filePath); err == nil { - if key := clean.Auth(string(data)); key != "" { - _ = os.Setenv(envVar, key) - } - } -} diff --git a/internal/ai/vision/vision_env.go b/internal/ai/vision/vision_env.go new file mode 100644 index 000000000..f2a3369e0 --- /dev/null +++ b/internal/ai/vision/vision_env.go @@ -0,0 +1,61 @@ +package vision + +import ( + "os" + "strings" + "sync" + + "github.com/photoprism/photoprism/internal/ai/vision/ollama" + "github.com/photoprism/photoprism/internal/ai/vision/openai" + "github.com/photoprism/photoprism/pkg/clean" + "github.com/photoprism/photoprism/pkg/fs" +) + +var ensureEnvOnce sync.Once + +// ensureEnv loads environment-backed credentials once so adapters can look up +// OPENAI_API_KEY / OLLAMA_API_KEY even when operators rely on *_FILE fallbacks. +// Future engine integrations can reuse this hook to normalize additional +// secrets. +func ensureEnv() { + ensureEnvOnce.Do(func() { + loadEnvKeyFromFile(openai.APIKeyEnv, openai.APIKeyFileEnv) + loadEnvKeyFromFile(ollama.APIKeyEnv, ollama.APIKeyFileEnv) + + // Init the Ollama base URL by trimming trailing slashes or using the default. + initEnvUrl(ollama.BaseUrlEnv, ollama.DefaultBaseUrl) + }) +} + +// initEnvUrl ensures that the variable contains no trailing +// slashes and sets a default value if it is missing. +func initEnvUrl(envName, defaultUrl string) { + if base := strings.TrimSpace(os.Getenv(envName)); base != "" { + if normalized := strings.TrimRight(base, "/"); normalized != base { + _ = os.Setenv(envName, normalized) + } + } else if defaultUrl != "" { + _ = os.Setenv(envName, defaultUrl) + } +} + +// loadEnvKeyFromFile populates envVar from fileVar when the environment value +// is empty and the referenced file exists and is non-empty. +func loadEnvKeyFromFile(envVar, fileVar string) { + if os.Getenv(envVar) != "" { + return + } + + filePath := strings.TrimSpace(os.Getenv(fileVar)) + + if !fs.FileExistsNotEmpty(filePath) { + return + } + + // #nosec G304 path provided via env + if data, err := os.ReadFile(filePath); err == nil { + if key := clean.Auth(string(data)); key != "" { + _ = os.Setenv(envVar, key) + } + } +} diff --git a/internal/ai/vision/vision_env_test.go b/internal/ai/vision/vision_env_test.go index 73e38cde4..f391f37df 100644 --- a/internal/ai/vision/vision_env_test.go +++ b/internal/ai/vision/vision_env_test.go @@ -6,6 +6,31 @@ import ( "testing" ) +func TestInitEnvUrl(t *testing.T) { + const envName = "TEST_OLLAMA_BASE_URL" + + // Case: trims trailing slash. + t.Setenv(envName, "http://example.com/") + initEnvUrl(envName, "") + if got := os.Getenv(envName); got != "http://example.com" { + t.Fatalf("trim: expected http://example.com, got %s", got) + } + + // Case: sets default when unset. + t.Setenv(envName, "") + initEnvUrl(envName, "http://default.local") + if got := os.Getenv(envName); got != "http://default.local" { + t.Fatalf("default: expected http://default.local, got %s", got) + } + + // Case: leaves already-normalized value untouched. + t.Setenv(envName, "http://kept.local") + initEnvUrl(envName, "http://ignored.local") + if got := os.Getenv(envName); got != "http://kept.local" { + t.Fatalf("preserve: expected http://kept.local, got %s", got) + } +} + // TestLoadEnvKeyFromFile verifies that loadEnvKeyFromFile reads API keys from // *_FILE variables when the primary env var is empty. func TestLoadEnvKeyFromFile(t *testing.T) {