Skip to main content
stiggVercelMiddleware() governs a Vercel AI SDK 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. 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

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

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:
3

Wrap the model

Tell the middleware what to check each call against:
4

Name the customer on each call

Pass the customer in providerOptions.stigg:
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.
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.To see each decision while you test, add a hook to the middleware:

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. Each configuration below adds to the one before it.

Metering only

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

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. This example and the one in 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.
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.
The entries work as they do in the LangChain handler. 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: 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:
At the top level, catch the denial and show it to the user:
If Stigg can’t be reached to check, the SDK follows checkErrorAction, which lets the call run by default. See 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. 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 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:
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.

Best practices

  • Set the customer on your server. Take it from the logged-in user, never from the request. See 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 for the whole task. See 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.
  • 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.

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.