# Which AI agent spent the money on your OpenAI/Anthropic bill

> Source: <https://github.com/Nu11P01nt3r3xc3pt10n/spaturzu-sdks>
> Published: 2026-06-17 09:30:40+00:00

Open-source client SDKs for [ spaturzu](https://spaturzu.superchiu.org) —
per-agent LLM cost attribution, budget enforcement, and cross-provider
fallback. Wrap your existing provider client (OpenAI, Anthropic, Bedrock,
Gemini, Mistral) and every call is metered, attributed to the agent that made
it, and optionally budget-capped — without changing how you call the model.

Already calling OpenAI, Anthropic, Bedrock, Gemini, or Mistral? Change a single
import — from `import OpenAI from "openai"`

to
`import OpenAI from "@spaturzu/sdk/openai"`

. Construction and call sites stay
exactly the same, and you can attribute each call to the agent that made it:

``` python
import OpenAI from "@spaturzu/sdk/openai";

// Swap one import — reads SPATURZU_API_KEY + OPENAI_API_KEY from env.
const openai = new OpenAI();

// Tag any call with the agent that made it — one line, no closure.
await openai.withAgent("support-triage").chat.completions.create({ /* … */ });
```

That's the whole migration — no new objects, no wrappers around your call
sites. **Python is identical:** `from spaturzu.openai import OpenAI`

, then
`client.with_agent("support-triage").chat.completions.create(...)`

. The same
one-import swap works for every provider — `@spaturzu/sdk/anthropic`

,
`/bedrock`

, `/google`

, `/mistral`

(Python: `spaturzu.anthropic`

, and so on).

Running multi-step workflows? Group several calls under one agent with

`run()`

instead — see[What you can do]below.

| SDK | Package | Docs |
|---|---|---|
TypeScript / Node |
`@spaturzu/sdk` |

[README](/Nu11P01nt3r3xc3pt10n/spaturzu-sdks/blob/main/typescript/README.md)**Python**`spaturzu`

[README](/Nu11P01nt3r3xc3pt10n/spaturzu-sdks/blob/main/python/README.md)**OpenClaw plugin*** experimental*`@spaturzu/openclaw`

[README](/Nu11P01nt3r3xc3pt10n/spaturzu-sdks/blob/main/openclaw/README.md)

⚠️ and not yet ready for production use. APIs may change without notice.`@spaturzu/openclaw`

is a work in progress

Both the TypeScript and Python SDKs treat the underlying provider clients as
**optional dependencies** — install only the ones you actually call.

**Python** — published on PyPI as
[ spaturzu](https://pypi.org/project/spaturzu/) (Python 3.10+):

```
pip install spaturzu                   # core
pip install "spaturzu[openai]"         # with the OpenAI integration
pip install "spaturzu[all]"            # every provider integration
```

**TypeScript / Node** — published on npm as
[ @spaturzu/sdk](https://www.npmjs.com/package/@spaturzu/sdk) (ESM-only):

```
npm install @spaturzu/sdk
# …then whichever provider clients you call (these are normal npm packages):
npm install openai @anthropic-ai/sdk @aws-sdk/client-bedrock-runtime @google/genai @mistralai/mistralai
```

Once installed, every import in this README works as written. `pnpm`

/ `yarn`

accept the same package name (`pnpm add @spaturzu/sdk`

).

**Full docs & guides:**[https://spaturzu.superchiu.org/docs](https://spaturzu.superchiu.org/docs)** TypeScript / Node API:**`typescript/README.md`

**Python API:**`python/README.md`

**OpenClaw plugin**(experimental):`openclaw/README.md`

**For AI tools / LLMs:**[https://spaturzu.superchiu.org/llms.txt](https://spaturzu.superchiu.org/llms.txt)

The examples below all build on this one-time setup. Every later snippet
reuses `spaturzu`

/`sp`

, `openai`

, and `messages`

from here.

``` python
// TypeScript
import { Spaturzu } from "@spaturzu/sdk";
import OpenAI from "openai";

const spaturzu = new Spaturzu({ apiKey: process.env.SPATURZU_API_KEY });
const openai = spaturzu.wrapOpenAI(new OpenAI());

const messages = [{ role: "user", content: "Summarize the latest sales report." }];
python
# Python
import os
from spaturzu import spaturzu
from openai import OpenAI

sp = spaturzu(api_key=os.environ["SPATURZU_API_KEY"])
openai = sp.wrap_openai(OpenAI())

messages = [{"role": "user", "content": "Summarize the latest sales report."}]
```

Wrapping is transparent: the wrapped client has the

same methods and return typesas the original. You keep calling the provider exactly as before — spaturzu just meters each call in the background.

Wrap a block of work in `run("name", …)`

and every model call inside it is
billed to that agent — so the dashboard shows cost *per agent*, not one
undifferentiated total.

``` js
// TypeScript
await spaturzu.run("researcher", async () => {
  await openai.chat.completions.create({ model: "gpt-4o", messages });
});
# Python
with sp.run("researcher"):
    openai.chat.completions.create(model="gpt-4o", messages=messages)
```

Nested `run()`

calls share one run id and extend the agent path, so a
multi-step workflow shows up as a tree (`research › synthesize`

).

``` js
// TypeScript
await spaturzu.run("research", async () => {
  await openai.chat.completions.create({ model: "gpt-4o", messages }); // path: research

  await spaturzu.run("synthesize", async () => {
    await openai.chat.completions.create({ model: "gpt-4o", messages }); // path: research › synthesize
  });
});
# Python
with sp.run("research"):
    openai.chat.completions.create(model="gpt-4o", messages=messages)        # path: research
    with sp.run("synthesize"):
        openai.chat.completions.create(model="gpt-4o", messages=messages)    # path: research › synthesize
```

Set tags globally on the client, or per-frame on a `run()`

. Frame tags merge
with the global ones (inner wins on conflict), so you can break spend down by
any dimension you like.

``` js
// TypeScript
const spaturzu = new Spaturzu({
  apiKey: process.env.SPATURZU_API_KEY,
  tags: { env: "prod", team: "growth" },        // on every call
});

await spaturzu.run("billing-agent", { tags: { customer: "acme" } }, async () => {
  await openai.chat.completions.create({ model: "gpt-4o", messages });
});
# Python
sp = spaturzu(
    api_key=os.environ["SPATURZU_API_KEY"],
    tags={"env": "prod", "team": "growth"},       # on every call
)

with sp.run("billing-agent", tags={"customer": "acme"}):
    openai.chat.completions.create(model="gpt-4o", messages=messages)
```

When an agent's budget is exhausted, the wrapped call raises
`BudgetExceededError`

**before** it reaches the provider — so a refused call
costs nothing. (Use `onBreach: "warn"`

/ `"on_breach": "warn"`

to log and
proceed instead of throwing.)

``` js
// TypeScript
import { BudgetExceededError } from "@spaturzu/sdk";

const capped = spaturzu.wrapOpenAI(new OpenAI(), {
  budget: { hardCap: true, onBreach: "throw" },
});

try {
  await capped.chat.completions.create({ model: "gpt-4o", messages });
} catch (err) {
  if (err instanceof BudgetExceededError) {
    // budget hit — the request never left your process, so no tokens were spent
  }
}
python
# Python
from spaturzu import BudgetExceededError

capped = sp.wrap_openai(OpenAI(), budget={"hard_cap": True, "on_breach": "throw"})

try:
    capped.chat.completions.create(model="gpt-4o", messages=messages)
except BudgetExceededError:
    pass  # call never reached OpenAI — no spend
```

Give a wrap a fallback chain. On a retryable error (429 / 5xx / connection),
spaturzu transparently retries the next provider — and **translates the
response back to your primary provider's shape**, so your code is unchanged.

``` python
// TypeScript
import Anthropic from "@anthropic-ai/sdk";

const resilient = spaturzu.wrapOpenAI(new OpenAI(), {
  fallback: [
    { provider: "anthropic", client: new Anthropic(), model: "claude-3-5-haiku-20241022" },
  ],
});

// If OpenAI is down, this is served by Anthropic — still returns an OpenAI-shaped response.
const r = await resilient.chat.completions.create({ model: "gpt-4o", messages });
python
# Python
from anthropic import Anthropic

resilient = sp.wrap_openai(OpenAI(), fallback=[
    {"provider": "anthropic", "client": Anthropic(), "model": "claude-3-5-haiku-20241022"},
])

r = resilient.chat.completions.create(model="gpt-4o", messages=messages)
```

All 20 directional provider pairs are supported. v1 fallback is non-streaming, text-only (no tools /

`response_format`

).

The same five providers, the same shape, in both languages. Streaming and sync/async calls are metered automatically — no extra configuration.

| Provider | TypeScript | Python |
|---|---|---|
| OpenAI (+ OpenAI-compatible) | `wrapOpenAI` |
`wrap_openai` |
| Anthropic | `wrapAnthropic` |
`wrap_anthropic` |
| Amazon Bedrock | `wrapBedrock` |
`wrap_bedrock` |
| Google Gemini | `wrapGemini` |
`wrap_gemini` |
| Mistral | `wrapMistral` |
`wrap_mistral` |

Short-lived processes(CLIs, serverless): call`spaturzu.flush()`

/`sp.flush()`

before exit so queued metering rows are sent.

For the complete API — every option, streaming details, and per-provider
notes — see the [TypeScript](/Nu11P01nt3r3xc3pt10n/spaturzu-sdks/blob/main/typescript/README.md) and
[Python](/Nu11P01nt3r3xc3pt10n/spaturzu-sdks/blob/main/python/README.md) READMEs, or the full docs at
** https://spaturzu.superchiu.org/docs**.

```
sdks/
├── typescript/   @spaturzu/sdk        — Node/TS SDK (5 providers, 20 fallback pairs)
├── python/       spaturzu             — Python SDK (parity with the TS surface)
└── openclaw/     @spaturzu/openclaw   — OpenClaw metering/budget plugin (WIP)
```

**TypeScript packages** (managed as a pnpm workspace):

```
pnpm install
pnpm build       # build all packages
pnpm test        # run all test suites
pnpm typecheck
```

**Python SDK:**

```
cd python
python3 -m venv .venv
.venv/bin/pip install -e ".[dev,all]"
.venv/bin/pytest
```

[MIT](/Nu11P01nt3r3xc3pt10n/spaturzu-sdks/blob/main/LICENSE) © Superchiu Ltd

spaturzu is a product of **Superchiu Ltd**.
