Optional LLM: setup and verbs¶
Everything here is off by default. Paperful’s PDF loop (run / attach /
lint / fix-metadata / dedupe) never needs a model. Turn the LLM on only
for the LLM-backed verbs below, and only after paperful doctor shows the
LLM row green. extract-sections (optional GROBID then optional LLM),
rag questions, critique, and style-only rewrite run
deterministic rules without a model; their --llm lanes (and
critique --lane / --advanced, rewrite areas other than style) need [llm].enabled.
Feasibility first. Trying one summary is not the same job as summarizing or synthesizing ~1.5k PDFs. Hardware bands, overnight local runs, OpenRouter / LiteLLM setup, free-tier caveats, and order-of-magnitude hosted cost: AI for notes and Ask.
Two ways to run a model: use Ollama on your computer when your RAM/VRAM
can support the tags you need (default provider = "ollama"). Or install
paperful[llm] and set provider = "litellm" to call a hosted API
(OpenAI, Anthropic, or another LiteLLM provider) without running weights
locally. RAG embeddings follow the same split ([rag].embed_provider).
Design rules (see architecture § LLM layer):
Local first. Default provider is Ollama on
127.0.0.1:11434. Nothing leaves the machine unless you opt into a remote endpoint, and then every command prints a yellow egress notice.Disk first, apply second. Each verb writes under
state/before any Zotero write; Zotero writes go through the same library adapter asattach.Grounded. Prompts only see item metadata plus text extracted from a PDF already on disk (
out/orstate/pdf-cache/). Nothing is fetched from the web for a model call.No silent defaults. API keys live in the environment, never in
config.toml;browser_agentis never in the default source list.runappends it only when[llm].enabledand the extra is installed.
1. Install¶
Ollama (default, local)¶
# macOS
brew install ollama && ollama serve # or the Ollama.app menu-bar daemon
# Linux
curl -fsSL https://ollama.com/install.sh | sh
ollama pull qwen2.5:7b # title / identity / summaries
ollama pull qwen2.5:14b # recommended floor for `recover` (browsing agent)
# See browser-agent-models.md for VRAM bands, ranked tags, and avoid list.
No Python extra is needed for Ollama: Paperful talks HTTP to the daemon.
Verb |
Works well with |
Notes |
|---|---|---|
|
7B–12B instruct models ( |
JSON-capable instruct tags; thinking models are fine but slower. |
|
14B+ ( |
Small models loop on cookie banners and publisher menus; |
LiteLLM (paid / OpenAI-compatible APIs)¶
uv sync --extra llm # installs litellm
export OPENAI_API_KEY=... # or ANTHROPIC_API_KEY, OPENROUTER_API_KEY, etc.
[llm]
enabled = true
provider = "litellm"
model = "openai/gpt-4.1-mini" # LiteLLM model id (provider/model)
# api_base = "https://openrouter.ai/api/v1" # e.g. OpenRouter; optional otherwise
Rules: ollama/… model ids are rejected on the LiteLLM path (use
provider = "ollama" instead); api_base must be http(s). With LiteLLM,
title/abstract/PDF excerpts (and, for recover, page text) leave your machine.
OpenRouter walkthrough, free-tier limits, and cost sketch:
AI for notes and Ask.
Browser agent (recover / last run lane)¶
uv sync --extra browser-agent # browser-use + ollama client; needs Python >= 3.11
uv run paperful session login scholar # the agent reuses this Chromium profile
# or: PAPERFUL_IMAGE_MODE=heavy docker compose build
Paperful core supports Python 3.10; the extra is marked
python_version >= "3.11" so uv sync on 3.10 simply skips it and recover
exits 1 with a hint. If your .python-version is 3.10, run
uv python pin 3.12 && uv sync --extra browser-agent. The heavy Compose
image already includes this extra (headed login still on the host).
2. Configure¶
[llm]
enabled = true
provider = "ollama" # ollama | litellm
model = "qwen2.5:7b"
base_url = "http://127.0.0.1:11434"
allow_remote = false # true only for a non-loopback Ollama host
timeout_s = 120
# max_num_ctx = 32768 # cap for the Ollama context window on summarize / synthesize
[fix_metadata]
llm_title = false # verb B: grounded title proposals
[lint]
llm_pdf_match = false # verb C: pdf_identity_mismatch finding
llm_pdf_match_min_confidence = 0.6
[critique] # verb C2: suggested comments only
llm = false
# lanes = [] # reviewers, marg, adversary, outline, claims, verify, context, context-web (see critique.md)
# advanced = false # shim → marg when lanes is empty
# vale = false # house-style pass; needs vale on PATH (not an LLM)
# glossary = true # fuzzy house expansions from state/glossary/
# [[critique.reviewers]] # omit the table for one builtin JSON reviewer
# id = "line-editor"
# prompt = "prompts/critique-line-editor.md"
[rewrite] # verb C2.5: apply a critique pack
areas = ["style"] # style-only glossary/Vale swaps need no model
# outputs = ["edits", "tracked"] # also odt, pdf
# comments = "off"
# [[rewrite.specialists]]
# id = "style"
# model = "" # empty → [llm].model; may be an XtraGPT tag
# prompt = "default" # or prompts/rewrite-style.md
[summarize] # verb D
prompt_template = "default" # or a path, e.g. "prompts/summary.md"
max_context_chars = 24000
tag = "paperful-summary"
# dest = "both" # disk | zotero | both
# order = "library" # library | newest | oldest
# style = "plain" # plain | ste (ASD-STE100 constraints)
# ste_violation_policy = "warn" # warn | fail (when style = ste)
# ste_word_list_path = "" # optional approved-word list (one word/line)
[synthesize] # verb E
prompt_template = "default"
max_context_chars = 24000
tag = "paperful-report"
# dest = "both" # disk | zotero | both
# timeout_s = 300 # default is max([llm].timeout_s, 300)
[browser_agent] # last run lane + recover --item
max_steps = 20
max_wall_s = 300
# during_run = true # false keeps recover as --item only
# # also: --no-browser-agent on one run
# model = "qwen2.5:14b" # override [llm].model for browsing only
# fallback_model = "qwen3:30b" # one retry before final not_found (not on captcha)
# use_vision = false # true needs a VL tag; see browser-agent-models.md
# use_thinking = true # false for tags that reject thinking=True
Full key table: Configuration § LLM.
Ollama tag ranking and acceptance tests: browser-agent-models.md.
fallback_model, use_vision, and recover --from-last-run are documented in config.md. Remaining gap: num_ctx pass-through — ROADMAP § browser-use.
3. Check¶
uv run paperful doctor
Row |
Green |
Amber |
|---|---|---|
|
daemon reachable and model tag present (Ollama), or |
disabled is also green; amber = unreachable, model not pulled, extra missing, non-loopback URL without |
|
|
extra missing ( |
doctor never sends a paid completion: Ollama is probed via /api/tags,
LiteLLM by import only. The TTY guide prints the fix for each amber row.
4. Verbs¶
A. recover — browser-agent PDF recovery¶
Model choice (Ollama tags, VRAM bands, acceptance test): browser-agent-models.md.
On run, when [llm].enabled and paperful[browser-agent] are available,
Paperful appends browser_agent as a late serial lane. Policy order tries
Scholar once immediately before the agent (the agent wall clock spaces Scholar
requests). When opt-in Camofox is also on, order is Scholar →
camofox → browser_agent (separate phases; a Scholar or Camofox hit skips
later lanes). The agent
fires only if one of those vault lanes was tried and failed (not merely
skipped as inapplicable). Playwright releases the session profile first.
[browser_agent].during_run = false turns that auto-lane off for every run.
--no-browser-agent skips it for one run (or all); --browser-agent
forces it on even when during_run is false. Sci-Hub, when
opted in, stays after recover.
paperful recover --item still targets named keys without waiting for other
lanes:
uv run paperful recover --item ABCD1234 --dry-run # shows the start URL only
uv run paperful recover --item ABCD1234 # runs the agent, attaches on success
uv run paperful recover --item K1 --item K2 --no-attach
uv run paperful recover --from-last-run # default mode: browser_agent_miss
uv run paperful recover --from-last-run --from-last-run-mode missing -n 5
Start URL is
https://doi.org/<DOI>when the item has a DOI, else its URL.One item at a time, on the session vault profile. Never in
DEFAULT_SOURCES.Hard CAPTCHAs are not solved. Local Chrome does not solve publisher Cloudflare or Turnstile; that solver is a Browser Use Cloud feature, and this install does not use it. When the page is a robot check, a Cloudflare verification, or a publisher bot wall, Paperful stops that attempt, marks it
captcha, and skipsbrowser_agentfor the rest of the process. Later items stayretryable. A paywall stops that item only. Sci-Hub’s ALTCHA check is separate and still completed when it can be. Timeouts / no download →not_found.Mileage varies for both
browser_agentand opt-in Camofox: bulk recover often sees long strings of timeouts, Cloudflare / soft bot walls, DOI pages with no PDF control, and empty downloads. Expect frequent misses; keep these lanes off when you want a fast, predictable pass over HTTP / vault sources only. Model and acceptance detail: browser-agent-models.md.Access blocks (403 / “Request blocked” / paywall with no free PDF) are instructed as immediate stop — the agent must not open search engines or support/help pages. If it navigates to Google/Bing/etc. or a support/contact path anyway, Paperful force-stops that attempt.
As soon as a valid PDF lands in the recover download folder (size stable across two polls), Paperful calls
agent.stop()so the step budget does not keep running after the click already succeeded.Success lands like any other source: PDF under
out/, manifest line withsource = "browser_agent", attach through the normal path. A dedicatedrecoverwritesstate/runs/<stamp>-recover.json; auto-recover onrunis counted in that run’s report (command = "run").Prints an orange disclaimer: you are responsible for publisher terms; remote LLMs may receive page content; a local LLM does not send page content off your computer.
Do not run two commands concurrently against the same vault: both would open the same Chromium profile.
B. Grounded title proposals (fix-metadata)¶
With [fix_metadata].llm_title = true, items flagged title_all_caps,
title_html, or title_filename get a proposed clean title grounded in the
abstract and first two PDF pages. ALL CAPS titles already get a deterministic
Title Case patch without the LLM; the model can still override when enabled.
Proposals that share no content words with the grounding are dropped. They land
in state/metadata-patches.jsonl with source = "llm_title" and are written
only on fix-metadata --apply.
C. PDF identity check (lint)¶
With [lint].llm_pdf_match = true, lint asks the model whether the first
two pages match the record (title, author surnames, year, DOI). A false, or a
true below llm_pdf_match_min_confidence, becomes the finding
pdf_identity_mismatch (detail carries the model’s reason and confidence).
Nothing is deleted or re-attached; you decide. Model or extraction failure is
silent (no finding).
C2. critique --lane — suggested review lanes only¶
uv run paperful critique --file ./draft.pdf --lane reviewers
uv run paperful critique --file ./draft.pdf --llm --reviewer line-editor
uv run paperful critique --file ./draft.pdf --llm --reviewer openreview
uv run paperful critique --file ./draft.pdf --lane marg
uv run paperful critique --file ./draft.pdf --lane adversary
uv run paperful critique --file ./draft.pdf --lane outline
uv run paperful critique --file ./draft.pdf --lane claims
uv run paperful critique --file ./draft.pdf --lane verify
uv run paperful critique --file ./draft.pdf --lane reviewers --lane marg --grobid
uv run paperful critique --file ./draft.pdf --lane context
uv run paperful critique --item ABCD1234 --llm --force
Additive. The default pack is checkers, metrics, and heuristic AI-tells with
no model. --lane reviewers (or --llm / [critique].llm) asks for a
contribution summary, up to eight comments tied to a finding code or a quoted
span, and author questions. --lane marg (or --advanced) adds a MARG-S-style
multi-agent pass. --lane adversary adds a structured validity red-team.
--lane outline reviews the extract as a plan (section skeleton plus claim/warrant/scope pressure).
--lane claims builds an intra-doc claim ledger (support in this extract only).
--lane verify is a local section-swarm stress-test (not Deep Review; local
models will not match hosted proof recall). --lane context adds a
ScholarPeer-inspired local historian /
scout / Q&A pass; --lane context-web may query OpenAlex / SearXNG (egress
notice; skipped offline). Lanes run together. --grobid /
[grobid].enabled changes MARG-S / claims / verify PDF chunks. Every row is
lane=suggested. Ungrounded spans are dropped. No overall score. Identity
mismatch refuses review lanes unless --force. Optional
[[critique.reviewers]] run one call each (per-id model and prompt) and do
not replace marg, adversary, outline, claims, verify, or context. A Markdown prompt file becomes a Reviewer
note** in pack.md; the builtin prompt still returns JSON comments. Optional
--vale house-style lint is a separate checked pass (no model). See
critique.md.
C2.5. rewrite — apply a critique pack¶
uv run paperful rewrite --file ./draft.docx
uv run paperful rewrite --pack state/critiques/file-draft/20261027T... --area cites --comments substantive
uv run paperful rewrite --pack PATH --output edits --dry-run
Pack required. Never re-runs critique. Default area is style (glossary
suggested and obvious Vale swaps are deterministic and run with [llm]
off). Other areas (cites, structure, comments) need a model for
findings without a deterministic replacement. Body edits must not invent
numbers or cite keys. Retracted / withdrawn / EoC cites become comments,
never deletes. Optional [[rewrite.specialists]] can point a specialist at
an XtraGPT Ollama tag; ICLR/ReviseQA criteria are ML-shaped. Optional
[rewrite].openxml_audit (uv sync --extra openxml) validates
tracked.docx / .pptx / .odt with a Microsoft Open XML SDK port
(ODF adapter for .odt). --output odt / --output pdf write those
formats from any source. See
rewrite.md.
D. summarize — grounded summary note¶
uv run paperful summarize --item ABCD1234 # disk HTML + tagged child note
uv run paperful summarize -C BBNJ --to disk # HTML only; Zotero tree stays clean
uv run paperful summarize -C BBNJ --year-from 2023 -T journalArticle --limit 5
uv run paperful summarize -C BBNJ --order newest # recent publications first
uv run paperful summarize --item ABCD1234 --prompt prompts/one-liner.md
Full PDF text is extracted (
pdftotext, thenpypdf) and reduced tomax_context_charsas head + detected headings + tail.The builtin prompt asks for Objective / Methods / Key findings / Limitations in simple HTML. Local models often answer in Markdown anyway; Paperful converts headings, bullets, bold, and code fences so the Zotero note renders cleanly.
Every summary ends with a provenance footer:
paperful · <model> · <UTC date> · prompt <sha8>(prefixedremote LLMwhen the provider is not loopback).--to disk|zotero|both(defaultboth, or[summarize].dest) chooses the write.diskisstate/summaries/<key>.html.zoterois one child note tagged[summarize].tag. Without--force, items that already have the requested outputs for the current model (and prompt sha, when local) are skipped — safe to resume after an interrupt. A different model is a different audit trail and is rewritten. A partial pair (disk HTML without the note, or the reverse) is finished from a matching other side without calling the model again.--forceregenerates even when the footer matches.--to diskleaves the Zotero tree clean.--applystill means “this run must write the note” and exits 1 together with--to disk.--order newest|oldest|library(default[summarize].order, orlibrary) sets queue order before--limit. Usenewestwhen the model is slow and recent papers matter first. Undated items stay at the end undernewest/oldest. Item type stays a filter (-T), not an order key.Ollama receives
num_ctxsized from the prompt (about 3 characters per token, plus reply headroom, rounded up, capped by[llm].max_num_ctx). The model tag has to actually support that window. If Ollama logs a truncation warning, lowermax_context_charsor raisemax_num_ctx.If
[lint].llm_pdf_matchis on and the item is flagged,summarizerefuses it; pass--forceto override.[summarize].style = steappends ASD-STE100 Simplified Technical English constraints to the prompt (≤25 words/sentence, ≤6 sentences/paragraph, prefer active voice, approved vocabulary with domain terms allowed). The prompt SHA changes, so existing plain summaries are regenerated. After the model replies, a heuristic check runs:ste_violation_policy = warn(default) keeps the summary and warns;failmarks the item failed. Optionalste_word_list_pathenables vocabulary checks against an approved list.
E. synthesize — summary of summaries¶
uv run paperful synthesize -C BBNJ --year-from 2021 --year-to 2026 -T journalArticle
uv run paperful synthesize -C BBNJ --dry-run # counts and chunk plan, no model call
uv run paperful synthesize -C BBNJ --to disk # state/reports/ only
uv run paperful synthesize --library --to zotero --report-collection BBNJ
uv run paperful synthesize -C BBNJ --force # ignore the up-to-date sidecar
Reads summary notes already produced by
summarize. A file understate/summaries/<key>.htmlwins; otherwise the tagged child note is read. PDFs are not opened again. Items with neither are listed under Not included and are not sent to the model.Notes are packed under
[synthesize].max_context_chars. One chunk is one completion. Several chunks are synthesised in batches, then combined. If that still overflows, Paperful reduces again, at most three times, then exits 1 and asks you to narrow the scope or raise the budget.When there is more than one first-level chunk, Paperful completes the first three (or fewer), forecasts whether three reduce passes can fit the budget, and may run one extra reduce on that sample. If the forecast still leaves more than one chunk, it exits 1 before the rest of the chunks run.
Bundled
--layout(or[synthesize].layout) chooses the HTML shape, citing only[Surname Year]:default— Corpus / Themes / Points of agreement / Disagreements and tensions / Gaps and open questions / Suggested reading orderconflict— camps, methods behind each camp, what agrees / disagrees / is undecided (no smoothed narrative)review— Abstract / Research question / Methods in this set / Findings / Limits / Conclusions--prompt FILEoverrides the bundled layout; its SHA is stamped in the footer. Distinct from Ask--focus conflict(RAG excerpts, not summaries).
After the model text, Paperful appends a Sources list (key, DOI, and the source note’s model and date), the Not included list, and any
[Surname Year]token that matches no source.Disk output is
state/reports/<slug>.htmlplus a<slug>.jsonsidecar (paperful.synthesis.v1) of source hashes. A later run with the same hashes, layout, prompt SHA, and destination skips the model;--forceregenerates. Non-default layouts append the layout id to the slug.Zotero output is one standalone note in each collection you named with
-C(or in--report-collection), taggedpaperful-reportandpaperful-report:<slug>. Re-runs update that note.--libraryor--itemwith no collection cannot file a note: pass--report-collectionor--to disk.A run report lands at
state/runs/<stamp>-synthesize.jsonand does not replacestate/last-run.json.
Custom prompt file example (prompts/one-liner.md):
Write one paragraph (max 120 words) stating the research question, the
approach, and the single most important finding. Plain HTML <p> only.
Use only the metadata and document text provided.
The file’s SHA-256 is stamped in the footer so you can tell which prompt produced which note.
F. extract-sections — structured IMRaD JSON (optional LLM)¶
uv run paperful extract-sections -C BBNJ
uv run paperful extract-sections -C BBNJ --llm # fill missing MVP fields only
uv run paperful extract-sections --library --dry-run
Rules always run without
[llm].enabled. Heading split uses the shared synonym list (see[extract_sections]andstate/section-headings.toml).--llm(or[extract_sections].llm) asks the model for missingabstract,methodology,conclusion, orresearch_questionsonly. Outputs must ground in the excerpt or they are dropped.Writes
state/sections/<key>.json(paperful.sections.v1). Reusesstate/rag/text/when present; otherwise parses the mirror PDF. No Zotero writes. Distinct fromsummarize(HTML prose) andrag questions(RQ queue forrag answered). See rag.md.
5. Docker¶
light images omit litellm and browser-use. heavy
(PAPERFUL_IMAGE_MODE=heavy) installs both, so the browser_agent lane can
run in the container (headed session login still needs the host vault).
B/C/D work inside the container against a host Ollama.
Keep loopback in config so the same config.toml works for host
uv run and Compose:
[llm]
enabled = true
base_url = "http://127.0.0.1:11434" # rewritten inside the container
Inside Docker, Paperful rewrites a loopback [llm].base_url to
host.docker.internal (Compose sets PAPERFUL_OLLAMA_HOST and extra_hosts,
same pattern as Zotero). A remote hostname stays as configured and still needs
allow_remote = true. The Docker bridge is treated as local — you do not
need allow_remote = true for it.
Docker Desktop (macOS/Windows): leave Ollama on its default loopback bind
(127.0.0.1:11434). Compose reaches it via host.docker.internal the same way
it reaches Zotero. Do not force OLLAMA_HOST=0.0.0.0 on Desktop — that can
break the gateway path.
Linux Docker Engine: bind the host daemon so host-gateway can connect:
OLLAMA_HOST=0.0.0.0 ollama serve
Optional overrides: set PAPERFUL_OLLAMA_HOST in .env, or pin an explicit
URL with allow_remote = true:
base_url = "http://host.docker.internal:11434"
allow_remote = true
docker compose run --rm paperful doctor shows the LLM row. See
Docker.
6. Troubleshooting¶
Symptom |
Cause / fix |
|---|---|
|
Set |
|
Config still pointing at container loopback without this Paperful rewrite, or Ollama not running on the host. Keep |
|
|
|
Set |
|
|
|
Switch provider or model id |
|
|
|
|
|
|
|
Model too small or wrong class for UI tools; try 14B+ via |
|
Extra missing, |
|
Use a VL tag ( |
|
Set a different tag than |
|
Last report had no |
Multimodal / 400 from Ollama on recover |
|
Summary note shows raw |
Update Paperful (0.5+ converts Markdown); re-run |
|
Scanned PDF. Run |
Report seems to ignore half the notes |
Ollama truncated the prompt. Lower |
Not included list is long |
Those items have no summary yet. Run |