ai-audit-logger 0.1.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.
- ai_audit_logger-0.1.0/PKG-INFO +217 -0
- ai_audit_logger-0.1.0/README.md +184 -0
- ai_audit_logger-0.1.0/ai_audit_logger/__init__.py +53 -0
- ai_audit_logger-0.1.0/ai_audit_logger/client.py +29 -0
- ai_audit_logger-0.1.0/ai_audit_logger/config.py +14 -0
- ai_audit_logger-0.1.0/ai_audit_logger/wrapper.py +153 -0
- ai_audit_logger-0.1.0/ai_audit_logger.egg-info/PKG-INFO +217 -0
- ai_audit_logger-0.1.0/ai_audit_logger.egg-info/SOURCES.txt +12 -0
- ai_audit_logger-0.1.0/ai_audit_logger.egg-info/dependency_links.txt +1 -0
- ai_audit_logger-0.1.0/ai_audit_logger.egg-info/requires.txt +11 -0
- ai_audit_logger-0.1.0/ai_audit_logger.egg-info/top_level.txt +1 -0
- ai_audit_logger-0.1.0/pyproject.toml +3 -0
- ai_audit_logger-0.1.0/setup.cfg +4 -0
- ai_audit_logger-0.1.0/setup.py +38 -0
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ai-audit-logger
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Drop-in audit logging for LLM API calls (OpenAI, Anthropic)
|
|
5
|
+
Project-URL: Source, https://github.com/elliottj06/ai-audit-logger
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/elliottj06/ai-audit-logger/issues
|
|
7
|
+
Keywords: llm openai anthropic audit logging ai compliance observability
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
|
+
Classifier: Topic :: System :: Logging
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: httpx>=0.24.0
|
|
17
|
+
Provides-Extra: openai
|
|
18
|
+
Requires-Dist: openai>=1.0.0; extra == "openai"
|
|
19
|
+
Provides-Extra: anthropic
|
|
20
|
+
Requires-Dist: anthropic>=0.20.0; extra == "anthropic"
|
|
21
|
+
Provides-Extra: all
|
|
22
|
+
Requires-Dist: openai>=1.0.0; extra == "all"
|
|
23
|
+
Requires-Dist: anthropic>=0.20.0; extra == "all"
|
|
24
|
+
Dynamic: classifier
|
|
25
|
+
Dynamic: description
|
|
26
|
+
Dynamic: description-content-type
|
|
27
|
+
Dynamic: keywords
|
|
28
|
+
Dynamic: project-url
|
|
29
|
+
Dynamic: provides-extra
|
|
30
|
+
Dynamic: requires-dist
|
|
31
|
+
Dynamic: requires-python
|
|
32
|
+
Dynamic: summary
|
|
33
|
+
|
|
34
|
+
# AI Audit Logger
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+

