Metadata-Version: 2.4
Name: byok
Version: 0.4.0
Summary: BYO - Python SDK for BYOK
License: FSL-1.1-MIT
Project-URL: Homepage, https://github.com/treadiehq/byo
Project-URL: Repository, https://github.com/treadiehq/byo/tree/main/packages/sdk-python
Project-URL: Issues, https://github.com/treadiehq/byo/issues
Keywords: byo,byok,openai,anthropic,stripe,planetscale,proxy,api-keys
Requires-Python: >=3.9
Description-Content-Type: text/markdown
Requires-Dist: httpx>=0.25.0

# BYO PYTHON SDK for BYOK

Official Python SDK for BYO — BYOK (Bring Your Own Key).

## Installation

```bash
pip install byok
```

## Quick Start

```python
import os
from byo import BYOK

byok = BYOK(api_key=os.environ["BYOK_API_KEY"])

# Proxy an OpenAI request
openai = byok.openai(ref_id="customer_123")
response = openai.responses.create(
    model="gpt-4.1",
    input="Hello from BYOK!"
)
print(response)
```

## OpenAI

### Responses API

```python
openai = byok.openai(ref_id="customer_123")
response = openai.responses.create(model="gpt-4.1", input="What is BYOK?")
```

### Chat Completions API

```python
openai = byok.openai(ref_id="customer_123")
response = openai.chat.completions.create(
    model="gpt-4.1",
    messages=[{"role": "user", "content": "Hello!"}]
)
```

## Anthropic

### Messages API

```python
claude = byok.anthropic(ref_id="customer_123")
response = claude.messages.create(
    model="claude-sonnet-4-20250514",
    max_tokens=1024,
    messages=[{"role": "user", "content": "Hello!"}]
)
```

## Google AI Studio

### Generate Content

```python
gemini = byok.google(ref_id="customer_123")
response = gemini.generate_content.create(
    model="gemini-2.0-flash",
    contents=[{"parts": [{"text": "Hello!"}]}]
)
```

## Azure OpenAI

### Chat Completions

```python
azure = byok.azure_openai(ref_id="customer_123")
response = azure.chat.completions.create(
    model="gpt-4",
    messages=[{"role": "user", "content": "Hello!"}]
)
```

Azure OpenAI requires `provider_config` when connecting the key:

```python
byok.keys.connect(
    provider="azure-openai",
    ref_id="customer_123",
    provider_key="your-azure-api-key",
    provider_config={
        "baseUrl": "https://your-resource.openai.azure.com",
        "deploymentName": "gpt-4",
    },
)
```

## AWS Bedrock

### Converse

```python
bedrock = byok.bedrock(ref_id="customer_123")
response = bedrock.converse.create(
    modelId="anthropic.claude-3-haiku-20240307-v1:0",
    messages=[{"role": "user", "content": [{"text": "Hello!"}]}]
)
```

AWS Bedrock requires `provider_config` when connecting the key:

```python
byok.keys.connect(
    provider="bedrock",
    ref_id="customer_123",
    provider_key="your-aws-secret-access-key",
    provider_config={
        "accessKeyId": "AKIA...",
        "region": "us-east-1",
    },
)
```

## Key Management

```python
# Connect a provider key
byok.keys.connect(
    provider="openai",
    ref_id="customer_123",
    provider_key="sk-..."
)

# Validate a stored key
result = byok.keys.validate(provider="openai", ref_id="customer_123")

# Revoke a stored key
byok.keys.revoke(provider="openai", ref_id="customer_123")
```

## Usage and cost

Every proxied LLM call captures the model, prompt/completion tokens and an
estimated USD cost. Read it back per customer (`ref_id`) to power your own
billing UI:

```python
from datetime import datetime, timedelta, timezone

usage = byok.usage.get(ref_id="customer_123")
print(usage["totals"]["costUsd"], usage["byModel"], usage["daily"])

# Filter by date range and provider; pass datetime or ISO string.
last_7d = byok.usage.get(
    ref_id="customer_123",
    provider="openai",
    since=datetime.now(timezone.utc) - timedelta(days=7),
)

# 24-hour rollup used by the dashboard overview.
stats = byok.usage.stats()
```

The cost is calculated server-side using BYO's built-in pricing table. If
you call a model BYO doesn't have a price for (a fine-tune, a fresh model
release, an OpenAI-compatible local server), it'll show up in
`usage["unknownModels"]` so you know exactly what to add — and the cost
row will be `None` until you patch it:

