Skip to content

AI SDK

Last updated View as MarkdownAgent setup

The Vercel AI SDK is a TypeScript toolkit for building applications with large language models. The ai-search-provider package connects AI Search to the AI SDK, so you can generate responses grounded in your indexed content, retrieve chunks, and manage documents from the same API.

This guide builds a Worker that creates an AI Search instance, uploads and indexes a document, and then queries it with the AI SDK.

Prerequisites

  1. Sign up for a Cloudflare account.
  2. Install Node.js.

Node.js version manager

Use a Node version manager like Volta or nvm to avoid permission issues and change Node.js versions. Wrangler, discussed later in this guide, requires a Node version of 16.17.0 or later.

1. Create a Worker project

Create a new Worker project using the create-cloudflare CLI (C3). C3 is a command-line tool designed to help you set up and deploy new applications to Cloudflare.

Create a new project named ai-search-ai-sdk by running:

npm create cloudflare@latest -- ai-search-ai-sdk

For setup, select the following options:

  • For What would you like to start with?, choose Hello World example.
  • For Which template would you like to use?, choose Worker only.
  • For Which language do you want to use?, choose TypeScript.
  • For Do you want to use git for version control?, choose Yes.
  • For Do you want to deploy your application?, choose No (we will be making some changes before deploying).

Go to your application directory:

cd ai-search-ai-sdk

2. Install the AI SDK and provider

Install the AI SDK and the AI Search provider. The provider requires AI SDK v6 (ai@^6):

npm i ai ai-search-provider

Create a binding between your Worker and AI Search. Bindings allow your Worker to interact with resources on the Cloudflare Developer Platform.

Add the following to your Wrangler configuration file:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "ai_search_namespaces": [
    {
      "binding": "AI_SEARCH",
      "namespace": "default",
      "remote": true
    }
  ]
}
[[ai_search_namespaces]]
binding = "AI_SEARCH"
namespace = "default"
remote = true

This binds the default namespace to env.AI_SEARCH. The remote option lets wrangler dev proxy requests to your deployed instance, since AI Search does not run locally. The ai_search_namespaces binding requires a compatibility_date of 2026-03-27 or later, which new C3 projects already satisfy.

4. Create an instance and index content

Add a /setup route that creates an instance and uploads a document. Enable hybrid search at creation by setting index_method to index both vectors and keywords.

The create() method is on the namespace binding (env.AI_SEARCH), not on the provider client. Creating an instance that already exists throws, so the following code creates it and, on the next run, updates it instead.

src/index.jsjs
import { createAISearchNamespace } from "ai-search-provider";
import { generateText, streamText } from "ai";

const INSTANCE_NAME = "knowledge-base";

const SAMPLE_DOC = `# Caching on Cloudflare
Cloudflare caches static assets at the edge. Use Cache Rules to control what is
cached, set an Edge Cache TTL to control how long objects stay in cache, and
purge the cache after a deploy.`;

// Create the instance with hybrid search, or update it if it already exists.
async function ensureInstance(env) {
	// index_method with both vector and keyword enables hybrid search.
	const hybrid = { index_method: { vector: true, keyword: true } };
	try {
		await env.AI_SEARCH.create({ id: INSTANCE_NAME, ...hybrid });
	} catch {
		await env.AI_SEARCH.get(INSTANCE_NAME).update(hybrid);
	}
}

export default {
	async fetch(request, env) {
		const url = new URL(request.url);
		// createAISearchNamespace adapts the binding to the AI SDK provider API.
		const aiSearch = createAISearchNamespace({ binding: env.AI_SEARCH });

		// Visit /setup once to create the instance and index a document.
		if (url.pathname === "/setup") {
			await ensureInstance(env);

			const instance = aiSearch.get(INSTANCE_NAME);

			// upload() queues the file and returns immediately. Indexing runs in
			// the background, so poll the item's status until it is searchable.
			const { id, key } = await instance.items.upload("caching.md", SAMPLE_DOC);

			let info = await instance.items.get(id).info();
			while (info.status === "queued" || info.status === "running") {
				await new Promise((resolve) => setTimeout(resolve, 2_000));
				info = await instance.items.get(id).info();
			}

			return Response.json({ key, status: info.status });
		}

		// Query the instance (see the next step).
		return new Response("Visit /setup first, then query with ?q=");
	},
};
src/index.tsts
import { createAISearchNamespace } from "ai-search-provider";
import { generateText, streamText } from "ai";

interface Env {
	AI_SEARCH: AiSearchNamespace;
}

const INSTANCE_NAME = "knowledge-base";

