Skip to content

fix: recover specs when a model drops a closing bracket - #35

Merged
RubenGlez merged 1 commit into
mainfrom
fix/deepseek-json-recovery
Aug 14, 2026
Merged

fix: recover specs when a model drops a closing bracket#35
RubenGlez merged 1 commit into
mainfrom
fix/deepseek-json-recovery

Conversation

@RubenGlez

Copy link
Copy Markdown
Owner

The accuracy gate wasn't flaky — it was faithfully reporting a real bug that had been invisible for weeks.

What was actually happening

The gate's means were exact multiples of 1/14 and the worst fixture always scored 0.00 flat. With a weighted scorer (F1, partial credit per dimension) a slightly-wrong spec can't score zero, so every zero had to be the catch branch. It was:

threw: 4/14  |  scored-but-imperfect: 0

Every reply that parsed scored a perfect 1.00. The zeros weren't inaccuracy — they were exceptions. Capturing the raw HTTP response on the real buildOperation path:

[finish_reason=stop  completion_tokens=125]     ← not truncated by max_tokens
opening braces: 16 | closing braces: 15         ← one missing
chars after last }: ""                          ← no prose, no fences

deepseek-chat under-closes deeply nested response schemas and then stops, believing it finished — so all three attempts reproduced the identical mistake and the endpoint was documented as nothing at all.

This is not just a CI problem: every EasyDocs user on DeepSeek was losing ~30% of their endpoints. It's the cheapest supported provider and it's featured in BENCHMARK.md.

The fix, and why it isn't a one-liner

The obvious repair — append the missing } — parses, but produces the wrong document:

responses.security: Expected object, received array

The dropped bracket belonged before ,"security", not at the end. Closing at the end buries security inside responses, which then fails schema validation anyway.

So jsonCandidates yields each plausible reading (exact → closed at the end → closed at each member boundary) and the caller takes the first that satisfies OperationSchema. The schema is the only reliable arbiter of where the bracket belonged. A well-formed reply still takes the fast path and yields exactly one candidate.

Two supporting changes:

  • Retry guidance names the likely cause instead of echoing Expected ',' or '}' ... at position 557, which the model demonstrably ignored — all three attempts repeated the same error.
  • The gate prints why each imperfect fixture lost points. It previously reported a bare mean, which cannot distinguish "slightly wrong spec" from "generation threw and scored zero" — opposite fixes, and the second one hid this for weeks.

Measured effect

deepseek-chat on the eval suite, before:

mean=0.714   threw: 4/14      (samples across runs: 0.714, 0.786, 0.786, 0.857, 0.929)

After, three consecutive full gate runs:

deepseek/deepseek-chat   mean=0.951   worst post-orders.json (0.31)
deepseek/deepseek-chat   mean=0.951   worst post-orders.json (0.31)
deepseek/deepseek-chat   mean=0.964   worst post-orders.json (0.50)
openai/gpt-4o            mean=1.000
anthropic/claude-haiku   mean=1.000
✅ Accuracy gate passed (×3)

Zero generation failures, and the suite runs in half the time because it no longer burns three retries per failure.

post-orders.json now surfaces as a genuine partial miss (responses=0.00, responseSchema=0.00) rather than a hard zero — a real, separate accuracy issue that is now visible instead of masked.

Verification

  • build, lint, typecheck green; 266 tests pass (was 256).
  • 10 new tests in builder-json.test.ts, built on the verbatim malformed output captured from deepseek-chat. They assert the repair puts security at the top level and not inside responses — i.e. that it picked the correct insertion point, not merely a parseable one.
  • Also asserted: repair happens on the first attempt without burning retries, braces inside string values don't unbalance the count, well-formed and fenced output are untouched, and output broken some other way (a missing comma) is not papered over.

Not included

JSON mode (response_format: json_object) would prevent the malformation at the source rather than repairing it, but it's provider-specific and worth evaluating separately.

https://claude.ai/code/session_01GrfumUQ4vFUAgsFYwzDyK8

Smaller models reliably under-close deeply nested response schemas and then
stop with finish_reason "stop" — they believe they finished, so all three
generation attempts reproduced the same truncation and the endpoint was
documented as nothing at all.

Measured on deepseek-chat, this was the cause of *every* failing accuracy
fixture: 4 of 14 endpoints produced no spec, while every reply that did parse
scored perfectly. The eval gate had been reporting this as an accuracy problem
because it only prints a mean.

The reply is now re-read with the missing brackets restored. Crucially the
bracket is not simply appended: in the captured failures the model under-closed
just before a trailing top-level key, so closing at the end parses but nests
`security` inside `responses` — valid JSON, wrong document. Each candidate
reading is validated against OperationSchema, which is the only reliable
arbiter of where the bracket belonged.

Also:
- Retry guidance names the likely cause instead of echoing the parser position,
  which models ignored.
- The gate prints why each imperfect fixture lost points, so a hard zero is
  distinguishable from a slightly-wrong spec.

deepseek-chat on the eval suite: 0.714-0.929 (straddling the 0.85 gate, 4-5
generation failures) → 0.951-0.964 with zero failures, over three consecutive
gate runs.

Claude-Session: https://claude.ai/code/session_01GrfumUQ4vFUAgsFYwzDyK8
@RubenGlez
RubenGlez merged commit 9e90cb8 into main Aug 14, 2026
3 checks passed
@RubenGlez
RubenGlez deleted the fix/deepseek-json-recovery branch August 14, 2026 14:24
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant