import { Inject, Injectable, Logger, type OnModuleInit } from '@nestjs/common'; import { routingRules, type Db, sql } from '@mosaic/db'; import { DB } from '../../database/database.module.js'; import type { RoutingCondition, RoutingAction } from './routing.types.js'; /** Seed-time routing rule descriptor */ interface RoutingRuleSeed { name: string; priority: number; conditions: RoutingCondition[]; action: RoutingAction; } export const DEFAULT_ROUTING_RULES: RoutingRuleSeed[] = [ { name: 'Complex coding → Opus', priority: 1, conditions: [ { field: 'taskType', operator: 'eq', value: 'coding' }, { field: 'complexity', operator: 'eq', value: 'complex' }, ], action: { provider: 'anthropic', model: 'claude-opus-4-6' }, }, { name: 'Moderate coding → Sonnet', priority: 2, conditions: [ { field: 'taskType', operator: 'eq', value: 'coding' }, { field: 'complexity', operator: 'eq', value: 'moderate' }, ], action: { provider: 'anthropic', model: 'claude-sonnet-4-6' }, }, { name: 'Simple coding → Codex', priority: 3, conditions: [ { field: 'taskType', operator: 'eq', value: 'coding' }, { field: 'complexity', operator: 'eq', value: 'simple' }, ], action: { provider: 'openai', model: 'codex-gpt-5-4' }, }, { name: 'Research → Codex', priority: 4, conditions: [{ field: 'taskType', operator: 'eq', value: 'research' }], action: { provider: 'openai', model: 'codex-gpt-5-4' }, }, { name: 'Summarization → GLM-5', priority: 5, conditions: [{ field: 'taskType', operator: 'eq', value: 'summarization' }], action: { provider: 'zai', model: 'glm-5' }, }, { name: 'Analysis with reasoning → Opus', priority: 6, conditions: [ { field: 'taskType', operator: 'eq', value: 'analysis' }, { field: 'requiredCapabilities', operator: 'includes', value: 'reasoning' }, ], action: { provider: 'anthropic', model: 'claude-opus-4-6' }, }, { name: 'Conversation → Sonnet', priority: 7, conditions: [{ field: 'taskType', operator: 'eq', value: 'conversation' }], action: { provider: 'anthropic', model: 'claude-sonnet-4-6' }, }, { name: 'Creative → Sonnet', priority: 8, conditions: [{ field: 'taskType', operator: 'eq', value: 'creative' }], action: { provider: 'anthropic', model: 'claude-sonnet-4-6' }, }, { name: 'Cheap/general → Haiku', priority: 9, conditions: [{ field: 'costTier', operator: 'eq', value: 'cheap' }], action: { provider: 'anthropic', model: 'claude-haiku-4-5' }, }, { name: 'Fallback → Sonnet', priority: 10, conditions: [], action: { provider: 'anthropic', model: 'claude-sonnet-4-6' }, }, { name: 'Offline → Ollama', priority: 99, conditions: [{ field: 'costTier', operator: 'eq', value: 'local' }], action: { provider: 'ollama', model: 'llama3.2' }, }, ]; @Injectable() export class DefaultRoutingRulesSeed implements OnModuleInit { private readonly logger = new Logger(DefaultRoutingRulesSeed.name); constructor(@Inject(DB) private readonly db: Db) {} async onModuleInit(): Promise { await this.seedDefaultRules(); } /** * Insert default routing rules into the database if the table is empty. * Skips seeding if any system-scoped rules already exist. */ async seedDefaultRules(): Promise { const rows = await this.db .select({ count: sql`count(*)::int` }) .from(routingRules) .where(sql`scope = 'system'`); const count = rows[0]?.count ?? 0; if (count > 0) { this.logger.debug( `Skipping default routing rules seed — ${count} system rule(s) already exist`, ); return; } this.logger.log(`Seeding ${DEFAULT_ROUTING_RULES.length} default routing rules`); await this.db.insert(routingRules).values( DEFAULT_ROUTING_RULES.map((rule) => ({ name: rule.name, priority: rule.priority, scope: 'system' as const, conditions: rule.conditions as unknown as Record[], action: rule.action as unknown as Record, enabled: true, })), ); this.logger.log('Default routing rules seeded successfully'); } }