tokenspend 0.1.1__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.
tokenspend-0.1.1/LICENSE
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 TokenSpend
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in all
|
|
13
|
+
copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
21
|
+
SOFTWARE.
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: tokenspend
|
|
3
|
+
Version: 0.1.1
|
|
4
|
+
Summary: Official Python client for the TokenSpend API
|
|
5
|
+
Project-URL: Documentation, https://tokenspend.dev/docs/router
|
|
6
|
+
Project-URL: Homepage, https://tokenspend.dev
|
|
7
|
+
Project-URL: Repository, https://github.com/haseebc2000/token-spend
|
|
8
|
+
Author: TokenSpend
|
|
9
|
+
License-Expression: MIT
|
|
10
|
+
License-File: LICENSE
|
|
11
|
+
Keywords: ai,api,llm,tokenspend
|
|
12
|
+
Classifier: Development Status :: 3 - Alpha
|
|
13
|
+
Classifier: Intended Audience :: Developers
|
|
14
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
15
|
+
Classifier: Programming Language :: Python :: 3
|
|
16
|
+
Classifier: Programming Language :: Python :: 3.9
|
|
17
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
18
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
19
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
20
|
+
Classifier: Programming Language :: Python :: 3.13
|
|
21
|
+
Requires-Python: >=3.9
|
|
22
|
+
Description-Content-Type: text/markdown
|
|
23
|
+
|
|
24
|
+
# TokenSpend Python
|
|
25
|
+
|
|
26
|
+
Official Python client for the TokenSpend API. It has no runtime dependencies.
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
pip install tokenspend
|
|
30
|
+
export TOKENSPEND_API_KEY=tsr_...
|
|
31
|
+
```
|
|
32
|
+
|
|
33
|
+
```python
|
|
34
|
+
from tokenspend import TokenSpend
|
|
35
|
+
|
|
36
|
+
client = TokenSpend()
|
|
37
|
+
response = client.chat.completions.create(
|
|
38
|
+
model="xai/grok-4.5",
|
|
39
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
40
|
+
)
|
|
41
|
+
print(response.choices[0].message.content)
|
|
42
|
+
```
|
|
43
|
+
|
|
44
|
+
Set `stream=True` to iterate over server-sent chunks. `client.messages.create(...)`
|
|
45
|
+
uses the Anthropic-compatible Messages API. See https://tokenspend.dev/docs/router.
|
|
46
|
+
|
|
47
|
+
Every registry model uses the same method:
|
|
48
|
+
|
|
49
|
+
```python
|
|
50
|
+
for model in (
|
|
51
|
+
"xai/grok-4.5",
|
|
52
|
+
"meta/muse-spark-1.2",
|
|
53
|
+
"thinking-machines/inkling",
|
|
54
|
+
"moonshot/kimi-k3",
|
|
55
|
+
):
|
|
56
|
+
response = client.chat.completions.create(
|
|
57
|
+
model=model,
|
|
58
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
59
|
+
)
|
|
60
|
+
```
|
|
61
|
+
|
|
62
|
+
The SDK never accepts a serving-host selector.
|
|
@@ -0,0 +1,39 @@
|
|
|
1
|
+
# TokenSpend Python
|
|
2
|
+
|
|
3
|
+
Official Python client for the TokenSpend API. It has no runtime dependencies.
|
|
4
|
+
|
|
5
|
+
```bash
|
|
6
|
+
pip install tokenspend
|
|
7
|
+
export TOKENSPEND_API_KEY=tsr_...
|
|
8
|
+
```
|
|
9
|
+
|
|
10
|
+
```python
|
|
11
|
+
from tokenspend import TokenSpend
|
|
12
|
+
|
|
13
|
+
client = TokenSpend()
|
|
14
|
+
response = client.chat.completions.create(
|
|
15
|
+
model="xai/grok-4.5",
|
|
16
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
17
|
+
)
|
|
18
|
+
print(response.choices[0].message.content)
|
|
19
|
+
```
|
|
20
|
+
|
|
21
|
+
Set `stream=True` to iterate over server-sent chunks. `client.messages.create(...)`
|
|
22
|
+
uses the Anthropic-compatible Messages API. See https://tokenspend.dev/docs/router.
|
|
23
|
+
|
|
24
|
+
Every registry model uses the same method:
|
|
25
|
+
|
|
26
|
+
```python
|
|
27
|
+
for model in (
|
|
28
|
+
"xai/grok-4.5",
|
|
29
|
+
"meta/muse-spark-1.2",
|
|
30
|
+
"thinking-machines/inkling",
|
|
31
|
+
"moonshot/kimi-k3",
|
|
32
|
+
):
|
|
33
|
+
response = client.chat.completions.create(
|
|
34
|
+
model=model,
|
|
35
|
+
messages=[{"role": "user", "content": "Hello"}],
|
|
36
|
+
)
|
|
37
|
+
```
|
|
38
|
+
|
|
39
|
+
The SDK never accepts a serving-host selector.
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling>=1.26"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
[project]
|
|
6
|
+
name = "tokenspend"
|
|
7
|
+
version = "0.1.1"
|
|
8
|
+
description = "Official Python client for the TokenSpend API"
|
|
9
|
+
readme = "README.md"
|
|
10
|
+
license = "MIT"
|
|
11
|
+
requires-python = ">=3.9"
|
|
12
|
+
authors = [{ name = "TokenSpend" }]
|
|
13
|
+
keywords = ["ai", "llm", "tokenspend", "api"]
|
|
14
|
+
classifiers = [
|
|
15
|
+
"Development Status :: 3 - Alpha",
|
|
16
|
+
"Intended Audience :: Developers",
|
|
17
|
+
"License :: OSI Approved :: MIT License",
|
|
18
|
+
"Programming Language :: Python :: 3",
|
|
19
|
+
"Programming Language :: Python :: 3.9",
|
|
20
|
+
"Programming Language :: Python :: 3.10",
|
|
21
|
+
"Programming Language :: Python :: 3.11",
|
|
22
|
+
"Programming Language :: Python :: 3.12",
|
|
23
|
+
"Programming Language :: Python :: 3.13",
|
|
24
|
+
]
|
|
25
|
+
|
|
26
|
+
[project.urls]
|
|
27
|
+
Documentation = "https://tokenspend.dev/docs/router"
|
|
28
|
+
Homepage = "https://tokenspend.dev"
|
|
29
|
+
Repository = "https://github.com/haseebc2000/token-spend"
|
|
30
|
+
|
|
31
|
+
[tool.hatch.build.targets.wheel]
|
|
32
|
+
packages = ["src/tokenspend"]
|
|
33
|
+
|
|
34
|
+
[tool.hatch.build.targets.sdist]
|
|
35
|
+
include = ["src/tokenspend", "README.md", "LICENSE", "pyproject.toml"]
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
"""Dependency-free TokenSpend API client."""
|
|
2
|
+
|
|
3
|
+
from __future__ import annotations
|
|
4
|
+
|
|
5
|
+
import json
|
|
6
|
+
import os
|
|
7
|
+
from collections.abc import Iterator, Mapping
|
|
8
|
+
from typing import Any, BinaryIO
|
|
9
|
+
from urllib.error import HTTPError, URLError
|
|
10
|
+
from urllib.request import Request, urlopen
|
|
11
|
+
|
|
12
|
+
|
|
13
|
+
DEFAULT_BASE_URL = "https://api.tokenspend.dev/v1"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
class TokenSpendError(RuntimeError):
|
|
17
|
+
"""An API or transport error returned by TokenSpend."""
|
|
18
|
+
|
|
19
|
+
def __init__(self, message: str, *, status_code: int | None = None, body: Any = None):
|
|
20
|
+
super().__init__(message)
|
|
21
|
+
self.status_code = status_code
|
|
22
|
+
self.body = body
|
|
23
|
+
|
|
24
|
+
|
|
25
|
+
class APIResponse(dict[str, Any]):
|
|
26
|
+
"""Dictionary response with convenient attribute access."""
|
|
27
|
+
|
|
28
|
+
def __getattr__(self, name: str) -> Any:
|
|
29
|
+
try:
|
|
30
|
+
return self[name]
|
|
31
|
+
except KeyError as exc:
|
|
32
|
+
raise AttributeError(name) from exc
|
|
33
|
+
|
|
34
|
+
|
|
35
|
+
def _coerce(value: Any) -> Any:
|
|
36
|
+
if isinstance(value, dict):
|
|
37
|
+
return APIResponse({key: _coerce(item) for key, item in value.items()})
|
|
38
|
+
if isinstance(value, list):
|
|
39
|
+
return [_coerce(item) for item in value]
|
|
40
|
+
return value
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
def _decode_json(raw: bytes) -> Any:
|
|
44
|
+
if not raw:
|
|
45
|
+
return APIResponse()
|
|
46
|
+
try:
|
|
47
|
+
return _coerce(json.loads(raw.decode("utf-8")))
|
|
48
|
+
except (UnicodeDecodeError, json.JSONDecodeError) as exc:
|
|
49
|
+
raise TokenSpendError("TokenSpend returned an invalid JSON response.") from exc
|
|
50
|
+
|
|
51
|
+
|
|
52
|
+
class Stream(Iterator[APIResponse]):
|
|
53
|
+
"""Iterator over server-sent JSON chunks."""
|
|
54
|
+
|
|
55
|
+
def __init__(self, response: BinaryIO):
|
|
56
|
+
self._response = response
|
|
57
|
+
self._iterator = iter(response)
|
|
58
|
+
|
|
59
|
+
def __iter__(self) -> Stream:
|
|
60
|
+
return self
|
|
61
|
+
|
|
62
|
+
def __next__(self) -> APIResponse:
|
|
63
|
+
for raw_line in self._iterator:
|
|
64
|
+
line = raw_line.decode("utf-8").strip()
|
|
65
|
+
if not line.startswith("data:"):
|
|
66
|
+
continue
|
|
67
|
+
data = line[5:].strip()
|
|
68
|
+
if data == "[DONE]":
|
|
69
|
+
self.close()
|
|
70
|
+
raise StopIteration
|
|
71
|
+
if data:
|
|
72
|
+
return _coerce(json.loads(data))
|
|
73
|
+
self.close()
|
|
74
|
+
raise StopIteration
|
|
75
|
+
|
|
76
|
+
def close(self) -> None:
|
|
77
|
+
close = getattr(self._response, "close", None)
|
|
78
|
+
if close:
|
|
79
|
+
close()
|
|
80
|
+
|
|
81
|
+
def __enter__(self) -> Stream:
|
|
82
|
+
return self
|
|
83
|
+
|
|
84
|
+
def __exit__(self, *_: Any) -> None:
|
|
85
|
+
self.close()
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
class _Resource:
|
|
89
|
+
def __init__(self, client: TokenSpend, path: str):
|
|
90
|
+
self._client = client
|
|
91
|
+
self._path = path
|
|
92
|
+
|
|
93
|
+
def create(self, **params: Any) -> APIResponse | Stream:
|
|
94
|
+
return self._client._post(self._path, params, stream=bool(params.get("stream")))
|
|
95
|
+
|
|
96
|
+
|
|
97
|
+
class _Chat:
|
|
98
|
+
def __init__(self, client: TokenSpend):
|
|
99
|
+
self.completions = _Resource(client, "/chat/completions")
|
|
100
|
+
|
|
101
|
+
|
|
102
|
+
class TokenSpend:
|
|
103
|
+
"""Client for TokenSpend's OpenAI and Anthropic-compatible endpoints."""
|
|
104
|
+
|
|
105
|
+
def __init__(
|
|
106
|
+
self,
|
|
107
|
+
api_key: str | None = None,
|
|
108
|
+
*,
|
|
109
|
+
base_url: str = DEFAULT_BASE_URL,
|
|
110
|
+
timeout: float = 120,
|
|
111
|
+
default_headers: Mapping[str, str] | None = None,
|
|
112
|
+
):
|
|
113
|
+
resolved_key = api_key or os.environ.get("TOKENSPEND_API_KEY") or os.environ.get("TOKENSPEND_RAIL_KEY")
|
|
114
|
+
if not resolved_key:
|
|
115
|
+
raise TokenSpendError("Set TOKENSPEND_API_KEY or pass api_key to TokenSpend().")
|
|
116
|
+
self.api_key = resolved_key
|
|
117
|
+
self.base_url = base_url.rstrip("/")
|
|
118
|
+
self.timeout = timeout
|
|
119
|
+
self.default_headers = dict(default_headers or {})
|
|
120
|
+
self.chat = _Chat(self)
|
|
121
|
+
self.messages = _Resource(self, "/messages")
|
|
122
|
+
|
|
123
|
+
def _post(self, path: str, payload: Mapping[str, Any], *, stream: bool) -> APIResponse | Stream:
|
|
124
|
+
headers = {
|
|
125
|
+
"accept": "text/event-stream" if stream else "application/json",
|
|
126
|
+
"authorization": f"Bearer {self.api_key}",
|
|
127
|
+
"content-type": "application/json",
|
|
128
|
+
"user-agent": "tokenspend-python/0.1.1",
|
|
129
|
+
**self.default_headers,
|
|
130
|
+
}
|
|
131
|
+
request = Request(
|
|
132
|
+
f"{self.base_url}{path}",
|
|
133
|
+
data=json.dumps(payload, separators=(",", ":")).encode("utf-8"),
|
|
134
|
+
headers=headers,
|
|
135
|
+
method="POST",
|
|
136
|
+
)
|
|
137
|
+
try:
|
|
138
|
+
response = urlopen(request, timeout=self.timeout)
|
|
139
|
+
if stream:
|
|
140
|
+
return Stream(response)
|
|
141
|
+
with response:
|
|
142
|
+
return _decode_json(response.read())
|
|
143
|
+
except HTTPError as exc:
|
|
144
|
+
raw = exc.read()
|
|
145
|
+
try:
|
|
146
|
+
body = _decode_json(raw)
|
|
147
|
+
except TokenSpendError:
|
|
148
|
+
body = raw.decode("utf-8", errors="replace")
|
|
149
|
+
message = body.get("error", {}).get("message") if isinstance(body, dict) else None
|
|
150
|
+
raise TokenSpendError(message or f"TokenSpend request failed with status {exc.code}.", status_code=exc.code, body=body) from exc
|
|
151
|
+
except URLError as exc:
|
|
152
|
+
raise TokenSpendError(f"Could not reach TokenSpend: {exc.reason}") from exc
|