> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stigg.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Vercel AI SDK

> Govern a Vercel AI SDK app with one language model middleware: every model call and paid tool is checked before it runs, and its usage is reported as the agent that made it.

`stiggVercelMiddleware()` governs a [Vercel AI SDK](https://ai-sdk.dev) app. Pass it to the AI SDK's own `wrapLanguageModel`, and every call made on that model is checked before it runs, and its usage is reported after. That covers `generateText`, `streamText` and each step of an agent's loop, in `ai` 6 and 7. Paid tools go through the same middleware. For how Stigg governs a team of agents and what it's used for, see [Agent frameworks](https://docs.stigg.io/documentation/ai-agents/overview.md#agent-frameworks).

The AI SDK is a library, so no Vercel account is involved unless you call models through Vercel's AI Gateway. Your model provider still charges you as usual.

## Key terms

| Term | Meaning |
| - | - |
| Middleware | An object the AI SDK runs around each model call. Stigg's middleware uses it to check access before the call and report usage after. |
| Step | One model call. An agent's loop makes one step per model call, and a tool that runs another agent makes that agent's steps. |
| `providerOptions` | The AI SDK's per-call field for provider settings. Stigg reads the call's own `stiggContext` from `providerOptions.stigg`. |

## Quick start

You need an AI SDK app, a Stigg server API key, and a credit type in Stigg, here `credits`, that your customers have.

**1. Install the SDK**

```bash theme={null}
npm install @stigg/agent-sdk-js
```

The middleware imports nothing from `ai`, so the SDK adds no dependency on it.

**2. Create the Stigg client**

Create it once, when your server starts:

```typescript theme={null}
import { StiggAgentSDK } from '@stigg/agent-sdk-js';

const stigg = new StiggAgentSDK(process.env.STIGG_SERVER_API_KEY);
```

**3. Wrap the model**

Tell the middleware what to check each call against:

```typescript theme={null}
import { stiggVercelMiddleware } from '@stigg/agent-sdk-js';
import { anthropic } from '@ai-sdk/anthropic';
import { wrapLanguageModel } from 'ai';

const model = wrapLanguageModel({
  model: anthropic('claude-sonnet-5-5'),
  middleware: stiggVercelMiddleware({ stigg, stiggContext: { entitlement: { currencyId: 'credits' } } }),
});
```

**4. Name the customer on each call**

Pass the customer in `providerOptions.stigg`:

```typescript theme={null}
import { generateText } from 'ai';

await generateText({
  model,
  prompt,
  providerOptions: { stigg: { customerId: user.customerId } },
});
```

`providerOptions.stigg` is the call's own `stiggContext`, merged over the middleware's. Stigg removes it before the provider sees the call, so it's never sent, not even through the AI Gateway. A call with no customer, on the middleware or on the call, is denied before the model is called. See [Naming the customer safely](#naming-the-customer-safely).

**5. Check that it works**

Make one call. Then confirm in Stigg that the customer's `credits` balance went down. If it didn't, the token counts the middleware reports aren't converted into credits yet. See [The credit balance does not go down](https://docs.stigg.io/documentation/ai-agents/usage-metering.md#the-credit-balance-does-not-go-down).

To see each decision while you test, add a hook to the middleware:

```typescript theme={null}
hooks: { onDecision: (decision, context) => console.log(context.agent, decision.hasAccess) },
```

## Naming the customer safely

The customer ID decides who pays for a call. If it comes from the user's request, a user can send another customer's ID and charge their usage to that customer. Always resolve it on your server, from the logged-in user, and set it in `providerOptions.stigg` there.

* **Never read it from the request body.** Take it from your auth session, as `user.customerId` does in the examples on this page.
* **A default customer** in the middleware's `stiggContext: { customerId }` is charged for any call that names none. Set one only in a process that serves one customer.
* **`idempotencyKey` is not allowed** in `providerOptions.stigg`. The AI SDK passes one call's `providerOptions` to every step and retry, so they would all share it. The SDK keys each step by the model's response ID instead.

## Configurations

The middleware takes a `stiggContext`, `tools` and `hooks`. The `stiggContext` works as it does for a [wrapped client](https://docs.stigg.io/documentation/ai-agents/overview.md#what-it-is). Each configuration below adds to the one before it.

### Metering only

Leave out the entitlement, and every call is reported but never checked:

```typescript theme={null}
const model = wrapLanguageModel({
  model: anthropic('claude-sonnet-5-5'),
  middleware: stiggVercelMiddleware({ stigg }),
});
```

### One budget for every agent

Add an entitlement, as in the quick start, and every call made on that model is checked against the customer's credits. Use the same wrapped model for every agent, and they all draw on one balance.

### A budget per agent

The AI SDK has no names for agents. A sub-agent is a `generate()` call inside a tool. So give each agent its own model, wrapped as that agent. Register a parent agent and one agent per team member in Stigg first, as in [Register the agents](https://docs.stigg.io/documentation/ai-agents/orchestrators/langchain.md#a-budget-per-agent).

This example and the one in [When access is denied](#when-access-is-denied) need `ai` 7, because `callOptionsSchema`, `prepareCall`, `contextSchema` and `toolsContext` are new in it. The middleware itself works the same in `ai` 6.

```typescript theme={null}
import { ToolLoopAgent, tool } from 'ai';
import { z } from 'zod';

const credits = { currencyId: 'credits' };

const governed = (agentId: string) =>
  wrapLanguageModel({
    model: anthropic('claude-sonnet-5-5'),
    middleware: stiggVercelMiddleware({
      stigg,
      stiggContext: { attributionKey: 'agentId', agentId, entitlement: credits },
    }),
  });

// The customer each agent's calls are for.
const Request = z.object({ customerId: z.string() });

const researcher = new ToolLoopAgent({
  model: governed('agent-researcher'),
  tools: { web_search },
  callOptionsSchema: Request,
  prepareCall: ({ options, ...call }) => ({
    ...call,
    providerOptions: { ...call.providerOptions, stigg: { customerId: options.customerId } },
  }),
});

const lead = new ToolLoopAgent({
  model: governed('agent-lead'),
  tools: {
    research: tool({
      inputSchema: z.object({ brief: z.string() }),
      contextSchema: Request,
      execute: async ({ brief }, { context }) =>
        (await researcher.generate({ prompt: brief, options: context })).text,
    }),
  },
  callOptionsSchema: Request,
  prepareCall: ({ options, ...call }) => ({
    ...call,
    providerOptions: { ...call.providerOptions, stigg: { customerId: options.customerId } },
    toolsContext: { research: options },
  }),
});

await lead.generate({ prompt, options: { customerId: user.customerId } });
```

Each call is now checked against its agent's budget and the parent's. When the researcher runs out, its calls are denied, and the lead keeps working.

* **`callOptionsSchema` and `prepareCall`** give an agent per-call data. `prepareCall` puts the customer where the agent's model calls read it, and in the context of the tool that runs the sub-agent.
* **Use one governed model per agent.** A call counts against the agent named in its model's middleware, so a model shared by two agents reports everything as the first.

## Tools

Model calls are always checked and reported. A tool is only checked and reported when the middleware's `tools` lists it, because most tools cost nothing. The middleware never sees a tool run, so pass your tools through the same middleware's `tools()`. It wraps each listed tool's `execute`, so the tool is checked before it runs and reported after it succeeds.

```typescript theme={null}
const stiggMiddleware = stiggVercelMiddleware({
  stigg,
  stiggContext: { attributionKey: 'agentId', agentId: 'agent-researcher', entitlement: credits },
  tools: {
    // Checked, then one unit per call.
    send_email: { currencyId: 'credits' },

    // Checked, then reported as one credit per search result.
    web_search: {
      entitlement: { currencyId: 'credits' },
      usage: ({ output }) => ({ credits: output.results.length }),
    },

    // Every tool not listed here...
    '*': { currencyId: 'credits' },
    // ...except the tool that runs the writer agent, whose model calls are already reported.
    writer: false,
  },
});

const researcher = new ToolLoopAgent({
  model: wrapLanguageModel({ model: anthropic('claude-sonnet-5-5'), middleware: stiggMiddleware }),
  tools: stiggMiddleware.tools({ send_email, web_search, writer }),
});
```

The entries work as they do in the [LangChain handler](https://docs.stigg.io/documentation/ai-agents/orchestrators/langchain.md#tools). A `usage` function receives the tool's `output` exactly as `execute` returned it.

* **Who pays.** A tool call is checked and reported as the model step that asked for it, with the same agent and the customer from that step's `providerOptions.stigg`. There is nothing new to pass.
* **Order.** A tool is checked after the step that asked for it is reported, so a tool that shares the model's budget sees what the model spent.
* **One middleware.** Call `tools()` on the middleware of the model that calls those tools. It finds each tool call by its ID, which only that middleware saw.
* **Approved tools.** A tool with `needsApproval` runs at the start of the next call, before any model step, so there is no step to take the customer from. Name the customer in the tool's own context: `toolsContext: { send_email: { stigg: { customerId } } }` in `ai` 7, or `experimental_context: { stigg: { customerId } }` in `ai` 6. A tool with a `contextSchema` only gets what the schema declares, so add `stigg` to it. With neither, the tool falls back to the middleware's `stiggContext`, and the SDK warns once per tool.
* **Parallel calls.** The tools one step asks for run at once, so all of them are checked before any reports. Together they can spend a little past the budget.
* **Streaming tools.** An `execute` written as `async function*` streams its results through and is reported from its last one.
* **Events.** A tool is reported under `tool.<name>`. Put a meter on it.

## When access is denied

Stigg reports a denial by throwing an `AccessDeniedError` from the denied model call, and your code decides what to do with it. Where the denial surfaces depends on the call:

| Denied call | What happens |
| - | - |
| A model call of the agent you called `generate()` on | `generate()` rejects with the error. |
| A model call of a sub-agent run inside a tool | If the tool doesn't catch it, the AI SDK passes the error's message to the calling model as the tool's result, and the agent carries on. |
| A model call inside `streamText` or `agent.stream()` | The stream doesn't reject. The error goes to `onError`, and the stream ends. |
| A paid tool | The tool doesn't run. It fails with a short error, such as `web_search is not available: BudgetExceeded.`, which the AI SDK passes to the model as the tool's result, and the agent carries on. |

Use `stigg.denial(err)` to read the denial. It returns the decision, with a `message` you can show, or `undefined` for any other error. For a denied tool, pass it the error from the `tool-error` part.

Always catch a denial in a tool that runs a sub-agent. Left uncaught, the error's message becomes the tool's result, so your model, and possibly your user, sees Stigg's reason instead of an answer. Catch it and fall back, or return something neutral. In this example, a researcher that runs out of budget hands the brief to a cheaper `generalist` agent, with no paid search:

```typescript theme={null}
research: tool({
  inputSchema: z.object({ brief: z.string() }),
  contextSchema: Request,
  execute: async ({ brief }, { context }) => {
    try {
      return (await researcher.generate({ prompt: brief, options: context })).text;
    } catch (err) {
      if (!stigg.denial(err)) throw err;   // not a Stigg denial
      return (await generalist.generate({ prompt: brief, options: context })).text;
    }
  },
}),
```

At the top level, catch the denial and show it to the user:

```typescript theme={null}
try {
  await lead.generate({ prompt, options: { customerId: user.customerId } });
} catch (err) {
  const decision = stigg.denial(err);
  if (!decision) throw err;
  showUpgradePrompt(decision.message);   // your own UI
}
```

If Stigg can't be reached to check, the SDK follows `checkErrorAction`, which lets the call run by default. See [Fallback strategy](https://docs.stigg.io/documentation/ai-agents/access-enforcement.md#fallback-strategy). Reporting usage never throws into the call.

## How usage is reported

Each call is reported under one of two events, `vercel-ai.generate` for a model call and `vercel-ai.stream` for a streamed one. Put a meter on both, or a meter on one misses the other's usage.

| Metric | What it is |
| - | - |
| `input_tokens` | Every input token, cached ones included. |
| `output_tokens` | Output tokens. |
| `total_tokens` | Input and output together. |
| `cache_read_tokens` | Input read from the provider's cache, when the provider reports it. |
| `cache_write_tokens` | Input written to the provider's cache, when the provider reports it. |

`input_tokens` already includes the cached input, as OpenAI's counts do. To price a cache read lower, price `cache_read_tokens` at its own rate and subtract it from the input. A [usage mapper](https://docs.stigg.io/documentation/ai-agents/usage-metering.md#usage-mappers) reads `usage.input` as uncached input only.

* **A model that reports no usage**, such as an OpenAI-compatible server without `include_usage`, is reported from an estimate of its output, about 4 characters per token, and counted as a metering failure. This applies to `generateText` and `streamText` alike.
* **A streamed call** is reported once, when the stream ends, from the usage on its last part.
* **A stream the user stops** with an `abortSignal`, such as by pressing stop, is reported from an estimate of the output it produced, about 4 characters per token. Its input tokens are unknown. A stop is normal, so it isn't counted as a metering failure.
* **A stop after the model answered but before any output** leaves nothing to estimate. It's counted as a metering failure, with no metrics.
* **A stop before the model answered** reports nothing.
* **A timeout or a stream that fails partway** is reported from the same estimate and counted as a metering failure. A timeout includes the AI SDK's `timeout` option and your own `AbortSignal.timeout()`.
* **A stream your code stops reading** without an `abortSignal`, such as a `break` out of `for await`, isn't reported at all. Pass the request's signal to `streamText()`.
* **An attempt that fails before the model answers**, such as one the AI SDK retries, reports nothing.

A metering failure goes to your `onError` hook, so you can see where the meter ran on an estimate or on nothing.

## With other middleware

`wrapLanguageModel` takes a list of middleware, and the first one runs outermost. Put Stigg's last, next to the model, after logging, caching and guardrails:

```typescript theme={null}
wrapLanguageModel({ model, middleware: [guardrails, cache, stiggVercelMiddleware({ stigg, stiggContext })] });
```

A cached answer that never reaches the model is then neither checked nor reported, and a request your guardrails reject costs nothing.

* **A `prepareStep` that returns `providerOptions`** is merged into the call's own, so `providerOptions.stigg` is kept. Leave `stigg` out of what it returns. Set there, it gives every step a new `stigg` object, so a step no longer waits for the previous streamed step's report and can spend a little past the budget.
* **A `prepareStep` that returns a different `model`** runs that step on it. Wrap that model too, or the step isn't governed.

## Observability

Hooks, tracing spans and flushing work as they do in the LangChain handler. See [Observability](https://docs.stigg.io/documentation/ai-agents/orchestrators/langchain.md#observability).

## Best practices

* **Set the customer on your server.** Take it from the logged-in user, never from the request. See [Naming the customer safely](#naming-the-customer-safely).
* **Wrap every model a step can run on.** A model that isn't wrapped, including one a `prepareStep` swaps in, isn't checked or reported.
* **Pass an `abortSignal` to streams.** A stream stopped with a signal is reported from an estimate. A stream your code stops reading without one is not reported at all.
* **Keep parallel spend within budget.** Each check reads the balance at that moment, so calls that start together can each pass. Keep limits a little below the true cap, or [reserve credits](https://docs.stigg.io/documentation/ai-agents/access-enforcement.md#reserving-credits-for-a-task) for the whole task. See [Caching and accuracy](https://docs.stigg.io/documentation/ai-agents/access-enforcement.md#caching-and-accuracy).
* **Choose small credit units.** Agent budgets count whole units and round each call up, so define your credit type in small units, such as thousandths of a dollar, to keep rounding negligible. See [Usage governance](https://docs.stigg.io/documentation/ai-agents/usage-governance.md).
* **Roll out without denials.** Start with metering only, or register agents with no limit, then add limits once usage looks right. See [Observe-only rollout](https://docs.stigg.io/documentation/ai-agents/usage-governance.md#observe-only-rollout).

## Not supported yet

* **Tools the provider runs.** A provider tool, such as Anthropic's web search, runs at the provider and never goes through `execute`, so it can't be checked.
* **Client-side tools.** A tool with no `execute` runs in your client, so it isn't governed.
* **Vercel Workflow.** Not tested yet. A durable step may rebuild the model from its provider, which would drop the middleware and leave that step ungoverned. Check that a workflow's steps are reported before you rely on them.


## Related topics

- [AI agents](https://docs.stigg.io/documentation/ai-agents/overview.md)
- [JavaScript](https://docs.stigg.io/api-and-sdks/changelog/agent-sdk/javascript.md)
- [Usage governance](https://docs.stigg.io/documentation/ai-agents/usage-governance.md)
- [Providers and frameworks](https://docs.stigg.io/documentation/ai-agents/providers/overview.md)
- [Access enforcement](https://docs.stigg.io/documentation/ai-agents/access-enforcement.md)


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.