import type Database from 'better-sqlite3'; import type { EmbeddingSettingsUpdateDto } from '$lib/dtos/embedding-settings.js'; import { createProviderFromProfile, getDefaultLocalProfile } from '$lib/server/embeddings/registry.js'; import { EmbeddingProfileMapper } from '$lib/server/mappers/embedding-profile.mapper.js'; import { EmbeddingProfile, EmbeddingProfileEntity } from '$lib/server/models/embedding-profile.js'; import { EmbeddingSettings } from '$lib/server/models/embedding-settings.js'; import { InvalidInputError } from '$lib/server/utils/validation.js'; export class EmbeddingSettingsService { constructor(private readonly db: Database.Database) {} getSettings(): EmbeddingSettings { const profiles = this.loadProfiles(); const activeProfile = profiles.find((profile) => profile.isDefault && profile.enabled) ?? null; return new EmbeddingSettings({ profiles, activeProfile }); } async updateSettings(input: EmbeddingSettingsUpdateDto): Promise { const now = Math.floor(Date.now() / 1000); this.db.prepare('UPDATE embedding_profiles SET is_default = 0, updated_at = ?').run(now); if (input.activeProfileId === null) { return this.getSettings(); } const profile = input.activeProfileId === 'local-default' ? this.buildDefaultLocalProfile(now) : this.buildCustomProfile(input, now); const available = await createProviderFromProfile(profile).isAvailable(); if (!available) { throw new InvalidInputError( `Could not connect to the "${profile.providerKind}" provider. Check your configuration.` ); } this.persistProfile(profile); return this.getSettings(); } private loadProfiles(): EmbeddingProfile[] { return this.db .prepare('SELECT * FROM embedding_profiles ORDER BY is_default DESC, created_at ASC') .all() .map((row) => EmbeddingProfileMapper.fromEntity(new EmbeddingProfileEntity(row as never))); } private buildDefaultLocalProfile(now: number): EmbeddingProfile { const defaultLocal = getDefaultLocalProfile(); return new EmbeddingProfile({ id: defaultLocal.id, providerKind: defaultLocal.providerKind, title: defaultLocal.title, enabled: true, isDefault: true, model: defaultLocal.model, dimensions: defaultLocal.dimensions, config: {}, createdAt: this.getCreatedAt(defaultLocal.id, now), updatedAt: now }); } private buildCustomProfile(input: EmbeddingSettingsUpdateDto, now: number): EmbeddingProfile { const candidate = input.profile; if (!candidate) { throw new InvalidInputError('profile is required for custom embedding providers'); } if (candidate.id !== input.activeProfileId) { throw new InvalidInputError('activeProfileId must match profile.id'); } if (!candidate.title || !candidate.model) { throw new InvalidInputError('profile title and model are required'); } return new EmbeddingProfile({ id: candidate.id, providerKind: candidate.providerKind, title: candidate.title, enabled: true, isDefault: true, model: candidate.model, dimensions: candidate.dimensions, config: candidate.config, createdAt: this.getCreatedAt(candidate.id, now), updatedAt: now }); } private getCreatedAt(id: string, fallback: number): number { return ( this.db .prepare< [string], { created_at: number } >('SELECT created_at FROM embedding_profiles WHERE id = ?') .get(id)?.created_at ?? fallback ); } private persistProfile(profile: EmbeddingProfile): void { this.db .prepare( `INSERT INTO embedding_profiles (id, provider_kind, title, enabled, is_default, model, dimensions, config, created_at, updated_at) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ON CONFLICT(id) DO UPDATE SET provider_kind = excluded.provider_kind, title = excluded.title, enabled = excluded.enabled, is_default = excluded.is_default, model = excluded.model, dimensions = excluded.dimensions, config = excluded.config, updated_at = excluded.updated_at` ) .run( profile.id, profile.providerKind, profile.title, profile.enabled ? 1 : 0, profile.isDefault ? 1 : 0, profile.model, profile.dimensions, JSON.stringify(profile.config), profile.createdAt, profile.updatedAt ); } }