Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
14 changes: 14 additions & 0 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -221,6 +221,20 @@ with budget("$5/hr + 100 calls/hr", name="api-tier", backend=backend) as b:

Works with `AsyncRedisBackend` for async workflows. Circuit breaker built in — configurable threshold + cooldown. Fail-open or fail-closed.

### Per-user / per-tenant enforcement

```python
from shekel.backends.redis import RedisBackend

backend = RedisBackend() # reads REDIS_URL from env

with budget(max_usd=0.10, tenant_id=user.id, name="api", backend=backend) as b:
run_agent()
# Each user gets their own isolated $0.10 cap — same Redis, zero per-tenant config
```

Quota management: `backend.set_tenant_limit(...)`, `backend.get_tenant_spend(...)`, `backend.reset_tenant(...)`, `backend.list_tenants(...)`. Inspect from the command line with `shekel tenants list --name api`.

### Rolling-window rate limits

```python
Expand Down
46 changes: 46 additions & 0 deletions docs/api-reference.md
Original file line number Diff line number Diff line change
Expand Up @@ -48,6 +48,9 @@ def budget(
| `loop_guard_window_seconds` | `float` | `60.0` | Rolling window duration in seconds. `0` = all-time cap (no rolling window). Only applies when `loop_guard=True`. |
| `max_velocity` | `str \| None` | `None` | Spend velocity cap. Format: `"$<amount>/<unit>"` (e.g. `"$0.50/min"`, `"$5/hr"`). Raises `SpendVelocityExceededError` when the burn rate exceeds this threshold. |
| `warn_velocity` | `str \| None` | `None` | Soft velocity warning threshold. Same format as `max_velocity`. Must be less than `max_velocity`. Fires `on_warn` callback when crossed; does not raise. |
| `tenant_id` | `str \| None` | `None` | Tenant or user identifier for per-tenant spend isolation. When set, Redis state is namespaced under `shekel:tb:{name}:{tenant_id}`. Requires `name` and `backend`. Empty string raises `ValueError`. |
| `backend` | `RedisBackend \| AsyncRedisBackend \| None` | `None` | Redis backend for distributed or per-tenant enforcement. Required when `tenant_id` is set. |
| `window_seconds` | `float \| None` | `None` | Rolling-window duration in seconds. Required (or inferred from a spec string) for temporal budgets. Default when `tenant_id` is set: `86400 * 30` (30 days). |

### Returns

Expand Down Expand Up @@ -172,6 +175,7 @@ The budget context manager object.
| `switched_at_usd` | `float \| None` | USD spent when fallback occurred, or `None`. |
| `fallback_spent` | `float` | USD spent on the fallback model. |
| `loop_guard_counts` | `dict[str, int]` | Per-tool call counts recorded by the loop guard. Empty dict when `loop_guard=False`. Keys are tool names; values are total calls recorded within the current window. |
| `tenant_id` | `str \| None` | Tenant identifier passed to `budget()`, or `None` if not set. |

### Nested Budget Properties {#nested-budget-properties}

Expand Down Expand Up @@ -444,6 +448,38 @@ with budget("$5/hr + 100 calls/hr", name="api-tier", backend=backend) as b:

**Raises:** `BudgetConfigMismatchError` if `budget_name` is already registered with different limits or windows.

### Per-Tenant Methods

| Method | Returns | Description |
|---|---|---|
| `get_tenant_spend(name, tenant_id)` | `float` | Current window spend for the tenant. Returns `0.0` if unknown. |
| `get_tenant_limit(name, tenant_id)` | `float \| None` | Active spend limit for the tenant. Returns `None` if no limit recorded. |
| `set_tenant_limit(name, tenant_id, max_usd)` | `None` | Override the tenant's spend limit without resetting accumulated spend. |
| `reset_tenant(name, tenant_id)` | `None` | Zero out accumulated spend while preserving the limit. |
| `list_tenants(name)` | `list[str]` | All tenant IDs that have recorded spend for the budget name. |

```python
from shekel.backends.redis import RedisBackend

backend = RedisBackend()

# Inspect a tenant
spent = backend.get_tenant_spend(name="api", tenant_id="user-42")
limit = backend.get_tenant_limit(name="api", tenant_id="user-42")

# Adjust quota
backend.set_tenant_limit(name="api", tenant_id="user-42", max_usd=0.50)

# Reset at billing period rollover
backend.reset_tenant(name="api", tenant_id="user-42")

# Enumerate all tenants
for tid in backend.list_tenants(name="api"):
print(tid, backend.get_tenant_spend(name="api", tenant_id=tid))
```

See [Per-Tenant Budgets](usage/per-tenant-budgets.md) for the full guide.

---

## `AsyncRedisBackend`
Expand All @@ -461,6 +497,16 @@ async with budget("$5/hr", name="api", backend=backend) as b:

Constructor and parameters are identical to `RedisBackend`.

All five per-tenant methods are available as coroutines:

```python
spent = await backend.get_tenant_spend(name="api", tenant_id="user-42")
limit = await backend.get_tenant_limit(name="api", tenant_id="user-42")
await backend.set_tenant_limit(name="api", tenant_id="user-42", max_usd=0.50)
await backend.reset_tenant(name="api", tenant_id="user-42")
tenants = await backend.list_tenants(name="api")
```

---

## `@with_budget`
Expand Down
21 changes: 21 additions & 0 deletions docs/usage/distributed-budgets.md
Original file line number Diff line number Diff line change
Expand Up @@ -291,8 +291,29 @@ See [Docker & Containers](../docker.md) for a full production setup.

---

## Per-Tenant Namespacing

When `tenant_id` is passed to `budget()`, shekel appends it to the Redis key so each tenant gets fully isolated state:

```
shekel:tb:{name}:{tenant_id} # with tenant_id
shekel:tb:{name} # without tenant_id (shared)
```

This means you can enforce per-user spend caps in a SaaS app with a single shared `RedisBackend`:

```python
with budget(max_usd=0.10, tenant_id=user.id, name="api", backend=backend) as b:
run_agent()
```

See **[Per-Tenant Budgets](per-tenant-budgets.md)** for the full guide, including quota management methods and the `shekel tenants` CLI.

---

## Next Steps

- **[Per-Tenant Budgets](per-tenant-budgets.md)** - Per-user spend isolation for SaaS apps
- **[Temporal Budgets](temporal-budgets.md)** - Rolling-window budget fundamentals
- **[Docker & Containers](../docker.md)** - Full containerized production setup
- **[API Reference](../api-reference.md)** - Complete parameter reference
Loading
Loading