- Fix admin sidebar: remove duplicate Teams, add Storage nav item - Analytics: client-scoped queries, super_admin sees all (including NULL client_id) - Storage management: list/download/delete presentations with file metadata - Settings page with brand config router - AI usage tracking: new AIUsageModel, ai_usage_service, analytics endpoint - Master deck → template bridge: _register_as_template creates TemplateModel + PresentationLayoutCodeModel so parsed layouts appear in template picker - Multi-provider LLM vision in parser: Anthropic/Google/OpenAI with asyncio.to_thread - Fix PPTX upload 400: accept by .pptx extension (browser sends octet-stream) - Fix reparse FK violation: presentation_id=None for parse_master_deck jobs - Worker job_timeout increased to 1800s for LLM-heavy master deck parsing - PYTHONUNBUFFERED=1 in docker-compose worker for real-time log output - Auth: clientId in /me response, dev-login cookie improvements - Frontend: auth slice clientId, master-deck thumbnails, storage page Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
80 lines
2.4 KiB
Python
80 lines
2.4 KiB
Python
"""Fire-and-forget AI usage logging service."""
|
|
import asyncio
|
|
import uuid
|
|
from typing import Optional
|
|
|
|
from services.database import async_session_maker
|
|
from utils.llm_context import get_llm_context
|
|
|
|
|
|
def log(
|
|
provider: str,
|
|
model: str,
|
|
call_type: str,
|
|
input_tokens: Optional[int] = None,
|
|
output_tokens: Optional[int] = None,
|
|
total_tokens: Optional[int] = None,
|
|
duration_ms: Optional[int] = None,
|
|
user_id: Optional[uuid.UUID] = None,
|
|
client_id: Optional[uuid.UUID] = None,
|
|
presentation_id: Optional[uuid.UUID] = None,
|
|
) -> None:
|
|
"""Log AI usage asynchronously (fire-and-forget).
|
|
|
|
If user_id/client_id/presentation_id are not provided,
|
|
reads them from the LLMCallContext contextvar.
|
|
"""
|
|
ctx = get_llm_context()
|
|
if ctx:
|
|
user_id = user_id or ctx.user_id
|
|
client_id = client_id or ctx.client_id
|
|
presentation_id = presentation_id or ctx.presentation_id
|
|
|
|
asyncio.create_task(
|
|
_write_log(
|
|
provider=provider,
|
|
model=model,
|
|
call_type=call_type,
|
|
input_tokens=input_tokens,
|
|
output_tokens=output_tokens,
|
|
total_tokens=total_tokens,
|
|
duration_ms=duration_ms,
|
|
user_id=user_id,
|
|
client_id=client_id,
|
|
presentation_id=presentation_id,
|
|
)
|
|
)
|
|
|
|
|
|
async def _write_log(
|
|
provider: str,
|
|
model: str,
|
|
call_type: str,
|
|
input_tokens: Optional[int],
|
|
output_tokens: Optional[int],
|
|
total_tokens: Optional[int],
|
|
duration_ms: Optional[int],
|
|
user_id: Optional[uuid.UUID],
|
|
client_id: Optional[uuid.UUID],
|
|
presentation_id: Optional[uuid.UUID],
|
|
) -> None:
|
|
try:
|
|
from models.sql.ai_usage import AIUsageModel
|
|
|
|
async with async_session_maker() as session:
|
|
entry = AIUsageModel(
|
|
provider=provider,
|
|
model=model,
|
|
call_type=call_type,
|
|
input_tokens=input_tokens,
|
|
output_tokens=output_tokens,
|
|
total_tokens=total_tokens or ((input_tokens or 0) + (output_tokens or 0)),
|
|
duration_ms=duration_ms,
|
|
user_id=user_id,
|
|
client_id=client_id,
|
|
presentation_id=presentation_id,
|
|
)
|
|
session.add(entry)
|
|
await session.commit()
|
|
except Exception as e:
|
|
print(f"AI usage log error: {e}")
|