tokenguard-sdk 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.
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: tokenguard-sdk
3
+ Version: 0.1.0
4
+ Summary: Monitor AI agents, track LLM costs, debug failures — TokenGuard Python SDK
5
+ Author: TokenGuard
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/acher112/TokenGuard
8
+ Project-URL: Documentation, https://tokenguard-app-two.vercel.app/docs
9
+ Keywords: ai,llm,monitoring,tracing,openai,anthropic,cost
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: requests>=2.28.0
23
+ Provides-Extra: openai
24
+ Requires-Dist: openai>=1.0.0; extra == "openai"
25
+ Provides-Extra: anthropic
26
+ Requires-Dist: anthropic>=0.18.0; extra == "anthropic"
27
+
28
+ # tokenguard (Python SDK)
29
+
30
+ **Monitor AI agents, track LLM costs, debug failures — in 2 lines of code.**
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install tokenguard
36
+ ```
37
+
38
+ ## Quick Start — Auto-wrap Groq (zero code change)
39
+
40
+ ```python
41
+ from groq import Groq
42
+ from tokenguard import TokenGuard
43
+
44
+ aw = TokenGuard(
45
+ api_key="tg_live_...", # from your dashboard Settings
46
+ base_url="http://localhost:3000", # your TokenGuard URL
47
+ )
48
+
49
+ # Wrap your existing Groq client — ONE LINE
50
+ groq = aw.wrap_groq(Groq(api_key="..."), agent_name="DietAgent")
51
+
52
+ # Use exactly as before — tracking happens automatically
53
+ response = groq.chat.completions.create(
54
+ model="llama3-8b-8192",
55
+ messages=[{"role": "user", "content": "Give me a low-carb meal plan"}],
56
+ )
57
+
58
+ print(response.choices[0].message.content)
59
+ # → Your dashboard now shows cost, tokens, latency for this call
60
+ ```
61
+
62
+ ## Quick Start — Auto-wrap OpenAI
63
+
64
+ ```python
65
+ from openai import OpenAI
66
+ from tokenguard import TokenGuard
67
+
68
+ aw = TokenGuard(api_key="tg_live_...", base_url="http://localhost:3000")
69
+
70
+ # Wrap your OpenAI client
71
+ openai = aw.wrap_openai(OpenAI(api_key="..."), agent_name="SupportBot")
72
+
73
+ response = openai.chat.completions.create(
74
+ model="gpt-4o",
75
+ messages=[{"role": "user", "content": "Hello"}],
76
+ )
77
+ ```
78
+
79
+ ## Manual Tracing (full control)
80
+
81
+ ```python
82
+ from tokenguard import TokenGuard
83
+
84
+ aw = TokenGuard(api_key="tg_live_...", base_url="http://localhost:3000")
85
+
86
+ def get_diet_plan(user_message):
87
+ with aw.trace("DietSuggestionAgent") as trace:
88
+
89
+ # Track the Groq call
90
+ llm = trace.llm(model="llama3-8b-8192", provider="groq")
91
+ response = groq_client.chat.completions.create(
92
+ model="llama3-8b-8192",
93
+ messages=[{"role": "user", "content": user_message}],
94
+ )
95
+ llm.end(
96
+ input_tokens=response.usage.prompt_tokens,
97
+ output_tokens=response.usage.completion_tokens,
98
+ )
99
+
100
+ # Track a tool call (optional)
101
+ tool = trace.tool("nutrition_database_lookup")
102
+ foods = lookup_foods(user_message)
103
+ tool.end(result=foods)
104
+
105
+ return response.choices[0].message.content
106
+ ```
107
+
108
+ ## What appears in your dashboard
109
+
110
+ Every call shows:
111
+ - 💰 **Cost** — exact cost per request
112
+ - ⏱️ **Latency** — how long it took
113
+ - 🔢 **Tokens** — input and output token counts
114
+ - 🐛 **Errors** — full error details with stack traces
115
+ - 📊 **Agent breakdown** — which agents cost the most
116
+
117
+ ## Supported providers
118
+
119
+ | Provider | Method | Notes |
120
+ |---|---|---|
121
+ | **Groq** | `aw.wrap_groq(client)` | LLaMA, Mixtral, Gemma |
122
+ | **OpenAI** | `aw.wrap_openai(client)` | GPT-4o, GPT-4o-mini |
123
+ | **Any LLM** | Manual `trace.llm()` | Works with any provider |
124
+
125
+ ## Before you exit your script
126
+
127
+ ```python
128
+ aw.flush() # makes sure all traces are sent before process ends
129
+ ```
@@ -0,0 +1,102 @@
1
+ # tokenguard (Python SDK)
2
+
3
+ **Monitor AI agents, track LLM costs, debug failures — in 2 lines of code.**
4
+
5
+ ## Install
6
+
7
+ ```bash
8
+ pip install tokenguard
9
+ ```
10
+
11
+ ## Quick Start — Auto-wrap Groq (zero code change)
12
+
13
+ ```python
14
+ from groq import Groq
15
+ from tokenguard import TokenGuard
16
+
17
+ aw = TokenGuard(
18
+ api_key="tg_live_...", # from your dashboard Settings
19
+ base_url="http://localhost:3000", # your TokenGuard URL
20
+ )
21
+
22
+ # Wrap your existing Groq client — ONE LINE
23
+ groq = aw.wrap_groq(Groq(api_key="..."), agent_name="DietAgent")
24
+
25
+ # Use exactly as before — tracking happens automatically
26
+ response = groq.chat.completions.create(
27
+ model="llama3-8b-8192",
28
+ messages=[{"role": "user", "content": "Give me a low-carb meal plan"}],
29
+ )
30
+
31
+ print(response.choices[0].message.content)
32
+ # → Your dashboard now shows cost, tokens, latency for this call
33
+ ```
34
+
35
+ ## Quick Start — Auto-wrap OpenAI
36
+
37
+ ```python
38
+ from openai import OpenAI
39
+ from tokenguard import TokenGuard
40
+
41
+ aw = TokenGuard(api_key="tg_live_...", base_url="http://localhost:3000")
42
+
43
+ # Wrap your OpenAI client
44
+ openai = aw.wrap_openai(OpenAI(api_key="..."), agent_name="SupportBot")
45
+
46
+ response = openai.chat.completions.create(
47
+ model="gpt-4o",
48
+ messages=[{"role": "user", "content": "Hello"}],
49
+ )
50
+ ```
51
+
52
+ ## Manual Tracing (full control)
53
+
54
+ ```python
55
+ from tokenguard import TokenGuard
56
+
57
+ aw = TokenGuard(api_key="tg_live_...", base_url="http://localhost:3000")
58
+
59
+ def get_diet_plan(user_message):
60
+ with aw.trace("DietSuggestionAgent") as trace:
61
+
62
+ # Track the Groq call
63
+ llm = trace.llm(model="llama3-8b-8192", provider="groq")
64
+ response = groq_client.chat.completions.create(
65
+ model="llama3-8b-8192",
66
+ messages=[{"role": "user", "content": user_message}],
67
+ )
68
+ llm.end(
69
+ input_tokens=response.usage.prompt_tokens,
70
+ output_tokens=response.usage.completion_tokens,
71
+ )
72
+
73
+ # Track a tool call (optional)
74
+ tool = trace.tool("nutrition_database_lookup")
75
+ foods = lookup_foods(user_message)
76
+ tool.end(result=foods)
77
+
78
+ return response.choices[0].message.content
79
+ ```
80
+
81
+ ## What appears in your dashboard
82
+
83
+ Every call shows:
84
+ - 💰 **Cost** — exact cost per request
85
+ - ⏱️ **Latency** — how long it took
86
+ - 🔢 **Tokens** — input and output token counts
87
+ - 🐛 **Errors** — full error details with stack traces
88
+ - 📊 **Agent breakdown** — which agents cost the most
89
+
90
+ ## Supported providers
91
+
92
+ | Provider | Method | Notes |
93
+ |---|---|---|
94
+ | **Groq** | `aw.wrap_groq(client)` | LLaMA, Mixtral, Gemma |
95
+ | **OpenAI** | `aw.wrap_openai(client)` | GPT-4o, GPT-4o-mini |
96
+ | **Any LLM** | Manual `trace.llm()` | Works with any provider |
97
+
98
+ ## Before you exit your script
99
+
100
+ ```python
101
+ aw.flush() # makes sure all traces are sent before process ends
102
+ ```
@@ -0,0 +1,40 @@
1
+ [build-system]
2
+ requires = ["setuptools>=68", "wheel"]
3
+ build-backend = "setuptools.build_meta"
4
+
5
+ [project]
6
+ name = "tokenguard-sdk"
7
+ version = "0.1.0"
8
+ description = "Monitor AI agents, track LLM costs, debug failures — TokenGuard Python SDK"
9
+ readme = "README.md"
10
+ license = { text = "MIT" }
11
+ authors = [{ name = "TokenGuard" }]
12
+ requires-python = ">=3.8"
13
+ keywords = ["ai", "llm", "monitoring", "tracing", "openai", "anthropic", "cost"]
14
+ classifiers = [
15
+ "Development Status :: 4 - Beta",
16
+ "Intended Audience :: Developers",
17
+ "License :: OSI Approved :: MIT License",
18
+ "Programming Language :: Python :: 3",
19
+ "Programming Language :: Python :: 3.8",
20
+ "Programming Language :: Python :: 3.9",
21
+ "Programming Language :: Python :: 3.10",
22
+ "Programming Language :: Python :: 3.11",
23
+ "Programming Language :: Python :: 3.12",
24
+ "Topic :: Software Development :: Libraries",
25
+ ]
26
+ dependencies = [
27
+ "requests>=2.28.0",
28
+ ]
29
+
30
+ [project.optional-dependencies]
31
+ openai = ["openai>=1.0.0"]
32
+ anthropic = ["anthropic>=0.18.0"]
33
+
34
+ [project.urls]
35
+ Homepage = "https://github.com/acher112/TokenGuard"
36
+ Documentation = "https://tokenguard-app-two.vercel.app/docs"
37
+
38
+ [tool.setuptools.packages.find]
39
+ where = ["."]
40
+ include = ["tokenguard*"]
@@ -0,0 +1,4 @@
1
+ [egg_info]
2
+ tag_build =
3
+ tag_date = 0
4
+
@@ -0,0 +1,11 @@
1
+ """
2
+ TokenGuard Python SDK
3
+ Monitor AI agents, track LLM costs, debug failures.
4
+ """
5
+
6
+ from .client import TokenGuard, wrap_openai, wrap_groq
7
+ from .trace import TraceContext
8
+
9
+ __version__ = "0.1.0"
10
+ __all__ = ["TokenGuard", "TraceContext", "wrap_openai", "wrap_groq"]
11
+
@@ -0,0 +1,303 @@
1
+ """
2
+ TokenGuard Python Client — main entry point.
3
+
4
+ Usage:
5
+ from tokenguard import TokenGuard
6
+
7
+ aw = TokenGuard(api_key="tg_live_...", base_url="http://localhost:3000")
8
+
9
+ # Option 1: Auto-wrap OpenAI
10
+ client = aw.wrap_openai(OpenAI())
11
+
12
+ # Option 2: Auto-wrap Groq
13
+ client = aw.wrap_groq(Groq())
14
+
15
+ # Option 3: Manual tracing
16
+ with aw.trace("my-agent") as trace:
17
+ llm = trace.llm(model="gpt-4o", provider="openai")
18
+ response = openai_client.chat.completions.create(...)
19
+ llm.end(
20
+ input_tokens=response.usage.prompt_tokens,
21
+ output_tokens=response.usage.completion_tokens,
22
+ )
23
+ """
24
+ from __future__ import annotations
25
+
26
+ import json
27
+ import time
28
+ from contextlib import contextmanager
29
+ from datetime import datetime, timezone
30
+ from typing import Any, Generator, Optional
31
+
32
+ from .transport import Transport
33
+ from .trace import TraceContext
34
+
35
+
36
+ def _now_iso() -> str:
37
+ """Return UTC datetime in Zod-compatible format: 2026-09-15T12:03:19.123Z"""
38
+ return datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
39
+
40
+
41
+ import os
42
+
43
+ DEFAULT_BASE_URL = os.environ.get("TOKENGUARD_BASE_URL", "https://tokenguard-app-two.vercel.app")
44
+
45
+
46
+ class TokenGuard:
47
+ """
48
+ TokenGuard Python SDK client.
49
+
50
+ Args:
51
+ api_key: Your TokenGuard API key (from dashboard or TOKENGUARD_API_KEY env var)
52
+ base_url: URL of your TokenGuard deployment (default: https://tokenguard-app-two.vercel.app)
53
+ debug: Print debug logs (default: False)
54
+ """
55
+
56
+ def __init__(
57
+ self,
58
+ api_key: Optional[str] = None,
59
+ base_url: Optional[str] = None,
60
+ debug: bool = False,
61
+ ):
62
+ resolved_key = api_key or os.environ.get("TOKENGUARD_API_KEY")
63
+ if not resolved_key:
64
+ raise ValueError(
65
+ "TokenGuard: api_key is required. Pass api_key='tg_live_...' or set TOKENGUARD_API_KEY in your environment."
66
+ )
67
+
68
+ self._api_key = resolved_key
69
+ self._base_url = (base_url or DEFAULT_BASE_URL).rstrip("/")
70
+ self._debug = debug
71
+ self._transport = Transport(
72
+ api_key=self._api_key,
73
+ base_url=self._base_url,
74
+ debug=debug,
75
+ )
76
+
77
+ # ─── Context manager trace ────────────────────────────────────────────────
78
+
79
+ @contextmanager
80
+ def trace(
81
+ self,
82
+ agent_name: str,
83
+ user_id: Optional[str] = None,
84
+ tags: Optional[list] = None,
85
+ session_id: Optional[str] = None,
86
+ ) -> Generator[TraceContext, None, None]:
87
+ """
88
+ Context manager that wraps an agent execution in a trace.
89
+
90
+ Usage:
91
+ with aw.trace("diet-agent", session_id="session-abc123") as trace:
92
+ llm = trace.llm(model="llama3-8b-8192", provider="groq")
93
+ response = groq.chat.completions.create(...)
94
+ llm.end(
95
+ input_tokens=response.usage.prompt_tokens,
96
+ output_tokens=response.usage.completion_tokens,
97
+ )
98
+ """
99
+ ctx = TraceContext()
100
+ started_at = _now_iso()
101
+ start_time = time.time()
102
+
103
+ try:
104
+ yield ctx
105
+ except Exception as e:
106
+ ctx.error(e)
107
+ ctx.status = "failed"
108
+ raise
109
+ finally:
110
+ ended_at = _now_iso()
111
+ duration_ms = int((time.time() - start_time) * 1000)
112
+
113
+ payload = {
114
+ "agentName": agent_name,
115
+ "status": ctx.status,
116
+ "startedAt": started_at,
117
+ "endedAt": ended_at,
118
+ "durationMs": duration_ms,
119
+ "steps": ctx.steps,
120
+ }
121
+
122
+ if user_id:
123
+ payload["userId"] = user_id
124
+ if tags:
125
+ payload["tags"] = tags
126
+ if session_id:
127
+ payload["sessionId"] = session_id
128
+
129
+ self._transport.enqueue(payload)
130
+
131
+
132
+ # ─── OpenAI auto-instrumentation ─────────────────────────────────────────
133
+
134
+ def wrap_openai(self, client: Any, agent_name: str = "OpenAIAgent", session_id: Optional[str] = None) -> Any:
135
+ """
136
+ Wrap an OpenAI client to automatically track all chat.completions.create calls.
137
+
138
+ Usage:
139
+ from openai import OpenAI
140
+ client = aw.wrap_openai(OpenAI(), session_id="my-session-123")
141
+ # All calls now tracked automatically
142
+ response = client.chat.completions.create(model="gpt-4o", messages=[...])
143
+ """
144
+ return _wrap_client(client, self._transport, agent_name, provider="openai", session_id=session_id)
145
+
146
+ # ─── Groq auto-instrumentation ────────────────────────────────────────────
147
+
148
+ def wrap_groq(self, client: Any, agent_name: str = "GroqAgent", session_id: Optional[str] = None) -> Any:
149
+ """
150
+ Wrap a Groq client to automatically track all chat.completions.create calls.
151
+
152
+ Usage:
153
+ from groq import Groq
154
+ client = aw.wrap_groq(Groq(), session_id="my-session-123")
155
+ # All calls now tracked automatically
156
+ response = client.chat.completions.create(model="llama3-8b-8192", messages=[...])
157
+ """
158
+ return _wrap_client(client, self._transport, agent_name, provider="groq", session_id=session_id)
159
+
160
+
161
+ # ─── Flush ────────────────────────────────────────────────────────────────
162
+
163
+ def flush(self) -> None:
164
+ """Wait for all pending traces to be sent. Call before your process exits."""
165
+ self._transport.flush()
166
+
167
+
168
+ # ─── Client wrapper (works for both OpenAI and Groq) ─────────────────────────
169
+
170
+ class _WrappedCompletions:
171
+ """Wraps the chat.completions object to intercept .create() calls."""
172
+
173
+ def __init__(self, original_completions: Any, transport: Transport, agent_name: str, provider: str, session_id: Optional[str] = None):
174
+ self._completions = original_completions
175
+ self._transport = transport
176
+ self._agent_name = agent_name
177
+ self._provider = provider
178
+ self._session_id = session_id
179
+
180
+ def create(self, **kwargs: Any) -> Any:
181
+ started_at = _now_iso()
182
+ start_time = time.time()
183
+ model = kwargs.get("model", "unknown")
184
+
185
+ try:
186
+ response = self._completions.create(**kwargs)
187
+ ended_at = _now_iso()
188
+ duration_ms = int((time.time() - start_time) * 1000)
189
+
190
+ # Extract token usage
191
+ input_tokens = 0
192
+ output_tokens = 0
193
+ if hasattr(response, "usage") and response.usage:
194
+ input_tokens = getattr(response.usage, "prompt_tokens", 0) or 0
195
+ output_tokens = getattr(response.usage, "completion_tokens", 0) or 0
196
+
197
+ # Extract request / response content
198
+ request_json = None
199
+ response_json = None
200
+ try:
201
+ if "messages" in kwargs:
202
+ request_json = json.dumps(kwargs.get("messages", []))
203
+ if hasattr(response, "choices") and response.choices:
204
+ first_choice = response.choices[0]
205
+ if hasattr(first_choice, "message") and hasattr(first_choice.message, "content"):
206
+ response_json = first_choice.message.content
207
+ except Exception:
208
+ pass
209
+
210
+ payload = {
211
+ "agentName": self._agent_name,
212
+ "status": "success",
213
+ "startedAt": started_at,
214
+ "endedAt": ended_at,
215
+ "durationMs": duration_ms,
216
+ "steps": [{
217
+ "stepType": "llm",
218
+ "sequence": 1,
219
+ "name": f"{self._provider}/{model}",
220
+ "startedAt": started_at,
221
+ "endedAt": ended_at,
222
+ "durationMs": duration_ms,
223
+ "llmCall": {
224
+ "modelName": model,
225
+ "provider": self._provider,
226
+ "inputTokens": input_tokens,
227
+ "outputTokens": output_tokens,
228
+ "requestJson": request_json,
229
+ "responseJson": response_json,
230
+ },
231
+ }],
232
+ }
233
+
234
+ if self._session_id:
235
+ payload["sessionId"] = self._session_id
236
+
237
+ self._transport.enqueue(payload)
238
+ return response
239
+
240
+ except Exception as e:
241
+ ended_at = _now_iso()
242
+ duration_ms = int((time.time() - start_time) * 1000)
243
+
244
+ payload = {
245
+ "agentName": self._agent_name,
246
+ "status": "failed",
247
+ "startedAt": started_at,
248
+ "endedAt": ended_at,
249
+ "durationMs": duration_ms,
250
+ "steps": [{
251
+ "stepType": "error",
252
+ "sequence": 1,
253
+ "name": "LLM call failed",
254
+ "startedAt": started_at,
255
+ "endedAt": ended_at,
256
+ "durationMs": duration_ms,
257
+ "error": {
258
+ "errorType": type(e).__name__,
259
+ "message": str(e),
260
+ },
261
+ }],
262
+ }
263
+
264
+ if self._session_id:
265
+ payload["sessionId"] = self._session_id
266
+
267
+ self._transport.enqueue(payload)
268
+ raise
269
+
270
+
271
+ class _WrappedChat:
272
+ def __init__(self, original_chat: Any, transport: Transport, agent_name: str, provider: str, session_id: Optional[str] = None):
273
+ self.completions = _WrappedCompletions(
274
+ original_chat.completions, transport, agent_name, provider, session_id=session_id
275
+ )
276
+
277
+
278
+ class _WrappedClient:
279
+ """Proxy client that intercepts chat.completions.create calls."""
280
+
281
+ def __init__(self, original_client: Any, transport: Transport, agent_name: str, provider: str, session_id: Optional[str] = None):
282
+ self._original = original_client
283
+ self.chat = _WrappedChat(original_client.chat, transport, agent_name, provider, session_id=session_id)
284
+
285
+ def __getattr__(self, name: str) -> Any:
286
+ return getattr(self._original, name)
287
+
288
+
289
+ def _wrap_client(client: Any, transport: Transport, agent_name: str, provider: str, session_id: Optional[str] = None) -> Any:
290
+ return _WrappedClient(client, transport, agent_name, provider, session_id=session_id)
291
+
292
+
293
+ def wrap_openai(client: Any, tg: Optional[TokenGuard] = None, agent_name: str = "OpenAIAgent", session_id: Optional[str] = None) -> Any:
294
+ """Convenience function to wrap an OpenAI client."""
295
+ guard = tg if tg is not None else TokenGuard()
296
+ return guard.wrap_openai(client, agent_name=agent_name, session_id=session_id)
297
+
298
+
299
+ def wrap_groq(client: Any, tg: Optional[TokenGuard] = None, agent_name: str = "GroqAgent", session_id: Optional[str] = None) -> Any:
300
+ """Convenience function to wrap a Groq client."""
301
+ guard = tg if tg is not None else TokenGuard()
302
+ return guard.wrap_groq(client, agent_name=agent_name, session_id=session_id)
303
+
@@ -0,0 +1,155 @@
1
+ """
2
+ TraceContext — holds steps for a single agent trace execution.
3
+ Used as a context manager or directly.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import time
8
+ from contextlib import contextmanager
9
+ from datetime import datetime, timezone
10
+ from typing import Any, Dict, List, Optional
11
+
12
+
13
+ def _now_iso() -> str:
14
+ """Return UTC datetime in Zod-compatible format: 2026-09-15T12:03:19.123Z"""
15
+ return datetime.now(tz=timezone.utc).strftime("%Y-%m-%dT%H:%M:%S.%f")[:-3] + "Z"
16
+
17
+
18
+ class LlmSpan:
19
+ """Tracks a single LLM call within a trace."""
20
+
21
+ def __init__(self, model: str, provider: str, steps: List[Dict], sequence: int):
22
+ self._model = model
23
+ self._provider = provider
24
+ self._steps = steps
25
+ self._sequence = sequence
26
+ self._started_at = _now_iso()
27
+ self._start_time = time.time()
28
+
29
+ def end(
30
+ self,
31
+ input_tokens: int,
32
+ output_tokens: int,
33
+ response: Any = None,
34
+ ) -> None:
35
+ duration_ms = int((time.time() - self._start_time) * 1000)
36
+ self._steps.append({
37
+ "stepType": "llm",
38
+ "sequence": self._sequence,
39
+ "name": f"{self._provider}/{self._model}",
40
+ "startedAt": self._started_at,
41
+ "endedAt": _now_iso(),
42
+ "durationMs": duration_ms,
43
+ "llmCall": {
44
+ "modelName": self._model,
45
+ "provider": self._provider,
46
+ "inputTokens": input_tokens,
47
+ "outputTokens": output_tokens,
48
+ },
49
+ })
50
+
51
+ def fail(self, error: Exception) -> None:
52
+ duration_ms = int((time.time() - self._start_time) * 1000)
53
+ self._steps.append({
54
+ "stepType": "error",
55
+ "sequence": self._sequence,
56
+ "name": "LLM call failed",
57
+ "startedAt": self._started_at,
58
+ "endedAt": _now_iso(),
59
+ "durationMs": duration_ms,
60
+ "error": {
61
+ "errorType": type(error).__name__,
62
+ "message": str(error),
63
+ },
64
+ })
65
+
66
+
67
+ class ToolSpan:
68
+ """Tracks a single tool/function call within a trace."""
69
+
70
+ def __init__(self, name: str, steps: List[Dict], sequence: int):
71
+ self._name = name
72
+ self._steps = steps
73
+ self._sequence = sequence
74
+ self._started_at = _now_iso()
75
+ self._start_time = time.time()
76
+
77
+ def end(self, result: Any = None) -> None:
78
+ duration_ms = int((time.time() - self._start_time) * 1000)
79
+ self._steps.append({
80
+ "stepType": "tool",
81
+ "sequence": self._sequence,
82
+ "name": self._name,
83
+ "startedAt": self._started_at,
84
+ "endedAt": _now_iso(),
85
+ "durationMs": duration_ms,
86
+ "toolCall": {
87
+ "toolName": self._name,
88
+ "status": "success",
89
+ "resultJson": str(result) if result is not None else None,
90
+ },
91
+ })
92
+
93
+ def fail(self, error: Exception) -> None:
94
+ duration_ms = int((time.time() - self._start_time) * 1000)
95
+ self._steps.append({
96
+ "stepType": "error",
97
+ "sequence": self._sequence,
98
+ "name": f"Tool failed: {self._name}",
99
+ "startedAt": self._started_at,
100
+ "endedAt": _now_iso(),
101
+ "durationMs": duration_ms,
102
+ "error": {
103
+ "errorType": type(error).__name__,
104
+ "message": str(error),
105
+ },
106
+ })
107
+
108
+
109
+ class TraceContext:
110
+ """
111
+ Active trace context. Add LLM calls, tool calls, and errors to it.
112
+
113
+ Usage:
114
+ with aw.trace("my-agent") as trace:
115
+ llm = trace.llm(model="gpt-4o", provider="openai")
116
+ response = client.chat.completions.create(...)
117
+ llm.end(input_tokens=100, output_tokens=50)
118
+ """
119
+
120
+ def __init__(self):
121
+ self._steps: List[Dict] = []
122
+ self._sequence = 0
123
+ self.status = "success"
124
+
125
+ def llm(self, model: str, provider: str = "openai") -> LlmSpan:
126
+ """Start tracking an LLM call."""
127
+ self._sequence += 1
128
+ return LlmSpan(model=model, provider=provider, steps=self._steps, sequence=self._sequence)
129
+
130
+ def tool(self, name: str) -> ToolSpan:
131
+ """Start tracking a tool/function call."""
132
+ self._sequence += 1
133
+ return ToolSpan(name=name, steps=self._steps, sequence=self._sequence)
134
+
135
+ def error(self, error: Exception, wasted_cost_usd: float = 0.0) -> None:
136
+ """Record an error that occurred during the trace."""
137
+ self._sequence += 1
138
+ self._steps.append({
139
+ "stepType": "error",
140
+ "sequence": self._sequence,
141
+ "name": type(error).__name__,
142
+ "startedAt": _now_iso(),
143
+ "endedAt": _now_iso(),
144
+ "durationMs": 0,
145
+ "error": {
146
+ "errorType": type(error).__name__,
147
+ "message": str(error),
148
+ "wastedCostUsd": str(wasted_cost_usd),
149
+ },
150
+ })
151
+ self.status = "failed"
152
+
153
+ @property
154
+ def steps(self) -> List[Dict]:
155
+ return self._steps
@@ -0,0 +1,89 @@
1
+ """
2
+ Transport — sends trace payloads to the TokenGuard ingest API.
3
+ Handles retries, timeouts, and never crashes the host application.
4
+ """
5
+ from __future__ import annotations
6
+
7
+ import json
8
+ import time
9
+ import threading
10
+ from typing import Any, Dict, List, Optional
11
+ import requests
12
+
13
+
14
+ class Transport:
15
+ """
16
+ Thread-safe HTTP transport with buffered queue and retry logic.
17
+ Sends traces in a background thread so it never blocks the app.
18
+ """
19
+
20
+ def __init__(
21
+ self,
22
+ api_key: str,
23
+ base_url: str,
24
+ max_retries: int = 3,
25
+ retry_delay: float = 0.5,
26
+ timeout: float = 10.0,
27
+ debug: bool = False,
28
+ ):
29
+ self.api_key = api_key
30
+ self.ingest_url = base_url.rstrip("/") + "/api/v1/ingest"
31
+ self.max_retries = max_retries
32
+ self.retry_delay = retry_delay
33
+ self.timeout = timeout
34
+ self.debug = debug
35
+
36
+ self._queue: List[Dict[str, Any]] = []
37
+ self._lock = threading.Lock()
38
+
39
+ def enqueue(self, payload: Dict[str, Any]) -> None:
40
+ """Add a trace payload to the send queue and dispatch immediately."""
41
+ with self._lock:
42
+ self._queue.append(payload)
43
+
44
+ # Send in background thread so it never blocks the caller
45
+ thread = threading.Thread(target=self._send, args=(payload,), daemon=True)
46
+ thread.start()
47
+
48
+ def _send(self, payload: Dict[str, Any]) -> None:
49
+ """Send a single payload with retries."""
50
+ headers = {
51
+ "Authorization": f"Bearer {self.api_key}",
52
+ "Content-Type": "application/json",
53
+ }
54
+
55
+ for attempt in range(self.max_retries):
56
+ try:
57
+ response = requests.post(
58
+ self.ingest_url,
59
+ json=payload,
60
+ headers=headers,
61
+ timeout=self.timeout,
62
+ )
63
+
64
+ if response.status_code == 201:
65
+ if self.debug:
66
+ print(f"[TokenGuard] Trace sent: {response.json().get('traceId')}")
67
+ return
68
+
69
+ # 4xx errors — don't retry
70
+ if 400 <= response.status_code < 500:
71
+ if self.debug:
72
+ print(f"[TokenGuard] Client error {response.status_code}: {response.text}")
73
+ return
74
+
75
+ # 5xx — retry
76
+ if self.debug:
77
+ print(f"[TokenGuard] Server error {response.status_code}, retrying ({attempt + 1}/{self.max_retries})")
78
+
79
+ except requests.exceptions.RequestException as e:
80
+ if self.debug:
81
+ print(f"[TokenGuard] Request failed: {e}, retrying ({attempt + 1}/{self.max_retries})")
82
+
83
+ if attempt < self.max_retries - 1:
84
+ time.sleep(self.retry_delay * (2 ** attempt))
85
+
86
+ def flush(self) -> None:
87
+ """Wait for all background threads to complete (best-effort)."""
88
+ # Give background threads up to 5 seconds to finish
89
+ time.sleep(0.5)
@@ -0,0 +1,129 @@
1
+ Metadata-Version: 2.4
2
+ Name: tokenguard-sdk
3
+ Version: 0.1.0
4
+ Summary: Monitor AI agents, track LLM costs, debug failures — TokenGuard Python SDK
5
+ Author: TokenGuard
6
+ License: MIT
7
+ Project-URL: Homepage, https://github.com/acher112/TokenGuard
8
+ Project-URL: Documentation, https://tokenguard-app-two.vercel.app/docs
9
+ Keywords: ai,llm,monitoring,tracing,openai,anthropic,cost
10
+ Classifier: Development Status :: 4 - Beta
11
+ Classifier: Intended Audience :: Developers
12
+ Classifier: License :: OSI Approved :: MIT License
13
+ Classifier: Programming Language :: Python :: 3
14
+ Classifier: Programming Language :: Python :: 3.8
15
+ Classifier: Programming Language :: Python :: 3.9
16
+ Classifier: Programming Language :: Python :: 3.10
17
+ Classifier: Programming Language :: Python :: 3.11
18
+ Classifier: Programming Language :: Python :: 3.12
19
+ Classifier: Topic :: Software Development :: Libraries
20
+ Requires-Python: >=3.8
21
+ Description-Content-Type: text/markdown
22
+ Requires-Dist: requests>=2.28.0
23
+ Provides-Extra: openai
24
+ Requires-Dist: openai>=1.0.0; extra == "openai"
25
+ Provides-Extra: anthropic
26
+ Requires-Dist: anthropic>=0.18.0; extra == "anthropic"
27
+
28
+ # tokenguard (Python SDK)
29
+
30
+ **Monitor AI agents, track LLM costs, debug failures — in 2 lines of code.**
31
+
32
+ ## Install
33
+
34
+ ```bash
35
+ pip install tokenguard
36
+ ```
37
+
38
+ ## Quick Start — Auto-wrap Groq (zero code change)
39
+
40
+ ```python
41
+ from groq import Groq
42
+ from tokenguard import TokenGuard
43
+
44
+ aw = TokenGuard(
45
+ api_key="tg_live_...", # from your dashboard Settings
46
+ base_url="http://localhost:3000", # your TokenGuard URL
47
+ )
48
+
49
+ # Wrap your existing Groq client — ONE LINE
50
+ groq = aw.wrap_groq(Groq(api_key="..."), agent_name="DietAgent")
51
+
52
+ # Use exactly as before — tracking happens automatically
53
+ response = groq.chat.completions.create(
54
+ model="llama3-8b-8192",
55
+ messages=[{"role": "user", "content": "Give me a low-carb meal plan"}],
56
+ )
57
+
58
+ print(response.choices[0].message.content)
59
+ # → Your dashboard now shows cost, tokens, latency for this call
60
+ ```
61
+
62
+ ## Quick Start — Auto-wrap OpenAI
63
+
64
+ ```python
65
+ from openai import OpenAI
66
+ from tokenguard import TokenGuard
67
+
68
+ aw = TokenGuard(api_key="tg_live_...", base_url="http://localhost:3000")
69
+
70
+ # Wrap your OpenAI client
71
+ openai = aw.wrap_openai(OpenAI(api_key="..."), agent_name="SupportBot")
72
+
73
+ response = openai.chat.completions.create(
74
+ model="gpt-4o",
75
+ messages=[{"role": "user", "content": "Hello"}],
76
+ )
77
+ ```
78
+
79
+ ## Manual Tracing (full control)
80
+
81
+ ```python
82
+ from tokenguard import TokenGuard
83
+
84
+ aw = TokenGuard(api_key="tg_live_...", base_url="http://localhost:3000")
85
+
86
+ def get_diet_plan(user_message):
87
+ with aw.trace("DietSuggestionAgent") as trace:
88
+
89
+ # Track the Groq call
90
+ llm = trace.llm(model="llama3-8b-8192", provider="groq")
91
+ response = groq_client.chat.completions.create(
92
+ model="llama3-8b-8192",
93
+ messages=[{"role": "user", "content": user_message}],
94
+ )
95
+ llm.end(
96
+ input_tokens=response.usage.prompt_tokens,
97
+ output_tokens=response.usage.completion_tokens,
98
+ )
99
+
100
+ # Track a tool call (optional)
101
+ tool = trace.tool("nutrition_database_lookup")
102
+ foods = lookup_foods(user_message)
103
+ tool.end(result=foods)
104
+
105
+ return response.choices[0].message.content
106
+ ```
107
+
108
+ ## What appears in your dashboard
109
+
110
+ Every call shows:
111
+ - 💰 **Cost** — exact cost per request
112
+ - ⏱️ **Latency** — how long it took
113
+ - 🔢 **Tokens** — input and output token counts
114
+ - 🐛 **Errors** — full error details with stack traces
115
+ - 📊 **Agent breakdown** — which agents cost the most
116
+
117
+ ## Supported providers
118
+
119
+ | Provider | Method | Notes |
120
+ |---|---|---|
121
+ | **Groq** | `aw.wrap_groq(client)` | LLaMA, Mixtral, Gemma |
122
+ | **OpenAI** | `aw.wrap_openai(client)` | GPT-4o, GPT-4o-mini |
123
+ | **Any LLM** | Manual `trace.llm()` | Works with any provider |
124
+
125
+ ## Before you exit your script
126
+
127
+ ```python
128
+ aw.flush() # makes sure all traces are sent before process ends
129
+ ```
@@ -0,0 +1,11 @@
1
+ README.md
2
+ pyproject.toml
3
+ tokenguard/__init__.py
4
+ tokenguard/client.py
5
+ tokenguard/trace.py
6
+ tokenguard/transport.py
7
+ tokenguard_sdk.egg-info/PKG-INFO
8
+ tokenguard_sdk.egg-info/SOURCES.txt
9
+ tokenguard_sdk.egg-info/dependency_links.txt
10
+ tokenguard_sdk.egg-info/requires.txt
11
+ tokenguard_sdk.egg-info/top_level.txt
@@ -0,0 +1,7 @@
1
+ requests>=2.28.0
2
+
3
+ [anthropic]
4
+ anthropic>=0.18.0
5
+
6
+ [openai]
7
+ openai>=1.0.0
@@ -0,0 +1 @@
1
+ tokenguard