Paraphase-4B Adapter Pack — rewrite + extraction LoRAs
One raw Qwen3.5-4B base, task LoRAs, and one measured tool guard. This is the multi-task companion to Paraphase-4B, which is the merged humanizer release.
Use the raw base here for adapters. The paraphase-4b-Q8_0.gguf in the
main repo is the merged rewrite model; it is not the adapter base. The raw
qwen3.5-4b-Q8_0.gguf in this pack is the base the 4B LoRAs were trained on.
The 2B extraction LoRA applies to raw openbmb/MiniCPM5-2B (see below).
Quick start
# extraction (4B)
llama-server -m qwen3.5-4b-Q8_0.gguf \
--lora gguf/paraphase-4b-extract-lora.gguf -c 2048
# rewrite (4B)
llama-server -m qwen3.5-4b-Q8_0.gguf \
--lora gguf/paraphase-4b-rewrite-lora.gguf -c 2048
# extraction (2B, on-device tier; official MiniCPM5-2B GGUF)
llama-server -m MiniCPM5-2B-Q8_0.gguf \
--lora gguf/paraphase-2b-extract-lora.gguf -c 2048
# tool calls: base only; apply toolguard.py to the returned calls
llama-server -m qwen3.5-4b-Q8_0.gguf -c 2048
Send "chat_template_kwargs": {"enable_thinking": false} with Qwen3.5
requests; otherwise the template starts with a thinking preamble.
What is measured
Extraction adapter (the win)
Frozen test: 50 real AESLC validation emails, disjoint from training. Labels
are deterministic regex spans; this measures agreement with that labeler, not
human-judged extraction. Labeler v2 (2026-10-23): amount spans now include
magnitude words ($12 million, 1.2 million dollars, $12k), fixing the one
flaw in v1 — same 50 emails, re-labeled; the shipped adapters are trained on
v2 labels and measured on extract_smoke.v2.
| metric | raw Q8 base | + extract LoRA |
|---|---|---|
| JSON valid | 1.00 | 1.00 |
| exact match | 0.12 | 0.40 |
| micro F1 | 0.522 | 0.674 |
| date F1 | 0.407 | 0.868 |
| amount F1 | 0.730 | 0.722 |
| action-item F1 | 0.522 | 0.658 |
| empty-row accuracy | 0.40 | 0.80 |
| non-empty recall | 0.756 | 0.889 |
The adapter raises exact match from 6/50 to 20/50 and doubles date F1. Under
v2 labels amount F1 becomes meaningful for the first time: the raw base is
already strong on amounts (0.730) and the adapter holds it (0.722) while
gaining everywhere else. Only 4/50 test emails contain amounts, so the amount
row still carries less weight than dates/actions. The v1 numbers (exact 0.42,
micro 0.670 on extract_smoke.v1) remain on record in the project docs.
2B extraction adapter (on-device tier)
Same recipe on raw openbmb/MiniCPM5-2B (150 steps, ~2 minutes of training,
peak 6.49 GiB), trained and measured on the same v2 labels — at one-third the
download (2B Q8 1.4 GB + 50 MB LoRA):
| metric | 2B base | 2B + extract LoRA | 4B + extract LoRA (reference) |
|---|---|---|---|
| JSON valid | 0.96 | 1.00 | 1.00 |
| exact match | 0.14 | 0.34 | 0.40 |
| micro F1 | 0.277 | 0.631 | 0.674 |
| date F1 | 0.313 | 0.806 | 0.868 |
| amount F1 | 0.438 | 0.766 | 0.722 |
| action F1 | 0.272 | 0.614 | 0.658 |
| non-empty recall | 0.378 | 0.867 | 0.889 |
Base GGUF: openbmb/MiniCPM5-2B-GGUF
(MiniCPM5-2B-Q8_0.gguf). MiniCPM5 has no thinking preamble, so no
enable_thinking flag is needed.
Tool path (base + guard)
Frozen 20-item tool-call smoke:
| candidate | accuracy |
|---|---|
| raw Q8 base | 0.70 |
raw base + toolguard.py |
0.75 |
| tool LoRA (withdrawn) | 0.65 |
The tool LoRA fixed one wrong-tool case but made two web-search queries more
verbose, so it is not included. The guard is a narrow deterministic fix:
when a request starts with Email/Send, contains an explicit address, and the
model chose get_contact_email, it converts the call to send_email with the
parsed to/subject/body. It changes exactly one frozen item and never touches
web search or no-tool cases.
Rewrite adapter
The rewrite LoRA is the adapter merged into Paraphase-4B. The main model card has the voice50 and Pangram evidence: 50/50 valid, fact fidelity 0.970, copy reuse 0.260 with the retry policy, tell rate 0.08.
Files
| file | size | notes |
|---|---|---|
qwen3.5-4b-Q8_0.gguf |
4.3 GB | raw Qwen3.5-4B base; Q8_0 |
gguf/paraphase-4b-extract-lora.gguf |
42.5 MB | extraction adapter, llama.cpp |
gguf/paraphase-4b-rewrite-lora.gguf |
42.5 MB | rewrite adapter, llama.cpp |
gguf/paraphase-2b-extract-lora.gguf |
50.3 MB | 2B extraction adapter (base: openbmb/MiniCPM5-2B) |
peft/rewrite/ |
85 MB | PEFT adapter for transformers |
peft/extract/ |
85 MB | PEFT adapter for transformers |
peft/extract-2b/ |
101 MB | PEFT 2B extraction adapter |
toolguard.py |
3 KB | narrow send/contact guard |
task-adapter-manifest.json |
2 KB | hashes, sizes, measured endpoints |
LICENSE |
Apache-2.0 | inherited from Qwen3.5-4B |
Extraction prompt
System:
You extract structured data from email. Return only valid JSON.
User:
Extract the dates and times, money amounts, and action items from the email.
Return only a JSON object with keys "dates", "amounts", and "action_items".
Dates and amounts must be strings copied exactly from the email.
Each action item is the sentence that states a request or task.
Use [] when a field has none.
EMAIL:
<subject and body>
The adapter returns JSON such as:
{"dates": ["Thursday", "3pm", "Friday"], "amounts": ["1,240 dollars"], "action_items": ["Please send the signed copy by Friday."]}
Tool guard
from toolguard import guard_tool_call
calls, changed = guard_tool_call(user_text, model_tool_calls)
The guard only handles the explicit-address send/contact confusion. It is not a general tool router and it does not repair arbitrary argument errors.
PEFT / transformers
from transformers import AutoModelForCausalLM, AutoTokenizer
from peft import PeftModel
base = "Qwen/Qwen3.5-4B"
tok = AutoTokenizer.from_pretrained(base)
model = AutoModelForCausalLM.from_pretrained(base, device_map="auto")
model = PeftModel.from_pretrained(model, "peft/extract")
Load the base in bf16 or 4-bit; the adapter is r=16, alpha=32.
Reproduce
# raw base
python convert_hf_to_gguf.py <Qwen3.5-4B snapshot> \
--outfile qwen3.5-4b-Q8_0.gguf --outtype q8_0
# extraction: 427 train / 48 val AESLC, 150 steps
PYTHONPATH=. python scripts/train_task_adapter.py --task extract \
--data data/processed/extract_v1/train.jsonl \
--out data/checkpoints/extract-adapter --steps 150
# extraction eval
PYTHONPATH=. python scripts/eval_extract.py \
--gguf qwen3.5-4b-Q8_0.gguf \
--lora gguf/paraphase-4b-extract-lora.gguf --tag extract-adapter
# tool guard (stored base outputs, no model call)
PYTHONPATH=. python scripts/eval_tool_guard.py
The full route and data provenance are summarized in
task-adapter-manifest.json and the main
Paraphase-4B card.
Limitations
- Extraction labels are regex-derived, not human annotations. Amount F1 is low-confidence; dates and action items carry the result.
- Action-item labels are heuristic sentences; the adapter can beat or lose to the labeler for reasons unrelated to extraction quality.
- The tool guard fixes one demonstrated case; the strict tool instrument is mostly a query-phrasing test.
- One LoRA per llama-server process; adapter hot-swapping is not implemented.
- No detector was used for these adapters. Pangram evidence belongs to the rewrite model, not the extraction task.
License
Apache-2.0, inherited from Qwen3.5-4B. The base model is Qwen/Qwen3.5-4B.