What it does
Lendmind-Intake-4b turns a UK mortgage adviser's message about a client into an IntakeFrame v4: one JSON object
with the message's intents, the lenders it names, the client's stated figures and choices, each with the adviser's
own words as its quote, and (as a second task) up to three plain questions for what a lender's affordability
calculator still needs. A program then finds every quote in the message and reads the value from it. The model is
trained to read figures, not to calculate or infer them, and to serve no verdict on a message that only asks about
the market or follows up an earlier turn; Known limitations lists where it still does.
Two tasks, one adapter, two system prompts: EXTRACT (message → frame) and ASK (frame → the questions).
Intents, as the frame defines them: A, which lenders would lend this case (or whether one named lender would);
B, what one named lender's criteria say on a point of policy; C, which products fit; MARKET, a criteria
question across lenders; UNKNOWN, none of these, or a follow-up to an earlier turn.
Intended use and scope
Intended use: structured extraction from UK mortgage adviser notes, behind a constrained reader like the one
described below, as one input to a system in which people make the decisions.
Not financial advice. Not for consumer use. Not for making lending decisions. The model reads what an adviser
wrote; it does not assess affordability, eligibility or suitability, and its output is not a recommendation. Do not
put it in front of consumers, and do not use its output to decide whether anyone is lent money.
Out of scope: any lending, affordability or eligibility decision; consumer-facing use; messages that are not
adviser notes about a case; languages other than English and markets other than the UK; use without a constrained
reader (a plain chat completion from this adapter carries none of the numbers below).
How it is run
The numbers on this card come from the adapter under Lendmind's constrained reader, not from the adapter alone:
- greedy decoding under the IntakeFrame v4 JSON schema, thinking off;
- every quote constrained to a stretch of the message, standing as whole words, so a value the message does not
contain cannot be served;
- the override drop: any value the decoder had to force against the model's own top choice is withheld before
anything is read, scored or served;
- the ASK task decoded under a per-message schema: one question per input to ask for, in order, with no figures.
The decoding constraint is open source as quotebound
(Apache-2.0): JSON-Schema-constrained greedy decoding for Qwen3.5 on Apple silicon, quotes bound to the user's
message (--quote-source user), a record of every token the constraint forced (from which a caller can withhold
those values), exact speculative decoding, and the constraint as a small C library for other decoders. quotebound
loads adapters in its own layout, not this PEFT file. It does not contain the reader's prompts, the IntakeFrame v4
schema, the override drop, the rules that decide what to ask, or the scorer, and those are not part of this
upload either. The numbers here were measured with Lendmind's internal build of the decoder that quotebound was
released from, not with the public release.
Reference hardware for the numbers: Apple M4 Pro, 48 GB, MLX 0.32.3, bf16 weights, the adapter applied unmerged,
plain greedy decoding. Exact speculative drafts are byte-identical to plain greedy on the 470 dev rows (measured with
rc1's adapter) and cut EXTRACT from 7.10 s to 2.31 s and ASK from 2.40 s to 0.97 s a row on that Mac.
Loading the adapter
import torchfrom peft import PeftModelfrom transformers import AutoModelForImageTextToText, AutoTokenizer BASE, BASE_REV = "Qwen/Qwen3.5-4B", "851bf6e806efd8d0a36b00ddf55e13ccb7b8cd0a"tokenizer = AutoTokenizer.from_pretrained(BASE, revision=BASE_REV)base = AutoModelForImageTextToText.from_pretrained(BASE, revision=BASE_REV, dtype=torch.bfloat16)model = PeftModel.from_pretrained(base, "lendmindltd/lendmind-intake-4b", revision="v1.0-rc2")
Load the base with AutoModelForImageTextToText (the checkpoint's Qwen3_5ForConditionalGeneration):
target_modules is a pattern over that class's model.language_model.layers.* projections, and it matches no module
of the text-only class. Nothing in the vision tower is adapted. Prompts are rendered with the base's chat template,
generation prompt on and enable_thinking=False.
Parity of this PEFT file with the trained adapter
adapter_model.safetensors is a rename of the trained adapter's 400 float32 tensors to PEFT's names, with no
transpose and no cast: every tensor is byte-identical to the file the trainer wrote. Loaded onto the base with
PeftModel.from_pretrained (transformers 5.14.1, peft 0.21.2, torch 2.13.0), it was compared on 22 Oct 2026, on CPU
in float32, with the trainer's own PyTorch LoRA layer holding the trained tensors (scale alpha / r = 2.0), on 10 fixed
dev prompts (7 EXTRACT, 3 ASK):
- 200 of 200 adapted modules wrapped by PEFT, every
lora_A / lora_B bit-identical to the trained tensors;
- logits max-abs difference 0.0 over every prompt position and every greedy step (bar 1e-2);
- greedy tokens identical on 10 of 10 prompts (1,160 tokens, each to its stop token), and
generate() gives the
same tokens.
Not covered: bf16, a GPU, and the constrained reader, which is not part of the adapter.
Training
Table | |
|---|
| base | Qwen/Qwen3.5-4B, revision 851bf6e806efd8d0a36b00ddf55e13ccb7b8cd0a, bf16; hybrid Gated DeltaNet and gated attention, 32 layers, hidden 2560, tied embeddings |
| adapter | LoRA rank 16, alpha 32 (scale 2.0), on q/k/v/o_proj (8 full-attention layers), in_proj_qkv, in_proj_z, out_proj (24 Gated DeltaNet layers) and gate/up/down_proj (32 layers) of the language model: 200 modules, 400 float32 tensors, 122 MB |
| recipe | rc1's: learning rate 1e-4, linear warmup over 5% of steps then cosine to zero, AdamW (0.9, 0.999, eps 1e-8), no weight decay, global-norm clip 1.0, 2 epochs, batches packed to 4,096 tokens, max length 2,048, gradient checkpointing, seed 0; loss on the assistant turns only |
| trainer | a PyTorch LoRA trainer (PyTorch 2.11.0 + CUDA 12.8, transformers 5.14.1, flash-linear-attention 0.5.2), bf16 |
| hardware | one NVIDIA L4 (24 GB) on a Google Cloud Spot VM, 19 Oct 2026; the final attempt ran 1.14 h after two Spot reclaims |
| steps | 1,141 of 1,141; this checkpoint is step 1,000, the best dev loss (0.010765; 1.033436 before the first step; rc1: 0.009915) |
| training data | 4,741 examples: 2,971 EXTRACT + 1,770 ASK; dev 470 (288 EXTRACT + 182 ASK), rc1's |
| label rules | IntakeFrame v4 labelling rules v2.2 at training time |
| second seed | trained with the same recipe (seed 1): 1,143 steps, best at step 1,143, dev loss 0.011002; the same 4 of 5 bars on s2_unseen + s3; not part of this release |
Training data provenance
Synthetic throughout: no real client, adviser or lender data.
- 3,416 rows from rc1's training build: adviser messages written by four open models (Gemma 4 E4B-it, Qwen3-8B,
Mistral Small 3.2 24B, Phi-4) run locally from code-generated cases, labelled by construction and audited (label
rules v2 and v2.1); the exact writer checkpoints are listed on the
v1.0-rc1 card.
- 1,325 rows from the generator's training split: 1,000 code-generated seeds, texts written by a larger open-weight model called through an API, every
label verified against its seed by two judges of different model families (Claude Opus 5.5, Gemini 4); gold where
both agree (810 gold, 190 contested left out).
- Dev: the same 470 rows as rc1. The evaluation set (parts s1, s2, s3) is never used in training; the training
builder drops any text it shares with the evaluation set, by hash.
The training data and the evaluation set are not published.
Evaluation
The evaluation set is held by Lendmind and never published or used in training. Two strata are reported:
s2_unseen, adviser messages from writers (models) whose texts are in no training row, and s3, the training
data generator's held-out set. The headline is the two together (547 messages scored).
The bars:
- figure precision, tier 1 (≥ .98): of the tier-1 figures served (annual income, purchase price, deposit,
deposit %, LTV, loan, age, term in years), the share at the label's value;
- intent precision, lowest of A/B/C (≥ .95);
- clarification recall, designed (≥ .90): of the messages written to need a clarifying question, the share whose
question is asked;
- MARKET/UNKNOWN read as A (0): messages labelled MARKET or UNKNOWN (or a refusal) that are read as a case;
- frames returned (1.0): the share of messages for which a schema-valid frame is returned.
The labelling rules were revised after audits of the evaluation labels; v2.5 is the latest, and the tables below
give v2.1 (the version the release candidate was first judged on) and v2.5. A revision changes the expected labels,
not the model's outputs; for rc2 it moved no bar on any stratum.
Labels v2.1, s2_unseen + s3 (547 messages)
Table with columns: reader, figure precision tier 1 (bar ≥ .98), intent precision, lowest of A/B/C (≥ .95), clarification recall, designed (≥ .90), MARKET/UNKNOWN read as A (0), frames returned (1.0), bars met| reader | figure precision tier 1 (bar ≥ .98) | intent precision, lowest of A/B/C (≥ .95) | clarification recall, designed (≥ .90) | MARKET/UNKNOWN read as A (0) | frames returned (1.0) | bars met |
|---|
| lendmind-intake-4b-v1.0-rc2 (seed 0) | .991 | .975 | .929 | 1 of 111 | 1.000 | 4 of 5 |
| seed 1 (not released) | .989 | .983 | .935 | 1 of 111 | 1.000 | 4 of 5 |
rc1 (tag v1.0-rc1) | .975 | .778 | .858 | 3 of 111 | 1.000 | 1 of 5 |
By s3 difficulty level (the generator's levels 1 to 5; bars met of 5), rc2 / seed 1 / rc1: level 1 4/5/4, level 2
5/4/3, level 3 4/5/2, level 4 4/4/2, level 5 4/4/1.
Labels v2.5, rc2 by stratum
Table with columns: stratum, messages, figure precision tier 1 (≥ .98), intent precision, lowest of A/B/C (≥ .95), clarification recall, designed (≥ .90), MARKET/UNKNOWN read as A (0), frames returned (1.0), bars met| stratum | messages | figure precision tier 1 (≥ .98) | intent precision, lowest of A/B/C (≥ .95) | clarification recall, designed (≥ .90) | MARKET/UNKNOWN read as A (0) | frames returned (1.0) | bars met |
|---|
| s2_unseen + s3 | 547 | .991 (16 wrong of 1,796) | .975 | .929 (157 of 169) | 1 of 111 | 1.000 | 4 of 5 |
| s2_unseen | 106 | .967 (7 wrong of 209) | .955 | .857 (12 of 14) | 0 of 37 | 1.000 | 3 of 5 |
| s3 | 441 | .994 (9 wrong of 1,587) | .979 | .935 (145 of 155) | 1 of 74 | 1.000 | 4 of 5 |
Table with columns: reader, s2_unseen + s3, bars met, s2_unseen, bars met, s3, bars met| reader | s2_unseen + s3 | bars met | s2_unseen | bars met | s3 | bars met |
|---|
| lendmind-intake-4b-v1.0-rc2 (seed 0) | .991 (16 of 1,796) | 4 of 5 | .967 (7 of 209) | 3 of 5 | .994 (9 of 1,587) | 4 of 5 |
| seed 1 (not released) | .989 (19 of 1,796) | 4 of 5 | .957 (9 of 208) | 3 of 5 | .994 (10 of 1,588) | 4 of 5 |
rc1 (tag v1.0-rc1) | .975 (44 of 1,769) | 1 of 5 | .969 (6 of 196) | 3 of 5 | .976 (38 of 1,573) | 1 of 5 |
rc2 does not meet the figure-precision bar on messages from unseen writers. On s2_unseen, under labels v2.5, it
serves 7 wrong tier-1 figures of 209 (precision .967; under v2.1, 8 of 209, .962). The bar is .98, which at that
volume allows at most 4 wrong. Its clarification recall there, .857 (12 of 14 designed asks), is also under its bar,
so rc2 meets 3 of 5 bars on s2_unseen; rc1 and the second seed also meet 3 of 5 there.
On the dev set (288 EXTRACT messages, with the 182 ASK rows behind them; dev labels v2): figure precision .995
(842 of 846), figure recall .995, choices precision .978 and recall .985, lenders precision and recall 1.000, lowest
intent precision .955, clarification recall .967 (319 of 330), no MARKET/UNKNOWN message read as A (0 of 63), every
frame returned: all five bars met on dev, as for rc1.
Latency is reported, not yet a bar: p95 11.5 s a message on the evaluation set on the M4 Pro with plain greedy
decoding (rc1: 13.0 s).
Known limitations
- One MARKET/UNKNOWN message is read as a case. For rc2 it is a follow-up that carries a complete second case;
the model serves a frame instead of asking for the use case. The second seed reads that message right and instead
misses a refusal written as the last short sentence after a full case in shorthand. Both are errors under the
labelling rules.
- Unseen writers. On messages from writers the model never saw (s2_unseen), figure precision (.967 under labels
v2.5, 7 wrong of 209) and clarification recall (.857) are under their bars. The gain over rc1 is on the training
data generator's distribution (s3), not on unseen writers.
- Numbers are read, never computed. A total, a conversion or a range is not labelled; a figure in a range or an
uncorrected correction is left out and asked for.
- Speed. About 24 tokens a second bf16 on the M4 Pro with plain greedy decoding; exact drafts cut EXTRACT from
7.10 s to 2.31 s a row and ASK from 2.40 s to 0.97 s (dev rows, byte-identical). About 10 to 12 tokens a second on
an NVIDIA L4 with a CUDA port of the reader, plain greedy, one sequence at a time.
- CUDA is not byte-identical. A CUDA port of the reader (measured with rc1) matched the Mac on every s2_unseen
rate but served one new wrong value on the dev set and one on another evaluation stratum, each at a near-tie between
the top two tokens. A 0.3-logit margin floor on each value closes that on the dev set at the cost of nine values
withheld on the Mac; the evaluation set has not been checked under it. Every serving path must pass the full bar
before use.
- English, UK mortgage domain only. Written for UK adviser notes, intermediary lending criteria and lender
affordability calculators; it has not been evaluated on anything else.
Files
Table with columns: file, what it is| file | what it is |
|---|
adapter_model.safetensors | the LoRA weights, PEFT names, float32 (sha256 5b969bc4198d10eed4170a0b88cdf752a9379c5a8d79d8a12f799edd1f19a235) |
adapter_config.json | the PEFT config: r 16, lora_alpha 32, target_modules over the language model only |
LICENSE | Apache License 2.0, the text distributed with the base model |
NOTICE | the modification and attribution notices |
README.md | this card |
Licences and provenance
- This adapter: Apache License 2.0, © 2026 Lendmind Ltd.
- Base:
Qwen/Qwen3.5-4B, Apache License 2.0. This model is a modification of it; LICENSE is the licence text
and NOTICE states the modification.
- Writers of training texts: Gemma 4 E4B-it (Apache 2.0), Qwen3-8B and Mistral Small 3.2 24B (Apache 2.0), Phi-4
(MIT), and a larger open-weight model called through an API for the generator split. Judges of labels: Claude Opus 5.5 and
Gemini 4 (verification only; they wrote no training text).
- No personal data: every message is synthetic; the evaluation set and the training data are not published.
Versions
Table with columns: name, result| name | result |
|---|
| lendmind-intake-4b-v0.1 … v0.4 | earlier internal rounds, not released |
| lendmind-intake-4b-v1.0-rc1 | step 700 of 792; 1 of 5 bars on s2_unseen + s3 (labels v2.1 and v2.5); tag v1.0-rc1 of this repository |
| lendmind-intake-4b-v1.0-rc2 | step 1,000 of 1,141; 4 of 5 bars on s2_unseen + s3 (labels v2.1 and v2.5); 3 of 5 on s2_unseen |
| lendmind-intake-4b-v1.0 | not yet: all 5 bars on the evaluation set |