diff --git a/docs/MCP-SERVER.md b/docs/MCP-SERVER.md index 0483ef19f8..54039690eb 100644 --- a/docs/MCP-SERVER.md +++ b/docs/MCP-SERVER.md @@ -66,6 +66,11 @@ See [IDE Configs](integrations/ide-configs.md) for Antigravity, Cursor, Copilot, | `omniroute_list_compression_combos` | List named compression combos and routing assignments | | `omniroute_compression_combo_stats` | Analytics grouped by compression combo and engine | +`omniroute_compression_status` reports MCP description compression separately under +`analytics.mcpDescriptionCompression`. Those values are metadata-size estimates for MCP listable +descriptions (`tools`, `prompts`, `resources`, and `resourceTemplates`); they are not provider usage +receipts and are marked with `source: "mcp_metadata_estimate"`. + See [Compression Engines](compression-engines.md) and [RTK Compression](rtk-compression.md) for the runtime compression model behind these tools. diff --git a/docs/compression-rules-format.md b/docs/compression-rules-format.md index a3187b9c8e..e54343968b 100644 --- a/docs/compression-rules-format.md +++ b/docs/compression-rules-format.md @@ -19,13 +19,18 @@ Each pack contains replacements that apply to normal prose after protected regio "category": "filler", "rules": [ { - "name": "remove_basically", - "pattern": "\\bbasically\\b", - "replacement": "", + "name": "question_to_directive", + "pattern": "\\b(?:Can you explain why|Could you show me how)\\b\\s*", + "replacement": "Explain why ", + "replacementMap": { + "can you explain why": "Explain why ", + "could you show me how": "Show how " + }, + "flags": "gi", "context": "all", - "category": "filler", + "category": "context", "minIntensity": "lite", - "description": "Remove filler adverb." + "description": "Convert verbose questions into direct requests." } ] } @@ -33,21 +38,25 @@ Each pack contains replacements that apply to normal prose after protected regio ### Caveman Fields -| Field | Required | Description | -| ---------------------- | -------- | ---------------------------------------------------------------- | -| `language` | yes | BCP-47-like language key such as `en`, `pt-BR`, `es` | -| `category` | yes | Pack category filename/category, for example `filler` or `dedup` | -| `rules` | yes | Array of regex replacement rules | -| `rules[].name` | yes | Stable rule name | -| `rules[].pattern` | yes | JavaScript regex source, compiled with `gi` | -| `rules[].replacement` | yes | Replacement string | -| `rules[].context` | no | `all`, `user`, `assistant`, or `system`; default `all` | -| `rules[].category` | no | `filler`, `context`, `structural`, `dedup`, `terse`, or `ultra` | -| `rules[].minIntensity` | no | `lite`, `full`, or `ultra`; default `lite` | -| `rules[].description` | no | Human-readable rule summary | +| Field | Required | Description | +| ------------------------ | -------- | ---------------------------------------------------------------- | +| `language` | yes | BCP-47-like language key such as `en`, `pt-BR`, `es` | +| `category` | yes | Pack category filename/category, for example `filler` or `dedup` | +| `rules` | yes | Array of regex replacement rules | +| `rules[].name` | yes | Stable rule name | +| `rules[].pattern` | yes | JavaScript regex source | +| `rules[].flags` | no | JavaScript regex flags; default `gi` | +| `rules[].replacement` | no | Replacement string or fallback when `replacementMap` misses | +| `rules[].replacementMap` | no | Match-specific replacements keyed by normalized matched text | +| `rules[].context` | no | `all`, `user`, `assistant`, or `system`; default `all` | +| `rules[].category` | no | `filler`, `context`, `structural`, `dedup`, `terse`, or `ultra` | +| `rules[].minIntensity` | no | `lite`, `full`, or `ultra`; default `lite` | +| `rules[].description` | no | Human-readable rule summary | -Caveman file packs are data-only. Replacement functions are reserved for built-in TypeScript rules; -JSON packs use string replacements only. +Use `flags` when case-sensitive matching matters, for example article removal before lowercase prose +without stripping `the OpenAI API`. Use `replacementMap` when one regex has multiple alternatives +that need different outputs; this keeps JSON rule packs data-only while preserving the behavior of +the richer built-in TypeScript replacement functions. ## RTK Filter Packs diff --git a/open-sse/mcp-server/README.md b/open-sse/mcp-server/README.md index 80803a6013..f70c86014f 100644 --- a/open-sse/mcp-server/README.md +++ b/open-sse/mcp-server/README.md @@ -155,6 +155,11 @@ omniroute --mcp | 26 | `omniroute_list_compression_combos` | `read:compression` | List named compression combos and routing assignments | | 27 | `omniroute_compression_combo_stats` | `read:compression` | Read analytics grouped by compression combo and engine | +MCP listable metadata descriptions are compressed at registration/list time when description +compression is enabled. `omniroute_compression_status` exposes those savings separately as +`analytics.mcpDescriptionCompression` with `source: "mcp_metadata_estimate"`, so clients do not +mistake metadata shrink estimates for provider token receipts. + --- ## Client Examples diff --git a/open-sse/mcp-server/descriptionCompressor.ts b/open-sse/mcp-server/descriptionCompressor.ts index b18a5ea976..e8ad2e173e 100644 --- a/open-sse/mcp-server/descriptionCompressor.ts +++ b/open-sse/mcp-server/descriptionCompressor.ts @@ -32,6 +32,9 @@ const descriptionCompressionStats: McpDescriptionCompressionStats = { estimatedTokensSaved: 0, }; +const MCP_LIST_CONTAINER_KEYS = new Set(["tools", "prompts", "resources", "resourceTemplates"]); +const MCP_METADATA_DESCRIPTION_FIELDS = ["description"]; + function isDisabledEnvValue(value: string | undefined): boolean { return !!value && ["0", "false", "off", "no"].includes(value.trim().toLowerCase()); } @@ -84,8 +87,9 @@ export function maybeCompressMcpDescription( descriptionCompressionStats.estimatedTokensSaved += Math.ceil( (result.before - result.after) / 4 ); + return result.compressed; } - return result.compressed; + return description; } export function compressDescriptionsInPlace( @@ -109,6 +113,52 @@ export function compressDescriptionsInPlace( } } +function clonePlainMetadata(value: T): T { + if (!value || typeof value !== "object") return value; + return JSON.parse(JSON.stringify(value)) as T; +} + +function compressMcpListContainersInPlace( + value: unknown, + options: DescriptionCompressionOptions = {} +): void { + if (!value || typeof value !== "object") return; + if (Array.isArray(value)) { + for (const item of value) compressMcpListContainersInPlace(item, options); + return; + } + + for (const [key, nested] of Object.entries(value as Record)) { + if (MCP_LIST_CONTAINER_KEYS.has(key) && Array.isArray(nested)) { + compressDescriptionsInPlace(nested, MCP_METADATA_DESCRIPTION_FIELDS, options); + } else if (nested && typeof nested === "object") { + compressMcpListContainersInPlace(nested, options); + } + } +} + +export function compressMcpListMetadata( + value: T, + options: DescriptionCompressionOptions = {} +): T { + if (!isMcpDescriptionCompressionEnabled(options)) return value; + const clone = clonePlainMetadata(value); + compressMcpListContainersInPlace(clone, options); + return clone; +} + +export function compressMcpRegistryMetadata>( + metadata: T, + options: DescriptionCompressionOptions = {} +): T { + if (!isMcpDescriptionCompressionEnabled(options)) return metadata; + const clone: Record = { ...metadata }; + if (typeof clone.description === "string") { + clone.description = maybeCompressMcpDescription(clone.description, options); + } + return clone as T; +} + export function getMcpDescriptionCompressionStats(): McpDescriptionCompressionStats { return { ...descriptionCompressionStats }; } diff --git a/open-sse/mcp-server/server.ts b/open-sse/mcp-server/server.ts index 0a50fbbcd5..d2b5e58c95 100644 --- a/open-sse/mcp-server/server.ts +++ b/open-sse/mcp-server/server.ts @@ -76,7 +76,7 @@ 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 { compressMcpRegistryMetadata } from "./descriptionCompressor.ts"; import { getDbInstance } from "../../src/lib/db/core.ts"; import { normalizeQuotaResponse } from "../../src/shared/contracts/quota.ts"; import { resolveOmniRouteBaseUrl } from "../../src/shared/utils/resolveOmniRouteBaseUrl.ts"; @@ -594,14 +594,30 @@ export function createMcpServer(): McpServer { 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); + const metadata = compressMcpRegistryMetadata(config, { + enabled: mcpDescriptionCompressionEnabled, + }); + return registerTool(name, metadata, handler as never); }) as typeof server.registerTool; + const registerPrompt = server.registerPrompt.bind(server); + server.registerPrompt = ((name: string, config: Record, handler: unknown) => { + const metadata = compressMcpRegistryMetadata(config, { + enabled: mcpDescriptionCompressionEnabled, + }); + return registerPrompt(name, metadata as never, handler as never); + }) as typeof server.registerPrompt; + const registerResource = server.registerResource.bind(server); + server.registerResource = (( + name: string, + uriOrTemplate: unknown, + config: Record, + readCallback: unknown + ) => { + const metadata = compressMcpRegistryMetadata(config, { + enabled: mcpDescriptionCompressionEnabled, + }); + return registerResource(name, uriOrTemplate as never, metadata as never, readCallback as never); + }) as typeof server.registerResource; // Register essential tools server.registerTool( diff --git a/open-sse/mcp-server/tools/compressionTools.ts b/open-sse/mcp-server/tools/compressionTools.ts index 3e8c4d1ed2..087e78a119 100644 --- a/open-sse/mcp-server/tools/compressionTools.ts +++ b/open-sse/mcp-server/tools/compressionTools.ts @@ -55,8 +55,12 @@ export async function handleCompressionStatus( }; mcpDescriptionCompression: { descriptionsCompressed: number; + charsBefore: number; + charsAfter: number; charsSaved: number; estimatedTokensSaved: number; + source: "mcp_metadata_estimate"; + notProviderUsage: true; }; }; cacheStats: { @@ -99,8 +103,12 @@ export async function handleCompressionStatus( realUsage: analyticsSummary.realUsage, mcpDescriptionCompression: { descriptionsCompressed: mcpDescriptionStats.descriptionsCompressed, + charsBefore: mcpDescriptionStats.charsBefore, + charsAfter: mcpDescriptionStats.charsAfter, charsSaved: mcpDescriptionStats.charsSaved, estimatedTokensSaved: mcpDescriptionStats.estimatedTokensSaved, + source: "mcp_metadata_estimate" as const, + notProviderUsage: true as const, }, }, cacheStats: cacheStats diff --git a/open-sse/services/compression/caveman.ts b/open-sse/services/compression/caveman.ts index a57195b02c..b5f2de4755 100644 --- a/open-sse/services/compression/caveman.ts +++ b/open-sse/services/compression/caveman.ts @@ -28,7 +28,20 @@ const RULE_KEYWORDS: Record = { redundant_phrasing: ["make sure", "be sure"], redundant_because: ["due to the fact", "the reason is because"], redundant_directive: ["it is important", "you should", "remember to"], - pleasantries: ["sure", "certainly", "of course", "happy to"], + pleasantries: [ + "sure", + "certainly", + "of course", + "happy to", + "thanks", + "thank you", + "glad to help", + "glad to", + "no problem", + "you're welcome", + "youre welcome", + "absolutely", + ], polite_framing: [ "please", "kindly", diff --git a/open-sse/services/compression/cavemanRules.ts b/open-sse/services/compression/cavemanRules.ts index 9e74aa3de9..c94ad6419e 100644 --- a/open-sse/services/compression/cavemanRules.ts +++ b/open-sse/services/compression/cavemanRules.ts @@ -28,7 +28,7 @@ const CAVEMAN_RULES: CavemanRule[] = [ { name: "pleasantries", pattern: - /\b(?:sure|certainly|of course|happy to|i'?d be happy to|i would be happy to)\b[,.!?\s]*/gi, + /(? { 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", - "create a thorough": "create", - "explain in detail": "explain", + "provide a detailed explanation of": "explain ", + "give me a comprehensive explanation of": "explain ", + "write an in-depth explanation of": "explain ", + "create a thorough explanation of": "explain ", + "provide a detailed": "provide ", + "give me a comprehensive": "give ", + "write an in-depth": "write ", + "create a thorough": "create ", + "explain in detail": "explain ", }; const lower = match.toLowerCase(); return map[lower] ?? match; @@ -86,7 +86,7 @@ const CAVEMAN_RULES: CavemanRule[] = [ }, { name: "articles", - pattern: /\b(?:a|an|the)\s+(?=[a-z])/gi, + pattern: /\b(?:[Aa]n|[Aa]|[Tt]he)\s+(?=[a-z])/g, replacement: "", context: "all", category: "terse", @@ -185,10 +185,10 @@ const CAVEMAN_RULES: CavemanRule[] = [ replacement: (match: string): string => { const trimmed = match.trimEnd().toLowerCase(); const map: Record = { - "can you explain why": "Explain why", - "could you show me how": "Show how", - "would you tell me": "Tell me", - "can you tell me": "Tell me", + "can you explain why": "Explain why ", + "could you show me how": "Show how ", + "would you tell me": "Tell me ", + "can you tell me": "Tell me ", }; return map[trimmed] ?? match; }, diff --git a/open-sse/services/compression/engines/rtk/index.ts b/open-sse/services/compression/engines/rtk/index.ts index 61fd9d535a..70d6efc529 100644 --- a/open-sse/services/compression/engines/rtk/index.ts +++ b/open-sse/services/compression/engines/rtk/index.ts @@ -8,6 +8,7 @@ import { applyLineFilter } from "./lineFilter.ts"; import { smartTruncate } from "./smartTruncate.ts"; import { normalizeCodeLanguage, stripCode } from "./codeStripper.ts"; import { maybePersistRtkRawOutput, type RtkRawOutputPointer } from "./rawOutput.ts"; +import { isTextBlock } from "../../messageContent.ts"; type Message = { role: string; @@ -191,31 +192,6 @@ function shouldCompressMessage(message: Message, config: RtkConfig): boolean { return false; } -function mapStringContent( - content: Message["content"], - transform: (text: string) => string -): Message["content"] { - if (typeof content === "string") return transform(content); - if (!Array.isArray(content)) return content; - return content.map((part) => { - if (part && typeof part === "object" && part.type === "text" && typeof part.text === "string") { - return { ...part, text: transform(part.text) }; - } - return part; - }); -} - -function extractContentText(content: Message["content"]): string { - if (typeof content === "string") return content; - if (!Array.isArray(content)) return ""; - return content - .map((part) => - part && typeof part === "object" && typeof part.text === "string" ? part.text : "" - ) - .filter(Boolean) - .join("\n"); -} - export function processRtkText( text: string, options: { command?: string | null; config?: Partial } = {} @@ -310,6 +286,64 @@ export function processRtkText( }; } +function processRtkContent( + content: Message["content"], + config: RtkConfig +): { + content: Message["content"]; + compressed: boolean; + techniquesUsed: string[]; + rulesApplied: string[]; + rawOutputPointers: RtkRawOutputPointer[]; +} { + const techniquesUsed: string[] = []; + const rulesApplied: string[] = []; + const rawOutputPointers: RtkRawOutputPointer[] = []; + + const collect = (processed: RtkProcessResult) => { + techniquesUsed.push(...processed.techniquesUsed); + rulesApplied.push(...processed.rulesApplied); + if (processed.rawOutputPointers) rawOutputPointers.push(...processed.rawOutputPointers); + }; + + if (typeof content === "string") { + if (!content) { + return { content, compressed: false, techniquesUsed, rulesApplied, rawOutputPointers }; + } + const processed = processRtkText(content, { config }); + collect(processed); + return { + content: processed.compressed ? processed.text : content, + compressed: processed.compressed, + techniquesUsed, + rulesApplied, + rawOutputPointers, + }; + } + + if (!Array.isArray(content)) { + return { content, compressed: false, techniquesUsed, rulesApplied, rawOutputPointers }; + } + + let compressed = false; + const nextContent = content.map((part) => { + if (!isTextBlock(part) || !part.text) return part; + const processed = processRtkText(part.text, { config }); + collect(processed); + if (!processed.compressed) return part; + compressed = true; + return { ...part, text: processed.text }; + }); + + return { + content: compressed ? nextContent : content, + compressed, + techniquesUsed, + rulesApplied, + rawOutputPointers, + }; +} + export function applyRtkCompression( body: Record, options: { config?: Partial; stepConfig?: Record } = {} @@ -328,16 +362,14 @@ export function applyRtkCompression( const rawOutputPointers: RtkRawOutputPointer[] = []; const compressedMessages = messages.map((message) => { if (!shouldCompressMessage(message, config)) return message; - const text = extractContentText(message.content); - if (!text) return message; - const processed = processRtkText(text, { config }); + const processed = processRtkContent(message.content, config); allTechniques.push(...processed.techniquesUsed); allRules.push(...processed.rulesApplied); - if (processed.rawOutputPointers) rawOutputPointers.push(...processed.rawOutputPointers); + rawOutputPointers.push(...processed.rawOutputPointers); if (!processed.compressed) return message; return { ...message, - content: mapStringContent(message.content, () => processed.text), + content: processed.content, }; }); diff --git a/open-sse/services/compression/ruleLoader.ts b/open-sse/services/compression/ruleLoader.ts index ba149d8515..6bea11b030 100644 --- a/open-sse/services/compression/ruleLoader.ts +++ b/open-sse/services/compression/ruleLoader.ts @@ -9,7 +9,9 @@ type CavemanRuleContext = CavemanRule["context"]; interface FileRule { name: string; pattern: string; - replacement: string; + replacement?: string; + replacementMap?: Record; + flags?: string; context?: CavemanRuleContext; category?: CavemanRuleCategory; minIntensity?: CavemanIntensity; @@ -32,8 +34,32 @@ const VALID_CONTEXTS = new Set(["all", "user", "system", "assistant"]); const VALID_CATEGORIES = new Set(["filler", "context", "structural", "dedup", "terse", "ultra"]); const VALID_INTENSITIES = new Set(["lite", "full", "ultra"]); const cache = new Map(); +let rulesDirCache: string | null = null; + +function normalizeReplacementKey(value: string): string { + return value.trim().replace(/\s+/g, " ").toLowerCase(); +} + +function compileReplacement(rule: FileRule): CavemanRule["replacement"] { + if (!rule.replacementMap) return rule.replacement ?? ""; + + const normalizedMap = new Map( + Object.entries(rule.replacementMap).map(([key, value]) => [normalizeReplacementKey(key), value]) + ); + const fallback = rule.replacement; + return (match: string) => { + const normalized = normalizeReplacementKey(match); + if (normalizedMap.has(normalized)) return normalizedMap.get(normalized) ?? ""; + return fallback ?? match; + }; +} + +function getRuleFlags(rule: FileRule): string { + return rule.flags ?? "gi"; +} function getRulesDir(): string { + if (rulesDirCache) return rulesDirCache; const moduleDir = path.dirname(fileURLToPath(import.meta.url)); const candidates = [ path.join(moduleDir, "rules"), @@ -41,19 +67,20 @@ function getRulesDir(): string { path.join(process.cwd(), "open-sse", "services", "compression", "rules"), path.join(process.cwd(), "app", "open-sse", "services", "compression", "rules"), ]; - return ( + rulesDirCache = candidates.find((candidate, index) => { return candidates.indexOf(candidate) === index && fs.existsSync(candidate); - }) ?? candidates[0] - ); + }) ?? candidates[0]; + return rulesDirCache; } function compileRule(rule: FileRule, source: string): CavemanRule { try { + const flags = getRuleFlags(rule); return { name: rule.name, - pattern: new RegExp(rule.pattern, "gi"), - replacement: rule.replacement, + pattern: new RegExp(rule.pattern, flags), + replacement: compileReplacement(rule), context: rule.context ?? "all", category: rule.category ?? "filler", minIntensity: rule.minIntensity ?? "lite", @@ -87,6 +114,7 @@ export function validateRulePack(pack: unknown): { valid: boolean; errors: strin return; } const entry = rule as Partial; + const flags = typeof entry.flags === "string" ? entry.flags : "gi"; if (typeof entry.name !== "string" || !entry.name.trim()) { errors.push(`rules[${index}].name must be a non-empty string`); } @@ -94,15 +122,39 @@ export function validateRulePack(pack: unknown): { valid: boolean; errors: strin errors.push(`rules[${index}].pattern must be a non-empty string`); } else { try { - new RegExp(entry.pattern, "gi"); + new RegExp(entry.pattern, flags); } catch (error) { const message = error instanceof Error ? error.message : String(error); errors.push(`rules[${index}].pattern is invalid: ${message}`); } } - if (typeof entry.replacement !== "string") { + if (entry.flags !== undefined && typeof entry.flags !== "string") { + errors.push(`rules[${index}].flags must be a string`); + } + if (entry.replacement !== undefined && typeof entry.replacement !== "string") { errors.push(`rules[${index}].replacement must be a string`); } + if (entry.replacementMap !== undefined) { + if ( + !entry.replacementMap || + typeof entry.replacementMap !== "object" || + Array.isArray(entry.replacementMap) + ) { + errors.push(`rules[${index}].replacementMap must be an object`); + } else { + Object.entries(entry.replacementMap).forEach(([key, replacement]) => { + if (!key.trim()) { + errors.push(`rules[${index}].replacementMap contains an empty key`); + } + if (typeof replacement !== "string") { + errors.push(`rules[${index}].replacementMap.${key} must be a string`); + } + }); + } + } + if (typeof entry.replacement !== "string" && entry.replacementMap === undefined) { + errors.push(`rules[${index}] must define replacement or replacementMap`); + } if (entry.context !== undefined && !VALID_CONTEXTS.has(entry.context)) { errors.push(`rules[${index}].context is invalid`); } diff --git a/open-sse/services/compression/rules/_schema.json b/open-sse/services/compression/rules/_schema.json index 5182229389..9ec4fc4724 100644 --- a/open-sse/services/compression/rules/_schema.json +++ b/open-sse/services/compression/rules/_schema.json @@ -10,11 +10,16 @@ "type": "array", "items": { "type": "object", - "required": ["name", "pattern", "replacement"], + "required": ["name", "pattern"], "properties": { "name": { "type": "string" }, "pattern": { "type": "string" }, + "flags": { "type": "string" }, "replacement": { "type": "string" }, + "replacementMap": { + "type": "object", + "additionalProperties": { "type": "string" } + }, "context": { "enum": ["all", "user", "system", "assistant"] }, "category": { "enum": ["filler", "context", "structural", "dedup", "terse", "ultra"] }, "minIntensity": { "enum": ["lite", "full", "ultra"] }, diff --git a/open-sse/services/compression/rules/en/context.json b/open-sse/services/compression/rules/en/context.json index 9f0964692e..70ef148408 100644 --- a/open-sse/services/compression/rules/en/context.json +++ b/open-sse/services/compression/rules/en/context.json @@ -14,6 +14,12 @@ "name": "explanatory_prefix", "pattern": "\\b(?:The function appears to be handling|The code seems to|The class is|This module is)\\b", "replacement": "Function:", + "replacementMap": { + "the function appears to be handling": "Function:", + "the code seems to": "Code:", + "the class is": "Class:", + "this module is": "Module:" + }, "context": "all", "category": "context", "minIntensity": "lite" @@ -21,7 +27,13 @@ { "name": "question_to_directive", "pattern": "\\b(?:Can you explain why|Could you show me how|Would you tell me|Can you tell me)\\b\\s*", - "replacement": "Explain why", + "replacement": "Explain why ", + "replacementMap": { + "can you explain why": "Explain why ", + "could you show me how": "Show how ", + "would you tell me": "Tell me ", + "can you tell me": "Tell me " + }, "context": "user", "category": "context", "minIntensity": "lite" @@ -62,6 +74,12 @@ "name": "purpose_statement", "pattern": "\\b(?:for the purpose of|with the goal of|in an effort to|for every)\\b", "replacement": "for", + "replacementMap": { + "for the purpose of": "for", + "with the goal of": "to", + "in an effort to": "to", + "for every": "per" + }, "context": "all", "category": "context", "minIntensity": "lite" diff --git a/open-sse/services/compression/rules/en/filler.json b/open-sse/services/compression/rules/en/filler.json index 975d7c324c..88e4a6978a 100644 --- a/open-sse/services/compression/rules/en/filler.json +++ b/open-sse/services/compression/rules/en/filler.json @@ -4,7 +4,7 @@ "rules": [ { "name": "pleasantries", - "pattern": "(? { assert.equal(stats.descriptionsCompressed, 1); assert.ok(stats.estimatedTokensSaved > 0); }); + + it("runs offline parity against upstream Caveman fixture files", () => { + const fixturePairs = readdirSync(upstreamFixtureDir) + .filter((entry) => entry.endsWith(".original.md")) + .map((originalName) => ({ + name: originalName.replace(/\.original\.md$/, ""), + originalPath: path.join(upstreamFixtureDir, originalName), + compressedPath: path.join( + upstreamFixtureDir, + originalName.replace(/\.original\.md$/, ".md") + ), + })); + + assert.ok(fixturePairs.length >= 5, "expected upstream fixture pairs"); + + for (const fixture of fixturePairs) { + const original = readFileSync(fixture.originalPath, "utf8"); + const expected = readFileSync(fixture.compressedPath, "utf8"); + const ours = omniroutePromptCompression(original); + const upstreamShrink = upstream.compress(original).compressed; + + assert.ok( + expected.length < original.length, + `${fixture.name}: upstream fixture did not reduce` + ); + if (ours.fallbackApplied) { + assert.equal( + ours.text, + original, + `${fixture.name}: fallback must preserve original fixture verbatim` + ); + } else { + assert.ok(ours.text.length < original.length, `${fixture.name}: OmniRoute did not reduce`); + assert.ok( + ours.text.length <= Math.ceil(Math.max(expected.length, upstreamShrink.length) * 1.35), + `${fixture.name}: OmniRoute drifted too far from upstream fixture budget` + ); + } + + for (const protectedPattern of [/```[\s\S]*?```/g, /`[^`\n]+`/g, /https?:\/\/\S+/g]) { + const protectedValues = original.match(protectedPattern) ?? []; + for (const protectedValue of protectedValues) { + assert.ok( + ours.text.includes(protectedValue), + `${fixture.name}: protected content changed: ${protectedValue.slice(0, 80)}` + ); + } + } + } + }); }); diff --git a/tests/unit/compression/caveman-v379.test.ts b/tests/unit/compression/caveman-v379.test.ts index 04a777d18f..00e8acdfff 100644 --- a/tests/unit/compression/caveman-v379.test.ts +++ b/tests/unit/compression/caveman-v379.test.ts @@ -58,6 +58,36 @@ describe("Caveman v3.7.9 rule parity", () => { assert.match(ultra, /\bres\b/); }); + it("preserves articles before proper nouns, numbers, and code-like tokens", () => { + const text = compress( + "Use the OpenAI API, the 404 error, the config.api.endpoint() function, and the database." + ); + + assert.match(text, /\bthe OpenAI API\b/); + assert.match(text, /\bthe 404 error\b/); + assert.match(text, /\bthe config\.api\.endpoint\(\) function\b/); + assert.doesNotMatch(text, /\bthe database\b/i); + }); + + it("removes upstream Caveman pleasantry variants without breaking make sure to", () => { + const text = compress( + "Thanks, thank you, glad to help, I'd be glad to, no problem, you're welcome, absolutely. Please make sure to review the database." + ); + + for (const phrase of [ + "thanks", + "thank you", + "glad to help", + "I'd be glad to", + "no problem", + "you're welcome", + "absolutely", + ]) { + assert.doesNotMatch(text, new RegExp(phrase.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"), "i")); + } + assert.match(text, /\bensure review database\b/i); + }); + it("keeps system prompts unchanged when preserveSystemPrompt is enabled", () => { const body = { messages: [ diff --git a/tests/unit/compression/compressionMcpTools.test.ts b/tests/unit/compression/compressionMcpTools.test.ts index fb59dc082f..4adeb9a1e5 100644 --- a/tests/unit/compression/compressionMcpTools.test.ts +++ b/tests/unit/compression/compressionMcpTools.test.ts @@ -75,6 +75,14 @@ describe("handleCompressionStatus", () => { const result = await handleCompressionStatus({}); assert.equal(typeof result.analytics.compressedRequests, "number"); }); + + it("labels MCP description savings as metadata estimates, not provider usage", async () => { + const result = await handleCompressionStatus({}); + assert.equal(result.analytics.mcpDescriptionCompression.source, "mcp_metadata_estimate"); + assert.equal(result.analytics.mcpDescriptionCompression.notProviderUsage, true); + assert.equal(typeof result.analytics.mcpDescriptionCompression.charsBefore, "number"); + assert.equal(typeof result.analytics.mcpDescriptionCompression.charsAfter, "number"); + }); }); describe("handleCompressionConfigure", () => { diff --git a/tests/unit/compression/mcp-description-compressor.test.ts b/tests/unit/compression/mcp-description-compressor.test.ts index f9915b6748..13426429e7 100644 --- a/tests/unit/compression/mcp-description-compressor.test.ts +++ b/tests/unit/compression/mcp-description-compressor.test.ts @@ -2,6 +2,8 @@ import { describe, it } from "node:test"; import assert from "node:assert/strict"; import { compressDescriptionsInPlace, + compressMcpListMetadata, + compressMcpRegistryMetadata, compressMcpDescription, getMcpDescriptionCompressionStats, maybeCompressMcpDescription, @@ -45,6 +47,49 @@ describe("MCP description compression", () => { assert.deepEqual(payload.result.tools[1].inputSchema.description, { not: "string" }); }); + it("compresses only MCP list metadata containers, not tool-call response bodies", () => { + resetMcpDescriptionCompressionStats(); + const payload = { + result: { + tools: [ + { name: "get_weather", description: "The function returns the weather for a city." }, + ], + prompts: [{ name: "ask", description: "The prompt asks for a detailed summary." }], + resources: [{ name: "docs", description: "The resource contains the project docs." }], + resourceTemplates: [ + { name: "doc", description: "The template returns the matching project document." }, + ], + content: [ + { + type: "text", + description: "The tool response body should remain unchanged.", + }, + ], + }, + }; + + const output = compressMcpListMetadata(payload); + + assert.doesNotMatch(output.result.tools[0].description, /\bthe\b/i); + assert.doesNotMatch(output.result.prompts[0].description, /\bthe\b/i); + assert.doesNotMatch(output.result.resources[0].description, /\bthe\b/i); + assert.doesNotMatch(output.result.resourceTemplates[0].description, /\bthe\b/i); + assert.equal( + output.result.content[0].description, + "The tool response body should remain unchanged." + ); + }); + + it("compresses registry metadata for tools, prompts, and resources", () => { + const metadata = compressMcpRegistryMetadata({ + description: "The function returns the current weather for a city.", + inputSchema: {}, + }); + + assert.doesNotMatch(metadata.description as string, /\bthe\b/i); + assert.deepEqual(metadata.inputSchema, {}); + }); + it("honors settings and environment kill switches", () => { const originalEnv = process.env.OMNIROUTE_MCP_DESCRIPTION_COMPRESSION; const originalAliasEnv = process.env.OMNIROUTE_MCP_COMPRESS_DESCRIPTIONS; @@ -79,6 +124,8 @@ describe("MCP description compression", () => { assert.notEqual(output, input); assert.equal(stats.descriptionsCompressed, 1); + assert.equal(stats.charsBefore, input.length); + assert.equal(stats.charsAfter, output.length); assert.ok(stats.charsSaved > 0); assert.ok(stats.estimatedTokensSaved > 0); }); diff --git a/tests/unit/compression/pipeline-integration.test.ts b/tests/unit/compression/pipeline-integration.test.ts index 08434783d3..2c794fe158 100644 --- a/tests/unit/compression/pipeline-integration.test.ts +++ b/tests/unit/compression/pipeline-integration.test.ts @@ -68,4 +68,39 @@ describe("compression pipeline integration", () => { ["rtk", "caveman"] ); }); + + it("keeps multipart tool output safe through stacked RTK then Caveman", () => { + const imagePart = { type: "image_url", image_url: { url: "data:image/png;base64,abc" } }; + const body = { + messages: [ + { + role: "tool", + content: [ + { + type: "text", + text: Array.from({ length: 12 }, () => "first repeated tool line").join("\n"), + }, + imagePart, + { + type: "text", + text: Array.from({ length: 12 }, () => "second repeated tool line").join("\n"), + }, + ], + }, + ], + }; + + const result = applyStackedCompression(body, [ + { engine: "rtk", intensity: "standard" }, + { engine: "caveman", intensity: "full" }, + ]); + const content = (result.body.messages as typeof body.messages)[0].content; + + assert.equal(result.stats?.engine, "stacked"); + assert.ok(Array.isArray(content)); + assert.match(content[0].text ?? "", /first repeated tool line/); + assert.match(content[2].text ?? "", /second repeated tool line/); + assert.notEqual(content[0].text, content[2].text); + assert.deepEqual(content[1], imagePart); + }); }); diff --git a/tests/unit/compression/rtk-engine.test.ts b/tests/unit/compression/rtk-engine.test.ts index c2e7a73fec..7406f53634 100644 --- a/tests/unit/compression/rtk-engine.test.ts +++ b/tests/unit/compression/rtk-engine.test.ts @@ -86,4 +86,30 @@ describe("RTK compression engine", () => { assert.equal(result.compressed, true); assert.equal(result.stats?.mode, "rtk"); }); + + it("compresses multipart text parts independently without duplicating output", () => { + const imagePart = { type: "image_url", image_url: { url: "data:image/png;base64,abc" } }; + const body = { + messages: [ + { + role: "tool", + content: [ + { type: "text", text: Array.from({ length: 12 }, () => "alpha noisy line").join("\n") }, + imagePart, + { type: "text", text: Array.from({ length: 12 }, () => "beta noisy line").join("\n") }, + ], + }, + ], + }; + + const result = applyRtkCompression(body); + const content = (result.body.messages as typeof body.messages)[0].content; + + assert.equal(result.compressed, true); + assert.ok(Array.isArray(content)); + assert.match(content[0].text ?? "", /alpha noisy line/); + assert.match(content[2].text ?? "", /beta noisy line/); + assert.notEqual(content[0].text, content[2].text); + assert.deepEqual(content[1], imagePart); + }); }); diff --git a/tests/unit/compression/rule-loader.test.ts b/tests/unit/compression/rule-loader.test.ts index 2ae63a190f..60c87f0c56 100644 --- a/tests/unit/compression/rule-loader.test.ts +++ b/tests/unit/compression/rule-loader.test.ts @@ -42,6 +42,38 @@ describe("Caveman file-based rule loader", () => { ); }); + it("validates flags and replacement maps", () => { + assert.equal( + validateRulePack({ + language: "xx", + category: "context", + rules: [ + { + name: "mapped", + pattern: "\\b(?:one|two)\\b", + flags: "g", + replacementMap: { one: "1", two: "2" }, + }, + ], + }).valid, + true + ); + assert.equal( + validateRulePack({ + language: "xx", + category: "context", + rules: [ + { + name: "bad_map", + pattern: "\\b(?:one|two)\\b", + replacementMap: { one: 1 }, + }, + ], + }).valid, + false + ); + }); + it("falls back to hardcoded rules when language pack is missing", () => { const rules = getRulesForContext("user", "full", "missing-language"); assert.ok(rules.some((rule) => rule.name === "polite_framing")); @@ -55,4 +87,26 @@ describe("Caveman file-based rule loader", () => { assert.ok(!text.toLowerCase().includes("detailed")); assert.ok(text.includes("provide") || text.includes("explain")); }); + + it("applies mapped replacements without semantic regressions", () => { + const fullRules = getRulesForContext("user", "full", "en"); + const ultraRules = getRulesForContext("user", "ultra", "en"); + + assert.equal( + applyRulesToText("Could you show me how to configure auth?", fullRules).text, + "Show how to configure auth?" + ); + assert.equal( + applyRulesToText("The code seems to parse JSON.", fullRules).text, + "Code: parse JSON." + ); + assert.equal( + applyRulesToText("The value was generated by the server.", fullRules).text, + "value generated by server." + ); + assert.equal( + applyRulesToText("The dependencies are outdated.", ultraRules).text, + "deps are outdated." + ); + }); });