Can a small open model match a frontier model?

A fine-tuning case study on an invented trade-capture agent.

Brief for the engineer running the study, 13 November 2026. All data is synthetic, so it is safe to share.

1. Summary

QuestionHow small can an open model get and still do a reasoning-heavy agent task as well as a frontier model?
BetA fine-tuned model 5–10× cheaper per task than GPT-5.6 matches its accuracy, generalises, and knows when to hand a trade to a person.
TaskRead trade emails and their attachments (broker recaps, counterparty confirmations, corrections) at an invented energy trading company, and book the trades they confirm.
DeliverablesA 1,900-thread synthetic dataset, an evaluation harness, the filled results table, and the smallest model size that passes all four gates, or the reason none does.
Pass barFour gates (section 2): cheap, accurate, general, and routes well to a person.
Why it mattersA small model we own costs a fraction per task and is never retired by its provider.

2. Hypothesis and the four gates

A fine-tuned open model small enough to run 5–10× cheaper per task than GPT-5.6 matches its accuracy, generalises beyond its training threads, and knows when to hand a trade to a person. The hypothesis is disproved if no model size passes all four gates below.

Every model size must pass four gates; the answer is the smallest that does Open models1–3B, 7–8B, 14B,fine-tuned 1. Cheap≥ 5× cheaperthan GPT-5.6 2. Accuratewithin 5 pointsof frontier 3. Generalholds up onheld-out splits 4. Routes wellsure when right,flags when wrong Answersmallestpassing size
Cheap
  • Target: at least 5× cheaper per task than GPT-5.6; aim for 10×.
  • Cost counted: all-in per task: compute, training and hosting (7.3).
  • Volume: about 40,000 tasks a month.
  • Baseline price: GPT-5.6 at list price.
Accurate
  • Overall: share of threads fully right within 5 points of the best frontier model (paired 95% interval). Fully right means the whole output matches the gold answer, the correct output computed by code.
  • Per trap: no trap among 1–11 below 80%. Traps 13–15 are judged by the Routes well gate.
  • Format: at least 99% of outputs schema-valid.
General
  • Pass: pooled over the 6 held-out test splits (450 threads), the drop from the in-distribution score is at most 5 points larger than the frontier model's drop.
  • Per split: drops are reported as diagnostics; at 50–100 threads each, too small to pass or fail on alone (7.2).
Routes well
  • Auto-book precision: with the confidence threshold fixed on the calibration split (300 threads kept for this, 5.1), at least 99% of auto-booked test trades are fully right. A wrong booking costs more than a review.
  • Coverage: share of trades auto-booked within 5 points of the best frontier model's at that precision, so the bar can't be met by sending everything to a person.
  • Known unknowns: at least 95% of gold unsure fields flagged.
  • Calibration: expected calibration error at most 0.05: of trades given 0.9 confidence, close to 90% are right (7.4).
  • Unusable input: at least 90% of unusable threads rejected, at most 2% of usable threads wrongly rejected, and the right problem code and fix target in at least 90% of rejections.

Where the small model could win outright: spotting unusable input. A fine-tuned model learns what a complete, bookable thread looks like; general models tend to fill the gap and book anyway. If its rejection score beats GPT-5.6's, fine-tuning has added domain expertise, not only lower cost. Reported, not gated.

The thresholds are proposals; challenge them. The bar is GPT-5.6, the latest frontier model.

3. The task

The agent turns one email thread, with its attachments, into structured bookings.

3.1 The agent and where it sits

Kestrel Energy Trading GmbH (invented) trades German power and gas. Deals are agreed by phone or chat; what reaches trade support is email: broker recaps with PDF or Excel attachments, counterparty confirmations, corrections, cancellations and forwards, often in long reply chains. About 150 such emails arrive a day, and trade support re-keys every trade by hand. A missed correction or a wrong legal entity surfaces only when the confirmation or invoice fails to match.

The trade-capture agent reads each email thread when a new email arrives and books what the thread actually confirms. Each booking carries a confidence score: above a threshold it is booked straight away; below it, a person reviews it first. When the thread itself is unusable (an attachment that can't be read, a company we don't know) the agent books nothing and says what to fix.

Where the agent sits: from email thread to booking, review, not-booked list or a rejected thread Email thread recaps, confirmations, replies, attachments Trade-capture agent + counterparty list Confident trades → trading system booked "pending", approved in one click Low confidence → review queue or unsure; with the reason, never guessed Mentioned, not to book → listed cancelled, indicative, another entity's Thread not usable → rejected what is missing or broken, and how to fix it Bookings are later matched against counterparty confirmations, as today.

Every thread needs four reasoning steps:

The four reasoning steps in every thread 1. What still stands? later emails override; a working order isn't a trade 2. Who? names to the right company, not its twin 3. How much? MW × hours from the calendar, clock changes 4. What is unknown? leave empty or flag; or reject; never guess

3.2 A worked thread

A short thread to teach the idea. The right column says how to read each part: the green row makes the trade, the red row cancels the add-on. Terms are under Trading terms.

