feat: add trim_keep_prefix to keep leading messages on hard trim

Hard-delete path can preserve the first K history messages, insert a "..." gap, and strip dangling tool_use at the cut. Default 0 keeps previous behavior.
This commit is contained in:
Jiaqing Liang committed 2026-07-13 16:03:40 +08:00
1 parent 5e31b4d59a
commit 0700e57597
2 files changed
+22 -11

No files matched your search

+21 -11
View File
@@ -82,8 +82,7 @@ def _sanitize_leading_user_msg(msg):
if not isinstance(block, dict): continue
if block.get('type') == 'tool_result':
c = block.get('content', '')
if isinstance(c, list): # content 本身也可能是 list[{type:text,text:...}]
texts.extend(b.get('text', '') for b in c if isinstance(b, dict))
if isinstance(c, list): texts.extend(b.get('text', '') for b in c if isinstance(b, dict))
else: texts.append(str(c))
elif block.get('type') == 'text': texts.append(block.get('text', ''))
msg['content'] = [{"type": "text", "text": '\n'.join(t for t in texts if t)}]
@@ -98,17 +97,27 @@ print = safeprint
def trim_messages_history(history, sess):
cap = sess.context_win * 3
target = int(cap * getattr(sess, 'trim_keep_rate', 0.6))
def cost(): return sum(len(json.dumps(m, ensure_ascii=False)) for m in history)
kp = sess.trim_keep_prefix
def cost(ms): return sum(len(json.dumps(m, ensure_ascii=False)) for m in ms)
compress_history_tags(history, interval=getattr(sess, 'cut_msg_interval', 5))
print(f'[Debug] Current context: {cost()} chars, {len(history)} messages.')
if cost() <= cap: return
print(f'[Debug] Current context: {cost(history)} chars, {len(history)} messages.')
if cost(history) <= cap: return
compress_history_tags(history, keep_recent=4, force=True)
if cost() <= target: return
while len(history) > 9 and cost() > target:
history.pop(0)
while history and history[0].get('role') != 'user': history.pop(0)
if history and history[0].get('role') == 'user': history[0] = _sanitize_leading_user_msg(history[0])
print(f'[Debug] Trimmed context, current: {cost()} chars, {len(history)} messages.')
if cost(history) <= target: return
pre, post = history[:kp], history[kp:]
while len(post) > 9 and cost(pre) + cost(post) > target:
post.pop(0)
while post and post[0].get('role') != 'user': post.pop(0)
if post and post[0].get('role') == 'user': post[0] = _sanitize_leading_user_msg(post[0])
if kp and pre:
m = pre[-1]
if m.get('role') == 'assistant' and isinstance(m.get('content'), list):
m['content'] = [b for b in m['content'] if not (isinstance(b, dict) and b.get('type') == 'tool_use')] or [{"type": "text", "text": "..."}]
_d = lambda: [{"type": "text", "text": "..."}]
gap = [{"role": "assistant", "content": _d()}] if m.get('role') == 'user' else [{"role": "user", "content": _d()}, {"role": "assistant", "content": _d()}]
history[:] = pre + gap + post
else: history[:] = pre + post
print(f'[Debug] Trimmed context, current: {cost(history)} chars, {len(history)} messages.')
def auto_make_url(base, path):
b, p = base.rstrip('/'), path.strip('/')
@@ -539,6 +548,7 @@ class BaseSession:
self.context_win = cfg.get('context_win', default_context_win)
self.maxlen_multiplier = min(max(self.context_win / default_context_win * 0.85, 1.0), 3.0)
self.cut_msg_interval = int(default_cut_msg_interval * self.maxlen_multiplier)
self.trim_keep_prefix = max(0, int(cfg.get('trim_keep_prefix', 0) or 0))
self.history = []; self.lock = threading.Lock(); self.system = ""
self.name = cfg.get('name', self.model)
self.extra_sys_prompt = cfg.get('extra_sys_prompt', '')
+1
View File
@@ -67,6 +67,7 @@
# ─── 容量 / 超时 ─────────────────────────────────────────────────────────────
# context_win 默认 30000(DeepSeek 默认 70000)。用于历史裁剪,并联动
# 压缩频率和工具结果上限;不是硬上下文限制。
# trim_keep_prefix 默认 0。硬删时保留最前 K 条,切点插 "..." 并剥尾部 tool_use。不影响 tag 压缩。
# max_retries 默认 1。_openai_stream 遇到 429/408/5xx 的自动重试次数。
# connect_timeout 连接超时秒数,默认 5。
# read_timeout 流式读取超时秒数,默认 30。