01
Same model. Same task. Same seeds.
One LFM2.5-1.2B-Instruct route, served by llama.cpp with the shipped chat
template. The left terminal talks to it directly; the right terminal talks to it through
droplet bridge. These are real recorded transcripts.
What to expect: the bridged side still ends in suite FAILED. Order is restored, and the strict check still catches the extra call the model makes on its own. That honesty is the point.
Direct: the chain dies
$ droplet doctor --base http://127.0.0.1:8402
[INFO] backend: llama.cpp server, build b1-5f55650
[INFO] model: LFM2.5-1.2B-Instruct-Q4_K_M.gguf
[OK ] liquid model detected: profile lfm-1.2b
[OK ] native tool template: template declares LFM tool format
[OK ] reasoning extraction: non-reasoning model
[FAIL] assistant tool_calls history: template renders past assistant tool calls as an EMPTY turn — measured effect: 1.2B chains collapse at 2 steps; 24B loses steps 5-6
remedy: route through `droplet bridge` (history_carry), or fix the chat template upstream (add tool_calls rendering)
evidence: per-run A/B: 1.2B ordered 0/6 -> 6/6 (exact 0/6 -> 0/6); 24B exact 0/3 -> 3/3
[WARN] tool_choice modes: string 'none' works; 'required' cannot force a call (grammar permits unbounded pre-call content; burns max_tokens, finish=length); OBJECT form (named tool) is silently ignored with only a server-side log
remedy: avoid named tool_choice, or use droplet bridge named_choice filtering; treat 'required' as advisory
evidence: session-2 wire runs (0/6 required; conflict probe 6/6 wrong tool)
[INFO] raw /completion users: special tokens are stripped by default; pass "special": true or markers never appear
evidence: session-1, 27/27
verdict: 1 FAIL (assistant tool_calls history), 1 warn — droplet bridge or an upstream template fix is needed on this route
[exit 1]
$ droplet verify --base http://127.0.0.1:8402 --out report_direct
[droplet] verify: wrote report_direct/droplet-report.json and report_direct/droplet-report.md
[droplet] plain: compat=True ordered=None exact=None grounded=None correct=None failure_layer=None
[droplet] one_tool: compat=True ordered=True exact=True grounded=True correct=True failure_layer=None
[droplet] chain4: compat=True ordered=False exact=False grounded=True correct=False failure_layer=template
[droplet] verify: suite FAILED
[exit 1]
Through droplet bridge: order restored, the strict check still catches the model's extra call
$ droplet bridge --listen 127.0.0.1:8420 --target http://127.0.0.1:8402 --log bridge.jsonl &
[droplet] bridge listening on 127.0.0.1:8420 -> http://127.0.0.1:8402 profile=auto interventions=['history_carry', 'named_choice', 'required_guard']
[droplet] target identified: llamacpp b1-5f55650 model=/tmp/graphometer-demo/models/LFM2.5-1.2B-Instruct-Q4_K_M.gguf template_drops_history=True
$ droplet verify --base http://127.0.0.1:8420 --out report_bridged
[droplet] GET /props 200
[droplet] POST /apply-template 200
[droplet] POST /v1/chat/completions 200
[droplet] POST /v1/chat/completions 200
[droplet] POST /v1/chat/completions 200 applied=history_carry
[droplet] POST /v1/chat/completions 200
[droplet] POST /v1/chat/completions 200 applied=history_carry
[droplet] POST /v1/chat/completions 200 applied=history_carry
[droplet] POST /v1/chat/completions 200 applied=history_carry
[droplet] POST /v1/chat/completions 200 applied=history_carry
[droplet] POST /v1/chat/completions 200 applied=history_carry
[droplet] verify: wrote report_bridged/droplet-report.json and report_bridged/droplet-report.md
[droplet] plain: compat=True ordered=None exact=None grounded=None correct=None failure_layer=None
[droplet] one_tool: compat=True ordered=True exact=True grounded=True correct=True failure_layer=None
[droplet] chain4: compat=True ordered=True exact=False grounded=False correct=False failure_layer=model
[droplet] verify: suite FAILED
[exit 1]
$ kill 1274559
[droplet] shutting down (in-flight streams closing)
The only difference between the two sides is whether the model gets
to see the record of its own past actions. Direct, the chain dies at step two, and the
report blames the template, which is plumbing we can fix. Bridged, every requested call
runs in order, and the report blames the model instead, because this small model adds one
call nobody asked for. That is the failure_layer line in both terminals, the
one that says whose fault it is, flipping from template to model.
The strict check still fails, and it should. Droplet gives the model its memory back. It
does not make the model obedient, and it says so.
02
Three commands
$ droplet doctor
Read-only diagnosis of a serving route: what is healthy, what is broken, whose fault it is (the plumbing or the model), and the exact remedy. It never installs, mutates, or silently repairs anything.
When something is wrong
$ droplet doctor --base http://127.0.0.1:8402
[FAIL] assistant tool_calls history: template renders past assistant tool calls as an EMPTY turn — measured effect: 1.2B chains collapse at 2 steps; 24B loses steps 5-6
remedy: route through `droplet bridge` (history_carry), or fix the chat template upstream (add tool_calls rendering)
verdict: 1 FAIL (assistant tool_calls history), 1 warn — droplet bridge or an upstream template fix is needed on this route
When nothing is
$ droplet doctor --base http://127.0.0.1:8402 --live
[OK ] assistant tool_calls history: template renders past tool calls — multi-step chains safe
verdict: healthy — no intervention needed (1 advisory warn). Direct use is fine; you don't need droplet bridge here.
Both verdicts are first-class results. The 8B's template is correct, so the 8B gets the green one. A tool that tells you when you don't need it is a tool you can trust when it says you do.
$ droplet bridge
A loopback-only pass-through with one history repair, plus two small guards around
tool_choice, the setting a client uses to ask for a particular tool. The
history repair gives the model back the record of its own past tool calls. Every
modification is disclosed on the wire, and everything else relays byte-identical. It
never executes tools, never retries, never repairs malformed model output.
$ droplet bridge --listen 127.0.0.1:8420 --target http://127.0.0.1:8402 --log bridge.jsonl &
[droplet] bridge listening on 127.0.0.1:8420 -> http://127.0.0.1:8402 profile=auto interventions=['history_carry', 'named_choice', 'required_guard']
[droplet] POST /v1/chat/completions 200 applied=history_carry
$ droplet verify
A reproducible compatibility suite that writes an honest report. It scores the things that can go wrong one at a time: did the protocol work, did every requested call happen in order, did only the requested calls happen, was the answer grounded, was it correct, and whose fault any miss was. None of that gets squashed into a single ambiguous "pass." The suite only passes when the strict check passes, the one that wants the requested calls and nothing else.
$ droplet verify --base http://127.0.0.1:8420 --out report_bridged
[droplet] plain: compat=True ordered=None exact=None grounded=None correct=None failure_layer=None
[droplet] one_tool: compat=True ordered=True exact=True grounded=True correct=True failure_layer=None
[droplet] chain4: compat=True ordered=True exact=False grounded=False correct=False failure_layer=model
[droplet] verify: suite FAILED
03
The numbers
Every figure below is from recorded runs; nothing is extrapolated. These are small seeded A/B reproductions, so the number of runs behind each cell is the denominator you can see, not a large-n leaderboard. Full per-run ledgers ship in the evidence pack.
| Measurement | Direct (shipped template) | Repaired (ordered · exact) |
|---|---|---|
| 1.2B multi-step tool chains, seeded A/B | 0/6 on both scores | ordered 6/6 · exact 0/6 through droplet bridge. The model adds one call nobody asked for on every run, and the report says so. |
| The same repair in a live coding assistant (OpenCode) | 0/3 on both scores | ordered 3/3 · exact 2/3 through the bridge. One run repeated a calculator call. |
| 24B six-step chains | 0/3 on both scores | ordered 3/3 · exact 3/3 through the bridge, and ordered 3/3 · exact 3/3 with the template fix alone, no bridge |
| 8B healthy route through the bridge (it needs nothing) | not applicable | 18/18 task and protocol probes unchanged. Byte-parity is separately proven by hash comparison. No regression, and no need. |
Droplet restores the model's memory of its own actions. It does not make the model smarter or more obedient. Final-answer correctness is largely unchanged, and under the strict exact score the repaired 1.2B still adds a call nobody asked for. The report names that as the model's own behavior instead of hiding it.
The 8B is a strong tool-caller and needs nothing here, and Ollama users typically don't need the bridge, because Ollama's own renderer already preserves tool history.
04
Fixed at the root
The permanent fix is a few corrected lines in the chat templates themselves. All three fixes exist, are proven live, and are being submitted upstream.
- LFM2.5-1.2B-Instruct template renders past assistant tool calls, so the model keeps its memory of its own actions; the model's own extra call can still fail the strict check Fix prepared and reproduced
- LFM2-24B-A2B template the same history repair, and six-step chains complete ordered 3/3 · exact 3/3 direct, with no bridge Fix prepared and reproduced
- LFM2.5-1.2B-Thinking template the model's thinking arrives separated in its own reasoning_content field, and the leak is gone Fix prepared and reproduced
With a fixed template in place, the doctor verdict flips to healthy for all three models. On the 24B, the verify suite then passes directly, exact on both scores. On the 1.2B, the chains run in order and the template is no longer at fault (ordered 3/3, exact 0/3 in our runs), but the strict check can still fail because the model adds a call of its own, the same leftover you see through the bridge. If these fixes merge, the bridge's history repair retires, and that is the plan.