LLM providers¶
The engine's LLM is provider-agnostic. One environment variable selects the backend; the rest of the pipeline — multimodal analysis, text fallback, rate-limit handling — is identical across providers.
Selecting a provider¶
llm/factory.py reads LLM_PROVIDER and constructs the matching provider:
LLM_PROVIDER |
Class | Key |
|---|---|---|
gemini |
GeminiProvider |
GEMINI_API_KEY |
openai |
OpenAIProvider |
OPENAI_API_KEY |
anthropic |
AnthropicProvider |
ANTHROPIC_API_KEY |
An unknown value raises at startup, so a typo fails fast rather than silently degrading.
What a provider must do¶
All providers implement the same interface (llm/provider.py), so the corp_ann processor doesn't care which is active:
analyze_announcement(...)— multimodal: takes rendered page images + context and returns anAnnouncementAnalysis(summary, category, and whether more pages are needed).analyze_text_announcement(...)— text fallback: takes extracted PDF text and returns the same structure.
Both raise a shared exception hierarchy the pipeline handles uniformly:
| Exception | Meaning | Pipeline response |
|---|---|---|
LLMRateLimitError(retry_after) |
Provider 429 | Item re-queued; worker sleeps retry_after. |
LLMContextWindowError |
Prompt too large | Multimodal batch is shrunk and retried. |
LLMResponseFormatError |
Malformed structured output | One reformat retry. |
LLMProviderError |
Other provider failure | Multimodal → falls back to text analysis. |
Model overrides¶
Each provider has a sensible default model; override it per provider:
OpenAI-compatible & local servers¶
The OpenAI provider honours OPENAI_BASE_URL, so any OpenAI-compatible endpoint works — including a local vLLM server for offline experimentation with no API costs.
LLM_PROVIDER=openai
OPENAI_BASE_URL=http://host.docker.internal:8000/v1 # local vLLM
OPENAI_MODEL=mlx-community/Qwen3-VL-4B-Instruct-4bit
OPENAI_API_KEY=not-needed # placeholder for local servers
host.docker.internal from the engine container
The Compose engine service declares extra_hosts: ["host.docker.internal:host-gateway"], so a container can reach a server running on the host (including on Linux). Point OPENAI_BASE_URL at http://host.docker.internal:<port>/v1, not localhost — inside the container localhost is the container itself.
Local multimodal models are memory-hungry
Multimodal analysis sends several page images per request, and the processor pool runs multiple items concurrently. A small quantised vision model on a laptop can exhaust memory under that load. When experimenting locally, reduce the processor pool size (resize corp_ann to 1 in the console, or set pool_size in its registry config) so only one item is analysed at a time.
Rate-limit handling¶
Providers detect 429s per their own API shape and raise LLMRateLimitError with a retry_after when the provider supplies one. The ConsumerPool re-queues the item and backs off, and the processor keeps its inflight guard so the re-queue isn't deduplicated away. In-provider inline retries are also capped so a single item can't block a worker indefinitely.