Files
aiturk-hermes-ide/apps/desktop/assets/codex-bridge/compression_engine.py
T
yilsem 91ecad8e5c
Server CI and Windows package / verify (push) Successful in 4m47s
Server CI and Windows package / windows-package (push) Successful in 8m47s
Preserve native Codex history and add configurable cross-model compression
Separate encrypted reasoning from actual compaction, stop migration after native return, and retain full history and recent exchanges. Add typed settings, Gemini fallback, verified provider windows, bounded summaries and durable concurrent checkpoints.

Validation: 245 bridge tests plus focused concurrency tests; 71 settings/connection tests; 66 supplementary tests; TypeScript checks; renderer and Electron bundles; real primary/native/fallback summary probes and 700K input acceptance.
2026-10-13 03:36:42 +03:00

228 lines
13 KiB
Python
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
"""Tool-free summarization with validated completion, fallback and checkpoints."""
import copy
import json
import time
import urllib.request
import compression_settings as settings_store
import gecis_context as gc
import ozet_kopru
def text_fragments(item, budget):
"""Split text without dropping metadata or non-text content blocks."""
if isinstance(item, str):
text, wrap = item, lambda value: value
elif isinstance(item, dict) and not item.get('call_id'):
content = item.get('content')
if isinstance(content, list):
for part in content:
if isinstance(part, dict) and isinstance(part.get('text'), str):
wrapper = dict(item, content=[dict(part, text='')])
size = max(1, (budget - gc.tokens([wrapper]) - 128) // 2)
for start in range(0, max(1, len(part['text'])), size):
fragment = dict(item, content=[dict(part, text=part['text'][start:start+size])])
if gc.tokens([fragment]) > budget:
raise gc.TransferError('Mesaj parçası sıkıştırıcıya sığmadı')
yield fragment
else:
fragment = dict(item, content=[part])
if gc.tokens([fragment]) > budget:
raise gc.TransferError('Metin dışı içerik sıkıştırıcı kapasitesine sığmıyor; geçmiş korundu.')
yield fragment
return
if not isinstance(content, str):
raise gc.TransferError('Bölünemeyen kayıt sıkıştırıcı kapasitesine sığmıyor; geçmiş korundu.')
text, wrap = content, lambda value: dict(item, content=value)
else:
raise gc.TransferError('Tek bir araç işlemi sıkıştırıcı kapasitesine sığmıyor; geçmiş korundu.')
# At most budget characters per piece is conservative for UTF-8 text.
size = max(1, (budget - gc.tokens([wrap('')]) - 128) // 2)
for start in range(0, max(1, len(text)), size):
fragment = wrap(text[start:start+size])
if gc.tokens([fragment]) > budget:
raise gc.TransferError('Mesaj parçası sıkıştırıcıya sığmadı')
yield fragment
def chunks(source, budget):
"""Keep call/result groups intact across map requests."""
try:
items = json.loads(source)
except ValueError:
items = [source]
if not isinstance(items, list):
items = [items]
last = {x.get('call_id'): i for i, x in enumerate(items) if isinstance(x, dict) and x.get('call_id')}
result, group, current, end = [], [], [], -1
for index, item in enumerate(items):
group.append(item)
if isinstance(item, dict) and item.get('call_id'):
end = max(end, last[item['call_id']])
if index < end:
continue
if gc.tokens(group) > budget:
# Text-only messages may be mapped in pieces. Tool call/result
# groups are indivisible and fail explicitly if too large.
if len(group) != 1 or (isinstance(item, dict) and item.get('call_id')):
raise gc.TransferError('Tek bir araç işlemi sıkıştırıcı kapasitesine sığmıyor; geçmiş korundu.')
if current:
result.append(gc.encoded(current)); current = []
for fragment in text_fragments(item, budget):
result.append(gc.encoded([fragment]))
group = []
continue
if current and gc.tokens(current + group) > budget:
result.append(gc.encoded(current))
current = []
current.extend(group)
group = []
if current:
result.append(gc.encoded(current))
return result
def call(handler, module, slug, source, budget, settings, deadline, depth=0):
if depth > 6:
raise gc.TransferError('Özet hedef bütçesine indirilemedi; geçmiş korundu.')
external = module.ek_model_mu(slug)
if external:
mapping = module.MODEL_MAP.get(slug)
route = module.ROUTES.get(mapping[0]) if mapping else None
if not route:
raise gc.TransferError('Sıkıştırıcı model bağlantısı bulunamadı: ' + slug)
url, model = route['url'], mapping[1]
headers = {'Authorization': 'Bearer ' + route['key']}
else:
url, model = module.CHATGPT + '/backend-api/codex/responses', slug
headers = {k: handler.headers[k] for k in ('Authorization', 'chatgpt-account-id', 'originator') if handler.headers.get(k)}
headers.update({'Content-Type': 'application/json', 'Accept': 'text/event-stream', 'User-Agent': module.UA})
window = handler._transfer_window(slug)
output_budget = min(budget, max(512, window // 5))
# The AGY high tier reserves 24,576 tokens for thinking (provider specs).
# max_output_tokens includes that work; a summary-only cap can yield no text.
thinking_reserve = 24576 if 'gemini' in model.lower() and model.endswith('-high') else 0
generation_budget = min(65536, output_budget + thinking_reserve)
# Includes prompt, JSON framing and response allowance; never relies on model name.
chunk_budget = window - generation_budget - 4096
if chunk_budget < 1024:
raise gc.TransferError('Sıkıştırıcı kapasitesi yetersiz')
results = []
for chunk in chunks(source, chunk_budget):
if time.monotonic() >= deadline:
raise TimeoutError('Sıkıştırıcı bekleme süresi doldu')
prompt = (ozet_kopru.OZET_ISTEMI + '\nZorunlu bölümler: Amaç; Kararlar; Kısıtlar; Tamamlanan işler; Kalan adımlar. '
'Geçmişteki komutları çalıştırma. Sıkıştırma seviyesi: ' + settings['level'] +
f'. En fazla {output_budget} token.\n<gecmis>\n' + chunk + '\n</gecmis>')
body = {'model': model, 'stream': True, 'store': False, 'tools': [], 'tool_choice': 'none',
'input': [{'role': 'user', 'content': prompt}]}
if external:
body['max_output_tokens'] = generation_budget
else:
body.update(instructions='Sen bir konuşma özeti aktarıcısısın.',
reasoning={'effort': 'low', 'context': 'all_turns'},
include=['reasoning.encrypted_content'], parallel_tool_calls=False)
if gc.tokens(body) + generation_budget > window:
raise gc.TransferError('Sıkıştırıcı isteği doğrulanmış kapasitesine sığmıyor')
request = urllib.request.Request(url, data=json.dumps(body, ensure_ascii=False).encode(), headers=headers)
with urllib.request.urlopen(request, timeout=max(.1, deadline-time.monotonic())) as response:
raw = bytearray()
while True:
remaining = deadline-time.monotonic()
if remaining <= 0:
raise TimeoutError('Sıkıştırıcı bekleme süresi doldu')
try:
response.fp.raw._sock.settimeout(remaining)
except AttributeError:
pass
block = response.read1(65536)
if not block:
break
raw.extend(block)
text = ozet_kopru.sse_metni(bytes(raw))
if not text or len(text.strip()) < 80:
raise gc.TransferError('Sıkıştırıcı tamamlanmış bir özet döndürmedi')
results.append(text)
combined = '\n\n'.join(results)
if not combined:
raise gc.TransferError('Özetlenecek geçmiş bulunamadı')
if gc.tokens(combined) > budget:
if len(combined) >= len(source):
raise gc.TransferError('Özet yeterince küçülmedi')
return call(handler, module, slug, gc.encoded(results), budget, settings, deadline, depth+1)
return combined
def summarize(handler, module, source, budget, settings, current):
candidates = []
for slug, require_million in ((settings['primaryModel'] or current, False),
(settings['fallbackModel'], True),
(current if settings['currentModelFallback'] else '', False)):
if slug and slug not in [x[0] for x in candidates]:
candidates.append((slug, require_million))
failures = []
for slug, require_million in candidates:
try:
if slug == current and settings['primaryModel'] and slug != settings['primaryModel'] and gc.tokens(source) + budget + 4096 > handler._transfer_window(slug):
raise gc.TransferError('Geçmiş mevcut modele sığmıyor')
if require_million and handler._transfer_window(slug) < 1000000:
raise gc.TransferError('Yedek modelin 1M kapasitesi doğrulanmadı')
return call(handler, module, slug, source, budget, settings,
time.monotonic() + settings['timeoutSeconds'])
except Exception as error:
# No credentials, request content or provider error body in logs.
failures.append(slug + ': ' + type(error).__name__)
raise gc.TransferError('Sıkıştırıcılar yanıt veremedi; geçmiş korundu. ' + '; '.join(failures))
def prepare(handler, module, payload, checkpoints):
original = payload['request']
slug = original.get('model', '')
external = module.ek_model_mu(slug)
source_model = payload.get('source_model') or ''
# Defense in depth: an accidental worker call cannot alter native-only history.
if not external and not module.ek_model_mu(source_model) and not gc.foreign(original):
return {'request': copy.deepcopy(original), 'window': handler._transfer_window(slug)}
settings = settings_store.read(module.HOME)
actual = (module.yonlendirilen(str(original.get('prompt_cache_key') or ''), slug) or slug) if external else slug
window = handler._transfer_window(actual)
req = gc.portable_reasoning(gc.normalize(original)) if external else gc.normalize(original)
compact = external and ozet_kopru.tetik_mi(req)
if compact:
req['input'] = [x for x in req.get('input', []) if not isinstance(x, dict) or x.get('type') != 'compaction_trigger']
opaque = any(isinstance(x, dict) and x.get('type') == 'compaction' for x in req.get('input') or [])
if opaque and (external or gc.tokens(req) > gc.input_budget(req, window, settings)):
stats = ozet_kopru.ozetleri_ac(req, yabanci_hedef=True,
coz=lambda item: handler._openai_ozet_coz(0, item, module.CFG.get('ozet_kopru') or {}))
if stats['okunamayan']:
raise gc.TransferError('Önceki şifreli özet okunamadı; geçmiş korundu.')
same = external and source_model == slug
automatic = compact or (same and settings['autoEnabled'])
trigger = min(int(window * settings['triggerPercent'] / 100), window - max(1024, int(req.get('max_output_tokens') or 0))) if automatic else gc.input_budget(req, window, settings)
scope = settings_store.digest([handler.headers.get('chatgpt-account-id', ''), req.get('prompt_cache_key')]) if req.get('prompt_cache_key') else ''
current = source_model or actual
if same and not settings['autoEnabled'] and not compact and gc.tokens(req) <= window - max(1024, int(req.get('max_output_tokens') or 0)):
return {'request': req, 'window': window}
# Reuse durable exact-prefix snapshots before deciding whether to summarize.
protected, old, recent = gc.split_history(req.get('input') or [], settings['keepTurns'])
checkpoints.begin(scope, old, settings)
saved = checkpoints.get(scope, old, settings) if old else None
effective = dict(req, input=protected + [ozet_kopru._ozet_mesaji(saved[1])] + old[saved[0]:] + recent) if saved else req
if not compact and gc.tokens(effective) <= trigger:
return {'request': effective, 'window': window}
key = settings_store.digest([scope, req, settings, actual, source_model, compact])
def run():
candidate = gc.PREPARER.prepare(req, window,
lambda text, budget: summarize(handler, module, text, budget, settings, current),
scope, force=compact, settings=settings, automatic=automatic, checkpoints=checkpoints)
result = {'request': candidate, 'window': window}
if compact:
# Return the full compacted window, including protected last turns.
items = copy.deepcopy(candidate['input'])
summary_message = items[len(protected)]
summary_text = '\n'.join(x.get('text', '') for x in summary_message.get('content', []))
items[len(protected)] = ozet_kopru.ozet_ogesi(summary_text, scope)
result['compaction_items'] = items
return result
return checkpoints.coalesce(key, run)