ctxoptimizer 0.4.1__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (83) hide show
  1. ctxoptimizer/__init__.py +5 -0
  2. ctxoptimizer/__main__.py +14 -0
  3. ctxoptimizer/admin_security.py +130 -0
  4. ctxoptimizer/analysis/__init__.py +1 -0
  5. ctxoptimizer/analysis/baseline.py +69 -0
  6. ctxoptimizer/analysis/break_even.py +49 -0
  7. ctxoptimizer/analysis/drift.py +151 -0
  8. ctxoptimizer/analysis/edits.py +148 -0
  9. ctxoptimizer/analysis/markers.py +34 -0
  10. ctxoptimizer/analysis/recommend.py +114 -0
  11. ctxoptimizer/analysis/segments.py +57 -0
  12. ctxoptimizer/analysis/session.py +62 -0
  13. ctxoptimizer/analysis/state.py +239 -0
  14. ctxoptimizer/analysis/tokens.py +62 -0
  15. ctxoptimizer/analysis/waste.py +322 -0
  16. ctxoptimizer/cli.py +873 -0
  17. ctxoptimizer/config.py +512 -0
  18. ctxoptimizer/dashboard/__init__.py +1 -0
  19. ctxoptimizer/dashboard/api.py +537 -0
  20. ctxoptimizer/dashboard/service.py +157 -0
  21. ctxoptimizer/dashboard/static/.gitkeep +1 -0
  22. ctxoptimizer/dashboard/templates/.gitkeep +1 -0
  23. ctxoptimizer/dashboard/templates/doctor.html +46 -0
  24. ctxoptimizer/dashboard/templates/live.html +91 -0
  25. ctxoptimizer/dashboard/templates/overview.html +172 -0
  26. ctxoptimizer/dashboard/templates/pricing.html +115 -0
  27. ctxoptimizer/dashboard/templates/request.html +111 -0
  28. ctxoptimizer/dashboard/templates/session.html +67 -0
  29. ctxoptimizer/dashboard/templates/settings.html +79 -0
  30. ctxoptimizer/dashboard/templates/table.html +56 -0
  31. ctxoptimizer/dashboard/templates/transforms.html +118 -0
  32. ctxoptimizer/dashboard/templates/waste.html +120 -0
  33. ctxoptimizer/dashboard/views.py +429 -0
  34. ctxoptimizer/data/cache_limits.json +23 -0
  35. ctxoptimizer/data/default_config.toml +185 -0
  36. ctxoptimizer/data/pricing.json +421 -0
  37. ctxoptimizer/doctor.py +129 -0
  38. ctxoptimizer/guard.py +139 -0
  39. ctxoptimizer/ids.py +73 -0
  40. ctxoptimizer/log.py +73 -0
  41. ctxoptimizer/mcp.py +124 -0
  42. ctxoptimizer/models.py +145 -0
  43. ctxoptimizer/pipeline.py +126 -0
  44. ctxoptimizer/profiles.py +152 -0
  45. ctxoptimizer/providers/__init__.py +37 -0
  46. ctxoptimizer/providers/anthropic.py +105 -0
  47. ctxoptimizer/providers/base.py +191 -0
  48. ctxoptimizer/providers/gemini.py +143 -0
  49. ctxoptimizer/providers/ollama.py +133 -0
  50. ctxoptimizer/providers/openai_chat.py +135 -0
  51. ctxoptimizer/providers/openai_responses.py +121 -0
  52. ctxoptimizer/proxy.py +665 -0
  53. ctxoptimizer/routing.py +259 -0
  54. ctxoptimizer/server.py +319 -0
  55. ctxoptimizer/service.py +150 -0
  56. ctxoptimizer/telemetry/__init__.py +1 -0
  57. ctxoptimizer/telemetry/blobs.py +57 -0
  58. ctxoptimizer/telemetry/expand.py +62 -0
  59. ctxoptimizer/telemetry/pricing.py +168 -0
  60. ctxoptimizer/telemetry/redact.py +53 -0
  61. ctxoptimizer/telemetry/sse.py +145 -0
  62. ctxoptimizer/telemetry/store.py +829 -0
  63. ctxoptimizer/telemetry/usage.py +435 -0
  64. ctxoptimizer/transforms/__init__.py +87 -0
  65. ctxoptimizer/transforms/ansi.py +21 -0
  66. ctxoptimizer/transforms/anthropic_cache.py +100 -0
  67. ctxoptimizer/transforms/backref.py +74 -0
  68. ctxoptimizer/transforms/base.py +93 -0
  69. ctxoptimizer/transforms/cache_serve.py +146 -0
  70. ctxoptimizer/transforms/dedupe_lines.py +69 -0
  71. ctxoptimizer/transforms/eligibility.py +24 -0
  72. ctxoptimizer/transforms/openai_hints.py +84 -0
  73. ctxoptimizer/transforms/progress.py +52 -0
  74. ctxoptimizer/transforms/stale_result_elision.py +109 -0
  75. ctxoptimizer/transforms/truncate.py +100 -0
  76. ctxoptimizer/transforms/volatile.py +177 -0
  77. ctxoptimizer/transforms/whitespace.py +25 -0
  78. ctxoptimizer-0.4.1.dist-info/METADATA +196 -0
  79. ctxoptimizer-0.4.1.dist-info/RECORD +83 -0
  80. ctxoptimizer-0.4.1.dist-info/WHEEL +4 -0
  81. ctxoptimizer-0.4.1.dist-info/entry_points.txt +3 -0
  82. ctxoptimizer-0.4.1.dist-info/licenses/LICENSE +21 -0
  83. ctxoptimizer-0.4.1.dist-info/licenses/NOTICE +13 -0
