meterspw-sdk 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.
- meterspw_sdk-0.2.0/.gitignore +7 -0
- meterspw_sdk-0.2.0/LICENSE +23 -0
- meterspw_sdk-0.2.0/PKG-INFO +201 -0
- meterspw_sdk-0.2.0/README.md +152 -0
- meterspw_sdk-0.2.0/pyproject.toml +58 -0
- meterspw_sdk-0.2.0/src/meterspw/__init__.py +56 -0
- meterspw_sdk-0.2.0/src/meterspw/_version.py +10 -0
- meterspw_sdk-0.2.0/src/meterspw/_wire.py +383 -0
- meterspw_sdk-0.2.0/src/meterspw/aio.py +220 -0
- meterspw_sdk-0.2.0/src/meterspw/client.py +319 -0
- meterspw_sdk-0.2.0/src/meterspw/errors.py +72 -0
- meterspw_sdk-0.2.0/src/meterspw/models.py +139 -0
- meterspw_sdk-0.2.0/src/meterspw/retry.py +89 -0
- meterspw_sdk-0.2.0/tests/test_aio.py +253 -0
- meterspw_sdk-0.2.0/tests/test_sdk.py +443 -0
- meterspw_sdk-0.2.0/tests/test_security.py +134 -0
|
@@ -0,0 +1,23 @@
|
|
|
1
|
+
MIT License
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2026 S&T Integrated Solutions, LLC
|
|
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.
|
|
22
|
+
|
|
23
|
+
https://scitechsolutions.ai
|
|
@@ -0,0 +1,201 @@
|
|
|
1
|
+
Metadata-Version: 2.5
|
|
2
|
+
Name: meterspw-sdk
|
|
3
|
+
Version: 0.2.0
|
|
4
|
+
Summary: Meter-SPW-v1.0 — deterministic LLM routing with metered savings (BYOK)
|
|
5
|
+
Project-URL: Homepage, https://spw.scitechsolutions.ai
|
|
6
|
+
Author-email: "S&T Integrated Solutions, LLC" <support@scitechsolutions.ai>
|
|
7
|
+
License: MIT License
|
|
8
|
+
|
|
9
|
+
Copyright (c) 2026 S&T Integrated Solutions, LLC
|
|
10
|
+
|
|
11
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
12
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
13
|
+
in the Software without restriction, including without limitation the rights
|
|
14
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
15
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
16
|
+
furnished to do so, subject to the following conditions:
|
|
17
|
+
|
|
18
|
+
The above copyright notice and this permission notice shall be included in all
|
|
19
|
+
copies or substantial portions of the Software.
|
|
20
|
+
|
|
21
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
22
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
23
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
24
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
25
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
26
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
|
27
|
+
SOFTWARE.
|
|
28
|
+
|
|
29
|
+
https://scitechsolutions.ai
|
|
30
|
+
License-File: LICENSE
|
|
31
|
+
Keywords: anthropic,byok,cost,llm,openai,router
|
|
32
|
+
Classifier: Development Status :: 4 - Beta
|
|
33
|
+
Classifier: Intended Audience :: Developers
|
|
34
|
+
Classifier: License :: OSI Approved :: MIT License
|
|
35
|
+
Classifier: Programming Language :: Python :: 3.10
|
|
36
|
+
Classifier: Programming Language :: Python :: 3.11
|
|
37
|
+
Classifier: Programming Language :: Python :: 3.12
|
|
38
|
+
Classifier: Typing :: Typed
|
|
39
|
+
Requires-Python: >=3.10
|
|
40
|
+
Provides-Extra: async
|
|
41
|
+
Requires-Dist: httpx>=0.24; extra == 'async'
|
|
42
|
+
Provides-Extra: dev
|
|
43
|
+
Requires-Dist: httpx>=0.24; extra == 'dev'
|
|
44
|
+
Requires-Dist: mypy>=1.8; extra == 'dev'
|
|
45
|
+
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
|
|
46
|
+
Requires-Dist: pytest>=7; extra == 'dev'
|
|
47
|
+
Requires-Dist: ruff>=0.5; extra == 'dev'
|
|
48
|
+
Description-Content-Type: text/markdown
|
|
49
|
+
|
|
50
|
+
# meterspw-sdk
|
|
51
|
+
|
|
52
|
+
Python client for **Meter-SPW** — deterministic LLM routing with metered savings.
|
|
53
|
+
|
|
54
|
+
You bring your own provider keys. Meter-SPW routes each prompt to the cheapest
|
|
55
|
+
model that can answer it, caches what repeats, and meters what it saved.
|
|
56
|
+
|
|
57
|
+
```bash
|
|
58
|
+
pip install meterspw-sdk
|
|
59
|
+
```
|
|
60
|
+
|
|
61
|
+
Zero runtime dependencies — the default transport is `urllib` from the standard
|
|
62
|
+
library.
|
|
63
|
+
|
|
64
|
+
## Use
|
|
65
|
+
|
|
66
|
+
```python
|
|
67
|
+
from meterspw import MeterSpwClient
|
|
68
|
+
|
|
69
|
+
with MeterSpwClient() as spw: # key from ROUTER_API_KEY
|
|
70
|
+
out = spw.relay(
|
|
71
|
+
"Summarise this contract in three bullets.",
|
|
72
|
+
model="claude-sonnet-5",
|
|
73
|
+
max_tokens=2000,
|
|
74
|
+
)
|
|
75
|
+
print(out.content)
|
|
76
|
+
print(out.model_used) # what ACTUALLY answered
|
|
77
|
+
print(out.savings.usd) # what that saved you
|
|
78
|
+
```
|
|
79
|
+
|
|
80
|
+
`model` is what you want; `model_used` is what answered. The difference is the
|
|
81
|
+
product.
|
|
82
|
+
|
|
83
|
+
### Attachments
|
|
84
|
+
|
|
85
|
+
```python
|
|
86
|
+
out = spw.relay_multipart(
|
|
87
|
+
"Summarise the attached report.",
|
|
88
|
+
model="claude-sonnet-5",
|
|
89
|
+
max_tokens=2000,
|
|
90
|
+
files=["report.pdf"],
|
|
91
|
+
)
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
### Know where you stand
|
|
95
|
+
|
|
96
|
+
```python
|
|
97
|
+
b = spw.balance()
|
|
98
|
+
print(b.usd, b.accrued_micro_usd, b.remaining_usd)
|
|
99
|
+
if b.passthrough:
|
|
100
|
+
print("credit exhausted — calls still work, but nothing is being routed")
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
**Check `passthrough`.** At zero credit the router degrades to a transparent
|
|
104
|
+
proxy: your calls keep succeeding, with no routing, no cache and no savings.
|
|
105
|
+
Nothing fails, which is exactly why it is easy to miss.
|
|
106
|
+
|
|
107
|
+
**Three numbers, because two books.** `usd` is the ledger. `accrued` is this
|
|
108
|
+
period's fees, which have not been taken yet — they settle at the monthly
|
|
109
|
+
close. `remaining` is what you should plan against.
|
|
110
|
+
|
|
111
|
+
### Your receipts
|
|
112
|
+
|
|
113
|
+
```python
|
|
114
|
+
rows = spw.verdicts(window="7d", q="haiku") # the last 7 days, searched
|
|
115
|
+
```
|
|
116
|
+
|
|
117
|
+
`window` is one of `30m 1h 6h 12h 24h 7d 4w 12m` (a period ending now); `q`
|
|
118
|
+
matches the decision, the requested model and the served model. The router
|
|
119
|
+
applies both, so the same two values count and export exactly these rows:
|
|
120
|
+
|
|
121
|
+
```bash
|
|
122
|
+
curl -H "Authorization: Bearer $ROUTER_API_KEY" \
|
|
123
|
+
"https://spw.scitechsolutions.ai/v1/verdicts/count?window=7d&q=haiku"
|
|
124
|
+
curl -H "Authorization: Bearer $ROUTER_API_KEY" -o verdicts-7d.csv \
|
|
125
|
+
"https://spw.scitechsolutions.ai/v1/verdicts.csv?window=7d&q=haiku"
|
|
126
|
+
```
|
|
127
|
+
|
|
128
|
+
The export is refused, with the count, when more than 200 000 rows match —
|
|
129
|
+
narrow the window or the search.
|
|
130
|
+
|
|
131
|
+
### Long requests
|
|
132
|
+
|
|
133
|
+
A proxy between you and the router closes a connection that stays silent for
|
|
134
|
+
about 100 seconds, and the router is silent until the verified answer exists.
|
|
135
|
+
So `relay()` asks for keep-alive delivery: the router answers within a second,
|
|
136
|
+
keeps the line warm every 15 seconds while the model works, and delivers the
|
|
137
|
+
whole answer at the end — the same result, nothing to change on your side.
|
|
138
|
+
|
|
139
|
+
`timeout` bounds **silence** on the line, not the whole call. A long answer is
|
|
140
|
+
bounded by the router's own request limit (the operator's
|
|
141
|
+
`ROUTER_REQUEST_TIMEOUT_SECS`), not by this number.
|
|
142
|
+
|
|
143
|
+
Over raw HTTP, send the header yourself and read the last `data:` line:
|
|
144
|
+
|
|
145
|
+
```bash
|
|
146
|
+
curl -N -X POST https://spw.scitechsolutions.ai/v1/relay \
|
|
147
|
+
-H "Authorization: Bearer $ROUTER_API_KEY" \
|
|
148
|
+
-H "Accept: text/event-stream" \
|
|
149
|
+
-H "Content-Type: application/json" \
|
|
150
|
+
-d '{"prompt": "Summarise this contract in three bullets.", "model": "claude-sonnet-5", "max_tokens": 2000}'
|
|
151
|
+
# -N is optional: curl prints each line as it arrives instead of all at the end.
|
|
152
|
+
```
|
|
153
|
+
|
|
154
|
+
```
|
|
155
|
+
: keep-alive ← every 15 s while the model works
|
|
156
|
+
event: result
|
|
157
|
+
data: {"decision": "relayed", "model_used": "…", "content": "…", "verdict_id": "…", "latency_ms": 1840, …}
|
|
158
|
+
```
|
|
159
|
+
|
|
160
|
+
An `event: error` line with `{"status": 502, "error": "…"}` takes the place
|
|
161
|
+
of `result` if the router fails late. `/v1/relay-multipart` takes the same
|
|
162
|
+
header. Without it the reply is one JSON body — and a call longer than about
|
|
163
|
+
100 seconds dies at the proxy.
|
|
164
|
+
|
|
165
|
+
## Errors
|
|
166
|
+
|
|
167
|
+
| Exception | When | Retry? |
|
|
168
|
+
|---|---|---|
|
|
169
|
+
| `AuthError` | 401/403 — key wrong or revoked | no |
|
|
170
|
+
| `RateLimited` | 429 | yes, honours `Retry-After` |
|
|
171
|
+
| `UploadCapacityFull` | 503 — upload slots full | yes, honours `Retry-After` |
|
|
172
|
+
| `Exhausted` | 502 — the router spent every lever | **no** |
|
|
173
|
+
| `TransportError` | never reached the server | yes |
|
|
174
|
+
|
|
175
|
+
`Exhausted` is not retryable on purpose. The router already climbed the model
|
|
176
|
+
ladder, repaired and re-called before answering — each of those attempts billed
|
|
177
|
+
**your** provider key. Asking again spends the same levers for the same failure.
|
|
178
|
+
The message names what it tried.
|
|
179
|
+
|
|
180
|
+
Retries use exponential backoff with deterministic jitter and a wall-clock
|
|
181
|
+
budget, and never extend past your own deadline. Configure with `RetryPolicy`.
|
|
182
|
+
|
|
183
|
+
## Configuration
|
|
184
|
+
|
|
185
|
+
| | |
|
|
186
|
+
|---|---|
|
|
187
|
+
| `ROUTER_API_KEY` | your Meter-SPW key (or pass `api_key=`) |
|
|
188
|
+
| `ROUTER_BASE_URL` | defaults to `https://spw.scitechsolutions.ai` |
|
|
189
|
+
|
|
190
|
+
The SDK never sees a provider key. Those are stored once in the dashboard and
|
|
191
|
+
used by the router on your behalf — the only credential here spends credit, not
|
|
192
|
+
inference.
|
|
193
|
+
|
|
194
|
+
## Support
|
|
195
|
+
|
|
196
|
+
support@scitechsolutions.ai — quote `RelayResult.verdict_id` or the
|
|
197
|
+
`request_id` from an error and we can find the exact call.
|
|
198
|
+
|
|
199
|
+
---
|
|
200
|
+
|
|
201
|
+
© S&T Integrated Solutions, LLC — MIT licensed.
|
|
@@ -0,0 +1,152 @@
|
|
|
1
|
+
# meterspw-sdk
|
|
2
|
+
|
|
3
|
+
Python client for **Meter-SPW** — deterministic LLM routing with metered savings.
|
|
4
|
+
|
|
5
|
+
You bring your own provider keys. Meter-SPW routes each prompt to the cheapest
|
|
6
|
+
model that can answer it, caches what repeats, and meters what it saved.
|
|
7
|
+
|
|
8
|
+
```bash
|
|
9
|
+
pip install meterspw-sdk
|
|
10
|
+
```
|
|
11
|
+
|
|
12
|
+
Zero runtime dependencies — the default transport is `urllib` from the standard
|
|
13
|
+
library.
|
|
14
|
+
|
|
15
|
+
## Use
|
|
16
|
+
|
|
17
|
+
```python
|
|
18
|
+
from meterspw import MeterSpwClient
|
|
19
|
+
|
|
20
|
+
with MeterSpwClient() as spw: # key from ROUTER_API_KEY
|
|
21
|
+
out = spw.relay(
|
|
22
|
+
"Summarise this contract in three bullets.",
|
|
23
|
+
model="claude-sonnet-5",
|
|
24
|
+
max_tokens=2000,
|
|
25
|
+
)
|
|
26
|
+
print(out.content)
|
|
27
|
+
print(out.model_used) # what ACTUALLY answered
|
|
28
|
+
print(out.savings.usd) # what that saved you
|
|
29
|
+
```
|
|
30
|
+
|
|
31
|
+
`model` is what you want; `model_used` is what answered. The difference is the
|
|
32
|
+
product.
|
|
33
|
+
|
|
34
|
+
### Attachments
|
|
35
|
+
|
|
36
|
+
```python
|
|
37
|
+
out = spw.relay_multipart(
|
|
38
|
+
"Summarise the attached report.",
|
|
39
|
+
model="claude-sonnet-5",
|
|
40
|
+
max_tokens=2000,
|
|
41
|
+
files=["report.pdf"],
|
|
42
|
+
)
|
|
43
|
+
```
|
|
44
|
+
|
|
45
|
+
### Know where you stand
|
|
46
|
+
|
|
47
|
+
```python
|
|
48
|
+
b = spw.balance()
|
|
49
|
+
print(b.usd, b.accrued_micro_usd, b.remaining_usd)
|
|
50
|
+
if b.passthrough:
|
|
51
|
+
print("credit exhausted — calls still work, but nothing is being routed")
|
|
52
|
+
```
|
|
53
|
+
|
|
54
|
+
**Check `passthrough`.** At zero credit the router degrades to a transparent
|
|
55
|
+
proxy: your calls keep succeeding, with no routing, no cache and no savings.
|
|
56
|
+
Nothing fails, which is exactly why it is easy to miss.
|
|
57
|
+
|
|
58
|
+
**Three numbers, because two books.** `usd` is the ledger. `accrued` is this
|
|
59
|
+
period's fees, which have not been taken yet — they settle at the monthly
|
|
60
|
+
close. `remaining` is what you should plan against.
|
|
61
|
+
|
|
62
|
+
### Your receipts
|
|
63
|
+
|
|
64
|
+
```python
|
|
65
|
+
rows = spw.verdicts(window="7d", q="haiku") # the last 7 days, searched
|
|
66
|
+
```
|
|
67
|
+
|
|
68
|
+
`window` is one of `30m 1h 6h 12h 24h 7d 4w 12m` (a period ending now); `q`
|
|
69
|
+
matches the decision, the requested model and the served model. The router
|
|
70
|
+
applies both, so the same two values count and export exactly these rows:
|
|
71
|
+
|
|
72
|
+
```bash
|
|
73
|
+
curl -H "Authorization: Bearer $ROUTER_API_KEY" \
|
|
74
|
+
"https://spw.scitechsolutions.ai/v1/verdicts/count?window=7d&q=haiku"
|
|
75
|
+
curl -H "Authorization: Bearer $ROUTER_API_KEY" -o verdicts-7d.csv \
|
|
76
|
+
"https://spw.scitechsolutions.ai/v1/verdicts.csv?window=7d&q=haiku"
|
|
77
|
+
```
|
|
78
|
+
|
|
79
|
+
The export is refused, with the count, when more than 200 000 rows match —
|
|
80
|
+
narrow the window or the search.
|
|
81
|
+
|
|
82
|
+
### Long requests
|
|
83
|
+
|
|
84
|
+
A proxy between you and the router closes a connection that stays silent for
|
|
85
|
+
about 100 seconds, and the router is silent until the verified answer exists.
|
|
86
|
+
So `relay()` asks for keep-alive delivery: the router answers within a second,
|
|
87
|
+
keeps the line warm every 15 seconds while the model works, and delivers the
|
|
88
|
+
whole answer at the end — the same result, nothing to change on your side.
|
|
89
|
+
|
|
90
|
+
`timeout` bounds **silence** on the line, not the whole call. A long answer is
|
|
91
|
+
bounded by the router's own request limit (the operator's
|
|
92
|
+
`ROUTER_REQUEST_TIMEOUT_SECS`), not by this number.
|
|
93
|
+
|
|
94
|
+
Over raw HTTP, send the header yourself and read the last `data:` line:
|
|
95
|
+
|
|
96
|
+
```bash
|
|
97
|
+
curl -N -X POST https://spw.scitechsolutions.ai/v1/relay \
|
|
98
|
+
-H "Authorization: Bearer $ROUTER_API_KEY" \
|
|
99
|
+
-H "Accept: text/event-stream" \
|
|
100
|
+
-H "Content-Type: application/json" \
|
|
101
|
+
-d '{"prompt": "Summarise this contract in three bullets.", "model": "claude-sonnet-5", "max_tokens": 2000}'
|
|
102
|
+
# -N is optional: curl prints each line as it arrives instead of all at the end.
|
|
103
|
+
```
|
|
104
|
+
|
|
105
|
+
```
|
|
106
|
+
: keep-alive ← every 15 s while the model works
|
|
107
|
+
event: result
|
|
108
|
+
data: {"decision": "relayed", "model_used": "…", "content": "…", "verdict_id": "…", "latency_ms": 1840, …}
|
|
109
|
+
```
|
|
110
|
+
|
|
111
|
+
An `event: error` line with `{"status": 502, "error": "…"}` takes the place
|
|
112
|
+
of `result` if the router fails late. `/v1/relay-multipart` takes the same
|
|
113
|
+
header. Without it the reply is one JSON body — and a call longer than about
|
|
114
|
+
100 seconds dies at the proxy.
|
|
115
|
+
|
|
116
|
+
## Errors
|
|
117
|
+
|
|
118
|
+
| Exception | When | Retry? |
|
|
119
|
+
|---|---|---|
|
|
120
|
+
| `AuthError` | 401/403 — key wrong or revoked | no |
|
|
121
|
+
| `RateLimited` | 429 | yes, honours `Retry-After` |
|
|
122
|
+
| `UploadCapacityFull` | 503 — upload slots full | yes, honours `Retry-After` |
|
|
123
|
+
| `Exhausted` | 502 — the router spent every lever | **no** |
|
|
124
|
+
| `TransportError` | never reached the server | yes |
|
|
125
|
+
|
|
126
|
+
`Exhausted` is not retryable on purpose. The router already climbed the model
|
|
127
|
+
ladder, repaired and re-called before answering — each of those attempts billed
|
|
128
|
+
**your** provider key. Asking again spends the same levers for the same failure.
|
|
129
|
+
The message names what it tried.
|
|
130
|
+
|
|
131
|
+
Retries use exponential backoff with deterministic jitter and a wall-clock
|
|
132
|
+
budget, and never extend past your own deadline. Configure with `RetryPolicy`.
|
|
133
|
+
|
|
134
|
+
## Configuration
|
|
135
|
+
|
|
136
|
+
| | |
|
|
137
|
+
|---|---|
|
|
138
|
+
| `ROUTER_API_KEY` | your Meter-SPW key (or pass `api_key=`) |
|
|
139
|
+
| `ROUTER_BASE_URL` | defaults to `https://spw.scitechsolutions.ai` |
|
|
140
|
+
|
|
141
|
+
The SDK never sees a provider key. Those are stored once in the dashboard and
|
|
142
|
+
used by the router on your behalf — the only credential here spends credit, not
|
|
143
|
+
inference.
|
|
144
|
+
|
|
145
|
+
## Support
|
|
146
|
+
|
|
147
|
+
support@scitechsolutions.ai — quote `RelayResult.verdict_id` or the
|
|
148
|
+
`request_id` from an error and we can find the exact call.
|
|
149
|
+
|
|
150
|
+
---
|
|
151
|
+
|
|
152
|
+
© S&T Integrated Solutions, LLC — MIT licensed.
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
[build-system]
|
|
2
|
+
requires = ["hatchling"]
|
|
3
|
+
build-backend = "hatchling.build"
|
|
4
|
+
|
|
5
|
+
# ONE version literal, read from the source. See src/meterspw/_version.py.
|
|
6
|
+
[tool.hatch.version]
|
|
7
|
+
path = "src/meterspw/_version.py"
|
|
8
|
+
|
|
9
|
+
[project]
|
|
10
|
+
name = "meterspw-sdk"
|
|
11
|
+
dynamic = ["version"]
|
|
12
|
+
description = "Meter-SPW-v1.0 — deterministic LLM routing with metered savings (BYOK)"
|
|
13
|
+
readme = "README.md"
|
|
14
|
+
requires-python = ">=3.10"
|
|
15
|
+
license = { file = "LICENSE" }
|
|
16
|
+
authors = [{ name = "S&T Integrated Solutions, LLC", email = "support@scitechsolutions.ai" }]
|
|
17
|
+
keywords = ["llm", "router", "byok", "cost", "openai", "anthropic"]
|
|
18
|
+
classifiers = [
|
|
19
|
+
"Development Status :: 4 - Beta",
|
|
20
|
+
"Intended Audience :: Developers",
|
|
21
|
+
"License :: OSI Approved :: MIT License",
|
|
22
|
+
"Programming Language :: Python :: 3.10",
|
|
23
|
+
"Programming Language :: Python :: 3.11",
|
|
24
|
+
"Programming Language :: Python :: 3.12",
|
|
25
|
+
"Typing :: Typed",
|
|
26
|
+
]
|
|
27
|
+
# ZERO runtime dependencies, deliberately. This package carries a CREDENTIAL,
|
|
28
|
+
# and every transitive dependency is code that could read it — an SDK ends up
|
|
29
|
+
# installed in environments its authors never see. The synchronous client's
|
|
30
|
+
# transport is `urllib` from the standard library, so installing this package
|
|
31
|
+
# pulls nothing into an environment its author never chose.
|
|
32
|
+
dependencies = []
|
|
33
|
+
|
|
34
|
+
[project.optional-dependencies]
|
|
35
|
+
# The ASYNC client only. `MeterSpwClient` never needs this, so a caller who
|
|
36
|
+
# does not use asyncio never installs it — which is the whole point of the
|
|
37
|
+
# extra rather than a plain dependency.
|
|
38
|
+
async = ["httpx>=0.24"]
|
|
39
|
+
dev = ["pytest>=7", "pytest-asyncio>=0.23", "httpx>=0.24", "ruff>=0.5", "mypy>=1.8"]
|
|
40
|
+
|
|
41
|
+
[project.urls]
|
|
42
|
+
Homepage = "https://spw.scitechsolutions.ai"
|
|
43
|
+
|
|
44
|
+
[tool.hatch.build.targets.wheel]
|
|
45
|
+
packages = ["src/meterspw"]
|
|
46
|
+
|
|
47
|
+
[tool.ruff]
|
|
48
|
+
line-length = 100
|
|
49
|
+
target-version = "py310"
|
|
50
|
+
|
|
51
|
+
[tool.mypy]
|
|
52
|
+
python_version = "3.10"
|
|
53
|
+
strict = true
|
|
54
|
+
|
|
55
|
+
[tool.pytest.ini_options]
|
|
56
|
+
# The async tests are plain coroutines; without this every one of them is
|
|
57
|
+
# collected, skipped with a warning, and reports as a pass.
|
|
58
|
+
asyncio_mode = "auto"
|
|
@@ -0,0 +1,56 @@
|
|
|
1
|
+
"""Meter-SPW-v1.0 — deterministic LLM routing with metered savings.
|
|
2
|
+
|
|
3
|
+
from meterspw import MeterSpwClient
|
|
4
|
+
|
|
5
|
+
with MeterSpwClient() as spw:
|
|
6
|
+
out = spw.relay("Hello", model="claude-sonnet-5", max_tokens=64)
|
|
7
|
+
print(out.content, "saved", out.savings.usd)
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
from ._version import __version__
|
|
11
|
+
from .client import DEFAULT_BASE_URL, MeterSpwClient
|
|
12
|
+
from .errors import (
|
|
13
|
+
ApiError,
|
|
14
|
+
AuthError,
|
|
15
|
+
ConfigError,
|
|
16
|
+
Exhausted,
|
|
17
|
+
MeterSpwError,
|
|
18
|
+
RateLimited,
|
|
19
|
+
TransportError,
|
|
20
|
+
UploadCapacityFull,
|
|
21
|
+
)
|
|
22
|
+
from .models import Balance, RelayResult, Savings
|
|
23
|
+
from .retry import RetryPolicy
|
|
24
|
+
|
|
25
|
+
__all__ = [
|
|
26
|
+
"DEFAULT_BASE_URL",
|
|
27
|
+
"ApiError",
|
|
28
|
+
"AuthError",
|
|
29
|
+
"Balance",
|
|
30
|
+
"ConfigError",
|
|
31
|
+
"Exhausted",
|
|
32
|
+
"MeterSpwClient",
|
|
33
|
+
"MeterSpwError",
|
|
34
|
+
"RateLimited",
|
|
35
|
+
"RelayResult",
|
|
36
|
+
"RetryPolicy",
|
|
37
|
+
"Savings",
|
|
38
|
+
"TransportError",
|
|
39
|
+
"UploadCapacityFull",
|
|
40
|
+
"__version__",
|
|
41
|
+
]
|
|
42
|
+
|
|
43
|
+
|
|
44
|
+
def __getattr__(name: str) -> object: # pragma: no cover - thin lazy shim
|
|
45
|
+
"""Expose `AsyncMeterSpwClient` without importing httpx eagerly.
|
|
46
|
+
|
|
47
|
+
The synchronous client has no dependencies — this package carries a
|
|
48
|
+
credential, and every transitive dependency is code that could read it.
|
|
49
|
+
Importing `meterspw` must therefore never pull `httpx`, so the async
|
|
50
|
+
client is resolved only when someone actually asks for it.
|
|
51
|
+
"""
|
|
52
|
+
if name == "AsyncMeterSpwClient":
|
|
53
|
+
from .aio import AsyncMeterSpwClient
|
|
54
|
+
|
|
55
|
+
return AsyncMeterSpwClient
|
|
56
|
+
raise AttributeError(f"module {__name__!r} has no attribute {name!r}")
|
|
@@ -0,0 +1,10 @@
|
|
|
1
|
+
"""The ONE version literal in this package.
|
|
2
|
+
|
|
3
|
+
`pyproject.toml` reads it via `[tool.hatch.version]`, the User-Agent is built
|
|
4
|
+
from it, and the release workflow asserts the git tag matches it. AGaaS's
|
|
5
|
+
release once compared the tag against two of four copies and the two it
|
|
6
|
+
skipped were the two that were wrong — so there is exactly one, and a test
|
|
7
|
+
keeps it that way.
|
|
8
|
+
"""
|
|
9
|
+
|
|
10
|
+
__version__ = "0.2.0"
|