fix(llm): add fetch-models endpoint, fix SQL Lab INSERT (client_id truncation, sync mode, target_column, timestamp normalization)

- Add POST /api/llm/providers/fetch-models route with LLMClient.fetch_models()
- Add target_column to TranslationJob model/schema/service/orchestrator
- Fix SQL Lab execute: truncate client_id to 11 chars (varchar(11))
- Switch SQL Lab to sync mode (runAsync: false) — no Celery workers
- Fix polling: unwrap nested result from Superset query API
- Fix ClickHouse timestamp: normalize float timestamps to YYYY-MM-DD
This commit is contained in:
2026-05-13 20:06:15 +03:00
parent 39ab647851
commit a3c7c402b7
276 changed files with 1923 additions and 1822 deletions

View File

@@ -75,6 +75,69 @@ async def get_providers(
# #endregion get_providers
# #region fetch_models [TYPE Function]
# @BRIEF Fetch available models from an LLM provider by base_url+provider_type, or by provider_id.
# @PRE: User is authenticated. Either provider_id or base_url+provider_type must be provided.
# @POST: Returns a list of available model IDs.
# @RELATION CALLS -> [LLMProviderService]
# @RELATION CALLS -> [LLMClient]
@router.post("/providers/fetch-models")
async def fetch_models(
payload: dict,
current_user: User = Depends(get_current_active_user),
db: Session = Depends(get_db),
):
from ...plugins.llm_analysis.service import LLMClient
from ...plugins.llm_analysis.models import LLMProviderType
base_url = (payload.get("base_url") or "").strip()
provider_type_str = (payload.get("provider_type") or "").strip()
api_key = payload.get("api_key") or ""
provider_id = payload.get("provider_id") or ""
# Resolve provider_id to stored credentials if no direct api_key given
if not api_key and provider_id:
service = LLMProviderService(db)
db_provider = service.get_provider(provider_id)
if not db_provider:
raise HTTPException(status_code=404, detail="Provider not found")
base_url = base_url or db_provider.base_url
provider_type_str = provider_type_str or db_provider.provider_type
stored_key = service.get_decrypted_api_key(provider_id)
if stored_key:
api_key = stored_key
if not base_url:
raise HTTPException(status_code=400, detail="base_url is required")
if not provider_type_str:
raise HTTPException(status_code=400, detail="provider_type is required")
try:
provider_type = LLMProviderType(provider_type_str)
except ValueError:
raise HTTPException(status_code=400, detail=f"Invalid provider_type: {provider_type_str}")
client = LLMClient(
provider_type=provider_type,
api_key=api_key or "sk-placeholder",
base_url=base_url,
default_model="",
)
try:
models = await client.fetch_models()
return {"models": models}
except Exception as e:
logger.warning(
f"[llm_routes.fetch_models] Failed to fetch models: {e}",
extra={"src": "llm_routes.fetch_models"},
)
raise HTTPException(status_code=502, detail=str(e))
# #endregion fetch_models
# #region get_llm_status [TYPE Function]
# @BRIEF Returns whether LLM runtime is configured for dashboard validation.
# @PRE: User is authenticated.

View File

@@ -2,11 +2,12 @@
# @BRIEF Translation Run execution, history, status, records and batches routes.
# @LAYER: API
from datetime import datetime, timezone
from fastapi import APIRouter, Depends, HTTPException, status, Query
from typing import Any, Dict, List, Optional
from sqlalchemy.orm import Session
from ....core.database import get_db
from ....core.database import get_db, SessionLocal
from ....core.logger import logger, belief_scope
from ....schemas.auth import User
from ....dependencies import get_current_user, has_permission, get_config_manager
@@ -39,11 +40,107 @@ async def run_translation(
try:
orch = TranslationOrchestrator(db, config_manager, current_user.username)
run = orch.start_run(job_id=job_id, is_scheduled=False)
# Execute asynchronously in background
# 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()
try:
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(
"Background execute: run not found",
extra={"src": "translate_routes", "run_id": run.id},
)
return
# execute_run internally calls self.db.commit()
bg_orch.execute_run(bg_run)
# ----- VERIFICATION -----
# After execute_run the run object from bg_db is detached after
# commit. Re-query to verify the status was persisted in the
# database and is a terminal state.
check_run = bg_db.query(TRModel).filter(TRModel.id == run.id).first()
if check_run and check_run.status in ("COMPLETED", "FAILED", "CANCELLED"):
logger.reason(
"Background execute verified",
extra={
"src": "translate_routes",
"run_id": run.id,
"status": check_run.status,
},
)
else:
# execute_run appeared to succeed but the run is still in a
# non-terminal state — manually fail it so the frontend
# polling can detect the terminal state and stop spinning.
actual_status = check_run.status if check_run else "NOT_FOUND"
logger.explore(
"Background execute: run not in terminal state after commit",
extra={
"src": "translate_routes",
"run_id": run.id,
"status": actual_status,
},
)
if check_run:
check_run.status = "FAILED"
check_run.error_message = (
"Background execution did not reach terminal state "
f"(was: {actual_status})"
)
bg_db.commit()
except Exception as bg_err:
logger.explore(
"Background execute failed",
extra={
"src": "translate_routes",
"run_id": run.id,
"error": str(bg_err),
},
)
# Mark the run as FAILED so the frontend polling can detect a
# terminal state and stop spinning.
try:
fb_db = SessionLocal()
try:
fb_run = fb_db.query(TRModel).filter(TRModel.id == run.id).first()
if fb_run:
fb_run.status = "FAILED"
fb_run.error_message = f"Background execute error: {bg_err}"
fb_run.completed_at = datetime.now(timezone.utc)
fb_db.commit()
logger.reason(
"Background execute: run marked FAILED",
extra={"src": "translate_routes", "run_id": run.id},
)
finally:
fb_db.close()
except Exception as fb_err:
logger.explore(
"Background execute: unable to mark run FAILED",
extra={
"src": "translate_routes",
"run_id": run.id,
"error": str(fb_err),
},
)
finally:
bg_db.close()
threading.Thread(
target=orch.execute_run,
args=(run,),
target=_background_execute,
daemon=True,
).start()
return _run_to_response(run)