driftlock 0.2.0__tar.gz
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.
- driftlock-0.2.0/LICENSE +21 -0
- driftlock-0.2.0/PKG-INFO +537 -0
- driftlock-0.2.0/README.md +506 -0
- driftlock-0.2.0/driftlock/__init__.py +72 -0
- driftlock-0.2.0/driftlock/alerts.py +170 -0
- driftlock-0.2.0/driftlock/anthropic_client.py +537 -0
- driftlock-0.2.0/driftlock/cache.py +162 -0
- driftlock-0.2.0/driftlock/cli.py +363 -0
- driftlock-0.2.0/driftlock/client.py +610 -0
- driftlock-0.2.0/driftlock/config.py +30 -0
- driftlock-0.2.0/driftlock/context.py +44 -0
- driftlock-0.2.0/driftlock/drift.py +97 -0
- driftlock-0.2.0/driftlock/logger.py +105 -0
- driftlock-0.2.0/driftlock/metrics.py +84 -0
- driftlock-0.2.0/driftlock/optimization.py +252 -0
- driftlock-0.2.0/driftlock/policy.py +341 -0
- driftlock-0.2.0/driftlock/pricing.py +91 -0
- driftlock-0.2.0/driftlock/providers/__init__.py +5 -0
- driftlock-0.2.0/driftlock/providers/anthropic_provider.py +29 -0
- driftlock-0.2.0/driftlock/providers/base.py +20 -0
- driftlock-0.2.0/driftlock/providers/openai_provider.py +26 -0
- driftlock-0.2.0/driftlock/storage.py +418 -0
- driftlock-0.2.0/driftlock/streaming.py +164 -0
- driftlock-0.2.0/driftlock/tokenizer.py +66 -0
- driftlock-0.2.0/driftlock.egg-info/PKG-INFO +537 -0
- driftlock-0.2.0/driftlock.egg-info/SOURCES.txt +45 -0
- driftlock-0.2.0/driftlock.egg-info/dependency_links.txt +1 -0
- driftlock-0.2.0/driftlock.egg-info/entry_points.txt +2 -0
- driftlock-0.2.0/driftlock.egg-info/requires.txt +15 -0
- driftlock-0.2.0/driftlock.egg-info/top_level.txt +1 -0
- driftlock-0.2.0/pyproject.toml +52 -0
- driftlock-0.2.0/setup.cfg +4 -0
- driftlock-0.2.0/tests/test_alerts.py +88 -0
- driftlock-0.2.0/tests/test_async.py +103 -0
- driftlock-0.2.0/tests/test_cache.py +372 -0
- driftlock-0.2.0/tests/test_client.py +217 -0
- driftlock-0.2.0/tests/test_demo.py +313 -0
- driftlock-0.2.0/tests/test_drift.py +109 -0
- driftlock-0.2.0/tests/test_forecast.py +72 -0
- driftlock-0.2.0/tests/test_optimization.py +332 -0
- driftlock-0.2.0/tests/test_per_user_budget.py +105 -0
- driftlock-0.2.0/tests/test_policy.py +129 -0
- driftlock-0.2.0/tests/test_pricing.py +47 -0
- driftlock-0.2.0/tests/test_providers.py +67 -0
- driftlock-0.2.0/tests/test_storage.py +58 -0
- driftlock-0.2.0/tests/test_streaming.py +108 -0
- driftlock-0.2.0/tests/test_velocity.py +117 -0
driftlock-0.2.0/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Driftlock
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
driftlock-0.2.0/PKG-INFO
ADDED
|
@@ -0,0 +1,537 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: driftlock
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: LLM cost governance and control layer — supports OpenAI, Anthropic, and more
|
|
5
|
+
License-Expression: MIT
|
|
6
|
+
Project-URL: Homepage, https://github.com/your-org/driftlock
|
|
7
|
+
Project-URL: Repository, https://github.com/your-org/driftlock
|
|
8
|
+
Project-URL: Bug Tracker, https://github.com/your-org/driftlock/issues
|
|
9
|
+
Keywords: openai,anthropic,llm,cost,policy,governance,middleware
|
|
10
|
+
Classifier: Development Status :: 3 - Alpha
|
|
11
|
+
Classifier: Intended Audience :: Developers
|
|
12
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
13
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
14
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
15
|
+
Requires-Python: >=3.11
|
|
16
|
+
Description-Content-Type: text/markdown
|
|
17
|
+
License-File: LICENSE
|
|
18
|
+
Requires-Dist: openai>=1.0.0
|
|
19
|
+
Provides-Extra: anthropic
|
|
20
|
+
Requires-Dist: anthropic>=0.30.0; extra == "anthropic"
|
|
21
|
+
Provides-Extra: fastapi
|
|
22
|
+
Requires-Dist: fastapi>=0.110.0; extra == "fastapi"
|
|
23
|
+
Requires-Dist: uvicorn[standard]>=0.29.0; extra == "fastapi"
|
|
24
|
+
Provides-Extra: dev
|
|
25
|
+
Requires-Dist: pytest>=8.0; extra == "dev"
|
|
26
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == "dev"
|
|
27
|
+
Requires-Dist: pytest-mock>=3.14; extra == "dev"
|
|
28
|
+
Requires-Dist: httpx>=0.27; extra == "dev"
|
|
29
|
+
Requires-Dist: ruff>=0.4; extra == "dev"
|
|
30
|
+
Dynamic: license-file
|
|
31
|
+
|
|
32
|
+
# Driftlock
|
|
33
|
+
|
|
34
|
+
**LLM cost governance and control layer for Python.**
|
|
35
|
+
|
|
36
|
+
Driftlock sits between your application and LLM providers (OpenAI, Anthropic). It enforces cost policies, detects runaway spending, tracks per-call telemetry, and prevents budget overruns — with a drop-in API wrapper and zero changes to existing call sites.
|
|
37
|
+
|
|
38
|
+
---
|
|
39
|
+
|
|
40
|
+
## Install
|
|
41
|
+
|
|
42
|
+
Not yet on PyPI. Install directly from source:
|
|
43
|
+
|
|
44
|
+
```bash
|
|
45
|
+
git clone https://github.com/your-org/driftlock
|
|
46
|
+
cd driftlock
|
|
47
|
+
pip install -e .
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
With Anthropic support:
|
|
51
|
+
|
|
52
|
+
```bash
|
|
53
|
+
pip install -e ".[anthropic]"
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
With FastAPI support:
|
|
57
|
+
|
|
58
|
+
```bash
|
|
59
|
+
pip install -e ".[fastapi]"
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
Requires Python ≥ 3.11.
|
|
63
|
+
|
|
64
|
+
---
|
|
65
|
+
|
|
66
|
+
## Try It Now (no API key needed)
|
|
67
|
+
|
|
68
|
+
```bash
|
|
69
|
+
git clone https://github.com/your-org/driftlock && cd driftlock
|
|
70
|
+
pip install -e .
|
|
71
|
+
python examples/demo.py
|
|
72
|
+
```
|
|
73
|
+
|
|
74
|
+
This runs a fully-mocked demo in-process — no API key, no network calls, no cost. It exercises every major feature: tracking, optimization, budget guardrails, cache, and context tags.
|
|
75
|
+
|
|
76
|
+
---
|
|
77
|
+
|
|
78
|
+
## 60-Second Quickstart (real API)
|
|
79
|
+
|
|
80
|
+
```bash
|
|
81
|
+
export OPENAI_API_KEY=sk-... # or ANTHROPIC_API_KEY=sk-ant-...
|
|
82
|
+
driftlock demo
|
|
83
|
+
```
|
|
84
|
+
|
|
85
|
+
Driftlock makes one cheap request (`gpt-4o-mini` or `claude-haiku-4-5`) under a default policy and prints a receipt:
|
|
86
|
+
|
|
87
|
+
```
|
|
88
|
+
Driftlock demo — provider=openai model=gpt-4o-mini
|
|
89
|
+
|
|
90
|
+
┌─ Receipt ──────────────────────────────────────────────┐
|
|
91
|
+
│ provider : openai │
|
|
92
|
+
│ model : gpt-4o-mini │
|
|
93
|
+
│ tokens : 23 (15 in / 8 out) │
|
|
94
|
+
│ cost : $0.000007 │
|
|
95
|
+
│ latency : 412 ms │
|
|
96
|
+
│ db : ./driftlock.sqlite │
|
|
97
|
+
└────────────────────────────────────────────────────────┘
|
|
98
|
+
|
|
99
|
+
Next steps:
|
|
100
|
+
driftlock stats # aggregate cost + token totals
|
|
101
|
+
driftlock recent # last 20 calls
|
|
102
|
+
driftlock forecast # projected monthly spend
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
---
|
|
106
|
+
|
|
107
|
+
## Basic Integration — OpenAI
|
|
108
|
+
|
|
109
|
+
```python
|
|
110
|
+
from driftlock import DriftlockClient
|
|
111
|
+
|
|
112
|
+
# Replace openai.OpenAI() with DriftlockClient().
|
|
113
|
+
# All other arguments are forwarded to the OpenAI client unchanged.
|
|
114
|
+
client = DriftlockClient(api_key="sk-...")
|
|
115
|
+
|
|
116
|
+
response = client.chat.completions.create(
|
|
117
|
+
model="gpt-4o-mini",
|
|
118
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
119
|
+
)
|
|
120
|
+
```
|
|
121
|
+
|
|
122
|
+
Every call is logged, costed, and saved to a local SQLite file.
|
|
123
|
+
|
|
124
|
+
```json
|
|
125
|
+
{
|
|
126
|
+
"level": "INFO",
|
|
127
|
+
"logger": "driftlock",
|
|
128
|
+
"message": "model=gpt-4o-mini | tokens=157 | latency=421ms | cost=$0.000033",
|
|
129
|
+
"metrics": {
|
|
130
|
+
"timestamp": "2025-03-01T12:00:00+00:00",
|
|
131
|
+
"model": "gpt-4o-mini",
|
|
132
|
+
"prompt_tokens": 120,
|
|
133
|
+
"completion_tokens": 37,
|
|
134
|
+
"total_tokens": 157,
|
|
135
|
+
"latency_ms": 421.3,
|
|
136
|
+
"estimated_cost_usd": 0.0000330
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
```
|
|
140
|
+
|
|
141
|
+
### Async
|
|
142
|
+
|
|
143
|
+
```python
|
|
144
|
+
response = await client.chat.completions.acreate(
|
|
145
|
+
model="gpt-4o-mini",
|
|
146
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
147
|
+
)
|
|
148
|
+
```
|
|
149
|
+
|
|
150
|
+
### Streaming
|
|
151
|
+
|
|
152
|
+
```python
|
|
153
|
+
for chunk in client.chat.completions.create(
|
|
154
|
+
model="gpt-4o-mini",
|
|
155
|
+
messages=[{"role": "user", "content": "Tell me a story."}],
|
|
156
|
+
stream=True,
|
|
157
|
+
):
|
|
158
|
+
print(chunk.choices[0].delta.content or "", end="", flush=True)
|
|
159
|
+
# Metrics are logged and saved when the stream closes.
|
|
160
|
+
```
|
|
161
|
+
|
|
162
|
+
---
|
|
163
|
+
|
|
164
|
+
## Basic Integration — Anthropic
|
|
165
|
+
|
|
166
|
+
Requires `pip install -e ".[anthropic]"`.
|
|
167
|
+
|
|
168
|
+
```python
|
|
169
|
+
from driftlock import AnthropicDriftlockClient
|
|
170
|
+
|
|
171
|
+
client = AnthropicDriftlockClient(api_key="sk-ant-...")
|
|
172
|
+
|
|
173
|
+
response = client.messages.create(
|
|
174
|
+
model="claude-3-5-sonnet-20241022",
|
|
175
|
+
max_tokens=1024,
|
|
176
|
+
messages=[{"role": "user", "content": "Hello!"}],
|
|
177
|
+
)
|
|
178
|
+
```
|
|
179
|
+
|
|
180
|
+
`max_tokens` is required by Anthropic. The `system` parameter is a top-level kwarg, not a message role — same as the native SDK.
|
|
181
|
+
|
|
182
|
+
---
|
|
183
|
+
|
|
184
|
+
## Labelling Calls
|
|
185
|
+
|
|
186
|
+
Use `_dl_endpoint` and `_dl_labels` to annotate individual calls. These are stripped before the request reaches the provider.
|
|
187
|
+
|
|
188
|
+
```python
|
|
189
|
+
response = client.chat.completions.create(
|
|
190
|
+
model="gpt-4o-mini",
|
|
191
|
+
messages=[...],
|
|
192
|
+
_dl_endpoint="summarise_article", # logical function name
|
|
193
|
+
_dl_labels={"user_id": "u_123", "team": "growth"},
|
|
194
|
+
)
|
|
195
|
+
```
|
|
196
|
+
|
|
197
|
+
`user_id` and `team_id` in labels are indexed in SQLite for fast per-user queries.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
## Ambient Tagging
|
|
202
|
+
|
|
203
|
+
Attach labels to all calls within a scope without modifying every call site — useful in middleware:
|
|
204
|
+
|
|
205
|
+
```python
|
|
206
|
+
import driftlock
|
|
207
|
+
|
|
208
|
+
with driftlock.tag(request_id="req_abc", user_id="u_42", feature="chat"):
|
|
209
|
+
response = client.chat.completions.create(...)
|
|
210
|
+
```
|
|
211
|
+
|
|
212
|
+
Tags from nested `driftlock.tag()` blocks merge; inner values override outer ones. Per-call `_dl_labels` always wins.
|
|
213
|
+
|
|
214
|
+
---
|
|
215
|
+
|
|
216
|
+
## Configuration
|
|
217
|
+
|
|
218
|
+
```python
|
|
219
|
+
from driftlock import DriftlockClient, DriftlockConfig
|
|
220
|
+
|
|
221
|
+
config = DriftlockConfig(
|
|
222
|
+
log_json=True, # JSON logs (default). False = human-readable.
|
|
223
|
+
log_level="INFO",
|
|
224
|
+
storage_backend="sqlite", # "sqlite" | "none"
|
|
225
|
+
db_path="driftlock.sqlite",
|
|
226
|
+
prompt_token_warning_threshold=4000, # Warn if prompt > N tokens.
|
|
227
|
+
cost_warning_threshold=0.10, # Warn if a single call costs > $X.
|
|
228
|
+
default_labels={"env": "prod"}, # Attached to every tracked call.
|
|
229
|
+
)
|
|
230
|
+
|
|
231
|
+
client = DriftlockClient(api_key="sk-...", config=config)
|
|
232
|
+
```
|
|
233
|
+
|
|
234
|
+
---
|
|
235
|
+
|
|
236
|
+
## Policy Engine
|
|
237
|
+
|
|
238
|
+
The policy engine enforces governance rules before every API call. Rules are evaluated in order; the first block raises `PolicyViolationError`.
|
|
239
|
+
|
|
240
|
+
```python
|
|
241
|
+
from driftlock import (
|
|
242
|
+
DriftlockClient,
|
|
243
|
+
PolicyEngine,
|
|
244
|
+
MonthlyBudgetRule,
|
|
245
|
+
MaxCostPerRequestRule,
|
|
246
|
+
VelocityLimitRule,
|
|
247
|
+
CostVelocityRule,
|
|
248
|
+
PerUserBudgetRule,
|
|
249
|
+
ForecastBudgetRule,
|
|
250
|
+
RestrictModelRule,
|
|
251
|
+
TagBasedModelDowngradeRule,
|
|
252
|
+
PolicyViolationError,
|
|
253
|
+
)
|
|
254
|
+
|
|
255
|
+
policy = PolicyEngine(rules=[
|
|
256
|
+
MonthlyBudgetRule(max_usd=100.0), # Block at $100/month workspace
|
|
257
|
+
MaxCostPerRequestRule(max_usd=0.10), # Block single calls > $0.10
|
|
258
|
+
VelocityLimitRule(max_requests=60, window_seconds=60), # 60 req/min circuit breaker
|
|
259
|
+
])
|
|
260
|
+
|
|
261
|
+
client = DriftlockClient(api_key="sk-...", policy=policy)
|
|
262
|
+
|
|
263
|
+
try:
|
|
264
|
+
response = client.chat.completions.create(...)
|
|
265
|
+
except PolicyViolationError as e:
|
|
266
|
+
print(f"Blocked by {e.rule_name}: {e.decision.metadata}")
|
|
267
|
+
```
|
|
268
|
+
|
|
269
|
+
### Available Rules
|
|
270
|
+
|
|
271
|
+
| Rule | What it does |
|
|
272
|
+
|---|---|
|
|
273
|
+
| `MonthlyBudgetRule(max_usd, scope="workspace"\|"user")` | Block once monthly spend cap is reached |
|
|
274
|
+
| `MaxCostPerRequestRule(max_usd)` | Block a single call if estimated cost exceeds the limit |
|
|
275
|
+
| `PerUserBudgetRule(user_budgets, default_max_usd)` | Per-user monthly caps from a dict |
|
|
276
|
+
| `ForecastBudgetRule(monthly_budget_usd, lookback_days=7)` | Block when projected 30-day spend will exceed budget |
|
|
277
|
+
| `VelocityLimitRule(max_requests, window_seconds, scope="workspace"\|"user")` | Circuit breaker on request rate |
|
|
278
|
+
| `CostVelocityRule(max_cost_usd, window_seconds)` | Circuit breaker on spend rate (e.g. $5/hour) |
|
|
279
|
+
| `RestrictModelRule(disallowed_models, condition=None)` | Block calls to specific models |
|
|
280
|
+
| `TagBasedModelDowngradeRule(condition, downgrade_to)` | Silently swap model based on labels |
|
|
281
|
+
|
|
282
|
+
### Per-User Budgets
|
|
283
|
+
|
|
284
|
+
```python
|
|
285
|
+
policy = PolicyEngine(rules=[
|
|
286
|
+
PerUserBudgetRule(
|
|
287
|
+
user_budgets={"power_user": 20.0, "free_tier": 1.0},
|
|
288
|
+
default_max_usd=5.0, # applied to any user_id not in the dict
|
|
289
|
+
),
|
|
290
|
+
])
|
|
291
|
+
# user_id is read from _dl_labels={"user_id": "..."} or ambient tags
|
|
292
|
+
```
|
|
293
|
+
|
|
294
|
+
### Forecast-Based Blocking
|
|
295
|
+
|
|
296
|
+
```python
|
|
297
|
+
policy = PolicyEngine(rules=[
|
|
298
|
+
ForecastBudgetRule(monthly_budget_usd=50.0, lookback_days=7),
|
|
299
|
+
])
|
|
300
|
+
# Blocks before the budget is actually exhausted — proactive, not reactive
|
|
301
|
+
```
|
|
302
|
+
|
|
303
|
+
### Model Governance
|
|
304
|
+
|
|
305
|
+
```python
|
|
306
|
+
policy = PolicyEngine(rules=[
|
|
307
|
+
# Block GPT-4o on free plan users
|
|
308
|
+
RestrictModelRule(
|
|
309
|
+
disallowed_models={"gpt-4o", "gpt-4"},
|
|
310
|
+
condition=lambda ctx: ctx["labels"].get("plan") == "free",
|
|
311
|
+
),
|
|
312
|
+
# Auto-downgrade free users to mini
|
|
313
|
+
TagBasedModelDowngradeRule(
|
|
314
|
+
condition=lambda ctx: ctx["labels"].get("plan") == "free",
|
|
315
|
+
downgrade_to="gpt-4o-mini",
|
|
316
|
+
),
|
|
317
|
+
])
|
|
318
|
+
```
|
|
319
|
+
|
|
320
|
+
---
|
|
321
|
+
|
|
322
|
+
## Alerts
|
|
323
|
+
|
|
324
|
+
Fire-and-forget notifications when policies trip or cost thresholds are crossed.
|
|
325
|
+
|
|
326
|
+
```python
|
|
327
|
+
from driftlock import DriftlockConfig, WebhookAlertChannel, SlackAlertChannel, LogAlertChannel
|
|
328
|
+
|
|
329
|
+
config = DriftlockConfig(
|
|
330
|
+
alert_channels=[
|
|
331
|
+
SlackAlertChannel(webhook_url="https://hooks.slack.com/services/..."),
|
|
332
|
+
WebhookAlertChannel(url="https://example.com/hooks/driftlock"),
|
|
333
|
+
LogAlertChannel(), # logs to Python logging at WARNING level
|
|
334
|
+
]
|
|
335
|
+
)
|
|
336
|
+
```
|
|
337
|
+
|
|
338
|
+
Alert events: `policy_block`, `cost_warning`, `budget_threshold`, `velocity_trip`.
|
|
339
|
+
|
|
340
|
+
Delivery failures are logged at WARNING level and never propagate to the caller.
|
|
341
|
+
|
|
342
|
+
---
|
|
343
|
+
|
|
344
|
+
## Cost Reduction Engine
|
|
345
|
+
|
|
346
|
+
Enable the optimization pipeline to automatically trim prompts, cap output, and fall back to cheaper models:
|
|
347
|
+
|
|
348
|
+
```python
|
|
349
|
+
from driftlock import DriftlockClient, OptimizationConfig
|
|
350
|
+
|
|
351
|
+
client = DriftlockClient(
|
|
352
|
+
api_key="sk-...",
|
|
353
|
+
optimization=OptimizationConfig(
|
|
354
|
+
max_prompt_tokens=3000, # trim history if prompt exceeds this
|
|
355
|
+
keep_last_n_messages=10, # always keep the N most recent turns
|
|
356
|
+
always_keep_system=True, # never drop the system message
|
|
357
|
+
default_max_output_tokens=512, # cap output when caller omits max_tokens
|
|
358
|
+
max_cost_per_request_usd=0.05, # abort if estimated cost > $0.05
|
|
359
|
+
budget_exceeded_action="fallback",
|
|
360
|
+
fallback_model="gpt-4o-mini",
|
|
361
|
+
),
|
|
362
|
+
)
|
|
363
|
+
```
|
|
364
|
+
|
|
365
|
+
Every call logs an `optimization` block showing tokens and cost saved:
|
|
366
|
+
|
|
367
|
+
```json
|
|
368
|
+
{
|
|
369
|
+
"optimization": {
|
|
370
|
+
"original_prompt_tokens": 3840,
|
|
371
|
+
"optimized_prompt_tokens": 142,
|
|
372
|
+
"tokens_saved": 3698,
|
|
373
|
+
"cost_saved_usd": 0.0005547,
|
|
374
|
+
"optimizations_applied": ["prompt_trim", "output_cap"],
|
|
375
|
+
"quality_risk": true
|
|
376
|
+
}
|
|
377
|
+
}
|
|
378
|
+
```
|
|
379
|
+
|
|
380
|
+
---
|
|
381
|
+
|
|
382
|
+
## Response Cache
|
|
383
|
+
|
|
384
|
+
Exact in-memory cache (LRU + TTL). Returns stored responses for identical requests without hitting the API:
|
|
385
|
+
|
|
386
|
+
```python
|
|
387
|
+
from driftlock import DriftlockClient, CacheConfig
|
|
388
|
+
|
|
389
|
+
client = DriftlockClient(
|
|
390
|
+
api_key="sk-...",
|
|
391
|
+
cache=CacheConfig(
|
|
392
|
+
ttl_seconds=600, # entries expire after 10 minutes
|
|
393
|
+
max_entries=500, # LRU eviction above this
|
|
394
|
+
),
|
|
395
|
+
)
|
|
396
|
+
```
|
|
397
|
+
|
|
398
|
+
Cache hits report `cost=$0.00` and record tokens and dollars saved. Streaming responses are never cached.
|
|
399
|
+
|
|
400
|
+
```python
|
|
401
|
+
client.cache_stats()
|
|
402
|
+
# {"enabled": True, "size": 12, "hits": 48, "misses": 14, "hit_rate": 0.7742}
|
|
403
|
+
```
|
|
404
|
+
|
|
405
|
+
---
|
|
406
|
+
|
|
407
|
+
## Reading Metrics
|
|
408
|
+
|
|
409
|
+
```python
|
|
410
|
+
# Aggregate stats (all time)
|
|
411
|
+
client.stats()
|
|
412
|
+
# {'calls': 42, 'total_tokens': 18500, 'total_cost_usd': 0.003245, ...}
|
|
413
|
+
|
|
414
|
+
# Filter by endpoint, model, or time window
|
|
415
|
+
client.stats(endpoint="summarise_article")
|
|
416
|
+
client.stats(model="gpt-4o")
|
|
417
|
+
client.stats(since="2025-03-01T00:00:00+00:00")
|
|
418
|
+
|
|
419
|
+
# Recent calls
|
|
420
|
+
client.recent_calls(limit=10)
|
|
421
|
+
|
|
422
|
+
# Projected monthly spend
|
|
423
|
+
client.forecast(lookback_days=7)
|
|
424
|
+
# {'daily_avg_usd': 0.0004, 'projected_monthly_usd': 0.012, ...}
|
|
425
|
+
|
|
426
|
+
# Prompt drift detection (detect template changes by endpoint)
|
|
427
|
+
client.prompt_drift(endpoint="summarise_article")
|
|
428
|
+
```
|
|
429
|
+
|
|
430
|
+
---
|
|
431
|
+
|
|
432
|
+
## CLI
|
|
433
|
+
|
|
434
|
+
Inspect telemetry without writing code:
|
|
435
|
+
|
|
436
|
+
```bash
|
|
437
|
+
driftlock stats # aggregate totals
|
|
438
|
+
driftlock stats --since 7d # last 7 days
|
|
439
|
+
driftlock stats --endpoint summarise # filter by endpoint
|
|
440
|
+
driftlock recent --limit 20 # last 20 calls
|
|
441
|
+
driftlock forecast --lookback 7 # projected monthly spend
|
|
442
|
+
driftlock top-endpoints --since 7d # most expensive endpoints
|
|
443
|
+
driftlock top-users --since 30d # per-user spend
|
|
444
|
+
driftlock models # spend by model
|
|
445
|
+
driftlock drift summarise_article # prompt change history
|
|
446
|
+
driftlock --db /path/to/other.db stats # point at a different db
|
|
447
|
+
```
|
|
448
|
+
|
|
449
|
+
Set `DRIFTLOCK_DB_PATH` to override the default `driftlock.sqlite` path.
|
|
450
|
+
|
|
451
|
+
---
|
|
452
|
+
|
|
453
|
+
## Environment Variables
|
|
454
|
+
|
|
455
|
+
| Variable | Default | Effect |
|
|
456
|
+
|---|---|---|
|
|
457
|
+
| `DRIFTLOCK_ENABLED` | `true` | Set to `false` to pass through all calls with zero overhead |
|
|
458
|
+
| `DRIFTLOCK_TRACK_ONLY` | `false` | Track metrics but skip optimization and policy enforcement |
|
|
459
|
+
| `DRIFTLOCK_DB_PATH` | `driftlock.sqlite` | Override the SQLite file path for CLI commands |
|
|
460
|
+
|
|
461
|
+
---
|
|
462
|
+
|
|
463
|
+
## FastAPI Example
|
|
464
|
+
|
|
465
|
+
See [examples/fastapi_app.py](examples/fastapi_app.py) for a full integration with middleware tagging, optimization, and cache.
|
|
466
|
+
|
|
467
|
+
```bash
|
|
468
|
+
OPENAI_API_KEY=sk-... uvicorn examples.fastapi_app:app --reload
|
|
469
|
+
```
|
|
470
|
+
|
|
471
|
+
---
|
|
472
|
+
|
|
473
|
+
## Project Structure
|
|
474
|
+
|
|
475
|
+
```
|
|
476
|
+
driftlock/
|
|
477
|
+
├── __init__.py # Public API
|
|
478
|
+
├── client.py # DriftlockClient (OpenAI wrapper, sync + async)
|
|
479
|
+
├── anthropic_client.py # AnthropicDriftlockClient (opt-in)
|
|
480
|
+
├── config.py # DriftlockConfig
|
|
481
|
+
├── policy.py # PolicyEngine + all rules
|
|
482
|
+
├── alerts.py # AlertChannel, Webhook/Slack/Log implementations
|
|
483
|
+
├── metrics.py # CallMetrics dataclass
|
|
484
|
+
├── pricing.py # OpenAI + Anthropic pricing table
|
|
485
|
+
├── storage.py # SQLiteStorage + NoopStorage (auto-migrating)
|
|
486
|
+
├── optimization.py # OptimizationPipeline, OptimizationConfig
|
|
487
|
+
├── cache.py # ResponseCache (LRU+TTL), CacheConfig
|
|
488
|
+
├── streaming.py # StreamingInterceptor (deferred metrics)
|
|
489
|
+
├── drift.py # Prompt hash + drift detection
|
|
490
|
+
├── cli.py # driftlock CLI entry point
|
|
491
|
+
├── context.py # driftlock.tag() context manager
|
|
492
|
+
├── logger.py # Structured JSON logger
|
|
493
|
+
├── tokenizer.py # tiktoken + char fallback
|
|
494
|
+
└── providers/ # NormalizedUsage, OpenAIProvider, AnthropicProvider
|
|
495
|
+
|
|
496
|
+
examples/
|
|
497
|
+
├── basic_usage.py
|
|
498
|
+
├── fastapi_app.py
|
|
499
|
+
└── dashboard_app.py
|
|
500
|
+
|
|
501
|
+
tests/ # 131 tests
|
|
502
|
+
```
|
|
503
|
+
|
|
504
|
+
---
|
|
505
|
+
|
|
506
|
+
## Roadmap
|
|
507
|
+
|
|
508
|
+
| Feature | Status |
|
|
509
|
+
|---|---|
|
|
510
|
+
| OpenAI chat wrapper (sync + async) | ✅ |
|
|
511
|
+
| Anthropic Messages wrapper (sync + async) | ✅ |
|
|
512
|
+
| Token tracking + cost estimation | ✅ |
|
|
513
|
+
| Latency measurement | ✅ |
|
|
514
|
+
| SQLite storage (auto-migrating) | ✅ |
|
|
515
|
+
| Structured JSON logging | ✅ |
|
|
516
|
+
| Policy engine (budget, velocity, model) | ✅ |
|
|
517
|
+
| Per-user / per-team budget caps | ✅ |
|
|
518
|
+
| Forecast-based budget blocking | ✅ |
|
|
519
|
+
| Velocity + cost circuit breakers | ✅ |
|
|
520
|
+
| Prompt optimization pipeline | ✅ |
|
|
521
|
+
| Exact in-memory response cache | ✅ |
|
|
522
|
+
| Streaming support | ✅ |
|
|
523
|
+
| Prompt drift detection | ✅ |
|
|
524
|
+
| Alert channels (Slack, Webhook, Log) | ✅ |
|
|
525
|
+
| Ambient tagging context manager | ✅ |
|
|
526
|
+
| CLI (stats, forecast, drift, top-users) | ✅ |
|
|
527
|
+
| OpenTelemetry export | Planned |
|
|
528
|
+
| Redis cache backend | Planned |
|
|
529
|
+
| Semantic (embedding-based) cache | Planned |
|
|
530
|
+
| Gemini adapter | Planned |
|
|
531
|
+
| PyPI release | Planned |
|
|
532
|
+
|
|
533
|
+
---
|
|
534
|
+
|
|
535
|
+
## License
|
|
536
|
+
|
|
537
|
+
MIT
|