fix(translate): Kilo API response_format, reasoning_effort skip, structured_outputs fallback, refusal handling

- Enable response_format (json_object) for all providers including Kilo/OpenRouter
- Skip reasoning_effort for Kilo/OpenRouter (returns 400 — mandatory reasoning)
- Add structured_outputs fallback: retry once without response_format if upstream
  provider (e.g. StepFun) rejects it with 'structured_outputs is not supported'
- Handle model refusal field (refusal) instead of treating as empty content
- Fix None-handling guards for message/finish_reason/content fields
- Apply same fixes to both preview.py and executor.py _call_openai_compatible
- Connect orphaned TermCorrectionPopup, BulkReplaceModal, BulkCorrectionSidebar
This commit is contained in:
2026-05-15 18:09:00 +03:00
parent 0fbf8f65bf
commit fdf48491a1
18 changed files with 1888 additions and 336 deletions

View File

@@ -23,6 +23,7 @@ from collections.abc import Callable
from datetime import UTC, datetime
from typing import Any
from sqlalchemy import text
from sqlalchemy.orm import Session, joinedload, selectinload
from ...core.config_manager import ConfigManager
@@ -72,6 +73,7 @@ class TranslationOrchestrator:
job_id: str,
is_scheduled: bool = False,
trigger_type: str | None = None,
full_translation: bool = False,
) -> TranslationRun:
with belief_scope("TranslationOrchestrator.start_run"):
# Load and validate job
@@ -110,6 +112,7 @@ class TranslationOrchestrator:
"batch_size": job.batch_size,
"upsert_strategy": job.upsert_strategy,
"dictionary_ids": self._compute_dict_snapshot_hash(job_id),
"full_translation": full_translation,
}
# Compute key_hash from source_key_cols
@@ -249,6 +252,15 @@ class TranslationOrchestrator:
self.db.commit()
return run
# Check if executor cancelled itself due to cancellation flag
if run.status == "CANCELLED":
self._update_language_stats(run.id, language_stats_map)
self.db.commit()
logger.reflect("Run cancelled via cancellation flag", {
"run_id": run.id,
})
return run
# Aggregate per-language statistics after executor completes
self._update_language_stats(run.id, language_stats_map)
@@ -746,7 +758,32 @@ class TranslationOrchestrator:
# @SIDE_EFFECT: DB write; records event.
def cancel_run(self, run_id: str) -> TranslationRun:
with belief_scope("TranslationOrchestrator.cancel_run"):
run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first()
# Set short lock timeout to avoid blocking on row lock held by
# background executor thread (which holds RowExclusiveLock during
# batch processing). If the row is locked, we fall back to setting
# a cancellation flag via direct SQL UPDATE, which the executor
# checks after each batch commit.
try:
self.db.execute(text("SET LOCAL lock_timeout = '3s'"))
run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first()
except Exception:
# Row is locked — set cancellation flag via direct SQL
logger.explore("Row lock timeout — setting cancellation flag via direct SQL", {
"run_id": run_id,
})
self.db.execute(
text("UPDATE translation_runs SET error_message = 'CANCEL_REQUESTED' WHERE id = :run_id"),
{"run_id": run_id},
)
self.db.commit()
run = self.db.query(TranslationRun).filter(TranslationRun.id == run_id).first()
if not run:
raise ValueError(f"Run '{run_id}' not found")
logger.reflect("Cancellation flag set for locked run", {
"run_id": run_id,
})
return run
if not run:
raise ValueError(f"Run '{run_id}' not found")