@@ -0,0 +1,74 @@
1
+ """Duplicate tool-result back-references within a request (SPEC 12.6)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from hashlib import sha256
7
+ from typing import Any
8
+
9
+ from ctxoptimizer.analysis.tokens import estimate_tokens
10
+ from ctxoptimizer.config import Config
11
+ from ctxoptimizer.models import RequestContext, TransformReport
12
+ from ctxoptimizer.providers.base import ProviderAdapter
13
+ from ctxoptimizer.transforms.ansi import clean as ansi_clean
14
+ from ctxoptimizer.transforms.eligibility import is_never_touch
15
+ from ctxoptimizer.transforms.progress import clean as progress_clean
16
+
17
+
18
+ class DuplicateBackref:
19
+ id = "duplicate_backref"
20
+
21
+ def applies(
22
+ self, ctx: RequestContext, payload: dict[str, Any], adapter: ProviderAdapter
23
+ ) -> bool:
24
+ return ctx.config.transforms.duplicate_backref.enabled
25
+
26
+ def apply(
27
+ self,
28
+ ctx: RequestContext,
29
+ payload: dict[str, Any],
30
+ adapter: ProviderAdapter,
31
+ config: Config,
32
+ ) -> TransformReport:
33
+ started = time.perf_counter()
34
+ settings = config.transforms.duplicate_backref
35
+ seen: dict[bytes, str] = {}
36
+ items = 0
37
+ bytes_before = 0
38
+ bytes_after = 0
39
+ tokens_before = 0
40
+ tokens_after = 0
41
+ for ref in adapter.iter_tool_results(payload):
42
+ if is_never_touch(ref, config) and not settings.include_never_touch:
43
+ continue
44
+ text = ref.get_text()
45
+ if text is None or len(text.encode("utf-8")) < settings.min_bytes:
46
+ continue
47
+ sigma = progress_clean(ansi_clean(text))
48
+ digest = sha256(sigma.encode("utf-8")).digest()
49
+ if digest not in seen:
50
+ seen[digest] = ref.tool_use_id or "earlier"
51
+ continue
52
+ stub = f"[ctxoptimizer: output identical to earlier tool result {seen[digest]}]"
53
+ if len(stub.encode("utf-8")) >= len(text.encode("utf-8")):
54
+ continue
55
+ items += 1
56
+ bytes_before += len(text.encode("utf-8"))
57
+ bytes_after += len(stub.encode("utf-8"))
58
+ tokens_before += estimate_tokens(text)
59
+ tokens_after += estimate_tokens(stub)
60
+ ref.set_text(stub)
61
+ return TransformReport(
62
+ transform_id=self.id,
63
+ ran=True,
64
+ changed=items > 0,
65
+ items_touched=items,
66
+ bytes_before=bytes_before,
67
+ bytes_after=bytes_after,
68
+ est_tokens_before=tokens_before,
69
+ est_tokens_after=tokens_after,
70
+ duration_ms=(time.perf_counter() - started) * 1000,
71
+ )
72
+
73
+
74
+ __all__ = ["DuplicateBackref"]
@@ -0,0 +1,93 @@
1
+ """Transform protocol and the pure-text transform driver (SPEC section 12.0.1)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from collections.abc import Callable
7
+ from typing import Any, Protocol
8
+
9
+ from ctxoptimizer.analysis.tokens import estimate_tokens
10
+ from ctxoptimizer.config import Config
11
+ from ctxoptimizer.models import RequestContext, ToolResultRef, TransformReport
12
+ from ctxoptimizer.providers.base import ProviderAdapter
13
+
14
+
15
+ class Transform(Protocol):
16
+ id: str
17
+
18
+ def applies(
19
+ self, ctx: RequestContext, payload: dict[str, Any], adapter: ProviderAdapter
20
+ ) -> bool: ...
21
+
22
+ def apply(
23
+ self,
24
+ ctx: RequestContext,
25
+ payload: dict[str, Any],
26
+ adapter: ProviderAdapter,
27
+ config: Config,
28
+ ) -> TransformReport: ...
29
+
30
+
31
+ class TextTransform:
32
+ """Run a pure ``str -> str`` function over every eligible tool result."""
33
+
34
+ def __init__(
35
+ self,
36
+ transform_id: str,
37
+ clean: Callable[[str], str],
38
+ *,
39
+ enabled: bool = True,
40
+ eligible: Callable[[ToolResultRef], bool] | None = None,
41
+ ) -> None:
42
+ self.id = transform_id
43
+ self._clean = clean
44
+ self._enabled = enabled
45
+ self._eligible = eligible or (lambda ref: True)
46
+
47
+ def applies(
48
+ self, ctx: RequestContext, payload: dict[str, Any], adapter: ProviderAdapter
49
+ ) -> bool:
50
+ return self._enabled
51
+
52
+ def apply(
53
+ self,
54
+ ctx: RequestContext,
55
+ payload: dict[str, Any],
56
+ adapter: ProviderAdapter,
57
+ config: Config,
58
+ ) -> TransformReport:
59
+ started = time.perf_counter()
60
+ items = 0
61
+ bytes_before = 0
62
+ bytes_after = 0
63
+ tokens_before = 0
64
+ tokens_after = 0
65
+ for ref in adapter.iter_tool_results(payload):
66
+ if not self._eligible(ref):
67
+ continue
68
+ text = ref.get_text()
69
+ if text is None:
70
+ continue
71
+ new_text = self._clean(text)
72
+ if new_text == text:
73
+ continue
74
+ items += 1
75
+ bytes_before += len(text.encode("utf-8"))
76
+ bytes_after += len(new_text.encode("utf-8"))
77
+ tokens_before += estimate_tokens(text)
78
+ tokens_after += estimate_tokens(new_text)
79
+ ref.set_text(new_text)
80
+ return TransformReport(
81
+ transform_id=self.id,
82
+ ran=True,
83
+ changed=items > 0,
84
+ items_touched=items,
85
+ bytes_before=bytes_before,
86
+ bytes_after=bytes_after,
87
+ est_tokens_before=tokens_before,
88
+ est_tokens_after=tokens_after,
89
+ duration_ms=(time.perf_counter() - started) * 1000,
90
+ )
91
+
92
+
93
+ __all__ = ["TextTransform", "Transform"]
@@ -0,0 +1,146 @@
1
+ """Byte-true cross-request substitution (SPEC section 12.12)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from collections import OrderedDict
7
+ from dataclasses import dataclass
8
+ from hashlib import sha256
9
+ from typing import Any
10
+
11
+ from ctxoptimizer.analysis.tokens import estimate_tokens
12
+ from ctxoptimizer.config import Config
13
+ from ctxoptimizer.models import RequestContext, TransformReport
14
+ from ctxoptimizer.providers.base import ProviderAdapter
15
+ from ctxoptimizer.transforms.ansi import clean as ansi_clean
16
+ from ctxoptimizer.transforms.progress import clean as progress_clean
17
+
18
+
19
+ @dataclass(slots=True)
20
+ class CacheEntry:
21
+ hash: bytes
22
+ size: int
23
+ first_turn: int
24
+ ref: str
25
+
26
+
27
+ class CacheServeState:
28
+ """Bounded per-session content map (INV-17 says no silent loss)."""
29
+
30
+ def __init__(self, max_sessions: int = 256, max_entries: int = 512) -> None:
31
+ self.max_sessions = max_sessions
32
+ self.max_entries = max_entries
33
+ self._sessions: OrderedDict[str, OrderedDict[bytes, CacheEntry]] = OrderedDict()
34
+
35
+ def session(self, session_id: str) -> OrderedDict[bytes, CacheEntry]:
36
+ entries = self._sessions.get(session_id)
37
+ if entries is None:
38
+ if len(self._sessions) >= self.max_sessions:
39
+ self._sessions.popitem(last=False)
40
+ entries = OrderedDict()
41
+ self._sessions[session_id] = entries
42
+ else:
43
+ self._sessions.move_to_end(session_id)
44
+ return entries
45
+
46
+ def put(self, entries: OrderedDict[bytes, CacheEntry], entry: CacheEntry) -> None:
47
+ entries[entry.hash] = entry
48
+ entries.move_to_end(entry.hash)
49
+ while len(entries) > self.max_entries:
50
+ entries.popitem(last=False)
51
+
52
+ def entries(self, session_id: str) -> dict[bytes, CacheEntry]:
53
+ return dict(self.session(session_id))
54
+
55
+
56
+ class CacheServe:
57
+ id = "cache_serve"
58
+
59
+ def __init__(self, state: CacheServeState) -> None:
60
+ self.state = state
61
+
62
+ def applies(
63
+ self, ctx: RequestContext, payload: dict[str, Any], adapter: ProviderAdapter
64
+ ) -> bool:
65
+ return (
66
+ ctx.config.transforms.cache_serve.enabled
67
+ and ctx.config.telemetry.capture_payloads != "off"
68
+ )
69
+
70
+ def apply(
71
+ self,
72
+ ctx: RequestContext,
73
+ payload: dict[str, Any],
74
+ adapter: ProviderAdapter,
75
+ config: Config,
76
+ ) -> TransformReport:
77
+ started = time.perf_counter()
78
+ settings = config.transforms.cache_serve
79
+ turns = _assistant_turns(payload)
80
+ boundary = max(
81
+ 0,
82
+ ((turns - settings.keep_recent_turns) // settings.batch_turns) * settings.batch_turns,
83
+ )
84
+ entries = self.state.session(ctx.session_id)
85
+ seen: set[bytes] = set()
86
+ stubs = 0
87
+ tokens_before = 0
88
+ tokens_after = 0
89
+ for ref in adapter.iter_tool_results(payload):
90
+ text = ref.get_text()
91
+ if text is None or len(text.encode("utf-8")) < settings.min_bytes:
92
+ continue
93
+ sigma = progress_clean(ansi_clean(text))
94
+ digest = sha256(sigma.encode("utf-8")).digest()
95
+ if digest in seen:
96
+ continue
97
+ seen.add(digest)
98
+ entry = entries.get(digest)
99
+ if (
100
+ entry is not None
101
+ and entry.first_turn < boundary
102
+ and stubs < settings.max_stubs_per_request
103
+ ):
104
+ stub = (
105
+ f"[ctxoptimizer: content identical to turn {entry.first_turn} "
106
+ f"({entry.size / 1024:.1f} KB, sha256 {entry.ref}...); unchanged. "
107
+ f"reread ref={entry.ref} | ctxoptimizer_expand({entry.ref}) retrieves it]"
108
+ )
109
+ if len(stub.encode("utf-8")) < len(text.encode("utf-8")):
110
+ tokens_before += estimate_tokens(text)
111
+ tokens_after += estimate_tokens(stub)
112
+ ref.set_text(stub)
113
+ stubs += 1
114
+ continue
115
+ if entry is None:
116
+ self.state.put(
117
+ entries,
118
+ CacheEntry(
119
+ hash=digest,
120
+ size=len(text.encode("utf-8")),
121
+ first_turn=turns,
122
+ ref=sha256(text.encode("utf-8")).hexdigest()[:8],
123
+ ),
124
+ )
125
+ return TransformReport(
126
+ transform_id=self.id,
127
+ ran=True,
128
+ changed=stubs > 0,
129
+ items_touched=stubs,
130
+ est_tokens_before=tokens_before,
131
+ est_tokens_after=tokens_after,
132
+ duration_ms=(time.perf_counter() - started) * 1000,
133
+ notes={"boundary": boundary, "turns": turns, "stubs": stubs},
134
+ )
135
+
136
+
137
+ def _assistant_turns(payload: dict[str, Any]) -> int:
138
+ items = payload.get("messages")
139
+ if not isinstance(items, list):
140
+ items = payload.get("input")
141
+ if not isinstance(items, list):
142
+ return 0
143
+ return sum(1 for item in items if isinstance(item, dict) and item.get("role") == "assistant")
144
+
145
+
146
+ __all__ = ["CacheEntry", "CacheServe", "CacheServeState"]
@@ -0,0 +1,69 @@
1
+ """Exact and near-duplicate line collapsing (SPEC section 12.3)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ SIGNAL = re.compile(
8
+ r"error|fail(?:ed|ure)?|exception|fatal|warning|traceback|panic",
9
+ re.IGNORECASE,
10
+ )
11
+ _TS = re.compile(r"\b\d{4}-\d{2}-\d{2}T\S+|\b\d{2}:\d{2}:\d{2}\b")
12
+ _HEX = re.compile(r"\b[0-9a-f]{8,}\b", re.IGNORECASE)
13
+ _DIGITS = re.compile(r"\d+")
14
+
15
+
16
+ def clean(text: str, min_repeat: int = 3) -> str:
17
+ lines = text.split("\n")
18
+ exact: list[str] = []
19
+ index = 0
20
+ while index < len(lines):
21
+ end = index
22
+ while end + 1 < len(lines) and lines[end + 1].rstrip() == lines[index].rstrip():
23
+ end += 1
24
+ run = end - index + 1
25
+ if run >= min_repeat:
26
+ exact.append(lines[index])
27
+ exact.append(f"[ctxoptimizer: line repeated {run - 1} more times]")
28
+ else:
29
+ exact.extend(lines[index : end + 1])
30
+ index = end + 1
31
+ return "\n".join(_collapse_near(exact, min_repeat))
32
+
33
+
34
+ def _collapse_near(lines: list[str], min_repeat: int) -> list[str]:
35
+ result: list[str] = []
36
+ index = 0
37
+ while index < len(lines):
38
+ line = lines[index]
39
+ if line.startswith("[ctxoptimizer:") or SIGNAL.search(line):
40
+ result.append(line)
41
+ index += 1
42
+ continue
43
+ key = _normalise(line)
44
+ end = index
45
+ while (
46
+ end + 1 < len(lines)
47
+ and not lines[end + 1].startswith("[ctxoptimizer:")
48
+ and not SIGNAL.search(lines[end + 1])
49
+ and _normalise(lines[end + 1]) == key
50
+ ):
51
+ end += 1
52
+ run = end - index + 1
53
+ if run >= 2 * min_repeat:
54
+ result.append(lines[index])
55
+ result.append(f"[ctxoptimizer: {run - 2} similar lines omitted]")
56
+ result.append(lines[end])
57
+ else:
58
+ result.extend(lines[index : end + 1])
59
+ index = end + 1
60
+ return result
61
+
62
+
63
+ def _normalise(line: str) -> str:
64
+ line = _TS.sub("<ts>", line)
65
+ line = _HEX.sub("<id>", line)
66
+ return _DIGITS.sub("#", line)
67
+
68
+
69
+ __all__ = ["clean"]
@@ -0,0 +1,24 @@
1
+ """Tool eligibility from the ``[tools]`` globs (SPEC section 12.0.4)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from fnmatch import fnmatchcase
6
+
7
+ from ctxoptimizer.config import Config
8
+ from ctxoptimizer.models import ToolResultRef
9
+
10
+
11
+ def matches(name: str, patterns: list[str]) -> bool:
12
+ lowered = name.lower()
13
+ return any(fnmatchcase(lowered, pattern.lower()) for pattern in patterns)
14
+
15
+
16
+ def is_never_touch(ref: ToolResultRef, config: Config) -> bool:
17
+ return bool(ref.tool_name and matches(ref.tool_name, config.tools.never_touch))
18
+
19
+
20
+ def is_compactable(ref: ToolResultRef, config: Config) -> bool:
21
+ return bool(ref.tool_name and matches(ref.tool_name, config.tools.compactable))
22
+
23
+
24
+ __all__ = ["is_compactable", "is_never_touch", "matches"]
@@ -0,0 +1,84 @@
1
+ """OpenAI prompt_cache_key hints (SPEC section 12.10)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from hashlib import sha256
7
+ from typing import Any
8
+ from urllib.parse import urlsplit
9
+
10
+ from ctxoptimizer.config import Config
11
+ from ctxoptimizer.models import Endpoint, Provider, RequestContext, TransformReport
12
+ from ctxoptimizer.providers.base import ProviderAdapter
13
+
14
+
15
+ class OpenAIHints:
16
+ id = "openai_hints"
17
+
18
+ def applies(
19
+ self, ctx: RequestContext, payload: dict[str, Any], adapter: ProviderAdapter
20
+ ) -> bool:
21
+ settings = ctx.config.transforms.openai_hints
22
+ if not settings.enabled:
23
+ return False
24
+ if ctx.provider is not Provider.OPENAI:
25
+ return False
26
+ if ctx.endpoint not in {Endpoint.CHAT, Endpoint.RESPONSES}:
27
+ return False
28
+ entry = ctx.config.upstreams.get(ctx.upstream_name)
29
+ if entry is None:
30
+ return False
31
+ url = entry if isinstance(entry, str) else entry.url
32
+ if (urlsplit(url).hostname or "").lower() != "api.openai.com":
33
+ return False
34
+ if settings.send_prompt_cache_key and "prompt_cache_key" not in payload:
35
+ return True
36
+ if (
37
+ settings.inject_stream_usage
38
+ and ctx.endpoint is Endpoint.CHAT
39
+ and payload.get("stream") is True
40
+ ):
41
+ options = payload.get("stream_options")
42
+ return not isinstance(options, dict) or options.get("include_usage") is not True
43
+ return False
44
+
45
+ def apply(
46
+ self,
47
+ ctx: RequestContext,
48
+ payload: dict[str, Any],
49
+ adapter: ProviderAdapter,
50
+ config: Config,
51
+ ) -> TransformReport:
52
+ started = time.perf_counter()
53
+ settings = config.transforms.openai_hints
54
+ changed = False
55
+ notes: dict[str, object] = {}
56
+ if settings.send_prompt_cache_key and "prompt_cache_key" not in payload:
57
+ key = "ctxp-" + sha256(ctx.session_id.encode("utf-8")).hexdigest()[:24]
58
+ payload["prompt_cache_key"] = key
59
+ notes["key"] = key
60
+ changed = True
61
+ if (
62
+ settings.inject_stream_usage
63
+ and ctx.endpoint is Endpoint.CHAT
64
+ and payload.get("stream") is True
65
+ ):
66
+ options = payload.get("stream_options")
67
+ if not isinstance(options, dict):
68
+ options = {}
69
+ payload["stream_options"] = options
70
+ if options.get("include_usage") is not True:
71
+ options["include_usage"] = True
72
+ notes["inject_stream_usage"] = True
73
+ changed = True
74
+ return TransformReport(
75
+ transform_id=self.id,
76
+ ran=True,
77
+ changed=changed,
78
+ items_touched=len(notes),
79
+ duration_ms=(time.perf_counter() - started) * 1000,
80
+ notes=notes,
81
+ )
82
+
83
+
84
+ __all__ = ["OpenAIHints"]
@@ -0,0 +1,52 @@
1
+ """Progress-bar and carriage-return collapsing (SPEC section 12.2)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ PROGRESS = re.compile(
8
+ r"(?:[#=>█▉▊▋▌▍▎▏.\- ]{8,}\s*\d{1,3}%"
9
+ r"|\b\d+(?:\.\d+)?\s?(?:kB|MB|GB|KiB|MiB)/s\b"
10
+ r"|^(?:Downloading|Fetching|Receiving objects|Resolving deltas|"
11
+ r"Compressing objects|Unpacking|Counting objects|Collecting|Installing)\b"
12
+ r".*?(?:\d{1,3}%|\d+\s?(?:kB|MB|GB|KiB|MiB)))",
13
+ re.IGNORECASE,
14
+ )
15
+ SIGNAL = re.compile(r"error|fail|fatal|warning", re.IGNORECASE)
16
+
17
+
18
+ def clean(text: str) -> str:
19
+ lines = [_last_carriage_segment(line) for line in text.split("\n")]
20
+ result: list[str] = []
21
+ index = 0
22
+ while index < len(lines):
23
+ line = lines[index]
24
+ if not _is_progress(line):
25
+ result.append(line)
26
+ index += 1
27
+ continue
28
+ end = index
29
+ while end + 1 < len(lines) and _is_progress(lines[end + 1]):
30
+ end += 1
31
+ run = end - index + 1
32
+ if run >= 3:
33
+ result.append(f"[ctxoptimizer: {run} progress updates collapsed]")
34
+ result.append(lines[end])
35
+ else:
36
+ result.extend(lines[index : end + 1])
37
+ index = end + 1
38
+ return "\n".join(result)
39
+
40
+
41
+ def _last_carriage_segment(line: str) -> str:
42
+ if "\r" not in line:
43
+ return line
44
+ segments = [part for part in line.split("\r") if part]
45
+ return segments[-1] if segments else ""
46
+
47
+
48
+ def _is_progress(line: str) -> bool:
49
+ return bool(PROGRESS.search(line)) and not SIGNAL.search(line)
50
+
51
+
52
+ __all__ = ["clean"]
@@ -0,0 +1,109 @@
1
+ """Opt-in stale tool-result elision (SPEC section 12.8, P6-4)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import time
6
+ from typing import Any
7
+
8
+ from ctxoptimizer.config import Config
9
+ from ctxoptimizer.models import RequestContext, TransformReport
10
+ from ctxoptimizer.providers.base import ProviderAdapter
11
+
12
+
13
+ class StaleResultElision:
14
+ id = "stale_result_elision"
15
+
16
+ def applies(
17
+ self, ctx: RequestContext, payload: dict[str, Any], adapter: ProviderAdapter
18
+ ) -> bool:
19
+ return ctx.config.transforms.stale_result_elision.enabled
20
+
21
+ def apply(
22
+ self,
23
+ ctx: RequestContext,
24
+ payload: dict[str, Any],
25
+ adapter: ProviderAdapter,
26
+ config: Config,
27
+ ) -> TransformReport:
28
+ started = time.perf_counter()
29
+ settings = config.transforms.stale_result_elision
30
+ turns = assistant_turns(payload)
31
+ boundary = max(
32
+ 0,
33
+ ((turns - settings.keep_recent_turns) // settings.batch_turns) * settings.batch_turns,
34
+ )
35
+ turn_by_index = _turn_by_index(payload)
36
+ touched = 0
37
+ before = 0
38
+ after = 0
39
+ for ref in adapter.iter_tool_results(payload):
40
+ text = ref.get_text()
41
+ if text is None or text.startswith("[ctxoptimizer:"):
42
+ continue
43
+ size = len(text.encode("utf-8"))
44
+ if size < settings.min_bytes:
45
+ continue
46
+ turn = turn_by_index.get(ref.message_index, turns)
47
+ if turn >= boundary:
48
+ continue
49
+ stub = (
50
+ f"[ctxoptimizer: tool result elided ({size / 1024:.1f} KB); "
51
+ "re-run the tool if needed]"
52
+ )
53
+ if len(stub.encode("utf-8")) >= size:
54
+ continue
55
+ before += size
56
+ after += len(stub.encode("utf-8"))
57
+ ref.set_text(stub)
58
+ touched += 1
59
+ return TransformReport(
60
+ transform_id=self.id,
61
+ ran=True,
62
+ changed=touched > 0,
63
+ items_touched=touched,
64
+ bytes_before=before,
65
+ bytes_after=after,
66
+ duration_ms=(time.perf_counter() - started) * 1000,
67
+ notes={"boundary": boundary, "turns": turns, "elided": touched},
68
+ )
69
+
70
+
71
+ def assistant_turns(payload: dict[str, Any]) -> int:
72
+ return sum(1 for item in _items(payload) if _is_assistant(item))
73
+
74
+
75
+ def _turn_by_index(payload: dict[str, Any]) -> dict[int, int]:
76
+ turns = 0
77
+ result: dict[int, int] = {}
78
+ for index, item in enumerate(_items(payload)):
79
+ if _is_assistant(item):
80
+ turns += 1
81
+ result[index] = turns
82
+ return result
83
+
84
+
85
+ def _items(payload: dict[str, Any]) -> list[Any]:
86
+ value = payload.get("messages")
87
+ if isinstance(value, list):
88
+ return value
89
+ value = payload.get("contents")
90
+ if isinstance(value, list):
91
+ return value
92
+ value = payload.get("input")
93
+ if isinstance(value, str):
94
+ return [{"type": "message", "role": "user", "content": value}]
95
+ return value if isinstance(value, list) else []
96
+
97
+
98
+ def _is_assistant(item: Any) -> bool:
99
+ if not isinstance(item, dict):
100
+ return False
101
+ if item.get("type") == "function_call":
102
+ return True
103
+ role = item.get("role")
104
+ if role is None and item.get("type") == "message":
105
+ role = item.get("role")
106
+ return role in {"assistant", "model"}
107
+
108
+
109
+ __all__ = ["StaleResultElision", "assistant_turns"]