llm-cost-guard 0.1.2__py3-none-any.whl → 0.2.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.
- llm_cost_guard/__init__.py +16 -1
- llm_cost_guard/audit.py +480 -0
- llm_cost_guard/backends/__init__.py +1 -1
- llm_cost_guard/backends/redis_backend.py +557 -0
- llm_cost_guard/tracker.py +114 -3
- {llm_cost_guard-0.1.2.dist-info → llm_cost_guard-0.2.0.dist-info}/METADATA +76 -1
- {llm_cost_guard-0.1.2.dist-info → llm_cost_guard-0.2.0.dist-info}/RECORD +10 -8
- {llm_cost_guard-0.1.2.dist-info → llm_cost_guard-0.2.0.dist-info}/WHEEL +0 -0
- {llm_cost_guard-0.1.2.dist-info → llm_cost_guard-0.2.0.dist-info}/entry_points.txt +0 -0
- {llm_cost_guard-0.1.2.dist-info → llm_cost_guard-0.2.0.dist-info}/licenses/LICENSE +0 -0
llm_cost_guard/__init__.py
CHANGED
|
@@ -15,8 +15,16 @@ from llm_cost_guard.exceptions import (
|
|
|
15
15
|
TrackingUnavailableError,
|
|
16
16
|
RateLimitExceededError,
|
|
17
17
|
)
|
|
18
|
+
from llm_cost_guard.audit import (
|
|
19
|
+
AuditLogger,
|
|
20
|
+
AuditBackend,
|
|
21
|
+
AuditEvent,
|
|
22
|
+
AuditEventType,
|
|
23
|
+
LoggingAuditBackend,
|
|
24
|
+
FileAuditBackend,
|
|
25
|
+
)
|
|
18
26
|
|
|
19
|
-
__version__ = "0.
|
|
27
|
+
__version__ = "0.2.0"
|
|
20
28
|
|
|
21
29
|
__all__ = [
|
|
22
30
|
# Core
|
|
@@ -29,6 +37,13 @@ __all__ = [
|
|
|
29
37
|
"CostRecord",
|
|
30
38
|
"CostReport",
|
|
31
39
|
"HealthStatus",
|
|
40
|
+
# Audit
|
|
41
|
+
"AuditLogger",
|
|
42
|
+
"AuditBackend",
|
|
43
|
+
"AuditEvent",
|
|
44
|
+
"AuditEventType",
|
|
45
|
+
"LoggingAuditBackend",
|
|
46
|
+
"FileAuditBackend",
|
|
32
47
|
# Exceptions
|
|
33
48
|
"LLMCostGuardError",
|
|
34
49
|
"BudgetExceededError",
|
llm_cost_guard/audit.py
ADDED
|
@@ -0,0 +1,480 @@
|
|
|
1
|
+
"""
|
|
2
|
+
Audit logging for LLM Cost Guard.
|
|
3
|
+
|
|
4
|
+
Provides compliance-ready audit trails for:
|
|
5
|
+
- Budget changes (created, modified, deleted)
|
|
6
|
+
- Budget exceeded events
|
|
7
|
+
- Configuration changes
|
|
8
|
+
- Rate limit events
|
|
9
|
+
"""
|
|
10
|
+
|
|
11
|
+
import json
|
|
12
|
+
import logging
|
|
13
|
+
from abc import ABC, abstractmethod
|
|
14
|
+
from dataclasses import dataclass, field, asdict
|
|
15
|
+
from datetime import datetime
|
|
16
|
+
from enum import Enum
|
|
17
|
+
from typing import Any, Dict, List, Optional, Callable
|
|
18
|
+
|
|
19
|
+
logger = logging.getLogger(__name__)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
class AuditEventType(str, Enum):
|
|
23
|
+
"""Types of audit events."""
|
|
24
|
+
|
|
25
|
+
# Budget events
|
|
26
|
+
BUDGET_CREATED = "budget.created"
|
|
27
|
+
BUDGET_MODIFIED = "budget.modified"
|
|
28
|
+
BUDGET_DELETED = "budget.deleted"
|
|
29
|
+
BUDGET_WARNING = "budget.warning"
|
|
30
|
+
BUDGET_EXCEEDED = "budget.exceeded"
|
|
31
|
+
BUDGET_RESET = "budget.reset"
|
|
32
|
+
|
|
33
|
+
# Rate limit events
|
|
34
|
+
RATE_LIMIT_EXCEEDED = "rate_limit.exceeded"
|
|
35
|
+
|
|
36
|
+
# Configuration events
|
|
37
|
+
CONFIG_CHANGED = "config.changed"
|
|
38
|
+
BACKEND_CHANGED = "backend.changed"
|
|
39
|
+
PRICING_UPDATED = "pricing.updated"
|
|
40
|
+
|
|
41
|
+
# Tracking events
|
|
42
|
+
TRACKING_FAILURE = "tracking.failure"
|
|
43
|
+
FALLBACK_ACTIVATED = "fallback.activated"
|
|
44
|
+
|
|
45
|
+
# Cost events
|
|
46
|
+
COST_RECORDED = "cost.recorded"
|
|
47
|
+
COST_ANOMALY = "cost.anomaly"
|
|
48
|
+
|
|
49
|
+
|
|
50
|
+
@dataclass
|
|
51
|
+
class AuditEvent:
|
|
52
|
+
"""Represents an audit event."""
|
|
53
|
+
|
|
54
|
+
event_type: AuditEventType
|
|
55
|
+
timestamp: datetime = field(default_factory=datetime.now)
|
|
56
|
+
actor: Optional[str] = None # Who initiated the action (user/system)
|
|
57
|
+
resource: Optional[str] = None # What was affected (budget name, etc.)
|
|
58
|
+
details: Dict[str, Any] = field(default_factory=dict)
|
|
59
|
+
metadata: Dict[str, Any] = field(default_factory=dict)
|
|
60
|
+
|
|
61
|
+
def to_dict(self) -> Dict[str, Any]:
|
|
62
|
+
"""Convert to dictionary for serialization."""
|
|
63
|
+
return {
|
|
64
|
+
"event_type": self.event_type.value,
|
|
65
|
+
"timestamp": self.timestamp.isoformat(),
|
|
66
|
+
"actor": self.actor,
|
|
67
|
+
"resource": self.resource,
|
|
68
|
+
"details": self.details,
|
|
69
|
+
"metadata": self.metadata,
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
def to_json(self) -> str:
|
|
73
|
+
"""Convert to JSON string."""
|
|
74
|
+
return json.dumps(self.to_dict())
|
|
75
|
+
|
|
76
|
+
|
|
77
|
+
class AuditBackend(ABC):
|
|
78
|
+
"""Abstract base class for audit backends."""
|
|
79
|
+
|
|
80
|
+
@abstractmethod
|
|
81
|
+
def log(self, event: AuditEvent) -> None:
|
|
82
|
+
"""Log an audit event."""
|
|
83
|
+
pass
|
|
84
|
+
|
|
85
|
+
@abstractmethod
|
|
86
|
+
def query(
|
|
87
|
+
self,
|
|
88
|
+
event_type: Optional[AuditEventType] = None,
|
|
89
|
+
start_date: Optional[datetime] = None,
|
|
90
|
+
end_date: Optional[datetime] = None,
|
|
91
|
+
resource: Optional[str] = None,
|
|
92
|
+
limit: int = 100,
|
|
93
|
+
) -> List[AuditEvent]:
|
|
94
|
+
"""Query audit events."""
|
|
95
|
+
pass
|
|
96
|
+
|
|
97
|
+
|
|
98
|
+
class LoggingAuditBackend(AuditBackend):
|
|
99
|
+
"""
|
|
100
|
+
Audit backend that logs to Python logging.
|
|
101
|
+
|
|
102
|
+
Suitable for development and when audit logs should go to
|
|
103
|
+
existing log aggregation systems (CloudWatch, DataDog, etc.).
|
|
104
|
+
"""
|
|
105
|
+
|
|
106
|
+
def __init__(
|
|
107
|
+
self,
|
|
108
|
+
logger_name: str = "llm_cost_guard.audit",
|
|
109
|
+
log_level: int = logging.INFO,
|
|
110
|
+
):
|
|
111
|
+
self._logger = logging.getLogger(logger_name)
|
|
112
|
+
self._log_level = log_level
|
|
113
|
+
self._events: List[AuditEvent] = [] # In-memory for query support
|
|
114
|
+
self._max_events = 10000 # Limit in-memory storage
|
|
115
|
+
|
|
116
|
+
def log(self, event: AuditEvent) -> None:
|
|
117
|
+
"""Log an audit event."""
|
|
118
|
+
# Log to Python logger
|
|
119
|
+
self._logger.log(
|
|
120
|
+
self._log_level,
|
|
121
|
+
f"AUDIT: {event.event_type.value} | resource={event.resource} | {event.details}"
|
|
122
|
+
)
|
|
123
|
+
|
|
124
|
+
# Store in memory for queries
|
|
125
|
+
self._events.append(event)
|
|
126
|
+
|
|
127
|
+
# Trim if too many events
|
|
128
|
+
if len(self._events) > self._max_events:
|
|
129
|
+
self._events = self._events[-self._max_events:]
|
|
130
|
+
|
|
131
|
+
def query(
|
|
132
|
+
self,
|
|
133
|
+
event_type: Optional[AuditEventType] = None,
|
|
134
|
+
start_date: Optional[datetime] = None,
|
|
135
|
+
end_date: Optional[datetime] = None,
|
|
136
|
+
resource: Optional[str] = None,
|
|
137
|
+
limit: int = 100,
|
|
138
|
+
) -> List[AuditEvent]:
|
|
139
|
+
"""Query audit events."""
|
|
140
|
+
results = []
|
|
141
|
+
|
|
142
|
+
for event in reversed(self._events): # Most recent first
|
|
143
|
+
if event_type and event.event_type != event_type:
|
|
144
|
+
continue
|
|
145
|
+
if start_date and event.timestamp < start_date:
|
|
146
|
+
continue
|
|
147
|
+
if end_date and event.timestamp > end_date:
|
|
148
|
+
continue
|
|
149
|
+
if resource and event.resource != resource:
|
|
150
|
+
continue
|
|
151
|
+
|
|
152
|
+
results.append(event)
|
|
153
|
+
|
|
154
|
+
if len(results) >= limit:
|
|
155
|
+
break
|
|
156
|
+
|
|
157
|
+
return results
|
|
158
|
+
|
|
159
|
+
|
|
160
|
+
class FileAuditBackend(AuditBackend):
|
|
161
|
+
"""
|
|
162
|
+
Audit backend that writes to a file (JSON Lines format).
|
|
163
|
+
|
|
164
|
+
Suitable for compliance requirements where audit logs need
|
|
165
|
+
to be stored in a specific location.
|
|
166
|
+
"""
|
|
167
|
+
|
|
168
|
+
def __init__(self, file_path: str):
|
|
169
|
+
self._file_path = file_path
|
|
170
|
+
|
|
171
|
+
def log(self, event: AuditEvent) -> None:
|
|
172
|
+
"""Log an audit event."""
|
|
173
|
+
with open(self._file_path, "a") as f:
|
|
174
|
+
f.write(event.to_json() + "\n")
|
|
175
|
+
|
|
176
|
+
def query(
|
|
177
|
+
self,
|
|
178
|
+
event_type: Optional[AuditEventType] = None,
|
|
179
|
+
start_date: Optional[datetime] = None,
|
|
180
|
+
end_date: Optional[datetime] = None,
|
|
181
|
+
resource: Optional[str] = None,
|
|
182
|
+
limit: int = 100,
|
|
183
|
+
) -> List[AuditEvent]:
|
|
184
|
+
"""Query audit events from file."""
|
|
185
|
+
results = []
|
|
186
|
+
|
|
187
|
+
try:
|
|
188
|
+
with open(self._file_path, "r") as f:
|
|
189
|
+
lines = f.readlines()
|
|
190
|
+
except FileNotFoundError:
|
|
191
|
+
return []
|
|
192
|
+
|
|
193
|
+
for line in reversed(lines): # Most recent first
|
|
194
|
+
if not line.strip():
|
|
195
|
+
continue
|
|
196
|
+
|
|
197
|
+
data = json.loads(line)
|
|
198
|
+
event = AuditEvent(
|
|
199
|
+
event_type=AuditEventType(data["event_type"]),
|
|
200
|
+
timestamp=datetime.fromisoformat(data["timestamp"]),
|
|
201
|
+
actor=data.get("actor"),
|
|
202
|
+
resource=data.get("resource"),
|
|
203
|
+
details=data.get("details", {}),
|
|
204
|
+
metadata=data.get("metadata", {}),
|
|
205
|
+
)
|
|
206
|
+
|
|
207
|
+
if event_type and event.event_type != event_type:
|
|
208
|
+
continue
|
|
209
|
+
if start_date and event.timestamp < start_date:
|
|
210
|
+
continue
|
|
211
|
+
if end_date and event.timestamp > end_date:
|
|
212
|
+
continue
|
|
213
|
+
if resource and event.resource != resource:
|
|
214
|
+
continue
|
|
215
|
+
|
|
216
|
+
results.append(event)
|
|
217
|
+
|
|
218
|
+
if len(results) >= limit:
|
|
219
|
+
break
|
|
220
|
+
|
|
221
|
+
return results
|
|
222
|
+
|
|
223
|
+
|
|
224
|
+
class CompositeAuditBackend(AuditBackend):
|
|
225
|
+
"""
|
|
226
|
+
Audit backend that writes to multiple backends.
|
|
227
|
+
|
|
228
|
+
Useful for sending audit logs to both local storage and remote systems.
|
|
229
|
+
"""
|
|
230
|
+
|
|
231
|
+
def __init__(self, backends: List[AuditBackend]):
|
|
232
|
+
self._backends = backends
|
|
233
|
+
|
|
234
|
+
def log(self, event: AuditEvent) -> None:
|
|
235
|
+
"""Log an audit event to all backends."""
|
|
236
|
+
for backend in self._backends:
|
|
237
|
+
try:
|
|
238
|
+
backend.log(event)
|
|
239
|
+
except Exception as e:
|
|
240
|
+
logger.error(f"Audit backend failed: {e}")
|
|
241
|
+
|
|
242
|
+
def query(
|
|
243
|
+
self,
|
|
244
|
+
event_type: Optional[AuditEventType] = None,
|
|
245
|
+
start_date: Optional[datetime] = None,
|
|
246
|
+
end_date: Optional[datetime] = None,
|
|
247
|
+
resource: Optional[str] = None,
|
|
248
|
+
limit: int = 100,
|
|
249
|
+
) -> List[AuditEvent]:
|
|
250
|
+
"""Query from first backend that succeeds."""
|
|
251
|
+
for backend in self._backends:
|
|
252
|
+
try:
|
|
253
|
+
return backend.query(event_type, start_date, end_date, resource, limit)
|
|
254
|
+
except Exception as e:
|
|
255
|
+
logger.warning(f"Audit query failed on backend: {e}")
|
|
256
|
+
return []
|
|
257
|
+
|
|
258
|
+
|
|
259
|
+
class AuditLogger:
|
|
260
|
+
"""
|
|
261
|
+
Main audit logger for LLM Cost Guard.
|
|
262
|
+
|
|
263
|
+
Usage:
|
|
264
|
+
audit = AuditLogger(backend=FileAuditBackend("audit.log"))
|
|
265
|
+
|
|
266
|
+
audit.log_budget_created(budget)
|
|
267
|
+
audit.log_budget_exceeded(budget, current_spending)
|
|
268
|
+
"""
|
|
269
|
+
|
|
270
|
+
def __init__(
|
|
271
|
+
self,
|
|
272
|
+
backend: Optional[AuditBackend] = None,
|
|
273
|
+
enabled: bool = True,
|
|
274
|
+
actor: Optional[str] = None,
|
|
275
|
+
):
|
|
276
|
+
self._backend = backend or LoggingAuditBackend()
|
|
277
|
+
self._enabled = enabled
|
|
278
|
+
self._default_actor = actor or "system"
|
|
279
|
+
self._event_callbacks: Dict[AuditEventType, List[Callable[[AuditEvent], None]]] = {}
|
|
280
|
+
|
|
281
|
+
def _log(self, event: AuditEvent) -> None:
|
|
282
|
+
"""Internal logging method."""
|
|
283
|
+
if not self._enabled:
|
|
284
|
+
return
|
|
285
|
+
|
|
286
|
+
if event.actor is None:
|
|
287
|
+
event.actor = self._default_actor
|
|
288
|
+
|
|
289
|
+
self._backend.log(event)
|
|
290
|
+
|
|
291
|
+
# Trigger callbacks
|
|
292
|
+
callbacks = self._event_callbacks.get(event.event_type, [])
|
|
293
|
+
for callback in callbacks:
|
|
294
|
+
try:
|
|
295
|
+
callback(event)
|
|
296
|
+
except Exception as e:
|
|
297
|
+
logger.error(f"Audit callback failed: {e}")
|
|
298
|
+
|
|
299
|
+
def on_event(
|
|
300
|
+
self,
|
|
301
|
+
event_type: AuditEventType,
|
|
302
|
+
callback: Callable[[AuditEvent], None],
|
|
303
|
+
) -> None:
|
|
304
|
+
"""Register a callback for an event type."""
|
|
305
|
+
if event_type not in self._event_callbacks:
|
|
306
|
+
self._event_callbacks[event_type] = []
|
|
307
|
+
self._event_callbacks[event_type].append(callback)
|
|
308
|
+
|
|
309
|
+
# Budget events
|
|
310
|
+
def log_budget_created(
|
|
311
|
+
self,
|
|
312
|
+
budget_name: str,
|
|
313
|
+
limit: float,
|
|
314
|
+
period: str,
|
|
315
|
+
action: str,
|
|
316
|
+
actor: Optional[str] = None,
|
|
317
|
+
) -> None:
|
|
318
|
+
"""Log budget creation."""
|
|
319
|
+
self._log(AuditEvent(
|
|
320
|
+
event_type=AuditEventType.BUDGET_CREATED,
|
|
321
|
+
actor=actor,
|
|
322
|
+
resource=budget_name,
|
|
323
|
+
details={
|
|
324
|
+
"limit": limit,
|
|
325
|
+
"period": period,
|
|
326
|
+
"action": action,
|
|
327
|
+
},
|
|
328
|
+
))
|
|
329
|
+
|
|
330
|
+
def log_budget_modified(
|
|
331
|
+
self,
|
|
332
|
+
budget_name: str,
|
|
333
|
+
old_values: Dict[str, Any],
|
|
334
|
+
new_values: Dict[str, Any],
|
|
335
|
+
actor: Optional[str] = None,
|
|
336
|
+
) -> None:
|
|
337
|
+
"""Log budget modification."""
|
|
338
|
+
self._log(AuditEvent(
|
|
339
|
+
event_type=AuditEventType.BUDGET_MODIFIED,
|
|
340
|
+
actor=actor,
|
|
341
|
+
resource=budget_name,
|
|
342
|
+
details={
|
|
343
|
+
"old_values": old_values,
|
|
344
|
+
"new_values": new_values,
|
|
345
|
+
},
|
|
346
|
+
))
|
|
347
|
+
|
|
348
|
+
def log_budget_deleted(
|
|
349
|
+
self,
|
|
350
|
+
budget_name: str,
|
|
351
|
+
actor: Optional[str] = None,
|
|
352
|
+
) -> None:
|
|
353
|
+
"""Log budget deletion."""
|
|
354
|
+
self._log(AuditEvent(
|
|
355
|
+
event_type=AuditEventType.BUDGET_DELETED,
|
|
356
|
+
actor=actor,
|
|
357
|
+
resource=budget_name,
|
|
358
|
+
))
|
|
359
|
+
|
|
360
|
+
def log_budget_warning(
|
|
361
|
+
self,
|
|
362
|
+
budget_name: str,
|
|
363
|
+
current_spending: float,
|
|
364
|
+
limit: float,
|
|
365
|
+
utilization: float,
|
|
366
|
+
) -> None:
|
|
367
|
+
"""Log budget warning."""
|
|
368
|
+
self._log(AuditEvent(
|
|
369
|
+
event_type=AuditEventType.BUDGET_WARNING,
|
|
370
|
+
resource=budget_name,
|
|
371
|
+
details={
|
|
372
|
+
"current_spending": current_spending,
|
|
373
|
+
"limit": limit,
|
|
374
|
+
"utilization_percent": utilization * 100,
|
|
375
|
+
},
|
|
376
|
+
))
|
|
377
|
+
|
|
378
|
+
def log_budget_exceeded(
|
|
379
|
+
self,
|
|
380
|
+
budget_name: str,
|
|
381
|
+
current_spending: float,
|
|
382
|
+
limit: float,
|
|
383
|
+
action_taken: str,
|
|
384
|
+
) -> None:
|
|
385
|
+
"""Log budget exceeded event."""
|
|
386
|
+
self._log(AuditEvent(
|
|
387
|
+
event_type=AuditEventType.BUDGET_EXCEEDED,
|
|
388
|
+
resource=budget_name,
|
|
389
|
+
details={
|
|
390
|
+
"current_spending": current_spending,
|
|
391
|
+
"limit": limit,
|
|
392
|
+
"action_taken": action_taken,
|
|
393
|
+
},
|
|
394
|
+
))
|
|
395
|
+
|
|
396
|
+
def log_budget_reset(
|
|
397
|
+
self,
|
|
398
|
+
budget_name: str,
|
|
399
|
+
reason: str = "period_end",
|
|
400
|
+
actor: Optional[str] = None,
|
|
401
|
+
) -> None:
|
|
402
|
+
"""Log budget reset."""
|
|
403
|
+
self._log(AuditEvent(
|
|
404
|
+
event_type=AuditEventType.BUDGET_RESET,
|
|
405
|
+
actor=actor,
|
|
406
|
+
resource=budget_name,
|
|
407
|
+
details={"reason": reason},
|
|
408
|
+
))
|
|
409
|
+
|
|
410
|
+
# Rate limit events
|
|
411
|
+
def log_rate_limit_exceeded(
|
|
412
|
+
self,
|
|
413
|
+
limit_name: str,
|
|
414
|
+
current: int,
|
|
415
|
+
limit: int,
|
|
416
|
+
retry_after: float,
|
|
417
|
+
) -> None:
|
|
418
|
+
"""Log rate limit exceeded event."""
|
|
419
|
+
self._log(AuditEvent(
|
|
420
|
+
event_type=AuditEventType.RATE_LIMIT_EXCEEDED,
|
|
421
|
+
resource=limit_name,
|
|
422
|
+
details={
|
|
423
|
+
"current": current,
|
|
424
|
+
"limit": limit,
|
|
425
|
+
"retry_after_seconds": retry_after,
|
|
426
|
+
},
|
|
427
|
+
))
|
|
428
|
+
|
|
429
|
+
# Tracking events
|
|
430
|
+
def log_tracking_failure(
|
|
431
|
+
self,
|
|
432
|
+
error: str,
|
|
433
|
+
backend: str,
|
|
434
|
+
action_taken: str,
|
|
435
|
+
) -> None:
|
|
436
|
+
"""Log tracking failure."""
|
|
437
|
+
self._log(AuditEvent(
|
|
438
|
+
event_type=AuditEventType.TRACKING_FAILURE,
|
|
439
|
+
resource=backend,
|
|
440
|
+
details={
|
|
441
|
+
"error": error,
|
|
442
|
+
"action_taken": action_taken,
|
|
443
|
+
},
|
|
444
|
+
))
|
|
445
|
+
|
|
446
|
+
def log_fallback_activated(
|
|
447
|
+
self,
|
|
448
|
+
original_backend: str,
|
|
449
|
+
fallback_backend: str,
|
|
450
|
+
reason: str,
|
|
451
|
+
) -> None:
|
|
452
|
+
"""Log fallback activation."""
|
|
453
|
+
self._log(AuditEvent(
|
|
454
|
+
event_type=AuditEventType.FALLBACK_ACTIVATED,
|
|
455
|
+
details={
|
|
456
|
+
"original_backend": original_backend,
|
|
457
|
+
"fallback_backend": fallback_backend,
|
|
458
|
+
"reason": reason,
|
|
459
|
+
},
|
|
460
|
+
))
|
|
461
|
+
|
|
462
|
+
# Query methods
|
|
463
|
+
def query(
|
|
464
|
+
self,
|
|
465
|
+
event_type: Optional[AuditEventType] = None,
|
|
466
|
+
start_date: Optional[datetime] = None,
|
|
467
|
+
end_date: Optional[datetime] = None,
|
|
468
|
+
resource: Optional[str] = None,
|
|
469
|
+
limit: int = 100,
|
|
470
|
+
) -> List[AuditEvent]:
|
|
471
|
+
"""Query audit events."""
|
|
472
|
+
return self._backend.query(event_type, start_date, end_date, resource, limit)
|
|
473
|
+
|
|
474
|
+
def get_budget_history(
|
|
475
|
+
self,
|
|
476
|
+
budget_name: str,
|
|
477
|
+
limit: int = 100,
|
|
478
|
+
) -> List[AuditEvent]:
|
|
479
|
+
"""Get audit history for a specific budget."""
|
|
480
|
+
return self._backend.query(resource=budget_name, limit=limit)
|
|
@@ -40,7 +40,7 @@ def get_backend(backend_url: str, **kwargs) -> Backend:
|
|
|
40
40
|
return PostgresBackend(backend_url, **kwargs)
|
|
41
41
|
|
|
42
42
|
if backend_url.startswith("redis://"):
|
|
43
|
-
from llm_cost_guard.backends.
|
|
43
|
+
from llm_cost_guard.backends.redis_backend import RedisBackend
|
|
44
44
|
|
|
45
45
|
return RedisBackend(backend_url, **kwargs)
|
|
46
46
|
|