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:
@@ -138,6 +138,10 @@ async def fetch_models(
|
||||
except ValueError:
|
||||
raise HTTPException(status_code=400, detail=f"Invalid provider_type: {provider_type_str}")
|
||||
|
||||
# Release DB connection before the network call to avoid idle-in-transaction
|
||||
# blocking other queries while we wait for the LLM API response.
|
||||
db.close()
|
||||
|
||||
client = LLMClient(
|
||||
provider_type=provider_type,
|
||||
api_key=api_key or "sk-placeholder",
|
||||
|
||||
@@ -30,33 +30,42 @@ from ._router import router
|
||||
# @PRE: User has translate.job.execute permission.
|
||||
# @POST: Returns the created translation run.
|
||||
@router.post("/jobs/{job_id}/run", status_code=status.HTTP_201_CREATED)
|
||||
async def run_translation(
|
||||
def run_translation(
|
||||
job_id: str,
|
||||
full_translation: bool = Query(False, description="Translate ALL rows (skip new-key-only filter)"),
|
||||
current_user: User = Depends(get_current_user),
|
||||
_ = Depends(has_permission("translate.job", "EXECUTE")),
|
||||
db: Session = Depends(get_db),
|
||||
config_manager: ConfigManager = Depends(get_config_manager),
|
||||
):
|
||||
"""Execute a translation job (trigger a run)."""
|
||||
logger.reason(f"run_translation — Job: {job_id}, User: {current_user.username}", extra={"src": "translate_routes"})
|
||||
"""Execute a translation job (trigger a run).
|
||||
|
||||
- Normal run (default): translates only new/changed rows
|
||||
- Full translation (full_translation=true): re-translates ALL rows from the datasource
|
||||
"""
|
||||
logger.reason(
|
||||
f"run_translation — Job: {job_id}, User: {current_user.username}, full: {full_translation}",
|
||||
extra={"src": "translate_routes"},
|
||||
)
|
||||
try:
|
||||
orch = TranslationOrchestrator(db, config_manager, current_user.username)
|
||||
run = orch.start_run(job_id=job_id, is_scheduled=False)
|
||||
run = orch.start_run(job_id=job_id, is_scheduled=False, full_translation=full_translation)
|
||||
# The request-scoped db session will be closed after this handler returns.
|
||||
# The background thread must use its OWN session to avoid operating on a
|
||||
# closed or expunged session.
|
||||
import threading
|
||||
|
||||
def _background_execute():
|
||||
bg_db = SessionLocal()
|
||||
from ....models.translate import TranslationRun as TRModel
|
||||
bg_db = None
|
||||
try:
|
||||
bg_db = SessionLocal()
|
||||
bg_orch = TranslationOrchestrator(
|
||||
bg_db, config_manager,
|
||||
current_user.username if current_user else None,
|
||||
)
|
||||
# Re-fetch the run within the background session to get a fresh,
|
||||
# attached object
|
||||
from ....models.translate import TranslationRun as TRModel
|
||||
bg_run = bg_db.query(TRModel).filter(TRModel.id == run.id).first()
|
||||
if bg_run is None:
|
||||
logger.explore(
|
||||
@@ -139,7 +148,8 @@ async def run_translation(
|
||||
},
|
||||
)
|
||||
finally:
|
||||
bg_db.close()
|
||||
if bg_db is not None:
|
||||
bg_db.close()
|
||||
|
||||
threading.Thread(
|
||||
target=_background_execute,
|
||||
@@ -159,7 +169,7 @@ async def run_translation(
|
||||
# @PRE: User has translate.job.execute permission.
|
||||
# @POST: Returns the updated translation run.
|
||||
@router.post("/runs/{run_id}/retry")
|
||||
async def retry_run(
|
||||
def retry_run(
|
||||
run_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
_ = Depends(has_permission("translate.job", "EXECUTE")),
|
||||
@@ -185,7 +195,7 @@ async def retry_run(
|
||||
# @PRE: User has translate.job.execute permission.
|
||||
# @POST: Returns the updated run.
|
||||
@router.post("/runs/{run_id}/retry-insert")
|
||||
async def retry_insert(
|
||||
def retry_insert(
|
||||
run_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
_ = Depends(has_permission("translate.job", "EXECUTE")),
|
||||
@@ -211,7 +221,7 @@ async def retry_insert(
|
||||
# @PRE: User has translate.job.execute permission.
|
||||
# @POST: Run is cancelled.
|
||||
@router.post("/runs/{run_id}/cancel")
|
||||
async def cancel_run(
|
||||
def cancel_run(
|
||||
run_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
_ = Depends(has_permission("translate.job", "EXECUTE")),
|
||||
@@ -234,7 +244,7 @@ async def cancel_run(
|
||||
# @PRE: User has translate.history.view permission.
|
||||
# @POST: Returns list of runs.
|
||||
@router.get("/jobs/{job_id}/runs")
|
||||
async def get_run_history(
|
||||
def get_run_history(
|
||||
job_id: str,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(20, ge=1, le=100),
|
||||
@@ -259,7 +269,7 @@ async def get_run_history(
|
||||
# @PRE: User has translate.history.view permission.
|
||||
# @POST: Returns run details with statistics.
|
||||
@router.get("/runs/{run_id}")
|
||||
async def get_run_status(
|
||||
def get_run_status(
|
||||
run_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
_ = Depends(has_permission("translate.history", "VIEW")),
|
||||
@@ -281,7 +291,7 @@ async def get_run_status(
|
||||
# @PRE: User has translate.history.view permission.
|
||||
# @POST: Returns paginated records.
|
||||
@router.get("/runs/{run_id}/records")
|
||||
async def get_run_records(
|
||||
def get_run_records(
|
||||
run_id: str,
|
||||
page: int = Query(1, ge=1),
|
||||
page_size: int = Query(50, ge=1, le=500),
|
||||
@@ -310,7 +320,7 @@ async def get_run_records(
|
||||
# @PRE: User has translate.job.view permission.
|
||||
# @POST: Returns list of batches.
|
||||
@router.get("/runs/{run_id}/batches")
|
||||
async def get_batches(
|
||||
def get_batches(
|
||||
run_id: str,
|
||||
current_user: User = Depends(get_current_user),
|
||||
_ = Depends(has_permission("translate.job", "VIEW")),
|
||||
@@ -356,7 +366,7 @@ async def get_batches(
|
||||
# @POST: TranslationLanguage.source_language_detected is updated; language_overridden is set to True.
|
||||
# @SIDE_EFFECT: DB write.
|
||||
@router.put("/runs/{run_id}/records/{record_id}/languages/{language_code}/override-language")
|
||||
async def override_detected_language(
|
||||
def override_detected_language(
|
||||
run_id: str,
|
||||
record_id: str,
|
||||
language_code: str,
|
||||
@@ -434,7 +444,7 @@ async def override_detected_language(
|
||||
# @PRE: User has translate.job.execute permission. Run, record, and language entry exist.
|
||||
# @POST: TranslationLanguage.final_value and user_edit are updated. Optional dictionary submission.
|
||||
@router.put("/runs/{run_id}/records/{record_id}/languages/{language_code}")
|
||||
async def inline_edit_translation(
|
||||
def inline_edit_translation(
|
||||
run_id: str,
|
||||
record_id: str,
|
||||
language_code: str,
|
||||
@@ -483,7 +493,7 @@ async def inline_edit_translation(
|
||||
# @PRE: User has translate.job.execute permission. Run exists.
|
||||
# @POST: If preview=false, matching translations are updated. Optional dictionary submission.
|
||||
@router.post("/runs/{run_id}/bulk-replace")
|
||||
async def bulk_find_replace(
|
||||
def bulk_find_replace(
|
||||
run_id: str,
|
||||
payload: BulkFindReplaceRequest,
|
||||
current_user: User = Depends(get_current_user),
|
||||
|
||||
Reference in New Issue
Block a user