"""LLM 호출 계량과 감사 훅의 공통 실행 경로.""" from __future__ import annotations import time from collections.abc import Awaitable, Callable from typing import Any from ..engine_client import EngineClient, GenerateRequest, GenerateResponse LlmAuditHook = Callable[[dict[str, Any]], Awaitable[None]] async def record_llm_audit( audit_hook: LlmAuditHook | None, **payload: Any, ) -> None: """감사 저장소 장애가 사용자 응답을 막지 않도록 훅 실패를 격리한다.""" if audit_hook is None: return try: await audit_hook(payload) except Exception: return async def generate_with_audit( engine: EngineClient, request: GenerateRequest, audit_hook: LlmAuditHook | None, *, timeout: float | None = None, ) -> GenerateResponse: """비스트리밍 LLM 호출의 지연·토큰·비용 기록을 한 계약으로 고정한다.""" started = time.perf_counter() # deep-loop처럼 호출별 deadline이 필요한 경로가 audit 경계를 지나도 사라지지 # 않게 한다. 기본(None)은 기존 엔진 client deadline을 그대로 쓴다. response = ( await engine.generate(request, timeout=timeout) if timeout is not None else await engine.generate(request) ) latency_ms = int((time.perf_counter() - started) * 1000) await record_llm_audit( audit_hook, session_id=request.session_id, provider=response.provider, model=response.model, tokens_in=response.tokens_in, tokens_out=response.tokens_out, cost_usd=response.cost_usd, inference_geo=response.inference_geo, latency_ms=latency_ms, ) return response