Metadata-Version: 2.5
Name: meterspw-sdk
Version: 0.2.0
Summary: Meter-SPW-v1.0 — deterministic LLM routing with metered savings (BYOK)
Project-URL: Homepage, https://spw.scitechsolutions.ai
Author-email: "S&T Integrated Solutions, LLC" <support@scitechsolutions.ai>
License: MIT License
        
        Copyright (c) 2026 S&T Integrated Solutions, LLC
        
        Permission is hereby granted, free of charge, to any person obtaining a copy
        of this software and associated documentation files (the "Software"), to deal
        in the Software without restriction, including without limitation the rights
        to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
        copies of the Software, and to permit persons to whom the Software is
        furnished to do so, subject to the following conditions:
        
        The above copyright notice and this permission notice shall be included in all
        copies or substantial portions of the Software.
        
        THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
        IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
        FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
        AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
        LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
        OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
        SOFTWARE.
        
        https://scitechsolutions.ai
License-File: LICENSE
Keywords: anthropic,byok,cost,llm,openai,router
Classifier: Development Status :: 4 - Beta
Classifier: Intended Audience :: Developers
Classifier: License :: OSI Approved :: MIT License
Classifier: Programming Language :: Python :: 3.10
Classifier: Programming Language :: Python :: 3.11
Classifier: Programming Language :: Python :: 3.12
Classifier: Typing :: Typed
Requires-Python: >=3.10
Provides-Extra: async
Requires-Dist: httpx>=0.24; extra == 'async'
Provides-Extra: dev
Requires-Dist: httpx>=0.24; extra == 'dev'
Requires-Dist: mypy>=1.8; extra == 'dev'
Requires-Dist: pytest-asyncio>=0.23; extra == 'dev'
Requires-Dist: pytest>=7; extra == 'dev'
Requires-Dist: ruff>=0.5; extra == 'dev'
Description-Content-Type: text/markdown

# meterspw-sdk

Python client for **Meter-SPW** — deterministic LLM routing with metered savings.

You bring your own provider keys. Meter-SPW routes each prompt to the cheapest
model that can answer it, caches what repeats, and meters what it saved.

```bash
pip install meterspw-sdk
```

Zero runtime dependencies — the default transport is `urllib` from the standard
library.

## Use

```python
from meterspw import MeterSpwClient

with MeterSpwClient() as spw:            # key from ROUTER_API_KEY
    out = spw.relay(
        "Summarise this contract in three bullets.",
        model="claude-sonnet-5",
        max_tokens=2000,
    )
    print(out.content)
    print(out.model_used)                # what ACTUALLY answered
    print(out.savings.usd)               # what that saved you
```

`model` is what you want; `model_used` is what answered. The difference is the
product.

### Attachments

```python
out = spw.relay_multipart(
    "Summarise the attached report.",
    model="claude-sonnet-5",
    max_tokens=2000,
    files=["report.pdf"],
)
```

### Know where you stand

```python
b = spw.balance()
print(b.usd, b.accrued_micro_usd, b.remaining_usd)
if b.passthrough:
    print("credit exhausted — calls still work, but nothing is being routed")
```

**Check `passthrough`.** At zero credit the router degrades to a transparent
proxy: your calls keep succeeding, with no routing, no cache and no savings.
Nothing fails, which is exactly why it is easy to miss.

**Three numbers, because two books.** `usd` is the ledger. `accrued` is this
period's fees, which have not been taken yet — they settle at the monthly
close. `remaining` is what you should plan against.

### Your receipts

```python
rows = spw.verdicts(window="7d", q="haiku")      # the last 7 days, searched
```

`window` is one of `30m 1h 6h 12h 24h 7d 4w 12m` (a period ending now); `q`
matches the decision, the requested model and the served model. The router
applies both, so the same two values count and export exactly these rows:

```bash
curl -H "Authorization: Bearer $ROUTER_API_KEY" \
  "https://spw.scitechsolutions.ai/v1/verdicts/count?window=7d&q=haiku"
curl -H "Authorization: Bearer $ROUTER_API_KEY" -o verdicts-7d.csv \
  "https://spw.scitechsolutions.ai/v1/verdicts.csv?window=7d&q=haiku"
```

The export is refused, with the count, when more than 200 000 rows match —
narrow the window or the search.

### Long requests

A proxy between you and the router closes a connection that stays silent for
about 100 seconds, and the router is silent until the verified answer exists.
So `relay()` asks for keep-alive delivery: the router answers within a second,
keeps the line warm every 15 seconds while the model works, and delivers the
whole answer at the end — the same result, nothing to change on your side.

`timeout` bounds **silence** on the line, not the whole call. A long answer is
bounded by the router's own request limit (the operator's
`ROUTER_REQUEST_TIMEOUT_SECS`), not by this number.

Over raw HTTP, send the header yourself and read the last `data:` line:

```bash
curl -N -X POST https://spw.scitechsolutions.ai/v1/relay \
  -H "Authorization: Bearer $ROUTER_API_KEY" \
  -H "Accept: text/event-stream" \
  -H "Content-Type: application/json" \
  -d '{"prompt": "Summarise this contract in three bullets.", "model": "claude-sonnet-5", "max_tokens": 2000}'
# -N is optional: curl prints each line as it arrives instead of all at the end.
```

```
: keep-alive          ← every 15 s while the model works
event: result
data: {"decision": "relayed", "model_used": "…", "content": "…", "verdict_id": "…", "latency_ms": 1840, …}
```

An `event: error` line with `{"status": 502, "error": "…"}` takes the place
of `result` if the router fails late. `/v1/relay-multipart` takes the same
header. Without it the reply is one JSON body — and a call longer than about
100 seconds dies at the proxy.

## Errors

| Exception | When | Retry? |
|---|---|---|
| `AuthError` | 401/403 — key wrong or revoked | no |
| `RateLimited` | 429 | yes, honours `Retry-After` |
| `UploadCapacityFull` | 503 — upload slots full | yes, honours `Retry-After` |
| `Exhausted` | 502 — the router spent every lever | **no** |
| `TransportError` | never reached the server | yes |

`Exhausted` is not retryable on purpose. The router already climbed the model
ladder, repaired and re-called before answering — each of those attempts billed
**your** provider key. Asking again spends the same levers for the same failure.
The message names what it tried.

Retries use exponential backoff with deterministic jitter and a wall-clock
budget, and never extend past your own deadline. Configure with `RetryPolicy`.

## Configuration

| | |
|---|---|
| `ROUTER_API_KEY` | your Meter-SPW key (or pass `api_key=`) |
| `ROUTER_BASE_URL` | defaults to `https://spw.scitechsolutions.ai` |

The SDK never sees a provider key. Those are stored once in the dashboard and
used by the router on your behalf — the only credential here spends credit, not
inference.

## Support

support@scitechsolutions.ai — quote `RelayResult.verdict_id` or the
`request_id` from an error and we can find the exact call.

---

© S&T Integrated Solutions, LLC — MIT licensed.
