tokensor 0.1.0__py3-none-any.whl

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/__init__.py ADDED
@@ -0,0 +1,72 @@
1
+ from __future__ import annotations
2
+ from typing import Any
3
+
4
+ from tokensor._config import load_config
5
+ from tokensor._client import TokensorClient
6
+ from tokensor._adapters._openai import WrappedOpenAI, WrappedAsyncOpenAI
7
+ from tokensor._adapters._anthropic import WrappedAnthropic, WrappedAsyncAnthropic
8
+
9
+ _OPENAI_NAMES = {"OpenAI", "AzureOpenAI"}
10
+ _ASYNC_OPENAI_NAMES = {"AsyncOpenAI", "AsyncAzureOpenAI"}
11
+ _ANTHROPIC_NAMES = {"Anthropic"}
12
+ _ASYNC_ANTHROPIC_NAMES = {"AsyncAnthropic"}
13
+
14
+
15
+ def wrap(
16
+ client: Any,
17
+ *,
18
+ api_key: str | None = None,
19
+ host: str | None = None,
20
+ provider: str | None = None,
21
+ _async: bool = False,
22
+ ) -> Any:
23
+ """Wrap an OpenAI or Anthropic client to capture LLM call metadata.
24
+
25
+ Args:
26
+ client: An openai.OpenAI, openai.AsyncOpenAI, anthropic.Anthropic,
27
+ or anthropic.AsyncAnthropic instance.
28
+ api_key: Tokensor API key. Falls back to TOKENSOR_API_KEY env var.
29
+ host: Tokensor API host. Falls back to TOKENSOR_HOST env var or default.
30
+ provider: "openai" or "anthropic". Auto-detected from class name if omitted.
31
+ _async: Force async wrapper. Usually auto-detected from class name.
32
+
33
+ Returns:
34
+ A wrapped client with the same interface as the original.
35
+ """
36
+ config = load_config(api_key=api_key, host=host)
37
+ tokensor_client = TokensorClient(config)
38
+
39
+ class_name = type(client).__name__
40
+
41
+ resolved_provider = provider
42
+ resolved_async = _async
43
+
44
+ if resolved_provider is None:
45
+ if class_name in _OPENAI_NAMES:
46
+ resolved_provider = "openai"
47
+ elif class_name in _ASYNC_OPENAI_NAMES:
48
+ resolved_provider = "openai"
49
+ resolved_async = True
50
+ elif class_name in _ANTHROPIC_NAMES:
51
+ resolved_provider = "anthropic"
52
+ elif class_name in _ASYNC_ANTHROPIC_NAMES:
53
+ resolved_provider = "anthropic"
54
+ resolved_async = True
55
+ else:
56
+ tokensor_client.shutdown()
57
+ raise ValueError(
58
+ f"Cannot detect provider from class '{class_name}'. "
59
+ "Pass provider='openai' or provider='anthropic' explicitly."
60
+ )
61
+
62
+ if resolved_provider == "openai":
63
+ if resolved_async:
64
+ return WrappedAsyncOpenAI(client, tokensor_client)
65
+ return WrappedOpenAI(client, tokensor_client)
66
+ elif resolved_provider == "anthropic":
67
+ if resolved_async:
68
+ return WrappedAsyncAnthropic(client, tokensor_client)
69
+ return WrappedAnthropic(client, tokensor_client)
70
+ else:
71
+ tokensor_client.shutdown()
72
+ raise ValueError(f"Unknown provider '{resolved_provider}'. Use 'openai' or 'anthropic'.")
File without changes
@@ -0,0 +1,120 @@
1
+ from __future__ import annotations
2
+ import time
3
+ from datetime import datetime, timezone
4
+ from typing import Any
5
+
6
+ from tokensor._client import TokensorClient
7
+ from tokensor._hash import endpoint_hash
8
+ from tokensor._models import EventPayload
9
+
10
+
11
+ def _record_message(
12
+ tokensor_client: TokensorClient,
13
+ h: str,
14
+ model: str,
15
+ messages: list | None,
16
+ max_tokens: int | None,
17
+ called_at: datetime,
18
+ start: float,
19
+ response: Any | None,
20
+ exc: Exception | None,
21
+ ) -> None:
22
+ if response is not None:
23
+ status_code: int | None = 200
24
+ input_tokens = getattr(response.usage, "input_tokens", 0) or 0
25
+ output_tokens = getattr(response.usage, "output_tokens", 0) or 0
26
+ cache_creation_tokens = getattr(response.usage, "cache_creation_input_tokens", 0) or 0
27
+ cache_read_tokens = getattr(response.usage, "cache_read_input_tokens", 0) or 0
28
+ finish_reason = getattr(response, "stop_reason", None)
29
+ else:
30
+ status_code = getattr(exc, "status_code", None)
31
+ input_tokens = 0
32
+ output_tokens = 0
33
+ cache_creation_tokens = 0
34
+ cache_read_tokens = 0
35
+ finish_reason = None
36
+
37
+ latency_ms = int((time.monotonic() - start) * 1000)
38
+ event = EventPayload(
39
+ called_at=called_at,
40
+ model=model,
41
+ endpoint_hash=h,
42
+ input_tokens=input_tokens,
43
+ output_tokens=output_tokens,
44
+ cache_creation_tokens=cache_creation_tokens,
45
+ cache_read_tokens=cache_read_tokens,
46
+ message_count=len(messages) if messages is not None else None,
47
+ max_tokens=max_tokens,
48
+ finish_reason=finish_reason,
49
+ latency_ms=latency_ms,
50
+ status_code=status_code,
51
+ )
52
+ tokensor_client.enqueue(event)
53
+
54
+
55
+ class _Messages:
56
+ def __init__(self, original_messages, tokensor_client: TokensorClient) -> None:
57
+ self._messages = original_messages
58
+ self._tokensor = tokensor_client
59
+
60
+ def create(self, model: str, messages: list | None = None, max_tokens: int | None = None, **kwargs) -> Any:
61
+ h = endpoint_hash(depth=2)
62
+ start = time.monotonic()
63
+ called_at = datetime.now(timezone.utc)
64
+ response, exc_to_raise = None, None
65
+ try:
66
+ response = self._messages.create(
67
+ model=model, messages=messages, max_tokens=max_tokens, **kwargs
68
+ )
69
+ except Exception as e:
70
+ exc_to_raise = e
71
+ _record_message(self._tokensor, h, model, messages, max_tokens, called_at, start, response, exc_to_raise)
72
+ if exc_to_raise is not None:
73
+ raise exc_to_raise
74
+ return response
75
+
76
+
77
+ class _AsyncMessages:
78
+ def __init__(self, original_messages, tokensor_client: TokensorClient) -> None:
79
+ self._messages = original_messages
80
+ self._tokensor = tokensor_client
81
+
82
+ async def create(self, model: str, messages: list | None = None, max_tokens: int | None = None, **kwargs) -> Any:
83
+ h = endpoint_hash(depth=2)
84
+ start = time.monotonic()
85
+ called_at = datetime.now(timezone.utc)
86
+ response, exc_to_raise = None, None
87
+ try:
88
+ response = await self._messages.create(
89
+ model=model, messages=messages, max_tokens=max_tokens, **kwargs
90
+ )
91
+ except Exception as e:
92
+ exc_to_raise = e
93
+ _record_message(self._tokensor, h, model, messages, max_tokens, called_at, start, response, exc_to_raise)
94
+ if exc_to_raise is not None:
95
+ raise exc_to_raise
96
+ return response
97
+
98
+
99
+ class WrappedAnthropic:
100
+ """Wraps anthropic.Anthropic to capture call metadata."""
101
+
102
+ def __init__(self, client: Any, tokensor_client: TokensorClient) -> None:
103
+ self._client = client
104
+ self._tokensor = tokensor_client
105
+ self.messages = _Messages(client.messages, tokensor_client)
106
+
107
+ def __getattr__(self, name: str) -> Any:
108
+ return getattr(self._client, name)
109
+
110
+
111
+ class WrappedAsyncAnthropic:
112
+ """Wraps anthropic.AsyncAnthropic to capture call metadata."""
113
+
114
+ def __init__(self, client: Any, tokensor_client: TokensorClient) -> None:
115
+ self._client = client
116
+ self._tokensor = tokensor_client
117
+ self.messages = _AsyncMessages(client.messages, tokensor_client)
118
+
119
+ def __getattr__(self, name: str) -> Any:
120
+ return getattr(self._client, name)
@@ -0,0 +1,124 @@
1
+ from __future__ import annotations
2
+ import time
3
+ from datetime import datetime, timezone
4
+ from typing import Any
5
+
6
+ from tokensor._client import TokensorClient
7
+ from tokensor._hash import endpoint_hash
8
+ from tokensor._models import EventPayload
9
+
10
+
11
+ def _record_completion(
12
+ tokensor_client: TokensorClient,
13
+ h: str,
14
+ model: str,
15
+ messages: list | None,
16
+ max_tokens: int | None,
17
+ called_at: datetime,
18
+ start: float,
19
+ response: Any | None,
20
+ exc: Exception | None,
21
+ ) -> None:
22
+ if response is not None:
23
+ status_code: int | None = 200
24
+ input_tokens = getattr(response.usage, "prompt_tokens", 0) or 0
25
+ output_tokens = getattr(response.usage, "completion_tokens", 0) or 0
26
+ finish_reason = (
27
+ getattr(response.choices[0], "finish_reason", None) if response.choices else None
28
+ )
29
+ else:
30
+ status_code = getattr(exc, "status_code", None)
31
+ input_tokens = 0
32
+ output_tokens = 0
33
+ finish_reason = None
34
+
35
+ latency_ms = int((time.monotonic() - start) * 1000)
36
+ event = EventPayload(
37
+ called_at=called_at,
38
+ model=model,
39
+ endpoint_hash=h,
40
+ input_tokens=input_tokens,
41
+ output_tokens=output_tokens,
42
+ message_count=len(messages) if messages is not None else None,
43
+ max_tokens=max_tokens,
44
+ finish_reason=finish_reason,
45
+ latency_ms=latency_ms,
46
+ status_code=status_code,
47
+ )
48
+ tokensor_client.enqueue(event)
49
+
50
+
51
+ class _ChatCompletions:
52
+ def __init__(self, original_chat, tokensor_client: TokensorClient) -> None:
53
+ self._chat = original_chat
54
+ self._tokensor = tokensor_client
55
+
56
+ @property
57
+ def completions(self):
58
+ return self
59
+
60
+ def create(self, model: str, messages: list | None = None, max_tokens: int | None = None, **kwargs) -> Any:
61
+ h = endpoint_hash(depth=2)
62
+ start = time.monotonic()
63
+ called_at = datetime.now(timezone.utc)
64
+ response, exc_to_raise = None, None
65
+ try:
66
+ response = self._chat.completions.create(
67
+ model=model, messages=messages, max_tokens=max_tokens, **kwargs
68
+ )
69
+ except Exception as e:
70
+ exc_to_raise = e
71
+ _record_completion(self._tokensor, h, model, messages, max_tokens, called_at, start, response, exc_to_raise)
72
+ if exc_to_raise is not None:
73
+ raise exc_to_raise
74
+ return response
75
+
76
+
77
+ class _AsyncChatCompletions:
78
+ def __init__(self, original_chat, tokensor_client: TokensorClient) -> None:
79
+ self._chat = original_chat
80
+ self._tokensor = tokensor_client
81
+
82
+ @property
83
+ def completions(self):
84
+ return self
85
+
86
+ async def create(self, model: str, messages: list | None = None, max_tokens: int | None = None, **kwargs) -> Any:
87
+ h = endpoint_hash(depth=2)
88
+ start = time.monotonic()
89
+ called_at = datetime.now(timezone.utc)
90
+ response, exc_to_raise = None, None
91
+ try:
92
+ response = await self._chat.completions.create(
93
+ model=model, messages=messages, max_tokens=max_tokens, **kwargs
94
+ )
95
+ except Exception as e:
96
+ exc_to_raise = e
97
+ _record_completion(self._tokensor, h, model, messages, max_tokens, called_at, start, response, exc_to_raise)
98
+ if exc_to_raise is not None:
99
+ raise exc_to_raise
100
+ return response
101
+
102
+
103
+ class WrappedOpenAI:
104
+ """Wraps openai.OpenAI to capture call metadata."""
105
+
106
+ def __init__(self, client: Any, tokensor_client: TokensorClient) -> None:
107
+ self._client = client
108
+ self._tokensor = tokensor_client
109
+ self.chat = _ChatCompletions(client.chat, tokensor_client)
110
+
111
+ def __getattr__(self, name: str) -> Any:
112
+ return getattr(self._client, name)
113
+
114
+
115
+ class WrappedAsyncOpenAI:
116
+ """Wraps openai.AsyncOpenAI to capture call metadata."""
117
+
118
+ def __init__(self, client: Any, tokensor_client: TokensorClient) -> None:
119
+ self._client = client
120
+ self._tokensor = tokensor_client
121
+ self.chat = _AsyncChatCompletions(client.chat, tokensor_client)
122
+
123
+ def __getattr__(self, name: str) -> Any:
124
+ return getattr(self._client, name)
tokensor/_client.py ADDED
@@ -0,0 +1,131 @@
1
+ from __future__ import annotations
2
+ import functools
3
+ import logging
4
+ import queue
5
+ import threading
6
+ import time
7
+ from typing import Callable, TypeVar
8
+
9
+ import httpx
10
+
11
+ from tokensor._config import TokensorConfig
12
+ from tokensor._models import EventPayload, events_to_batch_payload
13
+
14
+ logger = logging.getLogger("tokensor")
15
+
16
+ _SENTINEL = object()
17
+ _MAX_RETRIES = 3
18
+ _RETRY_DELAYS = (0.5, 1.0, 2.0)
19
+
20
+ F = TypeVar("F", bound=Callable)
21
+
22
+
23
+ def _retryable(max_retries: int = _MAX_RETRIES, delays: tuple = _RETRY_DELAYS) -> Callable[[F], F]:
24
+ """Decorator: retry on httpx errors or 5xx responses; discard permanently on 4xx."""
25
+ def decorator(fn: F) -> F:
26
+ @functools.wraps(fn)
27
+ def wrapper(self, events: list[EventPayload]) -> None:
28
+ for attempt in range(max_retries):
29
+ try:
30
+ result = fn(self, events)
31
+ if result is not None and result < 500:
32
+ return # success or permanent 4xx (already logged inside fn)
33
+ if result is not None:
34
+ # 5xx — fall through to retry
35
+ logger.warning(
36
+ "Tokensor: server error %d for %d events (attempt %d/%d)",
37
+ result, len(events), attempt + 1, max_retries,
38
+ )
39
+ except Exception as exc:
40
+ logger.warning(
41
+ "Tokensor: network error sending %d events (attempt %d/%d): %s",
42
+ len(events), attempt + 1, max_retries, exc,
43
+ )
44
+
45
+ if attempt < max_retries - 1:
46
+ time.sleep(delays[attempt])
47
+
48
+ logger.error(
49
+ "Tokensor: permanently dropping %d events after %d retries",
50
+ len(events), max_retries,
51
+ )
52
+ return wrapper # type: ignore[return-value]
53
+ return decorator
54
+
55
+
56
+ class TokensorClient:
57
+ """HTTP sender with a background batch queue thread."""
58
+
59
+ def __init__(self, config: TokensorConfig) -> None:
60
+ self._config = config
61
+ self._queue: queue.Queue = queue.Queue()
62
+ self._stop_event = threading.Event()
63
+ self._thread = threading.Thread(target=self._run, daemon=True)
64
+ self._thread.start()
65
+
66
+ def __enter__(self) -> "TokensorClient":
67
+ return self
68
+
69
+ def __exit__(self, *_) -> None:
70
+ self.shutdown()
71
+
72
+ def enqueue(self, event: EventPayload) -> None:
73
+ """Add event to the batch queue. Returns immediately."""
74
+ self._queue.put_nowait(event)
75
+
76
+ def shutdown(self) -> None:
77
+ """Signal the background thread to stop, flush remaining events, and wait."""
78
+ self._stop_event.set()
79
+ self._queue.put_nowait(_SENTINEL) # Wake the thread if it's blocked on get()
80
+ self._thread.join(timeout=10)
81
+
82
+ def _run(self) -> None:
83
+ batch: list[EventPayload] = []
84
+ deadline = time.monotonic() + self._config.flush_interval
85
+
86
+ while True:
87
+ timeout = max(0.001, deadline - time.monotonic())
88
+ try:
89
+ item = self._queue.get(timeout=timeout)
90
+ except queue.Empty:
91
+ # flush_interval elapsed — send whatever we have
92
+ if batch:
93
+ self._send(batch)
94
+ batch = []
95
+ deadline = time.monotonic() + self._config.flush_interval
96
+ continue
97
+
98
+ if item is _SENTINEL:
99
+ # Drain any remaining items enqueued before shutdown
100
+ try:
101
+ while True:
102
+ item2 = self._queue.get_nowait()
103
+ if item2 is not _SENTINEL:
104
+ batch.append(item2)
105
+ except queue.Empty:
106
+ pass
107
+ if batch:
108
+ self._send(batch)
109
+ break
110
+
111
+ batch.append(item)
112
+ if len(batch) >= self._config.batch_size:
113
+ self._send(batch)
114
+ batch = []
115
+ deadline = time.monotonic() + self._config.flush_interval
116
+
117
+ @_retryable()
118
+ def _send(self, events: list[EventPayload]) -> int | None:
119
+ """POST a batch to the API. Returns status_code; decorator handles retries."""
120
+ resp = httpx.post(
121
+ f"{self._config.host}/v1/events/batch",
122
+ json=events_to_batch_payload(events),
123
+ headers={"X-Tokensor-Key": self._config.api_key},
124
+ timeout=5.0,
125
+ )
126
+ if 400 <= resp.status_code < 500:
127
+ logger.error(
128
+ "Tokensor: dropping %d events — server rejected with %d",
129
+ len(events), resp.status_code,
130
+ )
131
+ return resp.status_code
tokensor/_config.py ADDED
@@ -0,0 +1,30 @@
1
+ from __future__ import annotations
2
+ import os
3
+ from dataclasses import dataclass
4
+
5
+
6
+ @dataclass
7
+ class TokensorConfig:
8
+ api_key: str
9
+ host: str = "https://api.tokensor.com"
10
+ batch_size: int = 50
11
+ flush_interval: float = 2.0
12
+
13
+
14
+ def load_config(
15
+ api_key: str | None = None,
16
+ host: str | None = None,
17
+ batch_size: int | None = None,
18
+ flush_interval: float | None = None,
19
+ ) -> TokensorConfig:
20
+ resolved_key = api_key or os.environ.get("TOKENSOR_API_KEY")
21
+ if not resolved_key:
22
+ raise ValueError(
23
+ "Tokensor API key is required. Pass api_key= or set TOKENSOR_API_KEY environment variable."
24
+ )
25
+ return TokensorConfig(
26
+ api_key=resolved_key,
27
+ host=host if host is not None else os.environ.get("TOKENSOR_HOST", "https://api.tokensor.com"),
28
+ batch_size=batch_size if batch_size is not None else int(os.environ.get("TOKENSOR_BATCH_SIZE", "50")),
29
+ flush_interval=flush_interval if flush_interval is not None else float(os.environ.get("TOKENSOR_FLUSH_INTERVAL", "2.0")),
30
+ )
tokensor/_hash.py ADDED
@@ -0,0 +1,32 @@
1
+ from __future__ import annotations
2
+ import hashlib
3
+ import inspect
4
+
5
+
6
+ def endpoint_hash(depth: int = 2) -> str:
7
+ """Return a SHA256 hex string identifying the call site at `depth` frames up.
8
+
9
+ Uses the function's first line number (co_firstlineno) so the hash is stable
10
+ across multiple calls from the same function, regardless of which line within
11
+ the function makes the LLM call.
12
+
13
+ depth=0: endpoint_hash itself (rarely useful)
14
+ depth=1: the direct caller of this function
15
+ depth=2: the caller's caller (default — identifies the user's call site)
16
+ """
17
+ frame = inspect.currentframe()
18
+ try:
19
+ target = frame
20
+ for _ in range(depth):
21
+ if target.f_back is None:
22
+ break
23
+ target = target.f_back
24
+ key = (
25
+ f"{target.f_code.co_filename}:"
26
+ f"{target.f_code.co_name}:"
27
+ f"{target.f_code.co_firstlineno}"
28
+ )
29
+ finally:
30
+ del frame
31
+
32
+ return hashlib.sha256(key.encode()).hexdigest()
tokensor/_models.py ADDED
@@ -0,0 +1,29 @@
1
+ from __future__ import annotations
2
+ from dataclasses import dataclass, asdict
3
+ from datetime import datetime
4
+ from typing import Any
5
+
6
+
7
+ @dataclass
8
+ class EventPayload:
9
+ called_at: datetime
10
+ model: str
11
+ endpoint_hash: str
12
+ input_tokens: int
13
+ output_tokens: int
14
+ cache_creation_tokens: int = 0
15
+ cache_read_tokens: int = 0
16
+ message_count: int | None = None
17
+ max_tokens: int | None = None
18
+ finish_reason: str | None = None
19
+ latency_ms: int | None = None
20
+ status_code: int | None = None
21
+
22
+ def to_dict(self) -> dict[str, Any]:
23
+ d = asdict(self)
24
+ d["called_at"] = self.called_at.isoformat()
25
+ return d
26
+
27
+
28
+ def events_to_batch_payload(events: list[EventPayload]) -> dict[str, Any]:
29
+ return {"events": [e.to_dict() for e in events]}
@@ -0,0 +1,100 @@
1
+ Metadata-Version: 2.4
2
+ Name: tokensor
3
+ Version: 0.1.0
4
+ Summary: LLM token observability SDK — wraps OpenAI and Anthropic clients to capture metadata
5
+ Project-URL: Homepage, https://tokensor.com
6
+ Project-URL: Repository, https://github.com/tokensor/tokensor
7
+ Project-URL: Bug Tracker, https://github.com/tokensor/tokensor/issues
8
+ Author-email: Tokensor <hello@tokensor.com>
9
+ License: MIT
10
+ Keywords: anthropic,cost,llm,observability,openai,tokens
11
+ Classifier: Development Status :: 3 - Alpha
12
+ Classifier: Intended Audience :: Developers
13
+ Classifier: License :: OSI Approved :: MIT License
14
+ Classifier: Programming Language :: Python :: 3
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 :: Python Modules
20
+ Requires-Python: >=3.9
21
+ Requires-Dist: httpx>=0.28
22
+ Provides-Extra: anthropic
23
+ Requires-Dist: anthropic>=0.30; extra == 'anthropic'
24
+ Provides-Extra: dev
25
+ Requires-Dist: anthropic>=0.30; extra == 'dev'
26
+ Requires-Dist: openai>=1.0; extra == 'dev'
27
+ Requires-Dist: pytest-asyncio>=0.24; extra == 'dev'
28
+ Requires-Dist: pytest-cov>=6.0; extra == 'dev'
29
+ Requires-Dist: pytest>=8.3; extra == 'dev'
30
+ Requires-Dist: respx>=0.21; extra == 'dev'
31
+ Provides-Extra: openai
32
+ Requires-Dist: openai>=1.0; extra == 'openai'
33
+ Description-Content-Type: text/markdown
34
+
35
+ # tokensor
36
+
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
+
39
+ ## Install
40
+
41
+ ```bash
42
+ pip install tokensor
43
+ # With OpenAI support:
44
+ pip install "tokensor[openai]"
45
+ # With Anthropic support:
46
+ pip install "tokensor[anthropic]"
47
+ ```
48
+
49
+ ## Quickstart
50
+
51
+ Get your API key from [app.tokensor.com](https://app.tokensor.com) → Settings → API Keys.
52
+
53
+ **OpenAI:**
54
+
55
+ ```python
56
+ import os
57
+ import openai
58
+ import tokensor
59
+
60
+ client = tokensor.wrap(
61
+ openai.OpenAI(),
62
+ api_key=os.environ["TOKENSOR_API_KEY"],
63
+ )
64
+
65
+ # Use exactly like openai.OpenAI()
66
+ response = client.chat.completions.create(
67
+ model="gpt-4o-mini",
68
+ messages=[{"role": "user", "content": "Hello"}],
69
+ )
70
+ ```
71
+
72
+ **Anthropic:**
73
+
74
+ ```python
75
+ import os
76
+ import anthropic
77
+ import tokensor
78
+
79
+ client = tokensor.wrap(
80
+ anthropic.Anthropic(),
81
+ api_key=os.environ["TOKENSOR_API_KEY"],
82
+ )
83
+
84
+ message = client.messages.create(
85
+ model="claude-3-5-haiku-latest",
86
+ max_tokens=1024,
87
+ messages=[{"role": "user", "content": "Hello"}],
88
+ )
89
+ ```
90
+
91
+ ## Environment variables
92
+
93
+ | Variable | Required | Description |
94
+ |----------|----------|-------------|
95
+ | `TOKENSOR_API_KEY` | yes | Your Tokensor API key |
96
+ | `TOKENSOR_HOST` | no | Override API host (default: `https://api.tokensor.com`) |
97
+
98
+ ## License
99
+
100
+ MIT
@@ -0,0 +1,11 @@
1
+ tokensor/__init__.py,sha256=hKbYWcvB9a2AO_RoY7Eki9XWkaCU-Hk7qOM1nQZ9f84,2680
2
+ tokensor/_client.py,sha256=FmSw2rAKij_9QjD80Fn9Qnmx94RuOVWQwFKXooubLig,4721
3
+ tokensor/_config.py,sha256=ZfLj2igjKpl2H4qj2ELx4vybXLMQUkzlfM_DMOOb6qE,1046
4
+ tokensor/_hash.py,sha256=u2vSyuMxJExI-RTKCmB_GkYQ1puDvqTqmdPOp2UY1Ts,1011
5
+ tokensor/_models.py,sha256=IINuudIUIF9n3Y0A6j-eLwFePx1iRbbqEHhnJbPtOMI,779
6
+ tokensor/_adapters/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
7
+ tokensor/_adapters/_anthropic.py,sha256=uwy_ze4BG31MwWzgLPOonYXnNKUlYfwehS_dgD9v0m4,4304
8
+ tokensor/_adapters/_openai.py,sha256=1wBVy4OFkmXm3_iQ_54spk-kisWzjBtJ55z1yJoWU-s,4151
9
+ tokensor-0.1.0.dist-info/METADATA,sha256=iJHcqN8Y73AE_8CrYD7yNfiSZSwDOa2H26Zya3jku-g,2820
10
+ tokensor-0.1.0.dist-info/WHEEL,sha256=QccIxa26bgl1E6uMy58deGWi-0aeIkkangHcxk2kWfw,87
11
+ tokensor-0.1.0.dist-info/RECORD,,
@@ -0,0 +1,4 @@
1
+ Wheel-Version: 1.0
2
+ Generator: hatchling 1.29.0
3
+ Root-Is-Purelib: true
4
+ Tag: py3-none-any