front: configurable compaction model + live progress to the client

Auto-compaction can now summarize with a DIFFERENT model than the one
serving the request, with a global default (config.json `compaction`) and
a per-model override (models.json `auto_compact_model`). Empty = the
request's own model, as before.

- config: new CompactionConfig (enabled/pct/strategy/model) + round-trip
- text.py: resolve effective settings (per-model over global), resolve the
  summarizer LAZILY (only when actually over threshold, so a separate model
  isn't loaded on every request); map-reduce the dropped history into chunks
  sized to the CHOSEN summarizer's own context, reducing iteratively until
  it fits; stream status + live per-chunk progress to the client as content
  deltas (queue-bridged from the summarizer's callback)
- admin: global compaction card (settings) + per-model summarizer dropdown
  (models, shown only for the summarize strategy)

Raw two-pass path is skipped (prompt is built from system + last user turn).
Co-Authored-By: 's avatarClaude Opus 4.8 <noreply@anthropic.com>
parent 2b2bdc72
......@@ -2682,7 +2682,8 @@ async def api_model_configure(request: Request, username: str = Depends(require_
"balanced_gpu_percent", "acceleration",
"cache_type_k", "cache_type_v", "turboquant", "engine", "engine_fallback",
"quant_backend", "kv_cache_budget_mb", "kv_cache_slots", "mmproj",
"auto_compact", "auto_compact_pct", "auto_compact_strategy"):
"auto_compact", "auto_compact_pct", "auto_compact_strategy",
"auto_compact_model"):
if key in data:
entry[key] = data[key]
......@@ -3432,6 +3433,12 @@ async def api_get_settings(username: str = Depends(require_admin)):
"extra_env": c.ds4.extra_env,
"auto_build": c.ds4.auto_build,
},
"compaction": {
"enabled": c.compaction.enabled,
"pct": c.compaction.pct,
"strategy": c.compaction.strategy,
"model": c.compaction.model,
},
"broker": {
"enabled": c.broker.enabled,
"base_url": c.broker.base_url,
......@@ -3720,6 +3727,22 @@ async def api_save_settings(request: Request, username: str = Depends(require_ad
if "auto_build" in d:
c.ds4.auto_build = bool(d["auto_build"])
if "compaction" in data:
cp = data["compaction"] or {}
if "enabled" in cp:
c.compaction.enabled = bool(cp["enabled"])
if "pct" in cp:
try:
c.compaction.pct = max(50, min(99, int(cp["pct"])))
except (TypeError, ValueError):
pass
if "strategy" in cp:
_st = (cp.get("strategy") or "drop_oldest").strip()
if _st in ("drop_oldest", "keep_head_tail", "summarize"):
c.compaction.strategy = _st
if "model" in cp:
c.compaction.model = (cp.get("model") or "").strip()
if "broker" in data:
bro = data["broker"]
c.broker.enabled = bool(bro.get("enabled", c.broker.enabled))
......
......@@ -707,12 +707,19 @@ window.__DEFAULT_WHISPER_SERVER_PATH__ = {{ default_whisper_server_path|tojson }
</div>
<div class="form-row" style="margin:0">
<label class="form-label">Strategy</label>
<select id="cfg-autocompact-strategy" class="form-input">
<select id="cfg-autocompact-strategy" class="form-input" onchange="_toggleAutoCompact()">
<option value="drop_oldest">Drop oldest — keep system + most recent turns</option>
<option value="keep_head_tail">Keep head + tail — also keep the first turn, drop the middle</option>
<option value="summarize">Summarize — replace the dropped middle with an LLM summary</option>
</select>
</div>
<div class="form-row" id="cfg-autocompact-model-row" style="margin:0;grid-column:1/-1;display:none">
<label class="form-label">Summarizer model <span class="muted">(summarize only)</span></label>
<select id="cfg-autocompact-model" class="form-input">
<option value="">Same as request model (default)</option>
</select>
<span class="form-hint" style="font-size:11px">A separate (e.g. smaller/faster) model can write the summary while the main model answers. The dropped history is chunked to fit the chosen model's own context. Empty = the request's own model summarizes. Leave empty to inherit the global default.</span>
</div>
</div>
<div style="display:flex;gap:1.5rem;flex-wrap:wrap;margin-top:.75rem">
......@@ -2812,6 +2819,36 @@ async function freeDiskConfirm(idx){
function _toggleAutoCompact(){
const on = document.getElementById('cfg-autocompact').checked;
document.getElementById('cfg-autocompact-opts').style.display = on ? 'grid' : 'none';
const strat = document.getElementById('cfg-autocompact-strategy').value;
const mrow = document.getElementById('cfg-autocompact-model-row');
if(mrow) mrow.style.display = (on && strat === 'summarize') ? 'block' : 'none';
}
// Populate the summarizer-model dropdown from the configured text/LLM models,
// preserving the current selection.
function _populateCompactModelSelect(selected){
const sel = document.getElementById('cfg-autocompact-model');
if(!sel) return;
const cur = selected != null ? selected : sel.value;
const seen = new Set();
let html = '<option value="">Same as request model (default)</option>';
(_localModels||[]).forEach(m=>{
const caps = m.capabilities||[];
const type = m.defaultType||'text_models';
const isText = type==='text_models' || type==='gguf_models' || type==='vision_models'
|| caps.includes('chat') || caps.includes('text') || caps.includes('tools');
if(!isText) return;
const id = m.path || m.label;
if(!id || seen.has(id)) return;
seen.add(id);
html += `<option value="${esc(id)}">${esc(m.label||id)}</option>`;
});
// Keep a previously-saved value even if its model isn't in the local list.
if(cur && !seen.has(cur)){
html += `<option value="${esc(cur)}">${esc(cur)}</option>`;
}
sel.innerHTML = html;
sel.value = cur || '';
}
/* ── type checkbox helpers ─────────────────────────────── */
......@@ -3197,6 +3234,7 @@ function openCfgModal(idx, cfgIdx){
document.getElementById('cfg-autocompact').checked = !!s.auto_compact;
document.getElementById('cfg-autocompact-pct').value = s.auto_compact_pct != null ? s.auto_compact_pct : 85;
document.getElementById('cfg-autocompact-strategy').value = s.auto_compact_strategy || 'drop_oldest';
_populateCompactModelSelect(s.auto_compact_model || '');
_toggleAutoCompact();
document.getElementById('cfg-4bit').checked = !!s.load_in_4bit;
document.getElementById('cfg-8bit').checked = !!s.load_in_8bit;
......@@ -3586,6 +3624,7 @@ async function saveModelConfig(){
auto_compact: document.getElementById('cfg-autocompact').checked,
auto_compact_pct: parseInt(document.getElementById('cfg-autocompact-pct').value) || 85,
auto_compact_strategy: document.getElementById('cfg-autocompact-strategy').value || 'drop_oldest',
auto_compact_model: document.getElementById('cfg-autocompact-model').value || '',
max_gpu_percent: isNaN(maxGpu) ? null : maxGpu,
manual_ram_gb: isNaN(ramGb) ? null : ramGb,
load_in_4bit: document.getElementById('cfg-4bit').checked,
......
......@@ -493,6 +493,40 @@
</div>
</div>
</div>
<!-- Auto-compact context -->
<div class="card">
<div class="card-title">Auto-compact context</div>
<p class="form-hint" style="margin-bottom:.6rem">Global <b>defaults</b> for shrinking an over-long chat history before generation so requests don't error out on context overflow. Per-model settings on the <b>Models</b> page override these. OFF by default.</p>
<div class="form-row">
<label style="display:flex;align-items:center;gap:.5rem;cursor:pointer">
<input type="checkbox" id="s-compact-enabled" onchange="toggleCompactFields()">
<span style="font-size:13px;font-weight:500">Enable auto-compaction by default</span>
</label>
</div>
<div id="compact-fields" style="display:none">
<div style="display:grid;grid-template-columns:1fr 1fr;gap:.75rem">
<div class="form-row">
<label class="form-label">Trigger at <span class="muted">(% of context)</span></label>
<input type="number" id="s-compact-pct" class="form-input" min="50" max="99" step="1" placeholder="85">
<span class="form-hint">Compacts to ~65% of the context window when the prompt reaches this %.</span>
</div>
<div class="form-row">
<label class="form-label">Strategy</label>
<select id="s-compact-strategy" class="form-input">
<option value="drop_oldest">Drop oldest — keep system + most recent turns</option>
<option value="keep_head_tail">Keep head + tail — also keep the first turn, drop the middle</option>
<option value="summarize">Summarize — replace the dropped middle with an LLM summary</option>
</select>
</div>
</div>
<div class="form-row">
<label class="form-label">Summarizer model <span class="muted">(summarize strategy)</span></label>
<input type="text" id="s-compact-model" class="form-input" placeholder="(empty = the model serving the request)">
<span class="form-hint">Model id/alias that writes the summary. Empty = the request's own model summarizes itself. Point it at a smaller/faster model to summarize old turns while the main model answers — the dropped history is chunked to fit the chosen model's own context window. The client receives a status message and live progress while compaction runs.</span>
</div>
</div>
</div>
{% endblock %}
{% block scripts %}
......@@ -561,6 +595,10 @@ function toggleDs4Fields(){
document.getElementById('ds4-fields').style.display =
document.getElementById('s-ds4-enabled').checked ? 'block' : 'none';
}
function toggleCompactFields(){
document.getElementById('compact-fields').style.display =
document.getElementById('s-compact-enabled').checked ? 'block' : 'none';
}
function toggleHttps(){
document.getElementById('https-fields').style.display =
document.getElementById('s-https').checked ? 'block' : 'none';
......@@ -675,6 +713,13 @@ async function loadSettings(){
document.getElementById('s-broker-reconnect-max').value = broker.reconnect_max_delay_seconds ?? 60;
document.getElementById('s-broker-ws-ping').value = broker.websocket_ping_interval ?? 20;
toggleBrokerFields();
// Auto-compact context
const compact = d.compaction || {};
document.getElementById('s-compact-enabled').checked = !!compact.enabled;
document.getElementById('s-compact-pct').value = compact.pct ?? 85;
document.getElementById('s-compact-strategy').value = compact.strategy || 'drop_oldest';
document.getElementById('s-compact-model').value = compact.model ?? '';
toggleCompactFields();
// Thermal protection
const therm = d.thermal || {};
document.getElementById('s-therm-gpu-enabled').checked = therm.gpu_enabled !== false;
......@@ -788,6 +833,12 @@ async function saveSettings(){
expert_cache_reserve_gb: parseInt(document.getElementById('s-ds4-expert-cache-reserve').value) || 0,
extra_env: document.getElementById('s-ds4-extra-env').value.trim(),
},
compaction:{
enabled: document.getElementById('s-compact-enabled').checked,
pct: parseInt(document.getElementById('s-compact-pct').value) || 85,
strategy: document.getElementById('s-compact-strategy').value || 'drop_oldest',
model: document.getElementById('s-compact-model').value.trim(),
},
broker:{
enabled: document.getElementById('s-broker-enabled').checked,
base_url: document.getElementById('s-broker-base-url').value.trim(),
......
This diff is collapsed.
......@@ -214,6 +214,24 @@ class EnhanceConfig:
allow_rife_ncnn: bool = False # allow the external rife-ncnn-vulkan binary instead of a torch model
@dataclass
class CompactionConfig:
"""Global defaults for auto-compaction of an over-long chat history.
Per-model settings in a models.json entry (``auto_compact``,
``auto_compact_pct``, ``auto_compact_strategy``, ``auto_compact_model``)
OVERRIDE the values here; when a model leaves one unset, the global default
below applies. ``model`` selects which model performs the summarization for
the ``summarize`` strategy — empty means use the same model that serves the
request. Pointing it at a smaller/faster model lets that model summarize the
old turns while the big model answers; the dropped history is chunked to fit
the chosen summarizer's own context window before it is summarized."""
enabled: bool = False
pct: int = 85 # compact when the prompt reaches this % of n_ctx
strategy: str = "drop_oldest" # drop_oldest | keep_head_tail | summarize
model: str = "" # model id/alias that summarizes; "" = same as request
@dataclass
class Ds4Config:
"""DeepSeek V4 via ds4 (antirez/DwarfStar) external-worker configuration.
......@@ -273,6 +291,7 @@ class Config:
jobs: JobsConfig = field(default_factory=JobsConfig)
enhance: EnhanceConfig = field(default_factory=EnhanceConfig)
ds4: Ds4Config = field(default_factory=Ds4Config)
compaction: CompactionConfig = field(default_factory=CompactionConfig)
broker: BrokerConfig = field(default_factory=BrokerConfig)
system_prompt: Optional[str] = None
tools_closer_prompt: bool = False
......@@ -457,6 +476,7 @@ class ConfigManager:
jobs=_dc(JobsConfig, config_data.get("jobs", {})),
enhance=_dc(EnhanceConfig, config_data.get("enhance", {})),
ds4=_dc(Ds4Config, config_data.get("ds4", {})),
compaction=_dc(CompactionConfig, config_data.get("compaction", {})),
broker=_dc(BrokerConfig, config_data.get("broker", {})),
system_prompt=config_data.get("system_prompt"),
tools_closer_prompt=config_data.get("tools_closer_prompt", False),
......@@ -625,6 +645,12 @@ class ConfigManager:
"extra_env": self.config.ds4.extra_env,
"auto_build": self.config.ds4.auto_build,
},
"compaction": {
"enabled": self.config.compaction.enabled,
"pct": self.config.compaction.pct,
"strategy": self.config.compaction.strategy,
"model": self.config.compaction.model,
},
"broker": {
"enabled": self.config.broker.enabled,
"base_url": self.config.broker.base_url,
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment