# atrium-llm-enrich — the published environment contract (atrium-project#60). # # Copy to `.env` and edit. Do not commit `.env` (it is gitignored at .gitignore:151 # — keep it that way; this is the one repo whose contract includes an API key). # # This file is the COMPLETE ledger: every variable the api image reads, at the # default that is in the CODE. The operator-facing subset across all five services is # the table in the hub's docs/k8s_deployment.md; this file's layout is fixed by # docs/templates/env.example.template in ufal/atrium-project. # # TAG LEGEND: [required] raises at startup without it · [secret] never a literal # anywhere committed, use a k8s Secret · [operator] an ordinary deployment knob · # [algorithmic] changes what the service COMPUTES, not how it is deployed · # [build] read by compose/Dockerfile or baked at build time · [limit] a limit # (atrium-project#53): reported in GET /info, listed in service/README.md § Limits # # HOW THIS FILE REACHES THE PROCESS # docker compose → delivered. docker-compose.yaml's `llm-enrich-api` service # declares `env_file: .env`, so everything below is injected # into the container. Eight names (LLM_BACKEND, the OpenRouter # and Ollama connection variables, ALLOWED_ORIGINS, # MAX_UPLOAD_MB, PORT) are ALSO named explicitly in the # `environment:` block, because they need a value different # from a bare pass-through (OLLAMA_HOST's compose default # deliberately differs from the code default — see below) or # because compose itself needs the value (PORT, for the # `ports:` mapping). Compose ALSO reads this file for ${VAR} # substitution inside docker-compose.yaml itself; that is a # DIFFERENT mechanism from the injection above. # Kubernetes → NOT delivered. Use the manifest's own env: block. See # ufal/atrium-project docs/k8s_deployment.md. # python -m service.api # → NOT delivered. Nothing here calls load_dotenv(); export the # values yourself: set -a; . ./.env; set +a # # BLANK IS NOT UNIVERSALLY "UNSET". ALLOWED_ORIGINS is the trap: a blank value parses # to an EMPTY origin list and blocks every browser request, where omitting the line # gives you `*` — so it ships commented below, not blank. # ── §0 Required before first start ───────────────────────────────────────────── # [required] OPENROUTER_API_KEY and OPENROUTER_MODEL are required for the DEFAULT # backend (LLM_BACKEND=openrouter, below); OLLAMA_MODEL is required in their place if # you set LLM_BACKEND=ollama instead. See §5 for all three and the full backend # selection. The reference k8s manifest ships no Secret for OPENROUTER_API_KEY, which # is why llm-enrich cannot be deployed from it unmodified (atrium-project#60; # docs/k8s_deployment.md's "Known limits"). # ── §1 HTTP surface ───────────────────────────────────────────────────────────── # [operator] Port the api service binds, and the port service/healthcheck.py probes # inside the container. In Kubernetes, change `containerPort` in the same edit. # (atrium-project#58) PORT=8000 # [operator] Bind address. Leave at 0.0.0.0. service/healthcheck.py always probes # 127.0.0.1 by design and never reads HOST, so 127.0.0.1 here gives a container that # reports HEALTHY to every probe while being unreachable from outside it. This is the # one value whose failure mode is silent. # HOST=0.0.0.0 # [operator] CSV of CORS origins. Code default `*` (every origin) — # service/atrium_service.py:370 falls back to it when unset. This repo's compose # default is also `*`, unlike alto-postprocess's and page-classification's narrower # localhost lists. Ships commented so an operator who leaves it alone gets the real # code default; uncomment to narrow it for a real deployment. # ALLOWED_ORIGINS=* # [limit] Canonical upload limit in megabytes, per uploaded part (the file and # document_json) and for the whole /extract_keywords_text body. 10 is THIS repo's # built-in (tool_limits.py, `upload_limit(10)`); the five services do not share one # value — alto 25, llm-enrich 10, nlp-enrich 5, page-classification 10, translator 50. # This is the only repo whose compose file passes it through. Over it → 413 # limit_exceeded. Default: 10. MAX_UPLOAD_MB=10 # [deprecated] Do not set. A byte-valued fallback kept working for one release for # callers that already exported it (strategy §4.5); MAX_UPLOAD_MB wins whenever both # are present (service/atrium_service.py:353-364). Listed commented-out so the # contract is complete and the deprecation is discoverable. # MAX_UPLOAD_BYTES= # [operator] Path the container's own HEALTHCHECK probes. Change it only alongside # the route itself. # HEALTHCHECK_PATH=/health # ── §2 Lifecycle ──────────────────────────────────────────────────────────────── # [operator] Seconds uvicorn waits for in-flight requests after SIGTERM. llm-enrich # calls the model per line inside the request, so a single request can legitimately # run for minutes — this is one of the two services most likely to need a raise. # Raise it together with the deployment's grace period (terminationGracePeriodSeconds # in k8s, see docs/k8s_deployment.md in the hub). GRACEFUL_SHUTDOWN_S=20 # [operator] Root logger level for the `python -m service.api` start path. All five # services honour it identically (atrium-project#61). LOG_LEVEL=INFO # [operator] Filesystem auto-reload. Development only — never in a deployment. # Deliberately NOT wired into any compose or k8s template. # RELOAD=false # ── §3 Backing services (atrium-project#63) ───────────────────────────────────── # None — this service's backend selection lives in §5 (LLM_BACKEND). The #63 # attachable-endpoint pattern (a self-hosted alternative to a public default) does # not apply here: OpenRouter and Ollama are alternative PROVIDERS, not a fallback # host for the same one. # ── §4 Image / provenance ──────────────────────────────────────────────────────── # [build] Image tag used by docker-compose: on release, para_config.txt's [tool] version # WITHOUT the leading `v` (para_config.txt says `v0.8.0`, GHCR carries `0.8.0` -- # atrium-project#69, roadmap B3). Read only by docker-compose.yaml; no Python here reads it. ATRIUM_VERSION=dev # [build] Baked at build time; stamped into paradata as runner_ref. ATRIUM_RUNNER_REF=dev # [build] Override either only if you publish under another name. # ATRIUM_RUNNER_IMAGE=ghcr.io/ufal/atrium-llm-enrich-api:dev # ATRIUM_RUNNER_REPO=https://github.com/ufal/atrium-llm-enrich # [build] The uid docker-compose.yaml runs the containers as (`user: ${ATRIUM_UID:-10001}:0`); # no Python here reads it. On Linux set it to your own uid -- `echo "ATRIUM_UID=$(id -u)" >> .env` # (compose does not run `$(...)` itself) -- so the container can write ./data, which a clone # owns as you. Docker Desktop needs nothing. A named volume created by an older image is # writable by uid 10001 only: recreate it once (`docker compose down -v`) after setting # this. (atrium-project#69, roadmap B6) # ATRIUM_UID=10001 # [build] Correlation id copied verbatim into paradata `request_id`. Read ONCE from # the process environment at import (atrium_paradata.py's module-level constants), # so it identifies a batch run, not an individual HTTP request. # ATRIUM_REQUEST_ID= # [build] Where the HuggingFace model cache lives. The Dockerfile sets this and # docker-compose.yaml mounts a named volume at it; without the volume the cache is # re-downloaded on every start and the service comes up DEGRADED on a host with no # egress. # HF_HOME=/cache/huggingface # ── §5 Service knobs — operator ────────────────────────────────────────────────── # [operator] Which LLM backend the service loads: `openrouter` (a hosted API, needs # a key) or `ollama` (a self-hosted server, needs a reachable host). This choice # decides which of the two blocks below you must fill in; the other is ignored # entirely. LLM_BACKEND=openrouter # [operator] Config file read for any value not supplied by the environment. The # environment always wins. Relative to the working directory. LLM_CONFIG=llm_config.txt # [required, secret] OpenRouter backend, only when LLM_BACKEND=openrouter. Never # commit a real value — not to this file, not to .env. In Kubernetes do NOT put it # in the manifest's env: block as a literal either: use a Secret and a # secretKeyRef. service/api.py:128-131 raises at startup without it. OPENROUTER_API_KEY= # [required] An OpenRouter model id, e.g. openai/gpt-4o-mini. service/api.py:129-134 # raises at startup without it. OPENROUTER_MODEL= # [operator] Optional attribution headers OpenRouter shows on your dashboard. # Neither affects behaviour. # OPENROUTER_SITE_URL= OPENROUTER_APP_NAME=atrium-llm-enrich # [operator] Ollama backend, only when LLM_BACKEND=ollama. ⚠️ The CODE default is # http://localhost:11434, which is wrong INSIDE a container — it points at the # container's own loopback, where no Ollama runs. docker-compose.yaml supplies # http://host.docker.internal:11434 instead, which is the correct value for a # host-run Ollama and applies ONLY under compose. Ships COMMENTED, deliberately: an # ACTIVE value here would be injected via env_file and defeat compose's safer # default the same way an active ALLOWED_ORIGINS would (see §1) — leaving it # commented is what lets `${OLLAMA_HOST:-host.docker.internal}` in # docker-compose.yaml actually win. In Kubernetes, set this explicitly to the Ollama # Service's DNS name; leaving it unset there gives you the broken code default, not # the working compose one. # OLLAMA_HOST=http://localhost:11434 # [required] An Ollama model tag, e.g. llama3.1:8b, required for this backend — # service/api.py:150 raises without it. # OLLAMA_MODEL= # Limits (atrium-project#53) — every limit of this service except MAX_UPLOAD_MB (§1), # declared in tool_limits.py, reported with its current value in GET /info (`limits`; # `limits_meta` names the variable and whether the value came from the environment, the # config file or the default), and listed in service/README.md's `## Limits` table. A # limit that shaped a result without refusing it is returned in the response's # `limits_applied`. A malformed value stops the service at startup, naming the variable. # # [limit] Per-call read timeout in seconds (both backends); a timeout is retried. Raise # it for long documents or a slow self-hosted model. Default: 300. LLM_TIMEOUT=300 # [limit] Attempts of one call (both backends). Only a timeout, a connection error, # HTTP 429 or 5xx is retried; another 4xx is refused at once with the provider's reply. # Once they run out: document mode → 502, line mode → that line is an error. Default: 3. LLM_MAX_RETRIES=3 # [limit] Most tokens one reply may have — OpenRouter `max_tokens`, Ollama # `num_predict`. A reply cut at it is never used: document mode → 422 limit_exceeded, # line mode → the line gets no result, with a `skipped` note. Default: 2048. LLM_MAX_NEW_TOKENS=2048 # [limit] Line mode: consecutive failed lines after which the document is given up, with # a `stopped` note (the lines before keep their results). Default: 10. LLM_MAX_CONSECUTIVE_ERRORS=10 # [limit] Context window of the selected model, in tokens. It sizes the vocabulary # prompt — terms that do not fit are left out, with a standing `trimmed` note — and, in # document mode, the document: one that would not fit with the prompt and the reply → # 413 limit_exceeded. Sent to Ollama as num_ctx. Rule 3: ships COMMENTED, because its # default depends on the backend — 128000 for openrouter, 32000 for ollama (each # client's CLI default) — after CONTEXT_WINDOW in llm_config.txt, when that sets one. # Set it to the window of the model you selected above. # LLM_CONTEXT_WINDOW=128000 # ── §6 Completeness ledger — algorithmic, deprecated, unreachable ──────────────── # [algorithmic] Archaeological vocabulary JSON. Falls back to VOCAB_PATH in # llm_config.txt, then data_samples/vocab/union_nested.json. # VOCAB_PATH=data_samples/vocab/union_nested.json