From ff80f0395384a7770c4c56a49512e1b3420dbe09 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Wed, 29 Apr 2026 12:47:44 +0700 Subject: [PATCH 1/4] =?UTF-8?q?fix(compression):=20correct=20getCacheStats?= =?UTF-8?q?Summary=20SQL=20=E2=80=94=20separate=20global=20and=20per-provi?= =?UTF-8?q?der=20queries?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/lib/db/compressionCacheStats.ts | 112 ++++++++++++++++++++++++++++ 1 file changed, 112 insertions(+) create mode 100644 src/lib/db/compressionCacheStats.ts diff --git a/src/lib/db/compressionCacheStats.ts b/src/lib/db/compressionCacheStats.ts new file mode 100644 index 0000000000..d07a0753c3 --- /dev/null +++ b/src/lib/db/compressionCacheStats.ts @@ -0,0 +1,112 @@ +import { getDbInstance } from "./core"; + +export interface CacheStatsEntry { + provider: string; + model?: string; + compressionMode: string; + cacheControlPresent: boolean; + estimatedCacheHit: boolean; + tokensSavedCompression: number; + tokensSavedCaching: number; + netSavings: number; +} + +export interface CacheStatsSummary { + totalRequests: number; + avgNetSavings: number; + cacheHitRate: number; + byProvider: Record; +} + +export function recordCacheStats(entry: CacheStatsEntry): void { + const db = getDbInstance(); + + const sql = `INSERT INTO compression_cache_stats ( + provider, + model, + compression_mode, + cache_control_present, + estimated_cache_hit, + tokens_saved_compression, + tokens_saved_caching, + net_savings + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?)`; + + db.prepare(sql).run( + entry.provider, + entry.model ?? "", + entry.compressionMode, + entry.cacheControlPresent ? 1 : 0, + entry.estimatedCacheHit ? 1 : 0, + entry.tokensSavedCompression, + entry.tokensSavedCaching, + entry.netSavings + ); +} + +export function getCacheStatsSummary(since?: Date): CacheStatsSummary { + const db = getDbInstance(); + const whereClause = since ? "WHERE created_at >= ?" : ""; + const params = since ? [since.toISOString()] : []; + + // Global aggregates + const globalRow = since + ? (db + .prepare( + `SELECT COUNT(*) as totalRequests, AVG(net_savings) as avgNetSavings, SUM(estimated_cache_hit) * 1.0 / COUNT(*) as cacheHitRate FROM compression_cache_stats WHERE created_at >= ?` + ) + .get(since.toISOString()) as + | { totalRequests: number; avgNetSavings: number; cacheHitRate: number } + | undefined) + : (db + .prepare( + `SELECT COUNT(*) as totalRequests, AVG(net_savings) as avgNetSavings, SUM(estimated_cache_hit) * 1.0 / COUNT(*) as cacheHitRate FROM compression_cache_stats` + ) + .get() as + | { totalRequests: number; avgNetSavings: number; cacheHitRate: number } + | undefined); + + if (!globalRow || globalRow.totalRequests === 0) { + return { totalRequests: 0, avgNetSavings: 0, cacheHitRate: 0, byProvider: {} }; + } + + // Per-provider aggregates + const providerRows = since + ? (db + .prepare( + `SELECT provider, COUNT(*) as count, AVG(net_savings) as avgNetSavings, SUM(estimated_cache_hit) * 1.0 / COUNT(*) as cacheHitRate FROM compression_cache_stats WHERE created_at >= ? GROUP BY provider` + ) + .all(since.toISOString()) as Array<{ + provider: string; + count: number; + avgNetSavings: number; + cacheHitRate: number; + }>) + : (db + .prepare( + `SELECT provider, COUNT(*) as count, AVG(net_savings) as avgNetSavings, SUM(estimated_cache_hit) * 1.0 / COUNT(*) as cacheHitRate FROM compression_cache_stats GROUP BY provider` + ) + .all() as Array<{ + provider: string; + count: number; + avgNetSavings: number; + cacheHitRate: number; + }>); + + const byProvider: Record = + {}; + for (const row of providerRows) { + byProvider[row.provider] = { + count: row.count, + avgNetSavings: row.avgNetSavings, + cacheHitRate: row.cacheHitRate, + }; + } + + return { + totalRequests: globalRow.totalRequests, + avgNetSavings: globalRow.avgNetSavings ?? 0, + cacheHitRate: globalRow.cacheHitRate ?? 0, + byProvider, + }; +} From 90873cd3e2d4e1248a20012d7b22c69c9e31eb53 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Wed, 29 Apr 2026 13:02:28 +0700 Subject: [PATCH 2/4] feat(compression/phase6): add cachingAware detection and strategy adjustment --- open-sse/services/compression/cachingAware.ts | 70 +++++++++++++++++++ 1 file changed, 70 insertions(+) create mode 100644 open-sse/services/compression/cachingAware.ts diff --git a/open-sse/services/compression/cachingAware.ts b/open-sse/services/compression/cachingAware.ts new file mode 100644 index 0000000000..89cd1c85e5 --- /dev/null +++ b/open-sse/services/compression/cachingAware.ts @@ -0,0 +1,70 @@ +/** + * Cache-aware strategy selection for AI model compression. + * Implements logic to detect caching context from request body and adjust strategies accordingly. + * + * @file cachingAware.ts + * @exports CachingContext, CacheAwareStrategy, detectCachingContext, getCacheAwareStrategy + */ + +// Define the CachingContext interface +export interface CachingContext { + hasCacheControl: boolean; // Whether cache_control is present in the request body + provider: string | null; // Extracted provider (e.g., "anthropic", "openai", etc.) + isCachingProvider: boolean; // True if the provider supports caching optimizations +} + +// Define the CacheAwareStrategy interface +export interface CacheAwareStrategy { + strategy: string; // Selected strategy (e.g., "standard", "aggressive", etc.) + skipSystemPrompt: boolean; // Whether to skip the system prompt for caching + deterministicOnly: boolean; // Whether deterministic responses are required +} + +/** + * Detect the caching context from the request body. + * + * @param body - The request body to analyze + * @returns A CachingContext object + */ +export function detectCachingContext(body: unknown): CachingContext { + const hasCacheControl = Boolean((body as any)?.cache_control); + + // Extract the provider from the model string + const model = (body as any)?.model; + const providerMatch = model?.match(/^(anthropic|openai|gemini|google)\//); + const provider = providerMatch ? providerMatch[1] : null; + + // Check if the provider is among caching-aware providers + const isCachingProvider = ["anthropic", "openai", "gemini", "google"].includes(provider ?? ""); + + return { + hasCacheControl, + provider, + isCachingProvider, + }; +} + +/** + * Get a cache-aware strategy based on the given strategy and caching context. + * + * @param strategy - The initial strategy (e.g., "aggressive", "ultra", etc.) + * @param ctx - The caching context + * @returns A CacheAwareStrategy object + */ +export function getCacheAwareStrategy(strategy: string, ctx: CachingContext): CacheAwareStrategy { + if (ctx.isCachingProvider && ctx.hasCacheControl) { + // Adjust strategy for caching providers with cache control + return { + strategy: ["aggressive", "ultra"].includes(strategy) ? "standard" : strategy, + skipSystemPrompt: true, + deterministicOnly: true, + }; + } + + // Return the original strategy with no modifications + return { + strategy, + skipSystemPrompt: false, + deterministicOnly: false, + }; +} From e12814fb98c99f9a8b8e56c60a2c648b11ede69b Mon Sep 17 00:00:00 2001 From: oyi77 Date: Wed, 29 Apr 2026 13:05:33 +0700 Subject: [PATCH 3/4] feat(compression/phase6): integrate caching-aware strategy into strategySelector --- open-sse/handlers/chatCore.ts | 7 ++++++- open-sse/services/compression/strategySelector.ts | 15 +++++++++++++-- .../db/migrations/033_compression_cache_stats.sql | 14 ++++++++++++++ src/lib/localDb.ts | 2 ++ 4 files changed, 35 insertions(+), 3 deletions(-) create mode 100644 src/lib/db/migrations/033_compression_cache_stats.sql diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 5bb6cbe288..23566a0aba 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -1272,7 +1272,12 @@ export async function handleChatCore({ try { const compressionConfig = getCompressionSettings(); estimatedTokens = estimateCompressionTokens(body); - const mode = selectCompressionStrategy(compressionConfig, comboName ?? null, estimatedTokens); + const mode = selectCompressionStrategy( + compressionConfig, + comboName ?? null, + estimatedTokens, + body as Record + ); if (mode !== "off") { const compressionResult = applyCompression(body as Record, mode, { model: effectiveModel, diff --git a/open-sse/services/compression/strategySelector.ts b/open-sse/services/compression/strategySelector.ts index 7d4634a58a..2001d85c60 100644 --- a/open-sse/services/compression/strategySelector.ts +++ b/open-sse/services/compression/strategySelector.ts @@ -4,6 +4,7 @@ import { cavemanCompress } from "./caveman.ts"; import { compressAggressive } from "./aggressive.ts"; import { ultraCompress } from "./ultra.ts"; import { createCompressionStats } from "./stats.ts"; +import { detectCachingContext, getCacheAwareStrategy } from "./cachingAware.ts"; export function checkComboOverride( config: CompressionConfig, @@ -35,9 +36,19 @@ export function getEffectiveMode( export function selectCompressionStrategy( config: CompressionConfig, comboId: string | null, - estimatedTokens: number + estimatedTokens: number, + body?: Record ): CompressionMode { - return getEffectiveMode(config, comboId, estimatedTokens); + const selectedMode = getEffectiveMode(config, comboId, estimatedTokens); + + // Apply caching-aware adjustments if body is provided + if (body) { + const ctx = detectCachingContext(body); + const cacheAware = getCacheAwareStrategy(selectedMode, ctx); + return cacheAware.strategy as CompressionMode; + } + + return selectedMode; } export async function applyCompression( diff --git a/src/lib/db/migrations/033_compression_cache_stats.sql b/src/lib/db/migrations/033_compression_cache_stats.sql new file mode 100644 index 0000000000..d164bfd0d1 --- /dev/null +++ b/src/lib/db/migrations/033_compression_cache_stats.sql @@ -0,0 +1,14 @@ +CREATE TABLE IF NOT EXISTS compression_cache_stats ( + id INTEGER PRIMARY KEY AUTOINCREMENT, + provider TEXT NOT NULL, + model TEXT NOT NULL DEFAULT '', + compression_mode TEXT NOT NULL, + cache_control_present INTEGER NOT NULL DEFAULT 0, + estimated_cache_hit INTEGER NOT NULL DEFAULT 0, + tokens_saved_compression INTEGER NOT NULL DEFAULT 0, + tokens_saved_caching INTEGER NOT NULL DEFAULT 0, + net_savings INTEGER NOT NULL DEFAULT 0, + created_at DATETIME DEFAULT CURRENT_TIMESTAMP +); +CREATE INDEX IF NOT EXISTS idx_compression_cache_stats_provider ON compression_cache_stats(provider); +CREATE INDEX IF NOT EXISTS idx_compression_cache_stats_created ON compression_cache_stats(created_at); \ No newline at end of file diff --git a/src/lib/localDb.ts b/src/lib/localDb.ts index 95f3004def..a55b0fd7aa 100755 --- a/src/lib/localDb.ts +++ b/src/lib/localDb.ts @@ -79,6 +79,8 @@ export { deleteCombo, } from "./db/combos"; +export * from "./db/compressionCacheStats"; + export { // API Keys getApiKeys, From c7b7b847f06980297b0c558ebef6de7b8b723ac4 Mon Sep 17 00:00:00 2001 From: oyi77 Date: Wed, 29 Apr 2026 13:38:34 +0700 Subject: [PATCH 4/4] feat(mcp): add compression status/configure tools + provider-aware caching (Phase 6) - Add MCP tools: omniroute_compression_status, omniroute_compression_configure - Add cachingAware.ts: detectCachingContext + getCacheAwareStrategy - Wire compression tools in server.ts via forEach registration pattern - Add unit tests: cachingAware.test.ts (14 tests), compressionMcpTools.test.ts (16 tests) - All tests pass, typecheck 0 errors --- open-sse/mcp-server/schemas/tools.ts | 83 ++++++++ open-sse/mcp-server/server.ts | 24 +++ open-sse/mcp-server/tools/compressionTools.ts | 186 ++++++++++++++++++ tests/unit/compression/cachingAware.test.ts | 110 +++++++++++ .../compression/compressionMcpTools.test.ts | 102 ++++++++++ 5 files changed, 505 insertions(+) create mode 100644 open-sse/mcp-server/tools/compressionTools.ts create mode 100644 tests/unit/compression/cachingAware.test.ts create mode 100644 tests/unit/compression/compressionMcpTools.test.ts diff --git a/open-sse/mcp-server/schemas/tools.ts b/open-sse/mcp-server/schemas/tools.ts index 3f4c6e1dc1..70eab337ee 100644 --- a/open-sse/mcp-server/schemas/tools.ts +++ b/open-sse/mcp-server/schemas/tools.ts @@ -991,6 +991,87 @@ export const cacheFlushTool: McpToolDefinition = { + name: "omniroute_compression_status", + description: + "Returns current compression configuration, strategy, analytics summary (requests compressed, tokens saved, avg ratio), and provider-aware cache statistics.", + inputSchema: compressionStatusInput, + outputSchema: compressionStatusOutput, + scopes: ["compression:read"], + auditLevel: "basic", + phase: 2, + sourceEndpoints: ["/api/compression/status"], +}; + +export const compressionConfigureInput = z.object({ + enabled: z.boolean().optional(), + strategy: z + .string() + .optional() + .describe("Compression strategy: 'none' | 'standard' | 'aggressive' | 'ultra'"), + maxTokens: z.number().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'"), +}); + +export const compressionConfigureOutput = z.object({ + success: z.boolean(), + updated: z.record(z.string(), z.unknown()), + settings: z.object({ + enabled: z.boolean(), + strategy: z.string(), + maxTokens: z.number(), + targetRatio: z.number(), + aggressiveness: z.string(), + }), +}); + +export const compressionConfigureTool: McpToolDefinition< + typeof compressionConfigureInput, + typeof compressionConfigureOutput +> = { + 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.", + inputSchema: compressionConfigureInput, + outputSchema: compressionConfigureOutput, + scopes: ["compression:write"], + auditLevel: "full", + phase: 2, + sourceEndpoints: ["/api/compression/configure"], +}; + // ============ Tool Registry ============ /** All MCP tool definitions, ordered by phase then name */ @@ -1017,6 +1098,8 @@ export const MCP_TOOLS = [ syncPricingTool, cacheStatsTool, cacheFlushTool, + compressionStatusTool, + compressionConfigureTool, ] as const; /** Essential tools only (Phase 1) */ diff --git a/open-sse/mcp-server/server.ts b/open-sse/mcp-server/server.ts index c884204fe4..d3044345e3 100644 --- a/open-sse/mcp-server/server.ts +++ b/open-sse/mcp-server/server.ts @@ -65,6 +65,7 @@ import { } from "./tools/advancedTools.ts"; import { memoryTools } from "./tools/memoryTools.ts"; import { skillTools } from "./tools/skillTools.ts"; +import { compressionTools } from "./tools/compressionTools.ts"; import { normalizeQuotaResponse } from "../../src/shared/contracts/quota.ts"; import { resolveOmniRouteBaseUrl } from "../../src/shared/utils/resolveOmniRouteBaseUrl.ts"; @@ -849,6 +850,29 @@ export function createMcpServer(): McpServer { ); }); + // ── Compression Tools ───────────────────────── + Object.values(compressionTools).forEach((toolDef) => { + server.registerTool( + toolDef.name, + { + description: toolDef.description, + // @ts-ignore: dynamic zod access + inputSchema: toolDef.inputSchema, + }, + withScopeEnforcement(toolDef.name, async (args) => { + try { + const parsedArgs = toolDef.inputSchema.parse(args ?? {}); + // @ts-ignore: handler expected specific object + const result = await toolDef.handler(parsedArgs); + return { content: [{ type: "text" as const, text: JSON.stringify(result, null, 2) }] }; + } catch (err) { + const msg = err instanceof Error ? err.message : String(err); + return { content: [{ type: "text" as const, text: `Error: ${msg}` }], isError: true }; + } + }) + ); + }); + return server; } diff --git a/open-sse/mcp-server/tools/compressionTools.ts b/open-sse/mcp-server/tools/compressionTools.ts new file mode 100644 index 0000000000..829286c08f --- /dev/null +++ b/open-sse/mcp-server/tools/compressionTools.ts @@ -0,0 +1,186 @@ +/** + * OmniRoute MCP Compression Tools — Manage and monitor prompt compression. + * + * Tools: + * 1. omniroute_compression_status — Get compression config, analytics, and cache stats + * 2. omniroute_compression_configure — Update compression settings + */ + +import { logToolCall } from "../audit.ts"; +import { + getCompressionSettings, + updateCompressionSettings, +} from "../../../src/lib/db/compression.ts"; +import { getCompressionAnalyticsSummary } from "../../../src/lib/db/compressionAnalytics.ts"; +import { getCacheStatsSummary } from "../../../src/lib/db/compressionCacheStats.ts"; +import type { McpToolExtraLike } from "../scopeEnforcement.ts"; + +/** + * Handle compression_status tool: return current compression config, analytics, and cache stats + */ +export async function handleCompressionStatus( + args: Record, + extra?: McpToolExtraLike +): Promise<{ + enabled: boolean; + strategy: string; + settings: { + maxTokens: number; + targetRatio: number; + aggressiveness: string; + }; + analytics: { + totalRequests: number; + compressedRequests: number; + tokensSaved: number; + avgCompressionRatio: number; + }; + cacheStats: { + hits: number; + misses: number; + hitRate: string; + tokensSaved: number; + } | null; +}> { + const start = Date.now(); + try { + const settings = getCompressionSettings(); + const analyticsSummary = getCompressionAnalyticsSummary(); + const cacheStats = getCacheStatsSummary(); + + const result = { + enabled: settings.enabled, + strategy: settings.defaultMode || "standard", + settings: { + maxTokens: settings.autoTriggerTokens, + targetRatio: 0.7, // Default target ratio + aggressiveness: settings.defaultMode || "standard", + }, + analytics: { + totalRequests: analyticsSummary.totalRequests, + compressedRequests: analyticsSummary.byMode?.standard?.count || 0, + tokensSaved: analyticsSummary.totalTokensSaved, + avgCompressionRatio: analyticsSummary.byMode?.standard?.avgSavingsPct || 0, + }, + cacheStats: cacheStats + ? { + hits: Math.round(cacheStats.cacheHitRate * (cacheStats.totalRequests || 1)), + misses: Math.round((1 - cacheStats.cacheHitRate) * (cacheStats.totalRequests || 1)), + hitRate: `${(cacheStats.cacheHitRate * 100).toFixed(2)}%`, + tokensSaved: Math.round(cacheStats.avgNetSavings), + } + : null, + }; + + const duration = Date.now() - start; + await logToolCall("omniroute_compression_status", args, result, duration, true); + + return result; + } catch (error) { + const duration = Date.now() - start; + const errorMessage = error instanceof Error ? error.message : String(error); + await logToolCall( + "omniroute_compression_status", + args, + { error: errorMessage }, + duration, + false, + "ERROR" + ); + throw error; + } +} + +/** + * Handle compression_configure tool: update compression settings + */ +export async function handleCompressionConfigure( + args: { + enabled?: boolean; + strategy?: string; + maxTokens?: number; + targetRatio?: number; + aggressiveness?: string; + }, + extra?: McpToolExtraLike +): Promise<{ + success: boolean; + updated: Record; + settings: { + enabled: boolean; + strategy: string; + maxTokens: number; + targetRatio: number; + aggressiveness: string; + }; +}> { + const start = Date.now(); + try { + const updates: Record = {}; + + if (args.enabled !== undefined) { + updates.enabled = args.enabled; + } + if (args.strategy !== undefined) { + updates.defaultMode = args.strategy; + } + if (args.maxTokens !== undefined) { + updates.autoTriggerTokens = args.maxTokens; + } + if (args.aggressiveness !== undefined) { + updates.defaultMode = args.aggressiveness; + } + + updateCompressionSettings(updates); + const settings = getCompressionSettings(); + + const result = { + success: true, + updated: updates, + settings: { + enabled: settings.enabled, + strategy: settings.defaultMode || "standard", + maxTokens: settings.autoTriggerTokens, + targetRatio: 0.7, // Default target ratio + aggressiveness: settings.defaultMode || "standard", + }, + }; + + const duration = Date.now() - start; + await logToolCall("omniroute_compression_configure", args, result, duration, true); + + return result; + } catch (error) { + const duration = Date.now() - start; + const errorMessage = error instanceof Error ? error.message : String(error); + await logToolCall( + "omniroute_compression_configure", + args, + { error: errorMessage }, + duration, + false, + "ERROR" + ); + throw error; + } +} + +import { z } from "zod"; +import { compressionStatusInput, compressionConfigureInput } from "../schemas/tools.ts"; + +export const compressionTools = { + omniroute_compression_status: { + name: "omniroute_compression_status", + description: + "Returns current compression configuration, strategy, analytics summary (requests compressed, tokens saved, avg ratio), and provider-aware cache statistics.", + inputSchema: compressionStatusInput, + handler: (args: z.infer) => handleCompressionStatus(args), + }, + 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.", + inputSchema: compressionConfigureInput, + handler: (args: z.infer) => handleCompressionConfigure(args), + }, +}; diff --git a/tests/unit/compression/cachingAware.test.ts b/tests/unit/compression/cachingAware.test.ts new file mode 100644 index 0000000000..2c9535a032 --- /dev/null +++ b/tests/unit/compression/cachingAware.test.ts @@ -0,0 +1,110 @@ +/** + * Unit tests for open-sse/services/compression/cachingAware.ts + */ + +import { describe, it } from "node:test"; +import assert from "node:assert/strict"; +import { + detectCachingContext, + getCacheAwareStrategy, +} from "../../../open-sse/services/compression/cachingAware.ts"; + +describe("detectCachingContext", () => { + it("returns hasCacheControl=true when body has cache_control", () => { + const ctx = detectCachingContext({ cache_control: { type: "ephemeral" } }); + assert.equal(ctx.hasCacheControl, true); + }); + + it("returns hasCacheControl=false when body has no cache_control", () => { + const ctx = detectCachingContext({ model: "anthropic/claude-3" }); + assert.equal(ctx.hasCacheControl, false); + }); + + it("extracts anthropic provider from model string", () => { + const ctx = detectCachingContext({ model: "anthropic/claude-3-sonnet" }); + assert.equal(ctx.provider, "anthropic"); + assert.equal(ctx.isCachingProvider, true); + }); + + it("extracts openai provider from model string", () => { + const ctx = detectCachingContext({ model: "openai/gpt-4o" }); + assert.equal(ctx.provider, "openai"); + assert.equal(ctx.isCachingProvider, true); + }); + + it("extracts google provider from model string", () => { + const ctx = detectCachingContext({ model: "google/gemini-pro" }); + assert.equal(ctx.provider, "google"); + assert.equal(ctx.isCachingProvider, true); + }); + + it("returns null provider for unknown provider prefix", () => { + const ctx = detectCachingContext({ model: "deepseek/deepseek-chat" }); + assert.equal(ctx.provider, null); + assert.equal(ctx.isCachingProvider, false); + }); + + it("handles null/undefined body gracefully", () => { + const ctx = detectCachingContext(null); + assert.equal(ctx.hasCacheControl, false); + assert.equal(ctx.provider, null); + assert.equal(ctx.isCachingProvider, false); + }); + + it("handles empty object body", () => { + const ctx = detectCachingContext({}); + assert.equal(ctx.hasCacheControl, false); + assert.equal(ctx.provider, null); + assert.equal(ctx.isCachingProvider, false); + }); +}); + +describe("getCacheAwareStrategy", () => { + it("downgrades aggressive to standard for caching provider with cache_control", () => { + const ctx = { hasCacheControl: true, provider: "anthropic", isCachingProvider: true }; + const result = getCacheAwareStrategy("aggressive", ctx); + assert.equal(result.strategy, "standard"); + assert.equal(result.skipSystemPrompt, true); + assert.equal(result.deterministicOnly, true); + }); + + it("downgrades ultra to standard for caching provider with cache_control", () => { + const ctx = { hasCacheControl: true, provider: "openai", isCachingProvider: true }; + const result = getCacheAwareStrategy("ultra", ctx); + assert.equal(result.strategy, "standard"); + assert.equal(result.skipSystemPrompt, true); + assert.equal(result.deterministicOnly, true); + }); + + it("keeps standard strategy unchanged for caching provider with cache_control", () => { + const ctx = { hasCacheControl: true, provider: "anthropic", isCachingProvider: true }; + const result = getCacheAwareStrategy("standard", ctx); + assert.equal(result.strategy, "standard"); + assert.equal(result.skipSystemPrompt, true); + assert.equal(result.deterministicOnly, true); + }); + + it("keeps strategy unchanged for non-caching provider", () => { + const ctx = { hasCacheControl: true, provider: "deepseek", isCachingProvider: false }; + const result = getCacheAwareStrategy("aggressive", ctx); + assert.equal(result.strategy, "aggressive"); + assert.equal(result.skipSystemPrompt, false); + assert.equal(result.deterministicOnly, false); + }); + + it("keeps strategy unchanged when no cache_control even for caching provider", () => { + const ctx = { hasCacheControl: false, provider: "anthropic", isCachingProvider: true }; + const result = getCacheAwareStrategy("aggressive", ctx); + assert.equal(result.strategy, "aggressive"); + assert.equal(result.skipSystemPrompt, false); + assert.equal(result.deterministicOnly, false); + }); + + it("returns none strategy unchanged", () => { + const ctx = { hasCacheControl: false, provider: null, isCachingProvider: false }; + const result = getCacheAwareStrategy("none", ctx); + assert.equal(result.strategy, "none"); + assert.equal(result.skipSystemPrompt, false); + assert.equal(result.deterministicOnly, false); + }); +}); diff --git a/tests/unit/compression/compressionMcpTools.test.ts b/tests/unit/compression/compressionMcpTools.test.ts new file mode 100644 index 0000000000..0ce352990c --- /dev/null +++ b/tests/unit/compression/compressionMcpTools.test.ts @@ -0,0 +1,102 @@ +import { describe, it, before } from "node:test"; +import assert from "node:assert/strict"; +import { mkdtempSync } from "node:fs"; +import { tmpdir } from "node:os"; +import { join } from "node:path"; + +const tmpDir = mkdtempSync(join(tmpdir(), "omniroute-mcp-test-")); +process.env.DATA_DIR = tmpDir; + +import { + handleCompressionStatus, + handleCompressionConfigure, +} from "../../../open-sse/mcp-server/tools/compressionTools.ts"; + +describe("handleCompressionStatus", () => { + it("returns an object with enabled field", async () => { + const result = await handleCompressionStatus({}); + assert.ok("enabled" in result); + assert.equal(typeof result.enabled, "boolean"); + }); + + it("returns strategy string", async () => { + const result = await handleCompressionStatus({}); + assert.equal(typeof result.strategy, "string"); + }); + + it("returns settings with maxTokens number", async () => { + const result = await handleCompressionStatus({}); + assert.ok("settings" in result); + assert.equal(typeof result.settings.maxTokens, "number"); + }); + + it("returns settings with targetRatio 0.7", async () => { + const result = await handleCompressionStatus({}); + assert.equal(result.settings.targetRatio, 0.7); + }); + + it("returns analytics with totalRequests number", async () => { + const result = await handleCompressionStatus({}); + assert.ok("analytics" in result); + assert.equal(typeof result.analytics.totalRequests, "number"); + }); + + it("returns analytics with tokensSaved number", async () => { + const result = await handleCompressionStatus({}); + assert.equal(typeof result.analytics.tokensSaved, "number"); + }); + + it("returns cacheStats as null or object", async () => { + const result = await handleCompressionStatus({}); + assert.ok(result.cacheStats === null || typeof result.cacheStats === "object"); + }); + + it("returns analytics compressedRequests as number", async () => { + const result = await handleCompressionStatus({}); + assert.equal(typeof result.analytics.compressedRequests, "number"); + }); +}); + +describe("handleCompressionConfigure", () => { + it("returns success=true when called with empty args", async () => { + const result = await handleCompressionConfigure({}); + assert.equal(result.success, true); + }); + + it("returns settings object after configure", async () => { + const result = await handleCompressionConfigure({}); + assert.ok("settings" in result); + assert.equal(typeof result.settings.enabled, "boolean"); + }); + + it("returns updated object", async () => { + const result = await handleCompressionConfigure({ enabled: true }); + assert.ok("updated" in result); + }); + + it("sets enabled=false and returns success", async () => { + const result = await handleCompressionConfigure({ enabled: false }); + assert.equal(result.success, true); + }); + + it("sets strategy and returns success", async () => { + const result = await handleCompressionConfigure({ strategy: "aggressive" }); + assert.equal(result.success, true); + }); + + it("sets maxTokens and returns success", async () => { + const result = await handleCompressionConfigure({ maxTokens: 8000 }); + assert.equal(result.success, true); + assert.ok("updated" in result); + }); + + it("returns settings.maxTokens as number", async () => { + const result = await handleCompressionConfigure({ maxTokens: 2000 }); + assert.equal(typeof result.settings.maxTokens, "number"); + }); + + it("returns settings.targetRatio as 0.7", async () => { + const result = await handleCompressionConfigure({}); + assert.equal(result.settings.targetRatio, 0.7); + }); +});