mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-27 00:02:21 +01:00
Merge current dev and refresh PR #6738 from the 7011 integration
This commit is contained in:
@@ -13,6 +13,7 @@ from typing import Any, Dict, List, Optional
|
||||
from src.model_context import estimate_text_tokens, get_context_length, estimate_tokens
|
||||
from src.llm_core import llm_call_async
|
||||
from src.endpoint_resolver import resolve_endpoint
|
||||
from src.utility_effort import effort_for_call
|
||||
from src.settings import get_setting
|
||||
from core.models import ChatMessage
|
||||
|
||||
@@ -91,6 +92,83 @@ def prune_multimodal_images(
|
||||
return pruned
|
||||
|
||||
|
||||
_IMAGE_TURN_LABEL_RE = re.compile(r"\[Image(?:s)?(?:\s+attached)?:\s*([^\]]+)\]", re.IGNORECASE)
|
||||
|
||||
|
||||
def _image_turn_label(message: Dict) -> str:
|
||||
"""Best-effort '[Image: name]' label for a message whose image parts are dropped."""
|
||||
name = ""
|
||||
content = message.get("content") if isinstance(message, dict) else None
|
||||
if isinstance(content, list):
|
||||
for item in content:
|
||||
if isinstance(item, dict) and item.get("type") in _MULTIMODAL_IMAGE_TYPES:
|
||||
candidate = item.get("name") or (item.get("image_url") or {}).get("name")
|
||||
if candidate:
|
||||
name = str(candidate)
|
||||
break
|
||||
if not name:
|
||||
text = _content_as_text(content)
|
||||
match = _IMAGE_TURN_LABEL_RE.search(text)
|
||||
if match:
|
||||
name = match.group(1).strip()
|
||||
return f"[Image: {name}]" if name else "[Image]"
|
||||
|
||||
|
||||
def keep_recent_image_turns(messages: List[Dict], *, keep: int = 2) -> List[Dict]:
|
||||
"""Keep image parts only for the last `keep` messages that carry an image.
|
||||
|
||||
Older image blocks are dropped and replaced with a short ``[Image: name]``
|
||||
text label so the model still knows a picture was shared (and its name)
|
||||
without paying the image tokens on every later turn. Non-text, non-image
|
||||
multimodal blocks (e.g. audio) are left untouched.
|
||||
"""
|
||||
limit = max(0, int(keep))
|
||||
image_messages = [
|
||||
index for index, message in enumerate(messages)
|
||||
if isinstance(message, dict)
|
||||
and isinstance(message.get("content"), list)
|
||||
and any(
|
||||
isinstance(item, dict) and item.get("type") in _MULTIMODAL_IMAGE_TYPES
|
||||
for item in message["content"]
|
||||
)
|
||||
]
|
||||
if len(image_messages) <= limit:
|
||||
return list(messages)
|
||||
selected = set(image_messages[len(image_messages) - limit:]) if limit else set()
|
||||
|
||||
trimmed: List[Dict] = []
|
||||
for message_index, message in enumerate(messages):
|
||||
if message_index in selected or not isinstance(message, dict):
|
||||
trimmed.append(message)
|
||||
continue
|
||||
content = message.get("content")
|
||||
if not isinstance(content, list):
|
||||
trimmed.append(message)
|
||||
continue
|
||||
kept_blocks = [
|
||||
item for item in content
|
||||
if not (isinstance(item, dict) and item.get("type") in _MULTIMODAL_IMAGE_TYPES)
|
||||
]
|
||||
if len(kept_blocks) == len(content):
|
||||
trimmed.append(message)
|
||||
continue
|
||||
label = _image_turn_label(message)
|
||||
# Attach the label to the trailing text block when possible so we do not
|
||||
# introduce an extra content block; otherwise append a fresh text block.
|
||||
labelled = False
|
||||
for item in reversed(kept_blocks):
|
||||
if isinstance(item, dict) and item.get("type") == "text":
|
||||
item["text"] = (item.get("text") or "").rstrip() + f"\n{label}"
|
||||
labelled = True
|
||||
break
|
||||
if not labelled:
|
||||
kept_blocks.append({"type": "text", "text": label})
|
||||
cloned = dict(message)
|
||||
cloned["content"] = kept_blocks
|
||||
trimmed.append(cloned)
|
||||
return trimmed
|
||||
|
||||
|
||||
COMPACT_THRESHOLD = 0.85 # Trigger compaction at 85% of context window
|
||||
SUMMARY_MAX_TOKENS = 1024
|
||||
SMALL_CONTEXT_LIMIT = 8192 # Models with context <= this get aggressive trimming
|
||||
@@ -556,10 +634,8 @@ async def maybe_compact(
|
||||
)
|
||||
|
||||
# Use utility model if configured, otherwise fall back to session model
|
||||
util_url, util_model, util_headers = resolve_endpoint("utility", owner=owner)
|
||||
compact_url = util_url or endpoint_url
|
||||
compact_model = util_model or model
|
||||
compact_headers = util_headers if util_url else headers
|
||||
# Compact with the chat model itself (the one being evaluated), never a separate utility model.
|
||||
compact_url, compact_model, compact_headers = endpoint_url, model, headers
|
||||
|
||||
prompt = SELF_SUMMARY_SYSTEM_PROMPT.replace(
|
||||
"{count}", str(len(older))
|
||||
@@ -580,6 +656,7 @@ async def maybe_compact(
|
||||
max_tokens=SUMMARY_MAX_TOKENS,
|
||||
headers=compact_headers,
|
||||
timeout=30,
|
||||
reasoning_effort=effort_for_call(compact_url, compact_model, owner, session),
|
||||
)
|
||||
except Exception as e:
|
||||
logger.error(f"Compaction summary failed: {e}")
|
||||
|
||||
Reference in New Issue
Block a user