Skip to Content
Next.js & AI SDKVercel AI SDK
SDKs

Vercel AI SDK

Add a sponsored card to an existing AI SDK chat. Keep your streamText() call, provider, tools and model configuration. NotPixel matches an ad in parallel and sends it as a data-notpixel-ad part before the final UI stream event. It never changes the model’s text or system prompt.

Install

The adapter is included in notpixel@4.1.0. Keep your application’s compatible AI SDK and provider packages, and choose where NotPixel processes the ad context:

# Local processing (default) npm install notpixel # Or remote processing npm install notpixel

For a new AI SDK 7 chat, also install ai@^7 and @ai-sdk/react@^4. Use matching AI SDK package versions for your existing chat:

AI SDKReact packageRuntime
ai@6@ai-sdk/react@3Node.js 18+; Next.js may require a newer version
ai@7@ai-sdk/react@4Node.js 22+

Install the provider package you already use, such as @ai-sdk/openai, in a version compatible with your AI SDK major. Provider packages have their own version numbers; the complete example pairs ai@7 with @ai-sdk/openai@4. The adapter has no runtime import of ai; other NotPixel imports do not require it.

Package installation does not ask questions or require CLI login. For guided account connection and local/remote selection, run npx notpixel@latest install inside your existing application. See Installation.

Vercel AI SDK and Vercel AI Gateway are different integrations. This adapter composes your application’s UI stream. The existing vercel/... model route in NotPixel connects to AI Gateway. You do not need Gateway or Vercel hosting to use this adapter.

Choose local or remote processing

Create the default local client on your Node.js server:

import Ads from 'notpixel'; const ads = new Ads({ publisherId: process.env.NOTPIXEL_PUBLISHER_ID, cache: false, });

For remote processing, use the same installation and create this client instead:

import Ads from 'notpixel/remote'; const ads = new Ads({ publisherId: process.env.NOTPIXEL_PUBLISHER_ID, cache: false, });

Local processing generates the vector in your application’s backend. Remote processing sends sanitized matching context to NotPixel without initializing a local model. Both clients work with the same adapter below. Local failures never switch automatically to remote processing.

Server integration

With AI SDK 7, wrap the UI stream before creating the HTTP response:

import { withNotPixelAds } from 'notpixel/ai-sdk'; import { createUIMessageStreamResponse, toUIMessageStream } from 'ai'; // Use the local or remote ads client configured above. // Keep your existing streamText() result and model configuration. const stream = withNotPixelAds({ stream: toUIMessageStream({ stream: result.stream }), ads, input: lastUserText, signal: request.signal, requestId: opportunityId, request, placement: process.env.NOTPIXEL_PLACEMENT, }); return createUIMessageStreamResponse({ stream });

On AI SDK 6, replace only the stream conversion:

stream: result.toUIMessageStream(),

The input must be a complete UI message stream, including its start and finish events. Wrap it once per assistant response, outside tool-step callbacks. Do not pass textStream or fullStream directly.

OptionMeaning
streamYour existing complete UI message stream
adsAn Ads instance
inputExplicit matching context, normally the last user text
requestIdOptional stable ID for retries of one ad opportunity; generated if omitted
placementOptional public code copied from the Publisher console
requestIncoming web request for automatic origin and preview detection
signalRequest cancellation signal
adTimeoutMsTotal ad budget, including embeddings; default 3000, 0 disables ads

Reuse a request ID only for retries of the same opportunity. Use a new one for another turn or regeneration. Keep ad caching disabled for this recipe so distinct turns retain distinct tracking identifiers. Do not use a model tool-call ID as the opportunity ID.

Render with useChat

import type { NotPixelUIMessage } from 'notpixel/ai-sdk'; import { useChat } from '@ai-sdk/react'; import { DefaultChatTransport } from 'ai'; const { messages } = useChat<NotPixelUIMessage>({ transport: new DefaultChatTransport({ api: '/api/chat' }), }); // Within your existing message renderer: message.parts.map(part => { if (part.type === 'data-notpixel-ad') { // Render part.data with AdCard and the tracking recipe below. } });

NotPixelUIMessage<Metadata, OtherData, Tools> preserves your own message types. Alternatively, extend your existing UIMessage data map with the exported NotPixelDataParts type. The ad part contains the public Ad object, including requestId, impressionId, publisherId and expiration when present. NotPixelAdPart types the stream chunk.

Use the Next.js example for the complete renderer, visibility tracking, clicks, hide and report actions. Skip expired ads. Render test ads without tracking, even though they have no impression ID. Require the impression ID for live ads.

Privacy and conversation history

Only the explicit matching input goes to local embedding generation. With local processing, NotPixel receives its vector instead of the prompt text. With explicit remote processing, the sanitized matching input is sent to NotPixel. If local inference fails, the chat continues without an ad. Your chosen AI provider still receives the conversation supplied by your application.

Remove data-notpixel-ad parts before converting UI messages back into model messages. The AI SDK ignores data parts by default, but explicit removal also protects applications that supply their own convertDataPart callback. Keep other application data and tool parts intact.

const modelInput = messages.map(message => ({ ...message, parts: message.parts.filter(part => part.type !== 'data-notpixel-ad'), })); const modelMessages = await convertToModelMessages(modelInput);

Timeouts, cancellation and persistence

Text and tools stream immediately. The ad’s three-second budget starts when the wrapper is created, so a long model response does not introduce another three-second wait. A faster response may wait for the remainder of that budget before its final event. No fill, NotPixel errors or timeout produce no ad and preserve the chat.

Pass the same request signal to streamText({ abortSignal }) and withNotPixelAds({ signal }). Disconnecting the output cancels the upstream stream reader and ad request; the host must also wire its provider’s abort signal. An embedding computation already running may finish locally, but it cannot start a later ad HTTP request after cancellation. Model error, abort, content filtering or interruption suppresses the ad.

With local processing, the model may need a download and initialization on a cold instance. Allow the local inference dependencies’ build scripts in your package manager and use a Node.js runtime. Consider await ads.embedding.preload() during application warm-up in long-lived servers. A cold initialization that exceeds the ad budget yields no ad; there is no automatic remote fallback. Remote processing does not download or preload a local model and uses the same total ad budget. Edge runtimes are not supported by this recipe.

Existing model and upstream UI callbacks keep their original timing. If your persistence callback must include the sponsored part, put it outside the wrapper:

const stream = createUIMessageStream<NotPixelUIMessage>({ execute({ writer }) { writer.merge(withNotPixelAds({ stream: originalUIStream, ads, input: lastUserText })); }, onFinish({ responseMessage }) { // Persist responseMessage, including its structured ad part. }, });

AI SDK 7 names the outer completion callback onEnd; onFinish remains a compatibility alias. Resume and durable replay infrastructure remain the host application’s responsibility.

Last updated on
Was this helpful?