Skip to content

fix(translate): retry a sentence batch whose reply cannot be read - #437

Merged
petertzy merged 2 commits into
petertzy:mainfrom
tnalbant:fix/sentence-batch-unreadable-reply
Oct 17, 2026
Merged

petertzy merged 2 commits into
petertzy:mainfrom
tnalbant:fix/sentence-batch-unreadable-reply

Conversation

@tnalbant

Copy link
Copy Markdown
Contributor

What changed

The sentence-by-sentence translation path treated an unusable model reply as a fatal, unhandled
error: _coerce_translation_pairs raised a bare RuntimeError for a reply that was not JSON, that
had no pairs, or that was missing a translation for one of the units, and
_request_sentence_translation_batch_once did the same for an empty reply. The router's
except Exception turned 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_object replaces _strip_json_code_fence: it takes the JSON out of the reply
      regardless of surrounding prose (fenced block first, otherwise the outermost {…} span), and
      returns 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.
    • _UnusableSentenceReplyError is introduced as the internal signal for "this reply cannot be
      read as pairs" (invalid JSON / no pairs / incomplete pairs / empty reply).
    • _request_sentence_translation_batch_from_provider now handles that signal the same way it
      already 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 message
      and a meaningful status instead of a bare 500.

What I did not change

  • The 422 "AI translation requires a configured provider API key." in the report comes from the
    API-key resolution path (_get_ai_api_key_for_provider). I could not reproduce it — that needs the
    reporter'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.
  • The single-shot /translate mode still raises a plain RuntimeError (HTTP 500) on an empty
    reply. 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:

  • Before the change (source change stashed, new tests in place): 6 failed, 16 passed — the six new
    tests are red with RuntimeError: AI provider returned invalid sentence translation JSON.
  • After the change: 125 passed, 24 subtests passed across test_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.
  • Mutation controls: removing the tolerant JSON extraction turns only
    test_prose_wrapped_json_reply_is_accepted red; removing the unusable-reply branch turns the five
    recovery/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: already
    formatted; git diff --check: clean.

New tests in tests/test_provider_request_retry.py::TestUnusableSentenceReplyRecovery cover:
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's uv sync --extra dev
pulls 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); the
identical 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.

@petertzy

Copy link
Copy Markdown
Owner

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.

@petertzy
petertzy merged commit bc4cac6 into petertzy:main Oct 17, 2026
2 checks passed
@tnalbant
tnalbant deleted the fix/sentence-batch-unreadable-reply branch October 18, 2026 11:05
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.

2 participants