# Vercel AI SDK

Source: https://immiscible.fly.dev/docs/sdks/integrations/vercel-ai

# Vercel AI SDK

Two parts: model routing through the gateway (provider settings plus a middleware that keeps every call inside the run), and a guard on each tool's `execute`.

Tested against `ai` 7 with `@ai-sdk/openai` 4, through `generateText` with a real tool loop on the fake server.

```shell
npm install @immiscible/sdk ai @ai-sdk/openai zod   # or @ai-sdk/anthropic
```

```ts
import { generateText, wrapLanguageModel, tool, stepCountIs } from 'ai';
import { createOpenAI } from '@ai-sdk/openai';
import { z } from 'zod';
import { Immiscible } from '@immiscible/sdk';
import { guardAiTools, immiscibleMiddleware } from '@immiscible/sdk/ai';

const immiscible = new Immiscible().run({ client: 'vercel-ai' });

// Model routing: the provider points at the gateway; the middleware adds the run's headers to every call.
const provider = createOpenAI(immiscible.gateway.aiSdkOpenAI({ taskId: 'weekly-shop' }));
const model = wrapLanguageModel({
  model: provider.chat('gpt-5-mini'),               // .chat(): the gateway speaks Chat Completions, not Responses
  middleware: immiscibleMiddleware({ client: immiscible }),
});

// Tool calls: each execute asks first and settles after.
const tools = guardAiTools({
  buy: tool({
    description: 'Buy groceries from a supermarket',
    inputSchema: z.object({ pence: z.number().int(), domain: z.string() }),
    execute: async ({ pence, domain }) => placeOrder(pence, domain),
  }),
  search,
}, {
  client: immiscible,
  mapToAction: ({ name, args }) => name === 'search' ? null
    : Immiscible.paymentAction({ amount: args.pence, currency: 'GBP', merchant: args.domain, provenance: [{ source: 'user' }] }),
});

const { text } = await generateText({ model, tools, prompt: 'Buy milk and bread from tesco.com', stopWhen: stepCountIs(5) });
```

For Anthropic models: `createAnthropic(immiscible.gateway.aiSdkAnthropic())` (base URL `<base>/anthropic/v1`).

## The middleware

`immiscibleMiddleware({ client, headers })`:

- `transformParams` adds the run's `traceparent` (a fresh span per call) and session header, plus any static `headers` such as `x-immiscible-task-id`, to every model call;
- `wrapGenerate` and `wrapStream` read the session the gateway issued back off the response, so the next model call and the next action carry it.

It works with any provider pointed at the gateway. With `gateway.aiSdkOpenAI()` the provider's `fetch` already does the same, and the two agree. Its `specificationVersion` defaults to `v4` (ai 7); pass `{ specificationVersion: 'v3' }` for ai 6 or `'v2'` for ai 5 if your type checker asks. The AI SDK does not check it at runtime.

## Tools

`guardAiTool(name, def, opts)` guards one definition; `guardAiTools(set, opts)` guards a whole tool set. Tools without `execute` (client-side tools) pass through: the browser runs them, so guard them where they run. The tool call id is the idempotency key. A refusal is returned as the tool's result, so the model reads it and tells the person; the abort signal of the generation also aborts a wait for approval.

Options are the same as for the [OpenAI Agents SDK](https://immiscible.fly.dev/docs/sdks/integrations/openai-agents.md#options).

Example: [`packages/immiscible-js/examples/vercel-ai.mjs`](https://github.com/efr7-7/immiscible/blob/main/packages/immiscible-js/examples/vercel-ai.mjs), which runs against the fake with `--demo`.