const SAMPLE_DOC = `# Caching on Cloudflare
Cloudflare caches static assets at the edge. Use Cache Rules to control what is
cached, set an Edge Cache TTL to control how long objects stay in cache, and
purge the cache after a deploy.`;

// Create the instance with hybrid search, or update it if it already exists.
async function ensureInstance(env: Env) {
	// index_method with both vector and keyword enables hybrid search.
	const hybrid = { index_method: { vector: true, keyword: true } };
	try {
		await env.AI_SEARCH.create({ id: INSTANCE_NAME, ...hybrid });
	} catch {
		await env.AI_SEARCH.get(INSTANCE_NAME).update(hybrid);
	}
}

export default {
	async fetch(request, env): Promise<Response> {
		const url = new URL(request.url);
		// createAISearchNamespace adapts the binding to the AI SDK provider API.
		const aiSearch = createAISearchNamespace({ binding: env.AI_SEARCH });

		// Visit /setup once to create the instance and index a document.
		if (url.pathname === "/setup") {
			await ensureInstance(env);

			const instance = aiSearch.get(INSTANCE_NAME);

			// upload() queues the file and returns immediately. Indexing runs in
			// the background, so poll the item's status until it is searchable.
			const { id, key } = await instance.items.upload("caching.md", SAMPLE_DOC);

			let info = await instance.items.get(id).info();
			while (info.status === "queued" || info.status === "running") {
				await new Promise((resolve) => setTimeout(resolve, 2_000));
				info = await instance.items.get(id).info();
			}

			return Response.json({ key, status: info.status });
		}

		// Query the instance (see the next step).
		return new Response("Visit /setup first, then query with ?q=");
	},
} satisfies ExportedHandler<Env>;

AiSearchNamespace is an ambient type available after you run wrangler types.

5. Query your instance

There are three ways to query your instance from the AI SDK. Pick the one that fits your application:

Generate a response

Pass instance.chat() to generateText, and AI Search retrieves relevant content and generates a response in one call.

Replace the query placeholder in your fetch handler with the following:

src/index.jsjs
const query = url.searchParams.get("q") ?? "How does caching work?";

// chat() returns an AI SDK model that retrieves matching chunks and generates
// a grounded answer in one call, so there is no separate search step.
const { text, sources } = await generateText({
	model: aiSearch.get(INSTANCE_NAME).chat({
		ai_search_options: {
			// "hybrid" ranks results from both the vector and keyword indexes.
			retrieval: { retrieval_type: "hybrid", max_num_results: 5 },
		},
	}),
	messages: [{ role: "user", content: query }],
});

// `sources` holds the retrieved chunks, so you can cite them alongside `text`.
return Response.json({ text, sources });
src/index.tsts
const query = url.searchParams.get("q") ?? "How does caching work?";

// chat() returns an AI SDK model that retrieves matching chunks and generates
// a grounded answer in one call, so there is no separate search step.
const { text, sources } = await generateText({
	model: aiSearch.get(INSTANCE_NAME).chat({
		ai_search_options: {
			// "hybrid" ranks results from both the vector and keyword indexes.
			retrieval: { retrieval_type: "hybrid", max_num_results: 5 },
		},
	}),
	messages: [{ role: "user", content: query }],
});

// `sources` holds the retrieved chunks, so you can cite them alongside `text`.
return Response.json({ text, sources });

AI Search returns the retrieved chunks as AI SDK source parts in sources, so you can cite them alongside the generated text. Because the instance indexes both vectors and keywords, retrieval_type: "hybrid" uses both.

Stream a response

For longer responses, use streamText instead of generateText. AI Search emits the retrieved chunks as source parts before the first text part.

// streamText returns right away; tokens stream in as they are generated.
const result = streamText({
	model: aiSearch.get(INSTANCE_NAME).chat(),
	messages: [{ role: "user", content: query }],
});

// toTextStreamResponse() streams the generated text only.
return result.toTextStreamResponse();
// streamText returns right away; tokens stream in as they are generated.
const result = streamText({
	model: aiSearch.get(INSTANCE_NAME).chat(),
	messages: [{ role: "user", content: query }],
});

// toTextStreamResponse() streams the generated text only.
return result.toTextStreamResponse();

toTextStreamResponse() sends the generated text and drops the sources. To stream the retrieved chunks as well, return a UI message stream with sendSources enabled, or read result.fullStream directly:

// sendSources forwards each retrieved chunk as a source-url part. They arrive
// before the first text part, so you can render citations as the answer streams.
return result.toUIMessageStreamResponse({ sendSources: true });
// sendSources forwards each retrieved chunk as a source-url part. They arrive
// before the first text part, so you can render citations as the answer streams.
return result.toUIMessageStreamResponse({ sendSources: true });

