mirror of
https://github.com/diegosouzapw/OmniRoute.git
synced 2026-07-26 09:52:11 +03:00
Release v3.7.9 (#1917)
* chore(release): v3.7.9 — gemini-cli cloud code separation * chore(provider): Update Jina AI model catalog (#1874) Integrated into release/v3.7.9 * docs: update CHANGELOG for PR 1874 and retroactive credits * fix: resolve stream defaults and codex prompt mapping (#1873, #1872) * chore(compression): start caveman compression update * feat(compression): expand caveman compression and analytics pipeline Add caveman intensity levels, output mode instructions, validation, and preview diffs across the compression pipeline. Extend MCP and dashboard settings to support auto-trigger mode, system prompt preservation, MCP description compression, and caveman rule metadata. Record richer compression analytics with receipt fields, validation fallbacks, output mode data, and add the related database migration. Improve preservation handling for code, URLs, markdown, math, and other protected content while adding broad unit, integration, and golden-set coverage for caveman parity and compression behavior. * feat(compression): expose rule intensities and track usd savings Add estimated USD savings to compression analytics so saved tokens can be reported in cost terms alongside existing token metrics. Expose caveman rule intensity metadata for settings consumers and add a settings API route alias for rule lookup. Also preserve system prompts when aggressive compression falls back to lite mode. * feat(compression): RTK compression roadmap (#1889) * chore(rtk): initialize compression roadmap branch * feat(compression): add RTK engine and compression combos Introduce RTK command-aware tool-output compression alongside stacked RTK -> Caveman pipelines for mixed prompt contexts. Add engine registration, declarative RTK filter packs, language-aware Caveman rule loading, compression combo persistence and assignments, analytics grouped by engine/combo, and new MCP/API endpoints for configuration, previews, filters, and combo management. Expose the new capabilities in the dashboard with dedicated Context & Cache pages for Caveman, RTK, and compression combos, and update docs, i18n strings, migrations, and tests to cover the expanded compression surface. * feat(compression): expand RTK DSL, filter catalog, and recovery APIs Add RTK parity features across the compression pipeline, dashboard, and management APIs. This expands the built-in filter catalog, adds trust-gated custom filter loading, inline filter verification, code stripping, smarter detection, and optional redacted raw-output retention for authenticated recovery. Also extend Caveman with file-based multilingual rule packs, localized output-mode instructions, stricter preview/config schemas, engine registry metadata, analytics fields, and broad unit test coverage for RTK, rule loading, and stacked compression behavior. * fix(auth): protect oauth routes and health reset operations Require authenticated dashboard access for OAuth endpoints that can create or import provider connections when login enforcement is enabled. Move `/api/monitoring/health` to the readonly public route list so safe methods remain public while DELETE now returns 401 for anonymous requests. Also update Next.js native `.node` handling to avoid webpack parse failures from external packages such as ngrok and keytar, and add coverage for the new auth behavior. * build(compression): ship RTK rule and filter assets with app bundles Include compression JSON assets in Next output tracing, prepublish copies, and pack artifact policy checks so standalone and packaged builds can load RTK filters and caveman rule packs at runtime. Also harden compression runtime behavior by resolving alternate asset directories, scoping rule cache entries by source path, carrying RTK raw output pointers through stacked runs, degrading oversized preview diffs, and applying combo language/output mode defaults during chat routing. Add coverage for packaging rules, provider-scoped model parsing, smart truncate edge cases, raw output retention, and combo-driven compression behavior. * docs(workflows): update local repo paths to OmniRoute Replace outdated `/home/diegosouzapw/dev/proxys/9router` references with the current `OmniRoute` directory across deploy, release, and version bump workflow guides so local command examples match the renamed repository layout * feat(compression): complete RTK parity coverage * test(build): align next config assertions --------- Co-authored-by: diegosouzapw <diego.souza.pw@gmail.com> * feat(compression): expand caveman parity and MCP metadata compression Compress MCP registry and list metadata descriptions for tools, prompts, resources, and resource templates while keeping tool-response bodies unchanged. Expose those savings in compression status as `mcp_metadata_estimate` metadata rather than provider usage. Add Caveman rule-pack support for custom regex flags and match-specific replacement maps, update English rules for upstream parity, and tighten article and pleasantry handling. Also process RTK multipart text blocks independently so mixed media content compresses safely without duplicating output. * feat(compression): unify config validation and persist MCP savings Centralize compression config schemas across settings, preview, RTK, and combo APIs to enforce consistent validation for stacked pipelines and engine-specific options. Expand caveman and stacked compression behavior by applying default combos at runtime, surfacing validation and fallback metadata, and exposing aggressive and ultra adapter schemas for configuration UI and tests. Persist MCP description compression snapshots into analytics without counting them as provider usage, and extend the dashboard with the new RTK controls and localized labels. * fix(auth): require dashboard management auth for compression preview Block preview requests unless they come from a valid management session token so protected settings cannot be probed through the preview API. Add unit coverage for unauthenticated requests, invalid bearer tokens, and successful authenticated preview execution. * fix(compression): preserve stacked defaults and secure metadata routes Only apply saved default compression combos when they contain a stacked pipeline so seeded Caveman-only defaults do not replace the builtin stacked behavior. Also require management auth for compression language pack and rules metadata endpoints, and defer usage receipt attachment until compression analytics writes have completed to keep analytics records consistent. * fix(compression): align seeded standard savings combo with stacked default Update the seeded default compression combo to use the RTK then Caveman pipeline in both fresh installs and upgraded databases. Add a targeted migration and runtime guard that only rewrites the legacy seeded record when its original metadata and single-step pipeline still match, preserving user-customized default combos. Refresh docs and tests to reflect the stacked default and expanded RTK filter catalog. * docs(compression): document RTK+Caveman stacked savings ranges Refresh the compression docs and README to describe the default stacked pipeline in terms of eligible-context savings instead of the older generic token-saving range. Add upstream RTK and Caveman benchmark references, explain the multiplicative savings math behind the stacked default, and update feature summaries plus package metadata to match the revised positioning. * feat(image-gen): add NanoGPT image generation provider (#1899) Integrated into release/v3.7.9 * fix(codex): sanitize raw responses input (#1895) Integrated into release/v3.7.9 * Fix combo provider breaker profile handling (#1891) Integrated into release/v3.7.9 * fix(combos): align strategy contracts (#1892) Integrated into release/v3.7.9 * feat(proxy): move proxy configuration to dedicated System → Proxy page (#1907) Integrated into release/v3.7.9 * feat: add K/M/B/T cost shortener to prevent UI overflow (#1902) Integrated into release/v3.7.9 * feat(providers): implement bulk paste for extra API keys (#1916) Integrated into release/v3.7.9 * fix(migrations): treat duplicate-column ALTER as no-op (#1886) Integrated into release/v3.7.9 * fix(analytics): robust model pricing resolution, dark mode charts and SQL aggregation fixes (#1896) Integrated into release/v3.7.9 (migration renumbered to 044) * fix(oauth): per-connection mutex for rotating refresh tokens (#1885) Integrated into release/v3.7.9 * fix: resolve 3 bugs — Codex tool normalization (#1914), image gen proxy (#1904), zero-arg MCP tools (#1898) - fix(codex): flatten Chat Completions tool format to Responses format in normalizeCodexTools. Prevents 'Missing required parameter: tools[0].name' upstream errors when clients send {type:'function', function:{name,...}} instead of {type:'function', name,...}. - fix(proxy): add proxy-aware execution context to image generation route. Image requests now correctly use proxy settings from the connection's ProxyRegistry assignment, matching the pattern used by chat pipeline. - fix(translator): inject properties:{} into zero-argument MCP tool schemas during Anthropic→OpenAI translation. OpenAI strict mode requires explicit properties even for empty object schemas. Closes #1914, Closes #1904, Closes #1898 * chore(release): v3.7.9 — all changes in ONE commit * fix: allow local ollama provider connections (#1893) * fix(copilot): emit compatible reasoning text deltas (#1919) Integrated into release/v3.7.9 * fix(api-manager): show validation errors inline in modals, not behind backdrop (#1920) Integrated into release/v3.7.9 * docs: update changelog and pr body with merged prs * fix(providers): route agentrouter through anthropic endpoint headers Update the AgentRouter provider registry to use the Claude-compatible messages API and required Anthropic-style authentication headers. This bypasses unauthorized_client_error responses and exposes the supported model list through passthrough configuration. Also update the changelog and release PR notes to document the fix for #1921 * feat(logs): show compression tokens in request log UI (#1923) * docs: add PR #1923 to changelog --------- Co-authored-by: diegosouzapw <diego.souza.pw@gmail.com> Co-authored-by: backryun <bakryun0718@proton.me> Co-authored-by: Aculeasis <42580940+Aculeasis@users.noreply.github.com> Co-authored-by: Raxxoor <manker_lol@hotmail.com> Co-authored-by: Randi <55005611+rdself@users.noreply.github.com> Co-authored-by: Paijo <14921983+oyi77@users.noreply.github.com> Co-authored-by: Tubagus <54710482+0xtbug@users.noreply.github.com> Co-authored-by: smartenok-ops <smartenok@gmail.com> Co-authored-by: Gi99lin <74502520+Gi99lin@users.noreply.github.com> Co-authored-by: ivan-mezentsev <ivan@mezentsev.me> Co-authored-by: Andrew Munsell <andrew@wizardapps.net>
This commit is contained in:
committed by
GitHub
parent
0cf33fdaa3
commit
99c6dc7fd6
@@ -17,7 +17,7 @@ Deploy OmniRoute to the Akamai VPS using `npm pack + scp` + PM2.
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/9router && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts
|
||||
```
|
||||
|
||||
### 2. Copy to Akamai VPS and install
|
||||
|
||||
@@ -22,7 +22,7 @@ Deploy OmniRoute to the production VPSs using `npm pack + scp` + PM2.
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/9router && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts
|
||||
```
|
||||
|
||||
### 2. Copy to both VPS and install
|
||||
|
||||
@@ -17,7 +17,7 @@ Deploy OmniRoute to the Local VPS using `npm pack + scp` + PM2.
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/9router && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts
|
||||
```
|
||||
|
||||
### 2. Copy to Local VPS and install
|
||||
|
||||
@@ -226,7 +226,7 @@ git checkout main
|
||||
git pull origin main
|
||||
|
||||
# Build and pack locally
|
||||
cd /home/diegosouzapw/dev/proxys/9router && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute && rm -f omniroute-*.tgz && rm -rf .next/cache app/.next/cache && npm run build:cli && rm -rf app/logs app/coverage app/.git app/.app-build-backup* && npm pack --ignore-scripts
|
||||
|
||||
# Deploy to LOCAL VPS (192.168.0.15)
|
||||
scp omniroute-*.tgz root@192.168.0.15:/tmp/
|
||||
|
||||
@@ -20,7 +20,7 @@ Automatically bump the project version, generate CHANGELOG entries from git hist
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/9router
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
CURRENT_VERSION=$(node -p "require('./package.json').version")
|
||||
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null || echo "")
|
||||
CURRENT_BRANCH=$(git branch --show-current)
|
||||
@@ -64,7 +64,7 @@ npm version "$VERSION" --no-git-tag-version
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/9router
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
LAST_TAG=$(git describe --tags --abbrev=0 2>/dev/null)
|
||||
echo "=== Commits since $LAST_TAG ==="
|
||||
git log "$LAST_TAG"..HEAD --pretty=format:"%h %s" --no-merges | head -100
|
||||
@@ -133,7 +133,7 @@ The date must be today's date in `YYYY-MM-DD` format.
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/9router
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
|
||||
# Update docs/openapi.yaml version
|
||||
@@ -156,7 +156,7 @@ echo "✓ All workspace packages synced to $VERSION"
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/9router
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
OLD_VERSION_PATTERN='[0-9]\+\.[0-9]\+\.[0-9]\+'
|
||||
|
||||
@@ -174,7 +174,7 @@ echo "✓ llm.txt → $VERSION"
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/9router
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
npm install
|
||||
echo "✓ Lock file regenerated"
|
||||
```
|
||||
@@ -236,7 +236,7 @@ For each file in `docs/` (excluding `docs/i18n/`), review if CHANGELOG changes a
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/9router
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
npm run lint
|
||||
```
|
||||
|
||||
@@ -245,7 +245,7 @@ npm run lint
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/9router
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
npm test
|
||||
```
|
||||
|
||||
@@ -254,7 +254,7 @@ npm test
|
||||
// turbo
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/9router
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
echo "Expected version: $VERSION"
|
||||
echo ""
|
||||
@@ -299,7 +299,7 @@ grep "^## \[" CHANGELOG.md | head -2
|
||||
// turbo-all
|
||||
|
||||
```bash
|
||||
cd /home/diegosouzapw/dev/proxys/9router
|
||||
cd /home/diegosouzapw/dev/proxys/OmniRoute
|
||||
git add -A
|
||||
VERSION=$(node -p "require('./package.json').version")
|
||||
git commit -m "chore(release): bump to v$VERSION — changelog, docs, version sync"
|
||||
|
||||
34
AGENTS.md
34
AGENTS.md
@@ -6,7 +6,7 @@ Unified AI proxy/router — route any LLM through one endpoint. Multi-provider s
|
||||
with **160+ providers** (OpenAI, Anthropic, Gemini, DeepSeek, Groq, xAI, Mistral, Fireworks,
|
||||
Cohere, NVIDIA, Cerebras, Pollinations, Puter, Cloudflare AI, HuggingFace, DeepInfra,
|
||||
SambaNova, Meta Llama API, Moonshot AI, AI21 Labs, Databricks, Snowflake, and many more)
|
||||
with **MCP Server** (29 tools), **A2A v0.3 Protocol**, and **Electron desktop app**.
|
||||
with **MCP Server** (37 tools), **A2A v0.3 Protocol**, and **Electron desktop app**.
|
||||
|
||||
## Stack
|
||||
|
||||
@@ -292,17 +292,28 @@ compression pipeline), and more.
|
||||
|
||||
Modular prompt compression that runs proactively before the existing reactive context manager.
|
||||
|
||||
- **`strategySelector.ts`**: Selects compression mode based on config, combo overrides, auto-trigger
|
||||
thresholds. Priority: combo override > auto-trigger > default mode > off.
|
||||
- **`strategySelector.ts`**: Selects compression mode based on config, compression combo assignments,
|
||||
combo overrides, auto-trigger thresholds, and defaults. Priority: assigned compression combo >
|
||||
combo override > auto-trigger > default mode > off.
|
||||
- **`lite.ts`**: 5 lite-mode techniques: `collapseWhitespace`, `dedupSystemPrompt`,
|
||||
`compressToolResults`, `removeRedundantContent`, `replaceImageUrls`. Target: 10-15% savings at
|
||||
<1ms latency.
|
||||
- **`caveman.ts` / `cavemanRules.ts`**: Caveman-style semantic condensation backed by built-in
|
||||
rules plus file-loaded language packs under `compression/rules/`.
|
||||
- **`engines/rtk/`**: Rule-based terminal/tool-output compression inspired by RTK patterns. Detects
|
||||
command output classes, applies JSON filter packs, deduplicates repeated lines, strips ANSI/code
|
||||
noise, and preserves errors/actionable context. The RTK JSON DSL supports replace,
|
||||
match-output short-circuit, strip/keep, per-line truncation, head/tail/max-line truncation,
|
||||
inline tests, trust-gated project/global custom filters, and optional redacted raw-output
|
||||
retention for authenticated recovery.
|
||||
- **`engines/registry.ts`**: Registers engines (`caveman`, `rtk`) and powers stacked pipelines.
|
||||
- **`stats.ts`**: Per-request compression stats tracking (original tokens, compressed tokens,
|
||||
savings %, techniques used).
|
||||
- **`types.ts`**: `CompressionMode` (off/lite/standard/aggressive/ultra), `CompressionConfig`,
|
||||
`CompressionStats`, `CompressionResult`.
|
||||
- DB settings in `src/lib/db/compression.ts`, API route at `src/app/api/settings/compression/`.
|
||||
- Phase 1 implements lite mode only; standard/aggressive/ultra are placeholders for Phase 2.
|
||||
savings %, techniques used, engine breakdown, compression combo id).
|
||||
- **`types.ts`**: `CompressionMode` (off/lite/standard/aggressive/ultra/rtk/stacked),
|
||||
`CompressionConfig`, `CompressionStats`, `CompressionResult`.
|
||||
- DB settings in `src/lib/db/compression.ts`, compression combos in
|
||||
`src/lib/db/compressionCombos.ts`, API routes under `src/app/api/settings/compression/`,
|
||||
`src/app/api/context/*`, and preview/language-pack routes under `src/app/api/compression/*`.
|
||||
|
||||
#### Combo Routing Engine (`combo.ts`)
|
||||
|
||||
@@ -323,7 +334,7 @@ Policy engine modules: `policyEngine.ts`, `comboResolver.ts`, `costRules.ts`,
|
||||
|
||||
### MCP Server (`open-sse/mcp-server/`)
|
||||
|
||||
29 tools, 3 transports (stdio / SSE / Streamable HTTP). Scoped auth (10 scopes), Zod schemas.
|
||||
37 tools, 3 transports (stdio / SSE / Streamable HTTP). Scoped auth (10 scopes), Zod schemas.
|
||||
|
||||
**Core tools** (20): get_health, list_combos, get_combo_metrics, switch_combo, check_quota,
|
||||
route_request, cost_report, list_models_catalog, web_search, simulate_route, set_budget_guard,
|
||||
@@ -332,6 +343,11 @@ best_combo_for_task, explain_route, get_session_snapshot, db_health_check, sync_
|
||||
|
||||
**Cache tools** (2): cache_stats, cache_flush.
|
||||
|
||||
**Compression tools** (5): compression_status, compression_configure, set_compression_engine,
|
||||
list_compression_combos, compression_combo_stats.
|
||||
|
||||
**1proxy tools** (3): oneproxy_fetch, oneproxy_rotate, oneproxy_stats.
|
||||
|
||||
**Memory tools** (3): memory_search, memory_add, memory_clear.
|
||||
|
||||
**Skill tools** (4): skills_list, skills_enable, skills_execute, skills_executions.
|
||||
|
||||
49
CHANGELOG.md
49
CHANGELOG.md
@@ -2,6 +2,55 @@
|
||||
|
||||
## [Unreleased]
|
||||
|
||||
## [3.7.9] — 2026-05-03
|
||||
|
||||
### ✨ New Features
|
||||
|
||||
- **feat(compression):** major upgrade to Caveman and RTK compression pipelines (#1876, #1889):
|
||||
- Add RTK tool-output compression, stacked Caveman + RTK pipelines, compression combo assignments, dashboard context pages, MCP management tools, and language-aware Caveman rule packs.
|
||||
- Expand RTK parity with a 39-filter catalog, RTK-style JSON DSL stages, inline verify/benchmark coverage, trust-gated custom filters, expanded command detection, and redacted raw-output recovery.
|
||||
- Expose rule intensities, track USD savings, unify config validation, and persist MCP savings.
|
||||
- Expand Caveman parity and MCP metadata compression.
|
||||
- **feat(provider):** update Jina AI model catalog to support Embeddings and Rerank natively (#1874 — thanks @backryun)
|
||||
- **feat(provider):** add NanoGPT image generation provider (#1899 — thanks @Aculeasis)
|
||||
- **feat(ui):** move proxy configuration to dedicated System → Proxy page (#1907 — thanks @oyi77)
|
||||
- **feat(ui):** add K/M/B/T cost shortener utility (#1902 — thanks @oyi77)
|
||||
- **feat(providers):** implement bulk paste for extra API keys (#1916 — thanks @0xtbug)
|
||||
- **feat(analytics):** usage history API key backfill + dark mode pricing (#1896 — thanks @Gi99lin)
|
||||
- **feat(logs):** show RTK and Caveman compression token savings accurately in request log UI (#1923 — thanks @emdash)
|
||||
|
||||
### 🐛 Bug Fixes
|
||||
|
||||
- **fix(providers):** allow local OpenAI-compatible endpoints (like Ollama) to be added without an API key (fixes #1893)
|
||||
- **fix(providers):** bypass AgentRouter unauthorized_client_error by spoofing Claude CLI headers via Anthropic endpoints (fixes #1921)
|
||||
- **fix(copilot):** emit compatible reasoning text deltas (#1919 — thanks @ivan-mezentsev)
|
||||
- **fix(api-manager):** show validation errors inline in modals, not behind (#1920 — thanks @andrewmunsell)
|
||||
- **fix(compression):** align seeded standard savings combo with stacked default, preserve stacked defaults, and secure metadata routes.
|
||||
- **fix(gemini-cli):** separate Cloud Code transport from Antigravity (#1869 — thanks @dhaern)
|
||||
- **fix(codex):** map prompt field to input array for Cursor compatibility (fixes #1872)
|
||||
- **fix(core):** align stream parameter default to false per strict OpenAI spec (fixes #1873)
|
||||
- **fix(ui):** restore Next.js CSP `unsafe-eval` in production `script-src` to fix unresponsive Onboarding button (fixes #1883)
|
||||
- **fix(proxy):** globally strip `prompt_cache_retention` in `BaseExecutor` to prevent upstream 400 errors from strict endpoints like droid/gemini-2-pro (fixes #1884)
|
||||
- **fix(ui):** include `isOpen` dependency in `EditConnectionModal` state sync to ensure `maxConcurrent` is properly hydrated when reopening the modal (fixes #1859)
|
||||
- **fix(security):** remediate 4 polynomial-redos CodeQL alerts in compression regexes by bounding repetitions and removing overlapping quantifiers
|
||||
- **fix(codex):** flatten Chat Completions tool format to Codex Responses format in `normalizeCodexTools` — prevents `Missing required parameter: tools[0].name` upstream errors (#1914 — thanks @tranduykhanh030)
|
||||
- **fix(proxy):** add proxy-aware execution context to image generation route — proxy settings are now correctly applied for image providers behind restricted networks (#1904 — thanks @Aculeasis)
|
||||
- **fix(translator):** inject `properties: {}` into zero-argument MCP tool schemas during Anthropic→OpenAI translation — prevents 400 errors from OpenAI strict schema validation (#1898 — thanks @bryceIT)
|
||||
- **fix(codex):** sanitize raw responses input (#1895 — thanks @dhaern)
|
||||
- **fix(combos):** align strategy contracts (#1892 — thanks @dhaern)
|
||||
- **fix(combos):** fix combo provider breaker profile handling (#1891 — thanks @rdself)
|
||||
- **fix(migrations):** duplicate-column no-op fix (#1886 — thanks @smartenok-ops)
|
||||
- **fix(auth):** per-connection OAuth refresh mutex (#1885 — thanks @smartenok-ops)
|
||||
- **fix(auth):** require dashboard management auth for compression preview
|
||||
|
||||
### 📝 Documentation
|
||||
|
||||
- **docs(compression):** document RTK+Caveman stacked savings ranges
|
||||
|
||||
### 🏆 Release Attribution & Retroactive Credits
|
||||
|
||||
- **@payne0420** (PR #1828 / #1839) — Implementation of the **Rate Limit Watchdog** and environment overrides. (This feature was manually backported to v3.7.8, causing the automatic GitHub Release notes to omit the author's credit).
|
||||
|
||||
---
|
||||
|
||||
## [3.7.8] — 2026-05-01
|
||||
|
||||
100
README.md
100
README.md
@@ -2,9 +2,9 @@
|
||||
|
||||
# 🚀 OmniRoute — The Free AI Gateway
|
||||
|
||||
### Never stop coding. Save 15-75% tokens with prompt compression + auto-fallback to **FREE & low-cost AI models**.
|
||||
### Never stop coding. Save 15-95% eligible tokens with RTK+Caveman compression + auto-fallback to **FREE & low-cost AI models**.
|
||||
|
||||
_The most complete open-source AI proxy — **one endpoint**, **160+ providers**, **13 routing strategies**, zero downtime. Multi-platform: **Web**, **Desktop (Electron)**, **Mobile (PWA + Termux)**. Fully extensible via **MCP Server (29 tools)**, **A2A Protocol**, and **Memory/Skills** systems. Available in **40+ languages**._
|
||||
_The most complete open-source AI proxy — **one endpoint**, **160+ providers**, **13 routing strategies**, zero downtime. Multi-platform: **Web**, **Desktop (Electron)**, **Mobile (PWA + Termux)**. Fully extensible via **MCP Server (37 tools)**, **A2A Protocol**, and **Memory/Skills** systems. Available in **40+ languages**._
|
||||
|
||||
**Chat Completions • Responses API • Embeddings • Image Generation • Video • Music • Audio Speech/Transcription • Reranking • Moderations • Web Search • MCP Server • A2A Protocol • 4,600+ Tests • 100% TypeScript**
|
||||
|
||||
@@ -20,7 +20,7 @@ _The most complete open-source AI proxy — **one endpoint**, **160+ providers**
|
||||
|
||||
<a href="https://trendshift.io/repositories/23589" target="_blank"><img src="https://trendshift.io/api/badge/repositories/23589" alt="diegosouzapw%2FOmniRoute | Trendshift" style="width: 250px; height: 55px;" width="250" height="55"/></a>
|
||||
|
||||
[🚀 Quick Start](#-quick-start) • [💡 Features](#-key-features) • [🗜️ Compression](#%EF%B8%8F-prompt-compression--save-15-75-tokens-automatically) • [💰 Pricing](#-pricing-at-a-glance) • [🎯 Use Cases](#-use-cases--ready-made-combo-playbooks) • [🌍 Proxy](#-bypass-geographic-blocks--use-ai-from-any-country) • [❓ FAQ](#-frequently-asked-questions) • [📖 Docs](#-documentation) • [💬 WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
[🚀 Quick Start](#-quick-start) • [💡 Features](#-key-features) • [🗜️ Compression](#%EF%B8%8F-prompt-compression--save-15-95-eligible-tokens-automatically) • [💰 Pricing](#-pricing-at-a-glance) • [🎯 Use Cases](#-use-cases--ready-made-combo-playbooks) • [🌍 Proxy](#-bypass-geographic-blocks--use-ai-from-any-country) • [❓ FAQ](#-frequently-asked-questions) • [📖 Docs](#-documentation) • [💬 WhatsApp](https://chat.whatsapp.com/JI7cDQ1GyaiDHhVBpLxf8b?mode=gi_t)
|
||||
|
||||
</div>
|
||||
|
||||
@@ -219,7 +219,7 @@ _Connect any AI-powered IDE or CLI tool through OmniRoute — free API gateway f
|
||||
|
||||
**OmniRoute solves all of this:**
|
||||
|
||||
✅ **Prompt Compression** — auto-compress prompts & tool outputs, save 15-75% tokens per request
|
||||
✅ **Prompt Compression** — auto-compress prompts & tool outputs, save 15-95% eligible tokens per request with RTK+Caveman stacked mode
|
||||
✅ **Maximize subscriptions** — track quota, use every bit before reset
|
||||
✅ **Auto fallback** — Subscription → API Key → Cheap → Free, zero downtime
|
||||
✅ **Multi-account** — round-robin between accounts per provider
|
||||
@@ -410,7 +410,7 @@ Alibaba · Amazon Q · AssemblyAI · Baidu Qianfan · Baseten · Black Forest La
|
||||
↓
|
||||
┌──────────────────────────────────────────────────┐
|
||||
│ OmniRoute (Smart Router) │
|
||||
│ • 🗜️ Prompt Compression (save 15-75% tokens) │
|
||||
│ • 🗜️ Prompt Compression (save 15-95% eligible) │
|
||||
│ • Format translation (OpenAI ↔ Claude ↔ Gemini) │
|
||||
│ • Quota tracking + Embeddings + Images │
|
||||
│ • Auto token refresh + Rate limit management │
|
||||
@@ -424,14 +424,14 @@ Alibaba · Amazon Q · AssemblyAI · Baidu Qianfan · Baseten · Black Forest La
|
||||
│ ↓ budget limit
|
||||
└─→ [Tier 4: FREE] Qoder, Qwen, Kiro (unlimited)
|
||||
|
||||
Result: Never stop coding, minimal cost + 15-75% token savings
|
||||
Result: Never stop coding, minimal cost + 15-95% eligible token savings
|
||||
```
|
||||
|
||||
---
|
||||
|
||||
## 🗜️ Prompt Compression — Save 15-75% Tokens Automatically
|
||||
## 🗜️ Prompt Compression — Save 15-95% Eligible Tokens Automatically
|
||||
|
||||
> **Why use many token when few token do trick?** OmniRoute's built-in compression pipeline reduces token usage on every request — before it even reaches the provider. Inspired by [Caveman](https://github.com/JuliusBrussee/caveman) (⭐ 51K+).
|
||||
> **Why use many token when few token do trick?** OmniRoute's built-in compression pipeline reduces token usage before requests reach the provider. It combines ideas from [RTK - Rust Token Killer](https://github.com/rtk-ai/rtk) and [Caveman](https://github.com/JuliusBrussee/caveman) (⭐ 51K+).
|
||||
|
||||
### How It Works
|
||||
|
||||
@@ -440,16 +440,18 @@ Every request passes through the compression pipeline **transparently** — no c
|
||||
```
|
||||
┌──────────────────┐ ┌─────────────────────────────┐ ┌──────────────┐
|
||||
│ Client sends │────▶│ OmniRoute Compression │────▶│ Provider │
|
||||
│ full prompt │ │ Pipeline (5 modes) │ │ receives │
|
||||
│ full prompt │ │ Pipeline (7 options) │ │ receives │
|
||||
│ (10,000 tok) │ │ │ │ compressed │
|
||||
│ │ │ 🪶 Lite ........... ~15% │ │ (2,500 tok) │
|
||||
│ │ │ 🪶 Lite ........... ~15% │ │ (~1,080 tok)│
|
||||
│ │ │ 🪨 Standard ....... ~30% │ │ │
|
||||
│ │ │ ⚡ Aggressive ..... ~50% │ │ 💰 75% saved │
|
||||
│ │ │ ⚡ Aggressive ..... ~50% │ │ 💰 up to 95%│
|
||||
│ │ │ 🔥 Ultra .......... ~75% │ │ │
|
||||
│ │ │ 🧰 RTK ............ 60-90% │ │ │
|
||||
│ │ │ 🔗 Stacked ........ 78-95% │ │ │
|
||||
└──────────────────┘ └─────────────────────────────┘ └──────────────┘
|
||||
```
|
||||
|
||||
### 5 Compression Modes
|
||||
### 7 Compression Options
|
||||
|
||||
| Mode | Savings | Technique | Best For |
|
||||
| ------------------------- | ------- | ----------------------------------------------------------------------------------------------- | -------------------------------------- |
|
||||
@@ -458,6 +460,36 @@ Every request passes through the compression pipeline **transparently** — no c
|
||||
| **🪨 Standard (Caveman)** | ~30% | 30+ regex rules: filler removal, context condensation, structural compression, multi-turn dedup | Daily coding with Claude/Codex |
|
||||
| **⚡ Aggressive** | ~50% | All standard + progressive message aging + tool result summarization + LLM-based compression | Long sessions with many tool calls |
|
||||
| **🔥 Ultra** | ~75% | All aggressive + heuristic token pruning + stopword removal + score-based filtering | Maximum savings when tokens are scarce |
|
||||
| **🧰 RTK** | 60-90% | 49 command-aware filters, RTK-style JSON DSL, verify gate, trust-gated custom filters | Shell/test/build/git output in agents |
|
||||
| **🔗 Stacked** | 78-95% | RTK first, then Caveman input condensation; ~89% with upstream average math | Mixed prompts with tool logs + prose |
|
||||
|
||||
### RTK + Caveman Savings Math
|
||||
|
||||
These numbers are based on the upstream project READMEs under `_references/_outros`:
|
||||
|
||||
| Source | Upstream claim used by OmniRoute docs |
|
||||
| ------- | ------------------------------------------------------------------------------------------------------------------- |
|
||||
| Caveman | `~75%` fewer output tokens; benchmark average `65%` output savings, range `22-87%`; `~46%` input compression tool |
|
||||
| RTK | `60-90%` command-output token savings; sample session `~118,000 -> ~23,900` tokens, which is `79.7%` saved (`~80%`) |
|
||||
|
||||
For the default stacked compression combo, OmniRoute runs:
|
||||
|
||||
```txt
|
||||
RTK -> Caveman
|
||||
```
|
||||
|
||||
When both engines can act on the same tool/context payload, the savings compound:
|
||||
|
||||
```txt
|
||||
combined = 1 - (1 - RTK savings) * (1 - Caveman input savings)
|
||||
average = 1 - (1 - 0.80) * (1 - 0.46) = 89.2%
|
||||
range = 1 - (1 - 0.60..0.90) * (1 - 0.46) = 78.4-94.6%
|
||||
```
|
||||
|
||||
Caveman output mode is separate from prompt compression. When enabled for responses, use Caveman's
|
||||
own upstream output numbers: `65%` average, `~75%` headline, `22-87%` observed range. Total bill
|
||||
savings depend on the prompt/output mix, but coding-agent sessions are often tool-context heavy, so
|
||||
the `RTK -> Caveman` combo is the best default for maximum context savings.
|
||||
|
||||
### Before & After (Standard/Caveman Mode)
|
||||
|
||||
@@ -481,6 +513,8 @@ Request Body
|
||||
├─ lite.ts ─────────────── Whitespace, dedup, image URLs, redundant content
|
||||
├─ caveman.ts ──────────── 30+ regex rules via cavemanRules.ts
|
||||
│ └─ preservation.ts ─── Protects code blocks, URLs, JSON from compression
|
||||
├─ engines/rtk/ ────────── Command detection + JSON DSL filters + raw-output recovery
|
||||
├─ engines/registry.ts ─── Shared engine registry for caveman, RTK, and stacked
|
||||
├─ aggressive.ts ───────── Summarizer + tool result compressor + progressive aging
|
||||
│ ├─ summarizer.ts ───── Rule-based message summarization
|
||||
│ ├─ toolResultCompressor.ts ── file/grep/shell/JSON/error compression
|
||||
@@ -492,7 +526,7 @@ Request Body
|
||||
### Configuration
|
||||
|
||||
```
|
||||
Dashboard → Settings → Compression → Pick your mode
|
||||
Dashboard → Context & Cache → Caveman / RTK / Compression Combos
|
||||
```
|
||||
|
||||
Or per-combo override:
|
||||
@@ -508,9 +542,11 @@ Or per-combo override:
|
||||
|
||||
Auto-trigger: set `autoTriggerTokens` to automatically enable compression when a request exceeds a token threshold.
|
||||
|
||||
> 🪨 **Fun fact:** The standard/caveman mode is inspired by [Caveman](https://github.com/JuliusBrussee/caveman) — the viral project that proved "caveman speak" cuts 65% of tokens while keeping 100% technical accuracy. OmniRoute takes this further with a **5-mode pipeline** that goes from gentle whitespace cleanup all the way to aggressive heuristic pruning.
|
||||
Compression combos can also assign a named compression pipeline to routing combos, so a coding combo can use RTK + Caveman while a paid subscription combo stays on lite mode.
|
||||
|
||||
📖 **Full compression documentation:** [`docs/COMPRESSION_GUIDE.md`](docs/COMPRESSION_GUIDE.md)
|
||||
> 🪨 **Fun fact:** The standard/caveman mode is inspired by [Caveman](https://github.com/JuliusBrussee/caveman) — the viral project that reports 65% average output-token savings while keeping technical accuracy. OmniRoute takes this further with a **7-option pipeline** and a default `RTK -> Caveman` combo that can reach ~89% average savings on eligible tool/context payloads.
|
||||
|
||||
📖 **Full compression documentation:** [`docs/COMPRESSION_GUIDE.md`](docs/COMPRESSION_GUIDE.md) • [`docs/RTK_COMPRESSION.md`](docs/RTK_COMPRESSION.md) • [`docs/COMPRESSION_ENGINES.md`](docs/COMPRESSION_ENGINES.md) • [`docs/COMPRESSION_RULES_FORMAT.md`](docs/COMPRESSION_RULES_FORMAT.md) • [`docs/COMPRESSION_LANGUAGE_PACKS.md`](docs/COMPRESSION_LANGUAGE_PACKS.md)
|
||||
|
||||
---
|
||||
|
||||
@@ -933,8 +969,8 @@ Then in `/dashboard/media` → **Transcription** tab: upload any audio or video
|
||||
| ---------------------------------------------------------------------------------------------------- | -------------------------------- |
|
||||
| 🧠 **Smart 4-Tier Fallback** — Subscription → API → Cheap → Free | Never stop coding, zero downtime |
|
||||
| 🔄 **Format Translation** — OpenAI ↔ Claude ↔ Gemini ↔ Responses API | Works with ANY CLI tool |
|
||||
| 🗜️ **Prompt Compression** — 5-mode pipeline (off → lite → standard → aggressive → ultra) | Save 15-75% tokens automatically |
|
||||
| 🤖 **MCP Server** — 29 tools, 3 transports (stdio/SSE/HTTP), 10 scopes | IDE/agent tool integration |
|
||||
| 🗜️ **Prompt Compression** — 7 options including Caveman, RTK, and stacked pipelines | Save 15-95% eligible tokens |
|
||||
| 🤖 **MCP Server** — 37 tools, 3 transports (stdio/SSE/HTTP), 10 scopes | IDE/agent tool integration |
|
||||
| 🛡️ **Resilience Engine** — circuit breakers, cooldowns, TLS spoofing, anti-thundering herd | Auto-recovery from any failure |
|
||||
| 🎵 **10 Multi-Modal APIs** — chat, embed, images, video, music, TTS, STT, moderation, rerank, search | One endpoint for everything |
|
||||
| 🌍 **3-Level Proxy** — global, per-provider, per-key + 1proxy free marketplace | Access AI from any country |
|
||||
@@ -1142,7 +1178,7 @@ curl http://localhost:20128/.well-known/agent.json
|
||||
|
||||
- [User Guide](docs/USER_GUIDE.md) — Providers, combos, CLI integration
|
||||
- [API Reference](docs/API_REFERENCE.md) — All endpoints with examples
|
||||
- [MCP Server](open-sse/mcp-server/README.md) — 29 tools, IDE configs
|
||||
- [MCP Server](open-sse/mcp-server/README.md) — 37 tools, IDE configs
|
||||
- [A2A Server](src/lib/a2a/README.md) — JSON-RPC, skills, streaming
|
||||
- [Environment Config](docs/ENVIRONMENT.md) — Complete `.env` reference
|
||||
- [VM Deployment](docs/VM_DEPLOYMENT_GUIDE.md) — VM + nginx + Cloudflare
|
||||
@@ -1325,16 +1361,20 @@ See the [Proxy Guide](docs/PROXY_GUIDE.md) for setup instructions.
|
||||
|
||||
### 🧠 Features & Architecture
|
||||
|
||||
| Document | Description |
|
||||
| -------------------------------------------------------- | ---------------------------------------------------------------------- |
|
||||
| [Architecture](docs/ARCHITECTURE.md) | System architecture, data flow, and internals |
|
||||
| [Compression Guide](docs/COMPRESSION_GUIDE.md) | 5-mode pipeline: off / lite / standard / aggressive / ultra |
|
||||
| [Resilience Guide](docs/RESILIENCE_GUIDE.md) | Circuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing |
|
||||
| [Auto-Combo Engine](docs/AUTO-COMBO.md) | 6-factor scoring, mode packs, self-healing |
|
||||
| [Proxy Guide](docs/PROXY_GUIDE.md) | 3-level proxy system, 1proxy marketplace, registry CRUD |
|
||||
| [Free Tiers](docs/FREE_TIERS.md) | 25+ free API providers consolidated directory |
|
||||
| [Features Gallery](docs/FEATURES.md) | Visual dashboard tour with screenshots |
|
||||
| [Codebase Documentation](docs/CODEBASE_DOCUMENTATION.md) | Beginner-friendly codebase walkthrough |
|
||||
| Document | Description |
|
||||
| ---------------------------------------------------------------- | ----------------------------------------------------------------------------- |
|
||||
| [Architecture](docs/ARCHITECTURE.md) | System architecture, data flow, and internals |
|
||||
| [Compression Guide](docs/COMPRESSION_GUIDE.md) | 7-option pipeline: off / lite / standard / aggressive / ultra / RTK / stacked |
|
||||
| [RTK Compression](docs/RTK_COMPRESSION.md) | Command-output compression, filters, trust, verify, raw-output recovery |
|
||||
| [Compression Engines](docs/COMPRESSION_ENGINES.md) | Caveman, RTK, stacked pipelines, dashboard/API/MCP surfaces |
|
||||
| [Compression Rules Format](docs/COMPRESSION_RULES_FORMAT.md) | JSON rule-pack schemas for Caveman and RTK filters |
|
||||
| [Compression Language Packs](docs/COMPRESSION_LANGUAGE_PACKS.md) | Language detection and Caveman rule-pack authoring |
|
||||
| [Resilience Guide](docs/RESILIENCE_GUIDE.md) | Circuit breakers, cooldowns, queue, anti-thundering herd, TLS spoofing |
|
||||
| [Auto-Combo Engine](docs/AUTO-COMBO.md) | 6-factor scoring, mode packs, self-healing |
|
||||
| [Proxy Guide](docs/PROXY_GUIDE.md) | 3-level proxy system, 1proxy marketplace, registry CRUD |
|
||||
| [Free Tiers](docs/FREE_TIERS.md) | 25+ free API providers consolidated directory |
|
||||
| [Features Gallery](docs/FEATURES.md) | Visual dashboard tour with screenshots |
|
||||
| [Codebase Documentation](docs/CODEBASE_DOCUMENTATION.md) | Beginner-friendly codebase walkthrough |
|
||||
|
||||
### 🤖 Protocols & APIs
|
||||
|
||||
@@ -1459,10 +1499,12 @@ gh release create v2.0.0 --title "v2.0.0" --generate-notes
|
||||
|
||||
Special thanks to **[9router](https://github.com/decolua/9router)** by **[decolua](https://github.com/decolua)** — the original project that inspired this fork. OmniRoute builds upon that incredible foundation with additional features, multi-modal APIs, and a full TypeScript rewrite.
|
||||
|
||||
Special thanks to **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** — the original Go implementation that inspired this JavaScript port.
|
||||
Special thanks to **[CLIProxyAPI](https://github.com/router-for-me/CLIProxyAPI)** by **[router-for-me](https://github.com/router-for-me)** — the original Go implementation that inspired this JavaScript port.
|
||||
|
||||
Special thanks to **[Caveman](https://github.com/JuliusBrussee/caveman)** by **[JuliusBrussee](https://github.com/JuliusBrussee)** (⭐ 51K+) — the viral "why use many token when few token do trick" project whose caveman-speak compression philosophy inspired OmniRoute's standard compression mode and 30+ filler/condensation regex rules.
|
||||
|
||||
Special thanks to **[RTK - Rust Token Killer](https://github.com/rtk-ai/rtk)** by **[RTK AI](https://github.com/rtk-ai)** — the high-performance command-output compression project whose terminal, build, test, git, and tool-output filtering model inspired OmniRoute's RTK engine, JSON filter DSL, raw-output recovery, and stacked RTK → Caveman compression pipeline.
|
||||
|
||||
---
|
||||
|
||||
## 📄 License
|
||||
|
||||
@@ -216,14 +216,32 @@ Response example:
|
||||
|
||||
### Settings
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
| ------------------------------- | ------------- | ---------------------- |
|
||||
| `/api/settings` | GET/PUT/PATCH | General settings |
|
||||
| `/api/settings/proxy` | GET/PUT | Network proxy config |
|
||||
| `/api/settings/proxy/test` | POST | Test proxy connection |
|
||||
| `/api/settings/ip-filter` | GET/PUT | IP allowlist/blocklist |
|
||||
| `/api/settings/thinking-budget` | GET/PUT | Reasoning token budget |
|
||||
| `/api/settings/system-prompt` | GET/PUT | Global system prompt |
|
||||
| Endpoint | Method | Description |
|
||||
| ------------------------------- | ------------- | ------------------------- |
|
||||
| `/api/settings` | GET/PUT/PATCH | General settings |
|
||||
| `/api/settings/proxy` | GET/PUT | Network proxy config |
|
||||
| `/api/settings/proxy/test` | POST | Test proxy connection |
|
||||
| `/api/settings/ip-filter` | GET/PUT | IP allowlist/blocklist |
|
||||
| `/api/settings/thinking-budget` | GET/PUT | Reasoning token budget |
|
||||
| `/api/settings/system-prompt` | GET/PUT | Global system prompt |
|
||||
| `/api/settings/compression` | GET/PUT | Global compression config |
|
||||
|
||||
### Context & Compression
|
||||
|
||||
| Endpoint | Method | Description |
|
||||
| -------------------------------------- | -------------- | ------------------------------------------------------------------------ |
|
||||
| `/api/compression/preview` | POST | Preview off/lite/standard/aggressive/ultra/RTK/stacked compression |
|
||||
| `/api/compression/language-packs` | GET | List available Caveman language packs |
|
||||
| `/api/compression/rules` | GET | List Caveman rule metadata |
|
||||
| `/api/context/caveman/config` | GET/PUT | Caveman-specific settings alias |
|
||||
| `/api/context/rtk/config` | GET/PUT | RTK-specific settings, including custom filters and raw-output retention |
|
||||
| `/api/context/rtk/filters` | GET | RTK filter catalog and custom-filter diagnostics |
|
||||
| `/api/context/rtk/test` | POST | Run RTK preview/test against a text payload |
|
||||
| `/api/context/rtk/raw-output/[id]` | GET | Read retained redacted raw output by pointer id |
|
||||
| `/api/context/combos` | GET/POST | Compression combo list/create |
|
||||
| `/api/context/combos/[id]` | GET/PUT/DELETE | Compression combo detail/update/delete |
|
||||
| `/api/context/combos/[id]/assignments` | GET/PUT | Assign compression combos to routing combos |
|
||||
| `/api/context/analytics` | GET | Compression analytics alias |
|
||||
|
||||
### Monitoring
|
||||
|
||||
@@ -451,11 +469,12 @@ Content-Type: application/json
|
||||
2. Route handler calls `handleChat`, `handleEmbedding`, `handleAudioTranscription`, or `handleImageGeneration`
|
||||
3. Model is resolved (direct provider/model or alias/combo)
|
||||
4. Credentials selected from local DB with account availability filtering
|
||||
5. For chat: `handleChatCore` — format detection, translation, cache check, idempotency check
|
||||
6. Provider executor sends upstream request
|
||||
7. Response translated back to client format (chat) or returned as-is (embeddings/images/audio)
|
||||
8. Usage/logging recorded
|
||||
9. Fallback applies on errors according to combo rules
|
||||
5. For chat: `handleChatCore` checks semantic/signature cache and resolves combo compression settings
|
||||
6. Proactive compression runs before provider translation when enabled (`lite`, Caveman, RTK, or stacked)
|
||||
7. Provider executor sends upstream request
|
||||
8. Response translated back to client format (chat) or returned as-is (embeddings/images/audio)
|
||||
9. Usage, compression analytics, and request logs are recorded
|
||||
10. Fallback applies on errors according to combo rules
|
||||
|
||||
Full architecture reference: [`ARCHITECTURE.md`](ARCHITECTURE.md)
|
||||
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](ARCHITECTURE.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/ARCHITECTURE.md) | 🇪🇸 [Español](i18n/es/ARCHITECTURE.md) | 🇫🇷 [Français](i18n/fr/ARCHITECTURE.md) | 🇮🇹 [Italiano](i18n/it/ARCHITECTURE.md) | 🇷🇺 [Русский](i18n/ru/ARCHITECTURE.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/ARCHITECTURE.md) | 🇩🇪 [Deutsch](i18n/de/ARCHITECTURE.md) | 🇮🇳 [हिन्दी](i18n/in/ARCHITECTURE.md) | 🇹🇭 [ไทย](i18n/th/ARCHITECTURE.md) | 🇺🇦 [Українська](i18n/uk-UA/ARCHITECTURE.md) | 🇸🇦 [العربية](i18n/ar/ARCHITECTURE.md) | 🇯🇵 [日本語](i18n/ja/ARCHITECTURE.md) | 🇻🇳 [Tiếng Việt](i18n/vi/ARCHITECTURE.md) | 🇧🇬 [Български](i18n/bg/ARCHITECTURE.md) | 🇩🇰 [Dansk](i18n/da/ARCHITECTURE.md) | 🇫🇮 [Suomi](i18n/fi/ARCHITECTURE.md) | 🇮🇱 [עברית](i18n/he/ARCHITECTURE.md) | 🇭🇺 [Magyar](i18n/hu/ARCHITECTURE.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/ARCHITECTURE.md) | 🇰🇷 [한국어](i18n/ko/ARCHITECTURE.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/ARCHITECTURE.md) | 🇳🇱 [Nederlands](i18n/nl/ARCHITECTURE.md) | 🇳🇴 [Norsk](i18n/no/ARCHITECTURE.md) | 🇵🇹 [Português (Portugal)](i18n/pt/ARCHITECTURE.md) | 🇷🇴 [Română](i18n/ro/ARCHITECTURE.md) | 🇵🇱 [Polski](i18n/pl/ARCHITECTURE.md) | 🇸🇰 [Slovenčina](i18n/sk/ARCHITECTURE.md) | 🇸🇪 [Svenska](i18n/sv/ARCHITECTURE.md) | 🇵🇭 [Filipino](i18n/phi/ARCHITECTURE.md) | 🇨🇿 [Čeština](i18n/cs/ARCHITECTURE.md)
|
||||
|
||||
_Last updated: 2026-04-15_
|
||||
_Last updated: 2026-05-02_
|
||||
|
||||
## Executive Summary
|
||||
|
||||
@@ -52,12 +52,13 @@ Core capabilities:
|
||||
- Compliance audit logging with opt-out per API key
|
||||
- Eval framework for LLM quality assurance
|
||||
- Health dashboard with real-time provider circuit breaker status
|
||||
- MCP Server (29 tools) with 3 transports (stdio/SSE/Streamable HTTP)
|
||||
- MCP Server (37 tools) with 3 transports (stdio/SSE/Streamable HTTP)
|
||||
- A2A Server (JSON-RPC 2.0 + SSE) with skills and task lifecycle
|
||||
- Memory system (extraction, injection, retrieval, summarization)
|
||||
- Skills system (registry, executor, sandbox, built-in skills)
|
||||
- MITM proxy with certificate management and DNS handling
|
||||
- Prompt injection guard middleware
|
||||
- Prompt compression pipeline with Caveman, RTK, stacked pipelines, compression combos, language packs, and analytics
|
||||
- ACP (Agent Communication Protocol) registry
|
||||
- Modular OAuth providers (13 individual modules under `src/lib/oauth/providers/`)
|
||||
- Uninstall/full-uninstall scripts
|
||||
@@ -112,6 +113,9 @@ Main pages under `src/app/(dashboard)/dashboard/`:
|
||||
- `/dashboard/health` — uptime, circuit breakers, rate limits, quota-monitored sessions
|
||||
- `/dashboard/logs` — request/proxy/audit/console logs
|
||||
- `/dashboard/settings` — system settings tabs (general, routing, combo defaults, etc.)
|
||||
- `/dashboard/context/caveman` — Caveman compression rules, language packs, preview, and output mode
|
||||
- `/dashboard/context/rtk` — RTK command-output filters, preview, and runtime safety settings
|
||||
- `/dashboard/context/combos` — named compression pipelines assigned to routing combos
|
||||
- `/dashboard/api-manager` — API key lifecycle and model permissions
|
||||
|
||||
## High-Level System Context
|
||||
@@ -203,6 +207,8 @@ Management domains:
|
||||
- IP filter: `src/app/api/settings/ip-filter` (GET/PUT)
|
||||
- Thinking budget: `src/app/api/settings/thinking-budget` (GET/PUT)
|
||||
- System prompt: `src/app/api/settings/system-prompt` (GET/PUT)
|
||||
- Compression: `src/app/api/settings/compression`, `src/app/api/compression/*`, and
|
||||
`src/app/api/context/*`
|
||||
- Sessions: `src/app/api/sessions` (GET)
|
||||
- Rate limits: `src/app/api/rate-limits` (GET)
|
||||
- Resilience: `src/app/api/resilience` (GET/PATCH) — request queue, connection cooldown, provider breaker, wait-for-cooldown config
|
||||
@@ -252,6 +258,8 @@ Services (business logic):
|
||||
- Rate limit management: `open-sse/services/rateLimitManager.ts`
|
||||
- Circuit breaker: `open-sse/services/circuitBreaker.ts`
|
||||
- Context handoff: `open-sse/services/contextHandoff.ts` — handoff summary generation and injection for context-relay strategy
|
||||
- Compression: `open-sse/services/compression/*` — proactive compression before provider translation;
|
||||
includes Caveman rules, RTK filters, stacked pipelines, compression combos, stats, and validation
|
||||
- Codex quota fetcher: `open-sse/services/codexQuotaFetcher.ts` — fetches Codex quota for context-relay handoff decisions
|
||||
- Cooldown-aware retry: `src/sse/services/cooldownAwareRetry.ts` — per-model cooldown retries with configurable `requestRetry` / `maxRetryIntervalSec`
|
||||
- Safe outbound fetch: `src/shared/network/safeOutboundFetch.ts` — guarded provider/model fetch with SSRF guard, private-URL blocking, retry, and timeout
|
||||
@@ -630,6 +638,12 @@ flowchart LR
|
||||
- `src/app/api/settings/ip-filter`: IP allowlist/blocklist (GET/PUT)
|
||||
- `src/app/api/settings/thinking-budget`: thinking token budget config (GET/PUT)
|
||||
- `src/app/api/settings/system-prompt`: global system prompt (GET/PUT)
|
||||
- `src/app/api/settings/compression`: global compression settings (GET/PUT)
|
||||
- `src/app/api/compression/*`: compression preview, rule metadata, and language packs
|
||||
- `src/app/api/context/caveman/config`: Caveman settings alias (GET/PUT)
|
||||
- `src/app/api/context/rtk/*`: RTK config, filter catalog, test endpoint, and raw-output recovery
|
||||
- `src/app/api/context/combos*`: compression combo CRUD and routing-combo assignments
|
||||
- `src/app/api/context/analytics`: compression analytics alias
|
||||
- `src/app/api/sessions`: active session listing (GET)
|
||||
- `src/app/api/rate-limits`: per-account rate limit status (GET)
|
||||
- `src/app/api/sync/tokens`: sync token CRUD (GET/POST)
|
||||
|
||||
143
docs/COMPRESSION_ENGINES.md
Normal file
143
docs/COMPRESSION_ENGINES.md
Normal file
@@ -0,0 +1,143 @@
|
||||
# Compression Engines
|
||||
|
||||
OmniRoute compression is built around engine contracts. A mode can run one engine directly
|
||||
(`caveman` or `rtk`) or a deterministic stacked pipeline that executes multiple engines in order.
|
||||
|
||||
## Modes
|
||||
|
||||
| Mode | Engine path | Intended input |
|
||||
| ------------ | ---------------------------------- | -------------------------------------------- |
|
||||
| `off` | none | Exact prompt preservation |
|
||||
| `lite` | Caveman lite helpers | Low-risk always-on cleanup |
|
||||
| `standard` | Caveman | Natural-language prompt condensation |
|
||||
| `aggressive` | Caveman + history/tool summarizers | Long chat sessions |
|
||||
| `ultra` | Caveman + pruning helpers | Context-limit recovery |
|
||||
| `rtk` | RTK | Terminal, shell, build, test, and git output |
|
||||
| `stacked` | Pipeline, default `rtk -> caveman` | Mixed tool logs and prose, max savings |
|
||||
|
||||
## Engine Registry
|
||||
|
||||
The registry lives in `open-sse/services/compression/engines/registry.ts`. Engines expose a shared
|
||||
contract:
|
||||
|
||||
- `id`: stable engine id such as `caveman` or `rtk`
|
||||
- `label`: dashboard-readable name
|
||||
- `supports(mode)`: whether the engine can execute a compression mode
|
||||
- `compress(input)`: transforms text/messages and returns stats
|
||||
|
||||
`strategySelector.ts` registers the built-in engines before compression runs. This lets preview,
|
||||
runtime compression, stacked mode, tests, and future engines use the same execution path.
|
||||
|
||||
## Caveman
|
||||
|
||||
Caveman mode focuses on semantic condensation of normal prose:
|
||||
|
||||
- preserves code blocks, URLs, JSON, paths, and structured data
|
||||
- removes filler, hedging, repeated context, and verbose connective phrasing
|
||||
- supports language-aware file rule packs in `open-sse/services/compression/rules/`
|
||||
- remains available through the legacy `standard`, `aggressive`, and `ultra` modes
|
||||
|
||||
The dashboard surface is `Dashboard -> Context & Cache -> Caveman`.
|
||||
|
||||
Caveman upstream reports `~75%` fewer output tokens, `65%` average output savings in benchmarks
|
||||
with a `22-87%` range, and a `~46%` input-compression tool. OmniRoute uses the Caveman input-side
|
||||
number when documenting stacked prompt/context savings; Caveman output mode remains a separate
|
||||
response-behavior feature.
|
||||
|
||||
## RTK
|
||||
|
||||
RTK mode focuses on command and tool output:
|
||||
|
||||
- detects output classes such as `git status`, `git branch`, `git diff`, Vitest/Jest/Pytest,
|
||||
Cargo/Go tests, TypeScript/Vite/Webpack builds, ESLint, npm audit/installs, Docker logs,
|
||||
shell `find`/`grep`, stack traces, and generic logs
|
||||
- applies 49 JSON filters from `open-sse/services/compression/engines/rtk/filters/`
|
||||
- supports the RTK-style declarative pipeline: ANSI stripping, replace, match-output short-circuit,
|
||||
strip/keep lines, per-line truncation, head/tail/max-line truncation, and on-empty fallback
|
||||
- supports trust-gated project filters in `.rtk/filters.json` and global filters in
|
||||
`DATA_DIR/rtk/filters.json`
|
||||
- strips ANSI sequences, progress noise, repeated lines, and unhelpful boilerplate
|
||||
- preserves actionable failures, warnings, summaries, changed files, and tail context
|
||||
- can optionally retain redacted raw output for recovery/debugging through authenticated management
|
||||
routes
|
||||
|
||||
The dashboard surface is `Dashboard -> Context & Cache -> RTK`.
|
||||
|
||||
Operational details for custom filters, trust, verify, and raw-output recovery live in
|
||||
[`RTK_COMPRESSION.md`](RTK_COMPRESSION.md).
|
||||
|
||||
RTK upstream reports `60-90%` savings for command-output compression. Its README example shows a
|
||||
30-minute Claude Code session going from `~118,000` tokens to `~23,900`, or `79.7%` saved.
|
||||
|
||||
## Stacked Pipelines
|
||||
|
||||
Stacked mode runs pipeline steps in order. The default is:
|
||||
|
||||
```txt
|
||||
rtk -> caveman
|
||||
```
|
||||
|
||||
Use this for coding-agent sessions where a prompt combines command output with human or assistant
|
||||
prose. RTK reduces noisy tool logs first, then Caveman compresses remaining natural language.
|
||||
|
||||
Pipeline steps are configured with `stackedPipeline` in compression settings or through compression
|
||||
combos.
|
||||
|
||||
When both engines reduce the same eligible payload, savings compound:
|
||||
|
||||
```txt
|
||||
combined = 1 - (1 - RTK savings) * (1 - Caveman input savings)
|
||||
average = 1 - (1 - 0.80) * (1 - 0.46) = 89.2%
|
||||
range = 1 - (1 - 0.60..0.90) * (1 - 0.46) = 78.4-94.6%
|
||||
```
|
||||
|
||||
## Compression Combos
|
||||
|
||||
Compression combos are named compression profiles that can be assigned to routing combos:
|
||||
|
||||
- `compression_combos`: stores mode, pipeline, RTK config, language config, and default marker
|
||||
- `compression_combo_assignments`: maps a compression combo to a routing combo
|
||||
- runtime integration resolves an assigned compression combo before generic combo overrides
|
||||
- analytics include `compression_combo_id` and `engine`
|
||||
|
||||
Dashboard surface: `Dashboard -> Context & Cache -> Compression Combos`.
|
||||
|
||||
## API Surface
|
||||
|
||||
| Route | Purpose |
|
||||
| -------------------------------------- | ------------------------------------------ |
|
||||
| `/api/settings/compression` | Global compression settings |
|
||||
| `/api/compression/preview` | Preview any compression mode |
|
||||
| `/api/compression/language-packs` | List available Caveman language packs |
|
||||
| `/api/context/caveman/config` | Caveman settings alias |
|
||||
| `/api/context/rtk/config` | RTK defaults and settings |
|
||||
| `/api/context/rtk/filters` | RTK filter catalog |
|
||||
| `/api/context/rtk/test` | RTK preview/test endpoint |
|
||||
| `/api/context/rtk/raw-output/[id]` | Authenticated redacted raw-output recovery |
|
||||
| `/api/context/combos` | Compression combo CRUD |
|
||||
| `/api/context/combos/[id]/assignments` | Routing-combo assignment CRUD |
|
||||
| `/api/context/analytics` | Compression analytics alias |
|
||||
|
||||
Management routes require management authentication or API-key policy checks.
|
||||
|
||||
## MCP Tools
|
||||
|
||||
Compression exposes five MCP tools:
|
||||
|
||||
| Tool | Scope | Purpose |
|
||||
| ----------------------------------- | ------------------- | -------------------------------- |
|
||||
| `omniroute_compression_status` | `read:compression` | Settings, analytics, cache stats |
|
||||
| `omniroute_compression_configure` | `write:compression` | Update global settings |
|
||||
| `omniroute_set_compression_engine` | `write:compression` | Set mode and optional pipeline |
|
||||
| `omniroute_list_compression_combos` | `read:compression` | List compression combos |
|
||||
| `omniroute_compression_combo_stats` | `read:compression` | Read combo/engine analytics |
|
||||
|
||||
## Validation
|
||||
|
||||
The focused gates for this area are:
|
||||
|
||||
```bash
|
||||
node --import tsx/esm --test tests/unit/compression/rtk-*.test.ts tests/unit/compression/pipeline-integration.test.ts tests/unit/compression/context-compression-api.test.ts
|
||||
node --import tsx/esm --test tests/unit/compression/*.test.ts tests/golden-set/*.test.ts tests/integration/compression-pipeline.test.ts tests/unit/api/compression/compression-api.test.ts
|
||||
npm run typecheck:core
|
||||
```
|
||||
@@ -1,6 +1,6 @@
|
||||
# 🗜️ Prompt Compression Guide — OmniRoute
|
||||
|
||||
> Save 15-75% on token costs automatically. For a quick overview, see the [README Compression section](../README.md#%EF%B8%8F-prompt-compression--save-15-75-tokens-automatically).
|
||||
> Save 15-95% on eligible context automatically. For a quick overview, see the [README Compression section](../README.md#%EF%B8%8F-prompt-compression--save-15-95-eligible-tokens-automatically).
|
||||
|
||||
## Overview
|
||||
|
||||
@@ -19,6 +19,8 @@ Client Request
|
||||
→ Standard: Caveman-speak filler removal (~30%)
|
||||
→ Aggressive: History aging + summarization (~50%)
|
||||
→ Ultra: Heuristic pruning + code-block thinning (~75%)
|
||||
→ RTK: Command-aware terminal/tool-output filtering (60-90% upstream range)
|
||||
→ Stacked: Ordered multi-engine pipeline, usually RTK then Caveman (78-95% eligible range)
|
||||
→ Compressed Request → Provider
|
||||
```
|
||||
|
||||
@@ -77,6 +79,65 @@ Maximum compression for token-critical scenarios:
|
||||
|
||||
**Best for:** When you're hitting context limits repeatedly.
|
||||
|
||||
### RTK Mode (60-90% upstream range)
|
||||
|
||||
RTK mode is optimized for verbose tool outputs that appear in coding-agent sessions:
|
||||
|
||||
- Detects command/output classes such as `git status`, `git diff`, `git log`, test runners,
|
||||
TypeScript/Vite/Webpack builds, ESLint/Biome/Prettier, npm audit/installs, Docker logs, infra
|
||||
output, and generic shell output
|
||||
- Applies JSON filter packs from `open-sse/services/compression/engines/rtk/filters/`
|
||||
- Ships 49 built-in filters with inline verify samples
|
||||
- Removes ANSI control sequences, progress bars, repeated lines, and non-actionable noise
|
||||
- Preserves failures, errors, warnings, changed files, summaries, and the tail of long output
|
||||
- Supports trust-gated project filters, global filters, and optional redacted raw-output recovery
|
||||
|
||||
**Best for:** Agent sessions with shell, build, test, git, grep, and file-output transcripts.
|
||||
|
||||
### Stacked Mode (78-95% eligible range)
|
||||
|
||||
Stacked mode runs multiple compression engines in a deterministic order. The default pipeline is:
|
||||
|
||||
```txt
|
||||
RTK -> Caveman
|
||||
```
|
||||
|
||||
That order keeps terminal/tool output compact first, then applies Caveman semantic condensation to
|
||||
the remaining natural-language prompt. Stacked pipelines can be configured globally or through
|
||||
compression combos assigned to routing combos.
|
||||
|
||||
**Best for:** Mixed context with large tool logs plus human instructions or assistant summaries.
|
||||
|
||||
---
|
||||
|
||||
## Upstream Savings Math
|
||||
|
||||
OmniRoute documents compression savings from two sources: upstream project benchmarks and
|
||||
OmniRoute's own engine composition.
|
||||
|
||||
| Source | Upstream README number used here |
|
||||
| ------- | --------------------------------------------------------------------------------------------------------------------- |
|
||||
| Caveman | `~75%` fewer output tokens, `65%` benchmark average output savings, `22-87%` range, and `~46%` input compression tool |
|
||||
| RTK | `60-90%` command-output savings; sample session `~118,000 -> ~23,900` tokens, or `79.7%` saved (`~80%`) |
|
||||
|
||||
For overlapping tool/context payloads, the default OmniRoute combo stacks the engines:
|
||||
|
||||
```txt
|
||||
RTK -> Caveman
|
||||
```
|
||||
|
||||
The combined savings are multiplicative, not additive:
|
||||
|
||||
```txt
|
||||
combined = 1 - (1 - RTK savings) * (1 - Caveman input savings)
|
||||
average = 1 - (1 - 0.80) * (1 - 0.46) = 89.2%
|
||||
range = 1 - (1 - 0.60..0.90) * (1 - 0.46) = 78.4-94.6%
|
||||
```
|
||||
|
||||
That `78-95%` number applies when both RTK and Caveman can reduce the same input/context payload.
|
||||
Caveman response output mode is separate: when enabled, use Caveman's own output savings (`65%`
|
||||
average, `~75%` headline, `22-87%` range). Total billing savings depend on your prompt/output mix.
|
||||
|
||||
---
|
||||
|
||||
## Token Savings Visualization
|
||||
@@ -87,6 +148,8 @@ With Lite: 40K tokens sent (15% saved — safe, always-on)
|
||||
With Standard: 33K tokens sent (30% saved — caveman-speak rules)
|
||||
With Aggressive: 24K tokens sent (50% saved — aging + summarization)
|
||||
With Ultra: 12K tokens sent (75% saved — heuristic pruning)
|
||||
With RTK: 19K-5K tokens sent (60-90% saved on command/tool output)
|
||||
With Stacked: 10K-2.5K tokens sent (78-95% eligible RTK+Caveman range)
|
||||
```
|
||||
|
||||
---
|
||||
@@ -95,25 +158,29 @@ With Ultra: 12K tokens sent (75% saved — heuristic pruning)
|
||||
|
||||
### Dashboard
|
||||
|
||||
Navigate to `Dashboard → Settings → Compression`:
|
||||
Navigate to `Dashboard → Context & Cache`:
|
||||
|
||||
- **Default Mode** — sets the system-wide compression mode
|
||||
- **Caveman** — mode selection, language packs, preview, and global defaults
|
||||
- **RTK** — command-filter preview, RTK safety settings, and filter catalog
|
||||
- **Compression Combos** — named engine pipelines assigned to routing combos
|
||||
- **Auto-Trigger Threshold** — automatically engage compression when token count exceeds threshold
|
||||
- **Per-Combo Override** — each combo can have its own compression mode
|
||||
|
||||
### Per-Combo Override
|
||||
|
||||
In `Dashboard → Combos → [Your Combo] → Advanced`, set compression mode per combo:
|
||||
In `Dashboard → Context & Cache → Compression Combos`, assign a compression combo to a routing
|
||||
combo:
|
||||
|
||||
```txt
|
||||
Combo: "free-forever"
|
||||
Mode: Standard
|
||||
Compression Combo: "coding-agent-stack"
|
||||
Pipeline: RTK -> Caveman
|
||||
Targets:
|
||||
1. gc/gemini-3-flash
|
||||
2. if/kimi-k2-thinking
|
||||
```
|
||||
|
||||
This lets you use aggressive compression on free providers while keeping lite mode on paid subscriptions.
|
||||
This lets you use stacked compression on free/coding providers while keeping lite mode on paid
|
||||
subscriptions.
|
||||
|
||||
### API
|
||||
|
||||
@@ -124,7 +191,20 @@ curl http://localhost:20128/api/settings/compression
|
||||
# Update compression settings
|
||||
curl -X PUT http://localhost:20128/api/settings/compression \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"defaultMode":"lite","autoTriggerThreshold":32000}'
|
||||
-d '{"defaultMode":"stacked","autoTriggerMode":"stacked","autoTriggerTokens":32000}'
|
||||
|
||||
# Preview a specific RTK/stacked payload
|
||||
curl -X POST http://localhost:20128/api/compression/preview \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"mode":"rtk","messages":[{"role":"tool","content":"npm test output here"}]}'
|
||||
|
||||
# List RTK filter packs
|
||||
curl http://localhost:20128/api/context/rtk/filters
|
||||
|
||||
# Test RTK directly with optional command metadata
|
||||
curl -X POST http://localhost:20128/api/context/rtk/test \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{"command":"npm test","text":"FAIL tests/example.test.ts\nError: boom"}'
|
||||
```
|
||||
|
||||
---
|
||||
@@ -136,11 +216,14 @@ The compression engine **always preserves:**
|
||||
- ✅ Code blocks (fenced and inline)
|
||||
- ✅ URLs and file paths
|
||||
- ✅ JSON structures and structured data
|
||||
- ✅ API keys, tokens, and identifiers
|
||||
- ✅ Identifiers and protected technical tokens
|
||||
- ✅ Mathematical expressions
|
||||
- ✅ Tool/function call definitions
|
||||
- ✅ System prompts (in lite mode)
|
||||
|
||||
RTK raw-output recovery redacts common API keys, bearer tokens, Slack tokens, AWS access keys,
|
||||
passwords, tokens, and secrets before anything is persisted.
|
||||
|
||||
---
|
||||
|
||||
## Compression Stats
|
||||
@@ -154,7 +237,10 @@ Every compressed request includes stats in the server logs:
|
||||
"savingsPercent": 15.0,
|
||||
"techniquesUsed": ["collapseWhitespace", "dedupSystemPrompt"],
|
||||
"mode": "lite",
|
||||
"latencyMs": 0.8
|
||||
"engine": "caveman",
|
||||
"compressionComboId": "coding-agent-stack",
|
||||
"durationMs": 0.8,
|
||||
"rtkRawOutputPointers": []
|
||||
}
|
||||
```
|
||||
|
||||
@@ -166,13 +252,16 @@ Every compressed request includes stats in the server logs:
|
||||
| ------- | ------------------------------------ | ---------- |
|
||||
| Phase 1 | Off, Lite | ✅ Shipped |
|
||||
| Phase 2 | Standard, Aggressive, Ultra | ✅ Shipped |
|
||||
| Phase 3 | Per-model adaptive, ML-based pruning | 🗓️ Planned |
|
||||
| Phase 3 | RTK, Stacked, Compression Combos | ✅ Shipped |
|
||||
| Phase 4 | Per-model adaptive, ML-based pruning | 🗓️ Planned |
|
||||
|
||||
---
|
||||
|
||||
## Acknowledgments
|
||||
|
||||
Standard mode compression rules are inspired by **[Caveman](https://github.com/JuliusBrussee/caveman)** by **[JuliusBrussee](https://github.com/JuliusBrussee)** (⭐ 51K+) — the viral "why use many token when few token do trick" project.
|
||||
Standard mode compression rules are inspired by **[Caveman](https://github.com/JuliusBrussee/caveman)** by **[JuliusBrussee](https://github.com/JuliusBrussee)** (⭐ 51K+) — the viral "why use many token when few token do trick" project. Caveman reports `~75%` fewer output tokens, `65%` benchmark average output savings, a `22-87%` output range, and a `~46%` input-compression tool.
|
||||
|
||||
RTK mode is inspired by **[RTK - Rust Token Killer](https://github.com/rtk-ai/rtk)** by **[RTK AI](https://github.com/rtk-ai)** — the high-performance command-output compression project for terminal, build, test, git, and tool-output filtering. RTK reports `60-90%` savings, with its README sample session showing `~80%` saved.
|
||||
|
||||
---
|
||||
|
||||
@@ -181,3 +270,7 @@ Standard mode compression rules are inspired by **[Caveman](https://github.com/J
|
||||
- [Environment Config](ENVIRONMENT.md) — Compression environment variables
|
||||
- [Architecture Guide](ARCHITECTURE.md) — Compression pipeline internals
|
||||
- [User Guide](USER_GUIDE.md) — Getting started with compression
|
||||
- [RTK Compression](RTK_COMPRESSION.md) — RTK filters, trust model, verify gate, raw-output recovery
|
||||
- [Compression Engines](COMPRESSION_ENGINES.md) — Caveman, RTK, stacked, APIs, MCP, dashboard
|
||||
- [Compression Rules Format](COMPRESSION_RULES_FORMAT.md) — JSON rule-pack format
|
||||
- [Compression Language Packs](COMPRESSION_LANGUAGE_PACKS.md) — Language-specific Caveman rules
|
||||
|
||||
96
docs/COMPRESSION_LANGUAGE_PACKS.md
Normal file
96
docs/COMPRESSION_LANGUAGE_PACKS.md
Normal file
@@ -0,0 +1,96 @@
|
||||
# Compression Language Packs
|
||||
|
||||
Caveman compression can load language-specific rule packs in addition to the built-in English rules.
|
||||
This keeps the core engine stable while allowing Portuguese, Spanish, German, French, Japanese, and
|
||||
future language packs to evolve independently.
|
||||
|
||||
## Location
|
||||
|
||||
Language packs live under:
|
||||
|
||||
```txt
|
||||
open-sse/services/compression/rules/<language>/
|
||||
```
|
||||
|
||||
Current shipped packs include:
|
||||
|
||||
| Language | Directory |
|
||||
| ------------------- | -------------- |
|
||||
| English | `rules/en/` |
|
||||
| Portuguese (Brazil) | `rules/pt-BR/` |
|
||||
| Spanish | `rules/es/` |
|
||||
| German | `rules/de/` |
|
||||
| French | `rules/fr/` |
|
||||
| Japanese | `rules/ja/` |
|
||||
|
||||
## Language Detection
|
||||
|
||||
`languageDetector.ts` uses lightweight heuristics to infer the language from prompt text. The
|
||||
configured default language is still respected, and detection can be disabled by config when exact
|
||||
control is required.
|
||||
|
||||
Detection output is used only to choose rule packs. It does not change provider routing, locale
|
||||
selection, or UI language.
|
||||
|
||||
## Config Shape
|
||||
|
||||
Compression settings can include:
|
||||
|
||||
```json
|
||||
{
|
||||
"languageConfig": {
|
||||
"enabled": true,
|
||||
"defaultLanguage": "en",
|
||||
"autoDetect": true,
|
||||
"enabledPacks": ["en", "pt-BR", "es", "de", "fr", "ja"]
|
||||
},
|
||||
"cavemanConfig": {
|
||||
"language": "en",
|
||||
"autoDetectLanguage": true,
|
||||
"enabledLanguagePacks": ["en", "pt-BR", "es", "de", "fr", "ja"]
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`languageConfig` controls dashboard/preview defaults. `cavemanConfig` is the runtime engine config
|
||||
used when Caveman compresses message text.
|
||||
|
||||
## Adding a Language Pack
|
||||
|
||||
1. Create `open-sse/services/compression/rules/<language>/<pack>.json`.
|
||||
2. Use the Caveman rule format from `docs/COMPRESSION_RULES_FORMAT.md`.
|
||||
3. Keep replacements conservative and avoid changing code, identifiers, URLs, or JSON.
|
||||
4. Add or update tests for language selection and replacement behavior.
|
||||
5. Expose new dashboard/i18n labels if the language appears in UI selectors.
|
||||
|
||||
## API
|
||||
|
||||
Available packs can be queried with:
|
||||
|
||||
```bash
|
||||
curl http://localhost:20128/api/compression/language-packs
|
||||
```
|
||||
|
||||
The preview endpoint accepts language config overrides:
|
||||
|
||||
```bash
|
||||
curl -X POST http://localhost:20128/api/compression/preview \
|
||||
-H "Content-Type: application/json" \
|
||||
-d '{
|
||||
"mode": "standard",
|
||||
"text": "Por favor, eu gostaria que voce basicamente resumisse isso.",
|
||||
"config": {
|
||||
"languageConfig": {
|
||||
"defaultLanguage": "pt-BR",
|
||||
"autoDetect": true
|
||||
}
|
||||
}
|
||||
}'
|
||||
```
|
||||
|
||||
## Operational Notes
|
||||
|
||||
- English built-in rules remain the fallback when a language pack is missing.
|
||||
- Invalid built-in JSON packs fail validation so release assets do not silently degrade.
|
||||
- Rule packs are data-only and should not import code or run arbitrary logic.
|
||||
- The compression analytics layer records the selected mode and engine, not full prompt text.
|
||||
186
docs/COMPRESSION_RULES_FORMAT.md
Normal file
186
docs/COMPRESSION_RULES_FORMAT.md
Normal file
@@ -0,0 +1,186 @@
|
||||
# Compression Rules Format
|
||||
|
||||
Compression rules are JSON files loaded at runtime. They are intentionally data-only so new
|
||||
language packs and RTK command filters can be reviewed without changing engine code.
|
||||
|
||||
## Caveman Rule Packs
|
||||
|
||||
Caveman rule packs live under:
|
||||
|
||||
```txt
|
||||
open-sse/services/compression/rules/<language>/<pack>.json
|
||||
```
|
||||
|
||||
Each pack contains replacements that apply to normal prose after protected regions are isolated.
|
||||
|
||||
```json
|
||||
{
|
||||
"language": "en",
|
||||
"category": "filler",
|
||||
"rules": [
|
||||
{
|
||||
"name": "question_to_directive",
|
||||
"pattern": "\\b(?:Can you explain why|Could you show me how)\\b\\s*",
|
||||
"replacement": "Explain why ",
|
||||
"replacementMap": {
|
||||
"can you explain why": "Explain why ",
|
||||
"could you show me how": "Show how "
|
||||
},
|
||||
"flags": "gi",
|
||||
"context": "all",
|
||||
"category": "context",
|
||||
"minIntensity": "lite",
|
||||
"description": "Convert verbose questions into direct requests."
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### Caveman Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
| ------------------------ | -------- | ---------------------------------------------------------------- |
|
||||
| `language` | yes | BCP-47-like language key such as `en`, `pt-BR`, `es` |
|
||||
| `category` | yes | Pack category filename/category, for example `filler` or `dedup` |
|
||||
| `rules` | yes | Array of regex replacement rules |
|
||||
| `rules[].name` | yes | Stable rule name |
|
||||
| `rules[].pattern` | yes | JavaScript regex source |
|
||||
| `rules[].flags` | no | JavaScript regex flags; default `gi` |
|
||||
| `rules[].replacement` | no | Replacement string or fallback when `replacementMap` misses |
|
||||
| `rules[].replacementMap` | no | Match-specific replacements keyed by normalized matched text |
|
||||
| `rules[].context` | no | `all`, `user`, `assistant`, or `system`; default `all` |
|
||||
| `rules[].category` | no | `filler`, `context`, `structural`, `dedup`, `terse`, or `ultra` |
|
||||
| `rules[].minIntensity` | no | `lite`, `full`, or `ultra`; default `lite` |
|
||||
| `rules[].description` | no | Human-readable rule summary |
|
||||
|
||||
Use `flags` when case-sensitive matching matters, for example article removal before lowercase prose
|
||||
without stripping `the OpenAI API`. Use `replacementMap` when one regex has multiple alternatives
|
||||
that need different outputs; this keeps JSON rule packs data-only while preserving the behavior of
|
||||
the richer built-in TypeScript replacement functions.
|
||||
|
||||
## RTK Filter Packs
|
||||
|
||||
RTK filters live under:
|
||||
|
||||
```txt
|
||||
open-sse/services/compression/engines/rtk/filters/<filter>.json
|
||||
```
|
||||
|
||||
Each filter describes how to recognize and compress a command-output family.
|
||||
|
||||
```json
|
||||
{
|
||||
"id": "test-vitest",
|
||||
"label": "Vitest output",
|
||||
"category": "test",
|
||||
"priority": 92,
|
||||
"match": {
|
||||
"outputTypes": ["test-vitest"],
|
||||
"commands": ["vitest", "npm test", "npm run test"],
|
||||
"patterns": ["\\bFAIL\\b", "\\bPASS\\b", "\\bTest Files\\b"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"replace": [{ "pattern": "\\s+\\[[0-9]+ms\\]", "replacement": "" }],
|
||||
"matchOutput": [
|
||||
{ "pattern": "All tests passed", "message": "vitest: ok", "unless": "FAIL|Error:" }
|
||||
],
|
||||
"includePatterns": ["FAIL", "Error:", "Test Files", "Tests"],
|
||||
"dropPatterns": ["^\\s*$", "Duration\\s+\\d+"],
|
||||
"collapsePatterns": ["^\\s+at "],
|
||||
"deduplicate": true,
|
||||
"truncateLineAt": 240,
|
||||
"maxLines": 160,
|
||||
"headLines": 24,
|
||||
"tailLines": 40,
|
||||
"onEmpty": "vitest: ok",
|
||||
"filterStderr": false
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["FAIL", "Error:", "AssertionError"],
|
||||
"summaryPatterns": ["Test Files", "Tests", "Snapshots"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "keeps failing tests",
|
||||
"command": "vitest",
|
||||
"input": "FAIL test/a.test.ts\\nError: boom\\nTest Files 1 failed",
|
||||
"expected": "FAIL test/a.test.ts\\nError: boom\\nTest Files 1 failed"
|
||||
}
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
### RTK Fields
|
||||
|
||||
| Field | Required | Description |
|
||||
| -------------------------- | -------- | ------------------------------------------------------------------------------ |
|
||||
| `id` | yes | Stable filter id |
|
||||
| `label` | yes | Dashboard-readable name |
|
||||
| `category` | yes | Filter family: git, test, build, shell, docker, package, infra, cloud, generic |
|
||||
| `priority` | no | Higher priority wins when multiple filters match |
|
||||
| `match.outputTypes` | no | Detector output ids that select this filter |
|
||||
| `match.commands` | no | Command tokens that select this filter |
|
||||
| `match.patterns` | no | Regex patterns that select this filter from output text |
|
||||
| `rules.stripAnsi` | no | Remove ANSI escape sequences before regex stages |
|
||||
| `rules.replace` | no | Ordered regex substitutions applied line by line |
|
||||
| `rules.matchOutput` | no | Short-circuit output rules with optional `unless` guard |
|
||||
| `rules.includePatterns` | no | Lines to prefer preserving |
|
||||
| `rules.dropPatterns` | no | Lines to remove as noise |
|
||||
| `rules.collapsePatterns` | no | Repeated matching lines that can be collapsed |
|
||||
| `rules.deduplicate` | no | Collapse duplicate normalized lines |
|
||||
| `rules.truncateLineAt` | no | Unicode-safe per-line character limit |
|
||||
| `rules.maxLines` | no | Maximum retained lines before tail preservation |
|
||||
| `rules.headLines` | no | Head lines retained during truncation |
|
||||
| `rules.tailLines` | no | Tail lines retained for recent context |
|
||||
| `rules.onEmpty` | no | Fallback message when filtering removes all content |
|
||||
| `rules.filterStderr` | no | Normalize common stderr prefixes before later filtering stages |
|
||||
| `preserve.errorPatterns` | no | Error lines that should survive truncation |
|
||||
| `preserve.summaryPatterns` | no | Summary lines that should survive truncation |
|
||||
| `tests[]` | no | Inline verification samples used by the RTK verify gate |
|
||||
|
||||
RTK applies declarative stages in this order: `stripAnsi`, `filterStderr`, `replace`,
|
||||
`matchOutput`, `dropPatterns`/`includePatterns`, `truncateLineAt`, `headLines`/`tailLines`,
|
||||
`maxLines`, and `onEmpty`.
|
||||
|
||||
Custom filters can be loaded from:
|
||||
|
||||
1. Project `.rtk/filters.json` files only after a matching `.rtk/trust.json` hash is present or
|
||||
`trustProjectFilters` is enabled.
|
||||
2. Global `DATA_DIR/rtk/filters.json`.
|
||||
3. Built-in filters.
|
||||
|
||||
Project/global custom files may contain one filter object or an array of filter objects. Invalid
|
||||
custom filters are skipped with diagnostics; invalid built-in filters fail validation.
|
||||
|
||||
Project trust file:
|
||||
|
||||
```json
|
||||
{
|
||||
"filtersSha256": "0123456789abcdef..."
|
||||
}
|
||||
```
|
||||
|
||||
The environment override `OMNIROUTE_RTK_TRUST_PROJECT_FILTERS=1` trusts project filters without a
|
||||
hash and should be limited to controlled local development.
|
||||
|
||||
## Safety Rules
|
||||
|
||||
- Keep rules idempotent: running the same filter twice should not corrupt output.
|
||||
- Preserve exact error text, file paths, line numbers, and command summaries where possible.
|
||||
- Avoid rules that modify code blocks, JSON payloads, URLs, or secrets.
|
||||
- Add unit coverage for new command families in detector/filter tests.
|
||||
- Add `tests[]` samples to every built-in filter and to shared custom filters.
|
||||
|
||||
## Validation
|
||||
|
||||
Rule packs are validated before use. Built-in Caveman packs and built-in RTK filters fail fast
|
||||
during validation so broken release assets are caught before shipment. Custom RTK filters are
|
||||
skipped with diagnostics when parsing or trust validation fails.
|
||||
|
||||
Focused validation:
|
||||
|
||||
```bash
|
||||
node --import tsx/esm --test tests/unit/compression/rule-loader.test.ts tests/unit/compression/language-packs.test.ts
|
||||
node --import tsx/esm --test tests/unit/compression/rtk-verify.test.ts tests/unit/compression/rtk-dsl-pipeline.test.ts
|
||||
```
|
||||
@@ -497,6 +497,12 @@ The logging system writes to both stdout and rotated log files. All configuratio
|
||||
| `CONTEXT_LENGTH_DEFAULT` | `128000` | Global fallback max context length for models without explicit config. |
|
||||
| `USAGE_TOKEN_BUFFER` | `100` | Extra token headroom reserved when tracking usage quotas. |
|
||||
|
||||
### Compression
|
||||
|
||||
| Variable | Default | Description |
|
||||
| ------------------------------------- | ------- | ------------------------------------------------------------------------------------------------------------- |
|
||||
| `OMNIROUTE_RTK_TRUST_PROJECT_FILTERS` | unset | Trust project `.rtk/filters.json` without a `.rtk/trust.json` hash. Use only in controlled local development. |
|
||||
|
||||
### Low-RAM Docker Example
|
||||
|
||||
```bash
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# OmniRoute — Dashboard Features Gallery
|
||||
|
||||
🌐 **Languages:** 🇺🇸 [English](FEATURES.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/FEATURES.md) | 🇪🇸 [Español](i18n/es/FEATURES.md) | 🇫🇷 [Français](i18n/fr/FEATURES.md) | 🇮🇹 [Italiano](i18n/it/FEATURES.md) | 🇷🇺 [Русский](i18n/ru/FEATURES.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/FEATURES.md) | 🇩🇪 [Deutsch](i18n/de/FEATURES.md) | 🇮🇳 [हिन्दी](i18n/in/FEATURES.md) | 🇹🇭 [ไทย](i18n/th/FEATURES.md) | 🇺🇦 [Українська](i18n/uk-UA/FEATURES.md) | 🇸🇦 [العربية](i18n/ar/FEATURES.md) | 🇯🇵 [日本語](i18n/ja/FEATURES.md) | 🇻🇳 [Tiếng Việt](i18n/vi/FEATURES.md) | 🇧🇬 [Български](i18n/bg/FEATURES.md) | 🇩🇰 [Dansk](i18n/da/FEATURES.md) | 🇫🇮 [Suomi](i18n/fi/FEATURES.md) | 🇮🇱 [עברית](i18n/he/FEATURES.md) | 🇭🇺 [Magyar](i18n/hu/FEATURES.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/FEATURES.md) | 🇰🇷 [한국어](i18n/ko/FEATURES.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/FEATURES.md) | 🇳🇱 [Nederlands](i18n/nl/FEATURES.md) | 🇳🇴 [Norsk](i18n/no/FEATURES.md) | 🇵🇹 [Português (Portugal)](i18n/pt/FEATURES.md) | 🇷🇴 [Română](i18n/ro/FEATURES.md) | 🇵🇱 [Polski](i18n/pl/FEATURES.md) | 🇸🇰 [Slovenčina](i18n/sk/FEATURES.md) | 🇸🇪 [Svenska](i18n/sv/FEATURES.md) | 🇵🇭 [Filipino](i18n/phi/FEATURES.md) | 🇨🇿 [Čeština](i18n/cs/FEATURES.md)
|
||||
🌐 **Main README translations:** 🇺🇸 [English](../README.md) | 🇧🇷 [Português (Brasil)](i18n/pt-BR/README.md) | 🇪🇸 [Español](i18n/es/README.md) | 🇫🇷 [Français](i18n/fr/README.md) | 🇮🇹 [Italiano](i18n/it/README.md) | 🇷🇺 [Русский](i18n/ru/README.md) | 🇨🇳 [中文 (简体)](i18n/zh-CN/README.md) | 🇩🇪 [Deutsch](i18n/de/README.md) | 🇮🇳 [हिन्दी](i18n/in/README.md) | 🇹🇭 [ไทย](i18n/th/README.md) | 🇺🇦 [Українська](i18n/uk-UA/README.md) | 🇸🇦 [العربية](i18n/ar/README.md) | 🇯🇵 [日本語](i18n/ja/README.md) | 🇻🇳 [Tiếng Việt](i18n/vi/README.md) | 🇧🇬 [Български](i18n/bg/README.md) | 🇩🇰 [Dansk](i18n/da/README.md) | 🇫🇮 [Suomi](i18n/fi/README.md) | 🇮🇱 [עברית](i18n/he/README.md) | 🇭🇺 [Magyar](i18n/hu/README.md) | 🇮🇩 [Bahasa Indonesia](i18n/id/README.md) | 🇰🇷 [한국어](i18n/ko/README.md) | 🇲🇾 [Bahasa Melayu](i18n/ms/README.md) | 🇳🇱 [Nederlands](i18n/nl/README.md) | 🇳🇴 [Norsk](i18n/no/README.md) | 🇵🇹 [Português (Portugal)](i18n/pt/README.md) | 🇷🇴 [Română](i18n/ro/README.md) | 🇵🇱 [Polski](i18n/pl/README.md) | 🇸🇰 [Slovenčina](i18n/sk/README.md) | 🇸🇪 [Svenska](i18n/sv/README.md) | 🇵🇭 [Filipino](i18n/phi/README.md) | 🇨🇿 [Čeština](i18n/cs/README.md)
|
||||
|
||||
Visual guide to every section of the OmniRoute dashboard.
|
||||
|
||||
@@ -109,7 +109,21 @@ Configurable via combo-level or global settings:
|
||||
- **Max Messages For Summary** — How much recent history to condense
|
||||
- **Summary Model** — Optional override model for generating the handoff summary
|
||||
|
||||
Currently supports Codex account rotation. See [Context Relay documentation](features/context-relay.md).
|
||||
Currently supports Codex account rotation. See [Context Relay documentation](ARCHITECTURE.md).
|
||||
|
||||
---
|
||||
|
||||
## 🗜️ Prompt Compression _(v3.7.9+)_
|
||||
|
||||
Context & Cache now exposes dedicated pages for Caveman, RTK, and Compression Combos:
|
||||
|
||||
- **Caveman** — language-aware rule packs, preview, output-mode controls, and analytics
|
||||
- **RTK** — command-aware compression for shell, git, test, build, package, Docker, infra, JSON, and stack-trace output
|
||||
- **Compression Combos** — named pipelines such as `rtk -> caveman` assigned to routing combos; the default stacked math reaches `~89%` average and `78-95%` eligible-context savings when both engines apply
|
||||
- **Raw-output recovery** — optional redacted RTK raw-output pointers for debugging compressed failures
|
||||
|
||||
See [Compression Guide](COMPRESSION_GUIDE.md), [RTK Compression](RTK_COMPRESSION.md), and
|
||||
[Compression Engines](COMPRESSION_ENGINES.md).
|
||||
|
||||
---
|
||||
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
# OmniRoute MCP Server Documentation
|
||||
|
||||
> Model Context Protocol server with 16 intelligent tools
|
||||
> Model Context Protocol server with 37 tools across routing, cache, compression, memory, skills, and proxy operations
|
||||
|
||||
## Installation
|
||||
|
||||
@@ -19,7 +19,8 @@ omniroute --dev # MCP auto-starts on /mcp endpoint
|
||||
|
||||
## IDE Configuration
|
||||
|
||||
See [IDE Configs](integrations/ide-configs.md) for Antigravity, Cursor, Copilot, and Claude Desktop setup.
|
||||
See [MCP Client Configuration](SETUP_GUIDE.md#mcp-client-configuration) for Claude Desktop,
|
||||
Cursor, Cline, and compatible MCP client setup.
|
||||
|
||||
---
|
||||
|
||||
@@ -49,20 +50,55 @@ See [IDE Configs](integrations/ide-configs.md) for Antigravity, Cursor, Copilot,
|
||||
| `omniroute_explain_route` | Explain a past routing decision |
|
||||
| `omniroute_get_session_snapshot` | Full session state: costs, tokens, errors |
|
||||
|
||||
## Cache Tools (2)
|
||||
|
||||
| Tool | Description |
|
||||
| :---------------------- | :-------------------------------------------------- |
|
||||
| `omniroute_cache_stats` | Semantic cache, prompt-cache, and idempotency stats |
|
||||
| `omniroute_cache_flush` | Flush cache globally or by signature/model |
|
||||
|
||||
## Compression Tools (5)
|
||||
|
||||
| Tool | Description |
|
||||
| :---------------------------------- | :------------------------------------------------------------- |
|
||||
| `omniroute_compression_status` | Compression settings, analytics summary, and cache-aware stats |
|
||||
| `omniroute_compression_configure` | Configure compression mode, threshold, and runtime options |
|
||||
| `omniroute_set_compression_engine` | Set Caveman, RTK, or stacked compression mode and pipeline |
|
||||
| `omniroute_list_compression_combos` | List named compression combos and routing assignments |
|
||||
| `omniroute_compression_combo_stats` | Analytics grouped by compression combo and engine |
|
||||
|
||||
`omniroute_compression_status` reports MCP description compression separately under
|
||||
`analytics.mcpDescriptionCompression`. Those values are metadata-size estimates for MCP listable
|
||||
descriptions (`tools`, `prompts`, `resources`, and `resourceTemplates`); they are not provider usage
|
||||
receipts and are marked with `source: "mcp_metadata_estimate"`.
|
||||
|
||||
See [Compression Engines](COMPRESSION_ENGINES.md) and [RTK Compression](RTK_COMPRESSION.md) for
|
||||
the runtime compression model behind these tools.
|
||||
|
||||
## Other Tool Groups
|
||||
|
||||
The remaining MCP surface includes 1proxy tools, memory tools, and skill tools. The live source of
|
||||
truth is `open-sse/mcp-server/tools/` and `open-sse/mcp-server/schemas/tools.ts`.
|
||||
|
||||
## Authentication
|
||||
|
||||
MCP tools are authenticated via API key scopes. Each tool requires specific scopes:
|
||||
|
||||
| Scope | Tools |
|
||||
| :------------- | :----------------------------------------------- |
|
||||
| `read:health` | get_health, get_provider_metrics |
|
||||
| `read:combos` | list_combos, get_combo_metrics |
|
||||
| `write:combos` | switch_combo |
|
||||
| `read:quota` | check_quota |
|
||||
| `write:route` | route_request, simulate_route, test_combo |
|
||||
| `read:usage` | cost_report, get_session_snapshot, explain_route |
|
||||
| `write:config` | set_budget_guard, set_resilience_profile |
|
||||
| `read:models` | list_models_catalog, best_combo_for_task |
|
||||
| Scope | Tools |
|
||||
| :-------------------- | :------------------------------------------------------------------- |
|
||||
| `read:health` | get_health, get_provider_metrics |
|
||||
| `read:combos` | list_combos, get_combo_metrics |
|
||||
| `write:combos` | switch_combo |
|
||||
| `read:quota` | check_quota |
|
||||
| `write:route` | route_request, simulate_route, test_combo |
|
||||
| `read:usage` | cost_report, get_session_snapshot, explain_route |
|
||||
| `write:config` | set_budget_guard, set_resilience_profile |
|
||||
| `read:models` | list_models_catalog, best_combo_for_task |
|
||||
| `read:cache` | cache_stats |
|
||||
| `write:cache` | cache_flush |
|
||||
| `read:compression` | compression_status, list_compression_combos, compression_combo_stats |
|
||||
| `write:compression` | compression_configure, set_compression_engine |
|
||||
| `execute:completions` | route_request, test_combo |
|
||||
|
||||
## Audit Logging
|
||||
|
||||
@@ -74,10 +110,10 @@ Every tool call is logged to `mcp_tool_audit` with:
|
||||
|
||||
## Files
|
||||
|
||||
| File | Purpose |
|
||||
| :------------------------------------------- | :------------------------------------------ |
|
||||
| `open-sse/mcp-server/server.ts` | MCP server creation + 16 tool registrations |
|
||||
| `open-sse/mcp-server/transport.ts` | Stdio + HTTP transport |
|
||||
| `open-sse/mcp-server/auth.ts` | API key + scope validation |
|
||||
| `open-sse/mcp-server/audit.ts` | Tool call audit logging |
|
||||
| `open-sse/mcp-server/tools/advancedTools.ts` | 8 advanced tool handlers |
|
||||
| File | Purpose |
|
||||
| :------------------------------------------- | :------------------------------------------------ |
|
||||
| `open-sse/mcp-server/server.ts` | MCP server creation and scoped tool registrations |
|
||||
| `open-sse/mcp-server/transport.ts` | Stdio + HTTP transport |
|
||||
| `open-sse/mcp-server/auth.ts` | API key + scope validation |
|
||||
| `open-sse/mcp-server/audit.ts` | Tool call audit logging |
|
||||
| `open-sse/mcp-server/tools/advancedTools.ts` | 8 advanced tool handlers |
|
||||
|
||||
234
docs/RTK_COMPRESSION.md
Normal file
234
docs/RTK_COMPRESSION.md
Normal file
@@ -0,0 +1,234 @@
|
||||
# RTK Compression
|
||||
|
||||
RTK compression is OmniRoute's command-aware compression engine for terminal and tool output. It is
|
||||
designed for coding-agent sessions where most context growth comes from test logs, build output,
|
||||
package manager noise, shell transcripts, Docker output, git output, and stack traces.
|
||||
|
||||
RTK can run directly with `defaultMode: "rtk"` or as the first step in a stacked pipeline, usually:
|
||||
|
||||
```txt
|
||||
rtk -> caveman
|
||||
```
|
||||
|
||||
That order compresses noisy machine output first, then lets Caveman condense remaining prose.
|
||||
|
||||
Upstream RTK reports `60-90%` command-output savings. Its README sample session goes from
|
||||
`~118,000` standard tokens to `~23,900` RTK tokens, which is `79.7%` saved (`~80%`). OmniRoute uses
|
||||
that upstream average for the stacked savings calculation with Caveman input compression:
|
||||
|
||||
```txt
|
||||
RTK average: 80% saved
|
||||
Caveman input: 46% saved
|
||||
Stacked: 1 - (1 - 0.80) * (1 - 0.46) = 89.2% saved
|
||||
Range: 1 - (1 - 0.60..0.90) * (1 - 0.46) = 78.4-94.6%
|
||||
```
|
||||
|
||||
## What It Compresses
|
||||
|
||||
The built-in catalog currently ships 49 filters across these categories:
|
||||
|
||||
| Category | Examples |
|
||||
| --------- | ------------------------------------------------------------- |
|
||||
| `git` | `git status`, `git branch`, `git diff`, `git log` |
|
||||
| `test` | Vitest, Jest, Pytest, Playwright, Go tests, Cargo tests |
|
||||
| `build` | TypeScript, ESLint, Biome, Prettier, Vite, Webpack, Turbo, Nx |
|
||||
| `package` | `npm install`, `npm audit`, `pip`, `uv sync`, Poetry, Bundler |
|
||||
| `shell` | `ls`, `find`, `grep`, generic shell logs |
|
||||
| `docker` | `docker ps`, Docker logs |
|
||||
| `infra` | Terraform, OpenTofu, `systemctl status` |
|
||||
| `generic` | JSON output, stack traces, generic output fallback |
|
||||
|
||||
The detector in `open-sse/services/compression/engines/rtk/commandDetector.ts` classifies output
|
||||
before filter selection. Filters can also match by command pattern or output regex when a command
|
||||
class is not enough.
|
||||
|
||||
## Filter Resolution
|
||||
|
||||
RTK loads filters in this order:
|
||||
|
||||
1. Project filters from `.rtk/filters.json`, only when trusted.
|
||||
2. Global filters from `DATA_DIR/rtk/filters.json`.
|
||||
3. Built-in filters from `open-sse/services/compression/engines/rtk/filters/`.
|
||||
|
||||
Project filters are intentionally trust-gated because regex filters can change how tool output is
|
||||
shown to agents. A project filter file is accepted when one of these is true:
|
||||
|
||||
- `rtkConfig.trustProjectFilters` is `true`.
|
||||
- `OMNIROUTE_RTK_TRUST_PROJECT_FILTERS=1` is set.
|
||||
- `.rtk/trust.json` contains the SHA-256 hash of `.rtk/filters.json`.
|
||||
|
||||
Trust file example:
|
||||
|
||||
```json
|
||||
{
|
||||
"filtersSha256": "0123456789abcdef..."
|
||||
}
|
||||
```
|
||||
|
||||
Custom filters can be one filter object or an array of filter objects. Invalid custom filters are
|
||||
skipped and reported by `/api/context/rtk/filters` diagnostics. Invalid built-in filters fail fast.
|
||||
|
||||
## Filter DSL
|
||||
|
||||
Filters use the JSON schema described in [Compression Rules Format](COMPRESSION_RULES_FORMAT.md).
|
||||
The runtime applies these stages in order:
|
||||
|
||||
```txt
|
||||
stripAnsi -> filterStderr -> replace -> matchOutput -> drop/include lines
|
||||
-> truncateLineAt -> head/tail/maxLines -> onEmpty
|
||||
```
|
||||
|
||||
Important fields:
|
||||
|
||||
| Field | Purpose |
|
||||
| ---------------------------- | -------------------------------------------------------------- |
|
||||
| `rules.stripAnsi` | Remove terminal color/control sequences before matching |
|
||||
| `rules.filterStderr` | Normalize common stderr prefixes before matching/filtering |
|
||||
| `rules.replace` | Apply ordered regex replacements |
|
||||
| `rules.matchOutput` | Return a compact summary when output matches a known condition |
|
||||
| `rules.matchOutput[].unless` | Skip the shortcut when an error/failure pattern is present |
|
||||
| `rules.dropPatterns` | Remove noisy lines |
|
||||
| `rules.includePatterns` | Prefer actionable lines |
|
||||
| `rules.collapsePatterns` | Collapse repeated matching lines |
|
||||
| `rules.truncateLineAt` | Unicode-safe per-line truncation |
|
||||
| `rules.onEmpty` | Fallback message if all lines are filtered out |
|
||||
| `tests[]` | Inline samples used by the verify gate |
|
||||
|
||||
Built-in filters are expected to include inline `tests[]` samples. Custom filters should include
|
||||
them too, especially when they are shared across projects.
|
||||
|
||||
## Configuration
|
||||
|
||||
Global settings are available through `/api/settings/compression`. RTK-specific settings are also
|
||||
available through `/api/context/rtk/config`.
|
||||
|
||||
```json
|
||||
{
|
||||
"defaultMode": "stacked",
|
||||
"autoTriggerMode": "stacked",
|
||||
"autoTriggerTokens": 32000,
|
||||
"stackedPipeline": [
|
||||
{ "engine": "rtk", "intensity": "standard" },
|
||||
{ "engine": "caveman", "intensity": "full" }
|
||||
],
|
||||
"rtkConfig": {
|
||||
"enabled": true,
|
||||
"intensity": "standard",
|
||||
"applyToToolResults": true,
|
||||
"applyToCodeBlocks": false,
|
||||
"applyToAssistantMessages": false,
|
||||
"enabledFilters": [],
|
||||
"disabledFilters": [],
|
||||
"maxLinesPerResult": 120,
|
||||
"maxCharsPerResult": 12000,
|
||||
"deduplicateThreshold": 3,
|
||||
"customFiltersEnabled": true,
|
||||
"trustProjectFilters": false,
|
||||
"rawOutputRetention": "never",
|
||||
"rawOutputMaxBytes": 1048576
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
`enabledFilters` and `disabledFilters` use filter ids, for example `test-vitest` or `git-diff`.
|
||||
|
||||
## API
|
||||
|
||||
| Route | Method | Purpose |
|
||||
| ---------------------------------- | ------ | -------------------------------------------- |
|
||||
| `/api/context/rtk/config` | GET | Read RTK config |
|
||||
| `/api/context/rtk/config` | PUT | Update RTK config |
|
||||
| `/api/context/rtk/filters` | GET | List filter catalog and load diagnostics |
|
||||
| `/api/context/rtk/test` | POST | Preview RTK compression for one text payload |
|
||||
| `/api/context/rtk/raw-output/[id]` | GET | Read retained redacted raw output |
|
||||
| `/api/compression/preview` | POST | Preview any compression mode |
|
||||
|
||||
RTK test payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"command": "npm test",
|
||||
"text": "FAIL tests/example.test.ts\nAssertionError: expected true\nTest Files 1 failed",
|
||||
"config": {
|
||||
"intensity": "standard"
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Compression preview payload:
|
||||
|
||||
```json
|
||||
{
|
||||
"mode": "stacked",
|
||||
"messages": [
|
||||
{
|
||||
"role": "tool",
|
||||
"content": "FAIL tests/example.test.ts\nAssertionError: expected true\nTest Files 1 failed"
|
||||
}
|
||||
],
|
||||
"config": {
|
||||
"rtkConfig": {
|
||||
"rawOutputRetention": "failures"
|
||||
}
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
Management routes require dashboard management auth or the matching API-key policy.
|
||||
|
||||
## Raw Output Recovery
|
||||
|
||||
RTK normally returns only compressed text. For debugging, `rawOutputRetention` can retain redacted
|
||||
raw output:
|
||||
|
||||
| Value | Behavior |
|
||||
| ---------- | ------------------------------------------------------- |
|
||||
| `never` | Do not retain raw output |
|
||||
| `failures` | Retain only likely failure output |
|
||||
| `always` | Retain every compressed RTK raw output, after redaction |
|
||||
|
||||
Retained files are written under:
|
||||
|
||||
```txt
|
||||
DATA_DIR/rtk/raw-output/
|
||||
```
|
||||
|
||||
Secrets are redacted before persistence, including common bearer tokens, API keys, Slack tokens,
|
||||
AWS access keys, and assignment-style `token=...`, `secret=...`, `password=...` values. Analytics
|
||||
stores only the pointer id, size, and hash metadata.
|
||||
|
||||
## Verify Gate
|
||||
|
||||
The focused verify gate runs built-in inline filter tests without shelling out to external commands:
|
||||
|
||||
```bash
|
||||
node --import tsx/esm --test tests/unit/compression/rtk-verify.test.ts
|
||||
```
|
||||
|
||||
The broader RTK gate is:
|
||||
|
||||
```bash
|
||||
node --import tsx/esm --test \
|
||||
tests/unit/compression/rtk-*.test.ts \
|
||||
tests/unit/compression/pipeline-integration.test.ts \
|
||||
tests/unit/compression/context-compression-api.test.ts
|
||||
```
|
||||
|
||||
Run the broad compression gate before release:
|
||||
|
||||
```bash
|
||||
node --import tsx/esm --test \
|
||||
tests/unit/compression/*.test.ts \
|
||||
tests/golden-set/*.test.ts \
|
||||
tests/integration/compression-pipeline.test.ts \
|
||||
tests/unit/api/compression/compression-api.test.ts
|
||||
```
|
||||
|
||||
## Extending RTK
|
||||
|
||||
1. Add or update a filter JSON file.
|
||||
2. Include at least one `tests[]` sample that proves the important behavior.
|
||||
3. Add a fixture under `tests/unit/compression/fixtures/rtk/` for new command families.
|
||||
4. Add command detection coverage when introducing a new output class.
|
||||
5. Run the verify and broad RTK gates.
|
||||
6. If the filter is project-local, commit `.rtk/filters.json` and refresh `.rtk/trust.json` only after review.
|
||||
@@ -138,7 +138,7 @@ Add to your MCP settings:
|
||||
}
|
||||
```
|
||||
|
||||
**Full MCP documentation:** [MCP Server README](../open-sse/mcp-server/README.md) — 29 tools, IDE configs, Python/TS/Go clients.
|
||||
**Full MCP documentation:** [MCP Server README](../open-sse/mcp-server/README.md) — 37 tools, IDE configs, Python/TS/Go clients.
|
||||
|
||||
### A2A Setup (Agent-to-Agent Protocol)
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
openapi: 3.1.0
|
||||
info:
|
||||
title: OmniRoute API
|
||||
version: 3.7.8
|
||||
version: 3.7.9
|
||||
description: |
|
||||
OmniRoute is a local-first AI API proxy router. It provides an OpenAI-compatible
|
||||
endpoint that routes requests to multiple AI providers with load balancing,
|
||||
@@ -51,6 +51,8 @@ tags:
|
||||
description: Routing combo management
|
||||
- name: Settings
|
||||
description: Application settings
|
||||
- name: Compression
|
||||
description: Prompt compression, RTK filters, Caveman rules, and compression combos
|
||||
- name: Usage
|
||||
description: Usage analytics and logs
|
||||
- name: Translator
|
||||
@@ -734,6 +736,202 @@ paths:
|
||||
"200":
|
||||
description: Updated settings
|
||||
|
||||
/api/settings/compression:
|
||||
get:
|
||||
tags: [Compression]
|
||||
summary: Get global compression settings
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Current compression settings
|
||||
put:
|
||||
tags: [Compression]
|
||||
summary: Update global compression settings
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
defaultMode:
|
||||
type: string
|
||||
enum: [off, lite, standard, aggressive, ultra, rtk, stacked]
|
||||
autoTriggerMode:
|
||||
type: string
|
||||
enum: [off, lite, standard, aggressive, ultra, rtk, stacked]
|
||||
autoTriggerTokens:
|
||||
type: integer
|
||||
minimum: 0
|
||||
rtkConfig:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
stackedPipeline:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
responses:
|
||||
"200":
|
||||
description: Updated compression settings
|
||||
|
||||
/api/compression/preview:
|
||||
post:
|
||||
tags: [Compression]
|
||||
summary: Preview compression for a message payload
|
||||
security:
|
||||
- BearerAuth: []
|
||||
- ManagementSessionAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [messages, mode]
|
||||
properties:
|
||||
mode:
|
||||
type: string
|
||||
enum: [off, lite, standard, aggressive, ultra, rtk, stacked]
|
||||
messages:
|
||||
type: array
|
||||
items:
|
||||
type: object
|
||||
required: [role, content]
|
||||
properties:
|
||||
role:
|
||||
type: string
|
||||
content:
|
||||
oneOf:
|
||||
- type: string
|
||||
- type: array
|
||||
items: {}
|
||||
config:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
responses:
|
||||
"200":
|
||||
description: Compression preview with diff, validation, and stats
|
||||
|
||||
/api/compression/language-packs:
|
||||
get:
|
||||
tags: [Compression]
|
||||
summary: List Caveman compression language packs
|
||||
security:
|
||||
- BearerAuth: []
|
||||
- ManagementSessionAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Available languages and rule-pack metadata
|
||||
|
||||
/api/compression/rules:
|
||||
get:
|
||||
tags: [Compression]
|
||||
summary: List Caveman compression rule metadata
|
||||
security:
|
||||
- BearerAuth: []
|
||||
- ManagementSessionAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Caveman rule metadata
|
||||
|
||||
/api/context/rtk/config:
|
||||
get:
|
||||
tags: [Compression]
|
||||
summary: Get RTK compression settings
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: Current RTK config
|
||||
put:
|
||||
tags: [Compression]
|
||||
summary: Update RTK compression settings
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
properties:
|
||||
enabled:
|
||||
type: boolean
|
||||
intensity:
|
||||
type: string
|
||||
enum: [minimal, standard, aggressive]
|
||||
customFiltersEnabled:
|
||||
type: boolean
|
||||
trustProjectFilters:
|
||||
type: boolean
|
||||
rawOutputRetention:
|
||||
type: string
|
||||
enum: [never, failures, always]
|
||||
rawOutputMaxBytes:
|
||||
type: integer
|
||||
responses:
|
||||
"200":
|
||||
description: Updated RTK config
|
||||
|
||||
/api/context/rtk/filters:
|
||||
get:
|
||||
tags: [Compression]
|
||||
summary: List RTK filters and load diagnostics
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
responses:
|
||||
"200":
|
||||
description: RTK filter catalog and diagnostics
|
||||
|
||||
/api/context/rtk/test:
|
||||
post:
|
||||
tags: [Compression]
|
||||
summary: Run RTK compression preview for text
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
requestBody:
|
||||
required: true
|
||||
content:
|
||||
application/json:
|
||||
schema:
|
||||
type: object
|
||||
required: [text]
|
||||
properties:
|
||||
text:
|
||||
type: string
|
||||
command:
|
||||
type: string
|
||||
config:
|
||||
type: object
|
||||
additionalProperties: true
|
||||
responses:
|
||||
"200":
|
||||
description: Detection and RTK compression result
|
||||
|
||||
/api/context/rtk/raw-output/{id}:
|
||||
get:
|
||||
tags: [Compression]
|
||||
summary: Read retained redacted RTK raw output
|
||||
security:
|
||||
- ManagementSessionAuth: []
|
||||
parameters:
|
||||
- in: path
|
||||
name: id
|
||||
required: true
|
||||
schema:
|
||||
type: string
|
||||
pattern: "^[a-f0-9]{24}$"
|
||||
responses:
|
||||
"200":
|
||||
description: Raw output text
|
||||
"404":
|
||||
description: Raw output not found
|
||||
|
||||
/api/settings/payload-rules:
|
||||
get:
|
||||
tags: [Settings]
|
||||
|
||||
4
electron/package-lock.json
generated
4
electron/package-lock.json
generated
@@ -1,12 +1,12 @@
|
||||
{
|
||||
"name": "omniroute-desktop",
|
||||
"version": "3.7.8",
|
||||
"version": "3.7.9",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "omniroute-desktop",
|
||||
"version": "3.7.8",
|
||||
"version": "3.7.9",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"better-sqlite3": "^12.8.0",
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "omniroute-desktop",
|
||||
"version": "3.7.8",
|
||||
"version": "3.7.9",
|
||||
"description": "OmniRoute Desktop Application",
|
||||
"main": "main.js",
|
||||
"author": {
|
||||
|
||||
4
llm.txt
4
llm.txt
@@ -8,7 +8,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
|
||||
**Key value:** One endpoint (`http://localhost:20128/v1`), unlimited models, zero downtime, minimal cost.
|
||||
|
||||
**Current version:** 3.7.7
|
||||
**Current version:** 3.7.9
|
||||
|
||||
## Tech Stack
|
||||
|
||||
@@ -279,7 +279,7 @@ OmniRoute solves the problem of managing multiple AI provider subscriptions, quo
|
||||
└── .env.example # Environment variable template
|
||||
```
|
||||
|
||||
## Key Features (v3.7.7)
|
||||
## Key Features (v3.7.9)
|
||||
|
||||
### Core Proxy
|
||||
- **160+ AI providers** with automatic format translation
|
||||
|
||||
103
next.config.mjs
103
next.config.mjs
@@ -8,7 +8,7 @@ const projectRoot = dirname(fileURLToPath(import.meta.url));
|
||||
const scriptSrc =
|
||||
process.env.NODE_ENV === "development"
|
||||
? "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:"
|
||||
: "script-src 'self' 'unsafe-inline' blob:";
|
||||
: "script-src 'self' 'unsafe-inline' 'unsafe-eval' blob:";
|
||||
const contentSecurityPolicy = [
|
||||
"default-src 'self'",
|
||||
"base-uri 'self'",
|
||||
@@ -79,9 +79,13 @@ const nextConfig = {
|
||||
},
|
||||
outputFileTracingRoot: projectRoot,
|
||||
outputFileTracingIncludes: {
|
||||
// Migration SQL files are read via fs.readFileSync at runtime and are NOT
|
||||
// auto-traced by webpack/turbopack — include them explicitly.
|
||||
"/*": ["./src/lib/db/migrations/**/*"],
|
||||
// Migration SQL and compression rule/filter JSON files are read via fs at
|
||||
// runtime and are NOT always auto-traced by webpack/turbopack.
|
||||
"/*": [
|
||||
"./src/lib/db/migrations/**/*",
|
||||
"./open-sse/services/compression/engines/rtk/filters/**/*.json",
|
||||
"./open-sse/services/compression/rules/**/*.json",
|
||||
],
|
||||
},
|
||||
outputFileTracingExcludes: {
|
||||
// Planning/task docs are not runtime assets and can break standalone copies
|
||||
@@ -136,97 +140,6 @@ const nextConfig = {
|
||||
images: {
|
||||
unoptimized: true,
|
||||
},
|
||||
webpack: (config, { isServer, webpack }) => {
|
||||
if (isServer) {
|
||||
// Webpack IgnorePlugin: skip thread-stream test files that contain
|
||||
// intentionally broken syntax/imports (they cause Turbopack build errors)
|
||||
config.plugins.push(
|
||||
new webpack.IgnorePlugin({
|
||||
resourceRegExp: /\/test\//,
|
||||
contextRegExp: /thread-stream/,
|
||||
})
|
||||
);
|
||||
|
||||
// ── Turbopack / Next.js 16 module-hash patch (#394, #396, #398) ────────
|
||||
//
|
||||
// Next.js 16 (with or without Turbopack) compiles the instrumentation hook
|
||||
// into a separate chunk and emits hashed require() calls such as:
|
||||
// require('better-sqlite3-90e2652d1716b047')
|
||||
// require('zod-dcb22c6336e0bc69')
|
||||
// require('pino-28069d5257187539')
|
||||
//
|
||||
// These hashed names don't exist in node_modules and cause a 500 at
|
||||
// startup on all npm global installs (issues #394, #396, #398).
|
||||
//
|
||||
// We use two strategies:
|
||||
// 1. Exact-name externals for all known server-side packages.
|
||||
// 2. Hash-strip catch-all: any require('<name>-<16hexchars>[/subpath]')
|
||||
// strips the hash suffix and falls through to the real package name.
|
||||
//
|
||||
const HASH_PATTERN = /^(.+)-[0-9a-f]{16}(\/.*)?$/;
|
||||
|
||||
const KNOWN_EXTERNALS = new Set([
|
||||
"better-sqlite3",
|
||||
"keytar",
|
||||
"@ngrok/ngrok",
|
||||
"wreq-js",
|
||||
"zod",
|
||||
"pino",
|
||||
"pino-pretty",
|
||||
"pino-abstract-transport",
|
||||
"child_process",
|
||||
"fs",
|
||||
"path",
|
||||
"os",
|
||||
"crypto",
|
||||
"net",
|
||||
"tls",
|
||||
"http",
|
||||
"https",
|
||||
"stream",
|
||||
"buffer",
|
||||
"util",
|
||||
"process",
|
||||
]);
|
||||
|
||||
const prev = config.externals ?? [];
|
||||
const prevArr = Array.isArray(prev) ? prev : [prev];
|
||||
config.externals = [
|
||||
...prevArr,
|
||||
({ request }, callback) => {
|
||||
// Case 1: Exact known package — treat as external
|
||||
if (KNOWN_EXTERNALS.has(request)) {
|
||||
return callback(null, `commonjs ${request}`);
|
||||
}
|
||||
// Case 2: Hash-suffixed name — strip hash, preserve subpath
|
||||
// e.g. "better-sqlite3-90e2652d1716b047" → "better-sqlite3"
|
||||
// "zod-dcb22c6336e0bc69" → "zod"
|
||||
// "zod-dcb22c6336e0bc69/v3" → "zod/v3"
|
||||
// "zod-dcb22c6336e0bc69/v4-mini" → "zod/v4-mini"
|
||||
const hashMatch = request?.match?.(HASH_PATTERN);
|
||||
if (hashMatch) {
|
||||
const resolved = hashMatch[2] ? `${hashMatch[1]}${hashMatch[2]}` : hashMatch[1];
|
||||
return callback(null, `commonjs ${resolved}`);
|
||||
}
|
||||
callback();
|
||||
},
|
||||
];
|
||||
} else {
|
||||
// Ignore native Node.js modules in browser bundle
|
||||
config.resolve.fallback = {
|
||||
...config.resolve.fallback,
|
||||
fs: false,
|
||||
path: false,
|
||||
child_process: false,
|
||||
net: false,
|
||||
tls: false,
|
||||
crypto: false,
|
||||
process: false,
|
||||
os: false,
|
||||
};
|
||||
}
|
||||
return config;
|
||||
},
|
||||
|
||||
async headers() {
|
||||
return [
|
||||
|
||||
@@ -173,13 +173,59 @@ export const EMBEDDING_PROVIDERS: Record<string, EmbeddingProvider> = {
|
||||
{ id: "text-embedding-3-large", name: "Text Embedding 3 Large (GitHub)", dimensions: 3072 },
|
||||
],
|
||||
},
|
||||
|
||||
"jina-ai": {
|
||||
id: "jina-ai",
|
||||
baseUrl: "https://api.jina.ai/v1/embeddings",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
models: [
|
||||
{
|
||||
id: "jina-embeddings-v5-text-small",
|
||||
name: "Jina Embeddings v5 Text Small",
|
||||
dimensions: 1024,
|
||||
},
|
||||
{ id: "jina-embeddings-v5-text-nano", name: "Jina Embeddings v5 Text Nano", dimensions: 768 },
|
||||
{ id: "jina-code-embeddings-1.5b", name: "Jina Code Embeddings 1.5B", dimensions: 1536 },
|
||||
{ id: "jina-code-embeddings-0.5b", name: "Jina Code Embeddings 0.5B", dimensions: 896 },
|
||||
{ id: "jina-embeddings-v4", name: "Jina Embeddings v4", dimensions: 2048 },
|
||||
{ id: "jina-clip-v2", name: "Jina CLIP v2", dimensions: 1024 },
|
||||
{ id: "jina-colbert-v2", name: "Jina ColBERT v2", dimensions: 128 },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const EMBEDDING_PROVIDER_ALIASES: Record<string, string> = {
|
||||
jina: "jina-ai",
|
||||
voyage: "voyage-ai",
|
||||
};
|
||||
|
||||
function resolveEmbeddingProviderId(providerId: string): string {
|
||||
return EMBEDDING_PROVIDER_ALIASES[providerId] || providerId;
|
||||
}
|
||||
|
||||
function normalizeProviderScopedModelId(providerId: string, modelId: string): string {
|
||||
const resolvedProvider = resolveEmbeddingProviderId(providerId);
|
||||
const provider = EMBEDDING_PROVIDERS[resolvedProvider];
|
||||
if (provider?.models.some((model) => model.id === modelId)) return modelId;
|
||||
|
||||
const providerScopedModelId = `${resolvedProvider}/${modelId}`;
|
||||
if (provider?.models.some((model) => model.id === providerScopedModelId)) {
|
||||
return providerScopedModelId;
|
||||
}
|
||||
|
||||
return modelId.startsWith(`${providerId}/`) ? modelId.slice(providerId.length + 1) : modelId;
|
||||
}
|
||||
|
||||
function toProviderScopedModelId(providerId: string, modelId: string): string {
|
||||
return modelId.startsWith(`${providerId}/`) ? modelId : `${providerId}/${modelId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get embedding provider config by ID
|
||||
*/
|
||||
export function getEmbeddingProvider(providerId: string): EmbeddingProvider | null {
|
||||
return EMBEDDING_PROVIDERS[providerId] || null;
|
||||
return EMBEDDING_PROVIDERS[resolveEmbeddingProviderId(providerId)] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -195,10 +241,23 @@ export function parseEmbeddingModel(
|
||||
// Check for "provider/model" format
|
||||
const slashIdx = modelStr.indexOf("/");
|
||||
if (slashIdx > 0) {
|
||||
const rawProvider = modelStr.slice(0, slashIdx);
|
||||
const resolvedProvider = resolveEmbeddingProviderId(rawProvider);
|
||||
|
||||
if (EMBEDDING_PROVIDERS[resolvedProvider]) {
|
||||
return {
|
||||
provider: resolvedProvider,
|
||||
model: normalizeProviderScopedModelId(resolvedProvider, modelStr.slice(slashIdx + 1)),
|
||||
};
|
||||
}
|
||||
|
||||
// Phase 1: Try each hardcoded provider prefix
|
||||
for (const [providerId] of Object.entries(EMBEDDING_PROVIDERS)) {
|
||||
if (modelStr.startsWith(providerId + "/")) {
|
||||
return { provider: providerId, model: modelStr.slice(providerId.length + 1) };
|
||||
return {
|
||||
provider: providerId,
|
||||
model: normalizeProviderScopedModelId(providerId, modelStr.slice(providerId.length + 1)),
|
||||
};
|
||||
}
|
||||
}
|
||||
// Phase 2: Try dynamic provider_nodes prefix
|
||||
@@ -233,7 +292,7 @@ export function getAllEmbeddingModels() {
|
||||
for (const [providerId, config] of Object.entries(EMBEDDING_PROVIDERS)) {
|
||||
for (const model of config.models) {
|
||||
models.push({
|
||||
id: `${providerId}/${model.id}`,
|
||||
id: toProviderScopedModelId(providerId, model.id),
|
||||
name: model.name,
|
||||
provider: providerId,
|
||||
dimensions: model.dimensions,
|
||||
|
||||
@@ -437,6 +437,20 @@ export const IMAGE_PROVIDERS: Record<string, ImageProviderConfig> = {
|
||||
models: [{ id: "topaz-enhance", name: "topaz-enhance", inputModalities: ["image"] }],
|
||||
supportedSizes: ["1024x1024"],
|
||||
},
|
||||
nanogpt: {
|
||||
id: "nanogpt",
|
||||
baseUrl: "https://nano-gpt.com/api/v1/images/generations",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
format: "openai",
|
||||
models: [
|
||||
{ id: "qwen-image", name: "Qwen Image", inputModalities: ["text", "image"] },
|
||||
{ id: "z-image-turbo", name: "Z Image Turbo" },
|
||||
{ id: "chroma", name: "Chroma" },
|
||||
{ id: "hidream", name: "Hidream I1 Full" },
|
||||
],
|
||||
supportedSizes: ["1024x1024", "1024x1280", "1280x1024"],
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
|
||||
@@ -672,17 +672,39 @@ export const REGISTRY: Record<string, RegistryEntry> = {
|
||||
agentrouter: {
|
||||
id: "agentrouter",
|
||||
alias: "agentrouter",
|
||||
format: "openai",
|
||||
format: "claude",
|
||||
executor: "default",
|
||||
baseUrl: "https://agentrouter.org/v1/chat/completions",
|
||||
baseUrl: "https://agentrouter.org/v1/messages",
|
||||
authType: "apikey",
|
||||
authHeader: "bearer",
|
||||
authHeader: "x-api-key",
|
||||
defaultContextLength: 128000,
|
||||
headers: {
|
||||
"HTTP-Referer": "https://endpoint-proxy.local",
|
||||
"X-Title": "OmniRoute",
|
||||
"Anthropic-Version": ANTHROPIC_VERSION_HEADER,
|
||||
"Anthropic-Beta": ANTHROPIC_BETA_CLAUDE_OAUTH,
|
||||
"Anthropic-Dangerous-Direct-Browser-Access": "true",
|
||||
"User-Agent": CLAUDE_CLI_USER_AGENT,
|
||||
"X-App": "cli",
|
||||
"X-Stainless-Helper-Method": "stream",
|
||||
"X-Stainless-Retry-Count": "0",
|
||||
"X-Stainless-Runtime-Version": CLAUDE_CLI_STAINLESS_RUNTIME_VERSION,
|
||||
"X-Stainless-Package-Version": CLAUDE_CLI_STAINLESS_PACKAGE_VERSION,
|
||||
"X-Stainless-Runtime": "node",
|
||||
"X-Stainless-Lang": "js",
|
||||
"X-Stainless-Arch": mapStainlessArch(),
|
||||
"X-Stainless-Os": mapStainlessOs(),
|
||||
"X-Stainless-Timeout": "600",
|
||||
},
|
||||
models: [{ id: "auto", name: "Auto (Best Available)" }],
|
||||
models: [
|
||||
{ id: "claude-haiku-4-5-20251001", name: "Claude 4.5 Haiku" },
|
||||
{ id: "claude-opus-4-6", name: "Claude 4.6 Opus" },
|
||||
{ id: "deepseek-r1-0528", name: "DeepSeek R1 0528" },
|
||||
{ id: "deepseek-v3.1", name: "DeepSeek V3.1" },
|
||||
{ id: "deepseek-v3.2", name: "DeepSeek V3.2" },
|
||||
{ id: "glm-4.5", name: "GLM 4.5" },
|
||||
{ id: "glm-4.6", name: "GLM 4.6" },
|
||||
{ id: "glm-5.1", name: "GLM 5.1" },
|
||||
],
|
||||
passthroughModels: true,
|
||||
},
|
||||
|
||||
openrouter: {
|
||||
|
||||
@@ -69,20 +69,41 @@ export const RERANK_PROVIDERS = {
|
||||
models: [
|
||||
{ id: "jina-reranker-v3", name: "Jina Reranker v3" },
|
||||
{ id: "jina-reranker-m0", name: "Jina Reranker m0" },
|
||||
{
|
||||
id: "jina-reranker-v2-base-multilingual",
|
||||
name: "Jina Reranker v2 Base Multilingual",
|
||||
},
|
||||
{ id: "jina-colbert-v2", name: "Jina ColBERT v2" },
|
||||
],
|
||||
},
|
||||
};
|
||||
|
||||
const RERANK_PROVIDER_ALIASES = {
|
||||
jina: "jina-ai",
|
||||
voyage: "voyage-ai",
|
||||
};
|
||||
|
||||
function resolveRerankProviderId(providerId) {
|
||||
return RERANK_PROVIDER_ALIASES[providerId] || providerId;
|
||||
}
|
||||
|
||||
function normalizeProviderScopedModelId(providerId, modelId) {
|
||||
const resolvedProvider = resolveRerankProviderId(providerId);
|
||||
const provider = RERANK_PROVIDERS[resolvedProvider];
|
||||
if (provider?.models.some((model) => model.id === modelId)) return modelId;
|
||||
|
||||
const providerScopedModelId = `${resolvedProvider}/${modelId}`;
|
||||
if (provider?.models.some((model) => model.id === providerScopedModelId)) {
|
||||
return providerScopedModelId;
|
||||
}
|
||||
|
||||
return modelId.startsWith(`${providerId}/`) ? modelId.slice(providerId.length + 1) : modelId;
|
||||
}
|
||||
|
||||
function toProviderScopedModelId(providerId, modelId) {
|
||||
return modelId.startsWith(`${providerId}/`) ? modelId : `${providerId}/${modelId}`;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get rerank provider config by ID
|
||||
*/
|
||||
export function getRerankProvider(providerId) {
|
||||
return RERANK_PROVIDERS[providerId] || null;
|
||||
return RERANK_PROVIDERS[resolveRerankProviderId(providerId)] || null;
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -92,10 +113,25 @@ export function getRerankProvider(providerId) {
|
||||
export function parseRerankModel(modelStr) {
|
||||
if (!modelStr) return { provider: null, model: null };
|
||||
|
||||
const slashIdx = modelStr.indexOf("/");
|
||||
if (slashIdx > 0) {
|
||||
const rawProvider = modelStr.slice(0, slashIdx);
|
||||
const resolvedProvider = resolveRerankProviderId(rawProvider);
|
||||
if (RERANK_PROVIDERS[resolvedProvider]) {
|
||||
return {
|
||||
provider: resolvedProvider,
|
||||
model: normalizeProviderScopedModelId(resolvedProvider, modelStr.slice(slashIdx + 1)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
// Try each provider prefix
|
||||
for (const [providerId, config] of Object.entries(RERANK_PROVIDERS)) {
|
||||
if (modelStr.startsWith(providerId + "/")) {
|
||||
return { provider: providerId, model: modelStr.slice(providerId.length + 1) };
|
||||
return {
|
||||
provider: providerId,
|
||||
model: normalizeProviderScopedModelId(providerId, modelStr.slice(providerId.length + 1)),
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
@@ -117,7 +153,7 @@ export function getAllRerankModels() {
|
||||
for (const [providerId, config] of Object.entries(RERANK_PROVIDERS)) {
|
||||
for (const model of config.models) {
|
||||
models.push({
|
||||
id: `${providerId}/${model.id}`,
|
||||
id: toProviderScopedModelId(providerId, model.id),
|
||||
name: model.name,
|
||||
provider: providerId,
|
||||
});
|
||||
|
||||
@@ -293,6 +293,9 @@ export class BaseExecutor {
|
||||
});
|
||||
}
|
||||
|
||||
// Fix #1884: Cursor sends prompt_cache_retention which breaks strict upstream endpoints
|
||||
delete cloned.prompt_cache_retention;
|
||||
|
||||
// Also clean up top level optional fields that commonly cause issues when empty
|
||||
const optionalKeys = ["user", "stop", "seed", "response_format"];
|
||||
for (const key of optionalKeys) {
|
||||
|
||||
@@ -26,6 +26,7 @@ import {
|
||||
getRememberedResponseConversationItems,
|
||||
getRememberedResponseFunctionCalls,
|
||||
} from "../services/responsesToolCallState.ts";
|
||||
import { sanitizeResponsesInputItems } from "../services/responsesInputSanitizer.ts";
|
||||
import { getThinkingBudgetConfig, ThinkingMode } from "../services/thinkingBudget.ts";
|
||||
import { CORS_HEADERS } from "../utils/cors.ts";
|
||||
import { createRequire } from "module";
|
||||
@@ -45,6 +46,11 @@ type WreqWebSocket = {
|
||||
onclose: (() => void) | null;
|
||||
};
|
||||
type WebsocketFn = (url: string, opts?: Record<string, unknown>) => Promise<WreqWebSocket>;
|
||||
type ResponsesMessageInput = {
|
||||
role?: unknown;
|
||||
phase?: unknown;
|
||||
content?: unknown;
|
||||
};
|
||||
|
||||
let _websocketFn: WebsocketFn | null = null;
|
||||
let _wreqChecked = false;
|
||||
@@ -605,6 +611,40 @@ function normalizeCodexTools(body: Record<string, unknown>): void {
|
||||
return false;
|
||||
}
|
||||
|
||||
// Codex Responses API requires function tools in flat Responses format:
|
||||
// { type: "function", name, description, parameters }
|
||||
// Some clients/translators send Chat Completions shape:
|
||||
// { type: "function", function: { name, description, parameters } }
|
||||
// which upstream rejects with "Missing required parameter: tools[0].name".
|
||||
// Flatten the nested `function` wrapper into top-level fields (#1914).
|
||||
const functionObject =
|
||||
tool.function && typeof tool.function === "object" && !Array.isArray(tool.function)
|
||||
? (tool.function as Record<string, unknown>)
|
||||
: null;
|
||||
const description =
|
||||
typeof tool.description === "string"
|
||||
? tool.description
|
||||
: typeof functionObject?.description === "string"
|
||||
? functionObject.description
|
||||
: "";
|
||||
const parameters =
|
||||
tool.parameters && typeof tool.parameters === "object" && !Array.isArray(tool.parameters)
|
||||
? tool.parameters
|
||||
: functionObject?.parameters &&
|
||||
typeof functionObject.parameters === "object" &&
|
||||
!Array.isArray(functionObject.parameters)
|
||||
? functionObject.parameters
|
||||
: { type: "object", properties: {} };
|
||||
|
||||
// Rewrite in-place to Responses format
|
||||
for (const key of Object.keys(tool)) {
|
||||
delete tool[key];
|
||||
}
|
||||
tool.type = "function";
|
||||
tool.name = name;
|
||||
if (description) tool.description = description;
|
||||
tool.parameters = parameters;
|
||||
|
||||
validToolNames.add(name);
|
||||
return true;
|
||||
});
|
||||
@@ -1185,19 +1225,49 @@ export class CodexExecutor extends BaseExecutor {
|
||||
// Issue #1832 & #1853: Map messages to input for clients like Cursor 5.5 that use responses/compact but send messages instead of input.
|
||||
// This MUST run before convertSystemToDeveloperRole and stripStoredItemReferences.
|
||||
if (!body.input && Array.isArray(body.messages)) {
|
||||
body.input = body.messages.map((msg: any) => ({
|
||||
body.input = body.messages.map((msg: ResponsesMessageInput) => ({
|
||||
type: "message",
|
||||
role: typeof msg.role === "string" ? msg.role : "user",
|
||||
...(typeof msg.phase === "string" ? { phase: msg.phase } : {}),
|
||||
content:
|
||||
typeof msg.content === "string"
|
||||
? [{ type: "input_text", text: msg.content }]
|
||||
: Array.isArray(msg.content)
|
||||
? msg.content.map((c: any) => {
|
||||
if (c && c.type === "text") return { type: "input_text", text: c.text };
|
||||
return c;
|
||||
? msg.content.map((contentPart: unknown) => {
|
||||
if (
|
||||
contentPart &&
|
||||
typeof contentPart === "object" &&
|
||||
!Array.isArray(contentPart) &&
|
||||
(contentPart as Record<string, unknown>).type === "text"
|
||||
) {
|
||||
return {
|
||||
type: "input_text",
|
||||
text: (contentPart as Record<string, unknown>).text,
|
||||
};
|
||||
}
|
||||
return contentPart;
|
||||
})
|
||||
: [],
|
||||
}));
|
||||
} else if (!body.input && typeof body.prompt === "string" && body.prompt.trim()) {
|
||||
// Issue #1872: Cursor occasionally passes the request as `prompt` instead of `messages`.
|
||||
body.input = [
|
||||
{
|
||||
type: "message",
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: body.prompt }],
|
||||
},
|
||||
];
|
||||
} else if (!body.input && Array.isArray(body.prompt)) {
|
||||
body.input = body.prompt.map((p: any) => ({
|
||||
type: "message",
|
||||
role: "user",
|
||||
content: [{ type: "input_text", text: typeof p === "string" ? p : JSON.stringify(p) }],
|
||||
}));
|
||||
}
|
||||
|
||||
if (Array.isArray(body.input)) {
|
||||
body.input = sanitizeResponsesInputItems(body.input, false);
|
||||
}
|
||||
|
||||
// ── Cache-aware system prompt handling (both paths) ──
|
||||
@@ -1355,7 +1425,7 @@ export class CodexExecutor extends BaseExecutor {
|
||||
delete body.seed;
|
||||
// max_tokens and max_output_tokens already deleted above (before passthrough return)
|
||||
delete body.user; // Cursor sends this but Codex doesn't support it
|
||||
delete body.prompt_cache_retention; // Cursor sends this but Codex doesn't support it
|
||||
|
||||
delete body.metadata; // Cursor sends this but Codex doesn't support it
|
||||
delete body.stream_options; // Cursor sends this but Codex doesn't support it
|
||||
delete body.safety_identifier; // Droid CLI sends this but Codex doesn't support it
|
||||
|
||||
@@ -922,6 +922,28 @@ function buildExecutorClientHeaders(
|
||||
return Object.keys(normalized).length > 0 ? normalized : null;
|
||||
}
|
||||
|
||||
function isCopilotClient(
|
||||
headers: Headers | Record<string, unknown> | null | undefined,
|
||||
userAgent?: string | null
|
||||
) {
|
||||
const isMatch = (value: unknown) =>
|
||||
typeof value === "string" && value.toLowerCase().includes("copilot");
|
||||
|
||||
if (isMatch(userAgent)) return true;
|
||||
|
||||
if (headers instanceof Headers) {
|
||||
for (const [key, value] of headers) {
|
||||
if (isMatch(key) || isMatch(value)) return true;
|
||||
}
|
||||
} else if (headers && typeof headers === "object") {
|
||||
for (const [key, value] of Object.entries(headers)) {
|
||||
if (isMatch(key) || isMatch(value)) return true;
|
||||
}
|
||||
}
|
||||
|
||||
return false;
|
||||
}
|
||||
|
||||
export async function handleChatCore({
|
||||
body,
|
||||
modelInfo,
|
||||
@@ -954,6 +976,7 @@ export async function handleChatCore({
|
||||
const suffix = extra ? ` ${JSON.stringify(extra)}` : "";
|
||||
log?.info?.("STAGE_TRACE", `${traceId} ${label} t=${elapsed}ms${suffix}`);
|
||||
};
|
||||
let tokensCompressed: number | null = null;
|
||||
const persistFailureUsage = (statusCode: number, errorCode?: string | null) => {
|
||||
saveRequestUsage({
|
||||
provider: provider || "unknown",
|
||||
@@ -1099,6 +1122,7 @@ export async function handleChatCore({
|
||||
});
|
||||
const isDroidCLI =
|
||||
userAgent?.toLowerCase().includes("droid") || userAgent?.toLowerCase().includes("codex-cli");
|
||||
const copilotCompatibleReasoning = isCopilotClient(clientRawRequest?.headers, userAgent);
|
||||
const clientResponseFormat =
|
||||
sourceFormat === FORMATS.OPENAI_RESPONSES && !isResponsesEndpoint && !isDroidCLI
|
||||
? FORMATS.OPENAI
|
||||
@@ -1178,6 +1202,23 @@ export async function handleChatCore({
|
||||
const capturePipelineStreamChunks =
|
||||
detailedLoggingEnabled && getCallLogPipelineCaptureStreamChunks();
|
||||
const skillRequestId = generateRequestId();
|
||||
let compressionAnalyticsWritePromise: Promise<void> | null = null;
|
||||
const attachCompressionUsageReceiptAfterAnalytics = (
|
||||
usage: Record<string, unknown>,
|
||||
source: "provider" | "estimated" | "stream"
|
||||
) => {
|
||||
const pendingWrite = compressionAnalyticsWritePromise;
|
||||
void (async () => {
|
||||
try {
|
||||
if (pendingWrite) await pendingWrite;
|
||||
const { attachCompressionUsageReceipt } =
|
||||
await import("../../src/lib/db/compressionAnalytics.ts");
|
||||
attachCompressionUsageReceipt(skillRequestId, usage, source);
|
||||
} catch {
|
||||
// Compression analytics are best-effort and must never affect responses.
|
||||
}
|
||||
})();
|
||||
};
|
||||
const pipelineSessionId =
|
||||
(clientRawRequest?.headers && typeof clientRawRequest.headers.get === "function"
|
||||
? clientRawRequest.headers.get("x-omniroute-session-id")
|
||||
@@ -1285,6 +1326,7 @@ export async function handleChatCore({
|
||||
comboName,
|
||||
comboStepId,
|
||||
comboExecutionKey,
|
||||
tokensCompressed,
|
||||
cacheSource: cacheSource === "semantic" ? "semantic" : "upstream",
|
||||
apiKeyId: apiKeyInfo?.id || null,
|
||||
apiKeyName: apiKeyInfo?.name || null,
|
||||
@@ -1560,6 +1602,8 @@ export async function handleChatCore({
|
||||
// This prevents "prompt too long" errors for large-but-not-full contexts.
|
||||
const allMessages =
|
||||
body?.messages || body?.input || body?.contents || body?.request?.contents || [];
|
||||
let cavemanOutputModeApplied = false;
|
||||
let cavemanOutputModeIntensity: string | null = null;
|
||||
if (body && Array.isArray(allMessages) && allMessages.length > 0) {
|
||||
let estimatedTokens = estimateTokens(JSON.stringify(allMessages));
|
||||
let promptCompressionEnabled = false;
|
||||
@@ -1594,6 +1638,91 @@ export async function handleChatCore({
|
||||
log?.debug?.("COMPRESSION", "Prompt compression disabled or unavailable");
|
||||
}
|
||||
let compressionComboKey = comboName ?? null;
|
||||
let compressionComboApplied = false;
|
||||
type RuntimeCompressionCombo = {
|
||||
id: string;
|
||||
pipeline: NonNullable<CompressionConfig["stackedPipeline"]>;
|
||||
languagePacks: string[];
|
||||
outputMode: boolean;
|
||||
outputModeIntensity: string;
|
||||
};
|
||||
const isBuiltinStackedPipeline = (
|
||||
pipeline: CompressionConfig["stackedPipeline"] | undefined
|
||||
): boolean => {
|
||||
if (!Array.isArray(pipeline) || pipeline.length !== 2) return false;
|
||||
const [first, second] = pipeline;
|
||||
return (
|
||||
first?.engine === "rtk" &&
|
||||
(first.intensity === undefined || first.intensity === "standard") &&
|
||||
!first.config &&
|
||||
second?.engine === "caveman" &&
|
||||
(second.intensity === undefined || second.intensity === "full") &&
|
||||
!second.config
|
||||
);
|
||||
};
|
||||
const applyCompressionComboConfig = (
|
||||
compressionCombo: RuntimeCompressionCombo | null,
|
||||
routingOverrideIds: string[] = []
|
||||
): boolean => {
|
||||
if (!compressionCombo || compressionCombo.pipeline.length === 0) return false;
|
||||
const comboLanguagePacks = [
|
||||
...new Set(
|
||||
compressionCombo.languagePacks
|
||||
.map((pack) => pack.trim())
|
||||
.filter((pack) => pack.length > 0)
|
||||
),
|
||||
];
|
||||
const comboOutputIntensity = (
|
||||
["lite", "full", "ultra"].includes(compressionCombo.outputModeIntensity)
|
||||
? compressionCombo.outputModeIntensity
|
||||
: (config.cavemanOutputMode?.intensity ?? "full")
|
||||
) as "lite" | "full" | "ultra";
|
||||
const comboDefaultLanguage =
|
||||
comboLanguagePacks.find((pack) => pack === config.languageConfig?.defaultLanguage) ??
|
||||
comboLanguagePacks[0] ??
|
||||
config.languageConfig?.defaultLanguage ??
|
||||
"en";
|
||||
const comboOverrides = { ...(config.comboOverrides ?? {}) };
|
||||
for (const id of routingOverrideIds) {
|
||||
if (id) comboOverrides[id] = "stacked";
|
||||
}
|
||||
config = {
|
||||
...config,
|
||||
compressionComboId: compressionCombo.id,
|
||||
stackedPipeline: compressionCombo.pipeline,
|
||||
languageConfig: {
|
||||
...(config.languageConfig ?? {
|
||||
enabled: false,
|
||||
defaultLanguage: "en",
|
||||
autoDetect: true,
|
||||
enabledPacks: ["en"],
|
||||
}),
|
||||
enabled: true,
|
||||
defaultLanguage: comboDefaultLanguage,
|
||||
enabledPacks:
|
||||
comboLanguagePacks.length > 0
|
||||
? comboLanguagePacks
|
||||
: (config.languageConfig?.enabledPacks ?? ["en"]),
|
||||
},
|
||||
cavemanOutputMode: {
|
||||
...(config.cavemanOutputMode ?? {
|
||||
enabled: false,
|
||||
intensity: "full",
|
||||
autoClarity: true,
|
||||
}),
|
||||
enabled: compressionCombo.outputMode,
|
||||
intensity: comboOutputIntensity,
|
||||
},
|
||||
comboOverrides,
|
||||
};
|
||||
compressionComboApplied = true;
|
||||
return true;
|
||||
};
|
||||
const isStackedCompressionCombo = (
|
||||
compressionCombo: RuntimeCompressionCombo | null
|
||||
): compressionCombo is RuntimeCompressionCombo => {
|
||||
return Boolean(compressionCombo && compressionCombo.pipeline.length > 1);
|
||||
};
|
||||
if (isCombo && comboName) {
|
||||
try {
|
||||
const { getComboByName } = await import("../../src/lib/localDb");
|
||||
@@ -1616,7 +1745,9 @@ export async function handleChatCore({
|
||||
comboMode === "lite" ||
|
||||
comboMode === "standard" ||
|
||||
comboMode === "aggressive" ||
|
||||
comboMode === "ultra"
|
||||
comboMode === "ultra" ||
|
||||
comboMode === "rtk" ||
|
||||
comboMode === "stacked"
|
||||
) {
|
||||
config = {
|
||||
...config,
|
||||
@@ -1628,6 +1759,27 @@ export async function handleChatCore({
|
||||
};
|
||||
compressionComboKey = comboName;
|
||||
}
|
||||
const routingComboIds = [
|
||||
comboConfig?.id,
|
||||
comboName,
|
||||
comboName.startsWith("combo/") ? comboName.substring(6) : null,
|
||||
].filter((id): id is string => typeof id === "string" && id.length > 0);
|
||||
if (routingComboIds.length > 0) {
|
||||
const { getCompressionComboForRoutingCombo } =
|
||||
await import("../../src/lib/db/compressionCombos.ts");
|
||||
const assignedCompressionCombo =
|
||||
routingComboIds
|
||||
.map((id) => getCompressionComboForRoutingCombo(id))
|
||||
.find((combo) => combo !== null) ?? null;
|
||||
if (
|
||||
applyCompressionComboConfig(
|
||||
assignedCompressionCombo as RuntimeCompressionCombo | null,
|
||||
routingComboIds
|
||||
)
|
||||
) {
|
||||
compressionComboKey = comboName;
|
||||
}
|
||||
}
|
||||
} catch (err) {
|
||||
log?.debug?.(
|
||||
"COMPRESSION",
|
||||
@@ -1636,6 +1788,66 @@ export async function handleChatCore({
|
||||
);
|
||||
}
|
||||
}
|
||||
const modeBeforeOutputTransform = selectCompressionStrategy(
|
||||
config,
|
||||
compressionComboKey,
|
||||
estimatedTokens,
|
||||
body as Record<string, unknown>,
|
||||
{ provider, targetFormat, model: effectiveModel }
|
||||
);
|
||||
if (
|
||||
modeBeforeOutputTransform === "stacked" &&
|
||||
!compressionComboApplied &&
|
||||
!config.compressionComboId &&
|
||||
isBuiltinStackedPipeline(config.stackedPipeline)
|
||||
) {
|
||||
try {
|
||||
const { getDefaultCompressionCombo } =
|
||||
await import("../../src/lib/db/compressionCombos.ts");
|
||||
const defaultCompressionCombo = getDefaultCompressionCombo();
|
||||
if (
|
||||
isStackedCompressionCombo(defaultCompressionCombo as RuntimeCompressionCombo | null) &&
|
||||
applyCompressionComboConfig(defaultCompressionCombo as RuntimeCompressionCombo | null)
|
||||
) {
|
||||
log?.debug?.(
|
||||
"COMPRESSION",
|
||||
`Default compression combo applied: ${defaultCompressionCombo?.id}`
|
||||
);
|
||||
}
|
||||
} catch (err) {
|
||||
log?.debug?.(
|
||||
"COMPRESSION",
|
||||
"Default compression combo lookup skipped: " +
|
||||
(err instanceof Error ? err.message : String(err))
|
||||
);
|
||||
}
|
||||
}
|
||||
if (config.cavemanOutputMode?.enabled) {
|
||||
try {
|
||||
const { applyCavemanOutputMode } = await import("../services/compression/outputMode.ts");
|
||||
const outputModeLanguage =
|
||||
config.languageConfig?.enabled === true ? config.languageConfig.defaultLanguage : "en";
|
||||
const outputMode = applyCavemanOutputMode(
|
||||
body as Parameters<typeof applyCavemanOutputMode>[0],
|
||||
config.cavemanOutputMode,
|
||||
outputModeLanguage
|
||||
);
|
||||
if (outputMode.applied) {
|
||||
body = outputMode.body as typeof body;
|
||||
cavemanOutputModeApplied = true;
|
||||
cavemanOutputModeIntensity = config.cavemanOutputMode.intensity;
|
||||
estimatedTokens = estimateTokens(JSON.stringify(body?.messages ?? body?.input ?? []));
|
||||
log?.debug?.("COMPRESSION", "Caveman output mode instruction applied");
|
||||
} else if (outputMode.skippedReason && outputMode.skippedReason !== "disabled") {
|
||||
log?.debug?.("COMPRESSION", `Caveman output mode skipped: ${outputMode.skippedReason}`);
|
||||
}
|
||||
} catch (err) {
|
||||
log?.debug?.(
|
||||
"COMPRESSION",
|
||||
"Caveman output mode skipped: " + (err instanceof Error ? err.message : String(err))
|
||||
);
|
||||
}
|
||||
}
|
||||
const compressionInputBody = body as Record<string, unknown>;
|
||||
const mode = selectCompressionStrategy(
|
||||
config,
|
||||
@@ -1644,80 +1856,138 @@ export async function handleChatCore({
|
||||
compressionInputBody,
|
||||
{ provider, targetFormat, model: effectiveModel }
|
||||
);
|
||||
let compressionAnalyticsRecorded = false;
|
||||
if (mode !== "off") {
|
||||
const result = applyCompression(compressionInputBody, mode, {
|
||||
model: effectiveModel,
|
||||
config,
|
||||
});
|
||||
if (result.compressed && result.stats) {
|
||||
body = result.body as typeof body;
|
||||
estimatedTokens = result.stats.compressedTokens;
|
||||
trackCompressionStats(result.stats);
|
||||
void (async () => {
|
||||
try {
|
||||
const { insertCompressionAnalyticsRow } =
|
||||
await import("../../src/lib/db/compressionAnalytics.ts");
|
||||
insertCompressionAnalyticsRow({
|
||||
timestamp: new Date().toISOString(),
|
||||
combo_id: comboName ?? null,
|
||||
provider: provider ?? null,
|
||||
mode,
|
||||
original_tokens: result.stats.originalTokens,
|
||||
compressed_tokens: result.stats.compressedTokens,
|
||||
tokens_saved: Math.max(
|
||||
if (result.stats) {
|
||||
if (result.compressed) {
|
||||
body = result.body as typeof body;
|
||||
estimatedTokens = result.stats.compressedTokens;
|
||||
tokensCompressed = Math.max(
|
||||
0,
|
||||
result.stats.originalTokens - result.stats.compressedTokens
|
||||
);
|
||||
}
|
||||
|
||||
if (result.compressed || result.stats.fallbackApplied || cavemanOutputModeApplied) {
|
||||
trackCompressionStats(result.stats);
|
||||
compressionAnalyticsRecorded = true;
|
||||
compressionAnalyticsWritePromise = (async () => {
|
||||
try {
|
||||
const { insertCompressionAnalyticsRow } =
|
||||
await import("../../src/lib/db/compressionAnalytics.ts");
|
||||
const { calculateCost } = await import("../../src/lib/usage/costCalculator.ts");
|
||||
const tokensSaved = Math.max(
|
||||
0,
|
||||
result.stats.originalTokens - result.stats.compressedTokens
|
||||
),
|
||||
duration_ms: result.stats.durationMs ?? null,
|
||||
request_id: skillRequestId,
|
||||
});
|
||||
} catch (err) {
|
||||
log?.debug?.(
|
||||
"COMPRESSION",
|
||||
"Compression analytics write skipped: " +
|
||||
(err instanceof Error ? err.message : String(err))
|
||||
);
|
||||
}
|
||||
})();
|
||||
void (async () => {
|
||||
try {
|
||||
const { detectCachingContext } =
|
||||
await import("../services/compression/cachingAware.ts");
|
||||
const { recordCacheStats } =
|
||||
await import("../../src/lib/db/compressionCacheStats.ts");
|
||||
const cacheContext = detectCachingContext(compressionInputBody, {
|
||||
provider,
|
||||
targetFormat,
|
||||
model: effectiveModel,
|
||||
});
|
||||
const tokensSavedCompression = Math.max(
|
||||
0,
|
||||
result.stats.originalTokens - result.stats.compressedTokens
|
||||
);
|
||||
recordCacheStats({
|
||||
provider: cacheContext.provider ?? provider ?? "unknown",
|
||||
model: effectiveModel ?? "",
|
||||
compressionMode: mode,
|
||||
cacheControlPresent: cacheContext.hasCacheControl,
|
||||
estimatedCacheHit: cacheContext.hasCacheControl && cacheContext.isCachingProvider,
|
||||
tokensSavedCompression,
|
||||
tokensSavedCaching: 0,
|
||||
netSavings: tokensSavedCompression,
|
||||
});
|
||||
} catch (err) {
|
||||
log?.debug?.(
|
||||
"COMPRESSION",
|
||||
"Compression cache stats write skipped: " +
|
||||
(err instanceof Error ? err.message : String(err))
|
||||
);
|
||||
}
|
||||
})();
|
||||
log?.info?.(
|
||||
"COMPRESSION",
|
||||
`Prompt compressed (${mode}): ${result.stats.originalTokens} -> ${result.stats.compressedTokens} tokens (${result.stats.savingsPercent}% saved, techniques: ${result.stats.techniquesUsed.join(",")})`
|
||||
);
|
||||
);
|
||||
const estimatedUsdSaved = await calculateCost(
|
||||
provider ?? "",
|
||||
effectiveModel ?? "",
|
||||
{
|
||||
input: tokensSaved,
|
||||
}
|
||||
);
|
||||
insertCompressionAnalyticsRow({
|
||||
timestamp: new Date().toISOString(),
|
||||
combo_id: comboName ?? null,
|
||||
provider: provider ?? null,
|
||||
mode,
|
||||
engine: result.stats.engine ?? mode,
|
||||
compression_combo_id:
|
||||
result.stats.compressionComboId ?? config.compressionComboId ?? null,
|
||||
original_tokens: result.stats.originalTokens,
|
||||
compressed_tokens: result.stats.compressedTokens,
|
||||
tokens_saved: tokensSaved,
|
||||
duration_ms: result.stats.durationMs ?? null,
|
||||
request_id: skillRequestId,
|
||||
estimated_usd_saved: estimatedUsdSaved || null,
|
||||
validation_fallback: result.stats.fallbackApplied ? 1 : 0,
|
||||
output_mode: cavemanOutputModeApplied ? cavemanOutputModeIntensity : null,
|
||||
rtk_raw_output_pointer: result.stats.rtkRawOutputPointers?.[0]?.id ?? null,
|
||||
rtk_raw_output_bytes: result.stats.rtkRawOutputPointers?.[0]?.bytes ?? null,
|
||||
});
|
||||
} catch (err) {
|
||||
log?.debug?.(
|
||||
"COMPRESSION",
|
||||
"Compression analytics write skipped: " +
|
||||
(err instanceof Error ? err.message : String(err))
|
||||
);
|
||||
}
|
||||
})();
|
||||
}
|
||||
|
||||
if (result.compressed) {
|
||||
void (async () => {
|
||||
try {
|
||||
const { detectCachingContext } =
|
||||
await import("../services/compression/cachingAware.ts");
|
||||
const { recordCacheStats } =
|
||||
await import("../../src/lib/db/compressionCacheStats.ts");
|
||||
const cacheContext = detectCachingContext(compressionInputBody, {
|
||||
provider,
|
||||
targetFormat,
|
||||
model: effectiveModel,
|
||||
});
|
||||
const tokensSavedCompression = Math.max(
|
||||
0,
|
||||
result.stats.originalTokens - result.stats.compressedTokens
|
||||
);
|
||||
recordCacheStats({
|
||||
provider: cacheContext.provider ?? provider ?? "unknown",
|
||||
model: effectiveModel ?? "",
|
||||
compressionMode: mode,
|
||||
cacheControlPresent: cacheContext.hasCacheControl,
|
||||
estimatedCacheHit: cacheContext.hasCacheControl && cacheContext.isCachingProvider,
|
||||
tokensSavedCompression,
|
||||
tokensSavedCaching: 0,
|
||||
netSavings: tokensSavedCompression,
|
||||
});
|
||||
} catch (err) {
|
||||
log?.debug?.(
|
||||
"COMPRESSION",
|
||||
"Compression cache stats write skipped: " +
|
||||
(err instanceof Error ? err.message : String(err))
|
||||
);
|
||||
}
|
||||
})();
|
||||
log?.info?.(
|
||||
"COMPRESSION",
|
||||
`Prompt compressed (${mode}): ${result.stats.originalTokens} -> ${result.stats.compressedTokens} tokens (${result.stats.savingsPercent}% saved, techniques: ${result.stats.techniquesUsed.join(",")})`
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cavemanOutputModeApplied && !compressionAnalyticsRecorded) {
|
||||
compressionAnalyticsWritePromise = (async () => {
|
||||
try {
|
||||
const { insertCompressionAnalyticsRow } =
|
||||
await import("../../src/lib/db/compressionAnalytics.ts");
|
||||
insertCompressionAnalyticsRow({
|
||||
timestamp: new Date().toISOString(),
|
||||
combo_id: comboName ?? null,
|
||||
provider: provider ?? null,
|
||||
mode: "output-caveman",
|
||||
engine: "caveman-output",
|
||||
compression_combo_id: config.compressionComboId ?? null,
|
||||
original_tokens: estimatedTokens,
|
||||
compressed_tokens: estimatedTokens,
|
||||
tokens_saved: 0,
|
||||
request_id: skillRequestId,
|
||||
output_mode: cavemanOutputModeIntensity,
|
||||
});
|
||||
} catch (err) {
|
||||
log?.debug?.(
|
||||
"COMPRESSION",
|
||||
"Caveman output analytics write skipped: " +
|
||||
(err instanceof Error ? err.message : String(err))
|
||||
);
|
||||
}
|
||||
})();
|
||||
}
|
||||
} catch (err) {
|
||||
log?.warn?.(
|
||||
"COMPRESSION",
|
||||
@@ -1793,6 +2063,7 @@ export async function handleChatCore({
|
||||
if (compressionResult.compressed) {
|
||||
body = compressionResult.body;
|
||||
const stats = compressionResult.stats;
|
||||
tokensCompressed = Math.max(0, (stats?.original ?? 0) - (stats?.final ?? 0));
|
||||
const layersInfo =
|
||||
stats && "layers" in stats && Array.isArray(stats.layers)
|
||||
? ` (layers: ${stats.layers.map((l: { name: string }) => l.name).join(", ")})`
|
||||
@@ -3264,6 +3535,9 @@ export async function handleChatCore({
|
||||
|
||||
// Log usage for non-streaming responses
|
||||
const usage = extractUsageFromResponse(responseBody, provider);
|
||||
if (usage && typeof usage === "object") {
|
||||
attachCompressionUsageReceiptAfterAnalytics(usage as Record<string, unknown>, "provider");
|
||||
}
|
||||
appendRequestLog({ model, provider, connectionId, tokens: usage, status: "200 OK" }).catch(
|
||||
() => {}
|
||||
);
|
||||
@@ -3636,6 +3910,7 @@ export async function handleChatCore({
|
||||
|
||||
// Track cache token metrics for streaming responses
|
||||
if (streamUsage && typeof streamUsage === "object") {
|
||||
attachCompressionUsageReceiptAfterAnalytics(streamUsage as Record<string, unknown>, "stream");
|
||||
const inputTokens = streamUsage.prompt_tokens || 0;
|
||||
const cachedTokens = toPositiveNumber(
|
||||
streamUsage.cache_read_input_tokens ??
|
||||
@@ -3777,7 +4052,8 @@ export async function handleChatCore({
|
||||
streamStateBody,
|
||||
onStreamComplete,
|
||||
apiKeyInfo,
|
||||
handleStreamFailure
|
||||
handleStreamFailure,
|
||||
copilotCompatibleReasoning
|
||||
);
|
||||
} else if (needsTranslation(targetFormat, clientResponseFormat)) {
|
||||
// Standard translation for other providers
|
||||
@@ -3793,7 +4069,8 @@ export async function handleChatCore({
|
||||
streamStateBody,
|
||||
onStreamComplete,
|
||||
apiKeyInfo,
|
||||
handleStreamFailure
|
||||
handleStreamFailure,
|
||||
copilotCompatibleReasoning
|
||||
);
|
||||
} else {
|
||||
log?.debug?.("STREAM", `Standard passthrough mode`);
|
||||
|
||||
@@ -45,6 +45,7 @@ const OPENAI_IMAGE_TO_IMAGE_MODELS = new Set([
|
||||
"flux-kontext-max",
|
||||
"flux-kontext",
|
||||
"flux-kontext-pro",
|
||||
"qwen-image",
|
||||
]);
|
||||
|
||||
const BFL_MODEL_ENDPOINTS = {
|
||||
|
||||
@@ -691,6 +691,9 @@ export function sanitizeStreamingChunk(parsed: unknown): unknown {
|
||||
}
|
||||
if (deltaRecord.reasoning_content !== undefined) {
|
||||
delta.reasoning_content = deltaRecord.reasoning_content;
|
||||
}
|
||||
if (deltaRecord.reasoning_text !== undefined) {
|
||||
delta.reasoning_text = deltaRecord.reasoning_text;
|
||||
} else if (typeof deltaRecord.reasoning === "string" && deltaRecord.reasoning) {
|
||||
// Alias: some providers use 'reasoning' instead of 'reasoning_content'
|
||||
delta.reasoning_content = deltaRecord.reasoning;
|
||||
|
||||
@@ -145,12 +145,20 @@ omniroute --mcp
|
||||
|
||||
### Cache and Compression Tools
|
||||
|
||||
| # | Tool | Scopes | Description |
|
||||
| --- | --------------------------------- | ------------------- | ---------------------------------------------------------------------------- |
|
||||
| 21 | `omniroute_cache_stats` | `read:cache` | Semantic cache, prompt-cache, and idempotency statistics |
|
||||
| 22 | `omniroute_cache_flush` | `write:cache` | Flush cache entries globally or by signature/model |
|
||||
| 23 | `omniroute_compression_status` | `read:compression` | Compression settings, analytics summary, and provider-aware cache statistics |
|
||||
| 24 | `omniroute_compression_configure` | `write:compression` | Configure compression mode and trigger thresholds at runtime |
|
||||
| # | Tool | Scopes | Description |
|
||||
| --- | ----------------------------------- | ------------------- | ---------------------------------------------------------------------------- |
|
||||
| 21 | `omniroute_cache_stats` | `read:cache` | Semantic cache, prompt-cache, and idempotency statistics |
|
||||
| 22 | `omniroute_cache_flush` | `write:cache` | Flush cache entries globally or by signature/model |
|
||||
| 23 | `omniroute_compression_status` | `read:compression` | Compression settings, analytics summary, and provider-aware cache statistics |
|
||||
| 24 | `omniroute_compression_configure` | `write:compression` | Configure compression mode and trigger thresholds at runtime |
|
||||
| 25 | `omniroute_set_compression_engine` | `write:compression` | Set Caveman, RTK, or stacked compression mode and pipeline |
|
||||
| 26 | `omniroute_list_compression_combos` | `read:compression` | List named compression combos and routing assignments |
|
||||
| 27 | `omniroute_compression_combo_stats` | `read:compression` | Read analytics grouped by compression combo and engine |
|
||||
|
||||
MCP listable metadata descriptions are compressed at registration/list time when description
|
||||
compression is enabled. `omniroute_compression_status` exposes those savings separately as
|
||||
`analytics.mcpDescriptionCompression` with `source: "mcp_metadata_estimate"`, so clients do not
|
||||
mistake metadata shrink estimates for provider token receipts.
|
||||
|
||||
---
|
||||
|
||||
@@ -540,12 +548,12 @@ The MCP server supports **fine-grained scope enforcement** for multi-tenant envi
|
||||
| `read:usage` | `cost_report`, `explain_route`, `get_session_snapshot` |
|
||||
| `read:models` | `list_models_catalog` |
|
||||
| `read:cache` | `cache_stats` |
|
||||
| `read:compression` | `compression_status` |
|
||||
| `read:compression` | `compression_status`, `list_compression_combos`, `compression_combo_stats` |
|
||||
| `write:combos` | `switch_combo` |
|
||||
| `write:budget` | `set_budget_guard` |
|
||||
| `write:resilience` | `set_resilience_profile` |
|
||||
| `write:cache` | `cache_flush` |
|
||||
| `write:compression` | `compression_configure` |
|
||||
| `write:compression` | `compression_configure`, `set_compression_engine` |
|
||||
| `execute:completions` | `route_request`, `test_combo` |
|
||||
|
||||
**Wildcard scopes:** Use `read:*` to grant all read scopes, or `*` for full access.
|
||||
@@ -581,7 +589,7 @@ mcp-server/
|
||||
├── audit.ts # SQLite audit logger (SHA-256 input hashing)
|
||||
├── scopeEnforcement.ts # Fine-grained scope enforcement
|
||||
├── schemas/
|
||||
│ ├── tools.ts # Zod schemas for all 16 tools (input/output/scopes)
|
||||
│ ├── tools.ts # Zod schemas for core, cache, compression, and proxy tools
|
||||
│ ├── a2a.ts # A2A protocol types (Agent Card, Task, JSON-RPC)
|
||||
│ ├── audit.ts # Audit & routing decision types + hash helpers
|
||||
│ └── index.ts # Schema barrel export
|
||||
|
||||
243
open-sse/mcp-server/descriptionCompressor.ts
Normal file
243
open-sse/mcp-server/descriptionCompressor.ts
Normal file
@@ -0,0 +1,243 @@
|
||||
import { applyRulesToText } from "../services/compression/caveman.ts";
|
||||
import { getRulesForContext } from "../services/compression/cavemanRules.ts";
|
||||
import {
|
||||
extractPreservedBlocks,
|
||||
restorePreservedBlocks,
|
||||
} from "../services/compression/preservation.ts";
|
||||
|
||||
export interface DescriptionCompressionResult {
|
||||
compressed: string;
|
||||
before: number;
|
||||
after: number;
|
||||
changed: boolean;
|
||||
}
|
||||
|
||||
export interface DescriptionCompressionOptions {
|
||||
enabled?: boolean;
|
||||
}
|
||||
|
||||
export interface McpDescriptionCompressionStats {
|
||||
descriptionsCompressed: number;
|
||||
charsBefore: number;
|
||||
charsAfter: number;
|
||||
charsSaved: number;
|
||||
estimatedTokensSaved: number;
|
||||
}
|
||||
|
||||
const descriptionCompressionStats: McpDescriptionCompressionStats = {
|
||||
descriptionsCompressed: 0,
|
||||
charsBefore: 0,
|
||||
charsAfter: 0,
|
||||
charsSaved: 0,
|
||||
estimatedTokensSaved: 0,
|
||||
};
|
||||
|
||||
const persistedDescriptionCompressionStats: McpDescriptionCompressionStats = {
|
||||
descriptionsCompressed: 0,
|
||||
charsBefore: 0,
|
||||
charsAfter: 0,
|
||||
charsSaved: 0,
|
||||
estimatedTokensSaved: 0,
|
||||
};
|
||||
|
||||
const MCP_LIST_CONTAINER_KEYS = new Set(["tools", "prompts", "resources", "resourceTemplates"]);
|
||||
const MCP_METADATA_DESCRIPTION_FIELDS = ["description"];
|
||||
|
||||
function isDisabledEnvValue(value: string | undefined): boolean {
|
||||
return !!value && ["0", "false", "off", "no"].includes(value.trim().toLowerCase());
|
||||
}
|
||||
|
||||
export function isMcpDescriptionCompressionEnabled(
|
||||
options: DescriptionCompressionOptions = {}
|
||||
): boolean {
|
||||
if (isDisabledEnvValue(process.env.OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS)) return false;
|
||||
if (isDisabledEnvValue(process.env.OMNIROUTE_MCP_DESCRIPTION_COMPRESSION)) return false;
|
||||
return options.enabled !== false;
|
||||
}
|
||||
|
||||
export function compressMcpDescription(description: string): DescriptionCompressionResult {
|
||||
if (!description) {
|
||||
return { compressed: description, before: 0, after: 0, changed: false };
|
||||
}
|
||||
|
||||
const { text, blocks } = extractPreservedBlocks(description);
|
||||
const rules = getRulesForContext("all", "full");
|
||||
const applied = applyRulesToText(text, rules).text;
|
||||
const normalized = applied
|
||||
.replace(/[ \t]{2,}/g, " ")
|
||||
.replace(/[ \t]+([,.;:!?])/g, "$1")
|
||||
.replace(/\n{3,}/g, "\n\n")
|
||||
.replace(/(^|[.!?][ \t]+|\n+[ \t]*)([a-z])/g, (_match, prefix: string, char: string) => {
|
||||
return `${prefix}${char.toUpperCase()}`;
|
||||
})
|
||||
.trim();
|
||||
const compressed = restorePreservedBlocks(normalized, blocks);
|
||||
|
||||
return {
|
||||
compressed,
|
||||
before: description.length,
|
||||
after: compressed.length,
|
||||
changed: compressed !== description,
|
||||
};
|
||||
}
|
||||
|
||||
export function maybeCompressMcpDescription(
|
||||
description: string,
|
||||
options: DescriptionCompressionOptions = {}
|
||||
): string {
|
||||
if (!isMcpDescriptionCompressionEnabled(options)) return description;
|
||||
const result = compressMcpDescription(description);
|
||||
if (result.changed && result.after < result.before) {
|
||||
descriptionCompressionStats.descriptionsCompressed += 1;
|
||||
descriptionCompressionStats.charsBefore += result.before;
|
||||
descriptionCompressionStats.charsAfter += result.after;
|
||||
descriptionCompressionStats.charsSaved += result.before - result.after;
|
||||
descriptionCompressionStats.estimatedTokensSaved += Math.ceil(
|
||||
(result.before - result.after) / 4
|
||||
);
|
||||
return result.compressed;
|
||||
}
|
||||
return description;
|
||||
}
|
||||
|
||||
export function compressDescriptionsInPlace(
|
||||
value: unknown,
|
||||
fieldNames: string[] = ["description"],
|
||||
options: DescriptionCompressionOptions = {}
|
||||
): void {
|
||||
if (!value || typeof value !== "object") return;
|
||||
const fields = new Set(fieldNames);
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) compressDescriptionsInPlace(item, fieldNames, options);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {
|
||||
if (fields.has(key) && typeof nested === "string") {
|
||||
(value as Record<string, unknown>)[key] = maybeCompressMcpDescription(nested, options);
|
||||
} else if (nested && typeof nested === "object") {
|
||||
compressDescriptionsInPlace(nested, fieldNames, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function clonePlainMetadata<T>(value: T): T {
|
||||
if (!value || typeof value !== "object") return value;
|
||||
return JSON.parse(JSON.stringify(value)) as T;
|
||||
}
|
||||
|
||||
function compressMcpListContainersInPlace(
|
||||
value: unknown,
|
||||
options: DescriptionCompressionOptions = {}
|
||||
): void {
|
||||
if (!value || typeof value !== "object") return;
|
||||
if (Array.isArray(value)) {
|
||||
for (const item of value) compressMcpListContainersInPlace(item, options);
|
||||
return;
|
||||
}
|
||||
|
||||
for (const [key, nested] of Object.entries(value as Record<string, unknown>)) {
|
||||
if (MCP_LIST_CONTAINER_KEYS.has(key) && Array.isArray(nested)) {
|
||||
compressDescriptionsInPlace(nested, MCP_METADATA_DESCRIPTION_FIELDS, options);
|
||||
} else if (nested && typeof nested === "object") {
|
||||
compressMcpListContainersInPlace(nested, options);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export function compressMcpListMetadata<T>(
|
||||
value: T,
|
||||
options: DescriptionCompressionOptions = {}
|
||||
): T {
|
||||
if (!isMcpDescriptionCompressionEnabled(options)) return value;
|
||||
const clone = clonePlainMetadata(value);
|
||||
compressMcpListContainersInPlace(clone, options);
|
||||
return clone;
|
||||
}
|
||||
|
||||
export function compressMcpRegistryMetadata<T extends Record<string, unknown>>(
|
||||
metadata: T,
|
||||
options: DescriptionCompressionOptions = {}
|
||||
): T {
|
||||
if (!isMcpDescriptionCompressionEnabled(options)) return metadata;
|
||||
const clone: Record<string, unknown> = { ...metadata };
|
||||
if (typeof clone.description === "string") {
|
||||
clone.description = maybeCompressMcpDescription(clone.description, options);
|
||||
}
|
||||
return clone as T;
|
||||
}
|
||||
|
||||
export function getMcpDescriptionCompressionStats(): McpDescriptionCompressionStats {
|
||||
return { ...descriptionCompressionStats };
|
||||
}
|
||||
|
||||
function getUnpersistedMcpDescriptionCompressionStats(): McpDescriptionCompressionStats {
|
||||
return {
|
||||
descriptionsCompressed:
|
||||
descriptionCompressionStats.descriptionsCompressed -
|
||||
persistedDescriptionCompressionStats.descriptionsCompressed,
|
||||
charsBefore:
|
||||
descriptionCompressionStats.charsBefore - persistedDescriptionCompressionStats.charsBefore,
|
||||
charsAfter:
|
||||
descriptionCompressionStats.charsAfter - persistedDescriptionCompressionStats.charsAfter,
|
||||
charsSaved:
|
||||
descriptionCompressionStats.charsSaved - persistedDescriptionCompressionStats.charsSaved,
|
||||
estimatedTokensSaved:
|
||||
descriptionCompressionStats.estimatedTokensSaved -
|
||||
persistedDescriptionCompressionStats.estimatedTokensSaved,
|
||||
};
|
||||
}
|
||||
|
||||
export async function snapshotMcpDescriptionCompressionStats(): Promise<McpDescriptionCompressionStats> {
|
||||
const delta = getUnpersistedMcpDescriptionCompressionStats();
|
||||
if (
|
||||
delta.descriptionsCompressed <= 0 ||
|
||||
delta.charsSaved <= 0 ||
|
||||
delta.estimatedTokensSaved <= 0
|
||||
) {
|
||||
return {
|
||||
descriptionsCompressed: 0,
|
||||
charsBefore: 0,
|
||||
charsAfter: 0,
|
||||
charsSaved: 0,
|
||||
estimatedTokensSaved: 0,
|
||||
};
|
||||
}
|
||||
|
||||
const originalTokens = Math.max(delta.estimatedTokensSaved, Math.ceil(delta.charsBefore / 4));
|
||||
const compressedTokens = Math.max(0, originalTokens - delta.estimatedTokensSaved);
|
||||
const { insertCompressionAnalyticsRow } =
|
||||
await import("../../src/lib/db/compressionAnalytics.ts");
|
||||
insertCompressionAnalyticsRow({
|
||||
timestamp: new Date().toISOString(),
|
||||
mode: "mcp-description",
|
||||
engine: "mcp-description",
|
||||
original_tokens: originalTokens,
|
||||
compressed_tokens: compressedTokens,
|
||||
tokens_saved: delta.estimatedTokensSaved,
|
||||
mcp_description_tokens_saved: delta.estimatedTokensSaved,
|
||||
});
|
||||
|
||||
persistedDescriptionCompressionStats.descriptionsCompressed =
|
||||
descriptionCompressionStats.descriptionsCompressed;
|
||||
persistedDescriptionCompressionStats.charsBefore = descriptionCompressionStats.charsBefore;
|
||||
persistedDescriptionCompressionStats.charsAfter = descriptionCompressionStats.charsAfter;
|
||||
persistedDescriptionCompressionStats.charsSaved = descriptionCompressionStats.charsSaved;
|
||||
persistedDescriptionCompressionStats.estimatedTokensSaved =
|
||||
descriptionCompressionStats.estimatedTokensSaved;
|
||||
|
||||
return delta;
|
||||
}
|
||||
|
||||
export function resetMcpDescriptionCompressionStats(): void {
|
||||
descriptionCompressionStats.descriptionsCompressed = 0;
|
||||
descriptionCompressionStats.charsBefore = 0;
|
||||
descriptionCompressionStats.charsAfter = 0;
|
||||
descriptionCompressionStats.charsSaved = 0;
|
||||
descriptionCompressionStats.estimatedTokensSaved = 0;
|
||||
persistedDescriptionCompressionStats.descriptionsCompressed = 0;
|
||||
persistedDescriptionCompressionStats.charsBefore = 0;
|
||||
persistedDescriptionCompressionStats.charsAfter = 0;
|
||||
persistedDescriptionCompressionStats.charsSaved = 0;
|
||||
persistedDescriptionCompressionStats.estimatedTokensSaved = 0;
|
||||
}
|
||||
@@ -10,6 +10,10 @@
|
||||
*/
|
||||
|
||||
import { z } from "zod";
|
||||
import {
|
||||
AUTO_ROUTING_STRATEGY_VALUES,
|
||||
ROUTING_STRATEGY_VALUES,
|
||||
} from "../../../src/shared/constants/routingStrategies.ts";
|
||||
|
||||
// ============ Shared Types ============
|
||||
|
||||
@@ -109,17 +113,7 @@ export const listCombosOutput = z.object({
|
||||
priority: z.number(),
|
||||
})
|
||||
),
|
||||
strategy: z.enum([
|
||||
"priority",
|
||||
"weighted",
|
||||
"round-robin",
|
||||
"context-relay",
|
||||
"strict-random",
|
||||
"random",
|
||||
"least-used",
|
||||
"cost-optimized",
|
||||
"auto",
|
||||
]),
|
||||
strategy: z.enum(ROUTING_STRATEGY_VALUES),
|
||||
enabled: z.boolean(),
|
||||
metrics: z
|
||||
.object({
|
||||
@@ -544,21 +538,9 @@ export const setBudgetGuardTool: McpToolDefinition<
|
||||
// --- Tool 11: omniroute_set_routing_strategy ---
|
||||
export const setRoutingStrategyInput = z.object({
|
||||
comboId: z.string().describe("Combo ID or name to update"),
|
||||
strategy: z
|
||||
.enum([
|
||||
"priority",
|
||||
"weighted",
|
||||
"round-robin",
|
||||
"context-relay",
|
||||
"strict-random",
|
||||
"random",
|
||||
"least-used",
|
||||
"cost-optimized",
|
||||
"auto",
|
||||
])
|
||||
.describe("Routing strategy to apply"),
|
||||
strategy: z.enum(ROUTING_STRATEGY_VALUES).describe("Routing strategy to apply"),
|
||||
autoRoutingStrategy: z
|
||||
.enum(["rules", "cost", "eco", "latency", "fast"])
|
||||
.enum(AUTO_ROUTING_STRATEGY_VALUES)
|
||||
.optional()
|
||||
.describe("Optional strategy used by auto mode (only used when strategy='auto')"),
|
||||
});
|
||||
@@ -1000,14 +982,41 @@ export const compressionStatusOutput = z.object({
|
||||
strategy: z.string(),
|
||||
settings: z.object({
|
||||
maxTokens: z.number(),
|
||||
autoTriggerMode: z.string(),
|
||||
targetRatio: z.number(),
|
||||
aggressiveness: z.string(),
|
||||
preserveSystemPrompt: z.boolean(),
|
||||
mcpDescriptionCompressionEnabled: z.boolean(),
|
||||
}),
|
||||
analytics: z.object({
|
||||
totalRequests: z.number(),
|
||||
compressedRequests: z.number(),
|
||||
tokensSaved: z.number(),
|
||||
avgCompressionRatio: z.number(),
|
||||
byMode: z.record(
|
||||
z.string(),
|
||||
z.object({
|
||||
count: z.number(),
|
||||
tokensSaved: z.number(),
|
||||
avgSavingsPct: z.number(),
|
||||
})
|
||||
),
|
||||
validationFallbacks: z.number(),
|
||||
requestsWithReceipts: z.number(),
|
||||
realUsage: z.object({
|
||||
requestsWithReceipts: z.number(),
|
||||
promptTokens: z.number(),
|
||||
completionTokens: z.number(),
|
||||
totalTokens: z.number(),
|
||||
cacheReadTokens: z.number(),
|
||||
cacheWriteTokens: z.number(),
|
||||
estimatedUsdSaved: z.number(),
|
||||
bySource: z.record(z.string(), z.number()),
|
||||
}),
|
||||
mcpDescriptionCompression: z.object({
|
||||
descriptionsCompressed: z.number(),
|
||||
charsSaved: z.number(),
|
||||
estimatedTokensSaved: z.number(),
|
||||
}),
|
||||
}),
|
||||
cacheStats: z
|
||||
.object({
|
||||
@@ -1037,12 +1046,21 @@ export const compressionStatusTool: McpToolDefinition<
|
||||
export const compressionConfigureInput = z.object({
|
||||
enabled: z.boolean().optional(),
|
||||
strategy: z
|
||||
.string()
|
||||
.enum(["off", "lite", "standard", "aggressive", "ultra", "rtk", "stacked"])
|
||||
.optional()
|
||||
.describe("Compression strategy: 'none' | 'standard' | 'aggressive' | 'ultra'"),
|
||||
maxTokens: z.number().optional().describe("Maximum tokens before compression triggers"),
|
||||
.describe("Compression mode"),
|
||||
autoTriggerMode: z
|
||||
.enum(["off", "lite", "standard", "aggressive", "ultra", "rtk", "stacked"])
|
||||
.optional(),
|
||||
maxTokens: z
|
||||
.number()
|
||||
.int()
|
||||
.min(0)
|
||||
.optional()
|
||||
.describe("Maximum tokens before compression triggers"),
|
||||
targetRatio: z.number().optional().describe("Target compression ratio (0.0–1.0)"),
|
||||
aggressiveness: z.string().optional().describe("Aggressiveness level: 'low' | 'medium' | 'high'"),
|
||||
preserveSystemPrompt: z.boolean().optional(),
|
||||
mcpDescriptionCompressionEnabled: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const compressionConfigureOutput = z.object({
|
||||
@@ -1051,9 +1069,11 @@ export const compressionConfigureOutput = z.object({
|
||||
settings: z.object({
|
||||
enabled: z.boolean(),
|
||||
strategy: z.string(),
|
||||
autoTriggerMode: z.string(),
|
||||
maxTokens: z.number(),
|
||||
targetRatio: z.number(),
|
||||
aggressiveness: z.string(),
|
||||
preserveSystemPrompt: z.boolean(),
|
||||
mcpDescriptionCompressionEnabled: z.boolean(),
|
||||
}),
|
||||
});
|
||||
|
||||
@@ -1063,7 +1083,7 @@ export const compressionConfigureTool: McpToolDefinition<
|
||||
> = {
|
||||
name: "omniroute_compression_configure",
|
||||
description:
|
||||
"Configure compression settings at runtime. Supports enabling/disabling compression, changing strategy (none/standard/aggressive/ultra), adjusting maxTokens threshold, targetRatio, and aggressiveness level.",
|
||||
"Configure compression settings at runtime. Supports enabling/disabling compression, changing strategy (off/lite/standard/aggressive/ultra/rtk/stacked), adjusting maxTokens threshold, targetRatio, auto-trigger mode, system prompt preservation, and MCP description compression.",
|
||||
inputSchema: compressionConfigureInput,
|
||||
outputSchema: compressionConfigureOutput,
|
||||
scopes: ["write:compression"],
|
||||
@@ -1072,6 +1092,72 @@ export const compressionConfigureTool: McpToolDefinition<
|
||||
sourceEndpoints: ["/api/compression/configure"],
|
||||
};
|
||||
|
||||
export const setCompressionEngineInput = z.object({
|
||||
engine: z.enum(["off", "caveman", "rtk", "stacked"]).optional(),
|
||||
cavemanIntensity: z.enum(["lite", "full", "ultra"]).optional(),
|
||||
rtkIntensity: z.enum(["minimal", "standard", "aggressive"]).optional(),
|
||||
outputMode: z.boolean().optional(),
|
||||
});
|
||||
|
||||
export const setCompressionEngineOutput = z.object({
|
||||
success: z.boolean(),
|
||||
settings: z.record(z.string(), z.unknown()),
|
||||
});
|
||||
|
||||
export const setCompressionEngineTool: McpToolDefinition<
|
||||
typeof setCompressionEngineInput,
|
||||
typeof setCompressionEngineOutput
|
||||
> = {
|
||||
name: "omniroute_set_compression_engine",
|
||||
description: "Set the active compression engine and Caveman/RTK runtime options.",
|
||||
inputSchema: setCompressionEngineInput,
|
||||
outputSchema: setCompressionEngineOutput,
|
||||
scopes: ["write:compression"],
|
||||
auditLevel: "full",
|
||||
phase: 2,
|
||||
sourceEndpoints: ["/api/settings/compression", "/api/context/rtk/config"],
|
||||
};
|
||||
|
||||
export const listCompressionCombosInput = z.object({});
|
||||
export const listCompressionCombosOutput = z.object({
|
||||
combos: z.array(z.record(z.string(), z.unknown())),
|
||||
});
|
||||
|
||||
export const listCompressionCombosTool: McpToolDefinition<
|
||||
typeof listCompressionCombosInput,
|
||||
typeof listCompressionCombosOutput
|
||||
> = {
|
||||
name: "omniroute_list_compression_combos",
|
||||
description: "List compression combos and their engine pipelines.",
|
||||
inputSchema: listCompressionCombosInput,
|
||||
outputSchema: listCompressionCombosOutput,
|
||||
scopes: ["read:compression"],
|
||||
auditLevel: "basic",
|
||||
phase: 2,
|
||||
sourceEndpoints: ["/api/context/combos"],
|
||||
};
|
||||
|
||||
export const compressionComboStatsInput = z.object({
|
||||
comboId: z.string().optional(),
|
||||
since: z.enum(["24h", "7d", "30d", "all"]).optional(),
|
||||
});
|
||||
|
||||
export const compressionComboStatsOutput = z.record(z.string(), z.unknown());
|
||||
|
||||
export const compressionComboStatsTool: McpToolDefinition<
|
||||
typeof compressionComboStatsInput,
|
||||
typeof compressionComboStatsOutput
|
||||
> = {
|
||||
name: "omniroute_compression_combo_stats",
|
||||
description: "Get compression analytics grouped by engine and compression combo.",
|
||||
inputSchema: compressionComboStatsInput,
|
||||
outputSchema: compressionComboStatsOutput,
|
||||
scopes: ["read:compression"],
|
||||
auditLevel: "basic",
|
||||
phase: 2,
|
||||
sourceEndpoints: ["/api/context/analytics"],
|
||||
};
|
||||
|
||||
// ============ 1proxy Tools ============
|
||||
|
||||
export const oneproxyFetchInput = z.object({
|
||||
@@ -1209,6 +1295,9 @@ export const MCP_TOOLS = [
|
||||
cacheFlushTool,
|
||||
compressionStatusTool,
|
||||
compressionConfigureTool,
|
||||
setCompressionEngineTool,
|
||||
listCompressionCombosTool,
|
||||
compressionComboStatsTool,
|
||||
oneproxyFetchTool,
|
||||
oneproxyRotateTool,
|
||||
oneproxyStatsTool,
|
||||
|
||||
@@ -76,6 +76,8 @@ import {
|
||||
import { memoryTools } from "./tools/memoryTools.ts";
|
||||
import { skillTools } from "./tools/skillTools.ts";
|
||||
import { compressionTools } from "./tools/compressionTools.ts";
|
||||
import { compressMcpRegistryMetadata } from "./descriptionCompressor.ts";
|
||||
import { getDbInstance } from "../../src/lib/db/core.ts";
|
||||
import { normalizeQuotaResponse } from "../../src/shared/contracts/quota.ts";
|
||||
import { resolveOmniRouteBaseUrl } from "../../src/shared/utils/resolveOmniRouteBaseUrl.ts";
|
||||
|
||||
@@ -95,6 +97,18 @@ const TOTAL_MCP_TOOL_COUNT =
|
||||
|
||||
type JsonRecord = Record<string, unknown>;
|
||||
|
||||
function readMcpDescriptionCompressionEnabled(): boolean {
|
||||
try {
|
||||
const row = getDbInstance()
|
||||
.prepare("SELECT value FROM key_value WHERE namespace = ? AND key = ?")
|
||||
.get("compression", "mcpDescriptionCompressionEnabled") as { value?: string } | undefined;
|
||||
if (!row?.value) return true;
|
||||
return JSON.parse(row.value) !== false;
|
||||
} catch {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
type TextToolResult = {
|
||||
content: Array<{ type: "text"; text: string }>;
|
||||
isError?: boolean;
|
||||
@@ -577,6 +591,33 @@ export function createMcpServer(): McpServer {
|
||||
name: "omniroute",
|
||||
version: process.env.npm_package_version || "1.8.1",
|
||||
});
|
||||
const mcpDescriptionCompressionEnabled = readMcpDescriptionCompressionEnabled();
|
||||
const registerTool = server.registerTool.bind(server);
|
||||
server.registerTool = ((name: string, config: Record<string, unknown>, handler: unknown) => {
|
||||
const metadata = compressMcpRegistryMetadata(config, {
|
||||
enabled: mcpDescriptionCompressionEnabled,
|
||||
});
|
||||
return registerTool(name, metadata, handler as never);
|
||||
}) as typeof server.registerTool;
|
||||
const registerPrompt = server.registerPrompt.bind(server);
|
||||
server.registerPrompt = ((name: string, config: Record<string, unknown>, handler: unknown) => {
|
||||
const metadata = compressMcpRegistryMetadata(config, {
|
||||
enabled: mcpDescriptionCompressionEnabled,
|
||||
});
|
||||
return registerPrompt(name, metadata as never, handler as never);
|
||||
}) as typeof server.registerPrompt;
|
||||
const registerResource = server.registerResource.bind(server);
|
||||
server.registerResource = ((
|
||||
name: string,
|
||||
uriOrTemplate: unknown,
|
||||
config: Record<string, unknown>,
|
||||
readCallback: unknown
|
||||
) => {
|
||||
const metadata = compressMcpRegistryMetadata(config, {
|
||||
enabled: mcpDescriptionCompressionEnabled,
|
||||
});
|
||||
return registerResource(name, uriOrTemplate as never, metadata as never, readCallback as never);
|
||||
}) as typeof server.registerResource;
|
||||
|
||||
// Register essential tools
|
||||
server.registerTool(
|
||||
|
||||
@@ -26,6 +26,11 @@ import {
|
||||
getComboModelString,
|
||||
getComboStepTarget,
|
||||
} from "../../../src/lib/combos/steps.ts";
|
||||
import type {
|
||||
AutoRoutingStrategyValue,
|
||||
RoutingStrategyValue,
|
||||
} from "../../../src/shared/constants/routingStrategies.ts";
|
||||
import { normalizeRoutingStrategy } from "../../../src/shared/constants/routingStrategies.ts";
|
||||
|
||||
const OMNIROUTE_BASE_URL = resolveOmniRouteBaseUrl();
|
||||
const OMNIROUTE_API_KEY = process.env.OMNIROUTE_API_KEY || "";
|
||||
@@ -372,17 +377,8 @@ export async function handleSetBudgetGuard(args: {
|
||||
|
||||
export async function handleSetRoutingStrategy(args: {
|
||||
comboId: string;
|
||||
strategy:
|
||||
| "priority"
|
||||
| "weighted"
|
||||
| "round-robin"
|
||||
| "context-relay"
|
||||
| "strict-random"
|
||||
| "random"
|
||||
| "least-used"
|
||||
| "cost-optimized"
|
||||
| "auto";
|
||||
autoRoutingStrategy?: "rules" | "cost" | "eco" | "latency" | "fast";
|
||||
strategy: RoutingStrategyValue;
|
||||
autoRoutingStrategy?: AutoRoutingStrategyValue;
|
||||
}) {
|
||||
const start = Date.now();
|
||||
try {
|
||||
@@ -424,8 +420,9 @@ export async function handleSetRoutingStrategy(args: {
|
||||
Object.keys(toRecord(combo.config)).length > 0 ? combo.config : comboData.config
|
||||
);
|
||||
|
||||
const normalizedStrategy = normalizeRoutingStrategy(args.strategy);
|
||||
let nextConfig: JsonRecord | undefined = undefined;
|
||||
if (args.strategy === "auto" && args.autoRoutingStrategy) {
|
||||
if (normalizedStrategy === "auto" && args.autoRoutingStrategy) {
|
||||
const currentAutoConfig = toRecord(currentConfig.auto);
|
||||
nextConfig = {
|
||||
...currentConfig,
|
||||
@@ -436,7 +433,7 @@ export async function handleSetRoutingStrategy(args: {
|
||||
};
|
||||
}
|
||||
|
||||
const payload: JsonRecord = { strategy: args.strategy };
|
||||
const payload: JsonRecord = { strategy: normalizedStrategy };
|
||||
if (nextConfig && Object.keys(nextConfig).length > 0) {
|
||||
payload.config = nextConfig;
|
||||
}
|
||||
@@ -451,16 +448,18 @@ export async function handleSetRoutingStrategy(args: {
|
||||
const updatedConfig = toRecord(updatedCombo.config);
|
||||
const resolvedAutoStrategy =
|
||||
toString(toRecord(updatedConfig.auto).routingStrategy) ||
|
||||
(args.strategy === "auto" ? (args.autoRoutingStrategy ?? "rules") : "");
|
||||
(normalizedStrategy === "auto" ? (args.autoRoutingStrategy ?? "rules") : "");
|
||||
|
||||
const result = {
|
||||
success: true,
|
||||
combo: {
|
||||
id: toString(updatedCombo.id, comboId),
|
||||
name: toString(updatedCombo.name, toString(combo.name, comboId)),
|
||||
strategy: toString(updatedCombo.strategy, args.strategy),
|
||||
strategy: toString(updatedCombo.strategy, normalizedStrategy),
|
||||
autoRoutingStrategy:
|
||||
toString(updatedCombo.strategy, args.strategy) === "auto" ? resolvedAutoStrategy : null,
|
||||
toString(updatedCombo.strategy, normalizedStrategy) === "auto"
|
||||
? resolvedAutoStrategy
|
||||
: null,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
@@ -13,7 +13,12 @@ import {
|
||||
} from "../../../src/lib/db/compression.ts";
|
||||
import { getCompressionAnalyticsSummary } from "../../../src/lib/db/compressionAnalytics.ts";
|
||||
import { getCacheStatsSummary } from "../../../src/lib/db/compressionCacheStats.ts";
|
||||
import { listCompressionCombos } from "../../../src/lib/db/compressionCombos.ts";
|
||||
import type { McpToolExtraLike } from "../scopeEnforcement.ts";
|
||||
import {
|
||||
getMcpDescriptionCompressionStats,
|
||||
snapshotMcpDescriptionCompressionStats,
|
||||
} from "../descriptionCompressor.ts";
|
||||
|
||||
/**
|
||||
* Handle compression_status tool: return current compression config, analytics, and cache stats
|
||||
@@ -26,14 +31,42 @@ export async function handleCompressionStatus(
|
||||
strategy: string;
|
||||
settings: {
|
||||
maxTokens: number;
|
||||
autoTriggerMode: string;
|
||||
targetRatio: number;
|
||||
aggressiveness: string;
|
||||
preserveSystemPrompt: boolean;
|
||||
mcpDescriptionCompressionEnabled: boolean;
|
||||
};
|
||||
analytics: {
|
||||
totalRequests: number;
|
||||
compressedRequests: number;
|
||||
tokensSaved: number;
|
||||
avgCompressionRatio: number;
|
||||
byMode: Record<string, { count: number; tokensSaved: number; avgSavingsPct: number }>;
|
||||
byEngine: Record<string, { count: number; tokensSaved: number; avgSavingsPct: number }>;
|
||||
byCompressionCombo: Record<string, { count: number; tokensSaved: number }>;
|
||||
validationFallbacks: number;
|
||||
requestsWithReceipts: number;
|
||||
realUsage: {
|
||||
requestsWithReceipts: number;
|
||||
promptTokens: number;
|
||||
completionTokens: number;
|
||||
totalTokens: number;
|
||||
cacheReadTokens: number;
|
||||
cacheWriteTokens: number;
|
||||
estimatedUsdSaved: number;
|
||||
bySource: Record<string, number>;
|
||||
};
|
||||
mcpDescriptionCompression: {
|
||||
descriptionsCompressed: number;
|
||||
charsBefore: number;
|
||||
charsAfter: number;
|
||||
charsSaved: number;
|
||||
estimatedTokensSaved: number;
|
||||
persistedEstimatedTokensSaved: number;
|
||||
persistedSnapshots: number;
|
||||
source: "mcp_metadata_estimate";
|
||||
notProviderUsage: true;
|
||||
};
|
||||
};
|
||||
cacheStats: {
|
||||
hits: number;
|
||||
@@ -45,7 +78,9 @@ export async function handleCompressionStatus(
|
||||
const start = Date.now();
|
||||
try {
|
||||
const settings = await getCompressionSettings();
|
||||
await snapshotMcpDescriptionCompressionStats();
|
||||
const analyticsSummary = getCompressionAnalyticsSummary();
|
||||
const mcpDescriptionStats = getMcpDescriptionCompressionStats();
|
||||
const cacheStats = getCacheStatsSummary();
|
||||
|
||||
const result = {
|
||||
@@ -53,14 +88,37 @@ export async function handleCompressionStatus(
|
||||
strategy: settings.defaultMode || "standard",
|
||||
settings: {
|
||||
maxTokens: settings.autoTriggerTokens,
|
||||
autoTriggerMode: settings.autoTriggerMode ?? "lite",
|
||||
targetRatio: 0.7, // Default target ratio
|
||||
aggressiveness: settings.defaultMode || "standard",
|
||||
preserveSystemPrompt: settings.preserveSystemPrompt,
|
||||
mcpDescriptionCompressionEnabled: settings.mcpDescriptionCompressionEnabled !== false,
|
||||
},
|
||||
analytics: {
|
||||
totalRequests: analyticsSummary.totalRequests,
|
||||
compressedRequests: analyticsSummary.byMode?.standard?.count || 0,
|
||||
compressedRequests: Object.values(analyticsSummary.byMode ?? {}).reduce(
|
||||
(sum, mode) => sum + mode.count,
|
||||
0
|
||||
),
|
||||
tokensSaved: analyticsSummary.totalTokensSaved,
|
||||
avgCompressionRatio: analyticsSummary.byMode?.standard?.avgSavingsPct || 0,
|
||||
avgCompressionRatio: analyticsSummary.avgSavingsPct,
|
||||
byMode: analyticsSummary.byMode ?? {},
|
||||
byEngine: analyticsSummary.byEngine ?? {},
|
||||
byCompressionCombo: analyticsSummary.byCompressionCombo ?? {},
|
||||
validationFallbacks: analyticsSummary.validationFallbacks,
|
||||
requestsWithReceipts: analyticsSummary.realUsage.requestsWithReceipts,
|
||||
realUsage: analyticsSummary.realUsage,
|
||||
mcpDescriptionCompression: {
|
||||
descriptionsCompressed: mcpDescriptionStats.descriptionsCompressed,
|
||||
charsBefore: mcpDescriptionStats.charsBefore,
|
||||
charsAfter: mcpDescriptionStats.charsAfter,
|
||||
charsSaved: mcpDescriptionStats.charsSaved,
|
||||
estimatedTokensSaved: mcpDescriptionStats.estimatedTokensSaved,
|
||||
persistedEstimatedTokensSaved:
|
||||
analyticsSummary.mcpDescriptionCompression.estimatedTokensSaved,
|
||||
persistedSnapshots: analyticsSummary.mcpDescriptionCompression.snapshots,
|
||||
source: "mcp_metadata_estimate" as const,
|
||||
notProviderUsage: true as const,
|
||||
},
|
||||
},
|
||||
cacheStats: cacheStats
|
||||
? {
|
||||
@@ -98,9 +156,11 @@ export async function handleCompressionConfigure(
|
||||
args: {
|
||||
enabled?: boolean;
|
||||
strategy?: string;
|
||||
autoTriggerMode?: string;
|
||||
maxTokens?: number;
|
||||
targetRatio?: number;
|
||||
aggressiveness?: string;
|
||||
preserveSystemPrompt?: boolean;
|
||||
mcpDescriptionCompressionEnabled?: boolean;
|
||||
},
|
||||
extra?: McpToolExtraLike
|
||||
): Promise<{
|
||||
@@ -109,9 +169,11 @@ export async function handleCompressionConfigure(
|
||||
settings: {
|
||||
enabled: boolean;
|
||||
strategy: string;
|
||||
autoTriggerMode: string;
|
||||
maxTokens: number;
|
||||
targetRatio: number;
|
||||
aggressiveness: string;
|
||||
preserveSystemPrompt: boolean;
|
||||
mcpDescriptionCompressionEnabled: boolean;
|
||||
};
|
||||
}> {
|
||||
const start = Date.now();
|
||||
@@ -124,11 +186,17 @@ export async function handleCompressionConfigure(
|
||||
if (args.strategy !== undefined) {
|
||||
updates.defaultMode = args.strategy;
|
||||
}
|
||||
if (args.autoTriggerMode !== undefined) {
|
||||
updates.autoTriggerMode = args.autoTriggerMode;
|
||||
}
|
||||
if (args.maxTokens !== undefined) {
|
||||
updates.autoTriggerTokens = args.maxTokens;
|
||||
}
|
||||
if (args.aggressiveness !== undefined) {
|
||||
updates.defaultMode = args.aggressiveness;
|
||||
if (args.preserveSystemPrompt !== undefined) {
|
||||
updates.preserveSystemPrompt = args.preserveSystemPrompt;
|
||||
}
|
||||
if (args.mcpDescriptionCompressionEnabled !== undefined) {
|
||||
updates.mcpDescriptionCompressionEnabled = args.mcpDescriptionCompressionEnabled;
|
||||
}
|
||||
|
||||
const settings = await updateCompressionSettings(updates);
|
||||
@@ -139,9 +207,11 @@ export async function handleCompressionConfigure(
|
||||
settings: {
|
||||
enabled: settings.enabled,
|
||||
strategy: settings.defaultMode || "standard",
|
||||
autoTriggerMode: settings.autoTriggerMode ?? "lite",
|
||||
maxTokens: settings.autoTriggerTokens,
|
||||
targetRatio: 0.7, // Default target ratio
|
||||
aggressiveness: settings.defaultMode || "standard",
|
||||
preserveSystemPrompt: settings.preserveSystemPrompt,
|
||||
mcpDescriptionCompressionEnabled: settings.mcpDescriptionCompressionEnabled !== false,
|
||||
},
|
||||
};
|
||||
|
||||
@@ -165,7 +235,64 @@ export async function handleCompressionConfigure(
|
||||
}
|
||||
|
||||
import { z } from "zod";
|
||||
import { compressionStatusInput, compressionConfigureInput } from "../schemas/tools.ts";
|
||||
import {
|
||||
compressionStatusInput,
|
||||
compressionConfigureInput,
|
||||
setCompressionEngineInput,
|
||||
listCompressionCombosInput,
|
||||
compressionComboStatsInput,
|
||||
} from "../schemas/tools.ts";
|
||||
|
||||
export async function handleSetCompressionEngine(
|
||||
args: z.infer<typeof setCompressionEngineInput>
|
||||
): Promise<{ success: boolean; settings: Record<string, unknown> }> {
|
||||
const updates: Record<string, unknown> = { enabled: true };
|
||||
if (args.engine) {
|
||||
updates.defaultMode = args.engine === "caveman" ? "standard" : args.engine;
|
||||
if (args.engine === "off") updates.enabled = false;
|
||||
}
|
||||
if (args.cavemanIntensity) {
|
||||
const current = await getCompressionSettings();
|
||||
updates.cavemanConfig = {
|
||||
...(current.cavemanConfig ?? {}),
|
||||
intensity: args.cavemanIntensity,
|
||||
};
|
||||
}
|
||||
if (args.rtkIntensity) {
|
||||
const current = await getCompressionSettings();
|
||||
updates.rtkConfig = {
|
||||
...(current.rtkConfig ?? {}),
|
||||
intensity: args.rtkIntensity,
|
||||
};
|
||||
}
|
||||
if (args.outputMode !== undefined) {
|
||||
const current = await getCompressionSettings();
|
||||
updates.cavemanOutputMode = {
|
||||
...(current.cavemanOutputMode ?? {}),
|
||||
enabled: args.outputMode,
|
||||
};
|
||||
}
|
||||
const settings = await updateCompressionSettings(updates);
|
||||
return { success: true, settings: settings as unknown as Record<string, unknown> };
|
||||
}
|
||||
|
||||
export async function handleListCompressionCombos(): Promise<{
|
||||
combos: ReturnType<typeof listCompressionCombos>;
|
||||
}> {
|
||||
return { combos: listCompressionCombos() };
|
||||
}
|
||||
|
||||
export async function handleCompressionComboStats(
|
||||
args: z.infer<typeof compressionComboStatsInput>
|
||||
): Promise<Record<string, unknown>> {
|
||||
const summary = getCompressionAnalyticsSummary(args.since === "all" ? undefined : args.since);
|
||||
if (!args.comboId) return summary as unknown as Record<string, unknown>;
|
||||
return {
|
||||
comboId: args.comboId,
|
||||
summary,
|
||||
combo: summary.byCompressionCombo[args.comboId] ?? { count: 0, tokensSaved: 0 },
|
||||
};
|
||||
}
|
||||
|
||||
export const compressionTools = {
|
||||
omniroute_compression_status: {
|
||||
@@ -178,8 +305,27 @@ export const compressionTools = {
|
||||
omniroute_compression_configure: {
|
||||
name: "omniroute_compression_configure",
|
||||
description:
|
||||
"Configure compression settings at runtime. Supports enabling/disabling compression, changing strategy (none/standard/aggressive/ultra), adjusting maxTokens threshold, targetRatio, and aggressiveness level.",
|
||||
"Configure compression settings at runtime. Supports enabling/disabling compression, changing strategy (off/lite/standard/aggressive/ultra/rtk/stacked), adjusting maxTokens threshold, targetRatio, auto-trigger mode, system prompt preservation, and MCP description compression.",
|
||||
inputSchema: compressionConfigureInput,
|
||||
handler: (args: z.infer<typeof compressionConfigureInput>) => handleCompressionConfigure(args),
|
||||
},
|
||||
omniroute_set_compression_engine: {
|
||||
name: "omniroute_set_compression_engine",
|
||||
description: "Set the active compression engine and Caveman/RTK runtime options.",
|
||||
inputSchema: setCompressionEngineInput,
|
||||
handler: (args: z.infer<typeof setCompressionEngineInput>) => handleSetCompressionEngine(args),
|
||||
},
|
||||
omniroute_list_compression_combos: {
|
||||
name: "omniroute_list_compression_combos",
|
||||
description: "List compression combos and their engine pipelines.",
|
||||
inputSchema: listCompressionCombosInput,
|
||||
handler: (_args: z.infer<typeof listCompressionCombosInput>) => handleListCompressionCombos(),
|
||||
},
|
||||
omniroute_compression_combo_stats: {
|
||||
name: "omniroute_compression_combo_stats",
|
||||
description: "Get compression analytics grouped by engine and compression combo.",
|
||||
inputSchema: compressionComboStatsInput,
|
||||
handler: (args: z.infer<typeof compressionComboStatsInput>) =>
|
||||
handleCompressionComboStats(args),
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{
|
||||
"name": "@omniroute/open-sse",
|
||||
"version": "3.7.8",
|
||||
"version": "3.7.9",
|
||||
"description": "Express SSE sidecar for OmniRoute — handles streaming, protocol translation, and provider orchestration",
|
||||
"type": "module",
|
||||
"main": "index.js",
|
||||
|
||||
@@ -51,12 +51,15 @@
|
||||
### Prompt Compression Pipeline
|
||||
|
||||
- **`compression/`** — Modular prompt compression running proactively before `contextManager.ts`.
|
||||
- `strategySelector.ts` — Selects mode (off/lite/standard/aggressive/ultra) with combo overrides and auto-trigger.
|
||||
- `strategySelector.ts` — Selects mode (off/lite/standard/aggressive/ultra/rtk/stacked) with compression combo assignments, combo overrides, and auto-trigger.
|
||||
- `lite.ts` — 5 lite techniques: whitespace collapse, system prompt dedup, tool result truncation, redundant removal, image URL placeholder.
|
||||
- `caveman.ts` / `cavemanRules.ts` — Caveman-style semantic condensation with file-loaded rule packs and language-aware rule selection.
|
||||
- `engines/registry.ts` — Engine registry used by standalone RTK/Caveman execution and stacked pipelines.
|
||||
- `engines/rtk/` — RTK tool-output compression: command detection, JSON filter packs, deduplication, smart truncation, ANSI/code noise stripping.
|
||||
- `stats.ts` — Per-request compression stats (original/compressed tokens, savings %, techniques).
|
||||
- `types.ts` — Shared types (`CompressionMode`, `CompressionConfig`, `CompressionStats`, `CompressionResult`).
|
||||
- `index.ts` — Barrel re-exports.
|
||||
- Phase 1: lite mode only. Standard/aggressive/ultra = Phase 2.
|
||||
- Dashboard/API surface: `/dashboard/context/caveman`, `/dashboard/context/rtk`, `/dashboard/context/combos`, `/api/context/*`, and `/api/compression/preview`.
|
||||
|
||||
### Auto-Routing & Adaptive
|
||||
|
||||
|
||||
@@ -511,12 +511,27 @@ export function getAllModelLockouts() {
|
||||
// ─── Provider Breaker Compatibility Wrappers ────────────────────────────────
|
||||
// Legacy helpers now delegate to the shared provider circuit breaker.
|
||||
|
||||
type ProviderBreakerProfile = Partial<
|
||||
Pick<
|
||||
ProviderProfile,
|
||||
"failureThreshold" | "resetTimeoutMs" | "circuitBreakerThreshold" | "circuitBreakerReset"
|
||||
>
|
||||
>;
|
||||
|
||||
function getProviderBreaker(provider: string | null | undefined) {
|
||||
return provider ? getCircuitBreaker(provider) : null;
|
||||
}
|
||||
|
||||
function configureProviderBreaker(
|
||||
provider: string | null | undefined,
|
||||
profile?: ProviderBreakerProfile | null
|
||||
) {
|
||||
if (!provider) return null;
|
||||
const profile = getProviderProfile(provider);
|
||||
|
||||
const resolvedProfile = { ...getProviderProfile(provider), ...(profile ?? {}) };
|
||||
return getCircuitBreaker(provider, {
|
||||
failureThreshold: profile.failureThreshold ?? profile.circuitBreakerThreshold,
|
||||
resetTimeout: profile.resetTimeoutMs ?? profile.circuitBreakerReset,
|
||||
failureThreshold: resolvedProfile.failureThreshold ?? resolvedProfile.circuitBreakerThreshold,
|
||||
resetTimeout: resolvedProfile.resetTimeoutMs ?? resolvedProfile.circuitBreakerReset,
|
||||
});
|
||||
}
|
||||
|
||||
@@ -551,7 +566,8 @@ export function getProviderCooldownRemainingMs(provider: string | null | undefin
|
||||
export function recordProviderFailure(
|
||||
provider: string | null | undefined,
|
||||
log?: { warn?: (...args: unknown[]) => void },
|
||||
connectionId?: string | null
|
||||
connectionId?: string | null,
|
||||
profile?: ProviderBreakerProfile | null
|
||||
): void {
|
||||
if (!provider) return;
|
||||
|
||||
@@ -570,7 +586,7 @@ export function recordProviderFailure(
|
||||
lastConnectionFailure.set(dedupKey, now);
|
||||
}
|
||||
|
||||
const breaker = getProviderBreaker(provider);
|
||||
const breaker = configureProviderBreaker(provider, profile);
|
||||
if (!breaker) return;
|
||||
|
||||
if (!breaker.canExecute()) return;
|
||||
|
||||
@@ -15,13 +15,4 @@ export {
|
||||
export { getTaskFitness, getTaskTypes } from "./taskFitness";
|
||||
export { SelfHealingManager, getSelfHealingManager } from "./selfHealing";
|
||||
export { MODE_PACKS, getModePack, getModePackNames } from "./modePacks";
|
||||
export {
|
||||
selectProvider,
|
||||
createAutoCombo,
|
||||
getAutoCombo,
|
||||
updateAutoCombo,
|
||||
deleteAutoCombo,
|
||||
listAutoCombos,
|
||||
type AutoComboConfig,
|
||||
type SelectionResult,
|
||||
} from "./engine";
|
||||
export { selectProvider, type AutoComboConfig, type SelectionResult } from "./engine";
|
||||
|
||||
@@ -48,6 +48,7 @@ import {
|
||||
resolveRequestRoutingTags,
|
||||
type RoutingTagMatchMode,
|
||||
} from "../../src/domain/tagRouter.ts";
|
||||
import { normalizeRoutingStrategy } from "../../src/shared/constants/routingStrategies.ts";
|
||||
|
||||
// Status codes that should mark round-robin target semaphores as cooling down.
|
||||
const TRANSIENT_FOR_SEMAPHORE = [429, 502, 503, 504];
|
||||
@@ -630,17 +631,25 @@ function sortTargetsByUsage(targets: ResolvedComboTarget[], comboName: string) {
|
||||
*/
|
||||
function sortModelsByContextSize(models) {
|
||||
const withContext = models.map((modelStr) => {
|
||||
const parsed = parseModel(modelStr);
|
||||
const provider = parsed.provider || parsed.providerAlias || "unknown";
|
||||
const model = parsed.model || modelStr;
|
||||
const limit = getModelContextLimit(provider, model);
|
||||
return { modelStr, context: limit ?? 0 };
|
||||
return { modelStr, context: getModelContextLimitForModelString(modelStr) ?? 0 };
|
||||
});
|
||||
withContext.sort((a, b) => b.context - a.context);
|
||||
return withContext.map((e) => e.modelStr);
|
||||
}
|
||||
|
||||
function getModelContextLimitForModelString(modelStr: string) {
|
||||
const parsed = parseModel(modelStr);
|
||||
const provider = parsed.provider || parsed.providerAlias || "unknown";
|
||||
const model = parsed.model || modelStr;
|
||||
return getModelContextLimit(provider, model);
|
||||
}
|
||||
|
||||
function sortTargetsByContextSize(targets: ResolvedComboTarget[]) {
|
||||
const hasKnownContext = targets.some(
|
||||
(target) => getModelContextLimitForModelString(target.modelStr) != null
|
||||
);
|
||||
if (!hasKnownContext) return targets;
|
||||
|
||||
const orderedModels = sortModelsByContextSize(targets.map((target) => target.modelStr));
|
||||
const byModel = new Map<string, ResolvedComboTarget[]>();
|
||||
for (const target of targets) {
|
||||
@@ -656,6 +665,38 @@ function sortTargetsByContextSize(targets: ResolvedComboTarget[]) {
|
||||
.filter((target): target is ResolvedComboTarget => target !== null);
|
||||
}
|
||||
|
||||
function getP2CTargetScore(
|
||||
target: ResolvedComboTarget,
|
||||
metrics: ReturnType<typeof getComboMetrics>
|
||||
): number {
|
||||
const breakerState = getCircuitBreaker(target.provider)?.getStatus?.()?.state;
|
||||
if (breakerState === "OPEN") return -Infinity;
|
||||
const modelMetric = metrics?.byModel?.[target.modelStr] || null;
|
||||
const successRate = Number(modelMetric?.successRate);
|
||||
const avgLatency = Number(modelMetric?.avgLatencyMs);
|
||||
const successScore = Number.isFinite(successRate) ? successRate / 100 : 0.5;
|
||||
const latencyScore =
|
||||
Number.isFinite(avgLatency) && avgLatency > 0 ? 1 / Math.log10(avgLatency + 10) : 0.25;
|
||||
const breakerPenalty = breakerState === "HALF_OPEN" ? 0.25 : 0;
|
||||
return successScore + latencyScore - breakerPenalty;
|
||||
}
|
||||
|
||||
function orderTargetsByPowerOfTwoChoices(targets: ResolvedComboTarget[], comboName: string) {
|
||||
if (targets.length <= 1) return targets;
|
||||
const metrics = getComboMetrics(comboName);
|
||||
const firstIndex = Math.floor(Math.random() * targets.length);
|
||||
let secondIndex = Math.floor(Math.random() * (targets.length - 1));
|
||||
if (secondIndex >= firstIndex) secondIndex++;
|
||||
|
||||
const first = targets[firstIndex];
|
||||
const second = targets[secondIndex];
|
||||
const selectedIndex =
|
||||
getP2CTargetScore(second, metrics) > getP2CTargetScore(first, metrics)
|
||||
? secondIndex
|
||||
: firstIndex;
|
||||
return [targets[selectedIndex], ...targets.filter((_, index) => index !== selectedIndex)];
|
||||
}
|
||||
|
||||
function toTextContent(content) {
|
||||
if (typeof content === "string") return content;
|
||||
if (!Array.isArray(content)) return "";
|
||||
@@ -1034,7 +1075,7 @@ export async function handleComboChat({
|
||||
relayOptions,
|
||||
signal,
|
||||
}) {
|
||||
const strategy = combo.strategy || "priority";
|
||||
const strategy = normalizeRoutingStrategy(combo.strategy || "priority");
|
||||
const relayConfig =
|
||||
strategy === "context-relay" ? resolveContextRelayConfig(relayOptions?.config || null) : null;
|
||||
|
||||
@@ -1427,6 +1468,14 @@ export async function handleComboChat({
|
||||
} else if (strategy === "random") {
|
||||
orderedTargets = fisherYatesShuffle([...orderedTargets]);
|
||||
log.info("COMBO", `Random shuffle: ${orderedTargets.length} targets`);
|
||||
} else if (strategy === "fill-first") {
|
||||
log.info(
|
||||
"COMBO",
|
||||
`Fill-first ordering: preserving priority order (${orderedTargets.length} targets)`
|
||||
);
|
||||
} else if (strategy === "p2c") {
|
||||
orderedTargets = orderTargetsByPowerOfTwoChoices(orderedTargets, combo.name);
|
||||
log.info("COMBO", `Power-of-two-choices ordering: selected ${orderedTargets[0]?.modelStr}`);
|
||||
} else if (strategy === "least-used") {
|
||||
orderedTargets = sortTargetsByUsage(orderedTargets, combo.name);
|
||||
log.info("COMBO", `Least-used ordering: ${orderedTargets[0]?.modelStr} has fewest requests`);
|
||||
@@ -1678,7 +1727,7 @@ export async function handleComboChat({
|
||||
|
||||
// Trigger shared provider circuit breaker for 5xx errors and connection failures
|
||||
if (isProviderFailureCode(result.status)) {
|
||||
recordProviderFailure(provider, log, target.connectionId);
|
||||
recordProviderFailure(provider, log, target.connectionId, profile);
|
||||
}
|
||||
|
||||
// Check if this is a transient error worth retrying on same model
|
||||
|
||||
@@ -5,39 +5,19 @@ import { applyAging } from "./progressiveAging.ts";
|
||||
import { RuleBasedSummarizer } from "./summarizer.ts";
|
||||
import { cavemanCompress } from "./caveman.ts";
|
||||
import { applyLiteCompression } from "./lite.ts";
|
||||
import { extractTextContent, replaceTextContent, type ChatMessageLike } from "./messageContent.ts";
|
||||
|
||||
const COMPRESSED_MARKER_RE = /^\[COMPRESSED:/;
|
||||
|
||||
interface ChatMessage {
|
||||
role: string;
|
||||
content?: string | Array<{ type: string; text?: string }>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
type ChatMessage = ChatMessageLike;
|
||||
|
||||
interface AggressiveCompressionResult {
|
||||
messages: ChatMessage[];
|
||||
stats: CompressionStats;
|
||||
}
|
||||
|
||||
function extractText(content?: string | Array<{ type: string; text?: string }>): string {
|
||||
if (typeof content === "string") return content;
|
||||
if (Array.isArray(content)) {
|
||||
return content
|
||||
.filter(
|
||||
(p): p is { type: string; text?: string } =>
|
||||
typeof p === "object" && p !== null && "text" in p
|
||||
)
|
||||
.map((p) => p.text ?? "")
|
||||
.join("\n");
|
||||
}
|
||||
return "";
|
||||
}
|
||||
|
||||
function setContent(msg: ChatMessage, newContent: string): ChatMessage {
|
||||
if (typeof msg.content === "string") {
|
||||
return { ...msg, content: newContent };
|
||||
}
|
||||
return { ...msg, content: [{ type: "text", text: newContent }] };
|
||||
return replaceTextContent(msg, newContent) as ChatMessage;
|
||||
}
|
||||
|
||||
function estimateTokens(text: string): number {
|
||||
@@ -71,7 +51,7 @@ export function compressAggressive(
|
||||
};
|
||||
|
||||
const originalTokens = messages.reduce(
|
||||
(sum, m) => sum + estimateTokens(extractText(m.content)),
|
||||
(sum, m) => sum + estimateTokens(extractTextContent(m.content)),
|
||||
0
|
||||
);
|
||||
resultStats.originalTokens = originalTokens;
|
||||
@@ -84,8 +64,9 @@ export function compressAggressive(
|
||||
// Step 1: Tool-result compression
|
||||
try {
|
||||
const afterToolResult = currentMessages.map((msg) => {
|
||||
if (cfg.preserveSystemPrompt !== false && msg.role === "system") return msg;
|
||||
if (msg.role !== "tool" && msg.role !== "function") return msg;
|
||||
const text = extractText(msg.content);
|
||||
const text = extractTextContent(msg.content);
|
||||
if (!text || COMPRESSED_MARKER_RE.test(text)) return msg;
|
||||
|
||||
const result = compressToolResult(text, cfg.toolStrategies);
|
||||
@@ -101,7 +82,12 @@ export function compressAggressive(
|
||||
|
||||
// Step 2: Progressive aging
|
||||
try {
|
||||
const agingResult = applyAging(currentMessages, cfg.thresholds, summarizer);
|
||||
const agingResult = applyAging(
|
||||
currentMessages,
|
||||
cfg.thresholds,
|
||||
summarizer,
|
||||
cfg.preserveSystemPrompt !== false
|
||||
);
|
||||
agingSavings = agingResult.saved;
|
||||
currentMessages = agingResult.messages as ChatMessage[];
|
||||
} catch (err) {
|
||||
@@ -112,7 +98,8 @@ export function compressAggressive(
|
||||
if (cfg.summarizerEnabled) {
|
||||
try {
|
||||
currentMessages = currentMessages.map((msg) => {
|
||||
const text = extractText(msg.content);
|
||||
if (cfg.preserveSystemPrompt !== false && msg.role === "system") return msg;
|
||||
const text = extractTextContent(msg.content);
|
||||
if (!text || COMPRESSED_MARKER_RE.test(text)) return msg;
|
||||
if (text.length <= cfg.maxTokensPerMessage * 4) return msg;
|
||||
|
||||
@@ -133,7 +120,7 @@ export function compressAggressive(
|
||||
|
||||
// Downgrade chain: if total savings < threshold, try caveman then lite
|
||||
const compressedTokens = currentMessages.reduce(
|
||||
(sum, m) => sum + estimateTokens(extractText(m.content)),
|
||||
(sum, m) => sum + estimateTokens(extractTextContent(m.content)),
|
||||
0
|
||||
);
|
||||
resultStats.compressedTokens = compressedTokens;
|
||||
@@ -157,7 +144,10 @@ export function compressAggressive(
|
||||
}
|
||||
|
||||
try {
|
||||
const liteResult = applyLiteCompression({ messages: currentMessages });
|
||||
const liteResult = applyLiteCompression(
|
||||
{ messages: currentMessages },
|
||||
{ preserveSystemPrompt: cfg.preserveSystemPrompt !== false }
|
||||
);
|
||||
if (liteResult?.compressed && liteResult.stats) {
|
||||
const liteSavings = liteResult.stats.savingsPercent ?? 0;
|
||||
if (liteSavings > resultStats.savingsPercent) {
|
||||
|
||||
@@ -1,12 +1,22 @@
|
||||
import type { CavemanConfig, CavemanRule, CompressionResult, CompressionMode } from "./types.ts";
|
||||
import type {
|
||||
CavemanConfig,
|
||||
CavemanRule,
|
||||
CompressionResult,
|
||||
CompressionMode,
|
||||
CompressionStats,
|
||||
} from "./types.ts";
|
||||
import { DEFAULT_CAVEMAN_CONFIG } from "./types.ts";
|
||||
import { CAVEMAN_RULES, getRulesForContext } from "./cavemanRules.ts";
|
||||
import { extractPreservedBlocks, restorePreservedBlocks } from "./preservation.ts";
|
||||
import { createCompressionStats, estimateCompressionTokens } from "./stats.ts";
|
||||
import { validateCompression } from "./validation.ts";
|
||||
import { mapTextContent } from "./messageContent.ts";
|
||||
import { detectCompressionLanguage } from "./languageDetector.ts";
|
||||
|
||||
interface ChatMessage {
|
||||
role: string;
|
||||
content?: string | Array<{ type: string; text?: string }>;
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
interface ChatRequestBody {
|
||||
@@ -14,14 +24,165 @@ interface ChatRequestBody {
|
||||
[key: string]: unknown;
|
||||
}
|
||||
|
||||
const RULE_KEYWORDS: Record<string, string[]> = {
|
||||
redundant_phrasing: ["make sure", "be sure"],
|
||||
redundant_because: ["due to the fact", "the reason is because"],
|
||||
redundant_directive: ["it is important", "you should", "remember to"],
|
||||
pleasantries: [
|
||||
"sure",
|
||||
"certainly",
|
||||
"of course",
|
||||
"happy to",
|
||||
"thanks",
|
||||
"thank you",
|
||||
"glad to help",
|
||||
"glad to",
|
||||
"no problem",
|
||||
"you're welcome",
|
||||
"youre welcome",
|
||||
"absolutely",
|
||||
],
|
||||
polite_framing: [
|
||||
"please",
|
||||
"kindly",
|
||||
"could you please",
|
||||
"would you please",
|
||||
"can you please",
|
||||
"i would like you",
|
||||
"i want you",
|
||||
"i need you",
|
||||
],
|
||||
hedging: [
|
||||
"it seems like",
|
||||
"it appears that",
|
||||
"i think that",
|
||||
"i believe that",
|
||||
"probably",
|
||||
"possibly",
|
||||
"maybe it",
|
||||
],
|
||||
verbose_instructions: [
|
||||
"provide a detailed",
|
||||
"give me a comprehensive",
|
||||
"write an in-depth",
|
||||
"create a thorough",
|
||||
"explain in detail",
|
||||
],
|
||||
filler_adverbs: ["basically", "essentially", "actually", "literally", "simply", "currently"],
|
||||
filler_phrases: ["i want to", "i need to", "i'd like to", "i'm looking for"],
|
||||
redundant_openers: ["hi there", "hello", "good morning", "hey"],
|
||||
verbose_requests: ["i was wondering", "would it be possible"],
|
||||
leader_phrases: [
|
||||
"i'll",
|
||||
"i will",
|
||||
"i can",
|
||||
"i'd",
|
||||
"let me",
|
||||
"you can",
|
||||
"we will",
|
||||
"we can",
|
||||
"let's",
|
||||
],
|
||||
self_reference: ["i am trying to", "i am working on", "i have been"],
|
||||
excessive_gratitude: ["thank you so much", "thanks in advance", "i really appreciate"],
|
||||
qualifier_removal: ["a bit", "a little", "somewhat", "kind of", "sort of"],
|
||||
softeners: ["if possible", "when you get a chance", "at your convenience", "just wondering"],
|
||||
uncertainty_fillers: ["i guess", "i suppose", "more or less", "in a way"],
|
||||
assistant_fillers: ["here's", "below is", "this is"],
|
||||
compound_collapse: ["and any potential"],
|
||||
explanatory_prefix: [
|
||||
"the function appears to be handling",
|
||||
"the code seems to",
|
||||
"the class is",
|
||||
"this module is",
|
||||
],
|
||||
question_to_directive: [
|
||||
"can you explain why",
|
||||
"could you show me how",
|
||||
"would you tell me",
|
||||
"can you tell me",
|
||||
],
|
||||
context_setup: ["i have the following code", "here is my code", "below is the code"],
|
||||
intent_clarification: [
|
||||
"what i'm trying to do",
|
||||
"my objective is to",
|
||||
"what i need is",
|
||||
"i'm aiming to",
|
||||
],
|
||||
background_removal: ["as you may know", "as we discussed earlier"],
|
||||
meta_commentary: ["note that", "keep in mind", "remember that"],
|
||||
purpose_statement: ["for the purpose of", "with the goal of", "in an effort to", "for every"],
|
||||
list_conjunction: ["and also", "as well as"],
|
||||
purpose_phrases: ["in order to", "so as to"],
|
||||
redundant_quantifiers: ["each and every", "any and all"],
|
||||
all_quantifier: ["any and all"],
|
||||
verbose_connectors: ["furthermore", "additionally", "moreover", "in addition"],
|
||||
transition_removal: ["on the other hand", "in contrast", "however"],
|
||||
emphasis_removal: ["very", "really", "extremely", "highly", "quite"],
|
||||
passive_voice: [
|
||||
"is being used",
|
||||
"is being called",
|
||||
"is being generated",
|
||||
"was created",
|
||||
"was generated",
|
||||
"was implemented",
|
||||
],
|
||||
repeated_context: [
|
||||
"as we discussed earlier",
|
||||
"as mentioned before",
|
||||
"as previously stated",
|
||||
"as i said before",
|
||||
],
|
||||
repeated_question: [
|
||||
"same question as before",
|
||||
"i asked this earlier",
|
||||
"this is the same question",
|
||||
],
|
||||
reestablished_context: ["going back to the code above", "referring back to", "returning to"],
|
||||
summary_replacement: ["to summarize", "in summary of our conversation", "to recap"],
|
||||
ultra_abbreviations: ["database"],
|
||||
ultra_config_abbreviation: ["configuration"],
|
||||
ultra_function_abbreviation: ["function"],
|
||||
ultra_request_abbreviation: ["request"],
|
||||
ultra_response_abbreviation: ["response"],
|
||||
ultra_implementation_abbreviation: ["implementation"],
|
||||
ultra_authentication_abbreviation: ["authentication"],
|
||||
ultra_authorization_abbreviation: ["authorization"],
|
||||
ultra_application_abbreviation: ["application"],
|
||||
ultra_dependency_abbreviation: ["dependency", "dependencies"],
|
||||
ultra_common_abbreviations: [
|
||||
"implementation",
|
||||
"authentication",
|
||||
"authorization",
|
||||
"application",
|
||||
"dependency",
|
||||
"dependencies",
|
||||
],
|
||||
};
|
||||
|
||||
const ARTICLE_HINT_RE = /\b(?:a|an|the)\b/;
|
||||
|
||||
function shouldAttemptRule(ruleName: string, lowerText: string): boolean {
|
||||
if (ruleName === "articles") {
|
||||
ARTICLE_HINT_RE.lastIndex = 0;
|
||||
return ARTICLE_HINT_RE.test(lowerText);
|
||||
}
|
||||
|
||||
const keywords = RULE_KEYWORDS[ruleName];
|
||||
return !keywords || keywords.some((keyword) => lowerText.includes(keyword));
|
||||
}
|
||||
|
||||
export function applyRulesToText(
|
||||
text: string,
|
||||
rules: CavemanRule[]
|
||||
): { text: string; appliedRules: string[] } {
|
||||
let result = text;
|
||||
const lowerResult = text.toLowerCase();
|
||||
const appliedRules: string[] = [];
|
||||
|
||||
for (const rule of rules) {
|
||||
if (!shouldAttemptRule(rule.name, lowerResult)) continue;
|
||||
|
||||
const before = result;
|
||||
const { pattern, replacement } = rule;
|
||||
if (typeof replacement === "function") {
|
||||
@@ -43,14 +204,76 @@ export function applyRulesToText(
|
||||
|
||||
function cleanupArtifacts(text: string): string {
|
||||
let result = text;
|
||||
result = result.replace(/ +/g, " ");
|
||||
result = result.replace(/ +$/gm, "");
|
||||
result = result.replace(/\n{3,}/g, "\n\n");
|
||||
result = result.replace(/^\n+/, "");
|
||||
result = result.replace(/\n+$/, "");
|
||||
if (result.includes(" ")) result = result.replace(/[ \t]{2,}/g, " ");
|
||||
if (/[\t ]+[,.;:!?]/.test(result)) result = result.replace(/[ \t]+([,.;:!?])/g, "$1");
|
||||
if (/[.!?]{2,}/.test(result)) result = result.replace(/([.!?]){2,}/g, "$1");
|
||||
if (/[ \t]\n/.test(result)) result = result.replace(/[ \t]+$/gm, "");
|
||||
if (result.endsWith(" ") || result.endsWith("\t")) result = result.trimEnd();
|
||||
if (result.includes("\n\n\n")) result = result.replace(/\n{3,}/g, "\n\n");
|
||||
if (result.startsWith("\n")) result = result.replace(/^\n+/, "");
|
||||
if (result.endsWith("\n")) result = result.replace(/\n+$/, "");
|
||||
return result;
|
||||
}
|
||||
|
||||
function recapitalizeSentences(text: string): string {
|
||||
return text.replace(
|
||||
/(^|[.!?][ \t]+|\n+[ \t]*)([a-z])/g,
|
||||
(_match, prefix: string, char: string) => {
|
||||
return `${prefix}${char.toUpperCase()}`;
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
function createCavemanStats(
|
||||
originalTokens: number,
|
||||
compressedTokens: number,
|
||||
techniquesUsed: string[],
|
||||
rulesApplied: string[] | undefined,
|
||||
durationMs: number
|
||||
): CompressionStats {
|
||||
const savingsPercent =
|
||||
originalTokens > 0
|
||||
? Math.round(((originalTokens - compressedTokens) / originalTokens) * 10000) / 100
|
||||
: 0;
|
||||
return {
|
||||
originalTokens,
|
||||
compressedTokens,
|
||||
savingsPercent,
|
||||
techniquesUsed,
|
||||
mode: "standard",
|
||||
timestamp: Date.now(),
|
||||
...(rulesApplied && rulesApplied.length > 0 ? { rulesApplied } : {}),
|
||||
durationMs,
|
||||
};
|
||||
}
|
||||
|
||||
function compileUserPreservePatterns(patterns: string[]): {
|
||||
patterns: RegExp[];
|
||||
warnings: string[];
|
||||
} {
|
||||
const compiled: RegExp[] = [];
|
||||
const warnings: string[] = [];
|
||||
for (const pattern of patterns) {
|
||||
try {
|
||||
compiled.push(new RegExp(pattern, "g"));
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
warnings.push(`Invalid preservePatterns regex ignored: ${pattern} (${message})`);
|
||||
}
|
||||
}
|
||||
return { patterns: compiled, warnings };
|
||||
}
|
||||
|
||||
const PROTECTED_STRUCTURE_RE =
|
||||
/```|~~~|`|https?:\/\/|\[[^\]\n]{1,1000}\]\([^)[ \t\n]{1,2000}(?:[ \t]+"[^"]{0,1000}")?\)|^#{1,6}\s+|^[ \t]*\|(?:[^|\n]{0,1000}\|){1,100}[ \t]*$|\$\$|\\\[|\\begin\{|^\s*#(?:set|show|let|import|include)\b|\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b|\bprocess\.env\.[A-Za-z_][A-Za-z0-9_]*\b|\$[A-Z_][A-Z0-9_]*\b|\b\d+(?:\.\d+){1,3}(?:[-+][A-Za-z0-9.-]+)?\b|\b[a-zA-Z_$][\w$]*(?:\.[a-zA-Z_$][\w$]*)+\(\)?|\b[A-Za-z_$][\w$]*[ \t]*\([^()\n]{0,1000}\)|(?:^|\s)(?:\.{0,2}\/[A-Za-z0-9_@./-]+|[A-Za-z]:\\[A-Za-z0-9_.\\/-]+)|\b(?:TypeError|ReferenceError|SyntaxError|RangeError|URIError|EvalError|Error|Exception):[^\n]{0,1000}/im;
|
||||
const PROTECTED_STRUCTURE_PREFILTER_RE = /[`~\[\]\|$#\\/:_()0-9]/;
|
||||
|
||||
function hasProtectedStructure(text: string): boolean {
|
||||
if (!PROTECTED_STRUCTURE_PREFILTER_RE.test(text)) return false;
|
||||
PROTECTED_STRUCTURE_RE.lastIndex = 0;
|
||||
return PROTECTED_STRUCTURE_RE.test(text);
|
||||
}
|
||||
|
||||
export function cavemanCompress(
|
||||
body: ChatRequestBody,
|
||||
options?: Partial<CavemanConfig>
|
||||
@@ -80,23 +303,29 @@ export function cavemanCompress(
|
||||
let totalOriginalTokens = 0;
|
||||
let totalCompressedTokens = 0;
|
||||
const allAppliedRules: string[] = [];
|
||||
const validationWarnings: string[] = [];
|
||||
const validationErrors: string[] = [];
|
||||
let fallbackApplied = false;
|
||||
let preservedBlockCount = 0;
|
||||
const customPreservation = compileUserPreservePatterns(config.preservePatterns ?? []);
|
||||
validationWarnings.push(...customPreservation.warnings);
|
||||
|
||||
const compressedMessages = body.messages.map((msg): ChatMessage => {
|
||||
// Only compress simple string content — multi-part messages (arrays)
|
||||
// would duplicate compressed text across all text parts if naively handled
|
||||
if (typeof msg.content !== "string") {
|
||||
const contentStr = Array.isArray(msg.content)
|
||||
? msg.content
|
||||
.map((part) => (part.type === "text" && part.text ? part.text : ""))
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
: "";
|
||||
totalOriginalTokens += estimateCompressionTokens(contentStr);
|
||||
totalCompressedTokens += estimateCompressionTokens(contentStr);
|
||||
if (typeof msg.content !== "string" && !Array.isArray(msg.content)) {
|
||||
return msg;
|
||||
}
|
||||
|
||||
const contentStr = msg.content;
|
||||
const contentStr =
|
||||
typeof msg.content === "string"
|
||||
? msg.content
|
||||
: msg.content
|
||||
.map((part) =>
|
||||
part && typeof part === "object" && "text" in part && typeof part.text === "string"
|
||||
? part.text
|
||||
: ""
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join("\n");
|
||||
totalOriginalTokens += estimateCompressionTokens(contentStr);
|
||||
|
||||
if (!contentStr || contentStr.length < config.minMessageLength) {
|
||||
@@ -109,33 +338,83 @@ export function cavemanCompress(
|
||||
return msg;
|
||||
}
|
||||
|
||||
const { text: extractedText, blocks } = extractPreservedBlocks(contentStr);
|
||||
const compressTextPart = (textPart: string): string => {
|
||||
if (!textPart || textPart.length < config.minMessageLength) return textPart;
|
||||
|
||||
const rules = getRulesForContext(msg.role).filter(
|
||||
(rule) => !config.skipRules.includes(rule.name)
|
||||
);
|
||||
const { text: rulesApplied, appliedRules } = applyRulesToText(extractedText, rules);
|
||||
allAppliedRules.push(...appliedRules);
|
||||
const shouldPreserve =
|
||||
customPreservation.patterns.length > 0 || hasProtectedStructure(textPart);
|
||||
const { text: extractedText, blocks } = shouldPreserve
|
||||
? extractPreservedBlocks(textPart, {
|
||||
preservePatterns: customPreservation.patterns,
|
||||
})
|
||||
: { text: textPart, blocks: [] };
|
||||
preservedBlockCount += blocks.length;
|
||||
|
||||
const restored = restorePreservedBlocks(rulesApplied, blocks);
|
||||
const detectedLanguage = config.autoDetectLanguage
|
||||
? detectCompressionLanguage(textPart)
|
||||
: (config.language ?? "en");
|
||||
const enabledPacks = config.enabledLanguagePacks ?? ["en", detectedLanguage];
|
||||
const language = enabledPacks.includes(detectedLanguage)
|
||||
? detectedLanguage
|
||||
: enabledPacks.includes("en")
|
||||
? "en"
|
||||
: detectedLanguage;
|
||||
const rules = getRulesForContext(msg.role, config.intensity, language).filter(
|
||||
(rule) => !config.skipRules.includes(rule.name)
|
||||
);
|
||||
const { text: rulesApplied, appliedRules } = applyRulesToText(extractedText, rules);
|
||||
allAppliedRules.push(...appliedRules);
|
||||
|
||||
const cleaned = cleanupArtifacts(restored);
|
||||
const normalized = cleanupArtifacts(recapitalizeSentences(rulesApplied));
|
||||
const cleaned =
|
||||
blocks.length > 0
|
||||
? cleanupArtifacts(restorePreservedBlocks(normalized, blocks))
|
||||
: normalized;
|
||||
if (shouldPreserve || blocks.length > 0) {
|
||||
const validation = validateCompression(textPart, cleaned);
|
||||
validationWarnings.push(...validation.warnings);
|
||||
if (!validation.valid) {
|
||||
validationErrors.push(...validation.errors);
|
||||
fallbackApplied = true;
|
||||
return textPart;
|
||||
}
|
||||
}
|
||||
|
||||
return cleaned;
|
||||
};
|
||||
|
||||
const compressedMessage = mapTextContent(msg, compressTextPart) as ChatMessage;
|
||||
const cleaned =
|
||||
typeof compressedMessage.content === "string"
|
||||
? compressedMessage.content
|
||||
: Array.isArray(compressedMessage.content)
|
||||
? compressedMessage.content
|
||||
.map((part) =>
|
||||
part && typeof part === "object" && "text" in part && typeof part.text === "string"
|
||||
? part.text
|
||||
: ""
|
||||
)
|
||||
.filter(Boolean)
|
||||
.join("\n")
|
||||
: contentStr;
|
||||
totalCompressedTokens += estimateCompressionTokens(cleaned);
|
||||
|
||||
return { ...msg, content: cleaned };
|
||||
return compressedMessage;
|
||||
});
|
||||
|
||||
const durationMs = performance.now() - startMs;
|
||||
const uniqueRules = [...new Set(allAppliedRules)];
|
||||
const stats = createCompressionStats(
|
||||
body as unknown as Record<string, unknown>,
|
||||
{ ...body, messages: compressedMessages } as unknown as Record<string, unknown>,
|
||||
"standard" as CompressionMode,
|
||||
const stats = createCavemanStats(
|
||||
totalOriginalTokens,
|
||||
totalCompressedTokens,
|
||||
uniqueRules.length > 0 ? ["caveman-rules"] : [],
|
||||
uniqueRules.length > 0 ? uniqueRules : undefined,
|
||||
Math.round(durationMs * 100) / 100
|
||||
);
|
||||
if (validationWarnings.length > 0) stats.validationWarnings = [...new Set(validationWarnings)];
|
||||
if (validationErrors.length > 0) stats.validationErrors = [...new Set(validationErrors)];
|
||||
if (fallbackApplied) stats.fallbackApplied = true;
|
||||
if (preservedBlockCount > 0) stats.preservedBlockCount = preservedBlockCount;
|
||||
|
||||
const compressed = totalCompressedTokens < totalOriginalTokens;
|
||||
|
||||
|
||||
@@ -1,14 +1,48 @@
|
||||
import type { CavemanRule } from "./types.ts";
|
||||
import { loadAllRulesForLanguage } from "./ruleLoader.ts";
|
||||
|
||||
const CAVEMAN_RULES: CavemanRule[] = [
|
||||
// ── Category 1: Filler Removal (10+ rules) ──────────────────────────
|
||||
|
||||
{
|
||||
name: "redundant_phrasing",
|
||||
pattern:
|
||||
/\b(?:make sure to|be sure to|due to the fact that|the reason is because|it is important to|you should|remember to)\b\s*/gi,
|
||||
replacement: (match: string): string => {
|
||||
const map: Record<string, string> = {
|
||||
"make sure to": "ensure ",
|
||||
"be sure to": "ensure ",
|
||||
"due to the fact that": "because ",
|
||||
"the reason is because": "because ",
|
||||
"it is important to": "",
|
||||
"you should": "",
|
||||
"remember to": "",
|
||||
};
|
||||
return map[match.trim().toLowerCase()] ?? "";
|
||||
},
|
||||
context: "all",
|
||||
category: "structural",
|
||||
minIntensity: "full",
|
||||
description: "Replace verbose stock phrases with shorter equivalents.",
|
||||
},
|
||||
{
|
||||
name: "pleasantries",
|
||||
pattern:
|
||||
/(?<!make\s)(?<!be\s)\b(?:i'?d be happy to|i would be happy to|i'?d be glad to|i would be glad to|glad to help|happy to|thank you|thanks|no problem|you'?re welcome|absolutely|certainly|of course|sure)\b[,.!?\s]*/gi,
|
||||
replacement: "",
|
||||
context: "all",
|
||||
category: "filler",
|
||||
minIntensity: "lite",
|
||||
description: "Drop conversational acknowledgements that do not change request meaning.",
|
||||
},
|
||||
{
|
||||
name: "polite_framing",
|
||||
pattern:
|
||||
/\b(?:please|kindly|could you please|would you please|can you please|I would like you to|I want you to|I need you to)\b\s*/gi,
|
||||
replacement: "",
|
||||
context: "all",
|
||||
category: "filler",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "hedging",
|
||||
@@ -16,65 +50,105 @@ const CAVEMAN_RULES: CavemanRule[] = [
|
||||
/\b(?:it seems like|it appears that|I think that|I believe that|probably|possibly|maybe it)\b\s*/gi,
|
||||
replacement: "",
|
||||
context: "all",
|
||||
category: "filler",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "verbose_instructions",
|
||||
pattern:
|
||||
/\b(?:provide a detailed|give me a comprehensive|write an in-depth|create a thorough|explain in detail)\b/gi,
|
||||
/\b(?:provide a detailed explanation of|give me a comprehensive explanation of|write an in-depth explanation of|create a thorough explanation of|provide a detailed|give me a comprehensive|write an in-depth|create a thorough|explain in detail)\b/gi,
|
||||
replacement: (match: string): string => {
|
||||
const map: Record<string, string> = {
|
||||
"provide a detailed": "provide",
|
||||
"give me a comprehensive": "give",
|
||||
"write an in-depth": "write",
|
||||
"create a thorough": "create",
|
||||
"explain in detail": "explain",
|
||||
"provide a detailed explanation of": "explain ",
|
||||
"give me a comprehensive explanation of": "explain ",
|
||||
"write an in-depth explanation of": "explain ",
|
||||
"create a thorough explanation of": "explain ",
|
||||
"provide a detailed": "provide ",
|
||||
"give me a comprehensive": "give ",
|
||||
"write an in-depth": "write ",
|
||||
"create a thorough": "create ",
|
||||
"explain in detail": "explain ",
|
||||
};
|
||||
const lower = match.toLowerCase();
|
||||
return map[lower] ?? match;
|
||||
},
|
||||
context: "all",
|
||||
category: "filler",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "filler_adverbs",
|
||||
pattern: /(?<![a-z])\b(?:basically|essentially|actually|literally|simply)\b\s*/gi,
|
||||
pattern: /(?<![a-z])\b(?:basically|essentially|actually|literally|simply|currently)\b\s*/gi,
|
||||
replacement: "",
|
||||
context: "all",
|
||||
category: "filler",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "articles",
|
||||
pattern: /\b(?:[Aa]n|[Aa]|[Tt]he)\s+(?=[a-z])/g,
|
||||
replacement: "",
|
||||
context: "all",
|
||||
category: "terse",
|
||||
minIntensity: "full",
|
||||
description: "Remove English articles from prose while protected technical tokens stay intact.",
|
||||
},
|
||||
{
|
||||
name: "filler_phrases",
|
||||
pattern: /^(?:I want to|I need to|I'd like to|I'm looking for)\b\s*/gim,
|
||||
replacement: "",
|
||||
context: "user",
|
||||
category: "filler",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "redundant_openers",
|
||||
pattern: /^(?:Hi there|Hello|Good morning|Hey)\s*[,.!?\s]?\s*/gim,
|
||||
replacement: "",
|
||||
context: "user",
|
||||
category: "filler",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "verbose_requests",
|
||||
pattern: /\b(?:I was wondering if you could|Would it be possible to)\b\s*/gi,
|
||||
replacement: "",
|
||||
context: "user",
|
||||
category: "filler",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "leader_phrases",
|
||||
pattern: /^(?:i'?ll|i will|i can|i'?d|let me|you can|we will|we can|let'?s)\s+(?=[a-z])/gim,
|
||||
replacement: "",
|
||||
context: "all",
|
||||
category: "terse",
|
||||
minIntensity: "full",
|
||||
description: "Remove leading helper phrases before the actual instruction or answer.",
|
||||
},
|
||||
{
|
||||
name: "self_reference",
|
||||
pattern: /^(?:I am trying to|I am working on|I have been)\b\s*/gim,
|
||||
replacement: "",
|
||||
context: "user",
|
||||
category: "filler",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "excessive_gratitude",
|
||||
pattern: /\b(?:Thank you so much|Thanks in advance|I really appreciate)\b[,.!?\s]*/gi,
|
||||
replacement: "",
|
||||
context: "all",
|
||||
category: "filler",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "qualifier_removal",
|
||||
pattern: /\b(?:a bit|a little|somewhat|kind of|sort of)\b\s*/gi,
|
||||
replacement: "",
|
||||
context: "all",
|
||||
category: "filler",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
|
||||
// ── Category 2: Context Condensation (8+ rules) ──────────────────────
|
||||
@@ -84,6 +158,8 @@ const CAVEMAN_RULES: CavemanRule[] = [
|
||||
pattern: /\band any potential\b/gi,
|
||||
replacement: "",
|
||||
context: "all",
|
||||
category: "context",
|
||||
minIntensity: "full",
|
||||
},
|
||||
{
|
||||
name: "explanatory_prefix",
|
||||
@@ -99,6 +175,8 @@ const CAVEMAN_RULES: CavemanRule[] = [
|
||||
return map[match.toLowerCase()] ?? match;
|
||||
},
|
||||
context: "all",
|
||||
category: "context",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "question_to_directive",
|
||||
@@ -107,20 +185,24 @@ const CAVEMAN_RULES: CavemanRule[] = [
|
||||
replacement: (match: string): string => {
|
||||
const trimmed = match.trimEnd().toLowerCase();
|
||||
const map: Record<string, string> = {
|
||||
"can you explain why": "Explain why",
|
||||
"could you show me how": "Show how",
|
||||
"would you tell me": "Tell me",
|
||||
"can you tell me": "Tell me",
|
||||
"can you explain why": "Explain why ",
|
||||
"could you show me how": "Show how ",
|
||||
"would you tell me": "Tell me ",
|
||||
"can you tell me": "Tell me ",
|
||||
};
|
||||
return map[trimmed] ?? match;
|
||||
},
|
||||
context: "user",
|
||||
category: "context",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "context_setup",
|
||||
pattern: /\b(?:I have the following code|Here is my code|Below is the code)\b\s*[:.]?\s*/gi,
|
||||
replacement: "Code:",
|
||||
context: "user",
|
||||
category: "context",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "intent_clarification",
|
||||
@@ -128,31 +210,40 @@ const CAVEMAN_RULES: CavemanRule[] = [
|
||||
/\b(?:What I'm trying to do is|My objective is to|What I need is|I'm aiming to)\b\s*/gi,
|
||||
replacement: "Goal:",
|
||||
context: "user",
|
||||
category: "context",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "background_removal",
|
||||
pattern: /\b(?:As you may know,?\s*|As we discussed earlier,?\s*)/gi,
|
||||
replacement: "",
|
||||
context: "all",
|
||||
category: "context",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "meta_commentary",
|
||||
pattern: /^(?:Note that|Keep in mind that|Remember that)\b\s*/gim,
|
||||
replacement: "",
|
||||
context: "all",
|
||||
category: "context",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "purpose_statement",
|
||||
pattern: /\b(?:for the purpose of|with the goal of|in an effort to)\b/gi,
|
||||
pattern: /\b(?:for the purpose of|with the goal of|in an effort to|for every)\b/gi,
|
||||
replacement: (match: string): string => {
|
||||
const map: Record<string, string> = {
|
||||
"for the purpose of": "for",
|
||||
"with the goal of": "to",
|
||||
"in an effort to": "to",
|
||||
"for every": "per",
|
||||
};
|
||||
return map[match.toLowerCase()] ?? match;
|
||||
},
|
||||
context: "all",
|
||||
category: "context",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
|
||||
// ── Category 3: Structural Compression (7+ rules) ────────────────────
|
||||
@@ -162,12 +253,16 @@ const CAVEMAN_RULES: CavemanRule[] = [
|
||||
pattern: /,\s*and also\s+|,\s*as well as\s+/gi,
|
||||
replacement: ", ",
|
||||
context: "all",
|
||||
category: "structural",
|
||||
minIntensity: "full",
|
||||
},
|
||||
{
|
||||
name: "purpose_phrases",
|
||||
pattern: /\b(?:in order to|so as to)\b\s*/gi,
|
||||
replacement: "to ",
|
||||
context: "all",
|
||||
category: "structural",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "redundant_quantifiers",
|
||||
@@ -181,32 +276,42 @@ const CAVEMAN_RULES: CavemanRule[] = [
|
||||
return map[match.toLowerCase()] ?? match;
|
||||
},
|
||||
context: "all",
|
||||
category: "structural",
|
||||
minIntensity: "full",
|
||||
},
|
||||
{
|
||||
name: "verbose_connectors",
|
||||
pattern: /\b(?:furthermore|additionally|moreover|in addition)\b\s*/gi,
|
||||
replacement: "also ",
|
||||
context: "all",
|
||||
category: "structural",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "transition_removal",
|
||||
pattern: /^(?:On the other hand,?\s*|In contrast,?\s*|However,?\s*)/gim,
|
||||
replacement: "",
|
||||
context: "all",
|
||||
category: "structural",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "emphasis_removal",
|
||||
pattern: /\b(?:very|really|extremely|highly|quite)\s+(?=[a-z])/gi,
|
||||
replacement: "",
|
||||
context: "all",
|
||||
category: "structural",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "passive_voice",
|
||||
pattern: /\b(?:is being used|is being called|was created|was generated|was implemented)\b/gi,
|
||||
pattern:
|
||||
/\b(?:is being used|is being called|is being generated|was created|was generated|was implemented)\b/gi,
|
||||
replacement: (match: string): string => {
|
||||
const map: Record<string, string> = {
|
||||
"is being used": "uses",
|
||||
"is being called": "calls",
|
||||
"is being generated": "generated",
|
||||
"was created": "created",
|
||||
"was generated": "generated",
|
||||
"was implemented": "implemented",
|
||||
@@ -214,6 +319,8 @@ const CAVEMAN_RULES: CavemanRule[] = [
|
||||
return map[match.toLowerCase()] ?? match;
|
||||
},
|
||||
context: "all",
|
||||
category: "structural",
|
||||
minIntensity: "full",
|
||||
},
|
||||
|
||||
// ── Category 4: Multi-Turn Dedup (5+ rules) ─────────────────────────
|
||||
@@ -224,6 +331,8 @@ const CAVEMAN_RULES: CavemanRule[] = [
|
||||
/\b(?:As we discussed earlier|As mentioned before|As previously stated|As I said before)\b[,.]?\s*/gi,
|
||||
replacement: "See above. ",
|
||||
context: "all",
|
||||
category: "dedup",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "repeated_question",
|
||||
@@ -231,12 +340,16 @@ const CAVEMAN_RULES: CavemanRule[] = [
|
||||
/\b(?:Same question as before|I asked this earlier|This is the same question)\b[,.]?\s*/gi,
|
||||
replacement: "[same question] ",
|
||||
context: "user",
|
||||
category: "dedup",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "reestablished_context",
|
||||
pattern: /\b(?:Going back to the code above|Referring back to|Returning to)\b\s*/gi,
|
||||
replacement: "Re: ",
|
||||
context: "all",
|
||||
category: "dedup",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
{
|
||||
name: "summary_replacement",
|
||||
@@ -244,15 +357,76 @@ const CAVEMAN_RULES: CavemanRule[] = [
|
||||
/\b(?:To summarize what we've discussed|In summary of our conversation|To recap)\b[,.]?\s*/gi,
|
||||
replacement: "Summary: ",
|
||||
context: "assistant",
|
||||
category: "dedup",
|
||||
minIntensity: "lite",
|
||||
},
|
||||
|
||||
// ── Category 5: Ultra Abbreviations ─────────────────────────────────
|
||||
|
||||
{
|
||||
name: "ultra_abbreviations",
|
||||
pattern:
|
||||
/\b(?:database|configuration|function|request|response|implementation|authentication|authorization|application|dependency|dependencies)\b/gi,
|
||||
replacement: (match: string): string => {
|
||||
const map: Record<string, string> = {
|
||||
database: "DB",
|
||||
configuration: "config",
|
||||
function: "fn",
|
||||
request: "req",
|
||||
response: "res",
|
||||
implementation: "impl",
|
||||
authentication: "auth",
|
||||
authorization: "authz",
|
||||
application: "app",
|
||||
dependency: "dep",
|
||||
dependencies: "deps",
|
||||
};
|
||||
return map[match.toLowerCase()] ?? match;
|
||||
},
|
||||
context: "all",
|
||||
category: "ultra",
|
||||
minIntensity: "ultra",
|
||||
},
|
||||
];
|
||||
|
||||
export function getRulesForContext(context: string): CavemanRule[] {
|
||||
return CAVEMAN_RULES.filter((rule) => rule.context === "all" || rule.context === context);
|
||||
const INTENSITY_RANK = { lite: 0, full: 1, ultra: 2 } as const;
|
||||
|
||||
export function getRulesForContext(
|
||||
context: string,
|
||||
intensity: "lite" | "full" | "ultra" = "full",
|
||||
language = "en"
|
||||
): CavemanRule[] {
|
||||
const rank = INTENSITY_RANK[intensity] ?? INTENSITY_RANK.full;
|
||||
const fileRules = language ? loadAllRulesForLanguage(language) : [];
|
||||
const rules = fileRules.length > 0 ? fileRules : CAVEMAN_RULES;
|
||||
const selected = rules.filter((rule) => {
|
||||
const minRank = INTENSITY_RANK[rule.minIntensity ?? "lite"];
|
||||
return (rule.context === "all" || rule.context === context) && minRank <= rank;
|
||||
});
|
||||
return selected.length > 0
|
||||
? selected
|
||||
: CAVEMAN_RULES.filter((rule) => {
|
||||
const minRank = INTENSITY_RANK[rule.minIntensity ?? "lite"];
|
||||
return (rule.context === "all" || rule.context === context) && minRank <= rank;
|
||||
});
|
||||
}
|
||||
|
||||
export function getRuleByName(name: string): CavemanRule | undefined {
|
||||
return CAVEMAN_RULES.find((rule) => rule.name === name);
|
||||
}
|
||||
|
||||
export function getCavemanRuleMetadata() {
|
||||
const intensities = ["lite", "full", "ultra"] as const;
|
||||
return CAVEMAN_RULES.map((rule) => ({
|
||||
name: rule.name,
|
||||
context: rule.context,
|
||||
category: rule.category ?? "terse",
|
||||
minIntensity: rule.minIntensity ?? "lite",
|
||||
intensities: intensities.filter(
|
||||
(intensity) => INTENSITY_RANK[intensity] >= INTENSITY_RANK[rule.minIntensity ?? "lite"]
|
||||
),
|
||||
description: rule.description ?? rule.name.replace(/_/g, " "),
|
||||
}));
|
||||
}
|
||||
|
||||
export { CAVEMAN_RULES };
|
||||
|
||||
117
open-sse/services/compression/diffHelper.ts
Normal file
117
open-sse/services/compression/diffHelper.ts
Normal file
@@ -0,0 +1,117 @@
|
||||
import type { CompressionStats } from "./types.ts";
|
||||
import { extractPreservedBlocks } from "./preservation.ts";
|
||||
import { validateCompression } from "./validation.ts";
|
||||
|
||||
export interface CompressionDiffSegment {
|
||||
type: "same" | "removed" | "added";
|
||||
text: string;
|
||||
}
|
||||
|
||||
export interface CompressionPreviewDiff {
|
||||
segments: CompressionDiffSegment[];
|
||||
preservedBlocks: Array<{ kind: string; preview: string }>;
|
||||
ruleRemovals: string[];
|
||||
validationWarnings: string[];
|
||||
validationErrors: string[];
|
||||
fallbackApplied: boolean;
|
||||
}
|
||||
|
||||
export interface CompressionPreviewDiffOptions {
|
||||
maxTokenProduct?: number;
|
||||
}
|
||||
|
||||
export const DEFAULT_MAX_PREVIEW_DIFF_TOKEN_PRODUCT = 1_000_000;
|
||||
|
||||
function tokenize(text: string): string[] {
|
||||
return text.match(/\s+|[^\s]+/g) ?? [];
|
||||
}
|
||||
|
||||
function getDiffSkipWarning(
|
||||
original: string,
|
||||
compressed: string,
|
||||
options: CompressionPreviewDiffOptions = {}
|
||||
): string | null {
|
||||
const maxTokenProduct = options.maxTokenProduct ?? DEFAULT_MAX_PREVIEW_DIFF_TOKEN_PRODUCT;
|
||||
if (maxTokenProduct <= 0) return null;
|
||||
|
||||
const originalTokens = tokenize(original).length;
|
||||
const compressedTokens = tokenize(compressed).length;
|
||||
if (originalTokens * compressedTokens <= maxTokenProduct) return null;
|
||||
|
||||
return `Preview diff omitted because token product ${originalTokens}x${compressedTokens} exceeds safe limit ${maxTokenProduct}.`;
|
||||
}
|
||||
|
||||
export function buildCompressionDiff(
|
||||
original: string,
|
||||
compressed: string
|
||||
): CompressionDiffSegment[] {
|
||||
const a = tokenize(original);
|
||||
const b = tokenize(compressed);
|
||||
const dp = Array.from({ length: a.length + 1 }, () => Array<number>(b.length + 1).fill(0));
|
||||
|
||||
for (let i = a.length - 1; i >= 0; i--) {
|
||||
for (let j = b.length - 1; j >= 0; j--) {
|
||||
dp[i][j] = a[i] === b[j] ? dp[i + 1][j + 1] + 1 : Math.max(dp[i + 1][j], dp[i][j + 1]);
|
||||
}
|
||||
}
|
||||
|
||||
const segments: CompressionDiffSegment[] = [];
|
||||
const push = (type: CompressionDiffSegment["type"], text: string) => {
|
||||
if (!text) return;
|
||||
const last = segments[segments.length - 1];
|
||||
if (last?.type === type) {
|
||||
last.text += text;
|
||||
} else {
|
||||
segments.push({ type, text });
|
||||
}
|
||||
};
|
||||
|
||||
let i = 0;
|
||||
let j = 0;
|
||||
while (i < a.length && j < b.length) {
|
||||
if (a[i] === b[j]) {
|
||||
push("same", a[i]);
|
||||
i++;
|
||||
j++;
|
||||
} else if (dp[i + 1][j] >= dp[i][j + 1]) {
|
||||
push("removed", a[i]);
|
||||
i++;
|
||||
} else {
|
||||
push("added", b[j]);
|
||||
j++;
|
||||
}
|
||||
}
|
||||
while (i < a.length) push("removed", a[i++]);
|
||||
while (j < b.length) push("added", b[j++]);
|
||||
|
||||
return segments;
|
||||
}
|
||||
|
||||
export function buildCompressionPreviewDiff(
|
||||
original: string,
|
||||
compressed: string,
|
||||
stats: CompressionStats | null | undefined,
|
||||
options: CompressionPreviewDiffOptions = {}
|
||||
): CompressionPreviewDiff {
|
||||
const validation = validateCompression(original, compressed);
|
||||
const preserved = extractPreservedBlocks(original).blocks.map((block) => ({
|
||||
kind: block.kind,
|
||||
preview: block.content.replace(/\s+/g, " ").slice(0, 120),
|
||||
}));
|
||||
const diffSkipWarning = getDiffSkipWarning(original, compressed, options);
|
||||
|
||||
return {
|
||||
segments: diffSkipWarning
|
||||
? [{ type: "same", text: "[diff omitted: input too large]" }]
|
||||
: buildCompressionDiff(original, compressed),
|
||||
preservedBlocks: preserved,
|
||||
ruleRemovals: stats?.rulesApplied ?? [],
|
||||
validationWarnings: [
|
||||
...(stats?.validationWarnings ?? []),
|
||||
...validation.warnings,
|
||||
...(diffSkipWarning ? [diffSkipWarning] : []),
|
||||
],
|
||||
validationErrors: [...(stats?.validationErrors ?? []), ...validation.errors],
|
||||
fallbackApplied: Boolean(stats?.fallbackApplied || validation.fallbackApplied),
|
||||
};
|
||||
}
|
||||
408
open-sse/services/compression/engines/cavemanAdapter.ts
Normal file
408
open-sse/services/compression/engines/cavemanAdapter.ts
Normal file
@@ -0,0 +1,408 @@
|
||||
import { applyLiteCompression } from "../lite.ts";
|
||||
import { cavemanCompress } from "../caveman.ts";
|
||||
import { compressAggressive } from "../aggressive.ts";
|
||||
import { ultraCompress } from "../ultra.ts";
|
||||
import { createCompressionStats } from "../stats.ts";
|
||||
import {
|
||||
DEFAULT_AGGRESSIVE_CONFIG,
|
||||
DEFAULT_ULTRA_CONFIG,
|
||||
type CavemanIntensity,
|
||||
} from "../types.ts";
|
||||
import type { CompressionEngine, EngineConfigField, EngineValidationResult } from "./types.ts";
|
||||
|
||||
const CAVEMAN_INTENSITIES: CavemanIntensity[] = ["lite", "full", "ultra"];
|
||||
|
||||
const CAVEMAN_SCHEMA: EngineConfigField[] = [
|
||||
{
|
||||
key: "intensity",
|
||||
type: "select",
|
||||
label: "Intensity",
|
||||
defaultValue: "full",
|
||||
options: CAVEMAN_INTENSITIES.map((value) => ({ value, label: value })),
|
||||
},
|
||||
{
|
||||
key: "minMessageLength",
|
||||
type: "number",
|
||||
label: "Minimum message length",
|
||||
defaultValue: 50,
|
||||
min: 0,
|
||||
max: 10000,
|
||||
},
|
||||
{
|
||||
key: "enabled",
|
||||
type: "boolean",
|
||||
label: "Enabled",
|
||||
defaultValue: true,
|
||||
},
|
||||
];
|
||||
|
||||
const AGGRESSIVE_SCHEMA: EngineConfigField[] = [
|
||||
{
|
||||
key: "summarizerEnabled",
|
||||
type: "boolean",
|
||||
label: "Summarizer enabled",
|
||||
defaultValue: DEFAULT_AGGRESSIVE_CONFIG.summarizerEnabled,
|
||||
},
|
||||
{
|
||||
key: "maxTokensPerMessage",
|
||||
type: "number",
|
||||
label: "Max tokens per message",
|
||||
defaultValue: DEFAULT_AGGRESSIVE_CONFIG.maxTokensPerMessage,
|
||||
min: 256,
|
||||
max: 32768,
|
||||
},
|
||||
{
|
||||
key: "minSavingsThreshold",
|
||||
type: "number",
|
||||
label: "Minimum savings threshold",
|
||||
defaultValue: DEFAULT_AGGRESSIVE_CONFIG.minSavingsThreshold,
|
||||
min: 0,
|
||||
max: 1,
|
||||
},
|
||||
{
|
||||
key: "preserveSystemPrompt",
|
||||
type: "boolean",
|
||||
label: "Preserve system prompt",
|
||||
defaultValue: true,
|
||||
},
|
||||
];
|
||||
|
||||
const ULTRA_SCHEMA: EngineConfigField[] = [
|
||||
{
|
||||
key: "enabled",
|
||||
type: "boolean",
|
||||
label: "Enabled",
|
||||
defaultValue: DEFAULT_ULTRA_CONFIG.enabled,
|
||||
},
|
||||
{
|
||||
key: "compressionRate",
|
||||
type: "number",
|
||||
label: "Compression rate",
|
||||
defaultValue: DEFAULT_ULTRA_CONFIG.compressionRate,
|
||||
min: 0,
|
||||
max: 1,
|
||||
},
|
||||
{
|
||||
key: "minScoreThreshold",
|
||||
type: "number",
|
||||
label: "Minimum score threshold",
|
||||
defaultValue: DEFAULT_ULTRA_CONFIG.minScoreThreshold,
|
||||
min: 0,
|
||||
max: 1,
|
||||
},
|
||||
{
|
||||
key: "slmFallbackToAggressive",
|
||||
type: "boolean",
|
||||
label: "Fallback to aggressive",
|
||||
defaultValue: DEFAULT_ULTRA_CONFIG.slmFallbackToAggressive,
|
||||
},
|
||||
{
|
||||
key: "modelPath",
|
||||
type: "string",
|
||||
label: "Model path",
|
||||
defaultValue: "",
|
||||
},
|
||||
{
|
||||
key: "maxTokensPerMessage",
|
||||
type: "number",
|
||||
label: "Max tokens per message",
|
||||
defaultValue: DEFAULT_ULTRA_CONFIG.maxTokensPerMessage,
|
||||
min: 0,
|
||||
max: 32768,
|
||||
},
|
||||
{
|
||||
key: "preserveSystemPrompt",
|
||||
type: "boolean",
|
||||
label: "Preserve system prompt",
|
||||
defaultValue: true,
|
||||
},
|
||||
];
|
||||
|
||||
function ok(): EngineValidationResult {
|
||||
return { valid: true, errors: [] };
|
||||
}
|
||||
|
||||
function isRecord(value: unknown): value is Record<string, unknown> {
|
||||
return !!value && typeof value === "object" && !Array.isArray(value);
|
||||
}
|
||||
|
||||
function validateBoolean(config: Record<string, unknown>, key: string, errors: string[]): void {
|
||||
if (config[key] !== undefined && typeof config[key] !== "boolean") {
|
||||
errors.push(`${key} must be a boolean`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateNumberRange(
|
||||
config: Record<string, unknown>,
|
||||
key: string,
|
||||
min: number,
|
||||
max: number,
|
||||
errors: string[]
|
||||
): void {
|
||||
const value = config[key];
|
||||
if (value === undefined) return;
|
||||
if (typeof value !== "number" || !Number.isFinite(value) || value < min || value > max) {
|
||||
errors.push(`${key} must be a number between ${min} and ${max}`);
|
||||
}
|
||||
}
|
||||
|
||||
function validateCavemanLikeConfig(config: Record<string, unknown>): EngineValidationResult {
|
||||
const errors: string[] = [];
|
||||
if (
|
||||
config.intensity !== undefined &&
|
||||
!CAVEMAN_INTENSITIES.includes(config.intensity as CavemanIntensity)
|
||||
) {
|
||||
errors.push("intensity must be lite, full, or ultra");
|
||||
}
|
||||
if (
|
||||
config.minMessageLength !== undefined &&
|
||||
(typeof config.minMessageLength !== "number" || config.minMessageLength < 0)
|
||||
) {
|
||||
errors.push("minMessageLength must be a non-negative number");
|
||||
}
|
||||
if (config.enabled !== undefined && typeof config.enabled !== "boolean") {
|
||||
errors.push("enabled must be a boolean");
|
||||
}
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
function validateAggressiveConfig(config: Record<string, unknown>): EngineValidationResult {
|
||||
const errors: string[] = [];
|
||||
validateBoolean(config, "summarizerEnabled", errors);
|
||||
validateBoolean(config, "preserveSystemPrompt", errors);
|
||||
validateNumberRange(config, "maxTokensPerMessage", 256, 32768, errors);
|
||||
validateNumberRange(config, "minSavingsThreshold", 0, 1, errors);
|
||||
|
||||
if (config.thresholds !== undefined) {
|
||||
if (!isRecord(config.thresholds)) {
|
||||
errors.push("thresholds must be an object");
|
||||
} else {
|
||||
for (const key of ["fullSummary", "moderate", "light", "verbatim"]) {
|
||||
validateNumberRange(config.thresholds, key, 1, 100, errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (config.toolStrategies !== undefined) {
|
||||
if (!isRecord(config.toolStrategies)) {
|
||||
errors.push("toolStrategies must be an object");
|
||||
} else {
|
||||
for (const key of ["fileContent", "grepSearch", "shellOutput", "json", "errorMessage"]) {
|
||||
validateBoolean(config.toolStrategies, key, errors);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
function validateUltraConfig(config: Record<string, unknown>): EngineValidationResult {
|
||||
const errors: string[] = [];
|
||||
validateBoolean(config, "enabled", errors);
|
||||
validateBoolean(config, "slmFallbackToAggressive", errors);
|
||||
validateBoolean(config, "preserveSystemPrompt", errors);
|
||||
validateNumberRange(config, "compressionRate", 0, 1, errors);
|
||||
validateNumberRange(config, "minScoreThreshold", 0, 1, errors);
|
||||
validateNumberRange(config, "maxTokensPerMessage", 0, 32768, errors);
|
||||
if (config.modelPath !== undefined && typeof config.modelPath !== "string") {
|
||||
errors.push("modelPath must be a string");
|
||||
}
|
||||
return { valid: errors.length === 0, errors };
|
||||
}
|
||||
|
||||
export const liteEngine: CompressionEngine = {
|
||||
id: "lite",
|
||||
name: "Lite",
|
||||
description: "Fast whitespace, tool-result and image URL reduction.",
|
||||
icon: "compress",
|
||||
targets: ["messages", "tool_results"],
|
||||
stackable: true,
|
||||
stackPriority: 5,
|
||||
metadata: {
|
||||
id: "lite",
|
||||
name: "Lite",
|
||||
description: "Fast whitespace, tool-result and image URL reduction.",
|
||||
inputScope: "messages",
|
||||
targetLatencyMs: 1,
|
||||
supportsPreview: true,
|
||||
stable: true,
|
||||
},
|
||||
apply(body, options) {
|
||||
return applyLiteCompression(body, {
|
||||
...options,
|
||||
preserveSystemPrompt: options?.config?.preserveSystemPrompt !== false,
|
||||
});
|
||||
},
|
||||
compress(body, config) {
|
||||
return this.apply(body, { stepConfig: config });
|
||||
},
|
||||
getConfigSchema() {
|
||||
return AGGRESSIVE_SCHEMA;
|
||||
},
|
||||
validateConfig(config) {
|
||||
return validateAggressiveConfig(config);
|
||||
},
|
||||
};
|
||||
|
||||
export const cavemanEngine: CompressionEngine = {
|
||||
id: "caveman",
|
||||
name: "Caveman",
|
||||
description: "Rule-based message compression with preservation and validation.",
|
||||
icon: "compress",
|
||||
targets: ["messages"],
|
||||
stackable: true,
|
||||
stackPriority: 20,
|
||||
metadata: {
|
||||
id: "caveman",
|
||||
name: "Caveman",
|
||||
description: "Rule-based message compression with preservation and validation.",
|
||||
inputScope: "messages",
|
||||
targetLatencyMs: 1,
|
||||
supportsPreview: true,
|
||||
stable: true,
|
||||
},
|
||||
apply(body, options) {
|
||||
const cavemanConfig = {
|
||||
...(options?.config?.cavemanConfig ?? {}),
|
||||
...(options?.stepConfig ?? {}),
|
||||
...(options?.config?.languageConfig?.enabled
|
||||
? {
|
||||
language: options.config.languageConfig.defaultLanguage,
|
||||
autoDetectLanguage: options.config.languageConfig.autoDetect,
|
||||
enabledLanguagePacks: options.config.languageConfig.enabledPacks,
|
||||
}
|
||||
: {}),
|
||||
...(options?.config?.preserveSystemPrompt !== false
|
||||
? {
|
||||
compressRoles: (options?.config?.cavemanConfig?.compressRoles ?? ["user"]).filter(
|
||||
(role) => role !== "system"
|
||||
),
|
||||
}
|
||||
: {}),
|
||||
};
|
||||
return cavemanCompress(body as Parameters<typeof cavemanCompress>[0], cavemanConfig);
|
||||
},
|
||||
compress(body, config) {
|
||||
return this.apply(body, { stepConfig: config });
|
||||
},
|
||||
getConfigSchema() {
|
||||
return CAVEMAN_SCHEMA;
|
||||
},
|
||||
validateConfig(config) {
|
||||
return validateCavemanLikeConfig(config);
|
||||
},
|
||||
};
|
||||
|
||||
export const aggressiveEngine: CompressionEngine = {
|
||||
id: "aggressive",
|
||||
name: "Aggressive",
|
||||
description: "Summarization, tool result compression and progressive aging.",
|
||||
icon: "speed",
|
||||
targets: ["messages", "tool_results"],
|
||||
stackable: true,
|
||||
stackPriority: 30,
|
||||
metadata: {
|
||||
id: "aggressive",
|
||||
name: "Aggressive",
|
||||
description: "Summarization, tool result compression and progressive aging.",
|
||||
inputScope: "messages",
|
||||
targetLatencyMs: 5,
|
||||
supportsPreview: true,
|
||||
stable: true,
|
||||
},
|
||||
apply(body, options) {
|
||||
const messages = (body.messages ?? []) as Array<{
|
||||
role: string;
|
||||
content?: string | Array<{ type: string; text?: string }>;
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
if (!Array.isArray(messages) || messages.length === 0) {
|
||||
return { body, compressed: false, stats: null };
|
||||
}
|
||||
const aggressiveConfig = {
|
||||
...(options?.config?.aggressive ?? {}),
|
||||
...(options?.stepConfig ?? {}),
|
||||
preserveSystemPrompt: options?.config?.preserveSystemPrompt !== false,
|
||||
};
|
||||
const result = compressAggressive(messages, aggressiveConfig);
|
||||
const compressedBody = { ...body, messages: result.messages };
|
||||
return {
|
||||
body: compressedBody,
|
||||
compressed: result.stats.savingsPercent > 0,
|
||||
stats: createCompressionStats(
|
||||
body,
|
||||
compressedBody,
|
||||
"aggressive",
|
||||
["aggressive"],
|
||||
result.stats.rulesApplied,
|
||||
result.stats.durationMs
|
||||
),
|
||||
};
|
||||
},
|
||||
compress(body, config) {
|
||||
return this.apply(body, { stepConfig: config });
|
||||
},
|
||||
getConfigSchema() {
|
||||
return AGGRESSIVE_SCHEMA;
|
||||
},
|
||||
validateConfig(config) {
|
||||
return validateAggressiveConfig(config);
|
||||
},
|
||||
};
|
||||
|
||||
export const ultraEngine: CompressionEngine = {
|
||||
id: "ultra",
|
||||
name: "Ultra",
|
||||
description: "Heuristic token pruning with optional local SLM fallback.",
|
||||
icon: "bolt",
|
||||
targets: ["messages"],
|
||||
stackable: true,
|
||||
stackPriority: 40,
|
||||
metadata: {
|
||||
id: "ultra",
|
||||
name: "Ultra",
|
||||
description: "Heuristic token pruning with optional local SLM fallback.",
|
||||
inputScope: "messages",
|
||||
targetLatencyMs: 5,
|
||||
supportsPreview: true,
|
||||
stable: true,
|
||||
},
|
||||
apply(body, options) {
|
||||
const messages = (body.messages ?? []) as Array<{
|
||||
role: string;
|
||||
content?: string | unknown[];
|
||||
[key: string]: unknown;
|
||||
}>;
|
||||
if (!Array.isArray(messages) || messages.length === 0) {
|
||||
return { body, compressed: false, stats: null };
|
||||
}
|
||||
const ultraConfig = {
|
||||
...(options?.config?.ultra ?? {}),
|
||||
...(options?.stepConfig ?? {}),
|
||||
preserveSystemPrompt: options?.config?.preserveSystemPrompt !== false,
|
||||
};
|
||||
const result = ultraCompress(messages, ultraConfig);
|
||||
const compressedBody = { ...body, messages: result.messages };
|
||||
return {
|
||||
body: compressedBody,
|
||||
compressed: result.stats.savingsPercent > 0,
|
||||
stats: createCompressionStats(
|
||||
body,
|
||||
compressedBody,
|
||||
"ultra",
|
||||
["ultra"],
|
||||
result.stats.rulesApplied,
|
||||
result.stats.durationMs
|
||||
),
|
||||
};
|
||||
},
|
||||
compress(body, config) {
|
||||
return this.apply(body, { stepConfig: config });
|
||||
},
|
||||
getConfigSchema() {
|
||||
return ULTRA_SCHEMA;
|
||||
},
|
||||
validateConfig(config) {
|
||||
return validateUltraConfig(config);
|
||||
},
|
||||
};
|
||||
14
open-sse/services/compression/engines/index.ts
Normal file
14
open-sse/services/compression/engines/index.ts
Normal file
@@ -0,0 +1,14 @@
|
||||
import { registerCompressionEngine, getCompressionEngine } from "./registry.ts";
|
||||
import { aggressiveEngine, cavemanEngine, liteEngine, ultraEngine } from "./cavemanAdapter.ts";
|
||||
import { rtkEngine } from "./rtk/index.ts";
|
||||
|
||||
let registered = false;
|
||||
|
||||
export function registerBuiltinCompressionEngines(): void {
|
||||
const engines = [liteEngine, cavemanEngine, aggressiveEngine, ultraEngine, rtkEngine];
|
||||
if (registered && engines.every((engine) => getCompressionEngine(engine.id))) return;
|
||||
for (const engine of engines) {
|
||||
if (!getCompressionEngine(engine.id)) registerCompressionEngine(engine);
|
||||
}
|
||||
registered = true;
|
||||
}
|
||||
87
open-sse/services/compression/engines/registry.ts
Normal file
87
open-sse/services/compression/engines/registry.ts
Normal file
@@ -0,0 +1,87 @@
|
||||
import type { CompressionEngine, EngineRegistryEntry, EngineValidationResult } from "./types.ts";
|
||||
|
||||
const ENGINES = new Map<string, EngineRegistryEntry>();
|
||||
|
||||
function assertValidEngine(engine: CompressionEngine): void {
|
||||
if (
|
||||
!engine?.id ||
|
||||
typeof engine.apply !== "function" ||
|
||||
typeof engine.compress !== "function" ||
|
||||
typeof engine.getConfigSchema !== "function" ||
|
||||
typeof engine.validateConfig !== "function"
|
||||
) {
|
||||
throw new Error("Invalid compression engine registration");
|
||||
}
|
||||
}
|
||||
|
||||
export function registerEngine(
|
||||
engine: CompressionEngine,
|
||||
defaultConfig: Record<string, unknown> = {}
|
||||
): void {
|
||||
assertValidEngine(engine);
|
||||
const validation = engine.validateConfig(defaultConfig);
|
||||
if (!validation.valid) {
|
||||
throw new Error(`Invalid default config for ${engine.id}: ${validation.errors.join("; ")}`);
|
||||
}
|
||||
ENGINES.set(engine.id, {
|
||||
engine,
|
||||
enabled: true,
|
||||
config: { ...defaultConfig },
|
||||
});
|
||||
}
|
||||
|
||||
export function registerCompressionEngine(engine: CompressionEngine): void {
|
||||
registerEngine(engine);
|
||||
}
|
||||
|
||||
export function unregisterCompressionEngine(id: string): boolean {
|
||||
return ENGINES.delete(id);
|
||||
}
|
||||
|
||||
export function getEngine(id: string): CompressionEngine | null {
|
||||
return ENGINES.get(id)?.engine ?? null;
|
||||
}
|
||||
|
||||
export function getCompressionEngine(id: string): CompressionEngine | null {
|
||||
return getEngine(id);
|
||||
}
|
||||
|
||||
export function getEngineEntry(id: string): EngineRegistryEntry | null {
|
||||
return ENGINES.get(id) ?? null;
|
||||
}
|
||||
|
||||
export function listEngines(): EngineRegistryEntry[] {
|
||||
return Array.from(ENGINES.values());
|
||||
}
|
||||
|
||||
export function listCompressionEngines(): CompressionEngine[] {
|
||||
return listEngines().map((entry) => entry.engine);
|
||||
}
|
||||
|
||||
export function listEnabledEngines(): EngineRegistryEntry[] {
|
||||
return listEngines().filter((entry) => entry.enabled);
|
||||
}
|
||||
|
||||
export function setEngineEnabled(id: string, enabled: boolean): boolean {
|
||||
const entry = ENGINES.get(id);
|
||||
if (!entry) return false;
|
||||
entry.enabled = enabled;
|
||||
return true;
|
||||
}
|
||||
|
||||
export function updateEngineConfig(
|
||||
id: string,
|
||||
config: Record<string, unknown>
|
||||
): EngineValidationResult {
|
||||
const entry = ENGINES.get(id);
|
||||
if (!entry) return { valid: false, errors: [`Unknown compression engine: ${id}`] };
|
||||
const nextConfig = { ...entry.config, ...config };
|
||||
const validation = entry.engine.validateConfig(nextConfig);
|
||||
if (!validation.valid) return validation;
|
||||
entry.config = nextConfig;
|
||||
return { valid: true, errors: [] };
|
||||
}
|
||||
|
||||
export function clearCompressionEngineRegistry(): void {
|
||||
ENGINES.clear();
|
||||
}
|
||||
128
open-sse/services/compression/engines/rtk/codeStripper.ts
Normal file
128
open-sse/services/compression/engines/rtk/codeStripper.ts
Normal file
@@ -0,0 +1,128 @@
|
||||
export type CodeLanguage =
|
||||
| "javascript"
|
||||
| "typescript"
|
||||
| "python"
|
||||
| "rust"
|
||||
| "go"
|
||||
| "ruby"
|
||||
| "java"
|
||||
| "unknown";
|
||||
|
||||
export interface CodeStripperOptions {
|
||||
removeComments?: boolean;
|
||||
removeEmptyLines?: boolean;
|
||||
collapseWhitespace?: boolean;
|
||||
preserveDocstrings?: boolean;
|
||||
}
|
||||
|
||||
const LANGUAGE_ALIASES: Record<string, CodeLanguage> = {
|
||||
js: "javascript",
|
||||
jsx: "javascript",
|
||||
javascript: "javascript",
|
||||
ts: "typescript",
|
||||
tsx: "typescript",
|
||||
typescript: "typescript",
|
||||
py: "python",
|
||||
python: "python",
|
||||
rs: "rust",
|
||||
rust: "rust",
|
||||
go: "go",
|
||||
rb: "ruby",
|
||||
ruby: "ruby",
|
||||
java: "java",
|
||||
};
|
||||
|
||||
export function normalizeCodeLanguage(language?: string | null): CodeLanguage {
|
||||
if (!language) return "unknown";
|
||||
return LANGUAGE_ALIASES[language.trim().toLowerCase()] ?? "unknown";
|
||||
}
|
||||
|
||||
export function detectCodeLanguage(text: string): CodeLanguage {
|
||||
if (/\b(?:interface|type)\s+\w+\s*=|:\s*(?:string|number|boolean)\b/.test(text)) {
|
||||
return "typescript";
|
||||
}
|
||||
if (/\b(?:const|let|function|import|export)\b|=>/.test(text)) return "javascript";
|
||||
if (/\bdef\s+\w+\(|\bimport\s+\w+|print\(/.test(text)) return "python";
|
||||
if (/\bfn\s+\w+\(|\blet\s+mut\b|println!\(/.test(text)) return "rust";
|
||||
if (/\bfunc\s+\w+\(|package\s+\w+/.test(text)) return "go";
|
||||
if (/\bclass\s+\w+|System\.out\.println/.test(text)) return "java";
|
||||
if (/\bdef\s+\w+|puts\s+|end\s*$/.test(text)) return "ruby";
|
||||
return "unknown";
|
||||
}
|
||||
|
||||
function removeSlashComments(text: string): string {
|
||||
return text
|
||||
.replace(/\/\*[\s\S]*?\*\//g, "")
|
||||
.replace(/^\s*\/\/.*$/gm, "")
|
||||
.replace(/\s+\/\/\s.*$/gm, "");
|
||||
}
|
||||
|
||||
function removeHashComments(text: string): string {
|
||||
return text.replace(/^\s*#.*$/gm, "").replace(/\s+#\s.*$/gm, "");
|
||||
}
|
||||
|
||||
function stripByLanguage(
|
||||
text: string,
|
||||
language: CodeLanguage,
|
||||
options: Required<CodeStripperOptions>
|
||||
): string {
|
||||
if (!options.removeComments) return text;
|
||||
|
||||
if (language === "javascript" || language === "typescript" || language === "rust") {
|
||||
return removeSlashComments(text);
|
||||
}
|
||||
if (language === "go" || language === "java") return removeSlashComments(text);
|
||||
if (language === "python") {
|
||||
const withoutDocstrings = options.preserveDocstrings
|
||||
? text
|
||||
: text.replace(/("""[\s\S]*?"""|'''[\s\S]*?''')/g, "");
|
||||
return removeHashComments(withoutDocstrings);
|
||||
}
|
||||
if (language === "ruby") {
|
||||
return removeHashComments(text.replace(/^=begin[\s\S]*?^=end/gm, ""));
|
||||
}
|
||||
return text;
|
||||
}
|
||||
|
||||
export function stripCode(
|
||||
text: string,
|
||||
language: CodeLanguage = "unknown",
|
||||
options: CodeStripperOptions = {}
|
||||
): {
|
||||
text: string;
|
||||
strippedLines: number;
|
||||
language: CodeLanguage;
|
||||
} {
|
||||
const resolvedLanguage = language === "unknown" ? detectCodeLanguage(text) : language;
|
||||
const opts: Required<CodeStripperOptions> = {
|
||||
removeComments: options.removeComments !== false,
|
||||
removeEmptyLines: options.removeEmptyLines !== false,
|
||||
collapseWhitespace: options.collapseWhitespace !== false,
|
||||
preserveDocstrings: options.preserveDocstrings === true,
|
||||
};
|
||||
const originalLines = text.split(/\r?\n/).length;
|
||||
let result = stripByLanguage(text, resolvedLanguage, opts);
|
||||
|
||||
if (opts.removeEmptyLines) result = result.replace(/^\s*$(?:\r?\n)?/gm, "");
|
||||
if (opts.collapseWhitespace) {
|
||||
result = result
|
||||
.split(/\r?\n/)
|
||||
.map((line) => line.replace(/[ \t]+$/g, ""))
|
||||
.join("\n");
|
||||
}
|
||||
|
||||
result = result.trim();
|
||||
const strippedLines = Math.max(0, originalLines - (result ? result.split(/\r?\n/).length : 0));
|
||||
return { text: result, strippedLines, language: resolvedLanguage };
|
||||
}
|
||||
|
||||
export function stripCodeComments(
|
||||
text: string,
|
||||
language = "typescript"
|
||||
): {
|
||||
text: string;
|
||||
stripped: boolean;
|
||||
} {
|
||||
const result = stripCode(text, normalizeCodeLanguage(language));
|
||||
return { text: result.text, stripped: result.strippedLines > 0 || result.text !== text };
|
||||
}
|
||||
465
open-sse/services/compression/engines/rtk/commandDetector.ts
Normal file
465
open-sse/services/compression/engines/rtk/commandDetector.ts
Normal file
@@ -0,0 +1,465 @@
|
||||
export interface CommandDetectionResult {
|
||||
type: string;
|
||||
command: string | null;
|
||||
confidence: number;
|
||||
category:
|
||||
| "git"
|
||||
| "test"
|
||||
| "build"
|
||||
| "shell"
|
||||
| "docker"
|
||||
| "package"
|
||||
| "infra"
|
||||
| "cloud"
|
||||
| "generic";
|
||||
matchedPatterns: string[];
|
||||
}
|
||||
|
||||
type Detector = {
|
||||
type: string;
|
||||
category: CommandDetectionResult["category"];
|
||||
commandPatterns: RegExp[];
|
||||
contentPatterns: RegExp[];
|
||||
};
|
||||
|
||||
const COMMAND_PREFIXES = [
|
||||
"git",
|
||||
"make",
|
||||
"terraform",
|
||||
"tofu",
|
||||
"opentofu",
|
||||
"systemctl",
|
||||
"npm",
|
||||
"pnpm",
|
||||
"yarn",
|
||||
"vitest",
|
||||
"jest",
|
||||
"pytest",
|
||||
"python",
|
||||
"go",
|
||||
"cargo",
|
||||
"tsc",
|
||||
"eslint",
|
||||
"webpack",
|
||||
"vite",
|
||||
"biome",
|
||||
"prettier",
|
||||
"turbo",
|
||||
"nx",
|
||||
"playwright",
|
||||
"ruff",
|
||||
"mypy",
|
||||
"pip",
|
||||
"uv",
|
||||
"poetry",
|
||||
"golangci-lint",
|
||||
"bundle",
|
||||
"rubocop",
|
||||
"docker",
|
||||
"aws",
|
||||
"gcloud",
|
||||
"ssh",
|
||||
"rsync",
|
||||
"curl",
|
||||
"wget",
|
||||
"ls",
|
||||
"find",
|
||||
"grep",
|
||||
"rg",
|
||||
"ag",
|
||||
"ps",
|
||||
"df",
|
||||
"du",
|
||||
];
|
||||
|
||||
const COMMAND_PREFIX_PATTERN = new RegExp(`^(?:${COMMAND_PREFIXES.join("|")})\\b`);
|
||||
|
||||
const DETECTORS: Detector[] = [
|
||||
{
|
||||
type: "git-status",
|
||||
category: "git",
|
||||
commandPatterns: [/^git\s+status\b/i],
|
||||
contentPatterns: [
|
||||
/^On branch /m,
|
||||
/^Changes (?:not staged|to be committed)/m,
|
||||
/^Untracked files:/m,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "git-branch",
|
||||
category: "git",
|
||||
commandPatterns: [/^git\s+branch\b/i, /^git\s+checkout\b/i, /^git\s+switch\b/i],
|
||||
contentPatterns: [/^\*\s+\S+/m, /Switched to (?:a new )?branch/i, /Already on ['"][^'"]+['"]/i],
|
||||
},
|
||||
{
|
||||
type: "git-diff",
|
||||
category: "git",
|
||||
commandPatterns: [/^git\s+diff\b/i, /^git\s+show\b/i],
|
||||
contentPatterns: [/^diff --git /m, /^@@\s+-\d+,\d+\s+\+\d+,\d+\s+@@/m],
|
||||
},
|
||||
{
|
||||
type: "git-log",
|
||||
category: "git",
|
||||
commandPatterns: [/^git\s+log\b/i],
|
||||
contentPatterns: [/^commit [0-9a-f]{7,40}/m, /^Author: /m],
|
||||
},
|
||||
{
|
||||
type: "make",
|
||||
category: "build",
|
||||
commandPatterns: [/^make\b/i],
|
||||
contentPatterns: [/^make\[\d+\]: (?:Entering|Leaving) directory/m, /make: \*\*\* /],
|
||||
},
|
||||
{
|
||||
type: "terraform-plan",
|
||||
category: "infra",
|
||||
commandPatterns: [/^terraform\s+plan\b/i],
|
||||
contentPatterns: [/Terraform will perform the following actions:/, /Plan: \d+ to add/i],
|
||||
},
|
||||
{
|
||||
type: "tofu-plan",
|
||||
category: "infra",
|
||||
commandPatterns: [/^(?:tofu|opentofu)\s+plan\b/i],
|
||||
contentPatterns: [/OpenTofu will perform the following actions:/, /Plan: \d+ to add/i],
|
||||
},
|
||||
{
|
||||
type: "systemctl-status",
|
||||
category: "infra",
|
||||
commandPatterns: [/^systemctl\s+status\b/i],
|
||||
contentPatterns: [/^\s*Loaded:\s+/m, /^\s*Active:\s+/m, /^●\s+\S+\.service/m],
|
||||
},
|
||||
{
|
||||
type: "test-vitest",
|
||||
category: "test",
|
||||
commandPatterns: [/^vitest\b/i, /^npm\s+(?:run\s+)?test:vitest\b/i],
|
||||
contentPatterns: [/\bvitest\b/i, /^ ✓ /m, /^ ❯ /m, /Test Files\s+\d+\s+(?:passed|failed)/i],
|
||||
},
|
||||
{
|
||||
type: "test-jest",
|
||||
category: "test",
|
||||
commandPatterns: [/^jest\b/i, /^npm\s+(?:run\s+)?test\b/i],
|
||||
contentPatterns: [/Test Suites:\s+\d+/i, /Tests:\s+\d+/i, /^PASS\s+/m, /^FAIL\s+/m],
|
||||
},
|
||||
{
|
||||
type: "test-pytest",
|
||||
category: "test",
|
||||
commandPatterns: [/^pytest\b/i, /^python\s+-m\s+pytest\b/i],
|
||||
contentPatterns: [/=+\s+(?:\d+\s+)?(?:passed|failed|errors?)/i, /^E\s+/m, /^FAILED /m],
|
||||
},
|
||||
{
|
||||
type: "test-cargo",
|
||||
category: "test",
|
||||
commandPatterns: [/^cargo\s+test\b/i, /^cargo\s+nextest\b/i],
|
||||
contentPatterns: [
|
||||
/^running \d+ tests?/m,
|
||||
/^test\s+[\w:.-]+\s+\.\.\.\s+(?:ok|FAILED|ignored)/m,
|
||||
/test result:\s+(?:ok|FAILED)/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "test-go",
|
||||
category: "test",
|
||||
commandPatterns: [/^go\s+test\b/i],
|
||||
contentPatterns: [/^(?:ok|FAIL)\s+[\w./-]+\s+[\d.]+s/m, /^--- FAIL: /m, /^panic: /m],
|
||||
},
|
||||
{
|
||||
type: "build-typescript",
|
||||
category: "build",
|
||||
commandPatterns: [/^tsc\b/i, /^npm\s+run\s+typecheck\b/i],
|
||||
contentPatterns: [/TS\d{4}:/, /error TS\d{4}/i],
|
||||
},
|
||||
{
|
||||
type: "build-eslint",
|
||||
category: "build",
|
||||
commandPatterns: [/^eslint\b/i, /^npm\s+run\s+lint\b/i],
|
||||
contentPatterns: [/\s+\d+:\d+\s+(?:error|warning)\s+/, /✖\s+\d+\s+problems?/],
|
||||
},
|
||||
{
|
||||
type: "build-webpack",
|
||||
category: "build",
|
||||
commandPatterns: [/^webpack\b/i, /^npx\s+webpack\b/i, /^npm\s+run\s+build:webpack\b/i],
|
||||
contentPatterns: [
|
||||
/webpack\s+\d/i,
|
||||
/compiled (?:successfully|with \d+ errors?)/i,
|
||||
/asset .+\.js/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "build-vite",
|
||||
category: "build",
|
||||
commandPatterns: [/^vite\s+build\b/i, /^npm\s+run\s+build\b/i, /^pnpm\s+build\b/i],
|
||||
contentPatterns: [/vite v[\d.]+/i, /✓ built in/i, /transforming \(\d+\)/i],
|
||||
},
|
||||
{
|
||||
type: "biome",
|
||||
category: "build",
|
||||
commandPatterns: [/^biome\b/i, /^npx\s+biome\b/i],
|
||||
contentPatterns: [/lint\/[A-Za-z0-9/.-]+/, /Checked \d+ files? in/i],
|
||||
},
|
||||
{
|
||||
type: "prettier",
|
||||
category: "build",
|
||||
commandPatterns: [/^prettier\b/i, /^npx\s+prettier\b/i],
|
||||
contentPatterns: [/^Checking formatting\.\.\./m, /Code style issues found/i],
|
||||
},
|
||||
{
|
||||
type: "turbo",
|
||||
category: "build",
|
||||
commandPatterns: [/^turbo\b/i, /^npx\s+turbo\b/i],
|
||||
contentPatterns: [/^• Packages in scope:/m, /^Tasks:\s+\d+\s+successful/m],
|
||||
},
|
||||
{
|
||||
type: "nx",
|
||||
category: "build",
|
||||
commandPatterns: [/^nx\b/i, /^npx\s+nx\b/i],
|
||||
contentPatterns: [/^NX\s+/m, /^> nx run /m],
|
||||
},
|
||||
{
|
||||
type: "playwright",
|
||||
category: "test",
|
||||
commandPatterns: [/^playwright\s+test\b/i, /^npx\s+playwright\s+test\b/i],
|
||||
contentPatterns: [/Running \d+ tests? using \d+ workers?/i, /^\s+\d+ failed/m],
|
||||
},
|
||||
{
|
||||
type: "npm-install",
|
||||
category: "package",
|
||||
commandPatterns: [/^(?:npm|pnpm|yarn)\s+(?:install|add|update)\b/i],
|
||||
contentPatterns: [
|
||||
/added \d+ packages/i,
|
||||
/packages are looking for funding/i,
|
||||
/audited \d+ packages/i,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "npm-audit",
|
||||
category: "package",
|
||||
commandPatterns: [/^(?:npm|pnpm|yarn)\s+audit\b/i],
|
||||
contentPatterns: [/found \d+ vulnerabilities/i, /\b(?:low|moderate|high|critical)\b/i],
|
||||
},
|
||||
{
|
||||
type: "ruff",
|
||||
category: "build",
|
||||
commandPatterns: [/^ruff\b/i, /^uv\s+run\s+ruff\b/i],
|
||||
contentPatterns: [/^[\w./-]+\.py:\d+:\d+:\s+[A-Z]\d+/m, /Found \d+ errors?\./i],
|
||||
},
|
||||
{
|
||||
type: "mypy",
|
||||
category: "build",
|
||||
commandPatterns: [/^mypy\b/i, /^python\s+-m\s+mypy\b/i],
|
||||
contentPatterns: [/^[\w./-]+\.py:\d+:\s+error:/m, /Found \d+ errors? in \d+ files?/i],
|
||||
},
|
||||
{
|
||||
type: "pip",
|
||||
category: "package",
|
||||
commandPatterns: [/^pip\s+(?:install|download|uninstall)\b/i, /^python\s+-m\s+pip\b/i],
|
||||
contentPatterns: [/^Collecting /m, /^Successfully installed /m],
|
||||
},
|
||||
{
|
||||
type: "uv-sync",
|
||||
category: "package",
|
||||
commandPatterns: [/^uv\s+sync\b/i, /^uv\s+pip\s+install\b/i],
|
||||
contentPatterns: [/^Resolved \d+ packages?/m, /^Installed \d+ packages?/m],
|
||||
},
|
||||
{
|
||||
type: "poetry-install",
|
||||
category: "package",
|
||||
commandPatterns: [/^poetry\s+install\b/i],
|
||||
contentPatterns: [/^Installing dependencies from lock file/m, /^Package operations:/m],
|
||||
},
|
||||
{
|
||||
type: "golangci-lint",
|
||||
category: "build",
|
||||
commandPatterns: [/^golangci-lint\b/i],
|
||||
contentPatterns: [/^[\w./-]+\.go:\d+:\d+:/m, /^\d+ issues?:/m],
|
||||
},
|
||||
{
|
||||
type: "bundle-install",
|
||||
category: "package",
|
||||
commandPatterns: [/^bundle\s+install\b/i],
|
||||
contentPatterns: [/^Fetching gem metadata from /m, /^Bundle complete!/m],
|
||||
},
|
||||
{
|
||||
type: "rubocop",
|
||||
category: "build",
|
||||
commandPatterns: [/^rubocop\b/i, /^bundle\s+exec\s+rubocop\b/i],
|
||||
contentPatterns: [/^Inspecting \d+ files/m, /^[\w./-]+\.rb:\d+:\d+:\s+[A-Z]:/m],
|
||||
},
|
||||
{
|
||||
type: "docker-ps",
|
||||
category: "docker",
|
||||
commandPatterns: [/^docker\s+ps\b/i],
|
||||
contentPatterns: [/^CONTAINER ID\s+IMAGE\s+COMMAND/m],
|
||||
},
|
||||
{
|
||||
type: "docker-logs",
|
||||
category: "docker",
|
||||
commandPatterns: [/^docker\s+logs\b/i, /^docker\s+compose\s+logs\b/i],
|
||||
contentPatterns: [
|
||||
/^\d{4}-\d{2}-\d{2}T\d{2}:\d{2}:\d{2}/m,
|
||||
/\b(?:ERROR|WARN|INFO)\b/,
|
||||
/^Attaching to /m,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "aws",
|
||||
category: "cloud",
|
||||
commandPatterns: [/^aws\b/i],
|
||||
contentPatterns: [/An error occurred \([A-Za-z0-9]+\) when calling/, /^(?:upload|download): /m],
|
||||
},
|
||||
{
|
||||
type: "gcloud",
|
||||
category: "cloud",
|
||||
commandPatterns: [/^gcloud\b/i],
|
||||
contentPatterns: [/^ERROR: \(gcloud\./m, /^Updated property \[/m],
|
||||
},
|
||||
{
|
||||
type: "ssh",
|
||||
category: "cloud",
|
||||
commandPatterns: [/^ssh\b/i],
|
||||
contentPatterns: [
|
||||
/Permission denied \(/,
|
||||
/Host key verification failed/,
|
||||
/Connection timed out/,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "rsync",
|
||||
category: "cloud",
|
||||
commandPatterns: [/^rsync\b/i],
|
||||
contentPatterns: [/^sending incremental file list/m, /^rsync error:/m],
|
||||
},
|
||||
{
|
||||
type: "curl",
|
||||
category: "cloud",
|
||||
commandPatterns: [/^curl\b/i],
|
||||
contentPatterns: [/curl: \(\d+\)/, /^HTTP\/\d(?:\.\d)? \d{3}/m],
|
||||
},
|
||||
{
|
||||
type: "wget",
|
||||
category: "cloud",
|
||||
commandPatterns: [/^wget\b/i],
|
||||
contentPatterns: [/^--\d{4}-\d{2}-\d{2}/m, /^ERROR \d{3}:/m],
|
||||
},
|
||||
{
|
||||
type: "json-output",
|
||||
category: "generic",
|
||||
commandPatterns: [/^jq\b/i, /^cat\s+.*\.json\b/i],
|
||||
contentPatterns: [/^\s*[\[{][\s\S]*[\]}]\s*$/],
|
||||
},
|
||||
{
|
||||
type: "shell-ls",
|
||||
category: "shell",
|
||||
commandPatterns: [/^ls(?:\s+-[A-Za-z]+)?\b/i],
|
||||
contentPatterns: [/^total \d+/m, /^\S+\s+\S+\s+\d+\s+\w+\s+\d{1,2}\s+/m],
|
||||
},
|
||||
{
|
||||
type: "shell-find",
|
||||
category: "shell",
|
||||
commandPatterns: [/^find\b/i],
|
||||
contentPatterns: [/^(?:\.{1,2}|\/|[\w.-]+\/).+/m],
|
||||
},
|
||||
{
|
||||
type: "shell-grep",
|
||||
category: "shell",
|
||||
commandPatterns: [/^(?:grep|rg|ag)\b/i],
|
||||
contentPatterns: [
|
||||
/^[\w./-]+\.(?:ts|tsx|js|jsx|py|go|rs|java|rb|md|json|ya?ml|txt):\d*:/m,
|
||||
/^[\w./-]+\/[\w./-]+:\d*:/m,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "shell-ps",
|
||||
category: "shell",
|
||||
commandPatterns: [/^ps\b/i],
|
||||
contentPatterns: [/^(?:USER\s+PID|\s*PID\s+)/m],
|
||||
},
|
||||
{
|
||||
type: "shell-df",
|
||||
category: "shell",
|
||||
commandPatterns: [/^df\b/i],
|
||||
contentPatterns: [/^Filesystem\s+.*Use%/m],
|
||||
},
|
||||
{
|
||||
type: "shell-du",
|
||||
category: "shell",
|
||||
commandPatterns: [/^du\b/i],
|
||||
contentPatterns: [/^\d+(?:\.\d+)?[KMGTP]?\s+\S+/m],
|
||||
},
|
||||
{
|
||||
type: "error-stacktrace",
|
||||
category: "generic",
|
||||
commandPatterns: [],
|
||||
contentPatterns: [
|
||||
/Traceback \(most recent call last\):/,
|
||||
/^\s+at\s+\S+\s+\(.+:\d+:\d+\)/m,
|
||||
/^panic: /m,
|
||||
/^thread '[^']+' panicked at/m,
|
||||
],
|
||||
},
|
||||
{
|
||||
type: "generic-error",
|
||||
category: "generic",
|
||||
commandPatterns: [],
|
||||
contentPatterns: [/Error:/, /Exception:/, /Traceback \(most recent call last\):/],
|
||||
},
|
||||
];
|
||||
|
||||
export function detectCommandFromText(text: string): string | null {
|
||||
const firstLines = text.split(/\r?\n/).slice(0, 4);
|
||||
for (const line of firstLines) {
|
||||
const trimmed = line.trim().replace(/^\$\s+/, "");
|
||||
if (!trimmed) continue;
|
||||
if (COMMAND_PREFIX_PATTERN.test(trimmed)) {
|
||||
return trimmed;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
export function detectCommandType(text: string, command?: string | null): CommandDetectionResult {
|
||||
const detectedCommand = command?.trim() || detectCommandFromText(text);
|
||||
let best: CommandDetectionResult | null = null;
|
||||
|
||||
for (const detector of DETECTORS) {
|
||||
const matchedPatterns: string[] = [];
|
||||
const commandMatched =
|
||||
Boolean(detectedCommand) &&
|
||||
detector.commandPatterns.some((pattern) => {
|
||||
const matched = pattern.test(detectedCommand ?? "");
|
||||
if (matched) matchedPatterns.push(pattern.source);
|
||||
return matched;
|
||||
});
|
||||
const contentMatches = detector.contentPatterns.filter((pattern) => {
|
||||
const matched = pattern.test(text);
|
||||
if (matched) matchedPatterns.push(pattern.source);
|
||||
return matched;
|
||||
}).length;
|
||||
if (!commandMatched && contentMatches === 0) continue;
|
||||
|
||||
const confidence = Math.min(1, (commandMatched ? 0.55 : 0) + contentMatches * 0.25);
|
||||
if (!best || confidence > best.confidence) {
|
||||
best = {
|
||||
type: detector.type,
|
||||
command: detectedCommand,
|
||||
confidence,
|
||||
category: detector.category,
|
||||
matchedPatterns,
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
return (
|
||||
best ?? {
|
||||
type: "unknown",
|
||||
command: detectedCommand,
|
||||
confidence: detectedCommand ? 0.35 : 0.1,
|
||||
category: "generic",
|
||||
matchedPatterns: [],
|
||||
}
|
||||
);
|
||||
}
|
||||
|
||||
export function listCommandTypes(): string[] {
|
||||
return DETECTORS.map((detector) => detector.type);
|
||||
}
|
||||
|
||||
export const detectCommandOutput = detectCommandType;
|
||||
37
open-sse/services/compression/engines/rtk/deduplicator.ts
Normal file
37
open-sse/services/compression/engines/rtk/deduplicator.ts
Normal file
@@ -0,0 +1,37 @@
|
||||
export interface DeduplicateOptions {
|
||||
threshold?: number;
|
||||
}
|
||||
|
||||
export function deduplicateRepeatedLines(
|
||||
text: string,
|
||||
options: DeduplicateOptions = {}
|
||||
): {
|
||||
text: string;
|
||||
collapsed: number;
|
||||
} {
|
||||
const threshold = Math.max(2, Math.floor(options.threshold ?? 3));
|
||||
const lines = text.split(/\r?\n/);
|
||||
const output: string[] = [];
|
||||
let collapsed = 0;
|
||||
|
||||
for (let index = 0; index < lines.length; index++) {
|
||||
const line = lines[index];
|
||||
let runLength = 1;
|
||||
while (index + runLength < lines.length && lines[index + runLength] === line) {
|
||||
runLength++;
|
||||
}
|
||||
|
||||
if (line.trim() && runLength >= threshold) {
|
||||
output.push(line);
|
||||
output.push(`[line repeated ${runLength - 1}x]`);
|
||||
output.push(`[rtk:dropped ${runLength - 1} repeated lines]`);
|
||||
collapsed += runLength - 1;
|
||||
index += runLength - 1;
|
||||
continue;
|
||||
}
|
||||
|
||||
output.push(line);
|
||||
}
|
||||
|
||||
return { text: output.join("\n"), collapsed };
|
||||
}
|
||||
220
open-sse/services/compression/engines/rtk/filterLoader.ts
Normal file
220
open-sse/services/compression/engines/rtk/filterLoader.ts
Normal file
@@ -0,0 +1,220 @@
|
||||
import fs from "node:fs";
|
||||
import { fileURLToPath } from "node:url";
|
||||
import path from "node:path";
|
||||
import os from "node:os";
|
||||
import crypto from "node:crypto";
|
||||
import { detectCommandType } from "./commandDetector.ts";
|
||||
import { validateRtkFilter, type RtkFilterDefinition } from "./filterSchema.ts";
|
||||
|
||||
let cache: RtkFilterDefinition[] | null = null;
|
||||
let cacheKey: string | null = null;
|
||||
let diagnostics: RtkFilterLoadDiagnostic[] = [];
|
||||
|
||||
export interface RtkFilterLoadDiagnostic {
|
||||
source: "project" | "global" | "builtin";
|
||||
path?: string;
|
||||
level: "warning" | "error";
|
||||
message: string;
|
||||
}
|
||||
|
||||
interface FilterSource {
|
||||
source: "project" | "global" | "builtin";
|
||||
path: string;
|
||||
trusted: boolean;
|
||||
}
|
||||
|
||||
interface RtkFilterLoadOptions {
|
||||
refresh?: boolean;
|
||||
customFiltersEnabled?: boolean;
|
||||
trustProjectFilters?: boolean;
|
||||
}
|
||||
|
||||
function getFiltersDir(): string {
|
||||
const moduleDir = path.dirname(fileURLToPath(import.meta.url));
|
||||
const candidates = [
|
||||
path.join(moduleDir, "filters"),
|
||||
path.join(moduleDir, "..", "services", "compression", "engines", "rtk", "filters"),
|
||||
path.join(process.cwd(), "open-sse", "services", "compression", "engines", "rtk", "filters"),
|
||||
path.join(
|
||||
process.cwd(),
|
||||
"app",
|
||||
"open-sse",
|
||||
"services",
|
||||
"compression",
|
||||
"engines",
|
||||
"rtk",
|
||||
"filters"
|
||||
),
|
||||
];
|
||||
return (
|
||||
candidates.find((candidate, index) => {
|
||||
return candidates.indexOf(candidate) === index && fs.existsSync(candidate);
|
||||
}) ?? candidates[0]
|
||||
);
|
||||
}
|
||||
|
||||
function getDataDir(): string {
|
||||
return process.env.DATA_DIR || path.join(os.homedir(), ".omniroute");
|
||||
}
|
||||
|
||||
function sha256(value: string): string {
|
||||
return crypto.createHash("sha256").update(value).digest("hex");
|
||||
}
|
||||
|
||||
function projectFiltersTrusted(
|
||||
filtersPath: string,
|
||||
trustProjectFilters = false
|
||||
): boolean | "changed" {
|
||||
if (trustProjectFilters) return true;
|
||||
if (process.env.OMNIROUTE_RTK_TRUST_PROJECT_FILTERS === "1") return true;
|
||||
const trustPath = path.join(path.dirname(filtersPath), "trust.json");
|
||||
if (!fs.existsSync(trustPath)) return false;
|
||||
try {
|
||||
const filtersHash = sha256(fs.readFileSync(filtersPath, "utf8"));
|
||||
const trust = JSON.parse(fs.readFileSync(trustPath, "utf8")) as Record<string, unknown>;
|
||||
const trustedHash =
|
||||
typeof trust.filtersSha256 === "string"
|
||||
? trust.filtersSha256
|
||||
: typeof trust.trustedFiltersSha256 === "string"
|
||||
? trust.trustedFiltersSha256
|
||||
: null;
|
||||
if (!trustedHash) return false;
|
||||
return trustedHash === filtersHash ? true : "changed";
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
function collectFilterSources(options: RtkFilterLoadOptions = {}): FilterSource[] {
|
||||
const sources: FilterSource[] = [];
|
||||
const projectPath = path.join(process.cwd(), ".rtk", "filters.json");
|
||||
if (options.customFiltersEnabled !== false && fs.existsSync(projectPath)) {
|
||||
const trusted = projectFiltersTrusted(projectPath, options.trustProjectFilters === true);
|
||||
if (trusted === true) {
|
||||
sources.push({ source: "project", path: projectPath, trusted: true });
|
||||
} else {
|
||||
diagnostics.push({
|
||||
source: "project",
|
||||
path: projectPath,
|
||||
level: "warning",
|
||||
message:
|
||||
trusted === "changed"
|
||||
? "Project RTK filters changed after trust and were skipped"
|
||||
: "Project RTK filters are untrusted and were skipped",
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
const globalPath = path.join(getDataDir(), "rtk", "filters.json");
|
||||
if (options.customFiltersEnabled !== false && fs.existsSync(globalPath)) {
|
||||
sources.push({ source: "global", path: globalPath, trusted: true });
|
||||
}
|
||||
|
||||
const builtinDir = getFiltersDir();
|
||||
if (fs.existsSync(builtinDir)) {
|
||||
for (const file of fs
|
||||
.readdirSync(builtinDir)
|
||||
.filter((entry) => entry.endsWith(".json"))
|
||||
.sort()) {
|
||||
sources.push({
|
||||
source: "builtin",
|
||||
path: path.join(builtinDir, file),
|
||||
trusted: true,
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
return sources;
|
||||
}
|
||||
|
||||
function parseFilterFile(source: FilterSource): RtkFilterDefinition[] {
|
||||
try {
|
||||
const parsed = JSON.parse(fs.readFileSync(source.path, "utf8"));
|
||||
const entries = Array.isArray(parsed) ? parsed : [parsed];
|
||||
return entries.map(validateRtkFilter);
|
||||
} catch (error) {
|
||||
const message = error instanceof Error ? error.message : String(error);
|
||||
if (source.source === "builtin") {
|
||||
throw new Error(`Invalid RTK filter ${path.basename(source.path)}: ${message}`);
|
||||
}
|
||||
diagnostics.push({
|
||||
source: source.source,
|
||||
path: source.path,
|
||||
level: "warning",
|
||||
message: `Invalid custom RTK filter skipped: ${message}`,
|
||||
});
|
||||
return [];
|
||||
}
|
||||
}
|
||||
|
||||
export function loadRtkFilters(options: RtkFilterLoadOptions = {}): RtkFilterDefinition[] {
|
||||
const currentCacheKey = [
|
||||
process.cwd(),
|
||||
getDataDir(),
|
||||
options.customFiltersEnabled === false ? "builtin-only" : "custom",
|
||||
options.trustProjectFilters === true ? "trusted-project" : "trust-file",
|
||||
process.env.OMNIROUTE_RTK_TRUST_PROJECT_FILTERS === "1" ? "env-trust" : "env-normal",
|
||||
].join("|");
|
||||
if (cache && cacheKey === currentCacheKey && !options.refresh) return cache;
|
||||
diagnostics = [];
|
||||
const filters: RtkFilterDefinition[] = [];
|
||||
for (const source of collectFilterSources(options)) {
|
||||
filters.push(...parseFilterFile(source));
|
||||
}
|
||||
|
||||
const sorted = filters.sort((a, b) => b.priority - a.priority || a.id.localeCompare(b.id));
|
||||
cache = sorted;
|
||||
cacheKey = currentCacheKey;
|
||||
return sorted;
|
||||
}
|
||||
|
||||
export function getRtkFilterLoadDiagnostics(): RtkFilterLoadDiagnostic[] {
|
||||
loadRtkFilters();
|
||||
return diagnostics.map((entry) => ({ ...entry }));
|
||||
}
|
||||
|
||||
export function getRtkFilterCatalog(): Array<
|
||||
Pick<
|
||||
RtkFilterDefinition,
|
||||
"id" | "name" | "description" | "commandTypes" | "category" | "priority"
|
||||
>
|
||||
> {
|
||||
return loadRtkFilters().map((filter) => ({
|
||||
id: filter.id,
|
||||
name: filter.name,
|
||||
description: filter.description,
|
||||
commandTypes: filter.commandTypes,
|
||||
category: filter.category,
|
||||
priority: filter.priority,
|
||||
}));
|
||||
}
|
||||
|
||||
export function matchRtkFilter(
|
||||
text: string,
|
||||
command?: string | null,
|
||||
options: RtkFilterLoadOptions = {}
|
||||
): RtkFilterDefinition | null {
|
||||
const detection = detectCommandType(text, command);
|
||||
const detectedCommand = detection.command ?? command ?? "";
|
||||
const matchesPattern = (pattern: string, value: string): boolean => {
|
||||
try {
|
||||
return new RegExp(pattern, "im").test(value);
|
||||
} catch {
|
||||
return false;
|
||||
}
|
||||
};
|
||||
const filters = loadRtkFilters(options);
|
||||
return (
|
||||
filters.find((filter) => filter.commandTypes.includes(detection.type)) ??
|
||||
filters.find(
|
||||
(filter) =>
|
||||
detectedCommand &&
|
||||
filter.commandPatterns.some((pattern) => matchesPattern(pattern, detectedCommand))
|
||||
) ??
|
||||
filters.find((filter) =>
|
||||
filter.matchPatterns.some((pattern) => matchesPattern(pattern, text))
|
||||
) ??
|
||||
filters.find((filter) => filter.commandTypes.includes("generic-output")) ??
|
||||
null
|
||||
);
|
||||
}
|
||||
182
open-sse/services/compression/engines/rtk/filterSchema.ts
Normal file
182
open-sse/services/compression/engines/rtk/filterSchema.ts
Normal file
@@ -0,0 +1,182 @@
|
||||
import { z } from "zod";
|
||||
|
||||
const rtkFilterCategorySchema = z.enum([
|
||||
"git",
|
||||
"test",
|
||||
"build",
|
||||
"shell",
|
||||
"docker",
|
||||
"package",
|
||||
"infra",
|
||||
"cloud",
|
||||
"generic",
|
||||
]);
|
||||
|
||||
const rtkReplaceRuleSchema = z
|
||||
.object({
|
||||
pattern: z.string().min(1),
|
||||
replacement: z.string(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const rtkMatchOutputRuleSchema = z
|
||||
.object({
|
||||
pattern: z.string().min(1),
|
||||
message: z.string(),
|
||||
unless: z.string().min(1).optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const rtkInlineTestSchema = z
|
||||
.object({
|
||||
name: z.string().min(1),
|
||||
input: z.string(),
|
||||
expected: z.string(),
|
||||
command: z.string().optional(),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const rtkFilterMatchSchema = z
|
||||
.object({
|
||||
commands: z.array(z.string().min(1)).default([]),
|
||||
patterns: z.array(z.string().min(1)).default([]),
|
||||
outputTypes: z.array(z.string().min(1)).default([]),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const rtkFilterRulesSchema = z
|
||||
.object({
|
||||
stripAnsi: z.boolean().default(false),
|
||||
replace: z.array(rtkReplaceRuleSchema).default([]),
|
||||
matchOutput: z.array(rtkMatchOutputRuleSchema).default([]),
|
||||
includePatterns: z.array(z.string()).default([]),
|
||||
dropPatterns: z.array(z.string()).default([]),
|
||||
collapsePatterns: z.array(z.string()).default([]),
|
||||
deduplicate: z.boolean().default(false),
|
||||
truncateLineAt: z.number().int().min(0).default(0),
|
||||
maxLines: z.number().int().min(0).default(0),
|
||||
headLines: z.number().int().min(0).default(20),
|
||||
tailLines: z.number().int().min(0).default(20),
|
||||
onEmpty: z.string().default(""),
|
||||
filterStderr: z.boolean().default(false),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const rtkFilterPreserveSchema = z
|
||||
.object({
|
||||
errorPatterns: z.array(z.string()).default([]),
|
||||
summaryPatterns: z.array(z.string()).default([]),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const rtkFilterPackSchema = z
|
||||
.object({
|
||||
id: z.string().min(1),
|
||||
label: z.string().min(1),
|
||||
description: z.string().default(""),
|
||||
category: rtkFilterCategorySchema,
|
||||
priority: z.number().int().min(0).max(100).default(50),
|
||||
match: rtkFilterMatchSchema,
|
||||
rules: rtkFilterRulesSchema.default({}),
|
||||
preserve: rtkFilterPreserveSchema.default({}),
|
||||
tests: z.array(rtkInlineTestSchema).default([]),
|
||||
})
|
||||
.strict();
|
||||
|
||||
const legacyRtkFilterSchema = z
|
||||
.object({
|
||||
id: z.string().min(1),
|
||||
name: z.string().min(1),
|
||||
description: z.string().default(""),
|
||||
commandTypes: z.array(z.string().min(1)).min(1),
|
||||
category: rtkFilterCategorySchema,
|
||||
priority: z.number().int().min(0).max(100).default(50),
|
||||
stripPatterns: z.array(z.string()).default([]),
|
||||
keepPatterns: z.array(z.string()).default([]),
|
||||
priorityPatterns: z.array(z.string()).default([]),
|
||||
collapsePatterns: z.array(z.string()).default([]),
|
||||
stripAnsi: z.boolean().default(false),
|
||||
replace: z.array(rtkReplaceRuleSchema).default([]),
|
||||
matchOutput: z.array(rtkMatchOutputRuleSchema).default([]),
|
||||
truncateLineAt: z.number().int().min(0).default(0),
|
||||
onEmpty: z.string().default(""),
|
||||
filterStderr: z.boolean().default(false),
|
||||
maxLines: z.number().int().min(0).default(0),
|
||||
preserveHead: z.number().int().min(0).default(20),
|
||||
preserveTail: z.number().int().min(0).default(20),
|
||||
tests: z.array(rtkInlineTestSchema).default([]),
|
||||
})
|
||||
.strict();
|
||||
|
||||
export const rtkFilterSchema = z.union([rtkFilterPackSchema, legacyRtkFilterSchema]);
|
||||
|
||||
export type RtkFilterPack = z.infer<typeof rtkFilterPackSchema>;
|
||||
|
||||
export interface RtkFilterDefinition {
|
||||
id: string;
|
||||
name: string;
|
||||
description: string;
|
||||
commandTypes: string[];
|
||||
commandPatterns: string[];
|
||||
matchPatterns: string[];
|
||||
category: z.infer<typeof rtkFilterCategorySchema>;
|
||||
priority: number;
|
||||
stripPatterns: string[];
|
||||
keepPatterns: string[];
|
||||
priorityPatterns: string[];
|
||||
collapsePatterns: string[];
|
||||
stripAnsi: boolean;
|
||||
replace: Array<{ pattern: string; replacement: string }>;
|
||||
matchOutput: Array<{ pattern: string; message: string; unless?: string }>;
|
||||
truncateLineAt: number;
|
||||
onEmpty: string;
|
||||
filterStderr: boolean;
|
||||
deduplicate: boolean;
|
||||
maxLines: number;
|
||||
preserveHead: number;
|
||||
preserveTail: number;
|
||||
tests: Array<{ name: string; input: string; expected: string; command?: string }>;
|
||||
}
|
||||
|
||||
function isCanonicalFilter(value: z.infer<typeof rtkFilterSchema>): value is RtkFilterPack {
|
||||
return "label" in value && "match" in value && "rules" in value;
|
||||
}
|
||||
|
||||
export function validateRtkFilter(value: unknown): RtkFilterDefinition {
|
||||
const parsed = rtkFilterSchema.parse(value);
|
||||
if (!isCanonicalFilter(parsed)) {
|
||||
return {
|
||||
...parsed,
|
||||
commandPatterns: [],
|
||||
matchPatterns: [],
|
||||
deduplicate: parsed.collapsePatterns.length > 0,
|
||||
};
|
||||
}
|
||||
|
||||
const preservePatterns = [...parsed.preserve.errorPatterns, ...parsed.preserve.summaryPatterns];
|
||||
return {
|
||||
id: parsed.id,
|
||||
name: parsed.label,
|
||||
description: parsed.description,
|
||||
commandTypes: parsed.match.outputTypes,
|
||||
commandPatterns: parsed.match.commands,
|
||||
matchPatterns: parsed.match.patterns,
|
||||
category: parsed.category,
|
||||
priority: parsed.priority,
|
||||
stripPatterns: parsed.rules.dropPatterns,
|
||||
keepPatterns: parsed.rules.includePatterns,
|
||||
priorityPatterns: preservePatterns,
|
||||
collapsePatterns: parsed.rules.collapsePatterns,
|
||||
stripAnsi: parsed.rules.stripAnsi,
|
||||
replace: parsed.rules.replace,
|
||||
matchOutput: parsed.rules.matchOutput,
|
||||
truncateLineAt: parsed.rules.truncateLineAt,
|
||||
onEmpty: parsed.rules.onEmpty,
|
||||
filterStderr: parsed.rules.filterStderr,
|
||||
deduplicate: parsed.rules.deduplicate,
|
||||
maxLines: parsed.rules.maxLines,
|
||||
preserveHead: parsed.rules.headLines,
|
||||
preserveTail: parsed.rules.tailLines,
|
||||
tests: parsed.tests,
|
||||
};
|
||||
}
|
||||
34
open-sse/services/compression/engines/rtk/filters/aws.json
Normal file
34
open-sse/services/compression/engines/rtk/filters/aws.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "aws",
|
||||
"label": "AWS CLI",
|
||||
"description": "Compress AWS CLI progress output while preserving errors and completed transfers.",
|
||||
"category": "cloud",
|
||||
"priority": 76,
|
||||
"match": {
|
||||
"outputTypes": ["aws"],
|
||||
"commands": ["^aws\\b"],
|
||||
"patterns": ["An error occurred \\([A-Za-z0-9]+\\) when calling", "^(?:upload|download): "]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"dropPatterns": ["^Completed \\d", "^\\s*$"],
|
||||
"includePatterns": ["^(?:upload|download): ", "An error occurred \\("],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": true,
|
||||
"maxLines": 120,
|
||||
"headLines": 30,
|
||||
"tailLines": 40
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["An error occurred", "AccessDenied", "Throttling", "ValidationError"],
|
||||
"summaryPatterns": ["^(?:upload|download): "]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "aws s3 cp",
|
||||
"input": "Completed 1.0 MiB/2.0 MiB\nupload: ./app.log to s3://bucket/app.log\nAn error occurred (AccessDenied) when calling the PutObject operation: denied\n",
|
||||
"expected": "upload: ./app.log to s3://bucket/app.log\nAn error occurred (AccessDenied) when calling the PutObject operation: denied"
|
||||
}
|
||||
]
|
||||
}
|
||||
35
open-sse/services/compression/engines/rtk/filters/biome.json
Normal file
35
open-sse/services/compression/engines/rtk/filters/biome.json
Normal file
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"id": "biome",
|
||||
"label": "Biome",
|
||||
"description": "Compact Biome lint/format output.",
|
||||
"category": "build",
|
||||
"priority": 80,
|
||||
"match": {
|
||||
"outputTypes": [],
|
||||
"commands": ["^biome\\b", "^npx\\s+biome\\b"],
|
||||
"patterns": ["Checked \\d+ files?", "biome"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"dropPatterns": ["^\\s*$", "^Checked \\d+ files? in"],
|
||||
"includePatterns": ["error", "warning", "✖", "Checked", "Fixed", "^\\s*>"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": true,
|
||||
"maxLines": 100,
|
||||
"headLines": 25,
|
||||
"tailLines": 40,
|
||||
"onEmpty": "biome: ok"
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["error", "✖"],
|
||||
"summaryPatterns": ["Checked", "Fixed"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "biome",
|
||||
"input": "Checked 10 files in 20ms\nerror lint/a.ts:1:1 boom\nFixed 1 file\n",
|
||||
"expected": "error lint/a.ts:1:1 boom\nFixed 1 file"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"id": "build-eslint",
|
||||
"label": "ESLint",
|
||||
"description": "Keep lint diagnostics and problem summary.",
|
||||
"category": "build",
|
||||
"priority": 88,
|
||||
"match": {
|
||||
"outputTypes": ["build-eslint"],
|
||||
"commands": ["^(?:eslint|npm\\s+run\\s+lint)\\b"],
|
||||
"patterns": ["\\s+\\d+:\\d+\\s+(?:error|warning)\\s+", "✖\\s+\\d+\\s+problems?"]
|
||||
},
|
||||
"rules": {
|
||||
"includePatterns": [
|
||||
"\\s+\\d+:\\d+\\s+(error|warning)\\s+",
|
||||
"✖\\s+\\d+\\s+problems?",
|
||||
"^/.*\\.(ts|tsx|js|jsx)$"
|
||||
],
|
||||
"dropPatterns": ["^\\s*$"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": false,
|
||||
"maxLines": 120,
|
||||
"headLines": 20,
|
||||
"tailLines": 40
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["error", "warning", "problems?"],
|
||||
"summaryPatterns": []
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "(?:eslint|npm run lint)",
|
||||
"input": "/repo/a.ts\n 1:1 error boom no-boom\n✖ 1 problem (1 error, 0 warnings)\n",
|
||||
"expected": "/repo/a.ts\n 1:1 error boom no-boom\n✖ 1 problem (1 error, 0 warnings)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"id": "build-typescript",
|
||||
"label": "TypeScript",
|
||||
"description": "Keep TypeScript diagnostics and file locations.",
|
||||
"category": "build",
|
||||
"priority": 93,
|
||||
"match": {
|
||||
"outputTypes": ["build-typescript"],
|
||||
"commands": ["^(?:tsc|npm\\s+run\\s+typecheck)\\b"],
|
||||
"patterns": ["TS\\d{4}:", "error TS\\d{4}"]
|
||||
},
|
||||
"rules": {
|
||||
"includePatterns": ["TS\\d{4}:", "error TS\\d{4}", "^Found \\d+ errors?"],
|
||||
"dropPatterns": ["^\\s*$"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": false,
|
||||
"maxLines": 120,
|
||||
"headLines": 20,
|
||||
"tailLines": 40
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["TS\\d{4}", "Found \\d+"],
|
||||
"summaryPatterns": []
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "(?:tsc|npm run typecheck)",
|
||||
"input": "src/a.ts:1:1 - error TS2322: Type string is not assignable\nFound 1 error.\n",
|
||||
"expected": "src/a.ts:1:1 - error TS2322: Type string is not assignable\nFound 1 error."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"id": "build-vite",
|
||||
"label": "Vite Build",
|
||||
"description": "Keep Vite errors and final build summary.",
|
||||
"category": "build",
|
||||
"priority": 83,
|
||||
"match": {
|
||||
"outputTypes": ["build-vite"],
|
||||
"commands": ["^vite\\s+build\\b", "^npm\\s+run\\s+build\\b", "^pnpm\\s+build\\b"],
|
||||
"patterns": ["vite v[\\d.]+", "✓ built in"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"matchOutput": [
|
||||
{
|
||||
"pattern": "✓ built in",
|
||||
"message": "vite: build ok",
|
||||
"unless": "error|failed"
|
||||
}
|
||||
],
|
||||
"includePatterns": ["error", "failed", "✓ built in", "dist/", "rendering chunks"],
|
||||
"dropPatterns": ["^transforming ", "^✓ \\d+ modules transformed", "^\\s*$"],
|
||||
"collapsePatterns": ["^dist/"],
|
||||
"deduplicate": true,
|
||||
"maxLines": 120,
|
||||
"headLines": 30,
|
||||
"tailLines": 45
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["error", "failed"],
|
||||
"summaryPatterns": ["✓ built in"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "vite build",
|
||||
"input": "vite v5.0.0 building for production...\n✓ 10 modules transformed.\n✓ built in 100ms\n",
|
||||
"expected": "vite: build ok"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "build-webpack",
|
||||
"label": "Webpack Build",
|
||||
"description": "Keep webpack errors/warnings and compilation summary.",
|
||||
"category": "build",
|
||||
"priority": 84,
|
||||
"match": {
|
||||
"outputTypes": ["build-webpack"],
|
||||
"commands": ["^webpack\\b", "^npx\\s+webpack\\b"],
|
||||
"patterns": ["webpack \\d", "compiled (?:successfully|with)"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"includePatterns": ["ERROR in", "WARNING in", "compiled ", "asset ", "Entrypoint"],
|
||||
"dropPatterns": ["^\\s*$", "^cacheable modules"],
|
||||
"collapsePatterns": ["^asset "],
|
||||
"deduplicate": true,
|
||||
"maxLines": 140,
|
||||
"headLines": 35,
|
||||
"tailLines": 50
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["ERROR in"],
|
||||
"summaryPatterns": ["compiled "]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "webpack",
|
||||
"input": "asset main.js 10 KiB\nERROR in ./src/a.ts\ncompiled with 1 error\n",
|
||||
"expected": "asset main.js 10 KiB\nERROR in ./src/a.ts\ncompiled with 1 error"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"id": "bundle-install",
|
||||
"label": "Bundle Install",
|
||||
"description": "Strip bundle install noise and keep final status.",
|
||||
"category": "package",
|
||||
"priority": 70,
|
||||
"match": {
|
||||
"outputTypes": [],
|
||||
"commands": ["^bundle\\s+install\\b"],
|
||||
"patterns": ["Bundle complete", "Using "]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"dropPatterns": ["^Using ", "^Fetching ", "^Installing ", "^\\s*$"],
|
||||
"includePatterns": ["Bundle complete", "Your bundle is complete", "ERROR", "Gem::"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": true,
|
||||
"maxLines": 100,
|
||||
"headLines": 25,
|
||||
"tailLines": 35,
|
||||
"onEmpty": "bundle: ok"
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["ERROR", "Gem::"],
|
||||
"summaryPatterns": ["Bundle complete", "Your bundle is complete"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "bundle install",
|
||||
"input": "Using rake 13.0\nInstalling pg 1.5\nBundle complete! 4 Gemfile dependencies\n",
|
||||
"expected": "Bundle complete! 4 Gemfile dependencies"
|
||||
}
|
||||
]
|
||||
}
|
||||
34
open-sse/services/compression/engines/rtk/filters/curl.json
Normal file
34
open-sse/services/compression/engines/rtk/filters/curl.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "curl",
|
||||
"label": "curl",
|
||||
"description": "Remove curl progress noise and keep HTTP status/errors.",
|
||||
"category": "cloud",
|
||||
"priority": 70,
|
||||
"match": {
|
||||
"outputTypes": ["curl"],
|
||||
"commands": ["^curl\\b"],
|
||||
"patterns": ["curl: \\(\\d+\\)", "^HTTP/\\d(?:\\.\\d)? \\d{3}"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"dropPatterns": ["^\\s*% Total\\s+% Received", "^\\s*\\d+\\s+\\d+", "^\\s*$"],
|
||||
"includePatterns": ["^HTTP/\\d(?:\\.\\d)? \\d{3}", "^curl: \\(\\d+\\)", "^[<{]"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": true,
|
||||
"maxLines": 100,
|
||||
"headLines": 30,
|
||||
"tailLines": 40
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["curl: \\(\\d+\\)", "^HTTP/\\d(?:\\.\\d)? [45]\\d\\d"],
|
||||
"summaryPatterns": ["^HTTP/\\d(?:\\.\\d)? \\d{3}"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "curl -i",
|
||||
"input": "% Total % Received\n 0 0 0 0\nHTTP/1.1 500 Internal Server Error\ncurl: (22) The requested URL returned error: 500\n",
|
||||
"expected": "HTTP/1.1 500 Internal Server Error\ncurl: (22) The requested URL returned error: 500"
|
||||
}
|
||||
]
|
||||
}
|
||||
34
open-sse/services/compression/engines/rtk/filters/df.json
Normal file
34
open-sse/services/compression/engines/rtk/filters/df.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "df",
|
||||
"label": "df",
|
||||
"description": "Keep filesystem usage table headers and mounted filesystems.",
|
||||
"category": "shell",
|
||||
"priority": 67,
|
||||
"match": {
|
||||
"outputTypes": ["shell-df"],
|
||||
"commands": ["^df\\b"],
|
||||
"patterns": ["^Filesystem\\s+.*Use%"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"dropPatterns": ["^tmpfs\\s", "^udev\\s", "^\\s*$"],
|
||||
"includePatterns": ["^Filesystem\\s+", "^/dev/\\S+\\s+"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": false,
|
||||
"maxLines": 80,
|
||||
"headLines": 25,
|
||||
"tailLines": 25
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["No such file", "Permission denied"],
|
||||
"summaryPatterns": ["Use%", "^/dev/"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "df -h",
|
||||
"input": "Filesystem Size Used Avail Use% Mounted on\nudev 2.0G 0 2.0G 0% /dev\n/dev/sda1 50G 45G 5.0G 90% /\n",
|
||||
"expected": "Filesystem Size Used Avail Use% Mounted on\n/dev/sda1 50G 45G 5.0G 90% /"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"id": "docker-logs",
|
||||
"label": "Docker Logs",
|
||||
"description": "Keep warnings/errors and recent Docker log context.",
|
||||
"category": "docker",
|
||||
"priority": 84,
|
||||
"match": {
|
||||
"outputTypes": ["docker-logs"],
|
||||
"commands": ["^docker\\s+(?:compose\\s+)?logs\\b"],
|
||||
"patterns": ["\\b(?:ERROR|WARN|INFO)\\b", "^Attaching to "]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"dropPatterns": ["^Attaching to ", "^\\s*$"],
|
||||
"includePatterns": [
|
||||
"ERROR",
|
||||
"WARN",
|
||||
"Exception",
|
||||
"Traceback",
|
||||
"failed",
|
||||
"listening",
|
||||
"started"
|
||||
],
|
||||
"collapsePatterns": ["INFO"],
|
||||
"deduplicate": true,
|
||||
"maxLines": 120,
|
||||
"headLines": 20,
|
||||
"tailLines": 60
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["ERROR", "Exception", "failed"],
|
||||
"summaryPatterns": ["started", "listening"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "docker (?:compose )?logs",
|
||||
"input": "Attaching to api\nINFO started\nINFO started\nERROR failed to connect\n",
|
||||
"expected": "INFO started\nERROR failed to connect"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"id": "docker-ps",
|
||||
"label": "Docker PS",
|
||||
"description": "Keep docker container table header and rows under a fixed cap.",
|
||||
"category": "docker",
|
||||
"priority": 60,
|
||||
"match": {
|
||||
"outputTypes": ["docker-ps"],
|
||||
"commands": ["^docker\\s+ps\\b"],
|
||||
"patterns": ["^CONTAINER ID\\s+IMAGE\\s+COMMAND"]
|
||||
},
|
||||
"rules": {
|
||||
"includePatterns": [],
|
||||
"dropPatterns": ["^\\s*$"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": false,
|
||||
"maxLines": 80,
|
||||
"headLines": 30,
|
||||
"tailLines": 20
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["^CONTAINER ID"],
|
||||
"summaryPatterns": []
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "docker ps",
|
||||
"input": "CONTAINER ID IMAGE COMMAND\nabc123 node \"npm start\"\n",
|
||||
"expected": "CONTAINER ID IMAGE COMMAND\nabc123 node \"npm start\""
|
||||
}
|
||||
]
|
||||
}
|
||||
33
open-sse/services/compression/engines/rtk/filters/du.json
Normal file
33
open-sse/services/compression/engines/rtk/filters/du.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"id": "du",
|
||||
"label": "du",
|
||||
"description": "Summarize disk-usage listings while preserving largest entries.",
|
||||
"category": "shell",
|
||||
"priority": 66,
|
||||
"match": {
|
||||
"outputTypes": ["shell-du"],
|
||||
"commands": ["^du\\b"],
|
||||
"patterns": ["^\\d+(?:\\.\\d+)?[KMGTP]?\\s+\\S+"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"dropPatterns": ["^\\s*$"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": true,
|
||||
"maxLines": 80,
|
||||
"headLines": 20,
|
||||
"tailLines": 30
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["Permission denied", "No such file"],
|
||||
"summaryPatterns": ["\\s+\\.$", "\\s+total$"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "du -sh *",
|
||||
"input": "4.0K package.json\n120M node_modules\n120M node_modules\n",
|
||||
"expected": "4.0K package.json\n120M node_modules\n120M node_modules"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"id": "error-stacktrace",
|
||||
"label": "Error Stacktrace",
|
||||
"description": "Collapse stack traces while preserving error text and top frames.",
|
||||
"category": "generic",
|
||||
"priority": 70,
|
||||
"match": {
|
||||
"outputTypes": ["error-stacktrace"],
|
||||
"commands": [],
|
||||
"patterns": ["Traceback \\(most recent call last\\):", "^\\s+at\\s+", "^panic: "]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"includePatterns": [
|
||||
"Error:",
|
||||
"Exception:",
|
||||
"Traceback",
|
||||
"^\\s+at ",
|
||||
"^panic:",
|
||||
"^thread ",
|
||||
"^Caused by:"
|
||||
],
|
||||
"dropPatterns": ["^\\s*$"],
|
||||
"collapsePatterns": ["^\\s+at "],
|
||||
"deduplicate": true,
|
||||
"maxLines": 80,
|
||||
"headLines": 25,
|
||||
"tailLines": 30
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["Error:", "Exception:", "^panic:", "Traceback"],
|
||||
"summaryPatterns": ["Caused by:"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"input": "Error: boom\n at fn (/repo/a.ts:1:1)\n at fn (/repo/a.ts:1:1)\nCaused by: bad\n",
|
||||
"expected": "Error: boom\n at fn (/repo/a.ts:1:1)\nCaused by: bad"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "gcloud",
|
||||
"label": "gcloud",
|
||||
"description": "Compress Google Cloud CLI output and preserve actionable errors.",
|
||||
"category": "cloud",
|
||||
"priority": 75,
|
||||
"match": {
|
||||
"outputTypes": ["gcloud"],
|
||||
"commands": ["^gcloud\\b"],
|
||||
"patterns": ["^ERROR: \\(gcloud\\.", "^Updated property \\["]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"dropPatterns": ["^Waiting for operation", "^\\s*$"],
|
||||
"includePatterns": ["^ERROR: \\(gcloud\\.", "^Updated property \\[", "^Created \\["],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": true,
|
||||
"maxLines": 120,
|
||||
"headLines": 30,
|
||||
"tailLines": 40
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["^ERROR: \\(gcloud\\.", "PERMISSION_DENIED", "NOT_FOUND"],
|
||||
"summaryPatterns": ["^Updated property \\[", "^Created \\["]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "gcloud run deploy",
|
||||
"input": "Waiting for operation [deploy]\nUpdated property [core/project].\nERROR: (gcloud.run.deploy) PERMISSION_DENIED: denied\n",
|
||||
"expected": "Updated property [core/project].\nERROR: (gcloud.run.deploy) PERMISSION_DENIED: denied"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,32 @@
|
||||
{
|
||||
"id": "generic-output",
|
||||
"label": "Generic Output",
|
||||
"description": "Fallback filter for unknown command output.",
|
||||
"category": "generic",
|
||||
"priority": 10,
|
||||
"match": {
|
||||
"outputTypes": ["generic-output", "generic-error"],
|
||||
"commands": [],
|
||||
"patterns": ["Error:", "Exception:", "Traceback \\(most recent call last\\):"]
|
||||
},
|
||||
"rules": {
|
||||
"includePatterns": [],
|
||||
"dropPatterns": ["^\\s*$"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": false,
|
||||
"maxLines": 120,
|
||||
"headLines": 35,
|
||||
"tailLines": 45
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["error", "failed", "exception", "traceback"],
|
||||
"summaryPatterns": []
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"input": "\nError: boom\n\nTraceback (most recent call last):\n file.py:1\n",
|
||||
"expected": "Error: boom\nTraceback (most recent call last):\n file.py:1"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,45 @@
|
||||
{
|
||||
"id": "git-branch",
|
||||
"label": "Git Branch",
|
||||
"description": "Compact git branch/switch output.",
|
||||
"category": "git",
|
||||
"priority": 94,
|
||||
"match": {
|
||||
"outputTypes": ["git-branch"],
|
||||
"commands": ["^git\\s+(?:branch|checkout|switch)\\b"],
|
||||
"patterns": ["^\\*\\s+\\S+", "Switched to (?:a new )?branch", "Already on"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"matchOutput": [
|
||||
{
|
||||
"pattern": "Switched to (?:a new )?branch",
|
||||
"message": "git: switched branch",
|
||||
"unless": "error|failed"
|
||||
},
|
||||
{
|
||||
"pattern": "Already on",
|
||||
"message": "git: already on branch"
|
||||
}
|
||||
],
|
||||
"includePatterns": ["^\\*\\s+\\S+", "^\\s{2}\\S+", "Switched to", "Already on"],
|
||||
"dropPatterns": ["^\\s*$"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": false,
|
||||
"maxLines": 60,
|
||||
"headLines": 20,
|
||||
"tailLines": 20
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["error", "failed"],
|
||||
"summaryPatterns": ["Switched to", "Already on"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "git (?:branch|checkout|switch)",
|
||||
"input": " main\n* feature/rtk\n release\n",
|
||||
"expected": " main\n* feature/rtk\n release"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"id": "git-diff",
|
||||
"label": "Git Diff",
|
||||
"description": "Drop low-signal diff metadata while preserving files, hunks, and changed lines.",
|
||||
"category": "git",
|
||||
"priority": 94,
|
||||
"match": {
|
||||
"outputTypes": ["git-diff"],
|
||||
"commands": ["^git\\s+(?:diff|show)\\b"],
|
||||
"patterns": ["^diff --git ", "^@@\\s+-\\d+,\\d+\\s+\\+\\d+,\\d+\\s+@@"]
|
||||
},
|
||||
"rules": {
|
||||
"includePatterns": [],
|
||||
"dropPatterns": ["^index [0-9a-f]+\\.\\.[0-9a-f]+.*$", "^--- a/", "^\\+\\+\\+ b/"],
|
||||
"collapsePatterns": ["^\\s*$"],
|
||||
"deduplicate": true,
|
||||
"maxLines": 180,
|
||||
"headLines": 40,
|
||||
"tailLines": 50
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["^diff --git ", "^@@ ", "^[+-](?![+-]{2})"],
|
||||
"summaryPatterns": []
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "git (?:diff|show)",
|
||||
"input": "diff --git a/a.ts b/a.ts\n@@ -1,2 +1,2 @@\n-console.log(\"old\")\n+console.log(\"new\")\n",
|
||||
"expected": "diff --git a/a.ts b/a.ts\n@@ -1,2 +1,2 @@\n-console.log(\"old\")\n+console.log(\"new\")\n"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"id": "git-log",
|
||||
"label": "Git Log",
|
||||
"description": "Keep commit identifiers and subjects from verbose logs.",
|
||||
"category": "git",
|
||||
"priority": 70,
|
||||
"match": {
|
||||
"outputTypes": ["git-log"],
|
||||
"commands": ["^git\\s+log\\b"],
|
||||
"patterns": ["^commit [0-9a-f]{7,40}", "^Author: "]
|
||||
},
|
||||
"rules": {
|
||||
"includePatterns": ["^commit [0-9a-f]{7,40}", "^\\s{4}\\S"],
|
||||
"dropPatterns": ["^Author: ", "^Date: ", "^\\s*$"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": false,
|
||||
"maxLines": 120,
|
||||
"headLines": 40,
|
||||
"tailLines": 20
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["^commit "],
|
||||
"summaryPatterns": []
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "git log",
|
||||
"input": "commit abcdef1234567890\nAuthor: Dev <dev@example.com>\nDate: today\n\n feat: add rtk\n",
|
||||
"expected": "commit abcdef1234567890\n feat: add rtk"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"id": "git-status",
|
||||
"label": "Git Status",
|
||||
"description": "Keep branch, staged/unstaged headings, and changed file lines.",
|
||||
"category": "git",
|
||||
"priority": 95,
|
||||
"match": {
|
||||
"outputTypes": ["git-status"],
|
||||
"commands": ["^git\\s+status\\b"],
|
||||
"patterns": ["^On branch ", "^Changes (?:not staged|to be committed)", "^Untracked files:"]
|
||||
},
|
||||
"rules": {
|
||||
"includePatterns": [
|
||||
"^On branch ",
|
||||
"^Your branch ",
|
||||
"^Changes ",
|
||||
"^Untracked files:",
|
||||
"^\\s*(modified|new file|deleted|renamed|both modified):",
|
||||
"^\\s*[MADRCU?!]{1,2}\\s+"
|
||||
],
|
||||
"dropPatterns": ["^\\s*$", "^\\s*\\(use .*\\)$"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": false,
|
||||
"maxLines": 80,
|
||||
"headLines": 12,
|
||||
"tailLines": 20
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["^Changes ", "^Untracked files:"],
|
||||
"summaryPatterns": []
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "git status",
|
||||
"input": "On branch main\nChanges not staged for commit:\n (use \"git add\" to update)\n\tmodified: src/app.ts\nnothing added to commit\n",
|
||||
"expected": "On branch main\nChanges not staged for commit:\n\tmodified: src/app.ts"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "golangci-lint",
|
||||
"label": "golangci-lint",
|
||||
"description": "Keep golangci-lint findings and summary.",
|
||||
"category": "build",
|
||||
"priority": 82,
|
||||
"match": {
|
||||
"outputTypes": [],
|
||||
"commands": ["^golangci-lint\\b"],
|
||||
"patterns": ["issues:", "level=error", ":\\d+:\\d+:"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"includePatterns": [":\\d+:\\d+:", "level=error", "issues:", "Error:"],
|
||||
"dropPatterns": ["^\\s*$"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": true,
|
||||
"maxLines": 140,
|
||||
"headLines": 35,
|
||||
"tailLines": 50
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["level=error", "Error:"],
|
||||
"summaryPatterns": ["issues:"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "golangci-lint",
|
||||
"input": "main.go:1:1: issue text\nlevel=error msg=\"runner failed\"\nissues: 1\n",
|
||||
"expected": "main.go:1:1: issue text\nlevel=error msg=\"runner failed\"\nissues: 1"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"id": "json-output",
|
||||
"label": "JSON Output",
|
||||
"description": "Keep actionable JSON status, error and message fields while preserving structure.",
|
||||
"category": "generic",
|
||||
"priority": 70,
|
||||
"match": {
|
||||
"outputTypes": ["json-output"],
|
||||
"commands": ["^(?:jq\\b|cat\\s+.*\\.json\\b)"],
|
||||
"patterns": ["^\\s*[\\[{][\\s\\S]*[\\]}]\\s*$"]
|
||||
},
|
||||
"rules": {
|
||||
"includePatterns": [
|
||||
"\"status\"\\s*:",
|
||||
"\"error\"\\s*:",
|
||||
"\"errors\"\\s*:",
|
||||
"\"code\"\\s*:",
|
||||
"\"message\"\\s*:",
|
||||
"[{}\\[\\],]"
|
||||
],
|
||||
"dropPatterns": ["^\\s*$"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": false,
|
||||
"maxLines": 120,
|
||||
"headLines": 20,
|
||||
"tailLines": 30
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["\"error\"", "\"errors\"", "\"message\""],
|
||||
"summaryPatterns": []
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "(?:jq\\b|cat .*\\.json\\b)",
|
||||
"input": "{\n \"status\": \"ok\",\n \"items\": [1,2,3]\n}\n",
|
||||
"expected": "{\n \"status\": \"ok\",\n \"items\": [1,2,3]\n}"
|
||||
}
|
||||
]
|
||||
}
|
||||
38
open-sse/services/compression/engines/rtk/filters/make.json
Normal file
38
open-sse/services/compression/engines/rtk/filters/make.json
Normal file
@@ -0,0 +1,38 @@
|
||||
{
|
||||
"id": "make",
|
||||
"label": "Make",
|
||||
"description": "Strip make directory chatter and empty lines.",
|
||||
"category": "build",
|
||||
"priority": 78,
|
||||
"match": {
|
||||
"outputTypes": [],
|
||||
"commands": ["^make\\b"],
|
||||
"patterns": ["make\\[\\d+\\]: (?:Entering|Leaving) directory"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"dropPatterns": [
|
||||
"^make\\[\\d+\\]: (?:Entering|Leaving) directory",
|
||||
"^\\s*$",
|
||||
"Nothing to be done for"
|
||||
],
|
||||
"collapsePatterns": ["^(?:cc|gcc|clang|g\\+\\+)\\b"],
|
||||
"deduplicate": true,
|
||||
"maxLines": 120,
|
||||
"headLines": 30,
|
||||
"tailLines": 40,
|
||||
"onEmpty": "make: ok"
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["error:", "failed", "\\*\\*\\*"],
|
||||
"summaryPatterns": ["built", "linking"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "make",
|
||||
"input": "make[1]: Entering directory '/repo'\ngcc -O2 foo.c\nmake[1]: Leaving directory '/repo'\n",
|
||||
"expected": "gcc -O2 foo.c"
|
||||
}
|
||||
]
|
||||
}
|
||||
34
open-sse/services/compression/engines/rtk/filters/mypy.json
Normal file
34
open-sse/services/compression/engines/rtk/filters/mypy.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "mypy",
|
||||
"label": "mypy",
|
||||
"description": "Keep mypy errors grouped by file and final summary.",
|
||||
"category": "build",
|
||||
"priority": 82,
|
||||
"match": {
|
||||
"outputTypes": [],
|
||||
"commands": ["^mypy\\b", "^python\\s+-m\\s+mypy\\b"],
|
||||
"patterns": [": error:", "Found \\d+ errors? in"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"includePatterns": [": error:", ": note:", "Found \\d+ errors?", "Success: no issues found"],
|
||||
"dropPatterns": ["^\\s*$"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": true,
|
||||
"maxLines": 160,
|
||||
"headLines": 40,
|
||||
"tailLines": 60
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": [": error:", "Found \\d+ errors?"],
|
||||
"summaryPatterns": ["Success:", "Found \\d+"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "mypy",
|
||||
"input": "app.py:1: error: Incompatible types\nFound 1 error in 1 file\n",
|
||||
"expected": "app.py:1: error: Incompatible types\nFound 1 error in 1 file"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"id": "npm-audit",
|
||||
"label": "npm audit",
|
||||
"description": "Keep vulnerability counts and remediation hints.",
|
||||
"category": "package",
|
||||
"priority": 84,
|
||||
"match": {
|
||||
"outputTypes": ["npm-audit"],
|
||||
"commands": ["^(?:npm|pnpm|yarn)\\s+audit\\b"],
|
||||
"patterns": ["found \\d+ vulnerabilities", "\\bcritical\\b"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"includePatterns": [
|
||||
"vulnerabilities",
|
||||
"critical",
|
||||
"high",
|
||||
"moderate",
|
||||
"low",
|
||||
"fix available",
|
||||
"npm audit fix",
|
||||
"No vulnerabilities"
|
||||
],
|
||||
"dropPatterns": ["^\\s*$", "^# npm audit report"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": true,
|
||||
"maxLines": 120,
|
||||
"headLines": 30,
|
||||
"tailLines": 45
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["critical", "high"],
|
||||
"summaryPatterns": ["vulnerabilities", "No vulnerabilities"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "(?:npm|pnpm|yarn) audit",
|
||||
"input": "# npm audit report\ncritical vulnerability in foo\nfound 1 vulnerabilities\nnpm audit fix\n",
|
||||
"expected": "critical vulnerability in foo\nfound 1 vulnerabilities\nnpm audit fix"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"id": "npm-install",
|
||||
"label": "Package Install",
|
||||
"description": "Keep install summaries, warnings and audit results.",
|
||||
"category": "package",
|
||||
"priority": 65,
|
||||
"match": {
|
||||
"outputTypes": ["npm-install"],
|
||||
"commands": ["^(?:npm|pnpm|yarn)\\s+(?:install|add|update)\\b"],
|
||||
"patterns": ["added \\d+ packages", "packages are looking for funding", "audited \\d+ packages"]
|
||||
},
|
||||
"rules": {
|
||||
"includePatterns": [
|
||||
"added \\d+ packages",
|
||||
"removed \\d+ packages",
|
||||
"changed \\d+ packages",
|
||||
"audited \\d+ packages",
|
||||
"vulnerabilities",
|
||||
"WARN",
|
||||
"ERR!"
|
||||
],
|
||||
"dropPatterns": ["^\\s*$", "^\\s*\\|", "^Progress:"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": false,
|
||||
"maxLines": 80,
|
||||
"headLines": 20,
|
||||
"tailLines": 30
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["WARN", "ERR", "vulnerabilities"],
|
||||
"summaryPatterns": []
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "(?:npm|pnpm|yarn) (?:install|add|update)",
|
||||
"input": "added 3 packages\npackages are looking for funding\naudited 3 packages\n",
|
||||
"expected": "added 3 packages\naudited 3 packages"
|
||||
}
|
||||
]
|
||||
}
|
||||
34
open-sse/services/compression/engines/rtk/filters/nx.json
Normal file
34
open-sse/services/compression/engines/rtk/filters/nx.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "nx",
|
||||
"label": "Nx",
|
||||
"description": "Compact Nx task graph and output.",
|
||||
"category": "build",
|
||||
"priority": 76,
|
||||
"match": {
|
||||
"outputTypes": [],
|
||||
"commands": ["^nx\\b", "^npx\\s+nx\\b"],
|
||||
"patterns": ["NX ", "Successfully ran target"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"dropPatterns": ["^\\s*$", "^> nx run"],
|
||||
"includePatterns": ["NX ", "ERROR", "Failed", "Successfully ran", "Running target"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": true,
|
||||
"maxLines": 120,
|
||||
"headLines": 25,
|
||||
"tailLines": 50
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["ERROR", "Failed"],
|
||||
"summaryPatterns": ["Successfully ran"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "nx",
|
||||
"input": "> nx run app:build\nNX Failed tasks:\nERROR boom\n",
|
||||
"expected": "NX Failed tasks:\nERROR boom"
|
||||
}
|
||||
]
|
||||
}
|
||||
41
open-sse/services/compression/engines/rtk/filters/pip.json
Normal file
41
open-sse/services/compression/engines/rtk/filters/pip.json
Normal file
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"id": "pip",
|
||||
"label": "pip",
|
||||
"description": "Strip pip install progress and keep install/failure summary.",
|
||||
"category": "package",
|
||||
"priority": 72,
|
||||
"match": {
|
||||
"outputTypes": [],
|
||||
"commands": ["^pip\\s+(?:install|sync|download)\\b", "^python\\s+-m\\s+pip\\s+install\\b"],
|
||||
"patterns": ["Successfully installed", "Requirement already satisfied"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"dropPatterns": [
|
||||
"^Collecting ",
|
||||
"^Downloading ",
|
||||
"^Installing collected packages:",
|
||||
"^Requirement already satisfied",
|
||||
"^\\s*$"
|
||||
],
|
||||
"includePatterns": ["Successfully installed", "ERROR:", "WARNING:", "failed", "Could not find"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": true,
|
||||
"maxLines": 100,
|
||||
"headLines": 25,
|
||||
"tailLines": 35,
|
||||
"onEmpty": "pip: ok"
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["ERROR:", "failed", "Could not find"],
|
||||
"summaryPatterns": ["Successfully installed"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "pip (?:install|sync|download)",
|
||||
"input": "Collecting requests\nDownloading requests.whl\nSuccessfully installed requests-2.0\n",
|
||||
"expected": "Successfully installed requests-2.0"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"id": "playwright",
|
||||
"label": "Playwright",
|
||||
"description": "Keep failed specs, error context, and summary.",
|
||||
"category": "test",
|
||||
"priority": 86,
|
||||
"match": {
|
||||
"outputTypes": [],
|
||||
"commands": ["^playwright\\s+test\\b", "^npx\\s+playwright\\s+test\\b"],
|
||||
"patterns": ["Running \\d+ tests?", "Error Context:", "failed"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"dropPatterns": ["^\\s*$", "^Running \\d+ tests? using"],
|
||||
"includePatterns": [
|
||||
"✘",
|
||||
"Error:",
|
||||
"Error Context:",
|
||||
"failed",
|
||||
"passed",
|
||||
"Slow test file",
|
||||
"^\\s*\\d+\\)"
|
||||
],
|
||||
"collapsePatterns": ["^\\s+at "],
|
||||
"deduplicate": true,
|
||||
"maxLines": 160,
|
||||
"headLines": 35,
|
||||
"tailLines": 60
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["Error:", "✘", "failed"],
|
||||
"summaryPatterns": ["passed", "failed"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "playwright test",
|
||||
"input": "Running 2 tests using 1 worker\n ✘ 1 login.spec.ts:3:1 › login\nError: expected visible\n 1 failed\n",
|
||||
"expected": " ✘ 1 login.spec.ts:3:1 › login\nError: expected visible\n 1 failed"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,35 @@
|
||||
{
|
||||
"id": "poetry-install",
|
||||
"label": "Poetry Install",
|
||||
"description": "Strip Poetry package operation noise.",
|
||||
"category": "package",
|
||||
"priority": 72,
|
||||
"match": {
|
||||
"outputTypes": [],
|
||||
"commands": ["^poetry\\s+(?:install|update)\\b"],
|
||||
"patterns": ["Package operations:", "Installing dependencies"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"dropPatterns": ["^ - Installing ", "^ - Updating ", "^Installing dependencies", "^\\s*$"],
|
||||
"includePatterns": ["Package operations:", "Writing lock file", "Error", "failed"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": true,
|
||||
"maxLines": 100,
|
||||
"headLines": 25,
|
||||
"tailLines": 35,
|
||||
"onEmpty": "poetry: ok"
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["Error", "failed"],
|
||||
"summaryPatterns": ["Package operations:", "Writing lock file"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "poetry (?:install|update)",
|
||||
"input": "Installing dependencies\nPackage operations: 2 installs, 0 updates\n - Installing foo (1.0)\nWriting lock file\n",
|
||||
"expected": "Package operations: 2 installs, 0 updates\nWriting lock file"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"id": "prettier",
|
||||
"label": "Prettier",
|
||||
"description": "Keep files that would be changed and final summary.",
|
||||
"category": "build",
|
||||
"priority": 79,
|
||||
"match": {
|
||||
"outputTypes": [],
|
||||
"commands": ["^prettier\\b", "^npx\\s+prettier\\b"],
|
||||
"patterns": ["\\[warn\\]", "All matched files use Prettier"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"matchOutput": [
|
||||
{
|
||||
"pattern": "All matched files use Prettier",
|
||||
"message": "prettier: ok"
|
||||
}
|
||||
],
|
||||
"dropPatterns": ["^\\s*$"],
|
||||
"includePatterns": ["\\[warn\\]", "Code style issues", "All matched files"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": true,
|
||||
"maxLines": 80,
|
||||
"headLines": 20,
|
||||
"tailLines": 30
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["\\[error\\]", "Code style issues"],
|
||||
"summaryPatterns": ["All matched files"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "prettier",
|
||||
"input": "[warn] src/a.ts\n[warn] Code style issues found in the above file. Forgot to run Prettier?\n",
|
||||
"expected": "[warn] src/a.ts\n[warn] Code style issues found in the above file. Forgot to run Prettier?"
|
||||
}
|
||||
]
|
||||
}
|
||||
34
open-sse/services/compression/engines/rtk/filters/ps.json
Normal file
34
open-sse/services/compression/engines/rtk/filters/ps.json
Normal file
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "ps",
|
||||
"label": "ps",
|
||||
"description": "Keep process table headers and process rows, dropping empty noise.",
|
||||
"category": "shell",
|
||||
"priority": 68,
|
||||
"match": {
|
||||
"outputTypes": ["shell-ps"],
|
||||
"commands": ["^ps\\b"],
|
||||
"patterns": ["^(?:USER\\s+PID|\\s*PID\\s+)"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"dropPatterns": ["^\\s*$"],
|
||||
"includePatterns": ["^(?:USER\\s+PID|\\s*PID\\s+)", "^\\S+\\s+\\d+\\s+", "^\\s*\\d+\\s+"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": false,
|
||||
"maxLines": 120,
|
||||
"headLines": 30,
|
||||
"tailLines": 50
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["defunct", "Permission denied"],
|
||||
"summaryPatterns": ["^(?:USER\\s+PID|\\s*PID\\s+)"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "ps aux",
|
||||
"input": "USER PID %CPU COMMAND\nroot 1 0.0 init\nnode 1234 5.0 node server.js\n",
|
||||
"expected": "USER PID %CPU COMMAND\nroot 1 0.0 init\nnode 1234 5.0 node server.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
33
open-sse/services/compression/engines/rtk/filters/rsync.json
Normal file
33
open-sse/services/compression/engines/rtk/filters/rsync.json
Normal file
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"id": "rsync",
|
||||
"label": "rsync",
|
||||
"description": "Drop rsync transfer chatter while preserving copied files and errors.",
|
||||
"category": "cloud",
|
||||
"priority": 73,
|
||||
"match": {
|
||||
"outputTypes": ["rsync"],
|
||||
"commands": ["^rsync\\b"],
|
||||
"patterns": ["^sending incremental file list", "^rsync error:"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"dropPatterns": ["^sending incremental file list", "^sent \\d", "^total size is", "^\\s*$"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": true,
|
||||
"maxLines": 140,
|
||||
"headLines": 35,
|
||||
"tailLines": 50
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["^rsync error:", "Permission denied", "No such file"],
|
||||
"summaryPatterns": ["^sent \\d", "^total size is"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "rsync -av",
|
||||
"input": "sending incremental file list\napp.js\nsent 120 bytes received 20 bytes\nrsync error: some files could not be transferred (code 23)\n",
|
||||
"expected": "app.js\nrsync error: some files could not be transferred (code 23)"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "rubocop",
|
||||
"label": "RuboCop",
|
||||
"description": "Keep RuboCop offenses and final summary.",
|
||||
"category": "build",
|
||||
"priority": 74,
|
||||
"match": {
|
||||
"outputTypes": ["rubocop"],
|
||||
"commands": ["^rubocop\\b", "^bundle\\s+exec\\s+rubocop\\b"],
|
||||
"patterns": ["^Inspecting \\d+ files", "^\\S+\\.rb:\\d+:\\d+: [A-Z]:"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"dropPatterns": ["^Inspecting \\d+ files", "^\\.+$", "^\\s*$"],
|
||||
"includePatterns": ["^\\S+\\.rb:\\d+:\\d+: [A-Z]:", "^\\d+ files inspected"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": true,
|
||||
"maxLines": 120,
|
||||
"headLines": 30,
|
||||
"tailLines": 45
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": [" [EF]: ", "Offenses:"],
|
||||
"summaryPatterns": ["^\\d+ files inspected"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "rubocop",
|
||||
"input": "Inspecting 2 files\n.C\napp/models/user.rb:12:3: C: Style/IfUnlessModifier: Favor modifier if usage.\n2 files inspected, 1 offense detected\n",
|
||||
"expected": "app/models/user.rb:12:3: C: Style/IfUnlessModifier: Favor modifier if usage.\n2 files inspected, 1 offense detected"
|
||||
}
|
||||
]
|
||||
}
|
||||
40
open-sse/services/compression/engines/rtk/filters/ruff.json
Normal file
40
open-sse/services/compression/engines/rtk/filters/ruff.json
Normal file
@@ -0,0 +1,40 @@
|
||||
{
|
||||
"id": "ruff",
|
||||
"label": "Ruff",
|
||||
"description": "Keep Ruff diagnostics and summary.",
|
||||
"category": "build",
|
||||
"priority": 82,
|
||||
"match": {
|
||||
"outputTypes": [],
|
||||
"commands": ["^ruff\\b", "^uv\\s+run\\s+ruff\\b"],
|
||||
"patterns": ["Found \\d+ errors?", "All checks passed"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"matchOutput": [
|
||||
{
|
||||
"pattern": "All checks passed",
|
||||
"message": "ruff: ok"
|
||||
}
|
||||
],
|
||||
"dropPatterns": ["^\\s*$"],
|
||||
"includePatterns": ["^[A-Z]\\d{3}", "Found \\d+", "error:", "warning:", "-->"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": true,
|
||||
"maxLines": 120,
|
||||
"headLines": 30,
|
||||
"tailLines": 40
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["^[A-Z]\\d{3}", "error:"],
|
||||
"summaryPatterns": ["Found \\d+", "All checks passed"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "ruff",
|
||||
"input": "F401 app.py:1:1 unused import\nFound 1 error.\n",
|
||||
"expected": "F401 app.py:1:1 unused import\nFound 1 error."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"id": "shell-find",
|
||||
"label": "find",
|
||||
"description": "Limit large find output while preserving head and tail.",
|
||||
"category": "shell",
|
||||
"priority": 80,
|
||||
"match": {
|
||||
"outputTypes": ["shell-find"],
|
||||
"commands": ["^find\\b"],
|
||||
"patterns": ["^(?:\\./|/|[A-Za-z0-9_.-]+/)"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"dropPatterns": ["^\\s*$"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": true,
|
||||
"maxLines": 120,
|
||||
"headLines": 40,
|
||||
"tailLines": 40
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["Permission denied", "No such file"],
|
||||
"summaryPatterns": []
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "find",
|
||||
"input": "./src/a.ts\n./src/b.ts\n./node_modules/pkg/index.js\n",
|
||||
"expected": "./src/a.ts\n./src/b.ts\n./node_modules/pkg/index.js"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
{
|
||||
"id": "shell-grep",
|
||||
"label": "grep/rg",
|
||||
"description": "Deduplicate grep output while preserving matches.",
|
||||
"category": "shell",
|
||||
"priority": 82,
|
||||
"match": {
|
||||
"outputTypes": ["shell-grep"],
|
||||
"commands": ["^(?:grep|rg|ag)\\b"],
|
||||
"patterns": [
|
||||
"^[\\w./-]+\\.(?:ts|tsx|js|jsx|py|go|rs|java|rb|md|json|ya?ml|txt):\\d*:",
|
||||
"^[\\w./-]+/[\\w./-]+:\\d*:"
|
||||
]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"dropPatterns": ["^\\s*$"],
|
||||
"includePatterns": ["^[\\w./-]+(?::\\d+)?:"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": true,
|
||||
"maxLines": 160,
|
||||
"headLines": 40,
|
||||
"tailLines": 60
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["No such file", "Permission denied"],
|
||||
"summaryPatterns": ["^[^:\\n]+(?::\\d+)?:"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "(?:grep|rg|ag)",
|
||||
"input": "src/a.ts:1:match\nsrc/a.ts:1:match\nsrc/b.ts:2:other\n",
|
||||
"expected": "src/a.ts:1:match\nsrc/a.ts:1:match\nsrc/b.ts:2:other"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,33 @@
|
||||
{
|
||||
"id": "shell-ls",
|
||||
"label": "Shell Listing",
|
||||
"description": "Keep directory listing boundaries without flooding the prompt.",
|
||||
"category": "shell",
|
||||
"priority": 50,
|
||||
"match": {
|
||||
"outputTypes": ["shell-ls"],
|
||||
"commands": ["^(?:ls(?:\\s+-[A-Za-z]+)?|find)\\b"],
|
||||
"patterns": ["^total \\d+", "^\\S+\\s+\\S+\\s+\\d+\\s+\\w+\\s+\\d{1,2}\\s+"]
|
||||
},
|
||||
"rules": {
|
||||
"includePatterns": [],
|
||||
"dropPatterns": ["^\\s*$"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": false,
|
||||
"maxLines": 100,
|
||||
"headLines": 40,
|
||||
"tailLines": 30
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["^total \\d+"],
|
||||
"summaryPatterns": []
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "(?:ls(?: -[A-Za-z]+)?|find)",
|
||||
"input": "total 8\ndrwxr-xr-x 2 user user 4096 May 1 src\n-rw-r--r-- 1 user user 10 May 1 package.json\n",
|
||||
"expected": "total 8\ndrwxr-xr-x 2 user user 4096 May 1 src\n-rw-r--r-- 1 user user 10 May 1 package.json"
|
||||
}
|
||||
]
|
||||
}
|
||||
43
open-sse/services/compression/engines/rtk/filters/ssh.json
Normal file
43
open-sse/services/compression/engines/rtk/filters/ssh.json
Normal file
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"id": "ssh",
|
||||
"label": "ssh",
|
||||
"description": "Compress SSH connection logs while preserving access and host-key failures.",
|
||||
"category": "cloud",
|
||||
"priority": 72,
|
||||
"match": {
|
||||
"outputTypes": ["ssh"],
|
||||
"commands": ["^ssh\\b"],
|
||||
"patterns": [
|
||||
"Permission denied \\(",
|
||||
"Host key verification failed",
|
||||
"Connection (?:closed|timed out)"
|
||||
]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"dropPatterns": ["^debug\\d:", "^\\s*$"],
|
||||
"includePatterns": [
|
||||
"Permission denied",
|
||||
"Host key verification failed",
|
||||
"Connection (?:closed|timed out)",
|
||||
"^Welcome "
|
||||
],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": true,
|
||||
"maxLines": 100,
|
||||
"headLines": 25,
|
||||
"tailLines": 40
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["Permission denied", "Host key verification failed", "Connection timed out"],
|
||||
"summaryPatterns": ["^Welcome "]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "ssh host",
|
||||
"input": "debug1: Connecting to host\nWelcome to Ubuntu\nPermission denied (publickey).\n",
|
||||
"expected": "Welcome to Ubuntu\nPermission denied (publickey)."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"id": "systemctl-status",
|
||||
"label": "systemctl status",
|
||||
"description": "Preserve unit status, active state, and journal errors.",
|
||||
"category": "infra",
|
||||
"priority": 77,
|
||||
"match": {
|
||||
"outputTypes": [],
|
||||
"commands": ["^systemctl\\s+status\\b"],
|
||||
"patterns": ["Loaded:", "Active:", "Main PID:"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"includePatterns": [
|
||||
"Loaded:",
|
||||
"Active:",
|
||||
"Main PID:",
|
||||
"CGroup:",
|
||||
"error",
|
||||
"failed",
|
||||
"Started",
|
||||
"Stopped"
|
||||
],
|
||||
"dropPatterns": ["^\\s*$"],
|
||||
"collapsePatterns": ["^\\s+Docs:"],
|
||||
"deduplicate": true,
|
||||
"maxLines": 100,
|
||||
"headLines": 30,
|
||||
"tailLines": 40
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["failed", "error"],
|
||||
"summaryPatterns": ["Active:", "Loaded:"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "systemctl status",
|
||||
"input": "Loaded: loaded (/etc/systemd/system/app.service)\nActive: failed (Result: exit-code)\nMain PID: 123\nMay 1 app[123]: error boot failed\n",
|
||||
"expected": "Loaded: loaded (/etc/systemd/system/app.service)\nActive: failed (Result: exit-code)\nMain PID: 123\nMay 1 app[123]: error boot failed"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
{
|
||||
"id": "terraform-plan",
|
||||
"label": "Terraform Plan",
|
||||
"description": "Keep plan actions and summary while stripping refresh noise.",
|
||||
"category": "infra",
|
||||
"priority": 88,
|
||||
"match": {
|
||||
"outputTypes": [],
|
||||
"commands": ["^terraform\\s+plan\\b"],
|
||||
"patterns": ["Terraform will perform the following actions", "Plan: \\d+ to add"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"dropPatterns": ["Refreshing state", "Acquiring state lock", "Releasing state lock", "^\\s*$"],
|
||||
"includePatterns": ["Terraform will perform", "^[ ]*[#~+\\-/]", "Plan: \\d+", "Error:"],
|
||||
"collapsePatterns": [],
|
||||
"deduplicate": false,
|
||||
"maxLines": 160,
|
||||
"headLines": 35,
|
||||
"tailLines": 50
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["Error:", "failed"],
|
||||
"summaryPatterns": ["Plan: \\d+"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "terraform plan",
|
||||
"input": "Refreshing state... [id=a]\nTerraform will perform the following actions:\n # aws_instance.web will be created\nPlan: 1 to add, 0 to change, 0 to destroy.\n",
|
||||
"expected": "Terraform will perform the following actions:\n # aws_instance.web will be created\nPlan: 1 to add, 0 to change, 0 to destroy."
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,42 @@
|
||||
{
|
||||
"id": "test-cargo",
|
||||
"label": "Cargo Test",
|
||||
"description": "Keep Cargo test failures and final summary.",
|
||||
"category": "test",
|
||||
"priority": 90,
|
||||
"match": {
|
||||
"outputTypes": ["test-cargo"],
|
||||
"commands": ["^cargo\\s+(?:test|nextest)\\b"],
|
||||
"patterns": ["^running \\d+ tests?", "test result:"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"includePatterns": [
|
||||
"^running \\d+ tests?",
|
||||
"^test .* \\.\\.\\. FAILED",
|
||||
"^failures:",
|
||||
"^---- ",
|
||||
"^thread ",
|
||||
"^test result:",
|
||||
"^error:"
|
||||
],
|
||||
"dropPatterns": ["^test .* \\.\\.\\. ok$", "^\\s*$"],
|
||||
"collapsePatterns": ["^\\s+at "],
|
||||
"deduplicate": true,
|
||||
"maxLines": 180,
|
||||
"headLines": 40,
|
||||
"tailLines": 70
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["FAILED", "^error:", "^thread "],
|
||||
"summaryPatterns": ["test result:"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "cargo (?:test|nextest)",
|
||||
"input": "running 2 tests\ntest a ... ok\ntest b ... FAILED\nfailures:\n---- b stdout ----\nthread b panicked at boom\ntest result: FAILED. 1 passed; 1 failed\n",
|
||||
"expected": "running 2 tests\ntest b ... FAILED\nfailures:\n---- b stdout ----\nthread b panicked at boom\ntest result: FAILED. 1 passed; 1 failed"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"id": "test-go",
|
||||
"label": "Go Test",
|
||||
"description": "Keep failing Go tests and package summaries.",
|
||||
"category": "test",
|
||||
"priority": 90,
|
||||
"match": {
|
||||
"outputTypes": ["test-go"],
|
||||
"commands": ["^go\\s+test\\b"],
|
||||
"patterns": ["^--- FAIL:", "^(?:ok|FAIL)\\s+"]
|
||||
},
|
||||
"rules": {
|
||||
"stripAnsi": true,
|
||||
"includePatterns": [
|
||||
"^--- FAIL:",
|
||||
"^FAIL",
|
||||
"^ok\\s+",
|
||||
"^panic:",
|
||||
"^\\s+.*_test\\.go:\\d+",
|
||||
"^\\s*Error"
|
||||
],
|
||||
"dropPatterns": ["^\\s*$"],
|
||||
"collapsePatterns": ["^=== RUN"],
|
||||
"deduplicate": true,
|
||||
"maxLines": 160,
|
||||
"headLines": 35,
|
||||
"tailLines": 60
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["^--- FAIL:", "^panic:", "^FAIL"],
|
||||
"summaryPatterns": ["^ok\\s+", "^FAIL\\s+"]
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "go test",
|
||||
"input": "=== RUN TestA\n--- FAIL: TestA (0.00s)\n a_test.go:1: boom\nFAIL\t./pkg\t0.1s\n",
|
||||
"expected": "--- FAIL: TestA (0.00s)\n a_test.go:1: boom\nFAIL\t./pkg\t0.1s"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,43 @@
|
||||
{
|
||||
"id": "test-jest",
|
||||
"label": "Jest",
|
||||
"description": "Keep Jest failures and summary while removing passing noise.",
|
||||
"category": "test",
|
||||
"priority": 90,
|
||||
"match": {
|
||||
"outputTypes": ["test-jest"],
|
||||
"commands": ["^(?:jest|npm\\s+(?:run\\s+)?test)\\b"],
|
||||
"patterns": ["Test Suites:\\s+\\d+", "Tests:\\s+\\d+", "^PASS\\s+", "^FAIL\\s+"]
|
||||
},
|
||||
"rules": {
|
||||
"includePatterns": [
|
||||
"^FAIL\\s+",
|
||||
"^\\s*●\\s+",
|
||||
"Expected:",
|
||||
"Received:",
|
||||
"Test Suites:",
|
||||
"Tests:",
|
||||
"Snapshots:",
|
||||
"Time:",
|
||||
"\\s+at\\s+"
|
||||
],
|
||||
"dropPatterns": ["^PASS\\s+", "^\\s*✓\\s+"],
|
||||
"collapsePatterns": ["^\\s*at "],
|
||||
"deduplicate": true,
|
||||
"maxLines": 140,
|
||||
"headLines": 24,
|
||||
"tailLines": 50
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["FAIL", "Expected:", "Received:", "Test Suites:"],
|
||||
"summaryPatterns": []
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "(?:jest|npm (?:run )?test)",
|
||||
"input": "PASS src/a.test.ts\nFAIL src/b.test.ts\nError: boom\nTest Suites: 1 failed, 1 passed\nTests: 1 failed, 1 passed\n",
|
||||
"expected": "FAIL src/b.test.ts\nTest Suites: 1 failed, 1 passed\nTests: 1 failed, 1 passed"
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
{
|
||||
"id": "test-pytest",
|
||||
"label": "Pytest",
|
||||
"description": "Keep pytest failures, tracebacks and terminal summary.",
|
||||
"category": "test",
|
||||
"priority": 90,
|
||||
"match": {
|
||||
"outputTypes": ["test-pytest"],
|
||||
"commands": ["^(?:pytest|python\\s+-m\\s+pytest)\\b"],
|
||||
"patterns": ["=+\\s+(?:\\d+\\s+)?(?:passed|failed|errors?)", "^E\\s+", "^FAILED "]
|
||||
},
|
||||
"rules": {
|
||||
"includePatterns": [
|
||||
"^FAILED ",
|
||||
"^ERROR ",
|
||||
"^E\\s+",
|
||||
"Traceback \\(most recent call last\\):",
|
||||
"=+ short test summary info =+",
|
||||
"=+ .*failed",
|
||||
"AssertionError"
|
||||
],
|
||||
"dropPatterns": ["^\\.", "^\\s*$"],
|
||||
"collapsePatterns": ["^E\\s+"],
|
||||
"deduplicate": true,
|
||||
"maxLines": 160,
|
||||
"headLines": 28,
|
||||
"tailLines": 60
|
||||
},
|
||||
"preserve": {
|
||||
"errorPatterns": ["FAILED", "ERROR", "Traceback", "AssertionError"],
|
||||
"summaryPatterns": []
|
||||
},
|
||||
"tests": [
|
||||
{
|
||||
"name": "inline sample",
|
||||
"command": "(?:pytest|python -m pytest)",
|
||||
"input": "FAILED tests/test_a.py::test_a - AssertionError\nE assert 1 == 2\n================ 1 failed, 2 passed ================\n",
|
||||
"expected": "FAILED tests/test_a.py::test_a - AssertionError\nE assert 1 == 2\n================ 1 failed, 2 passed ================"
|
||||
}
|
||||
]
|
||||
}
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user