tokensor 0.1.0__tar.gz → 0.2.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {tokensor-0.1.0 → tokensor-0.2.0}/PKG-INFO +20 -2
- {tokensor-0.1.0 → tokensor-0.2.0}/README.md +19 -1
- {tokensor-0.1.0 → tokensor-0.2.0}/pyproject.toml +4 -1
- tokensor-0.2.0/src/tokensor/_cli.py +45 -0
- tokensor-0.2.0/src/tokensor/_proxy_server.py +56 -0
- tokensor-0.2.0/tests/test_cli.py +52 -0
- {tokensor-0.1.0 → tokensor-0.2.0}/.env.example +0 -0
- {tokensor-0.1.0 → tokensor-0.2.0}/.gitignore +0 -0
- {tokensor-0.1.0 → tokensor-0.2.0}/src/tokensor/__init__.py +0 -0
- {tokensor-0.1.0 → tokensor-0.2.0}/src/tokensor/_adapters/__init__.py +0 -0
- {tokensor-0.1.0 → tokensor-0.2.0}/src/tokensor/_adapters/_anthropic.py +0 -0
- {tokensor-0.1.0 → tokensor-0.2.0}/src/tokensor/_adapters/_openai.py +0 -0
- {tokensor-0.1.0 → tokensor-0.2.0}/src/tokensor/_client.py +0 -0
- {tokensor-0.1.0 → tokensor-0.2.0}/src/tokensor/_config.py +0 -0
- {tokensor-0.1.0 → tokensor-0.2.0}/src/tokensor/_hash.py +0 -0
- {tokensor-0.1.0 → tokensor-0.2.0}/src/tokensor/_models.py +0 -0
- {tokensor-0.1.0 → tokensor-0.2.0}/tests/__init__.py +0 -0
- {tokensor-0.1.0 → tokensor-0.2.0}/tests/test_anthropic.py +0 -0
- {tokensor-0.1.0 → tokensor-0.2.0}/tests/test_client.py +0 -0
- {tokensor-0.1.0 → tokensor-0.2.0}/tests/test_config.py +0 -0
- {tokensor-0.1.0 → tokensor-0.2.0}/tests/test_hash.py +0 -0
- {tokensor-0.1.0 → tokensor-0.2.0}/tests/test_models.py +0 -0
- {tokensor-0.1.0 → tokensor-0.2.0}/tests/test_openai.py +0 -0
- {tokensor-0.1.0 → tokensor-0.2.0}/tests/test_wrap.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: tokensor
|
|
3
|
-
Version: 0.
|
|
3
|
+
Version: 0.2.0
|
|
4
4
|
Summary: LLM token observability SDK — wraps OpenAI and Anthropic clients to capture metadata
|
|
5
5
|
Project-URL: Homepage, https://tokensor.com
|
|
6
6
|
Project-URL: Repository, https://github.com/tokensor/tokensor
|
|
@@ -36,7 +36,25 @@ Description-Content-Type: text/markdown
|
|
|
36
36
|
|
|
37
37
|
LLM token observability for Python. Wraps your OpenAI or Anthropic client to capture call metadata (model, tokens, cost) and forward it to your Tokensor dashboard — one line of code, zero latency overhead.
|
|
38
38
|
|
|
39
|
-
##
|
|
39
|
+
## Zero-code proxy (recommended)
|
|
40
|
+
|
|
41
|
+
Install the CLI and run a local proxy — no code changes needed:
|
|
42
|
+
|
|
43
|
+
```bash
|
|
44
|
+
pipx install tokensor
|
|
45
|
+
tokensor start --api-key $TOKENSOR_API_KEY
|
|
46
|
+
```
|
|
47
|
+
|
|
48
|
+
Then point your LLM client at the proxy:
|
|
49
|
+
|
|
50
|
+
```bash
|
|
51
|
+
export OPENAI_BASE_URL=http://localhost:8080/proxy/openai/v1
|
|
52
|
+
export ANTHROPIC_BASE_URL=http://localhost:8080/proxy/anthropic/v1
|
|
53
|
+
```
|
|
54
|
+
|
|
55
|
+
Every call is now tracked in your Tokensor dashboard.
|
|
56
|
+
|
|
57
|
+
## Install (SDK)
|
|
40
58
|
|
|
41
59
|
```bash
|
|
42
60
|
pip install tokensor
|
|
@@ -2,7 +2,25 @@
|
|
|
2
2
|
|
|
3
3
|
LLM token observability for Python. Wraps your OpenAI or Anthropic client to capture call metadata (model, tokens, cost) and forward it to your Tokensor dashboard — one line of code, zero latency overhead.
|
|
4
4
|
|
|
5
|
-
##
|
|
5
|
+
## Zero-code proxy (recommended)
|
|
6
|
+
|
|
7
|
+
Install the CLI and run a local proxy — no code changes needed:
|
|
8
|
+
|
|
9
|
+
```bash
|
|
10
|
+
pipx install tokensor
|
|
11
|
+
tokensor start --api-key $TOKENSOR_API_KEY
|
|
12
|
+
```
|
|
13
|
+
|
|
14
|
+
Then point your LLM client at the proxy:
|
|
15
|
+
|
|
16
|
+
```bash
|
|
17
|
+
export OPENAI_BASE_URL=http://localhost:8080/proxy/openai/v1
|
|
18
|
+
export ANTHROPIC_BASE_URL=http://localhost:8080/proxy/anthropic/v1
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Every call is now tracked in your Tokensor dashboard.
|
|
22
|
+
|
|
23
|
+
## Install (SDK)
|
|
6
24
|
|
|
7
25
|
```bash
|
|
8
26
|
pip install tokensor
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "tokensor"
|
|
3
|
-
version = "0.
|
|
3
|
+
version = "0.2.0"
|
|
4
4
|
description = "LLM token observability SDK — wraps OpenAI and Anthropic clients to capture metadata"
|
|
5
5
|
readme = "README.md"
|
|
6
6
|
license = { text = "MIT" }
|
|
@@ -24,6 +24,9 @@ dependencies = [
|
|
|
24
24
|
"httpx>=0.28",
|
|
25
25
|
]
|
|
26
26
|
|
|
27
|
+
[project.scripts]
|
|
28
|
+
tokensor = "tokensor._cli:main"
|
|
29
|
+
|
|
27
30
|
[project.urls]
|
|
28
31
|
Homepage = "https://tokensor.com"
|
|
29
32
|
Repository = "https://github.com/tokensor/tokensor"
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
import argparse
|
|
2
|
+
import os
|
|
3
|
+
import sys
|
|
4
|
+
|
|
5
|
+
|
|
6
|
+
def main() -> None:
|
|
7
|
+
parser = argparse.ArgumentParser(
|
|
8
|
+
prog="tokensor",
|
|
9
|
+
description="Tokensor local proxy — intercept LLM calls with zero code changes",
|
|
10
|
+
)
|
|
11
|
+
parser.add_argument("--version", action="version", version=_get_version())
|
|
12
|
+
|
|
13
|
+
subparsers = parser.add_subparsers(dest="command")
|
|
14
|
+
|
|
15
|
+
start = subparsers.add_parser("start", help="Start the local proxy")
|
|
16
|
+
start.add_argument("--port", type=int, default=8080, help="Local port (default: 8080)")
|
|
17
|
+
start.add_argument(
|
|
18
|
+
"--api-key",
|
|
19
|
+
default=os.environ.get("TOKENSOR_API_KEY"),
|
|
20
|
+
help="Tokensor API key",
|
|
21
|
+
)
|
|
22
|
+
start.add_argument(
|
|
23
|
+
"--tokensor-host",
|
|
24
|
+
default=os.environ.get("TOKENSOR_HOST", "https://api.tokensor.com"),
|
|
25
|
+
help="Tokensor backend URL",
|
|
26
|
+
)
|
|
27
|
+
|
|
28
|
+
args = parser.parse_args()
|
|
29
|
+
|
|
30
|
+
if args.command == "start":
|
|
31
|
+
if not args.api_key:
|
|
32
|
+
print("Error: --api-key or TOKENSOR_API_KEY env var required", file=sys.stderr)
|
|
33
|
+
sys.exit(1)
|
|
34
|
+
from tokensor._proxy_server import run
|
|
35
|
+
run(port=args.port, api_key=args.api_key, tokensor_host=args.tokensor_host)
|
|
36
|
+
else:
|
|
37
|
+
parser.print_help()
|
|
38
|
+
|
|
39
|
+
|
|
40
|
+
def _get_version() -> str:
|
|
41
|
+
try:
|
|
42
|
+
from importlib.metadata import version
|
|
43
|
+
return version("tokensor")
|
|
44
|
+
except Exception:
|
|
45
|
+
return "unknown"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
from __future__ import annotations
|
|
2
|
+
|
|
3
|
+
from http.server import BaseHTTPRequestHandler, HTTPServer
|
|
4
|
+
|
|
5
|
+
import httpx
|
|
6
|
+
|
|
7
|
+
|
|
8
|
+
def run(port: int, api_key: str, tokensor_host: str) -> None:
|
|
9
|
+
host = tokensor_host.rstrip("/")
|
|
10
|
+
print(f"Tokensor proxy running on http://localhost:{port}")
|
|
11
|
+
print(f" OPENAI_BASE_URL=http://localhost:{port}/proxy/openai/v1")
|
|
12
|
+
print(f" ANTHROPIC_BASE_URL=http://localhost:{port}/proxy/anthropic/v1")
|
|
13
|
+
print("Press Ctrl+C to stop.\n")
|
|
14
|
+
|
|
15
|
+
client = httpx.Client(timeout=120.0, follow_redirects=False)
|
|
16
|
+
|
|
17
|
+
class Handler(BaseHTTPRequestHandler):
|
|
18
|
+
def do_POST(self) -> None:
|
|
19
|
+
self._forward("POST")
|
|
20
|
+
|
|
21
|
+
def do_GET(self) -> None:
|
|
22
|
+
self._forward("GET")
|
|
23
|
+
|
|
24
|
+
def _forward(self, method: str) -> None:
|
|
25
|
+
length = int(self.headers.get("Content-Length", 0))
|
|
26
|
+
body = self.rfile.read(length) if length else b""
|
|
27
|
+
|
|
28
|
+
forward_headers = {
|
|
29
|
+
k: v for k, v in self.headers.items()
|
|
30
|
+
if k.lower() not in {"host", "content-length", "transfer-encoding"}
|
|
31
|
+
}
|
|
32
|
+
forward_headers["X-Tokensor-Key"] = api_key
|
|
33
|
+
|
|
34
|
+
url = f"{host}{self.path}"
|
|
35
|
+
try:
|
|
36
|
+
resp = client.request(method, url, content=body, headers=forward_headers)
|
|
37
|
+
except Exception as exc:
|
|
38
|
+
self.send_error(502, str(exc))
|
|
39
|
+
return
|
|
40
|
+
|
|
41
|
+
self.send_response(resp.status_code)
|
|
42
|
+
for k, v in resp.headers.items():
|
|
43
|
+
if k.lower() in {"transfer-encoding", "connection"}:
|
|
44
|
+
continue
|
|
45
|
+
self.send_header(k, v)
|
|
46
|
+
self.end_headers()
|
|
47
|
+
self.wfile.write(resp.content)
|
|
48
|
+
|
|
49
|
+
def log_message(self, fmt: str, *args: object) -> None:
|
|
50
|
+
print(f" {self.path} → {args[1]}")
|
|
51
|
+
|
|
52
|
+
try:
|
|
53
|
+
HTTPServer(("localhost", port), Handler).serve_forever()
|
|
54
|
+
except KeyboardInterrupt:
|
|
55
|
+
print("\nStopped.")
|
|
56
|
+
client.close()
|
|
@@ -0,0 +1,52 @@
|
|
|
1
|
+
from unittest.mock import patch
|
|
2
|
+
|
|
3
|
+
|
|
4
|
+
def test_help_exits_cleanly():
|
|
5
|
+
from tokensor._cli import main
|
|
6
|
+
with patch("sys.argv", ["tokensor", "--help"]):
|
|
7
|
+
try:
|
|
8
|
+
main()
|
|
9
|
+
except SystemExit as e:
|
|
10
|
+
assert e.code == 0
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
def test_start_requires_api_key(capsys):
|
|
14
|
+
from tokensor._cli import main
|
|
15
|
+
with patch("sys.argv", ["tokensor", "start"]):
|
|
16
|
+
try:
|
|
17
|
+
main()
|
|
18
|
+
except SystemExit as e:
|
|
19
|
+
assert e.code == 1
|
|
20
|
+
captured = capsys.readouterr()
|
|
21
|
+
assert "api-key" in captured.err
|
|
22
|
+
|
|
23
|
+
|
|
24
|
+
def test_start_reads_env_api_key(monkeypatch):
|
|
25
|
+
monkeypatch.setenv("TOKENSOR_API_KEY", "ts_test")
|
|
26
|
+
calls = []
|
|
27
|
+
with patch("tokensor._proxy_server.run", side_effect=lambda **kw: calls.append(kw)):
|
|
28
|
+
with patch("sys.argv", ["tokensor", "start"]):
|
|
29
|
+
from tokensor._cli import main
|
|
30
|
+
main()
|
|
31
|
+
assert calls[0]["api_key"] == "ts_test"
|
|
32
|
+
assert calls[0]["port"] == 8080
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def test_start_uses_custom_port(monkeypatch):
|
|
36
|
+
monkeypatch.setenv("TOKENSOR_API_KEY", "ts_test")
|
|
37
|
+
calls = []
|
|
38
|
+
with patch("tokensor._proxy_server.run", side_effect=lambda **kw: calls.append(kw)):
|
|
39
|
+
with patch("sys.argv", ["tokensor", "start", "--port", "9090"]):
|
|
40
|
+
from tokensor._cli import main
|
|
41
|
+
main()
|
|
42
|
+
assert calls[0]["port"] == 9090
|
|
43
|
+
|
|
44
|
+
|
|
45
|
+
def test_start_uses_custom_host(monkeypatch):
|
|
46
|
+
monkeypatch.setenv("TOKENSOR_API_KEY", "ts_test")
|
|
47
|
+
calls = []
|
|
48
|
+
with patch("tokensor._proxy_server.run", side_effect=lambda **kw: calls.append(kw)):
|
|
49
|
+
with patch("sys.argv", ["tokensor", "start", "--tokensor-host", "http://localhost:8000"]):
|
|
50
|
+
from tokensor._cli import main
|
|
51
|
+
main()
|
|
52
|
+
assert calls[0]["tokensor_host"] == "http://localhost:8000"
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|