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, herecredits, that your customers have.
1
Install the SDK
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 inproviderOptions.stigg there.
- Never read it from the request body. Take it from your auth session, as
user.customerIddoes 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. idempotencyKeyis not allowed inproviderOptions.stigg. The AI SDK passes one call’sproviderOptionsto 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 astiggContext, 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 agenerate() 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.
callOptionsSchemaandprepareCallgive an agent per-call data.prepareCallputs 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’stools 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.
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
needsApprovalruns 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 } } }inai7, orexperimental_context: { stigg: { customerId } }inai6. A tool with acontextSchemaonly gets what the schema declares, so addstiggto it. With neither, the tool falls back to the middleware’sstiggContext, 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
executewritten asasync 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 anAccessDeniedError 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:
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 togenerateTextandstreamTextalike. - 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
timeoutoption and your ownAbortSignal.timeout(). - A stream your code stops reading without an
abortSignal, such as abreakout offor await, isn’t reported at all. Pass the request’s signal tostreamText(). - An attempt that fails before the model answers, such as one the AI SDK retries, reports nothing.
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
prepareStepthat returnsproviderOptionsis merged into the call’s own, soproviderOptions.stiggis kept. Leavestiggout of what it returns. Set there, it gives every step a newstiggobject, so a step no longer waits for the previous streamed step’s report and can spend a little past the budget. - A
prepareStepthat returns a differentmodelruns 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
prepareStepswaps in, isn’t checked or reported. - Pass an
abortSignalto 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
executeruns 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.