Python SDK for NullSpend — FinOps for AI agents
Project description
nullspend
Python SDK for NullSpend — FinOps for AI agents.
Installation
pip install nullspend
Quick Start
from nullspend import NullSpend, CostEventInput
ns = NullSpend(
base_url="https://nullspend.dev",
api_key="ns_live_sk_...",
)
# Report a cost event
ns.report_cost(CostEventInput(
provider="openai",
model="gpt-4o",
input_tokens=1200,
output_tokens=350,
cost_microdollars=5250,
tags={"environment": "production", "agent": "support-bot"},
))
# Check budget status
status = ns.check_budget()
for entity in status.entities:
print(f"{entity.entity_type}/{entity.entity_id}: "
f"${entity.remaining_microdollars / 1_000_000:.2f} remaining")
Features
- Cost event reporting (single and batch)
@track_tooldecorator +track()inline for tool cost tracking- Budget status and listing
- Cost analytics summaries
- Human-in-the-loop action management (create, poll, mark result)
propose_and_wait()high-level orchestrator- Tracked httpx transport with automatic cost tracking and enforcement
- Loop detection for stuck agents (client-side and proxy-side)
- Automatic retries with exponential backoff
- Idempotency keys on mutating requests
- Type hints throughout (py.typed)
Tool Cost Tracking
Track costs for non-LLM tools (API calls, web searches, database queries) with a decorator or inline:
from nullspend import NullSpend
ns = NullSpend(api_key="ns_live_sk_...", base_url="https://nullspend.dev")
# Decorator — reports cost after function executes, measures duration
@ns.track_tool(cost=0.02, tool_name="web_search")
def search(query: str) -> str:
return requests.get(f"https://api.example.com/search?q={query}").text
# Inline — reports cost and returns the result
result = ns.track(call_api(), cost=0.01, tool_name="search")
Costs are reported even if the tool raises an exception (the API call happened, tokens were consumed). Failed calls are tagged with _ns_error: "true" for dashboard filtering.
Optional fields: provider, model, tool_server, tags, customer. Uses batch reporter when configured, falls back to direct reporting.
Loop Detection
Detects agents stuck in infinite loops — repeated identical calls that burn budget without progress.
Proxy users: Loop detection is on by default. If your agent calls the same model with identical content 50+ times in 60 seconds, the proxy returns a 429 with code: "loop_detected". No configuration needed.
SDK users: Opt in with one line:
from nullspend import create_tracked_client
client = create_tracked_client("openai", loop_detection=True)
Customize thresholds:
from nullspend import create_tracked_client, LoopDetectionConfig
client = create_tracked_client("openai", loop_detection=LoopDetectionConfig(
max_calls=100, # higher for batch workloads
window_seconds=120, # wider window
))
Disabling: Set loop_max_calls=0 on the budget entity via the API or dashboard.
Error handling:
| Error | Code | When |
|---|---|---|
LoopDetectedError |
loop_detected |
Same model+content called 50+ times in 60s |
BudgetExceededError |
budget_exceeded |
Budget exhausted |
VelocityExceededError |
velocity_exceeded |
Spend rate exceeds velocity limit |
SessionLimitExceededError |
session_limit_exceeded |
Session spend cap reached |
TagBudgetExceededError |
tag_budget_exceeded |
Tag-level budget exhausted |
MandateViolationError |
mandate_violation |
Model/provider not allowed |
from nullspend import LoopDetectedError, BudgetExceededError
try:
response = client.chat.completions.create(...)
except LoopDetectedError as e:
print(f"Loop detected: {e.model} called {e.call_count} times")
print(f"Detection type: {e.detection_type}") # "per_key" or "aggregate"
except BudgetExceededError as e:
print(f"Budget exceeded: {e.remaining_microdollars} microdollars remaining")
if e.recovery:
print(f"Retryable: {e.recovery['retryable']}")
print(f"Owner action required: {e.recovery['owner_action_required']}")
Every denial error includes an optional recovery dict with machine-readable hints:
| Field | Type | Meaning |
|---|---|---|
retryable |
bool |
Whether the request can succeed if retried later |
owner_action_required |
bool |
Whether a human or config change is needed |
retry_after_seconds |
int | None |
Seconds to wait before retry (retryable denials only) |
docs |
str | None |
Documentation URL for this error type |
recovery is None when connecting to an older proxy that doesn't include it.
Documentation
See the NullSpend docs for full API reference.
Project details
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 nullspend-0.2.1.tar.gz.
File metadata
- Download URL: nullspend-0.2.1.tar.gz
- Upload date:
- Size: 80.4 kB
- Tags: Source
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
259bed9e68ad0b425f418c4554f152bfcff8dee811858104811fa7e9b3e3733e
|
|
| MD5 |
d975945f16a85de74a61f8795a8db465
|
|
| BLAKE2b-256 |
b0d7f7d7431660c5422bee0914566857e9a95a237a51096c38538a397e5682ab
|
Provenance
The following attestation bundles were made for nullspend-0.2.1.tar.gz:
Publisher:
publish-python.yml on NullSpend/nullspend
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nullspend-0.2.1.tar.gz -
Subject digest:
259bed9e68ad0b425f418c4554f152bfcff8dee811858104811fa7e9b3e3733e - Sigstore transparency entry: 1333169086
- Sigstore integration time:
-
Permalink:
NullSpend/nullspend@54ba6154113c893a4e75a1754637f9907aaf187a -
Branch / Tag:
refs/tags/python-sdk-v0.2.1 - Owner: https://github.com/NullSpend
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-python.yml@54ba6154113c893a4e75a1754637f9907aaf187a -
Trigger Event:
push
-
Statement type:
File details
Details for the file nullspend-0.2.1-py3-none-any.whl.
File metadata
- Download URL: nullspend-0.2.1-py3-none-any.whl
- Upload date:
- Size: 42.2 kB
- Tags: Python 3
- Uploaded using Trusted Publishing? Yes
- Uploaded via: twine/6.1.0 CPython/3.13.12
File hashes
| Algorithm | Hash digest | |
|---|---|---|
| SHA256 |
6604e6c6d591a7798f46b3a5e93dba6d4df14e466b661c319ed19fa6af4db215
|
|
| MD5 |
1db08b3364e3fe71ccadac84d912a8ee
|
|
| BLAKE2b-256 |
64ef4b171f76954b4f7d0d23fa8472608d2ca6308c4aca5c1f68b83eb5109084
|
Provenance
The following attestation bundles were made for nullspend-0.2.1-py3-none-any.whl:
Publisher:
publish-python.yml on NullSpend/nullspend
-
Statement:
-
Statement type:
https://in-toto.io/Statement/v1 -
Predicate type:
https://docs.pypi.org/attestations/publish/v1 -
Subject name:
nullspend-0.2.1-py3-none-any.whl -
Subject digest:
6604e6c6d591a7798f46b3a5e93dba6d4df14e466b661c319ed19fa6af4db215 - Sigstore transparency entry: 1333169278
- Sigstore integration time:
-
Permalink:
NullSpend/nullspend@54ba6154113c893a4e75a1754637f9907aaf187a -
Branch / Tag:
refs/tags/python-sdk-v0.2.1 - Owner: https://github.com/NullSpend
-
Access:
public
-
Token Issuer:
https://token.actions.githubusercontent.com -
Runner Environment:
github-hosted -
Publication workflow:
publish-python.yml@54ba6154113c893a4e75a1754637f9907aaf187a -
Trigger Event:
push
-
Statement type: