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,435 @@
1
+ """Incremental usage parsers for Anthropic, OpenAI Chat and OpenAI Responses.
2
+
3
+ The parsers are fed raw upstream bytes. ``feed`` never raises: a malformed stream
4
+ disables the parser and leaves whatever was already parsed (INV-3). Normalization
5
+ follows SPEC section 14.1 plus the corrections in ``docs/provider-notes.md``.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import math
11
+ from typing import Any, Protocol
12
+
13
+ import orjson
14
+
15
+ from ctxoptimizer.models import Endpoint, Provider, Usage
16
+ from ctxoptimizer.profiles import UpstreamProfile
17
+ from ctxoptimizer.telemetry.sse import SSEDecoder, SSEEvent
18
+
19
+ _CHAT_STYLES = frozenset({"openai", "deepseek", "openrouter", "gemini-openai", "ollama-openai"})
20
+
21
+
22
+ class UsageParser(Protocol):
23
+ """Side-channel parser fed raw upstream bytes; never raises outward."""
24
+
25
+ def feed(self, chunk: bytes) -> None: ...
26
+
27
+ def disable(self) -> None: ...
28
+
29
+ def result(self) -> Usage: ...
30
+
31
+
32
+ class NullUsageParser:
33
+ """Records that usage is unavailable, without inspecting the stream."""
34
+
35
+ def __init__(self) -> None:
36
+ self._disabled = False
37
+
38
+ def feed(self, chunk: bytes) -> None:
39
+ return
40
+
41
+ def disable(self) -> None:
42
+ self._disabled = True
43
+
44
+ @property
45
+ def disabled(self) -> bool:
46
+ return self._disabled
47
+
48
+ def result(self) -> Usage:
49
+ return Usage(source="none")
50
+
51
+
52
+ class _BaseParser:
53
+ def __init__(self) -> None:
54
+ self._decoder = SSEDecoder()
55
+ self._disabled = False
56
+ self._usage = Usage(source="none")
57
+ self._output_chars = 0
58
+
59
+ def feed(self, chunk: bytes) -> None:
60
+ if self._disabled:
61
+ return
62
+ try:
63
+ for event in self._decoder.feed(chunk):
64
+ self._handle(event)
65
+ except Exception:
66
+ self.disable()
67
+
68
+ def disable(self) -> None:
69
+ self._disabled = True
70
+
71
+ def result(self) -> Usage:
72
+ if self._usage.source != "provider" and self._output_chars:
73
+ self._usage.output = max(1, math.ceil(self._output_chars / 3.5))
74
+ self._usage.source = "estimated"
75
+ return self._usage
76
+
77
+ def _handle(self, event: SSEEvent) -> None:
78
+ raise NotImplementedError
79
+
80
+
81
+ class AnthropicUsageParser(_BaseParser):
82
+ """Anthropic sends full usage at message_start and cumulative at message_delta."""
83
+
84
+ def __init__(self) -> None:
85
+ super().__init__()
86
+ self._fields: dict[str, Any] = {}
87
+ self._cache_creation: dict[str, Any] | None = None
88
+
89
+ def _handle(self, event: SSEEvent) -> None:
90
+ payload = _json(event.data)
91
+ if payload is None:
92
+ return
93
+ kind = payload.get("type") or event.event
94
+ if kind == "message_start":
95
+ message = _dict(payload.get("message"))
96
+ usage = message.get("usage")
97
+ if isinstance(usage, dict):
98
+ self._merge(usage)
99
+ elif kind == "message_delta":
100
+ usage = payload.get("usage")
101
+ if isinstance(usage, dict):
102
+ self._merge(usage)
103
+ elif kind == "content_block_delta":
104
+ delta = _dict(payload.get("delta"))
105
+ if delta.get("type") == "text_delta":
106
+ self._output_chars += len(_text(delta.get("text")))
107
+ elif delta.get("type") == "thinking_delta":
108
+ self._output_chars += len(_text(delta.get("thinking")))
109
+
110
+ def _merge(self, usage: dict[str, Any]) -> None:
111
+ for key in (
112
+ "input_tokens",
113
+ "cache_read_input_tokens",
114
+ "cache_creation_input_tokens",
115
+ "output_tokens",
116
+ ):
117
+ value = usage.get(key)
118
+ if value is not None:
119
+ self._fields[key] = value
120
+ details = usage.get("output_tokens_details")
121
+ if isinstance(details, dict):
122
+ self._fields["output_tokens_details"] = details
123
+ cache_creation = usage.get("cache_creation")
124
+ if isinstance(cache_creation, dict):
125
+ self._cache_creation = cache_creation
126
+ self._usage = _anthropic_usage(self._fields, self._cache_creation)
127
+
128
+
129
+ class OpenAIChatUsageParser(_BaseParser):
130
+ """Chat Completions usage arrives in the final chunk when requested."""
131
+
132
+ def __init__(self, style: str = "openai") -> None:
133
+ super().__init__()
134
+ self._style = style
135
+
136
+ def _handle(self, event: SSEEvent) -> None:
137
+ data = event.data.strip()
138
+ if not data or data == "[DONE]":
139
+ return
140
+ payload = _json(data)
141
+ if payload is None:
142
+ return
143
+ usage = payload.get("usage")
144
+ if isinstance(usage, dict):
145
+ self._usage = _chat_usage(usage, self._style)
146
+ for choice in payload.get("choices") or []:
147
+ if not isinstance(choice, dict):
148
+ continue
149
+ delta = _dict(choice.get("delta"))
150
+ self._output_chars += len(_text(delta.get("content")))
151
+ self._output_chars += len(_text(delta.get("reasoning_content")))
152
+
153
+
154
+ class OpenAIResponsesUsageParser(_BaseParser):
155
+ """Responses reports true usage on the final Response object."""
156
+
157
+ def _handle(self, event: SSEEvent) -> None:
158
+ payload = _json(event.data)
159
+ if payload is None:
160
+ return
161
+ kind = payload.get("type") or event.event
162
+ if kind in {"response.completed", "response.incomplete", "response.failed"}:
163
+ response = _dict(payload.get("response"))
164
+ usage = response.get("usage")
165
+ if isinstance(usage, dict):
166
+ self._usage = _responses_usage(usage)
167
+ elif kind == "response.output_text.delta":
168
+ self._output_chars += len(_text(payload.get("delta")))
169
+
170
+
171
+ class GeminiUsageParser(_BaseParser):
172
+ """Native Gemini streams usage in ``usageMetadata`` (often on every chunk)."""
173
+
174
+ def _handle(self, event: SSEEvent) -> None:
175
+ payload = _json(event.data)
176
+ if payload is None:
177
+ return
178
+ raw = payload.get("usageMetadata")
179
+ if isinstance(raw, dict):
180
+ self._usage = _gemini_usage(raw)
181
+ for candidate in payload.get("candidates") or []:
182
+ if not isinstance(candidate, dict):
183
+ continue
184
+ content = _dict(candidate.get("content"))
185
+ for part in content.get("parts") or []:
186
+ if isinstance(part, dict):
187
+ self._output_chars += len(_text(part.get("text")))
188
+
189
+
190
+ class OllamaUsageParser:
191
+ """Ollama speaks newline-delimited JSON, not SSE."""
192
+
193
+ def __init__(self) -> None:
194
+ self._buffer = bytearray()
195
+ self._disabled = False
196
+ self._usage = Usage(source="none")
197
+ self._output_chars = 0
198
+
199
+ def feed(self, chunk: bytes) -> None:
200
+ if self._disabled:
201
+ return
202
+ self._buffer.extend(chunk)
203
+ while True:
204
+ index = self._buffer.find(b"\n")
205
+ if index < 0:
206
+ break
207
+ line = bytes(self._buffer[:index])
208
+ del self._buffer[: index + 1]
209
+ self._handle_line(line)
210
+
211
+ def disable(self) -> None:
212
+ self._disabled = True
213
+
214
+ def result(self) -> Usage:
215
+ if self._buffer:
216
+ self._handle_line(bytes(self._buffer))
217
+ self._buffer.clear()
218
+ if self._usage.source != "provider" and self._output_chars:
219
+ self._usage.output = max(1, math.ceil(self._output_chars / 3.5))
220
+ self._usage.source = "estimated"
221
+ return self._usage
222
+
223
+ def _handle_line(self, line: bytes) -> None:
224
+ if not line.strip():
225
+ return
226
+ try:
227
+ payload = orjson.loads(line)
228
+ except (orjson.JSONDecodeError, TypeError):
229
+ self.disable()
230
+ return
231
+ if not isinstance(payload, dict):
232
+ return
233
+ message = _dict(payload.get("message"))
234
+ self._output_chars += len(_text(message.get("content")))
235
+ self._output_chars += len(_text(payload.get("response")))
236
+ if "prompt_eval_count" in payload or "eval_count" in payload:
237
+ self._usage = _ollama_usage(payload)
238
+
239
+
240
+ def new_usage_parser(
241
+ provider: Provider, endpoint: Endpoint, profile: UpstreamProfile
242
+ ) -> UsageParser:
243
+ """Select the parser for one request from its endpoint and profile."""
244
+ if endpoint is Endpoint.RESPONSES:
245
+ return OpenAIResponsesUsageParser()
246
+ if profile.usage == "anthropic":
247
+ return AnthropicUsageParser()
248
+ if profile.usage == "gemini":
249
+ return GeminiUsageParser()
250
+ if profile.usage == "ollama":
251
+ return OllamaUsageParser()
252
+ if profile.usage in _CHAT_STYLES:
253
+ return OpenAIChatUsageParser(profile.usage)
254
+ return NullUsageParser()
255
+
256
+
257
+ def parse_usage_body(
258
+ provider: Provider, endpoint: Endpoint, profile: UpstreamProfile, body: bytes
259
+ ) -> Usage:
260
+ """Parse a non-streaming response body."""
261
+ try:
262
+ payload = orjson.loads(body)
263
+ except (orjson.JSONDecodeError, TypeError):
264
+ return Usage(source="none")
265
+ if not isinstance(payload, dict):
266
+ return Usage(source="none")
267
+ if endpoint is Endpoint.RESPONSES:
268
+ usage = payload.get("usage")
269
+ return _responses_usage(usage) if isinstance(usage, dict) else Usage(source="none")
270
+ if profile.usage == "gemini":
271
+ usage = payload.get("usageMetadata")
272
+ return _gemini_usage(usage) if isinstance(usage, dict) else Usage(source="none")
273
+ if profile.usage == "ollama":
274
+ return _ollama_usage(payload)
275
+ if profile.usage == "anthropic":
276
+ usage = payload.get("usage")
277
+ if not isinstance(usage, dict):
278
+ return Usage(source="none")
279
+ cache_creation = usage.get("cache_creation")
280
+ return _anthropic_usage(usage, cache_creation if isinstance(cache_creation, dict) else None)
281
+ if profile.usage in _CHAT_STYLES:
282
+ usage = payload.get("usage")
283
+ return (
284
+ _chat_usage(usage, profile.usage) if isinstance(usage, dict) else Usage(source="none")
285
+ )
286
+ return Usage(source="none")
287
+
288
+
289
+ def _anthropic_usage(raw: dict[str, Any], cache_creation: dict[str, Any] | None) -> Usage:
290
+ write_5m = _int(_dict(cache_creation).get("ephemeral_5m_input_tokens"))
291
+ write_1h = _int(_dict(cache_creation).get("ephemeral_1h_input_tokens"))
292
+ total_write = _int(raw.get("cache_creation_input_tokens"))
293
+ if write_5m is None and write_1h is None and total_write:
294
+ write_5m = total_write
295
+ details = _dict(raw.get("output_tokens_details"))
296
+ raw_out: dict[str, object] = dict(raw)
297
+ if cache_creation:
298
+ raw_out["cache_creation"] = cache_creation
299
+ return Usage(
300
+ fresh_input=_int(raw.get("input_tokens")),
301
+ cache_read=_int(raw.get("cache_read_input_tokens")),
302
+ cache_write_5m=write_5m,
303
+ cache_write_1h=write_1h,
304
+ output=_int(raw.get("output_tokens")),
305
+ reasoning=_int(details.get("thinking_tokens")),
306
+ source="provider",
307
+ raw=raw_out,
308
+ )
309
+
310
+
311
+ def _gemini_usage(raw: dict[str, Any]) -> Usage:
312
+ prompt = _int(raw.get("promptTokenCount")) or 0
313
+ cached = _int(raw.get("cachedContentTokenCount")) or 0
314
+ return Usage(
315
+ fresh_input=max(prompt - cached, 0),
316
+ cache_read=cached,
317
+ output=_int(raw.get("candidatesTokenCount")) or 0,
318
+ reasoning=_int(raw.get("thoughtsTokenCount")),
319
+ source="provider",
320
+ raw=dict(raw),
321
+ )
322
+
323
+
324
+ def _ollama_usage(raw: dict[str, Any]) -> Usage:
325
+ prompt = _int(raw.get("prompt_eval_count"))
326
+ output = _int(raw.get("eval_count"))
327
+ if prompt is None and output is None:
328
+ return Usage(source="none")
329
+ cached = _int(raw.get("prompt_eval_cached_count"))
330
+ cached_tokens = cached or 0
331
+ return Usage(
332
+ fresh_input=max((prompt or 0) - cached_tokens, 0),
333
+ cache_read=cached_tokens if cached is not None else None,
334
+ output=output,
335
+ source="provider",
336
+ raw=dict(raw),
337
+ )
338
+
339
+
340
+ def _chat_usage(raw: dict[str, Any], style: str) -> Usage:
341
+ prompt = _int(raw.get("prompt_tokens")) or 0
342
+ details = _dict(raw.get("prompt_tokens_details"))
343
+ cached = _int(details.get("cached_tokens")) or 0
344
+ written = _int(details.get("cache_write_tokens")) or 0
345
+ hit = _int(raw.get("prompt_cache_hit_tokens"))
346
+ miss = _int(raw.get("prompt_cache_miss_tokens"))
347
+ if hit is not None or miss is not None:
348
+ cached = hit or 0
349
+ fresh = miss if miss is not None else max(prompt - cached - written, 0)
350
+ else:
351
+ fresh = max(prompt - cached - written, 0)
352
+ completion_details = _dict(raw.get("completion_tokens_details"))
353
+ return Usage(
354
+ fresh_input=fresh,
355
+ cache_read=cached,
356
+ cache_write_30m=written,
357
+ output=_int(raw.get("completion_tokens")) or 0,
358
+ reasoning=_int(completion_details.get("reasoning_tokens")),
359
+ provider_cost_usd=_float(raw.get("cost")),
360
+ source="provider",
361
+ raw=dict(raw),
362
+ )
363
+
364
+
365
+ def _responses_usage(raw: dict[str, Any]) -> Usage:
366
+ input_tokens = _int(raw.get("input_tokens")) or 0
367
+ details = _dict(raw.get("input_tokens_details"))
368
+ cached = _int(details.get("cached_tokens")) or 0
369
+ written = _int(details.get("cache_write_tokens")) or 0
370
+ write_details = _dict(details.get("cache_write_tokens_details"))
371
+ write_30m = _int(write_details.get("ttl_30m_tokens"))
372
+ write_12h = _int(write_details.get("ttl_12h_tokens"))
373
+ if write_30m is None and write_12h is None:
374
+ write_30m = written
375
+ output_details = _dict(raw.get("output_tokens_details"))
376
+ return Usage(
377
+ fresh_input=max(input_tokens - cached - written, 0),
378
+ cache_read=cached,
379
+ cache_write_30m=write_30m,
380
+ cache_write_12h=write_12h,
381
+ output=_int(raw.get("output_tokens")) or 0,
382
+ reasoning=_int(output_details.get("reasoning_tokens")),
383
+ provider_cost_usd=_float(raw.get("cost")),
384
+ source="provider",
385
+ raw=dict(raw),
386
+ )
387
+
388
+
389
+ def _json(data: str) -> dict[str, Any] | None:
390
+ if not data:
391
+ return None
392
+ try:
393
+ value = orjson.loads(data)
394
+ except (orjson.JSONDecodeError, TypeError):
395
+ return None
396
+ return value if isinstance(value, dict) else None
397
+
398
+
399
+ def _dict(value: Any) -> dict[str, Any]:
400
+ return value if isinstance(value, dict) else {}
401
+
402
+
403
+ def _int(value: Any) -> int | None:
404
+ if isinstance(value, bool):
405
+ return None
406
+ if isinstance(value, int):
407
+ return value
408
+ if isinstance(value, float):
409
+ return int(value)
410
+ return None
411
+
412
+
413
+ def _float(value: Any) -> float | None:
414
+ if isinstance(value, bool):
415
+ return None
416
+ if isinstance(value, (int, float)):
417
+ return float(value)
418
+ return None
419
+
420
+
421
+ def _text(value: Any) -> str:
422
+ return value if isinstance(value, str) else ""
423
+
424
+
425
+ __all__ = [
426
+ "AnthropicUsageParser",
427
+ "GeminiUsageParser",
428
+ "NullUsageParser",
429
+ "OllamaUsageParser",
430
+ "OpenAIChatUsageParser",
431
+ "OpenAIResponsesUsageParser",
432
+ "UsageParser",
433
+ "new_usage_parser",
434
+ "parse_usage_body",
435
+ ]
@@ -0,0 +1,87 @@
1
+ """Deterministic payload transforms and the P3/P4 registry (SPEC section 12)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from ctxoptimizer.config import Config
6
+ from ctxoptimizer.transforms import ansi, dedupe_lines, progress, truncate, whitespace
7
+ from ctxoptimizer.transforms.anthropic_cache import AnthropicCache
8
+ from ctxoptimizer.transforms.backref import DuplicateBackref
9
+ from ctxoptimizer.transforms.base import TextTransform, Transform
10
+ from ctxoptimizer.transforms.cache_serve import CacheServe, CacheServeState
11
+ from ctxoptimizer.transforms.eligibility import is_compactable, is_never_touch
12
+ from ctxoptimizer.transforms.openai_hints import OpenAIHints
13
+ from ctxoptimizer.transforms.stale_result_elision import StaleResultElision
14
+ from ctxoptimizer.transforms.volatile import VolatileNormalizer
15
+
16
+
17
+ def build_transforms(
18
+ config: Config,
19
+ *,
20
+ disabled: set[str] | None = None,
21
+ cache_serve_state: CacheServeState | None = None,
22
+ ) -> list[Transform]:
23
+ """Return enabled transforms in the fixed pipeline order."""
24
+ transforms = config.transforms
25
+ removed = disabled or set()
26
+ result: list[Transform] = []
27
+
28
+ def add(transform: Transform) -> None:
29
+ if transform.id not in removed:
30
+ result.append(transform)
31
+
32
+ if transforms.ansi_strip.enabled:
33
+ add(
34
+ TextTransform(
35
+ "ansi_strip",
36
+ ansi.clean,
37
+ eligible=lambda ref: not is_never_touch(ref, config),
38
+ )
39
+ )
40
+ if transforms.progress_collapse.enabled:
41
+ add(
42
+ TextTransform(
43
+ "progress_collapse",
44
+ progress.clean,
45
+ eligible=lambda ref: not is_never_touch(ref, config),
46
+ )
47
+ )
48
+ if transforms.line_dedupe.enabled:
49
+ add(
50
+ TextTransform(
51
+ "line_dedupe",
52
+ lambda text: dedupe_lines.clean(text, transforms.line_dedupe.min_repeat),
53
+ eligible=lambda ref: is_compactable(ref, config),
54
+ )
55
+ )
56
+ if transforms.whitespace_trim.enabled:
57
+ add(
58
+ TextTransform(
59
+ "whitespace_trim",
60
+ lambda text: whitespace.clean(text, transforms.whitespace_trim.max_blank_run),
61
+ eligible=lambda ref: is_compactable(ref, config),
62
+ )
63
+ )
64
+ if transforms.smart_truncate.enabled:
65
+ add(
66
+ TextTransform(
67
+ "smart_truncate",
68
+ lambda text: truncate.clean(text, transforms.smart_truncate),
69
+ eligible=lambda ref: is_compactable(ref, config),
70
+ )
71
+ )
72
+ if transforms.duplicate_backref.enabled:
73
+ add(DuplicateBackref())
74
+ if transforms.cache_serve.enabled and cache_serve_state is not None:
75
+ add(CacheServe(cache_serve_state))
76
+ if transforms.stale_result_elision.enabled:
77
+ add(StaleResultElision())
78
+ if transforms.volatile_normalizer.enabled:
79
+ add(VolatileNormalizer())
80
+ if transforms.anthropic_cache.enabled:
81
+ add(AnthropicCache())
82
+ if transforms.openai_hints.enabled:
83
+ add(OpenAIHints())
84
+ return result
85
+
86
+
87
+ __all__ = ["build_transforms"]
@@ -0,0 +1,21 @@
1
+ """ANSI and control-character stripping (SPEC section 12.1)."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import re
6
+
7
+ CSI = re.compile(r"\x1b\[[0-?]*[ -/]*[@-~]")
8
+ OSC = re.compile(r"\x1b\][^\x07\x1b]*(?:\x07|\x1b\\)")
9
+ ESC = re.compile(r"\x1b[@-Z\\-_]")
10
+ C0 = re.compile(r"[\x00-\x08\x0b\x0c\x0e-\x1f]")
11
+
12
+
13
+ def clean(text: str) -> str:
14
+ """Remove ANSI CSI/OSC/ESC sequences and stray C0 controls."""
15
+ text = CSI.sub("", text)
16
+ text = OSC.sub("", text)
17
+ text = ESC.sub("", text)
18
+ return C0.sub("", text)
19
+
20
+
21
+ __all__ = ["clean"]
@@ -0,0 +1,100 @@
1
+ """Anthropic cache_control breakpoint injection (SPEC section 12.9)."""
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 Endpoint, Provider, RequestContext, TransformReport
10
+ from ctxoptimizer.providers.base import ProviderAdapter, blocks_of
11
+
12
+ _CACHEABLE = {"text", "tool_use", "tool_result", "image", "document"}
13
+
14
+
15
+ class AnthropicCache:
16
+ id = "anthropic_cache"
17
+
18
+ def applies(
19
+ self, ctx: RequestContext, payload: dict[str, Any], adapter: ProviderAdapter
20
+ ) -> bool:
21
+ return (
22
+ ctx.config.transforms.anthropic_cache.enabled
23
+ and ctx.provider is Provider.ANTHROPIC
24
+ and ctx.endpoint is Endpoint.MESSAGES
25
+ )
26
+
27
+ def apply(
28
+ self,
29
+ ctx: RequestContext,
30
+ payload: dict[str, Any],
31
+ adapter: ProviderAdapter,
32
+ config: Config,
33
+ ) -> TransformReport:
34
+ started = time.perf_counter()
35
+ settings = config.transforms.anthropic_cache
36
+ existing = _count_markers(payload)
37
+ remaining = settings.max_breakpoints - existing
38
+ added: list[str] = []
39
+ if remaining > 0:
40
+ system = payload.get("system")
41
+ if isinstance(system, str):
42
+ payload["system"] = [{"type": "text", "text": system}]
43
+ system = payload["system"]
44
+ if isinstance(system, list) and _mark_last(system, settings.ttl, added):
45
+ remaining -= 1
46
+ elif not system:
47
+ tools = payload.get("tools")
48
+ if isinstance(tools, list) and tools and remaining > 0:
49
+ if _mark_last(tools, settings.ttl, added):
50
+ remaining -= 1
51
+ messages = payload.get("messages")
52
+ if isinstance(messages, list) and remaining > 0 and len(messages) >= 3:
53
+ if _mark_last(blocks_of(messages[-3].get("content")), settings.ttl, added):
54
+ remaining -= 1
55
+ if isinstance(messages, list) and remaining > 0 and messages:
56
+ if _mark_last(blocks_of(messages[-1].get("content")), settings.ttl, added):
57
+ remaining -= 1
58
+ return TransformReport(
59
+ transform_id=self.id,
60
+ ran=True,
61
+ changed=bool(added),
62
+ items_touched=len(added),
63
+ duration_ms=(time.perf_counter() - started) * 1000,
64
+ notes={"added": added, "existing": existing},
65
+ )
66
+
67
+
68
+ def _count_markers(payload: dict[str, Any]) -> int:
69
+ total = 0
70
+ for block in payload.get("tools") or []:
71
+ if isinstance(block, dict) and "cache_control" in block:
72
+ total += 1
73
+ system = payload.get("system")
74
+ if isinstance(system, list):
75
+ total += sum(1 for block in system if isinstance(block, dict) and "cache_control" in block)
76
+ for message in payload.get("messages") or []:
77
+ if not isinstance(message, dict):
78
+ continue
79
+ total += sum(1 for block in blocks_of(message.get("content")) if "cache_control" in block)
80
+ return total
81
+
82
+
83
+ def _mark_last(blocks: list[dict[str, Any]], ttl: str, added: list[str]) -> bool:
84
+ for block in reversed(blocks):
85
+ if block.get("type") not in _CACHEABLE:
86
+ continue
87
+ if block.get("type") == "text" and not block.get("text"):
88
+ continue
89
+ if "cache_control" in block:
90
+ return False
91
+ marker: dict[str, Any] = {"type": "ephemeral"}
92
+ if ttl == "1h":
93
+ marker["ttl"] = "1h"
94
+ block["cache_control"] = marker
95
+ added.append(str(block.get("type")))
96
+ return True
97
+ return False
98
+
99
+
100
+ __all__ = ["AnthropicCache"]