import { ServerSettings, type ServerSettingsPatch } from "@t3tools/contracts"; import * as Option from "effect/Option"; import * as Schema from "effect/Schema"; import { deepMerge } from "./Struct.ts"; import { fromLenientJson } from "./schemaJson.ts"; import { createModelSelection } from "./model.ts"; import { getBackgroundActivityBaseProfile, normalizeBackgroundActivitySettings, resolveBackgroundActivitySettings, } from "./backgroundActivitySettings.ts"; const ServerSettingsJson = fromLenientJson(ServerSettings); const decodeServerSettingsJson = Schema.decodeUnknownOption(ServerSettingsJson); export interface PersistedServerObservabilitySettings { readonly otlpTracesUrl: string | undefined; readonly otlpMetricsUrl: string | undefined; } export function normalizePersistedServerSettingString( value: string | null | undefined, ): string | undefined { const trimmed = value?.trim(); return trimmed && trimmed.length > 0 ? trimmed : undefined; } export function extractPersistedServerObservabilitySettings(input: { readonly observability?: { readonly otlpTracesUrl?: string; readonly otlpMetricsUrl?: string; }; }): PersistedServerObservabilitySettings { return { otlpTracesUrl: normalizePersistedServerSettingString(input.observability?.otlpTracesUrl), otlpMetricsUrl: normalizePersistedServerSettingString(input.observability?.otlpMetricsUrl), }; } export function parsePersistedServerObservabilitySettings( raw: string, ): PersistedServerObservabilitySettings { const decoded = decodeServerSettingsJson(raw); if (Option.isSome(decoded)) { return extractPersistedServerObservabilitySettings(decoded.value); } return { otlpTracesUrl: undefined, otlpMetricsUrl: undefined }; } function shouldReplaceTextGenerationModelSelection( patch: ServerSettingsPatch["textGenerationModelSelection"] | undefined, ): boolean { return Boolean(patch && (patch.instanceId !== undefined || patch.model !== undefined)); } function mergeModelSelectionOptionsById(input: { current: ReadonlyArray<{ readonly id: string; readonly value: string | boolean }> | undefined; patch: ReadonlyArray<{ readonly id: string; readonly value: string | boolean }> | undefined; }): Array<{ id: string; value: string | boolean }> | undefined { if (input.patch === undefined) { return input.current ? [...input.current] : undefined; } if (input.patch.length === 0) { return undefined; } const merged = new Map((input.current ?? []).map((selection) => [selection.id, selection.value])); for (const selection of input.patch) { merged.set(selection.id, selection.value); } return [...merged.entries()].map(([id, value]) => ({ id, value })); } /** * Applies a server settings patch while treating textGenerationModelSelection as * replace-on-provider/model updates. This prevents stale nested options from * surviving a reset patch that intentionally omits options. */ export function applyServerSettingsPatch( current: ServerSettings, patch: ServerSettingsPatch, ): ServerSettings { const selectionPatch = patch.textGenerationModelSelection; const { automaticGitFetchInterval, providerHealthRefreshInterval, backgroundActivityProfile, backgroundActivity, ...patchForMerge } = patch; const backgroundActivityPatch = backgroundActivityProfile !== undefined ? { schemaVersion: 1 as const, profile: backgroundActivityProfile, overrides: {}, } : automaticGitFetchInterval !== undefined || providerHealthRefreshInterval !== undefined ? { schemaVersion: 1 as const, profile: "custom" as const, baseProfile: getBackgroundActivityBaseProfile(current.backgroundActivity), overrides: { ...current.backgroundActivity.overrides, ...(automaticGitFetchInterval !== undefined ? { automaticGitFetchInterval } : {}), ...(providerHealthRefreshInterval !== undefined ? { providerHealthRefreshInterval } : {}), }, } : undefined; const next = deepMerge(current, patchForMerge); const nextWithReplacementsBase = { ...next, ...(backgroundActivity !== undefined ? { backgroundActivity: deepMerge(current.backgroundActivity, backgroundActivity) } : {}), ...(backgroundActivityPatch !== undefined ? { backgroundActivity: backgroundActivityPatch } : {}), ...(patch.providerInstances !== undefined ? { providerInstances: patch.providerInstances } : {}), ...(automaticGitFetchInterval !== undefined ? { automaticGitFetchInterval } : {}), ...(providerHealthRefreshInterval !== undefined ? { providerHealthRefreshInterval } : {}), }; const normalizedBackgroundActivity = normalizeBackgroundActivitySettings( nextWithReplacementsBase.backgroundActivity, ); const resolvedBackgroundActivity = resolveBackgroundActivitySettings( normalizedBackgroundActivity, ); const nextWithReplacements = { ...nextWithReplacementsBase, backgroundActivity: normalizedBackgroundActivity, automaticGitFetchInterval: resolvedBackgroundActivity.automaticGitFetchInterval, providerHealthRefreshInterval: resolvedBackgroundActivity.providerHealthRefreshInterval, backgroundActivityProfile: resolvedBackgroundActivity.profile, }; if (!selectionPatch) { return nextWithReplacements; } const instanceId = selectionPatch.instanceId ?? current.textGenerationModelSelection.instanceId; const model = selectionPatch.model ?? current.textGenerationModelSelection.model; const options = shouldReplaceTextGenerationModelSelection(selectionPatch) ? selectionPatch.options : mergeModelSelectionOptionsById({ current: current.textGenerationModelSelection.options, patch: selectionPatch.options, }); return { ...nextWithReplacements, textGenerationModelSelection: createModelSelection(instanceId, model, options), }; }