|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
A self-hosted audit logger for LLM API calls. One line of code captures every OpenAI and Anthropic call — prompts, responses, tokens, cost, latency — and stores it in a queryable database.
|
|
41
|
+
|
|
42
|
+
Built for teams that need visibility into their AI usage for **debugging**, **cost control**, and **compliance** (EU AI Act, SOC 2).
|
|
43
|
+
|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## How it works
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
Your App → SDK (wrap your client) → Ingest API → Postgres
|
|
52
|
+
↓
|
|
53
|
+
CLI / Dashboard / Export
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The SDK is a thin wrapper around your existing OpenAI or Anthropic client. It intercepts every call, records it asynchronously (never blocks your app), and ships it to the server.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Quickstart
|
|
61
|
+
|
|
62
|
+
### 1. Start the server
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/elliottj06/ai-audit-logger
|
|
66
|
+
cd ai-audit-logger
|
|
67
|
+
docker compose up -d
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 2. Wrap your client — one line change
|
|
71
|
+
|
|
72
|
+
**OpenAI:**
|
|
73
|
+
```python
|
|
74
|
+
from openai import OpenAI
|
|
75
|
+
from ai_audit_logger import wrap
|
|
76
|
+
|
|
77
|
+
client = wrap(OpenAI())
|
|
78
|
+
|
|
79
|
+
# Every call now logged automatically — no other changes needed
|
|
80
|
+
response = client.chat.completions.create(
|
|
81
|
+
model="gpt-4o",
|
|
82
|
+
messages=[{"role": "user", "content": "Hello"}]
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Anthropic:**
|
|
87
|
+
```python
|
|
88
|
+
from anthropic import Anthropic
|
|
89
|
+
from ai_audit_logger import wrap
|
|
90
|
+
|
|
91
|
+
client = wrap(Anthropic())
|
|
92
|
+
|
|
93
|
+
response = client.messages.create(
|
|
94
|
+
model="claude-sonnet-4-6",
|
|
95
|
+
max_tokens=1024,
|
|
96
|
+
messages=[{"role": "user", "content": "Hello"}]
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 3. Query your logs
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pip install -r cli/requirements.txt
|
|
104
|
+
|
|
105
|
+
python cli/main.py tail # recent calls
|
|
106
|
+
python cli/main.py stats # cost + usage summary
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**`tail` output:**
|
|
110
|
+
```
|
|
111
|
+
Showing 3 of 3 entries
|
|
112
|
+
|
|
113
|
+
Timestamp Provider Model Tokens Cost Latency
|
|
114
|
+
------------------- ----------- ---------------------------- ------------ ------------- ---------
|
|
115
|
+
2026-11-04 14:23:01 openai gpt-4o 18→32 $0.00057 843ms
|
|
116
|
+
2026-11-04 14:23:02 anthropic claude-sonnet-4-6 20→41 $0.00068 1204ms
|
|
117
|
+
2026-11-04 14:23:03 openai gpt-4o-mini 0→0 n/a 120ms ERROR
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**`stats` output:**
|
|
121
|
+
|
|
122
|
+

|
|
123
|
+
|
|
124
|
+
### 4. Open the dashboard
|
|
125
|
+
|
|
126
|
+
With the server running, open **http://localhost:8000** for the web dashboard: summary cards, calls and cost by model, and a filterable table of recent calls (click any row for full detail).
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Try it without API keys
|
|
131
|
+
|
|
132
|
+
Run the included example to send simulated log entries and see the CLI in action:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
python examples/basic_usage.py
|
|
136
|
+
python cli/main.py tail
|
|
137
|
+
python cli/main.py stats
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Configuration
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from ai_audit_logger import configure
|
|
146
|
+
|
|
147
|
+
configure(
|
|
148
|
+
server_url="http://localhost:8000", # your server
|
|
149
|
+
app_name="my-chatbot", # tag logs by app
|
|
150
|
+
user_id="user-123", # tie logs to a user
|
|
151
|
+
session_id="session-abc", # group calls into sessions
|
|
152
|
+
redact_prompts=False, # True = omit prompt text from logs
|
|
153
|
+
enabled=True,
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
| Env var | Default | Description |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `AUDIT_SERVER_URL` | `http://localhost:8000` | Ingest server URL |
|
|
160
|
+
| `AUDIT_APP_NAME` | (none) | Tag logs with your app name |
|
|
161
|
+
| `AUDIT_ENABLED` | `true` | Set `false` to disable without code changes |
|
|
162
|
+
| `AUDIT_REDACT_PROMPTS` | `false` | Omit prompt/response text (privacy mode) |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## API
|
|
167
|
+
|
|
168
|
+
| Endpoint | Method | Description |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `/api/v1/log` | POST | Ingest a log entry |
|
|
171
|
+
| `/api/v1/logs` | GET | Query logs (filter by provider, model, user, app, session) |
|
|
172
|
+
| `/api/v1/logs/stats` | GET | Aggregated stats — total calls, cost, tokens, latency |
|
|
173
|
+
| `/health` | GET | Health check |
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Project structure
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
ai-audit-logger/
|
|
181
|
+
├── server/ # FastAPI ingest + query API
|
|
182
|
+
│ ├── main.py
|
|
183
|
+
│ ├── models.py # SQLAlchemy LogEntry model
|
|
184
|
+
│ ├── db.py # Postgres connection
|
|
185
|
+
│ ├── routes/
|
|
186
|
+
│ │ ├── ingest.py # POST /api/v1/log
|
|
187
|
+
│ │ └── query.py # GET /api/v1/logs + /stats
|
|
188
|
+
│ └── static/index.html # Web dashboard (served at /)
|
|
189
|
+
├── sdk/python/ # Python SDK
|
|
190
|
+
│ └── ai_audit_logger/
|
|
191
|
+
│ ├── __init__.py # wrap() + configure()
|
|
192
|
+
│ ├── wrapper.py # OpenAI + Anthropic wrappers
|
|
193
|
+
│ ├── client.py # Fire-and-forget HTTP sender
|
|
194
|
+
│ └── config.py # AuditConfig dataclass
|
|
195
|
+
├── cli/
|
|
196
|
+
│ └── main.py # tail / stats / get commands
|
|
197
|
+
├── examples/
|
|
198
|
+
│ └── basic_usage.py # Try it without API keys
|
|
199
|
+
└── docker-compose.yml
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Roadmap
|
|
205
|
+
|
|
206
|
+
- [ ] TypeScript / Node.js SDK
|
|
207
|
+
- [x] Web dashboard with cost charts
|
|
208
|
+
- [ ] LangChain integration
|
|
209
|
+
- [ ] Publish to PyPI (`pip install ai-audit-logger`)
|
|
210
|
+
- [ ] Tamper-evident log hashing for compliance exports
|
|
211
|
+
- [ ] Slack / email alerts on cost spikes
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
MIT
|
|
@@ -0,0 +1,184 @@
|
|
|
1
|
+
# AI Audit Logger
|
|
2
|
+
|
|
3
|
+

|
|
4
|
+

|
|
5
|
+

|
|
6
|
+
|
|
7
|
+
A self-hosted audit logger for LLM API calls. One line of code captures every OpenAI and Anthropic call — prompts, responses, tokens, cost, latency — and stores it in a queryable database.
|
|
8
|
+
|
|
9
|
+
Built for teams that need visibility into their AI usage for **debugging**, **cost control**, and **compliance** (EU AI Act, SOC 2).
|
|
10
|
+
|
|
11
|
+

|
|
12
|
+
|
|
13
|
+
---
|
|
14
|
+
|
|
15
|
+
## How it works
|
|
16
|
+
|
|
17
|
+
```
|
|
18
|
+
Your App → SDK (wrap your client) → Ingest API → Postgres
|
|
19
|
+
↓
|
|
20
|
+
CLI / Dashboard / Export
|
|
21
|
+
```
|
|
22
|
+
|
|
23
|
+
The SDK is a thin wrapper around your existing OpenAI or Anthropic client. It intercepts every call, records it asynchronously (never blocks your app), and ships it to the server.
|
|
24
|
+
|
|
25
|
+
---
|
|
26
|
+
|
|
27
|
+
## Quickstart
|
|
28
|
+
|
|
29
|
+
### 1. Start the server
|
|
30
|
+
|
|
31
|
+
```bash
|
|
32
|
+
git clone https://github.com/elliottj06/ai-audit-logger
|
|
33
|
+
cd ai-audit-logger
|
|
34
|
+
docker compose up -d
|
|
35
|
+
```
|
|
36
|
+
|
|
37
|
+
### 2. Wrap your client — one line change
|
|
38
|
+
|
|
39
|
+
**OpenAI:**
|
|
40
|
+
```python
|
|
41
|
+
from openai import OpenAI
|
|
42
|
+
from ai_audit_logger import wrap
|
|
43
|
+
|
|
44
|
+
client = wrap(OpenAI())
|
|
45
|
+
|
|
46
|
+
# Every call now logged automatically — no other changes needed
|
|
47
|
+
response = client.chat.completions.create(
|
|
48
|
+
model="gpt-4o",
|
|
49
|
+
messages=[{"role": "user", "content": "Hello"}]
|
|
50
|
+
)
|
|
51
|
+
```
|
|
52
|
+
|
|
53
|
+
**Anthropic:**
|
|
54
|
+
```python
|
|
55
|
+
from anthropic import Anthropic
|
|
56
|
+
from ai_audit_logger import wrap
|
|
57
|
+
|
|
58
|
+
client = wrap(Anthropic())
|
|
59
|
+
|
|
60
|
+
response = client.messages.create(
|
|
61
|
+
model="claude-sonnet-4-6",
|
|
62
|
+
max_tokens=1024,
|
|
63
|
+
messages=[{"role": "user", "content": "Hello"}]
|
|
64
|
+
)
|
|
65
|
+
```
|
|
66
|
+
|
|
67
|
+
### 3. Query your logs
|
|
68
|
+
|
|
69
|
+
```bash
|
|
70
|
+
pip install -r cli/requirements.txt
|
|
71
|
+
|
|
72
|
+
python cli/main.py tail # recent calls
|
|
73
|
+
python cli/main.py stats # cost + usage summary
|
|
74
|
+
```
|
|
75
|
+
|
|
76
|
+
**`tail` output:**
|
|
77
|
+
```
|
|
78
|
+
Showing 3 of 3 entries
|
|
79
|
+
|
|
80
|
+
Timestamp Provider Model Tokens Cost Latency
|
|
81
|
+
------------------- ----------- ---------------------------- ------------ ------------- ---------
|
|
82
|
+
2026-11-04 14:23:01 openai gpt-4o 18→32 $0.00057 843ms
|
|
83
|
+
2026-11-04 14:23:02 anthropic claude-sonnet-4-6 20→41 $0.00068 1204ms
|
|
84
|
+
2026-11-04 14:23:03 openai gpt-4o-mini 0→0 n/a 120ms ERROR
|
|
85
|
+
```
|
|
86
|
+
|
|
87
|
+
**`stats` output:**
|
|
88
|
+
|
|
89
|
+

|
|
90
|
+
|
|
91
|
+
### 4. Open the dashboard
|
|
92
|
+
|
|
93
|
+
With the server running, open **http://localhost:8000** for the web dashboard: summary cards, calls and cost by model, and a filterable table of recent calls (click any row for full detail).
|
|
94
|
+
|
|
95
|
+
---
|
|
96
|
+
|
|
97
|
+
## Try it without API keys
|
|
98
|
+
|
|
99
|
+
Run the included example to send simulated log entries and see the CLI in action:
|
|
100
|
+
|
|
101
|
+
```bash
|
|
102
|
+
python examples/basic_usage.py
|
|
103
|
+
python cli/main.py tail
|
|
104
|
+
python cli/main.py stats
|
|
105
|
+
```
|
|
106
|
+
|
|
107
|
+
---
|
|
108
|
+
|
|
109
|
+
## Configuration
|
|
110
|
+
|
|
111
|
+
```python
|
|
112
|
+
from ai_audit_logger import configure
|
|
113
|
+
|
|
114
|
+
configure(
|
|
115
|
+
server_url="http://localhost:8000", # your server
|
|
116
|
+
app_name="my-chatbot", # tag logs by app
|
|
117
|
+
user_id="user-123", # tie logs to a user
|
|
118
|
+
session_id="session-abc", # group calls into sessions
|
|
119
|
+
redact_prompts=False, # True = omit prompt text from logs
|
|
120
|
+
enabled=True,
|
|
121
|
+
)
|
|
122
|
+
```
|
|
123
|
+
|
|
124
|
+
| Env var | Default | Description |
|
|
125
|
+
|---|---|---|
|
|
126
|
+
| `AUDIT_SERVER_URL` | `http://localhost:8000` | Ingest server URL |
|
|
127
|
+
| `AUDIT_APP_NAME` | (none) | Tag logs with your app name |
|
|
128
|
+
| `AUDIT_ENABLED` | `true` | Set `false` to disable without code changes |
|
|
129
|
+
| `AUDIT_REDACT_PROMPTS` | `false` | Omit prompt/response text (privacy mode) |
|
|
130
|
+
|
|
131
|
+
---
|
|
132
|
+
|
|
133
|
+
## API
|
|
134
|
+
|
|
135
|
+
| Endpoint | Method | Description |
|
|
136
|
+
|---|---|---|
|
|
137
|
+
| `/api/v1/log` | POST | Ingest a log entry |
|
|
138
|
+
| `/api/v1/logs` | GET | Query logs (filter by provider, model, user, app, session) |
|
|
139
|
+
| `/api/v1/logs/stats` | GET | Aggregated stats — total calls, cost, tokens, latency |
|
|
140
|
+
| `/health` | GET | Health check |
|
|
141
|
+
|
|
142
|
+
---
|
|
143
|
+
|
|
144
|
+
## Project structure
|
|
145
|
+
|
|
146
|
+
```
|
|
147
|
+
ai-audit-logger/
|
|
148
|
+
├── server/ # FastAPI ingest + query API
|
|
149
|
+
│ ├── main.py
|
|
150
|
+
│ ├── models.py # SQLAlchemy LogEntry model
|
|
151
|
+
│ ├── db.py # Postgres connection
|
|
152
|
+
│ ├── routes/
|
|
153
|
+
│ │ ├── ingest.py # POST /api/v1/log
|
|
154
|
+
│ │ └── query.py # GET /api/v1/logs + /stats
|
|
155
|
+
│ └── static/index.html # Web dashboard (served at /)
|
|
156
|
+
├── sdk/python/ # Python SDK
|
|
157
|
+
│ └── ai_audit_logger/
|
|
158
|
+
│ ├── __init__.py # wrap() + configure()
|
|
159
|
+
│ ├── wrapper.py # OpenAI + Anthropic wrappers
|
|
160
|
+
│ ├── client.py # Fire-and-forget HTTP sender
|
|
161
|
+
│ └── config.py # AuditConfig dataclass
|
|
162
|
+
├── cli/
|
|
163
|
+
│ └── main.py # tail / stats / get commands
|
|
164
|
+
├── examples/
|
|
165
|
+
│ └── basic_usage.py # Try it without API keys
|
|
166
|
+
└── docker-compose.yml
|
|
167
|
+
```
|
|
168
|
+
|
|
169
|
+
---
|
|
170
|
+
|
|
171
|
+
## Roadmap
|
|
172
|
+
|
|
173
|
+
- [ ] TypeScript / Node.js SDK
|
|
174
|
+
- [x] Web dashboard with cost charts
|
|
175
|
+
- [ ] LangChain integration
|
|
176
|
+
- [ ] Publish to PyPI (`pip install ai-audit-logger`)
|
|
177
|
+
- [ ] Tamper-evident log hashing for compliance exports
|
|
178
|
+
- [ ] Slack / email alerts on cost spikes
|
|
179
|
+
|
|
180
|
+
---
|
|
181
|
+
|
|
182
|
+
## License
|
|
183
|
+
|
|
184
|
+
MIT
|
|
@@ -0,0 +1,53 @@
|
|
|
1
|
+
from .config import AuditConfig
|
|
2
|
+
from .client import AuditClient
|
|
3
|
+
from .wrapper import WrappedOpenAI, WrappedAnthropic
|
|
4
|
+
|
|
5
|
+
_default_config = AuditConfig()
|
|
6
|
+
_default_client = AuditClient(_default_config)
|
|
7
|
+
|
|
8
|
+
|
|
9
|
+
def configure(
|
|
10
|
+
server_url: str = None,
|
|
11
|
+
app_name: str = None,
|
|
12
|
+
user_id: str = None,
|
|
13
|
+
session_id: str = None,
|
|
14
|
+
redact_prompts: bool = False,
|
|
15
|
+
enabled: bool = True,
|
|
16
|
+
):
|
|
17
|
+
"""Configure global defaults for the audit logger."""
|
|
18
|
+
global _default_config, _default_client
|
|
19
|
+
_default_config = AuditConfig(
|
|
20
|
+
server_url=server_url or _default_config.server_url,
|
|
21
|
+
app_name=app_name,
|
|
22
|
+
user_id=user_id,
|
|
23
|
+
session_id=session_id,
|
|
24
|
+
redact_prompts=redact_prompts,
|
|
25
|
+
enabled=enabled,
|
|
26
|
+
)
|
|
27
|
+
_default_client = AuditClient(_default_config)
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
def wrap(client, config: AuditConfig = None):
|
|
31
|
+
"""
|
|
32
|
+
Wrap an OpenAI or Anthropic client to automatically log all LLM calls.
|
|
33
|
+
|
|
34
|
+
Usage:
|
|
35
|
+
from openai import OpenAI
|
|
36
|
+
from ai_audit_logger import wrap
|
|
37
|
+
|
|
38
|
+
client = wrap(OpenAI())
|
|
39
|
+
# All calls now logged automatically
|
|
40
|
+
"""
|
|
41
|
+
cfg = config or _default_config
|
|
42
|
+
audit = AuditClient(cfg)
|
|
43
|
+
class_name = type(client).__name__
|
|
44
|
+
|
|
45
|
+
if "OpenAI" in class_name:
|
|
46
|
+
return WrappedOpenAI(client, audit, cfg)
|
|
47
|
+
elif "Anthropic" in class_name:
|
|
48
|
+
return WrappedAnthropic(client, audit, cfg)
|
|
49
|
+
else:
|
|
50
|
+
raise ValueError(f"Unsupported client type: {class_name}. Expected OpenAI or Anthropic.")
|
|
51
|
+
|
|
52
|
+
|
|
53
|
+
__all__ = ["wrap", "configure", "AuditConfig", "AuditClient"]
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
import httpx
|
|
2
|
+
import threading
|
|
3
|
+
from .config import AuditConfig
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
class AuditClient:
|
|
7
|
+
def __init__(self, config: AuditConfig):
|
|
8
|
+
self.config = config
|
|
9
|
+
self._http = httpx.Client(timeout=config.timeout)
|
|
10
|
+
|
|
11
|
+
def log(self, **kwargs):
|
|
12
|
+
if not self.config.enabled:
|
|
13
|
+
return
|
|
14
|
+
# Fire-and-forget — never blocks the calling application
|
|
15
|
+
t = threading.Thread(target=self._send, kwargs=kwargs, daemon=True)
|
|
16
|
+
t.start()
|
|
17
|
+
|
|
18
|
+
def _send(self, **kwargs):
|
|
19
|
+
try:
|
|
20
|
+
self._http.post(
|
|
21
|
+
f"{self.config.server_url}/api/v1/log",
|
|
22
|
+
json=kwargs,
|
|
23
|
+
timeout=self.config.timeout,
|
|
24
|
+
)
|
|
25
|
+
except Exception:
|
|
26
|
+
pass # Silently drop — logging must never crash the app
|
|
27
|
+
|
|
28
|
+
def close(self):
|
|
29
|
+
self._http.close()
|
|
@@ -0,0 +1,14 @@
|
|
|
1
|
+
from dataclasses import dataclass, field
|
|
2
|
+
from typing import Optional
|
|
3
|
+
import os
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
@dataclass
|
|
7
|
+
class AuditConfig:
|
|
8
|
+
server_url: str = field(default_factory=lambda: os.getenv("AUDIT_SERVER_URL", "http://localhost:8000"))
|
|
9
|
+
app_name: Optional[str] = field(default_factory=lambda: os.getenv("AUDIT_APP_NAME"))
|
|
10
|
+
user_id: Optional[str] = None
|
|
11
|
+
session_id: Optional[str] = None
|
|
12
|
+
enabled: bool = field(default_factory=lambda: os.getenv("AUDIT_ENABLED", "true").lower() == "true")
|
|
13
|
+
redact_prompts: bool = field(default_factory=lambda: os.getenv("AUDIT_REDACT_PROMPTS", "false").lower() == "true")
|
|
14
|
+
timeout: float = 2.0
|
|
@@ -0,0 +1,153 @@
|
|
|
1
|
+
import time
|
|
2
|
+
import json
|
|
3
|
+
from typing import Optional
|
|
4
|
+
from .config import AuditConfig
|
|
5
|
+
from .client import AuditClient
|
|
6
|
+
|
|
7
|
+
# Cost per 1M tokens (input, output) in USD
|
|
8
|
+
COST_TABLE = {
|
|
9
|
+
"gpt-4o-mini": (0.15, 0.60),
|
|
10
|
+
"gpt-4o": (5.00, 15.00),
|
|
11
|
+
"gpt-4-turbo": (10.00, 30.00),
|
|
12
|
+
"gpt-3.5-turbo": (0.50, 1.50),
|
|
13
|
+
"claude-haiku-4-5": (0.80, 4.00),
|
|
14
|
+
"claude-sonnet-4-6": (3.00, 15.00),
|
|
15
|
+
"claude-opus-4-6": (15.00, 75.00),
|
|
16
|
+
"claude-3-5-sonnet": (3.00, 15.00),
|
|
17
|
+
"claude-3-5-haiku": (0.80, 4.00),
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
|
|
21
|
+
def _calc_cost(model: str, tokens_in: int, tokens_out: int) -> Optional[float]:
|
|
22
|
+
model_lower = model.lower()
|
|
23
|
+
for key, (cost_in, cost_out) in COST_TABLE.items():
|
|
24
|
+
if key in model_lower:
|
|
25
|
+
return (tokens_in * cost_in + tokens_out * cost_out) / 1_000_000
|
|
26
|
+
return None
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
# ── OpenAI wrappers ───────────────────────────────────────────────────────────
|
|
30
|
+
|
|
31
|
+
class WrappedOpenAI:
|
|
32
|
+
def __init__(self, client, audit: AuditClient, config: AuditConfig):
|
|
33
|
+
self._client = client
|
|
34
|
+
self.chat = WrappedChat(client.chat, audit, config)
|
|
35
|
+
|
|
36
|
+
def __getattr__(self, name):
|
|
37
|
+
return getattr(self._client, name)
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
class WrappedChat:
|
|
41
|
+
def __init__(self, chat, audit: AuditClient, config: AuditConfig):
|
|
42
|
+
self._chat = chat
|
|
43
|
+
self.completions = WrappedCompletions(chat.completions, audit, config)
|
|
44
|
+
|
|
45
|
+
def __getattr__(self, name):
|
|
46
|
+
return getattr(self._chat, name)
|
|
47
|
+
|
|
48
|
+
|
|
49
|
+
class WrappedCompletions:
|
|
50
|
+
def __init__(self, completions, audit: AuditClient, config: AuditConfig):
|
|
51
|
+
self._completions = completions
|
|
52
|
+
self._audit = audit
|
|
53
|
+
self._config = config
|
|
54
|
+
|
|
55
|
+
def create(self, **kwargs):
|
|
56
|
+
start = time.time()
|
|
57
|
+
error = None
|
|
58
|
+
response = None
|
|
59
|
+
try:
|
|
60
|
+
response = self._completions.create(**kwargs)
|
|
61
|
+
return response
|
|
62
|
+
except Exception as e:
|
|
63
|
+
error = str(e)
|
|
64
|
+
raise
|
|
65
|
+
finally:
|
|
66
|
+
latency_ms = (time.time() - start) * 1000
|
|
67
|
+
model = kwargs.get("model", "unknown")
|
|
68
|
+
tokens_in = tokens_out = None
|
|
69
|
+
response_text = None
|
|
70
|
+
|
|
71
|
+
if response:
|
|
72
|
+
if hasattr(response, "usage") and response.usage:
|
|
73
|
+
tokens_in = response.usage.prompt_tokens
|
|
74
|
+
tokens_out = response.usage.completion_tokens
|
|
75
|
+
if hasattr(response, "choices") and response.choices:
|
|
76
|
+
response_text = response.choices[0].message.content
|
|
77
|
+
|
|
78
|
+
self._audit.log(
|
|
79
|
+
provider="openai",
|
|
80
|
+
model=model,
|
|
81
|
+
prompt=None if self._config.redact_prompts else json.dumps(kwargs.get("messages", [])),
|
|
82
|
+
response=response_text,
|
|
83
|
+
tokens_in=tokens_in,
|
|
84
|
+
tokens_out=tokens_out,
|
|
85
|
+
latency_ms=latency_ms,
|
|
86
|
+
cost_usd=_calc_cost(model, tokens_in or 0, tokens_out or 0) if tokens_in else None,
|
|
87
|
+
session_id=self._config.session_id,
|
|
88
|
+
user_id=self._config.user_id,
|
|
89
|
+
app_name=self._config.app_name,
|
|
90
|
+
error=error,
|
|
91
|
+
)
|
|
92
|
+
|
|
93
|
+
def __getattr__(self, name):
|
|
94
|
+
return getattr(self._completions, name)
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
# ── Anthropic wrappers ────────────────────────────────────────────────────────
|
|
98
|
+
|
|
99
|
+
class WrappedAnthropic:
|
|
100
|
+
def __init__(self, client, audit: AuditClient, config: AuditConfig):
|
|
101
|
+
self._client = client
|
|
102
|
+
self.messages = WrappedAnthropicMessages(client.messages, audit, config)
|
|
103
|
+
|
|
104
|
+
def __getattr__(self, name):
|
|
105
|
+
return getattr(self._client, name)
|
|
106
|
+
|
|
107
|
+
|
|
108
|
+
class WrappedAnthropicMessages:
|
|
109
|
+
def __init__(self, messages, audit: AuditClient, config: AuditConfig):
|
|
110
|
+
self._messages = messages
|
|
111
|
+
self._audit = audit
|
|
112
|
+
self._config = config
|
|
113
|
+
|
|
114
|
+
def create(self, **kwargs):
|
|
115
|
+
start = time.time()
|
|
116
|
+
error = None
|
|
117
|
+
response = None
|
|
118
|
+
try:
|
|
119
|
+
response = self._messages.create(**kwargs)
|
|
120
|
+
return response
|
|
121
|
+
except Exception as e:
|
|
122
|
+
error = str(e)
|
|
123
|
+
raise
|
|
124
|
+
finally:
|
|
125
|
+
latency_ms = (time.time() - start) * 1000
|
|
126
|
+
model = kwargs.get("model", "unknown")
|
|
127
|
+
tokens_in = tokens_out = None
|
|
128
|
+
response_text = None
|
|
129
|
+
|
|
130
|
+
if response:
|
|
131
|
+
if hasattr(response, "usage"):
|
|
132
|
+
tokens_in = response.usage.input_tokens
|
|
133
|
+
tokens_out = response.usage.output_tokens
|
|
134
|
+
if hasattr(response, "content") and response.content:
|
|
135
|
+
response_text = response.content[0].text
|
|
136
|
+
|
|
137
|
+
self._audit.log(
|
|
138
|
+
provider="anthropic",
|
|
139
|
+
model=model,
|
|
140
|
+
prompt=None if self._config.redact_prompts else json.dumps(kwargs.get("messages", [])),
|
|
141
|
+
response=response_text,
|
|
142
|
+
tokens_in=tokens_in,
|
|
143
|
+
tokens_out=tokens_out,
|
|
144
|
+
latency_ms=latency_ms,
|
|
145
|
+
cost_usd=_calc_cost(model, tokens_in or 0, tokens_out or 0) if tokens_in else None,
|
|
146
|
+
session_id=self._config.session_id,
|
|
147
|
+
user_id=self._config.user_id,
|
|
148
|
+
app_name=self._config.app_name,
|
|
149
|
+
error=error,
|
|
150
|
+
)
|
|
151
|
+
|
|
152
|
+
def __getattr__(self, name):
|
|
153
|
+
return getattr(self._messages, name)
|
|
@@ -0,0 +1,217 @@
|
|
|
1
|
+
Metadata-Version: 2.4
|
|
2
|
+
Name: ai-audit-logger
|
|
3
|
+
Version: 0.1.0
|
|
4
|
+
Summary: Drop-in audit logging for LLM API calls (OpenAI, Anthropic)
|
|
5
|
+
Project-URL: Source, https://github.com/elliottj06/ai-audit-logger
|
|
6
|
+
Project-URL: Bug Tracker, https://github.com/elliottj06/ai-audit-logger/issues
|
|
7
|
+
Keywords: llm openai anthropic audit logging ai compliance observability
|
|
8
|
+
Classifier: Programming Language :: Python :: 3
|
|
9
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
10
|
+
Classifier: Operating System :: OS Independent
|
|
11
|
+
Classifier: Topic :: Software Development :: Libraries :: Python Modules
|
|
12
|
+
Classifier: Topic :: System :: Logging
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Requires-Python: >=3.9
|
|
15
|
+
Description-Content-Type: text/markdown
|
|
16
|
+
Requires-Dist: httpx>=0.24.0
|
|
17
|
+
Provides-Extra: openai
|
|
18
|
+
Requires-Dist: openai>=1.0.0; extra == "openai"
|
|
19
|
+
Provides-Extra: anthropic
|
|
20
|
+
Requires-Dist: anthropic>=0.20.0; extra == "anthropic"
|
|
21
|
+
Provides-Extra: all
|
|
22
|
+
Requires-Dist: openai>=1.0.0; extra == "all"
|
|
23
|
+
Requires-Dist: anthropic>=0.20.0; extra == "all"
|
|
24
|
+
Dynamic: classifier
|
|
25
|
+
Dynamic: description
|
|
26
|
+
Dynamic: description-content-type
|
|
27
|
+
Dynamic: keywords
|
|
28
|
+
Dynamic: project-url
|
|
29
|
+
Dynamic: provides-extra
|
|
30
|
+
Dynamic: requires-dist
|
|
31
|
+
Dynamic: requires-python
|
|
32
|
+
Dynamic: summary
|
|
33
|
+
|
|
34
|
+
# AI Audit Logger
|
|
35
|
+
|
|
36
|
+

|
|
37
|
+

|
|
38
|
+

|
|
39
|
+
|
|
40
|
+
A self-hosted audit logger for LLM API calls. One line of code captures every OpenAI and Anthropic call — prompts, responses, tokens, cost, latency — and stores it in a queryable database.
|
|
41
|
+
|
|
42
|
+
Built for teams that need visibility into their AI usage for **debugging**, **cost control**, and **compliance** (EU AI Act, SOC 2).
|
|
43
|
+
|
|
44
|
+

|
|
45
|
+
|
|
46
|
+
---
|
|
47
|
+
|
|
48
|
+
## How it works
|
|
49
|
+
|
|
50
|
+
```
|
|
51
|
+
Your App → SDK (wrap your client) → Ingest API → Postgres
|
|
52
|
+
↓
|
|
53
|
+
CLI / Dashboard / Export
|
|
54
|
+
```
|
|
55
|
+
|
|
56
|
+
The SDK is a thin wrapper around your existing OpenAI or Anthropic client. It intercepts every call, records it asynchronously (never blocks your app), and ships it to the server.
|
|
57
|
+
|
|
58
|
+
---
|
|
59
|
+
|
|
60
|
+
## Quickstart
|
|
61
|
+
|
|
62
|
+
### 1. Start the server
|
|
63
|
+
|
|
64
|
+
```bash
|
|
65
|
+
git clone https://github.com/elliottj06/ai-audit-logger
|
|
66
|
+
cd ai-audit-logger
|
|
67
|
+
docker compose up -d
|
|
68
|
+
```
|
|
69
|
+
|
|
70
|
+
### 2. Wrap your client — one line change
|
|
71
|
+
|
|
72
|
+
**OpenAI:**
|
|
73
|
+
```python
|
|
74
|
+
from openai import OpenAI
|
|
75
|
+
from ai_audit_logger import wrap
|
|
76
|
+
|
|
77
|
+
client = wrap(OpenAI())
|
|
78
|
+
|
|
79
|
+
# Every call now logged automatically — no other changes needed
|
|
80
|
+
response = client.chat.completions.create(
|
|
81
|
+
model="gpt-4o",
|
|
82
|
+
messages=[{"role": "user", "content": "Hello"}]
|
|
83
|
+
)
|
|
84
|
+
```
|
|
85
|
+
|
|
86
|
+
**Anthropic:**
|
|
87
|
+
```python
|
|
88
|
+
from anthropic import Anthropic
|
|
89
|
+
from ai_audit_logger import wrap
|
|
90
|
+
|
|
91
|
+
client = wrap(Anthropic())
|
|
92
|
+
|
|
93
|
+
response = client.messages.create(
|
|
94
|
+
model="claude-sonnet-4-6",
|
|
95
|
+
max_tokens=1024,
|
|
96
|
+
messages=[{"role": "user", "content": "Hello"}]
|
|
97
|
+
)
|
|
98
|
+
```
|
|
99
|
+
|
|
100
|
+
### 3. Query your logs
|
|
101
|
+
|
|
102
|
+
```bash
|
|
103
|
+
pip install -r cli/requirements.txt
|
|
104
|
+
|
|
105
|
+
python cli/main.py tail # recent calls
|
|
106
|
+
python cli/main.py stats # cost + usage summary
|
|
107
|
+
```
|
|
108
|
+
|
|
109
|
+
**`tail` output:**
|
|
110
|
+
```
|
|
111
|
+
Showing 3 of 3 entries
|
|
112
|
+
|
|
113
|
+
Timestamp Provider Model Tokens Cost Latency
|
|
114
|
+
------------------- ----------- ---------------------------- ------------ ------------- ---------
|
|
115
|
+
2026-11-04 14:23:01 openai gpt-4o 18→32 $0.00057 843ms
|
|
116
|
+
2026-11-04 14:23:02 anthropic claude-sonnet-4-6 20→41 $0.00068 1204ms
|
|
117
|
+
2026-11-04 14:23:03 openai gpt-4o-mini 0→0 n/a 120ms ERROR
|
|
118
|
+
```
|
|
119
|
+
|
|
120
|
+
**`stats` output:**
|
|
121
|
+
|
|
122
|
+

|
|
123
|
+
|
|
124
|
+
### 4. Open the dashboard
|
|
125
|
+
|
|
126
|
+
With the server running, open **http://localhost:8000** for the web dashboard: summary cards, calls and cost by model, and a filterable table of recent calls (click any row for full detail).
|
|
127
|
+
|
|
128
|
+
---
|
|
129
|
+
|
|
130
|
+
## Try it without API keys
|
|
131
|
+
|
|
132
|
+
Run the included example to send simulated log entries and see the CLI in action:
|
|
133
|
+
|
|
134
|
+
```bash
|
|
135
|
+
python examples/basic_usage.py
|
|
136
|
+
python cli/main.py tail
|
|
137
|
+
python cli/main.py stats
|
|
138
|
+
```
|
|
139
|
+
|
|
140
|
+
---
|
|
141
|
+
|
|
142
|
+
## Configuration
|
|
143
|
+
|
|
144
|
+
```python
|
|
145
|
+
from ai_audit_logger import configure
|
|
146
|
+
|
|
147
|
+
configure(
|
|
148
|
+
server_url="http://localhost:8000", # your server
|
|
149
|
+
app_name="my-chatbot", # tag logs by app
|
|
150
|
+
user_id="user-123", # tie logs to a user
|
|
151
|
+
session_id="session-abc", # group calls into sessions
|
|
152
|
+
redact_prompts=False, # True = omit prompt text from logs
|
|
153
|
+
enabled=True,
|
|
154
|
+
)
|
|
155
|
+
```
|
|
156
|
+
|
|
157
|
+
| Env var | Default | Description |
|
|
158
|
+
|---|---|---|
|
|
159
|
+
| `AUDIT_SERVER_URL` | `http://localhost:8000` | Ingest server URL |
|
|
160
|
+
| `AUDIT_APP_NAME` | (none) | Tag logs with your app name |
|
|
161
|
+
| `AUDIT_ENABLED` | `true` | Set `false` to disable without code changes |
|
|
162
|
+
| `AUDIT_REDACT_PROMPTS` | `false` | Omit prompt/response text (privacy mode) |
|
|
163
|
+
|
|
164
|
+
---
|
|
165
|
+
|
|
166
|
+
## API
|
|
167
|
+
|
|
168
|
+
| Endpoint | Method | Description |
|
|
169
|
+
|---|---|---|
|
|
170
|
+
| `/api/v1/log` | POST | Ingest a log entry |
|
|
171
|
+
| `/api/v1/logs` | GET | Query logs (filter by provider, model, user, app, session) |
|
|
172
|
+
| `/api/v1/logs/stats` | GET | Aggregated stats — total calls, cost, tokens, latency |
|
|
173
|
+
| `/health` | GET | Health check |
|
|
174
|
+
|
|
175
|
+
---
|
|
176
|
+
|
|
177
|
+
## Project structure
|
|
178
|
+
|
|
179
|
+
```
|
|
180
|
+
ai-audit-logger/
|
|
181
|
+
├── server/ # FastAPI ingest + query API
|
|
182
|
+
│ ├── main.py
|
|
183
|
+
│ ├── models.py # SQLAlchemy LogEntry model
|
|
184
|
+
│ ├── db.py # Postgres connection
|
|
185
|
+
│ ├── routes/
|
|
186
|
+
│ │ ├── ingest.py # POST /api/v1/log
|
|
187
|
+
│ │ └── query.py # GET /api/v1/logs + /stats
|
|
188
|
+
│ └── static/index.html # Web dashboard (served at /)
|
|
189
|
+
├── sdk/python/ # Python SDK
|
|
190
|
+
│ └── ai_audit_logger/
|
|
191
|
+
│ ├── __init__.py # wrap() + configure()
|
|
192
|
+
│ ├── wrapper.py # OpenAI + Anthropic wrappers
|
|
193
|
+
│ ├── client.py # Fire-and-forget HTTP sender
|
|
194
|
+
│ └── config.py # AuditConfig dataclass
|
|
195
|
+
├── cli/
|
|
196
|
+
│ └── main.py # tail / stats / get commands
|
|
197
|
+
├── examples/
|
|
198
|
+
│ └── basic_usage.py # Try it without API keys
|
|
199
|
+
└── docker-compose.yml
|
|
200
|
+
```
|
|
201
|
+
|
|
202
|
+
---
|
|
203
|
+
|
|
204
|
+
## Roadmap
|
|
205
|
+
|
|
206
|
+
- [ ] TypeScript / Node.js SDK
|
|
207
|
+
- [x] Web dashboard with cost charts
|
|
208
|
+
- [ ] LangChain integration
|
|
209
|
+
- [ ] Publish to PyPI (`pip install ai-audit-logger`)
|
|
210
|
+
- [ ] Tamper-evident log hashing for compliance exports
|
|
211
|
+
- [ ] Slack / email alerts on cost spikes
|
|
212
|
+
|
|
213
|
+
---
|
|
214
|
+
|
|
215
|
+
## License
|
|
216
|
+
|
|
217
|
+
MIT
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
README.md
|
|
2
|
+
pyproject.toml
|
|
3
|
+
setup.py
|
|
4
|
+
ai_audit_logger/__init__.py
|
|
5
|
+
ai_audit_logger/client.py
|
|
6
|
+
ai_audit_logger/config.py
|
|
7
|
+
ai_audit_logger/wrapper.py
|
|
8
|
+
ai_audit_logger.egg-info/PKG-INFO
|
|
9
|
+
ai_audit_logger.egg-info/SOURCES.txt
|
|
10
|
+
ai_audit_logger.egg-info/dependency_links.txt
|
|
11
|
+
ai_audit_logger.egg-info/requires.txt
|
|
12
|
+
ai_audit_logger.egg-info/top_level.txt
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
ai_audit_logger
|
|
@@ -0,0 +1,38 @@
|
|
|
1
|
+
from setuptools import setup, find_packages
|
|
2
|
+
import os
|
|
3
|
+
|
|
4
|
+
try:
|
|
5
|
+
readme = open(os.path.join(os.path.dirname(__file__), "../../README.md"), encoding="utf-8").read()
|
|
6
|
+
except FileNotFoundError:
|
|
7
|
+
readme = "Drop-in audit logging for LLM API calls (OpenAI, Anthropic)"
|
|
8
|
+
|
|
9
|
+
setup(
|
|
10
|
+
name="ai-audit-logger",
|
|
11
|
+
version="0.1.0",
|
|
12
|
+
description="Drop-in audit logging for LLM API calls (OpenAI, Anthropic)",
|
|
13
|
+
long_description=readme,
|
|
14
|
+
long_description_content_type="text/markdown",
|
|
15
|
+
packages=find_packages(),
|
|
16
|
+
python_requires=">=3.9",
|
|
17
|
+
install_requires=[
|
|
18
|
+
"httpx>=0.24.0",
|
|
19
|
+
],
|
|
20
|
+
extras_require={
|
|
21
|
+
"openai": ["openai>=1.0.0"],
|
|
22
|
+
"anthropic": ["anthropic>=0.20.0"],
|
|
23
|
+
"all": ["openai>=1.0.0", "anthropic>=0.20.0"],
|
|
24
|
+
},
|
|
25
|
+
classifiers=[
|
|
26
|
+
"Programming Language :: Python :: 3",
|
|
27
|
+
"License :: OSI Approved :: MIT License",
|
|
28
|
+
"Operating System :: OS Independent",
|
|
29
|
+
"Topic :: Software Development :: Libraries :: Python Modules",
|
|
30
|
+
"Topic :: System :: Logging",
|
|
31
|
+
"Intended Audience :: Developers",
|
|
32
|
+
],
|
|
33
|
+
keywords="llm openai anthropic audit logging ai compliance observability",
|
|
34
|
+
project_urls={
|
|
35
|
+
"Source": "https://github.com/elliottj06/ai-audit-logger",
|
|
36
|
+
"Bug Tracker": "https://github.com/elliottj06/ai-audit-logger/issues",
|
|
37
|
+
},
|
|
38
|
+
)
|