From 5aff529f0b287f5204901b415cee5784916f4293 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sat, 2 May 2026 06:05:25 -0300 Subject: [PATCH 01/87] =?UTF-8?q?chore(release):=20v3.7.9=20=E2=80=94=20ge?= =?UTF-8?q?mini-cli=20cloud=20code=20separation?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- CHANGELOG.md | 8 ++++++++ docs/openapi.yaml | 2 +- electron/package-lock.json | 4 ++-- electron/package.json | 2 +- llm.txt | 4 ++-- open-sse/package.json | 2 +- package-lock.json | 6 +++--- package.json | 2 +- 8 files changed, 19 insertions(+), 11 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 0e926f19c8..15361bd85f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -4,6 +4,14 @@ --- +## [3.7.9] — 2026-05-02 + +### 🐛 Bug Fixes + +- **fix(gemini-cli):** separate Cloud Code transport from Antigravity (#1869 — thanks @dhaern) + +--- + ## [3.7.8] — 2026-05-01 ### ✨ New Features diff --git a/docs/openapi.yaml b/docs/openapi.yaml index 76b54c78e7..e4277d69a5 100644 --- a/docs/openapi.yaml +++ b/docs/openapi.yaml @@ -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, diff --git a/electron/package-lock.json b/electron/package-lock.json index fbcbba7997..abe3c9d549 100644 --- a/electron/package-lock.json +++ b/electron/package-lock.json @@ -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", diff --git a/electron/package.json b/electron/package.json index 8a182a53c8..f984a9d83f 100644 --- a/electron/package.json +++ b/electron/package.json @@ -1,6 +1,6 @@ { "name": "omniroute-desktop", - "version": "3.7.8", + "version": "3.7.9", "description": "OmniRoute Desktop Application", "main": "main.js", "author": { diff --git a/llm.txt b/llm.txt index 8b50978e3d..7702e3f58b 100644 --- a/llm.txt +++ b/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 diff --git a/open-sse/package.json b/open-sse/package.json index 8654cf7640..2d62ae8fa9 100644 --- a/open-sse/package.json +++ b/open-sse/package.json @@ -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", diff --git a/package-lock.json b/package-lock.json index 0fc3bcf712..c9be8e0b17 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "omniroute", - "version": "3.7.8", + "version": "3.7.9", "lockfileVersion": 3, "requires": true, "packages": { "": { "name": "omniroute", - "version": "3.7.8", + "version": "3.7.9", "hasInstallScript": true, "license": "MIT", "workspaces": [ @@ -15206,7 +15206,7 @@ }, "open-sse": { "name": "@omniroute/open-sse", - "version": "3.7.8" + "version": "3.7.9" } } } diff --git a/package.json b/package.json index a971888c4a..9e6f28ddce 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "omniroute", - "version": "3.7.8", + "version": "3.7.9", "description": "Smart AI Router with auto fallback — route to FREE & cheap models, zero downtime. Works with Cursor, Cline, Claude Desktop, Codex, and any OpenAI-compatible tool.", "type": "module", "bin": { From c49929327a5ba7f40adc57811c860582e508049c Mon Sep 17 00:00:00 2001 From: backryun Date: Sat, 2 May 2026 21:52:24 +0900 Subject: [PATCH 02/87] chore(provider): Update Jina AI model catalog (#1874) Integrated into release/v3.7.9 --- open-sse/config/embeddingRegistry.ts | 56 ++++++++++- open-sse/config/rerankRegistry.ts | 43 ++++++-- .../dashboard/providers/[id]/page.tsx | 26 +++-- src/app/api/models/test/route.ts | 86 +++++++++++++++- src/app/api/providers/[id]/models/route.ts | 33 +++++-- src/app/api/v1/embeddings/route.ts | 6 +- src/app/api/v1/models/catalog.ts | 50 +++++++++- src/lib/db/models.ts | 5 + src/lib/providerModels/managedModelImport.ts | 4 +- src/lib/providerModels/modelDiscovery.ts | 3 + src/shared/validation/schemas.ts | 2 + ...embedding-rerank-provider-registry.test.ts | 6 +- tests/unit/models-catalog-route.test.ts | 97 +++++++++++++++++++ tests/unit/provider-models-route.test.ts | 19 +++- 14 files changed, 394 insertions(+), 42 deletions(-) diff --git a/open-sse/config/embeddingRegistry.ts b/open-sse/config/embeddingRegistry.ts index 795c43c811..178db272cf 100644 --- a/open-sse/config/embeddingRegistry.ts +++ b/open-sse/config/embeddingRegistry.ts @@ -173,13 +173,50 @@ export const EMBEDDING_PROVIDERS: Record = { { 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 = { + jina: "jina-ai", + voyage: "voyage-ai", +}; + +function resolveEmbeddingProviderId(providerId: string): string { + return EMBEDDING_PROVIDER_ALIASES[providerId] || providerId; +} + +function normalizeProviderScopedModelId(providerId: string, modelId: string): string { + 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 +232,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 +283,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, diff --git a/open-sse/config/rerankRegistry.ts b/open-sse/config/rerankRegistry.ts index 80f9bb5466..e47366c4e3 100644 --- a/open-sse/config/rerankRegistry.ts +++ b/open-sse/config/rerankRegistry.ts @@ -69,20 +69,32 @@ 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) { + 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 +104,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 +144,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, }); diff --git a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx index d9095ee893..dbe4e54395 100644 --- a/src/app/(dashboard)/dashboard/providers/[id]/page.tsx +++ b/src/app/(dashboard)/dashboard/providers/[id]/page.tsx @@ -2020,6 +2020,10 @@ export default function ProviderDetailPage() { modelId, modelName: model.name || modelId, source: "imported", + ...(typeof model.apiFormat === "string" ? { apiFormat: model.apiFormat } : {}), + ...(Array.isArray(model.supportedEndpoints) + ? { supportedEndpoints: model.supportedEndpoints } + : {}), }), }); // Also create an alias for routing @@ -4212,6 +4216,7 @@ function CustomModelsSection({ + @@ -4222,7 +4227,7 @@ function CustomModelsSection({ {t("supportedEndpointsLabel")}
- {["chat", "embeddings", "images", "audio"].map((ep) => ( + {["chat", "embeddings", "rerank", "images", "audio"].map((ep) => (
@@ -4347,6 +4354,7 @@ function CustomModelsSection({ + @@ -4357,7 +4365,7 @@ function CustomModelsSection({ {t("supportedEndpointsLabel")}
- {["chat", "embeddings", "images", "audio"].map((ep) => ( + {["chat", "embeddings", "rerank", "images", "audio"].map((ep) => (
diff --git a/src/app/api/models/test/route.ts b/src/app/api/models/test/route.ts index c113bc979e..65654b4861 100644 --- a/src/app/api/models/test/route.ts +++ b/src/app/api/models/test/route.ts @@ -2,8 +2,10 @@ import { randomUUID } from "node:crypto"; import { NextResponse } from "next/server"; import { POST as postChatCompletion } from "@/app/api/v1/chat/completions/route"; import { handleValidatedEmbeddingRequestBody } from "@/app/api/v1/embeddings/route"; +import { POST as postRerank } from "@/app/api/v1/rerank/route"; import { buildComboTestRequestBody, extractComboTestResponseText } from "@/lib/combos/testHealth"; import { requireManagementAuth } from "@/lib/api/requireManagementAuth"; +import { getCustomModels } from "@/lib/localDb"; import { z } from "zod"; const testModelSchema = z.object({ @@ -73,6 +75,45 @@ function buildInternalChatRequest(testBody: Record, signal: Abo }); } +function buildInternalRerankRequest(testBody: Record, signal: AbortSignal) { + return new Request(`${INTERNAL_ORIGIN}/v1/rerank`, { + method: "POST", + headers: { + "Content-Type": "application/json", + "X-Internal-Test": "combo-health-check", + "X-OmniRoute-No-Cache": "true", + "X-Request-Id": `model-test-${randomUUID()}`, + }, + body: JSON.stringify(testBody), + signal, + }); +} + +function stripFirstSegment(modelId: string): string | null { + const slashIdx = modelId.indexOf("/"); + return slashIdx > 0 ? modelId.slice(slashIdx + 1) : null; +} + +async function findCustomModelMetadata(providerId: string, modelId: string) { + try { + const customModels = await getCustomModels(providerId); + if (!Array.isArray(customModels)) return null; + + const candidates = new Set([modelId]); + const stripped = stripFirstSegment(modelId); + if (stripped) candidates.add(stripped); + if (modelId.startsWith(`${providerId}/`)) candidates.add(modelId.slice(providerId.length + 1)); + + return ( + customModels.find( + (model: any) => typeof model?.id === "string" && candidates.has(model.id) + ) || null + ); + } catch { + return null; + } +} + export async function POST(request: Request) { const authError = await requireManagementAuth(request); if (authError) return authError; @@ -108,19 +149,47 @@ export async function POST(request: Request) { } const startTime = Date.now(); + const customModel = await findCustomModelMetadata(providerId, fullModelStr); + const supportedEndpoints = Array.isArray(customModel?.supportedEndpoints) + ? customModel.supportedEndpoints + : []; + const apiFormat = typeof customModel?.apiFormat === "string" ? customModel.apiFormat : ""; + const lowerModel = fullModelStr.toLowerCase(); + const isRerank = + apiFormat === "rerank" || + supportedEndpoints.includes("rerank") || + lowerModel.includes("rerank"); const isEmbedding = - fullModelStr.toLowerCase().includes("embedding") || - fullModelStr.toLowerCase().includes("bge-") || - fullModelStr.toLowerCase().includes("text-embed"); + !isRerank && + (apiFormat === "embeddings" || + supportedEndpoints.includes("embeddings") || + lowerModel.includes("embedding") || + lowerModel.includes("bge-") || + lowerModel.includes("text-embed") || + lowerModel.includes("jina-clip") || + lowerModel.includes("colbert")); - const testBody = buildComboTestRequestBody(fullModelStr, isEmbedding); + const testBody = isRerank + ? { + model: fullModelStr, + query: "What is OmniRoute?", + documents: [ + "OmniRoute routes AI requests across configured providers.", + "This document is unrelated to the test query.", + ], + top_n: 1, + return_documents: false, + } + : buildComboTestRequestBody(fullModelStr, isEmbedding); const res = await runWithTimeout((signal) => isEmbedding ? handleValidatedEmbeddingRequestBody( testBody as Record & { model: string } ) - : postChatCompletion(buildInternalChatRequest(testBody, signal)) + : isRerank + ? postRerank(buildInternalRerankRequest(testBody, signal)) + : postChatCompletion(buildInternalChatRequest(testBody, signal)) ); const latencyMs = Date.now() - startTime; @@ -134,6 +203,13 @@ export async function POST(request: Request) { } const responseText = extractComboTestResponseText(responseBody); + if (isRerank) { + return NextResponse.json({ + status: "ok", + latencyMs, + responseText: "[Rerank completed successfully]", + }); + } if (!responseText && !isEmbedding) { return NextResponse.json( { diff --git a/src/app/api/providers/[id]/models/route.ts b/src/app/api/providers/[id]/models/route.ts index a980e93695..1e6e7e5962 100755 --- a/src/app/api/providers/[id]/models/route.ts +++ b/src/app/api/providers/[id]/models/route.ts @@ -65,7 +65,12 @@ import { } from "@/lib/providerModels/modelDiscovery"; type JsonRecord = Record; -type LocalCatalogModel = { id: string; name?: string }; +type LocalCatalogModel = { + id: string; + name?: string; + apiFormat?: string; + supportedEndpoints?: string[]; +}; const antigravityDiscoveryInflight = new Map< string, @@ -409,33 +414,41 @@ const STATIC_MODEL_PROVIDERS: Record Array<{ id: string; name: str * @param provider - Provider ID * @returns Array of models or undefined if provider doesn't use static models */ -export function getStaticModelsForProvider( - provider: string -): Array<{ id: string; name: string }> | undefined { +export function getStaticModelsForProvider(provider: string): LocalCatalogModel[] | undefined { const staticModelsFn = STATIC_MODEL_PROVIDERS[provider]; if (staticModelsFn) { return staticModelsFn(); } - const specialtyModels: Array<{ id: string; name: string }> = []; - const appendModels = (models: Array<{ id: string; name?: string }>) => { + const specialtyModels: LocalCatalogModel[] = []; + const appendModels = ( + models: Array<{ id: string; name?: string }>, + metadata?: Pick + ) => { for (const model of models) { if (specialtyModels.some((existing) => existing.id === model.id)) continue; specialtyModels.push({ id: model.id, name: model.name || model.id, + ...metadata, }); } }; const embeddingProvider = getEmbeddingProvider(provider); if (embeddingProvider) { - appendModels(embeddingProvider.models); + appendModels(embeddingProvider.models, { + apiFormat: "embeddings", + supportedEndpoints: ["embeddings"], + }); } const rerankProvider = getRerankProvider(provider); if (rerankProvider) { - appendModels(rerankProvider.models); + appendModels(rerankProvider.models, { + apiFormat: "rerank", + supportedEndpoints: ["rerank"], + }); } const imageProvider = getImageProvider(provider); @@ -837,6 +850,8 @@ export async function GET( return localCatalog.map((model) => ({ id: model.id, name: model.name || model.id, + ...(model.apiFormat ? { apiFormat: model.apiFormat } : {}), + ...(model.supportedEndpoints ? { supportedEndpoints: model.supportedEndpoints } : {}), ...(registryCatalogModels.length > 0 ? { owned_by: provider } : {}), })); }; @@ -1770,6 +1785,8 @@ export async function GET( models: localCatalog.map((m) => ({ id: m.id, name: m.name || m.id, + ...(m.apiFormat ? { apiFormat: m.apiFormat } : {}), + ...(m.supportedEndpoints ? { supportedEndpoints: m.supportedEndpoints } : {}), ...(registryCatalogModels.length > 0 ? { owned_by: provider } : {}), })), source: "local_catalog", diff --git a/src/app/api/v1/embeddings/route.ts b/src/app/api/v1/embeddings/route.ts index 9992f8273e..4fbe559448 100644 --- a/src/app/api/v1/embeddings/route.ts +++ b/src/app/api/v1/embeddings/route.ts @@ -23,6 +23,10 @@ import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; import { getAllCustomModels, getProviderNodes } from "@/lib/localDb"; +function toProviderScopedModelId(providerId: string, modelId: string): string { + return modelId.startsWith(`${providerId}/`) ? modelId : `${providerId}/${modelId}`; +} + /** * Handle CORS preflight */ @@ -59,7 +63,7 @@ export async function GET() { for (const model of models) { if (!model?.id || !Array.isArray(model.supportedEndpoints)) continue; if (!model.supportedEndpoints.includes("embeddings")) continue; - const fullId = `${providerId}/${model.id}`; + const fullId = toProviderScopedModelId(providerId, model.id); if (data.some((d) => d.id === fullId)) continue; data.push({ id: fullId, diff --git a/src/app/api/v1/models/catalog.ts b/src/app/api/v1/models/catalog.ts index 29520bfe98..4445b0d72c 100644 --- a/src/app/api/v1/models/catalog.ts +++ b/src/app/api/v1/models/catalog.ts @@ -210,6 +210,12 @@ export async function getUnifiedModelsResponse( if (authRejection) return authRejection; const { aliasToProviderId, providerIdToAlias } = buildAliasMaps(); + const resolveCanonicalProviderId = (aliasOrProviderId: string, fallbackProviderId?: string) => + aliasToProviderId[aliasOrProviderId] || + (fallbackProviderId ? aliasToProviderId[fallbackProviderId] : undefined) || + FALLBACK_ALIAS_TO_PROVIDER[aliasOrProviderId] || + fallbackProviderId || + aliasOrProviderId; // Issue #96: Allow blocking specific providers from the models list const blockedProviders: Set = new Set( @@ -322,7 +328,7 @@ export async function getUnifiedModelsResponse( // Add provider models (chat) for (const [alias, providerModels] of Object.entries(PROVIDER_MODELS)) { const providerId = aliasToProviderId[alias] || alias; - const canonicalProviderId = FALLBACK_ALIAS_TO_PROVIDER[alias] || providerId; + const canonicalProviderId = resolveCanonicalProviderId(alias, providerId); // Skip blocked providers (Issue #96) if (blockedProviders.has(alias) || blockedProviders.has(canonicalProviderId)) continue; @@ -387,7 +393,7 @@ export async function getUnifiedModelsResponse( const prefix = providerIdToPrefix[providerId]; const alias = prefix || providerIdToAlias[providerId] || providerId; - const canonicalProviderId = FALLBACK_ALIAS_TO_PROVIDER[alias] || providerId; + const canonicalProviderId = resolveCanonicalProviderId(alias, providerId); const parentProviderType = nodeIdToProviderType[providerId]; if ( @@ -405,12 +411,15 @@ export async function getUnifiedModelsResponse( const aliasId = `${alias}/${sm.id}`; const endpoints = Array.isArray(sm.supportedEndpoints) ? sm.supportedEndpoints : ["chat"]; + const apiFormat = typeof sm.apiFormat === "string" ? sm.apiFormat : "chat-completions"; let modelType: string | undefined; if (endpoints.includes("embeddings")) modelType = "embedding"; + else if (endpoints.includes("rerank")) modelType = "rerank"; else if (endpoints.includes("images")) modelType = "image"; else if (endpoints.includes("audio")) modelType = "audio"; const syncedFields = { ...(modelType ? { type: modelType } : {}), + ...(apiFormat !== "chat-completions" ? { api_format: apiFormat } : {}), ...(modelType === "audio" ? { subtype: "transcription" } : {}), ...(sm.inputTokenLimit ? { context_length: sm.inputTokenLimit } : {}), ...(endpoints.length > 1 || !endpoints.includes("chat") @@ -478,7 +487,7 @@ export async function getUnifiedModelsResponse( const isProviderActive = (provider: string) => { if (activeAliases.size === 0) return false; // No active connections = show nothing const alias = providerIdToAlias[provider] || provider; - const canonicalProviderId = FALLBACK_ALIAS_TO_PROVIDER[alias] || provider; + const canonicalProviderId = resolveCanonicalProviderId(alias, provider); // FIX #1752: Ensure blocked providers are not returned for non-chat models if ( @@ -492,16 +501,38 @@ export async function getUnifiedModelsResponse( return activeAliases.has(alias) || activeAliases.has(provider); }; + const hasEquivalentSpecialtyModel = ( + providerId: string, + rawModelId: string, + type: string, + scopedModelId: string + ) => + models.some((model: any) => { + if (model?.id === scopedModelId) return true; + if (model?.owned_by !== providerId || model?.type !== type) return false; + const existingRoot = + typeof model?.root === "string" + ? model.root + : typeof model?.id === "string" + ? model.id.split("/").pop() + : null; + return existingRoot === rawModelId; + }); + // Add embedding models (filtered by active providers) for (const embModel of getAllEmbeddingModels()) { if (!isProviderActive(embModel.provider)) continue; const rawModelId = embModel.id.split("/").pop() || embModel.id; if (!providerSupportsModel(embModel.provider, rawModelId)) continue; + if (hasEquivalentSpecialtyModel(embModel.provider, rawModelId, "embedding", embModel.id)) { + continue; + } models.push({ id: embModel.id, object: "model", created: timestamp, owned_by: embModel.provider, + root: rawModelId, type: "embedding", dimensions: embModel.dimensions, }); @@ -530,11 +561,15 @@ export async function getUnifiedModelsResponse( if (!isProviderActive(rerankModel.provider)) continue; const rawModelId = rerankModel.id.split("/").pop() || rerankModel.id; if (!providerSupportsModel(rerankModel.provider, rawModelId)) continue; + if (hasEquivalentSpecialtyModel(rerankModel.provider, rawModelId, "rerank", rerankModel.id)) { + continue; + } models.push({ id: rerankModel.id, object: "model", created: timestamp, owned_by: rerankModel.provider, + root: rawModelId, type: "rerank", }); } @@ -611,7 +646,7 @@ export async function getUnifiedModelsResponse( // For compatible providers, use the prefix from provider nodes const prefix = providerIdToPrefix[providerId]; const alias = prefix || providerIdToAlias[providerId] || providerId; - const canonicalProviderId = FALLBACK_ALIAS_TO_PROVIDER[alias] || providerId; + const canonicalProviderId = resolveCanonicalProviderId(alias, providerId); // Only include if provider is active — check alias, canonical ID, raw providerId, // or the parent provider type (for compatible providers whose node ID is a UUID) @@ -649,8 +684,15 @@ export async function getUnifiedModelsResponse( typeof model.apiFormat === "string" ? model.apiFormat : "chat-completions"; let modelType: string | undefined; if (endpoints.includes("embeddings")) modelType = "embedding"; + else if (endpoints.includes("rerank")) modelType = "rerank"; else if (endpoints.includes("images")) modelType = "image"; else if (endpoints.includes("audio")) modelType = "audio"; + if ( + modelType && + hasEquivalentSpecialtyModel(canonicalProviderId, modelId, modelType, aliasId) + ) { + continue; + } const visionFields = modelType === "chat" ? getVisionCapabilityFields(aliasId) || getVisionCapabilityFields(modelId) diff --git a/src/lib/db/models.ts b/src/lib/db/models.ts index a3b2fc7f2f..f749a4ffc8 100644 --- a/src/lib/db/models.ts +++ b/src/lib/db/models.ts @@ -351,6 +351,7 @@ export async function addCustomModel( | "chat-completions" | "responses" | "embeddings" + | "rerank" | "audio-transcriptions" | "audio-speech" | "images-generations" = "chat-completions", @@ -525,6 +526,7 @@ export interface SyncedAvailableModel { id: string; name: string; source: "imported"; + apiFormat?: string; supportedEndpoints?: string[]; inputTokenLimit?: number; outputTokenLimit?: number; @@ -561,6 +563,9 @@ function normalizeSyncedAvailableModel(model: unknown): SyncedAvailableModel | n id, name, source: "imported", + ...(toNonEmptyString(record.apiFormat) + ? { apiFormat: toNonEmptyString(record.apiFormat)! } + : {}), ...(supportedEndpoints && supportedEndpoints.length > 0 ? { supportedEndpoints } : {}), ...(typeof record.inputTokenLimit === "number" ? { inputTokenLimit: record.inputTokenLimit } diff --git a/src/lib/providerModels/managedModelImport.ts b/src/lib/providerModels/managedModelImport.ts index 227d96f9ae..30229794d1 100644 --- a/src/lib/providerModels/managedModelImport.ts +++ b/src/lib/providerModels/managedModelImport.ts @@ -21,7 +21,7 @@ export type ManagedImportedModel = { id: string; name: string; source: "imported"; - apiFormat: "chat-completions"; + apiFormat: string; supportedEndpoints?: string[]; inputTokenLimit?: number; outputTokenLimit?: number; @@ -48,7 +48,7 @@ function normalizeImportedModels(fetchedModels: unknown): ManagedImportedModel[] id: model.id, name: model.name || model.id, source: "imported", - apiFormat: "chat-completions", + apiFormat: toNonEmptyString(model.apiFormat) || "chat-completions", ...(Array.isArray(model.supportedEndpoints) && model.supportedEndpoints.length > 0 ? { supportedEndpoints: model.supportedEndpoints } : {}), diff --git a/src/lib/providerModels/modelDiscovery.ts b/src/lib/providerModels/modelDiscovery.ts index 89235b566a..5ab8a9fb9f 100644 --- a/src/lib/providerModels/modelDiscovery.ts +++ b/src/lib/providerModels/modelDiscovery.ts @@ -49,6 +49,9 @@ export function normalizeDiscoveredModels(models: unknown): SyncedAvailableModel id, name, source: "imported", + ...(toNonEmptyString(record.apiFormat) + ? { apiFormat: toNonEmptyString(record.apiFormat)! } + : {}), ...(supportedEndpoints && supportedEndpoints.length > 0 ? { supportedEndpoints } : {}), ...(typeof record.inputTokenLimit === "number" ? { inputTokenLimit: record.inputTokenLimit } diff --git a/src/shared/validation/schemas.ts b/src/shared/validation/schemas.ts index 9b6dbfdbe6..615091eae6 100644 --- a/src/shared/validation/schemas.ts +++ b/src/shared/validation/schemas.ts @@ -699,6 +699,7 @@ export const providerModelMutationSchema = z.object({ "chat-completions", "responses", "embeddings", + "rerank", "audio-transcriptions", "audio-speech", "images-generations", @@ -709,6 +710,7 @@ export const providerModelMutationSchema = z.object({ z.enum([ "chat", "embeddings", + "rerank", "images", "audio", "audio-transcriptions", diff --git a/tests/unit/embedding-rerank-provider-registry.test.ts b/tests/unit/embedding-rerank-provider-registry.test.ts index d4475127a3..eb38d16312 100644 --- a/tests/unit/embedding-rerank-provider-registry.test.ts +++ b/tests/unit/embedding-rerank-provider-registry.test.ts @@ -40,7 +40,7 @@ test("voyage-ai and jina-ai rerank registries expose supported models", () => { assert.ok(jina); assert.equal(jina.baseUrl, "https://api.jina.ai/v1/rerank"); assert.ok(jina.models.some((model) => model.id === "jina-reranker-v3")); - assert.ok(jina.models.some((model) => model.id === "jina-reranker-v2-base-multilingual")); + assert.ok(jina.models.some((model) => model.id === "jina-reranker-m0")); const parsedVoyage = parseRerankModel("voyage-ai/rerank-2.5"); assert.equal(parsedVoyage.provider, "voyage-ai"); @@ -50,6 +50,10 @@ test("voyage-ai and jina-ai rerank registries expose supported models", () => { assert.equal(parsedJina.provider, "jina-ai"); assert.equal(parsedJina.model, "jina-reranker-v3"); + const parsedJinaAlias = parseRerankModel("jina/jina-reranker-v3"); + assert.equal(parsedJinaAlias.provider, "jina-ai"); + assert.equal(parsedJinaAlias.model, "jina-reranker-v3"); + const all = getAllRerankModels(); assert.ok(all.some((model) => model.id === "voyage-ai/rerank-2.5")); assert.ok(all.some((model) => model.id === "jina-ai/jina-reranker-v3")); diff --git a/tests/unit/models-catalog-route.test.ts b/tests/unit/models-catalog-route.test.ts index 234b7e2d33..d1dd29d2af 100644 --- a/tests/unit/models-catalog-route.test.ts +++ b/tests/unit/models-catalog-route.test.ts @@ -473,6 +473,103 @@ test("v1 models catalog includes media, moderation, rerank, video, and music mod assert.equal((byId.get("comfyui/stable-audio-open") as any).type, "music"); }); +test("v1 models catalog does not duplicate imported Jina specialty models", async () => { + const connection = await seedConnection("jina-ai", { + name: "jina-synced", + apiKey: "jina-key", + }); + + await modelsDb.replaceSyncedAvailableModelsForConnection("jina-ai", (connection as any).id, [ + { + id: "jina-embeddings-v5-text-small", + name: "Jina Embeddings v5 Text Small", + source: "imported", + apiFormat: "embeddings", + supportedEndpoints: ["embeddings"], + }, + { + id: "jina-reranker-v3", + name: "Jina Reranker v3", + source: "imported", + apiFormat: "rerank", + supportedEndpoints: ["rerank"], + }, + ]); + + const response = await v1ModelsCatalog.getUnifiedModelsResponse( + new Request("http://localhost/api/v1/models") + ); + const body = (await response.json()) as any; + const visibleJinaEmbeddingRows = body.data.filter( + (item) => + item.owned_by === "jina-ai" && + item.root === "jina-embeddings-v5-text-small" && + item.type === "embedding" && + !item.parent + ); + const visibleJinaRerankRows = body.data.filter( + (item) => + item.owned_by === "jina-ai" && + item.root === "jina-reranker-v3" && + item.type === "rerank" && + !item.parent + ); + + assert.equal(response.status, 200); + assert.equal(visibleJinaEmbeddingRows.length, 1); + assert.equal(visibleJinaEmbeddingRows[0].id, "jina/jina-embeddings-v5-text-small"); + assert.equal(visibleJinaRerankRows.length, 1); + assert.equal(visibleJinaRerankRows[0].id, "jina/jina-reranker-v3"); +}); + +test("v1 models catalog does not duplicate custom Jina specialty models", async () => { + await seedConnection("jina-ai", { + name: "jina-custom", + apiKey: "jina-key", + }); + await modelsDb.addCustomModel( + "jina-ai", + "jina-embeddings-v5-text-small", + "Jina Embeddings v5 Text Small", + "imported", + "embeddings", + ["embeddings"] + ); + await modelsDb.addCustomModel( + "jina-ai", + "jina-reranker-v3", + "Jina Reranker v3", + "imported", + "rerank", + ["rerank"] + ); + + const response = await v1ModelsCatalog.getUnifiedModelsResponse( + new Request("http://localhost/api/v1/models") + ); + const body = (await response.json()) as any; + const visibleJinaEmbeddingRows = body.data.filter( + (item) => + item.owned_by === "jina-ai" && + item.root === "jina-embeddings-v5-text-small" && + item.type === "embedding" && + !item.parent + ); + const visibleJinaRerankRows = body.data.filter( + (item) => + item.owned_by === "jina-ai" && + item.root === "jina-reranker-v3" && + item.type === "rerank" && + !item.parent + ); + + assert.equal(response.status, 200); + assert.equal(visibleJinaEmbeddingRows.length, 1); + assert.equal(visibleJinaEmbeddingRows[0].id, "jina-ai/jina-embeddings-v5-text-small"); + assert.equal(visibleJinaRerankRows.length, 1); + assert.equal(visibleJinaRerankRows[0].id, "jina-ai/jina-reranker-v3"); +}); + test("v1 models catalog exposes image model input and output modalities for advanced image providers", async () => { await seedConnection("together", { name: "together-images" }); await seedConnection("topaz", { name: "topaz-images" }); diff --git a/tests/unit/provider-models-route.test.ts b/tests/unit/provider-models-route.test.ts index b524ff474d..a67d850f8d 100644 --- a/tests/unit/provider-models-route.test.ts +++ b/tests/unit/provider-models-route.test.ts @@ -355,8 +355,23 @@ test("provider models route returns the local catalog for embedding and rerank p assert.equal(jinaResponse.status, 200); assert.equal(jinaBody.provider, "jina-ai"); assert.equal(jinaBody.source, "local_catalog"); - assert.ok(jinaBody.models.some((model) => model.id === "jina-reranker-v3")); - assert.ok(jinaBody.models.some((model) => model.id === "jina-reranker-v2-base-multilingual")); + assert.ok( + jinaBody.models.some( + (model) => + model.id === "jina-embeddings-v5-text-small" && + model.apiFormat === "embeddings" && + model.supportedEndpoints?.includes("embeddings") + ) + ); + assert.ok( + jinaBody.models.some( + (model) => + model.id === "jina-reranker-v3" && + model.apiFormat === "rerank" && + model.supportedEndpoints?.includes("rerank") + ) + ); + assert.ok(jinaBody.models.some((model) => model.id === "jina-reranker-m0")); }); test("provider models route returns the local catalog for Runway video models", async () => { From f64b209750e8523dbb0ee277711678269b650489 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sat, 2 May 2026 09:52:54 -0300 Subject: [PATCH 03/87] docs: update CHANGELOG for PR 1874 and retroactive credits --- CHANGELOG.md | 8 ++++++++ 1 file changed, 8 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 15361bd85f..d5b422fc05 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -6,10 +6,18 @@ ## [3.7.9] — 2026-05-02 +### ✨ New Features + +- **feat(provider):** update Jina AI model catalog to support Embeddings and Rerank natively (#1874 — thanks @backryun) + ### 🐛 Bug Fixes - **fix(gemini-cli):** separate Cloud Code transport from Antigravity (#1869 — thanks @dhaern) +### 🏆 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 From fe25fdcfdce0d216ed96c104286fc65a873ed590 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sat, 2 May 2026 10:04:23 -0300 Subject: [PATCH 04/87] fix: resolve stream defaults and codex prompt mapping (#1873, #1872) --- CHANGELOG.md | 2 ++ open-sse/executors/codex.ts | 15 +++++++++++++++ open-sse/utils/aiSdkCompat.ts | 6 ++++-- 3 files changed, 21 insertions(+), 2 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index d5b422fc05..4707bf82ee 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -13,6 +13,8 @@ ### 🐛 Bug Fixes - **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) ### 🏆 Release Attribution & Retroactive Credits diff --git a/open-sse/executors/codex.ts b/open-sse/executors/codex.ts index e84be095b6..2e7b6f35fe 100644 --- a/open-sse/executors/codex.ts +++ b/open-sse/executors/codex.ts @@ -1198,6 +1198,21 @@ export class CodexExecutor extends BaseExecutor { }) : [], })); + } 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) }], + })); } // ── Cache-aware system prompt handling (both paths) ── diff --git a/open-sse/utils/aiSdkCompat.ts b/open-sse/utils/aiSdkCompat.ts index 91d284bfc1..5f0100b08f 100644 --- a/open-sse/utils/aiSdkCompat.ts +++ b/open-sse/utils/aiSdkCompat.ts @@ -22,8 +22,10 @@ export function resolveStreamFlag(bodyStream: unknown, acceptHeader: unknown): b // Explicit body value always wins if (bodyStream === true) return true; if (bodyStream === false) return false; - // No explicit stream param — fall back to Accept header heuristic - return !clientWantsJsonResponse(acceptHeader); + + // No explicit stream param — default to false per OpenAI spec + // (Fixes #1873 - previously relied on Accept header heuristic) + return false; } /** From a2d0db2a868a3bba12ecf541779fdc18a5d3acc4 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sat, 2 May 2026 10:55:16 -0300 Subject: [PATCH 05/87] chore(compression): start caveman compression update From 0f35c148ead1cb6e866bff22d2023b9c6f8c0f61 Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sat, 2 May 2026 12:39:04 -0300 Subject: [PATCH 06/87] 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. --- open-sse/handlers/chatCore.ts | 205 ++++++++---- open-sse/mcp-server/descriptionCompressor.ts | 122 ++++++++ open-sse/mcp-server/schemas/tools.ts | 50 ++- open-sse/mcp-server/server.ts | 25 ++ open-sse/mcp-server/tools/compressionTools.ts | 68 +++- open-sse/services/compression/aggressive.ts | 43 +-- open-sse/services/compression/caveman.ts | 293 ++++++++++++++++-- open-sse/services/compression/cavemanRules.ts | 172 +++++++++- open-sse/services/compression/diffHelper.ts | 88 ++++++ open-sse/services/compression/index.ts | 22 +- open-sse/services/compression/lite.ts | 28 +- .../services/compression/messageContent.ts | 79 +++++ open-sse/services/compression/outputMode.ts | 122 ++++++++ open-sse/services/compression/preservation.ts | 202 +++++++++--- .../services/compression/progressiveAging.ts | 38 +-- .../services/compression/strategySelector.ts | 34 +- open-sse/services/compression/types.ts | 35 +++ open-sse/services/compression/ultra.ts | 45 +-- open-sse/services/compression/validation.ts | 132 ++++++++ open-sse/utils/aiSdkCompat.ts | 6 +- scripts/scratch/test-migrations.ts | 89 +++--- .../analytics/CompressionAnalyticsTab.tsx | 70 ++++- .../components/CompressionSettingsTab.tsx | 206 +++++++++--- src/app/api/compression/preview/route.ts | 19 ++ src/app/api/compression/rules/route.ts | 12 + src/app/api/settings/compression/route.ts | 13 + src/lib/db/compression.ts | 38 +++ src/lib/db/compressionAnalytics.ts | 224 ++++++++++++- src/lib/db/migrationRunner.ts | 87 ++++++ .../migrations/041_compression_receipts.sql | 18 ++ .../golden-set/compression-caveman-v2.test.ts | 85 +++++ .../compression-upstream-parity.test.ts | 92 ++++++ .../chatcore-compression-integration.test.ts | 73 +++++ tests/unit/compression/caveman-v379.test.ts | 145 +++++++++ .../compression/compressionAnalytics.test.ts | 69 ++++- tests/unit/compression/diffHelper.test.ts | 36 +++ tests/unit/compression/lite.test.ts | 23 ++ .../mcp-description-compressor.test.ts | 85 +++++ tests/unit/compression/outputMode.test.ts | 89 ++++++ tests/unit/compression/validation.test.ts | 136 ++++++++ 40 files changed, 3073 insertions(+), 345 deletions(-) create mode 100644 open-sse/mcp-server/descriptionCompressor.ts create mode 100644 open-sse/services/compression/diffHelper.ts create mode 100644 open-sse/services/compression/messageContent.ts create mode 100644 open-sse/services/compression/outputMode.ts create mode 100644 open-sse/services/compression/validation.ts create mode 100644 src/app/api/compression/rules/route.ts create mode 100644 src/lib/db/migrations/041_compression_receipts.sql create mode 100644 tests/golden-set/compression-caveman-v2.test.ts create mode 100644 tests/golden-set/compression-upstream-parity.test.ts create mode 100644 tests/unit/compression/caveman-v379.test.ts create mode 100644 tests/unit/compression/diffHelper.test.ts create mode 100644 tests/unit/compression/mcp-description-compressor.test.ts create mode 100644 tests/unit/compression/outputMode.test.ts create mode 100644 tests/unit/compression/validation.test.ts diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index dbf58ed0cf..2f1773bcb4 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -1560,6 +1560,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; @@ -1576,6 +1578,30 @@ export async function handleChatCore({ ); } + if (compressionSettings?.cavemanOutputMode?.enabled) { + try { + const { applyCavemanOutputMode } = await import("../services/compression/outputMode.ts"); + const outputMode = applyCavemanOutputMode( + body as Parameters[0], + compressionSettings.cavemanOutputMode + ); + if (outputMode.applied) { + body = outputMode.body as typeof body; + cavemanOutputModeApplied = true; + cavemanOutputModeIntensity = compressionSettings.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)) + ); + } + } + // --- Modular Compression Pipeline (Phase 1 Lite + Phase 2 Standard/Caveman + Phase 3 Aggressive) --- // Runs BEFORE the existing reactive compressContext() to proactively reduce tokens. try { @@ -1644,80 +1670,117 @@ 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; + } + + if (result.compressed || result.stats.fallbackApplied || cavemanOutputModeApplied) { + trackCompressionStats(result.stats); + compressionAnalyticsRecorded = true; + 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( + 0, + result.stats.originalTokens - result.stats.compressedTokens + ), + duration_ms: result.stats.durationMs ?? null, + request_id: skillRequestId, + validation_fallback: result.stats.fallbackApplied ? 1 : 0, + output_mode: cavemanOutputModeApplied ? cavemanOutputModeIntensity : 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 - ), - 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(",")})` - ); + ); + 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) { + 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: "output-caveman", + 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", @@ -3264,6 +3327,13 @@ export async function handleChatCore({ // Log usage for non-streaming responses const usage = extractUsageFromResponse(responseBody, provider); + if (usage && typeof usage === "object") { + void import("../../src/lib/db/compressionAnalytics.ts") + .then(({ attachCompressionUsageReceipt }) => { + attachCompressionUsageReceipt(skillRequestId, usage, "provider"); + }) + .catch(() => {}); + } appendRequestLog({ model, provider, connectionId, tokens: usage, status: "200 OK" }).catch( () => {} ); @@ -3636,6 +3706,11 @@ export async function handleChatCore({ // Track cache token metrics for streaming responses if (streamUsage && typeof streamUsage === "object") { + void import("../../src/lib/db/compressionAnalytics.ts") + .then(({ attachCompressionUsageReceipt }) => { + attachCompressionUsageReceipt(skillRequestId, streamUsage, "stream"); + }) + .catch(() => {}); const inputTokens = streamUsage.prompt_tokens || 0; const cachedTokens = toPositiveNumber( streamUsage.cache_read_input_tokens ?? diff --git a/open-sse/mcp-server/descriptionCompressor.ts b/open-sse/mcp-server/descriptionCompressor.ts new file mode 100644 index 0000000000..b18a5ea976 --- /dev/null +++ b/open-sse/mcp-server/descriptionCompressor.ts @@ -0,0 +1,122 @@ +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, +}; + +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(/\s+([,.;:!?])/g, "$1") + .replace(/\n{3,}/g, "\n\n") + .replace(/(^|[.!?]\s+)([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; +} + +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)) { + if (fields.has(key) && typeof nested === "string") { + (value as Record)[key] = maybeCompressMcpDescription(nested, options); + } else if (nested && typeof nested === "object") { + compressDescriptionsInPlace(nested, fieldNames, options); + } + } +} + +export function getMcpDescriptionCompressionStats(): McpDescriptionCompressionStats { + return { ...descriptionCompressionStats }; +} + +export function resetMcpDescriptionCompressionStats(): void { + descriptionCompressionStats.descriptionsCompressed = 0; + descriptionCompressionStats.charsBefore = 0; + descriptionCompressionStats.charsAfter = 0; + descriptionCompressionStats.charsSaved = 0; + descriptionCompressionStats.estimatedTokensSaved = 0; +} diff --git a/open-sse/mcp-server/schemas/tools.ts b/open-sse/mcp-server/schemas/tools.ts index 401f39d3d5..fc9d0deef9 100644 --- a/open-sse/mcp-server/schemas/tools.ts +++ b/open-sse/mcp-server/schemas/tools.ts @@ -1000,14 +1000,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 +1064,19 @@ export const compressionStatusTool: McpToolDefinition< export const compressionConfigureInput = z.object({ enabled: z.boolean().optional(), strategy: z - .string() + .enum(["off", "lite", "standard", "aggressive", "ultra"]) .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"]).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 +1085,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 +1099,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), adjusting maxTokens threshold, targetRatio, auto-trigger mode, system prompt preservation, and MCP description compression.", inputSchema: compressionConfigureInput, outputSchema: compressionConfigureOutput, scopes: ["write:compression"], diff --git a/open-sse/mcp-server/server.ts b/open-sse/mcp-server/server.ts index 7b7d7d9c7f..0a50fbbcd5 100644 --- a/open-sse/mcp-server/server.ts +++ b/open-sse/mcp-server/server.ts @@ -76,6 +76,8 @@ import { import { memoryTools } from "./tools/memoryTools.ts"; import { skillTools } from "./tools/skillTools.ts"; import { compressionTools } from "./tools/compressionTools.ts"; +import { maybeCompressMcpDescription } 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; +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,17 @@ 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, handler: unknown) => { + const description = + typeof config.description === "string" + ? maybeCompressMcpDescription(config.description, { + enabled: mcpDescriptionCompressionEnabled, + }) + : config.description; + return registerTool(name, { ...config, description }, handler as never); + }) as typeof server.registerTool; // Register essential tools server.registerTool( diff --git a/open-sse/mcp-server/tools/compressionTools.ts b/open-sse/mcp-server/tools/compressionTools.ts index 795b053ef7..8732d09376 100644 --- a/open-sse/mcp-server/tools/compressionTools.ts +++ b/open-sse/mcp-server/tools/compressionTools.ts @@ -14,6 +14,7 @@ import { import { getCompressionAnalyticsSummary } from "../../../src/lib/db/compressionAnalytics.ts"; import { getCacheStatsSummary } from "../../../src/lib/db/compressionCacheStats.ts"; import type { McpToolExtraLike } from "../scopeEnforcement.ts"; +import { getMcpDescriptionCompressionStats } from "../descriptionCompressor.ts"; /** * Handle compression_status tool: return current compression config, analytics, and cache stats @@ -26,14 +27,34 @@ 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; + validationFallbacks: number; + requestsWithReceipts: number; + realUsage: { + requestsWithReceipts: number; + promptTokens: number; + completionTokens: number; + totalTokens: number; + cacheReadTokens: number; + cacheWriteTokens: number; + estimatedUsdSaved: number; + bySource: Record; + }; + mcpDescriptionCompression: { + descriptionsCompressed: number; + charsSaved: number; + estimatedTokensSaved: number; + }; }; cacheStats: { hits: number; @@ -46,6 +67,7 @@ export async function handleCompressionStatus( try { const settings = await getCompressionSettings(); const analyticsSummary = getCompressionAnalyticsSummary(); + const mcpDescriptionStats = getMcpDescriptionCompressionStats(); const cacheStats = getCacheStatsSummary(); const result = { @@ -53,14 +75,28 @@ 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 ?? {}, + validationFallbacks: analyticsSummary.validationFallbacks, + requestsWithReceipts: analyticsSummary.realUsage.requestsWithReceipts, + realUsage: analyticsSummary.realUsage, + mcpDescriptionCompression: { + descriptionsCompressed: mcpDescriptionStats.descriptionsCompressed, + charsSaved: mcpDescriptionStats.charsSaved, + estimatedTokensSaved: mcpDescriptionStats.estimatedTokensSaved, + }, }, cacheStats: cacheStats ? { @@ -98,9 +134,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 +147,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 +164,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 +185,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, }, }; @@ -178,7 +226,7 @@ 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), adjusting maxTokens threshold, targetRatio, auto-trigger mode, system prompt preservation, and MCP description compression.", inputSchema: compressionConfigureInput, handler: (args: z.infer) => handleCompressionConfigure(args), }, diff --git a/open-sse/services/compression/aggressive.ts b/open-sse/services/compression/aggressive.ts index 8e2d3e85c4..83c1acb86c 100644 --- a/open-sse/services/compression/aggressive.ts +++ b/open-sse/services/compression/aggressive.ts @@ -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; diff --git a/open-sse/services/compression/caveman.ts b/open-sse/services/compression/caveman.ts index e0c40ecdfa..464a6ac3a8 100644 --- a/open-sse/services/compression/caveman.ts +++ b/open-sse/services/compression/caveman.ts @@ -3,10 +3,13 @@ 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"; interface ChatMessage { role: string; content?: string | Array<{ type: string; text?: string }>; + [key: string]: unknown; } interface ChatRequestBody { @@ -14,14 +17,168 @@ interface ChatRequestBody { [key: string]: unknown; } +const RULE_KEYWORDS: Record = { + redundant_phrasing: [ + "make sure", + "be sure", + "due to the fact", + "the reason is because", + "it is important", + "you should", + "remember to", + ], + pleasantries: ["sure", "certainly", "of course", "happy to"], + 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"], + 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"], + 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", + "configuration", + "function", + "request", + "response", + "implementation", + "authentication", + "authorization", + "application", + "dependency", + ], +}; + +const KEYWORD_WORDS = new Map(); + +function extractLowerWords(lowerText: string): Set { + return new Set(lowerText.match(/[a-z]+(?:'[a-z]+)?/g) ?? []); +} + +function getKeywordWords(keyword: string): string[] { + const cached = KEYWORD_WORDS.get(keyword); + if (cached) return cached; + const words = keyword.match(/[a-z]+(?:'[a-z]+)?/g) ?? []; + KEYWORD_WORDS.set(keyword, words); + return words; +} + +function hasKeywordHint(keyword: string, words: Set): boolean { + const keywordWords = getKeywordWords(keyword); + return keywordWords.length === 0 || keywordWords.every((word) => words.has(word)); +} + +function shouldAttemptRule(ruleName: string, lowerText: string, words: Set): boolean { + if (ruleName === "articles") { + return words.has("a") || words.has("an") || words.has("the"); + } + + const keywords = RULE_KEYWORDS[ruleName]; + return ( + !keywords || + keywords.some((keyword) => hasKeywordHint(keyword, words) && lowerText.includes(keyword)) + ); +} + export function applyRulesToText( text: string, rules: CavemanRule[] ): { text: string; appliedRules: string[] } { let result = text; + const lowerResult = text.toLowerCase(); + const lowerWords = extractLowerWords(lowerResult); const appliedRules: string[] = []; for (const rule of rules) { + if (!shouldAttemptRule(rule.name, lowerResult, lowerWords)) continue; + const before = result; const { pattern, replacement } = rule; if (typeof replacement === "function") { @@ -43,14 +200,50 @@ 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(/ +/g, " "); + if (/[\t ]+[,.;:!?]/.test(result)) result = result.replace(/\s+([,.;:!?])/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(/(^|[.!?]\s+|\n+\s*)([a-z])/g, (_match, prefix: string, char: string) => { + return `${prefix}${char.toUpperCase()}`; + }); +} + +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]+\]\([^) \n]+(?:\s+"[^"]*")?\)|^#{1,6}\s+|^\s*\|.*\|\s*$|\$\$|\\\[|\\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$]*\s*\([^()\n]*\)|(?:^|\s)(?:\.{0,2}\/[A-Za-z0-9_@./-]+|[A-Za-z]:\\[A-Za-z0-9_.\\/-]+)|\b(?:TypeError|ReferenceError|SyntaxError|RangeError|URIError|EvalError|Error|Exception):[^\n]+/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 @@ -80,23 +273,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,21 +308,59 @@ 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 rules = getRulesForContext(msg.role, config.intensity).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; @@ -136,6 +373,10 @@ export function cavemanCompress( 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; diff --git a/open-sse/services/compression/cavemanRules.ts b/open-sse/services/compression/cavemanRules.ts index 51e9958d04..7050372ef3 100644 --- a/open-sse/services/compression/cavemanRules.ts +++ b/open-sse/services/compression/cavemanRules.ts @@ -3,12 +3,45 @@ import type { CavemanRule } from "./types.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 = { + "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: + /\b(?:sure|certainly|of course|happy to|i'?d be happy to|i would be happy to)\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,13 +49,19 @@ 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 = { + "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", @@ -33,48 +72,82 @@ const CAVEMAN_RULES: CavemanRule[] = [ return map[lower] ?? match; }, context: "all", + category: "filler", + minIntensity: "lite", }, { name: "filler_adverbs", - pattern: /(? { const map: Record = { "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 +252,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 +275,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 = { "is being used": "uses", "is being called": "calls", + "is being generated": "generated", "was created": "created", "was generated": "generated", "was implemented": "implemented", @@ -214,6 +318,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 +330,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 +339,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 +356,63 @@ 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 = { + 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" +): CavemanRule[] { + const rank = INTENSITY_RANK[intensity] ?? INTENSITY_RANK.full; + return 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() { + return CAVEMAN_RULES.map((rule) => ({ + name: rule.name, + context: rule.context, + category: rule.category ?? "terse", + minIntensity: rule.minIntensity ?? "lite", + description: rule.description ?? rule.name.replace(/_/g, " "), + })); +} + export { CAVEMAN_RULES }; diff --git a/open-sse/services/compression/diffHelper.ts b/open-sse/services/compression/diffHelper.ts new file mode 100644 index 0000000000..80632c7e51 --- /dev/null +++ b/open-sse/services/compression/diffHelper.ts @@ -0,0 +1,88 @@ +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; +} + +function tokenize(text: string): string[] { + return text.match(/\s+|[^\s]+/g) ?? []; +} + +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(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 +): 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), + })); + + return { + segments: buildCompressionDiff(original, compressed), + preservedBlocks: preserved, + ruleRemovals: stats?.rulesApplied ?? [], + validationWarnings: [...(stats?.validationWarnings ?? []), ...validation.warnings], + validationErrors: [...(stats?.validationErrors ?? []), ...validation.errors], + fallbackApplied: Boolean(stats?.fallbackApplied || validation.fallbackApplied), + }; +} diff --git a/open-sse/services/compression/index.ts b/open-sse/services/compression/index.ts index 98578a1f70..efc5b4ddb6 100644 --- a/open-sse/services/compression/index.ts +++ b/open-sse/services/compression/index.ts @@ -5,6 +5,8 @@ export type { CompressionResult, CavemanConfig, CavemanRule, + CavemanIntensity, + CavemanOutputModeConfig, AggressiveConfig, AgingThresholds, ToolStrategiesConfig, @@ -15,6 +17,7 @@ export type { export { DEFAULT_COMPRESSION_CONFIG, DEFAULT_CAVEMAN_CONFIG, + DEFAULT_CAVEMAN_OUTPUT_MODE_CONFIG, DEFAULT_AGGRESSIVE_CONFIG, } from "./types.ts"; @@ -28,8 +31,23 @@ export { } from "./lite.ts"; export { cavemanCompress, applyRulesToText } from "./caveman.ts"; -export { getRulesForContext, CAVEMAN_RULES } from "./cavemanRules.ts"; -export { extractPreservedBlocks, restorePreservedBlocks } from "./preservation.ts"; +export { getRulesForContext, getCavemanRuleMetadata, CAVEMAN_RULES } from "./cavemanRules.ts"; +export { + extractPreservedBlocks, + restorePreservedBlocks, + findFencedCodeBlocks, +} from "./preservation.ts"; +export type { PreservedBlock, PreservationOptions } from "./preservation.ts"; +export { validateCompression } from "./validation.ts"; +export type { ValidationResult } from "./validation.ts"; +export { + applyCavemanOutputMode, + buildCavemanOutputInstruction, + shouldBypassCavemanOutputMode, +} from "./outputMode.ts"; +export type { CavemanOutputModeResult } from "./outputMode.ts"; +export { buildCompressionDiff, buildCompressionPreviewDiff } from "./diffHelper.ts"; +export type { CompressionDiffSegment, CompressionPreviewDiff } from "./diffHelper.ts"; export { estimateCompressionTokens, diff --git a/open-sse/services/compression/lite.ts b/open-sse/services/compression/lite.ts index 5e530e74f5..fba65badbe 100644 --- a/open-sse/services/compression/lite.ts +++ b/open-sse/services/compression/lite.ts @@ -15,6 +15,7 @@ interface ChatBody { interface LiteCompressionOptions { model?: string; supportsVision?: boolean | null; + preserveSystemPrompt?: boolean; } function trimTrailingHorizontalWhitespace(line: string): string { @@ -62,13 +63,17 @@ function modelSupportsVision(model: string): boolean { ); } -export function collapseWhitespace(body: ChatBody): { +export function collapseWhitespace( + body: ChatBody, + options: LiteCompressionOptions = {} +): { body: ChatBody; applied: boolean; } { if (!body.messages) return { body, applied: false }; let applied = false; const messages = body.messages.map((msg) => { + if (options.preserveSystemPrompt === true && msg.role === "system") return msg; if (typeof msg.content !== "string") return msg; const normalized = normalizeMessageWhitespace(msg.content); if (normalized !== msg.content) applied = true; @@ -77,11 +82,15 @@ export function collapseWhitespace(body: ChatBody): { return { body: { ...body, messages }, applied }; } -export function dedupSystemPrompt(body: ChatBody): { +export function dedupSystemPrompt( + body: ChatBody, + options: LiteCompressionOptions = {} +): { body: ChatBody; applied: boolean; } { if (!body.messages) return { body, applied: false }; + if (options.preserveSystemPrompt === true) return { body, applied: false }; const seen = new Set(); let applied = false; const messages = body.messages.filter((msg) => { @@ -116,7 +125,10 @@ export function compressToolResults(body: ChatBody): { return { body: { ...body, messages }, applied }; } -export function removeRedundantContent(body: ChatBody): { +export function removeRedundantContent( + body: ChatBody, + options: LiteCompressionOptions = {} +): { body: ChatBody; applied: boolean; } { @@ -125,6 +137,10 @@ export function removeRedundantContent(body: ChatBody): { const messages: Message[] = []; for (let i = 0; i < body.messages.length; i++) { const msg = body.messages[i]; + if (options.preserveSystemPrompt === true && msg.role === "system") { + messages.push(msg); + continue; + } const contentStr = typeof msg.content === "string" ? msg.content : JSON.stringify(msg.content); if ( i > 0 && @@ -188,11 +204,11 @@ export function applyLiteCompression( let current = body as ChatBody; const techniquesApplied: string[] = []; - const r1 = collapseWhitespace(current); + const r1 = collapseWhitespace(current, options); current = r1.body; if (r1.applied) techniquesApplied.push("whitespace"); - const r2 = dedupSystemPrompt(current); + const r2 = dedupSystemPrompt(current, options); current = r2.body; if (r2.applied) techniquesApplied.push("system-dedup"); @@ -200,7 +216,7 @@ export function applyLiteCompression( current = r3.body; if (r3.applied) techniquesApplied.push("tool-compress"); - const r4 = removeRedundantContent(current); + const r4 = removeRedundantContent(current, options); current = r4.body; if (r4.applied) techniquesApplied.push("redundant-remove"); diff --git a/open-sse/services/compression/messageContent.ts b/open-sse/services/compression/messageContent.ts new file mode 100644 index 0000000000..7bd414d924 --- /dev/null +++ b/open-sse/services/compression/messageContent.ts @@ -0,0 +1,79 @@ +export interface TextBlock { + type?: string; + text?: string; + [key: string]: unknown; +} + +export interface ChatMessageLike { + role: string; + content?: string | TextBlock[] | unknown[]; + [key: string]: unknown; +} + +export function isTextBlock(value: unknown): value is TextBlock { + return ( + !!value && + typeof value === "object" && + "text" in value && + typeof (value as TextBlock).text === "string" && + ((value as TextBlock).type === undefined || + (value as TextBlock).type === "text" || + (value as TextBlock).type === "input_text") + ); +} + +export function extractTextContent(content: ChatMessageLike["content"]): string { + if (typeof content === "string") return content; + if (!Array.isArray(content)) return ""; + + const textParts: string[] = []; + for (const part of content) { + if (isTextBlock(part) && part.text) { + textParts.push(part.text); + } + } + return textParts.join("\n"); +} + +export function mapTextContent( + msg: ChatMessageLike, + transform: (text: string, index: number) => string +): ChatMessageLike { + if (typeof msg.content === "string") { + return { ...msg, content: transform(msg.content, 0) }; + } + if (!Array.isArray(msg.content)) return msg; + + let textIndex = 0; + let changed = false; + const content = msg.content.map((part) => { + if (!isTextBlock(part)) return part; + const nextText = transform(part.text ?? "", textIndex); + textIndex++; + if (nextText === part.text) return part; + changed = true; + return { ...part, text: nextText }; + }); + + return changed ? { ...msg, content } : msg; +} + +export function replaceTextContent(msg: ChatMessageLike, newText: string): ChatMessageLike { + if (typeof msg.content === "string" || !Array.isArray(msg.content)) { + return { ...msg, content: newText }; + } + + let replaced = false; + const content = msg.content.flatMap((part) => { + if (!isTextBlock(part)) return [part]; + if (replaced) return []; + replaced = true; + return [{ ...part, text: newText }]; + }); + + if (!replaced) { + return { ...msg, content: [{ type: "text", text: newText }, ...msg.content] }; + } + + return { ...msg, content }; +} diff --git a/open-sse/services/compression/outputMode.ts b/open-sse/services/compression/outputMode.ts new file mode 100644 index 0000000000..478ae863f2 --- /dev/null +++ b/open-sse/services/compression/outputMode.ts @@ -0,0 +1,122 @@ +import { DEFAULT_CAVEMAN_OUTPUT_MODE_CONFIG, type CavemanOutputModeConfig } from "./types.ts"; +import { extractTextContent } from "./messageContent.ts"; + +interface ChatMessage { + role: string; + content?: string | unknown[]; + [key: string]: unknown; +} + +interface ChatRequestBody { + messages?: ChatMessage[]; + instructions?: string; + [key: string]: unknown; +} + +export interface CavemanOutputModeResult { + body: ChatRequestBody; + applied: boolean; + skippedReason?: string; +} + +const CAVEMAN_INSTRUCTION_BY_INTENSITY = { + lite: "Respond concise. Drop filler, pleasantries, hedging. Keep full sentences, technical terms, code, errors, URLs, and identifiers exact.", + full: "Respond terse like smart caveman. Drop articles, filler, pleasantries, hedging. Fragments OK. Keep all technical substance, code, errors, URLs, identifiers exact.", + ultra: + "Respond ultra terse. Use short technical prose, arrows for causality, common abbreviations like DB/auth/config/req/res/fn. Never abbreviate code symbols, API names, error strings, URLs, or identifiers.", +} as const; + +const CAVEMAN_OUTPUT_MARKER = "[OmniRoute Caveman Output Mode]"; + +export function shouldBypassCavemanOutputMode(messages: ChatMessage[]): string | null { + const text = messages + .slice(-3) + .map((message) => extractTextContent(message.content).toLowerCase()) + .join("\n"); + + if (!text.trim()) return null; + if ( + /\b(security|vulnerability|exploit|credential leak|secret leak|malware|phishing)\b/.test(text) + ) { + return "security_warning"; + } + if (/\b(delete|drop table|truncate|destroy|wipe|irreversible|permanently remove)\b/.test(text)) { + return "irreversible_action"; + } + if ( + /\b(clarify|explain in detail|more detail|step by step|why exactly|what do you mean)\b/.test( + text + ) + ) { + return "clarification_requested"; + } + if ( + /\b(first|then|after that|before|rollback|backup)\b[\s\S]{0,240}\b(delete|drop|migrate|deploy|release)\b/.test( + text + ) + ) { + return "order_sensitive_sequence"; + } + return null; +} + +export function buildCavemanOutputInstruction(config: CavemanOutputModeConfig): string { + const intensity = config.intensity ?? "full"; + return `${CAVEMAN_OUTPUT_MARKER}\n${CAVEMAN_INSTRUCTION_BY_INTENSITY[intensity]}`; +} + +export function applyCavemanOutputMode( + body: ChatRequestBody, + options?: Partial +): CavemanOutputModeResult { + const config: CavemanOutputModeConfig = { + ...DEFAULT_CAVEMAN_OUTPUT_MODE_CONFIG, + ...options, + }; + if (!config.enabled) return { body, applied: false, skippedReason: "disabled" }; + + const messages = Array.isArray(body.messages) ? body.messages : null; + if (!messages || messages.length === 0) { + if (typeof body.instructions === "string") { + if (body.instructions.includes(CAVEMAN_OUTPUT_MARKER)) { + return { body, applied: false, skippedReason: "already_applied" }; + } + return { + body: { + ...body, + instructions: `${body.instructions.trim()}\n\n${buildCavemanOutputInstruction(config)}`, + }, + applied: true, + }; + } + return { body, applied: false, skippedReason: "no_messages" }; + } + + if (config.autoClarity !== false) { + const bypass = shouldBypassCavemanOutputMode(messages); + if (bypass) return { body, applied: false, skippedReason: bypass }; + } + + const alreadyApplied = messages.some( + (message) => + message.role === "system" && + typeof message.content === "string" && + message.content.includes(CAVEMAN_OUTPUT_MARKER) + ); + if (alreadyApplied) return { body, applied: false, skippedReason: "already_applied" }; + + const instruction = buildCavemanOutputInstruction(config); + const nextMessages = [...messages]; + const first = nextMessages[0]; + + if (first?.role === "system" && typeof first.content === "string") { + nextMessages[0] = { + ...first, + content: `${first.content.trim()}\n\n${instruction}`, + }; + } else { + nextMessages.unshift({ role: "system", content: instruction }); + } + + return { body: { ...body, messages: nextMessages }, applied: true }; +} diff --git a/open-sse/services/compression/preservation.ts b/open-sse/services/compression/preservation.ts index d65edbd84b..dcd382b92a 100644 --- a/open-sse/services/compression/preservation.ts +++ b/open-sse/services/compression/preservation.ts @@ -1,72 +1,183 @@ -interface PreservedBlock { +export interface PreservedBlock { placeholder: string; content: string; + kind: string; } -export function extractPreservedBlocks(text: string): { text: string; blocks: PreservedBlock[] } { +export interface PreservationOptions { + preservePatterns?: Array; +} + +interface CompiledPattern { + pattern: RegExp; + kind: string; +} + +const SENTINEL_PREFIX = "\u0000OMNI_CAVEMAN"; + +function randomSentinelSeed(): string { + const bytes = new Uint8Array(8); + const cryptoLike = globalThis.crypto; + if (cryptoLike?.getRandomValues) { + cryptoLike.getRandomValues(bytes); + return Array.from(bytes, (b) => b.toString(16).padStart(2, "0")).join(""); + } + return Math.random().toString(36).slice(2); +} + +function ensureGlobal(pattern: RegExp): RegExp { + const flags = pattern.flags.includes("g") ? pattern.flags : `${pattern.flags}g`; + return new RegExp(pattern.source, flags); +} + +function compileUserPatterns(patterns: Array | undefined): CompiledPattern[] { + if (!patterns?.length) return []; + const compiled: CompiledPattern[] = []; + for (const pattern of patterns) { + try { + compiled.push({ + pattern: typeof pattern === "string" ? new RegExp(pattern, "g") : ensureGlobal(pattern), + kind: "custom", + }); + } catch { + // Invalid user regexes are ignored by the hot path. Validation/preview exposes warnings. + } + } + return compiled; +} + +function replacePattern( + text: string, + pattern: RegExp, + kind: string, + addBlock: (content: string, kind: string) => string +): string { + pattern.lastIndex = 0; + return text.replace(pattern, (match) => { + if (!match) return match; + if (match.includes(SENTINEL_PREFIX)) return match; + return addBlock(match, kind); + }); +} + +export function findFencedCodeBlocks(text: string): string[] { + const blocks: string[] = []; + extractFencedCodeBlocks(text, (content) => { + blocks.push(content); + return content; + }); + return blocks; +} + +export function extractPreservedBlocks( + text: string, + options: PreservationOptions = {} +): { text: string; blocks: PreservedBlock[] } { const blocks: PreservedBlock[] = []; - let result = text; + const seed = randomSentinelSeed(); let counter = 0; - const addBlock = (content: string): string => { - const placeholder = `[PRESERVED_${counter}]`; - blocks.push({ placeholder, content }); + const addBlock = (content: string, kind: string): string => { + const placeholder = `${SENTINEL_PREFIX}_${seed}_${counter}\u0000`; + blocks.push({ placeholder, content, kind }); counter++; return placeholder; }; - // Extract fenced code blocks with a linear scan to avoid ReDoS-prone patterns. - result = extractFencedCodeBlocks(result, addBlock); + let result = text; - // Extract inline code (`...`) - result = result.replace(/`[^`\n]+`/g, (match) => addBlock(match)); + result = extractFrontmatter(result, addBlock); + result = extractFencedCodeBlocks(result, (content) => addBlock(content, "fenced_code")); - // Extract URLs - result = result.replace(/https?:\/\/[^\s)\]"'>]+/g, (match) => addBlock(match)); + const builtIns: CompiledPattern[] = [ + { pattern: /\$\$[\s\S]*?\$\$/g, kind: "math_block" }, + { pattern: /\\\[[\s\S]*?\\\]/g, kind: "math_block" }, + { pattern: /(?]+/gi, kind: "url" }, + { pattern: /\b[A-Z][A-Z0-9]*(?:_[A-Z0-9]+)+\b/g, kind: "const_case" }, + { pattern: /\bprocess\.env\.[A-Za-z_][A-Za-z0-9_]*\b/g, kind: "env_var" }, + { pattern: /\$[A-Z_][A-Z0-9_]*\b/g, kind: "env_var" }, + { pattern: /\b\d+(?:\.\d+){1,3}(?:[-+][A-Za-z0-9.-]+)?\b/g, kind: "version" }, + { pattern: /\b[a-zA-Z_$][\w$]*(?:\.[a-zA-Z_$][\w$]*)+\(\)?/g, kind: "dotted_identifier" }, + { pattern: /\b[A-Za-z_$][\w$]*\s*\([^()\n]*\)/g, kind: "function_call" }, + { + pattern: /(?:^|\s)(?:\.{0,2}\/[A-Za-z0-9_@./-]+|[A-Za-z]:\\[A-Za-z0-9_.\\/-]+)/g, + kind: "file_path", + }, + { + pattern: + /\b(?:TypeError|ReferenceError|SyntaxError|RangeError|URIError|EvalError|Error|Exception):[^\n]+/g, + kind: "error_message", + }, + ]; - // Extract file paths (Unix and Windows) - result = result.replace(/(?:\/[a-zA-Z0-9_./-]+|[a-zA-Z]:\\[a-zA-Z0-9_.\\/-]+)/g, (match) => { - if (match.length < 3) return match; - if (match.startsWith("[PRESERVED_")) return match; - return addBlock(match); - }); - - // Extract error-like patterns (TypeError:, Error:, 404, etc.) - result = result.replace( - /\b(?:TypeError|ReferenceError|SyntaxError|RangeError|URIError|EvalError|Error):\s*[^\s,;)]+/g, - (match) => addBlock(match) - ); + for (const { pattern, kind } of [...builtIns, ...compileUserPatterns(options.preservePatterns)]) { + result = replacePattern(result, ensureGlobal(pattern), kind, addBlock); + } return { text: result, blocks }; } +function extractFrontmatter( + text: string, + addBlock: (content: string, kind: string) => string +): string { + if (!text.startsWith("---\n")) return text; + const close = text.indexOf("\n---", 4); + if (close === -1) return text; + const closeEnd = text.indexOf("\n", close + 4); + const end = closeEnd === -1 ? text.length : closeEnd + 1; + return `${addBlock(text.slice(0, end), "frontmatter")}${text.slice(end)}`; +} + function extractFencedCodeBlocks(text: string, addBlock: (content: string) => string): string { + const lines = text.match(/[^\n]*(?:\n|$)/g) ?? [text]; let output = ""; - let cursor = 0; + let i = 0; - while (cursor < text.length) { - const start = text.indexOf("```", cursor); - if (start === -1) { - output += text.slice(cursor); - break; + while (i < lines.length) { + const line = lines[i]; + if (line === "" && i === lines.length - 1) break; + const opening = line.match(/^([ \t]{0,3})(`{3,}|~{3,})[^\n]*(?:\n|$)/); + if (!opening) { + output += line; + i++; + continue; } - const openingLineEnd = text.indexOf("\n", start + 3); - if (openingLineEnd === -1) { - output += text.slice(cursor); - break; + const fence = opening[2]; + const fenceChar = fence[0]; + const minLen = fence.length; + let block = line; + let j = i + 1; + let closed = false; + + while (j < lines.length) { + const candidate = lines[j]; + block += candidate; + const closeMatch = candidate.match(/^([ \t]{0,3})(`{3,}|~{3,})\s*(?:\n|$)/); + if (closeMatch && closeMatch[2][0] === fenceChar && closeMatch[2].length >= minLen) { + closed = true; + break; + } + j++; } - const closeStart = text.indexOf("\n```", openingLineEnd + 1); - if (closeStart === -1) { - output += text.slice(cursor); - break; + if (!closed) { + output += line; + i++; + continue; } - const closeEnd = closeStart + 4; - output += text.slice(cursor, start); - output += addBlock(text.slice(start, closeEnd)); - cursor = closeEnd; + output += addBlock(block); + i = j + 1; } return output; @@ -75,11 +186,14 @@ function extractFencedCodeBlocks(text: string, addBlock: (content: string) => st export function restorePreservedBlocks(text: string, blocks: PreservedBlock[]): string { let result = text; for (const block of blocks) { - result = result.replace(block.placeholder, () => block.content); + result = result.split(block.placeholder).join(block.content); } return result; } export function shouldPreserve(text: string, preservePatterns: RegExp[]): boolean { - return preservePatterns.some((pattern) => pattern.test(text)); + return preservePatterns.some((pattern) => { + pattern.lastIndex = 0; + return pattern.test(text); + }); } diff --git a/open-sse/services/compression/progressiveAging.ts b/open-sse/services/compression/progressiveAging.ts index 2b742ecf9f..f9f5399a60 100644 --- a/open-sse/services/compression/progressiveAging.ts +++ b/open-sse/services/compression/progressiveAging.ts @@ -2,6 +2,7 @@ import type { AgingThresholds, Summarizer } from "./types.ts"; import { DEFAULT_AGGRESSIVE_CONFIG } from "./types.ts"; import { applyLiteCompression } from "./lite.ts"; import { cavemanCompress } from "./caveman.ts"; +import { extractTextContent, replaceTextContent, type ChatMessageLike } from "./messageContent.ts"; const COMPRESSED_MARKER_RE = /^\[COMPRESSED:/; @@ -9,43 +10,24 @@ function estimateTokens(text: string): number { return Math.ceil(text.length / 4); } -interface ChatMessage { - role: string; - content?: string | Array<{ type: string; text?: string }>; -} - -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 ""; -} +type ChatMessage = ChatMessageLike; 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; } export function applyAging( messages: unknown[], thresholds?: AgingThresholds, - summarizer?: Summarizer + summarizer?: Summarizer, + preserveSystemPrompt = true ): { messages: unknown[]; saved: number } { const t = thresholds ?? DEFAULT_AGGRESSIVE_CONFIG.thresholds; const sum = summarizer ?? { summarize: (msgs: unknown[]) => { const typed = msgs as ChatMessage[]; const last = typed.filter((m) => m.role === "assistant").pop(); - return last ? extractText(last.content).slice(0, 200) : ""; + return last ? extractTextContent(last.content).slice(0, 200) : ""; }, }; @@ -58,9 +40,9 @@ export function applyAging( for (let i = 0; i < typed.length; i++) { const msg = typed[i]; - const text = extractText(msg.content); + const text = extractTextContent(msg.content); - if (COMPRESSED_MARKER_RE.test(text)) { + if ((preserveSystemPrompt && msg.role === "system") || COMPRESSED_MARKER_RE.test(text)) { result.push(msg); continue; } @@ -75,7 +57,7 @@ export function applyAging( const newContent = typeof compressed.body.messages[0].content === "string" ? compressed.body.messages[0].content - : extractText(compressed.body.messages[0].content); + : extractTextContent(compressed.body.messages[0].content); const tagged = `[COMPRESSED:aging:light] ${newContent}`; saved += estimateTokens(text) - estimateTokens(tagged); result.push(setContent(msg, tagged)); @@ -88,7 +70,7 @@ export function applyAging( const newContent = typeof compressed.body.messages[0].content === "string" ? compressed.body.messages[0].content - : extractText(compressed.body.messages[0].content); + : extractTextContent(compressed.body.messages[0].content); const tagged = `[COMPRESSED:aging:moderate] ${newContent}`; saved += estimateTokens(text) - estimateTokens(tagged); result.push(setContent(msg, tagged)); diff --git a/open-sse/services/compression/strategySelector.ts b/open-sse/services/compression/strategySelector.ts index 59164c8ce9..7419f4d538 100644 --- a/open-sse/services/compression/strategySelector.ts +++ b/open-sse/services/compression/strategySelector.ts @@ -32,7 +32,7 @@ export function getEffectiveMode( const comboMode = checkComboOverride(config, comboId); if (comboMode) return comboMode; - if (shouldAutoTrigger(config, estimatedTokens)) return "lite"; + if (shouldAutoTrigger(config, estimatedTokens)) return config.autoTriggerMode ?? "lite"; return config.defaultMode; } @@ -65,13 +65,23 @@ export function applyCompression( return { body, compressed: false, stats: null }; } if (mode === "lite") { - return applyLiteCompression(body, options); + return applyLiteCompression(body, { + ...options, + preserveSystemPrompt: options?.config?.preserveSystemPrompt !== false, + }); } if (mode === "standard") { - return cavemanCompress( - body as Parameters[0], - options?.config?.cavemanConfig - ); + const cavemanConfig = { + ...(options?.config?.cavemanConfig ?? {}), + ...(options?.config?.preserveSystemPrompt !== false + ? { + compressRoles: (options?.config?.cavemanConfig?.compressRoles ?? ["user"]).filter( + (role) => role !== "system" + ), + } + : {}), + }; + return cavemanCompress(body as Parameters[0], cavemanConfig); } if (mode === "aggressive") { const messages = (body.messages ?? []) as Array<{ @@ -82,7 +92,10 @@ export function applyCompression( if (!Array.isArray(messages) || messages.length === 0) { return { body, compressed: false, stats: null }; } - const aggressiveConfig = options?.config?.aggressive; + const aggressiveConfig = { + ...(options?.config?.aggressive ?? {}), + preserveSystemPrompt: options?.config?.preserveSystemPrompt !== false, + }; const result = compressAggressive(messages, aggressiveConfig); const compressedBody = { ...body, messages: result.messages }; return { @@ -107,8 +120,11 @@ export function applyCompression( if (!Array.isArray(messages) || messages.length === 0) { return { body, compressed: false, stats: null }; } - const ultraConfig = options?.config?.ultra; - const result = ultraCompress(messages, ultraConfig ?? {}); + const ultraConfig = { + ...(options?.config?.ultra ?? {}), + preserveSystemPrompt: options?.config?.preserveSystemPrompt !== false, + }; + const result = ultraCompress(messages, ultraConfig); const compressedBody = { ...body, messages: result.messages }; return { body: compressedBody, diff --git a/open-sse/services/compression/types.ts b/open-sse/services/compression/types.ts index f019f62bc6..443cc93fdf 100644 --- a/open-sse/services/compression/types.ts +++ b/open-sse/services/compression/types.ts @@ -9,6 +9,7 @@ */ export type CompressionMode = "off" | "lite" | "standard" | "aggressive" | "ultra"; +export type CavemanIntensity = "lite" | "full" | "ultra"; export interface CavemanRule { name: string; @@ -16,6 +17,9 @@ export interface CavemanRule { replacement: string | ((match: string, ...groups: string[]) => string); context: "all" | "user" | "system" | "assistant"; preservePatterns?: RegExp[]; + category?: "filler" | "context" | "structural" | "dedup" | "terse" | "ultra"; + description?: string; + minIntensity?: CavemanIntensity; } export interface CavemanConfig { @@ -24,16 +28,26 @@ export interface CavemanConfig { skipRules: string[]; minMessageLength: number; preservePatterns: string[]; + intensity: CavemanIntensity; +} + +export interface CavemanOutputModeConfig { + enabled: boolean; + intensity: CavemanIntensity; + autoClarity: boolean; } export interface CompressionConfig { enabled: boolean; defaultMode: CompressionMode; + autoTriggerMode?: CompressionMode; autoTriggerTokens: number; cacheMinutes: number; preserveSystemPrompt: boolean; + mcpDescriptionCompressionEnabled?: boolean; comboOverrides: Record; cavemanConfig?: CavemanConfig; + cavemanOutputMode?: CavemanOutputModeConfig; aggressive?: AggressiveConfig; ultra?: UltraConfig; } @@ -47,6 +61,16 @@ export interface CompressionStats { timestamp: number; rulesApplied?: string[]; durationMs?: number; + validationWarnings?: string[]; + validationErrors?: string[]; + fallbackApplied?: boolean; + preservedBlockCount?: number; + realUsage?: { + promptTokens?: number; + completionTokens?: number; + totalTokens?: number; + source: "provider" | "estimated" | "stream"; + }; aggressive?: { summarizerSavings: number; toolResultSavings: number; @@ -63,9 +87,11 @@ export interface CompressionResult { export const DEFAULT_COMPRESSION_CONFIG: CompressionConfig = { enabled: false, defaultMode: "off", + autoTriggerMode: "lite", autoTriggerTokens: 0, cacheMinutes: 5, preserveSystemPrompt: true, + mcpDescriptionCompressionEnabled: true, comboOverrides: {}, }; @@ -75,6 +101,13 @@ export const DEFAULT_CAVEMAN_CONFIG: CavemanConfig = { skipRules: [], minMessageLength: 50, preservePatterns: [], + intensity: "full", +}; + +export const DEFAULT_CAVEMAN_OUTPUT_MODE_CONFIG: CavemanOutputModeConfig = { + enabled: false, + intensity: "full", + autoClarity: true, }; /** Aging thresholds for progressive message degradation (Phase 3) */ @@ -101,6 +134,7 @@ export interface AggressiveConfig { summarizerEnabled: boolean; maxTokensPerMessage: number; minSavingsThreshold: number; + preserveSystemPrompt?: boolean; } /** Options for the Summarizer interface (Phase 3) */ @@ -159,6 +193,7 @@ export interface UltraConfig { * 0 = always apply when mode is "ultra". */ maxTokensPerMessage: number; + preserveSystemPrompt?: boolean; } export const DEFAULT_ULTRA_CONFIG: UltraConfig = { diff --git a/open-sse/services/compression/ultra.ts b/open-sse/services/compression/ultra.ts index 0c1dd8d219..1fc3cc0fc2 100644 --- a/open-sse/services/compression/ultra.ts +++ b/open-sse/services/compression/ultra.ts @@ -1,6 +1,7 @@ import { pruneByScore } from "./ultraHeuristic.ts"; import { DEFAULT_ULTRA_CONFIG } from "./types.ts"; import type { UltraConfig, CompressionStats, CompressionMode } from "./types.ts"; +import { extractTextContent, mapTextContent, type ChatMessageLike } from "./messageContent.ts"; const COMPRESSED_PREFIX = "[COMPRESSED:"; @@ -21,26 +22,7 @@ export interface UltraCompressResult { stats: CompressionStats; } -type Message = { role: string; content?: string | unknown[]; [key: string]: unknown }; - -function extractText(content: string | unknown[] | undefined): string { - if (!content) return ""; - if (typeof content === "string") return content; - return (content as Array<{ type?: string; text?: string }>) - .filter((b) => b.type === "text" && b.text) - .map((b) => b.text as string) - .join("\n"); -} - -function applyTextToContent( - original: string | unknown[] | undefined, - compressed: string -): string | unknown[] { - if (!original || typeof original === "string") return compressed; - return (original as Array<{ type?: string; text?: string; [k: string]: unknown }>).map((b) => - b.type === "text" ? { ...b, text: compressed } : b - ); -} +type Message = ChatMessageLike; export function ultraCompress( messages: Message[], @@ -57,21 +39,26 @@ export function ultraCompress( let compressedChars = 0; const compressed = messages.map((msg) => { - const text = extractText(msg.content); + if (effectiveConfig.preserveSystemPrompt !== false && msg.role === "system") return msg; + const text = extractTextContent(msg.content); if (!text) return msg; if (text.startsWith(COMPRESSED_PREFIX)) return msg; if (maxTokensPerMessage > 0 && Math.ceil(text.length / 4) <= maxTokensPerMessage) { return msg; } - originalChars += text.length; - const pruned = pruneByScore(text, compressionRate, minScoreThreshold); - compressedChars += pruned.length; - - return { - ...msg, - content: applyTextToContent(msg.content, pruned), - }; + let messageOriginalChars = 0; + let messageCompressedChars = 0; + const next = mapTextContent(msg, (textPart) => { + if (!textPart || textPart.startsWith(COMPRESSED_PREFIX)) return textPart; + messageOriginalChars += textPart.length; + const pruned = pruneByScore(textPart, compressionRate, minScoreThreshold); + messageCompressedChars += pruned.length; + return pruned; + }) as Message; + originalChars += messageOriginalChars; + compressedChars += messageCompressedChars; + return next; }); const originalTokens = Math.ceil(originalChars / 4); diff --git a/open-sse/services/compression/validation.ts b/open-sse/services/compression/validation.ts new file mode 100644 index 0000000000..cd4c75629e --- /dev/null +++ b/open-sse/services/compression/validation.ts @@ -0,0 +1,132 @@ +import { findFencedCodeBlocks } from "./preservation.ts"; + +export interface ValidationResult { + valid: boolean; + errors: string[]; + warnings: string[]; + fallbackApplied: boolean; +} + +function collectMatches(text: string, pattern: RegExp): string[] { + const matches: string[] = []; + pattern.lastIndex = 0; + let match: RegExpExecArray | null; + while ((match = pattern.exec(text)) !== null) { + if (match[0]) matches.push(match[0]); + if (match.index === pattern.lastIndex) pattern.lastIndex++; + } + return matches; +} + +function requireExactPresence( + label: string, + originalItems: string[], + compressed: string, + errors: string[] +) { + for (const item of originalItems) { + if (!compressed.includes(item)) { + const preview = item.replace(/\s+/g, " ").slice(0, 80); + errors.push(`${label} changed or missing: ${preview}`); + } + } +} + +export function validateCompression(original: string, compressed: string): ValidationResult { + const errors: string[] = []; + const warnings: string[] = []; + + if (typeof original !== "string" || typeof compressed !== "string") { + return { + valid: false, + errors: ["validation received non-string input"], + warnings, + fallbackApplied: true, + }; + } + + if (original.length > 0 && compressed.trim().length === 0) { + errors.push("compressed text is empty"); + } + + requireExactPresence("fenced code block", findFencedCodeBlocks(original), compressed, errors); + requireExactPresence("inline code", collectMatches(original, /`[^`\n]+`/g), compressed, errors); + requireExactPresence( + "URL", + collectMatches(original, /\bhttps?:\/\/[^\s)\]"'>]+/gi), + compressed, + errors + ); + requireExactPresence( + "markdown link", + collectMatches(original, /\[[^\]\n]+\]\([^) \n]+(?:\s+"[^"]*")?\)/g), + compressed, + errors + ); + requireExactPresence("frontmatter", collectFrontmatter(original), compressed, errors); + requireExactPresence("heading", collectMatches(original, /^#{1,6}\s+.+$/gm), compressed, errors); + requireExactPresence( + "table row", + collectMatches(original, /^\s*\|.*\|\s*$/gm), + compressed, + errors + ); + requireExactPresence( + "math block", + collectMatches(original, /\$\$[\s\S]*?\$\$/g), + compressed, + errors + ); + requireExactPresence( + "inline math", + collectMatches(original, /(? ${compressedFenceCount}` + ); + } + + if (compressed.length > original.length) { + warnings.push("compressed text is longer than original"); + } + + return { + valid: errors.length === 0, + errors, + warnings, + fallbackApplied: errors.length > 0, + }; +} + +function collectFrontmatter(text: string): string[] { + if (!text.startsWith("---\n")) return []; + const close = text.indexOf("\n---", 4); + if (close === -1) return []; + const closeEnd = text.indexOf("\n", close + 4); + const end = closeEnd === -1 ? text.length : closeEnd + 1; + return [text.slice(0, end)]; +} diff --git a/open-sse/utils/aiSdkCompat.ts b/open-sse/utils/aiSdkCompat.ts index 5f0100b08f..58ff08b738 100644 --- a/open-sse/utils/aiSdkCompat.ts +++ b/open-sse/utils/aiSdkCompat.ts @@ -23,9 +23,9 @@ export function resolveStreamFlag(bodyStream: unknown, acceptHeader: unknown): b if (bodyStream === true) return true; if (bodyStream === false) return false; - // No explicit stream param — default to false per OpenAI spec - // (Fixes #1873 - previously relied on Accept header heuristic) - return false; + // No explicit stream param — preserve OmniRoute's streaming default unless + // the client explicitly asks for JSON and does not also accept SSE. + return !clientWantsJsonResponse(acceptHeader); } /** diff --git a/scripts/scratch/test-migrations.ts b/scripts/scratch/test-migrations.ts index d908738b1d..f21ca16ca8 100644 --- a/scripts/scratch/test-migrations.ts +++ b/scripts/scratch/test-migrations.ts @@ -1,47 +1,56 @@ import Database from "better-sqlite3"; -import fs from "fs"; -import path from "path"; +import { runMigrations, getMigrationStatus } from "../../src/lib/db/migrationRunner.js"; -import { runMigrations, getMigrationStatus } from "../../src/lib/db/migrationRunner.ts"; +const db = new Database("/tmp/test-migrations.sqlite"); +db.exec( + "CREATE TABLE _omniroute_migrations (version TEXT PRIMARY KEY, name TEXT NOT NULL, applied_at TEXT NOT NULL DEFAULT (datetime('now')))" +); -const db = new Database(":memory:"); -db.exec(` - CREATE TABLE IF NOT EXISTS _omniroute_migrations ( - version TEXT PRIMARY KEY, - name TEXT NOT NULL, - applied_at TEXT NOT NULL DEFAULT (datetime('now')) - ); - INSERT INTO _omniroute_migrations (version, name) VALUES ('001', 'initial_schema'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('002', 'mcp_a2a_tables'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('003', 'provider_node_custom_paths'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('004', 'proxy_registry'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('005', 'combo_agent_fields'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('006', 'detailed_request_logs'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('007', 'search_request_type'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('008', 'registered_keys'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('009', 'requested_model'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('010', 'model_combo_mappings'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('011', 'webhooks'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('012', 'fix_token_input_cache_tokens'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('013', 'quota_snapshots'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('014', 'unified_log_artifacts'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('015', 'create_memories'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('016', 'create_skills'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('017', 'version_manager_upstream_proxy'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('018', 'call_logs_detailed_tokens'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('019', 'context_handoffs'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('020', 'combo_sort_order'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('021', 'combo_call_log_targets'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('026', 'call_logs_cache_source'); - INSERT INTO _omniroute_migrations (version, name) VALUES ('029', 'provider_connection_max_concurrent'); -`); - -console.log("Status before:", getMigrationStatus(db)); +const fakeApplied = [ + "001", + "002", + "003", + "004", + "005", + "006", + "007", + "008", + "009", + "010", + "011", + "012", + "013", + "014", + "015", + "016", + "017", + "018", + "019", + "020", + "021", +]; +for (const v of fakeApplied) { + db.prepare("INSERT INTO _omniroute_migrations (version, name) VALUES (?, ?)").run(v, "fake_" + v); +} +db.prepare("INSERT INTO _omniroute_migrations (version, name) VALUES (?, ?)").run( + "026", + "call_logs_cache_source" +); +db.prepare("INSERT INTO _omniroute_migrations (version, name) VALUES (?, ?)").run( + "029", + "provider_connection_max_concurrent" +); +console.log( + "Status before:", + getMigrationStatus(db).pending.map((p) => p.version) +); try { runMigrations(db, { isNewDb: false }); -} catch (e) { - console.error("Migration threw error:", e); +} catch (e: any) { + console.error("Error:", e.message); } - -console.log("Status after:", getMigrationStatus(db)); +console.log( + "Status after:", + getMigrationStatus(db).pending.map((p) => p.version) +); diff --git a/src/app/(dashboard)/dashboard/analytics/CompressionAnalyticsTab.tsx b/src/app/(dashboard)/dashboard/analytics/CompressionAnalyticsTab.tsx index 184eb936db..c0d08da196 100644 --- a/src/app/(dashboard)/dashboard/analytics/CompressionAnalyticsTab.tsx +++ b/src/app/(dashboard)/dashboard/analytics/CompressionAnalyticsTab.tsx @@ -17,6 +17,17 @@ interface CompressionAnalyticsSummary { byMode: Record; byProvider: Record; last24h: Array<{ hour: string; count: number; tokensSaved: number }>; + validationFallbacks: number; + realUsage: { + requestsWithReceipts: number; + promptTokens: number; + completionTokens: number; + totalTokens: number; + cacheReadTokens: number; + cacheWriteTokens: number; + estimatedUsdSaved: number; + bySource: Record; + }; } function StatCard({ @@ -178,7 +189,7 @@ export default function CompressionAnalyticsTab() { {/* KPI Cards */} -
+
+ +
+ {stats.realUsage.requestsWithReceipts > 0 && ( +
+

+ receipt_long + Real Usage Receipts +

+
+
+
Prompt tokens
+
+ {stats.realUsage.promptTokens.toLocaleString()} +
+
+
+
Completion tokens
+
+ {stats.realUsage.completionTokens.toLocaleString()} +
+
+
+
Total tokens
+
+ {stats.realUsage.totalTokens.toLocaleString()} +
+
+
+
Cache tokens
+
+ {( + (stats.realUsage.cacheReadTokens ?? 0) + (stats.realUsage.cacheWriteTokens ?? 0) + ).toLocaleString()} +
+
+
+
Sources
+
+ {Object.entries(stats.realUsage.bySource) + .map(([source, count]) => `${source}: ${count}`) + .join(", ")} +
+
+
+
+ )} + {/* Mode Breakdown */} {modes.length > 0 && (
diff --git a/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx b/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx index d6f4df413a..c072e37acf 100644 --- a/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx @@ -5,6 +5,7 @@ import { Card, Button } from "@/shared/components"; import { useTranslations } from "next-intl"; type CompressionMode = "off" | "lite" | "standard" | "aggressive" | "ultra"; +type CavemanIntensity = "lite" | "full" | "ultra"; interface CavemanConfig { enabled: boolean; @@ -12,6 +13,13 @@ interface CavemanConfig { skipRules: string[]; minMessageLength: number; preservePatterns: string[]; + intensity: CavemanIntensity; +} + +interface CavemanOutputModeConfig { + enabled: boolean; + intensity: CavemanIntensity; + autoClarity: boolean; } interface AggressiveConfig { @@ -45,15 +53,26 @@ interface UltraConfig { interface CompressionConfig { enabled: boolean; defaultMode: CompressionMode; + autoTriggerMode?: CompressionMode; autoTriggerTokens: number; cacheMinutes: number; preserveSystemPrompt: boolean; + mcpDescriptionCompressionEnabled?: boolean; comboOverrides: Record; cavemanConfig?: CavemanConfig; + cavemanOutputMode?: CavemanOutputModeConfig; aggressive?: AggressiveConfig; ultra?: UltraConfig; } +interface RuleMetadata { + name: string; + category: string; + context: string; + minIntensity: CavemanIntensity; + description: string; +} + const MODES: { value: CompressionMode; labelKey: string; descKey: string; icon: string }[] = [ { value: "off", @@ -93,38 +112,6 @@ const ROLE_OPTIONS: { value: "user" | "assistant" | "system"; labelKey: string } { value: "system", labelKey: "compressionRoleSystem" }, ]; -const ALL_CAVEMAN_RULES = [ - "polite_framing", - "hedging", - "verbose_instructions", - "filler_adverbs", - "filler_phrases", - "redundant_openers", - "verbose_requests", - "self_reference", - "excessive_gratitude", - "qualifier_removal", - "compound_collapse", - "explanatory_prefix", - "question_to_directive", - "context_setup", - "intent_clarification", - "background_removal", - "meta_commentary", - "purpose_statement", - "list_conjunction", - "purpose_phrases", - "redundant_quantifiers", - "verbose_connectors", - "transition_removal", - "emphasis_removal", - "passive_voice", - "repeated_context", - "repeated_question", - "reestablished_context", - "summary_replacement", -]; - export default function CompressionSettingsTab() { const t = useTranslations("settings"); const [config, setConfig] = useState({ @@ -140,6 +127,12 @@ export default function CompressionSettingsTab() { skipRules: [], minMessageLength: 50, preservePatterns: [], + intensity: "full", + }, + cavemanOutputMode: { + enabled: false, + intensity: "full", + autoClarity: true, }, aggressive: { thresholds: { fullSummary: 5, moderate: 3, light: 2, verbatim: 2 }, @@ -165,6 +158,7 @@ export default function CompressionSettingsTab() { const [saving, setSaving] = useState(false); const [loading, setLoading] = useState(true); const [status, setStatus] = useState<"" | "saved" | "error">(""); + const [ruleMetadata, setRuleMetadata] = useState([]); useEffect(() => { fetch("/api/settings/compression") @@ -174,6 +168,12 @@ export default function CompressionSettingsTab() { }) .catch(() => {}) .finally(() => setLoading(false)); + fetch("/api/compression/rules") + .then((r) => (r.ok ? r.json() : null)) + .then((data) => { + if (Array.isArray(data?.rules)) setRuleMetadata(data.rules); + }) + .catch(() => {}); }, []); const save = async (updates: Partial) => { @@ -326,6 +326,21 @@ export default function CompressionSettingsTab() {
+ + + +

{t("compressionSkipRules")}

{t("compressionSkipRulesDesc")}

- {ALL_CAVEMAN_RULES.map((rule) => ( + {ruleMetadata.map((rule) => ( ))}
@@ -482,6 +539,81 @@ export default function CompressionSettingsTab() {
)} + {config.enabled && config.cavemanOutputMode && ( +
+
+
+

Caveman output mode

+

+ Injects terse response instructions without rewriting provider output. +

+
+ +
+ + + + +
+ )} + {config.enabled && config.defaultMode === "aggressive" && config.aggressive && (
diff --git a/src/app/api/compression/preview/route.ts b/src/app/api/compression/preview/route.ts index 5b966ec3f2..a4bd60a685 100644 --- a/src/app/api/compression/preview/route.ts +++ b/src/app/api/compression/preview/route.ts @@ -3,6 +3,7 @@ import { z } from "zod"; import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy"; import { applyCompression } from "@omniroute/open-sse/services/compression/strategySelector"; import type { CompressionMode } from "@omniroute/open-sse/services/compression/types"; +import { buildCompressionPreviewDiff } from "@omniroute/open-sse/services/compression/diffHelper"; const PreviewRequestSchema = z.object({ messages: z @@ -67,6 +68,7 @@ export async function POST(req: Request) { const tokensSaved = Math.max(0, originalTokens - compressedTokens); const savingsPct = originalTokens > 0 ? Math.round((tokensSaved / originalTokens) * 100) : 0; const techniquesUsed: string[] = result.stats?.techniquesUsed ?? []; + const diff = buildCompressionPreviewDiff(originalText, compressedText, result.stats); return NextResponse.json({ original: originalText, @@ -77,6 +79,23 @@ export async function POST(req: Request) { savingsPct, techniquesUsed, durationMs, + mode, + intensity: null, + outputMode: null, + skippedReasons: [], + diff: diff.segments, + preservedBlocks: diff.preservedBlocks, + ruleRemovals: diff.ruleRemovals, + rulesApplied: diff.ruleRemovals, + validation: { + valid: diff.validationErrors.length === 0, + errors: diff.validationErrors, + warnings: diff.validationWarnings, + fallbackApplied: diff.fallbackApplied, + }, + validationWarnings: diff.validationWarnings, + validationErrors: diff.validationErrors, + fallbackApplied: diff.fallbackApplied, }); } catch (err: unknown) { const msg = err instanceof Error ? err.message : String(err); diff --git a/src/app/api/compression/rules/route.ts b/src/app/api/compression/rules/route.ts new file mode 100644 index 0000000000..e00093c8fa --- /dev/null +++ b/src/app/api/compression/rules/route.ts @@ -0,0 +1,12 @@ +import { NextResponse } from "next/server"; +import { enforceApiKeyPolicy } from "@/shared/utils/apiKeyPolicy"; +import { getCavemanRuleMetadata } from "@omniroute/open-sse/services/compression/cavemanRules"; + +export async function GET(req: Request) { + const policy = await enforceApiKeyPolicy(req, "settings"); + if (policy.rejection) return policy.rejection; + + return NextResponse.json({ + rules: getCavemanRuleMetadata(), + }); +} diff --git a/src/app/api/settings/compression/route.ts b/src/app/api/settings/compression/route.ts index 4262eb5019..c6400cdea2 100644 --- a/src/app/api/settings/compression/route.ts +++ b/src/app/api/settings/compression/route.ts @@ -5,6 +5,7 @@ import { isAuthenticated } from "@/shared/utils/apiAuth"; import { isValidationFailure, validateBody } from "@/shared/validation/helpers"; const compressionModeSchema = z.enum(["off", "lite", "standard", "aggressive", "ultra"]); +const cavemanIntensitySchema = z.enum(["lite", "full", "ultra"]); const cavemanConfigSchema = z .object({ @@ -13,6 +14,15 @@ const cavemanConfigSchema = z skipRules: z.array(z.string()).optional(), minMessageLength: z.number().int().min(0).optional(), preservePatterns: z.array(z.string()).optional(), + intensity: cavemanIntensitySchema.optional(), + }) + .strict(); + +const cavemanOutputModeSchema = z + .object({ + enabled: z.boolean().optional(), + intensity: cavemanIntensitySchema.optional(), + autoClarity: z.boolean().optional(), }) .strict(); @@ -56,11 +66,14 @@ const compressionSettingsUpdateSchema = z .object({ enabled: z.boolean().optional(), defaultMode: compressionModeSchema.optional(), + autoTriggerMode: compressionModeSchema.optional(), autoTriggerTokens: z.number().int().min(0).optional(), cacheMinutes: z.number().int().min(1).max(60).optional(), preserveSystemPrompt: z.boolean().optional(), + mcpDescriptionCompressionEnabled: z.boolean().optional(), comboOverrides: z.record(z.string(), compressionModeSchema).optional(), cavemanConfig: cavemanConfigSchema.optional(), + cavemanOutputMode: cavemanOutputModeSchema.optional(), aggressive: aggressiveConfigSchema.optional(), ultra: ultraConfigSchema.optional(), }) diff --git a/src/lib/db/compression.ts b/src/lib/db/compression.ts index 8d45f82f37..abbc8b1eb6 100644 --- a/src/lib/db/compression.ts +++ b/src/lib/db/compression.ts @@ -4,10 +4,12 @@ import { invalidateDbCache } from "./readCache"; import { DEFAULT_AGGRESSIVE_CONFIG, DEFAULT_CAVEMAN_CONFIG, + DEFAULT_CAVEMAN_OUTPUT_MODE_CONFIG, DEFAULT_COMPRESSION_CONFIG, DEFAULT_ULTRA_CONFIG, type AggressiveConfig, type CavemanConfig, + type CavemanOutputModeConfig, type CompressionConfig, type CompressionMode, type UltraConfig, @@ -47,6 +49,10 @@ function parseJsonSafe(raw: string | null): unknown { function normalizeCavemanConfig(value: unknown): CavemanConfig { const record = toRecord(value); + const intensity = + record.intensity === "lite" || record.intensity === "full" || record.intensity === "ultra" + ? record.intensity + : DEFAULT_CAVEMAN_CONFIG.intensity; return { ...DEFAULT_CAVEMAN_CONFIG, ...record, @@ -66,6 +72,26 @@ function normalizeCavemanConfig(value: unknown): CavemanConfig { preservePatterns: Array.isArray(record.preservePatterns) ? record.preservePatterns.filter((pattern): pattern is string => typeof pattern === "string") : DEFAULT_CAVEMAN_CONFIG.preservePatterns, + intensity, + }; +} + +function normalizeCavemanOutputModeConfig(value: unknown): CavemanOutputModeConfig { + const record = toRecord(value); + return { + ...DEFAULT_CAVEMAN_OUTPUT_MODE_CONFIG, + enabled: + typeof record.enabled === "boolean" + ? record.enabled + : DEFAULT_CAVEMAN_OUTPUT_MODE_CONFIG.enabled, + intensity: + record.intensity === "lite" || record.intensity === "full" || record.intensity === "ultra" + ? record.intensity + : DEFAULT_CAVEMAN_OUTPUT_MODE_CONFIG.intensity, + autoClarity: + typeof record.autoClarity === "boolean" + ? record.autoClarity + : DEFAULT_CAVEMAN_OUTPUT_MODE_CONFIG.autoClarity, }; } @@ -196,6 +222,7 @@ export async function getCompressionSettings(): Promise { const config: CompressionConfig = { ...DEFAULT_COMPRESSION_CONFIG, cavemanConfig: { ...DEFAULT_CAVEMAN_CONFIG }, + cavemanOutputMode: { ...DEFAULT_CAVEMAN_OUTPUT_MODE_CONFIG }, aggressive: normalizeAggressiveConfig(undefined), ultra: normalizeUltraConfig(undefined), }; @@ -217,6 +244,11 @@ export async function getCompressionSettings(): Promise { config.defaultMode = parsed as CompressionMode; } break; + case "autoTriggerMode": + if (typeof parsed === "string" && COMPRESSION_MODES.has(parsed as CompressionMode)) { + config.autoTriggerMode = parsed as CompressionMode; + } + break; case "autoTriggerTokens": config.autoTriggerTokens = typeof parsed === "number" && Number.isFinite(parsed) @@ -232,6 +264,9 @@ export async function getCompressionSettings(): Promise { case "preserveSystemPrompt": config.preserveSystemPrompt = parsed !== false; break; + case "mcpDescriptionCompressionEnabled": + config.mcpDescriptionCompressionEnabled = parsed !== false; + break; case "comboOverrides": if (parsed && typeof parsed === "object") { const overrides: Record = {}; @@ -246,6 +281,9 @@ export async function getCompressionSettings(): Promise { case "cavemanConfig": config.cavemanConfig = normalizeCavemanConfig(parsed); break; + case "cavemanOutputMode": + config.cavemanOutputMode = normalizeCavemanOutputModeConfig(parsed); + break; case "aggressive": case "aggressiveConfig": config.aggressive = normalizeAggressiveConfig(parsed); diff --git a/src/lib/db/compressionAnalytics.ts b/src/lib/db/compressionAnalytics.ts index f587a915ae..950c18cd39 100644 --- a/src/lib/db/compressionAnalytics.ts +++ b/src/lib/db/compressionAnalytics.ts @@ -11,6 +11,17 @@ export interface CompressionAnalyticsRow { tokens_saved: number; duration_ms?: number | null; request_id?: string | null; + actual_prompt_tokens?: number | null; + actual_completion_tokens?: number | null; + actual_total_tokens?: number | null; + actual_cache_read_tokens?: number | null; + actual_cache_write_tokens?: number | null; + estimated_usd_saved?: number | null; + mcp_description_tokens_saved?: number | null; + multimodal_skip_count?: number | null; + receipt_source?: string | null; + validation_fallback?: boolean | number | null; + output_mode?: string | null; } export interface CompressionAnalyticsSummary { @@ -21,14 +32,85 @@ export interface CompressionAnalyticsSummary { byMode: Record; byProvider: Record; last24h: Array<{ hour: string; count: number; tokensSaved: number }>; + validationFallbacks: number; + realUsage: { + requestsWithReceipts: number; + promptTokens: number; + completionTokens: number; + totalTokens: number; + cacheReadTokens: number; + cacheWriteTokens: number; + estimatedUsdSaved: number; + bySource: Record; + }; +} + +let columnsEnsured = false; + +function ensureCompressionAnalyticsColumns(): void { + if (columnsEnsured) return; + const db = getDbInstance(); + const rows = db.prepare("PRAGMA table_info(compression_analytics)").all() as Array<{ + name: string; + }>; + const columns = new Set(rows.map((row) => row.name)); + const addColumn = (name: string, sql: string) => { + if (!columns.has(name)) db.exec(sql); + }; + addColumn( + "actual_prompt_tokens", + "ALTER TABLE compression_analytics ADD COLUMN actual_prompt_tokens INTEGER" + ); + addColumn( + "actual_completion_tokens", + "ALTER TABLE compression_analytics ADD COLUMN actual_completion_tokens INTEGER" + ); + addColumn( + "actual_total_tokens", + "ALTER TABLE compression_analytics ADD COLUMN actual_total_tokens INTEGER" + ); + addColumn( + "actual_cache_read_tokens", + "ALTER TABLE compression_analytics ADD COLUMN actual_cache_read_tokens INTEGER" + ); + addColumn( + "actual_cache_write_tokens", + "ALTER TABLE compression_analytics ADD COLUMN actual_cache_write_tokens INTEGER" + ); + addColumn( + "estimated_usd_saved", + "ALTER TABLE compression_analytics ADD COLUMN estimated_usd_saved REAL" + ); + addColumn( + "mcp_description_tokens_saved", + "ALTER TABLE compression_analytics ADD COLUMN mcp_description_tokens_saved INTEGER DEFAULT 0" + ); + addColumn( + "multimodal_skip_count", + "ALTER TABLE compression_analytics ADD COLUMN multimodal_skip_count INTEGER DEFAULT 0" + ); + addColumn("receipt_source", "ALTER TABLE compression_analytics ADD COLUMN receipt_source TEXT"); + addColumn( + "validation_fallback", + "ALTER TABLE compression_analytics ADD COLUMN validation_fallback INTEGER DEFAULT 0" + ); + addColumn("output_mode", "ALTER TABLE compression_analytics ADD COLUMN output_mode TEXT"); + columnsEnsured = true; } export function insertCompressionAnalyticsRow(row: CompressionAnalyticsRow): void { const db = getDbInstance(); + ensureCompressionAnalyticsColumns(); db.prepare( ` - INSERT INTO compression_analytics (timestamp, combo_id, provider, mode, original_tokens, compressed_tokens, tokens_saved, duration_ms, request_id) - VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?) + INSERT INTO compression_analytics ( + timestamp, combo_id, provider, mode, original_tokens, compressed_tokens, tokens_saved, + duration_ms, request_id, actual_prompt_tokens, actual_completion_tokens, + actual_total_tokens, actual_cache_read_tokens, actual_cache_write_tokens, + estimated_usd_saved, mcp_description_tokens_saved, multimodal_skip_count, + receipt_source, validation_fallback, output_mode + ) + VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) ` ).run( row.timestamp, @@ -39,12 +121,90 @@ export function insertCompressionAnalyticsRow(row: CompressionAnalyticsRow): voi row.compressed_tokens, row.tokens_saved, row.duration_ms ?? null, - row.request_id ?? null + row.request_id ?? null, + row.actual_prompt_tokens ?? null, + row.actual_completion_tokens ?? null, + row.actual_total_tokens ?? null, + row.actual_cache_read_tokens ?? null, + row.actual_cache_write_tokens ?? null, + row.estimated_usd_saved ?? null, + row.mcp_description_tokens_saved ?? 0, + row.multimodal_skip_count ?? 0, + row.receipt_source ?? null, + row.validation_fallback ? 1 : 0, + row.output_mode ?? null ); } +export function attachCompressionUsageReceipt( + requestId: string | null | undefined, + usage: Record | null | undefined, + source: "provider" | "estimated" | "stream" = "provider" +): void { + if (!requestId || !usage || typeof usage !== "object") return; + const promptTokens = toFiniteInt(usage.prompt_tokens); + const completionTokens = toFiniteInt(usage.completion_tokens); + const totalTokens = + toFiniteInt(usage.total_tokens) ?? (promptTokens ?? 0) + (completionTokens ?? 0); + const promptDetails = + usage.prompt_tokens_details && typeof usage.prompt_tokens_details === "object" + ? (usage.prompt_tokens_details as Record) + : {}; + const cacheReadTokens = toFiniteInt( + usage.cache_read_input_tokens ?? usage.cached_tokens ?? promptDetails.cached_tokens + ); + const cacheWriteTokens = toFiniteInt( + usage.cache_creation_input_tokens ?? promptDetails.cache_creation_tokens + ); + if (promptTokens === null && completionTokens === null && totalTokens <= 0) return; + + const db = getDbInstance(); + ensureCompressionAnalyticsColumns(); + db.prepare( + ` + UPDATE compression_analytics + SET actual_prompt_tokens = ?, + actual_completion_tokens = ?, + actual_total_tokens = ?, + actual_cache_read_tokens = ?, + actual_cache_write_tokens = ?, + receipt_source = ? + WHERE request_id = ? + AND id = ( + SELECT id FROM compression_analytics + WHERE request_id = ? + ORDER BY id DESC + LIMIT 1 + ) + ` + ).run( + promptTokens, + completionTokens, + totalTokens, + cacheReadTokens, + cacheWriteTokens, + source, + requestId, + requestId + ); +} + +function toFiniteInt(value: unknown): number | null { + if (typeof value === "number" && Number.isFinite(value)) return Math.max(0, Math.floor(value)); + if (typeof value === "string" && value.trim()) { + const parsed = Number(value); + if (Number.isFinite(parsed)) return Math.max(0, Math.floor(parsed)); + } + return null; +} + +function appendCondition(whereClause: string, condition: string): string { + return whereClause ? `${whereClause} AND ${condition}` : `WHERE ${condition}`; +} + export function getCompressionAnalyticsSummary(since?: string): CompressionAnalyticsSummary { const db = getDbInstance(); + ensureCompressionAnalyticsColumns(); let cutoff: string | null = null; if (since === "24h") { @@ -136,6 +296,62 @@ export function getCompressionAnalyticsSummary(since?: string): CompressionAnaly const last24h = Array.from(last24hMap.values()); + const receiptRows = db + .prepare( + ` + SELECT receipt_source as source, COUNT(*) as cnt, + COALESCE(SUM(actual_prompt_tokens), 0) as prompt, + COALESCE(SUM(actual_completion_tokens), 0) as completion, + COALESCE(SUM(actual_total_tokens), 0) as total, + COALESCE(SUM(actual_cache_read_tokens), 0) as cacheRead, + COALESCE(SUM(actual_cache_write_tokens), 0) as cacheWrite, + COALESCE(SUM(estimated_usd_saved), 0) as usdSaved + FROM compression_analytics ${appendCondition(whereClause, "receipt_source IS NOT NULL")} + GROUP BY receipt_source + ` + ) + .all(...params) as Array<{ + source: string | null; + cnt: number; + prompt: number; + completion: number; + total: number; + cacheRead: number; + cacheWrite: number; + usdSaved: number; + }>; + + const realUsage = { + requestsWithReceipts: 0, + promptTokens: 0, + completionTokens: 0, + totalTokens: 0, + cacheReadTokens: 0, + cacheWriteTokens: 0, + estimatedUsdSaved: 0, + bySource: {} as Record, + }; + for (const row of receiptRows) { + const source = row.source ?? "unknown"; + realUsage.requestsWithReceipts += row.cnt; + realUsage.promptTokens += row.prompt; + realUsage.completionTokens += row.completion; + realUsage.totalTokens += row.total; + realUsage.cacheReadTokens += row.cacheRead; + realUsage.cacheWriteTokens += row.cacheWrite; + realUsage.estimatedUsdSaved += row.usdSaved; + realUsage.bySource[source] = row.cnt; + } + + const fallbackRow = db + .prepare( + ` + SELECT COUNT(*) as cnt + FROM compression_analytics ${appendCondition(whereClause, "validation_fallback = 1")} + ` + ) + .get(...params) as { cnt: number } | undefined; + return { totalRequests: scalar?.total ?? 0, totalTokensSaved: scalar?.totalSaved ?? 0, @@ -144,5 +360,7 @@ export function getCompressionAnalyticsSummary(since?: string): CompressionAnaly byMode, byProvider, last24h, + validationFallbacks: fallbackRow?.cnt ?? 0, + realUsage, }; } diff --git a/src/lib/db/migrationRunner.ts b/src/lib/db/migrationRunner.ts index 338d7aa9ec..307eb4e03b 100644 --- a/src/lib/db/migrationRunner.ts +++ b/src/lib/db/migrationRunner.ts @@ -266,6 +266,15 @@ function isSchemaAlreadyApplied( return hasColumn(db, "provider_connections", "max_concurrent"); case "040": return hasColumn(db, "proxy_registry", "source"); + case "041": + return ( + hasColumn(db, "compression_analytics", "actual_prompt_tokens") && + hasColumn(db, "compression_analytics", "actual_completion_tokens") && + hasColumn(db, "compression_analytics", "actual_total_tokens") && + hasColumn(db, "compression_analytics", "receipt_source") && + hasColumn(db, "compression_analytics", "validation_fallback") && + hasColumn(db, "compression_analytics", "output_mode") + ); default: return false; } @@ -299,6 +308,82 @@ function applySearchRequestTypeMigration(db: Database.Database): void { db.exec("CREATE INDEX IF NOT EXISTS idx_call_logs_request_type ON call_logs(request_type);"); } +function applyCompressionReceiptsMigration(db: Database.Database): void { + ensureColumn( + db, + "compression_analytics", + "actual_prompt_tokens", + "ALTER TABLE compression_analytics ADD COLUMN actual_prompt_tokens INTEGER" + ); + ensureColumn( + db, + "compression_analytics", + "actual_completion_tokens", + "ALTER TABLE compression_analytics ADD COLUMN actual_completion_tokens INTEGER" + ); + ensureColumn( + db, + "compression_analytics", + "actual_total_tokens", + "ALTER TABLE compression_analytics ADD COLUMN actual_total_tokens INTEGER" + ); + ensureColumn( + db, + "compression_analytics", + "actual_cache_read_tokens", + "ALTER TABLE compression_analytics ADD COLUMN actual_cache_read_tokens INTEGER" + ); + ensureColumn( + db, + "compression_analytics", + "actual_cache_write_tokens", + "ALTER TABLE compression_analytics ADD COLUMN actual_cache_write_tokens INTEGER" + ); + ensureColumn( + db, + "compression_analytics", + "estimated_usd_saved", + "ALTER TABLE compression_analytics ADD COLUMN estimated_usd_saved REAL" + ); + ensureColumn( + db, + "compression_analytics", + "mcp_description_tokens_saved", + "ALTER TABLE compression_analytics ADD COLUMN mcp_description_tokens_saved INTEGER DEFAULT 0" + ); + ensureColumn( + db, + "compression_analytics", + "multimodal_skip_count", + "ALTER TABLE compression_analytics ADD COLUMN multimodal_skip_count INTEGER DEFAULT 0" + ); + ensureColumn( + db, + "compression_analytics", + "receipt_source", + "ALTER TABLE compression_analytics ADD COLUMN receipt_source TEXT" + ); + ensureColumn( + db, + "compression_analytics", + "validation_fallback", + "ALTER TABLE compression_analytics ADD COLUMN validation_fallback INTEGER DEFAULT 0" + ); + ensureColumn( + db, + "compression_analytics", + "output_mode", + "ALTER TABLE compression_analytics ADD COLUMN output_mode TEXT" + ); + + db.exec(` + CREATE INDEX IF NOT EXISTS idx_compression_analytics_request_id + ON compression_analytics(request_id); + CREATE INDEX IF NOT EXISTS idx_compression_analytics_receipt_source + ON compression_analytics(receipt_source); + `); +} + function inferPhysicalSchemaBaseline(db: Database.Database): { version: string; description: string; @@ -641,6 +726,8 @@ export function runMigrations(db: Database.Database, options?: { isNewDb?: boole ); } else if (migration.version === "032") { applyApiKeyLifecycleMigration(db); + } else if (migration.version === "041") { + applyCompressionReceiptsMigration(db); } else { const sql = fs.readFileSync(migration.path, "utf-8"); db.exec(sql); diff --git a/src/lib/db/migrations/041_compression_receipts.sql b/src/lib/db/migrations/041_compression_receipts.sql new file mode 100644 index 0000000000..5e4c4a9ee3 --- /dev/null +++ b/src/lib/db/migrations/041_compression_receipts.sql @@ -0,0 +1,18 @@ +-- 041: Add real usage receipt fields to compression analytics. + +ALTER TABLE compression_analytics ADD COLUMN actual_prompt_tokens INTEGER; +ALTER TABLE compression_analytics ADD COLUMN actual_completion_tokens INTEGER; +ALTER TABLE compression_analytics ADD COLUMN actual_total_tokens INTEGER; +ALTER TABLE compression_analytics ADD COLUMN actual_cache_read_tokens INTEGER; +ALTER TABLE compression_analytics ADD COLUMN actual_cache_write_tokens INTEGER; +ALTER TABLE compression_analytics ADD COLUMN estimated_usd_saved REAL; +ALTER TABLE compression_analytics ADD COLUMN mcp_description_tokens_saved INTEGER DEFAULT 0; +ALTER TABLE compression_analytics ADD COLUMN multimodal_skip_count INTEGER DEFAULT 0; +ALTER TABLE compression_analytics ADD COLUMN receipt_source TEXT; +ALTER TABLE compression_analytics ADD COLUMN validation_fallback INTEGER DEFAULT 0; +ALTER TABLE compression_analytics ADD COLUMN output_mode TEXT; + +CREATE INDEX IF NOT EXISTS idx_compression_analytics_request_id + ON compression_analytics(request_id); +CREATE INDEX IF NOT EXISTS idx_compression_analytics_receipt_source + ON compression_analytics(receipt_source); diff --git a/tests/golden-set/compression-caveman-v2.test.ts b/tests/golden-set/compression-caveman-v2.test.ts new file mode 100644 index 0000000000..2256093162 --- /dev/null +++ b/tests/golden-set/compression-caveman-v2.test.ts @@ -0,0 +1,85 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { cavemanCompress } from "../../open-sse/services/compression/caveman.ts"; +import { estimateCompressionTokens } from "../../open-sse/services/compression/stats.ts"; + +function compressText(text: string, intensity: "lite" | "full" | "ultra" = "full"): string { + const result = cavemanCompress( + { messages: [{ role: "user", content: text }] }, + { + enabled: true, + compressRoles: ["user"], + skipRules: [], + minMessageLength: 0, + preservePatterns: [], + intensity, + } + ); + return (result.body.messages[0].content as string) ?? text; +} + +const verbosePrompts = [ + "Sure, I will make sure to provide a detailed explanation of the database connection issue because the application is currently creating a new connection for every request.", + "Of course, I would be happy to help. You should make sure to validate the request body before the handler calls the downstream API.", + "The reason is because the authentication middleware is being used before the token refresh function, and this creates a race condition.", + "I was wondering if you could explain why the response object is actually being generated twice in the implementation.", +]; + +describe("Caveman v3.7.9 golden set", () => { + it("compresses verbose model-style responses while preserving technical substance", () => { + const compressed = compressText(verbosePrompts[0]); + assert.doesNotMatch(compressed, /\bSure\b/i); + assert.doesNotMatch(compressed, /\bI will\b/i); + assert.match(compressed, /database connection issue/i); + assert.match(compressed, /new connection/i); + assert.match(compressed, /request/i); + }); + + it("preserves code blocks, URLs, paths, versions, and CONST_CASE", () => { + const input = [ + "Please make sure to review the following code.", + "```ts", + "const API_KEY_VALUE = process.env.API_KEY_VALUE;", + "```", + "See https://example.com/the/api and /src/the/file.ts for version 3.7.9.", + ].join("\n"); + const compressed = compressText(input); + assert.match(compressed, /```ts\nconst API_KEY_VALUE = process\.env\.API_KEY_VALUE;\n```/); + assert.match(compressed, /https:\/\/example\.com\/the\/api/); + assert.match(compressed, /\/src\/the\/file\.ts/); + assert.match(compressed, /3\.7\.9/); + }); + + it("preserves document structures, inline math, and multimodal text boundaries", () => { + const input = [ + "---", + "title: The Report", + "---", + "# The Heading", + "Please make sure to keep $the value$ exact.", + "| The Key | The Value |", + "| --- | --- |", + "| The row | The value |", + ].join("\n"); + const compressed = compressText(input); + assert.match(compressed, /^---\ntitle: The Report\n---/); + assert.match(compressed, /^# The Heading/m); + assert.match(compressed, /\$the value\$/); + assert.match(compressed, /^\| The Key \| The Value \|/m); + }); + + it("achieves >35% average savings on verbose Caveman prompts", () => { + const savings = verbosePrompts.map((prompt) => { + const compressed = compressText(prompt); + const before = estimateCompressionTokens(prompt); + const after = estimateCompressionTokens(compressed); + return ((before - after) / before) * 100; + }); + const avg = savings.reduce((sum, value) => sum + value, 0) / savings.length; + assert.ok(avg > 35, `Expected >35% average savings, got ${avg.toFixed(1)}%`); + }); + + it("keeps short clean messages readable", () => { + assert.equal(compressText("Fix auth bug."), "Fix auth bug."); + }); +}); diff --git a/tests/golden-set/compression-upstream-parity.test.ts b/tests/golden-set/compression-upstream-parity.test.ts new file mode 100644 index 0000000000..45b3e26209 --- /dev/null +++ b/tests/golden-set/compression-upstream-parity.test.ts @@ -0,0 +1,92 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { createRequire } from "node:module"; +import path from "node:path"; +import { cavemanCompress } from "../../open-sse/services/compression/caveman.ts"; +import { + compressDescriptionsInPlace, + getMcpDescriptionCompressionStats, + resetMcpDescriptionCompressionStats, +} from "../../open-sse/mcp-server/descriptionCompressor.ts"; + +const require = createRequire(import.meta.url); +const upstream = require( + path.resolve("_references/_outros/caveman/mcp-servers/caveman-shrink/compress.js") +) as { + compress(text: string): { compressed: string; before: number; after: number }; +}; + +function omnirouteCompress(text: string): string { + const result = cavemanCompress( + { messages: [{ role: "user", content: text }] }, + { + enabled: true, + compressRoles: ["user"], + skipRules: [], + minMessageLength: 0, + preservePatterns: [], + intensity: "full", + } + ); + return result.body.messages[0].content as string; +} + +const parityCases = [ + "The user is the owner of an account and should make sure to configure the database.", + "Sure, this just basically returns the value from config.api.endpoint().", + "I will perhaps connect to the database using API_KEY_VALUE and version 3.7.9.", + "Read just the file at /tmp/the/just/file.txt and see https://example.com/the/api.", +]; + +describe("upstream Caveman parity benchmark", () => { + it("matches core upstream shrink protections and savings direction", () => { + for (const input of parityCases) { + const ours = omnirouteCompress(input); + const theirs = upstream.compress(input).compressed; + assert.ok(ours.length <= input.length, `OmniRoute did not reduce: ${input}`); + assert.ok(theirs.length <= input.length, `Upstream did not reduce: ${input}`); + for (const protectedToken of [ + "config.api.endpoint()", + "API_KEY_VALUE", + "3.7.9", + "/tmp/the/just/file.txt", + "https://example.com/the/api", + ]) { + if (input.includes(protectedToken)) { + assert.match(ours, new RegExp(protectedToken.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"))); + } + } + } + }); + + it("stays within upstream token budget on representative prose", () => { + const input = + "Sure, I will make sure to return the current weather for a given location and the temperature in Fahrenheit."; + const ours = omnirouteCompress(input); + const theirs = upstream.compress(input).compressed; + assert.ok( + ours.length <= Math.ceil(theirs.length * 1.2), + `Expected OmniRoute within 20% of upstream shrink length. ours=${ours.length}, upstream=${theirs.length}` + ); + }); + + it("tracks MCP description shrink parity separately from prompt compression", () => { + resetMcpDescriptionCompressionStats(); + const originalDescription = + "The tool returns the current weather for a given location and the temperature in Fahrenheit."; + const payload = { + tools: [ + { + name: "get_weather", + description: originalDescription, + }, + ], + }; + + compressDescriptionsInPlace(payload); + const stats = getMcpDescriptionCompressionStats(); + assert.ok(payload.tools[0].description.length < originalDescription.length); + assert.equal(stats.descriptionsCompressed, 1); + assert.ok(stats.estimatedTokensSaved > 0); + }); +}); diff --git a/tests/integration/chatcore-compression-integration.test.ts b/tests/integration/chatcore-compression-integration.test.ts index 00cd119beb..8dc36291a9 100644 --- a/tests/integration/chatcore-compression-integration.test.ts +++ b/tests/integration/chatcore-compression-integration.test.ts @@ -624,3 +624,76 @@ test("chatCore integration: modular compression records analytics row best-effor globalThis.fetch = originalFetch; } }); + +test("chatCore integration: caveman output mode records analytics and receipts without prompt compression", async () => { + const provider = "openai"; + const model = "gpt-4"; + + await compressionDb.updateCompressionSettings({ + enabled: false, + defaultMode: "off", + autoTriggerTokens: 0, + cavemanOutputMode: { + enabled: true, + intensity: "full", + autoClarity: true, + }, + }); + + const connection = await providersDb.createProviderConnection({ + provider, + apiKey: "test-key", + isActive: true, + }); + + let capturedBody: any = null; + globalThis.fetch = async (_url: string | URL | Request, init?: RequestInit) => { + if (init?.body) { + capturedBody = JSON.parse(init.body as string); + } + return new Response( + JSON.stringify({ + choices: [{ message: { role: "assistant", content: "ok" } }], + usage: { prompt_tokens: 20, completion_tokens: 4, total_tokens: 24 }, + }), + { + status: 200, + headers: { "content-type": "application/json" }, + } + ); + }; + + try { + const result = await handleChatCore({ + body: { + model, + stream: false, + messages: [{ role: "user", content: "Summarize this implementation." }], + }, + modelInfo: { provider, model }, + credentials: { apiKey: "test-key" }, + log: { debug: () => {}, info: () => {}, warn: () => {}, error: () => {} }, + clientRawRequest: { endpoint: "/v1/chat/completions", headers: new Map() }, + connectionId: connection.id, + }); + + assert.ok(result.success, "Request should succeed"); + assert.match(capturedBody.messages[0].content, /Caveman Output Mode/); + + let summary = compressionAnalyticsDb.getCompressionAnalyticsSummary(); + for ( + let attempt = 0; + attempt < 20 && (summary.totalRequests === 0 || summary.realUsage.requestsWithReceipts === 0); + attempt += 1 + ) { + await new Promise((resolve) => setTimeout(resolve, 10)); + summary = compressionAnalyticsDb.getCompressionAnalyticsSummary(); + } + + assert.equal(summary.byMode["output-caveman"].count, 1); + assert.equal(summary.realUsage.requestsWithReceipts, 1); + assert.equal(summary.realUsage.totalTokens, 24); + } finally { + globalThis.fetch = originalFetch; + } +}); diff --git a/tests/unit/compression/caveman-v379.test.ts b/tests/unit/compression/caveman-v379.test.ts new file mode 100644 index 0000000000..a5f04c3ceb --- /dev/null +++ b/tests/unit/compression/caveman-v379.test.ts @@ -0,0 +1,145 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { cavemanCompress } from "../../../open-sse/services/compression/caveman.ts"; +import { getRuleByName } from "../../../open-sse/services/compression/cavemanRules.ts"; +import { applyCompression } from "../../../open-sse/services/compression/strategySelector.ts"; + +function compress(content: string, options = {}) { + const result = cavemanCompress( + { messages: [{ role: "user", content }] }, + { + enabled: true, + compressRoles: ["user"], + skipRules: [], + minMessageLength: 0, + preservePatterns: [], + intensity: "full", + ...options, + } + ); + return result.body.messages[0].content as string; +} + +describe("Caveman v3.7.9 rule parity", () => { + it("removes articles, pleasantries, leader phrases, and redundant phrasing", () => { + const text = compress( + "Sure, I will make sure to explain the reason is because the function uses a database." + ); + assert.doesNotMatch(text, /\bSure\b/i); + assert.doesNotMatch(text, /^I will\b/i); + assert.doesNotMatch(text, /\bmake sure to\b/i); + assert.doesNotMatch(text, /\bthe reason is because\b/i); + assert.doesNotMatch(text, /\bthe function\b/i); + assert.match(text, /\bensure\b/i); + assert.match(text, /\bbecause\b/i); + assert.match(text, /\bdatabase\b/i); + }); + + it("recapitalizes sentence starts after removals", () => { + const text = compress("Sure, the database connection fails. of course the fix is simple."); + assert.match(text, /^Database/); + assert.match(text, /\. Fix/); + }); + + it("supports intensity sub-levels for standard mode", () => { + const lite = compress("The database request uses the response object.", { intensity: "lite" }); + const full = compress("The database request uses the response object.", { intensity: "full" }); + const ultra = compress("The database request uses the response object.", { + intensity: "ultra", + }); + + assert.match(lite, /\bThe\b/); + assert.doesNotMatch(full, /\bThe\b/); + assert.match(ultra, /\bDB\b/); + assert.match(ultra, /\breq\b/); + assert.match(ultra, /\bres\b/); + }); + + it("keeps system prompts unchanged when preserveSystemPrompt is enabled", () => { + const body = { + messages: [ + { + role: "system", + content: "The assistant should make sure to preserve the detailed policy.", + }, + { + role: "user", + content: "Please make sure to summarize the database response.", + }, + ], + }; + const result = applyCompression(body, "standard", { + config: { + enabled: true, + defaultMode: "standard", + autoTriggerMode: "standard", + autoTriggerTokens: 0, + cacheMinutes: 5, + preserveSystemPrompt: true, + comboOverrides: {}, + cavemanConfig: { + enabled: true, + compressRoles: ["system", "user"], + skipRules: [], + minMessageLength: 0, + preservePatterns: [], + intensity: "full", + }, + }, + }); + + const messages = result.body.messages as Array<{ role: string; content: string }>; + assert.equal(messages[0].content, body.messages[0].content); + assert.doesNotMatch(messages[1].content, /\bPlease\b/i); + }); + + it("preserves multimodal non-text parts in aggressive and ultra modes", () => { + const body = { + messages: [ + { + role: "user", + content: [ + { type: "text", text: "Please make sure to explain the database response.".repeat(20) }, + { type: "image_url", image_url: { url: "data:image/png;base64,abc" } }, + ], + }, + ], + }; + + for (const mode of ["aggressive", "ultra"] as const) { + const result = applyCompression(body, mode, { + config: { + enabled: true, + defaultMode: mode, + autoTriggerTokens: 0, + cacheMinutes: 5, + preserveSystemPrompt: true, + comboOverrides: {}, + aggressive: undefined, + ultra: { + enabled: true, + compressionRate: 0.5, + minScoreThreshold: 0.3, + slmFallbackToAggressive: true, + maxTokensPerMessage: 0, + }, + }, + }); + const content = (result.body.messages as typeof body.messages)[0].content; + assert.ok(Array.isArray(content)); + assert.deepEqual(content[1], body.messages[0].content[1]); + } + }); + + it("exposes metadata for new rule names", () => { + for (const ruleName of [ + "articles", + "pleasantries", + "leader_phrases", + "redundant_phrasing", + "ultra_abbreviations", + ]) { + assert.ok(getRuleByName(ruleName), `missing rule ${ruleName}`); + } + }); +}); diff --git a/tests/unit/compression/compressionAnalytics.test.ts b/tests/unit/compression/compressionAnalytics.test.ts index 7bfb290c01..0490ad8326 100644 --- a/tests/unit/compression/compressionAnalytics.test.ts +++ b/tests/unit/compression/compressionAnalytics.test.ts @@ -11,6 +11,8 @@ const core = await import("../../../src/lib/db/core.ts"); core.resetDbInstance(); const { insertCompressionAnalyticsRow, getCompressionAnalyticsSummary } = await import("../../../src/lib/db/compressionAnalytics.ts"); +const { attachCompressionUsageReceipt } = + await import("../../../src/lib/db/compressionAnalytics.ts"); const { getDbInstance } = core; describe("compressionAnalytics", () => { @@ -52,8 +54,20 @@ describe("compressionAnalytics", () => { avgDurationMs: 0, byMode: {}, byProvider: {}, - last24h: [], + last24h: summary.last24h, + validationFallbacks: 0, + realUsage: { + requestsWithReceipts: 0, + promptTokens: 0, + completionTokens: 0, + totalTokens: 0, + cacheReadTokens: 0, + cacheWriteTokens: 0, + estimatedUsdSaved: 0, + bySource: {}, + }, }); + assert.equal(summary.last24h.length, 24); }); it("insert single row does not throw", () => { @@ -249,4 +263,57 @@ describe("compressionAnalytics", () => { assert(typeof bucket.tokensSaved === "number"); }); }); + + it("attaches real usage receipts to the latest compression row", () => { + insertCompressionAnalyticsRow({ + timestamp: new Date().toISOString(), + mode: "standard", + original_tokens: 1000, + compressed_tokens: 700, + tokens_saved: 300, + request_id: "req-receipt", + }); + attachCompressionUsageReceipt( + "req-receipt", + { + prompt_tokens: 710, + completion_tokens: 42, + total_tokens: 752, + prompt_tokens_details: { cached_tokens: 100, cache_creation_tokens: 12 }, + }, + "provider" + ); + const summary = getCompressionAnalyticsSummary(); + assert.equal(summary.realUsage.requestsWithReceipts, 1); + assert.equal(summary.realUsage.promptTokens, 710); + assert.equal(summary.realUsage.completionTokens, 42); + assert.equal(summary.realUsage.totalTokens, 752); + assert.equal(summary.realUsage.cacheReadTokens, 100); + assert.equal(summary.realUsage.cacheWriteTokens, 12); + assert.equal(summary.realUsage.bySource.provider, 1); + }); + + it("summarizes validation fallback and output mode rows", () => { + insertCompressionAnalyticsRow({ + timestamp: new Date().toISOString(), + mode: "output-caveman", + original_tokens: 900, + compressed_tokens: 900, + tokens_saved: 0, + request_id: "req-output", + validation_fallback: true, + output_mode: "full", + }); + attachCompressionUsageReceipt( + "req-output", + { prompt_tokens: 900, completion_tokens: 120, total_tokens: 1020 }, + "provider" + ); + + const summary = getCompressionAnalyticsSummary(); + assert.equal(summary.validationFallbacks, 1); + assert.equal(summary.byMode["output-caveman"].count, 1); + assert.equal(summary.realUsage.requestsWithReceipts, 1); + assert.equal(summary.realUsage.totalTokens, 1020); + }); }); diff --git a/tests/unit/compression/diffHelper.test.ts b/tests/unit/compression/diffHelper.test.ts new file mode 100644 index 0000000000..cbb424d838 --- /dev/null +++ b/tests/unit/compression/diffHelper.test.ts @@ -0,0 +1,36 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { buildCompressionPreviewDiff } from "../../../open-sse/services/compression/diffHelper.ts"; + +describe("compression preview diff", () => { + it("reports diff segments, preserved blocks, rules, validation warnings, and fallback status", () => { + const original = "Please use `exact_token` and the API."; + const compressed = "Use `exact_token` and API."; + const preview = buildCompressionPreviewDiff(original, compressed, { + originalTokens: 10, + compressedTokens: 6, + savingsPercent: 40, + techniquesUsed: ["caveman-rules"], + mode: "standard", + timestamp: Date.now(), + rulesApplied: ["polite_framing", "articles"], + validationWarnings: ["sample warning"], + }); + + assert.ok(preview.segments.some((segment) => segment.type === "removed")); + assert.ok(preview.preservedBlocks.some((block) => block.kind === "inline_code")); + assert.deepEqual(preview.ruleRemovals, ["polite_framing", "articles"]); + assert.match(preview.validationWarnings.join("\n"), /sample warning/); + assert.equal(preview.fallbackApplied, false); + }); + + it("reports validation errors for protected content loss", () => { + const preview = buildCompressionPreviewDiff( + "Use `exact_token` and https://example.com.", + "Use token.", + null + ); + assert.equal(preview.fallbackApplied, true); + assert.ok(preview.validationErrors.length >= 2); + }); +}); diff --git a/tests/unit/compression/lite.test.ts b/tests/unit/compression/lite.test.ts index c3119f2c5e..6070f7ea2d 100644 --- a/tests/unit/compression/lite.test.ts +++ b/tests/unit/compression/lite.test.ts @@ -8,6 +8,7 @@ import { removeRedundantContent, replaceImageUrls, } from "../../../open-sse/services/compression/lite.ts"; +import { applyCompression } from "../../../open-sse/services/compression/strategySelector.ts"; describe("collapseWhitespace", () => { it("collapses 3+ newlines to 2", () => { @@ -192,6 +193,28 @@ describe("applyLiteCompression", () => { assert.ok(result.stats.savingsPercent > 0); }); + it("preserves system prompt text when preserveSystemPrompt is enabled", () => { + const body = { + messages: [ + { role: "system", content: "Policy. Keep exact.\n\n\nDo not change." }, + { role: "user", content: "Please normalize this.\n\n\nThanks." }, + ], + }; + const result = applyCompression(body, "lite", { + config: { + enabled: true, + defaultMode: "lite", + autoTriggerTokens: 0, + cacheMinutes: 5, + preserveSystemPrompt: true, + comboOverrides: {}, + }, + }); + const messages = result.body.messages as typeof body.messages; + assert.equal(messages[0].content, body.messages[0].content); + assert.notEqual(messages[1].content, body.messages[1].content); + }); + it("returns no compression for clean input", () => { const body = { messages: [{ role: "user", content: "clean message" }], diff --git a/tests/unit/compression/mcp-description-compressor.test.ts b/tests/unit/compression/mcp-description-compressor.test.ts new file mode 100644 index 0000000000..f9915b6748 --- /dev/null +++ b/tests/unit/compression/mcp-description-compressor.test.ts @@ -0,0 +1,85 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { + compressDescriptionsInPlace, + compressMcpDescription, + getMcpDescriptionCompressionStats, + maybeCompressMcpDescription, + resetMcpDescriptionCompressionStats, +} from "../../../open-sse/mcp-server/descriptionCompressor.ts"; + +describe("MCP description compression", () => { + it("compresses MCP-style descriptions while preserving substance", () => { + const input = + "Get the current weather for a given location. Returns the temperature in Fahrenheit. Please make sure to provide the location as a city name."; + const result = compressMcpDescription(input); + assert.ok(result.after < result.before); + assert.match(result.compressed, /weather/i); + assert.match(result.compressed, /Fahrenheit/i); + assert.match(result.compressed, /city name/i); + assert.doesNotMatch(result.compressed, /\bthe\b/i); + }); + + it("preserves code, URLs, paths, identifiers, and versions", () => { + const input = + "Call config.api.endpoint() with API_KEY_VALUE from /tmp/the/file.txt. See https://example.com/the/api for version 3.7.9."; + const result = compressMcpDescription(input).compressed; + assert.match(result, /config\.api\.endpoint\(\)/); + assert.match(result, /API_KEY_VALUE/); + assert.match(result, /\/tmp\/the\/file\.txt/); + assert.match(result, /https:\/\/example\.com\/the\/api/); + assert.match(result, /3\.7\.9/); + }); + + it("walks nested list metadata and skips non-string fields", () => { + const payload = { + result: { + tools: [ + { name: "get_weather", description: "The function returns the weather for a city." }, + { name: "nested", inputSchema: { description: { not: "string" } } }, + ], + }, + }; + compressDescriptionsInPlace(payload.result); + assert.doesNotMatch(payload.result.tools[0].description, /\bthe\b/i); + assert.deepEqual(payload.result.tools[1].inputSchema.description, { not: "string" }); + }); + + it("honors settings and environment kill switches", () => { + const originalEnv = process.env.OMNIROUTE_MCP_DESCRIPTION_COMPRESSION; + const originalAliasEnv = process.env.OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS; + try { + delete process.env.OMNIROUTE_MCP_DESCRIPTION_COMPRESSION; + delete process.env.OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS; + const input = "The function returns the weather for a city."; + assert.equal(maybeCompressMcpDescription(input, { enabled: false }), input); + + process.env.OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS = "off"; + assert.equal(maybeCompressMcpDescription(input, { enabled: true }), input); + } finally { + if (originalEnv === undefined) { + delete process.env.OMNIROUTE_MCP_DESCRIPTION_COMPRESSION; + } else { + process.env.OMNIROUTE_MCP_DESCRIPTION_COMPRESSION = originalEnv; + } + if (originalAliasEnv === undefined) { + delete process.env.OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS; + } else { + process.env.OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS = originalAliasEnv; + } + } + }); + + it("records MCP description savings separately", () => { + resetMcpDescriptionCompressionStats(); + const input = + "The function returns the current weather for a city and the detailed forecast summary."; + const output = maybeCompressMcpDescription(input, { enabled: true }); + const stats = getMcpDescriptionCompressionStats(); + + assert.notEqual(output, input); + assert.equal(stats.descriptionsCompressed, 1); + assert.ok(stats.charsSaved > 0); + assert.ok(stats.estimatedTokensSaved > 0); + }); +}); diff --git a/tests/unit/compression/outputMode.test.ts b/tests/unit/compression/outputMode.test.ts new file mode 100644 index 0000000000..33de29d05e --- /dev/null +++ b/tests/unit/compression/outputMode.test.ts @@ -0,0 +1,89 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { + applyCavemanOutputMode, + buildCavemanOutputInstruction, + shouldBypassCavemanOutputMode, +} from "../../../open-sse/services/compression/outputMode.ts"; + +describe("Caveman output mode", () => { + it("injects a system instruction without post-processing output", () => { + const result = applyCavemanOutputMode( + { messages: [{ role: "user", content: "Summarize this API response." }] }, + { enabled: true, intensity: "full", autoClarity: true } + ); + assert.equal(result.applied, true); + assert.equal(result.body.messages?.[0]?.role, "system"); + assert.match(String(result.body.messages?.[0]?.content), /Caveman Output Mode/); + }); + + it("appends to an existing system prompt", () => { + const result = applyCavemanOutputMode( + { + messages: [ + { role: "system", content: "Follow tenant policy." }, + { role: "user", content: "Summarize logs." }, + ], + }, + { enabled: true, intensity: "lite", autoClarity: true } + ); + assert.equal(result.applied, true); + assert.match(String(result.body.messages?.[0]?.content), /Follow tenant policy/); + assert.match(String(result.body.messages?.[0]?.content), /Drop filler/); + }); + + it("does not inject the Caveman instruction twice", () => { + const body = { + messages: [ + { role: "system", content: "Follow tenant policy." }, + { role: "user", content: "Summarize logs." }, + ], + }; + const once = applyCavemanOutputMode(body, { + enabled: true, + intensity: "full", + autoClarity: true, + }).body; + const twice = applyCavemanOutputMode(once, { + enabled: true, + intensity: "full", + autoClarity: true, + }); + + assert.equal(twice.applied, false); + assert.equal(twice.skippedReason, "already_applied"); + const markerCount = String(twice.body.messages?.[0]?.content).match( + /OmniRoute Caveman Output Mode/g + )?.length; + assert.equal(markerCount, 1); + }); + + it("does not modify user content", () => { + const body = { messages: [{ role: "user", content: "Please explain this response." }] }; + const result = applyCavemanOutputMode(body, { + enabled: true, + intensity: "full", + autoClarity: true, + }); + assert.equal(result.body.messages?.at(-1)?.content, body.messages[0].content); + }); + + it("bypasses security, destructive, clarification, and order-sensitive prompts", () => { + const cases = [ + "Explain this security vulnerability in detail.", + "Delete all rows after backup confirmation.", + "Can you clarify what this means?", + "First backup then drop table during migration.", + ]; + for (const content of cases) { + assert.ok(shouldBypassCavemanOutputMode([{ role: "user", content }]), content); + } + }); + + it("builds intensity-specific instructions", () => { + assert.match( + buildCavemanOutputInstruction({ enabled: true, intensity: "ultra", autoClarity: true }), + /ultra terse/i + ); + }); +}); diff --git a/tests/unit/compression/validation.test.ts b/tests/unit/compression/validation.test.ts new file mode 100644 index 0000000000..f20d8862e2 --- /dev/null +++ b/tests/unit/compression/validation.test.ts @@ -0,0 +1,136 @@ +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { validateCompression } from "../../../open-sse/services/compression/validation.ts"; +import { + extractPreservedBlocks, + restorePreservedBlocks, +} from "../../../open-sse/services/compression/preservation.ts"; +import { cavemanCompress } from "../../../open-sse/services/compression/caveman.ts"; + +describe("compression preservation and validation", () => { + it("parses backtick and tilde fences with nested shorter fences", () => { + const input = [ + "Before", + "````markdown", + "```js", + "const theValue = 1;", + "```", + "````", + "~~~ts", + "const x = `the`;", + "~~~", + "After", + ].join("\n"); + const { text, blocks } = extractPreservedBlocks(input); + assert.equal(blocks.filter((block) => block.kind === "fenced_code").length, 2); + assert.equal(restorePreservedBlocks(text, blocks), input); + }); + + it("uses collision-resistant sentinels instead of predictable placeholders", () => { + const input = "User literal [PRESERVED_0] and `inline code` stay separate."; + const { text, blocks } = extractPreservedBlocks(input); + assert.ok(blocks.length > 0); + assert.doesNotMatch(text, /\[PRESERVED_0\]/); + assert.equal(restorePreservedBlocks(text, blocks), input); + }); + + it("preserves caveman-shrink protected identifiers and versions", () => { + const input = + "Set API_KEY_VALUE in process.env.API_KEY_VALUE before calling config.api.endpoint() on 3.7.9."; + const output = cavemanCompress( + { messages: [{ role: "user", content: input }] }, + { + enabled: true, + compressRoles: ["user"], + skipRules: [], + minMessageLength: 0, + preservePatterns: [], + intensity: "full", + } + ).body.messages[0].content as string; + + assert.match(output, /API_KEY_VALUE/); + assert.match(output, /process\.env\.API_KEY_VALUE/); + assert.match(output, /config\.api\.endpoint\(\)/); + assert.match(output, /3\.7\.9/); + }); + + it("preserves Typst, LaTeX, frontmatter, headings, and markdown tables", () => { + const input = [ + "---", + "title: The Test", + "---", + "# The Heading", + "| Column | Value |", + "| --- | --- |", + "| The key | The value |", + "$$", + "E = mc^2", + "$$", + "\\begin{align}a &= b\\end{align}", + "#set text(size: 12pt)", + "Please make sure to explain the document.", + ].join("\n"); + const output = cavemanCompress( + { messages: [{ role: "user", content: input }] }, + { + enabled: true, + compressRoles: ["user"], + skipRules: [], + minMessageLength: 0, + preservePatterns: [], + intensity: "full", + } + ).body.messages[0].content as string; + + assert.match(output, /^---\ntitle: The Test\n---/); + assert.match(output, /^# The Heading/m); + assert.match(output, /^\| Column \| Value \|/m); + assert.match(output, /\$\$\nE = mc\^2\n\$\$/); + assert.match(output, /\\begin\{align\}a &= b\\end\{align\}/); + assert.match(output, /^#set text\(size: 12pt\)/m); + }); + + it("preserves inline math and ignores normal dollar amounts", () => { + const input = "Please make sure to keep $the value$ exact and the cost as $10."; + const output = cavemanCompress( + { messages: [{ role: "user", content: input }] }, + { + enabled: true, + compressRoles: ["user"], + skipRules: [], + minMessageLength: 0, + preservePatterns: [], + intensity: "full", + } + ).body.messages[0].content as string; + + assert.match(output, /\$the value\$/); + assert.match(output, /\$10/); + }); + + it("reports validation failure when protected content is lost", () => { + const original = "Use `exact_token` and https://example.com/the/path."; + const validation = validateCompression(original, "Use token."); + assert.equal(validation.valid, false); + assert.equal(validation.fallbackApplied, true); + assert.ok(validation.errors.length >= 2); + }); + + it("falls back to original when validation detects structural loss", () => { + const original = "Please make sure to use `the exact token` in the request."; + const result = cavemanCompress( + { messages: [{ role: "user", content: original }] }, + { + enabled: true, + compressRoles: ["user"], + skipRules: [], + minMessageLength: 0, + preservePatterns: ["`[^`]+"], + intensity: "full", + } + ); + const output = result.body.messages[0].content as string; + assert.match(output, /`the exact token`/); + }); +}); From aa40bc34f584b7369dd098f823b2439a87b5f38f Mon Sep 17 00:00:00 2001 From: diegosouzapw Date: Sat, 2 May 2026 12:57:37 -0300 Subject: [PATCH 07/87] 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. --- open-sse/handlers/chatCore.ts | 18 +++++++++++++---- open-sse/services/compression/aggressive.ts | 5 ++++- open-sse/services/compression/cavemanRules.ts | 4 ++++ .../components/CompressionSettingsTab.tsx | 3 ++- .../api/settings/compression/rules/route.ts | 1 + tests/unit/compression/caveman-v379.test.ts | 9 ++++++++- .../compression-aggressive.test.ts | 20 +++++++++++++++++++ .../compression/compressionAnalytics.test.ts | 17 ++++++++++++++++ 8 files changed, 70 insertions(+), 7 deletions(-) create mode 100644 src/app/api/settings/compression/rules/route.ts diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 2f1773bcb4..9759e1e967 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -1689,6 +1689,18 @@ export async function handleChatCore({ 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 + ); + const estimatedUsdSaved = await calculateCost( + provider ?? "", + effectiveModel ?? "", + { + input: tokensSaved, + } + ); insertCompressionAnalyticsRow({ timestamp: new Date().toISOString(), combo_id: comboName ?? null, @@ -1696,12 +1708,10 @@ export async function handleChatCore({ mode, original_tokens: result.stats.originalTokens, compressed_tokens: result.stats.compressedTokens, - tokens_saved: Math.max( - 0, - result.stats.originalTokens - 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, }); diff --git a/open-sse/services/compression/aggressive.ts b/open-sse/services/compression/aggressive.ts index 83c1acb86c..a1edf3536a 100644 --- a/open-sse/services/compression/aggressive.ts +++ b/open-sse/services/compression/aggressive.ts @@ -144,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) { diff --git a/open-sse/services/compression/cavemanRules.ts b/open-sse/services/compression/cavemanRules.ts index 7050372ef3..906f64fc28 100644 --- a/open-sse/services/compression/cavemanRules.ts +++ b/open-sse/services/compression/cavemanRules.ts @@ -406,11 +406,15 @@ export function getRuleByName(name: string): CavemanRule | undefined { } 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, " "), })); } diff --git a/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx b/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx index c072e37acf..7d363e817f 100644 --- a/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx +++ b/src/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab.tsx @@ -70,6 +70,7 @@ interface RuleMetadata { category: string; context: string; minIntensity: CavemanIntensity; + intensities?: CavemanIntensity[]; description: string; } @@ -498,7 +499,7 @@ export default function CompressionSettingsTab() {
diff --git a/src/app/(dashboard)/dashboard/compression/page.tsx b/src/app/(dashboard)/dashboard/compression/page.tsx index 7f4c6363b6..a347872010 100644 --- a/src/app/(dashboard)/dashboard/compression/page.tsx +++ b/src/app/(dashboard)/dashboard/compression/page.tsx @@ -1,20 +1,5 @@ -"use client"; - -import CompressionSettingsTab from "@/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab"; +import { redirect } from "next/navigation"; export default function CompressionPage() { - return ( -
-
-

- compress - Compression -

-

- Configure context compression settings to reduce token usage and costs. -

-
- -
- ); + redirect("/dashboard/context/caveman"); } diff --git a/src/app/(dashboard)/dashboard/context/caveman/CavemanContextPageClient.tsx b/src/app/(dashboard)/dashboard/context/caveman/CavemanContextPageClient.tsx new file mode 100644 index 0000000000..cfac49fa4c --- /dev/null +++ b/src/app/(dashboard)/dashboard/context/caveman/CavemanContextPageClient.tsx @@ -0,0 +1,262 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { useTranslations } from "next-intl"; +import CompressionSettingsTab from "@/app/(dashboard)/dashboard/settings/components/CompressionSettingsTab"; + +type AnalyticsSummary = { + totalRequests: number; + totalTokensSaved: number; + avgSavingsPct: number; + avgDurationMs: number; + byEngine?: Record; + byMode?: Record; + last24h?: Array<{ hour: string; count: number; tokensSaved: number }>; +}; + +type LanguageConfig = { + enabled: boolean; + defaultLanguage: string; + autoDetect: boolean; + enabledPacks: string[]; +}; + +type OutputModeConfig = { + enabled: boolean; + intensity: "lite" | "full" | "ultra"; + autoClarity: boolean; +}; + +type CompressionSettings = { + languageConfig?: LanguageConfig; + cavemanOutputMode?: OutputModeConfig; +}; + +type LanguagePack = { language: string; ruleCount: number; categories?: string[] }; + +function formatNumber(value: number | undefined): string { + return new Intl.NumberFormat().format(value ?? 0); +} + +export default function CavemanContextPageClient() { + const t = useTranslations("contextCaveman"); + const [analytics, setAnalytics] = useState(null); + const [settings, setSettings] = useState(null); + const [languagePacks, setLanguagePacks] = useState([]); + const [saving, setSaving] = useState(false); + + const refreshSettings = () => { + fetch("/api/context/caveman/config") + .then((res) => (res.ok ? res.json() : null)) + .then((data) => setSettings(data)) + .catch(() => setSettings(null)); + }; + + useEffect(() => { + fetch("/api/context/analytics?since=7d") + .then((res) => (res.ok ? res.json() : null)) + .then((data) => setAnalytics(data)) + .catch(() => setAnalytics(null)); + fetch("/api/compression/language-packs") + .then((res) => (res.ok ? res.json() : null)) + .then((data) => setLanguagePacks(Array.isArray(data?.packs) ? data.packs : [])) + .catch(() => setLanguagePacks([])); + refreshSettings(); + }, []); + + const languageConfig: LanguageConfig = settings?.languageConfig ?? { + enabled: false, + defaultLanguage: "en", + autoDetect: true, + enabledPacks: ["en"], + }; + const outputMode: OutputModeConfig = settings?.cavemanOutputMode ?? { + enabled: false, + intensity: "full", + autoClarity: true, + }; + + const saveSettings = async (patch: Partial) => { + setSaving(true); + try { + const res = await fetch("/api/context/caveman/config", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(patch), + }); + if (res.ok) setSettings(await res.json()); + } finally { + setSaving(false); + } + }; + + const updateLanguageConfig = (patch: Partial) => { + saveSettings({ languageConfig: { ...languageConfig, ...patch } }); + }; + + const updateOutputMode = (patch: Partial) => { + saveSettings({ cavemanOutputMode: { ...outputMode, ...patch } }); + }; + + const togglePack = (language: string, enabled: boolean) => { + const enabledPacks = enabled + ? [...new Set([...languageConfig.enabledPacks, language])] + : languageConfig.enabledPacks.filter((pack) => pack !== language && pack !== "en"); + updateLanguageConfig({ enabledPacks }); + }; + + const cavemanStats = analytics?.byEngine?.caveman ?? analytics?.byEngine?.standard; + const modeBreakdown = useMemo(() => Object.entries(analytics?.byMode ?? {}), [analytics]); + const statCards = [ + [t("requests"), formatNumber(cavemanStats?.count ?? analytics?.totalRequests)], + [t("tokensSaved"), formatNumber(cavemanStats?.tokensSaved ?? analytics?.totalTokensSaved)], + [t("savingsPercent"), `${cavemanStats?.avgSavingsPct ?? analytics?.avgSavingsPct ?? 0}%`], + [t("avgLatency"), `${analytics?.avgDurationMs ?? 0}ms`], + ]; + const previewPrompt = `[OmniRoute Caveman Output Mode]\n${t(`preview.${outputMode.intensity}`)}`; + + return ( +
+
+
+ compress +
+

{t("title")}

+

{t("description")}

+
+
+
+ +
+ {statCards.map(([label, value]) => ( +
+

{label}

+

{value}

+
+ ))} +
+ +
+
+

{t("languagePacks")}

+

{t("languagePacksDesc")}

+
+
+ + + +
+
+ {languagePacks.map((pack) => ( + + ))} +
+
+ +
+
+

{t("analyticsTitle")}

+
+ {modeBreakdown.length === 0 ? ( +

{t("noAnalytics")}

+ ) : ( + modeBreakdown.map(([mode, stats]) => ( +
+ {mode} + + {formatNumber(stats.tokensSaved)} / {stats.avgSavingsPct}% + +
+ )) + )} +
+
+ +
+

{t("outputModeTitle")}

+

{t("outputModeDesc")}

+
+ + + +
+
+            {previewPrompt}
+          
+

+ {t("bypassConditions")}: security, irreversible, clarification, order-sensitive +

+
+
+ + +
+ ); +} diff --git a/src/app/(dashboard)/dashboard/context/caveman/page.tsx b/src/app/(dashboard)/dashboard/context/caveman/page.tsx new file mode 100644 index 0000000000..bfeae4c1cb --- /dev/null +++ b/src/app/(dashboard)/dashboard/context/caveman/page.tsx @@ -0,0 +1,5 @@ +import CavemanContextPageClient from "./CavemanContextPageClient"; + +export default function CavemanContextPage() { + return ; +} diff --git a/src/app/(dashboard)/dashboard/context/combos/CompressionCombosPageClient.tsx b/src/app/(dashboard)/dashboard/context/combos/CompressionCombosPageClient.tsx new file mode 100644 index 0000000000..58149a9aa6 --- /dev/null +++ b/src/app/(dashboard)/dashboard/context/combos/CompressionCombosPageClient.tsx @@ -0,0 +1,391 @@ +"use client"; + +import { useEffect, useState } from "react"; +import { useTranslations } from "next-intl"; + +type PipelineStep = { engine: string; intensity?: string }; +type CompressionCombo = { + id: string; + name: string; + description: string; + pipeline: PipelineStep[]; + languagePacks: string[]; + outputMode: boolean; + outputModeIntensity: string; + isDefault: boolean; +}; +type RoutingCombo = { id?: string; name?: string }; +type LanguagePack = { language: string; ruleCount: number }; + +const EMPTY_PIPELINE: PipelineStep[] = [ + { engine: "rtk", intensity: "standard" }, + { engine: "caveman", intensity: "full" }, +]; + +const ENGINE_INTENSITIES: Record = { + rtk: ["minimal", "standard", "aggressive"], + caveman: ["lite", "full", "ultra"], + lite: ["lite"], + aggressive: ["standard"], + ultra: ["ultra"], +}; + +export default function CompressionCombosPageClient() { + const t = useTranslations("contextCombos"); + const [combos, setCombos] = useState([]); + const [routingCombos, setRoutingCombos] = useState([]); + const [languagePacks, setLanguagePacks] = useState([]); + const [editingId, setEditingId] = useState(null); + const [name, setName] = useState(""); + const [description, setDescription] = useState(""); + const [pipeline, setPipeline] = useState(EMPTY_PIPELINE); + const [selectedPacks, setSelectedPacks] = useState(["en"]); + const [outputMode, setOutputMode] = useState(false); + const [outputModeIntensity, setOutputModeIntensity] = useState("full"); + const [assignmentIds, setAssignmentIds] = useState([]); + const [saving, setSaving] = useState(false); + + const refresh = () => { + fetch("/api/context/combos") + .then((res) => (res.ok ? res.json() : null)) + .then((data) => setCombos(Array.isArray(data?.combos) ? data.combos : [])) + .catch(() => {}); + }; + + useEffect(() => { + refresh(); + fetch("/api/combos") + .then((res) => (res.ok ? res.json() : null)) + .then((data) => setRoutingCombos(Array.isArray(data?.combos) ? data.combos : [])) + .catch(() => {}); + fetch("/api/compression/language-packs") + .then((res) => (res.ok ? res.json() : null)) + .then((data) => setLanguagePacks(Array.isArray(data?.packs) ? data.packs : [])) + .catch(() => {}); + }, []); + + const resetForm = () => { + setEditingId(null); + setName(""); + setDescription(""); + setPipeline(EMPTY_PIPELINE); + setSelectedPacks(["en"]); + setOutputMode(false); + setOutputModeIntensity("full"); + setAssignmentIds([]); + }; + + const loadAssignments = async (id: string) => { + const res = await fetch(`/api/context/combos/${id}/assignments`); + if (!res.ok) return []; + const data = await res.json(); + return Array.isArray(data?.assignments) + ? data.assignments.map((item: { routingComboId: string }) => item.routingComboId) + : []; + }; + + const editCombo = async (combo: CompressionCombo) => { + setEditingId(combo.id); + setName(combo.name); + setDescription(combo.description ?? ""); + setPipeline(combo.pipeline.length > 0 ? combo.pipeline : EMPTY_PIPELINE); + setSelectedPacks(combo.languagePacks?.length ? combo.languagePacks : ["en"]); + setOutputMode(Boolean(combo.outputMode)); + setOutputModeIntensity(combo.outputModeIntensity ?? "full"); + setAssignmentIds(await loadAssignments(combo.id)); + }; + + const saveCombo = async () => { + const trimmed = name.trim(); + if (!trimmed) return; + setSaving(true); + try { + const payload = { + name: trimmed, + description, + pipeline, + languagePacks: selectedPacks, + outputMode, + outputModeIntensity, + }; + const res = await fetch( + editingId ? `/api/context/combos/${editingId}` : "/api/context/combos", + { + method: editingId ? "PUT" : "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(payload), + } + ); + if (!res.ok) return; + const combo = await res.json(); + await fetch(`/api/context/combos/${combo.id}/assignments`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ routingComboIds: assignmentIds }), + }); + resetForm(); + refresh(); + } finally { + setSaving(false); + } + }; + + const deleteCombo = async (combo: CompressionCombo) => { + if (!confirm(t("deleteConfirm"))) return; + const res = await fetch(`/api/context/combos/${combo.id}`, { method: "DELETE" }); + if (res.ok) refresh(); + }; + + const setDefault = async (id: string) => { + const res = await fetch(`/api/context/combos/${id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ isDefault: true }), + }); + if (res.ok) refresh(); + }; + + const updateStep = (index: number, patch: Partial) => { + setPipeline((current) => + current.map((step, stepIndex) => { + if (stepIndex !== index) return step; + const next = { ...step, ...patch }; + const allowed = ENGINE_INTENSITIES[next.engine] ?? ["standard"]; + return { + ...next, + intensity: allowed.includes(next.intensity ?? "") ? next.intensity : allowed[0], + }; + }) + ); + }; + + const togglePack = (language: string, enabled: boolean) => { + setSelectedPacks((current) => + enabled + ? [...new Set([...current, language])] + : current.filter((item) => item !== language && item !== "en") + ); + }; + + const toggleAssignment = (id: string, enabled: boolean) => { + setAssignmentIds((current) => + enabled ? [...new Set([...current, id])] : current.filter((item) => item !== id) + ); + }; + + return ( +
+
+
+ hub +
+

{t("title")}

+

{t("description")}

+
+
+
+ +
+
+ setName(event.target.value)} + placeholder={t("name")} + className="rounded-lg border border-border bg-bg px-3 py-2 text-sm text-text-main" + /> + setDescription(event.target.value)} + placeholder={t("descriptionField")} + className="rounded-lg border border-border bg-bg px-3 py-2 text-sm text-text-main" + /> +
+ +
+
+

{t("pipeline")}

+ +
+ {pipeline.map((step, index) => ( +
+ + + +
+ ))} +
+ +
+
+

{t("languagePacks")}

+
+ {languagePacks.map((pack) => ( + + ))} +
+
+
+

{t("outputMode")}

+ + +
+
+

{t("assignToRouting")}

+
+ {routingCombos.length === 0 ? ( +

{t("noAssignments")}

+ ) : ( + routingCombos.map((combo) => { + const id = combo.id ?? combo.name ?? ""; + if (!id) return null; + return ( + + ); + }) + )} +
+
+
+ +
+ + {editingId && ( + + )} +
+
+ +
+ {combos.map((combo) => ( +
+
+
+

{combo.name}

+

{combo.description}

+
+ {combo.isDefault && ( + + {t("default")} + + )} +
+
+ {combo.pipeline.map((step, index) => ( + + {index + 1}. {step.engine} + {step.intensity ? `:${step.intensity}` : ""} + + ))} +
+

+ {t("languagePacks")}: {combo.languagePacks.join(", ")} +

+
+ + {!combo.isDefault && ( + + )} + {!combo.isDefault && ( + + )} +
+
+ ))} +
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/context/combos/page.tsx b/src/app/(dashboard)/dashboard/context/combos/page.tsx new file mode 100644 index 0000000000..934ab7d451 --- /dev/null +++ b/src/app/(dashboard)/dashboard/context/combos/page.tsx @@ -0,0 +1,5 @@ +import CompressionCombosPageClient from "./CompressionCombosPageClient"; + +export default function CompressionCombosPage() { + return ; +} diff --git a/src/app/(dashboard)/dashboard/context/rtk/RtkContextPageClient.tsx b/src/app/(dashboard)/dashboard/context/rtk/RtkContextPageClient.tsx new file mode 100644 index 0000000000..a7b148be8e --- /dev/null +++ b/src/app/(dashboard)/dashboard/context/rtk/RtkContextPageClient.tsx @@ -0,0 +1,299 @@ +"use client"; + +import { useEffect, useMemo, useState } from "react"; +import { useTranslations } from "next-intl"; + +type RtkFilter = { + id: string; + name: string; + description: string; + commandTypes: string[]; + category: string; + priority: number; +}; + +type RtkConfig = { + enabled: boolean; + intensity: "minimal" | "standard" | "aggressive"; + applyToToolResults: boolean; + applyToAssistantMessages: boolean; + applyToCodeBlocks: boolean; + enabledFilters: string[]; + disabledFilters: string[]; + maxLinesPerResult: number; + maxCharsPerResult: number; + deduplicateThreshold: number; +}; + +type AnalyticsSummary = { + totalRequests: number; + totalTokensSaved: number; + avgSavingsPct: number; + byEngine?: Record; +}; + +type PreviewResult = { + text?: string; + compressed?: boolean; + originalTokens?: number; + compressedTokens?: number; + techniquesUsed?: string[]; + detection?: { type: string; confidence: number; category: string }; + error?: string; +}; + +const SAMPLE_OUTPUT = `$ npm run typecheck +src/lib/example.ts:10:15 - error TS2322: Type 'string' is not assignable to type 'number'. + +10 const value: number = "bad"; + ~~~~~ + +Found 1 error in src/lib/example.ts:10`; + +function formatNumber(value: number | undefined): string { + return new Intl.NumberFormat().format(value ?? 0); +} + +export default function RtkContextPageClient() { + const t = useTranslations("contextRtk"); + const [filters, setFilters] = useState([]); + const [config, setConfig] = useState(null); + const [analytics, setAnalytics] = useState(null); + const [sample, setSample] = useState(SAMPLE_OUTPUT); + const [preview, setPreview] = useState(null); + const [saving, setSaving] = useState(false); + + useEffect(() => { + fetch("/api/context/rtk/filters") + .then((res) => (res.ok ? res.json() : null)) + .then((data) => setFilters(Array.isArray(data?.filters) ? data.filters : [])) + .catch(() => {}); + fetch("/api/context/rtk/config") + .then((res) => (res.ok ? res.json() : null)) + .then((data) => setConfig(data)) + .catch(() => {}); + fetch("/api/context/analytics?since=7d") + .then((res) => (res.ok ? res.json() : null)) + .then((data) => setAnalytics(data)) + .catch(() => {}); + }, []); + + const groupedFilters = useMemo(() => { + return filters.reduce>((groups, filter) => { + groups[filter.category] = [...(groups[filter.category] ?? []), filter]; + return groups; + }, {}); + }, [filters]); + + const activeFilterCount = useMemo(() => { + if (!config) return 0; + if (config.enabledFilters.length > 0) return config.enabledFilters.length; + return filters.filter((filter) => !config.disabledFilters.includes(filter.id)).length; + }, [config, filters]); + + const saveConfig = async (patch: Partial) => { + if (!config) return; + const nextConfig = { ...config, ...patch }; + setConfig(nextConfig); + setSaving(true); + try { + const res = await fetch("/api/context/rtk/config", { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify(patch), + }); + if (res.ok) setConfig(await res.json()); + } finally { + setSaving(false); + } + }; + + const toggleFilter = (filterId: string, enabled: boolean) => { + if (!config) return; + const disabledFilters = enabled + ? config.disabledFilters.filter((id) => id !== filterId) + : [...new Set([...config.disabledFilters, filterId])]; + saveConfig({ disabledFilters }); + }; + + const runPreview = async () => { + const res = await fetch("/api/context/rtk/test", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ text: sample, config: config ?? undefined }), + }); + setPreview(res.ok ? await res.json() : { error: await res.text() }); + }; + + const rtkStats = analytics?.byEngine?.rtk; + const statCards = [ + [t("tokensFiltered"), formatNumber(rtkStats?.tokensSaved ?? analytics?.totalTokensSaved)], + [t("filtersActive"), formatNumber(activeFilterCount)], + [t("requests"), formatNumber(rtkStats?.count ?? analytics?.totalRequests)], + [t("avgSavings"), `${rtkStats?.avgSavingsPct ?? analytics?.avgSavingsPct ?? 0}%`], + ]; + + return ( +
+
+
+ filter_alt +
+

{t("title")}

+

{t("description")}

+
+
+
+ +
+ {statCards.map(([label, value]) => ( +
+

{label}

+

{value}

+
+ ))} +
+ + {config && ( +
+
+ + + + +
+
+ {[ + ["applyToToolResults", t("toolResults")], + ["applyToAssistantMessages", t("assistantMessages")], + ["applyToCodeBlocks", t("codeBlocks")], + ].map(([key, label]) => ( + + ))} +
+
+ )} + +
+
+
+

{t("filterTesting")}

+ +
+