/ src / server / storage / settings / read.ts
read.ts
  1  import type {
  2    GlobalDefaults,
  3    ProviderProfileId,
  4  } from '@/lib/shared/chat'
  5  import { getDb } from '@/server/storage/db'
  6  import {
  7    normalizeAgentBackendsEnabled,
  8    normalizeNullableString,
  9    normalizeProviderEndpointMode,
 10    normalizeReasoningLevel,
 11    normalizeRetentionDays,
 12    normalizeThinkingLevel,
 13    normalizeWebSearchProvider,
 14  } from './validate'
 15  
 16  interface AppSettingsRow {
 17    id: number
 18    default_chat_model_id: string | null
 19    default_embedding_model_id: string | null
 20    default_transcription_model_id: string | null
 21    default_embedding_provider_id: string | null
 22    default_transcription_provider_id: string | null
 23    default_provider_id: string | null
 24    default_provider_endpoint_mode: string | null
 25    default_thinking_level: string | null
 26    default_reasoning_level: string | null
 27    web_search_provider: string | null
 28    web_search_max_results: number | null
 29    web_fetch_max_bytes: number | null
 30    agent_backends_enabled_json: string | null
 31    tool_call_log_retention_days: number | null
 32    updated_at: string | null
 33  }
 34  
 35  export function ensureAppSettingsRow(): void {
 36    const db = getDb()
 37    const existing = db
 38      .prepare('SELECT id FROM app_settings WHERE id = 1')
 39      .get() as { id: number } | undefined
 40    if (existing) return
 41  
 42    db.prepare(
 43      `INSERT INTO app_settings (
 44        id,
 45        default_chat_model_id,
 46        default_embedding_model_id,
 47        default_transcription_model_id,
 48        default_embedding_provider_id,
 49        default_transcription_provider_id,
 50        default_provider_id,
 51        default_provider_endpoint_mode,
 52        default_thinking_level,
 53        default_reasoning_level,
 54        web_search_provider,
 55        web_search_max_results,
 56        web_fetch_max_bytes,
 57        agent_backends_enabled_json,
 58        tool_call_log_retention_days,
 59        updated_at
 60      ) VALUES (1, NULL, NULL, NULL, NULL, NULL, NULL, 'auto', NULL, NULL, NULL, NULL, NULL, '{}', 30, ?)`,
 61    ).run(new Date().toISOString())
 62  }
 63  
 64  export function getAppSettings(): GlobalDefaults {
 65    const db = getDb()
 66    ensureAppSettingsRow()
 67  
 68    const row = db
 69      .prepare(
 70        `SELECT
 71          id,
 72          default_chat_model_id,
 73          default_embedding_model_id,
 74          default_transcription_model_id,
 75          default_embedding_provider_id,
 76          default_transcription_provider_id,
 77          default_provider_id,
 78          default_provider_endpoint_mode,
 79          default_thinking_level,
 80          default_reasoning_level,
 81          web_search_provider,
 82          web_search_max_results,
 83          web_fetch_max_bytes,
 84          agent_backends_enabled_json,
 85          tool_call_log_retention_days,
 86          updated_at
 87         FROM app_settings
 88         WHERE id = 1`,
 89      )
 90      .get() as AppSettingsRow | undefined
 91  
 92    return {
 93      chatModel: normalizeNullableString(row?.default_chat_model_id),
 94      embeddingModel: normalizeNullableString(row?.default_embedding_model_id),
 95      transcriptionModel: normalizeNullableString(
 96        row?.default_transcription_model_id,
 97      ),
 98      embeddingProviderId: normalizeNullableString(
 99        row?.default_embedding_provider_id,
100      ),
101      transcriptionProviderId: normalizeNullableString(
102        row?.default_transcription_provider_id,
103      ),
104      providerModelDefaults: readProviderModelDefaults(),
105      thinkingLevel: normalizeThinkingLevel(row?.default_thinking_level),
106      reasoningLevel: normalizeReasoningLevel(row?.default_reasoning_level),
107      defaultProviderId: normalizeNullableString(row?.default_provider_id),
108      providerEndpointMode: normalizeProviderEndpointMode(
109        row?.default_provider_endpoint_mode,
110      ),
111      webSearchProvider: normalizeWebSearchProvider(row?.web_search_provider),
112      webSearchMaxResults: row?.web_search_max_results ?? null,
113      webFetchMaxBytes: row?.web_fetch_max_bytes ?? null,
114      agentBackendsEnabled: normalizeAgentBackendsEnabled(
115        row?.agent_backends_enabled_json,
116      ),
117      toolCallLogRetentionDays: normalizeRetentionDays(
118        row?.tool_call_log_retention_days,
119      ),
120      updatedAt: normalizeNullableString(row?.updated_at),
121    }
122  }
123  
124  function readProviderModelDefaults(): Partial<Record<ProviderProfileId, string>> {
125    const db = getDb()
126    const rows = db
127      .prepare(
128        `SELECT
129           pp.id AS provider_id,
130           pm.provider_model_ref AS default_model_ref
131         FROM provider_profiles pp
132         LEFT JOIN provider_models pm
133           ON pm.provider_id = pp.id
134          AND pm.model_id = pp.default_model_id`,
135      )
136      .all() as Array<{ provider_id: string; default_model_ref: string | null }>
137  
138    const next: Partial<Record<ProviderProfileId, string>> = {}
139    for (const row of rows) {
140      const providerId = row.provider_id.trim()
141      const modelRef = normalizeNullableString(row.default_model_ref)
142      if (!providerId || !modelRef) continue
143      next[providerId] = modelRef
144    }
145    return next
146  }
147  
148  export function resolveDefaultProviderProfileId(
149    defaults: GlobalDefaults,
150  ): ProviderProfileId {
151    if (defaults.defaultProviderId) return defaults.defaultProviderId
152    return 'openai'
153  }
154  
155  export function resolveProviderScopedDefaultChatModel(
156    defaults: GlobalDefaults,
157    profileId: ProviderProfileId,
158  ): string | null {
159    return normalizeNullableString(
160      defaults.providerModelDefaults[profileId] ?? null,
161    )
162  }
163  
164  export function resolveDefaultChatModelForProfile(
165    defaults: GlobalDefaults,
166    profileId: ProviderProfileId,
167  ): string | null {
168    return resolveProviderScopedDefaultChatModel(defaults, profileId)
169  }