"""``POST /agent/chat`` — the vertical slice. One request exercises: JWT auth → quota reserve → LLM gateway (routing, prompt version, cache-prefix check, daily budget) → ``ob_ai_call`` cost row → quota commit/refund → unified envelope. Streaming (SSE, FR-AGT-01) is intentionally out of scope for the scaffold. """ from __future__ import annotations import uuid from fastapi import APIRouter from apps.api.deps import AgentServiceDep, CurrentUserId from apps.api.schemas import ChatRequest, ChatResponse, ChatUsage from packages.llm.errors import ( AllProvidersFailedError, CachePrefixViolationError, DailyBudgetExceededError, StructuredOutputError, ) from packages.shared.errors import AppError, ErrorCode from packages.shared.response import ok router = APIRouter(prefix="/agent", tags=["agent"]) @router.post("/chat", summary="Single-turn styling chat (non-streaming)") async def chat( payload: ChatRequest, user_id: CurrentUserId, agent: AgentServiceDep, ) -> dict[str, object]: request_key = payload.request_key or uuid.uuid4().hex try: result = await agent.chat(user_id=user_id, message=payload.message, request_key=request_key) except DailyBudgetExceededError as exc: raise AppError(ErrorCode.LLM_DAILY_BUDGET_EXCEEDED) from exc except StructuredOutputError as exc: raise AppError(ErrorCode.LLM_STRUCTURED_OUTPUT_FAILED) from exc except AllProvidersFailedError as exc: raise AppError(ErrorCode.LLM_ALL_PROVIDERS_FAILED) from exc except CachePrefixViolationError as exc: raise AppError(ErrorCode.LLM_PROMPT_CACHE_VIOLATION) from exc return ok( ChatResponse( reply=result.reply, provider=result.provider, model=result.model, prompt_id=result.prompt_id, prompt_version=result.prompt_version, call_id=str(result.call_id), usage=ChatUsage( tokens_in=result.tokens_in, tokens_out=result.tokens_out, tokens_cached=result.tokens_cached, latency_ms=result.latency_ms, cost_cents=str(result.cost_cents), ), quota_remaining=result.quota_remaining, ).model_dump() )