/** * AI module (Phase 12, TASK-0121). * * Wires the `@factory/ai` `AiGateway` into the API: * * - `AI_LEDGER` ({@link AiLedgerService}) — the gateway's usage sink * (observability; the durable `ai_messages` rows are written by the * feature services — see the ledger service's module doc). * - `AI_GATEWAY` — the gateway (provider registry + config resolution * from `workspace.settings.ai` over the env defaults). * - `AiBudgetGuardService` — the service-level, Postgres-backed budget * guard every AI endpoint awaits BEFORE the gateway call (402 * `AI_BUDGET_EXCEEDED` / 429); on exhaustion it notifies admins + * writes the `ai.budget_exceeded` audit event (24 h throttle). * * Provider selection is configuration-only (`AI_PROVIDER`/`AI_BASE_URL`/ * `AI_API_KEY`/`AI_MODEL` env or `workspace.settings.ai`): swapping the * provider changes zero application code (ROADMAP Phase 12 exit criterion). */ import { Module } from "@nestjs/common"; import { m } from "@factory/observability"; import { NotificationsModule } from "../notifications/notifications.module.js"; import { AiChatService } from "./ai-chat.service.js"; import { AiController } from "./ai.controller.js"; import { AiService } from "./ai.service.js"; import { AI_GATEWAY, buildAiGateway } from "./gateway-provider.js"; import { AiLedgerService, AI_LEDGER } from "./ledger.service.js"; import { AiBudgetGuardService } from "./budget-guard.service.js"; import { AiRateLimitService } from "./ai-rate-limit.js"; import { AiWritingService } from "./ai-writing.service.js"; import { AiWritingController } from "./ai-writing.controller.js"; import { DatabaseAiService } from "./database-ai.service.js"; import { DatabaseAiController } from "./database-ai.controller.js"; import { BlocksModule } from "../blocks/blocks.module.js"; import { DatabasesModule } from "../databases/databases.module.js"; import { PagesModule } from "../pages/pages.module.js"; import { SearchModule } from "../search/search.module.js"; import { TasksModule } from "../tasks/tasks.module.js"; import { RagService } from "./rag.service.js"; import { RagController } from "./rag.controller.js"; import { AiGenerateService } from "./generator/generate.service.js"; import { AiGenerateController } from "./generator/generate.controller.js"; import { AssistantService } from "./assistant/assistant.service.js"; import { AssistantController } from "./assistant/assistant.controller.js"; @Module({ // SearchModule exports SearchService — the RAG query path reuses its // fail-closed viewer resolver + the same OpenSearch client (no duplicate // permission-filter implementation — the spec's Security requirement). imports: [NotificationsModule, BlocksModule, DatabasesModule, PagesModule, SearchModule, TasksModule], controllers: [AiController, AiWritingController, DatabaseAiController, RagController, AiGenerateController, AssistantController], providers: [ { provide: AI_LEDGER, useClass: AiLedgerService }, AiBudgetGuardService, { provide: AI_GATEWAY, // Explicit `inject`: `useFactory` params have no `design:paramtypes` // metadata to infer from, so without it Nest calls the factory with // zero args and the ledger sink is undefined (caught by the TASK-0121 // capability suite: "ai usage sink failed"). inject: [AI_LEDGER], useFactory: (ledger: AiLedgerService) => buildAiGateway({ onBudgetExceeded: () => { // No-op: the service-level guard (AiBudgetGuardService) is the // enforcement + notification point. The gateway's in-memory // submit-time backstop never fires its hook — a backstop hit // throws before the provider call regardless. }, // TASK-0135: the spec's AI metrics are recorded HERE — the // gateway's usage sink is the ONE funnel every AI call flows // through (chat + embed + generate, all 7 call sites, success // AND error paths), with `model`/`tokensIn`/`tokensOut`/ // `latencyMs` already resolved. Recording centrally (instead of // at each service) means no call path is missed and none is // double-counted. The `model` label separates chat vs embed // models (different model strings), so the direction labels // stay spec-exact (in|out): embeddings meter as `out` (the // gateway's own `tokensOut` estimate) under the embed model. ledgerSink: (record) => { m.ai.tokens({ direction: "in", model: record.model }, record.tokensIn); m.ai.tokens({ direction: "out", model: record.model }, record.tokensOut); m.ai.request({ model: record.model }, record.latencyMs / 1000); return ledger.record(record); } }) }, AiService, AiChatService, AiRateLimitService, AiWritingService, DatabaseAiService, RagService, AiGenerateService, AssistantService ], exports: [AiService, AI_GATEWAY, AiBudgetGuardService, AiChatService, AiRateLimitService, AiWritingService, DatabaseAiService, RagService, AiGenerateService, AssistantService] }) export class AiModule {}