From dbdc7daade67e79d9bbe57b284ff584c81f1703c Mon Sep 17 00:00:00 2001 From: Diego Rodrigues de Sa e Souza <8016841+diegosouzapw@users.noreply.github.com> Date: Wed, 22 Jul 2026 16:15:57 -0300 Subject: [PATCH] feat(compression): per-model/endpoint compression exclusion filter (#8034) (#8064) * feat(compression): per-model/endpoint compression exclusion filter (#8034) * chore(quality): rebaseline compression.ts own-growth 845->850 (#8034 exclusions persistence) --------- Co-authored-by: Probe Test --- .../features/8034-compression-exclusions.md | 1 + config/quality/file-size-baseline.json | 7 +- docs/compression/COMPRESSION_ENGINES.md | 32 ++++ open-sse/handlers/chatCore.ts | 35 +++- open-sse/services/compression/exclusions.ts | 73 +++++++++ open-sse/services/compression/types.ts | 7 + .../exclusions/ExclusionsPanel.tsx | 105 ++++++++++++ .../dashboard/compression/exclusions/page.tsx | 11 ++ src/i18n/messages/en.json | 9 ++ src/i18n/messages/pt-BR.json | 9 ++ src/i18n/messages/vi.json | 9 ++ src/lib/db/compression.ts | 5 + .../constants/sidebarVisibility/sections.ts | 9 ++ .../constants/sidebarVisibility/types.ts | 1 + .../validation/compressionConfigSchemas.ts | 4 + tests/unit/compression-exclusions.test.ts | 126 +++++++++++++++ .../ui/compression-exclusions-page.test.tsx | 153 ++++++++++++++++++ 17 files changed, 593 insertions(+), 3 deletions(-) create mode 100644 changelog.d/features/8034-compression-exclusions.md create mode 100644 open-sse/services/compression/exclusions.ts create mode 100644 src/app/(dashboard)/dashboard/compression/exclusions/ExclusionsPanel.tsx create mode 100644 src/app/(dashboard)/dashboard/compression/exclusions/page.tsx create mode 100644 tests/unit/compression-exclusions.test.ts create mode 100644 tests/unit/ui/compression-exclusions-page.test.tsx diff --git a/changelog.d/features/8034-compression-exclusions.md b/changelog.d/features/8034-compression-exclusions.md new file mode 100644 index 0000000000..fb2a9e670a --- /dev/null +++ b/changelog.d/features/8034-compression-exclusions.md @@ -0,0 +1 @@ +- Add a per-model/endpoint compression exclusion filter: name model ids or `provider/model` patterns (`*` wildcard) that must never be compressed, checked before any engine runs so the request passes through byte-identical; configurable from a new Dashboard → Compression → Exclusions tab (#8034) diff --git a/config/quality/file-size-baseline.json b/config/quality/file-size-baseline.json index 9ba0f177d4..4ef7c67b34 100644 --- a/config/quality/file-size-baseline.json +++ b/config/quality/file-size-baseline.json @@ -183,7 +183,8 @@ "open-sse/handlers/sseParser.ts": 830, "open-sse/handlers/videoGeneration.ts": 1275, "_rebaseline_2026_07_22_8010_codex_responses_engine": "PR #8010 (@JxnLexn) own growth: open-sse/mcp-server/schemas/tools.ts 1497->1505 (+8 = threading the new \"codex-responses\" literal into the compressionConfigureInput strategy/autoTriggerMode Zod enums and setCompressionEngineInput engine enum, mirroring the existing rtk/omniglyph enum entries; no new tool). open-sse/services/compression/strategySelector.ts 1043->1054 (+11 = one new `if (mode === \"codex-responses\")` dispatch branch in runCompression that delegates 100% to the new codexResponsesEngine.apply, mirroring the existing rtk single-mode dispatch, plus threading config.codexResponsesConfig.preserveToolNames into the shared adaptBodyForCompression call at the 3 existing call sites). src/lib/db/compression.ts (untracked, new-file cap 800) 794->845 (+51 = normalizeCodexResponsesConfig, mirroring the existing normalizeRtkConfig normalizer, plus registering \"codex-responses\" in the COMPRESSION_MODES/STACKED_PIPELINE_ENGINE_IDS/SINGLE_MODE_ENGINE sets and the getCompressionSettings load/save switch) — added to the baseline at its current size. All three are cohesive dispatch/normalizer wiring at existing chokepoints (mirroring the prior compression-mode rebaselines #6534/#6556), not extractable without hiding the mode-dispatch boundary. Covered by tests/unit/compression/codex-responses.test.ts (6) + omniglyph-registries.test.ts/types.test.ts (22, updated for the new mode).", - "src/lib/db/compression.ts": 845, + "_rebaseline_2026_07_22_8034_compression_exclusions_persistence": "#8034 (compression exclusions) own growth: src/lib/db/compression.ts 845->850 (+5 = threading the new compressionExclusions field through the existing getCompressionSettings/saveCompressionSettings load/save switch over the shared key_value compression namespace — no new table, no raw SQL). Mirrors the prior compression-field rebaselines (#8010 codex-responses normalizer at the same chokepoint); the load/save switch is a single dispatch boundary, not extractable without hiding it. Covered by the PR's 8 node:test + 3 vitest cases.", + "src/lib/db/compression.ts": 850, "open-sse/mcp-server/schemas/tools.ts": 1505, "open-sse/mcp-server/server.ts": 1555, "open-sse/mcp-server/tools/advancedTools.ts": 1120, @@ -310,7 +311,9 @@ "_rebaseline_2026_07_19_7787_ic2_localdb_reexports": "PR #7787 (IC2 raw connections cache + lazy-decrypt) own growth: localDb.ts 805->807 (gate units, +2). localDb.ts is the re-export-only layer (hard rule #2 — no logic); the PR adds 4 new db/readCache re-exports (touchConnectionLastUsed, getCachedRawProviderConnections, getCachedProviderConnectionById, getCachedProviderNodes) required by existing barrel importers. Irreducible for a re-export list; frozen so it can only shrink.", "_rebaseline_2026_07_20_7819_autocandidateoverrides_reexport": "PR for #7819 (Level 1+2: read-only auto/* candidate transparency + per-API-key exclusions) own growth: localDb.ts 807->808 (+1). Adds a single `export * from \"./db/autoCandidateOverrides\"` barrel re-export (hard rule #2 — no logic) for the new DB module backing per-apiKey candidate exclusions. Irreducible for a re-export list; frozen so it can only shrink.", "src/lib/localDb.ts": 808, - "_rebaseline_2026_07_12_v3847_mergeprs_tail": "v3.8.47 /merge-prs tail (owner-approved): src/lib/localDb.ts NEW>800 (799->805, +6 re-exports countFreeProxies + recordFreeProxySyncErrors/clearFreeProxySyncErrors/getFreeProxySyncErrors + FreeProxySyncErrors type for #6909 free-pool relay-repair; re-export-only per Hard Rule #2, not extractable)." + "_rebaseline_2026_07_12_v3847_mergeprs_tail": "v3.8.47 /merge-prs tail (owner-approved): src/lib/localDb.ts NEW>800 (799->805, +6 re-exports countFreeProxies + recordFreeProxySyncErrors/clearFreeProxySyncErrors/getFreeProxySyncErrors + FreeProxySyncErrors type for #6909 free-pool relay-repair; re-export-only per Hard Rule #2, not extractable).", + "_rebaseline_2026_07_21_8034_compression_exclusions_sidebar": "#8034 (compression exclusions dashboard tab) own growth: sections.ts 796->806 (+10, one new COMPRESSION_CONTEXT_GROUP sidebar item linking /dashboard/compression/exclusions). The file was already 796/800 before this PR (organic growth from prior sidebar entries), so a single new nav item pushed it 6 lines over cap. Freezing at 806 (cannot grow further); the sidebar item array is data, not extractable logic.", + "src/shared/constants/sidebarVisibility/sections.ts": 806 }, "testCap": 800, "testFrozen": { diff --git a/docs/compression/COMPRESSION_ENGINES.md b/docs/compression/COMPRESSION_ENGINES.md index 2bd5a5b322..69a46b1584 100644 --- a/docs/compression/COMPRESSION_ENGINES.md +++ b/docs/compression/COMPRESSION_ENGINES.md @@ -313,6 +313,38 @@ Compression exposes five MCP tools: | `omniroute_list_compression_combos` | `read:compression` | List compression combos | | `omniroute_compression_combo_stats` | `read:compression` | Read combo/engine analytics | +## Scope & exclusions + +**Embeddings are never compressed.** `open-sse/handlers/embeddings.ts` never calls any +compression engine — the request/response bodies pass straight to the executor untouched. +This is structural today (embeddings and chat completions are disjoint handlers), not a +runtime check, but it means the vector-distortion concern in #8034 has no exposure surface +in the embeddings path. + +**Per-model/endpoint exclusion filter (#8034).** For chat completions, an operator can name +model ids / `provider/model` targets that must never be compressed — a guardrail useful if +compression is ever wired closer to an embeddings-adjacent path later, and generally useful +for any model whose exact byte-for-byte prompt matters (deterministic evals, cache-sensitive +prefixes, etc.). + +- Settings field: `exclusions?: string[]` on the global compression config + (`GET`/`PUT /api/settings/compression`), persisted via the existing `key_value` compression + namespace (`src/lib/db/compression.ts`) — no new table. +- Dashboard tab: **Dashboard → Compression → Exclusions** + (`/dashboard/compression/exclusions`). +- Pattern syntax: `*` is the only wildcard. Every other regex metacharacter in a pattern is + escaped before matching, so `gpt-5.6` matches the literal string only, never `gpt-5x6` + (ReDoS-safe, bounded, no nested quantifiers). Patterns match case-insensitively against + both the bare model id and the `provider/model` composite — `gpt-5-6`, `openai/gpt-5-6`, + and `openai/*` all work, and `*` alone excludes every model. +- Matching: `isCompressionExcluded()` / `normalizeCompressionExclusions()` in + `open-sse/services/compression/exclusions.ts`. `chatCore.ts` checks the excluded target + right after resolving compression settings, **before any engine runs**, and treats a match + exactly like compression being globally disabled — the request body is provably + byte-identical. The skip is recorded via `writeCompressionSkip(..., "excluded")` for + analytics visibility. +- Default (empty/absent list): identical to pre-#8034 behavior — nothing is excluded. + ## Known limitations - **LLMLingua-2 (SLM) requires co-located optional deps.** The worker only runs in a diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 6375a310cb..4d05448f6e 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -206,6 +206,7 @@ import { mergeResponseToolNameMap, } from "./chatCore/passthroughToolNames.ts"; import { resolveCompressionSettings } from "./chatCore/compressionSettings.ts"; +import { isCompressionExcluded } from "../services/compression/exclusions.ts"; import { isBuiltinStackedPipeline, isStackedCompressionCombo, @@ -1069,8 +1070,39 @@ export async function handleChatCore({ let estimatedTokens = estimateTokens(allMessages); const compressionSettingsResult = await resolveCompressionSettings(log); const compressionSettings: CompressionConfig | null = compressionSettingsResult.settings; - const promptCompressionEnabled = compressionSettingsResult.enabled; + // #8034 — operator-named model/endpoint exclusions bypass the whole pipeline, exactly + // like compression being globally disabled, so the body is provably byte-identical. + const compressionExcluded = isCompressionExcluded( + { provider, model: effectiveModel }, + compressionSettings?.exclusions + ); + let promptCompressionEnabled = compressionSettingsResult.enabled && !compressionExcluded; contextEditingEnabled = compressionSettingsResult.contextEditingEnabled; + if (compressionExcluded) { + void writeCompressionSkip( + { + stats: { + originalTokens: estimatedTokens, + compressedTokens: estimatedTokens, + savingsPercent: 0, + techniquesUsed: [], + mode: "off", + timestamp: Date.now(), + }, + provider, + effectiveModel, + effectiveServiceTier, + comboName, + mode: "off", + compressionComboId: null, + skillRequestId, + cavemanOutputModeApplied: false, + cavemanOutputModeIntensity: null, + log, + }, + "excluded" + ); + } // --- Modular Compression Pipeline (Phase 1 Lite + Phase 2 Standard/Caveman + Phase 3 Aggressive) --- // Runs BEFORE the existing reactive compressContext() to proactively reduce tokens. @@ -1095,6 +1127,7 @@ export async function handleChatCore({ preserveSystemPrompt: true, comboOverrides: {}, }; + if (compressionExcluded) config = { ...config, enabled: false }; if (!promptCompressionEnabled || !compressionSettings) { log?.debug?.("COMPRESSION", "Prompt compression disabled or unavailable"); } diff --git a/open-sse/services/compression/exclusions.ts b/open-sse/services/compression/exclusions.ts new file mode 100644 index 0000000000..edeb0c7b7b --- /dev/null +++ b/open-sse/services/compression/exclusions.ts @@ -0,0 +1,73 @@ +/** + * Compression Exclusions (#8034) — per-model/endpoint exclusion filter. + * + * Lets an operator name model ids / `provider/model` targets that must NEVER be + * compressed. Matching targets bypass the whole compression pipeline and pass + * through byte-identical (see `chatCore.ts` — the check runs before any engine + * executes). Default (empty/absent list) preserves pre-existing behavior exactly. + * + * Pattern syntax: `*` is the only wildcard. Every other regex metacharacter is + * escaped before the pattern is compiled, so a pattern like `gpt-5.6` matches the + * literal string only (not `gpt-5x6`) — never build a regex from raw operator + * input without escaping (ReDoS convention, see CLAUDE.md). + */ + +const MAX_EXCLUSIONS = 200; + +export type CompressionExclusions = string[]; + +/** Escape every regex metacharacter except `*`, which the caller re-inserts as `.*`. */ +function escapeExceptWildcard(pattern: string): string { + return pattern.replace(/[.+?^${}()|[\]\\]/g, "\\$&").replace(/\*/g, ".*"); +} + +/** Compile a single normalized (already lowercased/trimmed) pattern into a bounded regex. */ +function compilePattern(pattern: string): RegExp { + return new RegExp(`^${escapeExceptWildcard(pattern)}$`); +} + +/** + * Normalizes a raw settings value into a bounded list of lowercase, trimmed, deduped + * patterns. Drops non-strings and blanks. Caps length so a pathological settings value + * cannot make the per-request check expensive. + */ +export function normalizeCompressionExclusions(raw: unknown): CompressionExclusions { + if (!Array.isArray(raw)) return []; + const seen = new Set(); + const result: CompressionExclusions = []; + for (const entry of raw) { + if (typeof entry !== "string") continue; + const trimmed = entry.trim().toLowerCase(); + if (!trimmed || seen.has(trimmed)) continue; + seen.add(trimmed); + result.push(trimmed); + if (result.length >= MAX_EXCLUSIONS) break; + } + return result; +} + +/** + * True when the given target matches any configured exclusion pattern. Matches + * case-insensitively against both the bare model id and the `provider/model` + * composite, so `gpt-5-6`, `openai/gpt-5-6` and `openai/*` all work. + */ +export function isCompressionExcluded( + target: { provider?: string | null; model?: string | null }, + exclusions: CompressionExclusions | undefined +): boolean { + if (!exclusions || exclusions.length === 0) return false; + + const model = (target.model ?? "").trim().toLowerCase(); + const provider = (target.provider ?? "").trim().toLowerCase(); + if (!model && !provider) return false; + + const composite = provider && model ? `${provider}/${model}` : ""; + + for (const pattern of exclusions) { + if (!pattern) continue; + const regex = compilePattern(pattern); + if (model && regex.test(model)) return true; + if (composite && regex.test(composite)) return true; + } + return false; +} diff --git a/open-sse/services/compression/types.ts b/open-sse/services/compression/types.ts index 2e18c4b0ce..ed843cd972 100644 --- a/open-sse/services/compression/types.ts +++ b/open-sse/services/compression/types.ts @@ -266,6 +266,13 @@ export interface CompressionConfig { ultraSlmPrewarm?: boolean; /** Opt-in result memoization for deterministic engines only (default off). */ memoizeCompressionResults?: boolean; + /** + * #8034 — per-model/endpoint compression exclusion filter. Patterns are matched + * case-insensitively against both the bare model id and the `provider/model` + * composite (`*` is the only wildcard). Absent/empty → no exclusions, default + * behavior unchanged. See `open-sse/services/compression/exclusions.ts`. + */ + exclusions?: string[]; } export interface CompressionStats { diff --git a/src/app/(dashboard)/dashboard/compression/exclusions/ExclusionsPanel.tsx b/src/app/(dashboard)/dashboard/compression/exclusions/ExclusionsPanel.tsx new file mode 100644 index 0000000000..5c72b89190 --- /dev/null +++ b/src/app/(dashboard)/dashboard/compression/exclusions/ExclusionsPanel.tsx @@ -0,0 +1,105 @@ +"use client"; + +// ExclusionsPanel (#8034) — per-model/endpoint compression exclusion filter. +// +// Lets the operator name model ids / `provider/model` patterns that must never be +// compressed (`*` is the only wildcard). Persisted via the existing +// GET/PUT /api/settings/compression endpoint (`exclusions` field), read/normalized by +// `normalizeCompressionExclusions` (open-sse/services/compression/exclusions.ts). +// Default (empty list) preserves pre-existing behavior exactly. + +import { useEffect, useState } from "react"; +import { useTranslations } from "next-intl"; +import Card from "@/shared/components/Card"; +import Button from "@/shared/components/Button"; +import Textarea from "@/shared/components/Textarea"; + +function parsePatterns(raw: string): string[] { + return raw + .split("\n") + .map((line) => line.trim()) + .filter((line) => line.length > 0); +} + +export default function ExclusionsPanel() { + const t = useTranslations("settings"); + const [raw, setRaw] = useState(""); + const [loading, setLoading] = useState(true); + const [saving, setSaving] = useState(false); + const [status, setStatus] = useState<"" | "saved" | "error">(""); + + useEffect(() => { + fetch("/api/settings/compression") + .then((r) => (r.ok ? r.json() : null)) + .then((data: { exclusions?: string[] } | null) => { + if (data && Array.isArray(data.exclusions)) { + setRaw(data.exclusions.join("\n")); + } + }) + .catch(() => {}) + .finally(() => setLoading(false)); + }, []); + + const patterns = parsePatterns(raw); + + const save = async () => { + setSaving(true); + setStatus(""); + try { + const res = await fetch("/api/settings/compression", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ exclusions: patterns }), + }); + setStatus(res.ok ? "saved" : "error"); + if (res.ok) setTimeout(() => setStatus(""), 2000); + } catch { + setStatus("error"); + } finally { + setSaving(false); + } + }; + + return ( + +
+