All files / web/src/db/schema ai-usage.ts

100% Statements 77/77
100% Branches 1/1
100% Functions 0/0
100% Lines 77/77

Press n or j to go to the next uncovered block, b, p or k for the previous block.

1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 782x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 2x 39x 39x 39x 39x 39x 2x 2x 2x 2x  
import { createId } from '@paralleldrive/cuid2'
import { integer, real, sqliteTable, text, index } from 'drizzle-orm/sqlite-core'
 
/**
 * AI usage tracking table — records every external AI/ML API call with
 * the user who triggered it, the provider, model, and cost-relevant metrics.
 *
 * Raw metrics only — no dollar amounts stored. A reporting layer applies
 * current prices at query time.
 */
export const aiUsage = sqliteTable(
  'ai_usage',
  {
    id: text('id')
      .primaryKey()
      .$defaultFn(() => createId()),
 
    /** User who triggered the usage */
    userId: text('user_id').notNull(),
 
    /** Feature that caused the usage — typed by AiFeature const */
    feature: text('feature').notNull(),
 
    /** AI provider: 'openai' | 'anthropic' | 'gemini' | 'elevenlabs' */
    provider: text('provider').notNull(),
 
    /** Model used: 'gpt-5.2', 'gpt-4o-mini', 'gpt-image-1', etc. */
    model: text('model').notNull(),
 
    /**
     * API type for grouping:
     * 'chat_completions' | 'responses' | 'responses_streaming' |
     * 'realtime' | 'tts' | 'image' | 'embedding' | 'music'
     */
    apiType: text('api_type').notNull(),
 
    // --- Token metrics (null when not applicable) ---
    inputTokens: integer('input_tokens'),
    outputTokens: integer('output_tokens'),
    reasoningTokens: integer('reasoning_tokens'),
 
    // --- Audio metrics ---
    /** Audio input duration (e.g. user speaking in realtime session) */
    audioInputSeconds: real('audio_input_seconds'),
    /** Audio output duration (e.g. model speaking in realtime session) */
    audioOutputSeconds: real('audio_output_seconds'),
 
    // --- Image metrics ---
    imageCount: integer('image_count'),
 
    // --- Text metrics (for TTS input) ---
    inputCharacters: integer('input_characters'),
 
    // --- Music/general audio duration ---
    audioDurationSeconds: real('audio_duration_seconds'),
 
    // --- Optional link to background task ---
    backgroundTaskId: text('background_task_id'),
 
    // --- Freeform metadata (size, quality, voice session state, etc.) ---
    metadata: text('metadata', { mode: 'json' }),
 
    /** When this record was created */
    createdAt: integer('created_at', { mode: 'timestamp' })
      .notNull()
      .$defaultFn(() => new Date()),
  },
  (table) => ({
    userIdx: index('ai_usage_user_idx').on(table.userId),
    featureIdx: index('ai_usage_feature_idx').on(table.feature),
    createdAtIdx: index('ai_usage_created_at_idx').on(table.createdAt),
    userFeatureIdx: index('ai_usage_user_feature_idx').on(table.userId, table.feature),
  })
)
 
export type AiUsageRecord = typeof aiUsage.$inferSelect
export type NewAiUsageRecord = typeof aiUsage.$inferInsert