diff --git a/open-sse/handlers/chatCore.ts b/open-sse/handlers/chatCore.ts index 95c0406d72..aeba69dcf4 100644 --- a/open-sse/handlers/chatCore.ts +++ b/open-sse/handlers/chatCore.ts @@ -1569,14 +1569,83 @@ export async function handleChatCore({ const { selectCompressionStrategy, applyCompression } = await import("../services/compression/strategySelector.ts"); const { trackCompressionStats } = await import("../services/compression/stats.ts"); - const config = await getCompressionSettings(); - const mode = selectCompressionStrategy(config, comboName ?? null, estimatedTokens); + let config = await getCompressionSettings(); + let compressionComboKey = comboName ?? null; + if (isCombo && comboName) { + try { + const { getComboByName } = await import("../../src/lib/localDb"); + let comboConfig = await getComboByName(comboName); + if (!comboConfig && comboName.startsWith("combo/")) { + comboConfig = await getComboByName(comboName.substring(6)); + } + const comboRuntimeConfig = + comboConfig?.config && typeof comboConfig.config === "object" + ? (comboConfig.config as Record) + : {}; + const comboMode = + typeof comboRuntimeConfig.compressionMode === "string" + ? comboRuntimeConfig.compressionMode + : typeof comboConfig?.compressionOverride === "string" + ? comboConfig.compressionOverride + : null; + if ( + comboMode === "off" || + comboMode === "lite" || + comboMode === "standard" || + comboMode === "aggressive" || + comboMode === "ultra" + ) { + config = { + ...config, + comboOverrides: { + ...(config.comboOverrides ?? {}), + ...(comboName ? { [comboName]: comboMode } : {}), + ...(comboConfig?.id ? { [comboConfig.id]: comboMode } : {}), + }, + }; + compressionComboKey = comboName; + } + } catch (err) { + log?.debug?.( + "COMPRESSION", + "Combo compression override lookup skipped: " + + (err instanceof Error ? err.message : String(err)) + ); + } + } + const mode = selectCompressionStrategy(config, compressionComboKey, estimatedTokens); if (mode !== "off") { const result = applyCompression(body, 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( + 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)) + ); + } + })(); log?.info?.( "COMPRESSION", `Prompt compressed (${mode}): ${result.stats.originalTokens} -> ${result.stats.compressedTokens} tokens (${result.stats.savingsPercent}% saved, techniques: ${result.stats.techniquesUsed.join(",")})` diff --git a/src/app/(dashboard)/dashboard/analytics/CompressionAnalyticsTab.tsx b/src/app/(dashboard)/dashboard/analytics/CompressionAnalyticsTab.tsx new file mode 100644 index 0000000000..038b4afc71 --- /dev/null +++ b/src/app/(dashboard)/dashboard/analytics/CompressionAnalyticsTab.tsx @@ -0,0 +1,299 @@ +/** + * Compression Analytics Tab + * + * Shows compression request stats from call_logs (request_type = 'compression'), + * mode breakdown, provider breakdown, and cost/token savings summary. + */ + +"use client"; + +import { useEffect, useState } from "react"; + +interface CompressionAnalyticsSummary { + totalRequests: number; + totalTokensSaved: number; + avgSavingsPct: number; + avgDurationMs: number; + byMode: Record; + byProvider: Record; + last24h: Array<{ hour: string; count: number; tokensSaved: number }>; +} + +function StatCard({ + icon, + label, + value, + sub, +}: { + icon: string; + label: string; + value: string | number; + sub?: string; +}) { + return ( +
+
+ {icon} + {label} +
+
{value}
+ {sub &&
{sub}
} +
+ ); +} + +function ModeBar({ + mode, + count, + total, + tokensSaved, +}: { + mode: string; + count: number; + total: number; + tokensSaved: number; +}) { + const pct = total > 0 ? Math.round((count / total) * 100) : 0; + return ( +
+
+ {mode} + + {count} requests · {tokensSaved.toLocaleString()} tokens saved + +
+
+
+
+
{pct}%
+
+ ); +} + +function ProviderBar({ + provider, + count, + total, + tokensSaved, +}: { + provider: string; + count: number; + total: number; + tokensSaved: number; +}) { + const pct = total > 0 ? Math.round((count / total) * 100) : 0; + return ( +
+
+ {provider} + + {count} requests · {tokensSaved.toLocaleString()} tokens saved + +
+
+
+
+
{pct}%
+
+ ); +} + +export default function CompressionAnalyticsTab() { + const [stats, setStats] = useState(null); + const [loading, setLoading] = useState(true); + const [error, setError] = useState(null); + const [since, setSince] = useState<"24h" | "7d" | "30d" | "all">("24h"); + + useEffect(() => { + fetch(`/api/analytics/compression?since=${since}`) + .then((r) => r.json()) + .then((d) => { + setStats(d); + setLoading(false); + }) + .catch((e) => { + setError(e.message); + setLoading(false); + }); + }, [since]); + + if (loading) { + return ( +
+ progress_activity + Loading compression analytics… +
+ ); + } + + if (error || !stats) { + return ( +
+ compress + {error || "No compression data yet."} +

+ Compression requests will appear here after the first request via /v1/chat/completions + with compression enabled. +

+
+ ); + } + + const modes = Object.entries(stats.byMode).sort(([, a], [, b]) => b.count - a.count); + const providers = Object.entries(stats.byProvider).sort(([, a], [, b]) => b.count - a.count); + + // Calculate max tokens for hourly chart scaling + const maxTokensPerHour = Math.max(...stats.last24h.map((h) => h.tokensSaved), 1); + const maxCountPerHour = Math.max(...stats.last24h.map((h) => h.count), 1); + + return ( +
+ {/* Time Range Selector */} +
+ {(["24h", "7d", "30d", "all"] as const).map((range) => ( + + ))} +
+ + {/* KPI Cards */} +
+ + + + +
+ + {/* Mode Breakdown */} + {modes.length > 0 && ( +
+

+ tune + Mode Breakdown +

+
+ {modes.map(([mode, data]) => ( + + ))} +
+
+ )} + + {/* Provider Breakdown */} + {providers.length > 0 && ( +
+

+ hub + Provider Breakdown +

+
+ {providers.map(([prov, data]) => ( + + ))} +
+
+ )} + + {/* Last 24h Hourly Chart (CSS-only height-based bars) */} + {stats.last24h.length > 0 && ( +
+

+ show_chart + Last 24 Hours +

+
+ {stats.last24h.map((entry, idx) => { + const countPct = (entry.count / maxCountPerHour) * 100; + const tokenPct = (entry.tokensSaved / maxTokensPerHour) * 100; + return ( +
+
+
+ {entry.count} +
+
+
+ {entry.hour.substring(0, 2)} +
+
+ ); + })} +
+
+
Max requests/hour: {maxCountPerHour}
+
Max tokens/hour: {maxTokensPerHour.toLocaleString()}
+
+
+ )} + + {/* Empty state */} + {stats.totalRequests === 0 && ( +
+ + compress + +

No compression data yet

+

+ Use POST /v1/chat/completions with + compression configuration to start tracking compression analytics. +

+
+ )} + + {/* Info note */} +
+ info + + Compression analytics: Token savings tracked per mode (off, lite, + standard, aggressive, ultra) and provider. Hover over charts for details. Use the time + selector to view different time periods. + +
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/analytics/page.tsx b/src/app/(dashboard)/dashboard/analytics/page.tsx index ba2022064f..b900d4351c 100644 --- a/src/app/(dashboard)/dashboard/analytics/page.tsx +++ b/src/app/(dashboard)/dashboard/analytics/page.tsx @@ -4,6 +4,7 @@ import { useState, Suspense } from "react"; import { UsageAnalytics, CardSkeleton, SegmentedControl } from "@/shared/components"; import EvalsTab from "../usage/components/EvalsTab"; import SearchAnalyticsTab from "./SearchAnalyticsTab"; +import CompressionAnalyticsTab from "./CompressionAnalyticsTab"; import DiversityScoreCard from "./components/DiversityScoreCard"; import ProviderUtilizationTab from "./ProviderUtilizationTab"; import ComboHealthTab from "./ComboHealthTab"; @@ -19,6 +20,7 @@ export default function AnalyticsPage() { search: "Search request analytics — provider breakdown, cache hit rate, and cost tracking.", utilization: t("utilizationDescription"), comboHealth: t("comboHealthDescription"), + compression: t("compressionAnalyticsDescription"), }; return ( @@ -39,6 +41,7 @@ export default function AnalyticsPage() { { value: "search", label: "Search" }, { value: "utilization", label: t("utilization") }, { value: "comboHealth", label: t("comboHealth") }, + { value: "compression", label: t("compressionAnalyticsTitle") }, ]} value={activeTab} onChange={setActiveTab} @@ -56,6 +59,7 @@ export default function AnalyticsPage() { {activeTab === "search" && } {activeTab === "utilization" && } {activeTab === "comboHealth" && } + {activeTab === "compression" && }
); } diff --git a/src/app/(dashboard)/dashboard/combos/page.tsx b/src/app/(dashboard)/dashboard/combos/page.tsx index 73a275e506..d992b9f916 100644 --- a/src/app/(dashboard)/dashboard/combos/page.tsx +++ b/src/app/(dashboard)/dashboard/combos/page.tsx @@ -1524,6 +1524,36 @@ function ComboCard({ const tc = useTranslations("common"); const emailsVisible = useEmailPrivacyStore((s) => s.emailsVisible); const strategyDescription = getStrategyDescription(t, strategy); + const initialCompressionMode = combo?.config?.compressionMode || combo.compressionOverride || ""; + const [compressionOverride, setCompressionOverride] = useState(initialCompressionMode); + const [isSavingCompression, setIsSavingCompression] = useState(false); + + const handleCompressionOverrideChange = async (value) => { + setCompressionOverride(value); + setIsSavingCompression(true); + const nextConfig = { ...(combo.config || {}) }; + if (value) { + nextConfig.compressionMode = value; + } else { + delete nextConfig.compressionMode; + } + try { + const response = await fetch(`/api/combos/${combo.id}`, { + method: "PUT", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ config: nextConfig }), + }); + if (!response.ok) { + console.error("Failed to update compression override"); + setCompressionOverride(initialCompressionMode); + } + } catch (error) { + console.error("Error updating compression override:", error); + setCompressionOverride(initialCompressionMode); + } finally { + setIsSavingCompression(false); + } + }; return (
-
+
+
+ ); +} diff --git a/src/app/(dashboard)/dashboard/translator/components/PlaygroundMode.tsx b/src/app/(dashboard)/dashboard/translator/components/PlaygroundMode.tsx index 13ecdb958e..b99c176ea1 100644 --- a/src/app/(dashboard)/dashboard/translator/components/PlaygroundMode.tsx +++ b/src/app/(dashboard)/dashboard/translator/components/PlaygroundMode.tsx @@ -9,6 +9,15 @@ import dynamic from "next/dynamic"; const Editor = dynamic(() => import("@monaco-editor/react"), { ssr: false }); +interface CompressionPreviewResult { + originalTokens: number; + compressedTokens: number; + tokensSaved: number; + savingsPct: number; + techniquesUsed: string[]; + durationMs: number; +} + export default function PlaygroundMode() { const t = useTranslations("translator"); const tc = useTranslations("common"); @@ -22,6 +31,14 @@ export default function PlaygroundMode() { const [translating, setTranslating] = useState(false); const [detecting, setDetecting] = useState(false); const [activeTemplate, setActiveTemplate] = useState(null); + + // Compression preview state + const [compressionMode, setCompressionMode] = useState("standard"); + const [compressionResult, setCompressionResult] = useState(null); + const [compressionLoading, setCompressionLoading] = useState(false); + const [compressionError, setCompressionError] = useState(null); + const [showCompressionPanel, setShowCompressionPanel] = useState(false); + const templates = useMemo(() => getExampleTemplates(t), [t]); // Auto-detect format when input changes @@ -152,6 +169,33 @@ export default function PlaygroundMode() { setDetectedFormat(null); }; + const handleCompressionPreview = async () => { + if (!inputContent.trim()) return; + let messages; + try { + const parsed = JSON.parse(inputContent); + messages = parsed.messages ?? [{ role: "user", content: inputContent }]; + } catch { + messages = [{ role: "user", content: inputContent }]; + } + setCompressionLoading(true); + setCompressionError(null); + try { + const res = await fetch("/api/compression/preview", { + method: "POST", + headers: { "Content-Type": "application/json" }, + body: JSON.stringify({ messages, mode: compressionMode }), + }); + const data = await res.json(); + if (!res.ok) throw new Error(data.error ?? "Preview failed"); + setCompressionResult(data); + } catch (e: unknown) { + setCompressionError(e instanceof Error ? e.message : String(e)); + } finally { + setCompressionLoading(false); + } + }; + const srcMeta = FORMAT_META[sourceFormat] || FORMAT_META.openai; const tgtMeta = FORMAT_META[targetFormat] || FORMAT_META.openai; @@ -461,6 +505,85 @@ export default function PlaygroundMode() { )}
+ + {/* Compression Preview Panel */} + + + + {showCompressionPanel && ( +
+
+