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,829 @@
1
+ """SQLite schema, bounded writer thread, retention and purge (SPEC section 14.3).
2
+
3
+ Request handling never touches SQLite. It enqueues a dict; one writer thread
4
+ batches inserts. If the bounded queue is full the event is dropped and counted,
5
+ never blocking the proxy (SPEC section 4.3).
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import json
11
+ import logging
12
+ import os
13
+ import queue
14
+ import shutil
15
+ import sqlite3
16
+ import threading
17
+ import time
18
+ from pathlib import Path
19
+ from typing import Any
20
+
21
+ from ctxoptimizer.config import data_dir
22
+
23
+ logger = logging.getLogger(__name__)
24
+
25
+ SCHEMA_VERSION = 3
26
+
27
+ _SCHEMA = """
28
+ PRAGMA journal_mode=WAL;
29
+ PRAGMA synchronous=NORMAL;
30
+ PRAGMA foreign_keys=ON;
31
+
32
+ CREATE TABLE IF NOT EXISTS meta(key TEXT PRIMARY KEY, value TEXT NOT NULL);
33
+
34
+ CREATE TABLE IF NOT EXISTS sessions(
35
+ session_id TEXT PRIMARY KEY, project TEXT, client TEXT, provider TEXT, kind TEXT,
36
+ first_ts REAL NOT NULL, last_ts REAL NOT NULL, request_count INTEGER NOT NULL DEFAULT 0);
37
+
38
+ CREATE TABLE IF NOT EXISTS requests(
39
+ request_id TEXT PRIMARY KEY, ts REAL NOT NULL,
40
+ session_id TEXT, project TEXT, client TEXT,
41
+ provider TEXT NOT NULL, endpoint TEXT NOT NULL, upstream TEXT, model TEXT, stream INTEGER,
42
+ mode TEXT NOT NULL, mutated INTEGER NOT NULL DEFAULT 0, fallback_used INTEGER NOT NULL DEFAULT 0,
43
+ config_hash TEXT, status_code INTEGER, outcome TEXT,
44
+ ttfb_ms REAL, total_ms REAL, proxy_overhead_ms REAL,
45
+ orig_bytes INTEGER, final_bytes INTEGER, orig_est_tokens INTEGER, final_est_tokens INTEGER,
46
+ request_headers_json TEXT,
47
+ usage_source TEXT, fresh_input INTEGER, cache_read INTEGER,
48
+ cache_write_5m INTEGER, cache_write_1h INTEGER, cache_write_30m INTEGER, cache_write_12h INTEGER,
49
+ output_tokens INTEGER, reasoning_tokens INTEGER,
50
+ provider_cost_usd REAL, priced INTEGER NOT NULL DEFAULT 0, price_source TEXT,
51
+ cost_actual_usd REAL, cost_baseline_sim_usd REAL, cost_optimized_sim_usd REAL,
52
+ sim_read_baseline INTEGER, sim_read_optimized INTEGER,
53
+ error TEXT);
54
+ CREATE INDEX IF NOT EXISTS idx_requests_ts ON requests(ts);
55
+ CREATE INDEX IF NOT EXISTS idx_requests_session ON requests(session_id, ts);
56
+ CREATE INDEX IF NOT EXISTS idx_requests_project ON requests(project, ts);
57
+
58
+ CREATE TABLE IF NOT EXISTS transform_reports(
59
+ request_id TEXT NOT NULL REFERENCES requests(request_id) ON DELETE CASCADE,
60
+ transform_id TEXT NOT NULL, changed INTEGER NOT NULL, items_touched INTEGER,
61
+ bytes_before INTEGER, bytes_after INTEGER, est_tokens_saved INTEGER, duration_ms REAL,
62
+ discarded_reason TEXT, error TEXT, notes_json TEXT, fallback INTEGER NOT NULL DEFAULT 0,
63
+ PRIMARY KEY(request_id, transform_id));
64
+
65
+ CREATE TABLE IF NOT EXISTS drift_events(
66
+ id INTEGER PRIMARY KEY AUTOINCREMENT, request_id TEXT NOT NULL, session_id TEXT, ts REAL NOT NULL,
67
+ stream TEXT NOT NULL CHECK(stream IN ('original','final')),
68
+ cause TEXT NOT NULL, at_segment INTEGER, est_tokens_lost INTEGER, detail_json TEXT);
69
+
70
+ CREATE TABLE IF NOT EXISTS waste_findings(
71
+ finding_id TEXT PRIMARY KEY, ts REAL NOT NULL,
72
+ first_seen_ts REAL NOT NULL, last_seen_ts REAL NOT NULL,
73
+ session_id TEXT, project TEXT, detector TEXT NOT NULL, key_hash TEXT NOT NULL,
74
+ request_id TEXT, occurrences INTEGER NOT NULL DEFAULT 1,
75
+ est_tokens_wasted INTEGER, est_cost_usd REAL,
76
+ status TEXT NOT NULL DEFAULT 'new',
77
+ evidence_json TEXT NOT NULL);
78
+ CREATE INDEX IF NOT EXISTS idx_waste_session ON waste_findings(session_id, detector);
79
+ CREATE INDEX IF NOT EXISTS idx_waste_status ON waste_findings(status, est_cost_usd);
80
+
81
+ CREATE TABLE IF NOT EXISTS guard_events(
82
+ id INTEGER PRIMARY KEY AUTOINCREMENT, ts REAL NOT NULL, session_id TEXT,
83
+ transform_id TEXT NOT NULL, reason TEXT NOT NULL,
84
+ evidence_json TEXT, cleared_ts REAL);
85
+
86
+ CREATE TABLE IF NOT EXISTS blobs(
87
+ hash TEXT PRIMARY KEY, size INTEGER NOT NULL, created_ts REAL NOT NULL,
88
+ kind TEXT, data BLOB NOT NULL);
89
+
90
+ CREATE TABLE IF NOT EXISTS captures(
91
+ request_id TEXT NOT NULL, transform_id TEXT, location TEXT, before_hash TEXT, after_hash TEXT);
92
+
93
+ CREATE TABLE IF NOT EXISTS recommendation_state(
94
+ recommendation_id TEXT PRIMARY KEY, dismissed_until REAL NOT NULL, updated_ts REAL NOT NULL);
95
+ """
96
+
97
+ _REQUEST_COLUMNS = (
98
+ "request_id",
99
+ "ts",
100
+ "session_id",
101
+ "project",
102
+ "client",
103
+ "provider",
104
+ "endpoint",
105
+ "upstream",
106
+ "model",
107
+ "stream",
108
+ "mode",
109
+ "mutated",
110
+ "fallback_used",
111
+ "config_hash",
112
+ "status_code",
113
+ "outcome",
114
+ "ttfb_ms",
115
+ "total_ms",
116
+ "proxy_overhead_ms",
117
+ "orig_bytes",
118
+ "final_bytes",
119
+ "orig_est_tokens",
120
+ "final_est_tokens",
121
+ "request_headers_json",
122
+ "usage_source",
123
+ "fresh_input",
124
+ "cache_read",
125
+ "cache_write_5m",
126
+ "cache_write_1h",
127
+ "cache_write_30m",
128
+ "cache_write_12h",
129
+ "output_tokens",
130
+ "reasoning_tokens",
131
+ "provider_cost_usd",
132
+ "priced",
133
+ "price_source",
134
+ "cost_actual_usd",
135
+ "cost_baseline_sim_usd",
136
+ "cost_optimized_sim_usd",
137
+ "sim_read_baseline",
138
+ "sim_read_optimized",
139
+ "error",
140
+ )
141
+
142
+ _REQUEST_SQL = (
143
+ f"INSERT OR IGNORE INTO requests({', '.join(_REQUEST_COLUMNS)}) "
144
+ f"VALUES({', '.join('?' for _ in _REQUEST_COLUMNS)})"
145
+ )
146
+
147
+ _SESSION_SQL = """
148
+ INSERT INTO sessions(session_id, project, client, provider, kind, first_ts, last_ts, request_count)
149
+ VALUES(?, ?, ?, ?, ?, ?, ?, 1)
150
+ ON CONFLICT(session_id) DO UPDATE SET
151
+ last_ts = excluded.last_ts,
152
+ request_count = sessions.request_count + 1
153
+ """
154
+
155
+
156
+ class Store:
157
+ """One writer thread, one WAL SQLite file."""
158
+
159
+ def __init__(
160
+ self,
161
+ path: Path,
162
+ *,
163
+ queue_max: int = 10_000,
164
+ retention_days: int = 14,
165
+ ) -> None:
166
+ self.path = Path(path)
167
+ self.path.parent.mkdir(parents=True, exist_ok=True)
168
+ self.retention_days = retention_days
169
+ self._queue: queue.Queue[dict[str, Any] | None] = queue.Queue(maxsize=max(1, queue_max))
170
+ self._dropped = 0
171
+ self._lock = threading.Lock()
172
+ self._last_error: BaseException | None = None
173
+ self._migrate()
174
+ self._thread = threading.Thread(target=self._run, name="ctxoptimizer-store", daemon=True)
175
+ self._thread.start()
176
+
177
+ def record_request(self, record: dict[str, Any]) -> bool:
178
+ """Enqueue one request record. Returns False when the queue is full."""
179
+ try:
180
+ self._queue.put_nowait(_normalise(record))
181
+ return True
182
+ except queue.Full:
183
+ with self._lock:
184
+ self._dropped += 1
185
+ return False
186
+
187
+ def flush(self) -> None:
188
+ """Block until every queued record has been written."""
189
+ self._queue.join()
190
+
191
+ def close(self) -> None:
192
+ """Flush, stop the writer and join it."""
193
+ self._queue.put(None)
194
+ self._queue.join()
195
+ self._thread.join(timeout=5)
196
+
197
+ def stats(self) -> dict[str, int]:
198
+ with self._lock:
199
+ dropped = self._dropped
200
+ return {"dropped_events": dropped, "queued": self._queue.qsize()}
201
+
202
+ def fetch_recent(self, limit: int = 50) -> list[dict[str, Any]]:
203
+ with sqlite3.connect(self.path) as conn:
204
+ conn.row_factory = sqlite3.Row
205
+ rows = conn.execute(
206
+ "SELECT * FROM requests ORDER BY ts DESC LIMIT ?", (limit,)
207
+ ).fetchall()
208
+ return [dict(row) for row in rows]
209
+
210
+ def count_requests(self) -> int:
211
+ with sqlite3.connect(self.path) as conn:
212
+ return int(conn.execute("SELECT COUNT(*) FROM requests").fetchone()[0])
213
+
214
+ def fetch_sessions(self, limit: int = 50) -> list[dict[str, Any]]:
215
+ with sqlite3.connect(self.path) as conn:
216
+ conn.row_factory = sqlite3.Row
217
+ rows = conn.execute(
218
+ "SELECT * FROM sessions ORDER BY last_ts DESC LIMIT ?", (limit,)
219
+ ).fetchall()
220
+ return [dict(row) for row in rows]
221
+
222
+ def fetch_session_requests(self, session_id: str, limit: int = 100) -> list[dict[str, Any]]:
223
+ with sqlite3.connect(self.path) as conn:
224
+ conn.row_factory = sqlite3.Row
225
+ rows = conn.execute(
226
+ "SELECT * FROM requests WHERE session_id = ? ORDER BY ts DESC LIMIT ?",
227
+ (session_id, limit),
228
+ ).fetchall()
229
+ return [dict(row) for row in rows]
230
+
231
+ def fetch_request(self, request_id: str) -> dict[str, Any] | None:
232
+ with sqlite3.connect(self.path) as conn:
233
+ conn.row_factory = sqlite3.Row
234
+ row = conn.execute(
235
+ "SELECT * FROM requests WHERE request_id = ?", (request_id,)
236
+ ).fetchone()
237
+ return dict(row) if row is not None else None
238
+
239
+ def fetch_transform_reports(self, request_id: str) -> list[dict[str, Any]]:
240
+ with sqlite3.connect(self.path) as conn:
241
+ conn.row_factory = sqlite3.Row
242
+ rows = conn.execute(
243
+ "SELECT * FROM transform_reports WHERE request_id = ? ORDER BY rowid",
244
+ (request_id,),
245
+ ).fetchall()
246
+ return [dict(row) for row in rows]
247
+
248
+ def fetch_captures(self, request_id: str) -> list[dict[str, Any]]:
249
+ with sqlite3.connect(self.path) as conn:
250
+ conn.row_factory = sqlite3.Row
251
+ rows = conn.execute(
252
+ "SELECT * FROM captures WHERE request_id = ? ORDER BY rowid",
253
+ (request_id,),
254
+ ).fetchall()
255
+ return [dict(row) for row in rows]
256
+
257
+ def fetch_drift(self, limit: int = 200) -> list[dict[str, Any]]:
258
+ with sqlite3.connect(self.path) as conn:
259
+ conn.row_factory = sqlite3.Row
260
+ rows = conn.execute(
261
+ "SELECT * FROM drift_events ORDER BY ts DESC LIMIT ?", (limit,)
262
+ ).fetchall()
263
+ return [dict(row) for row in rows]
264
+
265
+ def fetch_guard_events(self, limit: int = 200) -> list[dict[str, Any]]:
266
+ with sqlite3.connect(self.path) as conn:
267
+ conn.row_factory = sqlite3.Row
268
+ rows = conn.execute(
269
+ "SELECT * FROM guard_events ORDER BY ts DESC LIMIT ?", (limit,)
270
+ ).fetchall()
271
+ return [dict(row) for row in rows]
272
+
273
+ def fetch_waste(self, status: str | None = None, limit: int = 500) -> list[dict[str, Any]]:
274
+ query = "SELECT * FROM waste_findings"
275
+ params: list[Any] = []
276
+ if status:
277
+ query += " WHERE status = ?"
278
+ params.append(status)
279
+ query += " ORDER BY est_cost_usd DESC, ts DESC LIMIT ?"
280
+ params.append(limit)
281
+ with sqlite3.connect(self.path) as conn:
282
+ conn.row_factory = sqlite3.Row
283
+ rows = conn.execute(query, params).fetchall()
284
+ return [dict(row) for row in rows]
285
+
286
+ def dismiss_recommendation(
287
+ self, recommendation_id: str, *, days: int = 30, now: float | None = None
288
+ ) -> None:
289
+ current = time.time() if now is None else now
290
+ until = current + max(1, days) * 86_400
291
+ with sqlite3.connect(self.path) as conn:
292
+ conn.execute(
293
+ "INSERT OR REPLACE INTO recommendation_state("
294
+ "recommendation_id, dismissed_until, updated_ts) VALUES(?, ?, ?)",
295
+ (recommendation_id, until, current),
296
+ )
297
+ conn.commit()
298
+
299
+ def suppressed_recommendations(self, now: float | None = None) -> set[str]:
300
+ current = time.time() if now is None else now
301
+ with sqlite3.connect(self.path) as conn:
302
+ rows = conn.execute(
303
+ "SELECT recommendation_id FROM recommendation_state WHERE dismissed_until > ?",
304
+ (current,),
305
+ ).fetchall()
306
+ return {str(row[0]) for row in rows}
307
+
308
+ def set_waste_status(self, finding_id: str, status: str) -> bool:
309
+ if status not in {"new", "ack", "fixed", "skipped"}:
310
+ return False
311
+ with sqlite3.connect(self.path) as conn:
312
+ cursor = conn.execute(
313
+ "UPDATE waste_findings SET status = ? WHERE finding_id = ?",
314
+ (status, finding_id),
315
+ )
316
+ conn.commit()
317
+ return cursor.rowcount > 0
318
+
319
+ def record_guard_event(
320
+ self,
321
+ session_id: str | None,
322
+ transform_id: str,
323
+ reason: str,
324
+ evidence: dict[str, Any] | None = None,
325
+ *,
326
+ cleared_ts: float | None = None,
327
+ ) -> None:
328
+ """Record a quarantine/enable event (rare dashboard write)."""
329
+ try:
330
+ with sqlite3.connect(self.path) as conn:
331
+ conn.execute(
332
+ "INSERT INTO guard_events("
333
+ "ts, session_id, transform_id, reason, evidence_json, cleared_ts"
334
+ ") VALUES(?, ?, ?, ?, ?, ?)",
335
+ (
336
+ time.time(),
337
+ session_id,
338
+ transform_id,
339
+ reason,
340
+ json.dumps(evidence or {}, default=str),
341
+ cleared_ts,
342
+ ),
343
+ )
344
+ conn.commit()
345
+ except sqlite3.Error as exc:
346
+ logger.warning("guard event write failed: %s", exc)
347
+
348
+ def timeseries(
349
+ self, metric: str, bucket: str = "hour", since_ts: float | None = None
350
+ ) -> list[dict[str, Any]]:
351
+ """Return a bucketed metric series for dashboard charts."""
352
+ columns = {
353
+ "requests": "COUNT(*)",
354
+ "cost": "COALESCE(SUM(cost_actual_usd), 0)",
355
+ "baseline": "COALESCE(SUM(cost_baseline_sim_usd), 0)",
356
+ "fresh": "COALESCE(SUM(fresh_input), 0)",
357
+ "cache_read": "COALESCE(SUM(cache_read), 0)",
358
+ "output": "COALESCE(SUM(output_tokens), 0)",
359
+ "tokens": "COALESCE(SUM(COALESCE(fresh_input, 0) + COALESCE(cache_read, 0) "
360
+ "+ COALESCE(output_tokens, 0)), 0)",
361
+ }
362
+ column = columns.get(metric, "COUNT(*)")
363
+ fmt = "%Y-%m-%dT%H:00:00" if bucket == "hour" else "%Y-%m-%d"
364
+ query = (
365
+ f"SELECT strftime('{fmt}', ts, 'unixepoch') AS bucket, {column} AS value " # noqa: S608
366
+ "FROM requests"
367
+ )
368
+ params: list[Any] = []
369
+ if since_ts is not None:
370
+ query += " WHERE ts >= ?"
371
+ params.append(since_ts)
372
+ query += " GROUP BY bucket ORDER BY bucket"
373
+ with sqlite3.connect(self.path) as conn:
374
+ conn.row_factory = sqlite3.Row
375
+ rows = conn.execute(query, params).fetchall()
376
+ return [dict(row) for row in rows]
377
+
378
+ def group_stats(
379
+ self, field: str, limit: int = 20, since_ts: float | None = None
380
+ ) -> list[dict[str, Any]]:
381
+ """Per-project/per-model rollups used by the overview page."""
382
+ column = "project" if field == "project" else "model"
383
+ query = (
384
+ f"SELECT {column} AS name, COUNT(*) AS requests, " # noqa: S608
385
+ "COALESCE(SUM(cost_actual_usd), 0) AS cost_usd, "
386
+ "COALESCE(SUM(fresh_input), 0) AS fresh_input, "
387
+ "COALESCE(SUM(cache_read), 0) AS cache_read, "
388
+ "COALESCE(SUM(output_tokens), 0) AS output_tokens "
389
+ "FROM requests"
390
+ )
391
+ params: list[Any] = []
392
+ if since_ts is not None:
393
+ query += " WHERE ts >= ?"
394
+ params.append(since_ts)
395
+ query += " GROUP BY name ORDER BY cost_usd DESC LIMIT ?"
396
+ params.append(limit)
397
+ with sqlite3.connect(self.path) as conn:
398
+ conn.row_factory = sqlite3.Row
399
+ rows = conn.execute(query, params).fetchall()
400
+ return [dict(row) for row in rows]
401
+
402
+ def transform_stats(self) -> list[dict[str, Any]]:
403
+ with sqlite3.connect(self.path) as conn:
404
+ conn.row_factory = sqlite3.Row
405
+ rows = conn.execute(
406
+ "SELECT transform_id, COUNT(*) AS runs, "
407
+ "COALESCE(SUM(changed), 0) AS changed_runs, "
408
+ "COALESCE(SUM(est_tokens_saved), 0) AS tokens_saved, "
409
+ "COALESCE(AVG(duration_ms), 0) AS avg_ms, "
410
+ "COALESCE(SUM(CASE WHEN discarded_reason = 'error' THEN 1 ELSE 0 END), 0) "
411
+ "AS errors, "
412
+ "COALESCE(SUM(CASE WHEN discarded_reason = 'no_regress' THEN 1 ELSE 0 END), 0) "
413
+ "AS no_regress, "
414
+ "COALESCE(SUM(fallback), 0) AS fallbacks, "
415
+ "MAX(ts) AS last_ts "
416
+ "FROM transform_reports "
417
+ "JOIN requests USING(request_id) "
418
+ "GROUP BY transform_id ORDER BY transform_id"
419
+ ).fetchall()
420
+ return [dict(row) for row in rows]
421
+
422
+ def export_rows(self, since_ts: float | None = None, limit: int = 1000) -> list[dict[str, Any]]:
423
+ query = "SELECT * FROM requests"
424
+ params: list[Any] = []
425
+ if since_ts is not None:
426
+ query += " WHERE ts >= ?"
427
+ params.append(since_ts)
428
+ query += " ORDER BY ts DESC LIMIT ?"
429
+ params.append(limit)
430
+ with sqlite3.connect(self.path) as conn:
431
+ conn.row_factory = sqlite3.Row
432
+ rows = conn.execute(query, params).fetchall()
433
+ return [dict(row) for row in rows]
434
+
435
+ def waste_summary(self, since_ts: float | None = None) -> dict[str, Any]:
436
+ query = (
437
+ "SELECT COUNT(*), COALESCE(SUM(est_tokens_wasted), 0), "
438
+ "COALESCE(SUM(est_cost_usd), 0) FROM waste_findings "
439
+ "WHERE status IN ('new', 'ack')"
440
+ )
441
+ params: list[Any] = []
442
+ if since_ts is not None:
443
+ query += " AND first_seen_ts >= ?"
444
+ params.append(since_ts)
445
+ with sqlite3.connect(self.path) as conn:
446
+ row = conn.execute(query, params).fetchone()
447
+ return {
448
+ "findings": int(row[0]),
449
+ "tokens": int(row[1]),
450
+ "cost_usd": float(row[2]),
451
+ }
452
+
453
+ def waste_timeseries(
454
+ self, bucket: str = "day", since_ts: float | None = None
455
+ ) -> list[dict[str, Any]]:
456
+ fmt = "%Y-%m-%dT%H:00:00" if bucket == "hour" else "%Y-%m-%d"
457
+ query = (
458
+ f"SELECT strftime('{fmt}', ts, 'unixepoch') AS bucket, " # noqa: S608
459
+ "COUNT(*) AS findings, COALESCE(SUM(est_tokens_wasted), 0) AS tokens, "
460
+ "COALESCE(SUM(est_cost_usd), 0) AS cost_usd FROM waste_findings "
461
+ "WHERE status IN ('new', 'ack')"
462
+ )
463
+ params: list[Any] = []
464
+ if since_ts is not None:
465
+ query += " AND first_seen_ts >= ?"
466
+ params.append(since_ts)
467
+ query += " GROUP BY bucket ORDER BY bucket"
468
+ with sqlite3.connect(self.path) as conn:
469
+ conn.row_factory = sqlite3.Row
470
+ rows = conn.execute(query, params).fetchall()
471
+ return [dict(row) for row in rows]
472
+
473
+ def unpriced(self, limit: int = 100) -> list[dict[str, Any]]:
474
+ with sqlite3.connect(self.path) as conn:
475
+ conn.row_factory = sqlite3.Row
476
+ rows = conn.execute(
477
+ "SELECT model, provider, COUNT(*) AS requests FROM requests "
478
+ "WHERE priced = 0 AND model IS NOT NULL "
479
+ "GROUP BY model, provider ORDER BY requests DESC LIMIT ?",
480
+ (limit,),
481
+ ).fetchall()
482
+ return [dict(row) for row in rows]
483
+
484
+ def summary(self, since_ts: float | None = None, project: str | None = None) -> dict[str, Any]:
485
+ query = (
486
+ "SELECT COUNT(*) AS requests, "
487
+ "COALESCE(SUM(cost_actual_usd), 0.0) AS cost_usd, "
488
+ "COALESCE(SUM(cost_baseline_sim_usd), 0.0) AS baseline_usd, "
489
+ "COALESCE(SUM(fresh_input), 0) AS fresh_input, "
490
+ "COALESCE(SUM(cache_read), 0) AS cache_read, "
491
+ "COALESCE(SUM(output_tokens), 0) AS output_tokens, "
492
+ "COALESCE(SUM(CASE WHEN COALESCE(orig_est_tokens, 0) > "
493
+ "COALESCE(final_est_tokens, 0) THEN orig_est_tokens - final_est_tokens "
494
+ "ELSE 0 END), 0) AS tokens_removed "
495
+ "FROM requests"
496
+ )
497
+ clauses: list[str] = []
498
+ params: list[Any] = []
499
+ if since_ts is not None:
500
+ clauses.append("ts >= ?")
501
+ params.append(since_ts)
502
+ if project:
503
+ clauses.append("project = ?")
504
+ params.append(project)
505
+ if clauses:
506
+ query += " WHERE " + " AND ".join(clauses)
507
+ with sqlite3.connect(self.path) as conn:
508
+ row = conn.execute(query, params).fetchone()
509
+ return {
510
+ "requests": int(row[0]),
511
+ "cost_usd": float(row[1]),
512
+ "baseline_usd": float(row[2]),
513
+ "fresh_input": int(row[3]),
514
+ "cache_read": int(row[4]),
515
+ "output_tokens": int(row[5]),
516
+ "tokens_removed": int(row[6]),
517
+ }
518
+
519
+ def prune(self, retention_days: int | None = None) -> int:
520
+ days = self.retention_days if retention_days is None else retention_days
521
+ cutoff = time.time() - days * 86_400
522
+ with sqlite3.connect(self.path) as conn:
523
+ deleted = conn.execute("DELETE FROM requests WHERE ts < ?", (cutoff,)).rowcount
524
+ conn.execute(
525
+ "DELETE FROM sessions WHERE session_id NOT IN "
526
+ "(SELECT DISTINCT session_id FROM requests)"
527
+ )
528
+ conn.commit()
529
+ return int(deleted)
530
+
531
+ def purge(self) -> None:
532
+ with sqlite3.connect(self.path) as conn:
533
+ for statement in (
534
+ "DELETE FROM captures",
535
+ "DELETE FROM blobs",
536
+ "DELETE FROM guard_events",
537
+ "DELETE FROM waste_findings",
538
+ "DELETE FROM drift_events",
539
+ "DELETE FROM transform_reports",
540
+ "DELETE FROM requests",
541
+ "DELETE FROM sessions",
542
+ ):
543
+ conn.execute(statement)
544
+ conn.commit()
545
+
546
+ def _migrate(self) -> None:
547
+ conn = sqlite3.connect(self.path)
548
+ try:
549
+ conn.execute("PRAGMA journal_mode=WAL")
550
+ conn.execute("PRAGMA synchronous=NORMAL")
551
+ conn.execute("PRAGMA foreign_keys=ON")
552
+ version = 0
553
+ if self.path.exists() and self.path.stat().st_size > 0:
554
+ try:
555
+ row = conn.execute(
556
+ "SELECT value FROM meta WHERE key = 'schema_version'"
557
+ ).fetchone()
558
+ except sqlite3.OperationalError:
559
+ row = None
560
+ version = int(row[0]) if row else 0
561
+ if version < SCHEMA_VERSION:
562
+ backup = self.path.with_name(f"{self.path.name}.bak-{version}")
563
+ try:
564
+ shutil.copy2(self.path, backup)
565
+ except OSError as exc:
566
+ logger.warning("database backup failed: %s", exc)
567
+ conn.executescript(_SCHEMA)
568
+ _ensure_column(conn, "requests", "request_headers_json", "TEXT")
569
+ _ensure_column(conn, "transform_reports", "fallback", "INTEGER NOT NULL DEFAULT 0")
570
+ row = conn.execute("SELECT value FROM meta WHERE key = 'schema_version'").fetchone()
571
+ current = int(row[0]) if row else 0
572
+ if current > SCHEMA_VERSION:
573
+ raise RuntimeError(f"database schema {current} is newer than {SCHEMA_VERSION}")
574
+ conn.execute(
575
+ "INSERT OR REPLACE INTO meta(key, value) VALUES('schema_version', ?)",
576
+ (str(SCHEMA_VERSION),),
577
+ )
578
+ conn.commit()
579
+ finally:
580
+ conn.close()
581
+ if os.name == "posix":
582
+ os.chmod(self.path, 0o600)
583
+
584
+ def _run(self) -> None:
585
+ conn = sqlite3.connect(self.path)
586
+ try:
587
+ conn.execute("PRAGMA journal_mode=WAL")
588
+ conn.execute("PRAGMA synchronous=NORMAL")
589
+ conn.execute("PRAGMA foreign_keys=ON")
590
+ while True:
591
+ first = self._queue.get()
592
+ if first is None:
593
+ self._queue.task_done()
594
+ break
595
+ batch: list[dict[str, Any]] = [first]
596
+ stop = False
597
+ deadline = time.monotonic() + 0.25
598
+ while len(batch) < 100:
599
+ try:
600
+ item = self._queue.get_nowait()
601
+ except queue.Empty:
602
+ break
603
+ if item is None:
604
+ self._queue.task_done()
605
+ stop = True
606
+ break
607
+ batch.append(item)
608
+ if time.monotonic() >= deadline:
609
+ break
610
+ try:
611
+ self._write(conn, batch)
612
+ except Exception as exc:
613
+ # Telemetry must never take the writer down (SPEC section 4.3).
614
+ self._last_error = exc
615
+ logger.warning("telemetry write failed: %s", exc)
616
+ for _ in batch:
617
+ self._queue.task_done()
618
+ if stop:
619
+ break
620
+ finally:
621
+ conn.close()
622
+
623
+ def _write(self, conn: sqlite3.Connection, batch: list[dict[str, Any]]) -> None:
624
+ conn.executemany(_REQUEST_SQL, [_request_row(record) for record in batch])
625
+ conn.executemany(
626
+ _SESSION_SQL,
627
+ [
628
+ (
629
+ record.get("session_id") or "unknown",
630
+ record.get("project"),
631
+ record.get("client"),
632
+ record.get("provider"),
633
+ record.get("kind") or "main",
634
+ record.get("ts") or time.time(),
635
+ record.get("ts") or time.time(),
636
+ )
637
+ for record in batch
638
+ ],
639
+ )
640
+ for record in batch:
641
+ request_id = record.get("request_id")
642
+ if not request_id:
643
+ continue
644
+ _insert_reports(conn, request_id, record.get("_transform_reports") or [])
645
+ fallback_ids = record.get("_fallback_transform_ids") or []
646
+ if fallback_ids:
647
+ conn.executemany(
648
+ "UPDATE transform_reports SET fallback = 1 "
649
+ "WHERE request_id = ? AND transform_id = ?",
650
+ [(request_id, transform_id) for transform_id in fallback_ids],
651
+ )
652
+ _insert_drift(conn, request_id, record)
653
+ _insert_findings(conn, record.get("_findings") or [])
654
+ _insert_captures(conn, request_id, record.get("_captures") or [])
655
+ conn.commit()
656
+
657
+
658
+ def _ensure_column(conn: sqlite3.Connection, table: str, column: str, declaration: str) -> None:
659
+ names = {row[1] for row in conn.execute(f"PRAGMA table_info({table})")}
660
+ if column not in names:
661
+ conn.execute(f"ALTER TABLE {table} ADD COLUMN {column} {declaration}")
662
+
663
+
664
+ def _get(value: object, name: str, default: Any = None) -> Any:
665
+ if isinstance(value, dict):
666
+ return value.get(name, default)
667
+ return getattr(value, name, default)
668
+
669
+
670
+ def _insert_reports(conn: sqlite3.Connection, request_id: str, reports: list[Any]) -> None:
671
+ rows: list[tuple[Any, ...]] = []
672
+ for report in reports:
673
+ before = _get(report, "est_tokens_before", 0) or 0
674
+ after = _get(report, "est_tokens_after", 0) or 0
675
+ notes = _get(report, "notes", {}) or {}
676
+ try:
677
+ notes_json = json.dumps(notes, default=str)
678
+ except (TypeError, ValueError):
679
+ notes_json = "{}"
680
+ rows.append(
681
+ (
682
+ request_id,
683
+ _get(report, "transform_id", "unknown"),
684
+ 1 if _get(report, "changed") else 0,
685
+ _get(report, "items_touched", 0),
686
+ _get(report, "bytes_before", 0),
687
+ _get(report, "bytes_after", 0),
688
+ max(int(before) - int(after), 0),
689
+ _get(report, "duration_ms", 0.0),
690
+ _get(report, "discarded_reason"),
691
+ _get(report, "error"),
692
+ notes_json,
693
+ )
694
+ )
695
+ if rows:
696
+ conn.executemany(
697
+ "INSERT OR REPLACE INTO transform_reports("
698
+ "request_id, transform_id, changed, items_touched, bytes_before, "
699
+ "bytes_after, est_tokens_saved, duration_ms, discarded_reason, error, "
700
+ "notes_json) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
701
+ rows,
702
+ )
703
+
704
+
705
+ def _insert_drift(conn: sqlite3.Connection, request_id: str, record: dict[str, Any]) -> None:
706
+ drift = record.get("_drift")
707
+ if drift is None:
708
+ return
709
+ conn.execute(
710
+ "INSERT INTO drift_events("
711
+ "request_id, session_id, ts, stream, cause, at_segment, est_tokens_lost, "
712
+ "detail_json) VALUES(?, ?, ?, 'original', ?, ?, ?, ?)",
713
+ (
714
+ request_id,
715
+ record.get("session_id"),
716
+ record.get("ts") or time.time(),
717
+ _get(drift, "cause", "history_mutated"),
718
+ _get(drift, "at_segment"),
719
+ _get(drift, "est_tokens_lost", 0),
720
+ json.dumps(_get(drift, "detail", {}), default=str),
721
+ ),
722
+ )
723
+
724
+
725
+ def _insert_findings(conn: sqlite3.Connection, findings: list[Any]) -> None:
726
+ rows: list[tuple[Any, ...]] = []
727
+ for finding in findings:
728
+ key_hash = _get(finding, "key_hash", b"")
729
+ if isinstance(key_hash, bytes):
730
+ key_hash = key_hash.hex()
731
+ session_id = _get(finding, "session_id")
732
+ detector = _get(finding, "detector", "unknown")
733
+ existing = conn.execute(
734
+ "SELECT finding_id, status FROM waste_findings "
735
+ "WHERE session_id IS ? AND detector = ? AND key_hash = ?",
736
+ (session_id, detector, str(key_hash)),
737
+ ).fetchone()
738
+ if existing is not None:
739
+ conn.execute(
740
+ "UPDATE waste_findings SET last_seen_ts = ?, occurrences = ?, "
741
+ "est_tokens_wasted = MAX(COALESCE(est_tokens_wasted, 0), ?), "
742
+ "est_cost_usd = COALESCE(?, est_cost_usd) "
743
+ "WHERE finding_id = ?",
744
+ (
745
+ _get(finding, "last_seen_ts", time.time()),
746
+ _get(finding, "occurrences", 1),
747
+ _get(finding, "est_tokens_wasted") or 0,
748
+ _get(finding, "est_cost_usd"),
749
+ existing[0],
750
+ ),
751
+ )
752
+ continue
753
+ rows.append(
754
+ (
755
+ _get(finding, "finding_id"),
756
+ _get(finding, "ts", time.time()),
757
+ _get(finding, "first_seen_ts", time.time()),
758
+ _get(finding, "last_seen_ts", time.time()),
759
+ session_id,
760
+ _get(finding, "project"),
761
+ detector,
762
+ str(key_hash),
763
+ _get(finding, "request_id"),
764
+ _get(finding, "occurrences", 1),
765
+ _get(finding, "est_tokens_wasted"),
766
+ _get(finding, "est_cost_usd"),
767
+ _get(finding, "status", "new"),
768
+ json.dumps(_get(finding, "evidence", {}), default=str),
769
+ )
770
+ )
771
+ if rows:
772
+ conn.executemany(
773
+ "INSERT OR IGNORE INTO waste_findings("
774
+ "finding_id, ts, first_seen_ts, last_seen_ts, session_id, project, "
775
+ "detector, key_hash, request_id, occurrences, est_tokens_wasted, "
776
+ "est_cost_usd, status, evidence_json) "
777
+ "VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)",
778
+ rows,
779
+ )
780
+
781
+
782
+ def _insert_captures(conn: sqlite3.Connection, request_id: str, captures: list[Any]) -> None:
783
+ rows = [
784
+ (
785
+ request_id,
786
+ _get(capture, "transform_id"),
787
+ _get(capture, "location"),
788
+ _get(capture, "before_hash"),
789
+ _get(capture, "after_hash"),
790
+ )
791
+ for capture in captures
792
+ ]
793
+ if rows:
794
+ conn.executemany(
795
+ "INSERT INTO captures(request_id, transform_id, location, before_hash, "
796
+ "after_hash) VALUES(?, ?, ?, ?, ?)",
797
+ rows,
798
+ )
799
+
800
+
801
+ def _request_row(record: dict[str, Any]) -> tuple[Any, ...]:
802
+ return tuple(record.get(column) for column in _REQUEST_COLUMNS)
803
+
804
+
805
+ def _normalise(record: dict[str, Any]) -> dict[str, Any]:
806
+ normalised = dict(record)
807
+ if normalised.get("ts") is None:
808
+ normalised["ts"] = time.time()
809
+ for key, fallback in (
810
+ ("session_id", "unknown"),
811
+ ("provider", "unknown"),
812
+ ("endpoint", "passthrough"),
813
+ ("mode", "shadow"),
814
+ ):
815
+ if not normalised.get(key):
816
+ normalised[key] = fallback
817
+ for key in ("mutated", "fallback_used", "priced"):
818
+ if normalised.get(key) is None:
819
+ normalised[key] = 0
820
+ return normalised
821
+
822
+
823
+ def default_db_path() -> Path:
824
+ path = data_dir() / "ctxoptimizer.db"
825
+ path.parent.mkdir(parents=True, exist_ok=True)
826
+ return path
827
+
828
+
829
+ __all__ = ["SCHEMA_VERSION", "Store", "default_db_path"]