Settings + fix startup

This commit is contained in:
2026-04-30 12:41:42 +02:00
parent 610dee365c
commit 62b5d0b998
16 changed files with 1340 additions and 247 deletions
+30 -46
View File
@@ -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_TOOL_ITERATIONS=10
REQUEST_TIMEOUT=30
@@ -8,84 +18,58 @@ LLM_TEMPERATURE=0.2
# Persistence
DATA_STORAGE_DIR=data
# Network configuration
# Network
HOST=0.0.0.0
PORT=3080
# Build informations (Synced with pyproject.toml via bootstrap)
ALFRED_VERSION=
IMAGE_NAME=
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.
# --- DATABASES ---
# Passwords and connection URIs are auto-generated in .env.secrets.
# Edit host/port/user/dbname here if needed.
# MongoDB (Application Data)
MONGO_URI=
MONGO_HOST=mongodb
MONGO_PORT=27017
MONGO_USER=alfred
MONGO_PASSWORD=
MONGO_DB_NAME=LibreChat
MONGO_DB_NAME=alfred
# PostgreSQL (Vector Database / RAG)
POSTGRES_URI=
POSTGRES_HOST=vectordb
POSTGRES_PORT=5432
POSTGRES_USER=alfred
POSTGRES_PASSWORD=
POSTGRES_DB_NAME=alfred
# --- EXTERNAL SERVICES ---
# Media Metadata (Required)
# Get your key at https://www.themoviedb.org/
TMDB_API_KEY=
# TMDB — Media metadata (required). Get your key at https://www.themoviedb.org/
# → TMDB_API_KEY goes in .env.secrets
TMDB_BASE_URL=https://api.themoviedb.org/3
# qBittorrent integration
# qBittorrent
# → QBITTORRENT_PASSWORD goes in .env.secrets
QBITTORRENT_URL=http://qbittorrent:16140
QBITTORRENT_USERNAME=admin
QBITTORRENT_PASSWORD=
QBITTORRENT_PORT=16140
# Meilisearch
MEILI_ENABLED=FALSE
MEILI_NO_ANALYTICS=TRUE
# → MEILI_MASTER_KEY goes in .env.secrets
# MEILI_ENABLED=false # KEY DOESN'T EXISTS => SEARCH IS THE PROPER KEY
SEARCH=false
MEILI_NO_ANALYTICS=true
MEILI_HOST=http://meilisearch:7700
MEILI_MASTER_KEY=
# --- 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
# Local LLM (Ollama)
OLLAMA_BASE_URL=http://ollama:11434
OLLAMA_MODEL=llama3.3:latest
#OLLAMA_BASE_URL=http://ollama:11434
#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=
OLLAMA_BASE_URL=http://10.0.0.11:11434
OLLAMA_MODEL=glm-4.7-flash:latest
# --- RAG ENGINE ---
# Enable/Disable the Retrieval Augmented Generation system
RAG_ENABLED=TRUE
RAG_API_URL=http://rag_api:8000
RAG_API_PORT=8000
+22 -42
View File
@@ -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_TOOL_ITERATIONS=10
REQUEST_TIMEOUT=30
@@ -8,84 +18,54 @@ LLM_TEMPERATURE=0.2
# Persistence
DATA_STORAGE_DIR=data
# Network configuration
# Network
HOST=0.0.0.0
PORT=3080
# Build informations (Synced with pyproject.toml via bootstrap)
ALFRED_VERSION=
IMAGE_NAME=
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.
# --- DATABASES ---
# Passwords and connection URIs are auto-generated in .env.secrets.
# Edit host/port/user/dbname here if needed.
# MongoDB (Application Data)
MONGO_URI=
MONGO_HOST=mongodb
MONGO_PORT=27017
MONGO_USER=alfred
MONGO_PASSWORD=
MONGO_DB_NAME=LibreChat
# PostgreSQL (Vector Database / RAG)
POSTGRES_URI=
POSTGRES_HOST=vectordb
POSTGRES_PORT=5432
POSTGRES_USER=alfred
POSTGRES_PASSWORD=
POSTGRES_DB_NAME=alfred
# --- EXTERNAL SERVICES ---
# Media Metadata (Required)
# Get your key at https://www.themoviedb.org/
TMDB_API_KEY=
# TMDB — Media metadata (required). Get your key at https://www.themoviedb.org/
# → TMDB_API_KEY goes in .env.secrets
TMDB_BASE_URL=https://api.themoviedb.org/3
# qBittorrent integration
# qBittorrent
# → QBITTORRENT_PASSWORD goes in .env.secrets
QBITTORRENT_URL=http://qbittorrent:16140
QBITTORRENT_USERNAME=admin
QBITTORRENT_PASSWORD=
QBITTORRENT_PORT=16140
# Meilisearch
# → MEILI_MASTER_KEY goes in .env.secrets
MEILI_ENABLED=FALSE
MEILI_NO_ANALYTICS=TRUE
MEILI_HOST=http://meilisearch:7700
MEILI_MASTER_KEY=
# --- 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
# Local LLM (Ollama)
OLLAMA_BASE_URL=http://ollama:11434
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 ---
# Enable/Disable the Retrieval Augmented Generation system
RAG_ENABLED=TRUE
RAG_API_URL=http://rag_api:8000
RAG_API_PORT=8000
+7 -8
View File
@@ -1,9 +1,8 @@
# Auto-generated from pyproject.toml — do not edit manually
export ALFRED_VERSION=0.1.7
export PYTHON_VERSION=3.14.3
export PYTHON_VERSION_SHORT=3.14
export IMAGE_NAME=alfred_media_organizer
export SERVICE_NAME=alfred
export LIBRECHAT_VERSION=v0.8.4
export RAG_VERSION=v0.7.3
export UV_VERSION=0.11.6
ALFRED_VERSION=0.1.7
PYTHON_VERSION=3.14.3
IMAGE_NAME=alfred_media_organizer
SERVICE_NAME=alfred
LIBRECHAT_VERSION=v0.8.4
RAG_VERSION=v0.7.3
UV_VERSION=0.11.6
+9 -1
View File
@@ -55,7 +55,7 @@ coverage.xml
Thumbs.db
# Secrets
.env
.env.secrets
# Backup files
*.backup
@@ -65,3 +65,11 @@ data/*
# Application logs
logs/*
# Documentation folder
docs/
# .md files
*.md
#
+28 -42
View File
@@ -2,38 +2,36 @@
# check=skip=InvalidDefaultArgInFrom
ARG PYTHON_VERSION
ARG PYTHON_VERSION_SHORT
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
# ===========================================
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 \
PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1
PYTHONUNBUFFERED=1 \
UV_PROJECT_ENVIRONMENT=/venv
# Install build dependencies (needs root)
RUN apt-get update \
&& apt-get install -y --no-install-recommends build-essential \
&& rm -rf /var/lib/apt/lists/*
# Install build dependencies
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get install -y --no-install-recommends build-essential
# Install uv globally (needs root) - Save cache for future
COPY --from=ghcr.io/astral-sh/uv:${UV_VERSION} /uv /usr/local/bin/uv
# Install uv globally
COPY --from=uv-bin /uv /usr/local/bin/uv
# Set working directory for dependency installation
WORKDIR /tmp
# Copy dependency files
COPY pyproject.toml uv.lock Makefile ./
# Install dependencies as root (to avoid permission issues with system packages)
RUN --mount=type=cache,target=/root/.cache/uv uv sync --system
# Install dependencies into /venv
RUN --mount=type=cache,target=/root/.cache/uv uv sync
COPY scripts/ ./scripts/
COPY .env.example ./
@@ -43,7 +41,7 @@ COPY .env.example ./
# ===========================================
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 scripts ./scripts
@@ -54,52 +52,40 @@ COPY tests/ ./tests
# ===========================================
FROM python:${PYTHON_VERSION}-slim-bookworm AS runtime
ARG PYTHON_VERSION_SHORT
# TODO: A-t-on encore besoin de toutes les clés ?
ENV LLM_PROVIDER=deepseek \
MEMORY_STORAGE_DIR=/data/memory \
PYTHONDONTWRITEBYTECODE=1 \
ENV PYTHONDONTWRITEBYTECODE=1 \
PYTHONUNBUFFERED=1 \
PYTHONPATH=/home/appuser \
PYTHONUNBUFFERED=1
PATH="/venv/bin:$PATH"
# Install runtime dependencies (needs root)
RUN apt-get update && apt-get install -y --no-install-recommends \
ca-certificates \
&& rm -rf /var/lib/apt/lists/* \
&& apt-get clean
# Install runtime dependencies
RUN --mount=type=cache,target=/var/cache/apt,sharing=locked \
--mount=type=cache,target=/var/lib/apt,sharing=locked \
apt-get update \
&& apt-get install -y --no-install-recommends ca-certificates
# Create non-root user
RUN useradd -m -u 1000 -s /bin/bash appuser
# Create data directories (needs root for /data)
# Create data directories
RUN mkdir -p /data /logs \
&& chown -R appuser:appuser /data /logs
# Switch to non-root user
USER appuser
# Set working directory (owned by appuser)
WORKDIR /home/appuser
# Copy Python packages 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 /usr/local/bin /usr/local/bin
# Copy venv from builder stage
COPY --from=builder /venv /venv
# Copy application code (already owned by appuser)
# Copy application code
COPY --chown=appuser:appuser alfred/ ./alfred
COPY --chown=appuser:appuser scripts/ ./scripts
COPY --chown=appuser:appuser .env.example ./
COPY --chown=appuser:appuser pyproject.toml ./
# Create volumes for persistent data
VOLUME ["/data", "/logs"]
# Expose port
EXPOSE 8000
# Health check
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", "-m", "uvicorn", "alfred.app:app", "--host", "0.0.0.0", "--port", "8000"]
CMD ["python", "-m", "uvicorn", "alfred.app:app", "--host", "0.0.0.0", "--port", "8000"]
+11 -9
View File
@@ -1,6 +1,7 @@
.DEFAULT_GOAL := help
# --- Load Config from pyproject.toml ---
export
-include .env.make
# --- Profiles management ---
@@ -9,10 +10,12 @@ p ?= full
PROFILES_PARAM := COMPOSE_PROFILES=$(p)
# --- Commands ---
DOCKER_COMPOSE := docker compose
DOCKER_BUILD := docker build --no-cache \
DOCKER_COMPOSE := docker compose \
--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_SHORT=$(PYTHON_VERSION_SHORT) \
--build-arg UV_VERSION=$(UV_VERSION)
# --- Phony ---
@@ -74,7 +77,7 @@ build-test: .env.make
# --- Dependencies ---
install:
@echo "Installing dependencies with $(RUNNER)..."
@echo "Installing dependencies with uv..."
@uv install \
&& echo "✓ Dependencies installed" \
|| (echo "✗ Installation failed" && exit 1)
@@ -86,7 +89,7 @@ install-hooks:
|| (echo "✗ Hook installation failed" && exit 1)
update:
@echo "Updating dependencies with $(RUNNER)..."
@echo "Updating dependencies with uv..."
@uv update \
&& echo "✓ Dependencies updated" \
|| (echo "✗ Update failed" && exit 1)
@@ -112,7 +115,7 @@ lint:
format:
@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 "✗ Formatting failed" && exit 1)
@@ -138,8 +141,7 @@ major minor patch: _check-main
_ci-dump-config:
@echo "image_name=$(IMAGE_NAME)"
@echo "python_version=$(PYTHON_VERSION)"
@echo "python_version_short=$(PYTHON_VERSION_SHORT)"
@echo "runner=$(RUNNER)"
@echo "uv_version=$(UV_VERSION)"
@echo "service_name=$(SERVICE_NAME)"
_ci-run-tests:build-test
@@ -176,7 +178,7 @@ help:
@echo ""
@echo "Dev & Quality:"
@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 " coverage Run tests and generate HTML report"
@echo " lint/format Quality and style checks"
+1 -1
View File
@@ -29,7 +29,7 @@ app = FastAPI(
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))
logger.info(f"Memory context initialized (path: {memory_path})")
+20 -56
View File
@@ -1,14 +1,14 @@
"""
Application settings — Alfred only.
Loaded from .env.alfred and .env.secrets (via docker-compose env_file).
At runtime, all variables are already in the environment — pydantic-settings
picks them up automatically.
Only declares what Alfred's Python code actually consumes.
Everything else (.env.alfred, .env.secrets) is loaded by Docker Compose
for other services and ignored here via extra="ignore".
"""
from pathlib import Path
from pydantic import Field, computed_field, field_validator
from pydantic import field_validator
from pydantic_settings import BaseSettings, SettingsConfigDict
BASE_DIR = Path(__file__).resolve().parent.parent
@@ -20,86 +20,38 @@ class ConfigurationError(Exception):
class Settings(BaseSettings):
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",
extra="ignore",
case_sensitive=False,
)
# --- APP ---
host: str = "0.0.0.0"
port: int = 3080
max_history_messages: int = 10
max_tool_iterations: int = 10
request_timeout: int = 30
llm_temperature: float = 0.2
data_storage_dir: str = "data"
# --- DATABASE ---
mongo_host: str = "mongodb"
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}"
)
# --- BUILD ---
alfred_version: str | None = None
# --- LLM ---
default_llm_provider: str = "local"
ollama_base_url: str = "http://ollama:11434"
# Models: ...
ollama_model: str = "llama3.3:latest"
deepseek_base_url: str = "https://api.deepseek.com"
deepseek_model: str = "deepseek-chat"
# --- API KEYS ---
tmdb_api_key: str | None = None
tmdb_base_url: str = "https://api.themoviedb.org/3"
deepseek_api_key: str | None = None
openai_api_key: str | None = None
anthropic_api_key: str | None = None
google_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 ---
@field_validator("llm_temperature")
@classmethod
@@ -129,5 +81,17 @@ class Settings(BaseSettings):
def is_deepseek_configured(self) -> bool:
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()
+7 -6
View File
@@ -8,7 +8,6 @@ services:
target: builder
args:
PYTHON_VERSION: ${PYTHON_VERSION}
PYTHON_VERSION_SHORT: ${PYTHON_VERSION_SHORT}
UV_VERSION: ${UV_VERSION}
command: python scripts/bootstrap.py
networks:
@@ -22,7 +21,6 @@ services:
context: .
args:
PYTHON_VERSION: ${PYTHON_VERSION}
PYTHON_VERSION_SHORT: ${PYTHON_VERSION_SHORT}
UV_VERSION: ${UV_VERSION}
depends_on:
alfred-init:
@@ -33,13 +31,15 @@ services:
required: true
- path: .env.secrets
required: true
- path: .env.make
required: true
volumes:
- ./data:/data
- ./logs:/logs
# TODO: Hot reload (comment out in production)
#- ./alfred:/home/appuser/alfred
- ./alfred:/home/appuser/alfred
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:
- alfred-net
@@ -89,9 +89,10 @@ services:
- path: .env.secrets
required: true
environment:
# Remap value name
- MONGO_INITDB_ROOT_USERNAME=${MONGO_USER}
- MONGO_INITDB_ROOT_PASSWORD=${MONGO_PASSWORD}
# Fix MongoDB + Linux kernel >= 6.19
- GLIBC_TUNABLES=glibc.cpu.hwcaps=-SHSTK
ports:
- "${MONGO_PORT}:${MONGO_PORT}"
volumes:
@@ -99,7 +100,7 @@ services:
- ./mongod.conf:/etc/mongod.conf:ro
command: ["mongod", "--config", "/etc/mongod.conf"]
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
timeout: 5s
retries: 5
+878
View File
@@ -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
+2 -4
View File
@@ -6,9 +6,7 @@ cache: true
endpoints:
anthropic:
apiKey: "${ANTHROPIC_API_KEY}"
models:
default: ["claude-sonnet-4-5", "claude-haiku-4-5", "claude-opus-4-5"]
fetch: false
models: ["claude-sonnet-4-5", "claude-haiku-4-5", "claude-opus-4-5"]
titleConvo: true
titleModel: "claude-haiku-4-5"
modelDisplayLabel: "Claude AI"
@@ -72,7 +70,7 @@ endpoints:
apiKey: "dummy_key"
baseURL: "http://alfred:8000/v1"
models:
default: ["local-deepseek-agent"]
default: ["glm-4.7-flash:latest"]
fetch: false
titleConvo: false
titleModel: "current_model"
+1 -1
View File
@@ -13,7 +13,7 @@ storage:
systemLog:
destination: file
path: /dev/stdout
logAppend: false
logAppend: true
verbosity: 0
quiet: true
component:
+43 -8
View File
@@ -87,6 +87,39 @@ def extract_python_version(version_string: str) -> tuple[str, str]:
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:
"""Write .env.make from pyproject.toml."""
project = toml_data["project"]
@@ -96,14 +129,13 @@ def write_env_make(toml_data: dict) -> None:
lines = [
"# Auto-generated from pyproject.toml — do not edit manually",
f"export ALFRED_VERSION={project['version']}",
f"export PYTHON_VERSION={python_full}",
f"export PYTHON_VERSION_SHORT={python_short}",
f"export IMAGE_NAME={alfred['image_name']}",
f"export SERVICE_NAME={alfred['service_name']}",
f"export LIBRECHAT_VERSION={alfred['librechat_version']}",
f"export RAG_VERSION={alfred['rag_version']}",
f"export UV_VERSION={alfred['uv_version']}",
f"ALFRED_VERSION={project['version']}",
f"PYTHON_VERSION={python_full}",
f"IMAGE_NAME={alfred['image_name']}",
f"SERVICE_NAME={alfred['service_name']}",
f"LIBRECHAT_VERSION={alfred['librechat_version']}",
f"RAG_VERSION={alfred['rag_version']}",
f"UV_VERSION={alfred['uv_version']}",
]
env_make_path = BASE_DIR / ".env.make"
@@ -138,6 +170,9 @@ def main() -> int:
print("\n🔐 Secrets:")
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:")
write_env_make(toml_data)
-1
View File
@@ -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",
f"export ALFRED_VERSION={config.alfred_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 SERVICE_NAME={config.service_name}",
f"export LIBRECHAT_VERSION={config.librechat_version}",
-22
View File
@@ -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())
+281
View File
@@ -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")