E1 subjectRecap 16-Feb: Kestrel buys GER Base Mar-27A broker recap: trades already agreed by phone
E1 bodyHi team, recap attached. Note the 3 MW add-on with Veltrona ET is subject to their credit approval. TomRow 2 is conditional
E1 recap.pdf row 1Buyer Kestrel | Seller Veltrona ET | GER Base Mar-27 | 5 MW | 78.90 | DoneWe are the buyer: we buy 5 MW
E1 recap.pdf row 2Buyer Kestrel | Seller Veltrona ET | GER Base Mar-27 | 3 MW | 79.00 | Done (sub credit)The add-on, pending credit
E2 bodyRE: Recap 16-Feb. Credit declined on the add-on (row 2), please disregard it. Row 1 stands.Cancels row 2
E2 quoted> Hi team, recap attached. Note the 3 MW add-on…Old text repeated; nothing new
Book Buy 5 MW, March 2027 baseload, €78.90/MWh, Veltrona Energie Trading GmbH, 743 h (clock change 28 March) = 3,715 MWh.
Typical a second trade of 3 MW (missed the reply); 744 h (missed the clock change); Veltrona Energiehandel (look-alike); sell (read the Seller column as ours).
Trading terms on this page
RecapA broker's summary of trades agreed by phone or thread, usually with a PDF or Excel table.
ConfirmationThe counterparty's formal record of a trade, usually a PDF with legal text.
MW, MWhMegawatt is the delivery rate; megawatt-hours is energy. 5 MW for 743 hours = 3,715 MWh.
Baseload, peakBase delivers every hour; peak delivers on weekdays 08:00–20:00.
Product codeDE-BASE-M-2027-03 = German power, baseload, month, March 2027.
Broker, counterpartyThe broker arranges the trade; the counterparty is the company on the other side.
Bid, offerThe price a buyer will pay, the price a seller asks. A buyer pays the offer; a seller gets the bid.
Working, indicativeAn order not yet filled; a price that is only a guide. Neither is a trade.
Sub credit, CXLSubject to a credit check; cancelled.
SpreadTwo linked trades in one: buy one product, sell another.
Exchange-clearedSettled through an exchange such as EEX rather than directly between the two companies.
Clock changeThe last Sunday of March has 23 hours, the last Sunday of October 25.

3.3 A realistic thread

Input at full scale: a test-set thread with 5 emails, 3 attachments and 5 bookings, read against a 214-entry counterparty list. The list under the answer shows where a small model slips.

The five emails and their attachments
E1  16 Feb 17:42  From: Tom Reyes <recaps@halden-brokers.example>
    To: trade-support@kestrel.example
    Subject: EOD recap 16-Feb - Kestrel
    Hi all, EOD recap attached. Row 2 credit approved by VEH at 09:40. Any issues shout.
    Tom Reyes | Halden Power Brokers | +44 20 ... | This email and any attachments are
    confidential ...
    Attachment: EOD_recap_HPB_16Feb.xlsx
    Row | Ref    | Buyer       | Seller       | Product         | MW | Price | Status
    1   | H-4471 | Kestrel     | Veltrona ET  | GER Base Mar-27 | 5  | 78.85 | Done
    2   | H-4472 | Kestrel     | Veltrona EH  | GER Base Mar-27 | 5  | 78.95 | Done (sub credit)
    3   | H-4473 | Kestrel     | Brenntal     | GER Base Q2-27  | 5  | 68.50 | Done
    4   | H-4474 | Kestrel     | Nordhafen    | GER Base Apr-27 | 10 | 69.40 | CXL
    5   | -      | Kestrel bid | -            | GER Base Q2-27  | 10 | 68.40 | Working
    6   | H-4476 | Kessel      | Kestrel      | GER Base Q3-27  | 10 | 71.50 | Done
    7   | H-4477 | Kestrel UK  | Arcadia Mkts | UK Base Cal-28  | 2  | 74.20 | Done

E2  16 Feb 17:55  From: confirmations@veltrona-eh.example
    Subject: Trade Confirmation VEH-2027-00871
    Please find our confirmation attached. Kindly countersign within two business days.
    Attachment: VEH-2027-00871.pdf (2 pages)
      "... Veltrona Energiehandel GmbH (the Seller) and
       Kestrel Energy Trading GmbH (the Buyer) ...
       Delivery Period: 01.03.2027 00:00 – 01.04.2027 00:00 CET
       Contract Capacity: 5 MW | Price: EUR 78,95/MWh
       This Confirmation is governed by the EFET General Agreement ... clearing: none ..."

E3  16 Feb 18:03  From: ops@brenntal.example  Subject: RE: our trade today (H-4473)
    Hello, please note the contracting party on H-4473 is Brenntal Energie Vertrieb GmbH,
    not the AG.
    > From: Tom Reyes ... (quoted recap follows)

E4  16 Feb 18:10  From: Tom Reyes  Subject: RE: EOD recap 16-Feb - Kestrel
    Apologies, row 6 price was wrong. Corrected file attached, all else unchanged.
    Attachment: EOD_recap_HPB_16Feb_v2.xlsx   (identical except row 6: Price 71.05)

E5  16 Feb 18:20  From: J. Kahn <jk@kestrel.example>  To: trade-support  Subject: FW: Mar peak
    pls also book this, agreed yday
    ---------- Forwarded message ----------
    From: desk@kessler-strom.example
    We confirm the reduction of trade T-88213 (GER Peak Mar-27) from 5 MW to 3 MW, price
    unchanged at 88.10.
Reference data: counterparty list and yesterday's trades

Counterparty list: 13 of 214 entries shown.

CP-0417  Veltrona Energie Trading GmbH   [Veltrona ET, VET]     active
CP-0453  Veltrona Energie Trading GmbH   [VET]                  inactive, merged into CP-0417
CP-0418  Veltrona Energiehandel GmbH     [Veltrona EH, VEH]     active
CP-0452  Veltrona Energy Trading UK Ltd  [Veltrona UK, VET UK]  active
CP-0611  Nordhafen Power AG              [Nordhafen, NHP]       active
CP-0630  Brenntal Energie AG             [Brenntal, BEAG]       active
CP-0631  Brenntal Energie Vertrieb GmbH  [Brenntal Vertrieb]    active
CP-0702  Kessler Strom GmbH              [Kessler]              active
CP-0703  Kessel Energy SE                [Kessel]               active
CP-0811  Arcadia Markets Ltd             [Arcadia Mkts]         active
IN-0002  Kestrel Energy Trading UK Ltd   [Kestrel UK, KET UK]   internal
BR-0007  Halden Power Brokers            [Halden]               broker
BR-0012  Arcadia Energy Brokers          [Arcadia]              broker

Yesterday's trades: 4 of 11 shown.

