Vercel AI SDK
Add JamJet Cloud governance to streamText / generateText with one line of middleware. Streaming-aware enforcement.
Vercel AI SDK
@jamjet/cloud-vercel provides a wrapLanguageModel middleware that adds JamJet Cloud governance to any AI SDK 5.x call — streamText, generateText, generateObject, all of them. Streaming-aware: policy enforcement runs as tool-call events stream from the model.
Install
pnpm add @jamjet/cloud @jamjet/cloud-vercelnpm install @jamjet/cloud @jamjet/cloud-vercelyarn add @jamjet/cloud @jamjet/cloud-vercelPeer deps: ai >=5 <6, @jamjet/cloud >=0.2.0. The @opentelemetry/* peers are optional — only needed for the telemetry exporter (see below).
Quick example
import { wrapLanguageModel, streamText } from 'ai'
import { openai } from '@ai-sdk/openai'
import { init, agent, policy, withAgent } from '@jamjet/cloud'
import { jamjetMiddleware } from '@jamjet/cloud-vercel'
init({ apiKey: process.env.JAMJET_API_KEY!, project: 'my-app' })
policy('block', 'wire_*')
const model = wrapLanguageModel({
model: openai('gpt-4o'),
middleware: jamjetMiddleware(),
})
await withAgent(agent('researcher'), async () => {
const result = await streamText({
model,
messages: [{ role: 'user', content: 'Hello!' }],
tools: { /* your tools */ },
})
for await (const chunk of result.textStream) {
process.stdout.write(chunk)
}
})What the middleware does
On every call (both wrapGenerate for non-streaming and wrapStream for streaming):
- Pre-call tool filter — strips tools matched by a
blockpolicy out of the request body before it reaches the model. - Pre-call budget check — estimates cost from prompt tokens; throws
JamjetBudgetExceededif the call would exceed the project ceiling. - Span open — attribution from the active agent + user context.
- Stream wrapping (streaming only) —
tool-callstream parts are evaluated mid-stream. Blocked tool-calls throwJamjetPolicyBlockedviacontroller.error()— yourfor awaitloop throws. - Post-call cost — actual cost computed from
usage.inputTokens/usage.outputTokenson thefinishevent, recorded against the budget. - Span recorded with
source: 'middleware'.
Identity overrides
jamjetMiddleware() accepts agent and user overrides for runtimes without AsyncLocalStorage (edge runtimes like Cloudflare Workers, Vercel Edge):
import { agent } from '@jamjet/cloud'
const model = wrapLanguageModel({
model: openai('gpt-4o'),
middleware: jamjetMiddleware({
agent: agent('explicit-agent'),
user: { userId: 'u_42' },
}),
})Resolution order: factory override → ALS context (withAgent/withUserContext) → init({ agent: 'default' }) default.
Telemetry exporter (optional)
@jamjet/cloud-vercel also includes an OTel SpanProcessor for the AI SDK's experimental_telemetry option. Use this to forward AI SDK telemetry spans into JamJet without using the middleware:
import { trace } from '@opentelemetry/api'
import { BasicTracerProvider } from '@opentelemetry/sdk-trace-base'
import { registerJamjetTelemetry } from '@jamjet/cloud-vercel'
const provider = new BasicTracerProvider()
trace.setGlobalTracerProvider(provider)
registerJamjetTelemetry()
// Now every AI SDK call with experimental_telemetry: { isEnabled: true } emits to JamJet
const result = await generateText({
model: openai('gpt-4o'),
experimental_telemetry: { isEnabled: true, functionId: 'plan3-smoke-greet' },
messages: [{ role: 'user', content: 'Hi!' }],
})The exporter is idempotent — calling registerJamjetTelemetry() twice is a no-op.
Customizing which span names get forwarded
By default, the exporter forwards spans matching common AI SDK span names (ai.generateText, ai.streamText, ai.generateObject, etc.). To customize:
registerJamjetTelemetry({
spanNames: ['ai.generateText', 'my-custom-span'],
})Or pass your own SpanProcessor if you have a custom OTel pipeline:
registerJamjetTelemetry({
spanProcessor: myCustomProcessor,
})Streaming behavior — closes the Plan 2 gap
@jamjet/[email protected] (the bare SDK) couldn't enforce policies mid-stream because it operated below the AI SDK's stream abstraction — by the time it saw chunks, they were already fragmented. The Vercel adapter sits at the right level: stream parts are typed (text, tool-call, finish, error) and the middleware can inspect each one as it flows through.
When a tool-call part matches a block policy, the adapter calls controller.error(new JamjetPolicyBlocked(...)) on the transform stream — your consumer's for await loop throws like a normal error. Earlier text and tool-call-delta parts that streamed before the block remain delivered (the consumer can render partial text up to the failure point).
Composition with telemetry
You can use both middleware and registerJamjetTelemetry simultaneously. The middleware emits spans with source: 'middleware'; the telemetry exporter emits spans with source: 'otel'. Server-side dedupe (by trace_id) is JamJet Cloud's responsibility — both spans appear in your dashboard with distinct labels for clarity.
Errors
| Error | When | Class from |
|---|---|---|
JamjetBudgetExceeded | pre-call estimate exceeds the budget | @jamjet/cloud |
JamjetPolicyBlocked | tool-call (mid-stream or post-decision) matches a block policy | @jamjet/cloud |
JamjetApprovalRejected / JamjetApprovalTimeout | only when requireApproval is invoked alongside | @jamjet/cloud |
Catch them as you would any other error in your for await loop or after await streamText(...).
Versioning
@jamjet/cloud-vercelmajor version tracks AI SDK majors (1.x for AI SDK 4, 0.x for AI SDK 5 — current).- AI SDK 4.x users: keep using
@jamjet/cloudwith the OpenAI/Anthropic auto-patcher (the bare SDK works without the adapter).
Source
github.com/jamjet-labs/jamjet/tree/main/sdk/typescript/packages/cloud-vercel