mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-08-03 13:52:09 +03:00
feat(compression): unified config panel — single source for engine on/off + level (Phase 1) (#4432)
* docs(compression): design spec for the unified compression config panel
Engine-centric central panel (single source for master + per-engine on/off + level),
default pipeline derived from the engines map; Combos page owns named ordered pipelines +
active-profile selection; per-engine pages keep only detailed config. Phased: (1) core
consolidation + Model A migration, (2) named profiles + active selector, (3) per-request
x-omniroute-compression header.
* docs(compression): Phase 1 implementation plan for the unified config panel
12 TDD tasks: engine catalog, engines map + activeComboId, deriveDefaultPlan,
migration 102 + backfill, resolveCompressionPlan, runtime wiring, API, default-combo
shim, the engine-grid panel, consolidation, menu. Ends with a recap + the pending
follow-ups (Phase 2 named profiles/active selector, Phase 3 header, deferred items).
* feat(compression): engine catalog metadata (levels, single-mode, order)
* feat(compression): add engines map + activeComboId to CompressionConfig
* feat(compression): deriveDefaultPlan (engines map -> mode/pipeline)
Pure function that converts a per-engine EngineToggle map + masterEnabled
flag into a { mode, stackedPipeline } plan: off when master is off or no
engines on; single-mode when exactly one single-mode engine is enabled;
stacked (sorted by stackPriority) otherwise.
* feat(compression): persist+backfill engines map and activeComboId (migration 102)
* feat(compression): resolveCompressionPlan precedence resolver (header>override>active>default)
* feat(compression): selectCompressionStrategy uses resolveCompressionPlan
* feat(api): settings/compression carries engines map + activeComboId
Extends compressionSettingsUpdateSchema with engines (Record<string,{enabled:boolean,level?:string}>)
and activeComboId (string|null) so the PUT route accepts and persists these fields.
GET already returns the full getCompressionSettings() object which includes both fields.
TDD: added route round-trip tests (PUT+GET) for engines, activeComboId, null clear,
and schema rejection of invalid engines shape.
* refactor(compression): default-combo route is a read-only shim (default derived from engines)
* feat(dashboard): engine-grid compression panel (single source for on/off + level)
* refactor(dashboard): remove duplicate compression toggles; per-engine pages keep only detailed config
* feat(compression): unified panel menu order + derived-pipeline integration coverage
* fix(compression): import ENGINE_IDS via types re-export so it resolves under vitest
The bare "@omniroute/open-sse/.../engineCatalog.ts" specifier resolves under tsc/tsx
but not under vitest's MCP config: Vite externalizes a brand-new open-sse module to
Node, which can't load the .ts subpath. types.ts is already in Vite's graph, so route
ENGINE_IDS through its re-export. Fixes 3 failing MCP vitest suites (cacheTools,
dbHealthTool, essentialTools).
* fix(compression): engines map drives dispatch only when explicitly panel-saved
Code-review finding: the legacy seeded default combo (present on every install via
migration 042/043) was silently overriding a panel-configured engines map — the
default-combo block in chatCore set compressionComboApplied=true, skipping the
engines-map override, so an operator's panel toggles were ignored.
Gate the engines-map path on a new runtime-only CompressionConfig.enginesExplicit
(true when a stored engines row exists). Panel-saved installs: the engines map is
authoritative (deriveDefaultPlanFromConfig + new enginesMapDerivesStackedPipeline
guard the chatCore default-combo block). Legacy/backfilled installs: the map is
display-only and dispatch stays on the historical defaultMode/default-combo path —
zero behaviour change until the operator opts in via the panel.
Also documents why resolveCacheAwareConfig's getCacheAwareStrategy(config.defaultMode)
arg is safe (skipSystemPrompt is mode-independent).
* docs(compression): add MDX frontmatter + escape inline brace to fix fumadocs build
docs/compression/**/*.md is compiled as MDX by the Next build (fumadocs,
source.config.ts). The two planning docs lacked the required `title`
frontmatter (build error: 'title: expected string, received undefined') and the
design doc had a bare {rtk,caveman} that MDX parsed as a JSX expression. Adds
frontmatter matching the sibling docs and backticks the brace. Fixes the
dast-smoke build step.
This commit is contained in:
committed by
GitHub
parent
3ad2043327
commit
95e6522720
@@ -0,0 +1,240 @@
|
||||
---
|
||||
title: "Unified Compression Config Panel — Design"
|
||||
version: 3.8.32
|
||||
lastUpdated: 2026-06-20
|
||||
---
|
||||
|
||||
# Unified Compression Config Panel — Design
|
||||
|
||||
**Status:** approved direction (2026-06-20), pending spec review
|
||||
**Base branch:** `release/v3.8.31`
|
||||
**Goal:** Make `/dashboard/context/settings` the single management panel for the master
|
||||
on/off and each compression engine's on/off + level, deriving the default pipeline from
|
||||
those toggles. Keep the Combos page solely for *chaining* (ordered named pipelines) +
|
||||
selecting the globally-active profile, and the per-engine pages solely for *detailed*
|
||||
config. Plan for (but phase) multiple named profiles with an active selector and a
|
||||
per-request override header.
|
||||
|
||||
---
|
||||
|
||||
## 1. Background — current state (the problem)
|
||||
|
||||
Compression on/off + level is split across **two stores and several UIs**, with real
|
||||
duplication:
|
||||
|
||||
- **`/api/settings/compression`** (DB `key_value` ns=`compression`, via `src/lib/db/compression.ts`)
|
||||
holds: `enabled` (master), `defaultMode`, `autoTriggerTokens`, `cavemanConfig`,
|
||||
`cavemanOutputMode`, `rtkConfig`, `aggressive`, `ultra`, language config, etc.
|
||||
- **`/api/context/combos/default`** (the *default combo pipeline*) holds the per-engine
|
||||
`enabled` + config for the **structural** engines (lite, headroom, session-dedup, ccr,
|
||||
llmlingua, aggressive, ultra) as pipeline steps. The per-engine detail pages
|
||||
(`EngineConfigPage.tsx`) read/write here via `setEngineInDefaultCombo`.
|
||||
- **`compression_combos`** table holds *named* pipelines assigned to routing combos.
|
||||
|
||||
Concrete duplications (from the UI audit):
|
||||
- Caveman on/off in **3** places (TokenSaverCard, CompressionSettingsTab, CavemanContextPageClient).
|
||||
- Caveman intensity in **3**; RTK intensity in **2**; Caveman output mode in **2**.
|
||||
- `lite/session-dedup/headroom/ccr/llmlingua` on/off **absent** from the central settings —
|
||||
only on their per-engine page (which writes the *default combo*, not the central config).
|
||||
|
||||
So "is engine X on?" is answered inconsistently (central config for caveman/rtk; default
|
||||
combo for the structural engines), and the **Combos page edits the same default pipeline**
|
||||
that a central panel would — the conceptual overlap the redesign must remove.
|
||||
|
||||
Out of scope as a *config* surface: `dashboard/compression/studio` is the Compression
|
||||
Studio (waterfall/cockpit analytics), not toggles — untouched.
|
||||
|
||||
Engines (the 10 stackable units): `lite, caveman, aggressive, ultra, rtk, headroom,
|
||||
session-dedup, ccr, llmlingua` (registered `CompressionEngine`s) + `mcpAccessibility`
|
||||
(separate path: compresses MCP tool-result outputs, not the chat pipeline). Each engine's
|
||||
`stackPriority` defines automatic ordering (session-dedup 3, ccr 4, lite 5, rtk 10,
|
||||
headroom 15, caveman 20, aggressive 30, llmlingua 35, ultra 40).
|
||||
|
||||
---
|
||||
|
||||
## 2. Design principles — the boundary
|
||||
|
||||
| Surface | Concept | Owns | Never does |
|
||||
|---|---|---|---|
|
||||
| **Panel** (`context/settings`) | "My **Default**: what is on + level" | master on/off; per-engine on/off + level | ordering/chaining; per-route assignment |
|
||||
| **Combos** (`context/combos`, menu #2) | "Named **profiles**: chaining + which is active + per-route assignment" | create/edit ordered named pipelines; pick the globally-active profile; assign to routing combos | engine on/off (inherits the active profile's membership) |
|
||||
| **Per-engine pages** (menu, after combos) | "Deep config of one engine" | filters, rules, language packs, thresholds | on/off; level (those live in the panel) |
|
||||
|
||||
**No duplication rule:** the **Panel owns the Default profile** (membership + level, order
|
||||
auto-derived by `stackPriority`). A **Combo is a *named alternative* profile** (membership
|
||||
+ level + *explicit order*). Different scopes (the one default vs named alternatives) — not
|
||||
the same object edited twice. The editable "default combo" store is **removed**; the
|
||||
default pipeline becomes **derived** from the panel.
|
||||
|
||||
---
|
||||
|
||||
## 3. Architecture
|
||||
|
||||
### 3.1 Resolution model (per request, most-specific wins)
|
||||
|
||||
```
|
||||
x-omniroute-compression header (per-request override)
|
||||
→ routing-combo override (comboOverrides[comboId]) (per-route)
|
||||
→ active profile (activeComboId: "default" | <comboId>) (global)
|
||||
→ Default = derived from panel engines map
|
||||
→ off (master disabled, or zero engines on)
|
||||
```
|
||||
|
||||
A single `resolveCompressionPlan(config, { comboId, header })` produces the effective
|
||||
`{ mode, stackedPipeline }` fed to the existing `applyCompressionAsync`. It supersedes
|
||||
today's `getEffectiveMode` (which only does combo-override → autoTrigger → defaultMode).
|
||||
|
||||
### 3.2 Data model (Model A — single source + derived pipeline)
|
||||
|
||||
**CompressionConfig gains an engines map** (the single source for the Default's on/off +
|
||||
level), replacing the editable default-combo store:
|
||||
|
||||
```ts
|
||||
interface EngineToggle {
|
||||
enabled: boolean;
|
||||
level?: string; // caveman/cavemanOutput: lite|full|ultra ; rtk: minimal|standard|aggressive ; others: undefined
|
||||
}
|
||||
interface CompressionConfig {
|
||||
enabled: boolean; // master
|
||||
engines: Record<CompressionEngineId, EngineToggle>; // NEW — the Default profile
|
||||
activeComboId: string | null; // NEW — null/"default" = derived default; else a compression_combos id
|
||||
autoTriggerTokens: number;
|
||||
autoTriggerMode?: CompressionMode; // kept (auto-trigger still selects a profile/mode on large prompts)
|
||||
preserveSystemPrompt: boolean;
|
||||
comboOverrides: Record<string, CompressionMode>; // existing per-routing-combo override
|
||||
// detailed per-engine config keeps living in its existing sub-objects
|
||||
// (cavemanConfig, rtkConfig, aggressive, ultra, …) — edited by the per-engine pages.
|
||||
}
|
||||
```
|
||||
|
||||
**Deriving the Default pipeline** from `engines` (pure function `deriveDefaultPlan`):
|
||||
- master `enabled === false` → `off`.
|
||||
- 0 engines enabled → `off`.
|
||||
- exactly 1 enabled and it is a single-mode engine (`lite|caveman|aggressive|ultra|rtk`) →
|
||||
that mode (single path — reuses today's `applyCompression(mode)`).
|
||||
- otherwise → `stacked` with the enabled engines in `stackPriority` order, each step's
|
||||
`intensity` from its `level` (the global `stackedPipeline` already accepts all 9 engines
|
||||
after the B-PIPELINE-DIVERGENCE fix).
|
||||
|
||||
The **stored `defaultMode` field** and the editable default combo
|
||||
(`/api/context/combos/default`, `setEngineInDefaultCombo`) are **removed**; the default is
|
||||
derived from `engines`. (The `CompressionMode` *type* persists — `comboOverrides`,
|
||||
`autoTriggerMode`, and the resolver's output still use it; only the stored `defaultMode`
|
||||
field is dropped.) A DB migration backfills `engines` from the current `defaultMode` +
|
||||
default-combo steps + caveman/rtk config so existing installs keep their behavior.
|
||||
|
||||
**Named combos** (`compression_combos`, existing table): N ordered pipelines. `activeComboId`
|
||||
selects which profile is globally active (`Default` or a named combo). Per-routing-combo
|
||||
assignment stays in `comboOverrides`.
|
||||
|
||||
`mcpAccessibility` keeps its own store (`/api/settings/compression/mcp-accessibility`,
|
||||
migration 056) — surfaced in the panel as a row that writes there, with a scope note.
|
||||
|
||||
### 3.3 The per-request header
|
||||
|
||||
`x-omniroute-compression: <value>` (mirrors the `x-omniroute-no-memory`/`no-cache` pattern,
|
||||
PR #4290; parsed in the request pipeline alongside the other omniroute headers). Values:
|
||||
- `off` → no compression for this request.
|
||||
- `default` → the derived Default profile.
|
||||
- `<combo-name|id>` → that named combo.
|
||||
- `engine:<id>` → a single engine (if that engine is enabled in the Default), e.g. `engine:rtk`.
|
||||
|
||||
Invalid/unknown value → ignored (falls through to the normal resolution); never errors the
|
||||
request. Header parsing + validation has a unit test asserting each form and the
|
||||
fall-through.
|
||||
|
||||
---
|
||||
|
||||
## 4. Screens
|
||||
|
||||
### 4.1 Panel — `context/settings` (engine grid)
|
||||
|
||||
A single client component (replacing the scattered `CompressionSettingsTab` +
|
||||
`CompressionTokenSaverCard` toggles):
|
||||
- **Master** on/off at top.
|
||||
- **Engine grid** (one row per engine, ordered by `stackPriority` so the row order mirrors
|
||||
run order): `[engine name + short desc] [on/off toggle] [level selector if applicable]
|
||||
[→ detail page]`. Level selector appears only for engines with levels
|
||||
(caveman lite|full|ultra, rtk minimal|standard|aggressive; caveman output mode as its own row).
|
||||
- **General** (auto-trigger tokens, preserve-system-prompt) below the grid.
|
||||
- Reads/writes the `engines` map + master via `GET/PUT /api/settings/compression` (single
|
||||
endpoint). The displayed default pipeline (derived) is shown read-only ("runs: rtk →
|
||||
caveman → …") so the user sees the effect without editing order here.
|
||||
|
||||
### 4.2 Combos — `context/combos` (menu #2)
|
||||
|
||||
- List of named combos; create/edit an **ordered** pipeline (drag/reorder + per-step level)
|
||||
— the only place for explicit chaining.
|
||||
- **Active profile selector**: `Default (panel)` | `<combo>` → writes `activeComboId`.
|
||||
- Assign a combo to a routing combo (existing `comboOverrides`).
|
||||
- Reuses the existing `CompressionCombosPageClient` / `comboFlowModel`; the `CompressionHub`
|
||||
"master mode selector" is removed (mode is now derived; the Hub becomes a read-only
|
||||
overview or is folded into the panel).
|
||||
|
||||
### 4.3 Per-engine pages (menu, after combos)
|
||||
|
||||
`EngineConfigPage` + the caveman/rtk custom pages: **lose** the on/off + level controls
|
||||
(moved to the panel) and keep only **detailed** config (filters, rules, language packs,
|
||||
thresholds, preview). They **stop writing** `/api/context/combos/default`; detailed config
|
||||
writes to its own sub-object in `/api/settings/compression` (or the existing
|
||||
caveman/rtk facade routes, which already proxy to it).
|
||||
|
||||
### 4.4 Navigation
|
||||
|
||||
`COMPRESSION_CONTEXT_GROUP` (`src/shared/constants/sidebarVisibility.ts`) reordered:
|
||||
**Settings (panel) → Combos → per-engine pages → Studio (analytics)**.
|
||||
|
||||
---
|
||||
|
||||
## 5. Consolidation / migration
|
||||
|
||||
- `CompressionTokenSaverCard` quick toggles → **absorbed into the panel; the card is removed**.
|
||||
- Duplicate caveman/rtk on/off + intensity in `CompressionSettingsTab` → removed.
|
||||
- `EngineConfigPage` → on/off+level removed; stops writing the default combo.
|
||||
- DB migration: backfill `engines` map + `activeComboId="default"` from current state
|
||||
(defaultMode + default-combo steps + caveman/rtk/ultra/aggressive enabled), so live
|
||||
installs preserve behavior. The editable default-combo route (`PUT /api/context/combos/default`)
|
||||
becomes a **read-only shim for one release** (returns the derived default; rejects writes
|
||||
with a deprecation note), then is removed.
|
||||
|
||||
---
|
||||
|
||||
## 6. Phasing
|
||||
|
||||
Each phase is its own implementation plan + PR, independently shippable and TDD'd
|
||||
(Hard Rule #18). `writing-plans` will author **Phase 1 first**; Phases 2–3 get their own
|
||||
plans later.
|
||||
|
||||
- **Phase 1 (core consolidation):** the `engines` map + `deriveDefaultPlan` + migration;
|
||||
the engine-grid panel; remove scattered/duplicate toggles; per-engine pages lose on/off;
|
||||
menu reorder. Delivers the single-panel goal. *No behavior change for existing installs
|
||||
(migration backfills).*
|
||||
- **Phase 2 (profiles):** multiple named combos + the `activeComboId` active-profile selector.
|
||||
- **Phase 3 (header):** the `x-omniroute-compression` per-request override.
|
||||
|
||||
Phases 2–3 reuse the Phase-1 resolution model (`resolveCompressionPlan`), which is built
|
||||
header/active-aware from the start so later phases only wire UI + header parsing.
|
||||
|
||||
---
|
||||
|
||||
## 7. Testing
|
||||
|
||||
- **Unit:** `deriveDefaultPlan` (every engines-map shape → expected mode/pipeline);
|
||||
migration backfill (old config → equivalent engines map); `resolveCompressionPlan`
|
||||
precedence (header > comboOverride > active > default > off); header parsing (each form +
|
||||
fall-through); panel reducer (toggle/level edits → config patch).
|
||||
- **Component (vitest):** the panel renders all engines, toggles persist, derived pipeline
|
||||
preview updates.
|
||||
- **Integration:** a config with engines `{rtk,caveman}` on → `applyCompressionAsync` runs the
|
||||
derived stacked pipeline; single engine on → single-mode path; equivalence with the old
|
||||
defaultMode behavior for the same logical config.
|
||||
- Both runners green (`test:unit` + `test:vitest`); typecheck:core clean; lint 0 errors.
|
||||
|
||||
---
|
||||
|
||||
## 8. Non-goals (YAGNI)
|
||||
|
||||
- No new compression engines in this work (the model just makes adding them trivial later).
|
||||
- No change to the engines' internal algorithms (the recent fixes are separate).
|
||||
- The Compression Studio (analytics) is not restructured.
|
||||
- Phases 2–3 UI polish (combo templates, sharing) is out of scope.
|
||||
@@ -0,0 +1,495 @@
|
||||
---
|
||||
title: "Unified Compression Config Panel — Phase 1 Implementation Plan"
|
||||
version: 3.8.32
|
||||
lastUpdated: 2026-06-20
|
||||
---
|
||||
|
||||
# Unified Compression Config Panel — Phase 1 Implementation Plan
|
||||
|
||||
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
|
||||
|
||||
**Goal:** Make `/dashboard/context/settings` the single source for the master + per-engine on/off + level, with the default compression pipeline DERIVED from those toggles, removing the scattered/duplicate toggles — with zero behavior change for existing installs (a migration backfills).
|
||||
|
||||
**Architecture:** Add an `engines` map (+`activeComboId`) to `CompressionConfig` as the single source. A pure `deriveDefaultPlan(engines)` turns it into `{mode, stackedPipeline}`; a pure `resolveCompressionPlan(config, ctx)` applies precedence (header→combo-override→active-profile→derived-default→off) and feeds the existing `applyCompressionAsync`. A DB migration backfills the map. The engine-grid panel reads/writes the map via the single `/api/settings/compression` endpoint; per-engine pages lose on/off+level; the editable default-combo route becomes a read-only shim.
|
||||
|
||||
**Tech Stack:** TypeScript, Next.js 16 App Router, Zod, SQLite (better-sqlite3), Node test runner + Vitest (component), React.
|
||||
|
||||
**Base:** worktree `feat/compression-config-panel-v3831` off `release/v3.8.31`. Spec: `docs/compression/2026-06-20-unified-compression-config-panel-design.md`.
|
||||
|
||||
**Conventions:** unit tests run `node --import tsx/esm --test tests/unit/<f>.test.ts`; component tests run `npm run test:vitest`. Each task ends by running the FULL compression suite (`node --import tsx/esm --test tests/unit/compression/*.test.ts`) + `npm run typecheck:core` before commit. Never `--no-verify`.
|
||||
|
||||
---
|
||||
|
||||
## File Structure
|
||||
|
||||
**Create:**
|
||||
- `open-sse/services/compression/engineCatalog.ts` — pure metadata: per-engine `{ id, label, stackPriority, levels?, isSingleMode }`. One source of truth for "which engines exist, which have levels, which can be a standalone mode".
|
||||
- `open-sse/services/compression/deriveDefaultPlan.ts` — pure: `engines` map → `{ mode, stackedPipeline }`.
|
||||
- `open-sse/services/compression/resolveCompressionPlan.ts` — pure: precedence resolver.
|
||||
- `src/lib/db/migrations/102_compression_engines_map.sql` — backfill `engines` + `activeComboId`.
|
||||
- `src/app/(dashboard)/dashboard/context/settings/CompressionPanel.tsx` — the engine-grid panel.
|
||||
- `tests/unit/compression/engine-catalog.test.ts`, `derive-default-plan.test.ts`, `resolve-compression-plan.test.ts`, `compression-engines-map-migration.test.ts`.
|
||||
- `tests/unit/ui/compressionPanel.test.tsx` (vitest).
|
||||
|
||||
**Modify:**
|
||||
- `open-sse/services/compression/types.ts` — add `EngineToggle`, `CompressionConfig.engines`, `activeComboId`; keep `CompressionMode` type; drop stored `defaultMode` usage (derive).
|
||||
- `src/lib/db/compression.ts` — normalize/persist `engines` + `activeComboId`.
|
||||
- `open-sse/services/compression/strategySelector.ts` — `selectCompressionStrategy`/`getEffectiveMode` delegate to `resolveCompressionPlan`.
|
||||
- `open-sse/handlers/chatCore.ts` — call `resolveCompressionPlan`.
|
||||
- `src/app/api/settings/compression/route.ts` — accept/return `engines` + `activeComboId`.
|
||||
- `src/app/api/context/combos/default/route.ts` — read-only shim (reject writes).
|
||||
- `src/app/(dashboard)/dashboard/context/settings/page.tsx` — render `CompressionPanel` (not the old tab).
|
||||
- `src/shared/components/compression/EngineConfigPage.tsx` + caveman/rtk client pages — remove on/off+level; stop writing default combo.
|
||||
- `src/shared/constants/sidebarVisibility.ts` — reorder `COMPRESSION_CONTEXT_GROUP`.
|
||||
|
||||
**Engine reference (`stackPriority`, levels, single-mode):**
|
||||
`session-dedup`(3,—,no) `ccr`(4,—,no) `lite`(5,—,yes) `rtk`(10,minimal|standard|aggressive,yes) `headroom`(15,—,no) `caveman`(20,lite|full|ultra,yes) `aggressive`(30,—,yes) `llmlingua`(35,—,no) `ultra`(40,—,yes). Plus `cavemanOutput`(intensity lite|full|ultra, separate from input caveman) and `mcpAccessibility`(no level, separate store).
|
||||
|
||||
---
|
||||
|
||||
## Task 1: Engine catalog (single source of engine metadata)
|
||||
|
||||
**Files:**
|
||||
- Create: `open-sse/services/compression/engineCatalog.ts`
|
||||
- Test: `tests/unit/compression/engine-catalog.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```ts
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { ENGINE_CATALOG, engineMeta, ENGINE_IDS } from "@omniroute/open-sse/services/compression/engineCatalog.ts";
|
||||
|
||||
test("catalog lists every engine with stackPriority", () => {
|
||||
for (const id of ["session-dedup","ccr","lite","rtk","headroom","caveman","aggressive","llmlingua","ultra"]) {
|
||||
assert.ok(engineMeta(id), `${id} present`);
|
||||
assert.equal(typeof engineMeta(id).stackPriority, "number");
|
||||
}
|
||||
});
|
||||
test("levels + single-mode flags are correct", () => {
|
||||
assert.deepEqual(engineMeta("rtk").levels, ["minimal","standard","aggressive"]);
|
||||
assert.deepEqual(engineMeta("caveman").levels, ["lite","full","ultra"]);
|
||||
assert.equal(engineMeta("headroom").levels, undefined);
|
||||
assert.equal(engineMeta("caveman").isSingleMode, true);
|
||||
assert.equal(engineMeta("headroom").isSingleMode, false);
|
||||
});
|
||||
test("ENGINE_IDS is ordered by stackPriority", () => {
|
||||
const ps = ENGINE_IDS.map((id) => engineMeta(id).stackPriority);
|
||||
assert.deepEqual(ps, [...ps].sort((a,b)=>a-b));
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run → FAIL** (`node --import tsx/esm --test tests/unit/compression/engine-catalog.test.ts`) — module not found.
|
||||
|
||||
- [ ] **Step 3: Implement** `engineCatalog.ts`:
|
||||
|
||||
```ts
|
||||
export interface EngineMeta {
|
||||
id: string;
|
||||
label: string;
|
||||
stackPriority: number;
|
||||
levels?: string[]; // intensity options; undefined = no level selector
|
||||
isSingleMode: boolean; // can be the effective mode when it is the only engine on
|
||||
description: string;
|
||||
}
|
||||
export const ENGINE_CATALOG: Record<string, EngineMeta> = {
|
||||
"session-dedup": { id:"session-dedup", label:"Session Dedup", stackPriority:3, isSingleMode:false, description:"Cross-turn block deduplication." },
|
||||
ccr: { id:"ccr", label:"CCR (Retrieval)", stackPriority:4, isSingleMode:false, description:"Content-addressed retrieval markers." },
|
||||
lite: { id:"lite", label:"Lite", stackPriority:5, isSingleMode:true, description:"Whitespace/format cleanup." },
|
||||
rtk: { id:"rtk", label:"RTK", stackPriority:10, levels:["minimal","standard","aggressive"], isSingleMode:true, description:"Command-output filtering." },
|
||||
headroom: { id:"headroom", label:"Headroom", stackPriority:15, isSingleMode:false, description:"Tabular JSON compaction." },
|
||||
caveman: { id:"caveman", label:"Caveman", stackPriority:20, levels:["lite","full","ultra"], isSingleMode:true, description:"Rule-based prose compression." },
|
||||
aggressive: { id:"aggressive", label:"Aggressive", stackPriority:30, isSingleMode:true, description:"Summarize + age old turns." },
|
||||
llmlingua: { id:"llmlingua", label:"LLMLingua (SLM)", stackPriority:35, isSingleMode:false, description:"Semantic pruning (ONNX)." },
|
||||
ultra: { id:"ultra", label:"Ultra", stackPriority:40, isSingleMode:true, description:"Heuristic token pruning (+ optional SLM)." },
|
||||
};
|
||||
export const ENGINE_IDS: string[] = Object.values(ENGINE_CATALOG).sort((a,b)=>a.stackPriority-b.stackPriority).map((e)=>e.id);
|
||||
export function engineMeta(id: string): EngineMeta { return ENGINE_CATALOG[id]; }
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run → PASS.**
|
||||
- [ ] **Step 5: Commit** `git add open-sse/services/compression/engineCatalog.ts tests/unit/compression/engine-catalog.test.ts && git commit -m "feat(compression): engine catalog metadata (levels, single-mode, order)"`
|
||||
|
||||
---
|
||||
|
||||
## Task 2: `EngineToggle` + `engines` map + `activeComboId` on the config type
|
||||
|
||||
**Files:**
|
||||
- Modify: `open-sse/services/compression/types.ts` (the `CompressionConfig` interface + `DEFAULT_COMPRESSION_CONFIG`)
|
||||
- Test: `tests/unit/compression/engine-catalog.test.ts` (extend) — assert the default config shape.
|
||||
|
||||
- [ ] **Step 1: Add the test** (append):
|
||||
|
||||
```ts
|
||||
import { DEFAULT_COMPRESSION_CONFIG } from "@omniroute/open-sse/services/compression/types.ts";
|
||||
test("default config has an engines map + activeComboId", () => {
|
||||
assert.equal(typeof DEFAULT_COMPRESSION_CONFIG.engines, "object");
|
||||
assert.equal(DEFAULT_COMPRESSION_CONFIG.activeComboId, null);
|
||||
// default-off: every engine disabled by default (opt-in preserved)
|
||||
for (const id of ENGINE_IDS) assert.equal(DEFAULT_COMPRESSION_CONFIG.engines[id]?.enabled, false);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run → FAIL.**
|
||||
- [ ] **Step 3: Implement** in `types.ts`: add `export interface EngineToggle { enabled: boolean; level?: string }`; add to `CompressionConfig`: `engines: Record<string, EngineToggle>;` and `activeComboId: string | null;`. In `DEFAULT_COMPRESSION_CONFIG` add `engines: Object.fromEntries(ENGINE_IDS.map((id)=>[id,{enabled:false}]))` (import `ENGINE_IDS`) and `activeComboId: null`. Keep `defaultMode` field for now (removed in Task 9 once derive is wired) to avoid breaking compilation.
|
||||
|
||||
- [ ] **Step 4: Run → PASS + `npm run typecheck:core`.**
|
||||
- [ ] **Step 5: Commit** `... -m "feat(compression): add engines map + activeComboId to CompressionConfig"`
|
||||
|
||||
---
|
||||
|
||||
## Task 3: `deriveDefaultPlan` (the heart — engines map → mode/pipeline)
|
||||
|
||||
**Files:**
|
||||
- Create: `open-sse/services/compression/deriveDefaultPlan.ts`
|
||||
- Test: `tests/unit/compression/derive-default-plan.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```ts
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { deriveDefaultPlan } from "@omniroute/open-sse/services/compression/deriveDefaultPlan.ts";
|
||||
|
||||
const on = (level) => ({ enabled: true, ...(level?{level}:{}) });
|
||||
test("master off / empty / none-on => off", () => {
|
||||
assert.deepEqual(deriveDefaultPlan({}, false), { mode:"off", stackedPipeline:[] });
|
||||
assert.deepEqual(deriveDefaultPlan({}, true), { mode:"off", stackedPipeline:[] });
|
||||
assert.deepEqual(deriveDefaultPlan({ rtk:{enabled:false} }, true), { mode:"off", stackedPipeline:[] });
|
||||
});
|
||||
test("exactly one single-mode engine => that mode", () => {
|
||||
assert.deepEqual(deriveDefaultPlan({ caveman: on("full") }, true), { mode:"standard", stackedPipeline:[] });
|
||||
assert.deepEqual(deriveDefaultPlan({ rtk: on("minimal") }, true), { mode:"rtk", stackedPipeline:[] });
|
||||
assert.deepEqual(deriveDefaultPlan({ lite: on() }, true), { mode:"lite", stackedPipeline:[] });
|
||||
});
|
||||
test("one non-single-mode engine => stacked with that engine", () => {
|
||||
const p = deriveDefaultPlan({ headroom: on() }, true);
|
||||
assert.equal(p.mode, "stacked");
|
||||
assert.deepEqual(p.stackedPipeline, [{ engine:"headroom" }]);
|
||||
});
|
||||
test("multiple engines => stacked in stackPriority order, levels as intensity", () => {
|
||||
const p = deriveDefaultPlan({ caveman: on("full"), rtk: on("standard"), headroom: on() }, true);
|
||||
assert.equal(p.mode, "stacked");
|
||||
assert.deepEqual(p.stackedPipeline, [
|
||||
{ engine:"rtk", intensity:"standard" }, // pri 10
|
||||
{ engine:"headroom" }, // pri 15
|
||||
{ engine:"caveman", intensity:"full" }, // pri 20
|
||||
]);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run → FAIL.**
|
||||
- [ ] **Step 3: Implement** `deriveDefaultPlan.ts`:
|
||||
|
||||
```ts
|
||||
import { ENGINE_CATALOG, engineMeta } from "./engineCatalog.ts";
|
||||
import type { EngineToggle } from "./types.ts";
|
||||
|
||||
const SINGLE_MODE_OF: Record<string,string> = { lite:"lite", caveman:"standard", aggressive:"aggressive", ultra:"ultra", rtk:"rtk" };
|
||||
|
||||
export interface DerivedPlan { mode: string; stackedPipeline: Array<{ engine: string; intensity?: string }>; }
|
||||
|
||||
export function deriveDefaultPlan(engines: Record<string, EngineToggle>, masterEnabled: boolean): DerivedPlan {
|
||||
if (!masterEnabled) return { mode:"off", stackedPipeline:[] };
|
||||
const onIds = Object.keys(ENGINE_CATALOG).filter((id) => engines[id]?.enabled === true);
|
||||
if (onIds.length === 0) return { mode:"off", stackedPipeline:[] };
|
||||
if (onIds.length === 1 && engineMeta(onIds[0]).isSingleMode) {
|
||||
return { mode: SINGLE_MODE_OF[onIds[0]], stackedPipeline:[] };
|
||||
}
|
||||
const ordered = onIds.sort((a,b)=>engineMeta(a).stackPriority-engineMeta(b).stackPriority);
|
||||
const stackedPipeline = ordered.map((id) => {
|
||||
const level = engines[id]?.level;
|
||||
return level ? { engine:id, intensity:level } : { engine:id };
|
||||
});
|
||||
return { mode:"stacked", stackedPipeline };
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run → PASS.**
|
||||
- [ ] **Step 5: Commit** `... -m "feat(compression): deriveDefaultPlan (engines map -> mode/pipeline)"`
|
||||
|
||||
---
|
||||
|
||||
## Task 4: Migration 102 — backfill `engines` + `activeComboId`
|
||||
|
||||
**Files:**
|
||||
- Create: `src/lib/db/migrations/102_compression_engines_map.sql`
|
||||
- Test: `tests/unit/compression/compression-engines-map-migration.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing test** (uses `resetDbInstance()` + closes handles in `test.after`, per repo rule):
|
||||
|
||||
```ts
|
||||
import { test, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { getDbInstance, resetDbInstance } from "@/lib/db/core.ts";
|
||||
import { getCompressionSettings } from "@/lib/db/compression.ts";
|
||||
|
||||
after(() => resetDbInstance());
|
||||
test("migration backfills engines map from prior defaultMode + default combo", () => {
|
||||
resetDbInstance();
|
||||
const db = getDbInstance(); // runs migrations incl. 102
|
||||
// simulate a pre-102 install: master on, defaultMode 'standard', caveman enabled
|
||||
db.prepare("INSERT OR REPLACE INTO key_value(namespace,key,value) VALUES('compression','enabled','true')").run();
|
||||
db.prepare("INSERT OR REPLACE INTO key_value(namespace,key,value) VALUES('compression','defaultMode','\"standard\"')").run();
|
||||
db.prepare("INSERT OR REPLACE INTO key_value(namespace,key,value) VALUES('compression','cavemanConfig','{\"enabled\":true}')").run();
|
||||
const cfg = getCompressionSettings();
|
||||
assert.equal(cfg.engines.caveman.enabled, true);
|
||||
assert.equal(cfg.activeComboId, null);
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run → FAIL.**
|
||||
- [ ] **Step 3: Implement** the SQL migration (idempotent; the row backfill of derived `engines` is done in `normalizeCompressionSettings` read-path — the SQL only seeds `activeComboId` default and a marker). Migration `102_compression_engines_map.sql`:
|
||||
|
||||
```sql
|
||||
-- Phase 1 of the unified compression panel: the engines map + activeComboId become the
|
||||
-- single source. The engines map is DERIVED on read (normalizeCompressionSettings) from the
|
||||
-- legacy defaultMode + default-combo steps + caveman/rtk/ultra/aggressive config, so existing
|
||||
-- installs keep their behavior. Here we only ensure activeComboId defaults to NULL ("default").
|
||||
INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES ('compression', 'activeComboId', 'null');
|
||||
```
|
||||
|
||||
(The read-path derivation in Task 5 is what makes `getCompressionSettings().engines` correct; the migration just guarantees `activeComboId` exists.)
|
||||
|
||||
- [ ] **Step 4: Run → PASS** (after Task 5's normalize is in — if running 4 before 5, expect the engines assertion to fail; do Task 5 then re-run). Commit after Task 5.
|
||||
|
||||
---
|
||||
|
||||
## Task 5: Persist + normalize `engines` / `activeComboId` (read-path derivation)
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/lib/db/compression.ts` (`getCompressionSettings`/`normalizeCompressionSettings`/`updateCompressionSettings`)
|
||||
- Test: reuse Task 4's migration test + add a round-trip test in the same file.
|
||||
|
||||
- [ ] **Step 1: Add round-trip test**:
|
||||
|
||||
```ts
|
||||
import { updateCompressionSettings } from "@/lib/db/compression.ts";
|
||||
test("engines map persists round-trip + activeComboId", () => {
|
||||
resetDbInstance(); getDbInstance();
|
||||
updateCompressionSettings({ enabled:true, engines:{ rtk:{enabled:true,level:"standard"}, caveman:{enabled:true,level:"full"} }, activeComboId:null });
|
||||
const cfg = getCompressionSettings();
|
||||
assert.equal(cfg.engines.rtk.enabled, true);
|
||||
assert.equal(cfg.engines.rtk.level, "standard");
|
||||
assert.equal(cfg.engines.caveman.level, "full");
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run → FAIL.**
|
||||
- [ ] **Step 3: Implement** in `compression.ts`:
|
||||
- In `normalizeCompressionSettings`: read stored `engines` if present; ELSE derive it from legacy fields — `engines[id].enabled` from: caveman/rtk/ultra/aggressive `*.enabled`; structural engines from the default-combo steps (read the default combo); single-modes from `defaultMode`. Levels from `cavemanConfig.intensity`/`rtkConfig.intensity`. Read `activeComboId` (default null).
|
||||
- In `updateCompressionSettings`: accept `engines` (validate each value `{enabled:boolean, level?:string}`) + `activeComboId` and persist as `key_value` rows (`engines` as one JSON row).
|
||||
- Add a Zod sub-schema `engineToggleSchema = z.object({ enabled: z.boolean(), level: z.string().optional() })` and `engines: z.record(engineToggleSchema).optional()`, `activeComboId: z.string().nullable().optional()`.
|
||||
|
||||
- [ ] **Step 4: Run → PASS** (Task 4 + Task 5 tests). `npm run typecheck:core`.
|
||||
- [ ] **Step 5: Commit** `git add src/lib/db/migrations/102_compression_engines_map.sql src/lib/db/compression.ts tests/unit/compression/compression-engines-map-migration.test.ts && git commit -m "feat(compression): persist+backfill engines map and activeComboId (migration 102)"`
|
||||
|
||||
---
|
||||
|
||||
## Task 6: `resolveCompressionPlan` (precedence resolver)
|
||||
|
||||
**Files:**
|
||||
- Create: `open-sse/services/compression/resolveCompressionPlan.ts`
|
||||
- Test: `tests/unit/compression/resolve-compression-plan.test.ts`
|
||||
|
||||
- [ ] **Step 1: Write the failing test**
|
||||
|
||||
```ts
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { resolveCompressionPlan } from "@omniroute/open-sse/services/compression/resolveCompressionPlan.ts";
|
||||
|
||||
const base = { enabled:true, engines:{ caveman:{enabled:true,level:"full"} }, activeComboId:null, comboOverrides:{} };
|
||||
test("derived default when no override/active/header", () => {
|
||||
assert.deepEqual(resolveCompressionPlan(base, {}), { mode:"standard", stackedPipeline:[] });
|
||||
});
|
||||
test("routing-combo override wins over default", () => {
|
||||
const cfg = { ...base, comboOverrides:{ cmb:"aggressive" } };
|
||||
assert.equal(resolveCompressionPlan(cfg, { comboId:"cmb" }).mode, "aggressive");
|
||||
});
|
||||
test("active named combo wins over default (Phase 2 wiring uses combos table; here pass it in)", () => {
|
||||
const cfg = { ...base, activeComboId:"c1" };
|
||||
const combos = { c1: [{ engine:"rtk", intensity:"standard" }] };
|
||||
const plan = resolveCompressionPlan(cfg, { combos });
|
||||
assert.equal(plan.mode, "stacked");
|
||||
assert.deepEqual(plan.stackedPipeline, [{ engine:"rtk", intensity:"standard" }]);
|
||||
});
|
||||
test("master off => off regardless", () => {
|
||||
assert.equal(resolveCompressionPlan({ ...base, enabled:false }, {}).mode, "off");
|
||||
});
|
||||
```
|
||||
|
||||
- [ ] **Step 2: Run → FAIL.**
|
||||
- [ ] **Step 3: Implement** `resolveCompressionPlan.ts`:
|
||||
|
||||
```ts
|
||||
import { deriveDefaultPlan, type DerivedPlan } from "./deriveDefaultPlan.ts";
|
||||
|
||||
export interface ResolveCtx {
|
||||
comboId?: string | null;
|
||||
header?: string | null; // x-omniroute-compression (Phase 3 parses+passes; Phase 1 callers pass undefined)
|
||||
combos?: Record<string, Array<{ engine:string; intensity?:string }>>; // named combo pipelines by id
|
||||
}
|
||||
export function resolveCompressionPlan(config: any, ctx: ResolveCtx): DerivedPlan {
|
||||
if (config?.enabled === false) return { mode:"off", stackedPipeline:[] };
|
||||
// 1. header (Phase 3 supplies parsed value; here it composes if present)
|
||||
if (ctx.header) {
|
||||
if (ctx.header === "off") return { mode:"off", stackedPipeline:[] };
|
||||
if (ctx.header !== "default") {
|
||||
const fromHeader = headerToPlan(ctx.header, config, ctx);
|
||||
if (fromHeader) return fromHeader; // unknown => fall through
|
||||
}
|
||||
}
|
||||
// 2. routing-combo override
|
||||
const ov = ctx.comboId ? config?.comboOverrides?.[ctx.comboId] : undefined;
|
||||
if (ov) return modeToPlan(ov, config);
|
||||
// 3. active named combo
|
||||
if (config?.activeComboId && ctx.combos?.[config.activeComboId]) {
|
||||
return { mode:"stacked", stackedPipeline: ctx.combos[config.activeComboId] };
|
||||
}
|
||||
// 4. derived default
|
||||
return deriveDefaultPlan(config?.engines ?? {}, config?.enabled !== false);
|
||||
}
|
||||
function modeToPlan(mode: string, config: any): DerivedPlan {
|
||||
return mode === "stacked"
|
||||
? { mode:"stacked", stackedPipeline: config?.stackedPipeline ?? [] }
|
||||
: { mode, stackedPipeline:[] };
|
||||
}
|
||||
function headerToPlan(h: string, config: any, ctx: ResolveCtx): DerivedPlan | null {
|
||||
if (h.startsWith("engine:")) { const id = h.slice(7); return config?.engines?.[id]?.enabled ? deriveDefaultPlan({ [id]: config.engines[id] }, true) : null; }
|
||||
if (ctx.combos?.[h]) return { mode:"stacked", stackedPipeline: ctx.combos[h] };
|
||||
return null;
|
||||
}
|
||||
```
|
||||
|
||||
- [ ] **Step 4: Run → PASS.**
|
||||
- [ ] **Step 5: Commit** `... -m "feat(compression): resolveCompressionPlan precedence resolver (header>override>active>default)"`
|
||||
|
||||
---
|
||||
|
||||
## Task 7: Wire the resolver into strategy selection + chatCore
|
||||
|
||||
**Files:**
|
||||
- Modify: `open-sse/services/compression/strategySelector.ts` (`selectCompressionStrategy`)
|
||||
- Modify: `open-sse/handlers/chatCore.ts` (the compression call site)
|
||||
- Test: `tests/unit/compression/strategySelector.test.ts` (extend with an engines-map case)
|
||||
|
||||
- [ ] **Step 1: Add test** asserting `selectCompressionStrategy` with `engines:{rtk:{enabled:true}}` + master on returns mode `rtk`; with `{rtk,caveman}` returns `stacked`. (Use the existing test's import + harness.)
|
||||
- [ ] **Step 2: Run → FAIL.**
|
||||
- [ ] **Step 3: Implement** — `selectCompressionStrategy` calls `resolveCompressionPlan(config, { comboId, combos })` and returns its `mode` (and expose the `stackedPipeline` so `applyCompressionAsync` uses the derived pipeline when mode==="stacked"). Load named `combos` from the combos DB module. In `chatCore.ts`, pass the active combo set; keep the `header` arg `undefined` (Phase 3 fills it). Keep `autoTriggerMode` behavior (auto-trigger still overrides to its mode on large prompts — apply BEFORE step 4 default).
|
||||
- [ ] **Step 4: Run → PASS** + full compression suite + typecheck.
|
||||
- [ ] **Step 5: Commit** `... -m "feat(compression): selectCompressionStrategy uses resolveCompressionPlan"`
|
||||
|
||||
---
|
||||
|
||||
## Task 8: API — `/api/settings/compression` carries `engines` + `activeComboId`
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app/api/settings/compression/route.ts`
|
||||
- Test: `tests/unit/api/compression/compression-api.test.ts` (extend)
|
||||
|
||||
- [ ] **Step 1: Add test**: `PUT` with `{engines:{rtk:{enabled:true,level:"standard"}}}` then `GET` returns it; error body has no stack (`!body.error?.message?.includes("at /")`).
|
||||
- [ ] **Step 2: Run → FAIL.**
|
||||
- [ ] **Step 3: Implement** — extend the route's Zod body schema with `engines` + `activeComboId` (reuse the db sub-schema); GET returns them; errors via `buildErrorBody`.
|
||||
- [ ] **Step 4: Run → PASS** + vitest if the route is covered there.
|
||||
- [ ] **Step 5: Commit** `... -m "feat(api): settings/compression carries engines map + activeComboId"`
|
||||
|
||||
---
|
||||
|
||||
## Task 9: Remove stored `defaultMode` write-path + default-combo editable route → shim
|
||||
|
||||
**Files:**
|
||||
- Modify: `open-sse/services/compression/types.ts` (drop `defaultMode` from the persisted shape; keep `CompressionMode` type)
|
||||
- Modify: `src/app/api/context/combos/default/route.ts` (PUT → 410/deprecation; GET → derived default read-only)
|
||||
- Test: `tests/unit/api/...` for the shim (PUT rejected, GET returns derived).
|
||||
|
||||
- [ ] **Step 1: Add test**: `PUT /api/context/combos/default` returns a deprecation error (not 200); `GET` returns the derived default pipeline.
|
||||
- [ ] **Step 2: Run → FAIL.**
|
||||
- [ ] **Step 3: Implement** — `setEngineInDefaultCombo` no longer the write path; PUT route returns `buildErrorBody` "deprecated: edit engines in /api/settings/compression"; GET returns `deriveDefaultPlan(config.engines, config.enabled)`. Remove remaining reads of stored `defaultMode` (derive).
|
||||
- [ ] **Step 4: Run → PASS** + full suite + typecheck.
|
||||
- [ ] **Step 5: Commit** `... -m "refactor(compression): derive default; default-combo route is a read-only shim"`
|
||||
|
||||
---
|
||||
|
||||
## Task 10: The engine-grid panel UI
|
||||
|
||||
**Files:**
|
||||
- Create: `src/app/(dashboard)/dashboard/context/settings/CompressionPanel.tsx`
|
||||
- Modify: `src/app/(dashboard)/dashboard/context/settings/page.tsx` (render `CompressionPanel`)
|
||||
- Test: `tests/unit/ui/compressionPanel.test.tsx` (vitest, `createRoot`+`act`)
|
||||
|
||||
- [ ] **Step 1: Write the failing component test**: render `CompressionPanel` with a stubbed `fetch` returning `{enabled:true, engines:{rtk:{enabled:true,level:"standard"}}}`; assert it renders a row per `ENGINE_IDS`, the rtk level shows "standard", toggling caveman issues a `PUT` with `engines.caveman.enabled:true`, and the derived-pipeline preview text appears.
|
||||
- [ ] **Step 2: Run → FAIL** (`npm run test:vitest`).
|
||||
- [ ] **Step 3: Implement** `CompressionPanel.tsx`: master toggle; map `ENGINE_IDS` → a row component `[label+desc][Toggle][LevelSelect if meta.levels][Link → /dashboard/context/<id>]`; a derived-pipeline preview computed client-side via `deriveDefaultPlan` (import the pure fn); a `cavemanOutput` row; an `mcpAccessibility` row (writes its own endpoint, with a "MCP tool outputs" note); general settings (auto-trigger, preserve system prompt). Save via `PUT /api/settings/compression` (debounced, merge-patch like the existing `save()` pattern in `CompressionSettingsTab`). Reuse existing primitives (`Toggle`, segmented control) from the current cards.
|
||||
- [ ] **Step 4:** Update `page.tsx` to render `<CompressionPanel/>`. Run → PASS.
|
||||
- [ ] **Step 5: Commit** `... -m "feat(dashboard): engine-grid compression panel (single source for on/off + level)"`
|
||||
|
||||
---
|
||||
|
||||
## Task 11: Consolidate — remove scattered/duplicate toggles + per-engine on/off
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/app/(dashboard)/dashboard/settings/components/CompressionTokenSaverCard.tsx` (remove; or strip to a read-only summary linking to the panel)
|
||||
- Modify: `CompressionSettingsTab.tsx` (remove duplicate caveman/rtk on/off + intensity sections; keep only things not in the panel, or delete if fully superseded)
|
||||
- Modify: `src/shared/components/compression/EngineConfigPage.tsx` + `CavemanContextPageClient.tsx` + `RtkContextPageClient.tsx` (remove on/off + level; keep detailed config; stop writing `/api/context/combos/default`)
|
||||
- Test: vitest render tests for the per-engine pages assert NO enabled toggle is present; the existing tests updated to the new shape (alignment, not masking).
|
||||
|
||||
- [ ] **Step 1:** Update the affected render tests to expect the new (toggle-free) shape; run → FAIL on the still-present toggles.
|
||||
- [ ] **Step 2:** Implement the removals; per-engine detail save writes detailed config to its facade route (caveman/rtk) or the settings sub-object.
|
||||
- [ ] **Step 3:** Run vitest + full compression suite → PASS.
|
||||
- [ ] **Step 4: Commit** `... -m "refactor(dashboard): remove duplicate compression toggles; per-engine pages keep only detailed config"`
|
||||
|
||||
---
|
||||
|
||||
## Task 12: Menu reorder + integration + full validation
|
||||
|
||||
**Files:**
|
||||
- Modify: `src/shared/constants/sidebarVisibility.ts` (`COMPRESSION_CONTEXT_GROUP`: Settings → Combos → per-engine → Studio)
|
||||
- Test: `tests/unit/...` sidebar order test (if one exists) + an integration test.
|
||||
|
||||
- [ ] **Step 1:** Add an integration test: build a config with `engines:{rtk:{enabled:true},caveman:{enabled:true,level:"full"}}`, call `selectCompressionStrategy` + `applyCompressionAsync` on a realistic body, assert the derived stacked pipeline ran (engineBreakdown has rtk+caveman) and equals the behavior of an explicit `[rtk,caveman]` stacked config. Run → (write fails first if any wiring gap).
|
||||
- [ ] **Step 2:** Reorder the sidebar group; update any sidebar order test (alignment).
|
||||
- [ ] **Step 3:** FULL validation: `npm run typecheck:core` (clean) · `npm run lint` (0 errors) · `node --import tsx/esm --test tests/unit/compression/*.test.ts` (green) · `npm run test:vitest` (green) · the api/integration compression tests.
|
||||
- [ ] **Step 4: Commit** `... -m "feat(compression): unified panel menu order + integration coverage"`
|
||||
|
||||
---
|
||||
|
||||
## Self-review notes (done while writing)
|
||||
- Spec coverage: panel (T10), combos boundary (default derived T3/T9; named/active resolver T6 — UI for active selection is Phase 2), per-engine detail-only (T11), header (resolver is header-aware T6; parsing+wiring is Phase 3), migration (T4/T5), menu (T12). ✓
|
||||
- Type consistency: `EngineToggle` (T2) used by `deriveDefaultPlan` (T3), `resolveCompressionPlan` (T6), normalize (T5), panel (T10). `DerivedPlan` shape consistent T3↔T6↔T7. ✓
|
||||
- No placeholders: each task has concrete code/tests. UI tasks (T10/T11) specify exact behavior + assertions; final per-line component code is produced at execution following the existing card patterns.
|
||||
|
||||
---
|
||||
|
||||
# 📌 RESUMO — o que ESTE plano (Fase 1) entrega
|
||||
|
||||
1. **Catálogo de engines** (`engineCatalog.ts`) — fonte única de metadados (níveis, single-mode, ordem).
|
||||
2. **Modelo de dados Fase A** — `engines` map + `activeComboId` em `CompressionConfig`, com migração 102 + backfill (zero mudança de comportamento).
|
||||
3. **`deriveDefaultPlan`** — pipeline default DERIVADO dos toggles (0/1/N engines → off/modo/stacked).
|
||||
4. **`resolveCompressionPlan`** — resolvedor de precedência (header > override por-rota > perfil ativo > default derivado > off), já header/active-aware.
|
||||
5. **Fiação no runtime** — `selectCompressionStrategy`/`chatCore` usam o resolvedor.
|
||||
6. **API** — `/api/settings/compression` carrega `engines` + `activeComboId`; rota `combos/default` vira shim read-only; `defaultMode` armazenado removido (derivado).
|
||||
7. **Painel engine-grid** — `CompressionPanel.tsx` como fonte única de master + on/off + nível, com preview do pipeline derivado.
|
||||
8. **Consolidação** — remove toggles duplicados (TokenSaverCard, CompressionSettingsTab) e tira on/off+nível das páginas por-engine (que ficam só com config detalhada).
|
||||
9. **Menu** — Settings → Combos → páginas por-engine → Studio.
|
||||
|
||||
---
|
||||
|
||||
# ⏳ PENDÊNCIAS — a fazer DEPOIS deste plano (cada uma vira seu próprio plano/PR)
|
||||
|
||||
### Fase 2 — Perfis nomeados + seletor de ativo
|
||||
- **UI de combos como perfis**: a página `context/combos` lista N combos nomeados, edita pipeline **ordenado** (drag/reorder + nível por step), e tem o seletor **"perfil ativo"** (`Default` | `<combo>`) gravando `activeComboId`. O resolvedor (Fase 1) já consome `activeComboId`; falta a UI + o carregamento dos combos nomeados no `selectCompressionStrategy`. Remover o "master mode selector" do `CompressionHub` (modo agora é derivado).
|
||||
|
||||
### Fase 3 — Header por-request `x-omniroute-compression`
|
||||
- **Parsing + wiring do header**: ler `x-omniroute-compression` no pipeline (espelhando `x-omniroute-no-memory`, PR #4290), passar como `ctx.header` ao `resolveCompressionPlan` (que já trata `off`/`default`/`<combo>`/`engine:<id>`). Doc no `API_REFERENCE` + teste de fetch-capture provando precedência por-request.
|
||||
|
||||
### Itens de compressão deferidos (do ciclo de fixes, independentes deste painel)
|
||||
- **B-OBSERVABILITY** (telemetria): engines no-op somem do `engineBreakdown` (não dá pra distinguir "rodou 0%" de "pulou"). Exige um refactor do modelo de breakdown (campo `ran`/`skipped`) que toca a UI Studio + testes que asseram `.length` — deferido conscientemente.
|
||||
- **B-CAVEMAN-PACKS**: `de`/`fr`/`ja` sem `dedup.json`+`ultra.json` (ultra==full nessas línguas). Conteúdo linguístico — adicionar os packs (ou contribuição), **sem** fallback EN que mutilaria.
|
||||
- **js-tiktoken 1.0.21→1.0.22**: bump trivial (o range `^1.0.20` já permite; só pinar o lockfile num install real).
|
||||
|
||||
### Decisão operacional pendente (não-código)
|
||||
- **Ligar o SLM (tier ultra) em produção**: validado ao vivo (49,4% real), mas mantido **OFF** por sua escolha. Quando decidir o trade-off (custo/latência/qualidade do pruning), ligar via o painel (após Fase 1) ou pela escrita de config — começando conservador (≥2000 tok).
|
||||
|
||||
### Portes upstream (opcionais, do audit — fora do escopo deste painel)
|
||||
- headroom *safety-rails*/BM25; filtros novos do rtk/token-savior; conformance GCF v3.1 (já cobrimos o `[..]:`); transformers.js 3.5.2→4.x (arriscado, major).
|
||||
@@ -1443,8 +1443,13 @@ export async function handleChatCore({
|
||||
// --- Modular Compression Pipeline (Phase 1 Lite + Phase 2 Standard/Caveman + Phase 3 Aggressive) ---
|
||||
// Runs BEFORE the existing reactive compressContext() to proactively reduce tokens.
|
||||
try {
|
||||
const { selectCompressionStrategy, applyCompressionAsync, resolveCacheAwareConfig } =
|
||||
await import("../services/compression/strategySelector.ts");
|
||||
const {
|
||||
selectCompressionStrategy,
|
||||
selectCompressionPlan,
|
||||
enginesMapDerivesStackedPipeline,
|
||||
applyCompressionAsync,
|
||||
resolveCacheAwareConfig,
|
||||
} = await import("../services/compression/strategySelector.ts");
|
||||
const { trackCompressionStats } = await import("../services/compression/stats.ts");
|
||||
let config: CompressionConfig = compressionSettings ?? {
|
||||
enabled: false,
|
||||
@@ -1622,7 +1627,12 @@ export async function handleChatCore({
|
||||
modeBeforeOutputTransform === "stacked" &&
|
||||
!compressionComboApplied &&
|
||||
!config.compressionComboId &&
|
||||
isBuiltinStackedPipeline(config.stackedPipeline)
|
||||
isBuiltinStackedPipeline(config.stackedPipeline) &&
|
||||
// Don't let the legacy default combo override a panel-configured engines map: when the
|
||||
// operator's explicit engines derive their own stacked pipeline, that pipeline (applied
|
||||
// below from compressionPlan.stackedPipeline) is authoritative. Legacy/backfilled
|
||||
// installs (enginesExplicit false) still fall through to the seeded default combo.
|
||||
!enginesMapDerivesStackedPipeline(config)
|
||||
) {
|
||||
try {
|
||||
const { getDefaultCompressionCombo } =
|
||||
@@ -1672,13 +1682,29 @@ export async function handleChatCore({
|
||||
}
|
||||
}
|
||||
const compressionInputBody = body as Record<string, unknown>;
|
||||
const mode = selectCompressionStrategy(
|
||||
const compressionPlan = selectCompressionPlan(
|
||||
config,
|
||||
compressionComboKey,
|
||||
estimatedTokens,
|
||||
compressionInputBody,
|
||||
{ provider, targetFormat, model: effectiveModel }
|
||||
);
|
||||
const mode = compressionPlan.mode as CompressionConfig["defaultMode"];
|
||||
// When the per-engine toggle map derives a stacked pipeline (and no named/routing
|
||||
// combo already set config.stackedPipeline), feed that derived pipeline through so
|
||||
// applyCompressionAsync (which reads config.stackedPipeline for stacked mode) runs the
|
||||
// engines the operator actually toggled on instead of the built-in rtk+caveman default.
|
||||
if (
|
||||
mode === "stacked" &&
|
||||
compressionPlan.stackedPipeline.length > 0 &&
|
||||
!compressionComboApplied &&
|
||||
!config.compressionComboId
|
||||
) {
|
||||
config = {
|
||||
...config,
|
||||
stackedPipeline: compressionPlan.stackedPipeline as CompressionConfig["stackedPipeline"],
|
||||
};
|
||||
}
|
||||
let compressionAnalyticsRecorded = false;
|
||||
if (mode !== "off") {
|
||||
// #3890: in a caching context, never compress the system prompt (cacheable prefix)
|
||||
|
||||
48
open-sse/services/compression/deriveDefaultPlan.ts
Normal file
48
open-sse/services/compression/deriveDefaultPlan.ts
Normal file
@@ -0,0 +1,48 @@
|
||||
import { ENGINE_CATALOG, engineMeta } from "./engineCatalog.ts";
|
||||
import type { EngineToggle } from "./types.ts";
|
||||
|
||||
/** Maps single-mode engine ids to the effective CompressionMode name. */
|
||||
const SINGLE_MODE_OF: Record<string, string> = {
|
||||
lite: "lite",
|
||||
caveman: "standard",
|
||||
aggressive: "aggressive",
|
||||
ultra: "ultra",
|
||||
rtk: "rtk",
|
||||
};
|
||||
|
||||
export interface DerivedPlan {
|
||||
mode: string;
|
||||
stackedPipeline: Array<{ engine: string; intensity?: string }>;
|
||||
}
|
||||
|
||||
/**
|
||||
* Derives the effective compression plan from the per-engine toggle map.
|
||||
*
|
||||
* Rules (evaluated in order):
|
||||
* 1. masterEnabled=false OR no engines on → { mode:"off", stackedPipeline:[] }
|
||||
* 2. Exactly one engine on AND it is single-mode → that engine's standalone mode
|
||||
* 3. Otherwise → { mode:"stacked", stackedPipeline: enabled engines sorted by stackPriority }
|
||||
*/
|
||||
export function deriveDefaultPlan(
|
||||
engines: Record<string, EngineToggle>,
|
||||
masterEnabled: boolean,
|
||||
): DerivedPlan {
|
||||
if (!masterEnabled) return { mode: "off", stackedPipeline: [] };
|
||||
|
||||
const onIds = Object.keys(ENGINE_CATALOG).filter((id) => engines[id]?.enabled === true);
|
||||
|
||||
if (onIds.length === 0) return { mode: "off", stackedPipeline: [] };
|
||||
|
||||
if (onIds.length === 1 && engineMeta(onIds[0]).isSingleMode) {
|
||||
return { mode: SINGLE_MODE_OF[onIds[0]], stackedPipeline: [] };
|
||||
}
|
||||
|
||||
const ordered = onIds.sort((a, b) => engineMeta(a).stackPriority - engineMeta(b).stackPriority);
|
||||
|
||||
const stackedPipeline = ordered.map((id) => {
|
||||
const level = engines[id]?.level;
|
||||
return level ? { engine: id, intensity: level } : { engine: id };
|
||||
});
|
||||
|
||||
return { mode: "stacked", stackedPipeline };
|
||||
}
|
||||
84
open-sse/services/compression/engineCatalog.ts
Normal file
84
open-sse/services/compression/engineCatalog.ts
Normal file
@@ -0,0 +1,84 @@
|
||||
export interface EngineMeta {
|
||||
id: string;
|
||||
label: string;
|
||||
stackPriority: number;
|
||||
levels?: string[]; // intensity options; undefined = no level selector
|
||||
isSingleMode: boolean; // can be the effective mode when it is the only engine on
|
||||
description: string;
|
||||
}
|
||||
|
||||
export const ENGINE_CATALOG: Record<string, EngineMeta> = {
|
||||
"session-dedup": {
|
||||
id: "session-dedup",
|
||||
label: "Session Dedup",
|
||||
stackPriority: 3,
|
||||
isSingleMode: false,
|
||||
description: "Cross-turn block deduplication.",
|
||||
},
|
||||
ccr: {
|
||||
id: "ccr",
|
||||
label: "CCR (Retrieval)",
|
||||
stackPriority: 4,
|
||||
isSingleMode: false,
|
||||
description: "Content-addressed retrieval markers.",
|
||||
},
|
||||
lite: {
|
||||
id: "lite",
|
||||
label: "Lite",
|
||||
stackPriority: 5,
|
||||
isSingleMode: true,
|
||||
description: "Whitespace/format cleanup.",
|
||||
},
|
||||
rtk: {
|
||||
id: "rtk",
|
||||
label: "RTK",
|
||||
stackPriority: 10,
|
||||
levels: ["minimal", "standard", "aggressive"],
|
||||
isSingleMode: true,
|
||||
description: "Command-output filtering.",
|
||||
},
|
||||
headroom: {
|
||||
id: "headroom",
|
||||
label: "Headroom",
|
||||
stackPriority: 15,
|
||||
isSingleMode: false,
|
||||
description: "Tabular JSON compaction.",
|
||||
},
|
||||
caveman: {
|
||||
id: "caveman",
|
||||
label: "Caveman",
|
||||
stackPriority: 20,
|
||||
levels: ["lite", "full", "ultra"],
|
||||
isSingleMode: true,
|
||||
description: "Rule-based prose compression.",
|
||||
},
|
||||
aggressive: {
|
||||
id: "aggressive",
|
||||
label: "Aggressive",
|
||||
stackPriority: 30,
|
||||
isSingleMode: true,
|
||||
description: "Summarize + age old turns.",
|
||||
},
|
||||
llmlingua: {
|
||||
id: "llmlingua",
|
||||
label: "LLMLingua (SLM)",
|
||||
stackPriority: 35,
|
||||
isSingleMode: false,
|
||||
description: "Semantic pruning (ONNX).",
|
||||
},
|
||||
ultra: {
|
||||
id: "ultra",
|
||||
label: "Ultra",
|
||||
stackPriority: 40,
|
||||
isSingleMode: true,
|
||||
description: "Heuristic token pruning (+ optional SLM).",
|
||||
},
|
||||
};
|
||||
|
||||
export const ENGINE_IDS: string[] = Object.values(ENGINE_CATALOG)
|
||||
.sort((a, b) => a.stackPriority - b.stackPriority)
|
||||
.map((e) => e.id);
|
||||
|
||||
export function engineMeta(id: string): EngineMeta {
|
||||
return ENGINE_CATALOG[id];
|
||||
}
|
||||
47
open-sse/services/compression/resolveCompressionPlan.ts
Normal file
47
open-sse/services/compression/resolveCompressionPlan.ts
Normal file
@@ -0,0 +1,47 @@
|
||||
import { deriveDefaultPlan, type DerivedPlan } from "./deriveDefaultPlan.ts";
|
||||
|
||||
export interface ResolveCtx {
|
||||
comboId?: string | null;
|
||||
header?: string | null; // x-omniroute-compression (Phase 3 parses+passes; Phase 1 callers pass undefined)
|
||||
combos?: Record<string, Array<{ engine: string; intensity?: string }>>; // named combo pipelines by id
|
||||
}
|
||||
|
||||
export function resolveCompressionPlan(config: any, ctx: ResolveCtx): DerivedPlan {
|
||||
if (config?.enabled === false) return { mode: "off", stackedPipeline: [] };
|
||||
|
||||
// 1. header (Phase 3 supplies parsed value; here it composes if present)
|
||||
if (ctx.header) {
|
||||
if (ctx.header === "off") return { mode: "off", stackedPipeline: [] };
|
||||
if (ctx.header !== "default") {
|
||||
const fromHeader = headerToPlan(ctx.header, config, ctx);
|
||||
if (fromHeader) return fromHeader; // unknown => fall through
|
||||
}
|
||||
}
|
||||
|
||||
// 2. routing-combo override
|
||||
const ov = ctx.comboId ? config?.comboOverrides?.[ctx.comboId] : undefined;
|
||||
if (ov) return modeToPlan(ov, config);
|
||||
|
||||
// 3. active named combo
|
||||
if (config?.activeComboId && ctx.combos?.[config.activeComboId]) {
|
||||
return { mode: "stacked", stackedPipeline: ctx.combos[config.activeComboId] };
|
||||
}
|
||||
|
||||
// 4. derived default
|
||||
return deriveDefaultPlan(config?.engines ?? {}, config?.enabled !== false);
|
||||
}
|
||||
|
||||
function modeToPlan(mode: string, config: any): DerivedPlan {
|
||||
return mode === "stacked"
|
||||
? { mode: "stacked", stackedPipeline: config?.stackedPipeline ?? [] }
|
||||
: { mode, stackedPipeline: [] };
|
||||
}
|
||||
|
||||
function headerToPlan(h: string, config: any, ctx: ResolveCtx): DerivedPlan | null {
|
||||
if (h.startsWith("engine:")) {
|
||||
const id = h.slice(7);
|
||||
return config?.engines?.[id]?.enabled ? deriveDefaultPlan({ [id]: config.engines[id] }, true) : null;
|
||||
}
|
||||
if (ctx.combos?.[h]) return { mode: "stacked", stackedPipeline: ctx.combos[h] };
|
||||
return null;
|
||||
}
|
||||
@@ -20,6 +20,8 @@ import {
|
||||
getCacheAwareStrategy,
|
||||
type CachingDetectionContext,
|
||||
} from "./cachingAware.ts";
|
||||
import { resolveCompressionPlan } from "./resolveCompressionPlan.ts";
|
||||
import { deriveDefaultPlan, type DerivedPlan } from "./deriveDefaultPlan.ts";
|
||||
|
||||
export function checkComboOverride(
|
||||
config: CompressionConfig,
|
||||
@@ -33,19 +35,122 @@ export function shouldAutoTrigger(config: CompressionConfig, estimatedTokens: nu
|
||||
return config.autoTriggerTokens > 0 && estimatedTokens >= config.autoTriggerTokens;
|
||||
}
|
||||
|
||||
/**
|
||||
* Resolves the effective compression plan (mode + derived stacked pipeline) WITHOUT
|
||||
* the caching-aware mode adjustment (that is layered on by {@link selectCompressionPlan}).
|
||||
*
|
||||
* Precedence — preserved from the historical {@link getEffectiveMode} ordering:
|
||||
* 1. master off → off
|
||||
* 2. routing-combo override (comboId)→ that mode (resolver honors it via ctx.comboId)
|
||||
* 3. auto-trigger (large prompt) → autoTriggerMode, BEFORE the plain derived default
|
||||
* 4. derived default → resolveCompressionPlan (engines map → mode/pipeline)
|
||||
*
|
||||
* Step 3 mirrors today's behaviour: auto-trigger takes precedence over the plain
|
||||
* derived default but never over an explicit routing-combo override.
|
||||
*
|
||||
* `combos` is `{}` in Phase 1 — the active named-combo selection UI is Phase 2, and the
|
||||
* resolver falls through to the derived default when no combo is supplied. chatCore still
|
||||
* resolves named/default combos via its own DB path (mutating config.stackedPipeline).
|
||||
*/
|
||||
function resolveBasePlan(
|
||||
config: CompressionConfig,
|
||||
comboId: string | null,
|
||||
estimatedTokens: number
|
||||
): DerivedPlan {
|
||||
if (!config.enabled) return { mode: "off", stackedPipeline: [] };
|
||||
|
||||
const comboMode = checkComboOverride(config, comboId);
|
||||
if (comboMode) {
|
||||
// A routing-combo "stacked" override still wants the configured stacked pipeline,
|
||||
// so route it through the resolver (which reads config.stackedPipeline for stacked).
|
||||
return resolveCompressionPlan(config, { comboId, combos: {} });
|
||||
}
|
||||
|
||||
if (shouldAutoTrigger(config, estimatedTokens)) {
|
||||
const mode = config.autoTriggerMode ?? "lite";
|
||||
return mode === "stacked"
|
||||
? { mode, stackedPipeline: config.stackedPipeline ?? [] }
|
||||
: { mode, stackedPipeline: [] };
|
||||
}
|
||||
|
||||
return deriveDefaultPlanFromConfig(config, comboId);
|
||||
}
|
||||
|
||||
/**
|
||||
* Derived-default step. The per-engine toggle map drives the default ONLY when it was
|
||||
* EXPLICITLY configured via the panel (a stored `engines` row — `config.enginesExplicit`).
|
||||
* For legacy installs the map is backfilled for DISPLAY only (so the panel shows current
|
||||
* state); dispatch falls back to the historical `config.defaultMode` so behaviour is
|
||||
* byte-for-byte preserved until the operator opts into the panel by saving. This avoids a
|
||||
* silent behaviour change for installs whose backfilled engine flags don't exactly match
|
||||
* their old defaultMode.
|
||||
*/
|
||||
function deriveDefaultPlanFromConfig(
|
||||
config: CompressionConfig,
|
||||
comboId: string | null
|
||||
): DerivedPlan {
|
||||
if (config.enginesExplicit) {
|
||||
// Panel-configured: the engines map (via the resolver, which stays header/active-combo
|
||||
// aware for Phases 2-3) is authoritative — including an explicit "everything off".
|
||||
return resolveCompressionPlan(config, { comboId, combos: {} });
|
||||
}
|
||||
|
||||
// Legacy path: defaultMode carries the effective mode (the engines map is display-only here).
|
||||
const legacyMode = config.defaultMode;
|
||||
if (legacyMode && legacyMode !== "off") {
|
||||
return legacyMode === "stacked"
|
||||
? { mode: legacyMode, stackedPipeline: config.stackedPipeline ?? [] }
|
||||
: { mode: legacyMode, stackedPipeline: [] };
|
||||
}
|
||||
|
||||
return { mode: "off", stackedPipeline: [] };
|
||||
}
|
||||
|
||||
/**
|
||||
* True when the EXPLICITLY-configured engines map (panel-saved) derives a multi-engine
|
||||
* stacked pipeline. chatCore uses this to know the panel's derived pipeline is authoritative
|
||||
* and the legacy default-combo fallback must NOT override it. Returns false for legacy
|
||||
* (non-explicit) installs so their historical default-combo path is preserved untouched.
|
||||
*/
|
||||
export function enginesMapDerivesStackedPipeline(config: CompressionConfig): boolean {
|
||||
if (!config.enginesExplicit) return false;
|
||||
const plan = deriveDefaultPlan(config.engines ?? {}, config.enabled !== false);
|
||||
return plan.mode === "stacked" && plan.stackedPipeline.length > 0;
|
||||
}
|
||||
|
||||
export function getEffectiveMode(
|
||||
config: CompressionConfig,
|
||||
comboId: string | null,
|
||||
estimatedTokens: number
|
||||
): CompressionMode {
|
||||
if (!config.enabled) return "off";
|
||||
return resolveBasePlan(config, comboId, estimatedTokens).mode as CompressionMode;
|
||||
}
|
||||
|
||||
const comboMode = checkComboOverride(config, comboId);
|
||||
if (comboMode) return comboMode;
|
||||
/**
|
||||
* Like {@link selectCompressionStrategy} but returns the full derived plan
|
||||
* (effective `mode` + `stackedPipeline`). When the resolver derives a `stacked`
|
||||
* plan from the per-engine toggle map, the pipeline is exposed here so the caller
|
||||
* can feed it to {@link applyCompressionAsync} (which reads config.stackedPipeline).
|
||||
* The caching-aware mode adjustment is applied to `mode` exactly as in
|
||||
* {@link selectCompressionStrategy}.
|
||||
*/
|
||||
export function selectCompressionPlan(
|
||||
config: CompressionConfig,
|
||||
comboId: string | null,
|
||||
estimatedTokens: number,
|
||||
body?: Record<string, unknown>,
|
||||
context?: CachingDetectionContext
|
||||
): DerivedPlan {
|
||||
const plan = resolveBasePlan(config, comboId, estimatedTokens);
|
||||
|
||||
if (shouldAutoTrigger(config, estimatedTokens)) return config.autoTriggerMode ?? "lite";
|
||||
// Apply caching-aware adjustments to the mode if body is provided
|
||||
if (body) {
|
||||
const ctx = detectCachingContext(body, context);
|
||||
const cacheAware = getCacheAwareStrategy(plan.mode as CompressionMode, ctx);
|
||||
return { ...plan, mode: cacheAware.strategy as CompressionMode };
|
||||
}
|
||||
|
||||
return config.defaultMode;
|
||||
return plan;
|
||||
}
|
||||
|
||||
export function selectCompressionStrategy(
|
||||
@@ -55,16 +160,7 @@ export function selectCompressionStrategy(
|
||||
body?: Record<string, unknown>,
|
||||
context?: CachingDetectionContext
|
||||
): CompressionMode {
|
||||
const selectedMode = getEffectiveMode(config, comboId, estimatedTokens);
|
||||
|
||||
// Apply caching-aware adjustments if body is provided
|
||||
if (body) {
|
||||
const ctx = detectCachingContext(body, context);
|
||||
const cacheAware = getCacheAwareStrategy(selectedMode, ctx);
|
||||
return cacheAware.strategy as CompressionMode;
|
||||
}
|
||||
|
||||
return selectedMode;
|
||||
return selectCompressionPlan(config, comboId, estimatedTokens, body, context).mode as CompressionMode;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -82,6 +178,10 @@ export function resolveCacheAwareConfig(
|
||||
): CompressionConfig {
|
||||
if (!body) return config;
|
||||
const ctx = detectCachingContext(body, context);
|
||||
// Only `skipSystemPrompt` is consumed here, and it depends solely on `ctx.isCachingProvider`
|
||||
// (NOT on the strategy arg — see getCacheAwareStrategy), so the stored `defaultMode` is a safe
|
||||
// input even though it may be "off" for a panel-configured install. If getCacheAwareStrategy is
|
||||
// ever extended to key `skipSystemPrompt` on the mode, pass the resolved effective mode instead.
|
||||
const cacheAware = getCacheAwareStrategy(config.defaultMode, ctx);
|
||||
if (cacheAware.skipSystemPrompt && config.preserveSystemPrompt === false) {
|
||||
return { ...config, preserveSystemPrompt: true };
|
||||
|
||||
@@ -9,6 +9,15 @@
|
||||
* Phase 5: 'rtk' and 'stacked' modes (tool-output filters + multi-engine pipeline).
|
||||
*/
|
||||
|
||||
import { ENGINE_IDS } from "./engineCatalog.ts";
|
||||
|
||||
// Re-export so consumers that already import from this module (e.g. src/lib/db/compression.ts)
|
||||
// can get ENGINE_IDS without a second bare `@omniroute/open-sse/...engineCatalog.ts` specifier.
|
||||
// That bare alias resolves under tsc/tsx but NOT under vitest (Vite externalizes a brand-new
|
||||
// open-sse module to Node, which then can't load the `.ts` subpath), whereas this module is
|
||||
// already in Vite's graph and its relative `./engineCatalog.ts` import resolves in-pipeline.
|
||||
export { ENGINE_IDS };
|
||||
|
||||
export type CompressionMode =
|
||||
| "off"
|
||||
| "lite"
|
||||
@@ -108,6 +117,11 @@ export interface CompressionPipelineStep {
|
||||
config?: Record<string, unknown>;
|
||||
}
|
||||
|
||||
export interface EngineToggle {
|
||||
enabled: boolean;
|
||||
level?: string;
|
||||
}
|
||||
|
||||
export interface CompressionConfig {
|
||||
enabled: boolean;
|
||||
defaultMode: CompressionMode;
|
||||
@@ -127,6 +141,17 @@ export interface CompressionConfig {
|
||||
ultra?: UltraConfig;
|
||||
/** Provider-delegated context editing (Claude/Anthropic only). */
|
||||
contextEditing?: ContextEditingConfig;
|
||||
/** Per-engine opt-in toggles for the config panel. */
|
||||
engines: Record<string, EngineToggle>;
|
||||
/** Active combo preset id, or null if none selected. */
|
||||
activeComboId: string | null;
|
||||
/**
|
||||
* Runtime-only (NOT persisted): true when a stored `engines` row exists, i.e. the operator
|
||||
* configured engines via the panel. When false, the `engines` map is a display-only backfill
|
||||
* and dispatch falls back to the legacy `defaultMode`/default-combo path (zero behaviour
|
||||
* change for installs that predate the panel). Set by `getCompressionSettings`.
|
||||
*/
|
||||
enginesExplicit?: boolean;
|
||||
}
|
||||
|
||||
export interface CompressionStats {
|
||||
@@ -193,6 +218,8 @@ export const DEFAULT_COMPRESSION_CONFIG: CompressionConfig = {
|
||||
{ engine: "rtk", intensity: "standard" },
|
||||
{ engine: "caveman", intensity: "full" },
|
||||
],
|
||||
engines: Object.fromEntries(ENGINE_IDS.map((id) => [id, { enabled: false }])),
|
||||
activeComboId: null,
|
||||
};
|
||||
|
||||
export const DEFAULT_CAVEMAN_CONFIG: CavemanConfig = {
|
||||
|
||||
@@ -84,10 +84,6 @@ export default function CavemanContextPageClient() {
|
||||
intensity: "lite",
|
||||
autoClarity: true,
|
||||
};
|
||||
const inputMode: InputModeConfig = {
|
||||
enabled: settings?.cavemanConfig?.enabled ?? false,
|
||||
intensity: (settings?.cavemanConfig?.intensity as InputModeConfig["intensity"]) ?? "lite",
|
||||
};
|
||||
const masterEnabled = settings?.enabled ?? false;
|
||||
|
||||
const saveSettings = async (patch: Partial<CompressionSettings>) => {
|
||||
@@ -108,11 +104,6 @@ export default function CavemanContextPageClient() {
|
||||
saveSettings({ languageConfig: { ...languageConfig, ...patch } });
|
||||
};
|
||||
|
||||
const updateInputMode = (patch: Partial<InputModeConfig>) => {
|
||||
const current = settings?.cavemanConfig ?? {};
|
||||
saveSettings({ cavemanConfig: { ...current, ...inputMode, ...patch } });
|
||||
};
|
||||
|
||||
const updateOutputMode = (patch: Partial<OutputModeConfig>) => {
|
||||
saveSettings({ cavemanOutputMode: { ...outputMode, ...patch } });
|
||||
};
|
||||
@@ -232,34 +223,6 @@ export default function CavemanContextPageClient() {
|
||||
</div>
|
||||
)}
|
||||
|
||||
<section className="rounded-lg border border-border bg-surface p-4">
|
||||
<h2 className="text-sm font-semibold text-text-main">{t("inputCompressionTitle")}</h2>
|
||||
<p className="mt-1 text-xs text-text-muted">{t("inputCompressionDesc")}</p>
|
||||
<div className="mt-3 flex flex-wrap gap-4 text-sm text-text-main">
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={inputMode.enabled}
|
||||
disabled={saving}
|
||||
onChange={(event) => updateInputMode({ enabled: event.target.checked })}
|
||||
/>
|
||||
{t("enabled")}
|
||||
</label>
|
||||
<select
|
||||
value={inputMode.intensity}
|
||||
disabled={saving}
|
||||
onChange={(event) =>
|
||||
updateInputMode({ intensity: event.target.value as InputModeConfig["intensity"] })
|
||||
}
|
||||
className="rounded-lg border border-border bg-bg px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="lite">lite</option>
|
||||
<option value="full">full</option>
|
||||
<option value="ultra">ultra</option>
|
||||
</select>
|
||||
</div>
|
||||
</section>
|
||||
|
||||
<section className="grid grid-cols-1 gap-4 lg:grid-cols-2">
|
||||
<div className="rounded-lg border border-border bg-surface p-4">
|
||||
<h2 className="text-sm font-semibold text-text-main">{t("analyticsTitle")}</h2>
|
||||
@@ -282,16 +245,9 @@ export default function CavemanContextPageClient() {
|
||||
<div className="rounded-lg border border-border bg-surface p-4">
|
||||
<h2 className="text-sm font-semibold text-text-main">{t("outputModeTitle")}</h2>
|
||||
<p className="mt-1 text-xs text-text-muted">{t("outputModeDesc")}</p>
|
||||
{/* On/off + intensity for caveman output mode live in the panel
|
||||
(/dashboard/context/settings). This page keeps the detailed knobs only. */}
|
||||
<div className="mt-3 flex flex-wrap gap-4 text-sm text-text-main">
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={outputMode.enabled}
|
||||
disabled={saving}
|
||||
onChange={(event) => updateOutputMode({ enabled: event.target.checked })}
|
||||
/>
|
||||
{t("enabled")}
|
||||
</label>
|
||||
<label className="flex items-center gap-2">
|
||||
<input
|
||||
type="checkbox"
|
||||
@@ -301,18 +257,6 @@ export default function CavemanContextPageClient() {
|
||||
/>
|
||||
{t("autoClarity")}
|
||||
</label>
|
||||
<select
|
||||
value={outputMode.intensity}
|
||||
disabled={saving}
|
||||
onChange={(event) =>
|
||||
updateOutputMode({ intensity: event.target.value as OutputModeConfig["intensity"] })
|
||||
}
|
||||
className="rounded-lg border border-border bg-bg px-3 py-2 text-sm"
|
||||
>
|
||||
<option value="lite">lite</option>
|
||||
<option value="full">full</option>
|
||||
<option value="ultra">ultra</option>
|
||||
</select>
|
||||
</div>
|
||||
<pre className="mt-3 overflow-auto rounded-lg border border-border bg-bg p-3 text-xs text-text-main">
|
||||
{previewPrompt}
|
||||
|
||||
@@ -172,64 +172,10 @@ export default function CompressionHub() {
|
||||
[settings]
|
||||
);
|
||||
|
||||
// ── Toggle a layer (enable/disable) ───────────────────────────────────────────
|
||||
// Routed through the dedicated `/default` endpoint (setEngineInDefaultCombo): it
|
||||
// accepts an empty pipeline (disabling the last layer) and inserts at the
|
||||
// stackPriority-correct position — the [id] route requires `pipeline.min(1)`.
|
||||
const toggleEngine = useCallback(
|
||||
async (engineId: string) => {
|
||||
if (!combo) return;
|
||||
const existingIndex = combo.pipeline.findIndex((s) => s.engine === engineId);
|
||||
const existingStep = existingIndex >= 0 ? combo.pipeline[existingIndex] : null;
|
||||
const enabledNow = Boolean(existingStep && existingStep.config?.enabled !== false);
|
||||
const prev = combo;
|
||||
|
||||
// Optimistic update (mirrors the server's insert-at-priority / remove logic).
|
||||
let optimistic: PipelineStep[];
|
||||
if (enabledNow) {
|
||||
optimistic = combo.pipeline.filter((s) => s.engine !== engineId);
|
||||
} else if (existingStep) {
|
||||
optimistic = combo.pipeline.map((step, index) =>
|
||||
index === existingIndex
|
||||
? { ...step, config: { ...(step.config ?? {}), enabled: true } }
|
||||
: step
|
||||
);
|
||||
} else {
|
||||
const priorityOf = (eid: string) => engines.find((e) => e.id === eid)?.stackPriority ?? 50;
|
||||
optimistic = [...combo.pipeline];
|
||||
let insertAt = optimistic.findIndex((s) => priorityOf(s.engine) > priorityOf(engineId));
|
||||
if (insertAt < 0) insertAt = optimistic.length;
|
||||
optimistic.splice(insertAt, 0, { engine: engineId });
|
||||
}
|
||||
setCombo({ ...combo, pipeline: optimistic });
|
||||
setError(null);
|
||||
|
||||
try {
|
||||
const res = await fetch("/api/context/combos/default", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({
|
||||
engineId,
|
||||
enabled: !enabledNow,
|
||||
config: { ...(existingStep?.config ?? {}), enabled: !enabledNow },
|
||||
}),
|
||||
});
|
||||
if (!res.ok) {
|
||||
setCombo(prev);
|
||||
setError("Failed to update layer.");
|
||||
return;
|
||||
}
|
||||
const updated = await res.json();
|
||||
if (Array.isArray(updated?.pipeline)) {
|
||||
setCombo({ ...prev, pipeline: updated.pipeline });
|
||||
}
|
||||
} catch {
|
||||
setCombo(prev);
|
||||
setError("Failed to update layer.");
|
||||
}
|
||||
},
|
||||
[combo, engines]
|
||||
);
|
||||
// Layer enable/disable moved to the single-source panel (/dashboard/context/settings,
|
||||
// the `engines` map). The old per-layer toggle here wrote the now-deprecated
|
||||
// /api/context/combos/default route (a 410 shim) — it has been removed. This Hub keeps
|
||||
// the read-only derived view + the reorder control (which uses the named-combo [id] route).
|
||||
|
||||
// ── Reorder an active layer ───────────────────────────────────────────────────
|
||||
// Persisted via the [id] route so the custom order survives (the `/default` route
|
||||
@@ -423,9 +369,16 @@ export default function CompressionHub() {
|
||||
</h2>
|
||||
<span className="text-xs text-text-muted">{activeSteps.length} layer(s)</span>
|
||||
</div>
|
||||
<p className="text-xs text-text-muted">
|
||||
Turn layers on/off and set their level in{" "}
|
||||
<a href="/dashboard/context/settings" className="underline hover:text-text-main">
|
||||
Compression Settings
|
||||
</a>
|
||||
. You can reorder the active layers here.
|
||||
</p>
|
||||
{activeSteps.length === 0 ? (
|
||||
<p className="rounded-lg border border-dashed border-border px-3 py-4 text-center text-xs text-text-muted">
|
||||
No active layers. Enable a layer below to build the pipeline.
|
||||
No active layers. Enable a layer in Compression Settings to build the pipeline.
|
||||
</p>
|
||||
) : (
|
||||
<ul className="flex flex-col gap-2">
|
||||
@@ -485,11 +438,6 @@ export default function CompressionHub() {
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">settings</span>
|
||||
</a>
|
||||
<Toggle
|
||||
checked
|
||||
onChange={() => toggleEngine(step.engine)}
|
||||
ariaLabel={`Disable ${engine?.name ?? step.engine}`}
|
||||
/>
|
||||
</li>
|
||||
);
|
||||
})}
|
||||
@@ -534,11 +482,6 @@ export default function CompressionHub() {
|
||||
>
|
||||
<span className="material-symbols-outlined text-[18px]">settings</span>
|
||||
</a>
|
||||
<Toggle
|
||||
checked={false}
|
||||
onChange={() => toggleEngine(engine.id)}
|
||||
ariaLabel={`Enable ${engine.name}`}
|
||||
/>
|
||||
</li>
|
||||
))}
|
||||
</ul>
|
||||
|
||||
@@ -188,31 +188,9 @@ export default function RtkContextPageClient() {
|
||||
|
||||
{config && (
|
||||
<section className="rounded-lg border border-border bg-surface p-4">
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-5">
|
||||
<label className="flex items-center gap-2 text-sm text-text-main">
|
||||
<input
|
||||
type="checkbox"
|
||||
checked={config.enabled}
|
||||
disabled={saving}
|
||||
onChange={(event) => saveConfig({ enabled: event.target.checked })}
|
||||
/>
|
||||
{t("enabled")}
|
||||
</label>
|
||||
<label className="flex flex-col gap-1 text-sm text-text-main">
|
||||
{t("intensity")}
|
||||
<select
|
||||
value={config.intensity}
|
||||
disabled={saving}
|
||||
onChange={(event) =>
|
||||
saveConfig({ intensity: event.target.value as RtkConfig["intensity"] })
|
||||
}
|
||||
className="rounded border border-border bg-bg px-2 py-1 text-sm"
|
||||
>
|
||||
<option value="minimal">{t("intensityMinimal")}</option>
|
||||
<option value="standard">{t("intensityStandard")}</option>
|
||||
<option value="aggressive">{t("intensityAggressive")}</option>
|
||||
</select>
|
||||
</label>
|
||||
{/* On/off + intensity now live in the panel (/dashboard/context/settings). This
|
||||
page edits RTK's detailed configuration only. */}
|
||||
<div className="grid grid-cols-1 gap-4 md:grid-cols-2 xl:grid-cols-4">
|
||||
<label className="flex flex-col gap-1 text-sm text-text-main">
|
||||
{t("maxLines")}
|
||||
<input
|
||||
|
||||
@@ -0,0 +1,362 @@
|
||||
"use client";
|
||||
|
||||
// CompressionPanel — the single-source engine-grid UI for compression.
|
||||
//
|
||||
// Renders the master on/off switch, one row per catalog engine (on/off + level +
|
||||
// link to its detail page), the cavemanOutput intensity row, the mcpAccessibility
|
||||
// toggle (its own endpoint / separate store), a read-only derived-pipeline preview,
|
||||
// and the general settings (auto-trigger tokens + preserve-system-prompt).
|
||||
//
|
||||
// Engine rows use the catalog label/description (hardcoded English) directly — NOT
|
||||
// i18n — so they stay deterministic. Human-facing chrome (master, general) keeps the
|
||||
// app's i18n via useTranslations("settings").
|
||||
|
||||
import Link from "next/link";
|
||||
import { useEffect, useState } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
// Import Card/Toggle from their direct module paths rather than the @/shared/components
|
||||
// barrel: the barrel transitively pulls a heavy/Node-only module that hangs the
|
||||
// vitest/jsdom component test. Direct imports resolve identically under Next.js.
|
||||
import Card from "@/shared/components/Card";
|
||||
import Toggle from "@/shared/components/Toggle";
|
||||
import {
|
||||
ENGINE_IDS,
|
||||
engineMeta,
|
||||
} from "../../../../../../open-sse/services/compression/engineCatalog.ts";
|
||||
import { deriveDefaultPlan } from "../../../../../../open-sse/services/compression/deriveDefaultPlan.ts";
|
||||
|
||||
type CavemanIntensity = "lite" | "full" | "ultra";
|
||||
|
||||
interface EngineToggle {
|
||||
enabled: boolean;
|
||||
level?: string;
|
||||
}
|
||||
|
||||
interface CavemanOutputModeConfig {
|
||||
enabled: boolean;
|
||||
intensity: CavemanIntensity;
|
||||
autoClarity: boolean;
|
||||
}
|
||||
|
||||
interface CompressionConfig {
|
||||
enabled: boolean;
|
||||
autoTriggerTokens: number;
|
||||
preserveSystemPrompt: boolean;
|
||||
engines: Record<string, EngineToggle>;
|
||||
activeComboId: string | null;
|
||||
cavemanOutputMode?: CavemanOutputModeConfig;
|
||||
}
|
||||
|
||||
const CAVEMAN_OUTPUT_LEVELS: CavemanIntensity[] = ["lite", "full", "ultra"];
|
||||
|
||||
const DEFAULT_CONFIG: CompressionConfig = {
|
||||
enabled: false,
|
||||
autoTriggerTokens: 0,
|
||||
preserveSystemPrompt: true,
|
||||
engines: {},
|
||||
activeComboId: null,
|
||||
cavemanOutputMode: { enabled: false, intensity: "full", autoClarity: true },
|
||||
};
|
||||
|
||||
function normalizeEngines(raw: unknown): Record<string, EngineToggle> {
|
||||
const engines: Record<string, EngineToggle> = {};
|
||||
const source = (raw && typeof raw === "object" ? raw : {}) as Record<string, EngineToggle>;
|
||||
for (const id of ENGINE_IDS) {
|
||||
const cur = source[id];
|
||||
engines[id] = cur ? { enabled: cur.enabled === true, ...(cur.level ? { level: cur.level } : {}) } : { enabled: false };
|
||||
}
|
||||
return engines;
|
||||
}
|
||||
|
||||
export default function CompressionPanel() {
|
||||
const t = useTranslations("settings");
|
||||
const [config, setConfig] = useState<CompressionConfig>(DEFAULT_CONFIG);
|
||||
const [mcpAccessibility, setMcpAccessibility] = useState(true);
|
||||
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: Partial<CompressionConfig> | null) => {
|
||||
if (data) {
|
||||
setConfig({
|
||||
...DEFAULT_CONFIG,
|
||||
...data,
|
||||
engines: normalizeEngines(data.engines),
|
||||
cavemanOutputMode: data.cavemanOutputMode ?? DEFAULT_CONFIG.cavemanOutputMode,
|
||||
});
|
||||
}
|
||||
})
|
||||
.catch(() => {})
|
||||
.finally(() => setLoading(false));
|
||||
|
||||
fetch("/api/settings/compression/mcp-accessibility")
|
||||
.then((r) => (r.ok ? r.json() : null))
|
||||
.then((data: { enabled?: boolean } | null) => {
|
||||
if (data && typeof data.enabled === "boolean") setMcpAccessibility(data.enabled);
|
||||
})
|
||||
.catch(() => {});
|
||||
}, []);
|
||||
|
||||
// Persist a merge-patch. The DB persists `engines` as one whole row, so callers that
|
||||
// touch an engine pass the full engines map to avoid dropping the other engines.
|
||||
const save = async (updates: Partial<CompressionConfig>) => {
|
||||
const next = { ...config, ...updates };
|
||||
setConfig(next);
|
||||
setSaving(true);
|
||||
setStatus("");
|
||||
try {
|
||||
const res = await fetch("/api/settings/compression", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify(updates),
|
||||
});
|
||||
if (res.ok) {
|
||||
setStatus("saved");
|
||||
setTimeout(() => setStatus(""), 2000);
|
||||
} else {
|
||||
setStatus("error");
|
||||
}
|
||||
} catch {
|
||||
setStatus("error");
|
||||
} finally {
|
||||
setSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const setEngine = (id: string, patch: Partial<EngineToggle>) => {
|
||||
const engines = {
|
||||
...config.engines,
|
||||
[id]: { ...(config.engines[id] ?? { enabled: false }), ...patch },
|
||||
};
|
||||
// Send the full engines map — the persistence layer stores it as one JSON row.
|
||||
save({ engines });
|
||||
};
|
||||
|
||||
const setCavemanOutput = (patch: Partial<CavemanOutputModeConfig>) => {
|
||||
const cavemanOutputMode: CavemanOutputModeConfig = {
|
||||
...(config.cavemanOutputMode ?? DEFAULT_CONFIG.cavemanOutputMode!),
|
||||
...patch,
|
||||
};
|
||||
save({ cavemanOutputMode });
|
||||
};
|
||||
|
||||
const toggleMcpAccessibility = async (enabled: boolean) => {
|
||||
setMcpAccessibility(enabled);
|
||||
try {
|
||||
await fetch("/api/settings/compression/mcp-accessibility", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ enabled }),
|
||||
});
|
||||
} catch {
|
||||
// Surface nothing — the row reflects optimistic local state; the next mount re-reads.
|
||||
}
|
||||
};
|
||||
|
||||
const derived = deriveDefaultPlan(config.engines, config.enabled);
|
||||
const derivedText =
|
||||
derived.mode === "off"
|
||||
? "off"
|
||||
: derived.stackedPipeline.length > 0
|
||||
? `runs: ${derived.stackedPipeline.map((s) => s.engine).join(" → ")}`
|
||||
: `mode: ${derived.mode}`;
|
||||
|
||||
if (loading) {
|
||||
return (
|
||||
<Card className="p-6">
|
||||
<p className="text-sm text-text-muted">{t("loading")}</p>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
|
||||
return (
|
||||
<Card className="p-6" data-testid="compression-panel">
|
||||
{/* Master */}
|
||||
<div className="mb-5 flex items-start justify-between gap-4">
|
||||
<div className="flex items-center gap-3">
|
||||
<div className="rounded-lg bg-blue-500/10 p-2 text-blue-500">
|
||||
<span className="material-symbols-outlined text-[20px]" aria-hidden="true">
|
||||
compress
|
||||
</span>
|
||||
</div>
|
||||
<div>
|
||||
<h3 className="text-lg font-semibold">{t("compressionTitle")}</h3>
|
||||
<p className="text-sm text-text-muted">{t("compressionDesc")}</p>
|
||||
</div>
|
||||
</div>
|
||||
<div className="flex items-center gap-3">
|
||||
{status === "saved" && (
|
||||
<span className="flex items-center gap-1 text-xs font-medium text-emerald-500">
|
||||
<span className="material-symbols-outlined text-[14px]">check_circle</span>{" "}
|
||||
{t("saved")}
|
||||
</span>
|
||||
)}
|
||||
{status === "error" && (
|
||||
<span className="flex items-center gap-1 text-xs font-medium text-red-500">
|
||||
<span className="material-symbols-outlined text-[14px]">error</span>{" "}
|
||||
{t("saveFailed")}
|
||||
</span>
|
||||
)}
|
||||
<Toggle
|
||||
size="md"
|
||||
checked={config.enabled}
|
||||
onChange={(enabled) => save({ enabled })}
|
||||
disabled={saving}
|
||||
ariaLabel={t("compressionTitle")}
|
||||
/>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* Derived pipeline preview */}
|
||||
<div
|
||||
data-testid="derived-pipeline-preview"
|
||||
className="mb-4 rounded-md border border-border/60 bg-bg-subtle px-3 py-2 text-xs text-text-muted"
|
||||
>
|
||||
<span className="font-medium text-text-main">Effective pipeline:</span> {derivedText}
|
||||
</div>
|
||||
|
||||
{/* Engine grid */}
|
||||
<div className={`divide-y divide-border ${config.enabled ? "" : "opacity-60"}`}>
|
||||
{ENGINE_IDS.map((id) => {
|
||||
const meta = engineMeta(id);
|
||||
const engine = config.engines[id] ?? { enabled: false };
|
||||
const levels = meta.levels;
|
||||
const level = engine.level ?? levels?.[0] ?? "";
|
||||
return (
|
||||
<div
|
||||
key={id}
|
||||
data-testid={`engine-row-${id}`}
|
||||
className="flex flex-col gap-2 py-3 sm:flex-row sm:items-center sm:justify-between"
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-text-main">
|
||||
{meta.label}
|
||||
<Link
|
||||
href={`/dashboard/context/${id}`}
|
||||
className="rounded border border-border bg-bg-subtle px-1.5 py-0.5 text-[10px] uppercase tracking-wider text-text-muted hover:border-primary/40 hover:text-primary"
|
||||
>
|
||||
{id}
|
||||
</Link>
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-text-muted">{meta.description}</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{levels && (
|
||||
<select
|
||||
value={level}
|
||||
onChange={(e) => setEngine(id, { level: e.target.value })}
|
||||
disabled={!config.enabled || !engine.enabled || saving}
|
||||
className="w-28 rounded border border-border bg-surface px-2 py-1 text-xs text-text-main"
|
||||
>
|
||||
{levels.map((lvl) => (
|
||||
<option key={lvl} value={lvl}>
|
||||
{lvl}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
)}
|
||||
<span data-testid={`engine-toggle-${id}`}>
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={engine.enabled}
|
||||
onChange={(enabled) => setEngine(id, { enabled })}
|
||||
disabled={!config.enabled || saving}
|
||||
ariaLabel={meta.label}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
|
||||
{/* cavemanOutput — response-output instruction injection (separate from the input engine) */}
|
||||
<div className="mt-2 flex flex-col gap-2 border-t border-border/30 py-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-text-main">
|
||||
{t("compressionSettingsCavemanOutputMode")}
|
||||
</p>
|
||||
<p className="mt-0.5 text-xs text-text-muted">
|
||||
Injects terse response instructions without rewriting provider output.
|
||||
</p>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
<select
|
||||
data-testid="caveman-output-intensity"
|
||||
value={config.cavemanOutputMode?.intensity ?? "full"}
|
||||
onChange={(e) => setCavemanOutput({ intensity: e.target.value as CavemanIntensity })}
|
||||
disabled={!config.cavemanOutputMode?.enabled || saving}
|
||||
className="w-28 rounded border border-border bg-surface px-2 py-1 text-xs text-text-main"
|
||||
>
|
||||
{CAVEMAN_OUTPUT_LEVELS.map((lvl) => (
|
||||
<option key={lvl} value={lvl}>
|
||||
{lvl}
|
||||
</option>
|
||||
))}
|
||||
</select>
|
||||
<span data-testid="caveman-output-toggle">
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={config.cavemanOutputMode?.enabled ?? false}
|
||||
onChange={(enabled) => setCavemanOutput({ enabled })}
|
||||
disabled={saving}
|
||||
ariaLabel={t("compressionSettingsCavemanOutputMode")}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{/* mcpAccessibility — writes its own endpoint / separate store */}
|
||||
<div className="flex flex-col gap-2 border-t border-border/30 py-3 sm:flex-row sm:items-center sm:justify-between">
|
||||
<div className="min-w-0 flex-1">
|
||||
<p className="text-sm font-medium text-text-main">{t("mcpAccessibilityTitle")}</p>
|
||||
<p className="mt-0.5 text-xs text-text-muted">
|
||||
Scopes MCP tool outputs (separate store).
|
||||
</p>
|
||||
</div>
|
||||
<span data-testid="mcp-accessibility-toggle">
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={mcpAccessibility}
|
||||
onChange={toggleMcpAccessibility}
|
||||
ariaLabel={t("mcpAccessibilityTitle")}
|
||||
/>
|
||||
</span>
|
||||
</div>
|
||||
|
||||
{/* General */}
|
||||
<div className="space-y-3 border-t border-border/30 pt-4">
|
||||
<h4 className="text-sm font-medium text-text-main">{t("compressionGeneral")}</h4>
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm text-text-muted">{t("compressionAutoTrigger")}</span>
|
||||
<div className="flex items-center gap-2">
|
||||
<input
|
||||
type="number"
|
||||
min={0}
|
||||
max={100000}
|
||||
value={config.autoTriggerTokens}
|
||||
onChange={(e) => save({ autoTriggerTokens: parseInt(e.target.value) || 0 })}
|
||||
className="w-24 rounded border border-border bg-surface px-2 py-1 text-sm text-text-main"
|
||||
/>
|
||||
<span className="text-xs text-text-muted">{t("tokens")}</span>
|
||||
</div>
|
||||
</label>
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm text-text-muted">{t("compressionPreserveSystem")}</span>
|
||||
<span data-testid="preserve-system-toggle">
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={config.preserveSystemPrompt}
|
||||
onChange={(preserveSystemPrompt) => save({ preserveSystemPrompt })}
|
||||
disabled={saving}
|
||||
ariaLabel={t("compressionPreserveSystem")}
|
||||
/>
|
||||
</span>
|
||||
</label>
|
||||
</div>
|
||||
</Card>
|
||||
);
|
||||
}
|
||||
@@ -1,11 +1,11 @@
|
||||
"use client";
|
||||
|
||||
import CompressionSettingsTab from "@/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab";
|
||||
import CompressionPanel from "./CompressionPanel";
|
||||
|
||||
export default function CompressionSettingsPage() {
|
||||
return (
|
||||
<div className="p-4">
|
||||
<CompressionSettingsTab />
|
||||
<CompressionPanel />
|
||||
</div>
|
||||
);
|
||||
}
|
||||
|
||||
@@ -5,7 +5,6 @@ import { Card, Button } from "@/shared/components";
|
||||
import { useTranslations } from "next-intl";
|
||||
import CompressionTokenSaverCard, {
|
||||
type CompressionTokenSaverConfig,
|
||||
type CompressionTokenSaverPatch,
|
||||
} from "./CompressionTokenSaverCard";
|
||||
|
||||
type CompressionMode = "off" | "lite" | "standard" | "aggressive" | "ultra" | "rtk" | "stacked";
|
||||
@@ -227,48 +226,6 @@ export default function CompressionSettingsTab() {
|
||||
}
|
||||
};
|
||||
|
||||
const saveTokenSaver = async (updates: CompressionTokenSaverPatch) => {
|
||||
const nextUpdates: Partial<CompressionConfig> = {};
|
||||
|
||||
if (typeof updates.enabled === "boolean") {
|
||||
nextUpdates.enabled = updates.enabled;
|
||||
}
|
||||
|
||||
if (updates.cavemanConfig) {
|
||||
nextUpdates.cavemanConfig = {
|
||||
...(config.cavemanConfig ?? {
|
||||
enabled: true,
|
||||
compressRoles: ["user"],
|
||||
skipRules: [],
|
||||
minMessageLength: 50,
|
||||
preservePatterns: [],
|
||||
intensity: "full",
|
||||
}),
|
||||
...updates.cavemanConfig,
|
||||
};
|
||||
}
|
||||
|
||||
if (updates.cavemanOutputMode) {
|
||||
nextUpdates.cavemanOutputMode = {
|
||||
...(config.cavemanOutputMode ?? {
|
||||
enabled: false,
|
||||
intensity: "full",
|
||||
autoClarity: true,
|
||||
}),
|
||||
...updates.cavemanOutputMode,
|
||||
};
|
||||
}
|
||||
|
||||
if (updates.rtkConfig) {
|
||||
nextUpdates.rtkConfig = {
|
||||
...(config.rtkConfig ?? { enabled: true, intensity: "standard" }),
|
||||
...updates.rtkConfig,
|
||||
};
|
||||
}
|
||||
|
||||
await save(nextUpdates);
|
||||
};
|
||||
|
||||
const toggleCavemanRole = (role: "user" | "assistant" | "system") => {
|
||||
const currentRoles = config.cavemanConfig?.compressRoles ?? ["user"];
|
||||
const newRoles = currentRoles.includes(role)
|
||||
@@ -322,7 +279,7 @@ export default function CompressionSettingsTab() {
|
||||
</div>
|
||||
|
||||
<div className="space-y-6">
|
||||
<CompressionTokenSaverCard config={config} saving={saving} onSave={saveTokenSaver} />
|
||||
<CompressionTokenSaverCard config={config} />
|
||||
|
||||
{config.enabled && (
|
||||
<div className="space-y-3">
|
||||
@@ -529,27 +486,9 @@ export default function CompressionSettingsTab() {
|
||||
/>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm text-text-muted">
|
||||
{t("compressionSettingsCavemanIntensity")}
|
||||
</span>
|
||||
<select
|
||||
value={config.cavemanConfig.intensity}
|
||||
onChange={(e) =>
|
||||
save({
|
||||
cavemanConfig: {
|
||||
...config.cavemanConfig!,
|
||||
intensity: e.target.value as CavemanIntensity,
|
||||
},
|
||||
})
|
||||
}
|
||||
className="w-28 px-2 py-1 text-sm rounded border border-border bg-surface text-text-main"
|
||||
>
|
||||
<option value="lite">lite</option>
|
||||
<option value="full">full</option>
|
||||
<option value="ultra">ultra</option>
|
||||
</select>
|
||||
</label>
|
||||
{/* Caveman intensity (level) is set in the panel
|
||||
(/dashboard/context/settings); kept out of this tab to avoid a
|
||||
duplicate level control. */}
|
||||
|
||||
<div className="space-y-2">
|
||||
<p className="text-sm text-text-muted">{t("compressionSkipRules")}</p>
|
||||
@@ -602,58 +541,16 @@ export default function CompressionSettingsTab() {
|
||||
|
||||
{config.enabled && config.cavemanOutputMode && (
|
||||
<div className="space-y-3 pt-4 border-t border-border/30">
|
||||
<div className="flex items-center justify-between">
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-text-main">
|
||||
{t("compressionSettingsCavemanOutputMode")}
|
||||
</h4>
|
||||
<p className="text-xs text-text-muted mt-0.5">
|
||||
Injects terse response instructions without rewriting provider output.
|
||||
</p>
|
||||
</div>
|
||||
<button
|
||||
onClick={() =>
|
||||
save({
|
||||
cavemanOutputMode: {
|
||||
...config.cavemanOutputMode!,
|
||||
enabled: !config.cavemanOutputMode!.enabled,
|
||||
},
|
||||
})
|
||||
}
|
||||
className={`relative w-10 h-5 rounded-full transition-colors ${
|
||||
config.cavemanOutputMode.enabled ? "bg-green-500" : "bg-border"
|
||||
}`}
|
||||
>
|
||||
<span
|
||||
className={`absolute top-0.5 w-4 h-4 rounded-full bg-white transition-transform ${
|
||||
config.cavemanOutputMode.enabled ? "left-5" : "left-0.5"
|
||||
}`}
|
||||
/>
|
||||
</button>
|
||||
<div>
|
||||
<h4 className="text-sm font-medium text-text-main">
|
||||
{t("compressionSettingsCavemanOutputMode")}
|
||||
</h4>
|
||||
<p className="text-xs text-text-muted mt-0.5">
|
||||
Injects terse response instructions without rewriting provider output. Its on/off
|
||||
and level are set in the panel (/dashboard/context/settings).
|
||||
</p>
|
||||
</div>
|
||||
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm text-text-muted">
|
||||
{t("compressionSettingsOutputIntensity")}
|
||||
</span>
|
||||
<select
|
||||
value={config.cavemanOutputMode.intensity}
|
||||
onChange={(e) =>
|
||||
save({
|
||||
cavemanOutputMode: {
|
||||
...config.cavemanOutputMode!,
|
||||
intensity: e.target.value as CavemanIntensity,
|
||||
},
|
||||
})
|
||||
}
|
||||
className="w-28 px-2 py-1 text-sm rounded border border-border bg-surface text-text-main"
|
||||
>
|
||||
<option value="lite">lite</option>
|
||||
<option value="full">full</option>
|
||||
<option value="ultra">ultra</option>
|
||||
</select>
|
||||
</label>
|
||||
|
||||
<label className="flex items-center justify-between">
|
||||
<span className="text-sm text-text-muted">
|
||||
{t("compressionSettingsAutoClarityBypass")}
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
"use client";
|
||||
|
||||
import Link from "next/link";
|
||||
import type { ReactNode } from "react";
|
||||
import { useTranslations } from "next-intl";
|
||||
import { Toggle } from "@/shared/components";
|
||||
|
||||
type CavemanIntensity = "lite" | "full" | "ultra";
|
||||
type RtkIntensity = "minimal" | "standard" | "aggressive";
|
||||
@@ -17,103 +15,49 @@ export interface CompressionTokenSaverConfig {
|
||||
|
||||
export type CompressionTokenSaverPatch = Partial<CompressionTokenSaverConfig>;
|
||||
|
||||
const CAVEMAN_LEVELS: { value: CavemanIntensity; label: string }[] = [
|
||||
{ value: "lite", label: "Lite" },
|
||||
{ value: "full", label: "Full" },
|
||||
{ value: "ultra", label: "Ultra" },
|
||||
];
|
||||
|
||||
const RTK_LEVELS: { value: RtkIntensity; label: string }[] = [
|
||||
{ value: "minimal", label: "Min" },
|
||||
{ value: "standard", label: "Std" },
|
||||
{ value: "aggressive", label: "Agg" },
|
||||
];
|
||||
|
||||
function SegmentedLevel<T extends string>({
|
||||
levels,
|
||||
value,
|
||||
onChange,
|
||||
disabled,
|
||||
}: {
|
||||
levels: readonly { value: T; label: string }[];
|
||||
value: T;
|
||||
onChange: (v: T) => void;
|
||||
disabled: boolean;
|
||||
}) {
|
||||
// Read-only summary. The engine on/off + level toggles that used to live here moved to
|
||||
// the single-source panel (/dashboard/context/settings). This card now only reflects the
|
||||
// current state and links to the panel — it no longer writes anything (the `onSave` prop
|
||||
// is accepted for backward compatibility but intentionally unused).
|
||||
function StatusPill({ on }: { on: boolean }) {
|
||||
return (
|
||||
<div
|
||||
className={`inline-flex rounded-md border border-border bg-bg-subtle p-0.5 ${
|
||||
disabled ? "opacity-50" : ""
|
||||
<span
|
||||
className={`rounded px-1.5 py-0.5 text-[10px] font-semibold uppercase tracking-wider ${
|
||||
on ? "bg-emerald-500/15 text-emerald-500" : "bg-border/50 text-text-muted"
|
||||
}`}
|
||||
>
|
||||
{levels.map((lvl) => {
|
||||
const active = lvl.value === value;
|
||||
return (
|
||||
<button
|
||||
key={lvl.value}
|
||||
type="button"
|
||||
onClick={() => !disabled && onChange(lvl.value)}
|
||||
disabled={disabled}
|
||||
className={`rounded px-2.5 py-0.5 text-[11px] font-medium transition-colors ${
|
||||
active ? "bg-primary text-white" : "text-text-muted hover:text-text-primary"
|
||||
} ${disabled ? "cursor-not-allowed" : "cursor-pointer"}`}
|
||||
>
|
||||
{lvl.label}
|
||||
</button>
|
||||
);
|
||||
})}
|
||||
</div>
|
||||
{on ? "on" : "off"}
|
||||
</span>
|
||||
);
|
||||
}
|
||||
|
||||
function EngineRow({
|
||||
function SummaryRow({
|
||||
title,
|
||||
description,
|
||||
href,
|
||||
badge,
|
||||
enabled,
|
||||
masterEnabled,
|
||||
saving,
|
||||
onToggle,
|
||||
href,
|
||||
on,
|
||||
level,
|
||||
}: {
|
||||
title: string;
|
||||
description: string;
|
||||
href: string;
|
||||
badge: string;
|
||||
enabled: boolean;
|
||||
masterEnabled: boolean;
|
||||
saving: boolean;
|
||||
onToggle: (v: boolean) => void;
|
||||
level: ReactNode;
|
||||
href: string;
|
||||
on: boolean;
|
||||
level: string;
|
||||
}) {
|
||||
const effective = masterEnabled && enabled;
|
||||
return (
|
||||
<div
|
||||
className={`flex flex-col gap-2 py-3 sm:flex-row sm:items-center sm:justify-between ${
|
||||
masterEnabled ? "" : "opacity-60"
|
||||
}`}
|
||||
>
|
||||
<div className="min-w-0 flex-1">
|
||||
<div className="flex items-center gap-2 text-sm font-medium text-text-main">
|
||||
{title}
|
||||
<Link
|
||||
href={href}
|
||||
className="rounded border border-border bg-bg-subtle px-1.5 py-0.5 text-[10px] uppercase tracking-wider text-text-muted hover:border-primary/40 hover:text-primary"
|
||||
>
|
||||
{badge}
|
||||
</Link>
|
||||
</div>
|
||||
<p className="mt-0.5 text-xs text-text-muted">{description}</p>
|
||||
<div className="flex items-center justify-between gap-3 py-2 text-sm text-text-main">
|
||||
<div className="flex items-center gap-2">
|
||||
{title}
|
||||
<Link
|
||||
href={href}
|
||||
className="rounded border border-border bg-bg-subtle px-1.5 py-0.5 text-[10px] uppercase tracking-wider text-text-muted hover:border-primary/40 hover:text-primary"
|
||||
>
|
||||
{badge}
|
||||
</Link>
|
||||
</div>
|
||||
<div className="flex shrink-0 items-center gap-2">
|
||||
{level}
|
||||
<Toggle
|
||||
size="sm"
|
||||
checked={effective}
|
||||
onChange={onToggle}
|
||||
disabled={!masterEnabled || saving}
|
||||
/>
|
||||
<div className="flex items-center gap-2 text-xs text-text-muted">
|
||||
<span>{level}</span>
|
||||
<StatusPill on={on} />
|
||||
</div>
|
||||
</div>
|
||||
);
|
||||
@@ -121,12 +65,11 @@ function EngineRow({
|
||||
|
||||
export default function CompressionTokenSaverCard({
|
||||
config,
|
||||
saving,
|
||||
onSave,
|
||||
}: {
|
||||
config: CompressionTokenSaverConfig;
|
||||
saving: boolean;
|
||||
onSave: (patch: CompressionTokenSaverPatch) => void | Promise<void>;
|
||||
// Kept for call-site compatibility; this card is read-only and never persists.
|
||||
saving?: boolean;
|
||||
onSave?: (patch: CompressionTokenSaverPatch) => void | Promise<void>;
|
||||
}) {
|
||||
const t = useTranslations("settings");
|
||||
const masterEnabled = config.enabled;
|
||||
@@ -147,93 +90,42 @@ export default function CompressionTokenSaverCard({
|
||||
<h4 className="flex items-center gap-2 text-base font-semibold text-text-main">
|
||||
<span className="material-symbols-outlined text-[21px] text-amber-500">bolt</span>
|
||||
{t("tokenSaverTitle")}
|
||||
{saving && (
|
||||
<span className="material-symbols-outlined text-[16px] animate-spin text-text-muted">
|
||||
sync
|
||||
</span>
|
||||
)}
|
||||
</h4>
|
||||
<p className="mt-1 text-sm text-text-muted">{t("tokenSaverSubtitle")}</p>
|
||||
</div>
|
||||
<Toggle
|
||||
size="md"
|
||||
checked={masterEnabled}
|
||||
onChange={(checked) => onSave({ enabled: checked })}
|
||||
disabled={saving}
|
||||
/>
|
||||
<StatusPill on={masterEnabled} />
|
||||
</div>
|
||||
|
||||
<div className="mt-4 divide-y divide-border">
|
||||
<EngineRow
|
||||
<div className="mt-3 divide-y divide-border">
|
||||
<SummaryRow
|
||||
title={t("tokenSaverToolOutput")}
|
||||
badge="RTK"
|
||||
href="/dashboard/context/rtk"
|
||||
description={t("tokenSaverToolOutputDesc")}
|
||||
enabled={rtk.enabled}
|
||||
masterEnabled={masterEnabled}
|
||||
saving={saving}
|
||||
onToggle={(enabled) => onSave({ rtkConfig: { ...rtk, enabled } })}
|
||||
level={
|
||||
<SegmentedLevel
|
||||
levels={RTK_LEVELS}
|
||||
value={rtk.intensity}
|
||||
onChange={(intensity) => onSave({ rtkConfig: { ...rtk, intensity } })}
|
||||
disabled={saving || !masterEnabled || !rtk.enabled}
|
||||
/>
|
||||
}
|
||||
href="/dashboard/context/settings"
|
||||
on={masterEnabled && rtk.enabled}
|
||||
level={rtk.intensity}
|
||||
/>
|
||||
<EngineRow
|
||||
<SummaryRow
|
||||
title={t("tokenSaverLlmOutput")}
|
||||
badge="Caveman"
|
||||
href="/dashboard/context/caveman"
|
||||
description={t("tokenSaverLlmOutputDesc")}
|
||||
enabled={cavemanOut.enabled}
|
||||
masterEnabled={masterEnabled}
|
||||
saving={saving}
|
||||
onToggle={(enabled) => onSave({ cavemanOutputMode: { ...cavemanOut, enabled } })}
|
||||
level={
|
||||
<SegmentedLevel
|
||||
levels={CAVEMAN_LEVELS}
|
||||
value={cavemanOut.intensity}
|
||||
onChange={(intensity) => onSave({ cavemanOutputMode: { ...cavemanOut, intensity } })}
|
||||
disabled={saving || !masterEnabled || !cavemanOut.enabled}
|
||||
/>
|
||||
}
|
||||
href="/dashboard/context/settings"
|
||||
on={masterEnabled && cavemanOut.enabled}
|
||||
level={cavemanOut.intensity}
|
||||
/>
|
||||
<EngineRow
|
||||
<SummaryRow
|
||||
title={t("tokenSaverInputCompression")}
|
||||
badge="Caveman"
|
||||
href="/dashboard/context/caveman"
|
||||
description={t("tokenSaverInputCompressionDesc")}
|
||||
enabled={cavemanIn.enabled}
|
||||
masterEnabled={masterEnabled}
|
||||
saving={saving}
|
||||
onToggle={(enabled) => onSave({ cavemanConfig: { ...cavemanIn, enabled } })}
|
||||
level={
|
||||
<SegmentedLevel
|
||||
levels={CAVEMAN_LEVELS}
|
||||
value={cavemanIn.intensity}
|
||||
onChange={(intensity) => onSave({ cavemanConfig: { ...cavemanIn, intensity } })}
|
||||
disabled={saving || !masterEnabled || !cavemanIn.enabled}
|
||||
/>
|
||||
}
|
||||
href="/dashboard/context/settings"
|
||||
on={masterEnabled && cavemanIn.enabled}
|
||||
level={cavemanIn.intensity}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div className="mt-4 flex items-start gap-2 border-t border-border pt-3 text-xs text-text-muted">
|
||||
<span className="material-symbols-outlined mt-px text-[16px]">info</span>
|
||||
<p>
|
||||
{t("tokenSaverFineTunePrefix")}{" "}
|
||||
<Link href="/dashboard/context/caveman" className="text-primary hover:underline">
|
||||
Caveman
|
||||
</Link>{" "}
|
||||
/{" "}
|
||||
<Link href="/dashboard/context/rtk" className="text-primary hover:underline">
|
||||
RTK
|
||||
</Link>
|
||||
, {t("tokenSaverFineTuneSuffix")}{" "}
|
||||
<Link href="/dashboard/context/combos" className="text-primary hover:underline">
|
||||
Engine Combos
|
||||
Turn these layers on/off and set their level in{" "}
|
||||
<Link href="/dashboard/context/settings" className="text-primary hover:underline">
|
||||
Compression Settings
|
||||
</Link>
|
||||
.
|
||||
</p>
|
||||
|
||||
@@ -1,48 +1,48 @@
|
||||
import { NextResponse } from "next/server";
|
||||
import { z } from "zod";
|
||||
import { getDefaultCompressionCombo, setEngineInDefaultCombo } from "@/lib/db/compressionCombos";
|
||||
import { deriveDefaultPlan } from "@omniroute/open-sse/services/compression/deriveDefaultPlan";
|
||||
import { getCompressionSettings } from "@/lib/db/compression";
|
||||
import { requireManagementAuth } from "@/lib/api/requireManagementAuth";
|
||||
import { isValidationFailure, validateBody } from "@/shared/validation/helpers";
|
||||
import { buildErrorBody } from "@omniroute/open-sse/utils/error";
|
||||
|
||||
const engineToggleSchema = z
|
||||
.object({
|
||||
engineId: z.string().trim().min(1).max(64),
|
||||
enabled: z.boolean(),
|
||||
config: z.record(z.string(), z.unknown()).optional(),
|
||||
})
|
||||
.strict();
|
||||
// The default compression pipeline is no longer editable here. It is DERIVED from the
|
||||
// per-engine toggle map (see open-sse deriveDefaultPlan). This route is a read-only shim:
|
||||
// - GET → returns the derived default plan for the live config.
|
||||
// - PUT/POST → rejected with a deprecation error (edit engines via /api/settings/compression).
|
||||
const DEPRECATION_STATUS = 410;
|
||||
const DEPRECATION_MESSAGE =
|
||||
"The default compression pipeline is now derived from the engines map. " +
|
||||
"Edit engines at /api/settings/compression.";
|
||||
|
||||
export async function GET(request: Request) {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
|
||||
const combo = getDefaultCompressionCombo();
|
||||
if (!combo) {
|
||||
return NextResponse.json({ error: "No default compression combo found" }, { status: 404 });
|
||||
}
|
||||
return NextResponse.json(combo);
|
||||
const config = await getCompressionSettings();
|
||||
const plan = deriveDefaultPlan(config.engines ?? {}, config.enabled !== false);
|
||||
|
||||
// Shape kept compatible with prior consumers: `pipeline` is the ordered list of
|
||||
// { engine, intensity? } steps, plus the effective `mode`.
|
||||
return NextResponse.json({
|
||||
mode: plan.mode,
|
||||
pipeline: plan.stackedPipeline,
|
||||
derived: true,
|
||||
});
|
||||
}
|
||||
|
||||
function deprecationResponse() {
|
||||
return NextResponse.json(buildErrorBody(DEPRECATION_STATUS, DEPRECATION_MESSAGE), {
|
||||
status: DEPRECATION_STATUS,
|
||||
});
|
||||
}
|
||||
|
||||
export async function PUT(request: Request) {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
return deprecationResponse();
|
||||
}
|
||||
|
||||
let rawBody: unknown;
|
||||
try {
|
||||
rawBody = await request.json();
|
||||
} catch {
|
||||
return NextResponse.json({ error: "Invalid JSON body" }, { status: 400 });
|
||||
}
|
||||
|
||||
const validation = validateBody(engineToggleSchema, rawBody);
|
||||
if (isValidationFailure(validation)) {
|
||||
return NextResponse.json({ error: validation.error }, { status: 400 });
|
||||
}
|
||||
|
||||
const { engineId, enabled, config } = validation.data;
|
||||
const combo = setEngineInDefaultCombo(engineId, enabled, config);
|
||||
if (!combo) {
|
||||
return NextResponse.json({ error: "No default compression combo found" }, { status: 404 });
|
||||
}
|
||||
return NextResponse.json(combo);
|
||||
export async function POST(request: Request) {
|
||||
const authError = await requireManagementAuth(request);
|
||||
if (authError) return authError;
|
||||
return deprecationResponse();
|
||||
}
|
||||
|
||||
@@ -5801,6 +5801,7 @@
|
||||
"storageUsageTokenBufferCurrent": "Current: {value}",
|
||||
"compressionSettingsAutoTriggerMode": "Auto trigger mode",
|
||||
"compressionSettingsMcpDescriptionCompression": "MCP description compression",
|
||||
"mcpAccessibilityTitle": "MCP accessibility output",
|
||||
"compressionSettingsCavemanIntensity": "Caveman intensity",
|
||||
"compressionSettingsCavemanOutputMode": "Caveman output mode",
|
||||
"compressionSettingsOutputIntensity": "Output intensity",
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
import { backupDbFile } from "./backup";
|
||||
import { getDefaultCompressionCombo } from "./compressionCombos";
|
||||
import { getDbInstance } from "./core";
|
||||
import { invalidateDbCache } from "./readCache";
|
||||
import {
|
||||
ENGINE_IDS,
|
||||
DEFAULT_AGGRESSIVE_CONFIG,
|
||||
DEFAULT_CAVEMAN_CONFIG,
|
||||
DEFAULT_CAVEMAN_OUTPUT_MODE_CONFIG,
|
||||
@@ -20,6 +22,7 @@ import {
|
||||
type CompressionConfig,
|
||||
type CompressionMode,
|
||||
type ContextEditingConfig,
|
||||
type EngineToggle,
|
||||
type McpAccessibilityConfig,
|
||||
type RtkConfig,
|
||||
type UltraConfig,
|
||||
@@ -377,6 +380,121 @@ function normalizeUltraConfig(value: unknown): UltraConfig {
|
||||
};
|
||||
}
|
||||
|
||||
// Single-mode → engine id mapping. Mirrors deriveDefaultPlan's SINGLE_MODE_OF: a legacy
|
||||
// install whose only signal is `defaultMode` should turn on the engine that mode runs, so the
|
||||
// derived engines map matches the old behavior. Keep conservative — these are the only modes
|
||||
// that map 1:1 to a single engine.
|
||||
const SINGLE_MODE_ENGINE: Partial<Record<CompressionMode, string>> = {
|
||||
lite: "lite",
|
||||
standard: "caveman",
|
||||
aggressive: "aggressive",
|
||||
ultra: "ultra",
|
||||
rtk: "rtk",
|
||||
};
|
||||
|
||||
function normalizeEngineToggle(value: unknown): EngineToggle | null {
|
||||
const record = toRecord(value);
|
||||
if (typeof record.enabled !== "boolean") return null;
|
||||
return {
|
||||
enabled: record.enabled,
|
||||
...(typeof record.level === "string" ? { level: record.level } : {}),
|
||||
};
|
||||
}
|
||||
|
||||
// Sanitize an engines map for persistence: keep only known engine ids with a well-formed
|
||||
// `{enabled, level?}` toggle. Mirrors the read-path validation so a malformed write can't poison
|
||||
// the stored row.
|
||||
function sanitizeEnginesForWrite(value: unknown): Record<string, EngineToggle> {
|
||||
const record = toRecord(value);
|
||||
const out: Record<string, EngineToggle> = {};
|
||||
for (const id of ENGINE_IDS) {
|
||||
const toggle = normalizeEngineToggle(record[id]);
|
||||
if (toggle) out[id] = toggle;
|
||||
}
|
||||
return out;
|
||||
}
|
||||
|
||||
// Read the stored `engines` JSON row, keeping only well-formed `{enabled, level?}` entries for
|
||||
// known engine ids. Returns null when no usable row exists so the caller falls back to deriving
|
||||
// the map from the legacy fields (B-backfill, migration 102).
|
||||
function parseStoredEnginesMap(value: unknown): Record<string, EngineToggle> | null {
|
||||
if (!value || typeof value !== "object") return null;
|
||||
const out: Record<string, EngineToggle> = {};
|
||||
let any = false;
|
||||
for (const id of ENGINE_IDS) {
|
||||
const toggle = normalizeEngineToggle((value as JsonRecord)[id]);
|
||||
if (toggle) {
|
||||
out[id] = toggle;
|
||||
any = true;
|
||||
}
|
||||
}
|
||||
return any ? out : null;
|
||||
}
|
||||
|
||||
// Derive the per-engine toggle map from the legacy compression fields so existing installs keep
|
||||
// their behavior before they ever write an `engines` row. Single-engine modes (caveman/rtk/ultra/
|
||||
// aggressive) come from their dedicated config blocks; structural engines (lite/headroom/
|
||||
// session-dedup/ccr/llmlingua) come from the default-combo pipeline. `defaultMode` is a last-resort
|
||||
// signal that turns on its single-mode engine when nothing else already did.
|
||||
function deriveEnginesMap(config: CompressionConfig): Record<string, EngineToggle> {
|
||||
let defaultComboEngines = new Set<string>();
|
||||
try {
|
||||
const combo = getDefaultCompressionCombo();
|
||||
if (combo) {
|
||||
defaultComboEngines = new Set(combo.pipeline.map((step) => step.engine));
|
||||
}
|
||||
} catch {
|
||||
defaultComboEngines = new Set<string>();
|
||||
}
|
||||
|
||||
const engines: Record<string, EngineToggle> = {};
|
||||
for (const id of ENGINE_IDS) {
|
||||
let enabled = false;
|
||||
let level: string | undefined;
|
||||
switch (id) {
|
||||
case "caveman":
|
||||
enabled = config.cavemanConfig?.enabled === true;
|
||||
if (typeof config.cavemanConfig?.intensity === "string") {
|
||||
level = config.cavemanConfig.intensity;
|
||||
}
|
||||
break;
|
||||
case "rtk":
|
||||
enabled = config.rtkConfig?.enabled === true;
|
||||
if (typeof config.rtkConfig?.intensity === "string") {
|
||||
level = config.rtkConfig.intensity;
|
||||
}
|
||||
break;
|
||||
case "ultra":
|
||||
enabled = config.ultra?.enabled === true;
|
||||
break;
|
||||
case "aggressive":
|
||||
enabled = aggressiveEnabled(config.aggressive);
|
||||
break;
|
||||
default:
|
||||
// Structural engines (lite/headroom/session-dedup/ccr/llmlingua): on when present in the
|
||||
// default-combo pipeline.
|
||||
enabled = defaultComboEngines.has(id);
|
||||
break;
|
||||
}
|
||||
engines[id] = { enabled, ...(level !== undefined ? { level } : {}) };
|
||||
}
|
||||
|
||||
// Last-resort defaultMode signal: if the legacy install only set defaultMode (no engine config),
|
||||
// turn on the engine that mode actually ran so the derived default matches the old behavior.
|
||||
const fallbackEngine = SINGLE_MODE_ENGINE[config.defaultMode];
|
||||
if (fallbackEngine && engines[fallbackEngine] && !engines[fallbackEngine].enabled) {
|
||||
engines[fallbackEngine] = { ...engines[fallbackEngine], enabled: true };
|
||||
}
|
||||
|
||||
return engines;
|
||||
}
|
||||
|
||||
// `aggressive` config doesn't carry a top-level `enabled` flag in its type, but legacy installs may
|
||||
// have stored one. Read it defensively for the derived engines map.
|
||||
function aggressiveEnabled(value: AggressiveConfig | undefined): boolean {
|
||||
return toRecord(value).enabled === true;
|
||||
}
|
||||
|
||||
export async function getCompressionSettings(): Promise<CompressionConfig> {
|
||||
const db = getDbInstance();
|
||||
if (
|
||||
@@ -400,8 +518,14 @@ export async function getCompressionSettings(): Promise<CompressionConfig> {
|
||||
aggressive: normalizeAggressiveConfig(undefined),
|
||||
ultra: normalizeUltraConfig(undefined),
|
||||
contextEditing: { ...DEFAULT_CONTEXT_EDITING_CONFIG },
|
||||
engines: {},
|
||||
activeComboId: null,
|
||||
};
|
||||
|
||||
// Tracks whether a usable stored `engines` row was found. When absent (pre-migration-102 install)
|
||||
// we derive the engines map from the legacy fields below so behavior is preserved.
|
||||
let storedEngines: Record<string, EngineToggle> | null = null;
|
||||
|
||||
for (const row of rows) {
|
||||
const record = toRecord(row);
|
||||
const key = typeof record.key === "string" ? record.key : null;
|
||||
@@ -483,9 +607,30 @@ export async function getCompressionSettings(): Promise<CompressionConfig> {
|
||||
case "contextEditing":
|
||||
config.contextEditing = normalizeContextEditingConfig(parsed);
|
||||
break;
|
||||
case "engines":
|
||||
storedEngines = parseStoredEnginesMap(parsed);
|
||||
break;
|
||||
case "activeComboId":
|
||||
config.activeComboId =
|
||||
typeof parsed === "string" && parsed.trim() ? parsed.trim() : null;
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
// Engines map: prefer the stored row; otherwise derive from the legacy fields (migration 102
|
||||
// backfill on the read path). Always fill EVERY id in ENGINE_IDS so the shape matches
|
||||
// DEFAULT_COMPRESSION_CONFIG.
|
||||
const derived = storedEngines ?? deriveEnginesMap(config);
|
||||
const engines: Record<string, EngineToggle> = {};
|
||||
for (const id of ENGINE_IDS) {
|
||||
engines[id] = derived[id] ?? { enabled: false };
|
||||
}
|
||||
config.engines = engines;
|
||||
// Runtime-only marker: dispatch trusts the engines map only when it was explicitly stored
|
||||
// (panel-saved). A backfilled map (no stored row) is display-only — dispatch stays on the
|
||||
// legacy defaultMode/default-combo path so existing installs keep their behaviour.
|
||||
config.enginesExplicit = storedEngines !== null;
|
||||
|
||||
// Store in TTL cache (5s expiry)
|
||||
compressionSettingsCache = {
|
||||
value: config,
|
||||
@@ -507,6 +652,12 @@ export async function updateCompressionSettings(
|
||||
const tx = db.transaction(() => {
|
||||
for (const [key, value] of Object.entries(updates)) {
|
||||
if (value === undefined) continue;
|
||||
// Persist the engines map as ONE sanitized JSON row so the read path always gets
|
||||
// well-formed { enabled, level? } toggles for known engine ids.
|
||||
if (key === "engines") {
|
||||
insert.run(NAMESPACE, key, JSON.stringify(sanitizeEnginesForWrite(value)));
|
||||
continue;
|
||||
}
|
||||
insert.run(NAMESPACE, key, JSON.stringify(value));
|
||||
}
|
||||
});
|
||||
|
||||
5
src/lib/db/migrations/102_compression_engines_map.sql
Normal file
5
src/lib/db/migrations/102_compression_engines_map.sql
Normal file
@@ -0,0 +1,5 @@
|
||||
-- Phase 1 of the unified compression panel: the engines map + activeComboId become the
|
||||
-- single source. The engines map is DERIVED on read (normalizeCompressionSettings) from the
|
||||
-- legacy defaultMode + default-combo steps + caveman/rtk/ultra/aggressive config, so existing
|
||||
-- installs keep their behavior. Here we only ensure activeComboId defaults to NULL ("default").
|
||||
INSERT OR IGNORE INTO key_value (namespace, key, value) VALUES ('compression', 'activeComboId', 'null');
|
||||
@@ -17,10 +17,20 @@ interface EngineEntry {
|
||||
configSchema: EngineConfigField[];
|
||||
}
|
||||
|
||||
interface ComboStep {
|
||||
engine: string;
|
||||
intensity?: string;
|
||||
config?: Record<string, unknown>;
|
||||
// Engines whose detailed config has a dedicated sub-object in the compression
|
||||
// settings store. The on/off + level for ALL engines now live in the panel
|
||||
// (/dashboard/context/settings, the `engines` map); only these have a place to
|
||||
// persist the extra per-engine fields edited on this page. Structural engines
|
||||
// (lite, headroom, session-dedup, ccr, llmlingua) have no sub-object yet — their
|
||||
// page keeps the detail form + preview but has nothing extra to persist this phase.
|
||||
const SETTINGS_SUBOBJECT: Record<string, string> = {
|
||||
aggressive: "aggressive",
|
||||
ultra: "ultra",
|
||||
};
|
||||
|
||||
interface CompressionSettings {
|
||||
engines?: Record<string, { enabled?: boolean; level?: string }>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface Analytics {
|
||||
@@ -95,7 +105,6 @@ function renderDiffSegment(segment: PreviewDiffSegment, index: number) {
|
||||
export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
// ── Data state ──────────────────────────────────────────────────────────
|
||||
const [engine, setEngine] = useState<EngineEntry | null>(null);
|
||||
const [enabled, setEnabled] = useState(false);
|
||||
const [configState, setConfigState] = useState<Record<string, unknown>>({});
|
||||
const [analytics, setAnalytics] = useState<Analytics | null>(null);
|
||||
const [loadError, setLoadError] = useState<string | null>(null);
|
||||
@@ -109,7 +118,6 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
|
||||
// ── Action state ────────────────────────────────────────────────────────
|
||||
const [saveError, setSaveError] = useState<string | null>(null);
|
||||
const [toggleError, setToggleError] = useState<string | null>(null);
|
||||
const [saving, setSaving] = useState(false);
|
||||
|
||||
// ── Initial load ────────────────────────────────────────────────────────
|
||||
@@ -123,13 +131,13 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
// Fire the three independent reads in parallel — load time is the slowest
|
||||
// single request, not their sum. Each resolves to null on failure (fail-soft).
|
||||
const asJson = (r: Response) => (r.ok ? r.json() : null);
|
||||
const [enginesData, comboData, analyticsData] = await Promise.all([
|
||||
const [enginesData, settingsData, analyticsData] = await Promise.all([
|
||||
fetch("/api/compression/engines")
|
||||
.then(asJson)
|
||||
.catch(() => null) as Promise<{ engines: EngineEntry[] } | null>,
|
||||
fetch("/api/context/combos/default")
|
||||
fetch("/api/settings/compression")
|
||||
.then(asJson)
|
||||
.catch(() => null) as Promise<{ pipeline?: ComboStep[] } | null>,
|
||||
.catch(() => null) as Promise<CompressionSettings | null>,
|
||||
fetch(`/api/context/analytics/engine?engineId=${engineId}&days=7`)
|
||||
.then(asJson)
|
||||
.catch(() => null) as Promise<Analytics | null>,
|
||||
@@ -142,25 +150,22 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
setLoadError("Failed to load engine information.");
|
||||
}
|
||||
|
||||
// Derive enabled + currentConfig from the default combo (404/null = defaults)
|
||||
let currentEnabled = false;
|
||||
let currentConfig: Record<string, unknown> = {};
|
||||
const step = comboData?.pipeline?.find((s) => s.engine === engineId);
|
||||
if (step) {
|
||||
currentEnabled = step.config?.enabled !== false;
|
||||
currentConfig = step.config ?? {};
|
||||
}
|
||||
// Detailed config lives in the engine's settings sub-object (when it has one);
|
||||
// the on/off + level moved to the panel. 404/null/missing = schema defaults.
|
||||
const subKey = SETTINGS_SUBOBJECT[engineId];
|
||||
const stored = subKey ? settingsData?.[subKey] : undefined;
|
||||
const currentConfig: Record<string, unknown> =
|
||||
stored && typeof stored === "object" ? (stored as Record<string, unknown>) : {};
|
||||
|
||||
if (!cancelled) {
|
||||
if (analyticsData) setAnalytics(analyticsData);
|
||||
setEngine(foundEngine);
|
||||
setEnabled(currentEnabled);
|
||||
// Seed configState from defaultValues then override with currentConfig
|
||||
// Seed configState from defaultValues then override with the stored sub-object.
|
||||
const defaults: Record<string, unknown> = {};
|
||||
for (const field of foundEngine?.configSchema ?? []) {
|
||||
defaults[field.key] = field.defaultValue;
|
||||
}
|
||||
setConfigState({ ...defaults, ...currentConfig, enabled: currentEnabled });
|
||||
setConfigState({ ...defaults, ...currentConfig });
|
||||
setLoading(false);
|
||||
}
|
||||
}
|
||||
@@ -173,42 +178,26 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
|
||||
// ── Handlers ─────────────────────────────────────────────────────────────
|
||||
|
||||
async function handleToggle() {
|
||||
const next = !enabled;
|
||||
const nextConfig = { ...configState, enabled: next };
|
||||
setEnabled(next);
|
||||
setConfigState(nextConfig);
|
||||
setToggleError(null);
|
||||
try {
|
||||
const res = await fetch("/api/context/combos/default", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ engineId, enabled: next, config: nextConfig }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
setToggleError("Failed to update engine state.");
|
||||
setEnabled(!next); // revert
|
||||
setConfigState(configState);
|
||||
}
|
||||
} catch {
|
||||
setToggleError("Failed to update engine state.");
|
||||
setEnabled(!next); // revert
|
||||
setConfigState(configState);
|
||||
}
|
||||
}
|
||||
|
||||
// Persist the engine's DETAILED config to its settings sub-object. The on/off +
|
||||
// level are owned by the panel (the `engines` map) and are NOT written here — so
|
||||
// this page never touches the deprecated /api/context/combos/default route.
|
||||
async function handleSave() {
|
||||
const nextEnabled = typeof configState.enabled === "boolean" ? configState.enabled : enabled;
|
||||
const nextConfig = { ...configState, enabled: nextEnabled };
|
||||
setEnabled(nextEnabled);
|
||||
setConfigState(nextConfig);
|
||||
const subKey = SETTINGS_SUBOBJECT[engineId];
|
||||
if (!subKey) {
|
||||
// Structural engines have no detail store yet — nothing to persist this phase.
|
||||
setSaveError(null);
|
||||
return;
|
||||
}
|
||||
// Strip the `enabled` key — engine on/off is the panel's responsibility.
|
||||
const { enabled: _ignored, ...detail } = configState;
|
||||
void _ignored;
|
||||
setSaving(true);
|
||||
setSaveError(null);
|
||||
try {
|
||||
const res = await fetch("/api/context/combos/default", {
|
||||
const res = await fetch("/api/settings/compression", {
|
||||
method: "PUT",
|
||||
headers: { "Content-Type": "application/json" },
|
||||
body: JSON.stringify({ engineId, enabled: nextEnabled, config: nextConfig }),
|
||||
body: JSON.stringify({ [subKey]: detail }),
|
||||
});
|
||||
if (!res.ok) {
|
||||
setSaveError("Failed to save configuration.");
|
||||
@@ -264,6 +253,8 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
|
||||
const subtitle = engine.metadata?.description ?? engine.description;
|
||||
const visibleConfigSchema = engine.configSchema.filter((field) => field.key !== "enabled");
|
||||
// Only engines with a dedicated settings sub-object can persist their detail here.
|
||||
const persistable = Boolean(SETTINGS_SUBOBJECT[engineId]);
|
||||
|
||||
return (
|
||||
<div className="flex flex-col gap-6 p-6 max-w-3xl">
|
||||
@@ -289,33 +280,14 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
</p>
|
||||
)}
|
||||
|
||||
{/* ── Enable toggle ── */}
|
||||
<div className="flex flex-col gap-2 rounded-lg border border-border bg-surface p-4">
|
||||
<div className="flex items-center justify-between">
|
||||
<div className="flex flex-col gap-0.5">
|
||||
<span className="text-sm font-medium text-text">Enable layer</span>
|
||||
<span className="text-xs text-text-muted">
|
||||
{enabled
|
||||
? "This layer is active in the default pipeline."
|
||||
: "This layer is inactive."}
|
||||
</span>
|
||||
</div>
|
||||
<input
|
||||
type="checkbox"
|
||||
data-toggle="enable"
|
||||
checked={enabled}
|
||||
onChange={handleToggle}
|
||||
className="h-4 w-4 accent-primary cursor-pointer"
|
||||
aria-label="Enable layer"
|
||||
/>
|
||||
</div>
|
||||
{toggleError && <p className="text-xs text-destructive">{toggleError}</p>}
|
||||
<p className="text-xs text-text-muted" data-testid="stacked-mode-notice">
|
||||
Enabled layers run when compression is in "stacked" mode. Configure it in{" "}
|
||||
{/* ── Panel pointer (on/off + level live there now) ── */}
|
||||
<div className="flex flex-col gap-1 rounded-lg border border-border bg-surface p-4">
|
||||
<p className="text-xs text-text-muted" data-testid="panel-pointer-notice">
|
||||
Turn this layer on/off and set its level in{" "}
|
||||
<a href="/dashboard/context/settings" className="underline hover:text-text">
|
||||
Compression Settings
|
||||
</a>
|
||||
.
|
||||
. This page edits its detailed configuration only.
|
||||
</p>
|
||||
</div>
|
||||
|
||||
@@ -332,13 +304,20 @@ export function EngineConfigPage({ engineId }: { engineId: string }) {
|
||||
<p className="text-sm text-text-muted">No additional configuration.</p>
|
||||
)}
|
||||
<div className="flex items-center gap-3 pt-1">
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="px-4 py-1.5 rounded bg-primary text-primary-foreground text-sm font-medium disabled:opacity-50"
|
||||
>
|
||||
{saving ? "Saving..." : "Save"}
|
||||
</button>
|
||||
{persistable ? (
|
||||
<button
|
||||
onClick={handleSave}
|
||||
disabled={saving}
|
||||
className="px-4 py-1.5 rounded bg-primary text-primary-foreground text-sm font-medium disabled:opacity-50"
|
||||
>
|
||||
{saving ? "Saving..." : "Save"}
|
||||
</button>
|
||||
) : (
|
||||
<p className="text-xs text-text-muted" data-testid="no-detail-store-notice">
|
||||
This layer is configured by the global settings; there is no per-engine override to
|
||||
save here yet.
|
||||
</p>
|
||||
)}
|
||||
{saveError && <p className="text-xs text-destructive">{saveError}</p>}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
@@ -9,8 +9,9 @@ export const HIDEABLE_SIDEBAR_ITEM_IDS = [
|
||||
"combos",
|
||||
"combos-live",
|
||||
"quota",
|
||||
// OmniProxy > Compression Context
|
||||
// OmniProxy > Compression Context (Settings → Combos → engines → Studio)
|
||||
"context-settings",
|
||||
"context-combos",
|
||||
"context-caveman",
|
||||
"context-rtk",
|
||||
"context-headroom",
|
||||
@@ -20,7 +21,6 @@ export const HIDEABLE_SIDEBAR_ITEM_IDS = [
|
||||
"context-lite",
|
||||
"context-aggressive",
|
||||
"context-ultra",
|
||||
"context-combos",
|
||||
"compression-studio",
|
||||
// OmniProxy > Tools
|
||||
"cli-code",
|
||||
@@ -235,6 +235,7 @@ export const COMPRESSION_CONTEXT_GROUP: SidebarItemGroup = {
|
||||
id: "compression-context",
|
||||
titleKey: "compressionContextGroup",
|
||||
titleFallback: "Compression Context",
|
||||
// Order: Settings (the unified panel) → Combos → per-engine pages → Studio (analytics).
|
||||
items: [
|
||||
{
|
||||
id: "context-settings",
|
||||
@@ -244,6 +245,13 @@ export const COMPRESSION_CONTEXT_GROUP: SidebarItemGroup = {
|
||||
subtitleFallback: "Global defaults",
|
||||
icon: "settings",
|
||||
},
|
||||
{
|
||||
id: "context-combos",
|
||||
href: "/dashboard/context/combos",
|
||||
i18nKey: "contextCombos",
|
||||
subtitleKey: "contextCombosSubtitle",
|
||||
icon: "hub",
|
||||
},
|
||||
{
|
||||
id: "context-caveman",
|
||||
href: "/dashboard/context/caveman",
|
||||
@@ -314,13 +322,6 @@ export const COMPRESSION_CONTEXT_GROUP: SidebarItemGroup = {
|
||||
subtitleFallback: "Heuristic pruning",
|
||||
icon: "bolt",
|
||||
},
|
||||
{
|
||||
id: "context-combos",
|
||||
href: "/dashboard/context/combos",
|
||||
i18nKey: "contextCombos",
|
||||
subtitleKey: "contextCombosSubtitle",
|
||||
icon: "hub",
|
||||
},
|
||||
{
|
||||
id: "compression-studio",
|
||||
href: "/dashboard/compression/studio",
|
||||
|
||||
@@ -170,6 +170,11 @@ export const stackedPipelineStepSchema = z.discriminatedUnion("engine", [
|
||||
.strict(),
|
||||
]);
|
||||
|
||||
export const engineToggleSchema = z.object({
|
||||
enabled: z.boolean(),
|
||||
level: z.string().optional(),
|
||||
});
|
||||
|
||||
export const compressionSettingsUpdateSchema = z
|
||||
.object({
|
||||
enabled: z.boolean().optional(),
|
||||
@@ -189,6 +194,8 @@ export const compressionSettingsUpdateSchema = z
|
||||
aggressive: aggressiveConfigSchema.optional(),
|
||||
ultra: ultraConfigSchema.optional(),
|
||||
contextEditing: contextEditingConfigSchema.optional(),
|
||||
engines: z.record(z.string(), engineToggleSchema).optional(),
|
||||
activeComboId: z.string().nullable().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
|
||||
@@ -1,7 +1,10 @@
|
||||
import { describe, it, beforeEach, afterEach } from "node:test";
|
||||
import { describe, it, beforeEach, afterEach, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import { dirname, join } from "node:path";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const __filename = fileURLToPath(import.meta.url);
|
||||
const __dirname = dirname(__filename);
|
||||
@@ -107,3 +110,104 @@ describe("Compression Settings API Schema Validation", () => {
|
||||
assert.equal(validRoles.length, 3);
|
||||
});
|
||||
});
|
||||
|
||||
// ─── Route round-trip: engines map + activeComboId ─────────────────────────
|
||||
// Mirrors the mcp-accessibility-config test harness: allocate a temp DATA_DIR,
|
||||
// import route + DB modules, tear down in after().
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-compression-route-"));
|
||||
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../../../src/lib/db/core.ts");
|
||||
const route = await import("../../../../src/app/api/settings/compression/route.ts");
|
||||
|
||||
function makeRequest(method: string, body?: unknown): Request {
|
||||
return new Request("http://localhost/api/settings/compression", {
|
||||
method,
|
||||
headers: body !== undefined ? { "content-type": "application/json" } : {},
|
||||
body: body !== undefined ? JSON.stringify(body) : undefined,
|
||||
// eslint-disable-next-line @typescript-eslint/no-explicit-any
|
||||
}) as any;
|
||||
}
|
||||
|
||||
describe("settings/compression route — engines + activeComboId", () => {
|
||||
beforeEach(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
});
|
||||
|
||||
after(() => {
|
||||
core.resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
if (ORIGINAL_DATA_DIR === undefined) delete process.env.DATA_DIR;
|
||||
else process.env.DATA_DIR = ORIGINAL_DATA_DIR;
|
||||
});
|
||||
|
||||
it("PUT engines map persists and GET returns engines + activeComboId", async () => {
|
||||
const putRes = await route.PUT(
|
||||
makeRequest("PUT", { engines: { rtk: { enabled: true, level: "standard" } } })
|
||||
);
|
||||
assert.equal(putRes.status, 200);
|
||||
|
||||
// Fresh DB handle so we read from storage, not from the write-path return value.
|
||||
core.resetDbInstance();
|
||||
|
||||
const getRes = await route.GET(makeRequest("GET"));
|
||||
assert.equal(getRes.status, 200);
|
||||
const body = await getRes.json();
|
||||
|
||||
assert.equal(
|
||||
body.engines?.rtk?.enabled,
|
||||
true,
|
||||
"engines.rtk.enabled should be true after PUT"
|
||||
);
|
||||
assert.equal(
|
||||
body.engines?.rtk?.level,
|
||||
"standard",
|
||||
"engines.rtk.level should be 'standard' after PUT"
|
||||
);
|
||||
// activeComboId is always present (null by default)
|
||||
assert.ok("activeComboId" in body, "GET response must include activeComboId");
|
||||
});
|
||||
|
||||
it("PUT activeComboId persists and is returned by GET", async () => {
|
||||
const putRes = await route.PUT(makeRequest("PUT", { activeComboId: "combo-abc" }));
|
||||
assert.equal(putRes.status, 200);
|
||||
|
||||
core.resetDbInstance();
|
||||
|
||||
const getRes = await route.GET(makeRequest("GET"));
|
||||
assert.equal(getRes.status, 200);
|
||||
const body = await getRes.json();
|
||||
assert.equal(body.activeComboId, "combo-abc");
|
||||
});
|
||||
|
||||
it("PUT activeComboId:null clears the active combo", async () => {
|
||||
// First set it, then clear.
|
||||
await route.PUT(makeRequest("PUT", { activeComboId: "combo-to-clear" }));
|
||||
core.resetDbInstance();
|
||||
await route.PUT(makeRequest("PUT", { activeComboId: null }));
|
||||
core.resetDbInstance();
|
||||
|
||||
const getRes = await route.GET(makeRequest("GET"));
|
||||
assert.equal(getRes.status, 200);
|
||||
const body = await getRes.json();
|
||||
assert.equal(body.activeComboId, null);
|
||||
});
|
||||
|
||||
it("PUT with invalid engines shape is rejected by schema validation (400)", async () => {
|
||||
// engines values must have an `enabled` boolean — passing a string should fail the schema.
|
||||
const putRes = await route.PUT(
|
||||
makeRequest("PUT", { engines: { rtk: { enabled: "yes" } } })
|
||||
);
|
||||
assert.equal(putRes.status, 400);
|
||||
const body = await putRes.json();
|
||||
// Validation failures use { error: { message, details } } via validateBody helper.
|
||||
assert.ok(body.error !== null && typeof body.error === "object", "error should be an object");
|
||||
const errorMessage: string =
|
||||
typeof body.error === "string" ? body.error : (body.error?.message ?? JSON.stringify(body.error));
|
||||
assert.ok(!errorMessage.includes("at /"), "error must not contain a stack trace");
|
||||
});
|
||||
});
|
||||
|
||||
@@ -1,5 +1,10 @@
|
||||
/**
|
||||
* TDD: GET /api/context/combos/default and PUT /api/context/combos/default
|
||||
* TDD: GET /api/context/combos/default is a read-only shim.
|
||||
*
|
||||
* The default compression pipeline is now DERIVED from the engines map
|
||||
* (open-sse deriveDefaultPlan) rather than editable here:
|
||||
* - GET → returns the derived default plan for the live config.
|
||||
* - PUT → rejected with a deprecation error (not 200); body carries no stack trace.
|
||||
*
|
||||
* Auth + isolation pattern mirrors tests/unit/api/context-analytics-engine-route.test.ts:
|
||||
* - makeManagementSessionRequest() for JWT cookie auth.
|
||||
@@ -12,6 +17,7 @@ import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
import { makeManagementSessionRequest } from "../../helpers/managementSession.ts";
|
||||
import { deriveDefaultPlan } from "@omniroute/open-sse/services/compression/deriveDefaultPlan.ts";
|
||||
|
||||
// ─── isolated temp DB ─────────────────────────────────────────────────────────
|
||||
|
||||
@@ -23,6 +29,7 @@ process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const core = await import("../../../src/lib/db/core.ts");
|
||||
const settingsDb = await import("../../../src/lib/db/settings.ts");
|
||||
const compressionDb = await import("../../../src/lib/db/compression.ts");
|
||||
const defaultRoute = await import("../../../src/app/api/context/combos/default/route.ts");
|
||||
|
||||
// ─── helpers ──────────────────────────────────────────────────────────────────
|
||||
@@ -56,54 +63,102 @@ test.after(() => {
|
||||
|
||||
// ─── tests ────────────────────────────────────────────────────────────────────
|
||||
|
||||
test("GET /api/context/combos/default returns the default combo", async () => {
|
||||
test("GET /api/context/combos/default returns the derived default plan (single-mode caveman)", async () => {
|
||||
await compressionDb.updateCompressionSettings({
|
||||
enabled: true,
|
||||
engines: { caveman: { enabled: true, level: "full" } },
|
||||
});
|
||||
|
||||
const req = await makeManagementSessionRequest("http://localhost/api/context/combos/default");
|
||||
const res = await defaultRoute.GET(req);
|
||||
assert.equal(res.status, 200, `Expected 200, got ${res.status}`);
|
||||
|
||||
const body = (await res.json()) as {
|
||||
id: string;
|
||||
isDefault: boolean;
|
||||
pipeline: Array<{ engine: string }>;
|
||||
mode: string;
|
||||
pipeline: Array<{ engine: string; intensity?: string }>;
|
||||
};
|
||||
assert.equal(typeof body.id, "string", "response should have an id string");
|
||||
assert.equal(body.isDefault, true, "returned combo should be the default");
|
||||
assert.ok(Array.isArray(body.pipeline), "pipeline should be an array");
|
||||
|
||||
// caveman alone is single-mode → effective mode "standard", empty stacked pipeline.
|
||||
const expected = deriveDefaultPlan({ caveman: { enabled: true, level: "full" } }, true);
|
||||
assert.equal(body.mode, expected.mode, `expected mode ${expected.mode}`);
|
||||
assert.equal(body.mode, "standard");
|
||||
assert.deepEqual(body.pipeline, expected.stackedPipeline);
|
||||
});
|
||||
|
||||
test("PUT /api/context/combos/default enabling headroom returns combo with headroom in pipeline", async () => {
|
||||
test("GET /api/context/combos/default returns the derived stacked pipeline (reflects enabled engines)", async () => {
|
||||
await compressionDb.updateCompressionSettings({
|
||||
enabled: true,
|
||||
engines: {
|
||||
caveman: { enabled: true, level: "full" },
|
||||
headroom: { enabled: true },
|
||||
},
|
||||
});
|
||||
|
||||
const req = await makeManagementSessionRequest("http://localhost/api/context/combos/default");
|
||||
const res = await defaultRoute.GET(req);
|
||||
assert.equal(res.status, 200, `Expected 200, got ${res.status}`);
|
||||
|
||||
const body = (await res.json()) as {
|
||||
mode: string;
|
||||
pipeline: Array<{ engine: string; intensity?: string }>;
|
||||
};
|
||||
|
||||
const expected = deriveDefaultPlan(
|
||||
{ caveman: { enabled: true, level: "full" }, headroom: { enabled: true } },
|
||||
true
|
||||
);
|
||||
assert.equal(body.mode, "stacked");
|
||||
assert.deepEqual(body.pipeline, expected.stackedPipeline);
|
||||
const engineIds = body.pipeline.map((s) => s.engine);
|
||||
assert.ok(engineIds.includes("caveman"), `expected caveman in derived pipeline, got: ${engineIds}`);
|
||||
});
|
||||
|
||||
test("GET /api/context/combos/default returns off when master switch is disabled", async () => {
|
||||
await compressionDb.updateCompressionSettings({
|
||||
enabled: false,
|
||||
engines: { caveman: { enabled: true, level: "full" } },
|
||||
});
|
||||
|
||||
const req = await makeManagementSessionRequest("http://localhost/api/context/combos/default");
|
||||
const res = await defaultRoute.GET(req);
|
||||
assert.equal(res.status, 200, `Expected 200, got ${res.status}`);
|
||||
|
||||
const body = (await res.json()) as { mode: string; pipeline: unknown[] };
|
||||
assert.equal(body.mode, "off");
|
||||
assert.deepEqual(body.pipeline, []);
|
||||
});
|
||||
|
||||
test("PUT /api/context/combos/default is deprecated and rejects writes (not 200)", async () => {
|
||||
const req = await makeManagementSessionRequest("http://localhost/api/context/combos/default", {
|
||||
method: "PUT",
|
||||
body: JSON.stringify({ engineId: "headroom", enabled: true }),
|
||||
});
|
||||
const res = await defaultRoute.PUT(req);
|
||||
assert.equal(res.status, 200, `Expected 200, got ${res.status}`);
|
||||
assert.notEqual(res.status, 200, "PUT must no longer succeed — the route is read-only");
|
||||
assert.ok(res.status >= 400, `expected a 4xx deprecation status, got ${res.status}`);
|
||||
|
||||
const body = (await res.json()) as {
|
||||
pipeline: Array<{ engine: string }>;
|
||||
};
|
||||
assert.ok(Array.isArray(body.pipeline), "pipeline should be an array");
|
||||
const engineIds = body.pipeline.map((s) => s.engine);
|
||||
assert.ok(engineIds.includes("headroom"), `expected headroom in pipeline, got: ${engineIds}`);
|
||||
const body = (await res.json()) as { error?: { message?: string } | string };
|
||||
// The deprecation message points editors at the engines settings.
|
||||
const message =
|
||||
typeof body.error === "string" ? body.error : (body.error?.message ?? JSON.stringify(body));
|
||||
assert.match(message, /derived|engines|deprecat/i, `unexpected deprecation message: ${message}`);
|
||||
|
||||
// Hard Rule #12: no raw stack trace leaks into the response body. Match the V8
|
||||
// stack-frame shape (" at fn (file:line:col)") rather than the bare "at " token,
|
||||
// which legitimately appears in the URL inside the deprecation message.
|
||||
assert.ok(
|
||||
!/\bat\s+\S+\s+\(?\/?\S+:\d+:\d+/.test(JSON.stringify(body)),
|
||||
"error body must not contain a stack trace"
|
||||
);
|
||||
assert.ok(!/\n\s+at\s/.test(JSON.stringify(body)), "error body must not contain a stack trace");
|
||||
});
|
||||
|
||||
test("PUT /api/context/combos/default with bad input returns 400", async () => {
|
||||
test("POST /api/context/combos/default is deprecated and rejects writes (not 200)", async () => {
|
||||
const req = await makeManagementSessionRequest("http://localhost/api/context/combos/default", {
|
||||
method: "PUT",
|
||||
// Missing required 'enabled' field
|
||||
body: JSON.stringify({ engineId: "headroom" }),
|
||||
method: "POST",
|
||||
body: JSON.stringify({ engineId: "headroom", enabled: true }),
|
||||
});
|
||||
const res = await defaultRoute.PUT(req);
|
||||
assert.equal(res.status, 400, `Expected 400 for missing 'enabled', got ${res.status}`);
|
||||
const body = (await res.json()) as { error: unknown };
|
||||
assert.ok(body.error !== undefined, "response should have an error field");
|
||||
});
|
||||
|
||||
test("PUT /api/context/combos/default with invalid JSON body returns 400", async () => {
|
||||
const req = await makeManagementSessionRequest("http://localhost/api/context/combos/default", {
|
||||
method: "PUT",
|
||||
body: "not-json",
|
||||
});
|
||||
const res = await defaultRoute.PUT(req);
|
||||
assert.equal(res.status, 400, `Expected 400 for invalid JSON, got ${res.status}`);
|
||||
const res = await defaultRoute.POST(req);
|
||||
assert.notEqual(res.status, 200, "POST must no longer succeed — the route is read-only");
|
||||
assert.ok(res.status >= 400, `expected a 4xx deprecation status, got ${res.status}`);
|
||||
});
|
||||
|
||||
@@ -0,0 +1,69 @@
|
||||
import { test, after } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import fs from "node:fs";
|
||||
import os from "node:os";
|
||||
import path from "node:path";
|
||||
|
||||
const TEST_DATA_DIR = fs.mkdtempSync(path.join(os.tmpdir(), "omniroute-compression-engines-"));
|
||||
const ORIGINAL_DATA_DIR = process.env.DATA_DIR;
|
||||
process.env.DATA_DIR = TEST_DATA_DIR;
|
||||
|
||||
const { getDbInstance, resetDbInstance } = await import("../../../src/lib/db/core.ts");
|
||||
const { getCompressionSettings, updateCompressionSettings } = await import(
|
||||
"../../../src/lib/db/compression.ts"
|
||||
);
|
||||
|
||||
function freshDir() {
|
||||
resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
fs.mkdirSync(TEST_DATA_DIR, { recursive: true });
|
||||
}
|
||||
|
||||
after(() => {
|
||||
resetDbInstance();
|
||||
fs.rmSync(TEST_DATA_DIR, { recursive: true, force: true });
|
||||
if (ORIGINAL_DATA_DIR === undefined) {
|
||||
delete process.env.DATA_DIR;
|
||||
} else {
|
||||
process.env.DATA_DIR = ORIGINAL_DATA_DIR;
|
||||
}
|
||||
});
|
||||
|
||||
test("migration backfills engines map from prior defaultMode + default combo", async () => {
|
||||
freshDir();
|
||||
const db = getDbInstance(); // runs migrations incl. 102
|
||||
// simulate a pre-102 install: master on, defaultMode 'standard', caveman enabled
|
||||
db.prepare(
|
||||
"INSERT OR REPLACE INTO key_value(namespace,key,value) VALUES('compression','enabled','true')"
|
||||
).run();
|
||||
db.prepare(
|
||||
"INSERT OR REPLACE INTO key_value(namespace,key,value) VALUES('compression','defaultMode','\"standard\"')"
|
||||
).run();
|
||||
db.prepare(
|
||||
"INSERT OR REPLACE INTO key_value(namespace,key,value) VALUES('compression','cavemanConfig','{\"enabled\":true}')"
|
||||
).run();
|
||||
const cfg = await getCompressionSettings();
|
||||
assert.equal(cfg.engines.caveman.enabled, true);
|
||||
assert.equal(cfg.activeComboId, null);
|
||||
// No stored engines row → backfilled map is display-only; dispatch stays on the legacy path.
|
||||
assert.equal(cfg.enginesExplicit, false);
|
||||
});
|
||||
|
||||
test("engines map persists round-trip + activeComboId", async () => {
|
||||
freshDir();
|
||||
getDbInstance();
|
||||
await updateCompressionSettings({
|
||||
enabled: true,
|
||||
engines: {
|
||||
rtk: { enabled: true, level: "standard" },
|
||||
caveman: { enabled: true, level: "full" },
|
||||
},
|
||||
activeComboId: null,
|
||||
});
|
||||
const cfg = await getCompressionSettings();
|
||||
assert.equal(cfg.engines.rtk.enabled, true);
|
||||
assert.equal(cfg.engines.rtk.level, "standard");
|
||||
assert.equal(cfg.engines.caveman.level, "full");
|
||||
// A stored engines row exists → the panel configured engines; dispatch trusts the map.
|
||||
assert.equal(cfg.enginesExplicit, true);
|
||||
});
|
||||
33
tests/unit/compression/derive-default-plan.test.ts
Normal file
33
tests/unit/compression/derive-default-plan.test.ts
Normal file
@@ -0,0 +1,33 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { deriveDefaultPlan } from "@omniroute/open-sse/services/compression/deriveDefaultPlan.ts";
|
||||
|
||||
const on = (level?: string) => ({ enabled: true, ...(level ? { level } : {}) });
|
||||
|
||||
test("master off / empty / none-on => off", () => {
|
||||
assert.deepEqual(deriveDefaultPlan({}, false), { mode: "off", stackedPipeline: [] });
|
||||
assert.deepEqual(deriveDefaultPlan({}, true), { mode: "off", stackedPipeline: [] });
|
||||
assert.deepEqual(deriveDefaultPlan({ rtk: { enabled: false } }, true), { mode: "off", stackedPipeline: [] });
|
||||
});
|
||||
|
||||
test("exactly one single-mode engine => that mode", () => {
|
||||
assert.deepEqual(deriveDefaultPlan({ caveman: on("full") }, true), { mode: "standard", stackedPipeline: [] });
|
||||
assert.deepEqual(deriveDefaultPlan({ rtk: on("minimal") }, true), { mode: "rtk", stackedPipeline: [] });
|
||||
assert.deepEqual(deriveDefaultPlan({ lite: on() }, true), { mode: "lite", stackedPipeline: [] });
|
||||
});
|
||||
|
||||
test("one non-single-mode engine => stacked with that engine", () => {
|
||||
const p = deriveDefaultPlan({ headroom: on() }, true);
|
||||
assert.equal(p.mode, "stacked");
|
||||
assert.deepEqual(p.stackedPipeline, [{ engine: "headroom" }]);
|
||||
});
|
||||
|
||||
test("multiple engines => stacked in stackPriority order, levels as intensity", () => {
|
||||
const p = deriveDefaultPlan({ caveman: on("full"), rtk: on("standard"), headroom: on() }, true);
|
||||
assert.equal(p.mode, "stacked");
|
||||
assert.deepEqual(p.stackedPipeline, [
|
||||
{ engine: "rtk", intensity: "standard" }, // pri 10
|
||||
{ engine: "headroom" }, // pri 15
|
||||
{ engine: "caveman", intensity: "full" }, // pri 20
|
||||
]);
|
||||
});
|
||||
133
tests/unit/compression/derived-pipeline-integration.test.ts
Normal file
133
tests/unit/compression/derived-pipeline-integration.test.ts
Normal file
@@ -0,0 +1,133 @@
|
||||
import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
|
||||
import { applyCompressionAsync } from "../../../open-sse/services/compression/index.ts";
|
||||
import { selectCompressionPlan } from "../../../open-sse/services/compression/strategySelector.ts";
|
||||
import { DEFAULT_COMPRESSION_CONFIG } from "../../../open-sse/services/compression/types.ts";
|
||||
import type {
|
||||
CompressionConfig,
|
||||
CompressionPipelineStep,
|
||||
} from "../../../open-sse/services/compression/types.ts";
|
||||
|
||||
/**
|
||||
* End-to-end coverage for the derived stacked pipeline (Task 12).
|
||||
*
|
||||
* Proves that the per-engine toggle map (`config.engines`) drives a derived
|
||||
* `stacked` plan whose pipeline, when fed back to {@link applyCompressionAsync},
|
||||
* runs the real rtk → caveman engines — and that the derived run is equivalent to
|
||||
* an explicit `stackedPipeline` config. i.e. "derived == explicit".
|
||||
*/
|
||||
describe("compression derived-pipeline integration (Task 12)", () => {
|
||||
// A realistic body: a noisy tool result (rtk dedupes) plus a prose user turn.
|
||||
function makeBody(): Record<string, unknown> {
|
||||
return {
|
||||
messages: [
|
||||
{
|
||||
role: "tool",
|
||||
content: Array.from({ length: 8 }, () => "same noisy tool output line").join("\n"),
|
||||
},
|
||||
{
|
||||
role: "user",
|
||||
content:
|
||||
"Please provide a detailed explanation of the authentication configuration and how it works",
|
||||
},
|
||||
],
|
||||
};
|
||||
}
|
||||
|
||||
// Engines map: only rtk + caveman(full) on. rtk has no level → no intensity in the
|
||||
// derived step; caveman level "full" → intensity "full".
|
||||
function deriveConfig(): CompressionConfig {
|
||||
return {
|
||||
...DEFAULT_COMPRESSION_CONFIG,
|
||||
enabled: true,
|
||||
// Clear the seeded default stackedPipeline so the derived plan is the only source.
|
||||
stackedPipeline: [],
|
||||
// Panel-configured: the engines map drives dispatch (a stored engines row exists).
|
||||
enginesExplicit: true,
|
||||
engines: {
|
||||
...DEFAULT_COMPRESSION_CONFIG.engines,
|
||||
rtk: { enabled: true },
|
||||
caveman: { enabled: true, level: "full" },
|
||||
},
|
||||
};
|
||||
}
|
||||
|
||||
const EXPLICIT_PIPELINE: CompressionPipelineStep[] = [
|
||||
{ engine: "rtk" },
|
||||
{ engine: "caveman", intensity: "full" },
|
||||
] as unknown as CompressionPipelineStep[];
|
||||
|
||||
it("derives a stacked plan with rtk → caveman(full) in stackPriority order", () => {
|
||||
const config = deriveConfig();
|
||||
// Enough tokens that auto-trigger is irrelevant (autoTriggerTokens is 0 by default,
|
||||
// so the derived default path is what we want — pass a real estimate anyway).
|
||||
const plan = selectCompressionPlan(config, null, 5000);
|
||||
|
||||
assert.equal(plan.mode, "stacked");
|
||||
assert.deepEqual(plan.stackedPipeline, [
|
||||
{ engine: "rtk" },
|
||||
{ engine: "caveman", intensity: "full" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("runs BOTH rtk and caveman when applying the derived pipeline", async () => {
|
||||
const config = deriveConfig();
|
||||
const plan = selectCompressionPlan(config, null, 5000);
|
||||
assert.equal(plan.mode, "stacked");
|
||||
|
||||
// Feed the derived pipeline back through the real async apply path.
|
||||
const runConfig: CompressionConfig = {
|
||||
...config,
|
||||
stackedPipeline: plan.stackedPipeline as CompressionPipelineStep[],
|
||||
};
|
||||
const result = await applyCompressionAsync(makeBody(), "stacked", { config: runConfig });
|
||||
|
||||
assert.equal(result.stats?.engine, "stacked");
|
||||
const ran = result.stats?.engineBreakdown?.map((e) => e.engine) ?? [];
|
||||
assert.deepEqual(ran, ["rtk", "caveman"], "both engines must run, rtk before caveman");
|
||||
});
|
||||
|
||||
it("derived pipeline is equivalent to an explicit stackedPipeline (derived == explicit)", async () => {
|
||||
const derivedConfig = deriveConfig();
|
||||
const derivedPlan = selectCompressionPlan(derivedConfig, null, 5000);
|
||||
assert.deepEqual(derivedPlan.stackedPipeline, EXPLICIT_PIPELINE);
|
||||
|
||||
const derivedResult = await applyCompressionAsync(makeBody(), "stacked", {
|
||||
config: {
|
||||
...derivedConfig,
|
||||
stackedPipeline: derivedPlan.stackedPipeline as CompressionPipelineStep[],
|
||||
},
|
||||
});
|
||||
|
||||
// Second config: NO engines map driving the plan — an explicit stackedPipeline only.
|
||||
const explicitConfig: CompressionConfig = {
|
||||
...DEFAULT_COMPRESSION_CONFIG,
|
||||
enabled: true,
|
||||
stackedPipeline: EXPLICIT_PIPELINE,
|
||||
engines: {}, // explicit-only: the engines map plays no part here
|
||||
};
|
||||
const explicitResult = await applyCompressionAsync(makeBody(), "stacked", {
|
||||
config: explicitConfig,
|
||||
});
|
||||
|
||||
// Same engines ran, in the same order.
|
||||
assert.deepEqual(
|
||||
derivedResult.stats?.engineBreakdown?.map((e) => e.engine),
|
||||
explicitResult.stats?.engineBreakdown?.map((e) => e.engine),
|
||||
"derived and explicit must run the same engine set in the same order"
|
||||
);
|
||||
|
||||
// Same compressed output text for the prose user turn.
|
||||
const userText = (r: typeof derivedResult): string => {
|
||||
const messages = r.body.messages as Array<{ role: string; content: unknown }>;
|
||||
const user = messages.find((m) => m.role === "user");
|
||||
return typeof user?.content === "string" ? user.content : JSON.stringify(user?.content);
|
||||
};
|
||||
assert.equal(
|
||||
userText(derivedResult),
|
||||
userText(explicitResult),
|
||||
"derived and explicit must produce identical compressed text"
|
||||
);
|
||||
});
|
||||
});
|
||||
28
tests/unit/compression/engine-catalog.test.ts
Normal file
28
tests/unit/compression/engine-catalog.test.ts
Normal file
@@ -0,0 +1,28 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { ENGINE_CATALOG, engineMeta, ENGINE_IDS } from "@omniroute/open-sse/services/compression/engineCatalog.ts";
|
||||
import { DEFAULT_COMPRESSION_CONFIG } from "@omniroute/open-sse/services/compression/types.ts";
|
||||
|
||||
test("catalog lists every engine with stackPriority", () => {
|
||||
for (const id of ["session-dedup","ccr","lite","rtk","headroom","caveman","aggressive","llmlingua","ultra"]) {
|
||||
assert.ok(engineMeta(id), `${id} present`);
|
||||
assert.equal(typeof engineMeta(id).stackPriority, "number");
|
||||
}
|
||||
});
|
||||
test("levels + single-mode flags are correct", () => {
|
||||
assert.deepEqual(engineMeta("rtk").levels, ["minimal","standard","aggressive"]);
|
||||
assert.deepEqual(engineMeta("caveman").levels, ["lite","full","ultra"]);
|
||||
assert.equal(engineMeta("headroom").levels, undefined);
|
||||
assert.equal(engineMeta("caveman").isSingleMode, true);
|
||||
assert.equal(engineMeta("headroom").isSingleMode, false);
|
||||
});
|
||||
test("ENGINE_IDS is ordered by stackPriority", () => {
|
||||
const ps = ENGINE_IDS.map((id) => engineMeta(id).stackPriority);
|
||||
assert.deepEqual(ps, [...ps].sort((a,b)=>a-b));
|
||||
});
|
||||
test("default config has an engines map + activeComboId", () => {
|
||||
assert.equal(typeof DEFAULT_COMPRESSION_CONFIG.engines, "object");
|
||||
assert.equal(DEFAULT_COMPRESSION_CONFIG.activeComboId, null);
|
||||
// default-off: every engine disabled by default (opt-in preserved)
|
||||
for (const id of ENGINE_IDS) assert.equal(DEFAULT_COMPRESSION_CONFIG.engines[id]?.enabled, false);
|
||||
});
|
||||
22
tests/unit/compression/resolve-compression-plan.test.ts
Normal file
22
tests/unit/compression/resolve-compression-plan.test.ts
Normal file
@@ -0,0 +1,22 @@
|
||||
import { test } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import { resolveCompressionPlan } from "@omniroute/open-sse/services/compression/resolveCompressionPlan.ts";
|
||||
|
||||
const base = { enabled:true, engines:{ caveman:{enabled:true,level:"full"} }, activeComboId:null, comboOverrides:{} };
|
||||
test("derived default when no override/active/header", () => {
|
||||
assert.deepEqual(resolveCompressionPlan(base, {}), { mode:"standard", stackedPipeline:[] });
|
||||
});
|
||||
test("routing-combo override wins over default", () => {
|
||||
const cfg = { ...base, comboOverrides:{ cmb:"aggressive" } };
|
||||
assert.equal(resolveCompressionPlan(cfg, { comboId:"cmb" }).mode, "aggressive");
|
||||
});
|
||||
test("active named combo wins over default (Phase 2 wiring uses combos table; here pass it in)", () => {
|
||||
const cfg = { ...base, activeComboId:"c1" };
|
||||
const combos = { c1: [{ engine:"rtk", intensity:"standard" }] };
|
||||
const plan = resolveCompressionPlan(cfg, { combos });
|
||||
assert.equal(plan.mode, "stacked");
|
||||
assert.deepEqual(plan.stackedPipeline, [{ engine:"rtk", intensity:"standard" }]);
|
||||
});
|
||||
test("master off => off regardless", () => {
|
||||
assert.equal(resolveCompressionPlan({ ...base, enabled:false }, {}).mode, "off");
|
||||
});
|
||||
@@ -2,12 +2,17 @@ import { describe, it } from "node:test";
|
||||
import assert from "node:assert/strict";
|
||||
import {
|
||||
selectCompressionStrategy,
|
||||
selectCompressionPlan,
|
||||
enginesMapDerivesStackedPipeline,
|
||||
getEffectiveMode,
|
||||
applyCompression,
|
||||
checkComboOverride,
|
||||
shouldAutoTrigger,
|
||||
} from "../../../open-sse/services/compression/strategySelector.ts";
|
||||
import type { CompressionConfig } from "../../../open-sse/services/compression/types.ts";
|
||||
import {
|
||||
DEFAULT_COMPRESSION_CONFIG,
|
||||
type CompressionConfig,
|
||||
} from "../../../open-sse/services/compression/types.ts";
|
||||
|
||||
const baseConfig: CompressionConfig = {
|
||||
enabled: true,
|
||||
@@ -18,6 +23,25 @@ const baseConfig: CompressionConfig = {
|
||||
comboOverrides: {},
|
||||
};
|
||||
|
||||
/**
|
||||
* Builds a PANEL-CONFIGURED config whose only enabled engines are the ones named (all others
|
||||
* off). `enginesExplicit: true` models a stored engines row (the operator used the panel), so
|
||||
* the engines map drives dispatch. Pass `enginesExplicit: false` via overrides to model a
|
||||
* legacy/backfilled install where dispatch falls back to defaultMode.
|
||||
*/
|
||||
function engineConfig(
|
||||
engines: CompressionConfig["engines"],
|
||||
overrides: Partial<CompressionConfig> = {}
|
||||
): CompressionConfig {
|
||||
return {
|
||||
...DEFAULT_COMPRESSION_CONFIG,
|
||||
enabled: true,
|
||||
engines,
|
||||
enginesExplicit: true,
|
||||
...overrides,
|
||||
};
|
||||
}
|
||||
|
||||
describe("checkComboOverride", () => {
|
||||
it("returns null when comboId is null", () => {
|
||||
assert.equal(checkComboOverride(baseConfig, null), null);
|
||||
@@ -105,6 +129,96 @@ describe("getEffectiveMode", () => {
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectCompressionStrategy resolves via the engines map (Task 7)", () => {
|
||||
it("resolves mode rtk when only rtk is enabled in the engines map", () => {
|
||||
const config = engineConfig({ rtk: { enabled: true } });
|
||||
assert.equal(selectCompressionStrategy(config, null, 0), "rtk");
|
||||
});
|
||||
|
||||
it("resolves mode stacked when rtk + caveman are both enabled, exposing the derived pipeline", () => {
|
||||
const config = engineConfig({
|
||||
rtk: { enabled: true },
|
||||
caveman: { enabled: true, level: "full" },
|
||||
});
|
||||
assert.equal(selectCompressionStrategy(config, null, 0), "stacked");
|
||||
const plan = selectCompressionPlan(config, null, 0);
|
||||
assert.equal(plan.mode, "stacked");
|
||||
// stackPriority order: rtk (10) before caveman (20).
|
||||
assert.deepEqual(plan.stackedPipeline, [
|
||||
{ engine: "rtk" },
|
||||
{ engine: "caveman", intensity: "full" },
|
||||
]);
|
||||
});
|
||||
|
||||
it("auto-trigger still overrides the derived default", () => {
|
||||
const config = engineConfig(
|
||||
{ rtk: { enabled: true } },
|
||||
{ autoTriggerTokens: 1000, autoTriggerMode: "aggressive" }
|
||||
);
|
||||
// Below threshold: derived default (rtk) wins.
|
||||
assert.equal(selectCompressionStrategy(config, null, 500), "rtk");
|
||||
// At/above threshold: auto-trigger mode wins.
|
||||
assert.equal(selectCompressionStrategy(config, null, 1500), "aggressive");
|
||||
});
|
||||
|
||||
it("routing-combo override still wins over the derived default", () => {
|
||||
const config = engineConfig(
|
||||
{ rtk: { enabled: true } },
|
||||
{ comboOverrides: { "my-combo": "off" } }
|
||||
);
|
||||
assert.equal(selectCompressionStrategy(config, "my-combo", 0), "off");
|
||||
});
|
||||
});
|
||||
|
||||
describe("engines map drives dispatch ONLY when explicit (zero behaviour change for legacy)", () => {
|
||||
it("legacy install (enginesExplicit false) ignores the backfilled engines map, uses defaultMode", () => {
|
||||
// A backfilled map with rtk+caveman would derive "stacked", but a legacy install must keep
|
||||
// its historical defaultMode until the operator saves via the panel.
|
||||
const legacy: CompressionConfig = {
|
||||
...DEFAULT_COMPRESSION_CONFIG,
|
||||
enabled: true,
|
||||
defaultMode: "lite",
|
||||
engines: { rtk: { enabled: true }, caveman: { enabled: true, level: "full" } },
|
||||
enginesExplicit: false,
|
||||
};
|
||||
assert.equal(selectCompressionStrategy(legacy, null, 0), "lite");
|
||||
});
|
||||
|
||||
it("explicit install (enginesExplicit true) uses the engines map over defaultMode", () => {
|
||||
const explicit = engineConfig(
|
||||
{ rtk: { enabled: true }, caveman: { enabled: true, level: "full" } },
|
||||
{ defaultMode: "lite" }
|
||||
);
|
||||
assert.equal(selectCompressionStrategy(explicit, null, 0), "stacked");
|
||||
});
|
||||
});
|
||||
|
||||
describe("enginesMapDerivesStackedPipeline", () => {
|
||||
it("true only for an explicit multi-engine stacked map", () => {
|
||||
assert.equal(
|
||||
enginesMapDerivesStackedPipeline(
|
||||
engineConfig({ rtk: { enabled: true }, caveman: { enabled: true, level: "full" } })
|
||||
),
|
||||
true
|
||||
);
|
||||
});
|
||||
it("false for a single-mode explicit map (not stacked)", () => {
|
||||
assert.equal(enginesMapDerivesStackedPipeline(engineConfig({ rtk: { enabled: true } })), false);
|
||||
});
|
||||
it("false for an empty/all-off explicit map", () => {
|
||||
assert.equal(enginesMapDerivesStackedPipeline(engineConfig({})), false);
|
||||
});
|
||||
it("false for a legacy (non-explicit) install even when the backfilled map is stacked", () => {
|
||||
const legacy: CompressionConfig = {
|
||||
...DEFAULT_COMPRESSION_CONFIG,
|
||||
enabled: true,
|
||||
engines: { rtk: { enabled: true }, caveman: { enabled: true, level: "full" } },
|
||||
enginesExplicit: false,
|
||||
};
|
||||
assert.equal(enginesMapDerivesStackedPipeline(legacy), false);
|
||||
});
|
||||
});
|
||||
|
||||
describe("selectCompressionStrategy", () => {
|
||||
it("returns effective mode", () => {
|
||||
assert.equal(selectCompressionStrategy(baseConfig, null, 100), "lite");
|
||||
|
||||
@@ -46,6 +46,7 @@ test("primary sidebar items place limits after cache", () => {
|
||||
"quota",
|
||||
"costs-quota-share",
|
||||
"context-settings",
|
||||
"context-combos",
|
||||
"context-caveman",
|
||||
"context-rtk",
|
||||
"context-headroom",
|
||||
@@ -55,7 +56,6 @@ test("primary sidebar items place limits after cache", () => {
|
||||
"context-lite",
|
||||
"context-aggressive",
|
||||
"context-ultra",
|
||||
"context-combos",
|
||||
"compression-studio",
|
||||
"cli-code",
|
||||
"cli-agents",
|
||||
@@ -81,6 +81,7 @@ test("context sidebar section sits between primary and cli", () => {
|
||||
.map((item) => ({ id: item.id, href: item.href })),
|
||||
[
|
||||
{ id: "context-settings", href: "/dashboard/context/settings" },
|
||||
{ id: "context-combos", href: "/dashboard/context/combos" },
|
||||
{ id: "context-caveman", href: "/dashboard/context/caveman" },
|
||||
{ id: "context-rtk", href: "/dashboard/context/rtk" },
|
||||
{ id: "context-headroom", href: "/dashboard/context/headroom" },
|
||||
@@ -90,7 +91,6 @@ test("context sidebar section sits between primary and cli", () => {
|
||||
{ id: "context-lite", href: "/dashboard/context/lite" },
|
||||
{ id: "context-aggressive", href: "/dashboard/context/aggressive" },
|
||||
{ id: "context-ultra", href: "/dashboard/context/ultra" },
|
||||
{ id: "context-combos", href: "/dashboard/context/combos" },
|
||||
]
|
||||
);
|
||||
});
|
||||
|
||||
@@ -134,6 +134,59 @@ describe("CompressionHub", () => {
|
||||
expect(text).toContain("Layer pipeline is active");
|
||||
});
|
||||
|
||||
it("INVARIANT #1: no per-layer control issues a PUT/POST to /api/context/combos/default", { timeout: 20000 }, async () => {
|
||||
const comboWrites: { method: string }[] = [];
|
||||
const json = (body: unknown, status = 200) =>
|
||||
new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation(
|
||||
async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = input.toString();
|
||||
if (url.includes("/api/context/combos/default")) {
|
||||
if (init?.method === "PUT" || init?.method === "POST") {
|
||||
comboWrites.push({ method: init.method });
|
||||
}
|
||||
return json({ id: "default", name: "Default", pipeline: [{ engine: "rtk" }] });
|
||||
}
|
||||
if (url.includes("/api/settings/compression")) {
|
||||
return json({ enabled: true, defaultMode: "stacked" });
|
||||
}
|
||||
if (url.includes("/api/compression/engines")) {
|
||||
return json(enginePayload());
|
||||
}
|
||||
if (url.includes("/api/context/combos") || url.includes("/api/combos")) {
|
||||
return json({ combos: [] });
|
||||
}
|
||||
if (url.includes("/api/compression/language-packs")) {
|
||||
return json({ packs: [] });
|
||||
}
|
||||
return json({}, 404);
|
||||
}
|
||||
);
|
||||
|
||||
const { default: CompressionHub } =
|
||||
await import("../../../src/app/(dashboard)/dashboard/context/combos/CompressionHub");
|
||||
|
||||
let container!: HTMLElement;
|
||||
await act(async () => {
|
||||
container = mountInContainer(<CompressionHub />);
|
||||
});
|
||||
await flush();
|
||||
|
||||
// Click every on/off switch in the Hub (master + any layer controls that remain).
|
||||
const switches = Array.from(container.querySelectorAll('[role="switch"]'));
|
||||
for (const sw of switches) {
|
||||
await act(async () => {
|
||||
(sw as HTMLElement).click();
|
||||
});
|
||||
await flush();
|
||||
}
|
||||
|
||||
expect(comboWrites).toHaveLength(0);
|
||||
});
|
||||
|
||||
it("shows the activation warning when Token Saver is off", async () => {
|
||||
setupFetchMock({ enabled: false, mode: "off", pipeline: [] });
|
||||
const { default: CompressionHub } =
|
||||
|
||||
220
tests/unit/ui/compressionPanel.test.tsx
Normal file
220
tests/unit/ui/compressionPanel.test.tsx
Normal file
@@ -0,0 +1,220 @@
|
||||
// @vitest-environment jsdom
|
||||
import React, { act } from "react";
|
||||
import { createRoot } from "react-dom/client";
|
||||
import { describe, it, expect, beforeEach, afterEach, vi } from "vitest";
|
||||
import {
|
||||
ENGINE_IDS,
|
||||
engineMeta,
|
||||
} from "../../../open-sse/services/compression/engineCatalog.ts";
|
||||
|
||||
// i18n does not resolve to a real locale in vitest/jsdom, so mock next-intl to echo
|
||||
// the key. This test therefore asserts ONLY on i18n-independent strings: catalog
|
||||
// labels/descriptions, engine ids, data-testid hooks, and the PUT request body.
|
||||
vi.mock("next-intl", () => ({
|
||||
useTranslations: () => (key: string) => key,
|
||||
}));
|
||||
|
||||
// ── Harness ─────────────────────────────────────────────────────────────────
|
||||
|
||||
const containers: HTMLElement[] = [];
|
||||
const roots: Array<{ unmount: () => void }> = [];
|
||||
|
||||
function mount(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 = "";
|
||||
});
|
||||
|
||||
async function flush() {
|
||||
await act(async () => {
|
||||
for (let i = 0; i < 10; i++) await Promise.resolve();
|
||||
});
|
||||
}
|
||||
|
||||
// ── Fetch stub ────────────────────────────────────────────────────────────────
|
||||
|
||||
interface CapturedPut {
|
||||
url: string;
|
||||
body: Record<string, unknown>;
|
||||
}
|
||||
|
||||
function setupFetchMock(): { puts: CapturedPut[] } {
|
||||
const puts: CapturedPut[] = [];
|
||||
const json = (body: unknown, status = 200) =>
|
||||
new Response(JSON.stringify(body), {
|
||||
status,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
|
||||
const initialConfig = {
|
||||
enabled: true,
|
||||
defaultMode: "stacked",
|
||||
autoTriggerTokens: 0,
|
||||
cacheMinutes: 5,
|
||||
preserveSystemPrompt: true,
|
||||
comboOverrides: {},
|
||||
engines: {
|
||||
rtk: { enabled: true, level: "standard" },
|
||||
caveman: { enabled: false },
|
||||
},
|
||||
activeComboId: null,
|
||||
cavemanOutputMode: { enabled: false, intensity: "full", autoClarity: true },
|
||||
};
|
||||
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation(
|
||||
async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = input.toString();
|
||||
const method = (init?.method ?? "GET").toUpperCase();
|
||||
|
||||
if (url.includes("/api/settings/compression/mcp-accessibility")) {
|
||||
if (method === "PUT") {
|
||||
puts.push({ url, body: JSON.parse(String(init?.body ?? "{}")) });
|
||||
return json({ enabled: true });
|
||||
}
|
||||
return json({ enabled: true, maxTextChars: 50000 });
|
||||
}
|
||||
|
||||
if (url.includes("/api/settings/compression")) {
|
||||
if (method === "PUT") {
|
||||
const body = JSON.parse(String(init?.body ?? "{}"));
|
||||
puts.push({ url, body });
|
||||
// Echo a merged config so the panel keeps a coherent state.
|
||||
return json({ ...initialConfig, ...body });
|
||||
}
|
||||
return json(initialConfig);
|
||||
}
|
||||
|
||||
return json({}, 404);
|
||||
}
|
||||
);
|
||||
|
||||
return { puts };
|
||||
}
|
||||
|
||||
// ── Tests ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
describe("CompressionPanel", () => {
|
||||
it("renders a row for every engine id in the catalog", async () => {
|
||||
setupFetchMock();
|
||||
const { default: CompressionPanel } = await import(
|
||||
"../../../src/app/(dashboard)/dashboard/context/settings/CompressionPanel"
|
||||
);
|
||||
|
||||
let container!: HTMLElement;
|
||||
await act(async () => {
|
||||
container = mount(<CompressionPanel />);
|
||||
});
|
||||
await flush();
|
||||
|
||||
for (const id of ENGINE_IDS) {
|
||||
const row = container.querySelector(`[data-testid="engine-row-${id}"]`);
|
||||
expect(row, `expected a row for engine "${id}"`).toBeTruthy();
|
||||
// Catalog label/description are hardcoded English (i18n-independent).
|
||||
expect(container.textContent).toContain(engineMeta(id).label);
|
||||
}
|
||||
});
|
||||
|
||||
it("shows the rtk level 'standard' as selected", async () => {
|
||||
setupFetchMock();
|
||||
const { default: CompressionPanel } = await import(
|
||||
"../../../src/app/(dashboard)/dashboard/context/settings/CompressionPanel"
|
||||
);
|
||||
|
||||
let container!: HTMLElement;
|
||||
await act(async () => {
|
||||
container = mount(<CompressionPanel />);
|
||||
});
|
||||
await flush();
|
||||
|
||||
const select = container.querySelector(
|
||||
`[data-testid="engine-row-rtk"] select`
|
||||
) as HTMLSelectElement | null;
|
||||
expect(select).toBeTruthy();
|
||||
expect(select?.value).toBe("standard");
|
||||
});
|
||||
|
||||
it("toggling caveman PUTs engines.caveman.enabled === true", async () => {
|
||||
const { puts } = setupFetchMock();
|
||||
const { default: CompressionPanel } = await import(
|
||||
"../../../src/app/(dashboard)/dashboard/context/settings/CompressionPanel"
|
||||
);
|
||||
|
||||
let container!: HTMLElement;
|
||||
await act(async () => {
|
||||
container = mount(<CompressionPanel />);
|
||||
});
|
||||
await flush();
|
||||
|
||||
// The data-testid hook wraps the Toggle; its inner <button role="switch"> is the
|
||||
// clickable element.
|
||||
const toggle = container.querySelector(
|
||||
`[data-testid="engine-toggle-caveman"] button`
|
||||
) as HTMLButtonElement | null;
|
||||
expect(toggle, "caveman toggle must exist").toBeTruthy();
|
||||
|
||||
await act(async () => {
|
||||
toggle!.click();
|
||||
});
|
||||
await flush();
|
||||
|
||||
const settingsPuts = puts.filter(
|
||||
(p) => p.url.includes("/api/settings/compression") && !p.url.includes("mcp-accessibility")
|
||||
);
|
||||
expect(settingsPuts.length).toBeGreaterThan(0);
|
||||
const lastEngines = settingsPuts
|
||||
.map((p) => p.body.engines as Record<string, { enabled: boolean }> | undefined)
|
||||
.filter(Boolean)
|
||||
.pop();
|
||||
expect(lastEngines).toBeTruthy();
|
||||
expect(lastEngines!.caveman.enabled).toBe(true);
|
||||
// Full engines map is sent (whole-row persistence), so rtk is not dropped.
|
||||
expect(lastEngines!.rtk.enabled).toBe(true);
|
||||
});
|
||||
|
||||
it("derived-pipeline preview reflects the enabled engines", async () => {
|
||||
setupFetchMock();
|
||||
const { default: CompressionPanel } = await import(
|
||||
"../../../src/app/(dashboard)/dashboard/context/settings/CompressionPanel"
|
||||
);
|
||||
|
||||
let container!: HTMLElement;
|
||||
await act(async () => {
|
||||
container = mount(<CompressionPanel />);
|
||||
});
|
||||
await flush();
|
||||
|
||||
const preview = container.querySelector(`[data-testid="derived-pipeline-preview"]`);
|
||||
expect(preview).toBeTruthy();
|
||||
// Only rtk is enabled in the initial config → preview mentions rtk, not caveman.
|
||||
expect(preview?.textContent).toContain("rtk");
|
||||
expect(preview?.textContent).not.toContain("caveman");
|
||||
});
|
||||
});
|
||||
@@ -93,6 +93,16 @@ const ANALYTICS_PAYLOAD = {
|
||||
days: 7,
|
||||
};
|
||||
|
||||
const SETTINGS_PAYLOAD = {
|
||||
enabled: true,
|
||||
engines: { headroom: { enabled: true } },
|
||||
aggressive: {
|
||||
summarizerEnabled: true,
|
||||
maxTokensPerMessage: 2048,
|
||||
minSavingsThreshold: 0.05,
|
||||
},
|
||||
};
|
||||
|
||||
function setupFetchMock() {
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation(async (input: RequestInfo | URL) => {
|
||||
const url = input.toString();
|
||||
@@ -102,6 +112,12 @@ function setupFetchMock() {
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
if (url.includes("/api/settings/compression")) {
|
||||
return new Response(JSON.stringify(SETTINGS_PAYLOAD), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
if (url.includes("/api/context/combos/default")) {
|
||||
return new Response(JSON.stringify(COMBO_PAYLOAD), {
|
||||
status: 200,
|
||||
@@ -152,7 +168,7 @@ describe("EngineConfigPage", () => {
|
||||
expect(container.textContent).toContain("Headroom");
|
||||
});
|
||||
|
||||
it("renders the enable toggle for the engine", async () => {
|
||||
it("does NOT render an engine on/off enable toggle (moved to the panel)", async () => {
|
||||
setupFetchMock();
|
||||
const { EngineConfigPage } =
|
||||
await import("../../../src/shared/components/compression/EngineConfigPage");
|
||||
@@ -166,13 +182,9 @@ describe("EngineConfigPage", () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
// Either a checkbox or a button that says "Ativar"
|
||||
const hasToggle =
|
||||
container.querySelector("input[type='checkbox'][data-toggle='enable']") !== null ||
|
||||
container.querySelector("[data-toggle='enable']") !== null ||
|
||||
container.textContent?.includes("Ativar") === true;
|
||||
|
||||
expect(hasToggle).toBe(true);
|
||||
// The on/off enable control now lives only in the panel (/dashboard/context/settings).
|
||||
expect(container.querySelector("[data-toggle='enable']")).toBeNull();
|
||||
expect(container.textContent).not.toContain("Enable layer");
|
||||
});
|
||||
|
||||
it("renders the config form field label from fetched schema (EngineConfigForm mounted)", async () => {
|
||||
@@ -193,7 +205,7 @@ describe("EngineConfigPage", () => {
|
||||
expect(container.textContent).toContain("Min rows");
|
||||
});
|
||||
|
||||
it("uses the layer switch as the only rendered Enabled control", async () => {
|
||||
it("keeps detailed config but renders no engine enable checkbox", async () => {
|
||||
setupFetchMock();
|
||||
const { EngineConfigPage } =
|
||||
await import("../../../src/shared/components/compression/EngineConfigPage");
|
||||
@@ -207,9 +219,11 @@ describe("EngineConfigPage", () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
const enableCheckboxes = container.querySelectorAll("input[type='checkbox']");
|
||||
expect(enableCheckboxes.length).toBe(1);
|
||||
expect(container.textContent).toContain("Enable layer");
|
||||
// The on/off enable toggle (a checkbox with data-toggle="enable") is gone; the
|
||||
// detailed config form (the schema fields minus `enabled`) still renders.
|
||||
expect(container.querySelector("input[type='checkbox'][data-toggle='enable']")).toBeNull();
|
||||
expect(container.textContent).toContain("Min rows");
|
||||
expect(container.textContent).toContain("Configuration");
|
||||
});
|
||||
|
||||
it("renders preview original, compressed text, and diff returned by the API", async () => {
|
||||
@@ -264,7 +278,7 @@ describe("EngineConfigPage", () => {
|
||||
expect(hasEmptyState).toBe(true);
|
||||
});
|
||||
|
||||
it("renders the stacked-mode prerequisite notice", async () => {
|
||||
it("points to the Compression Settings panel for enabling the layer", async () => {
|
||||
setupFetchMock();
|
||||
const { EngineConfigPage } =
|
||||
await import("../../../src/shared/components/compression/EngineConfigPage");
|
||||
@@ -278,29 +292,63 @@ describe("EngineConfigPage", () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
expect(container.textContent).toContain("stacked");
|
||||
// The on/off + level live in the panel now; the page surfaces a link to it.
|
||||
const settingsLink = container.querySelector('a[href="/dashboard/context/settings"]');
|
||||
expect(settingsLink).not.toBeNull();
|
||||
expect(container.textContent).toContain("Compression Settings");
|
||||
});
|
||||
|
||||
it("Fix #4: handleSave sends enabled=false when engine is disabled", async () => {
|
||||
// COMBO_PAYLOAD has empty pipeline → engine is disabled (enabled=false)
|
||||
const putCalls: { body: unknown }[] = [];
|
||||
it("INVARIANT #1: handleSave writes the detailed sub-object to settings/compression, never PUTs combos/default", async () => {
|
||||
const AGGRESSIVE_PAYLOAD = {
|
||||
engines: [
|
||||
{
|
||||
id: "aggressive",
|
||||
name: "Aggressive",
|
||||
description: "Aggressive engine",
|
||||
icon: "🗜️",
|
||||
stackable: true,
|
||||
stackPriority: 30,
|
||||
metadata: { description: "Aggressive metadata" },
|
||||
configSchema: [
|
||||
{ key: "enabled", type: "boolean", label: "Enabled", defaultValue: true },
|
||||
{
|
||||
key: "maxTokensPerMessage",
|
||||
type: "number",
|
||||
label: "Max tokens per message",
|
||||
defaultValue: 2048,
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
};
|
||||
const settingsPuts: { body: Record<string, unknown> }[] = [];
|
||||
const comboWrites: { method: string }[] = [];
|
||||
vi.spyOn(globalThis, "fetch").mockImplementation(
|
||||
async (input: RequestInfo | URL, init?: RequestInit) => {
|
||||
const url = input.toString();
|
||||
if (url.includes("/api/compression/engines")) {
|
||||
return new Response(JSON.stringify(ENGINE_PAYLOAD), {
|
||||
return new Response(JSON.stringify(AGGRESSIVE_PAYLOAD), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
}
|
||||
if (url.includes("/api/context/combos/default")) {
|
||||
if (url.includes("/api/settings/compression")) {
|
||||
if (init?.method === "PUT") {
|
||||
putCalls.push({ body: JSON.parse(init.body as string) });
|
||||
return new Response(JSON.stringify(COMBO_PAYLOAD), {
|
||||
status: 200,
|
||||
headers: { "Content-Type": "application/json" },
|
||||
});
|
||||
settingsPuts.push({ body: JSON.parse(init.body as string) });
|
||||
}
|
||||
return new Response(
|
||||
JSON.stringify({
|
||||
enabled: true,
|
||||
engines: { aggressive: { enabled: true } },
|
||||
aggressive: { maxTokensPerMessage: 2048 },
|
||||
}),
|
||||
{ status: 200, headers: { "Content-Type": "application/json" } }
|
||||
);
|
||||
}
|
||||
if (url.includes("/api/context/combos/default")) {
|
||||
// A PUT/POST here would violate INVARIANT #1 (the route is a 410 shim).
|
||||
if (init?.method === "PUT" || init?.method === "POST") {
|
||||
comboWrites.push({ method: init.method });
|
||||
}
|
||||
return new Response(JSON.stringify(COMBO_PAYLOAD), {
|
||||
status: 200,
|
||||
@@ -322,34 +370,32 @@ describe("EngineConfigPage", () => {
|
||||
|
||||
let container!: HTMLElement;
|
||||
await act(async () => {
|
||||
container = mountInContainer(<EngineConfigPage engineId="headroom" />);
|
||||
container = mountInContainer(<EngineConfigPage engineId="aggressive" />);
|
||||
});
|
||||
|
||||
// Let initial load complete
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
// Click "Save" — engine is disabled (COMBO_PAYLOAD pipeline is empty)
|
||||
const allButtons = Array.from(container.querySelectorAll("button"));
|
||||
const salvarBtn = allButtons.find(
|
||||
const salvarBtn = Array.from(container.querySelectorAll("button")).find(
|
||||
(b) => b.textContent?.includes("Save") || b.textContent?.includes("Salvar")
|
||||
);
|
||||
if (salvarBtn) {
|
||||
await act(async () => {
|
||||
salvarBtn.click();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
}
|
||||
expect(salvarBtn).toBeTruthy();
|
||||
await act(async () => {
|
||||
salvarBtn?.click();
|
||||
});
|
||||
await act(async () => {
|
||||
await Promise.resolve();
|
||||
});
|
||||
|
||||
// There should be at least one PUT call with enabled=false
|
||||
const putWithFalse = putCalls.find(
|
||||
(c) => (c.body as { enabled: boolean; engineId: string }).enabled === false
|
||||
// INVARIANT #1: no write ever lands on the deprecated default-combo route.
|
||||
expect(comboWrites).toHaveLength(0);
|
||||
// The detailed config persists to the engine's sub-object on settings/compression.
|
||||
expect(settingsPuts.length).toBeGreaterThan(0);
|
||||
const aggressivePut = settingsPuts.find(
|
||||
(c) => typeof c.body.aggressive === "object" && c.body.aggressive !== null
|
||||
);
|
||||
expect(putCalls.length).toBeGreaterThan(0);
|
||||
expect(putWithFalse).toBeDefined();
|
||||
expect(aggressivePut).toBeDefined();
|
||||
});
|
||||
|
||||
it("does not crash when all fetch calls fail (fail-soft)", async () => {
|
||||
|
||||
@@ -63,17 +63,30 @@ describe("COMPRESSION_CONTEXT_GROUP contains all 4 engine items", () => {
|
||||
assert.equal(item.labelFallback, "LLMLingua");
|
||||
});
|
||||
|
||||
it("4 engine items appear after context-rtk and before context-combos", () => {
|
||||
it("4 engine items appear after context-rtk and before compression-studio", () => {
|
||||
// Unified-panel order: Settings → Combos → per-engine pages → Studio.
|
||||
const ids = itemIds as string[];
|
||||
const rtkIdx = ids.indexOf("context-rtk");
|
||||
const combosIdx = ids.indexOf("context-combos");
|
||||
const studioIdx = ids.indexOf("compression-studio");
|
||||
assert.ok(rtkIdx !== -1, "context-rtk not found");
|
||||
assert.ok(combosIdx !== -1, "context-combos not found");
|
||||
assert.ok(studioIdx !== -1, "compression-studio not found");
|
||||
|
||||
for (const id of ENGINE_IDS) {
|
||||
const idx = ids.indexOf(id);
|
||||
assert.ok(idx > rtkIdx, `${id} should appear after context-rtk`);
|
||||
assert.ok(idx < combosIdx, `${id} should appear before context-combos`);
|
||||
assert.ok(idx < studioIdx, `${id} should appear before compression-studio`);
|
||||
}
|
||||
});
|
||||
|
||||
it("group order is Settings → Combos → engines → Studio", () => {
|
||||
const ids = itemIds as string[];
|
||||
assert.equal(ids[0], "context-settings", "Settings must be first");
|
||||
assert.equal(ids[1], "context-combos", "Combos must be second");
|
||||
assert.equal(ids[ids.length - 1], "compression-studio", "Studio must be last");
|
||||
// Combos precedes every per-engine page.
|
||||
const combosIdx = ids.indexOf("context-combos");
|
||||
for (const id of ["context-caveman", "context-rtk", ...ENGINE_IDS]) {
|
||||
assert.ok(ids.indexOf(id) > combosIdx, `${id} should appear after context-combos`);
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
Reference in New Issue
Block a user