Repository navigation
fix(translate): retry a sentence batch whose reply cannot be read - #437
Conversation
|
Approved. The branch has been merged with the latest main, and the implementation addresses the intermittent sentence-translation failures caused by unusable provider replies. Invalid JSON, prose-wrapped JSON, empty responses, truncated pair lists, and incomplete translations are now classified as retryable sentence-batch failures. The batch is recursively split so valid smaller requests can still complete. If a single unit remains unreadable, the API returns an actionable 502 response instead of an unhandled 500. The JSON extraction accepts fenced or prose-wrapped JSON while preserving failure behavior for replies that contain no usable object. Regression coverage verifies successful parsing, batch splitting, single-unit error mapping, and the router response. Validation passed: 125 translation-related tests, 497 Python tests, 157 frontend tests, frontend lint, Ruff check, and Ruff format check. Ready to merge. |
What changed
The sentence-by-sentence translation path treated an unusable model reply as a fatal, unhandled
error:
_coerce_translation_pairsraised a bareRuntimeErrorfor a reply that was not JSON, thathad no
pairs, or that was missing a translation for one of the units, and_request_sentence_translation_batch_oncedid the same for an empty reply. The router'sexcept Exceptionturned that into HTTP 500 "Internal Server Error", and nothing was retried —so one bad reply from the model ended the whole translation.
Free and smaller models produce exactly that kind of reply intermittently (prose around the JSON, a
truncated list, a dropped pair), which matches the "does not occur consistently" symptom in #427.
The JSON extraction was also too strict: it only stripped a code fence when the reply started
with
, so a reply like `Here is the translation:\njson … ``` ` failed outright.backend/ai_logic.py_extract_json_objectreplaces_strip_json_code_fence: it takes the JSON out of the replyregardless of surrounding prose (fenced block first, otherwise the outermost
{…}span), andreturns the text unchanged when there is no object at all — so a reply with no JSON is still
reported as unusable instead of being silently accepted.
_UnusableSentenceReplyErroris introduced as the internal signal for "this reply cannot beread as pairs" (invalid JSON / no pairs / incomplete pairs / empty reply).
_request_sentence_translation_batch_from_providernow handles that signal the same way italready handles
context_length_exceeded: the batch is split in half and retried recursively.A single unit that still cannot be read is surfaced as
ProviderRequestError(502, "The AI provider returned a sentence translation that could not be read. Please try again, or choose a different model."), so the user gets an actionable messageand a meaningful status instead of a bare 500.
What I did not change
422 "AI translation requires a configured provider API key."in the report comes from theAPI-key resolution path (
_get_ai_api_key_for_provider). I could not reproduce it — that needs thereporter's credential-store state, and I have no OpenRouter key — so this PR does not touch it and
is not a fix for that specific error message. It hardens the reply-handling path of the same
translation flow.
/translatemode still raises a plainRuntimeError(HTTP 500) on an emptyreply. That is a separate change; I left it alone to keep this diff focused.
How I validated this
Same test selection on both sides, no piped exit codes:
6 failed, 16 passed— the six newtests are red with
RuntimeError: AI provider returned invalid sentence translation JSON.125 passed, 24 subtests passedacrosstest_provider_request_retry.py,test_translation_units.py,test_ai_automation_logic.py,test_ai_provider_config_logic.py,test_ai_chat_endpoint.py,test_ai_models_env_api_key.py.test_prose_wrapped_json_reply_is_acceptedred; removing the unusable-reply branch turns the fiverecovery/error tests red while the parser test stays green — the tests measure the claimed
behaviour.
ruff check(0.15.10, the pinned version):All checks passed!;ruff format --check: alreadyformatted;
git diff --check: clean.New tests in
tests/test_provider_request_retry.py::TestUnusableSentenceReplyRecoverycover:prose-wrapped JSON is accepted; an unreadable reply splits the batch; a truncated reply splits the
batch; an incomplete pair list splits the batch; a single unit that stays unreadable raises the 502
provider error; and the batch endpoint returns 502 with the readable message (no stubs, the real
router is exercised).
Not verified: the full suite. This machine has no
uv, and the repository'suv sync --extra devpulls heavy optional dependencies (docling, markitdown[all], weasyprint, PyMuPDF, python-docx,
python-dotenv, markdown2), so I ran a minimal Python 3.13 environment. Running every module that
environment can import gives
8 failed, 188 passed, 13 collection errors (missing modules); theidentical selection on the unmodified baseline gives the same 8 failures, so they come from the
environment, not from this change. CI (ubuntu, Python 3.12,
python -m pytest) covers the full suite.AI assistance: this change was prepared with an AI coding assistant.