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
pip install opsen
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:
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:
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
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 |
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:
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.
Download files
Download the file for your platform. If you're not sure which to choose, learn more about installing packages.
Source Distribution
Built Distribution
Filter files by name, interpreter, ABI, and platform.
If you're not sure about the file name format, learn more about wheel file names.
Copy a direct link to the current filters
File details
Details for the file opsen-0.1.4.tar.gz.
File metadata
- Download URL: opsen-0.1.4.tar.gz
- Upload date:
- Size: 13.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
ef4b11c5103577dc130e2897be179a68dcd49aeee01cff991fa6cc1f21a60a7f
|
|
| MD5 |
cec457b754dcc8778c4f2472260149c9
|
|
| BLAKE2b-256 |
fd27ab9731ab1e517a0da235aeccac5d8836126765d806969040229a82dfb7d3
|
Provenance
The following attestation bundles were made for opsen-0.1.4.tar.gz:
Publisher:
release.yml on md322613/opsen
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
opsen-0.1.4.tar.gz -
Subject digest:
ef4b11c5103577dc130e2897be179a68dcd49aeee01cff991fa6cc1f21a60a7f - Sigstore transparency entry: 2754451882
- Sigstore integration time:
-
Permalink:
md322613/opsen@c1f831ea612c86822c0556481d2b1de7639855b0 -
Branch / Tag:
refs/tags/v0.1.4 - Owner: https://github.com/md322613
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c1f831ea612c86822c0556481d2b1de7639855b0 -
Trigger Event:
push
-
Statement type:
File details
Details for the file opsen-0.1.4-py3-none-any.whl.
File metadata
- Download URL: opsen-0.1.4-py3-none-any.whl
- Upload date:
- Size: 13.5 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via:
twine/7.0.0 CPython/3.13.14
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
4e017fe8300922bad292360775edde61bd783169184b8b6a3c154b71fbcc1ca7
|
|
| MD5 |
13e06a0295143be0a859c712f372b68c
|
|
| BLAKE2b-256 |
0e10179ddeb5bd42c99d6a82ffc58b8481fd6b150731a3efba723a508256d2d0
|
Provenance
The following attestation bundles were made for opsen-0.1.4-py3-none-any.whl:
Publisher:
release.yml on md322613/opsen
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
opsen-0.1.4-py3-none-any.whl -
Subject digest:
4e017fe8300922bad292360775edde61bd783169184b8b6a3c154b71fbcc1ca7 - Sigstore transparency entry: 2754451884
- Sigstore integration time:
-
Permalink:
md322613/opsen@c1f831ea612c86822c0556481d2b1de7639855b0 -
Branch / Tag:
refs/tags/v0.1.4 - Owner: https://github.com/md322613
-
Access:
private
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
release.yml@c1f831ea612c86822c0556481d2b1de7639855b0 -
Trigger Event:
push
-
Statement type: