"""Provider-neutral history preparation. Never edits the saved Codex transcript. Inference routing and voice are not responsibilities of this module. A caller supplies the target window and an AITURK summarizer; subscription-only requests must not enter the budget preparation path. """ import copy import hashlib import json import re import threading from collections import OrderedDict import ozet_kopru import compression_settings PREFIXES = {"custom_tool_call": "ctc", "custom_tool_call_output": "ctco", "function_call": "fc", "function_call_output": "fco"} RATIO = 0.70 class TransferError(ValueError): pass def encoded(value): return json.dumps(value, ensure_ascii=False, sort_keys=True, separators=(",", ":")) def tokens(value): # Deliberately more conservative than the old 3.2 bytes/token estimate. return (len(encoded(value).encode("utf-8")) + 1) // 2 def input_budget(req, window, settings=None, automatic=False): limit = int(window * RATIO) if settings is not None: limit = (int(window * settings['targetPercent'] / 100) if automatic else settings['maxTokens'] if settings['budgetMode'] == 'tokens' else int(window * settings['transferPercent'] / 100)) return min(limit, window - max(1024, int(req.get("max_output_tokens") or 0))) def foreign(req): for item in req.get("input") or []: if not isinstance(item, dict): continue identity = str(item.get("id") or "") want = PREFIXES.get(item.get("type")) if ("chatcmpl-" in identity or "astrabridge" in identity or str(item.get("encrypted_content") or "").startswith(ozet_kopru.ISARET) or (want and identity and not identity.startswith(want + "_"))): return True return False def normalize(req): """Repair local wrappers/IDs, retaining real OpenAI encrypted compaction. Only foreign server references (not inline tool results) are removed. Their visible summary/text, when supplied, is retained as ordinary context. """ out = copy.deepcopy(req) items = out.get("input") if not isinstance(items, list): return out result = [] for item in items: if not isinstance(item, dict): result.append(item) continue encrypted = item.get("encrypted_content") if item.get("type") == "compaction" and isinstance(encrypted, str) and encrypted.startswith(ozet_kopru.ISARET): text = ozet_kopru.ozet_metni(item) if not text: raise TransferError("Eski AITURK özeti okunamadı; geçmiş değiştirilmedi.") result.append(ozet_kopru._ozet_mesaji(text)) continue identity = item.get("id") if "chatcmpl-" in str(identity) and item.get("type") in ("reasoning", "item_reference"): visible = [p.get("text", "") for p in item.get("summary", []) if isinstance(p, dict)] if any(visible): result.append(ozet_kopru._ozet_mesaji("\n".join(visible))) continue prefix = PREFIXES.get(item.get("type")) if prefix and isinstance(identity, str) and identity and not identity.startswith(prefix + "_"): item["id"] = prefix + "_" + identity.split("_", 1)[-1] result.append(item) out["input"] = result return out def portable_reasoning(req): """Project reasoning onto its visible text for a cross-provider request. Encrypted reasoning is provider execution state, not a conversation compaction. Never ask another model to interpret that ciphertext. Messages, tool pairs and actual compaction items remain intact; callers must still render opaque compaction with its owner. Native-only requests bypass this projection, and the saved transcript is never modified. """ out = copy.deepcopy(req) items = out.get("input") if not isinstance(items, list): return out result = [] for item in items: if isinstance(item, dict) and item.get("type") == "agent_message": # Peer messages carry delegated tasks/results. External Responses # translators do not understand this Codex-specific item type. # Keep provenance as data, never as system/developer authority. content = item.get("content") or [] if isinstance(content, str): content = [{"type": "input_text", "text": content}] portable = [] for part in content: if isinstance(part, dict) and part.get("type") == "encrypted_content": # Codex peer delivery can put the literal task/result in # this wrapper. It is distinct from encrypted reasoning. # External translators otherwise silently drop the task. text = part.get("encrypted_content") if (not isinstance(text, str) or text.startswith("gAAAA") or re.fullmatch(r"[A-Za-z0-9_+/=-]{128,}", text)): raise TransferError("Alt ajan iletisi şifreli veya okunamıyor; görev eksik gönderilmedi.") portable.append({"type": "input_text", "text": text}) else: portable.append(part) result.append({"type": "message", "role": "user", "content": [ {"type": "input_text", "text": "Agent message: " + encoded({ "author": item.get("author"), "recipient": item.get("recipient")})} ] + portable}) continue if not isinstance(item, dict) or item.get("type") != "reasoning": result.append(item) continue visible = [] for field in ("summary", "content"): for part in item.get(field) or []: if isinstance(part, dict) and isinstance(part.get("text"), str) and part["text"]: visible.append(part["text"]) if visible: # Keep this on the assistant side: a synthetic user message would # change split_history's count of protected user turns. result.append({"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "\n".join(visible)}]}) out["input"] = result return out def split_history(items, keep_users=2): """Keep the last two user turns and complete tool pairs; never split a call. System/developer items are kept verbatim regardless of their position. """ users = [i for i, x in enumerate(items) if isinstance(x, dict) and x.get("role") == "user"] cut = users[-keep_users] if len(users) >= keep_users else (users[0] if users else len(items)) # Some clients put a user/hook message between a call and its result. while cut: tail_calls = {x.get("call_id") for x in items[cut:] if isinstance(x, dict) and x.get("call_id")} earlier = [i for i, x in enumerate(items[:cut]) if isinstance(x, dict) and x.get("call_id") in tail_calls] if not earlier: break cut = min(earlier) protected, old, recent = [], [], [] for i, item in enumerate(items): if isinstance(item, dict) and item.get("role") in ("system", "developer"): protected.append(item) elif i < cut: old.append(item) else: recent.append(item) return protected, old, recent class Preparer: def __init__(self): self.cache = OrderedDict() self.lock = threading.Lock() def prepare(self, request, window, summarize, scope="", force=False, settings=None, automatic=False, checkpoints=None): """Summarize old history to fit 70%, leaving the original untouched. summarize(text, max_tokens) must return a completed semantic summary or raise. Tool execution is forbidden for that separate model request. Cache keys include the caller scope, exact history and available budget. """ req = portable_reasoning(normalize(request)) window = int(window) if window < 1024: raise TransferError("Hedef modelin bağlam kapasitesi doğrulanamadı.") limit = input_budget(req, window, settings, automatic) keep = settings['keepTurns'] if settings else 2 protected, old, recent = split_history(req.get("input") or [], keep) original_old = old saved = checkpoints.get(scope, old, settings) if checkpoints and old else None if saved: old = [ozet_kopru._ozet_mesaji(saved[1])] + old[saved[0]:] req = dict(req, input=protected + old + recent) if not force and tokens(req) <= limit: return req base = dict(req, input=protected + recent) available = limit - tokens(base) - 256 # Preserve the last user turn even if keeping two turns is too expensive. if settings is None and (available < 1024 or not old): protected, old, recent = split_history(req.get("input") or [], keep_users=1) base = dict(req, input=protected + recent) available = limit - tokens(base) - 256 if available < 512 or not old: raise TransferError("Son mesaj ve zorunlu talimatlar hedefe sığmıyor; geçmiş korundu, modele eksik içerik gönderilmedi.") # Actual compaction/unknown opaque items still require owner rendering. # Reasoning execution state has already been projected to visible text. if any(isinstance(x, dict) and x.get("encrypted_content") for x in old): raise TransferError("Şifreli geçmiş önce sahibi tarafından okunabilir özete çevrilmeli.") source = encoded(old) factor = compression_settings.LEVELS[settings['level']] if settings else .5 budget = max(512, int(available * factor)) key = hashlib.sha256((scope + str(budget) + encoded(settings) + source).encode()).hexdigest() with self.lock: summary = self.cache.get(key) if summary is None: summary = summarize(source, budget) if not isinstance(summary, str) or len(summary.strip()) < 80: raise TransferError("AITURK özeti tamamlanamadı; eski geçmiş korundu.") candidate = dict(req, input=protected + [ozet_kopru._ozet_mesaji(summary)] + recent) if tokens(candidate) > limit: raise TransferError("Hazırlanan özet hedef bütçesine sığmadı; eski geçmiş korundu.") if checkpoints: checkpoints.save(scope, original_old, settings, summary) with self.lock: self.cache[key] = summary self.cache.move_to_end(key) while len(self.cache) > 32: self.cache.popitem(last=False) return candidate PREPARER = Preparer()