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:
@@ -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")
|
||||
|
||||
|
||||
Reference in New Issue
Block a user