```python
print(usage["unknownModels"])
# [{'provider': 'openai', 'model': 'ft:gpt-4o-...:abc',
#   'requests': 142, 'totalTokens': 184230}]
```

### Custom model prices

Patch in custom prices per project — useful for fine-tunes, niche
OpenAI-compatible endpoints, or anything BYO ships without a default for.
The shape mirrors the `BYO_MODEL_PRICING` env var; numbers are USD per
1,000,000 tokens. Changes take effect within ~60s on every API instance.

```python
byok.pricing.update(project_id, {
    "openai": {"my-finetune": {"input": 0.5, "output": 2.0}},
})

current = byok.pricing.get(project_id)

byok.pricing.clear(project_id)  # back to BYO's defaults
```

> **Streaming note:** to capture token counts on streamed OpenAI Chat
> Completions, include `stream_options={"include_usage": True}` in the
> request — otherwise providers don't return a `usage` block and the log
> row will have `tokens` and `costUsd` set to `None`.

## Logs

Inspect individual proxied requests, including the captured tokens and cost:

```python
result = byok.logs.list(ref_id="customer_123", success=False, limit=50)
for entry in result["data"]:
    print(entry["model"], entry["totalTokens"], entry["costUsd"])

log = byok.logs.get(result["data"][0]["id"])
```

## Error Handling

The SDK raises typed exceptions so you can branch on the failure mode without
parsing status codes by hand:

| Class                  | When it's raised                                   |
| ---------------------- | -------------------------------------------------- |
| `AuthenticationError`  | 401 — invalid or missing API key                   |
| `AuthorizationError`   | 403 — key isn't allowed to perform this action     |
| `ValidationError`      | 400 / 422 — bad payload                            |
| `NotFoundError`        | 404 — project / log / endpoint missing             |
| `RateLimitError`       | 429 — exposes `retry_after_ms` from `Retry-After`  |
| `ProviderError`        | Upstream provider (OpenAI, Anthropic, …) rejected  |
| `ServerError`          | 5xx from the BYO API                               |
| `NetworkError`         | Connection failure (`httpx` raised)                |
| `TimeoutError`         | Hit the SDK request timeout                        |
| `BYOKError`            | Base class — catch this for "anything from BYO"    |

```python
from byo import (
    BYOK, BYOKError, AuthenticationError,
    RateLimitError, ProviderError,
)

try:
    response = openai.responses.create(model="gpt-4.1", input="Hello")
except AuthenticationError:
    print("Invalid API key")
except RateLimitError as e:
    print(f"Rate limited; retry in {e.retry_after_ms}ms")
except ProviderError as e:
    print(f"Upstream {e.provider} rejected: {e.message}")
except BYOKError as e:
    print(f"Error {e.status_code}: {e.message}")
```

## Webhooks

Verify webhook deliveries from BYO with one call. The signature is a
constant-time HMAC-SHA256 over the raw request body using the endpoint's
signing secret:

```python
from byo import construct_webhook_event

@app.post("/webhooks/byo")
async def byo_webhook(request):
    body = await request.body()
    try:
        event = construct_webhook_event(
            body,
            request.headers.get("X-BYO-Signature"),
            os.environ["BYO_WEBHOOK_SECRET"],
        )
    except Exception:
        return Response(status_code=400)
    handle_event(event)
    return Response(status_code=200)
```

Need just the boolean? Use `verify_webhook_signature(payload, header, secret)`.

## Project Origin Allowlist

When you ship a publishable key (`byo_pk_*`) into a frontend, lock it down to
the origins that should be allowed to call `/keys/connect` and
`/connect/form`:

```python
byok.origins.update("proj_123", [
    "https://app.example.com",
    "https://staging.example.com",
])

byok.origins.clear("proj_123")  # empty list = unrestricted
```

## Audit Log

Every security-relevant mutation (project changes, key revokes, pricing
updates, origin allowlist edits, webhook config changes) is recorded in an
append-only audit log:

```python
from datetime import datetime, timedelta, timezone

result = byok.audit.list(
    project_id="proj_123",
    action="provider_key.revoked",
    since=datetime.now(timezone.utc) - timedelta(days=1),
)
for entry in result["data"]:
    print(entry["createdAt"], entry["action"], entry["actorId"])
```

## License

[FSL-1.1-MIT](LICENSE)
