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.
- ctxoptimizer/__init__.py +5 -0
- ctxoptimizer/__main__.py +14 -0
- ctxoptimizer/admin_security.py +130 -0
- ctxoptimizer/analysis/__init__.py +1 -0
- ctxoptimizer/analysis/baseline.py +69 -0
- ctxoptimizer/analysis/break_even.py +49 -0
- ctxoptimizer/analysis/drift.py +151 -0
- ctxoptimizer/analysis/edits.py +148 -0
- ctxoptimizer/analysis/markers.py +34 -0
- ctxoptimizer/analysis/recommend.py +114 -0
- ctxoptimizer/analysis/segments.py +57 -0
- ctxoptimizer/analysis/session.py +62 -0
- ctxoptimizer/analysis/state.py +239 -0
- ctxoptimizer/analysis/tokens.py +62 -0
- ctxoptimizer/analysis/waste.py +322 -0
- ctxoptimizer/cli.py +873 -0
- ctxoptimizer/config.py +512 -0
- ctxoptimizer/dashboard/__init__.py +1 -0
- ctxoptimizer/dashboard/api.py +537 -0
- ctxoptimizer/dashboard/service.py +157 -0
- ctxoptimizer/dashboard/static/.gitkeep +1 -0
- ctxoptimizer/dashboard/templates/.gitkeep +1 -0
- ctxoptimizer/dashboard/templates/doctor.html +46 -0
- ctxoptimizer/dashboard/templates/live.html +91 -0
- ctxoptimizer/dashboard/templates/overview.html +172 -0
- ctxoptimizer/dashboard/templates/pricing.html +115 -0
- ctxoptimizer/dashboard/templates/request.html +111 -0
- ctxoptimizer/dashboard/templates/session.html +67 -0
- ctxoptimizer/dashboard/templates/settings.html +79 -0
- ctxoptimizer/dashboard/templates/table.html +56 -0
- ctxoptimizer/dashboard/templates/transforms.html +118 -0
- ctxoptimizer/dashboard/templates/waste.html +120 -0
- ctxoptimizer/dashboard/views.py +429 -0
- ctxoptimizer/data/cache_limits.json +23 -0
- ctxoptimizer/data/default_config.toml +185 -0
- ctxoptimizer/data/pricing.json +421 -0
- ctxoptimizer/doctor.py +129 -0
- ctxoptimizer/guard.py +139 -0
- ctxoptimizer/ids.py +73 -0
- ctxoptimizer/log.py +73 -0
- ctxoptimizer/mcp.py +124 -0
- ctxoptimizer/models.py +145 -0
- ctxoptimizer/pipeline.py +126 -0
- ctxoptimizer/profiles.py +152 -0
- ctxoptimizer/providers/__init__.py +37 -0
- ctxoptimizer/providers/anthropic.py +105 -0
- ctxoptimizer/providers/base.py +191 -0
- ctxoptimizer/providers/gemini.py +143 -0
- ctxoptimizer/providers/ollama.py +133 -0
- ctxoptimizer/providers/openai_chat.py +135 -0
- ctxoptimizer/providers/openai_responses.py +121 -0
- ctxoptimizer/proxy.py +665 -0
- ctxoptimizer/routing.py +259 -0
- ctxoptimizer/server.py +319 -0
- ctxoptimizer/service.py +150 -0
- ctxoptimizer/telemetry/__init__.py +1 -0
- ctxoptimizer/telemetry/blobs.py +57 -0
- ctxoptimizer/telemetry/expand.py +62 -0
- ctxoptimizer/telemetry/pricing.py +168 -0
- ctxoptimizer/telemetry/redact.py +53 -0
- ctxoptimizer/telemetry/sse.py +145 -0
- ctxoptimizer/telemetry/store.py +829 -0
- ctxoptimizer/telemetry/usage.py +435 -0
- ctxoptimizer/transforms/__init__.py +87 -0
- ctxoptimizer/transforms/ansi.py +21 -0
- ctxoptimizer/transforms/anthropic_cache.py +100 -0
- ctxoptimizer/transforms/backref.py +74 -0
- ctxoptimizer/transforms/base.py +93 -0
- ctxoptimizer/transforms/cache_serve.py +146 -0
- ctxoptimizer/transforms/dedupe_lines.py +69 -0
- ctxoptimizer/transforms/eligibility.py +24 -0
- ctxoptimizer/transforms/openai_hints.py +84 -0
- ctxoptimizer/transforms/progress.py +52 -0
- ctxoptimizer/transforms/stale_result_elision.py +109 -0
- ctxoptimizer/transforms/truncate.py +100 -0
- ctxoptimizer/transforms/volatile.py +177 -0
- ctxoptimizer/transforms/whitespace.py +25 -0
- ctxoptimizer-0.4.1.dist-info/METADATA +196 -0
- ctxoptimizer-0.4.1.dist-info/RECORD +83 -0
- ctxoptimizer-0.4.1.dist-info/WHEEL +4 -0
- ctxoptimizer-0.4.1.dist-info/entry_points.txt +3 -0
- ctxoptimizer-0.4.1.dist-info/licenses/LICENSE +21 -0
- 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"]
|