-
Notifications
You must be signed in to change notification settings - Fork 0
Local Model Cutover
Mailroom is designed for provider-agnostic LLM usage. OpenRouter is the primary provider today, but switching to local models (Ollama, vLLM) is a configuration change — no code rewrite required.
The LLM layer is abstracted in two files:
llm/
├── client.py # get_llm(agent_name) → OpenAI client
└── providers.py # Provider configs: base URLs, models, auth
And one config file:
config/
└── taxonomy.yaml # agents: section — per-agent provider + model
Provider selection flow:
taxonomy.yaml (agent config)
→ llm/client.py (resolve agent name)
→ llm/providers.py (resolve provider config)
→ openai.OpenAI(base_url=..., api_key=...)
Qwen 3 7B (qwen3:7b) is the recommended primary local model for Mailroom:
- Strong structured JSON output (critical for extraction schemas)
- Good legal text understanding
- Available 14B variant for higher accuracy
- Part of the same Qwen family as OpenRouter's
qwen/qwen-3-7b
| Model Family | Available Sizes | Strengths | Weaknesses |
|---|---|---|---|
| Qwen 3 | 7b, 14b | Structured output, legal text | Medium resource usage |
| Qwen 2.5 | 14b, 32b | Multilingual, strong extraction | Larger size |
| Llama 3.1 | 8b, 70b | Reliable all-around | Weaker structured output |
| Llama 3.2 | 3b | Very fast, lightweight | Limited complex extraction |
| Mistral | 7b | Fast, good instructions | Less legal domain knowledge |
| Mistral Nemo | 12b | Good speed/quality balance | — |
| Mixtral | 8x7b | MoE — strong extraction | Higher memory usage |
| DeepSeek-R1 | 8b, 14b | Legal reasoning, analysis | Slower inference |
| Phi-4 | 14b | Document understanding | — |
| Gemma 2 | 9b, 27b | Instruction following | — |
| Command R | 35b, 104b | RAG, extraction | Very high resource usage |
Set a single environment variable to switch ALL agents to local:
export DEFAULT_PROVIDER=ollamaAll agents will now use Ollama with whatever model is specified in config/taxonomy.yaml. If you haven't changed the per-agent models, they'll default to qwen3:7b (the Ollama default).
Move agents one at a time, validating each before moving the next. This minimizes risk.
| Order | Agent | Risk | Rationale |
|---|---|---|---|
| 1 | Sorter | Low | Classification is the least accuracy-sensitive |
| 2 | Compliance Specialist | Low | Structured forms, predictable formats |
| 3 | Correspondence Specialist | Medium | Narrative text, moderate complexity |
| 4 | Corporate Records Specialist | Medium | Hierarchical data, moderate complexity |
| 5 | Contracts Specialist | Medium-High | Complex extraction, legal precision |
| 6 | Due Diligence Specialist | High | Risk detection nuance |
| 7 | Reporter | Medium | Summarization — lower stakes |
| 8 | Boss | Medium | Adjudication — lower frequency |
# 1. See current assignments
PYTHONPATH=src python src/scripts/cutover.py --list
# 2. Move one agent to local
PYTHONPATH=src python src/scripts/cutover.py --agent sorter --provider ollama --model qwen3:7b
# 3. Validate with tests
PYTHONPATH=src python src/scripts/cutover.py --validate --agent sorter
# 4. If validation passes, move to the next agent
PYTHONPATH=src python src/scripts/cutover.py --agent compliance_specialist --provider ollama --model qwen3:7b
PYTHONPATH=src python src/scripts/cutover.py --validate --agent compliance_specialist
# 5. If validation fails, roll back
PYTHONPATH=src python src/scripts/cutover.py --agent sorter --provider openrouter --model openai/gpt-4oEdit config/taxonomy.yaml:
agents:
sorter:
provider: ollama # ← changed from openrouter
model: qwen3:7b # ← changed from openai/gpt-4o
temperature: 0.1After all agents are cut over:
# Run the full test suite
pytest -v
# Compare extraction accuracy with golden fixtures
# This requires OpenRouter to still be available for comparison:
python -c "
# Run each fixture through both providers and compare extraction outputs
"| Capability | OpenRouter (GPT-4o) | Ollama (Qwen 3 7B) | Ollama (Llama 3.1 8B) |
|---|---|---|---|
| Structured JSON output | Excellent | Very Good | Good |
| Legal terminology | Excellent | Good | Fair |
| Instruction following | Excellent | Good | Very Good |
| Inference speed | Depends on provider | Fast (local GPU) | Fast (local GPU) |
| Cost per document | ~$0.01-0.05 | $0 (local) | $0 (local) |
| Data privacy | Documents leave infra | Documents stay local | Documents stay local |
| Availability | Requires internet | Fully offline | Fully offline |
You can run a mix of providers simultaneously. For example:
agents:
sorter:
provider: ollama # Fast local classification
model: qwen3:7b
contracts_specialist:
provider: openrouter # Cloud for complex contracts
model: openai/gpt-4o
compliance_specialist:
provider: ollama # Local for structured filings
model: qwen3:7b
due_diligence_specialist:
provider: openrouter # Cloud for risk detection
model: openai/gpt-4oThis gives you cost savings on simpler tasks while retaining accuracy on complex ones.
| Model Size | Min RAM | Recommended RAM | Min VRAM (GPU) |
|---|---|---|---|
| 7B/8B | 8 GB | 16 GB | 6 GB |
| 12B-14B | 16 GB | 32 GB | 10 GB |
| 32B-35B | 32 GB | 64 GB | 24 GB |
| 70B+ | 64 GB | 128 GB | 48 GB |
For pilot scale (dozens of documents/day), a machine with 16GB RAM and a GPU with 8GB+ VRAM running qwen3:7b is sufficient.
# List available models
docker exec mailroom-ollama ollama list
# Pull a model
docker exec mailroom-ollama ollama pull qwen3:7bIf the pipeline logs APIConnectionError or ConnectError:
- Verify the service is running:
# Ollama (Docker) docker compose -f src/config/docker/docker-compose.yml --profile local-llm ps curl http://localhost:11434/v1/models # vLLM curl http://localhost:8000/v1/models
- Confirm
OLLAMA_BASE_URL/VLLM_BASE_URLmatches the service (defaults:http://localhost:11434/v1,http://localhost:8000/v1). Note the/v1suffix is required — the OpenAI SDK appends/chat/completions, so omitting it produces a 404/connection error. - If running Ollama on the host (not Docker), make sure it exposes the OpenAI-compatible endpoint:
OLLAMA_HOST=0.0.0.0 ollama serve. - If agents still resolve to OpenRouter, check
DEFAULT_PROVIDERisn't overriding:PYTHONPATH=src python src/scripts/cutover.py --listshows the effective provider per agent.
The OpenAI SDK needs the OpenAI-compatible base URL. For Ollama that is http://<host>:11434/v1 (the raw :11434 root is not OpenAI-compatible). For vLLM it is http://<host>:8000/v1. Double-check there is no trailing slash and no extra path.
Some local models struggle with strict JSON schema mode. If you see _parse_error: true in extraction results:
- Try a larger model (14B instead of 7B)
- Try Llama 3.1 or DeepSeek-R1 for better instruction following
- Fall back to OpenRouter for that specific agent
agents/base.py:_call_structured deliberately embeds the literal token json in both the system and user messages (some providers gate response_format: json_object on that word). If a local provider still rejects the request:
- Check whether the provider supports
response_formatat all — some local serving stacks only accept it for specific models. - If your local model doesn't support
json_object, prefer a model that does (Qwen family), or route the offending agent back to OpenRouter. - vLLM: use an engine version that supports
guided_json/structured output and confirm the model is served with a compatible chat template.
Page images are only attached when the agent's model matches a vision.models substring in taxonomy.yaml. If your local model accepts images but pages never appear:
- Add the model substring to
vision.models(e.g."qwen","llava"). - Confirm
MAILROOM_VISION_ENABLEDisn't forcing vision off. - Confirm
pymupdf(fitz) is installed — it's required for PDF→image rendering (llm/vision.py). Without it,_render_doc_pagesis skipped regardless of config.
- Use quantized models (
qwen3:7b-q4_K_Mfor GGUF quants) - Enable GPU passthrough in Docker Compose
- Reduce context window (agents truncate to 12K-25K chars already)
- Check the model actually runs on GPU:
docker exec mailroom-ollama ollama ps(a CPU-only model will be listed without a GPU line)
- Drop to a smaller quant (e.g.
qwen3:7b-q4_K_Minstead ofqwen3:7bfp16) - Reduce
num_ctx/num_gpuin the Ollama model config (ollama run --keepaliveor Modelfile) - For vLLM, lower
--max-model-lenand--gpu-memory-utilizationto free VRAM
PYTHONPATH=src python src/scripts/cutover.py --validate --agent <name> runs the unit tests against the new provider/model. If it fails:
- Check the agent's
providerandmodelvalues resolved correctly:PYTHONPATH=src python src/scripts/cutover.py --list - Confirm the model is pulled:
docker exec mailroom-ollama ollama list - The tests never hit the real LLM — they validate the config plumbing, not the model's accuracy. For accuracy, run a pilot:
PYTHONPATH=src python src/scripts/run_pilot.py --real --source <corpus>
Smaller local models are often over-confident or under-confident. If everything lands in review:
- Verify the agent model actually serves the taxonomy classes (a model not fine-tuned for legal text may classify poorly).
- Compare against OpenRouter with
scripts/run_vision_sweep.py --realor a pilot diff:PYTHONPATH=src python src/scripts/run_pilot.py --real --baseline data/pilot_report_baseline.json. - Adjust
confidence.high/confidence.lowintaxonomy.yaml— thresholds are config, not code.
Mailroom — Multi-Agent Legal Document Processing Pipeline. Built with LangGraph and OpenRouter; SQLite by default, Postgres optional.
- Repo docs/ — canonical docs (architecture, agents, configuration, API, deployment, local models)
- Sister Repositories — the llm-mailroom umbrella map