# Server encryption key for API key storage (generate with: node -e "console.log(require('crypto').randomBytes(32).toString('hex'))"). # Required in production. Outside production it is optional: when unset, a key is # auto-generated and written to a file named .encryption-key next to the SQLite # database (not inside it), with 0600 permissions. Precedence: this env var, then # that key file, then a legacy key found in the old settings table (migrated to # the file on first boot), then a freshly generated key. ENCRYPTION_KEY=your-64-char-hex-key-here # Server port (default: 3001) PORT=3001 # Network interface the server listens on. Default '::' (dual-stack IPv4+IPv6); # hosts with IPv6 disabled fall back to IPv4 automatically. Set 0.0.0.0 for # IPv4-only or 127.0.0.1 to restrict to localhost. (Different from HOST_BIND # below, which only affects Docker port publishing.) # HOST=:: # First-run setup: the first account is created through the dashboard. A browser # on the same machine as the server can do this with no extra step. If the server # is reachable from other devices, creating that first account also requires a # one-time setup code, printed in the server logs at startup while no account # exists. This stops a stranger from claiming a freshly exposed install. # Docker only: which host interface the container's port is published on. # Defaults to 127.0.0.1, so the dashboard/API is reachable only from the # machine running Docker. To open it to other devices on your LAN (e.g. a # Raspberry Pi reached at http://192.168.1.x:3001), set HOST_BIND=0.0.0.0. # Only do this on a trusted network — the proxy is single-user and guarded # only by the unified API key. # HOST_BIND=0.0.0.0 # Max /v1 proxy requests per minute per client IP (default: 120). Set to 0 # to disable proxy rate limiting. # PROXY_RATE_LIMIT_RPM=120 # Max /api dashboard requests per minute per client IP (default: 600). This is a # flood guard — login has its own per-email lockout and key export its own much # tighter cap. Set to 0 to disable. # ADMIN_RATE_LIMIT_RPM=600 # JSON body limit in MB for the inference surfaces (/v1, /v1beta, /mcp, # Ollama /api/*) — default: 25. Vision requests embed base64 images in the # body and replay them with every turn, so large sessions can clear 10MB; # this ceiling only gates parsing — inbound image normalization (below) # shrinks the payload afterwards. Raise it if bigger payloads are rejected # with 413 'request_too_large'. # REQUEST_BODY_LIMIT_MB=25 # ChatGPT / MCP inference. ask_freellmapi uses "auto" unless this is set to # a model id or named chain (for example auto:coding). The timeout applies only # to the MCP inference tool; normal /v1 client requests keep their own limits. # MCP_INFERENCE_DEFAULT_MODEL=auto # MCP_INFERENCE_TIMEOUT_MS=120000 # Inbound image normalization: data-URL images over the threshold are # downscaled to the long-edge cap and re-encoded (JPEG, or PNG only when the # alpha channel carries real transparency) before routing, shrinking replayed # screenshots ~6-10x. Also normalizes exotic formats upstreams reject # (bmp/gif/tiff/avif/webp → jpeg/png); gif keeps its first frame only. Every # upstream resizes internally anyway (OpenAI to 2048px, Anthropic to 1568px), # so pixels beyond the cap are transport waste. Set IMAGE_NORMALIZE=off to # disable entirely. # IMAGE_NORMALIZE=on # IMAGE_NORMALIZE_MAX_DIMENSION=2048 # IMAGE_NORMALIZE_THRESHOLD_KB=1024 # IMAGE_NORMALIZE_QUALITY=90 # Outbound proxy for provider requests. Normally set in the dashboard # (Keys → Outbound proxy); these env vars are for headless installs. # Schemes: http, https, socks4, socks4a, socks5, socks5h. The `h`/`a` variants # resolve DNS at the proxy, which is what you want on a DNS-poisoned network. # Precedence: PROXY_URL → dashboard setting → ALL_PROXY → HTTPS_PROXY → # HTTP_PROXY. The standard vars are also read in their lower-case spellings. # PROXY_URL=socks5h://127.0.0.1:1080 # ALL_PROXY=socks5h://127.0.0.1:1080 # HTTPS_PROXY=http://proxy.corp.com:8080 # HTTP_PROXY=http://proxy.corp.com:8080 # # Application-layer Fetch Relay (opt-in). The relay target and its separate # bearer credential use Fetch-Relay-Target and Fetch-Relay-Authorization. # See docs/fetch-relay.md. # PROXY_MODE=fetch-relay # PROXY_URL=https://relay.example.workers.dev # FETCH_RELAY_TOKEN=generate-a-long-random-token # # IN DOCKER, 127.0.0.1 IS THE CONTAINER, NOT YOUR MACHINE. If the proxy runs # on the host (Clash, v2rayN, sing-box, a corporate client), use the host's # address instead, and make sure the proxy listens on more than loopback # (Clash: `allow-lan: true`): # PROXY_URL=socks5h://host.docker.internal:7890 # The bundled docker-compose.yml maps host.docker.internal to the host gateway # so this works on plain Linux Docker too, not just Docker Desktop. # Hosts that must be reached directly, bypassing the proxy above. Comma- # separated; a bare domain also covers its subdomains, and `*` disables the # proxy entirely. # NO_PROXY=localhost,127.0.0.1,.internal.corp # Local and LAN destinations (localhost, 127.0.0.0/8, ::1, 0.0.0.0, RFC1918 / # ULA / CGNAT addresses) always bypass the proxy — a remote proxy has no route # back to your own machine, and an IP literal makes Tor log "giving Tor only an # IP address" and refuse the connection. That is the right default for a local # Ollama / llama.cpp / LM Studio endpoint. # Set this to true only if proxying them is the point — e.g. an `ssh -D` # dynamic tunnel, where http://127.0.0.1:11434 through the SOCKS proxy is meant # to reach the REMOTE host's Ollama. # FREEAPI_PROXY_LOCAL_DESTINATIONS=true # Per-provider daily request cap, named PROVIDER_DAILY_REQUEST_CAP_ # (platform name upper-cased). Overrides the built-in default for that provider; # set to 0 to disable the cap. Example — cap OpenRouter at 50 requests/day: # PROVIDER_DAILY_REQUEST_CAP_OPENROUTER=50 # Per-provider chat HTTP timeout in milliseconds, named # PROVIDER_TIMEOUT_ (platform name upper-cased). Overrides the # built-in default for that provider (60s for most OpenAI-compatible platforms, # 180s NVIDIA, 120s Ollama Cloud / AI Horde / custom providers, 60s Google as # registered). On streaming requests this value also serves as the FIRST-BYTE # grace budget: how long the stream may stay silent before its first byte # (issue #584 — providers like NVIDIA NIM send SSE headers instantly, then # prefill long prompts for minutes). Set to 0 to disable the timeout entirely. # Read at startup. Example — give NVIDIA NIM's slow reasoning models 5 minutes: # PROVIDER_TIMEOUT_NVIDIA=300000 # ModelScope (魔搭) validates API keys with a paid 1-token chat completion # (GET /v1/models does not enforce auth), which spends the account's # magic-grain (魔粒) quota per probe — observed as 2 魔粒 per ultra-tier # request. To stop the 5-minute health pass from burning ~288 paid probes # per key per day, a successful validation is cached per key for this long # (milliseconds, default 86400000 = 24h). Set to 0 to probe on every pass. # A revoked key is still caught by the next real request's 401. # MODELSCOPE_VALIDATE_CACHE_MS=86400000 # Mid-stream inactivity timeout for streaming responses, in milliseconds # (default: 90000). If an SSE stream goes this long without a single byte # AFTER the first byte arrived, the gateway gives up on it (before any output # reached the client this fails over to the next candidate; after output was # sent the stream ends with an error frame). Time-to-FIRST-byte is governed by # the chat timeout above instead (whichever of the two is larger). Providers # that queue heavily on their free tiers can pause longer than 90s mid-stream — # raise this if streamed responses stop early. Set to 0 to disable the stall # watchdog entirely. # PROVIDER_STREAM_STALL_TIMEOUT_MS=90000 # # Per-provider variant, named PROVIDER_STREAM_STALL_TIMEOUT_ # (platform name upper-cased). Precedence: per-platform > global (above) > # built-in 90s default. Example — let Ollama Cloud pause 4 minutes mid-stream # without loosening the watchdog everywhere else: # PROVIDER_STREAM_STALL_TIMEOUT_OLLAMA=240000 # Base wall-clock budget for provider failover, in milliseconds (default: 45000). # The initial attempt and one failover hop are always allowed. After that, # exhaustion stops new attempts and can cancel an in-flight attempt still # waiting for its first usable response. Streaming responses disarm this timer # when the first usable response arrives. Successful TTFB history can widen # the budget for a slow endpoint (see below). Set to 0 to disable the budget # entirely, including adaptive widening, and retry until the attempt cap. # Can also be set at runtime via the fallback_time_budget_ms settings key, # which takes precedence. # FALLBACK_TIME_BUDGET_MS=45000 # Adaptive patience for slow endpoints: successful time-to-first-byte (TTFB) # samples are grouped by platform + endpoint_scope. The effective budget is # max(base budget, historical P95 TTFB + this buffer), in milliseconds. # Endpoints with no samples, and those whose P95 + buffer fits the base budget, # keep the base budget. The settings key slow_endpoint_buffer_ms takes # precedence over this env var. Set the buffer to 0 for no extra padding. # SLOW_ENDPOINT_BUFFER_MS=10000 # Successful TTFB sample window, in milliseconds (default: 7 days). # The settings key ttfb_budget_window_ms takes precedence. # TTFB_BUDGET_WINDOW_MS=604800000 # Sample-weight half-life, in milliseconds (default: 2 days). Older successes # count less so the budget adapts as an endpoint speeds up or slows down. # The settings key ttfb_budget_half_life_ms takes precedence. # TTFB_BUDGET_HALF_LIFE_MS=172800000 # Successful samples an endpoint needs before its history widens the budget # (default: 5). The widened budget is capped at three times the base budget. # The settings key ttfb_budget_min_samples takes precedence. # TTFB_BUDGET_MIN_SAMPLES=5 # Set to 1 to disable adaptive TTFB budgets and use only the base budget. # TTFB_BUDGET_DISABLED=1 # Add X-Fallback-Detail to responses (default: off). X-Fallback-Trail already # names every hop that failed and why; the detail header adds what each one # COST — "groq/llama-3.3-70b key1=rate_limited t=0+39000ms msg=..." — so a # caller can tell one 39s stall apart from four fast failures without opening # the dashboard. Off by default because it puts hop timings and provider error # text on the response. Can also be set at runtime via the # expose_fallback_detail_header settings key, which takes precedence. # FALLBACK_DETAIL_HEADER=1 # Check tool-call arguments against the schema the caller declared, and fail # over to another model when they violate it (default: off). lib/tool-args.ts # already repairs what it can prove is repairable; this catches what is left, # which today reaches Anthropic clients as a tool_use block with `input: {}` and # no indication anything went wrong. A different model usually gets the same # call right. Off by default because a false positive costs a failover hop — # turn it on, watch X-Fallback-Trail for `invalid_tool_arguments`, then decide. # Applies wherever the turn can still fail over — every non-streaming surface, # and the streams that buffer tool calls to the end (/v1/chat/completions, # /v1/messages, the Gemini and Ollama emulations). /v1/responses streaming is # the exception: it commits on the first tool-call delta, before the arguments # are complete, so a verdict there could only break a stream in flight. # Can also be set at runtime via the validate_tool_arguments settings key, # which takes precedence. # VALIDATE_TOOL_ARGUMENTS=1 # Request guardrails, both OFF (0) by default. Also runtime-tunable via the # request_max_tokens_budget / max_consecutive_upstream_fails settings keys # (dashboard API PUT /api/settings/guardrails), which take precedence. # # Per-request token budget: estimated input + requested max_tokens must fit # this ceiling or the request is rejected with a 413 before any provider is # tried; a request that sent no max_tokens gets its output capped to the # remainder instead. # REQUEST_MAX_TOKENS_BUDGET=0 # # Failover circuit breaker: after this many consecutive upstream failures in # one request, stop the failover chain with a 503 instead of trying every # remaining candidate of an unhealthy pool. # MAX_CONSECUTIVE_UPSTREAM_FAILS=0 # Opt-in response cache. When on, a successful /v1/chat/completions answer -- # streaming or not -- is stored in a bounded in-memory LRU keyed by a hash of the # request, so an IDENTICAL later request is served from memory without spending # any provider quota. Exact match only (no fuzzy matching), so a near-miss never # returns a different prompt's answer. Off by default; set to true to enable. # Can also be toggled at runtime via the dashboard (PUT /api/cache/config), and # per-request with the `X-FreeLLM-Cache: on|off` header. # RESPONSE_CACHE=false # Persist cached non-streaming answers to SQLite so they survive a restart # (streamed replays are memory-only and never persisted) (the daily # quota-reset re-run pattern). Only applies when the cache above is on. Note # this writes PLAINTEXT model responses to the database file, which the # memory-only cache never did; set it to false to keep caching but hold # everything in memory, as before. Entries are removed from disk the moment # they leave the in-memory cache (TTL expiry, eviction, DELETE /api/cache). # RESPONSE_CACHE_PERSIST=true # How long a cached answer stays fresh, in seconds (default: 3600 = 1 hour). # RESPONSE_CACHE_TTL_SECONDS=3600 # Only cache requests at/below this temperature; higher temperatures want fresh # variety, so they are never cached. Default 1.0 caches everything when enabled; # lower it (e.g. 0.2) to cache only near-deterministic calls. # RESPONSE_CACHE_MAX_TEMPERATURE=1.0 # Hard cap on stored entries, shared by the JSON and streaming stores; the # least-recently-used are evicted past this. # RESPONSE_CACHE_MAX_ENTRIES=5000 # Request-side prompt/context compression. The default is off. `lossless` # applies whitespace hygiene, exact repeated-block references, and reversible # tabular-JSON encoding; `standard` also filters tool output and superseded file # reads; `aggressive` adds age/relevance/budget condensation. The dashboard can # change this live under Settings. A request may lower the configured mode with # `X-FreeLLM-Compress`, but cannot override a global `off` master switch. # FREELLMAPI_COMPRESSION=off # Custom-provider URL policy. Cloud metadata and link-local addresses # (169.254.169.254, metadata.google.internal, fe80::/10, ...) are ALWAYS # blocked as custom provider base URLs. By default localhost and private LAN # addresses stay allowed so local Ollama / LM Studio / llama.cpp setups work. # If you host FreeLLMAPI on a VPS or anywhere the dashboard is reachable by # others, set this to also block loopback and RFC1918/ULA private ranges. # FREEAPI_BLOCK_PRIVATE_PROVIDER_URLS=true # Request analytics retention. Set either value to 0 to disable that limit. REQUEST_ANALYTICS_RETENTION_DAYS=90 REQUEST_ANALYTICS_MAX_ROWS=100000 # Provider quota observation log (the audit trail behind the per-key quota # state the dashboard shows). Only the newest row per pool is ever read, so the # history is pruned daily by age and by count. 0 disables that half. # QUOTA_OBSERVATIONS_RETENTION_DAYS=30 # QUOTA_OBSERVATIONS_MAX_ROWS=200000 # Persisted server logs behind the dashboard's log viewer. Only warn/error # lines are written to the database (the live view is an in-memory ring), so # these bounds are far tighter than the analytics ones above. Either value at 0 # disables that limit. # SERVER_LOGS_RETENTION_DAYS=7 # SERVER_LOGS_MAX_ROWS=50000 # Per-request caller identity (client IP + User-Agent) recorded into request # analytics and shown in the dashboard's "Recent calls" table. Set to false # to store nulls instead (aggregate analytics are unaffected). # REQUEST_ANALYTICS_LOG_CLIENT=true # Optional SQLite location. Useful on hosts where only one directory is mounted # persistently, or when you want to keep the DB outside server/data. # FREEAPI_DB_PATH=/app/server/data/freellmapi.db # Whether to restrict the directory holding the database to this account, so the # SQLite -wal/-shm sidecars are protected too (they are created on first write, # after any startup permission check could reach them). Done automatically for # the default server/data directory and for any directory FreeLLMAPI creates # itself; skipped for a FREEAPI_DB_PATH pointing somewhere pre-existing, since # that may be a shared location this server should not be locking down. Set to 1 # if the directory belongs to this account alone, or 0 to disable entirely. # FREEAPI_DB_DIR_HARDENING=1 # Optional encrypted SQLite backup. On startup, FreeLLMAPI restores this backup # if the configured DB file is missing; while running, it uploads a fresh backup # periodically. File paths and HTTP(S) URLs are supported. # FREEAPI_DB_BACKUP_PATH=/app/server/data/freellmapi.db.backup # FREEAPI_DB_BACKUP_URL=https://example.com/freellmapi.db.backup # FREEAPI_DB_BACKUP_TOKEN= # FREEAPI_DB_BACKUP_KEY=use-a-separate-64-char-hex-key-or-omit-to-use-ENCRYPTION_KEY # FREEAPI_DB_BACKUP_INTERVAL_MS=300000 # Optional declarative startup config. Use either inline JSON or a path to a JSON # file. It is applied idempotently after migrations on every boot — before the # HTTP listener starts, so an "admin" entry closes the first-run setup window # entirely. Optional "admin" creates the first dashboard account (only while no # account exists); optional "license" activates a freellmapi.co Premium key. # The JSON carries plaintext secrets — protect it like ENCRYPTION_KEY. # FREEAPI_CONFIG_PATH=/app/server/data/freellmapi.config.json # FREEAPI_CONFIG_JSON={"admin":{"email":"ops@example.com","password":"change-me"},"license":"...","keys":[{"platform":"groq","key":"gsk_...","label":"main"}],"routing":{"strategy":"balanced"}} # Context handoff on model switch. When enabled, FreeLLMAPI injects a compact # system message into the outbound request whenever a session switches from one # model to another (e.g. after a fallback). Off by default. # FREELLMAPI_CONTEXT_HANDOFF=on_model_switch # Manual application update checker. Set to off to hide it from Settings and # prevent Git discovery or outbound update-check requests. Enabled by default. # This also switches off the automatic release reminder, which is a separate # dashboard setting (Settings > General) and is off until turned on there. # FREELLMAPI_UPDATE_CHECK=off # Optional token used only for update checks against GitHub. The checker is # anonymous by default; use a narrowly scoped token only if higher rate limits # are needed. Generic GITHUB_TOKEN values are intentionally ignored. # FREELLMAPI_UPDATE_GITHUB_TOKEN= # FREELLMAPI_COMMIT_SHA and FREELLMAPI_INSTALL_METHOD are injected by official # Docker and desktop builds and normally should not be set in .env. # Comma-separated extra origins allowed to call the API from a browser. # localhost:5173, 127.0.0.1:5173, [::1]:5173 are allowed by default for the # Vite dev dashboard. Only set this if you serve the dashboard from a # different host than the API (e.g. http://my-server.local). # DASHBOARD_ORIGINS=http://my-server.local,http://192.168.1.50 # Content-Security-Policy `upgrade-insecure-requests` directive (#682). # Auto (unset): emit only when the request arrives over TLS or behind an # HTTPS reverse proxy (X-Forwarded-Proto: https). Plain-HTTP LAN installs # (HOST_BIND=0.0.0.0, no TLS in front) stay renderable. # true: force the directive on even over plain HTTP (rarely useful). # false: force it off even behind HTTPS (e.g. mixed-content reverse proxies). # CSP_UPGRADE_INSECURE_REQUESTS=false # Trust forwarded headers (X-Forwarded-For / X-Forwarded-Proto) from a reverse # proxy so Analytics and IP-based rate limiting see the real client (#1024). # Default (unset or false): do NOT trust forwarded headers — direct callers # cannot spoof the client IP. # true: trust every proxy hop (only use on a fully trusted network). # A number: trust that many hops in front of the server (TRUST_PROXY=1 for a # single reverse proxy such as Caddy, nginx or Traefik on the same host). # Otherwise: a comma-separated list of proxy addresses/CIDRs to trust, e.g. # TRUST_PROXY=100.64.0.0/10,192.168.1.10 # TRUST_PROXY=false # Per-model routing score multipliers (#738), JSON object of model id → 0..2. # 1.0 leaves a model unchanged, below 1 demotes it, above 1 promotes it, and 0 # means the automatic router never picks it (Manual priority still can). # Matching is by model id alone, across all platforms serving that id, and the # match is exact and case-sensitive. Copy ids from the dashboard Models page: # an id that matches nothing is silently ignored (the boot log flags it once the # catalog has synced). # MODEL_ROUTING_OVERRIDES={"gpt-4o": 0.2, "gpt-oss-120b": 1.5} # --- Advanced / embedding --------------------------------------------------- # Path to a prebuilt client `dist` directory to serve. Defaults to the bundled # client build; set this only if you build the dashboard separately. # CLIENT_DIST=/path/to/client/dist # Explicit path to the .env file to load. Useful for embedders (e.g. the # desktop app, where the code runs from inside a bundle). Defaults to ./.env. # FREEAPI_ENV_PATH=/path/to/.env