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,322 @@
1
+ """Deterministic waste detectors W1-W6 (SPEC section 13.6). Advisory only."""
2
+
3
+ from __future__ import annotations
4
+
5
+ from dataclasses import dataclass
6
+ from hashlib import sha256
7
+
8
+ from ctxoptimizer.analysis.drift import DriftEvent
9
+ from ctxoptimizer.ids import new_ulid
10
+ from ctxoptimizer.models import WasteFinding
11
+
12
+
13
+ @dataclass(frozen=True, slots=True)
14
+ class HashObservation:
15
+ hash: bytes
16
+ tokens: int
17
+ turn: int = 0
18
+ tool_name: str | None = None
19
+
20
+
21
+ @dataclass(frozen=True, slots=True)
22
+ class EditObservation:
23
+ """A whitespace-normalized edit pair; raw text is never persisted."""
24
+
25
+ path: str
26
+ old: str
27
+ new: str
28
+ tool_name: str | None = None
29
+ tokens: int = 0
30
+
31
+
32
+ def redundant_rereads(
33
+ current: list[HashObservation],
34
+ prior: dict[bytes, HashObservation],
35
+ *,
36
+ session_id: str,
37
+ project: str | None,
38
+ ts: float,
39
+ ) -> list[WasteFinding]:
40
+ """W1: content re-read after an earlier turn in the same session."""
41
+ findings: list[WasteFinding] = []
42
+ seen: set[bytes] = set()
43
+ for observation in current:
44
+ if observation.hash not in prior or observation.hash in seen:
45
+ continue
46
+ seen.add(observation.hash)
47
+ findings.append(
48
+ _finding(
49
+ "redundant_reread",
50
+ observation,
51
+ session_id,
52
+ project,
53
+ ts,
54
+ occurrences=2,
55
+ evidence={
56
+ "turn": observation.turn,
57
+ "first_seen_turn": prior[observation.hash].turn,
58
+ "tool": observation.tool_name,
59
+ },
60
+ )
61
+ )
62
+ return findings
63
+
64
+
65
+ def identical_outputs(
66
+ observations: list[HashObservation],
67
+ *,
68
+ session_id: str,
69
+ project: str | None,
70
+ ts: float,
71
+ min_occurrences: int = 3,
72
+ ) -> list[WasteFinding]:
73
+ """W2: the same tool output appears at least N times in one session."""
74
+ grouped: dict[bytes, list[HashObservation]] = {}
75
+ for observation in observations:
76
+ grouped.setdefault(observation.hash, []).append(observation)
77
+ findings: list[WasteFinding] = []
78
+ for items in grouped.values():
79
+ if len(items) < min_occurrences:
80
+ continue
81
+ tokens = items[0].tokens * (len(items) - 1)
82
+ findings.append(
83
+ _finding(
84
+ "identical_output",
85
+ items[0],
86
+ session_id,
87
+ project,
88
+ ts,
89
+ occurrences=len(items),
90
+ evidence={"turns": [item.turn for item in items]},
91
+ tokens=tokens,
92
+ )
93
+ )
94
+ return findings
95
+
96
+
97
+ def unbatched_fanout(
98
+ turns: list[list[str]],
99
+ *,
100
+ read_names: set[str],
101
+ session_id: str,
102
+ project: str | None,
103
+ ts: float,
104
+ min_calls: int = 4,
105
+ ) -> list[WasteFinding]:
106
+ """W3: many sequential read-like calls in one assistant turn."""
107
+ findings: list[WasteFinding] = []
108
+ for turn_index, names in enumerate(turns):
109
+ reads = [name for name in names if name.lower() in read_names]
110
+ if len(reads) < min_calls:
111
+ continue
112
+ findings.append(
113
+ WasteFinding(
114
+ finding_id=new_ulid(),
115
+ ts=ts,
116
+ session_id=session_id,
117
+ project=project,
118
+ detector="unbatched_fanout",
119
+ key_hash=sha256(f"{turn_index}:{','.join(reads)}".encode()).digest()[:16],
120
+ request_id=None,
121
+ occurrences=1,
122
+ first_seen_ts=ts,
123
+ last_seen_ts=ts,
124
+ evidence={"turn": turn_index, "calls": reads},
125
+ est_tokens_wasted=0,
126
+ est_cost_usd=None,
127
+ status="new",
128
+ )
129
+ )
130
+ return findings
131
+
132
+
133
+ def runaway_session(
134
+ previous_ts: float | None,
135
+ current_ts: float,
136
+ turns: int,
137
+ *,
138
+ session_id: str,
139
+ project: str | None,
140
+ tokens: int,
141
+ idle_gap_s: float = 900.0,
142
+ min_turns: int = 6,
143
+ ) -> WasteFinding | None:
144
+ """W4: an active session resumes after a long idle gap."""
145
+ if previous_ts is None or turns < min_turns:
146
+ return None
147
+ gap = current_ts - previous_ts
148
+ if gap <= idle_gap_s:
149
+ return None
150
+ return WasteFinding(
151
+ finding_id=new_ulid(),
152
+ ts=current_ts,
153
+ session_id=session_id,
154
+ project=project,
155
+ detector="runaway_session",
156
+ key_hash=sha256(f"{session_id}:{int(gap)}".encode()).digest()[:16],
157
+ request_id=None,
158
+ occurrences=1,
159
+ first_seen_ts=current_ts,
160
+ last_seen_ts=current_ts,
161
+ evidence={"gap_s": round(gap, 3), "turns": turns},
162
+ est_tokens_wasted=tokens,
163
+ est_cost_usd=None,
164
+ status="new",
165
+ )
166
+
167
+
168
+ def recurring_drift(
169
+ events: list[DriftEvent],
170
+ *,
171
+ session_id: str,
172
+ project: str | None,
173
+ ts: float,
174
+ min_occurrences: int = 2,
175
+ ) -> list[WasteFinding]:
176
+ """W5/W6: the same drift cause recurring in a session."""
177
+ grouped: dict[str, list[DriftEvent]] = {}
178
+ for event in events:
179
+ grouped.setdefault(event.cause, []).append(event)
180
+ findings: list[WasteFinding] = []
181
+ for cause, items in grouped.items():
182
+ if len(items) < min_occurrences:
183
+ continue
184
+ detector = (
185
+ "params_drift" if cause in {"params_changed", "model_switched"} else "recurring_drift"
186
+ )
187
+ findings.append(
188
+ WasteFinding(
189
+ finding_id=new_ulid(),
190
+ ts=ts,
191
+ session_id=session_id,
192
+ project=project,
193
+ detector=detector,
194
+ key_hash=sha256(f"{detector}:{cause}".encode()).digest()[:16],
195
+ request_id=None,
196
+ occurrences=len(items),
197
+ first_seen_ts=ts,
198
+ last_seen_ts=ts,
199
+ evidence={"cause": cause, "events": len(items)},
200
+ est_tokens_wasted=sum(item.est_tokens_lost for item in items),
201
+ est_cost_usd=None,
202
+ status="new",
203
+ )
204
+ )
205
+ return findings
206
+
207
+
208
+ def schema_bloat(
209
+ name: str,
210
+ tokens: int,
211
+ request_count: int,
212
+ *,
213
+ session_id: str,
214
+ project: str | None,
215
+ ts: float,
216
+ min_requests: int = 10,
217
+ ) -> WasteFinding | None:
218
+ """W8: an unused tool schema re-billed on every subsequent request."""
219
+ if request_count < min_requests:
220
+ return None
221
+ return WasteFinding(
222
+ finding_id=new_ulid(),
223
+ ts=ts,
224
+ session_id=session_id,
225
+ project=project,
226
+ detector="schema_bloat",
227
+ key_hash=sha256(f"schema_bloat:{name}".encode()).digest()[:16],
228
+ request_id=None,
229
+ occurrences=request_count,
230
+ first_seen_ts=ts,
231
+ last_seen_ts=ts,
232
+ evidence={"tool": name, "requests": request_count},
233
+ est_tokens_wasted=tokens * max(request_count - 1, 0),
234
+ est_cost_usd=None,
235
+ status="new",
236
+ )
237
+
238
+
239
+ def churn_edits(
240
+ edits: list[EditObservation],
241
+ last_by_path: dict[str, EditObservation],
242
+ *,
243
+ session_id: str,
244
+ project: str | None,
245
+ ts: float,
246
+ ) -> list[WasteFinding]:
247
+ """W7: whitespace-only edits and A->B->A reverts (opt-in)."""
248
+ findings: list[WasteFinding] = []
249
+ for edit in edits:
250
+ previous = last_by_path.get(edit.path)
251
+ kind: str | None = None
252
+ if edit.new != edit.old and edit.new.strip() == edit.old.strip():
253
+ kind = "whitespace_only"
254
+ elif previous is not None and edit.old == previous.new and edit.new == previous.old:
255
+ kind = "revert"
256
+ if kind is not None:
257
+ findings.append(
258
+ WasteFinding(
259
+ finding_id=new_ulid(),
260
+ ts=ts,
261
+ session_id=session_id,
262
+ project=project,
263
+ detector="churn_detector",
264
+ key_hash=sha256(f"churn:{kind}:{edit.path}".encode()).digest()[:16],
265
+ request_id=None,
266
+ occurrences=1,
267
+ first_seen_ts=ts,
268
+ last_seen_ts=ts,
269
+ evidence={
270
+ "kind": kind,
271
+ "path": edit.path,
272
+ "tool": edit.tool_name,
273
+ },
274
+ est_tokens_wasted=edit.tokens,
275
+ est_cost_usd=None,
276
+ status="new",
277
+ )
278
+ )
279
+ last_by_path[edit.path] = edit
280
+ return findings
281
+
282
+
283
+ def _finding(
284
+ detector: str,
285
+ observation: HashObservation,
286
+ session_id: str,
287
+ project: str | None,
288
+ ts: float,
289
+ *,
290
+ occurrences: int,
291
+ evidence: dict[str, object],
292
+ tokens: int | None = None,
293
+ ) -> WasteFinding:
294
+ return WasteFinding(
295
+ finding_id=new_ulid(),
296
+ ts=ts,
297
+ session_id=session_id,
298
+ project=project,
299
+ detector=detector,
300
+ key_hash=sha256(detector.encode() + observation.hash).digest()[:16],
301
+ request_id=None,
302
+ occurrences=occurrences,
303
+ first_seen_ts=ts,
304
+ last_seen_ts=ts,
305
+ evidence=evidence,
306
+ est_tokens_wasted=observation.tokens if tokens is None else tokens,
307
+ est_cost_usd=None,
308
+ status="new",
309
+ )
310
+
311
+
312
+ __all__ = [
313
+ "EditObservation",
314
+ "HashObservation",
315
+ "churn_edits",
316
+ "identical_outputs",
317
+ "recurring_drift",
318
+ "redundant_rereads",
319
+ "runaway_session",
320
+ "schema_bloat",
321
+ "unbatched_fanout",
322
+ ]