mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
* 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 <probe@example.com>
This commit is contained in:
committed by
GitHub
parent
f23d7770ec
commit
dbdc7daade
1
changelog.d/features/8034-compression-exclusions.md
Normal file
1
changelog.d/features/8034-compression-exclusions.md
Normal file
@@ -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)
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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");
|
||||
}
|
||||
|
||||
73
open-sse/services/compression/exclusions.ts
Normal file
73
open-sse/services/compression/exclusions.ts
Normal file
@@ -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<string>();
|
||||
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;
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 (
|
||||
<Card
|
||||
title={t("compressionExclusionsTitle")}
|
||||
subtitle={t("compressionExclusionsDesc")}
|
||||
data-testid="compression-exclusions-panel"
|
||||
>
|
||||
<div className="flex flex-col gap-3">
|
||||
<Textarea
|
||||
rows={8}
|
||||
value={raw}
|
||||
disabled={loading || saving}
|
||||
placeholder={t("compressionExclusionsPlaceholder")}
|
||||
onChange={(e) => setRaw(e.target.value)}
|
||||
data-testid="compression-exclusions-textarea"
|
||||
/>
|
||||
<div className="flex items-center justify-between gap-3">
|
||||
<span className="text-xs text-text-muted" data-testid="compression-exclusions-count">
|
||||
{patterns.length === 0
|
||||
? t("compressionExclusionsEmpty")
|
||||
: t("compressionExclusionsCount", { count: patterns.length })}
|
||||
</span>
|
||||
<div className="flex items-center gap-2">
|
||||
{status === "saved" && (
|
||||
<span className="text-xs text-emerald-600 dark:text-emerald-400">
|
||||
{t("compressionExclusionsSaved")}
|
||||
</span>
|
||||
)}
|
||||
<Button
|
||||
size="sm"
|
||||
variant="primary"
|
||||
loading={saving}
|
||||
disabled={loading || saving}
|
||||
onClick={save}
|
||||
data-testid="compression-exclusions-save"
|
||||
>
|
||||
{t("compressionExclusionsSave")}
|
||||
</Button>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import ExclusionsPanel from "./ExclusionsPanel";
|
||||
|
||||
export default function CompressionExclusionsPage() {
|
||||
return (
|
||||
<div className="flex flex-col gap-4 p-4">
|
||||
<ExclusionsPanel />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
@@ -1050,6 +1050,7 @@
|
||||
"combosLive": "Combo Studio",
|
||||
"combosLiveSubtitle": "Live routing cascade",
|
||||
"compressionStudio": "Compression Studio",
|
||||
"compressionExclusions": "Exclusions",
|
||||
"contextSettingsSubtitle": "Global defaults",
|
||||
"contextHeadroomSubtitle": "Tabular compaction",
|
||||
"contextSessionDedupSubtitle": "Cross-turn deduplication",
|
||||
@@ -1060,6 +1061,7 @@
|
||||
"contextUltraSubtitle": "Heuristic pruning",
|
||||
"contextOmniglyphSubtitle": "Context as images",
|
||||
"compressionStudioSubtitle": "Live engine cascade",
|
||||
"compressionExclusionsSubtitle": "Per-model/endpoint bypass",
|
||||
"chaosConfigSubtitle": "Multi-model parallel execution",
|
||||
"routingSection": "Routing",
|
||||
"protocolsSection": "Protocols",
|
||||
@@ -6876,6 +6878,13 @@
|
||||
"compressionPreserveSystemNever": "Never",
|
||||
"compressionLiveZoneTitle": "Cache-aligned Live Zone",
|
||||
"compressionLiveZoneDesc": "Keep the compressed conversation prefix stable and process only newly appended items.",
|
||||
"compressionExclusionsTitle": "Compression Exclusions",
|
||||
"compressionExclusionsDesc": "Model ids or provider/model patterns that must never be compressed. `*` is the only wildcard (e.g. `openai/*`, `*embedding*`). A matching request passes through byte-identical — no compression engine runs.",
|
||||
"compressionExclusionsPlaceholder": "One pattern per line, e.g.\nopenai/text-embedding-3-large\nanthropic/*",
|
||||
"compressionExclusionsSave": "Save",
|
||||
"compressionExclusionsSaved": "Saved",
|
||||
"compressionExclusionsCount": "{count, plural, one {# exclusion} other {# exclusions}} configured",
|
||||
"compressionExclusionsEmpty": "No exclusions configured — every model/endpoint is eligible for compression (default behavior).",
|
||||
"compressionCavemanConfig": "Caveman Engine Configuration",
|
||||
"compressionCavemanConfigDesc": "Fine-tune the rule-based compression engine",
|
||||
"compressionCavemanPanelHint": "Its on/off and level are set in the panel:",
|
||||
|
||||
@@ -1050,6 +1050,8 @@
|
||||
"combosLive": "Combo Studio",
|
||||
"combosLiveSubtitle": "Cascata de roteamento em tempo real",
|
||||
"compressionStudio": "Compression Studio",
|
||||
"compressionExclusions": "Exclusões",
|
||||
"compressionExclusionsSubtitle": "Bypass por modelo/endpoint",
|
||||
"contextSettingsSubtitle": "Padrões globais",
|
||||
"contextHeadroomSubtitle": "Compactação tabular",
|
||||
"contextSessionDedupSubtitle": "Deduplicação entre turnos",
|
||||
@@ -6874,6 +6876,13 @@
|
||||
"compressionPreserveSystemNever": "Nunca",
|
||||
"compressionLiveZoneTitle": "Zona ativa alinhada ao cache",
|
||||
"compressionLiveZoneDesc": "Mantém estável o prefixo comprimido da conversa e processa apenas os itens recém-adicionados.",
|
||||
"compressionExclusionsTitle": "Exclusões de Compressão",
|
||||
"compressionExclusionsDesc": "Ids de modelo ou padrões provider/model que nunca devem ser comprimidos. `*` é o único curinga (ex.: `openai/*`, `*embedding*`). Uma requisição correspondente passa byte-a-byte idêntica — nenhum engine de compressão é executado.",
|
||||
"compressionExclusionsPlaceholder": "Um padrão por linha, ex.:\nopenai/text-embedding-3-large\nanthropic/*",
|
||||
"compressionExclusionsSave": "Salvar",
|
||||
"compressionExclusionsSaved": "Salvo",
|
||||
"compressionExclusionsCount": "{count, plural, one {# exclusão configurada} other {# exclusões configuradas}}",
|
||||
"compressionExclusionsEmpty": "Nenhuma exclusão configurada — todo modelo/endpoint é elegível para compressão (comportamento padrão).",
|
||||
"compressionCavemanConfig": "Caveman Engine Configuration",
|
||||
"compressionCavemanConfigDesc": "Fine-tune the rule-based compression engine",
|
||||
"compressionCavemanPanelHint": "Ativado/desativado e o nível são definidos no painel:",
|
||||
|
||||
@@ -1050,6 +1050,7 @@
|
||||
"combosLive": "Combo Studio",
|
||||
"combosLiveSubtitle": "Chuỗi định tuyến trực tiếp",
|
||||
"compressionStudio": "Compression Studio",
|
||||
"compressionExclusions": "Loại trừ",
|
||||
"contextSettingsSubtitle": "Mặc định toàn cục",
|
||||
"contextHeadroomSubtitle": "Nén dạng bảng",
|
||||
"contextSessionDedupSubtitle": "Khử trùng lặp giữa các lượt",
|
||||
@@ -1060,6 +1061,7 @@
|
||||
"contextUltraSubtitle": "Lược bỏ theo kinh nghiệm",
|
||||
"contextOmniglyphSubtitle": "Ngữ cảnh dưới dạng hình ảnh",
|
||||
"compressionStudioSubtitle": "Chuỗi bộ máy trực tiếp",
|
||||
"compressionExclusionsSubtitle": "Bỏ qua theo từng model/endpoint",
|
||||
"chaosConfigSubtitle": "Thực thi song song nhiều mô hình",
|
||||
"routingSection": "Định tuyến",
|
||||
"protocolsSection": "Giao thức",
|
||||
@@ -6866,6 +6868,13 @@
|
||||
"compressionPreserveSystemNever": "Không bao giờ",
|
||||
"compressionLiveZoneTitle": "Vùng hoạt động theo bộ nhớ đệm",
|
||||
"compressionLiveZoneDesc": "Giữ ổn định tiền tố hội thoại đã nén và chỉ xử lý các mục mới được thêm vào.",
|
||||
"compressionExclusionsTitle": "Loại trừ khỏi Nén",
|
||||
"compressionExclusionsDesc": "Id model hoặc mẫu provider/model không bao giờ được nén. `*` là ký tự đại diện duy nhất (vd. `openai/*`, `*embedding*`). Yêu cầu khớp mẫu sẽ đi qua nguyên vẹn từng byte — không engine nén nào chạy.",
|
||||
"compressionExclusionsPlaceholder": "Mỗi dòng một mẫu, vd.:\nopenai/text-embedding-3-large\nanthropic/*",
|
||||
"compressionExclusionsSave": "Lưu",
|
||||
"compressionExclusionsSaved": "Đã lưu",
|
||||
"compressionExclusionsCount": "{count, plural, one {# loại trừ} other {# loại trừ}} đã cấu hình",
|
||||
"compressionExclusionsEmpty": "Chưa cấu hình loại trừ nào — mọi model/endpoint đều đủ điều kiện để nén (hành vi mặc định).",
|
||||
"compressionCavemanConfig": "Cấu hình động cơ Caveman",
|
||||
"compressionCavemanConfigDesc": "Tinh chỉnh động cơ nén dựa trên quy tắc",
|
||||
"compressionCavemanPanelHint": "Trạng thái bật/tắt và cấp độ của nó được thiết lập trong bảng điều khiển:",
|
||||
|
||||
@@ -30,6 +30,7 @@ import {
|
||||
type RtkConfig,
|
||||
type UltraConfig,
|
||||
} from "@omniroute/open-sse/services/compression/types.ts";
|
||||
import { normalizeCompressionExclusions } from "@omniroute/open-sse/services/compression/exclusions.ts";
|
||||
import { DEFAULT_CONTEXT_BUDGET } from "@omniroute/open-sse/services/compression/adaptiveCompression/types.ts";
|
||||
import { normalizeContextBudgetConfig } from "./compressionContextBudget";
|
||||
import {
|
||||
@@ -604,6 +605,7 @@ export async function getCompressionSettings(): Promise<CompressionConfig> {
|
||||
liveZone: { enabled: false },
|
||||
engines: {},
|
||||
activeComboId: null,
|
||||
exclusions: [],
|
||||
};
|
||||
|
||||
// Tracks whether a usable stored `engines` row was found. When absent (pre-migration-102 install)
|
||||
@@ -729,6 +731,9 @@ export async function getCompressionSettings(): Promise<CompressionConfig> {
|
||||
case "ultraSlmPrewarm":
|
||||
config.ultraSlmPrewarm = parsed === true;
|
||||
break;
|
||||
case "exclusions":
|
||||
config.exclusions = normalizeCompressionExclusions(parsed);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -197,6 +197,15 @@ export const COMPRESSION_CONTEXT_GROUP: SidebarItemGroup = {
|
||||
subtitleFallback: "Live engine cascade",
|
||||
icon: "monitoring",
|
||||
},
|
||||
{
|
||||
id: "compression-exclusions",
|
||||
href: "/dashboard/compression/exclusions",
|
||||
i18nKey: "compressionExclusions",
|
||||
labelFallback: "Exclusions",
|
||||
subtitleKey: "compressionExclusionsSubtitle",
|
||||
subtitleFallback: "Per-model/endpoint bypass",
|
||||
icon: "block",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
|
||||
@@ -23,6 +23,7 @@ export const HIDEABLE_SIDEBAR_ITEM_IDS = [
|
||||
"context-ultra",
|
||||
"context-omniglyph",
|
||||
"compression-studio",
|
||||
"compression-exclusions",
|
||||
// OmniProxy > Tools
|
||||
"cli-code",
|
||||
"cli-agents",
|
||||
|
||||
@@ -343,6 +343,10 @@ export const compressionSettingsUpdateSchema = z
|
||||
activeComboId: z.string().nullable().optional(),
|
||||
ultraEngine: z.enum(["heuristic", "slm"]).optional(),
|
||||
ultraSlmPrewarm: z.boolean().optional(),
|
||||
// #8034 — per-model/endpoint compression exclusion patterns. Bounded length/size so a
|
||||
// pathological PUT body can't blow up the per-request matcher; normalizeCompressionExclusions
|
||||
// (open-sse/services/compression/exclusions.ts) is the authoritative post-read normalizer.
|
||||
exclusions: z.array(z.string().max(200)).max(200).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
||||
126
tests/unit/compression-exclusions.test.ts
Normal file
126
tests/unit/compression-exclusions.test.ts
Normal file
@@ -0,0 +1,126 @@
|
||||
import test from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
isCompressionExcluded,
|
||||
normalizeCompressionExclusions,
|
||||
} from "../../open-sse/services/compression/exclusions.ts";
|
||||
import { applyStackedCompression } from "../../open-sse/services/compression/strategySelector.ts";
|
||||
import type { CompressionPipelineStep } from "../../open-sse/services/compression/types.ts";
|
||||
|
||||
// #8034 — per-model/endpoint compression exclusion filter.
|
||||
|
||||
test("isCompressionExcluded: excluded model is true, non-excluded is false", () => {
|
||||
const exclusions = normalizeCompressionExclusions(["text-embedding-3-large"]);
|
||||
assert.equal(isCompressionExcluded({ model: "text-embedding-3-large" }, exclusions), true);
|
||||
assert.equal(isCompressionExcluded({ model: "gpt-5-6" }, exclusions), false);
|
||||
});
|
||||
|
||||
test("isCompressionExcluded: provider/model composite and bare-model patterns both match", () => {
|
||||
const composite = normalizeCompressionExclusions(["openai/text-embedding-3-large"]);
|
||||
assert.equal(
|
||||
isCompressionExcluded({ provider: "openai", model: "text-embedding-3-large" }, composite),
|
||||
true
|
||||
);
|
||||
// Bare model alone (no provider given) does not match a provider-qualified pattern.
|
||||
assert.equal(isCompressionExcluded({ model: "text-embedding-3-large" }, composite), false);
|
||||
|
||||
const bare = normalizeCompressionExclusions(["text-embedding-3-large"]);
|
||||
assert.equal(
|
||||
isCompressionExcluded({ provider: "openai", model: "text-embedding-3-large" }, bare),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("isCompressionExcluded: provider/* wildcard matches every model of that provider; * excludes everything", () => {
|
||||
const providerWildcard = normalizeCompressionExclusions(["openai/*"]);
|
||||
assert.equal(
|
||||
isCompressionExcluded({ provider: "openai", model: "gpt-5-6" }, providerWildcard),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
isCompressionExcluded({ provider: "anthropic", model: "claude-opus" }, providerWildcard),
|
||||
false
|
||||
);
|
||||
|
||||
const everything = normalizeCompressionExclusions(["*"]);
|
||||
assert.equal(isCompressionExcluded({ provider: "anthropic", model: "claude-opus" }, everything), true);
|
||||
assert.equal(isCompressionExcluded({ model: "anything" }, everything), true);
|
||||
});
|
||||
|
||||
test("isCompressionExcluded: case-insensitive match", () => {
|
||||
const exclusions = normalizeCompressionExclusions(["OpenAI/GPT-5-6"]);
|
||||
assert.equal(
|
||||
isCompressionExcluded({ provider: "openai", model: "gpt-5-6" }, exclusions),
|
||||
true
|
||||
);
|
||||
assert.equal(
|
||||
isCompressionExcluded({ provider: "OPENAI", model: "GPT-5-6" }, exclusions),
|
||||
true
|
||||
);
|
||||
});
|
||||
|
||||
test("isCompressionExcluded: empty / absent / malformed exclusions => false (default unchanged)", () => {
|
||||
assert.equal(isCompressionExcluded({ model: "gpt-5-6" }, undefined), false);
|
||||
assert.equal(isCompressionExcluded({ model: "gpt-5-6" }, []), false);
|
||||
// Malformed raw input normalizes to an empty list.
|
||||
assert.equal(
|
||||
isCompressionExcluded({ model: "gpt-5-6" }, normalizeCompressionExclusions("not-an-array")),
|
||||
false
|
||||
);
|
||||
assert.equal(
|
||||
isCompressionExcluded({ model: "gpt-5-6" }, normalizeCompressionExclusions(null)),
|
||||
false
|
||||
);
|
||||
});
|
||||
|
||||
test("isCompressionExcluded: regex metacharacters in a pattern are escaped", () => {
|
||||
const exclusions = normalizeCompressionExclusions(["gpt-5.6"]);
|
||||
assert.equal(isCompressionExcluded({ model: "gpt-5.6" }, exclusions), true);
|
||||
// A literal `.` must not behave as "any character" — gpt-5x6 must NOT match.
|
||||
assert.equal(isCompressionExcluded({ model: "gpt-5x6" }, exclusions), false);
|
||||
});
|
||||
|
||||
test("normalizeCompressionExclusions: drops non-strings and dedupes", () => {
|
||||
const result = normalizeCompressionExclusions([
|
||||
"gpt-5-6",
|
||||
"GPT-5-6",
|
||||
" gpt-5-6 ",
|
||||
42,
|
||||
null,
|
||||
undefined,
|
||||
{ not: "a string" },
|
||||
"claude-opus",
|
||||
]);
|
||||
assert.deepEqual(result, ["gpt-5-6", "claude-opus"]);
|
||||
});
|
||||
|
||||
test("behavior: excluded target passes through byte-identical; non-excluded still compresses", () => {
|
||||
const pipeline: CompressionPipelineStep[] = [{ engine: "caveman", intensity: "full" }];
|
||||
const body = {
|
||||
messages: [
|
||||
{
|
||||
role: "user",
|
||||
content:
|
||||
"I would like to please politely and kindly ask you a question if that is okay with you.",
|
||||
},
|
||||
],
|
||||
};
|
||||
|
||||
const exclusions = normalizeCompressionExclusions(["openai/text-embedding-3-large"]);
|
||||
|
||||
// Excluded target: the caller must skip the whole pipeline before calling into it — this
|
||||
// assertion proves the gate condition itself, not applyStackedCompression's own no-op path.
|
||||
const excludedTarget = { provider: "openai", model: "text-embedding-3-large" };
|
||||
assert.equal(isCompressionExcluded(excludedTarget, exclusions), true);
|
||||
const passthroughBody = JSON.parse(JSON.stringify(body));
|
||||
assert.deepEqual(passthroughBody, body);
|
||||
|
||||
// Non-excluded target: the pipeline is not gated and still compresses (savings observed).
|
||||
const nonExcludedTarget = { provider: "openai", model: "gpt-5-6" };
|
||||
assert.equal(isCompressionExcluded(nonExcludedTarget, exclusions), false);
|
||||
const result = applyStackedCompression(JSON.parse(JSON.stringify(body)), pipeline, {
|
||||
preserveSystemPrompt: false,
|
||||
});
|
||||
assert.equal(result.compressed, true);
|
||||
assert.notEqual(JSON.stringify(result.body), JSON.stringify(body));
|
||||
});
|
||||
153
tests/unit/ui/compression-exclusions-page.test.tsx
Normal file
153
tests/unit/ui/compression-exclusions-page.test.tsx
Normal file
@@ -0,0 +1,153 @@
|
||||
// @vitest-environment jsdom
|
||||
import React, { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
|
||||
// #8034 — dashboard tab for the per-model/endpoint compression exclusion filter.
|
||||
|
||||
vi.mock("next-intl", () => ({
|
||||
useTranslations: () => (key: string) => key,
|
||||
useLocale: () => "en",
|
||||
}));
|
||||
|
||||
const containers: HTMLElement[] = [];
|
||||
const roots: Array<{ unmount: () => void }> = [];
|
||||
|
||||
function mountInContainer(ui: React.ReactElement): HTMLElement {
|
||||
const container = document.createElement("div");
|
||||
document.body.appendChild(container);
|
||||
containers.push(container);
|
||||
const root = createRoot(container);
|
||||
roots.push(root);
|
||||
act(() => {
|
||||
root.render(ui);
|
||||
});
|
||||
return container;
|
||||
}
|
||||
|
||||
beforeEach(() => {
|
||||
(
|
||||
globalThis as typeof globalThis & { IS_REACT_ACT_ENVIRONMENT?: boolean }
|
||||
).IS_REACT_ACT_ENVIRONMENT = true;
|
||||
});
|
||||
|
||||
afterEach(async () => {
|
||||
vi.restoreAllMocks();
|
||||
await act(async () => {
|
||||
while (roots.length > 0) {
|
||||
roots.pop()?.unmount();
|
||||
}
|
||||
});
|
||||
for (let i = 0; i < 10; i++) {
|
||||
await Promise.resolve();
|
||||
}
|
||||
while (containers.length > 0) {
|
||||
containers.pop()?.remove();
|
||||
}
|
||||
document.body.innerHTML = "";
|
||||
});
|
||||
|
||||
function setupFetchMock(exclusions: string[] = []) {
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation(async (input: RequestInfo | URL, init) => {
|
||||
const url = input.toString();
|
||||
if (url.includes("/api/settings/compression")) {
|
||||
if (init?.method === "PUT") {
|
||||
return new Response(JSON.stringify({ exclusions }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return new Response(JSON.stringify({ exclusions }), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
return new Response(JSON.stringify({}), { status: 404 });
|
||||
});
|
||||
}
|
||||
|
||||
describe("CompressionExclusionsPage", () => {
|
||||
it("mounts, loads existing exclusions, and renders the panel", async () => {
|
||||
setupFetchMock(["openai/text-embedding-3-large"]);
|
||||
const { default: CompressionExclusionsPage } = await import(
|
||||
"../../../src/app/(dashboard)/dashboard/compression/exclusions/page"
|
||||
);
|
||||
|
||||
let container!: HTMLElement;
|
||||
await act(async () => {
|
||||
container = mountInContainer(<CompressionExclusionsPage />);
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(container).toBeTruthy();
|
||||
const textarea = container.querySelector(
|
||||
'[data-testid="compression-exclusions-textarea"]'
|
||||
) as HTMLTextAreaElement | null;
|
||||
expect(textarea).toBeTruthy();
|
||||
expect(textarea?.value).toContain("openai/text-embedding-3-large");
|
||||
});
|
||||
|
||||
it("saves edited patterns via PUT /api/settings/compression", async () => {
|
||||
setupFetchMock([]);
|
||||
const { default: CompressionExclusionsPage } = await import(
|
||||
"../../../src/app/(dashboard)/dashboard/compression/exclusions/page"
|
||||
);
|
||||
|
||||
let container!: HTMLElement;
|
||||
await act(async () => {
|
||||
container = mountInContainer(<CompressionExclusionsPage />);
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const textarea = container.querySelector(
|
||||
'[data-testid="compression-exclusions-textarea"]'
|
||||
) as HTMLTextAreaElement;
|
||||
const nativeSetter = Object.getOwnPropertyDescriptor(
|
||||
window.HTMLTextAreaElement.prototype,
|
||||
"value"
|
||||
)?.set;
|
||||
|
||||
await act(async () => {
|
||||
nativeSetter?.call(textarea, "anthropic/*\ngpt-5-6");
|
||||
textarea.dispatchEvent(new Event("input", { bubbles: true }));
|
||||
});
|
||||
|
||||
const saveButton = container.querySelector(
|
||||
'[data-testid="compression-exclusions-save"]'
|
||||
) as HTMLButtonElement;
|
||||
await act(async () => {
|
||||
saveButton.click();
|
||||
await Promise.resolve();
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const putCall = (globalThis.fetch as unknown as { mock: { calls: unknown[][] } }).mock.calls.find(
|
||||
(call) => (call[1] as RequestInit | undefined)?.method === "PUT"
|
||||
);
|
||||
expect(putCall).toBeTruthy();
|
||||
const body = JSON.parse((putCall?.[1] as RequestInit).body as string);
|
||||
expect(body.exclusions).toEqual(["anthropic/*", "gpt-5-6"]);
|
||||
});
|
||||
|
||||
it("does not crash when fetch fails (fail-soft)", async () => {
|
||||
vi.spyOn(globalThis, "fetch").mockRejectedValue(new Error("Network error"));
|
||||
const { default: CompressionExclusionsPage } = await import(
|
||||
"../../../src/app/(dashboard)/dashboard/compression/exclusions/page"
|
||||
);
|
||||
|
||||
let container!: HTMLElement;
|
||||
await act(async () => {
|
||||
container = mountInContainer(<CompressionExclusionsPage />);
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(container).toBeTruthy();
|
||||
expect(container.parentNode).toBeTruthy();
|
||||
});
|
||||
});
|
||||
Reference in New Issue
Block a user