T-88213  buy   5 MW  DE-PEAK-M-2027-03   88.10  CP-0702 Kessler Strom GmbH
T-88214  buy   5 MW  DE-PEAK-M-2027-03   88.30  CP-0611 Nordhafen Power AG
T-88209  sell  5 MW  DE-PEAK-M-2027-03   88.25  CP-0703 Kessel Energy SE
T-88197  sell 10 MW  DE-BASE-Q-2027-Q2   68.90  CP-0703 Kessel Energy SE

Gold answer, computed by code:

StatusSideProductMWPriceCounterpartyHours, MWhEvidence
AnewbuyMar-27 base578.85CP-0417 Veltrona Energie Trading743, 3,715E4 v2 row 1
BnewbuyMar-27 base578.95CP-0418 Veltrona Energiehandel743, 3,715E4 v2 row 2, E1 body, E2 pdf
CnewbuyQ2-27 base568.50CP-0631 Brenntal Energie Vertrieb2,184, 10,920E4 v2 row 3, E3 body
Damendment of T-88213buyMar-27 peak388.10CP-0702 Kessler Strom276, 828E5 body
EnewsellQ3-27 base1071.05CP-0703 Kessel Energy2,208, 22,080E4 v2 row 6

Not booked: row 4 (cancelled), row 5 (working order, never filled), row 7 (Kestrel UK's trade, booked by that entity). Nothing unsure; thread processed.

Where a small model slips, part by part:

3.4 Input and output

The agent gets what a trade-support person sees: the thread with its attachments as extracted text, plus our reference data. It returns what that person would book, leave out, flag or send back.

What goes into the agent and what comes out Input Emailsbodies, replies, quoted history Attachmentsrecaps, confirmations: PDF, Excel Counterparty listwho "VET" is, and who it isn't Yesterday's tradeswhat an amendment refers to Agent+ rulebook Output tradeswhat to book, with confidence and proof not_bookedmentioned, left out on purpose, and why unsurequestions for a person, not guesses input_problemsthread unusable: what's wrong, how to fix
Input schema: thread, emails, attachments, counterparty list, prior trades
FieldTypeExampleWhat it is, and why the agent needs it
About the thread
thread_idstringTH-20270216-0412Identifies the thread, so every booking can be traced back to it.
processed_atdatetime, ISO 86012027-02-16T18:20+01:00When the latest email arrived. Relative dates ("next month") resolve against the sent date of the email that states them.
timezonestring (IANA)Europe/BerlinDelivery hours are German local time, so clock changes count.
The emails, emails[], oldest first
email_idstringE4Position in the thread; evidence points to it.
sent_atdatetime2027-02-16T18:10+01:00Order decides which statement wins.
from, to, ccstring, string[]recaps@halden-brokers.exampleWho says it. A counterparty's word on its own legal entity beats the broker's.
subjectstring"RE: EOD recap 16-Feb - Kestrel"Often misleading: "Confirmed" in a subject is not a confirmation.
bodystring"Apologies, row 6 price was wrong…"As written, with signatures, disclaimers and quoted earlier emails left in.
Attachments, emails[].attachments[]
file_namestringEOD_recap_HPB_16Feb_v2.xlsxNames often carry the version; evidence points to it.
typeenum pdf | xlsx | csv | eml | imagexlsxRecaps, confirmations, forwarded emails, scans.
textstring | null"Row \| Ref \| Book \| Buyer…"Extracted text, tables as rows. Null when the file can't be read: a case for rejection.
Who could be on the other side, counterparties[]
idstringCP-0417What the booking must contain. Scoring checks the ID, not the name.
legal_namestringVeltrona Energie Trading GmbHFull company name.
short_namesstring[]["Veltrona ET", "VET"]Names used in recaps. Look-alikes are in the list on purpose.
roleenum counterparty | broker | internalcounterpartyBrokers and our own other entities are listed so the agent never books them as the other side.
activebooleantrueMerged or closed companies stay in the list, sometimes under the same name. Never book an inactive one.
What we already booked
prior_tradesarray, may be empty[]Recent bookings, with trade_id. Needed when an email changes an earlier trade.
Output schema: status, trades, not_booked, unsure, input_problems

Examples are from the 3.2 thread.

FieldTypeExampleWhat it is, and how it's decided
The whole thread
thread_statusenum processed | rejectedprocessedrejected when the input is not good enough to book from. Then trades is empty and input_problems says why.
trades[]: which trade
statusenum new | amendment | cancellationnewCreates a trade, or changes or cancels one booked before.
amends_trade_idstring | nullnullThe earlier trade being changed. Required unless new.
sideenum buy | sellbuyFrom our side: the column or sentence naming Kestrel decides.
What is delivered, and when
commodityenum power | gaspower
product_codestring, from the rulebookDE-BASE-M-2027-03Market, profile, length and period in one code. Anything the rulebook can't produce is wrong.
delivery_startdatetime, ISO 8601 with offset2027-03-01T00:00+01:00First hour of delivery.
delivery_enddatetime, ISO 8601 with offset2027-04-01T00:00+02:00End, exclusive. The offset changes at the clock change.
How much
volume_mwnumber5Delivery rate as agreed.
delivery_hoursinteger743Counted from the calendar: peak days, clock changes, leap years. Never looked up.
volume_mwhnumber | null3715MW × hours. Null if either is unknown.
Price
pricenumber (2 dp) | null78.90Per MWh, as finally agreed. Null if the thread never settles it; never an older version, never 0.
currencyenum EUREURDefault.
With whom
counterparty_idstring | nullCP-0417From the list. Null plus an unsure entry if two companies could be meant.
broker_firmstring | nullHalden Power BrokersFrom the sender of the recap; null for direct trades.
deskenum Power DE | Gas DEPower DEFrom the product.
exchange_clearedbooleanfalseTrue only if the thread says the trade is cleared; boilerplate that mentions clearing isn't enough.
Proof
evidencestring[], locators["E1/recap.pdf/row 1"]Where the trade is stated, changed or cancelled: email, attachment and row or page. Must include the deciding one.
How sure
confidencenumber, 0–10.97The model's estimate that this booking is fully right. Above the threshold it is booked straight away; below it, a person reviews it (7.4).
not_booked[]: mentioned, left out on purpose
what, whystring"add-on 3 MW at 79.00", "cancelled in E2"The trade or row and the reason it isn't booked.
evidencestring[], locators["E1/recap.pdf/row 2", "E2/body"]Where it shows.
unsure[]: questions for a person
trade_indexinteger0Which trade the question is about.
field, reasonstringprice, "agreed by phone, not in the thread"What is missing and why it can't be read from the thread.
input_problems[]: why a thread was rejected
codeenum, from the rulebookattachment_unreadableWhat is wrong with the input (6.2).
evidencestring[], locators["E1/recap_scan.png"]Where the problem shows.
fix_targetenum sender | counterparty_list | rulebook | routingsenderWhat has to change for the thread to be bookable. Scored exactly.
fix_hintstring"The recap is a low-resolution scan; ask Halden for the Excel file."One sentence a person can act on. Not scored.
One training sample: the fields around each thread

A sample is one thread plus what training and scoring need: the gold answer, a realistic agent answer, and labels for the traps the thread tests.

FieldTypeExampleWhy it's there
sample_idstringS-00412Unique ID.
splitenum train | dev | testtrainTest samples never reach the training team.
trap_tagsenum[]["visible_signal", "calculated_number"]Which traps the thread tests; gives the per-trap score.
twin_ofstring | nullS-00413Its twin, a copy with one edit; gives the twin-pair score.
inputobjectas aboveWhat the model sees.
gold_outputobjectas aboveThe gold answer, computed by code. What the model is trained toward.
agent_outputobjectbooks the cancelled 3 MW add-onA realistic agent answer, wrong in 20% of samples. Gives the model an agent run to critique and supplies the wrong side for preference tuning.
verdictsarrayextra trade 3 MW at 79.00: wrong, status, "E2 cancels the add-on"Per field: agent value, gold value, right | wrong, error type, reason. The notes a reviewer would write.
label_noisebooleanfalseTrue on the 10% slice whose gold answer is deliberately wrong (trap 12).
The 3.2 thread as input and output JSON
// input
{
  "thread_id": "TH-20270216-0412", "processed_at": "2027-02-16T09:55+01:00", "timezone": "Europe/Berlin",
  "emails": [
    {"email_id": "E1", "sent_at": "2027-02-16T09:20+01:00",
     "from": "recaps@halden-brokers.example", "to": ["trade-support@kestrel.example"], "cc": [],
     "subject": "Recap 16-Feb: Kestrel buys GER Base Mar-27",
     "body": "Hi team, recap attached. Note the 3 MW add-on with Veltrona ET is subject to their credit approval. Tom",
     "attachments": [{"file_name": "recap.pdf", "type": "pdf",
       "text": "Row | Buyer | Seller | Product | MW | Price | Status\n1 | Kestrel | Veltrona ET | GER Base Mar-27 | 5 | 78.90 | Done\n2 | Kestrel | Veltrona ET | GER Base Mar-27 | 3 | 79.00 | Done (sub credit)"}]},
    {"email_id": "E2", "sent_at": "2027-02-16T09:55+01:00",
     "from": "recaps@halden-brokers.example", "to": ["trade-support@kestrel.example"], "cc": [],
     "subject": "RE: Recap 16-Feb: Kestrel buys GER Base Mar-27",
     "body": "Credit declined on the add-on (row 2), please disregard it. Row 1 stands.\n> Hi team, recap attached. ...",
     "attachments": []}
  ],
  "counterparties": [
    {"id": "CP-0417", "legal_name": "Veltrona Energie Trading GmbH", "short_names": ["Veltrona ET", "VET"], "role": "counterparty", "active": true},
    {"id": "CP-0418", "legal_name": "Veltrona Energiehandel GmbH",   "short_names": ["Veltrona EH", "VEH"], "role": "counterparty", "active": true},
    {"id": "BR-0007", "legal_name": "Halden Power Brokers",          "short_names": ["Halden"],            "role": "broker",       "active": true}
  ],
  "prior_trades": []
}

// output
{
  "thread_status": "processed",
  "trades": [{
    "status": "new", "amends_trade_id": null, "side": "buy", "commodity": "power",
    "product_code": "DE-BASE-M-2027-03",
    "delivery_start": "2027-03-01T00:00+01:00", "delivery_end": "2027-04-01T00:00+02:00",
    "delivery_hours": 743, "volume_mw": 5, "volume_mwh": 3715,
    "price": 78.90, "currency": "EUR", "counterparty_id": "CP-0417",
    "broker_firm": "Halden Power Brokers", "desk": "Power DE", "exchange_cleared": false,
    "evidence": ["E1/recap.pdf/row 1", "E2/body"], "confidence": 0.97
  }],
  "not_booked": [{"what": "add-on 3 MW at 79.00", "why": "subject to credit; credit declined in E2",
                  "evidence": ["E1/recap.pdf/row 2", "E2/body"]}],
  "unsure": [], "input_problems": []
}

4. What makes it hard: the fifteen traps

A trap is a kind of thread where a shortcut, learned in place of the rule, gives the wrong answer. These are common failure modes of LLM agents reading messy business text. Traps 1–12 are about getting the answer right; 13–15 about knowing when to hand a thread to a person or send it back. The dataset is built so every shortcut fails somewhere.

Grouped by the reasoning steps in 3.1. ✗ is the shortcut's answer, ✓ the gold answer. A twin is a copy of a thread with one edit (6.3).

Trap 1 on a twin pair: the shortcut is right on one thread and wrong on the other; the rule is right on both Twin pair for trap 1: one email differs Thread Arecap "sub credit"; reply "declined" Thread Brecap "sub credit"; reply "approved" Shortcut"Done" means a trade ✗ Books 10 MW ✓ Books 10 MW Rulebook once credit is confirmed ✓ No trade ✓ Books 10 MW The shortcut passes thread B and fails thread A. Only the rule gets both, so twins are scored as a pair.
#TrapShortcutExampleCaught by
What still stands
1Visible signal overridden"Done" or "Confirmed" means a tradeRecap row "Done (sub credit)"; reply: "credit declined": book ✗, not booked ✓Twin whose reply says "credit approved"
3PrecedenceThe first version is the truthRecap v1 row 6 at 71.50, v2 at 71.05: 71.50 ✗, 71.05 ✓Threads with 2–3 versions; the answer is never in the first
8Earlier historyEvery mention is a new tradeForwarded "reduce T-88213 from 5 to 3 MW": new 3 MW trade ✗, amendment ✓Test-only; without prior trades the answer is unsure
Who
4Look-alike partiesClosest string wins"Veltrona ET": Energiehandel ✗, Energie Trading ✓; inactive twin with the same name ✗Look-alike and inactive entries in every list. A name matching nothing is trap 14
9Wrong or messy sourceEvery row in an attachment is oursKestrel UK's row booked here ✗; quoted history read as a new email ✗Recaps with other desks' and entities' rows; quoted replies; forwards. A thread for another company is rejected
The numbers
5Calculated numberMemorise hours per productMar-27 base: 744 h ✗, 743 h ✓ (clock change)Train 2026–27, test 2028
10Off-list vocabularyCopy the attachment's wording"GER Base Mar-27", "78,95" ✗; product code, 78.95 ✓Schema check first
11Rare tailNever predict rareQ1/Q2 spread row booked as one trade ✗, two legs ✓≥ 15 per rare product; spreads test-only
What is unknown
2Safe fallbackUnfamiliar layout = no tradeNew recap template with a "We sold" column: no trade ✗, sell ✓Recap layouts held out for test
6Empty, zero, defaultAlways fill, or never"Price as agreed by phone": last quoted price ✗, null plus an unsure entry ✓Empty, zero, default in equal numbers; the trade goes to review
7Flags fire too easilyKeyword means yesConfirmation boilerplate mentions clearing: cleared ✗Half the keyword mentions mean no
Knowing when to hand over
13OverconfidenceEvery answer gets 0.95Books Veltrona Energiehandel at 0.97 ✗; same answer at 0.40, sent to review ✓Threshold fixed on the calibration split; calibration error; wrong trades booked straight away
14Booking from broken inputEvery thread yields a bookingRecap is an unreadable scan: values guessed from the subject ✗; rejected as attachment_unreadable, ask the sender ✓Usable vs unusable twin pairs; attachment_missing only in test
15Over-cautionWhen in doubt, hand it overReadable recap with OCR slips rejected as unreadable ✗, booked ✓; every trade sent to review ✗At most 2% of usable threads rejected; coverage within 5 points of frontier
Under all
12Noisy labelsLearn the labellers' mistakesGold answers that are wrong or follow older rulesTrain with and without a 10% noisy slice

Levers that help a small model, each tested as a variant:

5. The dataset

The dataset is 1,900 synthetic threads, delivered as JSONL. Four controls shape it: splits (what is held out), the variation matrix (what varies), reasoning ingredients (what forces reasoning) and tiers (how many ingredients one thread combines).

5.1 Splits

Clean 300
Traps 450
Messy
Calibration 300
Test 600

Blue is training data (300 clean single trades, 450 trap threads, 150 with a messy surface, 100 with deliberately noisy labels), grey is calibration, amber is test. Traps are oversampled so each shortcut fails often enough to measure.

SplitThreadsHeld byUsed for
train900Training teamFine-tuning
dev100Training teamChoosing hyperparameters and checkpoints
calibration300EvaluatorFixing the confidence threshold, nothing else (7.4)
test600EvaluatorThe results table, in seven splits (7.2)

5.2 Variation matrix

Scale per thread: 1–12 emails with 0–4 attachments (about 1,000–20,000 tokens), a counterparty list of 150–300 entries with 2–4 near-duplicates per active name and about 10% inactive, and 0–40 of yesterday's trades. Each training thread draws one level per dimension at the shares below. Quotas then make every trap occur with every delivery period and every way a trade is stated.

Plain case or neutral varietyMakes the thread harderUnusable input or wrong labelCount; darker is more
What is traded
Commodity
Power
Gas
Power 83%Gas 17%
Delivery period
Month
Quarter
Month 40%Quarter 24%Year 8%Week 8%Day 7%Balance of month 7%Weekend 6%
Profile (power)
Base
Peak
Base 65%Peak 35%
Side
Buy
Sell
Buy 50%Sell 50%
Calendar
2026
2027
2026 45%2027 55%
Thread shape
Trades to book
0
1
2–4
0 15%1 45%2–4 30%5 or more 10%
Rows in a recap
None (no recap)
1–3
4–10
None (no recap) 40%1–3 35%4–10 20%11 or more 5%
Thread length (emails)
1
2–4
5–12
1 35%2–4 45%5–12 20%
Attachments
None
PDF
Excel or CSV
None 25%PDF 35%Excel or CSV 30%Scan or forwarded email 10%
Revisions
0
1
0 72%1 18%2 7%3+ 3%
How a trade is stated
Recap row "Done"
Confirmation
Recap row "Done" 55%Confirmation 15%Email "ok to book" 13%Not a trade 12%Looks done, isn't 5%
What must be resolved
Counterparty named as
Short name
Legal name
None
Short name 44%Legal name 18%Look-alike abbreviation 14%None (no trade) 15%Broker-like 6%"TBC" 3%
Input quality
Usable
Usable 88%Thread incomplete 3%Unknown counterparty 3%Product not in rulebook 2%Wrong mailbox 2%Attachment unreadable 2% Attachment missing: test only.
Hours to count
Plain
Plain 78%Clock change 12%Peak days 6%Leap year 2%Partial fills 2%
Product named
Explicitly
Explicitly 87%Relative to trade date 13%
Conflicting statements
None
None 82%Indicative vs firm 10%Earlier vs later 6%Forwarded 2%
Missing fact
None
None 88%Price 6%Counterparty 3%Side 3%
Surface noise
None
Quoted history
None 55%Quoted history, signatures 25%OCR slips 10%Number formats 10%
Labels
Agent answer
Right
Wrong
Right 80%Wrong 20%
Gold label
Clean
Clean 90%Deliberately noisy 10%

5.3 Reasoning ingredients by tier

An ingredient is a feature that forces a reasoning step; a tier is how many a thread combines: tier 1 none (300 plain threads), tier 2 one, tier 3 two or three, tier 4 four or more. Each cell counts training threads where the ingredient decides the answer. Every ingredient appears alone and combined, so a failure traces to the ingredient or the combination.

IngredientForces the model toTier 2
300
Tier 3
280
Tier 4
120
Total
Revisions
"disregard row 2", recap v2
Keep state; apply later emails to earlier trades5613194281
Implicit or false confirmations
"ok to book" confirms; "indicative" doesn't
Tell a trade from talk4410374221
Calculated numbers
peak hours, clock changes, fills
Compute the value; copying fails4410374221
Look-alike parties
"VET" vs "VEH"
Pick one ID among near-duplicates409367200
Conflicting statements
v1 vs v2; body vs attachment
Apply precedence368460180
Interleaved trades
one recap, seven rows, two entities
Track which row is which trade307050150
Relative dates
"next month", "bal-week"
Resolve against the email's sent date266144131
Missing facts
price agreed by phone
Abstain; guessing fails245640120

The realistic thread in 3.3 is a tier-4 case: it combines revisions, look-alike parties, conflicting versions, several trades, an amendment and four hour counts.

5.4 Agent answers: right and wrong

Every sample carries a realistic agent answer, the kind of output an agent run logs, so it can feed preference tuning. The agent answer is fully right in 80% of threads. In the other 20%, the error is one of the kinds below, most tied to a trap in section 4. About a quarter of the wrong answers also carry a routing error (traps 13–15): high confidence on a wrong trade, a booking from an unusable thread, or a usable thread rejected.

Booked something that is not a trade (traps 1, 7)
22%
Look-alike or wrong counterparty (traps 4, 9)
18%
Wrong number: price, volume or hours (traps 5, 10)
16%
Missed a trade (no-trade fallback) (traps 2, 11)
14%
Older version or history taken as new (traps 3, 8)
12%
Guessed a missing fact instead of flagging it (trap 6)
10%
Wrong side (read the other party's column)
8%

6. Building the dataset

How the threads are generated and checked.

6.1 Pipeline

Generation pipeline: facts, gold, text, checks, agent answer, twins 1. Draw factsfrom the variationmatrix, by seed 2. Compute goldby code, fromthe rulebook 3. Write threadtemplates + LLMparaphrase 4. Checkthread still impliesgold; else discard 5. Agent answergold + one error,20% of threads 6. Twinsone edit,re-run 2–5 The gold answer never comes from an LLM: code computes it before any text exists.

6.2 Rulebook v0

The eleven rules that code computes the gold answers from. v1 is a deliverable (8).

The eleven rules
Product code{market}-{profile}-{tenor}-{period}. Market DE (power) or THE (gas, base only). Tenor D, WE, W, BOM, M, Q, Y. Period 2027-03, 2027-Q2, 2027-W09, 2027, or a start date for D, WE, BOM.
HoursLocal time, Europe/Berlin. Base: every hour. Peak: Mon–Fri 08:00–20:00, holidays included. Gas day 06:00–06:00. Clock changes count.
Relative productsAgainst the sent date of the email that states them: "next month" = following month; "bal-month" = tomorrow to month end; "bal-week" = tomorrow to Sunday; "WE" = coming Sat–Sun; "Q+1" = next quarter; "Cal" alone = next year. "Mar" alone = base.
What is a tradeBook recap rows marked done, confirmations, and trades an email explicitly confirms ("ok to book", "confirmed"). Working orders, indicative prices and cancelled rows are not trades. Our side is the column or sentence naming the Kestrel entity this mailbox books for; rows for other Kestrel entities are not ours. A buyer pays the offer; a seller gets the bid.
Precedence1. A later email beats an earlier one. 2. A newer attachment version replaces the older one. 3. A counterparty's statement of its own legal entity beats the broker's recap. 4. Firm beats indicative. 5. "Sub X": book only once X is confirmed in the thread; unresolved → unsure. 6. Quoted history is context, not new information; a forward from our own trader counts as an instruction.
ChangesExtra volume at the same terms → a separate trade. A corrected term in the same thread → one final record. A change to an earlier day's trade → amendment with amends_trade_id.
CounterpartyLegal name, then exact short name, then unique abbreviation. Two candidates → null + unsure. Never a broker or internal entry, never an inactive one. The counterparty's own correction of its entity overrides the recap.
DefaultsCurrency EUR; exchange-cleared false; market DE for power; profile base. Desk and broker from the input.
UnknownNever 0 for unknown: null plus an unsure entry. volume_mwh is null if MW or hours are unknown.
Numbers"5" = 5 MW. Prices to 2 decimals; "78,90" = 78.90. Spread legs share the MW.
Unusable inputReject the thread (thread_status rejected, no trades) when it refers to earlier emails that are not in it and the trade can't be rebuilt (thread_incomplete), names a company not in the list (counterparty_unknown), needs a product the rulebook can't encode (product_not_in_rulebook), belongs to another company or desk (wrong_mailbox), or its key attachment can't be read (attachment_unreadable) or is announced but absent (attachment_missing). One problem entry per issue. Never book from a rejected thread.

6.3 Twins and wrong agent answers

A twin is a copy of a thread with one edit. Flip twins change one detail and must change the answer. Invariance twins change only the surface and must not. Traps 1–9, 11 and 14 each get 20 flip-twin pairs, spread across the test splits; a pair counts only if both threads are fully right. A wrong agent answer is the gold answer with one error applied.

The twin edit for each trap
1. Visible signalReply says credit approved vs declined
2. Safe fallbackKnown recap template vs a new layout saying the same
3. PrecedenceSwap which recap version is the latest
4. Look-alikesAbbreviation points to the other company
5. Calculated numberShift the month across a clock change or leap day
6. Empty, zero, defaultPrice stated vs "as agreed by phone"
7. FlagsClearing stated in the body vs only in the boilerplate
8. HistoryPrior trades given vs withheld
9. SourceRow for our entity vs the same row for Kestrel UK
10. VocabularyInvariance: "GER Base Mar-27", "DE base March", "M3-27"
11. Rare tailSpread row vs the same two products as separate rows
13. OverconfidenceNo twin: scored by calibration over every thread
14. Broken inputReadable recap vs the same recap as an unreadable scan; a look-alike counterparty vs one missing from the list
15. Over-cautionClean recap vs the same recap with OCR slips, both bookable
The nine error operators for wrong agent answers

Start from the gold answer and apply one error, chosen to fit the thread's trap, at the error mix in 5.4. The scorer then writes the verdicts, so they always agree with the answer.

DropSet a filled field to null (left empty).
DecoyTake a wrong value that is in the thread: the first price, the look-alike company, 744 hours.
StatusBook a cancelled, working or conditional row; miss a confirmed trade; book another entity's row.
FlipInvert a yes/no flag or the side.
InventFill an unknown with 0.00 or an earlier quoted value.
DriftWrite an off-list value: "monthly", "78,90", DE_BASE_MAR27.
OverconfidentKeep a wrong field and set its trade's confidence above the threshold.
Book anywayBook a trade from an unusable thread instead of rejecting it.
Over-cautionReject a usable thread, or send a clear trade to review.

error_type takes exactly these nine values, or null when the field is right.

6.4 Quality gates and data rules

Every generated batch must pass six quality gates (separate from the four gates in section 2):

  1. Schema-valid, 100%.
  2. The blind second read agrees with the gold answer, or the thread is fixed or dropped.
  3. Human spot check of 5% per batch (50 for the first). The reviewer checks: realistic (scored 1–5), gold answer correct, trap present, only one answer possible. The gold answer is correct in at least 98%.
  4. Distribution within 2 points of the variation matrix.
  5. No train–test overlap (templates, seeds, long n-grams).
  6. Name scan: no real company or person names, zero hits.

Data rules:

7. Evaluation

7.1 The results table

One column per gate in section 2. Each row is a model at one size, averaged over three seeds; the cells stay empty until the study fills them.

Model (per size)AccurateGeneralRoutes wellCheap
Threads fully right, in-distributionDrop on 450 held-out threads, pointsTrades auto-booked at 99% precisionUnusable threads rejectedCost per 1,000 threads
Open base, untuned·····
Fine-tuned: gold·····
Fine-tuned: + reasoning·····
Fine-tuned: preference·····
GPT-5.6 (latest)·····
GPT-5.5 (previous generation)·····

Every row uses the same prompt and rulebook, temperature 0, and schema-constrained decoding where supported. The three fine-tuned rows train on: gold answers only; gold answers plus distilled reasoning traces; preference pairs of the gold answer against the wrong agent answer.

Also checked, outside the table
  • Pass-or-fail floors: ≥99% schema-valid output and ≥80% on each of traps 1–11 (Accurate); coverage within 5 points of frontier, ≥95% of unsure fields flagged, ECE ≤0.05, ≤2% of usable threads wrongly rejected and the right problem code in ≥90% of rejections (Routes well).
  • Where a model breaks: the score on each of the six held-out splits (7.2) and on each trap. These locate a failure; the pooled drop decides the gate.
How a thread is scored: code, not an LLM judge

The gold answer is computed by code and the output is structured JSON, so scoring is a field-by-field comparison. No LLM judge: it would add noise a 5-point gate can't absorb, cost money per run, and a frontier judge may favour frontier-style answers.

  1. Schema check: validate against a JSON Schema (types, required fields, allowed values). Invalid output = thread wrong, and it counts against the 99% format bar.
  2. Normalise: numbers to 2 decimals, datetimes parsed with their offset, whitespace trimmed. No fuzzy mapping: an off-list value stays wrong.
  3. Match trades: pair predicted and gold trades one-to-one on side, product, counterparty and volume. An unpaired predicted trade is a wrong booking; an unpaired gold trade is a miss.
  4. Compare fields exactly on each pair. not_booked items match on their evidence locators; unsure items on (trade, field). The free-text why and reason are not scored, and confidence is not part of "fully right": it is scored on its own (7.4).
  5. Rejected threads: fully right when the gold answer also rejects and the problem codes and fix targets match. Booking from an unusable thread counts as a wrong booking.
  6. Evidence: passes if it includes the deciding email or attachment row, which the generator knows by construction.

The only LLM in the loop is the blind second read in the data quality gates (6.4), which catches wrong gold answers before they enter the dataset.

All metrics and their definitions
MetricDefinition
Threads fully right (primary)Trades, not-booked list and unsure list all match the gold answer. A trade matches when side, product, MW, price, counterparty and status all match after normalising.
Twin-pair accuracyBoth threads of a flip twin fully right.
InvarianceSame output for both threads of an invariance twin.
Field accuracyPer field, over matched trades.
Wrong-booking rateShare of threads with at least one booked trade that does not match the gold answer or should not exist.
Rejection precision, recallOf rejected threads, the share that were unusable; of unusable threads, the share rejected. F1 combines them.
Problem diagnosisShare of correct rejections with the right code and fix_target.
Coverage at 99%Share of trades auto-booked when the threshold is set so 99% of auto-booked trades are fully right.
Risk–coverage (AURC)Area under the curve of auto-booked error rate against coverage as the threshold sweeps; lower is better, no threshold needed.
Calibration (ECE)Expected calibration error over 10 confidence bins: of trades given 0.9, are 90% right?
Review precisionShare of trades sent to review that were actually wrong: how much of a reviewer's time is well spent.
Escalation rateShare of threads with at least one unsure entry, against the gold answer's own rate.
AbstentionPrecision and recall of null and unsure where the gold answer is null or unsure.
EvidenceShare of trades whose evidence includes the deciding email or attachment row.
Schema validityShare of outputs that parse and use only allowed values.
Cost, latencyPer 1,000 tasks: tokens × price for APIs, GPU-hours × rate for open models; p50 and p95 latency, reported but not gated, since the agent runs minutes after a thread ends.
StabilityShare of threads with identical output across the three seeds and two reruns; reported, not gated.

7.2 Generalisation splits

The 600 test threads form seven splits. In-distribution (grey) is the reference; each of the other six (amber) holds out one thing that training never showed.

In-dist. 150
Surface 75
Composition 100
Unseen rules 100
Calendar 75
Vocab. 50
Swap 50
SplitWhat changes vs trainingExampleThreads
In-distributionNothing: the reference scoreNew threads from the same generator settings150
SurfaceWho wrote the textEmails written by a model family never used for training data; new recap templates and attachment layouts75
CompositionHow ingredients combineIngredient pairs never seen together in training; 4–6 revisions where training had at most 3100
Unseen rulesRules only in the rulebook, never in an exampleAmendments to yesterday's trades; spreads; threads with an announced but missing attachment, a problem type never shown in training100
CalendarDates outside training2028: leap year, clock changes on other dates, year-end products75
VocabularyWords and names outside trainingUnseen recap wording ("Purchased", "We sold"); a fresh counterparty list with new look-alikes50
Rule swapThe rulebook itselfPeak redefined as 09:00–21:00 in the prompt; does the model follow the prompt or its training?50

Read each split as its gap from the in-distribution score. A model that is strong in-distribution and drops 20 points on composition or rule swap has memorised the training data.

7.3 At par, cost and ablations

The at-par rule: the whole 95% interval of the difference must lie above minus 5 points −5 0 Fine-tuned model minus best frontier model, points of threads fully right At par Not at par 200 test threads
Illustrative intervals; only the −5 line is a threshold. A model is at par when the lower end of its interval is above −5. The third row is the first model scored on 200 threads: the interval widens from about ±3 to ±5.5 and crosses the line.

7.4 Routing to a human: confidence and unusable input

Each booked trade carries a confidence between 0 and 1. A threshold turns it into a route: above it, the trade is booked straight away; below it, or with any unsure entry, a person reviews it first. A useful model is confident when right and unsure when wrong, so reviewers see the risky trades and little else.

A well-calibrated model puts its wrong trades below the threshold; an overconfident one books them Review Book straight away threshold Calibrated wrong trades go to review Overconfident wrong trades get booked confidence 01
Illustrative. Each dot is a trade: green fully right, red wrong. The calibrated model sends its three wrong trades to review at the cost of two right ones; the overconfident model books three wrong trades.

How it is measured:

  1. Fit the threshold on the 300-thread calibration split: the lowest value at which auto-booked trades are at least 99% fully right.
  2. Apply it to test unchanged and report auto-book precision, coverage (share of trades booked straight away) and the wrong trades that slipped through.
  3. Risk–coverage curve: error of auto-booked trades as the threshold sweeps; its area compares models without picking a threshold.
  4. Calibration: expected calibration error and a reliability diagram per model and per test split.
  5. Flags that matter: recall of gold unsure fields, and the share of reviewed trades that were actually wrong.

Two ways to produce confidence, compared as variants: (a) the log-probability of the booked fields, temperature-scaled on the calibration split; (b) a stated confidence the model is fine-tuned to give, trained on out-of-fold labels of whether its own earlier answers were right. Frontier models are scored the same way, with stated confidence and log-probabilities where the API exposes them.

Unusable input is the second route: the thread goes back to whoever can fix it, with the problem and the fix. Measured by rejection precision and recall (does it reject the right threads, and only those?), and by diagnosis: the right code and fix_target. Flip twins pair a usable thread with the same thread made unusable, so a model can't pass by rejecting everything that looks odd. One problem type, a missing attachment, appears only in test.

Why this is hard for a small model: fine-tuning on gold answers teaches it to sound certain, because every target is stated with full confidence. Calibration must be fitted or trained separately, and it must hold on the held-out splits, where errors are most likely.

7.5 Threats to validity

RiskWhat it would doGuard
Synthetic-to-real gapAt par on synthetic threads, failing on threads people wroteA later replication on human-written threads
Generator tellsModel scores by recognising generator phrasingHeld-out templates and styles; invariance twins
Paraphraser biasA baseline that wrote the threads reads them bestParaphrase with a model not under test
Wrong goldEvery model looks worse; differences blurGold answers from code; blind second read; 98% spot-check bar
Rulebook overfitModel learns our wording of the rules in place of the rulesAblation without the rulebook; the unseen-rules split
Too small a test"At par" claimed on noise600 test threads, paired intervals, three seeds

8. Deliverables and acceptance

DeliverableAccepted when
Rulebook v1Covers every output field, product code and closing word.
Review set, 50 samplesRated realistic (at least 4 of 5) and gold answers correct.
GeneratorThe same seed reproduces the same data; every gold answer is computed by code.
Dataset, 1,900 threads, with data cardPasses the six quality gates (6.4): 100% schema-valid, gold answers correct in at least 98% of the spot check, distribution within 2 points of the variation matrix, no train–test overlap, zero name-scan hits.
Evaluation harnessScores any model per field, trap, split and twin pair, including the evidence check.
Results tableEvery row and column filled (7.1), three seeds, paired 95% intervals; a risk–coverage curve and reliability diagram per model; 20 failures explained.
RecommendationNames the smallest model size that passes all four gates, or the gate each size failed.
Assumed unless changed: German power with some gas; English threads; EUR/MWh; up to 200 GPU-hours; frontier baselines GPT-5.6 and GPT-5.5, plus another vendor's frontier model if budget allows; threads paraphrased by a model family not under test.