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.
228 lines
13 KiB
Python
228 lines
13 KiB
Python
"""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)
|