Settings + fix startup
This commit is contained in:
+30
-46
@@ -1,3 +1,13 @@
|
|||||||
|
# --- IMPORTANT ---
|
||||||
|
# Settings are split across multiple files for clarity.
|
||||||
|
# Files (loaded in this order, last wins):
|
||||||
|
# .env.alfred — app config and service addresses (safe to commit)
|
||||||
|
# .env.secrets — generated secrets, passwords, URIs and API keys (DO NOT COMMIT)
|
||||||
|
# .env.make — build metadata synced from pyproject.toml (safe to commit)
|
||||||
|
#
|
||||||
|
# To customize: edit .env.alfred for config, .env.secrets for secrets.
|
||||||
|
|
||||||
|
# --- Alfred ---
|
||||||
MAX_HISTORY_MESSAGES=10
|
MAX_HISTORY_MESSAGES=10
|
||||||
MAX_TOOL_ITERATIONS=10
|
MAX_TOOL_ITERATIONS=10
|
||||||
REQUEST_TIMEOUT=30
|
REQUEST_TIMEOUT=30
|
||||||
@@ -8,84 +18,58 @@ LLM_TEMPERATURE=0.2
|
|||||||
# Persistence
|
# Persistence
|
||||||
DATA_STORAGE_DIR=data
|
DATA_STORAGE_DIR=data
|
||||||
|
|
||||||
# Network configuration
|
# Network
|
||||||
HOST=0.0.0.0
|
HOST=0.0.0.0
|
||||||
PORT=3080
|
PORT=3080
|
||||||
|
|
||||||
# Build informations (Synced with pyproject.toml via bootstrap)
|
# --- DATABASES ---
|
||||||
ALFRED_VERSION=
|
# Passwords and connection URIs are auto-generated in .env.secrets.
|
||||||
IMAGE_NAME=
|
# Edit host/port/user/dbname here if needed.
|
||||||
LIBRECHAT_VERSION=
|
|
||||||
PYTHON_VERSION=
|
|
||||||
PYTHON_VERSION_SHORT=
|
|
||||||
RAG_VERSION=
|
|
||||||
RUNNER=
|
|
||||||
SERVICE_NAME=
|
|
||||||
|
|
||||||
# --- SECURITY KEYS (CRITICAL) ---
|
|
||||||
# These are used for session tokens and encrypting sensitive data in MongoDB.
|
|
||||||
# If you lose these, you lose access to encrypted stored credentials.
|
|
||||||
JWT_SECRET=
|
|
||||||
JWT_REFRESH_SECRET=
|
|
||||||
CREDS_KEY=
|
|
||||||
CREDS_IV=
|
|
||||||
|
|
||||||
# --- DATABASES (AUTO-SECURED) ---
|
|
||||||
# Alfred uses MongoDB for application state and PostgreSQL for Vector RAG.
|
|
||||||
# Passwords will be generated as 24-character secure tokens if left blank.
|
|
||||||
|
|
||||||
# MongoDB (Application Data)
|
# MongoDB (Application Data)
|
||||||
MONGO_URI=
|
|
||||||
MONGO_HOST=mongodb
|
MONGO_HOST=mongodb
|
||||||
MONGO_PORT=27017
|
MONGO_PORT=27017
|
||||||
MONGO_USER=alfred
|
MONGO_USER=alfred
|
||||||
MONGO_PASSWORD=
|
MONGO_DB_NAME=alfred
|
||||||
MONGO_DB_NAME=LibreChat
|
|
||||||
|
|
||||||
# PostgreSQL (Vector Database / RAG)
|
# PostgreSQL (Vector Database / RAG)
|
||||||
POSTGRES_URI=
|
|
||||||
POSTGRES_HOST=vectordb
|
POSTGRES_HOST=vectordb
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
POSTGRES_USER=alfred
|
POSTGRES_USER=alfred
|
||||||
POSTGRES_PASSWORD=
|
|
||||||
POSTGRES_DB_NAME=alfred
|
POSTGRES_DB_NAME=alfred
|
||||||
|
|
||||||
# --- EXTERNAL SERVICES ---
|
# --- EXTERNAL SERVICES ---
|
||||||
# Media Metadata (Required)
|
|
||||||
# Get your key at https://www.themoviedb.org/
|
# TMDB — Media metadata (required). Get your key at https://www.themoviedb.org/
|
||||||
TMDB_API_KEY=
|
# → TMDB_API_KEY goes in .env.secrets
|
||||||
TMDB_BASE_URL=https://api.themoviedb.org/3
|
TMDB_BASE_URL=https://api.themoviedb.org/3
|
||||||
|
|
||||||
# qBittorrent integration
|
# qBittorrent
|
||||||
|
# → QBITTORRENT_PASSWORD goes in .env.secrets
|
||||||
QBITTORRENT_URL=http://qbittorrent:16140
|
QBITTORRENT_URL=http://qbittorrent:16140
|
||||||
QBITTORRENT_USERNAME=admin
|
QBITTORRENT_USERNAME=admin
|
||||||
QBITTORRENT_PASSWORD=
|
|
||||||
QBITTORRENT_PORT=16140
|
QBITTORRENT_PORT=16140
|
||||||
|
|
||||||
# Meilisearch
|
# Meilisearch
|
||||||
MEILI_ENABLED=FALSE
|
# → MEILI_MASTER_KEY goes in .env.secrets
|
||||||
MEILI_NO_ANALYTICS=TRUE
|
# MEILI_ENABLED=false # KEY DOESN'T EXISTS => SEARCH IS THE PROPER KEY
|
||||||
|
SEARCH=false
|
||||||
|
MEILI_NO_ANALYTICS=true
|
||||||
MEILI_HOST=http://meilisearch:7700
|
MEILI_HOST=http://meilisearch:7700
|
||||||
MEILI_MASTER_KEY=
|
|
||||||
|
|
||||||
# --- LLM CONFIGURATION ---
|
# --- LLM CONFIGURATION ---
|
||||||
# Providers: 'local', 'openai', 'anthropic', 'deepseek', 'google', 'kimi'
|
# Providers: local, openai, anthropic, deepseek, google, kimi
|
||||||
|
# → API keys go in .env.secrets
|
||||||
DEFAULT_LLM_PROVIDER=local
|
DEFAULT_LLM_PROVIDER=local
|
||||||
|
|
||||||
# Local LLM (Ollama)
|
# Local LLM (Ollama)
|
||||||
OLLAMA_BASE_URL=http://ollama:11434
|
#OLLAMA_BASE_URL=http://ollama:11434
|
||||||
OLLAMA_MODEL=llama3.3:latest
|
#OLLAMA_MODEL=llama3.3:latest
|
||||||
|
|
||||||
# --- API KEYS (OPTIONAL) ---
|
OLLAMA_BASE_URL=http://10.0.0.11:11434
|
||||||
# Fill only the ones you intend to use.
|
OLLAMA_MODEL=glm-4.7-flash:latest
|
||||||
ANTHROPIC_API_KEY=
|
|
||||||
DEEPSEEK_API_KEY=
|
|
||||||
GOOGLE_API_KEY=
|
|
||||||
KIMI_API_KEY=
|
|
||||||
OPENAI_API_KEY=
|
|
||||||
|
|
||||||
# --- RAG ENGINE ---
|
# --- RAG ENGINE ---
|
||||||
# Enable/Disable the Retrieval Augmented Generation system
|
|
||||||
RAG_ENABLED=TRUE
|
RAG_ENABLED=TRUE
|
||||||
RAG_API_URL=http://rag_api:8000
|
RAG_API_URL=http://rag_api:8000
|
||||||
RAG_API_PORT=8000
|
RAG_API_PORT=8000
|
||||||
|
|||||||
+22
-42
@@ -1,3 +1,13 @@
|
|||||||
|
# --- IMPORTANT ---
|
||||||
|
# Settings are split across multiple files for clarity.
|
||||||
|
# Files (loaded in this order, last wins):
|
||||||
|
# .env.alfred — app config and service addresses (safe to commit)
|
||||||
|
# .env.secrets — generated secrets, passwords, URIs and API keys (DO NOT COMMIT)
|
||||||
|
# .env.make — build metadata synced from pyproject.toml (safe to commit)
|
||||||
|
#
|
||||||
|
# To customize: edit .env.alfred for config, .env.secrets for secrets.
|
||||||
|
|
||||||
|
# --- Alfred ---
|
||||||
MAX_HISTORY_MESSAGES=10
|
MAX_HISTORY_MESSAGES=10
|
||||||
MAX_TOOL_ITERATIONS=10
|
MAX_TOOL_ITERATIONS=10
|
||||||
REQUEST_TIMEOUT=30
|
REQUEST_TIMEOUT=30
|
||||||
@@ -8,84 +18,54 @@ LLM_TEMPERATURE=0.2
|
|||||||
# Persistence
|
# Persistence
|
||||||
DATA_STORAGE_DIR=data
|
DATA_STORAGE_DIR=data
|
||||||
|
|
||||||
# Network configuration
|
# Network
|
||||||
HOST=0.0.0.0
|
HOST=0.0.0.0
|
||||||
PORT=3080
|
PORT=3080
|
||||||
|
|
||||||
# Build informations (Synced with pyproject.toml via bootstrap)
|
# --- DATABASES ---
|
||||||
ALFRED_VERSION=
|
# Passwords and connection URIs are auto-generated in .env.secrets.
|
||||||
IMAGE_NAME=
|
# Edit host/port/user/dbname here if needed.
|
||||||
LIBRECHAT_VERSION=
|
|
||||||
PYTHON_VERSION=
|
|
||||||
PYTHON_VERSION_SHORT=
|
|
||||||
RAG_VERSION=
|
|
||||||
RUNNER=
|
|
||||||
SERVICE_NAME=
|
|
||||||
|
|
||||||
# --- SECURITY KEYS (CRITICAL) ---
|
|
||||||
# These are used for session tokens and encrypting sensitive data in MongoDB.
|
|
||||||
# If you lose these, you lose access to encrypted stored credentials.
|
|
||||||
JWT_SECRET=
|
|
||||||
JWT_REFRESH_SECRET=
|
|
||||||
CREDS_KEY=
|
|
||||||
CREDS_IV=
|
|
||||||
|
|
||||||
# --- DATABASES (AUTO-SECURED) ---
|
|
||||||
# Alfred uses MongoDB for application state and PostgreSQL for Vector RAG.
|
|
||||||
# Passwords will be generated as 24-character secure tokens if left blank.
|
|
||||||
|
|
||||||
# MongoDB (Application Data)
|
# MongoDB (Application Data)
|
||||||
MONGO_URI=
|
|
||||||
MONGO_HOST=mongodb
|
MONGO_HOST=mongodb
|
||||||
MONGO_PORT=27017
|
MONGO_PORT=27017
|
||||||
MONGO_USER=alfred
|
MONGO_USER=alfred
|
||||||
MONGO_PASSWORD=
|
|
||||||
MONGO_DB_NAME=LibreChat
|
MONGO_DB_NAME=LibreChat
|
||||||
|
|
||||||
# PostgreSQL (Vector Database / RAG)
|
# PostgreSQL (Vector Database / RAG)
|
||||||
POSTGRES_URI=
|
|
||||||
POSTGRES_HOST=vectordb
|
POSTGRES_HOST=vectordb
|
||||||
POSTGRES_PORT=5432
|
POSTGRES_PORT=5432
|
||||||
POSTGRES_USER=alfred
|
POSTGRES_USER=alfred
|
||||||
POSTGRES_PASSWORD=
|
|
||||||
POSTGRES_DB_NAME=alfred
|
POSTGRES_DB_NAME=alfred
|
||||||
|
|
||||||
# --- EXTERNAL SERVICES ---
|
# --- EXTERNAL SERVICES ---
|
||||||
# Media Metadata (Required)
|
|
||||||
# Get your key at https://www.themoviedb.org/
|
# TMDB — Media metadata (required). Get your key at https://www.themoviedb.org/
|
||||||
TMDB_API_KEY=
|
# → TMDB_API_KEY goes in .env.secrets
|
||||||
TMDB_BASE_URL=https://api.themoviedb.org/3
|
TMDB_BASE_URL=https://api.themoviedb.org/3
|
||||||
|
|
||||||
# qBittorrent integration
|
# qBittorrent
|
||||||
|
# → QBITTORRENT_PASSWORD goes in .env.secrets
|
||||||
QBITTORRENT_URL=http://qbittorrent:16140
|
QBITTORRENT_URL=http://qbittorrent:16140
|
||||||
QBITTORRENT_USERNAME=admin
|
QBITTORRENT_USERNAME=admin
|
||||||
QBITTORRENT_PASSWORD=
|
|
||||||
QBITTORRENT_PORT=16140
|
QBITTORRENT_PORT=16140
|
||||||
|
|
||||||
# Meilisearch
|
# Meilisearch
|
||||||
|
# → MEILI_MASTER_KEY goes in .env.secrets
|
||||||
MEILI_ENABLED=FALSE
|
MEILI_ENABLED=FALSE
|
||||||
MEILI_NO_ANALYTICS=TRUE
|
MEILI_NO_ANALYTICS=TRUE
|
||||||
MEILI_HOST=http://meilisearch:7700
|
MEILI_HOST=http://meilisearch:7700
|
||||||
MEILI_MASTER_KEY=
|
|
||||||
|
|
||||||
# --- LLM CONFIGURATION ---
|
# --- LLM CONFIGURATION ---
|
||||||
# Providers: 'local', 'openai', 'anthropic', 'deepseek', 'google', 'kimi'
|
# Providers: local, openai, anthropic, deepseek, google, kimi
|
||||||
|
# → API keys go in .env.secrets
|
||||||
DEFAULT_LLM_PROVIDER=local
|
DEFAULT_LLM_PROVIDER=local
|
||||||
|
|
||||||
# Local LLM (Ollama)
|
# Local LLM (Ollama)
|
||||||
OLLAMA_BASE_URL=http://ollama:11434
|
OLLAMA_BASE_URL=http://ollama:11434
|
||||||
OLLAMA_MODEL=llama3.3:latest
|
OLLAMA_MODEL=llama3.3:latest
|
||||||
|
|
||||||
# --- API KEYS (OPTIONAL) ---
|
|
||||||
# Fill only the ones you intend to use.
|
|
||||||
ANTHROPIC_API_KEY=
|
|
||||||
DEEPSEEK_API_KEY=
|
|
||||||
GOOGLE_API_KEY=
|
|
||||||
KIMI_API_KEY=
|
|
||||||
OPENAI_API_KEY=
|
|
||||||
|
|
||||||
# --- RAG ENGINE ---
|
# --- RAG ENGINE ---
|
||||||
# Enable/Disable the Retrieval Augmented Generation system
|
|
||||||
RAG_ENABLED=TRUE
|
RAG_ENABLED=TRUE
|
||||||
RAG_API_URL=http://rag_api:8000
|
RAG_API_URL=http://rag_api:8000
|
||||||
RAG_API_PORT=8000
|
RAG_API_PORT=8000
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
# Auto-generated from pyproject.toml — do not edit manually
|
# Auto-generated from pyproject.toml — do not edit manually
|
||||||
export ALFRED_VERSION=0.1.7
|
ALFRED_VERSION=0.1.7
|
||||||
export PYTHON_VERSION=3.14.3
|
PYTHON_VERSION=3.14.3
|
||||||
export PYTHON_VERSION_SHORT=3.14
|
IMAGE_NAME=alfred_media_organizer
|
||||||
export IMAGE_NAME=alfred_media_organizer
|
SERVICE_NAME=alfred
|
||||||
export SERVICE_NAME=alfred
|
LIBRECHAT_VERSION=v0.8.4
|
||||||
export LIBRECHAT_VERSION=v0.8.4
|
RAG_VERSION=v0.7.3
|
||||||
export RAG_VERSION=v0.7.3
|
UV_VERSION=0.11.6
|
||||||
export UV_VERSION=0.11.6
|
|
||||||
|
|||||||
+9
-1
@@ -55,7 +55,7 @@ coverage.xml
|
|||||||
Thumbs.db
|
Thumbs.db
|
||||||
|
|
||||||
# Secrets
|
# Secrets
|
||||||
.env
|
.env.secrets
|
||||||
|
|
||||||
# Backup files
|
# Backup files
|
||||||
*.backup
|
*.backup
|
||||||
@@ -65,3 +65,11 @@ data/*
|
|||||||
|
|
||||||
# Application logs
|
# Application logs
|
||||||
logs/*
|
logs/*
|
||||||
|
|
||||||
|
# Documentation folder
|
||||||
|
docs/
|
||||||
|
|
||||||
|
# .md files
|
||||||
|
*.md
|
||||||
|
|
||||||
|
#
|
||||||
|
|||||||
+27
-41
@@ -2,38 +2,36 @@
|
|||||||
# check=skip=InvalidDefaultArgInFrom
|
# check=skip=InvalidDefaultArgInFrom
|
||||||
|
|
||||||
ARG PYTHON_VERSION
|
ARG PYTHON_VERSION
|
||||||
ARG PYTHON_VERSION_SHORT
|
|
||||||
ARG UV_VERSION
|
ARG UV_VERSION
|
||||||
|
|
||||||
|
# Stage 0: uv binary (workaround — --from doesn't support ARG expansion)
|
||||||
|
FROM ghcr.io/astral-sh/uv:${UV_VERSION} AS uv-bin
|
||||||
|
|
||||||
# ===========================================
|
# ===========================================
|
||||||
# Stage 1: Builder
|
# Stage 1: Builder
|
||||||
# ===========================================
|
# ===========================================
|
||||||
FROM python:${PYTHON_VERSION}-slim-bookworm AS builder
|
FROM python:${PYTHON_VERSION}-slim-bookworm AS builder
|
||||||
|
|
||||||
# Re-declare ARGs after FROM to make them available in this stage
|
|
||||||
ARG UV_VERSION
|
|
||||||
|
|
||||||
# STFU - No need - Write logs asap
|
|
||||||
ENV DEBIAN_FRONTEND=noninteractive \
|
ENV DEBIAN_FRONTEND=noninteractive \
|
||||||
PYTHONDONTWRITEBYTECODE=1 \
|
PYTHONDONTWRITEBYTECODE=1 \
|
||||||
PYTHONUNBUFFERED=1
|
PYTHONUNBUFFERED=1 \
|
||||||
|
UV_PROJECT_ENVIRONMENT=/venv
|
||||||
|
|
||||||
# Install build dependencies (needs root)
|
# Install build dependencies
|
||||||
RUN apt-get update \
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
&& apt-get install -y --no-install-recommends build-essential \
|
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||||
&& rm -rf /var/lib/apt/lists/*
|
apt-get update \
|
||||||
|
&& apt-get install -y --no-install-recommends build-essential
|
||||||
|
|
||||||
# Install uv globally (needs root) - Save cache for future
|
# Install uv globally
|
||||||
COPY --from=ghcr.io/astral-sh/uv:${UV_VERSION} /uv /usr/local/bin/uv
|
COPY --from=uv-bin /uv /usr/local/bin/uv
|
||||||
|
|
||||||
# Set working directory for dependency installation
|
|
||||||
WORKDIR /tmp
|
WORKDIR /tmp
|
||||||
|
|
||||||
# Copy dependency files
|
|
||||||
COPY pyproject.toml uv.lock Makefile ./
|
COPY pyproject.toml uv.lock Makefile ./
|
||||||
|
|
||||||
# Install dependencies as root (to avoid permission issues with system packages)
|
# Install dependencies into /venv
|
||||||
RUN --mount=type=cache,target=/root/.cache/uv uv sync --system
|
RUN --mount=type=cache,target=/root/.cache/uv uv sync
|
||||||
|
|
||||||
COPY scripts/ ./scripts/
|
COPY scripts/ ./scripts/
|
||||||
COPY .env.example ./
|
COPY .env.example ./
|
||||||
@@ -43,7 +41,7 @@ COPY .env.example ./
|
|||||||
# ===========================================
|
# ===========================================
|
||||||
FROM builder AS test
|
FROM builder AS test
|
||||||
|
|
||||||
RUN --mount=type=cache,target=/root/.cache/uv uv sync --system -e .[dev]
|
RUN --mount=type=cache,target=/root/.cache/uv uv sync --group dev
|
||||||
|
|
||||||
COPY alfred/ ./alfred
|
COPY alfred/ ./alfred
|
||||||
COPY scripts ./scripts
|
COPY scripts ./scripts
|
||||||
@@ -54,51 +52,39 @@ COPY tests/ ./tests
|
|||||||
# ===========================================
|
# ===========================================
|
||||||
FROM python:${PYTHON_VERSION}-slim-bookworm AS runtime
|
FROM python:${PYTHON_VERSION}-slim-bookworm AS runtime
|
||||||
|
|
||||||
ARG PYTHON_VERSION_SHORT
|
ENV PYTHONDONTWRITEBYTECODE=1 \
|
||||||
|
PYTHONUNBUFFERED=1 \
|
||||||
# TODO: A-t-on encore besoin de toutes les clés ?
|
|
||||||
ENV LLM_PROVIDER=deepseek \
|
|
||||||
MEMORY_STORAGE_DIR=/data/memory \
|
|
||||||
PYTHONDONTWRITEBYTECODE=1 \
|
|
||||||
PYTHONPATH=/home/appuser \
|
PYTHONPATH=/home/appuser \
|
||||||
PYTHONUNBUFFERED=1
|
PATH="/venv/bin:$PATH"
|
||||||
|
|
||||||
# Install runtime dependencies (needs root)
|
# Install runtime dependencies
|
||||||
RUN apt-get update && apt-get install -y --no-install-recommends \
|
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
|
||||||
ca-certificates \
|
--mount=type=cache,target=/var/lib/apt,sharing=locked \
|
||||||
&& rm -rf /var/lib/apt/lists/* \
|
apt-get update \
|
||||||
&& apt-get clean
|
&& apt-get install -y --no-install-recommends ca-certificates
|
||||||
|
|
||||||
# Create non-root user
|
# Create non-root user
|
||||||
RUN useradd -m -u 1000 -s /bin/bash appuser
|
RUN useradd -m -u 1000 -s /bin/bash appuser
|
||||||
|
|
||||||
# Create data directories (needs root for /data)
|
# Create data directories
|
||||||
RUN mkdir -p /data /logs \
|
RUN mkdir -p /data /logs \
|
||||||
&& chown -R appuser:appuser /data /logs
|
&& chown -R appuser:appuser /data /logs
|
||||||
|
|
||||||
# Switch to non-root user
|
|
||||||
USER appuser
|
USER appuser
|
||||||
|
|
||||||
# Set working directory (owned by appuser)
|
|
||||||
WORKDIR /home/appuser
|
WORKDIR /home/appuser
|
||||||
|
|
||||||
# Copy Python packages from builder stage
|
# Copy venv from builder stage
|
||||||
COPY --from=builder /usr/local/lib/python${PYTHON_VERSION_SHORT}/site-packages /usr/local/lib/python${PYTHON_VERSION_SHORT}/site-packages
|
COPY --from=builder /venv /venv
|
||||||
COPY --from=builder /usr/local/bin /usr/local/bin
|
|
||||||
|
|
||||||
# Copy application code (already owned by appuser)
|
# Copy application code
|
||||||
COPY --chown=appuser:appuser alfred/ ./alfred
|
COPY --chown=appuser:appuser alfred/ ./alfred
|
||||||
COPY --chown=appuser:appuser scripts/ ./scripts
|
COPY --chown=appuser:appuser scripts/ ./scripts
|
||||||
COPY --chown=appuser:appuser .env.example ./
|
COPY --chown=appuser:appuser .env.example ./
|
||||||
COPY --chown=appuser:appuser pyproject.toml ./
|
COPY --chown=appuser:appuser pyproject.toml ./
|
||||||
|
|
||||||
# Create volumes for persistent data
|
|
||||||
VOLUME ["/data", "/logs"]
|
VOLUME ["/data", "/logs"]
|
||||||
|
|
||||||
# Expose port
|
|
||||||
EXPOSE 8000
|
EXPOSE 8000
|
||||||
|
|
||||||
# Health check
|
|
||||||
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \
|
||||||
CMD python -c "import requests; requests.get('http://localhost:8000/health', timeout=5).raise_for_status()" || exit 1
|
CMD python -c "import requests; requests.get('http://localhost:8000/health', timeout=5).raise_for_status()" || exit 1
|
||||||
|
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
.DEFAULT_GOAL := help
|
.DEFAULT_GOAL := help
|
||||||
|
|
||||||
# --- Load Config from pyproject.toml ---
|
# --- Load Config from pyproject.toml ---
|
||||||
|
export
|
||||||
-include .env.make
|
-include .env.make
|
||||||
|
|
||||||
# --- Profiles management ---
|
# --- Profiles management ---
|
||||||
@@ -9,10 +10,12 @@ p ?= full
|
|||||||
PROFILES_PARAM := COMPOSE_PROFILES=$(p)
|
PROFILES_PARAM := COMPOSE_PROFILES=$(p)
|
||||||
|
|
||||||
# --- Commands ---
|
# --- Commands ---
|
||||||
DOCKER_COMPOSE := docker compose
|
DOCKER_COMPOSE := docker compose \
|
||||||
DOCKER_BUILD := docker build --no-cache \
|
--env-file .env.alfred \
|
||||||
|
--env-file .env.secrets \
|
||||||
|
--env-file .env.make
|
||||||
|
DOCKER_BUILD := DOCKER_BUILDKIT=1 docker build \
|
||||||
--build-arg PYTHON_VERSION=$(PYTHON_VERSION) \
|
--build-arg PYTHON_VERSION=$(PYTHON_VERSION) \
|
||||||
--build-arg PYTHON_VERSION_SHORT=$(PYTHON_VERSION_SHORT) \
|
|
||||||
--build-arg UV_VERSION=$(UV_VERSION)
|
--build-arg UV_VERSION=$(UV_VERSION)
|
||||||
|
|
||||||
# --- Phony ---
|
# --- Phony ---
|
||||||
@@ -74,7 +77,7 @@ build-test: .env.make
|
|||||||
|
|
||||||
# --- Dependencies ---
|
# --- Dependencies ---
|
||||||
install:
|
install:
|
||||||
@echo "Installing dependencies with $(RUNNER)..."
|
@echo "Installing dependencies with uv..."
|
||||||
@uv install \
|
@uv install \
|
||||||
&& echo "✓ Dependencies installed" \
|
&& echo "✓ Dependencies installed" \
|
||||||
|| (echo "✗ Installation failed" && exit 1)
|
|| (echo "✗ Installation failed" && exit 1)
|
||||||
@@ -86,7 +89,7 @@ install-hooks:
|
|||||||
|| (echo "✗ Hook installation failed" && exit 1)
|
|| (echo "✗ Hook installation failed" && exit 1)
|
||||||
|
|
||||||
update:
|
update:
|
||||||
@echo "Updating dependencies with $(RUNNER)..."
|
@echo "Updating dependencies with uv..."
|
||||||
@uv update \
|
@uv update \
|
||||||
&& echo "✓ Dependencies updated" \
|
&& echo "✓ Dependencies updated" \
|
||||||
|| (echo "✗ Update failed" && exit 1)
|
|| (echo "✗ Update failed" && exit 1)
|
||||||
@@ -112,7 +115,7 @@ lint:
|
|||||||
|
|
||||||
format:
|
format:
|
||||||
@echo "Formatting code..."
|
@echo "Formatting code..."
|
||||||
@uv run ruff format . && $(RUNNER) run ruff check --fix . \
|
@uv run ruff format . && uv run ruff check --fix . \
|
||||||
&& echo "✓ Code formatted" \
|
&& echo "✓ Code formatted" \
|
||||||
|| (echo "✗ Formatting failed" && exit 1)
|
|| (echo "✗ Formatting failed" && exit 1)
|
||||||
|
|
||||||
@@ -138,8 +141,7 @@ major minor patch: _check-main
|
|||||||
_ci-dump-config:
|
_ci-dump-config:
|
||||||
@echo "image_name=$(IMAGE_NAME)"
|
@echo "image_name=$(IMAGE_NAME)"
|
||||||
@echo "python_version=$(PYTHON_VERSION)"
|
@echo "python_version=$(PYTHON_VERSION)"
|
||||||
@echo "python_version_short=$(PYTHON_VERSION_SHORT)"
|
@echo "uv_version=$(UV_VERSION)"
|
||||||
@echo "runner=$(RUNNER)"
|
|
||||||
@echo "service_name=$(SERVICE_NAME)"
|
@echo "service_name=$(SERVICE_NAME)"
|
||||||
|
|
||||||
_ci-run-tests:build-test
|
_ci-run-tests:build-test
|
||||||
@@ -176,7 +178,7 @@ help:
|
|||||||
@echo ""
|
@echo ""
|
||||||
@echo "Dev & Quality:"
|
@echo "Dev & Quality:"
|
||||||
@echo " setup Bootstrap .env and security keys"
|
@echo " setup Bootstrap .env and security keys"
|
||||||
@echo " install Install dependencies via $(RUNNER)"
|
@echo " install Install dependencies via uv"
|
||||||
@echo " test Run pytest suite"
|
@echo " test Run pytest suite"
|
||||||
@echo " coverage Run tests and generate HTML report"
|
@echo " coverage Run tests and generate HTML report"
|
||||||
@echo " lint/format Quality and style checks"
|
@echo " lint/format Quality and style checks"
|
||||||
|
|||||||
+1
-1
@@ -29,7 +29,7 @@ app = FastAPI(
|
|||||||
version="0.2.0",
|
version="0.2.0",
|
||||||
)
|
)
|
||||||
|
|
||||||
memory_path = Path(settings.data_storage) / "memory"
|
memory_path = Path(settings.data_storage_dir) / "memory"
|
||||||
init_memory(storage_dir=str(memory_path))
|
init_memory(storage_dir=str(memory_path))
|
||||||
logger.info(f"Memory context initialized (path: {memory_path})")
|
logger.info(f"Memory context initialized (path: {memory_path})")
|
||||||
|
|
||||||
|
|||||||
+20
-56
@@ -1,14 +1,14 @@
|
|||||||
"""
|
"""
|
||||||
Application settings — Alfred only.
|
Application settings — Alfred only.
|
||||||
|
|
||||||
Loaded from .env.alfred and .env.secrets (via docker-compose env_file).
|
Only declares what Alfred's Python code actually consumes.
|
||||||
At runtime, all variables are already in the environment — pydantic-settings
|
Everything else (.env.alfred, .env.secrets) is loaded by Docker Compose
|
||||||
picks them up automatically.
|
for other services and ignored here via extra="ignore".
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from pathlib import Path
|
from pathlib import Path
|
||||||
|
|
||||||
from pydantic import Field, computed_field, field_validator
|
from pydantic import field_validator
|
||||||
from pydantic_settings import BaseSettings, SettingsConfigDict
|
from pydantic_settings import BaseSettings, SettingsConfigDict
|
||||||
|
|
||||||
BASE_DIR = Path(__file__).resolve().parent.parent
|
BASE_DIR = Path(__file__).resolve().parent.parent
|
||||||
@@ -20,86 +20,38 @@ class ConfigurationError(Exception):
|
|||||||
|
|
||||||
class Settings(BaseSettings):
|
class Settings(BaseSettings):
|
||||||
model_config = SettingsConfigDict(
|
model_config = SettingsConfigDict(
|
||||||
env_file=[BASE_DIR / ".env.alfred", BASE_DIR / ".env.secrets"],
|
env_file=[BASE_DIR / ".env.alfred", BASE_DIR / ".env.secrets", BASE_DIR / ".env.make"],
|
||||||
env_file_encoding="utf-8",
|
env_file_encoding="utf-8",
|
||||||
extra="ignore",
|
extra="ignore",
|
||||||
case_sensitive=False,
|
case_sensitive=False,
|
||||||
)
|
)
|
||||||
|
|
||||||
# --- APP ---
|
# --- APP ---
|
||||||
host: str = "0.0.0.0"
|
|
||||||
port: int = 3080
|
|
||||||
max_history_messages: int = 10
|
max_history_messages: int = 10
|
||||||
max_tool_iterations: int = 10
|
max_tool_iterations: int = 10
|
||||||
request_timeout: int = 30
|
request_timeout: int = 30
|
||||||
llm_temperature: float = 0.2
|
llm_temperature: float = 0.2
|
||||||
data_storage_dir: str = "data"
|
data_storage_dir: str = "data"
|
||||||
|
|
||||||
# --- DATABASE ---
|
# --- BUILD ---
|
||||||
mongo_host: str = "mongodb"
|
alfred_version: str | None = None
|
||||||
mongo_port: int = 27017
|
|
||||||
mongo_user: str = "alfred"
|
|
||||||
mongo_password: str = Field(repr=False)
|
|
||||||
mongo_db_name: str = "LibreChat"
|
|
||||||
|
|
||||||
@computed_field(repr=False)
|
|
||||||
@property
|
|
||||||
def mongo_uri(self) -> str:
|
|
||||||
return (
|
|
||||||
f"mongodb://{self.mongo_user}:{self.mongo_password}"
|
|
||||||
f"@{self.mongo_host}:{self.mongo_port}/{self.mongo_db_name}"
|
|
||||||
f"?authSource=admin"
|
|
||||||
)
|
|
||||||
|
|
||||||
postgres_host: str = "vectordb"
|
|
||||||
postgres_port: int = 5432
|
|
||||||
postgres_user: str = "alfred"
|
|
||||||
postgres_password: str = Field(repr=False)
|
|
||||||
postgres_db_name: str = "alfred"
|
|
||||||
|
|
||||||
@computed_field(repr=False)
|
|
||||||
@property
|
|
||||||
def postgres_uri(self) -> str:
|
|
||||||
return (
|
|
||||||
f"postgresql://{self.postgres_user}:{self.postgres_password}"
|
|
||||||
f"@{self.postgres_host}:{self.postgres_port}/{self.postgres_db_name}"
|
|
||||||
)
|
|
||||||
|
|
||||||
# --- LLM ---
|
# --- LLM ---
|
||||||
default_llm_provider: str = "local"
|
default_llm_provider: str = "local"
|
||||||
ollama_base_url: str = "http://ollama:11434"
|
ollama_base_url: str = "http://ollama:11434"
|
||||||
# Models: ...
|
|
||||||
ollama_model: str = "llama3.3:latest"
|
ollama_model: str = "llama3.3:latest"
|
||||||
deepseek_base_url: str = "https://api.deepseek.com"
|
deepseek_base_url: str = "https://api.deepseek.com"
|
||||||
deepseek_model: str = "deepseek-chat"
|
deepseek_model: str = "deepseek-chat"
|
||||||
|
|
||||||
# --- API KEYS ---
|
# --- API KEYS ---
|
||||||
tmdb_api_key: str | None = None
|
tmdb_api_key: str | None = None
|
||||||
|
tmdb_base_url: str = "https://api.themoviedb.org/3"
|
||||||
deepseek_api_key: str | None = None
|
deepseek_api_key: str | None = None
|
||||||
openai_api_key: str | None = None
|
openai_api_key: str | None = None
|
||||||
anthropic_api_key: str | None = None
|
anthropic_api_key: str | None = None
|
||||||
google_api_key: str | None = None
|
google_api_key: str | None = None
|
||||||
kimi_api_key: str | None = None
|
kimi_api_key: str | None = None
|
||||||
|
|
||||||
# --- EXTERNAL SERVICES ---
|
|
||||||
tmdb_base_url: str = "https://api.themoviedb.org/3"
|
|
||||||
qbittorrent_url: str = "http://qbittorrent:16140"
|
|
||||||
qbittorrent_username: str = "admin"
|
|
||||||
qbittorrent_password: str = Field(repr=False)
|
|
||||||
|
|
||||||
# --- RAG ---
|
|
||||||
rag_enabled: bool = True
|
|
||||||
rag_api_url: str = "http://rag_api:8000"
|
|
||||||
embeddings_provider: str = "ollama"
|
|
||||||
# Models: ...
|
|
||||||
embeddings_model: str = "nomic-embed-text"
|
|
||||||
|
|
||||||
# --- MEILISEARCH ---
|
|
||||||
meili_enabled: bool = False
|
|
||||||
meili_no_analytics: bool = True
|
|
||||||
meili_host: str = "http://meilisearch:7700"
|
|
||||||
meili_master_key: str = Field(repr=False)
|
|
||||||
|
|
||||||
# --- VALIDATORS ---
|
# --- VALIDATORS ---
|
||||||
@field_validator("llm_temperature")
|
@field_validator("llm_temperature")
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -129,5 +81,17 @@ class Settings(BaseSettings):
|
|||||||
def is_deepseek_configured(self) -> bool:
|
def is_deepseek_configured(self) -> bool:
|
||||||
return bool(self.deepseek_api_key)
|
return bool(self.deepseek_api_key)
|
||||||
|
|
||||||
|
def is_openai_configured(self) -> bool:
|
||||||
|
return bool(self.openai_api_key)
|
||||||
|
|
||||||
|
def is_anthropic_configured(self) -> bool:
|
||||||
|
return bool(self.anthropic_api_key)
|
||||||
|
|
||||||
|
def is_google_configured(self) -> bool:
|
||||||
|
return bool(self.google_api_key)
|
||||||
|
|
||||||
|
def is_kimi_configured(self) -> bool:
|
||||||
|
return bool(self.kimi_api_key)
|
||||||
|
|
||||||
|
|
||||||
settings = Settings()
|
settings = Settings()
|
||||||
|
|||||||
+7
-6
@@ -8,7 +8,6 @@ services:
|
|||||||
target: builder
|
target: builder
|
||||||
args:
|
args:
|
||||||
PYTHON_VERSION: ${PYTHON_VERSION}
|
PYTHON_VERSION: ${PYTHON_VERSION}
|
||||||
PYTHON_VERSION_SHORT: ${PYTHON_VERSION_SHORT}
|
|
||||||
UV_VERSION: ${UV_VERSION}
|
UV_VERSION: ${UV_VERSION}
|
||||||
command: python scripts/bootstrap.py
|
command: python scripts/bootstrap.py
|
||||||
networks:
|
networks:
|
||||||
@@ -22,7 +21,6 @@ services:
|
|||||||
context: .
|
context: .
|
||||||
args:
|
args:
|
||||||
PYTHON_VERSION: ${PYTHON_VERSION}
|
PYTHON_VERSION: ${PYTHON_VERSION}
|
||||||
PYTHON_VERSION_SHORT: ${PYTHON_VERSION_SHORT}
|
|
||||||
UV_VERSION: ${UV_VERSION}
|
UV_VERSION: ${UV_VERSION}
|
||||||
depends_on:
|
depends_on:
|
||||||
alfred-init:
|
alfred-init:
|
||||||
@@ -33,13 +31,15 @@ services:
|
|||||||
required: true
|
required: true
|
||||||
- path: .env.secrets
|
- path: .env.secrets
|
||||||
required: true
|
required: true
|
||||||
|
- path: .env.make
|
||||||
|
required: true
|
||||||
volumes:
|
volumes:
|
||||||
- ./data:/data
|
- ./data:/data
|
||||||
- ./logs:/logs
|
- ./logs:/logs
|
||||||
# TODO: Hot reload (comment out in production)
|
# TODO: Hot reload (comment out in production)
|
||||||
#- ./alfred:/home/appuser/alfred
|
- ./alfred:/home/appuser/alfred
|
||||||
command: >
|
command: >
|
||||||
sh -c "python -u -m uvicorn alfred.app:app --host 0.0.0.0 --port 8000 2>&1 | tee -a /logs/alfred.log"
|
sh -c "python -u -m uvicorn alfred.app:app --host 0.0.0.0 --port 8000 --reload 2>&1 | tee -a /logs/alfred.log"
|
||||||
networks:
|
networks:
|
||||||
- alfred-net
|
- alfred-net
|
||||||
|
|
||||||
@@ -89,9 +89,10 @@ services:
|
|||||||
- path: .env.secrets
|
- path: .env.secrets
|
||||||
required: true
|
required: true
|
||||||
environment:
|
environment:
|
||||||
# Remap value name
|
|
||||||
- MONGO_INITDB_ROOT_USERNAME=${MONGO_USER}
|
- MONGO_INITDB_ROOT_USERNAME=${MONGO_USER}
|
||||||
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD}
|
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD}
|
||||||
|
# Fix MongoDB + Linux kernel >= 6.19
|
||||||
|
- GLIBC_TUNABLES=glibc.cpu.hwcaps=-SHSTK
|
||||||
ports:
|
ports:
|
||||||
- "${MONGO_PORT}:${MONGO_PORT}"
|
- "${MONGO_PORT}:${MONGO_PORT}"
|
||||||
volumes:
|
volumes:
|
||||||
@@ -99,7 +100,7 @@ services:
|
|||||||
- ./mongod.conf:/etc/mongod.conf:ro
|
- ./mongod.conf:/etc/mongod.conf:ro
|
||||||
command: ["mongod", "--config", "/etc/mongod.conf"]
|
command: ["mongod", "--config", "/etc/mongod.conf"]
|
||||||
healthcheck:
|
healthcheck:
|
||||||
test: mongosh --quiet -u "${MONGO_USER}" -p "${MONGO_PASSWORD}" --authenticationDatabase admin --eval "db.adminCommand('ping')"
|
test: bash -c "echo > /dev/tcp/localhost/27017"
|
||||||
interval: 10s
|
interval: 10s
|
||||||
timeout: 5s
|
timeout: 5s
|
||||||
retries: 5
|
retries: 5
|
||||||
|
|||||||
@@ -0,0 +1,878 @@
|
|||||||
|
#=====================================================================#
|
||||||
|
# LibreChat Configuration #
|
||||||
|
#=====================================================================#
|
||||||
|
# Please refer to the reference documentation for assistance #
|
||||||
|
# with configuring your LibreChat environment. #
|
||||||
|
# #
|
||||||
|
# https://www.librechat.ai/docs/configuration/dotenv #
|
||||||
|
#=====================================================================#
|
||||||
|
|
||||||
|
#==================================================#
|
||||||
|
# Server Configuration #
|
||||||
|
#==================================================#
|
||||||
|
|
||||||
|
HOST=localhost
|
||||||
|
PORT=3080
|
||||||
|
|
||||||
|
MONGO_URI=mongodb://127.0.0.1:27017/LibreChat
|
||||||
|
#The maximum number of connections in the connection pool. */
|
||||||
|
MONGO_MAX_POOL_SIZE=
|
||||||
|
#The minimum number of connections in the connection pool. */
|
||||||
|
MONGO_MIN_POOL_SIZE=
|
||||||
|
#The maximum number of connections that may be in the process of being established concurrently by the connection pool. */
|
||||||
|
MONGO_MAX_CONNECTING=
|
||||||
|
#The maximum number of milliseconds that a connection can remain idle in the pool before being removed and closed. */
|
||||||
|
MONGO_MAX_IDLE_TIME_MS=
|
||||||
|
#The maximum time in milliseconds that a thread can wait for a connection to become available. */
|
||||||
|
MONGO_WAIT_QUEUE_TIMEOUT_MS=
|
||||||
|
# Set to false to disable automatic index creation for all models associated with this connection. */
|
||||||
|
MONGO_AUTO_INDEX=
|
||||||
|
# Set to `false` to disable Mongoose automatically calling `createCollection()` on every model created on this connection. */
|
||||||
|
MONGO_AUTO_CREATE=
|
||||||
|
|
||||||
|
DOMAIN_CLIENT=http://localhost:3080
|
||||||
|
DOMAIN_SERVER=http://localhost:3080
|
||||||
|
|
||||||
|
NO_INDEX=true
|
||||||
|
# Use the address that is at most n number of hops away from the Express application.
|
||||||
|
# req.socket.remoteAddress is the first hop, and the rest are looked for in the X-Forwarded-For header from right to left.
|
||||||
|
# A value of 0 means that the first untrusted address would be req.socket.remoteAddress, i.e. there is no reverse proxy.
|
||||||
|
# Defaulted to 1.
|
||||||
|
TRUST_PROXY=1
|
||||||
|
|
||||||
|
# Minimum password length for user authentication
|
||||||
|
# Default: 8
|
||||||
|
# Note: When using LDAP authentication, you may want to set this to 1
|
||||||
|
# to bypass local password validation, as LDAP servers handle their own
|
||||||
|
# password policies.
|
||||||
|
# MIN_PASSWORD_LENGTH=8
|
||||||
|
|
||||||
|
# When enabled, the app will continue running after encountering uncaught exceptions
|
||||||
|
# instead of exiting the process. Not recommended for production unless necessary.
|
||||||
|
# CONTINUE_ON_UNCAUGHT_EXCEPTION=false
|
||||||
|
|
||||||
|
#===============#
|
||||||
|
# JSON Logging #
|
||||||
|
#===============#
|
||||||
|
|
||||||
|
# Use when process console logs in cloud deployment like GCP/AWS
|
||||||
|
CONSOLE_JSON=false
|
||||||
|
|
||||||
|
#===============#
|
||||||
|
# Debug Logging #
|
||||||
|
#===============#
|
||||||
|
|
||||||
|
DEBUG_LOGGING=true
|
||||||
|
DEBUG_CONSOLE=false
|
||||||
|
# Set to true to enable agent debug logging
|
||||||
|
AGENT_DEBUG_LOGGING=false
|
||||||
|
|
||||||
|
# Enable memory diagnostics (logs heap/RSS snapshots every 60s, auto-enabled with --inspect)
|
||||||
|
# MEM_DIAG=true
|
||||||
|
|
||||||
|
#=============#
|
||||||
|
# Permissions #
|
||||||
|
#=============#
|
||||||
|
|
||||||
|
# UID=1000
|
||||||
|
# GID=1000
|
||||||
|
|
||||||
|
#==============#
|
||||||
|
# Node Options #
|
||||||
|
#==============#
|
||||||
|
|
||||||
|
# NOTE: NODE_MAX_OLD_SPACE_SIZE is NOT recognized by Node.js directly.
|
||||||
|
# This variable is used as a build argument for Docker or CI/CD workflows,
|
||||||
|
# and is NOT used by Node.js to set the heap size at runtime.
|
||||||
|
# To configure Node.js memory, use NODE_OPTIONS, e.g.:
|
||||||
|
# NODE_OPTIONS="--max-old-space-size=6144"
|
||||||
|
# See: https://nodejs.org/api/cli.html#--max-old-space-sizesize-in-mib
|
||||||
|
NODE_MAX_OLD_SPACE_SIZE=6144
|
||||||
|
|
||||||
|
#===============#
|
||||||
|
# Configuration #
|
||||||
|
#===============#
|
||||||
|
# Use an absolute path, a relative path, or a URL
|
||||||
|
|
||||||
|
# CONFIG_PATH="/alternative/path/to/librechat.yaml"
|
||||||
|
|
||||||
|
#==================#
|
||||||
|
# Langfuse Tracing #
|
||||||
|
#==================#
|
||||||
|
|
||||||
|
# Get Langfuse API keys for your project from the project settings page: https://cloud.langfuse.com
|
||||||
|
|
||||||
|
# LANGFUSE_PUBLIC_KEY=
|
||||||
|
# LANGFUSE_SECRET_KEY=
|
||||||
|
# LANGFUSE_BASE_URL=
|
||||||
|
|
||||||
|
#===================================================#
|
||||||
|
# Endpoints #
|
||||||
|
#===================================================#
|
||||||
|
|
||||||
|
# ENDPOINTS=openAI,assistants,azureOpenAI,google,anthropic
|
||||||
|
|
||||||
|
PROXY=
|
||||||
|
|
||||||
|
#===================================#
|
||||||
|
# Known Endpoints - librechat.yaml #
|
||||||
|
#===================================#
|
||||||
|
# https://www.librechat.ai/docs/configuration/librechat_yaml/ai_endpoints
|
||||||
|
|
||||||
|
# ANYSCALE_API_KEY=
|
||||||
|
# APIPIE_API_KEY=
|
||||||
|
# COHERE_API_KEY=
|
||||||
|
# DEEPSEEK_API_KEY=
|
||||||
|
# DATABRICKS_API_KEY=
|
||||||
|
# FIREWORKS_API_KEY=
|
||||||
|
# GROQ_API_KEY=
|
||||||
|
# HUGGINGFACE_TOKEN=
|
||||||
|
# MISTRAL_API_KEY=
|
||||||
|
# OPENROUTER_KEY=
|
||||||
|
# PERPLEXITY_API_KEY=
|
||||||
|
# SHUTTLEAI_API_KEY=
|
||||||
|
# TOGETHERAI_API_KEY=
|
||||||
|
# UNIFY_API_KEY=
|
||||||
|
# XAI_API_KEY=
|
||||||
|
|
||||||
|
#============#
|
||||||
|
# Anthropic #
|
||||||
|
#============#
|
||||||
|
|
||||||
|
ANTHROPIC_API_KEY=user_provided
|
||||||
|
# ANTHROPIC_MODELS=claude-sonnet-4-6,claude-opus-4-6,claude-opus-4-20250514,claude-sonnet-4-20250514,claude-3-7-sonnet-20250219,claude-3-5-sonnet-20241022,claude-3-5-haiku-20241022,claude-3-opus-20240229,claude-3-sonnet-20240229,claude-3-haiku-20240307
|
||||||
|
# ANTHROPIC_REVERSE_PROXY=
|
||||||
|
|
||||||
|
# Set to true to use Anthropic models through Google Vertex AI instead of direct API
|
||||||
|
# ANTHROPIC_USE_VERTEX=
|
||||||
|
# ANTHROPIC_VERTEX_REGION=us-east5
|
||||||
|
|
||||||
|
#============#
|
||||||
|
# Azure #
|
||||||
|
#============#
|
||||||
|
|
||||||
|
# Note: these variables are DEPRECATED
|
||||||
|
# Use the `librechat.yaml` configuration for `azureOpenAI` instead
|
||||||
|
# You may also continue to use them if you opt out of using the `librechat.yaml` configuration
|
||||||
|
|
||||||
|
# AZURE_OPENAI_DEFAULT_MODEL=gpt-3.5-turbo # Deprecated
|
||||||
|
# AZURE_OPENAI_MODELS=gpt-3.5-turbo,gpt-4 # Deprecated
|
||||||
|
# AZURE_USE_MODEL_AS_DEPLOYMENT_NAME=TRUE # Deprecated
|
||||||
|
# AZURE_API_KEY= # Deprecated
|
||||||
|
# AZURE_OPENAI_API_INSTANCE_NAME= # Deprecated
|
||||||
|
# AZURE_OPENAI_API_DEPLOYMENT_NAME= # Deprecated
|
||||||
|
# AZURE_OPENAI_API_VERSION= # Deprecated
|
||||||
|
# AZURE_OPENAI_API_COMPLETIONS_DEPLOYMENT_NAME= # Deprecated
|
||||||
|
# AZURE_OPENAI_API_EMBEDDINGS_DEPLOYMENT_NAME= # Deprecated
|
||||||
|
|
||||||
|
#=================#
|
||||||
|
# AWS Bedrock #
|
||||||
|
#=================#
|
||||||
|
|
||||||
|
# BEDROCK_AWS_DEFAULT_REGION=us-east-1 # A default region must be provided
|
||||||
|
# BEDROCK_AWS_ACCESS_KEY_ID=someAccessKey
|
||||||
|
# BEDROCK_AWS_SECRET_ACCESS_KEY=someSecretAccessKey
|
||||||
|
# BEDROCK_AWS_SESSION_TOKEN=someSessionToken
|
||||||
|
|
||||||
|
# Note: This example list is not meant to be exhaustive. If omitted, all known, supported model IDs will be included for you.
|
||||||
|
# BEDROCK_AWS_MODELS=anthropic.claude-sonnet-4-6,anthropic.claude-opus-4-6-v1,anthropic.claude-3-5-sonnet-20240620-v1:0,meta.llama3-1-8b-instruct-v1:0
|
||||||
|
# Cross-region inference model IDs: us.anthropic.claude-sonnet-4-6,us.anthropic.claude-opus-4-6-v1,global.anthropic.claude-opus-4-6-v1
|
||||||
|
|
||||||
|
# See all Bedrock model IDs here: https://docs.aws.amazon.com/bedrock/latest/userguide/model-ids.html#model-ids-arns
|
||||||
|
|
||||||
|
# Notes on specific models:
|
||||||
|
# The following models are not support due to not supporting streaming:
|
||||||
|
# ai21.j2-mid-v1
|
||||||
|
|
||||||
|
# The following models are not support due to not supporting conversation history:
|
||||||
|
# ai21.j2-ultra-v1, cohere.command-text-v14, cohere.command-light-text-v14
|
||||||
|
|
||||||
|
#============#
|
||||||
|
# Google #
|
||||||
|
#============#
|
||||||
|
|
||||||
|
GOOGLE_KEY=user_provided
|
||||||
|
|
||||||
|
# GOOGLE_REVERSE_PROXY=
|
||||||
|
# Some reverse proxies do not support the X-goog-api-key header, uncomment to pass the API key in Authorization header instead.
|
||||||
|
# GOOGLE_AUTH_HEADER=true
|
||||||
|
|
||||||
|
# Gemini API (AI Studio)
|
||||||
|
# GOOGLE_MODELS=gemini-3.1-pro-preview,gemini-3.1-pro-preview-customtools,gemini-3.1-flash-lite-preview,gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite,gemini-2.0-flash,gemini-2.0-flash-lite
|
||||||
|
|
||||||
|
# Vertex AI
|
||||||
|
# GOOGLE_MODELS=gemini-3.1-pro-preview,gemini-3.1-pro-preview-customtools,gemini-3.1-flash-lite-preview,gemini-2.5-pro,gemini-2.5-flash,gemini-2.5-flash-lite,gemini-2.0-flash-001,gemini-2.0-flash-lite-001
|
||||||
|
|
||||||
|
# GOOGLE_TITLE_MODEL=gemini-2.0-flash-lite-001
|
||||||
|
|
||||||
|
# Google Cloud region for Vertex AI (used by both chat and image generation)
|
||||||
|
# GOOGLE_LOC=us-central1
|
||||||
|
|
||||||
|
# Alternative region env var for Gemini Image Generation
|
||||||
|
# GOOGLE_CLOUD_LOCATION=global
|
||||||
|
|
||||||
|
# Vertex AI Service Account Configuration
|
||||||
|
# Path to your Google Cloud service account JSON file
|
||||||
|
# GOOGLE_SERVICE_KEY_FILE=/path/to/service-account.json
|
||||||
|
|
||||||
|
# Google Safety Settings
|
||||||
|
# NOTE: These settings apply to both Vertex AI and Gemini API (AI Studio)
|
||||||
|
#
|
||||||
|
# For Vertex AI:
|
||||||
|
# To use the BLOCK_NONE setting, you need either:
|
||||||
|
# (a) Access through an allowlist via your Google account team, or
|
||||||
|
# (b) Switch to monthly invoiced billing: https://cloud.google.com/billing/docs/how-to/invoiced-billing
|
||||||
|
#
|
||||||
|
# For Gemini API (AI Studio):
|
||||||
|
# BLOCK_NONE is available by default, no special account requirements.
|
||||||
|
#
|
||||||
|
# Available options: BLOCK_NONE, BLOCK_ONLY_HIGH, BLOCK_MEDIUM_AND_ABOVE, BLOCK_LOW_AND_ABOVE
|
||||||
|
#
|
||||||
|
# GOOGLE_SAFETY_SEXUALLY_EXPLICIT=BLOCK_ONLY_HIGH
|
||||||
|
# GOOGLE_SAFETY_HATE_SPEECH=BLOCK_ONLY_HIGH
|
||||||
|
# GOOGLE_SAFETY_HARASSMENT=BLOCK_ONLY_HIGH
|
||||||
|
# GOOGLE_SAFETY_DANGEROUS_CONTENT=BLOCK_ONLY_HIGH
|
||||||
|
# GOOGLE_SAFETY_CIVIC_INTEGRITY=BLOCK_ONLY_HIGH
|
||||||
|
|
||||||
|
#========================#
|
||||||
|
# Gemini Image Generation #
|
||||||
|
#========================#
|
||||||
|
|
||||||
|
# Gemini Image Generation Tool (for Agents)
|
||||||
|
# Supports multiple authentication methods in priority order:
|
||||||
|
# 1. User-provided API key (via GUI)
|
||||||
|
# 2. GEMINI_API_KEY env var (admin-configured)
|
||||||
|
# 3. GOOGLE_KEY env var (shared with Google chat endpoint)
|
||||||
|
# 4. Vertex AI service account (via GOOGLE_SERVICE_KEY_FILE)
|
||||||
|
|
||||||
|
# Option A: Use dedicated Gemini API key for image generation
|
||||||
|
# GEMINI_API_KEY=your-gemini-api-key
|
||||||
|
|
||||||
|
# Vertex AI model for image generation (defaults to gemini-2.5-flash-image)
|
||||||
|
# GEMINI_IMAGE_MODEL=gemini-2.5-flash-image
|
||||||
|
|
||||||
|
#============#
|
||||||
|
# OpenAI #
|
||||||
|
#============#
|
||||||
|
|
||||||
|
OPENAI_API_KEY=user_provided
|
||||||
|
# OPENAI_MODELS=gpt-5,gpt-5-codex,gpt-5-mini,gpt-5-nano,o3-pro,o3,o4-mini,gpt-4.1,gpt-4.1-mini,gpt-4.1-nano,o3-mini,o1-pro,o1,gpt-4o,gpt-4o-mini
|
||||||
|
|
||||||
|
DEBUG_OPENAI=false
|
||||||
|
|
||||||
|
# TITLE_CONVO=false
|
||||||
|
# OPENAI_TITLE_MODEL=gpt-4o-mini
|
||||||
|
|
||||||
|
# OPENAI_SUMMARIZE=true
|
||||||
|
# OPENAI_SUMMARY_MODEL=gpt-4o-mini
|
||||||
|
|
||||||
|
# OPENAI_FORCE_PROMPT=true
|
||||||
|
|
||||||
|
# OPENAI_REVERSE_PROXY=
|
||||||
|
|
||||||
|
# OPENAI_ORGANIZATION=
|
||||||
|
|
||||||
|
#====================#
|
||||||
|
# Assistants API #
|
||||||
|
#====================#
|
||||||
|
|
||||||
|
ASSISTANTS_API_KEY=user_provided
|
||||||
|
# ASSISTANTS_BASE_URL=
|
||||||
|
# ASSISTANTS_MODELS=gpt-4o,gpt-4o-mini,gpt-3.5-turbo-0125,gpt-3.5-turbo-16k-0613,gpt-3.5-turbo-16k,gpt-3.5-turbo,gpt-4,gpt-4-0314,gpt-4-32k-0314,gpt-4-0613,gpt-3.5-turbo-0613,gpt-3.5-turbo-1106,gpt-4-0125-preview,gpt-4-turbo-preview,gpt-4-1106-preview
|
||||||
|
|
||||||
|
#==========================#
|
||||||
|
# Azure Assistants API #
|
||||||
|
#==========================#
|
||||||
|
|
||||||
|
# Note: You should map your credentials with custom variables according to your Azure OpenAI Configuration
|
||||||
|
# The models for Azure Assistants are also determined by your Azure OpenAI configuration.
|
||||||
|
|
||||||
|
# More info, including how to enable use of Assistants with Azure here:
|
||||||
|
# https://www.librechat.ai/docs/configuration/librechat_yaml/ai_endpoints/azure#using-assistants-with-azure
|
||||||
|
|
||||||
|
CREDS_KEY=f34be427ebb29de8d88c107a71546019685ed8b241d8f2ed00c3df97ad2566f0
|
||||||
|
CREDS_IV=e2341419ec3dd3d19b13a1a87fafcbfb
|
||||||
|
|
||||||
|
# Azure AI Search
|
||||||
|
#-----------------
|
||||||
|
AZURE_AI_SEARCH_SERVICE_ENDPOINT=
|
||||||
|
AZURE_AI_SEARCH_INDEX_NAME=
|
||||||
|
AZURE_AI_SEARCH_API_KEY=
|
||||||
|
|
||||||
|
AZURE_AI_SEARCH_API_VERSION=
|
||||||
|
AZURE_AI_SEARCH_SEARCH_OPTION_QUERY_TYPE=
|
||||||
|
AZURE_AI_SEARCH_SEARCH_OPTION_TOP=
|
||||||
|
AZURE_AI_SEARCH_SEARCH_OPTION_SELECT=
|
||||||
|
|
||||||
|
# OpenAI Image Tools Customization
|
||||||
|
#----------------
|
||||||
|
# IMAGE_GEN_OAI_API_KEY= # Create or reuse OpenAI API key for image generation tool
|
||||||
|
# IMAGE_GEN_OAI_BASEURL= # Custom OpenAI base URL for image generation tool
|
||||||
|
# IMAGE_GEN_OAI_AZURE_API_VERSION= # Custom Azure OpenAI deployments
|
||||||
|
# IMAGE_GEN_OAI_MODEL=gpt-image-1 # OpenAI image model (e.g., gpt-image-1, gpt-image-1.5)
|
||||||
|
# IMAGE_GEN_OAI_DESCRIPTION=
|
||||||
|
# IMAGE_GEN_OAI_DESCRIPTION_WITH_FILES=Custom description for image generation tool when files are present
|
||||||
|
# IMAGE_GEN_OAI_DESCRIPTION_NO_FILES=Custom description for image generation tool when no files are present
|
||||||
|
# IMAGE_EDIT_OAI_DESCRIPTION=Custom description for image editing tool
|
||||||
|
# IMAGE_GEN_OAI_PROMPT_DESCRIPTION=Custom prompt description for image generation tool
|
||||||
|
# IMAGE_EDIT_OAI_PROMPT_DESCRIPTION=Custom prompt description for image editing tool
|
||||||
|
|
||||||
|
# DALL·E
|
||||||
|
#----------------
|
||||||
|
# DALLE_API_KEY=
|
||||||
|
# DALLE3_API_KEY=
|
||||||
|
# DALLE2_API_KEY=
|
||||||
|
# DALLE3_SYSTEM_PROMPT=
|
||||||
|
# DALLE2_SYSTEM_PROMPT=
|
||||||
|
# DALLE_REVERSE_PROXY=
|
||||||
|
# DALLE3_BASEURL=
|
||||||
|
# DALLE2_BASEURL=
|
||||||
|
|
||||||
|
# DALL·E (via Azure OpenAI)
|
||||||
|
# Note: requires some of the variables above to be set
|
||||||
|
#----------------
|
||||||
|
# DALLE3_AZURE_API_VERSION=
|
||||||
|
# DALLE2_AZURE_API_VERSION=
|
||||||
|
|
||||||
|
# Flux
|
||||||
|
#-----------------
|
||||||
|
FLUX_API_BASE_URL=https://api.us1.bfl.ai
|
||||||
|
# FLUX_API_BASE_URL = 'https://api.bfl.ml';
|
||||||
|
|
||||||
|
# Get your API key at https://api.us1.bfl.ai/auth/profile
|
||||||
|
# FLUX_API_KEY=
|
||||||
|
|
||||||
|
# Google
|
||||||
|
#-----------------
|
||||||
|
GOOGLE_SEARCH_API_KEY=
|
||||||
|
GOOGLE_CSE_ID=
|
||||||
|
|
||||||
|
# Stable Diffusion
|
||||||
|
#-----------------
|
||||||
|
SD_WEBUI_URL=http://host.docker.internal:7860
|
||||||
|
|
||||||
|
# Tavily
|
||||||
|
#-----------------
|
||||||
|
TAVILY_API_KEY=
|
||||||
|
|
||||||
|
# Traversaal
|
||||||
|
#-----------------
|
||||||
|
TRAVERSAAL_API_KEY=
|
||||||
|
|
||||||
|
# WolframAlpha
|
||||||
|
#-----------------
|
||||||
|
WOLFRAM_APP_ID=
|
||||||
|
|
||||||
|
# Zapier
|
||||||
|
#-----------------
|
||||||
|
ZAPIER_NLA_API_KEY=
|
||||||
|
|
||||||
|
#==================================================#
|
||||||
|
# Search #
|
||||||
|
#==================================================#
|
||||||
|
|
||||||
|
SEARCH=true
|
||||||
|
MEILI_NO_ANALYTICS=true
|
||||||
|
MEILI_HOST=http://0.0.0.0:7700
|
||||||
|
MEILI_MASTER_KEY=DrhYf7zENyR6AlUCKmnz0eYASOQdl6zxH7s7MKFSfFCt
|
||||||
|
|
||||||
|
# Optional: Disable indexing, useful in a multi-node setup
|
||||||
|
# where only one instance should perform an index sync.
|
||||||
|
# MEILI_NO_SYNC=true
|
||||||
|
|
||||||
|
#==================================================#
|
||||||
|
# Speech to Text & Text to Speech #
|
||||||
|
#==================================================#
|
||||||
|
|
||||||
|
STT_API_KEY=
|
||||||
|
TTS_API_KEY=
|
||||||
|
|
||||||
|
#==================================================#
|
||||||
|
# RAG #
|
||||||
|
#==================================================#
|
||||||
|
# More info: https://www.librechat.ai/docs/configuration/rag_api
|
||||||
|
|
||||||
|
# RAG_OPENAI_BASEURL=
|
||||||
|
# RAG_OPENAI_API_KEY=
|
||||||
|
# RAG_USE_FULL_CONTEXT=
|
||||||
|
# EMBEDDINGS_PROVIDER=openai
|
||||||
|
# EMBEDDINGS_MODEL=text-embedding-3-small
|
||||||
|
|
||||||
|
#===================================================#
|
||||||
|
# User System #
|
||||||
|
#===================================================#
|
||||||
|
|
||||||
|
#========================#
|
||||||
|
# Moderation #
|
||||||
|
#========================#
|
||||||
|
|
||||||
|
OPENAI_MODERATION=false
|
||||||
|
OPENAI_MODERATION_API_KEY=
|
||||||
|
# OPENAI_MODERATION_REVERSE_PROXY=
|
||||||
|
|
||||||
|
BAN_VIOLATIONS=true
|
||||||
|
BAN_DURATION=1000 * 60 * 60 * 2
|
||||||
|
BAN_INTERVAL=20
|
||||||
|
|
||||||
|
LOGIN_VIOLATION_SCORE=1
|
||||||
|
REGISTRATION_VIOLATION_SCORE=1
|
||||||
|
CONCURRENT_VIOLATION_SCORE=1
|
||||||
|
MESSAGE_VIOLATION_SCORE=1
|
||||||
|
NON_BROWSER_VIOLATION_SCORE=20
|
||||||
|
TTS_VIOLATION_SCORE=0
|
||||||
|
STT_VIOLATION_SCORE=0
|
||||||
|
FORK_VIOLATION_SCORE=0
|
||||||
|
IMPORT_VIOLATION_SCORE=0
|
||||||
|
FILE_UPLOAD_VIOLATION_SCORE=0
|
||||||
|
|
||||||
|
LOGIN_MAX=7
|
||||||
|
LOGIN_WINDOW=5
|
||||||
|
REGISTER_MAX=5
|
||||||
|
REGISTER_WINDOW=60
|
||||||
|
|
||||||
|
LIMIT_CONCURRENT_MESSAGES=true
|
||||||
|
CONCURRENT_MESSAGE_MAX=2
|
||||||
|
|
||||||
|
LIMIT_MESSAGE_IP=true
|
||||||
|
MESSAGE_IP_MAX=40
|
||||||
|
MESSAGE_IP_WINDOW=1
|
||||||
|
|
||||||
|
LIMIT_MESSAGE_USER=false
|
||||||
|
MESSAGE_USER_MAX=40
|
||||||
|
MESSAGE_USER_WINDOW=1
|
||||||
|
|
||||||
|
ILLEGAL_MODEL_REQ_SCORE=5
|
||||||
|
|
||||||
|
#========================#
|
||||||
|
# Balance #
|
||||||
|
#========================#
|
||||||
|
|
||||||
|
# CHECK_BALANCE=false
|
||||||
|
# START_BALANCE=20000 # note: the number of tokens that will be credited after registration.
|
||||||
|
|
||||||
|
#========================#
|
||||||
|
# Registration and Login #
|
||||||
|
#========================#
|
||||||
|
|
||||||
|
ALLOW_EMAIL_LOGIN=true
|
||||||
|
ALLOW_REGISTRATION=true
|
||||||
|
ALLOW_SOCIAL_LOGIN=false
|
||||||
|
ALLOW_SOCIAL_REGISTRATION=false
|
||||||
|
ALLOW_PASSWORD_RESET=false
|
||||||
|
# ALLOW_ACCOUNT_DELETION=true # note: enabled by default if omitted/commented out
|
||||||
|
ALLOW_UNVERIFIED_EMAIL_LOGIN=true
|
||||||
|
|
||||||
|
SESSION_EXPIRY=1000 * 60 * 15
|
||||||
|
REFRESH_TOKEN_EXPIRY=(1000 * 60 * 60 * 24) * 7
|
||||||
|
|
||||||
|
JWT_SECRET=16f8c0ef4a5d391b26034086c628469d3f9f497f08163ab9b40137092f2909ef
|
||||||
|
JWT_REFRESH_SECRET=eaa5191f2914e30b9387fd84e254e4ba6fc51b4654968a9b0803b456a54b8418
|
||||||
|
|
||||||
|
# Discord
|
||||||
|
DISCORD_CLIENT_ID=
|
||||||
|
DISCORD_CLIENT_SECRET=
|
||||||
|
DISCORD_CALLBACK_URL=/oauth/discord/callback
|
||||||
|
|
||||||
|
# Facebook
|
||||||
|
FACEBOOK_CLIENT_ID=
|
||||||
|
FACEBOOK_CLIENT_SECRET=
|
||||||
|
FACEBOOK_CALLBACK_URL=/oauth/facebook/callback
|
||||||
|
|
||||||
|
# GitHub
|
||||||
|
GITHUB_CLIENT_ID=
|
||||||
|
GITHUB_CLIENT_SECRET=
|
||||||
|
GITHUB_CALLBACK_URL=/oauth/github/callback
|
||||||
|
# GitHub Enterprise
|
||||||
|
# GITHUB_ENTERPRISE_BASE_URL=
|
||||||
|
# GITHUB_ENTERPRISE_USER_AGENT=
|
||||||
|
|
||||||
|
# Google
|
||||||
|
GOOGLE_CLIENT_ID=
|
||||||
|
GOOGLE_CLIENT_SECRET=
|
||||||
|
GOOGLE_CALLBACK_URL=/oauth/google/callback
|
||||||
|
|
||||||
|
# Apple
|
||||||
|
APPLE_CLIENT_ID=
|
||||||
|
APPLE_TEAM_ID=
|
||||||
|
APPLE_KEY_ID=
|
||||||
|
APPLE_PRIVATE_KEY_PATH=
|
||||||
|
APPLE_CALLBACK_URL=/oauth/apple/callback
|
||||||
|
|
||||||
|
# OpenID
|
||||||
|
OPENID_CLIENT_ID=
|
||||||
|
OPENID_CLIENT_SECRET=
|
||||||
|
OPENID_ISSUER=
|
||||||
|
OPENID_SESSION_SECRET=
|
||||||
|
OPENID_SCOPE="openid profile email"
|
||||||
|
OPENID_CALLBACK_URL=/oauth/openid/callback
|
||||||
|
OPENID_REQUIRED_ROLE=
|
||||||
|
OPENID_REQUIRED_ROLE_TOKEN_KIND=
|
||||||
|
OPENID_REQUIRED_ROLE_PARAMETER_PATH=
|
||||||
|
OPENID_ADMIN_ROLE=
|
||||||
|
OPENID_ADMIN_ROLE_PARAMETER_PATH=
|
||||||
|
OPENID_ADMIN_ROLE_TOKEN_KIND=
|
||||||
|
# Set to determine which user info property returned from OpenID Provider to store as the User's username
|
||||||
|
OPENID_USERNAME_CLAIM=
|
||||||
|
# Set to determine which user info property returned from OpenID Provider to store as the User's name
|
||||||
|
OPENID_NAME_CLAIM=
|
||||||
|
# Set to determine which user info claim to use as the email/identifier for user matching (e.g., "upn" for Entra ID)
|
||||||
|
# When not set, defaults to: email -> preferred_username -> upn
|
||||||
|
OPENID_EMAIL_CLAIM=
|
||||||
|
# Optional audience parameter for OpenID authorization requests
|
||||||
|
OPENID_AUDIENCE=
|
||||||
|
|
||||||
|
OPENID_BUTTON_LABEL=
|
||||||
|
OPENID_IMAGE_URL=
|
||||||
|
# Set to true to automatically redirect to the OpenID provider when a user visits the login page
|
||||||
|
# This will bypass the login form completely for users, only use this if OpenID is your only authentication method
|
||||||
|
OPENID_AUTO_REDIRECT=false
|
||||||
|
# Set to true to use PKCE (Proof Key for Code Exchange) for OpenID authentication
|
||||||
|
OPENID_USE_PKCE=false
|
||||||
|
#Set to true to reuse openid tokens for authentication management instead of using the mongodb session and the custom refresh token.
|
||||||
|
OPENID_REUSE_TOKENS=
|
||||||
|
#By default, signing key verification results are cached in order to prevent excessive HTTP requests to the JWKS endpoint.
|
||||||
|
#If a signing key matching the kid is found, this will be cached and the next time this kid is requested the signing key will be served from the cache.
|
||||||
|
#Default is true.
|
||||||
|
OPENID_JWKS_URL_CACHE_ENABLED=
|
||||||
|
OPENID_JWKS_URL_CACHE_TIME= # 600000 ms eq to 10 minutes leave empty to disable caching
|
||||||
|
#Set to true to trigger token exchange flow to acquire access token for the userinfo endpoint.
|
||||||
|
OPENID_ON_BEHALF_FLOW_FOR_USERINFO_REQUIRED=
|
||||||
|
OPENID_ON_BEHALF_FLOW_USERINFO_SCOPE="user.read" # example for Scope Needed for Microsoft Graph API
|
||||||
|
# Set to true to use the OpenID Connect end session endpoint for logout
|
||||||
|
OPENID_USE_END_SESSION_ENDPOINT=
|
||||||
|
# URL to redirect to after OpenID logout (defaults to ${DOMAIN_CLIENT}/login)
|
||||||
|
OPENID_POST_LOGOUT_REDIRECT_URI=
|
||||||
|
# Maximum logout URL length before using logout_hint instead of id_token_hint (default: 2000)
|
||||||
|
OPENID_MAX_LOGOUT_URL_LENGTH=
|
||||||
|
|
||||||
|
#========================#
|
||||||
|
# SharePoint Integration #
|
||||||
|
#========================#
|
||||||
|
# Requires Entra ID (OpenID) authentication to be configured
|
||||||
|
|
||||||
|
# Enable SharePoint file picker in chat and agent panels
|
||||||
|
# ENABLE_SHAREPOINT_FILEPICKER=true
|
||||||
|
|
||||||
|
# SharePoint tenant base URL (e.g., https://yourtenant.sharepoint.com)
|
||||||
|
# SHAREPOINT_BASE_URL=https://yourtenant.sharepoint.com
|
||||||
|
|
||||||
|
# Microsoft Graph API And SharePoint scopes for file picker
|
||||||
|
# SHAREPOINT_PICKER_SHAREPOINT_SCOPE==https://yourtenant.sharepoint.com/AllSites.Read
|
||||||
|
# SHAREPOINT_PICKER_GRAPH_SCOPE=Files.Read.All
|
||||||
|
#========================#
|
||||||
|
|
||||||
|
# SAML
|
||||||
|
# Note: If OpenID is enabled, SAML authentication will be automatically disabled.
|
||||||
|
SAML_ENTRY_POINT=
|
||||||
|
SAML_ISSUER=
|
||||||
|
SAML_CERT=
|
||||||
|
SAML_CALLBACK_URL=/oauth/saml/callback
|
||||||
|
SAML_SESSION_SECRET=
|
||||||
|
|
||||||
|
# Attribute mappings (optional)
|
||||||
|
SAML_EMAIL_CLAIM=
|
||||||
|
SAML_USERNAME_CLAIM=
|
||||||
|
SAML_GIVEN_NAME_CLAIM=
|
||||||
|
SAML_FAMILY_NAME_CLAIM=
|
||||||
|
SAML_PICTURE_CLAIM=
|
||||||
|
SAML_NAME_CLAIM=
|
||||||
|
|
||||||
|
# Logint buttion settings (optional)
|
||||||
|
SAML_BUTTON_LABEL=
|
||||||
|
SAML_IMAGE_URL=
|
||||||
|
|
||||||
|
# Whether the SAML Response should be signed.
|
||||||
|
# - If "true", the entire `SAML Response` will be signed.
|
||||||
|
# - If "false" or unset, only the `SAML Assertion` will be signed (default behavior).
|
||||||
|
# SAML_USE_AUTHN_RESPONSE_SIGNED=
|
||||||
|
|
||||||
|
|
||||||
|
#===============================================#
|
||||||
|
# Microsoft Graph API / Entra ID Integration #
|
||||||
|
#===============================================#
|
||||||
|
|
||||||
|
# Enable Entra ID people search integration in permissions/sharing system
|
||||||
|
# When enabled, the people picker will search both local database and Entra ID
|
||||||
|
USE_ENTRA_ID_FOR_PEOPLE_SEARCH=false
|
||||||
|
|
||||||
|
# When enabled, entra id groups owners will be considered as members of the group
|
||||||
|
ENTRA_ID_INCLUDE_OWNERS_AS_MEMBERS=false
|
||||||
|
|
||||||
|
# Microsoft Graph API scopes needed for people/group search
|
||||||
|
# Default scopes provide access to user profiles and group memberships
|
||||||
|
OPENID_GRAPH_SCOPES=User.Read,People.Read,GroupMember.Read.All
|
||||||
|
|
||||||
|
# LDAP
|
||||||
|
LDAP_URL=
|
||||||
|
LDAP_BIND_DN=
|
||||||
|
LDAP_BIND_CREDENTIALS=
|
||||||
|
LDAP_USER_SEARCH_BASE=
|
||||||
|
#LDAP_SEARCH_FILTER="mail="
|
||||||
|
LDAP_CA_CERT_PATH=
|
||||||
|
# LDAP_TLS_REJECT_UNAUTHORIZED=
|
||||||
|
# LDAP_STARTTLS=
|
||||||
|
# LDAP_LOGIN_USES_USERNAME=true
|
||||||
|
# LDAP_ID=
|
||||||
|
# LDAP_USERNAME=
|
||||||
|
# LDAP_EMAIL=
|
||||||
|
# LDAP_FULL_NAME=
|
||||||
|
|
||||||
|
#========================#
|
||||||
|
# Email Password Reset #
|
||||||
|
#========================#
|
||||||
|
|
||||||
|
EMAIL_SERVICE=
|
||||||
|
EMAIL_HOST=
|
||||||
|
EMAIL_PORT=25
|
||||||
|
EMAIL_ENCRYPTION=
|
||||||
|
EMAIL_ENCRYPTION_HOSTNAME=
|
||||||
|
EMAIL_ALLOW_SELFSIGNED=
|
||||||
|
# Leave both empty for SMTP servers that do not require authentication
|
||||||
|
EMAIL_USERNAME=
|
||||||
|
EMAIL_PASSWORD=
|
||||||
|
EMAIL_FROM_NAME=
|
||||||
|
EMAIL_FROM=noreply@librechat.ai
|
||||||
|
|
||||||
|
#========================#
|
||||||
|
# Mailgun API #
|
||||||
|
#========================#
|
||||||
|
|
||||||
|
# MAILGUN_API_KEY=your-mailgun-api-key
|
||||||
|
# MAILGUN_DOMAIN=mg.yourdomain.com
|
||||||
|
# EMAIL_FROM=noreply@yourdomain.com
|
||||||
|
# EMAIL_FROM_NAME="LibreChat"
|
||||||
|
|
||||||
|
# # Optional: For EU region
|
||||||
|
# MAILGUN_HOST=https://api.eu.mailgun.net
|
||||||
|
|
||||||
|
#========================#
|
||||||
|
# Firebase CDN #
|
||||||
|
#========================#
|
||||||
|
|
||||||
|
FIREBASE_API_KEY=
|
||||||
|
FIREBASE_AUTH_DOMAIN=
|
||||||
|
FIREBASE_PROJECT_ID=
|
||||||
|
FIREBASE_STORAGE_BUCKET=
|
||||||
|
FIREBASE_MESSAGING_SENDER_ID=
|
||||||
|
FIREBASE_APP_ID=
|
||||||
|
|
||||||
|
#========================#
|
||||||
|
# S3 AWS Bucket #
|
||||||
|
#========================#
|
||||||
|
|
||||||
|
AWS_ENDPOINT_URL=
|
||||||
|
AWS_ACCESS_KEY_ID=
|
||||||
|
AWS_SECRET_ACCESS_KEY=
|
||||||
|
AWS_REGION=
|
||||||
|
AWS_BUCKET_NAME=
|
||||||
|
# Required for path-style S3-compatible providers (MinIO, Hetzner, Backblaze B2, etc.)
|
||||||
|
# that don't support virtual-hosted-style URLs (bucket.endpoint). Not needed for AWS S3.
|
||||||
|
# AWS_FORCE_PATH_STYLE=false
|
||||||
|
|
||||||
|
#========================#
|
||||||
|
# Azure Blob Storage #
|
||||||
|
#========================#
|
||||||
|
|
||||||
|
AZURE_STORAGE_CONNECTION_STRING=
|
||||||
|
AZURE_STORAGE_PUBLIC_ACCESS=false
|
||||||
|
AZURE_CONTAINER_NAME=files
|
||||||
|
|
||||||
|
#========================#
|
||||||
|
# Shared Links #
|
||||||
|
#========================#
|
||||||
|
|
||||||
|
ALLOW_SHARED_LINKS=true
|
||||||
|
# Allows unauthenticated access to shared links. Defaults to false (auth required) if not set.
|
||||||
|
ALLOW_SHARED_LINKS_PUBLIC=false
|
||||||
|
|
||||||
|
#==============================#
|
||||||
|
# Static File Cache Control #
|
||||||
|
#==============================#
|
||||||
|
|
||||||
|
# Leave commented out to use defaults: 1 day (86400 seconds) for s-maxage and 2 days (172800 seconds) for max-age
|
||||||
|
# NODE_ENV must be set to production for these to take effect
|
||||||
|
# STATIC_CACHE_MAX_AGE=172800
|
||||||
|
# STATIC_CACHE_S_MAX_AGE=86400
|
||||||
|
|
||||||
|
# If you have another service in front of your LibreChat doing compression, disable express based compression here
|
||||||
|
# DISABLE_COMPRESSION=true
|
||||||
|
|
||||||
|
# If you have gzipped version of uploaded image images in the same folder, this will enable gzip scan and serving of these images
|
||||||
|
# Note: The images folder will be scanned on startup and a ma kept in memory. Be careful for large number of images.
|
||||||
|
# ENABLE_IMAGE_OUTPUT_GZIP_SCAN=true
|
||||||
|
|
||||||
|
#===================================================#
|
||||||
|
# UI #
|
||||||
|
#===================================================#
|
||||||
|
|
||||||
|
APP_TITLE=LibreChat
|
||||||
|
# CUSTOM_FOOTER="My custom footer"
|
||||||
|
HELP_AND_FAQ_URL=https://librechat.ai
|
||||||
|
|
||||||
|
# SHOW_BIRTHDAY_ICON=true
|
||||||
|
|
||||||
|
# Google tag manager id
|
||||||
|
#ANALYTICS_GTM_ID=user provided google tag manager id
|
||||||
|
|
||||||
|
# limit conversation file imports to a certain number of bytes in size to avoid the container
|
||||||
|
# maxing out memory limitations by unremarking this line and supplying a file size in bytes
|
||||||
|
# such as the below example of 250 mib
|
||||||
|
# CONVERSATION_IMPORT_MAX_FILE_SIZE_BYTES=262144000
|
||||||
|
|
||||||
|
|
||||||
|
#===============#
|
||||||
|
# REDIS Options #
|
||||||
|
#===============#
|
||||||
|
|
||||||
|
# Enable Redis for caching and session storage
|
||||||
|
# USE_REDIS=true
|
||||||
|
# Enable Redis for resumable LLM streams (defaults to USE_REDIS value if not set)
|
||||||
|
# Set to false to use in-memory storage for streams while keeping Redis for other caches
|
||||||
|
# USE_REDIS_STREAMS=true
|
||||||
|
|
||||||
|
# Single Redis instance
|
||||||
|
# REDIS_URI=redis://127.0.0.1:6379
|
||||||
|
|
||||||
|
# Redis cluster (multiple nodes)
|
||||||
|
# REDIS_URI=redis://127.0.0.1:7001,redis://127.0.0.1:7002,redis://127.0.0.1:7003
|
||||||
|
|
||||||
|
# Redis with TLS/SSL encryption and CA certificate
|
||||||
|
# REDIS_URI=rediss://127.0.0.1:6380
|
||||||
|
# REDIS_CA=/path/to/ca-cert.pem
|
||||||
|
|
||||||
|
# Elasticache may need to use an alternate dnsLookup for TLS connections. see "Special Note: Aws Elasticache Clusters with TLS" on this webpage: https://www.npmjs.com/package/ioredis
|
||||||
|
# Enable alternative dnsLookup for redis
|
||||||
|
# REDIS_USE_ALTERNATIVE_DNS_LOOKUP=true
|
||||||
|
|
||||||
|
# Redis authentication (if required)
|
||||||
|
# REDIS_USERNAME=your_redis_username
|
||||||
|
# REDIS_PASSWORD=your_redis_password
|
||||||
|
|
||||||
|
# Redis key prefix configuration
|
||||||
|
# Use environment variable name for dynamic prefix (recommended for cloud deployments)
|
||||||
|
# REDIS_KEY_PREFIX_VAR=K_REVISION
|
||||||
|
# Or use static prefix directly
|
||||||
|
# REDIS_KEY_PREFIX=librechat
|
||||||
|
|
||||||
|
# Redis connection limits
|
||||||
|
# REDIS_MAX_LISTENERS=40
|
||||||
|
|
||||||
|
# Redis ping interval in seconds (0 = disabled, >0 = enabled)
|
||||||
|
# When set to a positive integer, Redis clients will ping the server at this interval to keep connections alive
|
||||||
|
# When unset or 0, no pinging is performed (recommended for most use cases)
|
||||||
|
# REDIS_PING_INTERVAL=300
|
||||||
|
|
||||||
|
# Force specific cache namespaces to use in-memory storage even when Redis is enabled
|
||||||
|
# Comma-separated list of CacheKeys
|
||||||
|
# Defaults to CONFIG_STORE,APP_CONFIG so YAML-derived config stays per-container (safe for blue/green deployments)
|
||||||
|
# Set to empty string to force all namespaces through Redis: FORCED_IN_MEMORY_CACHE_NAMESPACES=
|
||||||
|
# FORCED_IN_MEMORY_CACHE_NAMESPACES=CONFIG_STORE,APP_CONFIG
|
||||||
|
|
||||||
|
# Leader Election Configuration (for multi-instance deployments with Redis)
|
||||||
|
# Duration in seconds that the leader lease is valid before it expires (default: 25)
|
||||||
|
# LEADER_LEASE_DURATION=25
|
||||||
|
# Interval in seconds at which the leader renews its lease (default: 10)
|
||||||
|
# LEADER_RENEW_INTERVAL=10
|
||||||
|
# Maximum number of retry attempts when renewing the lease fails (default: 3)
|
||||||
|
# LEADER_RENEW_ATTEMPTS=3
|
||||||
|
# Delay in seconds between retry attempts when renewing the lease (default: 0.5)
|
||||||
|
# LEADER_RENEW_RETRY_DELAY=0.5
|
||||||
|
|
||||||
|
#==================================================#
|
||||||
|
# Others #
|
||||||
|
#==================================================#
|
||||||
|
# You should leave the following commented out #
|
||||||
|
|
||||||
|
# NODE_ENV=
|
||||||
|
|
||||||
|
# E2E_USER_EMAIL=
|
||||||
|
# E2E_USER_PASSWORD=
|
||||||
|
|
||||||
|
#=====================================================#
|
||||||
|
# Cache Headers #
|
||||||
|
#=====================================================#
|
||||||
|
# Headers that control caching of the index.html #
|
||||||
|
# Default configuration prevents caching to ensure #
|
||||||
|
# users always get the latest version. Customize #
|
||||||
|
# only if you understand caching implications. #
|
||||||
|
|
||||||
|
# INDEX_CACHE_CONTROL=no-cache, no-store, must-revalidate
|
||||||
|
# INDEX_PRAGMA=no-cache
|
||||||
|
# INDEX_EXPIRES=0
|
||||||
|
|
||||||
|
# no-cache: Forces validation with server before using cached version
|
||||||
|
# no-store: Prevents storing the response entirely
|
||||||
|
# must-revalidate: Prevents using stale content when offline
|
||||||
|
|
||||||
|
#=====================================================#
|
||||||
|
# OpenWeather #
|
||||||
|
#=====================================================#
|
||||||
|
OPENWEATHER_API_KEY=
|
||||||
|
|
||||||
|
#====================================#
|
||||||
|
# LibreChat Code Interpreter API #
|
||||||
|
#====================================#
|
||||||
|
|
||||||
|
# https://code.librechat.ai
|
||||||
|
# LIBRECHAT_CODE_API_KEY=your-key
|
||||||
|
|
||||||
|
#======================#
|
||||||
|
# Web Search #
|
||||||
|
#======================#
|
||||||
|
|
||||||
|
# Note: All of the following variable names can be customized.
|
||||||
|
# Omit values to allow user to provide them.
|
||||||
|
|
||||||
|
# For more information on configuration values, see:
|
||||||
|
# https://librechat.ai/docs/features/web_search
|
||||||
|
|
||||||
|
# Search Provider (Required)
|
||||||
|
# SERPER_API_KEY=your_serper_api_key
|
||||||
|
|
||||||
|
# Scraper (Required)
|
||||||
|
# FIRECRAWL_API_KEY=your_firecrawl_api_key
|
||||||
|
# Optional: Custom Firecrawl API URL
|
||||||
|
# FIRECRAWL_API_URL=your_firecrawl_api_url
|
||||||
|
|
||||||
|
# Reranker (Required)
|
||||||
|
# JINA_API_KEY=your_jina_api_key
|
||||||
|
# or
|
||||||
|
# COHERE_API_KEY=your_cohere_api_key
|
||||||
|
|
||||||
|
#======================#
|
||||||
|
# MCP Configuration #
|
||||||
|
#======================#
|
||||||
|
|
||||||
|
# Treat 401/403 responses as OAuth requirement when no oauth metadata found
|
||||||
|
# MCP_OAUTH_ON_AUTH_ERROR=true
|
||||||
|
|
||||||
|
# Timeout for OAuth detection requests in milliseconds
|
||||||
|
# MCP_OAUTH_DETECTION_TIMEOUT=5000
|
||||||
|
|
||||||
|
# Cache connection status checks for this many milliseconds to avoid expensive verification
|
||||||
|
# MCP_CONNECTION_CHECK_TTL=60000
|
||||||
|
|
||||||
|
# Skip code challenge method validation (e.g., for AWS Cognito that supports S256 but doesn't advertise it)
|
||||||
|
# When set to true, forces S256 code challenge even if not advertised in .well-known/openid-configuration
|
||||||
|
# MCP_SKIP_CODE_CHALLENGE_CHECK=false
|
||||||
|
|
||||||
|
# Circuit breaker: max connect/disconnect cycles before tripping (per server)
|
||||||
|
# MCP_CB_MAX_CYCLES=7
|
||||||
|
|
||||||
|
# Circuit breaker: sliding window (ms) for counting cycles
|
||||||
|
# MCP_CB_CYCLE_WINDOW_MS=45000
|
||||||
|
|
||||||
|
# Circuit breaker: cooldown (ms) after the cycle breaker trips
|
||||||
|
# MCP_CB_CYCLE_COOLDOWN_MS=15000
|
||||||
|
|
||||||
|
# Circuit breaker: max consecutive failed connection rounds before backoff
|
||||||
|
# MCP_CB_MAX_FAILED_ROUNDS=3
|
||||||
|
|
||||||
|
# Circuit breaker: sliding window (ms) for counting failed rounds
|
||||||
|
# MCP_CB_FAILED_WINDOW_MS=120000
|
||||||
|
|
||||||
|
# Circuit breaker: base backoff (ms) after failed round threshold is reached
|
||||||
|
# MCP_CB_BASE_BACKOFF_MS=30000
|
||||||
|
|
||||||
|
# Circuit breaker: max backoff cap (ms) for exponential backoff
|
||||||
|
# MCP_CB_MAX_BACKOFF_MS=300000
|
||||||
@@ -6,9 +6,7 @@ cache: true
|
|||||||
endpoints:
|
endpoints:
|
||||||
anthropic:
|
anthropic:
|
||||||
apiKey: "${ANTHROPIC_API_KEY}"
|
apiKey: "${ANTHROPIC_API_KEY}"
|
||||||
models:
|
models: ["claude-sonnet-4-5", "claude-haiku-4-5", "claude-opus-4-5"]
|
||||||
default: ["claude-sonnet-4-5", "claude-haiku-4-5", "claude-opus-4-5"]
|
|
||||||
fetch: false
|
|
||||||
titleConvo: true
|
titleConvo: true
|
||||||
titleModel: "claude-haiku-4-5"
|
titleModel: "claude-haiku-4-5"
|
||||||
modelDisplayLabel: "Claude AI"
|
modelDisplayLabel: "Claude AI"
|
||||||
@@ -72,7 +70,7 @@ endpoints:
|
|||||||
apiKey: "dummy_key"
|
apiKey: "dummy_key"
|
||||||
baseURL: "http://alfred:8000/v1"
|
baseURL: "http://alfred:8000/v1"
|
||||||
models:
|
models:
|
||||||
default: ["local-deepseek-agent"]
|
default: ["glm-4.7-flash:latest"]
|
||||||
fetch: false
|
fetch: false
|
||||||
titleConvo: false
|
titleConvo: false
|
||||||
titleModel: "current_model"
|
titleModel: "current_model"
|
||||||
|
|||||||
+1
-1
@@ -13,7 +13,7 @@ storage:
|
|||||||
systemLog:
|
systemLog:
|
||||||
destination: file
|
destination: file
|
||||||
path: /dev/stdout
|
path: /dev/stdout
|
||||||
logAppend: false
|
logAppend: true
|
||||||
verbosity: 0
|
verbosity: 0
|
||||||
quiet: true
|
quiet: true
|
||||||
component:
|
component:
|
||||||
|
|||||||
+43
-8
@@ -87,6 +87,39 @@ def extract_python_version(version_string: str) -> tuple[str, str]:
|
|||||||
raise ValueError(f"Invalid Python version: {version_string}")
|
raise ValueError(f"Invalid Python version: {version_string}")
|
||||||
|
|
||||||
|
|
||||||
|
def build_uris(env_alfred: Path, env_secrets: Path) -> None:
|
||||||
|
"""Build MONGO_URI and POSTGRES_URI from components and append them to .env.secrets."""
|
||||||
|
env = {**load_env_file(env_alfred), **load_env_file(env_secrets)}
|
||||||
|
existing = load_env_file(env_secrets)
|
||||||
|
|
||||||
|
computed = {
|
||||||
|
"MONGO_URI": (
|
||||||
|
f"mongodb://{env['MONGO_USER']}:{env['MONGO_PASSWORD']}"
|
||||||
|
f"@{env['MONGO_HOST']}:{env['MONGO_PORT']}/{env['MONGO_DB_NAME']}"
|
||||||
|
f"?authSource=admin"
|
||||||
|
),
|
||||||
|
"POSTGRES_URI": (
|
||||||
|
f"postgresql://{env['POSTGRES_USER']}:{env['POSTGRES_PASSWORD']}"
|
||||||
|
f"@{env['POSTGRES_HOST']}:{env['POSTGRES_PORT']}/{env['POSTGRES_DB_NAME']}"
|
||||||
|
),
|
||||||
|
}
|
||||||
|
|
||||||
|
content = env_secrets.read_text()
|
||||||
|
added = []
|
||||||
|
for key, value in computed.items():
|
||||||
|
if key in existing:
|
||||||
|
content = re.sub(rf"^{key}=.*$", f"{key}={value}", content, flags=re.MULTILINE)
|
||||||
|
else:
|
||||||
|
content = content.rstrip("\n") + f"\n{key}={value}\n"
|
||||||
|
added.append(key)
|
||||||
|
env_secrets.write_text(content)
|
||||||
|
|
||||||
|
if added:
|
||||||
|
print(f" + Computed: {', '.join(added)}")
|
||||||
|
else:
|
||||||
|
print(" ↻ URIs updated")
|
||||||
|
|
||||||
|
|
||||||
def write_env_make(toml_data: dict) -> None:
|
def write_env_make(toml_data: dict) -> None:
|
||||||
"""Write .env.make from pyproject.toml."""
|
"""Write .env.make from pyproject.toml."""
|
||||||
project = toml_data["project"]
|
project = toml_data["project"]
|
||||||
@@ -96,14 +129,13 @@ def write_env_make(toml_data: dict) -> None:
|
|||||||
|
|
||||||
lines = [
|
lines = [
|
||||||
"# Auto-generated from pyproject.toml — do not edit manually",
|
"# Auto-generated from pyproject.toml — do not edit manually",
|
||||||
f"export ALFRED_VERSION={project['version']}",
|
f"ALFRED_VERSION={project['version']}",
|
||||||
f"export PYTHON_VERSION={python_full}",
|
f"PYTHON_VERSION={python_full}",
|
||||||
f"export PYTHON_VERSION_SHORT={python_short}",
|
f"IMAGE_NAME={alfred['image_name']}",
|
||||||
f"export IMAGE_NAME={alfred['image_name']}",
|
f"SERVICE_NAME={alfred['service_name']}",
|
||||||
f"export SERVICE_NAME={alfred['service_name']}",
|
f"LIBRECHAT_VERSION={alfred['librechat_version']}",
|
||||||
f"export LIBRECHAT_VERSION={alfred['librechat_version']}",
|
f"RAG_VERSION={alfred['rag_version']}",
|
||||||
f"export RAG_VERSION={alfred['rag_version']}",
|
f"UV_VERSION={alfred['uv_version']}",
|
||||||
f"export UV_VERSION={alfred['uv_version']}",
|
|
||||||
]
|
]
|
||||||
|
|
||||||
env_make_path = BASE_DIR / ".env.make"
|
env_make_path = BASE_DIR / ".env.make"
|
||||||
@@ -138,6 +170,9 @@ def main() -> int:
|
|||||||
print("\n🔐 Secrets:")
|
print("\n🔐 Secrets:")
|
||||||
generate_secrets_file(BASE_DIR / ".env.secrets", secrets_spec)
|
generate_secrets_file(BASE_DIR / ".env.secrets", secrets_spec)
|
||||||
|
|
||||||
|
print("\n🔗 URIs:")
|
||||||
|
build_uris(BASE_DIR / ".env.alfred", BASE_DIR / ".env.secrets")
|
||||||
|
|
||||||
print("\n🔧 Build config:")
|
print("\n🔧 Build config:")
|
||||||
write_env_make(toml_data)
|
write_env_make(toml_data)
|
||||||
|
|
||||||
|
|||||||
@@ -72,7 +72,6 @@ def write_env_make(config: BuildConfig, base_dir: Path | None = None) -> None:
|
|||||||
"# Auto-generated from pyproject.toml — do not edit manually",
|
"# Auto-generated from pyproject.toml — do not edit manually",
|
||||||
f"export ALFRED_VERSION={config.alfred_version}",
|
f"export ALFRED_VERSION={config.alfred_version}",
|
||||||
f"export PYTHON_VERSION={config.python_version}",
|
f"export PYTHON_VERSION={config.python_version}",
|
||||||
f"export PYTHON_VERSION_SHORT={config.python_version_short}",
|
|
||||||
f"export IMAGE_NAME={config.image_name}",
|
f"export IMAGE_NAME={config.image_name}",
|
||||||
f"export SERVICE_NAME={config.service_name}",
|
f"export SERVICE_NAME={config.service_name}",
|
||||||
f"export LIBRECHAT_VERSION={config.librechat_version}",
|
f"export LIBRECHAT_VERSION={config.librechat_version}",
|
||||||
|
|||||||
@@ -1,22 +0,0 @@
|
|||||||
#!/usr/bin/env python3
|
|
||||||
"""Generate .env.make for CI/CD without generating secrets."""
|
|
||||||
|
|
||||||
import sys
|
|
||||||
|
|
||||||
from config_loader import load_build_config, write_env_make
|
|
||||||
|
|
||||||
|
|
||||||
def main():
|
|
||||||
"""Generate .env.make from pyproject.toml."""
|
|
||||||
try:
|
|
||||||
config = load_build_config()
|
|
||||||
write_env_make(config)
|
|
||||||
print("✅ .env.make generated successfully.")
|
|
||||||
return 0
|
|
||||||
except Exception as e:
|
|
||||||
print(f"❌ Failed to generate .env.make: {e}")
|
|
||||||
return 1
|
|
||||||
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
sys.exit(main())
|
|
||||||
@@ -0,0 +1,281 @@
|
|||||||
|
"""Tests for scripts/bootstrap.py — focus on secret safety and idempotency."""
|
||||||
|
|
||||||
|
import sys
|
||||||
|
from pathlib import Path
|
||||||
|
|
||||||
|
import pytest
|
||||||
|
|
||||||
|
# bootstrap.py lives in scripts/, not in a package — add it to path
|
||||||
|
sys.path.insert(0, str(Path(__file__).resolve().parent.parent / "scripts"))
|
||||||
|
|
||||||
|
from bootstrap import (
|
||||||
|
build_uris,
|
||||||
|
copy_example_if_missing,
|
||||||
|
extract_python_version,
|
||||||
|
generate_secrets_file,
|
||||||
|
load_env_file,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# Fixtures
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
SECRETS_SPEC = {
|
||||||
|
"JWT_SECRET": (32, "hex"),
|
||||||
|
"MONGO_PASSWORD": (16, "hex"),
|
||||||
|
"POSTGRES_PASSWORD": (16, "hex"),
|
||||||
|
}
|
||||||
|
|
||||||
|
ALFRED_ENV = """\
|
||||||
|
MONGO_HOST=mongodb
|
||||||
|
MONGO_PORT=27017
|
||||||
|
MONGO_USER=alfred
|
||||||
|
MONGO_DB_NAME=mydb
|
||||||
|
POSTGRES_HOST=vectordb
|
||||||
|
POSTGRES_PORT=5432
|
||||||
|
POSTGRES_USER=alfred
|
||||||
|
POSTGRES_DB_NAME=alfred
|
||||||
|
"""
|
||||||
|
|
||||||
|
SECRETS_ENV = """\
|
||||||
|
# Auto-generated secrets — DO NOT COMMIT
|
||||||
|
JWT_SECRET=deadbeef
|
||||||
|
MONGO_PASSWORD=cafebabe
|
||||||
|
POSTGRES_PASSWORD=f00dface
|
||||||
|
"""
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def secrets_file(tmp_path):
|
||||||
|
"""An existing .env.secrets with pre-generated values."""
|
||||||
|
p = tmp_path / ".env.secrets"
|
||||||
|
p.write_text(SECRETS_ENV)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
@pytest.fixture
|
||||||
|
def alfred_file(tmp_path):
|
||||||
|
p = tmp_path / ".env.alfred"
|
||||||
|
p.write_text(ALFRED_ENV)
|
||||||
|
return p
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# load_env_file
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestLoadEnvFile:
|
||||||
|
def test_parses_key_value_pairs(self, tmp_path):
|
||||||
|
f = tmp_path / ".env"
|
||||||
|
f.write_text("FOO=bar\nBAZ=qux\n")
|
||||||
|
assert load_env_file(f) == {"FOO": "bar", "BAZ": "qux"}
|
||||||
|
|
||||||
|
def test_ignores_comments_and_blanks(self, tmp_path):
|
||||||
|
f = tmp_path / ".env"
|
||||||
|
f.write_text("# comment\n\nFOO=bar\n")
|
||||||
|
assert load_env_file(f) == {"FOO": "bar"}
|
||||||
|
|
||||||
|
def test_missing_file_returns_empty(self, tmp_path):
|
||||||
|
assert load_env_file(tmp_path / "nonexistent") == {}
|
||||||
|
|
||||||
|
def test_value_with_equals_sign(self, tmp_path):
|
||||||
|
"""Values containing '=' must be preserved intact (e.g. base64)."""
|
||||||
|
f = tmp_path / ".env"
|
||||||
|
f.write_text("KEY=abc=def==\n")
|
||||||
|
assert load_env_file(f)["KEY"] == "abc=def=="
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# generate_secrets_file — the critical ones
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestGenerateSecretsFile:
|
||||||
|
def test_generates_all_secrets_on_first_run(self, tmp_path):
|
||||||
|
path = tmp_path / ".env.secrets"
|
||||||
|
generate_secrets_file(path, SECRETS_SPEC)
|
||||||
|
|
||||||
|
result = load_env_file(path)
|
||||||
|
assert set(SECRETS_SPEC.keys()) <= result.keys()
|
||||||
|
assert all(result[k] for k in SECRETS_SPEC) # non-empty
|
||||||
|
|
||||||
|
def test_never_overwrites_existing_secrets(self, secrets_file):
|
||||||
|
"""Core safety property: running bootstrap again must not change existing values."""
|
||||||
|
before = load_env_file(secrets_file)
|
||||||
|
|
||||||
|
generate_secrets_file(secrets_file, SECRETS_SPEC)
|
||||||
|
|
||||||
|
after = load_env_file(secrets_file)
|
||||||
|
for key in before:
|
||||||
|
assert after[key] == before[key], f"{key} was overwritten!"
|
||||||
|
|
||||||
|
def test_adds_missing_secrets_without_touching_existing(self, secrets_file):
|
||||||
|
"""Only keys absent from the file should be added."""
|
||||||
|
before = load_env_file(secrets_file)
|
||||||
|
# POSTGRES_PASSWORD already exists; JWT_SECRET already exists
|
||||||
|
# Add a new key to the spec that is not yet in the file
|
||||||
|
spec = {**SECRETS_SPEC, "NEW_SECRET": (16, "hex")}
|
||||||
|
|
||||||
|
generate_secrets_file(secrets_file, spec)
|
||||||
|
|
||||||
|
after = load_env_file(secrets_file)
|
||||||
|
# Existing values untouched
|
||||||
|
for key in before:
|
||||||
|
assert after[key] == before[key]
|
||||||
|
# New key added
|
||||||
|
assert "NEW_SECRET" in after
|
||||||
|
assert len(after["NEW_SECRET"]) == 32 # 16 bytes → 32 hex chars
|
||||||
|
|
||||||
|
def test_idempotent_across_multiple_runs(self, tmp_path):
|
||||||
|
"""Calling bootstrap N times must produce stable secrets."""
|
||||||
|
path = tmp_path / ".env.secrets"
|
||||||
|
|
||||||
|
generate_secrets_file(path, SECRETS_SPEC)
|
||||||
|
after_first = load_env_file(path)
|
||||||
|
|
||||||
|
generate_secrets_file(path, SECRETS_SPEC)
|
||||||
|
after_second = load_env_file(path)
|
||||||
|
|
||||||
|
assert after_first == after_second
|
||||||
|
|
||||||
|
def test_hex_secret_has_correct_length(self, tmp_path):
|
||||||
|
path = tmp_path / ".env.secrets"
|
||||||
|
generate_secrets_file(path, {"MY_KEY": (32, "hex")})
|
||||||
|
value = load_env_file(path)["MY_KEY"]
|
||||||
|
assert len(value) == 64 # 32 bytes → 64 hex chars
|
||||||
|
assert all(c in "0123456789abcdef" for c in value)
|
||||||
|
|
||||||
|
def test_preserves_comments_in_existing_file(self, secrets_file):
|
||||||
|
"""Comments in .env.secrets must survive a bootstrap run."""
|
||||||
|
generate_secrets_file(secrets_file, SECRETS_SPEC)
|
||||||
|
content = secrets_file.read_text()
|
||||||
|
assert "# Auto-generated secrets" in content
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# build_uris
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestBuildUris:
|
||||||
|
def test_writes_uris_to_secrets_file(self, alfred_file, secrets_file):
|
||||||
|
build_uris(alfred_file, secrets_file)
|
||||||
|
result = load_env_file(secrets_file)
|
||||||
|
|
||||||
|
assert "MONGO_URI" in result
|
||||||
|
assert "POSTGRES_URI" in result
|
||||||
|
|
||||||
|
def test_mongo_uri_contains_all_components(self, alfred_file, secrets_file):
|
||||||
|
build_uris(alfred_file, secrets_file)
|
||||||
|
uri = load_env_file(secrets_file)["MONGO_URI"]
|
||||||
|
|
||||||
|
assert "alfred" in uri # user
|
||||||
|
assert "cafebabe" in uri # password from secrets
|
||||||
|
assert "mongodb" in uri # host
|
||||||
|
assert "27017" in uri # port
|
||||||
|
assert "mydb" in uri # dbname
|
||||||
|
assert "authSource=admin" in uri
|
||||||
|
|
||||||
|
def test_postgres_uri_contains_all_components(self, alfred_file, secrets_file):
|
||||||
|
build_uris(alfred_file, secrets_file)
|
||||||
|
uri = load_env_file(secrets_file)["POSTGRES_URI"]
|
||||||
|
|
||||||
|
assert "alfred" in uri
|
||||||
|
assert "f00dface" in uri # password from secrets
|
||||||
|
assert "vectordb" in uri
|
||||||
|
assert "5432" in uri
|
||||||
|
assert uri.startswith("postgresql://")
|
||||||
|
|
||||||
|
def test_uri_is_updated_when_host_changes(self, tmp_path, secrets_file):
|
||||||
|
"""If MONGO_HOST changes in .env.alfred, the URI must reflect it."""
|
||||||
|
alfred = tmp_path / ".env.alfred"
|
||||||
|
alfred.write_text(ALFRED_ENV.replace("MONGO_HOST=mongodb", "MONGO_HOST=newhost"))
|
||||||
|
|
||||||
|
build_uris(alfred, secrets_file)
|
||||||
|
uri = load_env_file(secrets_file)["MONGO_URI"]
|
||||||
|
|
||||||
|
assert "@newhost:" in uri
|
||||||
|
assert "@mongodb:" not in uri
|
||||||
|
|
||||||
|
def test_uri_update_does_not_alter_other_secrets(self, alfred_file, secrets_file):
|
||||||
|
"""Recomputing URIs must not touch JWT_SECRET or passwords."""
|
||||||
|
before = load_env_file(secrets_file)
|
||||||
|
|
||||||
|
build_uris(alfred_file, secrets_file)
|
||||||
|
|
||||||
|
after = load_env_file(secrets_file)
|
||||||
|
for key in before:
|
||||||
|
assert after[key] == before[key], f"{key} was altered by build_uris!"
|
||||||
|
|
||||||
|
def test_uri_recomputed_on_repeated_calls(self, tmp_path, secrets_file):
|
||||||
|
"""Calling build_uris twice with different configs produces the latest URI."""
|
||||||
|
alfred_v1 = tmp_path / "alfred_v1"
|
||||||
|
alfred_v1.write_text(ALFRED_ENV)
|
||||||
|
build_uris(alfred_v1, secrets_file)
|
||||||
|
uri_v1 = load_env_file(secrets_file)["MONGO_URI"]
|
||||||
|
|
||||||
|
alfred_v2 = tmp_path / "alfred_v2"
|
||||||
|
alfred_v2.write_text(ALFRED_ENV.replace("MONGO_DB_NAME=mydb", "MONGO_DB_NAME=otherdb"))
|
||||||
|
build_uris(alfred_v2, secrets_file)
|
||||||
|
uri_v2 = load_env_file(secrets_file)["MONGO_URI"]
|
||||||
|
|
||||||
|
assert "mydb" not in uri_v2
|
||||||
|
assert "otherdb" in uri_v2
|
||||||
|
# Password unchanged across both calls
|
||||||
|
assert load_env_file(secrets_file)["MONGO_PASSWORD"] == "cafebabe"
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# copy_example_if_missing
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestCopyExampleIfMissing:
|
||||||
|
def test_copies_when_dst_missing(self, tmp_path):
|
||||||
|
src = tmp_path / "src.env"
|
||||||
|
src.write_text("FOO=bar\n")
|
||||||
|
dst = tmp_path / "dst.env"
|
||||||
|
|
||||||
|
copy_example_if_missing(src, dst, "test")
|
||||||
|
|
||||||
|
assert dst.read_text() == "FOO=bar\n"
|
||||||
|
|
||||||
|
def test_never_overwrites_existing_dst(self, tmp_path):
|
||||||
|
src = tmp_path / "src.env"
|
||||||
|
src.write_text("FOO=new\n")
|
||||||
|
dst = tmp_path / "dst.env"
|
||||||
|
dst.write_text("FOO=original\n")
|
||||||
|
|
||||||
|
copy_example_if_missing(src, dst, "test")
|
||||||
|
|
||||||
|
assert dst.read_text() == "FOO=original\n"
|
||||||
|
|
||||||
|
def test_silent_skip_when_src_missing(self, tmp_path):
|
||||||
|
"""Should not raise if the example file doesn't exist yet."""
|
||||||
|
dst = tmp_path / "dst.env"
|
||||||
|
copy_example_if_missing(tmp_path / "nonexistent.env", dst, "test")
|
||||||
|
assert not dst.exists()
|
||||||
|
|
||||||
|
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
# extract_python_version
|
||||||
|
# ---------------------------------------------------------------------------
|
||||||
|
|
||||||
|
|
||||||
|
class TestExtractPythonVersion:
|
||||||
|
@pytest.mark.parametrize("spec,expected_full,expected_short", [
|
||||||
|
("==3.14.3", "3.14.3", "3.14"),
|
||||||
|
("^3.12.0", "3.12.0", "3.12"),
|
||||||
|
("~3.11.1", "3.11.1", "3.11"),
|
||||||
|
("3.10.5", "3.10.5", "3.10"),
|
||||||
|
])
|
||||||
|
def test_parses_version_specifiers(self, spec, expected_full, expected_short):
|
||||||
|
full, short = extract_python_version(spec)
|
||||||
|
assert full == expected_full
|
||||||
|
assert short == expected_short
|
||||||
|
|
||||||
|
def test_raises_on_invalid_version(self):
|
||||||
|
with pytest.raises(ValueError):
|
||||||
|
extract_python_version("3")
|
||||||
Reference in New Issue
Block a user