Search as a tool

With chat(), AI Search searches your instance on every request. To let the model decide when to search instead, expose instance.search() as an AI SDK tool and pass it to a model that supports function calling, such as a Workers AI model. This is the pattern to use in an agent, where the model chooses between searching and other tools.

Install the Workers AI provider and Zod. Use version 3 of workers-ai-provider: the latest version 4 requires AI SDK v7, but ai-search-provider requires v6, so npm fails to install them together.

npm i workers-ai-provider@^3 zod

Add a Workers AI binding to your Wrangler configuration:

{
  "$schema": "./node_modules/wrangler/config-schema.json",
  "ai": {
    "binding": "AI"
  }
}
[ai]
binding = "AI"

Then define a search tool. The model calls it when it needs to retrieve content:

import { createWorkersAI } from "workers-ai-provider";
import { generateText, tool, stepCountIs } from "ai";
import { z } from "zod";

const instance = aiSearch.get(INSTANCE_NAME);
// The tool-caller must support function calling. Use a dedicated model here
// rather than the AI Search chat model, which retrieves on every request.
const workersai = createWorkersAI({ binding: env.AI });

const { text } = await generateText({
	model: workersai("@cf/zai-org/glm-5.2"),
	messages: [{ role: "user", content: query }],
	tools: {
		search_knowledge_base: tool({
			description: "Search the indexed knowledge base for relevant content.",
			inputSchema: z.object({
				query: z.string().describe("The search query"),
			}),
			// The model decides when to call this; it searches the instance.
			execute: async ({ query }) =>
				instance.search({
					query,
					ai_search_options: { retrieval: { max_num_results: 5 } },
				}),
		}),
	},
	// Cap the tool-call loop so the model cannot invoke tools indefinitely.
	stopWhen: stepCountIs(5),
});
import { createWorkersAI } from "workers-ai-provider";
import { generateText, tool, stepCountIs } from "ai";
import { z } from "zod";

const instance = aiSearch.get(INSTANCE_NAME);
// The tool-caller must support function calling. Use a dedicated model here
// rather than the AI Search chat model, which retrieves on every request.
const workersai = createWorkersAI({ binding: env.AI });

const { text } = await generateText({
	model: workersai("@cf/zai-org/glm-5.2"),
	messages: [{ role: "user", content: query }],
	tools: {
		search_knowledge_base: tool({
			description: "Search the indexed knowledge base for relevant content.",
			inputSchema: z.object({
				query: z.string().describe("The search query"),
			}),
			// The model decides when to call this; it searches the instance.
			execute: async ({ query }) =>
				instance.search({
					query,
					ai_search_options: { retrieval: { max_num_results: 5 } },
				}),
		}),
	},
	// Cap the tool-call loop so the model cannot invoke tools indefinitely.
	stopWhen: stepCountIs(5),
});

6. Test it locally

Before you deploy, confirm the whole flow works against your instance with wrangler dev, which proxies the remote AI Search binding. The output below is from the generate a response option.

Start a local development server:

npx wrangler dev

First, index the sample document by visiting /setup:

curl http://localhost:8787/setup

The first /setup can take a minute or two while the document indexes. When it finishes, you get back the item key and a completed status:

{ "key": "caching.md", "status": "completed" }

Then query the instance:

curl "http://localhost:8787/?q=How+does+caching+work%3F"

A working integration returns generated text grounded in your document, along with a sources array referencing the file it retrieved (fields trimmed):

{
	"text": "Cloudflare caches static assets at the edge...",
	"sources": [{ "sourceType": "url", "url": "caching.md" }]
}

If sources comes back empty, the document has not finished indexing yet. Run /setup again, then retry the query.

7. Deploy

Log in with your Cloudflare account:

npx wrangler login

Deploy your Worker to make it accessible on the Internet:

npx wrangler deploy

Considerations

  • The chat model is text-only. File and image message parts are not supported.
  • Generation options such as temperature and maxOutputTokens are passed through to the instance's generation model. maxOutputTokens truncates the response and sets finishReason to "length". If a model ignores an option, the result's warnings array stays empty, so an unsupported option fails silently.
  • AI Search uses the generation model configured on the instance by default. Pass instance.chat({ model: "..." }) to override it per request.

Next steps

Hybrid search

Combine vector and keyword search with configurable fusion.

Agents SDK

Build a stateful chat agent that searches your instance.

Was this helpful?