from __future__ import annotations import hashlib import json from typing import Any from app.models.base import async_session from app.models.chat_generation_task_event import ChatGenerationTaskEvent from app.models.chat_provider_call_log import ChatProviderCallLog from app.utils.id_gen import generate_id MAX_EXCERPT_CHARS = 2000 def _safe_json(data: Any) -> str | None: if data is None: return None try: return json.dumps(data, ensure_ascii=False, default=str) except Exception: return str(data) def _excerpt(data: Any, limit: int = MAX_EXCERPT_CHARS) -> str | None: text = _safe_json(data) if text is None: return None # Avoid storing secrets in logs. text = text.replace("Authorization", "Authorization-REDACTED") text = text.replace("api_key", "api_key_REDACTED") if len(text) > limit: return text[:limit] + "...[truncated]" return text def _hash(data: Any) -> str | None: text = _safe_json(data) if text is None: return None return hashlib.sha256(text.encode("utf-8")).hexdigest() async def log_task_event( task: Any | None = None, *, record: Any | None = None, task_id: str | None = None, record_id: str | None = None, event_type: str, from_status: str | None = None, to_status: str | None = None, from_stage: str | None = None, to_stage: str | None = None, message: str | None = None, detail: Any = None, ) -> None: """Write task event in a separate transaction; failure must not affect main flow.""" try: obj = task or record tid = task_id or record_id or (obj.id if obj else None) if not tid: return async with async_session() as db: db.add(ChatGenerationTaskEvent( id=generate_id(), task_id=tid, generation_mode=getattr(obj, "generation_mode", "chatapi_async"), event_type=event_type, from_status=from_status, to_status=to_status, from_stage=from_stage, to_stage=to_stage, message=message, detail_json=_excerpt(detail), )) await db.commit() except Exception: return async def log_provider_call( task: Any | None = None, *, record: Any | None = None, task_id: str | None = None, record_id: str | None = None, provider: str | None, api_type: str, model: str | None = None, engine_id: str | None = None, status: str, latency_ms: int | None = None, http_status: int | None = None, provider_task_id: str | None = None, request_data: Any = None, response_data: Any = None, prompt_tokens: int = 0, completion_tokens: int = 0, total_tokens: int = 0, error_code: str | None = None, error_message: str | None = None, ) -> None: """Write provider call log in a separate transaction; failure must not affect main flow.""" try: obj = task or record tid = task_id or record_id or (obj.id if obj else None) if not tid: return async with async_session() as db: db.add(ChatProviderCallLog( id=generate_id(), task_id=tid, generation_mode=getattr(obj, "generation_mode", "chatapi_async"), provider=provider, api_type=api_type, model=model, engine_id=engine_id, status=status, latency_ms=latency_ms, http_status=http_status, provider_task_id=provider_task_id, request_hash=_hash(request_data), response_hash=_hash(response_data), request_excerpt=_excerpt(request_data), response_excerpt=_excerpt(response_data), prompt_tokens=prompt_tokens or 0, completion_tokens=completion_tokens or 0, total_tokens=total_tokens or 0, error_code=error_code, error_message=error_message, )) await db.commit() except Exception: return