-
Stefy Lanza (nextime / spora ) authored
A VLM that loses its place on a page repeats the last thing it saw until it runs out of tokens, and the result arrives looking like a successful transcription. Every engine here already runs `detect_repeat_token` over the FINISHED reply and treats a looping one as retryable -- the waste is that the reply has to finish first, so a page that starts looping on its tenth token still emits its whole budget before anyone looks at it. Measured on 100 pages of a real judicial corpus (surya, A40, cap 4096 / 1 continuation): 4 pages looped and accounted for 18.9% of the run's entire generation time. The worst spent 450 s emitting 17,586 characters of one repeated clause. Replayed through the shipped guard: 19.3% of generation time not spent, 0.560 -> 0.690 pages/s (+23%), worst page 450 s -> 67 s. Over all four measured rungs, 12 of 12 looping pages caught and ZERO false positives across 366 clean pages. WHY THIS IS SAFE TO HAVE ON BY DEFAULT: it is the same function with the same thresholds that already judges the finished reply. A page abandoned here is a page the post-hoc check would have rejected anyway, reached sooner. Nothing new decides anything. Two measured margins against cutting a page that merely LOOKS repetitive for a while: nothing is judged before 2000 characters (the largest genuine page in the corpus is 5,054, median ~1,900), and the detector must fire on 2 consecutive checks. Swept over 393 page reads -- at 1200/1 there was one false positive, a page with a repetitive middle and genuine text after it; at 2000/2 none, with all 12 real loops still caught. Closing the stream is the point: the server cancels the sequence when the client goes away, so the tokens not waited for were never generated. chandra and olmocr own their HTTP call, so they stream it directly. surya does not -- it builds and sends its own request -- so the CLIENT it is handed is wrapped instead, in the worker hook we already own, using SURYA'S OWN detector so the abort and surya's retry decision can never disagree. The worker keeps its no-codai-imports rule. Four defects found while writing it, each worse than the feature: * one check per CALL made a persistence above 1 unreachable for a caller that fed a whole reply at once, and made every verdict a function of the server's chunk framing. Now one check per step boundary crossed. * falling back to a blocking call after a failed stream re-ran the page: two generations billed, and the second reply mistaken for the continuation of the first. Only a failure that consumed NOTHING is retryable now (StreamUnsupported); a server that ignores `stream` has its complete body read rather than discarded, so the common case costs no extra call at all. * a local `from codai.ocr.base import OcrError` shadowed the module-level one for the whole function, so `except OcrError` raised UnboundLocalError and swallowed a mid-stream transport failure as retryable -- the one case that must never be retried. * the surya hook installs on `_trace or _repair`, so the abort depended silently on `repair_accents`. Now named in that condition. A server proved unable to stream is not asked again (one wasted request per server per process, not one per page). 4xx to a streaming request means "cannot stream" and falls back; 5xx is a real error and is not retried against a broken server. 29 tests. Full suite 3849 passed, 15 pre-existing failures unchanged. Co-Authored-By:Claude Opus 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_01KnfkxBpcZ1v6ypRFFyhrVM
bca55f84