Skip to content

Vercel AI SDK

The AI SDK does not hand you a provider client to wrap. It hands you a language model. So Metergraph attaches as AI SDK middleware rather than through wrap().

This is TypeScript only. There is no Python equivalent, because the AI SDK is a TypeScript library.

src/llm.ts
import * as mg from "metergraph";
import { generateText, wrapLanguageModel } from "ai";
import { openai } from "@ai-sdk/openai";
const model = wrapLanguageModel({
model: openai("gpt-5-mini"),
middleware: mg.vercelAISDKMiddleware(),
});
await mg.trace("support-answer", () =>
generateText({ model, prompt: "Help this customer" }));

generateText and streamText are both covered: the middleware implements wrapGenerate and wrapStream, so the two calls take the same capture path.

The middleware initializes Metergraph itself the first time it is created, so you can hand it init() options directly instead of calling init() separately:

mg.vercelAISDKMiddleware({
repository: "owner/repository",
environment: "production",
});

If your application already calls mg.init() centrally, call it first and pass the middleware nothing.

The AI SDK’s middleware protocol has its own version number, separate from the AI SDK release. The middleware defaults to protocol v3, which is what AI SDK 6 uses and what AI SDK 7 still accepts. AI SDK 5 uses protocol v2, so it is the one case where you have to say something.

AI SDKCallNode.js
5vercelAISDKMiddleware({ aiSdkVersion: 5 })18 and newer
6vercelAISDKMiddleware()18 and newer
7vercelAISDKMiddleware()22 and newer

Metergraph itself supports Node.js 18 and newer. The Node 22 floor in the last row is the AI SDK 7 release’s own requirement, not Metergraph’s.

aiSdkVersion: 5 is the only accepted value for that option. It exists to map AI SDK 5 onto protocol v2 without asking you to know the protocol numbers.

specificationVersion sets the raw middleware protocol version directly, and accepts "v2", "v3" or "v4". Reach for it only if a future AI SDK needs a protocol the table above does not cover.

mg.vercelAISDKMiddleware({ specificationVersion: "v4" });

Repeating wrapLanguageModel at every call site is how instrumentation gets missed. Two patterns avoid it.

A provider registry takes the middleware once, and every model it hands out is captured:

src/models.ts
import { createProviderRegistry, gateway } from "ai";
import { anthropic } from "@ai-sdk/anthropic";
import { openai } from "@ai-sdk/openai";
import * as mg from "metergraph";
const registry = createProviderRegistry(
{ anthropic, gateway, openai },
{ languageModelMiddleware: mg.vercelAISDKMiddleware() },
);
export const model = registry.languageModel("gateway:anthropic/claude-sonnet-4.5");

An existing model factory is the other place to put it: wrap the single return path your application already funnels model construction through. Use the registry for new multi-provider code. Do not invent a factory purely for Metergraph.

Both patterns have a runnable example: registry, existing factory.

The middleware records the model ID from the language model, the standardized prompt and the call settings.

It deliberately does not record providerOptions, headers or abortSignal. Those carry credentials, callbacks and values that do not serialize, and the standardized prompt and settings are enough to reconstruct the call.

The endpoint is recorded as ai.doGenerate or ai.doStream.

The AI SDK’s provider string is normalized so rows line up with directly wrapped clients:

  • amazon-bedrock, aws-bedrock and aws all become bedrock
  • gemini, google-genai and anything starting google. become google
  • a prefixed name such as openai.chat becomes openai
  • a gateway provider (gateway, vercel, vercel-ai-gateway) resolves through the model ID’s creator prefix, so anthropic/claude-sonnet-4.5 becomes anthropic

See Vercel AI Gateway.

Each provider request in a multi-step tool loop becomes its own row. That is the point: a loop that quietly runs six model calls shows six costed rows, not one. Wrap the whole operation in mg.trace() so they read as one workflow, and mg.track() or mg.route() to name it.

The middleware wraps the returned stream rather than consuming it. Your for await loop sees every part, unchanged and in order. Time to first token comes from the first part that carries real output: a text or reasoning delta, a tool call, a tool result, a file or a source. The row is finalized when the stream ends, and a cancelled stream is finalized with an abandoned status rather than being dropped.

The middleware is telemetry. If capture cannot start, the call runs uninstrumented. If capture cannot finish, the row is dropped. Neither reaches your generateText result or your stream, and a fault while inspecting one stream part yields that part unchanged and moves on.

The middleware has no runtime dependency on ai. It is a plain object matching the middleware shape, so installing Metergraph does not pull the AI SDK in.