Metadata-Version: 2.4
Name: opsen
Version: 0.1.3
Summary: Run agents anywhere, on one bill.
Author: opsen
License: Apache-2.0
Project-URL: Homepage, https://opsen.dev
Project-URL: Documentation, https://opsen.dev/docs
Project-URL: Source, https://github.com/opsen-dev/opsen
Keywords: agents,sandbox,llm,e2b,modal
Requires-Python: >=3.10
Description-Content-Type: text/markdown

# opsen

Run your agents. Know what each one cost.

You point a session at a task ID; we run it, meter it, and tell you what that
task cost across compute and tokens together. Your agent code does not change —
not one line, not one import.

---

## Quickstart

```bash
pip install opsen
```

```python
from opsen import Client

c = Client(api_key=os.environ["OPSEN_KEY"])

with c.session("job_8812", labels={"customer": "acme"}, budget_usd=2.00) as s:
    s.fs.write("agent.py", open("agent.py").read())
    s.exec("python3 agent.py")
    print(s.cost())
```

```
Cost(total=$0.019806 compute=$0.000006 tokens=$0.019800 calls=3)
```

`agent.py` is unmodified. Its Anthropic or OpenAI client picks up a base URL we
inject into the session, so every model call is attributed without you
instrumenting anything.

The query most people actually came for:

```python
c.costs(group_by="labels.customer")
# [{"labels.customer": "acme", "total_usd": 412.55, "tasks": 1104}, ...]
```

Label sessions with whatever your business is organised by — customer, tenant,
workflow, feature — and group on it. We do not model your business; you attach
the labels and we join on them.

---

## What you don't have to do

**Manage session lifecycle.** TTL is mandatory and enforced. Idle sessions
suspend and resume transparently. Orphans are reaped. Wedged sessions are
detectable (`c.stuck_sessions()`) rather than something you discover on an
invoice.

**Instrument your agent.** No wrapper, no callback, no SDK inside the sandbox.

**Hold provider keys in the sandbox.** See below — this is the part worth
reading even if you skip the rest.

---

## Credentials

Register your provider key once:

```python
k = c.add_provider_key("anthropic", "sk-ant-...")
s = c.session("job_1", provider_key_id=k["id"])
```

The key is sealed at rest and **never enters the sandbox**. Your agent gets a
session token instead: scoped to one session, capped by that session's budget,
dead when the session ends, revocable, useless anywhere else.

The token arrives as `ANTHROPIC_API_KEY` (and the OpenAI and Google
equivalents) alongside a base URL, so your agent's SDK sends it the way it
sends any API key and nothing in your code changes. It is a credential, so it
travels in a header rather than the URL — a token in a path ends up in every
access log and proxy record between you and us, and rotating it would mean
rotating a URL you have embedded.

This matters because agent code is not trustworthy code. A prompt injection or
a bad dependency that exfiltrates a provider key gets a credential with no
spending limit and no expiry, usable from anywhere, against every project on
your account. The same attack against a session token gets the remaining budget
on one session.

Inbound credential headers from the sandbox are stripped, so an agent cannot
route around metering by supplying its own key.

---

## Budgets

```python
s = c.session("job_1", budget_usd=2.00)
```

Enforced at the model call, not reported afterwards. When the cap would be
breached the call returns 402 and the session is terminated.

**One honest limitation.** The cap is a pre-flight estimate. Output is bounded
exactly by `max_tokens`; input is not — we can only see the request body, and
the provider also counts system prompts, tool schemas and cache blocks that
never reach us. We start pessimistic and calibrate against what the provider
actually reports for your traffic. In practice this lands a few percent under
the cap. It is not a guarantee that you will never exceed a budget by a cent;
it is a guarantee that a runaway agent stops.

While a budgeted session is still calibrating, its first call runs alone and
concurrent calls get 429 with `budget_calibrating`. You cannot bound N calls
whose individual cost you cannot yet bound.

Tenant-level ceilings — spend per window, concurrent sessions, requests per
minute — bound the account rather than the session, so a leaked API key cannot
mint unlimited budgeted sessions.

---

## Errors, and why the distinction matters

A 429 from us and a 429 from your model provider need **opposite** responses.
Ours means slow down or raise a limit. Theirs means retry with backoff.
Every response carries `x-opsen-error-source`, and the SDK turns it into types:

| Exception | Cause | Retry? |
|---|---|---|
| `BudgetExceeded` | your session cap | **Never.** Retrying a cost control is how it becomes a cost leak |
| `SessionGone` | terminated, expired, reaped | No — start a new session |
| `TenantLimited` | our ceiling | Only if `.retryable` — true for rate, false for spend |
| `ProviderError` | the model provider failed | Yes, with backoff |

```python
try:
    result = s.call_model(payload)
except BudgetExceeded as e:
    alert(f"job {s.task_id} hit its cap at ${e.body['would_reach_usd']}")
except TenantLimited as e:
    if e.retryable: ...
```

`call_model` implements these rules already, including honouring `retry-after`
and jittering backoff. Most callers never touch it — your agent's own SDK goes
through the injected base URL — but it exists so the rules are written down
somewhere executable.

Failed calls are never billed and are reported separately as `failed_calls`, so
your cost-per-call has the right denominator.

---

## One worker, many jobs

A long-lived worker can be re-pointed. Cost stays with the job that incurred it:

```python
w = c.session("job_A")
w.exec("python3 worker.py")
w.retag("job_B")
w.exec("python3 worker.py")

c.task_cost("job_A")   # only job_A's compute and tokens
c.task_cost("job_B")
```

---

## Idle suspension

Agents spend most of their wall clock waiting on model responses, and billing
that as active compute is the default failure of every full-duration provider.
We suspend on idle and resume transparently.

Resuming costs roughly 180 ms on the next call, so this is a trade rather than
a free win. Named policies, because the right answer depends on how you value
latency:

| policy | idle window | for |
|---|---|---|
| `interactive` *(default)* | 15 min | user-facing agents where resume latency shows |
| `balanced` | 3 min | |
| `batch` | 30 s | long-idle background work |
| `never` | — | |

The default is conservative on purpose. At 2 vCPU / 4 GiB, suspending over a
two-minute idle window saves about half a cent and costs 180 ms of first-token
time — a bad trade for anything a human is waiting on.

---

## What happens if opsen is down

Your model calls go through our gateway, so this is a fair question to ask
before you route production traffic through anyone. The answer depends on
whether you asked for a cost guarantee.

**No budget set — we fail open.** The call is forwarded and served. Usage for
that window may not be recorded, and the response carries
`x-opsen-degraded: accounting` so a client that cares can buffer and reconcile.
You never asked us to guard anything, so we do not stand in the way.

**Budget set — we fail closed.** The call is refused with `503
cost_control_unavailable` rather than made without the cap you asked for.

That second one is deliberate and it is the less convenient choice, so here is
the reasoning. Outages correlate with runaway spend — provider degradation,
retry storms, agents looping on errors are the same conditions that break us
and the same ones that empty a budget. A cap that disappears exactly when it is
needed is not a cap. And the security model depends on it: your sandbox holds a
session token instead of your provider key precisely BECAUSE the token is
bounded by the cap. Failing open would quietly hand back an unbounded
credential.

So: set a budget on sessions where a wrong number costs you more than a paused
minute, and leave it unset where availability matters more. You choose per
session, and the behaviour is the same whether we are healthy or not.

---

## Runtimes

`runtime="auto"` picks a backend. You can pin one. Nothing else about the
backend is visible through the API, which is deliberate: the same code runs
against any of them, and you are not writing against a particular provider's
semantics.
