# DiscoveryGram configuration. # Copy to .env and fill in. See docs/CONFIGURATION.md for the full reference. # ============================================================================= # Telegram (required) # ============================================================================= TELEGRAM_BOT_TOKEN= # Comma-separated allow-list. At least one id is required — an empty list would # expose the bot to everyone. Send /whoami to the bot to discover your id. TELEGRAM_ALLOWED_USER_IDS= # Optional: restrict group chats too. Empty means every chat an allowed user writes in. TELEGRAM_ALLOWED_CHAT_IDS= # polling (default, works anywhere) | webhook (production, behind a reverse proxy) TELEGRAM_MODE=polling # Public HTTPS URL of the reverse proxy in front of the bot. Required for webhook mode. TELEGRAM_WEBHOOK_URL= # Telegram signs every delivery with this, so requests that reach the port from # anywhere else are rejected before being parsed. Strongly recommended. TELEGRAM_WEBHOOK_SECRET= TELEGRAM_WEBHOOK_LISTEN=0.0.0.0 TELEGRAM_WEBHOOK_PORT=8081 TELEGRAM_WEBHOOK_PATH=telegram TELEGRAM_PARSE_MODE=MarkdownV2 # ============================================================================= # NoteDiscovery (required) # ============================================================================= # Base URL *with port*. From inside Docker, a host instance is host.docker.internal. NOTEDISCOVERY_URL=http://host.docker.internal:8000 # Optional — an instance may run unauthenticated. Sent as X-API-Key. NOTEDISCOVERY_API_KEY= NOTEDISCOVERY_TIMEOUT=30 NOTEDISCOVERY_MAX_RETRIES=3 NOTEDISCOVERY_VERIFY_TLS=true # rest | mcp. REST is primary: NoteDiscovery's MCP server exposes a strict subset # of the REST API (no media upload, export, sharing or folder move/rename/delete). NOTEDISCOVERY_TRANSPORT=rest # GET /api/search has no server-side default cap, so we always send one. SEARCH_DEFAULT_LIMIT=50 # Shortest query NoteDiscovery will act on. It is a server constant that the API # does not expose (2 in 0.31.3); shorter queries are refused locally instead. SEARCH_MIN_QUERY_LENGTH=2 TREE_CACHE_TTL_S=300 # Where /quick captures land: one note per day, under this folder. INBOX_PATH=Inbox # With this off, a note aimed at a folder that does not exist is refused rather # than silently inventing a folder tree. AUTO_CREATE_PARENTS=true # ============================================================================= # MCP subprocess (only when NOTEDISCOVERY_TRANSPORT=mcp) # ============================================================================= MCP_ENABLED=false # docker = spawn `docker run` (requires /var/run/docker.sock mounted — root-equivalent # host access, opt in deliberately) # local = spawn `python -m mcp_server` from a vendored module MCP_LAUNCH_MODE=docker MCP_DOCKER_IMAGE=ghcr.io/gamosoft/notediscovery:latest MCP_STARTUP_TIMEOUT_S=30 # ============================================================================= # LLM router (phase 5) # ============================================================================= # Failover works on (provider, model) pairs. Each pair is retried # LLM_RETRIES_PER_MODEL times, then the next MODEL of the same provider is # tried; only when a provider's models run out does the next provider take # over. `make check-env` prints the exact ladder your configuration produces. # # LLM_CHAIN_CHAT=groq,ollama with GROQ_MODELS=a,b and OLLAMA_MODELS=c gives: # groq/a (x3) -> groq/b (x3) -> ollama/c (x3) -> give up LLM_CHAIN_CHAT=groq,cerebras,openrouter,ollama LLM_CHAIN_VISION=gemini,openrouter,ollama # Retries against the SAME (provider, model) pair before moving to the next model. LLM_RETRIES_PER_MODEL=3 LLM_BACKOFF_BASE_S=1.0 LLM_REQUEST_TIMEOUT_S=60 LLM_CIRCUIT_FAILURE_THRESHOLD=5 LLM_CIRCUIT_RESET_S=120 # 0 disables the per-user daily cap. It counts *requests*: one capture that # fails over across four rungs is one call against the user's budget. LLM_DAILY_CALL_LIMIT_PER_USER=100 # Burst limit per user, per rolling minute; 0 disables it. The daily cap bounds # spend, this bounds rate. Keep it loose: one photo capture is several calls # (vision, tidy, title, tags, summary), and a limit that refuses halfway through # leaves a half-written draft. LLM_USER_RATE_PER_MINUTE=20 # Provider calls in flight across the whole process; 0 disables the bound. # Back-pressure: past this, requests queue instead of piling onto a provider # that is already the slow part. LLM_MAX_CONCURRENT_REQUESTS=8 # --- Generation behaviour --------------------------------------------------- # Most tags a generated note may carry. A model asked for "some tags" will # happily return twenty, and the vault's tag index is shared across every note. GENERATED_TAGS_MAX=5 # Whether a generated note records the provider and model that made it, as an # HTML comment in the body — greppable, but invisible when the note is rendered. PROVENANCE_ENABLED=true # How many notes /ask may read as context. Each one is a vault read and tokens # in the prompt, so this bounds both cost and latency. ASK_CONTEXT_NOTES=5 # --- Provider credentials and model preferences ----------------------------- #
_MODELS and
_VISION_MODELS are ordered, comma-separated lists. # A provider with no key (except ollama) or no model for the task is skipped, # and the reason is logged at startup. # # Every
_BASE_URL below is optional and shown at its built-in default: set # one only to point at a proxy, a self-hosted gateway or a pinned API version. # See docs/LLM_PROVIDERS.md for per-provider setup and quirks. # OpenAI-compatible (/chat/completions). NVIDIA_API_KEY= NVIDIA_BASE_URL=https://integrate.api.nvidia.com/v1 NVIDIA_MODELS= NVIDIA_VISION_MODELS= OPENROUTER_API_KEY= # OPENROUTER_BASE_URL=https://openrouter.ai/api/v1 OPENROUTER_MODELS= OPENROUTER_VISION_MODELS= GROQ_API_KEY= # GROQ_BASE_URL=https://api.groq.com/openai/v1 GROQ_MODELS= GROQ_VISION_MODELS= # Gemini speaks its own dialect: key in x-goog-api-key, model in the URL path. GEMINI_API_KEY= # GEMINI_BASE_URL=https://generativelanguage.googleapis.com/v1beta GEMINI_MODELS= GEMINI_VISION_MODELS= # Workers AI puts the account id in the URL, so the key alone is not enough: # cloudflare in a chain without CLOUDFLARE_ACCOUNT_ID is skipped at startup. CLOUDFLARE_API_KEY= CLOUDFLARE_ACCOUNT_ID= # CLOUDFLARE_BASE_URL=https://api.cloudflare.com/client/v4 CLOUDFLARE_MODELS= CLOUDFLARE_VISION_MODELS= # Cerebras serves text-only models. It is dropped from the vision ladder even # if CEREBRAS_VISION_MODELS is set, so there is no such variable here. CEREBRAS_API_KEY= # CEREBRAS_BASE_URL=https://api.cerebras.ai/v1 CEREBRAS_MODELS= MISTRAL_API_KEY= # MISTRAL_BASE_URL=https://api.mistral.ai/v1 MISTRAL_MODELS= MISTRAL_VISION_MODELS= # Puter has no chat-completions endpoint; everything goes through one RPC # call. Its API carries no version guarantee, so put it last in a chain. PUTER_API_KEY= # PUTER_BASE_URL=https://api.puter.com PUTER_MODELS= PUTER_VISION_MODELS= # Ollama needs no key. The adapter uses its OpenAI-compatible surface at /v1; # a bare host is accepted and the suffix added for you. OLLAMA_BASE_URL=http://localhost:11434 OLLAMA_MODELS= OLLAMA_VISION_MODELS= # ============================================================================= # Sessions, cache and limits # ============================================================================= SESSION_BACKEND=memory REDIS_URL= SESSION_TTL_S=3600 RESULTS_PAGE_SIZE=5 # Default window for /recent, in days. Override per call with /recent 30. RECENT_DEFAULT_DAYS=7 TREE_PAGE_SIZE=10 LONG_NOTE_MODE=paged # Telegram's Bot API caps file downloads at 20 MB. A larger file is refused # before it is downloaded, so it costs no transfer at all. MAX_UPLOAD_MB=20 # search | quick. What a plain message (no command) does: run a search, or # capture into today's inbox note. With `quick`, a message is never also # searched — a thought you meant to keep must not become a query. DEFAULT_TEXT_ACTION=search # ============================================================================= # Observability # ============================================================================= LOG_LEVEL=INFO LOG_FORMAT=json HEALTH_PORT=8080 METRICS_ENABLED=false