/** * AI module — usage ledger sink (Phase 12, TASK-0121). * * The gateway's usage sink: called (fire-and-forget, never awaited by the * request path) after EVERY completed or failed-after-consume gateway call. * * **Why this is a logger, not a writer (recorded decision):** the durable * `ai_messages` rows are written by the feature services on the request * path — the chat service persists the visible assistant message (content + * token counts + tool calls) in the user's conversation; the feature * services (writing / RAG / generate) persist their hidden per-call * conversation row. If the sink ALSO wrote a row, every call would be * double-counted in the monthly rollup (the budget guard sums * `ai_messages` — a double ledger would 402 workspaces twice as fast). * * The sink therefore gives the gateway's metering an observability home * (AGENTS rule 26 — usage of the AI surface is an important operation): a * structured line per call (capability, model, provider, tokens, status, * latency). The durable ledger is `ai_messages` (one row per call, written * by the services); the nightly `usage.rollup` worker job reconciles it for * dashboards. */ import { Inject, Injectable } from "@nestjs/common"; import type { UsageRecord } from "@factory/ai"; import { PrismaService } from "../../common/prisma.service.js"; import { logger } from "../../common/logger.js"; export const AI_LEDGER = "AI_LEDGER"; @Injectable() export class AiLedgerService { constructor(@Inject(PrismaService) private readonly prisma: PrismaService) {} /** * The gateway's sink (see module doc). Observability only — the durable * row is the service's own `ai_messages` write for the same call. */ async record(record: UsageRecord): Promise { logger.info( `ai usage ${record.kind} ${record.model}/${record.provider} in=${record.tokensIn} out=${record.tokensOut} ${record.latencyMs}ms ${record.status} (gateway metering; the durable row is the service's ai_messages write)`, { event: "ai.usage", workspaceId: record.workspaceId, ...(record.userId ? { userId: record.userId } : {}), ...(record.capability ? { capability: record.capability } : {}), kind: record.kind, model: record.model, provider: record.provider, tokensIn: record.tokensIn, tokensOut: record.tokensOut, latencyMs: record.latencyMs, status: record.status } ); } }