---
title: Wire transport
summary: A spend ceiling and exact accounting on your provider fetch, no adapter needed.
canonical: https://docs.caveman.so/docs/agent-sdk/wire
license: Apache-2.0
capability: agent-sdk
updated: 2026-09-16T21:32:40-07:00
basis: inferred
---

# Wire transport

> A spend ceiling and exact accounting on your provider fetch, no adapter needed.
`createCavemanTransport` wraps the `fetch` your provider client already accepts and puts three things on the request itself: a spend ceiling that reserves before the call and settles on measured usage, exact provider-reported accounting per call, and provider-native cache metadata. It attaches at the provider client, so it works on any framework above it.

```ts
import { createAnthropic } from "@ai-sdk/anthropic";
import { createCavemanTransport } from "@caveman-ai/agent/wire";

const caveman = createCavemanTransport({
  budget: { maxTokens: 2_000 },
  onModelUsage: (usage) => console.log("usage:", JSON.stringify(usage)),
});

const anthropic = createAnthropic({ fetch: caveman.fetch });
```

Unreleased: this page documents `@caveman-ai/agent` 0.2.0 from source at commit 311be40 in caveman-ai/agent-sdk. Build it from the checkout as described on [Install](/docs/agent-sdk/install).

`caveman.meter` is the live meter when a budget is configured, so `meter.settled` reads the measured spend so far.

One call through it, against a stub upstream so the figures are fixed. The first line is that `onModelUsage`; the last two are the body the stub received and `caveman.meter.settled` after it answered:

```console
usage: {"schemaVersion":1,"provider":"anthropic","model":"claude-haiku-4-5","inputTokens":812,
        "outputTokens":96,"cacheReadTokens":0,"cacheWriteTokens":0,"reasoningTokens":null,
        "totalTokens":908,"cost":{"status":"unknown"}}
body max_tokens sent: 1875
meter settled: 908
```

The request asked for `max_tokens: 4000`. The meter could fund 1875 of them at that moment, so the body that left carried 1875. `meter.settled` then moved by the measured 908 rather than by the reserve.

## What it recognises

| Host | Path | Output field |
|---|---|---|
| `api.anthropic.com` and `*.anthropic.com` | `/v1/messages` | `max_tokens` |
| `api.openai.com` and `*.openai.com` | `/v1/chat/completions` | `max_completion_tokens` |
| `api.openai.com` and `*.openai.com` | `/v1/responses` | `max_output_tokens` |

Host and path together, because the cache grammars are provider-specific and a wrong guess would splice one provider's keys into another's body. Anything else goes straight through, unedited and unmetered:

```console
passed through: bedrock-runtime.us-east-1.amazonaws.com
```

A `GET`, a body that parses as anything other than a JSON object, or a body missing its `model` string also passes through untouched. Bedrock in particular carries model and region in the URL and needs SigV4 re-signing after any body edit, so it is left alone rather than half-handled.

## The ceiling

`budget` takes the same shape `run()` does, and the denomination is yours to declare. `maxTokens` is the honest default for portable use: the transport sees the request and leaves the billing arrangement behind it unknown, so a metered key and a subscription look alike. `maxUsd` asserts that this transport's key is billed in dollars.

Each call reserves its worst case before it leaves. The input bound is a UTF-8 byte count, which over-reserves by roughly three to four times and always lands high. The declared output allowance is reserved in full when the meter can fund it, and clamped into the body down to what is left when it falls short. A reservation with nothing left to draw on stops the call:

```console
call 6: settled 0.007752 USD
call 7: settled 0.009044 USD
call 8: cave_wire_budget_exhausted:budget_exhausted
```

Those are identical calls under `budget: { maxUsd: 0.01 }`, and the eighth threw. A request that failed to land cancels its reservation, so the cap is charged for calls that left.

Under a USD budget, a model the public catalog has no price for fails the same way rather than consuming an imaginary zero:

```console
uncataloged model: cave_wire_budget_exhausted:budget_exhausted
```

Usage the transport failed to read settles at the full reserve rather than at zero: an unmeasured call has yet to be shown cheap.

## The accounting

`onModelUsage` fires once per completed call. Counts are disjoint, so `inputTokens` excludes cache reads and writes, and reasoning tokens are a subset of output tokens. A field the provider left out stays `null`.

Anthropic splits one call's usage across `message_start` and `message_delta`; OpenAI reports it once in a final chunk, and only when the caller asked for it. Both are merged, on a JSON body or an SSE stream. Streaming responses are teed, so your stream stays pull-driven and untouched while the scanning branch reads usage fields out of the copy. Past `WIRE_MAX_USAGE_SCAN_BYTES`, 8 MiB, usage stays unknown.

`cost.status` is `estimated` with `basis: "public_catalog"` only when every count is known, including the reasoning split. Anthropic omits that split, so its records read `unknown` even though the meter can still settle exactly: reasoning is a subset of output, so pricing both extremes settles the figure when they agree and leaves it unknown when they differ.

OpenAI reports prompt tokens inclusive of cached tokens while this contract treats the four classes as disjoint, so cache reads are subtracted from the input count. OpenAI's API has no cache-write class at all, the one absence anywhere in this module that becomes a zero rather than a `null`.

## Cache metadata

`cache` defaults to `"gated"`, which releases only grammars proven against a live provider from this SDK. Today that is the OpenAI affinity routing key alone. The Anthropic and Bedrock splices are byte-parity tested against the engine's fixtures, and a live endpoint has yet to see one from here, so the gate holds them.

```ts
const caveman = createCavemanTransport({
  cache: "gated",
  scope: "support-desk",
  onCacheDecision: (d) =>
    console.log("cache:", d.provider, d.model, "applied:", d.applied, "reason:", d.reason, "heldByScope:", d.heldByScope),
});
```

```console
cache: anthropic claude-haiku-4-5 applied: false reason: applied heldByScope: true
```

`reason: applied` with `applied: false` and `heldByScope: true` is the planner choosing to act and the gate stopping the edit. `cache: "all"` releases every grammar the planner selects, and is an explicit opt-in to behaviour a live endpoint has yet to confirm. `cache: "off"` skips the planner entirely.

`scope` is the cache scope id and defaults to `caveman-wire`; requests sharing a prefix must share it. The epoch carries the digest of the stable slice (`system`, `tools`, `instructions`, `toolConfig`), so changed instructions open a new epoch rather than permanently reporting prefix drift. Any planning uncertainty sends the original bytes rather than a partial edit.

## Limits

Compaction and model routing stay off this transport on purpose. Both rewrite what the framework above believes it sent, which would desync its message state from the transcript the model actually saw. They belong on the adapter's model boundary: [Adapters](/docs/agent-sdk/adapters).

The transport is per client, so you can put it on one provider client and leave the rest of the process on the default `fetch` while you compare. It mints nothing and makes no efficiency claim of any kind; the figures it reports are provider-reported counts and public-catalog list prices, which [Numbers and limits](/docs/counting) defines.

A budget here is a local control in one process. It is not a provider invoice, a platform quota, or a reservation anything outside this process can see.
