/** * AI usage ledger. * * Every server-side AI provider call records one row here so admins can * audit spend per port, per feature, per user. Per-port budgets (stored * in `system_settings` under `ai.budget`) read this table to enforce * soft warnings and hard caps. * * Token-denominated rather than dollar-denominated so the cap survives * model price changes - and it's the unit both OpenAI and Anthropic * SDKs return in `response.usage`. */ import { pgTable, text, timestamp, integer, index } from 'drizzle-orm/pg-core'; import { ports } from './ports'; import { user } from './users'; export const aiUsageLedger = pgTable( 'ai_usage_ledger', { id: text('id') .primaryKey() .$defaultFn(() => crypto.randomUUID()), portId: text('port_id') .notNull() .references(() => ports.id, { onDelete: 'cascade' }), /** Optional - system-initiated calls (e.g. scheduled summarizers) won't have a user. */ userId: text('user_id').references(() => user.id, { onDelete: 'set null' }), /** Stable feature key: 'ocr', 'summary', 'embedding', 'reply_draft', etc. */ feature: text('feature').notNull(), /** 'openai' | 'claude' | 'tesseract' (free, recorded for parity). */ provider: text('provider').notNull(), model: text('model').notNull(), inputTokens: integer('input_tokens').notNull().default(0), outputTokens: integer('output_tokens').notNull().default(0), /** input + output. Indexed and used for budget rollup queries. */ totalTokens: integer('total_tokens').notNull().default(0), /** Provider-side request id for cross-referencing with provider logs. */ requestId: text('request_id'), createdAt: timestamp('created_at', { withTimezone: true }).notNull().defaultNow(), }, (table) => [ index('idx_ai_usage_port_created').on(table.portId, table.createdAt), index('idx_ai_usage_port_feature_created').on(table.portId, table.feature, table.createdAt), ], ); export type AiUsageRow = typeof aiUsageLedger.$inferSelect; export type NewAiUsageRow = typeof aiUsageLedger.$inferInsert;