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.
- tokenguard_sdk-0.1.0/PKG-INFO +129 -0
- tokenguard_sdk-0.1.0/README.md +102 -0
- tokenguard_sdk-0.1.0/pyproject.toml +40 -0
- tokenguard_sdk-0.1.0/setup.cfg +4 -0
- tokenguard_sdk-0.1.0/tokenguard/__init__.py +11 -0
- tokenguard_sdk-0.1.0/tokenguard/client.py +303 -0
- tokenguard_sdk-0.1.0/tokenguard/trace.py +155 -0
- tokenguard_sdk-0.1.0/tokenguard/transport.py +89 -0
- tokenguard_sdk-0.1.0/tokenguard_sdk.egg-info/PKG-INFO +129 -0
- tokenguard_sdk-0.1.0/tokenguard_sdk.egg-info/SOURCES.txt +11 -0
- tokenguard_sdk-0.1.0/tokenguard_sdk.egg-info/dependency_links.txt +1 -0
- tokenguard_sdk-0.1.0/tokenguard_sdk.egg-info/requires.txt +7 -0
- tokenguard_sdk-0.1.0/tokenguard_sdk.egg-info/top_level.txt +1 -0
|
@@ -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,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 @@
|
|
|
1
|
+
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
tokenguard
|