Cyber Catalyst Team commited on
Commit
cbc1879
·
1 Parent(s): b708c13

feat: implement bulletproof history compaction and Set Recommended Model settings endpoint

Browse files
Files changed (1) hide show
  1. backend.py +58 -37
backend.py CHANGED
@@ -657,53 +657,60 @@ IMPORTANT RULES:
657
 
658
  def compact_history(messages: list) -> list:
659
  """
660
- Compact conversation history to prevent context window overflow.
661
- Replaces massive tool call outputs with concise summaries if history is long.
662
  """
663
- # Only compact if messages count exceeds 15 (to maintain normal conversation)
664
- if len(messages) <= 15:
665
- return messages
666
-
667
- compacted = []
668
- # Always keep the system prompt (typically the first message)
669
- if messages and messages[0].get("role") == "system":
670
- compacted.append(messages[0])
671
  start_idx = 1
672
  else:
673
  start_idx = 0
674
-
675
- # Keep the last 4 messages exactly as they are to preserve immediate context
676
- recent_count = 4
677
- mid_messages = messages[start_idx:-recent_count]
678
- recent_messages = messages[-recent_count:]
679
 
680
- for msg in mid_messages:
 
 
 
681
  role = msg.get("role")
682
  content = msg.get("content") or ""
 
683
 
684
- if role == "tool":
685
- # Compress massive tool outputs (like bash stdout or file reads)
686
- if len(content) > 1000:
687
- summary = f"[Tool output compacted: {content[:200]}... (Total {len(content)} chars truncated for context preservation)]"
688
- compacted.append({
689
- "role": "tool",
690
- "tool_call_id": msg.get("tool_call_id"),
691
- "content": summary
692
- })
693
- continue
694
- elif role == "assistant" and msg.get("tool_calls"):
695
- # Keep tool calls metadata so the model's message-tool call mapping DAG doesn't break
 
 
 
 
 
 
696
  pass
697
-
698
- # Keep general messages, but truncate if they are too long
699
- if len(content) > 2000:
700
- msg = dict(msg)
701
- msg["content"] = content[:2000] + "\n[Content truncated for compaction]"
702
- compacted.append(msg)
 
 
 
703
 
704
- compacted.extend(recent_messages)
705
- log_activity(f"[Auto-Compaction] Compressed message history from {len(messages)} down to {len(compacted)}")
706
- return compacted
707
 
708
 
709
  @app.post("/v1/chat/completions")
@@ -2457,6 +2464,20 @@ async def download_backup(authorization: str = Header(None)):
2457
  raise HTTPException(status_code=500, detail=str(e))
2458
 
2459
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
2460
  @app.get("/health")
2461
  async def health():
2462
  return {
 
657
 
658
  def compact_history(messages: list) -> list:
659
  """
660
+ Bulletproof conversation history compaction.
661
+ Guarantees that the total context length stays well below the model's token limits.
662
  """
663
+ if not messages:
664
+ return []
665
+
666
+ # Find and preserve the system prompt
667
+ system_msg = None
668
+ if messages[0].get("role") == "system":
669
+ system_msg = messages[0]
 
670
  start_idx = 1
671
  else:
672
  start_idx = 0
673
+
674
+ other_messages = messages[start_idx:]
 
 
 
675
 
676
+ # 1. Truncate individually massive messages (e.g. file contents or massive bash outputs)
677
+ # Even recent messages should be compacted if they are ridiculously large!
678
+ compacted_others = []
679
+ for msg in other_messages:
680
  role = msg.get("role")
681
  content = msg.get("content") or ""
682
+ msg_copy = dict(msg)
683
 
684
+ if len(content) > 15000:
685
+ msg_copy["content"] = content[:5000] + f"\n\n[... Truncated {len(content) - 10000} characters to prevent model context limits from overflowing ...]\n\n" + content[-5000:]
686
+
687
+ compacted_others.append(msg_copy)
688
+
689
+ # 2. Enforce total character size budget (max ~300,000 characters / ~75,000 tokens)
690
+ # Iterate backwards from newest to oldest
691
+ final_list = []
692
+ total_chars = 0
693
+ max_budget = 300000
694
+
695
+ for msg in reversed(compacted_others):
696
+ msg_len = len(msg.get("content") or "")
697
+ if total_chars + msg_len < max_budget or len(final_list) < 2:
698
+ final_list.append(msg)
699
+ total_chars += msg_len
700
+ else:
701
+ # Drop older messages once we exceed context window budget
702
  pass
703
+
704
+ # Reverse back to chronological order
705
+ final_list.reverse()
706
+
707
+ if system_msg:
708
+ final_list.insert(0, system_msg)
709
+
710
+ if len(messages) != len(final_list):
711
+ log_activity(f"[Auto-Compaction] Sliced context window from {len(messages)} down to {len(final_list)} messages (Total chars: {total_chars})")
712
 
713
+ return final_list
 
 
714
 
715
 
716
  @app.post("/v1/chat/completions")
 
2464
  raise HTTPException(status_code=500, detail=str(e))
2465
 
2466
 
2467
+ class SetModelRequest(BaseModel):
2468
+ model: str
2469
+
2470
+ @app.post("/api/settings/model")
2471
+ async def set_recommended_model(req: SetModelRequest, authorization: str = Header(None)):
2472
+ auth(authorization)
2473
+ global RECOMMENDED_MODEL
2474
+ if req.model not in TOOL_CAPABLE_MODELS:
2475
+ raise HTTPException(status_code=400, detail="Invalid or unsupported agent model")
2476
+ RECOMMENDED_MODEL = req.model
2477
+ log_activity(f"[Settings] Recommended model manually updated to: {RECOMMENDED_MODEL}")
2478
+ return {"status": "ok", "recommended_model": RECOMMENDED_MODEL}
2479
+
2480
+
2481
  @app.get("/health")
2482
  async def health():
2483
  return {