feat: add privacy router and zero-cost safeguards
This commit is contained in:
@@ -0,0 +1,114 @@
|
||||
"""Metadata-only provider audit and local usage limits."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import sqlite3
|
||||
from contextlib import closing
|
||||
from dataclasses import dataclass
|
||||
from datetime import UTC, date, datetime
|
||||
from pathlib import Path
|
||||
|
||||
|
||||
class UsageStoreError(RuntimeError):
|
||||
"""Provider metadata could not be stored or read."""
|
||||
|
||||
|
||||
@dataclass(frozen=True, slots=True)
|
||||
class ProviderEvent:
|
||||
provider: str
|
||||
model: str
|
||||
success: bool
|
||||
error_category: str | None
|
||||
fallback: bool
|
||||
privacy_policy: str
|
||||
input_tokens: int | None = None
|
||||
output_tokens: int | None = None
|
||||
|
||||
|
||||
class SQLiteUsageStore:
|
||||
def __init__(self, database_path: Path) -> None:
|
||||
self.database_path = database_path
|
||||
try:
|
||||
self.database_path.parent.mkdir(parents=True, exist_ok=True)
|
||||
with closing(self._connect()) as connection:
|
||||
connection.execute(
|
||||
"""
|
||||
CREATE TABLE IF NOT EXISTS provider_events (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
occurred_at TEXT NOT NULL,
|
||||
provider TEXT NOT NULL,
|
||||
model TEXT NOT NULL,
|
||||
success INTEGER NOT NULL,
|
||||
error_category TEXT,
|
||||
fallback INTEGER NOT NULL,
|
||||
privacy_policy TEXT NOT NULL,
|
||||
input_tokens INTEGER,
|
||||
output_tokens INTEGER
|
||||
)
|
||||
"""
|
||||
)
|
||||
connection.execute(
|
||||
"""
|
||||
CREATE INDEX IF NOT EXISTS idx_provider_events_time_provider
|
||||
ON provider_events(occurred_at, provider)
|
||||
"""
|
||||
)
|
||||
connection.commit()
|
||||
except (OSError, sqlite3.Error) as exc:
|
||||
raise UsageStoreError(
|
||||
f"Die Nutzungsmetadatenbank kann nicht geöffnet werden: {database_path}"
|
||||
) from exc
|
||||
|
||||
def _connect(self) -> sqlite3.Connection:
|
||||
return sqlite3.connect(self.database_path, timeout=10)
|
||||
|
||||
def record(self, event: ProviderEvent) -> None:
|
||||
try:
|
||||
with closing(self._connect()) as connection:
|
||||
connection.execute(
|
||||
"""
|
||||
INSERT INTO provider_events (
|
||||
occurred_at, provider, model, success, error_category,
|
||||
fallback, privacy_policy, input_tokens, output_tokens
|
||||
) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
""",
|
||||
(
|
||||
datetime.now(UTC).isoformat(timespec="seconds"),
|
||||
event.provider,
|
||||
event.model,
|
||||
int(event.success),
|
||||
event.error_category,
|
||||
int(event.fallback),
|
||||
event.privacy_policy,
|
||||
event.input_tokens,
|
||||
event.output_tokens,
|
||||
),
|
||||
)
|
||||
connection.commit()
|
||||
except sqlite3.Error as exc:
|
||||
raise UsageStoreError("Nutzungsmetadaten konnten nicht gespeichert werden.") from exc
|
||||
|
||||
def cloud_requests_on(self, day: date | None = None) -> int:
|
||||
return self._count(day or datetime.now(UTC).date(), "provider = 'gemini'")
|
||||
|
||||
def local_fallbacks_on(self, day: date | None = None) -> int:
|
||||
return self._count(
|
||||
day or datetime.now(UTC).date(),
|
||||
"provider = 'ollama' AND fallback = 1",
|
||||
)
|
||||
|
||||
def _count(self, day: date, condition: str) -> int:
|
||||
start = f"{day.isoformat()}T00:00:00+00:00"
|
||||
end = f"{day.isoformat()}T23:59:59+00:00"
|
||||
try:
|
||||
with closing(self._connect()) as connection:
|
||||
row = connection.execute(
|
||||
f"""
|
||||
SELECT COUNT(*) FROM provider_events
|
||||
WHERE occurred_at BETWEEN ? AND ? AND {condition}
|
||||
""",
|
||||
(start, end),
|
||||
).fetchone()
|
||||
except sqlite3.Error as exc:
|
||||
raise UsageStoreError("Nutzungsmetadaten konnten nicht gelesen werden.") from exc
|
||||
return int(row[0])
|
||||
Reference in New Issue
Block a user