synckey 0.1.0__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.
synckey/config.py ADDED
@@ -0,0 +1,150 @@
1
+ """Configuration and paths.
2
+
3
+ State lives under $SYNCKEY_HOME (default ~/.synckey):
4
+ secret.key Fernet key for sealing provider credentials (0600)
5
+ synckey.db SQLite: keys, usage, events, key state, metadata
6
+ config.toml user settings and custom providers
7
+
8
+ The unified gateway key is generated once at init time; only its SHA-256 hash
9
+ is persisted, so the plaintext is shown exactly once.
10
+ """
11
+
12
+ from __future__ import annotations
13
+
14
+ import os
15
+ import secrets
16
+ import tomllib
17
+ from dataclasses import dataclass, field
18
+ from pathlib import Path
19
+
20
+ UNIFIED_KEY_PREFIX = "sk-synckey-"
21
+
22
+
23
+ def home() -> Path:
24
+ return Path(os.environ.get("SYNCKEY_HOME", Path.home() / ".synckey"))
25
+
26
+
27
+ def secret_key_path() -> Path:
28
+ return home() / "secret.key"
29
+
30
+
31
+ def db_path() -> Path:
32
+ return Path(os.environ.get("SYNCKEY_DB", home() / "synckey.db"))
33
+
34
+
35
+ def config_path() -> Path:
36
+ return home() / "config.toml"
37
+
38
+
39
+ @dataclass
40
+ class Settings:
41
+ host: str = "127.0.0.1"
42
+ port: int = 8787
43
+ request_timeout: float = 120.0
44
+ max_connections: int = 600
45
+ max_keepalive: int = 300
46
+ # Fallback: ordered preference when several providers serve the same model.
47
+ provider_priority: list[str] = field(default_factory=list)
48
+ cross_provider_fallback: bool = True
49
+ # Per-request attempt budget across the key pool.
50
+ max_retries: int = 6
51
+ default_cooldown: float = 20.0
52
+ # Tier fallback: allow cross-tier fallback to same-tier alternatives.
53
+ tier_fallback_enabled: bool = True
54
+ # Seconds of cooldown remaining before we skip to tier alternatives entirely.
55
+ deep_cooling_threshold: float = 60.0
56
+ # User-defined model -> tier overrides, e.g. {"my-model": "frontier"}.
57
+ tier_overrides: dict[str, str] = field(default_factory=dict)
58
+ # User-defined model pricing overrides, e.g. {"my-model": [1.0, 3.0]}.
59
+ price_overrides: dict[str, list] = field(default_factory=dict)
60
+ custom_providers: list[dict] = field(default_factory=list)
61
+ # Deferred queue: when all keys are cooling, accept the request, return a
62
+ # request id, run it when capacity frees, and serve the result on a later GET.
63
+ deferred_enabled: bool = True
64
+ deferred_ttl: float = 3600.0 # keep a finished response this long, then purge
65
+ deferred_poll: float = 5.0 # worker scan interval (seconds)
66
+ deferred_max_queue: int = 1000 # reject new deferrals beyond this many queued
67
+ deferred_max_queue_age: float = 86400.0 # drop a job that never ran within this window
68
+
69
+ @classmethod
70
+ def load(cls) -> "Settings":
71
+ path = config_path()
72
+ if not path.exists():
73
+ return cls()
74
+ data = tomllib.loads(path.read_text())
75
+ gw = data.get("gateway", {})
76
+ routing = data.get("routing", {})
77
+ tiers = data.get("tiers", {})
78
+ deferred = data.get("deferred", {})
79
+ return cls(
80
+ host=gw.get("host", cls.host),
81
+ port=gw.get("port", cls.port),
82
+ request_timeout=gw.get("request_timeout", cls.request_timeout),
83
+ max_connections=gw.get("max_connections", cls.max_connections),
84
+ max_keepalive=gw.get("max_keepalive", cls.max_keepalive),
85
+ provider_priority=routing.get("priority", []),
86
+ cross_provider_fallback=routing.get("cross_provider_fallback", True),
87
+ max_retries=routing.get("max_retries", cls.max_retries),
88
+ default_cooldown=routing.get("default_cooldown", cls.default_cooldown),
89
+ tier_fallback_enabled=routing.get("tier_fallback", True),
90
+ deep_cooling_threshold=routing.get("deep_cooling_threshold", cls.deep_cooling_threshold),
91
+ tier_overrides=tiers.get("overrides", {}),
92
+ price_overrides=tiers.get("prices", {}),
93
+ custom_providers=data.get("providers", []),
94
+ deferred_enabled=deferred.get("enabled", True),
95
+ deferred_ttl=deferred.get("ttl", cls.deferred_ttl),
96
+ deferred_poll=deferred.get("poll", cls.deferred_poll),
97
+ deferred_max_queue=deferred.get("max_queue", cls.deferred_max_queue),
98
+ deferred_max_queue_age=deferred.get("max_queue_age", cls.deferred_max_queue_age),
99
+ )
100
+
101
+ def save(self) -> None:
102
+ """Write current settings back to config.toml."""
103
+ import tomllib
104
+
105
+ path = config_path()
106
+ data = {}
107
+ if path.exists():
108
+ try:
109
+ data = tomllib.loads(path.read_text())
110
+ except Exception:
111
+ pass
112
+ data.setdefault("gateway", {})["host"] = self.host
113
+ data.setdefault("gateway", {})["port"] = self.port
114
+ data.setdefault("gateway", {})["request_timeout"] = self.request_timeout
115
+ data.setdefault("gateway", {})["max_connections"] = self.max_connections
116
+ data.setdefault("gateway", {})["max_keepalive"] = self.max_keepalive
117
+ data.setdefault("routing", {})["priority"] = self.provider_priority
118
+ data.setdefault("routing", {})["cross_provider_fallback"] = self.cross_provider_fallback
119
+ data.setdefault("routing", {})["max_retries"] = self.max_retries
120
+ data.setdefault("routing", {})["default_cooldown"] = self.default_cooldown
121
+ data.setdefault("routing", {})["tier_fallback"] = self.tier_fallback_enabled
122
+ data.setdefault("routing", {})["deep_cooling_threshold"] = self.deep_cooling_threshold
123
+ data.setdefault("tiers", {})["overrides"] = self.tier_overrides
124
+ data.setdefault("tiers", {})["prices"] = self.price_overrides
125
+ data["providers"] = self.custom_providers
126
+ data.setdefault("deferred", {})["enabled"] = self.deferred_enabled
127
+ data.setdefault("deferred", {})["ttl"] = self.deferred_ttl
128
+ data.setdefault("deferred", {})["poll"] = self.deferred_poll
129
+ data.setdefault("deferred", {})["max_queue"] = self.deferred_max_queue
130
+ data.setdefault("deferred", {})["max_queue_age"] = self.deferred_max_queue_age
131
+
132
+ try:
133
+ import tomli
134
+ except ImportError:
135
+ import toml as tomli
136
+ path.write_text(tomli.dumps(data))
137
+
138
+
139
+ def ensure_home() -> Path:
140
+ h = home()
141
+ h.mkdir(parents=True, exist_ok=True)
142
+ try:
143
+ os.chmod(h, 0o700)
144
+ except OSError:
145
+ pass
146
+ return h
147
+
148
+
149
+ def generate_unified_key() -> str:
150
+ return UNIFIED_KEY_PREFIX + secrets.token_urlsafe(32)
synckey/context.py ADDED
@@ -0,0 +1,75 @@
1
+ """Shared runtime wiring."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import os
6
+
7
+ from .config import Settings, db_path, secret_key_path
8
+ from .crypto import SecretBox
9
+ from .db import Database
10
+ from .keypool import KeyPool
11
+ from .prefs import Prefs
12
+ from .providers import BUILTIN, Provider
13
+ from .router import Router
14
+ from .state import StateStore
15
+ from .tiers import load_overrides
16
+
17
+
18
+ def merged_providers(settings: Settings) -> dict[str, Provider]:
19
+ providers = dict(BUILTIN)
20
+ for spec in settings.custom_providers:
21
+ try:
22
+ prov = Provider(
23
+ id=spec["id"],
24
+ name=spec.get("name", spec["id"]),
25
+ base_url=spec["base_url"],
26
+ models_path=spec.get("models_path", "/models"),
27
+ env=tuple(spec.get("env", ())),
28
+ patterns=tuple(spec.get("patterns", ())),
29
+ auth=spec.get("auth", "bearer"),
30
+ signup=spec.get("signup", ""),
31
+ notes=spec.get("notes", ""),
32
+ )
33
+ providers[prov.id] = prov
34
+ except KeyError:
35
+ continue
36
+ return providers
37
+
38
+
39
+ class Context:
40
+ def __init__(self) -> None:
41
+ self.settings = Settings.load()
42
+ self.providers = merged_providers(self.settings)
43
+ self.box = SecretBox(secret_key_path())
44
+ self.db = Database(db_path())
45
+ self.prefs = Prefs(self.db)
46
+ # Tier overrides come from two places: config.toml (static, versioned) and
47
+ # the DB (set via `synckey tier set`). DB wins on conflict.
48
+ merged_tiers = {**self.settings.tier_overrides, **self.prefs.tier_overrides()}
49
+ load_overrides(merged_tiers, self.settings.price_overrides)
50
+ self.states = StateStore(self.db.path)
51
+ self.pool = KeyPool(
52
+ self.db,
53
+ self.box,
54
+ self.states,
55
+ default_cooldown=self.settings.default_cooldown,
56
+ )
57
+ self.router = Router(self.db, self.providers, self.settings, self.prefs)
58
+
59
+ def first_secret(self, provider_id: str) -> str | None:
60
+ keys = self.db.list_keys(provider=provider_id, enabled_only=True)
61
+ if keys:
62
+ return self.box.open(keys[0].secret)
63
+ prov = self.providers.get(provider_id)
64
+ if prov:
65
+ for env in prov.env:
66
+ if os.environ.get(env):
67
+ return os.environ[env]
68
+ return None
69
+
70
+ def unified_key_hash(self) -> str | None:
71
+ return self.db.get_meta("unified_key_hash")
72
+
73
+ def close(self) -> None:
74
+ self.states.close()
75
+ self.db.close()
synckey/crypto.py ADDED
@@ -0,0 +1,41 @@
1
+ """At-rest encryption for stored provider secrets.
2
+
3
+ One symmetric key lives in ~/.synckey/secret.key with 0600 perms. Provider
4
+ credentials are sealed with Fernet so the database never holds plaintext. Lose
5
+ the secret file and the stored credentials are unrecoverable by design.
6
+ """
7
+
8
+ from __future__ import annotations
9
+
10
+ import os
11
+ from pathlib import Path
12
+
13
+ from cryptography.fernet import Fernet, InvalidToken
14
+
15
+
16
+ class SecretBox:
17
+ def __init__(self, key_path: Path):
18
+ self.key_path = key_path
19
+ self._fernet = Fernet(self._load_or_create())
20
+
21
+ def _load_or_create(self) -> bytes:
22
+ if self.key_path.exists():
23
+ return self.key_path.read_bytes()
24
+ self.key_path.parent.mkdir(parents=True, exist_ok=True)
25
+ key = Fernet.generate_key()
26
+ fd = os.open(self.key_path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600)
27
+ with os.fdopen(fd, "wb") as fh:
28
+ fh.write(key)
29
+ return key
30
+
31
+ def seal(self, plaintext: str) -> bytes:
32
+ return self._fernet.encrypt(plaintext.encode("utf-8"))
33
+
34
+ def open(self, token: bytes) -> str:
35
+ try:
36
+ return self._fernet.decrypt(token).decode("utf-8")
37
+ except InvalidToken as exc:
38
+ raise RuntimeError(
39
+ "Could not decrypt a stored secret. The secret.key file may have "
40
+ "changed. Re-add affected keys with `synckey setup`."
41
+ ) from exc
synckey/db.py ADDED
@@ -0,0 +1,426 @@
1
+ """SQLite persistence: credentials, usage records, events, metadata."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import hashlib
6
+ import json
7
+ import sqlite3
8
+ import time
9
+ from dataclasses import dataclass
10
+ from pathlib import Path
11
+
12
+ SCHEMA = """
13
+ CREATE TABLE IF NOT EXISTS meta (
14
+ name TEXT PRIMARY KEY,
15
+ value TEXT NOT NULL
16
+ );
17
+
18
+ CREATE TABLE IF NOT EXISTS keys (
19
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
20
+ provider TEXT NOT NULL,
21
+ label TEXT NOT NULL,
22
+ secret BLOB NOT NULL,
23
+ enabled INTEGER NOT NULL DEFAULT 1,
24
+ weight INTEGER NOT NULL DEFAULT 1,
25
+ rpm_limit REAL,
26
+ tpm_limit REAL,
27
+ created_at REAL NOT NULL
28
+ );
29
+
30
+ CREATE TABLE IF NOT EXISTS usage (
31
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
32
+ ts REAL NOT NULL,
33
+ provider TEXT NOT NULL,
34
+ model TEXT NOT NULL,
35
+ key_id INTEGER,
36
+ prompt_tokens INTEGER NOT NULL DEFAULT 0,
37
+ completion_tokens INTEGER NOT NULL DEFAULT 0,
38
+ total_tokens INTEGER NOT NULL DEFAULT 0,
39
+ cost_usd REAL,
40
+ status_code INTEGER NOT NULL DEFAULT 0,
41
+ latency_ms INTEGER NOT NULL DEFAULT 0,
42
+ stream INTEGER NOT NULL DEFAULT 0,
43
+ tier INTEGER,
44
+ error TEXT
45
+ );
46
+
47
+ CREATE TABLE IF NOT EXISTS events (
48
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
49
+ ts REAL NOT NULL,
50
+ type TEXT NOT NULL,
51
+ key_id INTEGER,
52
+ provider TEXT,
53
+ model TEXT,
54
+ fallback_to TEXT,
55
+ tier_from INTEGER,
56
+ tier_to INTEGER,
57
+ chain TEXT,
58
+ message TEXT
59
+ );
60
+
61
+ CREATE TABLE IF NOT EXISTS deferred (
62
+ id TEXT PRIMARY KEY,
63
+ created_at REAL NOT NULL,
64
+ status TEXT NOT NULL, -- queued | running | done | error
65
+ path TEXT NOT NULL, -- e.g. /chat/completions
66
+ body TEXT NOT NULL, -- JSON request body (non-stream)
67
+ model TEXT, -- original model string (may be null)
68
+ provider_hint TEXT,
69
+ floor INTEGER,
70
+ eta REAL, -- approx unix time the call can run
71
+ response BLOB, -- stored upstream bytes when done
72
+ status_code INTEGER,
73
+ error TEXT,
74
+ completed_at REAL,
75
+ attempts INTEGER NOT NULL DEFAULT 0
76
+ );
77
+
78
+ CREATE INDEX IF NOT EXISTS idx_usage_ts ON usage(ts);
79
+ CREATE INDEX IF NOT EXISTS idx_usage_provider ON usage(provider);
80
+ CREATE INDEX IF NOT EXISTS idx_events_ts ON events(ts);
81
+ CREATE INDEX IF NOT EXISTS idx_keys_provider ON keys(provider);
82
+ CREATE INDEX IF NOT EXISTS idx_deferred_status ON deferred(status);
83
+
84
+ CREATE TABLE IF NOT EXISTS routing_chains (
85
+ id INTEGER PRIMARY KEY AUTOINCREMENT,
86
+ ts REAL NOT NULL,
87
+ model TEXT NOT NULL,
88
+ provider TEXT NOT NULL,
89
+ key_id INTEGER,
90
+ chain TEXT NOT NULL
91
+ );
92
+ """
93
+
94
+ USAGE_COLS = (
95
+ "ts, provider, model, key_id, prompt_tokens, completion_tokens, "
96
+ "total_tokens, cost_usd, status_code, latency_ms, stream, tier, error"
97
+ )
98
+ USAGE_INSERT = f"INSERT INTO usage({USAGE_COLS}) VALUES (?,?,?,?,?,?,?,?,?,?,?,?,?)"
99
+
100
+
101
+ def tune(conn: sqlite3.Connection) -> None:
102
+ conn.execute("PRAGMA journal_mode=WAL")
103
+ conn.execute("PRAGMA synchronous=NORMAL")
104
+ conn.execute("PRAGMA busy_timeout=5000")
105
+ conn.execute("PRAGMA temp_store=MEMORY")
106
+ conn.execute("PRAGMA cache_size=-8000")
107
+
108
+
109
+ @dataclass
110
+ class KeyRecord:
111
+ id: int
112
+ provider: str
113
+ label: str
114
+ secret: bytes
115
+ enabled: bool
116
+ weight: int
117
+ rpm_limit: float | None
118
+ tpm_limit: float | None
119
+ created_at: float
120
+
121
+
122
+ def sha256(text: str) -> str:
123
+ return hashlib.sha256(text.encode("utf-8")).hexdigest()
124
+
125
+
126
+ class Database:
127
+ def __init__(self, path: Path):
128
+ self.path = Path(path)
129
+ self.path.parent.mkdir(parents=True, exist_ok=True)
130
+ self.conn = sqlite3.connect(self.path, check_same_thread=False)
131
+ self.conn.row_factory = sqlite3.Row
132
+ tune(self.conn)
133
+ self.conn.executescript(SCHEMA)
134
+ self.conn.commit()
135
+
136
+ def close(self) -> None:
137
+ self.conn.close()
138
+
139
+ # meta
140
+ def set_meta(self, name: str, value: str) -> None:
141
+ self.conn.execute(
142
+ "INSERT INTO meta(name, value) VALUES(?,?) "
143
+ "ON CONFLICT(name) DO UPDATE SET value=excluded.value",
144
+ (name, value),
145
+ )
146
+ self.conn.commit()
147
+
148
+ def get_meta(self, name: str) -> str | None:
149
+ row = self.conn.execute("SELECT value FROM meta WHERE name=?", (name,)).fetchone()
150
+ return row["value"] if row else None
151
+
152
+ def get_json(self, name: str, default=None):
153
+ raw = self.get_meta(name)
154
+ if not raw:
155
+ return default
156
+ try:
157
+ return json.loads(raw)
158
+ except ValueError:
159
+ return default
160
+
161
+ def set_json(self, name: str, obj) -> None:
162
+ self.set_meta(name, json.dumps(obj))
163
+
164
+ # keys
165
+ def add_key(
166
+ self,
167
+ provider: str,
168
+ label: str,
169
+ secret: bytes,
170
+ weight: int = 1,
171
+ rpm_limit: float | None = None,
172
+ tpm_limit: float | None = None,
173
+ ) -> int:
174
+ cur = self.conn.execute(
175
+ "INSERT INTO keys(provider, label, secret, weight, rpm_limit, tpm_limit, created_at) "
176
+ "VALUES(?,?,?,?,?,?,?)",
177
+ (provider, label, secret, weight, rpm_limit, tpm_limit, time.time()),
178
+ )
179
+ self.conn.commit()
180
+ return int(cur.lastrowid)
181
+
182
+ def remove_key(self, key_id: int) -> bool:
183
+ cur = self.conn.execute("DELETE FROM keys WHERE id=?", (key_id,))
184
+ self.conn.commit()
185
+ return cur.rowcount > 0
186
+
187
+ def set_key_enabled(self, key_id: int, enabled: bool) -> None:
188
+ self.conn.execute("UPDATE keys SET enabled=? WHERE id=?", (1 if enabled else 0, key_id))
189
+ self.conn.commit()
190
+
191
+ def set_key_limits(self, key_id: int, rpm: float | None, tpm: float | None) -> None:
192
+ self.conn.execute(
193
+ "UPDATE keys SET rpm_limit=?, tpm_limit=? WHERE id=?", (rpm, tpm, key_id)
194
+ )
195
+ self.conn.commit()
196
+
197
+ def list_keys(self, provider: str | None = None, enabled_only: bool = False) -> list[KeyRecord]:
198
+ sql = "SELECT * FROM keys"
199
+ clauses, params = [], []
200
+ if provider:
201
+ clauses.append("provider=?")
202
+ params.append(provider)
203
+ if enabled_only:
204
+ clauses.append("enabled=1")
205
+ if clauses:
206
+ sql += " WHERE " + " AND ".join(clauses)
207
+ sql += " ORDER BY provider, id"
208
+ return [
209
+ KeyRecord(
210
+ id=r["id"],
211
+ provider=r["provider"],
212
+ label=r["label"],
213
+ secret=r["secret"],
214
+ enabled=bool(r["enabled"]),
215
+ weight=r["weight"],
216
+ rpm_limit=r["rpm_limit"],
217
+ tpm_limit=r["tpm_limit"],
218
+ created_at=r["created_at"],
219
+ )
220
+ for r in self.conn.execute(sql, params).fetchall()
221
+ ]
222
+
223
+ def providers_with_keys(self) -> list[str]:
224
+ rows = self.conn.execute(
225
+ "SELECT DISTINCT provider FROM keys WHERE enabled=1 ORDER BY provider"
226
+ ).fetchall()
227
+ return [r["provider"] for r in rows]
228
+
229
+ def key_stats(self) -> dict[int, dict]:
230
+ """Per-key request/error/token/cost totals from usage log."""
231
+ rows = self.conn.execute(
232
+ "SELECT key_id, COUNT(*) reqs, "
233
+ "SUM(CASE WHEN status_code>=400 OR error IS NOT NULL THEN 1 ELSE 0 END) errs, "
234
+ "SUM(total_tokens) tokens, SUM(cost_usd) cost "
235
+ "FROM usage WHERE key_id IS NOT NULL GROUP BY key_id"
236
+ ).fetchall()
237
+ return {
238
+ r["key_id"]: {
239
+ "requests": r["reqs"],
240
+ "errors": r["errs"] or 0,
241
+ "tokens": r["tokens"] or 0,
242
+ "cost": r["cost"] or 0.0,
243
+ }
244
+ for r in rows
245
+ }
246
+
247
+ # events
248
+ def record_event(
249
+ self,
250
+ *,
251
+ type: str,
252
+ key_id: int | None = None,
253
+ provider: str | None = None,
254
+ model: str | None = None,
255
+ fallback_to: str | None = None,
256
+ tier_from: int | None = None,
257
+ tier_to: int | None = None,
258
+ chain: str | None = None,
259
+ message: str | None = None,
260
+ ) -> None:
261
+ self.conn.execute(
262
+ "INSERT INTO events(ts,type,key_id,provider,model,fallback_to,tier_from,tier_to,chain,message) "
263
+ "VALUES(?,?,?,?,?,?,?,?,?,?)",
264
+ (time.time(), type, key_id, provider, model, fallback_to, tier_from, tier_to, chain, message),
265
+ )
266
+ self.conn.commit()
267
+
268
+ def record_routing_chain(
269
+ self,
270
+ *,
271
+ model: str,
272
+ provider: str,
273
+ key_id: int,
274
+ chain: str,
275
+ ) -> None:
276
+ """Record the full routing chain for a request for TUI view 4."""
277
+ self.conn.execute(
278
+ "INSERT INTO routing_chains(ts, model, provider, key_id, chain) VALUES(?,?,?,?,?)",
279
+ (time.time(), model, provider, key_id, chain),
280
+ )
281
+ self.conn.commit()
282
+
283
+ def recent_events(self, limit: int = 50, event_type: str | None = None) -> list[sqlite3.Row]:
284
+ if event_type:
285
+ return self.conn.execute(
286
+ "SELECT * FROM events WHERE type=? ORDER BY ts DESC LIMIT ?", (event_type, limit)
287
+ ).fetchall()
288
+ return self.conn.execute(
289
+ "SELECT * FROM events ORDER BY ts DESC LIMIT ?", (limit,)
290
+ ).fetchall()
291
+
292
+ def recent_routing_chains(self, limit: int = 20) -> list[sqlite3.Row]:
293
+ """Return the most recent routing chains for TUI view 4."""
294
+ return self.conn.execute(
295
+ "SELECT * FROM routing_chains ORDER BY ts DESC LIMIT ?", (limit,)
296
+ ).fetchall()
297
+
298
+ def routing_chain_by_id(self, chain_id: int) -> sqlite3.Row | None:
299
+ return self.conn.execute(
300
+ "SELECT * FROM routing_chains WHERE id=?", (chain_id,)
301
+ ).fetchone()
302
+
303
+ # usage reads
304
+ def usage_summary(self, since: float | None = None) -> list[sqlite3.Row]:
305
+ sql = (
306
+ "SELECT provider, model, tier, COUNT(*) requests, "
307
+ "SUM(prompt_tokens) prompt_tokens, SUM(completion_tokens) completion_tokens, "
308
+ "SUM(total_tokens) total_tokens, SUM(cost_usd) cost_usd, "
309
+ "SUM(CASE WHEN status_code>=400 OR error IS NOT NULL THEN 1 ELSE 0 END) errors, "
310
+ "AVG(latency_ms) avg_latency FROM usage"
311
+ )
312
+ params: list = []
313
+ if since is not None:
314
+ sql += " WHERE ts>=?"
315
+ params.append(since)
316
+ sql += " GROUP BY provider, model ORDER BY total_tokens DESC"
317
+ return self.conn.execute(sql, params).fetchall()
318
+
319
+ def usage_totals(self, since: float | None = None) -> sqlite3.Row:
320
+ sql = (
321
+ "SELECT COUNT(*) requests, SUM(total_tokens) total_tokens, "
322
+ "SUM(prompt_tokens) prompt_tokens, SUM(completion_tokens) completion_tokens, "
323
+ "SUM(cost_usd) cost_usd, "
324
+ "SUM(CASE WHEN status_code>=400 OR error IS NOT NULL THEN 1 ELSE 0 END) errors "
325
+ "FROM usage"
326
+ )
327
+ params: list = []
328
+ if since is not None:
329
+ sql += " WHERE ts>=?"
330
+ params.append(since)
331
+ return self.conn.execute(sql, params).fetchone()
332
+
333
+ def recent_usage(self, limit: int = 25) -> list[sqlite3.Row]:
334
+ return self.conn.execute(
335
+ "SELECT * FROM usage ORDER BY ts DESC LIMIT ?", (limit,)
336
+ ).fetchall()
337
+
338
+ # deferred queue
339
+ def create_deferred(
340
+ self,
341
+ *,
342
+ id: str,
343
+ path: str,
344
+ body: str,
345
+ model: str | None,
346
+ provider_hint: str | None,
347
+ floor: int | None,
348
+ eta: float | None,
349
+ ) -> None:
350
+ self.conn.execute(
351
+ "INSERT INTO deferred(id, created_at, status, path, body, model, "
352
+ "provider_hint, floor, eta) VALUES(?,?,?,?,?,?,?,?,?)",
353
+ (id, time.time(), "queued", path, body, model, provider_hint, floor, eta),
354
+ )
355
+ self.conn.commit()
356
+
357
+ def get_deferred(self, id: str) -> sqlite3.Row | None:
358
+ return self.conn.execute("SELECT * FROM deferred WHERE id=?", (id,)).fetchone()
359
+
360
+ def queued_deferred(self, limit: int = 50) -> list[sqlite3.Row]:
361
+ return self.conn.execute(
362
+ "SELECT * FROM deferred WHERE status='queued' ORDER BY created_at LIMIT ?",
363
+ (limit,),
364
+ ).fetchall()
365
+
366
+ def count_deferred(self, status: str | None = None) -> int:
367
+ if status:
368
+ row = self.conn.execute(
369
+ "SELECT COUNT(*) c FROM deferred WHERE status=?", (status,)
370
+ ).fetchone()
371
+ else:
372
+ row = self.conn.execute("SELECT COUNT(*) c FROM deferred").fetchone()
373
+ return row["c"]
374
+
375
+ def recent_deferred(self, limit: int = 50) -> list[sqlite3.Row]:
376
+ return self.conn.execute(
377
+ "SELECT * FROM deferred ORDER BY created_at DESC LIMIT ?", (limit,)
378
+ ).fetchall()
379
+
380
+ def set_deferred_running(self, id: str) -> None:
381
+ self.conn.execute(
382
+ "UPDATE deferred SET status='running', attempts=attempts+1 WHERE id=?", (id,)
383
+ )
384
+ self.conn.commit()
385
+
386
+ def requeue_deferred(self, id: str, eta: float | None) -> None:
387
+ self.conn.execute(
388
+ "UPDATE deferred SET status='queued', eta=? WHERE id=?", (eta, id)
389
+ )
390
+ self.conn.commit()
391
+
392
+ def complete_deferred(self, id: str, status_code: int, response: bytes) -> None:
393
+ self.conn.execute(
394
+ "UPDATE deferred SET status='done', status_code=?, response=?, completed_at=? "
395
+ "WHERE id=?",
396
+ (status_code, response, time.time(), id),
397
+ )
398
+ self.conn.commit()
399
+
400
+ def fail_deferred(self, id: str, status_code: int, error: str) -> None:
401
+ self.conn.execute(
402
+ "UPDATE deferred SET status='error', status_code=?, error=?, completed_at=? "
403
+ "WHERE id=?",
404
+ (status_code, error[:500], time.time(), id),
405
+ )
406
+ self.conn.commit()
407
+
408
+ def reset_running_deferred(self) -> int:
409
+ """On startup, return any 'running' jobs (from a crash) to the queue."""
410
+ cur = self.conn.execute(
411
+ "UPDATE deferred SET status='queued' WHERE status='running'"
412
+ )
413
+ self.conn.commit()
414
+ return cur.rowcount
415
+
416
+ def purge_deferred(self, ttl: float, max_queue_age: float) -> int:
417
+ """Delete finished jobs older than `ttl` and queued jobs stuck past `max_queue_age`."""
418
+ now = time.time()
419
+ cur = self.conn.execute(
420
+ "DELETE FROM deferred WHERE "
421
+ "(status IN ('done','error') AND completed_at IS NOT NULL AND completed_at < ?) "
422
+ "OR (status='queued' AND created_at < ?)",
423
+ (now - ttl, now - max_queue_age),
424
+ )
425
+ self.conn.commit()
426
+ return cur.rowcount