* fix(#4685): meter post-compression model context * Release exp-v0.52.48: meter post-compression model context (#5872, #4685) --------- Co-authored-by: Rod Boev <rod.boev@gmail.com> Co-authored-by: nesquena-hermes <agent@nesquena-hermes>
This commit is contained in:
9 files changed
+263
-6
No files matched your search
@@ -13,6 +13,8 @@
|
||||
|
||||
### Fixed
|
||||
|
||||
- **The context-usage indicator is more accurate right after a context compaction.** After the conversation context was compressed, the indicator kept showing the previous provider-reported prompt size (the pre-compaction request) instead of the smaller prepared one, so it briefly overstated how full the window was. It now shows a short-lived, clearly-labeled post-compaction estimate that expires as soon as the next real provider count arrives — provider accounting, token metering, and recovery behavior are unchanged. Thanks @rodboev. (#5872, #4685)
|
||||
|
||||
- **A failed turn now shows the real provider error instead of "no response, try again".** When a turn hit a non-retryable provider error (e.g. HTTP 400 "invalid model" or a credentials problem), the WebUI showed a misleading "No response from provider — this often means a usage/rate limit was hit silently, try again in a moment" message and hid the actual, non-retryable cause. The terminal error the agent reports is now surfaced through to the turn-completion classifier, so you see an accurate, actionable message (e.g. "Model not found — check the model ID in Settings" / "Authentication failed") instead of being told to retry a request that can never succeed. The generic no-response message stays reserved for genuinely empty completions. Thanks @b3nw for the report. (#5940)
|
||||
|
||||
- **Public-share button labels now show in the right language.** A value-level locale swap shipped the public-share strings (Share / Stop sharing / confirmation dialogs) as Russian text in the English locale and English text in the Russian locale — key-parity checks didn't catch it because the keys themselves were balanced. Both locales are restored to their correct language, with a regression test that fails if the values are ever swapped again. Thanks @DrMaks22. (#5955)
|
||||
|
||||
@@ -1108,6 +1108,7 @@ class Session:
|
||||
context_engine_state=None,
|
||||
context_length=None, threshold_tokens=None,
|
||||
last_prompt_tokens=None,
|
||||
post_compression_context_tokens_estimate=None,
|
||||
compression_recovery=None,
|
||||
recommended_recovery_action=None,
|
||||
compression_recovery_source_session_id=None,
|
||||
@@ -1167,6 +1168,10 @@ class Session:
|
||||
self.context_length = context_length
|
||||
self.threshold_tokens = threshold_tokens
|
||||
self.last_prompt_tokens = last_prompt_tokens
|
||||
_post_compression_tokens = _parse_nonnegative_int(post_compression_context_tokens_estimate)
|
||||
self.post_compression_context_tokens_estimate = (
|
||||
_post_compression_tokens if _post_compression_tokens and _post_compression_tokens > 0 else None
|
||||
)
|
||||
self.compression_recovery = compression_recovery if isinstance(compression_recovery, dict) else {}
|
||||
self.recommended_recovery_action = recommended_recovery_action
|
||||
self.compression_recovery_source_session_id = (
|
||||
@@ -1261,6 +1266,7 @@ class Session:
|
||||
'context_engine', 'compression_anchor_engine', 'compression_anchor_mode',
|
||||
'compression_anchor_details', 'context_engine_state',
|
||||
'context_length', 'threshold_tokens', 'last_prompt_tokens',
|
||||
'post_compression_context_tokens_estimate',
|
||||
'compression_recovery', 'recommended_recovery_action',
|
||||
'compression_recovery_source_session_id', 'compression_recovery_action',
|
||||
'truncation_watermark',
|
||||
@@ -1622,6 +1628,7 @@ class Session:
|
||||
'context_length': self.context_length,
|
||||
'threshold_tokens': self.threshold_tokens,
|
||||
'last_prompt_tokens': self.last_prompt_tokens,
|
||||
'post_compression_context_tokens_estimate': self.post_compression_context_tokens_estimate,
|
||||
'compression_recovery': self.compression_recovery,
|
||||
'recommended_recovery_action': self.recommended_recovery_action,
|
||||
'gateway_routing': self.gateway_routing,
|
||||
|
||||
@@ -20510,6 +20510,7 @@ def _prepare_chat_start_session_for_stream(
|
||||
s.model = model
|
||||
s.model_provider = model_provider
|
||||
s.active_stream_id = stream_id
|
||||
s.post_compression_context_tokens_estimate = None
|
||||
s.pending_user_message = msg
|
||||
s.pending_attachments = attachments
|
||||
s.pending_started_at = started_at if started_at is not None else time.time()
|
||||
|
||||
@@ -4414,6 +4414,45 @@ def _prune_context_tool_results_after_compression(agent, context_messages):
|
||||
return pruned_messages
|
||||
|
||||
|
||||
def _estimate_post_compression_context_tokens(agent, context_messages, system_message):
|
||||
"""Return a display-only estimate for the prepared post-compression request."""
|
||||
try:
|
||||
from agent import model_metadata
|
||||
|
||||
messages = context_messages or []
|
||||
tools = getattr(agent, 'tools', None) or None
|
||||
request_estimator = getattr(model_metadata, 'estimate_request_tokens_rough', None)
|
||||
if callable(request_estimator):
|
||||
estimate = request_estimator(messages, system_prompt=system_message or '', tools=tools)
|
||||
else:
|
||||
message_estimator = getattr(model_metadata, 'estimate_messages_tokens_rough', None)
|
||||
if callable(message_estimator):
|
||||
estimate = message_estimator(messages)
|
||||
if system_message:
|
||||
estimate += message_estimator([{'role': 'system', 'content': system_message}])
|
||||
if tools:
|
||||
estimate += message_estimator([{'role': 'system', 'content': str(tools)}])
|
||||
else:
|
||||
try:
|
||||
from agent.context_compressor import _estimate_msg_budget_tokens
|
||||
except ImportError:
|
||||
return None
|
||||
|
||||
estimate = sum(
|
||||
_estimate_msg_budget_tokens(message)
|
||||
for message in messages
|
||||
if isinstance(message, dict)
|
||||
)
|
||||
if system_message:
|
||||
estimate += _estimate_msg_budget_tokens({'role': 'system', 'content': system_message})
|
||||
if tools:
|
||||
estimate += _estimate_msg_budget_tokens({'role': 'system', 'content': str(tools)})
|
||||
return estimate if isinstance(estimate, int) and estimate > 0 else None
|
||||
except Exception:
|
||||
logger.debug("post-compression context estimate failed", exc_info=True)
|
||||
return None
|
||||
|
||||
|
||||
def _restore_reasoning_metadata(previous_messages, updated_messages):
|
||||
"""Carry forward display-only metadata lost during API-safe history sanitization.
|
||||
|
||||
@@ -6675,6 +6714,7 @@ def _run_agent_streaming(
|
||||
'context_length': 0,
|
||||
'threshold_tokens': 0,
|
||||
'last_prompt_tokens': 0,
|
||||
'post_compression_context_tokens_estimate': None,
|
||||
}
|
||||
_session_obj = _current_live_usage_session()
|
||||
|
||||
@@ -6847,6 +6887,11 @@ def _run_agent_streaming(
|
||||
_usage[_field] = getattr(_session_obj, _field, 0) or 0
|
||||
except Exception:
|
||||
pass
|
||||
_post_compression_estimate = getattr(
|
||||
_session_obj, 'post_compression_context_tokens_estimate', None,
|
||||
)
|
||||
if isinstance(_post_compression_estimate, int) and _post_compression_estimate > 0:
|
||||
_usage['post_compression_context_tokens_estimate'] = _post_compression_estimate
|
||||
|
||||
_real_prompt_tokens = int(_usage.get('last_prompt_tokens') or 0)
|
||||
_usage['cache_hit_percent'] = prompt_cache_hit_percent(
|
||||
@@ -9039,6 +9084,11 @@ def _run_agent_streaming(
|
||||
agent,
|
||||
s.context_messages,
|
||||
)
|
||||
s.post_compression_context_tokens_estimate = _estimate_post_compression_context_tokens(
|
||||
agent,
|
||||
s.context_messages,
|
||||
workspace_system_msg,
|
||||
)
|
||||
visible_after = visible_messages_for_anchor(s.messages, auto_compression=True)
|
||||
# Find the LAST [CONTEXT COMPACTION] marker in s.messages
|
||||
# and count visible messages before it. This is the correct
|
||||
@@ -9752,6 +9802,12 @@ def _run_agent_streaming(
|
||||
_sess_lpt = getattr(s, 'last_prompt_tokens', 0) or 0
|
||||
if _sess_lpt:
|
||||
usage['last_prompt_tokens'] = _sess_lpt
|
||||
_post_compression_estimate = getattr(s, 'post_compression_context_tokens_estimate', None)
|
||||
usage['post_compression_context_tokens_estimate'] = (
|
||||
_post_compression_estimate
|
||||
if isinstance(_post_compression_estimate, int) and _post_compression_estimate > 0
|
||||
else None
|
||||
)
|
||||
# (reasoning trace already attached + saved above, before s.save())
|
||||
# Leftover-steer delivery: if a /steer was queued (via
|
||||
# api/chat/steer) but the agent finished its turn before
|
||||
|
||||
@@ -2058,6 +2058,7 @@ function _applySessionContextMetadataUpdate(data){
|
||||
S.session.context_length=data.session.context_length||0;
|
||||
S.session.threshold_tokens=data.session.threshold_tokens||0;
|
||||
S.session.last_prompt_tokens=data.session.last_prompt_tokens||0;
|
||||
S.session.post_compression_context_tokens_estimate=data.session.post_compression_context_tokens_estimate||null;
|
||||
if(typeof _syncCtxIndicator==='function'){
|
||||
const u=S.lastUsage||{};
|
||||
const _pick=(latest,stored,dflt=0)=>latest!=null?latest:(stored!=null?stored:dflt);
|
||||
@@ -2067,6 +2068,7 @@ function _applySessionContextMetadataUpdate(data){
|
||||
estimated_cost:_pick(u.estimated_cost,S.session.estimated_cost),
|
||||
context_length:S.session.context_length||0,
|
||||
last_prompt_tokens:_pick(u.last_prompt_tokens,S.session.last_prompt_tokens),
|
||||
post_compression_context_tokens_estimate:S.session.post_compression_context_tokens_estimate,
|
||||
threshold_tokens:S.session.threshold_tokens||0,
|
||||
});
|
||||
}
|
||||
|
||||
+1
-1
@@ -5458,7 +5458,7 @@ function attachLiveStream(activeSid, streamId, uploaded=[], options={}){
|
||||
if(d.usage){
|
||||
const _doneUsageFallback={...(S.lastUsage||{})};
|
||||
if(S.session){
|
||||
for(const _usageField of ['context_length','threshold_tokens','last_prompt_tokens']){
|
||||
for(const _usageField of ['context_length','threshold_tokens','last_prompt_tokens','post_compression_context_tokens_estimate']){
|
||||
if(_doneUsageFallback[_usageField]==null&&S.session[_usageField]!=null){
|
||||
_doneUsageFallback[_usageField]=S.session[_usageField];
|
||||
}
|
||||
|
||||
@@ -1341,6 +1341,7 @@ async function newSession(flash, options={}){
|
||||
cache_hit_percent:data.session.cache_hit_percent,
|
||||
context_length:data.session.context_length||0,
|
||||
last_prompt_tokens:data.session.last_prompt_tokens||0,
|
||||
post_compression_context_tokens_estimate:data.session.post_compression_context_tokens_estimate||null,
|
||||
threshold_tokens:data.session.threshold_tokens||0,
|
||||
});
|
||||
}
|
||||
@@ -2117,6 +2118,7 @@ async function loadSession(sid){
|
||||
cache_hit_percent: _pick(u.cache_hit_percent, _s.cache_hit_percent, null),
|
||||
context_length: _pickPositive(u.context_length, _s.context_length),
|
||||
last_prompt_tokens:_pick(u.last_prompt_tokens,_s.last_prompt_tokens),
|
||||
post_compression_context_tokens_estimate:_s.post_compression_context_tokens_estimate||null,
|
||||
threshold_tokens: _pick(_s.threshold_tokens, u.threshold_tokens),
|
||||
});
|
||||
}
|
||||
@@ -2726,6 +2728,7 @@ function _resolveSessionModelForDisplaySoon(sid){
|
||||
S.session.context_length=resolvedContextLength;
|
||||
S.session.threshold_tokens=data.session.threshold_tokens||0;
|
||||
S.session.last_prompt_tokens=data.session.last_prompt_tokens||0;
|
||||
S.session.post_compression_context_tokens_estimate=data.session.post_compression_context_tokens_estimate||null;
|
||||
S.session._modelResolutionDeferred=false;
|
||||
syncTopbar();
|
||||
if(typeof _syncCtxIndicator==='function'){
|
||||
@@ -2740,6 +2743,7 @@ function _resolveSessionModelForDisplaySoon(sid){
|
||||
cache_hit_percent:_pick(u.cache_hit_percent,S.session.cache_hit_percent,null),
|
||||
context_length:resolvedContextLength||u.context_length||0,
|
||||
last_prompt_tokens:_pick(u.last_prompt_tokens,S.session.last_prompt_tokens),
|
||||
post_compression_context_tokens_estimate:S.session.post_compression_context_tokens_estimate,
|
||||
threshold_tokens:data.session.threshold_tokens||0,
|
||||
});
|
||||
}
|
||||
|
||||
+13
-5
@@ -6096,6 +6096,9 @@ function _mergeUsageForCtxIndicator(latest, fallback){
|
||||
merged[field]=fallbackObj[field];
|
||||
}
|
||||
}
|
||||
if(!Object.hasOwn(latestObj,'post_compression_context_tokens_estimate')&&fallbackObj.post_compression_context_tokens_estimate!=null){
|
||||
merged.post_compression_context_tokens_estimate=fallbackObj.post_compression_context_tokens_estimate;
|
||||
}
|
||||
return merged;
|
||||
}
|
||||
|
||||
@@ -6116,7 +6119,10 @@ function _syncCtxIndicator(usage){
|
||||
// nonsense percentage (often >100%) on long sessions. When we have no
|
||||
// last-prompt data we render "·" + "tokens used" via the !hasPromptTok
|
||||
// branch below — honest "no data" instead of misleading "890% used".
|
||||
const postCompressionEstimate=Number(usage.post_compression_context_tokens_estimate)||0;
|
||||
const hasPostCompressionEstimate=postCompressionEstimate>0;
|
||||
const promptTok=usage.last_prompt_tokens||0;
|
||||
const contextPromptTok=hasPostCompressionEstimate?postCompressionEstimate:promptTok;
|
||||
const totalTok=(usage.input_tokens||0)+(usage.output_tokens||0);
|
||||
const cacheReadTok=usage.cache_read_tokens||0;
|
||||
const cacheWriteTok=usage.cache_write_tokens||0;
|
||||
@@ -6136,8 +6142,9 @@ function _syncCtxIndicator(usage){
|
||||
wrap.removeAttribute('aria-hidden');
|
||||
wrap.style.display='';
|
||||
}
|
||||
const hasPromptTok=!!promptTok;
|
||||
const rawPct=hasPromptTok?Math.round((promptTok/ctxWindow)*100):0;
|
||||
let hasPromptTok=!!promptTok;
|
||||
if(hasPostCompressionEstimate) hasPromptTok=true;
|
||||
const rawPct=hasPromptTok?Math.round((contextPromptTok/ctxWindow)*100):0;
|
||||
const pct=Math.min(100,rawPct);
|
||||
const overflowed=rawPct>100;
|
||||
const ring=$('ctxRingValue');
|
||||
@@ -6165,13 +6172,14 @@ function _syncCtxIndicator(usage){
|
||||
_setCtxCompressButton(compressBtn,compressText);
|
||||
const cacheHitPct=usage.cache_hit_percent;
|
||||
const cacheText=cacheHitPct!=null?t('usage_cache_hit_detail',cacheHitPct,_fmtTokens(cacheReadTok),_fmtTokens(cacheWriteTok)):'';
|
||||
let label=hasPromptTok?`Context window ${pct}% used`:`${_fmtTokens(totalTok)} tokens used`;
|
||||
const contextLabel=hasPostCompressionEstimate?'Estimated next model context':'Context window';
|
||||
let label=hasPromptTok?`${contextLabel} ${pct}% used`:`${_fmtTokens(totalTok)} tokens used`;
|
||||
if(!hasExplicitCtx&&hasPromptTok) label+=' (est. 128K)';
|
||||
if(cost) label+=` \u00b7 $${cost<0.01?cost.toFixed(4):cost.toFixed(2)}`;
|
||||
if(cacheText) label+=` \u00b7 ${cacheText}`;
|
||||
el.setAttribute('aria-label',label);
|
||||
const usageText=hasPromptTok?(overflowed?`${rawPct}% used (context exceeded)`:`${pct}% used (${100-pct}% left)`):`${_fmtTokens(totalTok)} tokens used`;
|
||||
const tokensText=hasPromptTok?`${_fmtTokens(promptTok)} / ${_fmtTokens(ctxWindow)} tokens used`:`In: ${_fmtTokens(usage.input_tokens||0)} \u00b7 Out: ${_fmtTokens(usage.output_tokens||0)}`;
|
||||
const usageText=hasPromptTok?(overflowed?`${contextLabel}: ${rawPct}% used (context exceeded)`:`${contextLabel}: ${pct}% used (${100-pct}% left)`):`${_fmtTokens(totalTok)} tokens used`;
|
||||
const tokensText=hasPromptTok?`${contextLabel}: ${_fmtTokens(contextPromptTok)} / ${_fmtTokens(ctxWindow)} tokens used`:`In: ${_fmtTokens(usage.input_tokens||0)} \u00b7 Out: ${_fmtTokens(usage.output_tokens||0)}`;
|
||||
if(usageLine) usageLine.textContent=usageText;
|
||||
if(tokensLine) tokensLine.textContent=tokensText;
|
||||
const threshold=usage.threshold_tokens||0;
|
||||
|
||||
@@ -0,0 +1,177 @@
|
||||
import json
|
||||
import subprocess
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
ROOT = Path(__file__).resolve().parents[1]
|
||||
|
||||
|
||||
def _run_context_indicator(usage):
|
||||
source = (ROOT / "static" / "ui.js").read_text(encoding="utf-8")
|
||||
start = source.index("function _syncCtxIndicator")
|
||||
end = source.index("// ── Touch support: toggle context tooltip on tap", start)
|
||||
indicator = source[start:end]
|
||||
script = f"""
|
||||
const nodes = {{}};
|
||||
for (const id of ['ctxIndicatorWrap', 'ctxIndicator', 'ctxRingValue', 'ctxPercent', 'ctxTooltipUsage', 'ctxTooltipTokens', 'ctxTooltipThreshold', 'ctxTooltipCost', 'ctxTooltipCompress', 'ctxCompressBtn']) {{
|
||||
nodes[id] = {{style: {{}}, classList: {{remove(){{}}, toggle(){{}}}}, removeAttribute(){{}}, setAttribute(name, value){{ this[name] = value; }}}};
|
||||
}}
|
||||
global.$ = id => nodes[id] || null;
|
||||
global.window = {{}};
|
||||
global._syncMobileCtxDisplay = () => {{}};
|
||||
global._setCtxCompressButton = () => {{}};
|
||||
global._fmtTokens = value => String(value);
|
||||
global.t = key => key;
|
||||
{indicator}
|
||||
_syncCtxIndicator({json.dumps(usage)});
|
||||
console.log(JSON.stringify({{percent: nodes.ctxPercent.textContent, label: nodes.ctxIndicator['aria-label'], usage: nodes.ctxTooltipUsage.textContent, tokens: nodes.ctxTooltipTokens.textContent}}));
|
||||
"""
|
||||
result = subprocess.run(
|
||||
["node", "-e", script],
|
||||
cwd=ROOT,
|
||||
check=True,
|
||||
capture_output=True,
|
||||
text=True,
|
||||
encoding="utf-8",
|
||||
)
|
||||
return json.loads(result.stdout)
|
||||
|
||||
|
||||
def test_context_indicator_uses_post_compression_estimate():
|
||||
indicator = _run_context_indicator(
|
||||
{
|
||||
"last_prompt_tokens": 100_000,
|
||||
"post_compression_context_tokens_estimate": 4_096,
|
||||
"context_length": 128_000,
|
||||
}
|
||||
)
|
||||
|
||||
assert indicator["percent"] == "3"
|
||||
assert indicator["label"].startswith("Estimated next model context")
|
||||
assert indicator["usage"].startswith("Estimated next model context")
|
||||
|
||||
|
||||
def test_post_compression_estimate_uses_pruned_request_and_preserves_last_prompt(monkeypatch):
|
||||
from api.streaming import _estimate_post_compression_context_tokens
|
||||
|
||||
calls = []
|
||||
|
||||
def estimate(messages, *, system_prompt, tools):
|
||||
calls.append((messages, system_prompt, tools))
|
||||
return 4_096
|
||||
|
||||
monkeypatch.setattr("agent.model_metadata.estimate_request_tokens_rough", estimate, raising=False)
|
||||
pruned = [{"role": "assistant", "content": "summary"}]
|
||||
agent = type("Agent", (), {"tools": [{"name": "read_file"}]})()
|
||||
|
||||
assert _estimate_post_compression_context_tokens(agent, pruned, "workspace") == 4_096
|
||||
assert calls == [(pruned, "workspace", agent.tools)]
|
||||
|
||||
|
||||
def test_post_compression_estimate_falls_back_when_request_estimator_is_unavailable(monkeypatch):
|
||||
from api.streaming import _estimate_post_compression_context_tokens
|
||||
|
||||
calls = []
|
||||
|
||||
def estimate_messages(messages):
|
||||
calls.append(messages)
|
||||
return len(messages) * 100
|
||||
|
||||
monkeypatch.delattr("agent.model_metadata.estimate_request_tokens_rough", raising=False)
|
||||
monkeypatch.setattr("agent.model_metadata.estimate_messages_tokens_rough", estimate_messages, raising=False)
|
||||
pruned = [{"role": "assistant", "content": "summary"}]
|
||||
agent = type("Agent", (), {"tools": [{"name": "read_file"}]})()
|
||||
|
||||
assert _estimate_post_compression_context_tokens(agent, pruned, "workspace") == 300
|
||||
assert calls == [
|
||||
pruned,
|
||||
[{"role": "system", "content": "workspace"}],
|
||||
[{"role": "system", "content": str(agent.tools)}],
|
||||
]
|
||||
|
||||
|
||||
def test_post_compression_estimate_uses_compressor_budget_counter_without_metadata_estimators(monkeypatch):
|
||||
import pytest
|
||||
|
||||
from api.streaming import _estimate_post_compression_context_tokens
|
||||
|
||||
context_compressor = pytest.importorskip("agent.context_compressor")
|
||||
|
||||
monkeypatch.delattr("agent.model_metadata.estimate_request_tokens_rough", raising=False)
|
||||
monkeypatch.delattr("agent.model_metadata.estimate_messages_tokens_rough", raising=False)
|
||||
pruned = [{"role": "assistant", "content": "summary"}]
|
||||
agent = type("Agent", (), {"tools": [{"name": "read_file"}]})()
|
||||
expected_messages = [
|
||||
pruned[0],
|
||||
{"role": "system", "content": "workspace"},
|
||||
{"role": "system", "content": str(agent.tools)},
|
||||
]
|
||||
|
||||
assert _estimate_post_compression_context_tokens(agent, pruned, "workspace") == sum(
|
||||
context_compressor._estimate_msg_budget_tokens(message)
|
||||
for message in expected_messages
|
||||
)
|
||||
|
||||
|
||||
def test_chat_start_clears_expired_post_compression_estimate(tmp_path, monkeypatch):
|
||||
from api.models import Session
|
||||
from api.routes import _prepare_chat_start_session_for_stream
|
||||
|
||||
saved = []
|
||||
monkeypatch.setattr(Session, "save", lambda self, *args, **kwargs: saved.append(self.post_compression_context_tokens_estimate))
|
||||
session = Session(session_id="issue4685-clear", post_compression_context_tokens_estimate=4_096)
|
||||
|
||||
_prepare_chat_start_session_for_stream(
|
||||
session,
|
||||
msg="next turn",
|
||||
attachments=[],
|
||||
workspace=str(tmp_path),
|
||||
model="test-model",
|
||||
model_provider=None,
|
||||
stream_id="stream-4685",
|
||||
started_at=1.0,
|
||||
)
|
||||
|
||||
assert session.post_compression_context_tokens_estimate is None
|
||||
assert saved == [None]
|
||||
|
||||
|
||||
def test_estimate_lineage_matrix(tmp_path, monkeypatch):
|
||||
from api import models
|
||||
|
||||
monkeypatch.setattr(models, "SESSION_DIR", tmp_path)
|
||||
direct = models.Session(session_id="issue4685-direct", post_compression_context_tokens_estimate=4_096)
|
||||
direct.save()
|
||||
restored = models.Session.load("issue4685-direct")
|
||||
child = models.Session(session_id="issue4685-child", parent_session_id=direct.session_id)
|
||||
cron = models.Session(session_id="issue4685-cron", session_source="cron")
|
||||
|
||||
assert restored.compact()["post_compression_context_tokens_estimate"] == 4_096
|
||||
assert child.post_compression_context_tokens_estimate is None
|
||||
assert cron.post_compression_context_tokens_estimate is None
|
||||
assert "post_compression_context_tokens_estimate" not in child.compact() or child.compact()["post_compression_context_tokens_estimate"] is None
|
||||
|
||||
|
||||
def test_context_indicator_without_estimate_preserves_current_behavior():
|
||||
historical = _run_context_indicator({"last_prompt_tokens": 100_000, "context_length": 128_000})
|
||||
no_data = _run_context_indicator({"input_tokens": 100_000, "output_tokens": 1})
|
||||
|
||||
assert historical["percent"] == "78"
|
||||
assert historical["label"].startswith("Context window 78% used")
|
||||
assert no_data["percent"] == "\N{MIDDLE DOT}"
|
||||
|
||||
|
||||
def test_reload_hydration_passes_post_compression_estimate_to_context_indicator():
|
||||
expected = "post_compression_context_tokens_estimate"
|
||||
for path, expected_calls in ((ROOT / "static" / "boot.js", 1), (ROOT / "static" / "sessions.js", 3)):
|
||||
source = path.read_text(encoding="utf-8")
|
||||
calls = source.split("_syncCtxIndicator({")[1:]
|
||||
|
||||
assert len(calls) == expected_calls
|
||||
for call in calls:
|
||||
assert expected in call.split("});", 1)[0]
|
||||
|
||||
boot = (ROOT / "static" / "boot.js").read_text(encoding="utf-8")
|
||||
sessions = (ROOT / "static" / "sessions.js").read_text(encoding="utf-8")
|
||||
assert "S.session.post_compression_context_tokens_estimate=data.session.post_compression_context_tokens_estimate||null;" in boot
|
||||
assert "S.session.post_compression_context_tokens_estimate=data.session.post_compression_context_tokens_estimate||null;" in sessions
|
||||
Reference in new issue
Block a user