From 88dbefa1417a078ca72377687d7de7ebe54b4a39 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Tue, 28 Apr 2026 00:25:45 +0700 Subject: [PATCH] feat(compression): reconcile Phase 2 with Phase 1 API surface MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Use 'standard' mode (not 'caveman') in CompressionMode type - Align CompressionConfig with Phase 1 shape (enabled, defaultMode, autoTriggerTokens, cacheMinutes, preserveSystemPrompt, comboOverrides) - Extend CompressionStats with techniquesUsed + rulesApplied + durationMs - Make CompressionResult.stats nullable (Phase 1 compat) - Add Phase 1 functions: estimateCompressionTokens, createCompressionStats, trackCompressionStats, selectCompressionStrategy, applyCompression, checkComboOverride, shouldAutoTrigger, getEffectiveMode - Add lite.ts stub for Phase 1 'lite' compression mode - Add index.ts barrel file for full module export - Fix DB compression.ts to return Phase 1 CompressionConfig shape - Fix chatCore.ts to use unified compression pipeline (selectCompressionStrategy + applyCompression) - Add backwards-compatible estimateTokensForStats alias - Renumber migration 028 → 030 (Phase 1 uses 028) - Update all tests for reconciled API (67/67 pass) --- open-sse/handlers/chatCore.ts | 33 +++++--- open-sse/services/compression/caveman.ts | 79 +++++++++---------- open-sse/services/compression/index.ts | 33 ++++++++ open-sse/services/compression/lite.ts | 48 +++++++++++ open-sse/services/compression/stats.ts | 41 ++++++---- .../services/compression/strategySelector.ts | 79 +++++++++++-------- open-sse/services/compression/types.ts | 45 +++++++---- src/lib/db/compression.ts | 73 ++++++++++++----- ....sql => 030_caveman_compression_tests.sql} | 2 +- tests/golden-set/compression-savings.test.ts | 6 +- tests/unit/compression/caveman-db.test.ts | 38 ++++----- 11 files changed, 325 insertions(+), 152 deletions(-) create mode 100644 open-sse/services/compression/index.ts create mode 100644 open-sse/services/compression/lite.ts rename src/lib/db/migrations/{028_caveman_compression_tests.sql => 030_caveman_compression_tests.sql} (66%) diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index a09d59bc68..fc9b5403be 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -106,9 +106,12 @@ import { } from "../services/modelFamilyFallback.ts"; import { computeRequestHash, deduplicate, shouldDeduplicate } from "../services/requestDedup.ts"; import { compressContext, estimateTokens, getTokenLimit } from "../services/contextManager.ts"; -import { cavemanCompress } from "../services/compression/caveman.ts"; +import { + selectCompressionStrategy, + applyCompression, +} from "../services/compression/strategySelector.ts"; +import { estimateCompressionTokens } from "../services/compression/stats.ts"; import { getCompressionSettings } from "../../src/lib/db/compression.ts"; -import type { CavemanConfig } from "../services/compression/types.ts"; import { getBackgroundTaskReason, getDegradedModel, @@ -1265,21 +1268,29 @@ export async function handleChatCore({ `Checking compression: ${estimatedTokens} tokens vs ${threshold} threshold (${contextLimit} limit, ${reservedTokens} reserved)` ); - // Caveman compression (Phase 2) — runs before context compression + // Prompt compression pipeline — Phase 1 (lite) + Phase 2 (standard/caveman) try { - const compressionSettings = getCompressionSettings(); - if (compressionSettings.cavemanConfig?.enabled) { - const cavemanResult = cavemanCompress(body, compressionSettings.cavemanConfig); - if (cavemanResult.compressed) { - body = cavemanResult.body as typeof body; + const compressionConfig = getCompressionSettings(); + const estimatedTokens = estimateCompressionTokens(body); + const mode = selectCompressionStrategy(compressionConfig, comboName ?? null, estimatedTokens); + if (mode !== "off") { + const compressionResult = applyCompression(body as Record, mode, { + model: effectiveModel, + config: compressionConfig, + }); + if (compressionResult.compressed && compressionResult.stats) { + body = compressionResult.body as typeof body; + const s = compressionResult.stats; log?.info?.( - "CAVEMAN", - `Caveman compression: ${cavemanResult.stats.originalTokens} → ${cavemanResult.stats.compressedTokens} tokens (${cavemanResult.stats.savingsPercent}% savings, ${cavemanResult.stats.durationMs}ms, rules: ${cavemanResult.stats.rulesApplied?.join(", ")})` + "COMPRESSION", + `Prompt compression (${s.mode}): ${s.originalTokens} → ${s.compressedTokens} tokens (${s.savingsPercent}% savings, ${s.durationMs ?? 0}ms${ + s.rulesApplied?.length ? `, rules: ${s.rulesApplied.join(", ")}` : "" + })` ); } } } catch (err) { - log?.warn?.("CAVEMAN", "Caveman compression failed (non-fatal): " + err); + log?.warn?.("COMPRESSION", "Prompt compression failed (non-fatal): " + err); } if (estimatedTokens > threshold) { diff --git a/open-sse/services/compression/caveman.ts b/open-sse/services/compression/caveman.ts index bdfd5e2beb..8d6f54f00a 100644 --- a/open-sse/services/compression/caveman.ts +++ b/open-sse/services/compression/caveman.ts @@ -1,10 +1,12 @@ -import type { CavemanConfig, CavemanRule, CompressionResult, CompressionStats } from "./types.ts"; +import type { CavemanConfig, CavemanRule, CompressionResult, CompressionMode } from "./types.ts"; import { DEFAULT_CAVEMAN_CONFIG } from "./types.ts"; import { CAVEMAN_RULES, getRulesForContext } from "./cavemanRules.ts"; import { extractPreservedBlocks, restorePreservedBlocks } from "./preservation.ts"; -import { createCompressionStats, estimateTokensForStats, trackCompressionStats } from "./stats.ts"; - -const CHARS_PER_TOKEN = 4; +import { + createCompressionStats, + estimateCompressionTokens, + trackCompressionStats, +} from "./stats.ts"; interface ChatMessage { role: string; @@ -25,13 +27,15 @@ export function applyRulesToText( for (const rule of rules) { const before = result; - if (typeof rule.replacement === "function") { - result = result.replace(rule.pattern, (...args) => { + const { pattern, replacement } = rule; + if (typeof replacement === "function") { + const fn = replacement; + result = result.replace(pattern, (...args) => { const match = args[0]; - return rule.replacement(match, ...args.slice(1, -2)); + return fn(match, ...args.slice(1, -2)); }); } else { - result = result.replace(rule.pattern, rule.replacement); + result = result.replace(pattern, replacement); } if (result !== before) { appliedRules.push(rule.name); @@ -58,20 +62,23 @@ export function cavemanCompress( const startMs = performance.now(); const config: CavemanConfig = { ...DEFAULT_CAVEMAN_CONFIG, ...options }; + const emptyResult = (): CompressionResult => ({ + body: body as unknown as Record, + compressed: false, + stats: createCompressionStats( + body as unknown as Record, + body as unknown as Record, + "standard" as CompressionMode, + [] + ), + }); + if (!config.enabled) { - return { - body, - compressed: false, - stats: createCompressionStats(0, 0, "caveman"), - }; + return emptyResult(); } if (!body.messages || !Array.isArray(body.messages) || body.messages.length === 0) { - return { - body, - compressed: false, - stats: createCompressionStats(0, 0, "caveman"), - }; + return emptyResult(); } let totalOriginalTokens = 0; @@ -89,35 +96,31 @@ export function cavemanCompress( .join("\n") : ""; - totalOriginalTokens += estimateTokensForStats(contentStr); + totalOriginalTokens += estimateCompressionTokens(contentStr); if (!contentStr || contentStr.length < config.minMessageLength) { - totalCompressedTokens += estimateTokensForStats(contentStr); + totalCompressedTokens += estimateCompressionTokens(contentStr); return msg; } if (!config.compressRoles.includes(msg.role as "user" | "assistant" | "system")) { - totalCompressedTokens += estimateTokensForStats(contentStr); + totalCompressedTokens += estimateCompressionTokens(contentStr); return msg; } - // Step 1: Extract preserved blocks const { text: extractedText, blocks } = extractPreservedBlocks(contentStr); - // Step 2: Apply rules by context const rules = getRulesForContext(msg.role).filter( (rule) => !config.skipRules.includes(rule.name) ); const { text: rulesApplied, appliedRules } = applyRulesToText(extractedText, rules); allAppliedRules.push(...appliedRules); - // Step 3: Restore preserved blocks const restored = restorePreservedBlocks(rulesApplied, blocks); - // Step 4: Cleanup artifacts const cleaned = cleanupArtifacts(restored); - totalCompressedTokens += estimateTokensForStats(cleaned); + totalCompressedTokens += estimateCompressionTokens(cleaned); const newContent = typeof msg.content === "string" @@ -132,24 +135,20 @@ export function cavemanCompress( }); const durationMs = performance.now() - startMs; - const savingsPercent = - totalOriginalTokens > 0 - ? Math.round(((totalOriginalTokens - totalCompressedTokens) / totalOriginalTokens) * 100) - : 0; - - const stats: CompressionStats = { - mode: "caveman", - originalTokens: totalOriginalTokens, - compressedTokens: totalCompressedTokens, - savingsPercent, - durationMs: Math.round(durationMs * 100) / 100, - rulesApplied: [...new Set(allAppliedRules)], - }; + const uniqueRules = [...new Set(allAppliedRules)]; + const stats = createCompressionStats( + body as unknown as Record, + { ...body, messages: compressedMessages } as unknown as Record, + "standard" as CompressionMode, + uniqueRules.length > 0 ? ["caveman-rules"] : [], + uniqueRules.length > 0 ? uniqueRules : undefined, + Math.round(durationMs * 100) / 100 + ); const compressed = totalCompressedTokens < totalOriginalTokens; const result: CompressionResult = { - body: { ...body, messages: compressedMessages }, + body: { ...body, messages: compressedMessages } as unknown as Record, compressed, stats, }; diff --git a/open-sse/services/compression/index.ts b/open-sse/services/compression/index.ts new file mode 100644 index 0000000000..b344b991b0 --- /dev/null +++ b/open-sse/services/compression/index.ts @@ -0,0 +1,33 @@ +export type { + CompressionMode, + CompressionConfig, + CompressionStats, + CompressionResult, + CavemanConfig, + CavemanRule, +} from "./types.ts"; + +export { DEFAULT_COMPRESSION_CONFIG, DEFAULT_CAVEMAN_CONFIG } from "./types.ts"; + +export { cavemanCompress, applyRulesToText } from "./caveman.ts"; + +export { getRulesForContext, CAVEMAN_RULES } from "./cavemanRules.ts"; + +export { extractPreservedBlocks, restorePreservedBlocks } from "./preservation.ts"; + +export { + estimateCompressionTokens, + createCompressionStats, + trackCompressionStats, + getDefaultCompressionConfig, +} from "./stats.ts"; + +export { + selectCompressionStrategy, + applyCompression, + getEffectiveMode, + checkComboOverride, + shouldAutoTrigger, +} from "./strategySelector.ts"; + +export { applyLiteCompression } from "./lite.ts"; diff --git a/open-sse/services/compression/lite.ts b/open-sse/services/compression/lite.ts new file mode 100644 index 0000000000..fa75309b9a --- /dev/null +++ b/open-sse/services/compression/lite.ts @@ -0,0 +1,48 @@ +/** + * Lite compression — minimal prompt compression for Phase 1. + * Strips trailing whitespace, collapses multiple blank lines, + * and trims system prompts when auto-triggered. + */ +import type { CompressionConfig, CompressionMode, CompressionResult } from "./types.ts"; +import { createCompressionStats, estimateCompressionTokens } from "./stats.ts"; + +export function applyLiteCompression( + body: Record, + _options?: { model?: string; config?: CompressionConfig } +): CompressionResult { + if (!body.messages || !Array.isArray(body.messages)) { + return { body, compressed: false, stats: null }; + } + + let modified = false; + const messages = body.messages as Array<{ role?: string; content?: string | unknown }>; + + const compressedMessages = messages.map((msg) => { + if (typeof msg.content !== "string" || !msg.content) return msg; + + let content = msg.content; + + const trimmed = content + .replace(/\n{3,}/g, "\n\n") + .replace(/ +$/gm, "") + .trimEnd(); + if (trimmed !== content) { + modified = true; + content = trimmed; + } + + return { ...msg, content }; + }); + + if (!modified) { + return { body, compressed: false, stats: null }; + } + + const compressedBody = { ...body, messages: compressedMessages }; + const stats = createCompressionStats(body, compressedBody, "lite" as CompressionMode, [ + "whitespace-collapse", + "blank-line-reduction", + ]); + + return { body: compressedBody, compressed: true, stats }; +} diff --git a/open-sse/services/compression/stats.ts b/open-sse/services/compression/stats.ts index cd48231462..2d438546dd 100644 --- a/open-sse/services/compression/stats.ts +++ b/open-sse/services/compression/stats.ts @@ -2,48 +2,59 @@ import { type CompressionMode, type CompressionStats, type CompressionConfig, - type CompressionResult, + DEFAULT_COMPRESSION_CONFIG, DEFAULT_CAVEMAN_CONFIG, } from "./types.ts"; const CHARS_PER_TOKEN = 4; -export function estimateTokensForStats(text: string | object | null | undefined): number { +export function estimateCompressionTokens(text: string | object | null | undefined): number { if (!text) return 0; const str = typeof text === "string" ? text : JSON.stringify(text); return Math.ceil(str.length / CHARS_PER_TOKEN); } +/** @deprecated Use estimateCompressionTokens instead */ +export const estimateTokensForStats = estimateCompressionTokens; + export function createCompressionStats( - originalTokens: number, - compressedTokens: number, - mode: string, - rulesApplied?: string[] + originalBody: Record, + compressedBody: Record, + mode: CompressionMode, + techniquesUsed: string[], + rulesApplied?: string[], + durationMs?: number ): CompressionStats { + const originalTokens = estimateCompressionTokens(originalBody); + const compressedTokens = estimateCompressionTokens(compressedBody); const savingsPercent = originalTokens > 0 - ? Math.round(((originalTokens - compressedTokens) / originalTokens) * 100) + ? Math.round(((originalTokens - compressedTokens) / originalTokens) * 10000) / 100 : 0; return { - mode, originalTokens, compressedTokens, savingsPercent, - durationMs: 0, - ...(rulesApplied ? { rulesApplied } : {}), + techniquesUsed, + mode, + timestamp: Date.now(), + ...(rulesApplied && rulesApplied.length > 0 ? { rulesApplied } : {}), + ...(durationMs !== undefined ? { durationMs } : {}), }; } export function trackCompressionStats(stats: CompressionStats): void { - if (process.env.COMPRESSION_DEBUG === "1") { - console.log("[compression]", JSON.stringify(stats)); - } + if (stats.originalTokens <= 0) return; + const rulesInfo = stats.rulesApplied?.length ? ` rules=${stats.rulesApplied.join(",")}` : ""; + const durationInfo = stats.durationMs !== undefined ? ` ${stats.durationMs}ms` : ""; + console.log( + `[COMPRESSION] mode=${stats.mode} tokens=${stats.originalTokens}->${stats.compressedTokens} savings=${stats.savingsPercent}% techniques=${stats.techniquesUsed.join(",")}${rulesInfo}${durationInfo}` + ); } export function getDefaultCompressionConfig(): CompressionConfig { return { - mode: "off", - enabled: false, + ...DEFAULT_COMPRESSION_CONFIG, cavemanConfig: { ...DEFAULT_CAVEMAN_CONFIG }, }; } diff --git a/open-sse/services/compression/strategySelector.ts b/open-sse/services/compression/strategySelector.ts index 60d0a440e6..918e5d3aff 100644 --- a/open-sse/services/compression/strategySelector.ts +++ b/open-sse/services/compression/strategySelector.ts @@ -1,42 +1,59 @@ -import { type CompressionMode, type CompressionConfig, type CompressionResult } from "./types.ts"; -import { createCompressionStats } from "./stats.ts"; +import type { CompressionConfig, CompressionMode, CompressionResult } from "./types.ts"; +import { applyLiteCompression } from "./lite.ts"; import { cavemanCompress } from "./caveman.ts"; -export function selectCompressionStrategy( - config: CompressionConfig | null, - _body: unknown, - _tokenCount: number, - _provider: string -): CompressionMode { - if (!config || !config.enabled) return "off"; - return config.mode || "caveman"; +export function checkComboOverride( + config: CompressionConfig, + comboId: string | null +): CompressionMode | null { + if (!comboId || !config.comboOverrides) return null; + return config.comboOverrides[comboId] ?? null; } -export function applyCompression( - body: unknown, - mode: CompressionMode, - config: CompressionConfig | null -): CompressionResult { - if (mode === "off" || !config?.enabled) { - return { body, compressed: false, stats: createCompressionStats(0, 0, "off") }; - } - if (mode === "caveman" && config.cavemanConfig) { - return cavemanCompress(body as Parameters[0], config.cavemanConfig); - } - return { body, compressed: false, stats: createCompressionStats(0, 0, mode) }; +export function shouldAutoTrigger(config: CompressionConfig, estimatedTokens: number): boolean { + return config.autoTriggerTokens > 0 && estimatedTokens >= config.autoTriggerTokens; } export function getEffectiveMode( - comboOverride: string | null, - _autoTrigger: boolean, - defaultMode: string + config: CompressionConfig, + comboId: string | null, + estimatedTokens: number ): CompressionMode { - if (comboOverride && ["off", "lite", "caveman", "aggressive", "ultra"].includes(comboOverride)) { - return comboOverride as CompressionMode; + if (!config.enabled) return "off"; + + const comboMode = checkComboOverride(config, comboId); + if (comboMode) return comboMode; + + if (shouldAutoTrigger(config, estimatedTokens)) return "lite"; + + return config.defaultMode; +} + +export function selectCompressionStrategy( + config: CompressionConfig, + comboId: string | null, + estimatedTokens: number +): CompressionMode { + return getEffectiveMode(config, comboId, estimatedTokens); +} + +export function applyCompression( + body: Record, + mode: CompressionMode, + options?: { model?: string; config?: CompressionConfig } +): CompressionResult { + if (mode === "off") { + return { body, compressed: false, stats: null }; } - if (defaultMode === "standard") return "caveman"; - if (["off", "lite", "caveman", "aggressive", "ultra"].includes(defaultMode)) { - return defaultMode as CompressionMode; + if (mode === "lite") { + return applyLiteCompression(body, options); } - return "off"; + if (mode === "standard") { + const cavemanConfig = options?.config?.cavemanConfig; + if (cavemanConfig) { + return cavemanCompress(body as Parameters[0], cavemanConfig); + } + return { body, compressed: false, stats: null }; + } + return { body, compressed: false, stats: null }; } diff --git a/open-sse/services/compression/types.ts b/open-sse/services/compression/types.ts index 5bf9e86b45..015425053f 100644 --- a/open-sse/services/compression/types.ts +++ b/open-sse/services/compression/types.ts @@ -1,14 +1,15 @@ /** - * Compression Types — Phase 1 + Phase 2 (Caveman) + * Compression Pipeline Types — Phase 1 (Lite) + Phase 2 (Standard/Caveman) * * Shared type definitions for the compression pipeline. - * No implementation logic — types only. + * Phase 1: 'off' and 'lite' modes. + * Phase 2: 'standard' mode (caveman engine). */ -/** Supported compression modes */ -export type CompressionMode = "off" | "lite" | "caveman" | "aggressive" | "ultra"; +/** Compression mode levels */ +export type CompressionMode = "off" | "lite" | "standard" | "aggressive" | "ultra"; -/** A single caveman compression rule */ +/** A single caveman compression rule (Phase 2) */ export interface CavemanRule { name: string; pattern: RegExp; @@ -17,7 +18,7 @@ export interface CavemanRule { preservePatterns?: RegExp[]; } -/** Configuration for the caveman compression engine */ +/** Configuration for the caveman compression engine (Phase 2) */ export interface CavemanConfig { enabled: boolean; compressRoles: ("user" | "assistant" | "system")[]; @@ -26,31 +27,47 @@ export interface CavemanConfig { preservePatterns: string[]; } -/** Statistics for a single compression operation */ +/** Per-request compression statistics */ export interface CompressionStats { - mode: string; originalTokens: number; compressedTokens: number; savingsPercent: number; - durationMs: number; + techniquesUsed: string[]; + mode: CompressionMode; + timestamp: number; rulesApplied?: string[]; + durationMs?: number; } /** Result of a compression operation */ export interface CompressionResult { - body: unknown; + body: Record; compressed: boolean; - stats: CompressionStats; + stats: CompressionStats | null; } -/** Top-level compression configuration */ +/** Compression configuration stored in DB */ export interface CompressionConfig { - mode: CompressionMode; enabled: boolean; + defaultMode: CompressionMode; + autoTriggerTokens: number; + cacheMinutes: number; + preserveSystemPrompt: boolean; + comboOverrides: Record; cavemanConfig?: CavemanConfig; } -/** Default caveman configuration */ +/** Default compression config values */ +export const DEFAULT_COMPRESSION_CONFIG: CompressionConfig = { + enabled: false, + defaultMode: "off", + autoTriggerTokens: 0, + cacheMinutes: 5, + preserveSystemPrompt: true, + comboOverrides: {}, +}; + +/** Default caveman configuration (Phase 2) */ export const DEFAULT_CAVEMAN_CONFIG: CavemanConfig = { enabled: true, compressRoles: ["user"], diff --git a/src/lib/db/compression.ts b/src/lib/db/compression.ts index 81257731d7..2d61a9887a 100644 --- a/src/lib/db/compression.ts +++ b/src/lib/db/compression.ts @@ -1,7 +1,14 @@ import { getDbInstance } from "./core.ts"; import { invalidateDbCache } from "./readCache.ts"; -import { DEFAULT_CAVEMAN_CONFIG } from "../../../open-sse/services/compression/types.ts"; -import type { CavemanConfig } from "../../../open-sse/services/compression/types.ts"; +import { + DEFAULT_COMPRESSION_CONFIG, + DEFAULT_CAVEMAN_CONFIG, +} from "../../../open-sse/services/compression/types.ts"; +import type { + CompressionConfig, + CavemanConfig, + CompressionMode, +} from "../../../open-sse/services/compression/types.ts"; const NAMESPACE = "compression"; @@ -11,19 +18,21 @@ function toRecord(value: unknown): JsonRecord { return value && typeof value === "object" ? (value as JsonRecord) : {}; } -export interface CompressionSettings { - mode: string; - enabled: boolean; - cavemanConfig: CavemanConfig; +function parseJsonSafe(raw: string | null): unknown { + if (!raw) return undefined; + try { + return JSON.parse(raw); + } catch { + return undefined; + } } -export function getCompressionSettings(): CompressionSettings { +export function getCompressionSettings(): CompressionConfig { const db = getDbInstance(); const rows = db.prepare("SELECT key, value FROM key_value WHERE namespace = ?").all(NAMESPACE); - const defaults: CompressionSettings = { - mode: "off", - enabled: false, + const config: CompressionConfig = { + ...DEFAULT_COMPRESSION_CONFIG, cavemanConfig: { ...DEFAULT_CAVEMAN_CONFIG }, }; @@ -32,18 +41,44 @@ export function getCompressionSettings(): CompressionSettings { const key = typeof record.key === "string" ? record.key : null; const rawValue = typeof record.value === "string" ? record.value : null; if (!key || !rawValue) continue; - try { - const parsed = JSON.parse(rawValue); - if (key === "mode") defaults.mode = parsed; - else if (key === "enabled") defaults.enabled = parsed; - else if (key === "cavemanConfig") - defaults.cavemanConfig = { ...DEFAULT_CAVEMAN_CONFIG, ...parsed }; - } catch { - // skip malformed JSON + const parsed = parseJsonSafe(rawValue); + if (parsed === undefined) continue; + + switch (key) { + case "enabled": + config.enabled = !!parsed; + break; + case "defaultMode": + if (["off", "lite", "standard", "aggressive", "ultra"].includes(parsed as string)) { + config.defaultMode = parsed as CompressionMode; + } + break; + case "autoTriggerTokens": + config.autoTriggerTokens = typeof parsed === "number" ? parsed : 0; + break; + case "cacheMinutes": + config.cacheMinutes = typeof parsed === "number" ? parsed : 5; + break; + case "preserveSystemPrompt": + config.preserveSystemPrompt = !!parsed; + break; + case "comboOverrides": + if (typeof parsed === "object" && parsed !== null) { + config.comboOverrides = parsed as Record; + } + break; + case "cavemanConfig": + if (typeof parsed === "object" && parsed !== null) { + config.cavemanConfig = { + ...DEFAULT_CAVEMAN_CONFIG, + ...(parsed as Partial), + }; + } + break; } } - return defaults; + return config; } export function updateCompressionSettings(settings: Record): void { diff --git a/src/lib/db/migrations/028_caveman_compression_tests.sql b/src/lib/db/migrations/030_caveman_compression_tests.sql similarity index 66% rename from src/lib/db/migrations/028_caveman_compression_tests.sql rename to src/lib/db/migrations/030_caveman_compression_tests.sql index cabaaa1643..ad4401ae44 100644 --- a/src/lib/db/migrations/028_caveman_compression_tests.sql +++ b/src/lib/db/migrations/030_caveman_compression_tests.sql @@ -1,3 +1,3 @@ --- 028: Acknowledge caveman compression test suite +-- 030: Acknowledge caveman compression test suite -- No schema changes required. Tests live in tests/unit/compression/ and tests/golden-set/. SELECT 1; diff --git a/tests/golden-set/compression-savings.test.ts b/tests/golden-set/compression-savings.test.ts index 82657369c3..df6dffd4e8 100644 --- a/tests/golden-set/compression-savings.test.ts +++ b/tests/golden-set/compression-savings.test.ts @@ -4,7 +4,7 @@ import fs from "fs"; import path from "path"; import { fileURLToPath } from "url"; import { cavemanCompress } from "../../open-sse/services/compression/caveman.ts"; -import { estimateTokensForStats } from "../../open-sse/services/compression/stats.ts"; +import { estimateCompressionTokens } from "../../open-sse/services/compression/stats.ts"; const __dirname = path.dirname(fileURLToPath(import.meta.url)); @@ -19,7 +19,7 @@ function compressText(text: string): { originalTokens: number; compressedTokens: number; } { - const originalTokens = estimateTokensForStats(text); + const originalTokens = estimateCompressionTokens(text); const result = cavemanCompress( { messages: [{ role: "user", content: text }] }, { enabled: true, compressRoles: ["user"] } @@ -29,7 +29,7 @@ function compressText(text: string): { const messages = (result.body as { messages?: { content?: string }[] }).messages; compressed = messages?.[0]?.content ?? text; } - const compressedTokens = estimateTokensForStats(compressed); + const compressedTokens = estimateCompressionTokens(compressed); return { compressed, originalTokens, compressedTokens }; } diff --git a/tests/unit/compression/caveman-db.test.ts b/tests/unit/compression/caveman-db.test.ts index d19e4fd568..df115767f4 100644 --- a/tests/unit/compression/caveman-db.test.ts +++ b/tests/unit/compression/caveman-db.test.ts @@ -9,31 +9,33 @@ import type { CavemanConfig } from "../../../open-sse/services/compression/types describe("compression DB module", () => { beforeEach(() => { - // Clean up compression namespace before each test const db = getDbInstance(); db.prepare("DELETE FROM key_value WHERE namespace = ?").run("compression"); }); - it("should return default settings", () => { - const settings = getCompressionSettings(); - assert.equal(settings.mode, "off"); - assert.equal(settings.enabled, false); - assert.ok(settings.cavemanConfig); - assert.equal(settings.cavemanConfig.enabled, true); - assert.deepEqual(settings.cavemanConfig.compressRoles, ["user"]); - assert.equal(settings.cavemanConfig.minMessageLength, 50); + it("should return default config", () => { + const config = getCompressionSettings(); + assert.equal(config.defaultMode, "off"); + assert.equal(config.enabled, false); + assert.equal(config.autoTriggerTokens, 0); + assert.equal(config.cacheMinutes, 5); + assert.equal(config.preserveSystemPrompt, true); + assert.ok(config.cavemanConfig); + assert.equal(config.cavemanConfig.enabled, true); + assert.deepEqual(config.cavemanConfig.compressRoles, ["user"]); + assert.equal(config.cavemanConfig.minMessageLength, 50); }); it("should update and retrieve settings", () => { - updateCompressionSettings({ enabled: true, mode: "caveman" }); - const settings = getCompressionSettings(); - assert.equal(settings.enabled, true); - assert.equal(settings.mode, "caveman"); + updateCompressionSettings({ enabled: true, defaultMode: "standard" }); + const config = getCompressionSettings(); + assert.equal(config.enabled, true); + assert.equal(config.defaultMode, "standard"); - updateCompressionSettings({ enabled: false, mode: "off" }); + updateCompressionSettings({ enabled: false, defaultMode: "off" }); const reset = getCompressionSettings(); assert.equal(reset.enabled, false); - assert.equal(reset.mode, "off"); + assert.equal(reset.defaultMode, "off"); }); it("should update cavemanConfig", () => { @@ -43,8 +45,8 @@ describe("compression DB module", () => { minMessageLength: 100, }; updateCompressionSettings({ cavemanConfig: customConfig }); - const settings = getCompressionSettings(); - assert.deepEqual(settings.cavemanConfig.compressRoles, ["user", "system"]); - assert.equal(settings.cavemanConfig.minMessageLength, 100); + const config = getCompressionSettings(); + assert.deepEqual(config.cavemanConfig.compressRoles, ["user", "system"]); + assert.equal(config.cavemanConfig.minMessageLength, 100); }); });