From 75894161e46aafe30a7db4ecc946af60f04495e4 Mon Sep 17 00:00:00 2001 From: Timothy Jaeryang Baek Date: Thu, 16 Jul 2026 02:35:44 -0400 Subject: [PATCH] refac --- backend/open_webui/main.py | 2 + .../open_webui/utils/context_compaction.py | 5 + .../admin/Settings/Interface.svelte | 12 ++ src/lib/components/chat/Chat.svelte | 23 +++- src/lib/components/chat/MessageInput.svelte | 109 +++++++++++++++--- .../MessageInput/CommandSuggestionList.svelte | 6 +- .../Commands/SlashCommands.svelte | 59 +++++----- src/lib/components/chat/SettingsModal.svelte | 31 ++--- src/lib/stores/index.ts | 1 + 9 files changed, 188 insertions(+), 60 deletions(-) diff --git a/backend/open_webui/main.py b/backend/open_webui/main.py index af27738a4a..38da5d2c86 100644 --- a/backend/open_webui/main.py +++ b/backend/open_webui/main.py @@ -1948,6 +1948,7 @@ async def get_app_config(request: Request): 'calendar.enable', 'automations.enable', 'notes.enable', + 'chat.context_compaction.enable', 'web.search.enable', 'web.search.confirmation.enable', 'web.search.confirmation.content', @@ -2017,6 +2018,7 @@ async def get_app_config(request: Request): 'enable_calendar': config.get('calendar.enable'), 'enable_automations': config.get('automations.enable'), 'enable_notes': config.get('notes.enable'), + 'enable_context_compaction': config.get('chat.context_compaction.enable'), 'enable_web_search': config.get('web.search.enable'), 'enable_web_search_confirmation': config.get('web.search.confirmation.enable'), 'web_search_confirmation_content': config.get('web.search.confirmation.content'), diff --git a/backend/open_webui/utils/context_compaction.py b/backend/open_webui/utils/context_compaction.py index ad18ea175f..e9a50599ae 100644 --- a/backend/open_webui/utils/context_compaction.py +++ b/backend/open_webui/utils/context_compaction.py @@ -145,6 +145,8 @@ async def compact_messages_for_request( async def compact_chat_branch(request, user, chat: Any, model_id: str, models: dict) -> dict: config = await _load_config() + if not config['enable']: + return {'ok': True, 'compacted': False, 'reason': 'disabled'} history = (chat.chat or {}).get('history') or {} current_id = history.get('currentId') @@ -225,6 +227,9 @@ async def get_chat_context_usage(chat: Any, model_id: str | None = None) -> dict return None config = await _load_config() + if not config['enable']: + return None + params = ((chat.chat or {}).get('params') or {}).copy() if model_id: params['model'] = model_id diff --git a/src/lib/components/admin/Settings/Interface.svelte b/src/lib/components/admin/Settings/Interface.svelte index 0b92b86c48..5d56aee356 100644 --- a/src/lib/components/admin/Settings/Interface.svelte +++ b/src/lib/components/admin/Settings/Interface.svelte @@ -13,6 +13,7 @@ import AdminSettingField from './AdminSettingField.svelte'; import AdminSettingRow from './AdminSettingRow.svelte'; import AdminSettingSection from './AdminSettingSection.svelte'; + import { config as appConfig } from '$lib/stores'; const dispatch = createEventDispatcher(); @@ -51,6 +52,17 @@ updateTaskConfig(localStorage.token, taskConfig), updateChatConfig(localStorage.token, chatConfig) ]); + appConfig.update((current) => + current + ? { + ...current, + features: { + ...current.features, + enable_context_compaction: chatConfig.ENABLE_CONTEXT_COMPACTION + } + } + : current + ); }; let workspaceModels: any[] = []; diff --git a/src/lib/components/chat/Chat.svelte b/src/lib/components/chat/Chat.svelte index fd7b5e397e..7cbfe13451 100644 --- a/src/lib/components/chat/Chat.svelte +++ b/src/lib/components/chat/Chat.svelte @@ -220,6 +220,8 @@ return next; }, 0); + $: contextCompactionEnabled = Boolean($config?.features?.enable_context_compaction); + const getContextThreshold = () => { const chatThreshold = Number(params?.compact_token_threshold); if (Number.isFinite(chatThreshold) && chatThreshold > 0) { @@ -229,7 +231,7 @@ const modelId = atSelectedModel?.id ?? selectedModels.find((id) => id); const model = $models.find((item) => item.id === modelId); const threshold = Number(model?.info?.params?.compact_token_threshold); - return Number.isFinite(threshold) && threshold > 0 ? threshold : 80000; + return Number.isFinite(threshold) && threshold > 0 ? threshold : null; }; const getContextUsage = () => { @@ -238,7 +240,9 @@ } const messages = createMessagesList(history, history.currentId); - const threshold = getContextThreshold(); + const threshold = contextCompactionEnabled + ? (getContextThreshold() ?? serverContextUsage?.threshold ?? null) + : null; const systemTokens = estimateTokens($settings?.system ?? ''); let estimatedTokens = systemTokens; let hasUsageCheckpoint = false; @@ -276,12 +280,12 @@ tokens: estimatedTokens, estimated_tokens: estimatedTokens, threshold, - percent: threshold > 0 ? Math.max(0, Math.round((estimatedTokens / threshold) * 100)) : 0, + percent: threshold > 0 ? Math.max(0, Math.round((estimatedTokens / threshold) * 100)) : null, source: 'estimated' }; }; - $: contextUsage = getContextUsage() ?? serverContextUsage; + $: contextUsage = getContextUsage() ?? (contextCompactionEnabled ? serverContextUsage : null); $: embeddedHeaderTitle = embeddedTitle || $chatTitle || $i18n.t('Chat'); let selectedToolIds = []; @@ -2400,6 +2404,11 @@ }; const handleManualCompact = async () => { + if (!contextCompactionEnabled) { + toast.message($i18n.t('Context compaction is disabled')); + return; + } + if (!$chatId || !history?.currentId) { toast.message($i18n.t('No chat to compact')); return; @@ -2430,7 +2439,9 @@ ? $i18n.t('Chat is too short to compact') : result?.reason === 'empty' ? $i18n.t('No chat to compact') - : $i18n.t('Nothing to compact'); + : result?.reason === 'disabled' + ? $i18n.t('Context compaction is disabled') + : $i18n.t('Nothing to compact'); toast.message(skippedReason, { id: toastId }); } @@ -3751,6 +3762,7 @@ dropzoneId={messageInputDropzoneId} chatId={$chatId} {contextUsage} + {contextCompactionEnabled} compactHandler={handleManualCompact} statusHandler={handleStatusCommand} forkHandler={handleForkChat} @@ -3866,6 +3878,7 @@ dropzoneId={messageInputDropzoneId} chatId={$chatId} {contextUsage} + {contextCompactionEnabled} compactHandler={handleManualCompact} statusHandler={handleStatusCommand} forkHandler={handleForkChat} diff --git a/src/lib/components/chat/MessageInput.svelte b/src/lib/components/chat/MessageInput.svelte index c50c70ff7a..fc60344d13 100644 --- a/src/lib/components/chat/MessageInput.svelte +++ b/src/lib/components/chat/MessageInput.svelte @@ -121,6 +121,7 @@ export let forkHandler: Function = () => {}; export let chatId = ''; export let contextUsage = null; + export let contextCompactionEnabled = false; export let autoScroll = false; export let generating = false; @@ -389,6 +390,79 @@ const trimNumber = (value: number) => value >= 10 ? String(Math.round(value)) : value.toFixed(1).replace(/\.0$/, ''); + const estimateTokens = (value) => { + if (value === null || value === undefined || value === '') { + return 0; + } + if (typeof value !== 'string') { + try { + value = JSON.stringify(value); + } catch { + value = String(value); + } + } + return Math.max(1, Math.floor(value.length / 4)); + }; + + const estimateMessagesTokens = (messages) => + messages.reduce((total, message) => { + let next = total + 4 + estimateTokens(message.content); + next += estimateTokens(message.output); + next += estimateTokens(message.tool_calls); + next += estimateTokens(message.files); + return next; + }, 0); + + const getLocalContextUsage = () => { + if (!history?.currentId) { + return null; + } + + const messages = createMessagesList(history, history.currentId); + if (!messages.length) { + return null; + } + + let summary = ''; + let startIdx = 0; + for (let idx = 0; idx < messages.length; idx += 1) { + const value = messages[idx]?.contextSummary ?? messages[idx]?.context_summary; + if (typeof value === 'string' && value.trim()) { + summary = value; + startIdx = idx; + } + } + + const activeMessages = messages.slice(startIdx); + let estimatedTokens = estimateTokens($settings?.system ?? ''); + let hasUsageCheckpoint = false; + + for (let idx = activeMessages.length - 1; idx >= 0; idx -= 1) { + const usage = activeMessages[idx]?.usage ?? activeMessages[idx]?.info?.usage; + const inputTokens = usage?.input_tokens ?? usage?.prompt_tokens; + if (inputTokens) { + hasUsageCheckpoint = true; + estimatedTokens = + Number(inputTokens || 0) + + Number(usage.output_tokens ?? usage.completion_tokens ?? 0) + + estimateMessagesTokens(activeMessages.slice(idx + 1)); + break; + } + } + + if (!hasUsageCheckpoint) { + estimatedTokens += estimateTokens(summary) + estimateMessagesTokens(activeMessages); + } + + return { + tokens: estimatedTokens, + estimated_tokens: estimatedTokens, + threshold: null, + percent: null, + source: 'estimated' + }; + }; + const copyStatusChatId = async () => { if (!chatId) return; await navigator.clipboard.writeText(chatId); @@ -398,11 +472,18 @@ }, 1600); }; - $: contextPercent = Math.max(0, Math.round(contextUsage?.percent ?? 0)); - $: contextValue = contextUsage - ? `${contextPercent}% ${formatTokenCount(contextUsage.estimated_tokens || contextUsage.tokens)}/${formatTokenCount(contextUsage.threshold)}` + $: statusContextUsage = contextUsage ?? getLocalContextUsage(); + $: contextHasThreshold = Number(statusContextUsage?.threshold) > 0; + $: contextPercent = contextHasThreshold + ? Math.max(0, Math.round(statusContextUsage?.percent ?? 0)) + : null; + $: contextTokens = formatTokenCount(statusContextUsage?.estimated_tokens || statusContextUsage?.tokens || 0); + $: contextValue = statusContextUsage + ? contextHasThreshold + ? `${contextPercent}% ${contextTokens}/${formatTokenCount(statusContextUsage.threshold)}` + : `${contextTokens} ${$i18n.t('tokens')}` : $i18n.t('unknown'); - $: contextBarPercent = Math.min(contextPercent, 100); + $: contextBarPercent = contextHasThreshold ? Math.min(contextPercent, 100) : 0; const getCommand = () => { const chatInput = document.getElementById('chat-input'); @@ -1086,12 +1167,12 @@ char: '/', render: getSuggestionRenderer(CommandSuggestionList, { i18n, - canCompact: () => !!history?.currentId, + canCompact: () => !!history?.currentId && contextCompactionEnabled, compactDisabled: () => isActive, canStatus: () => !!history?.currentId, canFork: () => !!history?.currentId, forkDisabled: () => isActive, - contextUsage: () => contextUsage, + contextUsage: () => statusContextUsage, onCompact: compactHandler, onStatus: statusHandler, onFork: forkHandler, @@ -1436,14 +1517,16 @@ {contextValue} -
+ {#if contextHasThreshold}
-
+ class="mt-1.5 h-0.5 overflow-hidden rounded-full bg-gray-100 dark:bg-white/8" + > +
+ + {/if} {#if messageQueue.length} diff --git a/src/lib/components/chat/MessageInput/CommandSuggestionList.svelte b/src/lib/components/chat/MessageInput/CommandSuggestionList.svelte index 9c3c0b7f6c..ed8cabe693 100644 --- a/src/lib/components/chat/MessageInput/CommandSuggestionList.svelte +++ b/src/lib/components/chat/MessageInput/CommandSuggestionList.svelte @@ -30,7 +30,10 @@ $: forkAvailable = typeof canFork === 'function' ? canFork() : canFork; $: isForkDisabled = typeof forkDisabled === 'function' ? forkDisabled() : forkDisabled; $: resolvedContextUsage = typeof contextUsage === 'function' ? contextUsage() : contextUsage; - $: contextPercent = Math.max(0, Math.round(resolvedContextUsage?.percent ?? 0)); + $: contextHasThreshold = Number(resolvedContextUsage?.threshold) > 0; + $: contextPercent = contextHasThreshold + ? Math.max(0, Math.round(resolvedContextUsage?.percent ?? 0)) + : null; let suggestionElement: any = null; let filteredItems: any[] = []; @@ -85,6 +88,7 @@ canFork={forkAvailable} forkDisabled={isForkDisabled} {contextPercent} + {contextHasThreshold} onSelect={(e) => { const { type, data } = e; diff --git a/src/lib/components/chat/MessageInput/Commands/SlashCommands.svelte b/src/lib/components/chat/MessageInput/Commands/SlashCommands.svelte index fc82d83e46..084379ac90 100644 --- a/src/lib/components/chat/MessageInput/Commands/SlashCommands.svelte +++ b/src/lib/components/chat/MessageInput/Commands/SlashCommands.svelte @@ -15,6 +15,7 @@ export let canFork = false; export let forkDisabled = false; export let contextPercent = 0; + export let contextHasThreshold = false; let selectedIdx = 0; export let filteredItems = []; @@ -23,7 +24,9 @@ let skills = []; let searchDebounceTimer: ReturnType; - $: contextCirclePercent = Math.min(Math.max(0, Math.round(contextPercent)), 100); + $: contextCirclePercent = contextHasThreshold + ? Math.min(Math.max(0, Math.round(contextPercent)), 100) + : 0; $: contextCircleOffset = 50.27 * (1 - contextCirclePercent / 100); $: commandItems = [ @@ -144,34 +147,38 @@ data-selected={commandIdx === selectedIdx} > - + {#if contextHasThreshold} + + {/if} Compact - - {contextCirclePercent}% full - + {#if contextHasThreshold} + + {contextCirclePercent}% full + + {/if} diff --git a/src/lib/components/chat/SettingsModal.svelte b/src/lib/components/chat/SettingsModal.svelte index 37df23fc7f..118a0c0ea2 100644 --- a/src/lib/components/chat/SettingsModal.svelte +++ b/src/lib/components/chat/SettingsModal.svelte @@ -665,6 +665,21 @@ title: 'Sub-agents', keywords: ['sub-agents', 'subagents', 'delegation', 'background', 'agents'] }, + { + id: 'admin:interface', + title: 'Interface', + keywords: ['interface', 'ui', 'appearance', 'banners', 'tasks', 'prompt suggestions', 'tags'] + }, + { + id: 'admin:audio', + title: 'Audio', + keywords: ['audio', 'voice', 'speech', 'tts', 'stt', 'whisper', 'deepgram', 'azure'] + }, + { + id: 'admin:images', + title: 'Images', + keywords: ['images', 'generation', 'dalle', 'stable diffusion', 'comfyui', 'automatic1111'] + }, { id: 'admin:evaluations', title: 'Evaluations', @@ -700,21 +715,7 @@ title: 'Pipelines', keywords: ['pipelines', 'workflows', 'filters', 'valves', 'middleware'] }, - { - id: 'admin:interface', - title: 'Interface', - keywords: ['interface', 'ui', 'appearance', 'banners', 'tasks', 'prompt suggestions', 'tags'] - }, - { - id: 'admin:audio', - title: 'Audio', - keywords: ['audio', 'voice', 'speech', 'tts', 'stt', 'whisper', 'deepgram', 'azure'] - }, - { - id: 'admin:images', - title: 'Images', - keywords: ['images', 'generation', 'dalle', 'stable diffusion', 'comfyui', 'automatic1111'] - }, + { id: 'admin:db', title: 'Database', diff --git a/src/lib/stores/index.ts b/src/lib/stores/index.ts index 16873000a5..4f591f9a87 100644 --- a/src/lib/stores/index.ts +++ b/src/lib/stores/index.ts @@ -322,6 +322,7 @@ type Config = { enable_admin_export: boolean; enable_admin_chat_access: boolean; enable_admin_analytics: boolean; + enable_context_compaction?: boolean; enable_community_sharing: boolean; enable_memories: boolean; enable_plugins?: boolean;