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