---
title: Ask AI
description: An AI-powered chat assistant that helps users understand your documentation with context-aware responses
type: conceptual
summary: An AI-powered chat assistant with context-aware search, persistent history, and suggested prompts for your documentation.
url: /docs/ask-ai
source: apps/template/content/docs/ask-ai.mdx
prerequisites:
  - /docs/env
related:
  - /docs/open-in-chat
  - /docs/llms-txt
  - /docs/mixedbread-retrieval
  - /docs/configuration
---

# Ask AI

The Ask AI feature provides an intelligent chat assistant built directly into your documentation. Users can ask questions about your docs and get instant, context-aware answers powered by AI. The assistant can search through your documentation, understand context, and provide helpful responses.

  Help me configure Ask AI for this Geistdocs site. Check my AI prompt, suggestions, environment variables, and search route, then recommend improvements for the assistant experience.

## How it works

The AI chat assistant is available in two ways:

### 1. Per-Page Quick Access

Each documentation page has an "Ask AI about this page" action in the page actions menu beside the title, and in the action row below the title on mobile. When a reader selects it, Ask AI:

1. Clears the current conversation, including the history stored in the browser, and opens the chat
2. Shows a card above the prompt input that names the page. Readers can remove the card to ask without the page
3. Sends the page with the reader's next message, then replaces the card with a chip on that message that names the page

Ask AI sends only the page's path. The chat route reads the page's full Markdown from the site's own content and grounds the answer in it, so readers can ask "What does this page cover?" without pasting anything. Only the next message carries the page, and the chip stays on that message after a reload.

The chat route only reads pages that Ask AI can see. When the path names a page hidden with `excludeFrom: [chat]` or `internal: true`, a page that the route's `filterPage` option removes, or no page at all, the route ignores the page and answers the question normally.

Changelog entries aren't part of the chat route's sources, so on changelog pages "Ask AI about this page" keeps the earlier behavior: it opens the chat with a prompt that asks Ask AI to read the entry URL, which readers can edit before sending.

The "Open in ChatGPT", "Open in Claude", and other external providers in the same menu still open the provider with a prompt that links to the page.

### 2. Global Chat Interface

Users can open the chat interface at any time by:

- Clicking the "Ask AI" button in the navbar
- Using the keyboard shortcut: `⌘I` (Mac) or `Ctrl+I` (Windows/Linux)

Once open, users can:

- Ask questions about any part of your documentation
- View the documentation pages used as sources for each answer
- Copy an answer as Markdown
- Expand the desktop panel to fill the viewport
- Get step-by-step guidance on complex topics

## Control Ask AI from your own components

`useGeistdocsChat` from `@vercel/geistdocs/controls` opens, closes, and sends messages to the Ask AI chat from your own client components. Call it from any component rendered inside `GeistdocsProvider`, such as a custom header, a callout, or an MDX component.

| Property       | Type                                               | Description                                                                                         |
| -------------- | -------------------------------------------------- | --------------------------------------------------------------------------------------------------- |
| `enabled`      | `boolean`                                          | `false` when `ai.enabled` is `false`. All actions are then no-ops.                                  |
| `isOpen`       | `boolean`                                          | Whether the chat panel is open.                                                                     |
| `open`         | `() => void`                                       | Opens the chat.                                                                                     |
| `close`        | `() => void`                                       | Closes the chat.                                                                                    |
| `toggle`       | `() => void`                                       | Opens the chat when it's closed and closes it when it's open.                                       |
| `openWithPage` | `(page?: { url: string; title?: string }) => void` | Starts a new conversation about a page, like "Ask AI about this page". Defaults to the current page. |
| `ask`          | `(text: string) => void`                           | Opens the chat and sends `text` as a user message.                                                  |

### Add an "Ask about this page" button to a custom header

`openWithPage` behaves like the "Ask AI about this page" action: it clears the conversation, opens the chat, and attaches the page to the reader's next message.

```tsx title="components/ask-about-page-button.tsx"
"use client";

import { useGeistdocsChat } from "@vercel/geistdocs/controls";

export const AskAboutPageButton = () => {
  const { enabled, openWithPage } = useGeistdocsChat();

  if (!enabled) {
    return null;
  }

  return (
    <button onClick={() => openWithPage()} type="button">
      Ask about this page
    </button>
  );
};
```

Without arguments, `openWithPage` attaches the current page by its path, and the card shows that path. Pass a `title` to show the page title instead, for example `openWithPage({ url: "/docs/getting-started", title: "Getting Started" })`. `url` is a site-relative path, with or without `basePath`. For an absolute URL, Ask AI sends only its path, so a production URL still resolves on preview deployments.

### Send a question with `ask`

`ask` opens the chat and sends the text as if the reader typed it. Use it for buttons that start a specific conversation:

```tsx title="components/ask-button.tsx"
"use client";

import { useGeistdocsChat } from "@vercel/geistdocs/controls";

export const AskButton = ({ question }: { question: string }) => {
  const { ask, enabled } = useGeistdocsChat();

  if (!enabled) {
    return null;
  }

  return (
    <button onClick={() => ask(question)} type="button">
      {question}
    </button>
  );
};
```

`ask` trims the text and ignores empty strings. When the chat hasn't loaded yet, Ask AI loads it, restores the stored conversation, and then sends the message. When a response is streaming, the message waits until the response finishes, and several `ask` calls are sent in order.

Call `openWithPage` before `ask` to send a question about a page in a new conversation:

```tsx
const { ask, openWithPage } = useGeistdocsChat();

const summarize = () => {
  openWithPage();
  ask("Summarize this page in three bullet points.");
};
```

## Add a chat footer

Set `ai.footer` to render content below the chat prompt in the desktop panel and mobile drawer. Use it for attribution, links, or other site-specific calls to action.

```tsx title="geistdocs.tsx"
export const ai = {
  footer: (
    <a href="https://example.com">
      Powered by your AI provider
    </a>
  ),
};
```

## Rename the chat panel

Set `ai.title` to change the chat panel heading, its accessible label, and the error message shown when the panel fails to load. The default is "Ask AI", and blank values fall back to it.

```tsx title="geistdocs.tsx"
export const ai = {
  title: "Ask Acme AI",
};
```

`ai.title` only changes the chat panel. The navbar "Ask AI" button and the "Ask AI about this page" page action keep their labels.

## Serve chat from a different route

Ask AI sends chat requests to `/api/chat` by default. Set `ai.endpoint` when the site must serve chat from another path, such as a microfrontend that owns `/api/ai-chat` and applies bot protection or Firewall rules to it.

```tsx title="geistdocs.tsx"
export const ai = {
  endpoint: "/api/ai-chat",
};
```

Then move the chat route adapter to the matching App Router path, for example `app/api/ai-chat/route.ts`. The adapter code stays the same.

`ai.endpoint` is an app-local path, like other Geistdocs paths. The chat client adds `basePath` automatically, so a site with `basePath: "/help"` and `endpoint: "/api/ai-chat"` calls `/help/api/ai-chat`. The client uses the same URL for eve agent prewarm requests, and the [OpenAPI document](/docs/agent-readiness) generated by `createOpenApiRoute` lists the chat operation at the configured path.

The value must start with a single `/` and can't include a query string, fragment, whitespace, backslashes, `.` or `..` segments (including percent-encoded ones), or a trailing slash. It also can't be a path another Geistdocs route serves, including `/api/search`, `/agents.md`, `/llms.txt`, `/sitemap.md`, `/.well-known/mcp.json`, and the site OpenAPI document path. An invalid `ai.endpoint` throws when `createOpenApiRoute` runs and makes the chat panel show its error state instead of sending requests to an unexpected URL.

## Copy an answer

Each finished Ask AI answer has a copy button below it that copies the answer's text as Markdown. The copied text leaves out the "Thinking..." and tool activity indicators and the source list. The button appears once the answer finishes streaming, isn't shown for an answer without text, and needs no configuration.

To copy the whole conversation, use the copy button in the chat header.

## Collect feedback on answers

Set `ai.messageFeedback` to `true` to show thumbs up and thumbs down buttons beside the copy button on each finished answer. `ai.messageFeedback` defaults to `false`.

```tsx title="geistdocs.tsx"
export const ai = {
  messageFeedback: true,
};
```

A vote is sent to the same Geistdocs feedback service that receives [page feedback](/docs/feedback) and creates a GitHub issue labeled with your `siteId`. The request contains these fields:

| Field    | Content                                                                                                                                                      |
| -------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `note`   | Starts with `[Ask AI]`, the vote emoji, and a one-line summary of the question. Then lists the vote, the full question, the source URLs shown for the answer, and the answer as Markdown. |
| `thumbs` | `"up"` or `"down"`. The feedback service labels the issue `thumbs-up` or `thumbs-down`.                                                                     |
| `url`    | The page the reader was on when they voted.                                                                                                                  |
| `label`  | Your `siteId`.                                                                                                                                               |
| `ua`, `ip` | The reader's user agent and IP address, as with page feedback.                                                                                             |

The question is the reader's most recent message before the answer. Questions and answers longer than 4,000 characters are shortened, and up to 10 source URLs are sent. Because the note includes the reader's question as typed, check your site's privacy requirements before you enable `ai.messageFeedback`.

Readers can vote once per answer. After the feedback service accepts a vote, the chosen button stays selected, both buttons are disabled, and a "Thanks for your feedback" toast appears. If the request fails, an error toast appears and the reader can vote again.

Ask AI saves the vote with the conversation in the browser, so the chosen button stays selected after a reload until the chat history is cleared. The vote is stored in the answer's message metadata, which `createChatRoute` ignores, so voting doesn't change later chat requests.

## Show the signed-in reader's avatar

Render `AskAIUser` from `@vercel/geistdocs/controls` to show the signed-in reader's avatar beside their messages in Ask AI. Geistdocs doesn't know who is signed in to your site, so you pass the reader's details at runtime from your own authentication.

`AskAIUser` renders nothing on the page. While it's mounted inside `GeistdocsProvider` with an `avatarUrl`, each user message shows a 24px round avatar. When `AskAIUser` unmounts or receives no `avatarUrl`, for example after the reader signs out, user messages render without an avatar again.

| Prop        | Type     | Description                                                                                                                                                 |
| ----------- | -------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `avatarUrl` | `string \| null` | URL of the reader's avatar. Only `https://` and `http://` URLs and root-relative paths, such as `/api/avatar`, are rendered. Other values, including `data:` URLs, are ignored. |
| `name`      | `string \| null` | The reader's name, used as the avatar's alt text. Without a name, the avatar is decorative and has empty alt text.                                          |

Create a client component that reads the reader from your authentication library and renders `AskAIUser`. This example uses a `useSession` hook. Replace it with your library's client-side session hook:

```tsx title="components/ask-ai-user.tsx"
"use client";

import { AskAIUser } from "@vercel/geistdocs/controls";
import { useSession } from "@/lib/auth-client";

export const SignedInAskAIUser = () => {
  const { data: session } = useSession();

  if (!session?.user.image) {
    return null;
  }

  return (
    <AskAIUser avatarUrl={session.user.image} name={session.user.name} />
  );
};
```

Then render it once inside `GeistdocsProvider` in your root layout:

```tsx title="app/[lang]/layout.tsx"
<GeistdocsProvider basePath={config.basePath} lang={lang}>
  <SignedInAskAIUser />
  <Navbar config={config} />
  {children}
  <Footer />
</GeistdocsProvider>
```

Avatars load lazily and are requested with `referrerPolicy="no-referrer"`, so the image host doesn't receive the docs page URL. An avatar that fails to load is hidden.

## AI SDK dependency ownership

Geistdocs Ask AI is built on AI SDK v6. Generated projects install `ai` v6 and `@ai-sdk/react` v3, and `@vercel/geistdocs` owns the chat client, transport, and `createChatRoute` server behavior.

When a consumer app already uses the AI SDK outside Geistdocs, treat that as app code. Upgrade the app code to the installed AI SDK version or let the package manager install separate versions if needed. Do not fork Geistdocs chat internals or downgrade Ask AI to match unrelated app usage.

Most projects should not customize the chat transport. If you do customize `DefaultChatTransport.prepareSendMessagesRequest`, preserve `messages` in the returned request body. The AI SDK passes `messages` separately from `body`, and returning a custom `body` replaces the default request body.

```ts
prepareSendMessagesRequest: ({ body, messages }) => ({
  body: {
    ...body,
    messages,
    currentRoute: pathname,
  },
});
```

## Choose an AI mode

Ask AI can run in the default AI Gateway mode, through a Vertex-backed proxy, or answered by a hosted eve framework agent.

### Default AI Gateway mode

Use the default mode when the Geistdocs site should call the Vercel AI Gateway directly.

1. Leave `GEISTDOCS_CHAT_PROXY_URL` unset.
2. Set `AI_GATEWAY_API_KEY` for local development. Vercel sets this automatically for deployments that have AI Gateway access.
3. Keep `NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL` set to the site's production host, such as `docs.example.com` or `localhost:3000` for local development.

In this mode, Geistdocs uses the local `search_docs` tool during the AI SDK `streamText` loop.

#### Choose a model

Pass the `model` option to `createChatRoute` to control which model answers Ask AI requests. It accepts any AI SDK `LanguageModel`, including AI Gateway model strings, and defaults to `openai/gpt-5.6-sol`.

```ts title="app/api/chat/route.ts"
import { createChatRoute } from "@vercel/geistdocs/routes/chat";
import { config } from "@/lib/geistdocs/config";
import { geistdocsSource } from "@/lib/geistdocs/source";

export const { POST, maxDuration } = createChatRoute({
  config,
  model: "xai/grok-4.5",
  sources: [geistdocsSource],
});
```

The `model` option only applies in AI Gateway mode. In proxy mode, the Vertex-backed service selects the model.

### Vertex-backed proxy mode

Use proxy mode when Ask AI should route model requests through a central Vertex-backed service instead of calling AI Gateway from each site.

1. Deploy or use the central Geistdocs platform proxy that exposes `POST /vertex`.
2. Generate a random token containing at least 32 bytes with `openssl rand -base64 32`.
3. Set the token as `GEISTDOCS_VERTEX_PROXY_TOKEN` on the platform proxy deployment.
4. Configure the Vertex deployment to trust the Geistdocs platform Vercel project with Deployment Protection Trusted Sources.
5. Set `GEISTDOCS_CHAT_PROXY_URL` on the Geistdocs site to the platform proxy URL, including `/vertex`:

```txt
https://<geistdocs-platform-deployment>/vertex
```

6. Set the same token as `GEISTDOCS_CHAT_PROXY_TOKEN` on the Geistdocs site.

When `GEISTDOCS_CHAT_PROXY_URL` is set, Geistdocs searches local docs on the first user message, injects the attached page (or the current page when no page is attached) and related docs into the final user message, forwards `{ messages, platform: "vercel" }` to the proxy, and streams the AI SDK UI stream response back to the browser. Set [`contextInjection: "page-only"`](#choose-what-context-the-proxy-or-agent-receives) to forward messages as sent and inline only a page the reader attaches.

The proxy request also carries the reader's `x-client-ja4` and `x-client-ip` values, read from the incoming `x-vercel-ja4-digest` and `x-forwarded-for` headers, unless `proxy.headers` sets them. `proxy.headers` accepts a static object or a function that receives the incoming chat `Request`; see [Forward per-request values in upstream headers](#forward-per-request-values-in-upstream-headers).

The Geistdocs site authenticates to the platform proxy with the server-only bearer token. The platform proxy forwards a Vercel OIDC token to Vertex in the `x-vercel-trusted-oidc-idp-token` header. The Vertex deployment should validate the caller through Trusted Sources; the Geistdocs site does not need a Vertex API key.

### Hosted eve agent mode

Use eve agent mode when a hosted [eve framework](https://eve.dev) agent should answer Ask AI requests. Configure it with a single field in `geistdocs.tsx`:

```tsx title="geistdocs.tsx"
export const ai = {
  eveAgent: {
    url: "https://help-eve.example.dev",
    prewarm: true,
  },
};
```

Set `prewarm: true` to start an empty session when the Ask AI client loads, including when a reader hovers or focuses the Ask AI control. The prewarm response binds the session to the reader's browser with the session cookie, and the first message waits for that request and reuses the session, avoiding workflow startup latency. Prewarming requires eve 0.59 or newer and [`GEISTDOCS_CHAT_SECRET`](#keep-eve-sessions-across-turns-with-geistdocs_chat_secret), and falls back to normal session creation if the early request fails. Leave it disabled when opening the chat should not allocate a durable session before the reader sends a message.

The URL flows to `createChatRoute` through the config object you already pass in `app/api/chat/route.ts` — no route changes are required. Geistdocs speaks eve's session API directly: it starts or resumes a durable eve session, streams the agent's NDJSON events, and translates them into the same UI message stream the chat panel already renders. With `GEISTDOCS_CHAT_SECRET` set, Geistdocs continues one eve session across turns by binding it to the reader's browser with a signed, httpOnly cookie.

Eve agent mode keeps the platform-owned behavior from proxy mode:

- Local docs retrieval runs on the first user message, and the attached page (or the current page when no page is attached) plus related docs are inlined into the message the agent receives. Set [`contextInjection: "page-only"`](#choose-what-context-the-proxy-or-agent-receives) when the agent does its own retrieval.
- Locally computed source citations are injected into the response stream, and `excludeFrom: [chat]` page visibility is enforced before anything reaches the agent.
- The agent's reasoning shows as a "Thinking..." indicator, its tool calls show as activity labels, and stopping a response cancels the in-flight eve turn. URLs in tool outputs show as source citations. To limit which tool calls the chat shows, read [Choose which agent tool calls the chat shows](#choose-which-agent-tool-calls-the-chat-shows).

**Authentication.** Requests carry a Vercel OIDC token minted per request, in two headers for the two layers that may guard the agent: `Authorization: Bearer` for eve's channel auth, and `x-vercel-trusted-oidc-idp-token` for Vercel Deployment Protection. Eve's default channel auth (`vercelOidc()`) accepts deployments from the same Vercel team out of the box; cross-team agents can allow the site with `vercelSubject`. If the agent deployment uses Deployment Protection, add the docs site's project as a Trusted Source. For agents with custom auth, pass server-only headers through the `eveAgent` option:

```ts title="app/api/chat/route.ts"
export const { POST, maxDuration } = createChatRoute({
  config,
  sources: [geistdocsSource],
  eveAgent: {
    headers: async () => ({ authorization: `Bearer ${await mintToken()}` }),
  },
});
```

Never put authentication material in `geistdocs.tsx`, because the config ships to the client bundle. The agent URL is public configuration; tokens are not.

The `eveAgent` option also accepts a `url` override for pointing staging deployments at a different agent, and a `secret` override for the session signing secret. Configuring both `proxy` and an eve agent throws at route creation.

| `eveAgent` option | Type                                  | Default            | Description |
| ----------------- | ------------------------------------- | ------------------ | ----------- |
| `url`             | `string`                              | `ai.eveAgent.url`  | Agent base URL. Overrides `ai.eveAgent.url`; setting it here alone enables eve agent mode without exposing the URL in `geistdocs.tsx`. |
| `headers`         | `HeadersInit \| (request: Request) => HeadersInit \| Promise<HeadersInit>` | OIDC headers | Extra request headers. An `authorization` header replaces the OIDC bearer token. |
| `secret`          | `string`                              | `GEISTDOCS_CHAT_SECRET` | Secret that signs the eve session cookie, at least 32 bytes. |
| `clientContext`   | `boolean`                             | `false`            | Send the current page as eve `clientContext` with every message. |
| `visibleTools`    | `string[]`                            | All tools          | Tool names whose calls the chat shows. |
| `toolCitations`   | `boolean`                             | `true`             | Forward tool outputs and show the URLs in them as source citations. |

#### Keep eve sessions across turns with `GEISTDOCS_CHAT_SECRET`

Eve agent mode needs the server-only `GEISTDOCS_CHAT_SECRET` environment variable to continue one eve session across turns and to prewarm sessions. The chat route binds each reader's eve session to an httpOnly cookie signed with this secret, so page scripts can't read the session ID or replay it from another browser. The browser only keeps the stream position and a short, non-reversible session tag in each answer's metadata.

The `geistdocs_eve_session` cookie has these attributes:

| Attribute  | Value                                                        |
| ---------- | ------------------------------------------------------------ |
| `HttpOnly` | Always set                                                   |
| `SameSite` | `Strict`                                                     |
| `Secure`   | Set when the request uses HTTPS, including behind a proxy that sends `x-forwarded-proto: https` |
| `Path`     | The chat route's own path, such as `/api/chat` or `/docs/api/chat` under a base path |
| `Max-Age`  | 7 days, renewed after every turn                             |

The chat route continues the cookie's session only when the conversation's latest answer carries that session's tag, or when the session was prewarmed and the conversation has no eve answers yet. In every other case, the route starts a new eve session, inlines up to 8,000 characters of the prior conversation into the first message, and sets a new cookie. This happens when:

- The reader deleted the chat, or the cookie expired after 7 days without a turn.
- Another tab started its own session. The cookie holds one session per browser, so alternating between two conversations starts a new session on each switch.
- The cookie was altered, signed with a different secret, or issued for a different agent URL. The route never contacts the session that such a cookie names.
- The eve session is no longer active on the agent.

Chats saved in a reader's browser by earlier Geistdocs versions carry session details in a format the route ignores, so they continue in a new session with the conversation inlined.

##### Set the secret

Generate a secret of at least 32 bytes:

```bash title="Terminal"
openssl rand -base64 32
```

Add the value as `GEISTDOCS_CHAT_SECRET` to your site's Vercel project environment variables for the Production and Preview environments, then redeploy. For local development, add it to `.env.local`. Use a separate secret for each docs site.

Geistdocs uses the value as-is, so any value of 32 bytes or more works, including an existing base64-encoded 32-byte secret (44 characters). Geistdocs signs with its own versioned prefix, so a secret that another service also signs with can't produce values that either service accepts from the other.

To read the secret from a differently named variable, pass it through the `eveAgent.secret` option. Never put the secret in `geistdocs.tsx` or give it a `NEXT_PUBLIC_` prefix.

```ts title="app/api/chat/route.ts"
export const { POST, maxDuration } = createChatRoute({
  config,
  sources: [geistdocsSource],
  eveAgent: {
    secret: process.env.ASK_AI_SESSION_SECRET,
  },
});
```

Rotating the secret invalidates every existing session cookie. Each reader's next message starts a new eve session with their conversation inlined, so rotation needs no downtime.

A secret shorter than 32 bytes throws when the route module loads, so a weak secret fails the deployment instead of signing cookies.

##### Serve the chat route from the same site

Browsers only send a `SameSite=Strict` cookie with requests that start on the same site as the cookie. The default `/api/chat` route lives on the same domain as your docs pages, so this works without configuration. If the docs pages call a chat route on a different site, the browser never sends the cookie and every turn starts a new eve session.

##### Run without a secret

Without `GEISTDOCS_CHAT_SECRET`, eve agent mode still answers every question, but it never sets or reads the session cookie:

- Every turn starts a new eve session, with up to 8,000 characters of the prior conversation inlined into the message.
- The route refuses `{ prewarm: true }` requests with a `400` response, so `prewarm: true` only adds a failed request when Ask AI loads.
- The server logs a warning that names `GEISTDOCS_CHAT_SECRET`.

### Choose what context the proxy or agent receives

The `contextInjection` option on `createChatRoute` controls the docs context Geistdocs adds to the messages it forwards in proxy and eve agent modes. The default AI Gateway mode ignores it, because that mode retrieves docs with its own tools and always inlines a page the reader attaches.

| `contextInjection` | Message without an attached page | Message with an attached page (`pageContext`) | Source citations Geistdocs adds |
| ------------------ | -------------------------------- | --------------------------------------------- | ------------------------------- |
| `"auto"` (default) | On the first user message, a 1,500-character excerpt of the current page plus up to 5 related pages from retrieval. Later messages are forwarded as sent. | On the first user message, the attached page replaces the current page in the same format. | One per injected page |
| `"page-only"`      | Forwarded as sent, on every turn | The attached page is inlined in full on the message that carries it, on any turn | None |

Use `"page-only"` when the proxy or agent retrieves documentation itself and should receive the reader's question unchanged. Both modes drop UI-only messages whose metadata sets `isPageContext: true`.

```ts title="app/api/chat/route.ts"
export const { POST, maxDuration } = createChatRoute({
  config,
  contextInjection: "page-only",
  proxy: { url: process.env.GEISTDOCS_CHAT_PROXY_URL! },
  sources: [geistdocsSource],
});
```

In `"page-only"` mode, proxy mode replaces the text of the last user message with the attached page followed by the question:

```md
Here's the content from the current page:

**Page:** Routing
**URL:** https://docs.example.com/docs/routing

---

<full page Markdown>

---

User question: How do I add a redirect?
```

Eve agent mode sends the attached page as a leading text part, with the page as one line of JSON, followed by the question. When the message starts a new eve session, including one that replaces an inactive session, the inlined conversation follows the page part instead:

```json
{
  "message": [
    {
      "type": "text",
      "text": "Page context:\n{\"title\":\"Routing\",\"url\":\"https://docs.example.com/docs/routing\",\"content\":\"<full page Markdown>\"}"
    },
    { "type": "text", "text": "How do I add a redirect?" }
  ]
}
```

For a `pageContext: { url }` reference, Geistdocs reads the page's full Markdown from the route's sources, as Geistdocs renders it for the page's Markdown route, and truncates it to `limits.maxPageContextChars` (200,000 characters by default). The URL is the page's absolute public URL on `siteUrl`, or on the request origin when `siteUrl` is unset. A legacy `pageContext: { title, url, content }` is used as sent. A reference to a page that is hidden from Ask AI or doesn't exist is ignored, and the message is forwarded as sent.

### Send the current page to an eve agent

Set `eveAgent.clientContext: true` to send the page the reader is viewing with every eve message. eve passes `clientContext` to every model call in that turn as turn-scoped context and doesn't store it in the session history.

```ts title="app/api/chat/route.ts"
export const { POST, maxDuration } = createChatRoute({
  config,
  eveAgent: { clientContext: true },
  sources: [geistdocsSource],
});
```

Geistdocs adds `clientContext` next to `message` in the body of every request that sends a message, including follow-ups to the session in the reader's session cookie and the first message of a session that replaces an inactive one:

```json
{
  "message": "How do I add a redirect?",
  "clientContext": {
    "currentPage": {
      "url": "https://docs.example.com/docs/routing",
      "title": "Routing"
    }
  }
}
```

The route derives `clientContext` from the `currentRoute` the Ask AI client already sends, so the client needs no changes:

- `url` is the absolute public URL of the route's path, with `basePath`, on `siteUrl` or the request origin. The query string, fragment, and any origin in `currentRoute` are ignored.
- `title` is the page title, up to 256 characters, when the route is a page Ask AI may see. For other routes, such as pages with `excludeFrom: [chat]`, `currentPage` carries only `url`.
- A request without `currentRoute`, or with a `currentRoute` longer than 2,048 characters, sends no `clientContext`.

The agent's eve version must accept `clientContext` on its session routes; eve 0.59 does. Prewarm requests never carry `clientContext`, because eve rejects it on a session created without a message.

### Choose which agent tool calls the chat shows

By default, the chat shows every eve tool call as an activity label, and URLs found in tool outputs become source citations. Two server-only `eveAgent` options change that:

- `visibleTools` lists the tool names whose calls reach the chat. Calls to other tools, with their results and any citations in them, aren't forwarded. An empty array hides every tool call.
- `toolCitations: false` keeps the activity label but sends each tool output to the chat as `null` and adds no source citations.

The client-side `ai.toolLabels` config in `geistdocs.tsx` replaces the `Running <tool name>...` activity label for the tools it names. It applies in every AI mode:

```tsx title="geistdocs.tsx"
export const ai = {
  toolLabels: {
    web_search: "Searching...",
    web_fetch: "Searching...",
  },
};
```

Tools without a non-blank label keep the default label.

### Forward per-request values in upstream headers

`proxy.headers` and `eveAgent.headers` accept a static object or a function. The function receives the incoming chat `Request` and can be async, so it can forward values from the reader's request. Existing functions that take no arguments keep working.

```ts title="app/api/chat/route.ts"
export const { POST, maxDuration } = createChatRoute({
  config,
  eveAgent: {
    headers: (request) => ({
      "x-evo-surface": "docs",
      "x-docs-locale": request.headers.get("accept-language") ?? "en",
    }),
  },
  sources: [geistdocsSource],
});
```

Use a static object for fixed values such as `x-evo-surface: docs`.

### Match a production Vertex or eve setup

Vercel's own docs answer Ask AI through a Vertex-backed service by default and through an eve agent behind a feature flag. Both responders do their own retrieval, so the route forwards questions unchanged, inlines a page only when the reader attaches one, and shows the eve agent's web tools under one generic label without citations.

Create one route per responder at module scope and choose between them per request. Keep `ai.eveAgent` out of `geistdocs.tsx` here: the eve route sets its URL with the server-only `eveAgent.url` option, because a route that has `proxy` throws when `ai.eveAgent.url` is set.

```ts title="app/api/ai-chat/route.ts"
import { createChatRoute } from "@vercel/geistdocs/routes/chat";
import { config } from "@/lib/geistdocs/config";
import { geistdocsSource } from "@/lib/geistdocs/source";
import { enableDocsAgent } from "@/flags";

const vertex = createChatRoute({
  config,
  contextInjection: "page-only",
  proxy: {
    url: `${process.env.VERTEX_URL}/api/chat/docs`,
    headers: {
      "x-vercel-protection-bypass": process.env.VERTEX_BYPASS_SECRET ?? "",
    },
  },
  sources: [geistdocsSource],
});

const agent = createChatRoute({
  config,
  contextInjection: "page-only",
  eveAgent: {
    url: process.env.EVE_AGENT_URL,
    clientContext: true,
    headers: { "x-evo-surface": "docs" },
    visibleTools: ["web_search", "web_fetch", "knowledge-search"],
    toolCitations: false,
  },
  sources: [geistdocsSource],
});

export const maxDuration = 800;

export const POST = async (request: Request) =>
  (await enableDocsAgent()) ? agent.POST(request) : vertex.POST(request);
```

```tsx title="geistdocs.tsx"
export const ai = {
  endpoint: "/api/ai-chat",
  toolLabels: {
    web_search: "Searching...",
    web_fetch: "Searching...",
    "knowledge-search": "Searching...",
  },
};
```

The Vertex route forwards `{ messages, platform: "vercel" }` with the `x-client-ja4` and `x-client-ip` headers, and the chat shows the citations Vertex returns in its tool outputs. The eve route keeps the default Vercel OIDC authentication. Sessions continue across turns when `GEISTDOCS_CHAT_SECRET` is set, and prewarming stays off because `ai.eveAgent.prewarm` isn't configured.

## Request size limits

`createChatRoute` rejects oversized requests before they reach the model, the chat proxy, or the eve agent. The same defaults apply in every AI mode:

| Limit option          | Default                      | Response when exceeded |
| --------------------- | ---------------------------- | ---------------------- |
| `maxRequestBytes`     | `1_000_000` bytes (1 MB)     | `413`                  |
| `maxMessages`         | `200` messages               | `400`                  |
| `maxPageContextChars` | `200_000` characters         | `400`                  |

Rejected requests receive a JSON body in the form `{ "error": "..." }`. The route reads the request body as a stream and stops as soon as it passes `maxRequestBytes`, and rejects a request whose `Content-Length` header is over the cap without reading the body. An oversized `pageContext.content` is rejected, not truncated.

`maxPageContextChars` only rejects `pageContext.content` that API callers send with the page text. The Ask AI client sends `pageContext: { url }` without content, and the route reads that page from the site's own sources; with `contextInjection: "page-only"`, that page's Markdown is truncated to `maxPageContextChars` before it is forwarded. `pageContext.url` must be a non-empty string of at most 2,048 characters in both shapes; other values receive `400`.

Change the limits with the `limits` option:

```ts title="app/api/chat/route.ts"
export const { POST, maxDuration } = createChatRoute({
  config,
  sources: [geistdocsSource],
  limits: {
    maxRequestBytes: 500_000,
    maxMessages: 100,
    maxPageContextChars: 100_000,
  },
});
```

In Vertex-backed proxy mode, the platform `/vertex` route also limits requests to 1,000,000 bytes and 200 messages. Increasing `maxRequestBytes` or `maxMessages` above those values does not raise the platform limits.

Request size limits don't control how often a client can call the route. `/api/chat` is public, and every accepted request calls a model or agent on your account, so wrap the route with your own rate limiting. For example, install `@vercel/firewall`, create a [Firewall rate limit rule](https://vercel.com/docs/vercel-firewall/vercel-waf/rate-limiting-sdk) with the rate limit ID `ask-ai`, and check it before calling `POST`:

```ts title="app/api/chat/route.ts"
import { checkRateLimit } from "@vercel/firewall";
import { createChatRoute } from "@vercel/geistdocs/routes/chat";
import { config } from "@/lib/geistdocs/config";
import { geistdocsSource } from "@/lib/geistdocs/source";

const chat = createChatRoute({ config, sources: [geistdocsSource] });

export const maxDuration = 800;

export const POST = async (request: Request) => {
  const { rateLimited } = await checkRateLimit("ask-ai", { request });

  if (rateLimited) {
    return Response.json({ error: "Too many requests." }, { status: 429 });
  }

  return chat.POST(request);
};
```

## Features

### Context-Aware Search

By default, the AI assistant includes a built-in `search_docs` tool that:

1. Searches through your documentation content
2. Finds relevant pages based on the user's question
3. Shows source citations with links to the referenced pages
4. Uses the content to provide accurate, contextual answers

When `GEISTDOCS_CHAT_PROXY_URL` is set, Geistdocs runs local documentation retrieval on the first user message before forwarding the request to the configured proxy. This gives proxy-backed services access to the current page and related local documentation without requiring them to read the site's source files directly. With `contextInjection: "page-only"`, Geistdocs skips this retrieval.

Set `ai.retrieval` to `"mixedbread"` to replace local keyword retrieval with semantic retrieval for default, proxy, and hosted eve modes. Mixedbread returns the matching content chunk, while the AI model and the visible search dialog remain unchanged. Geistdocs falls back to local Orama retrieval when Mixedbread is unavailable. Read [Improve Ask AI answers with Mixedbread](/docs/mixedbread-retrieval) to provision a Store and configure production sync.

### Persistent Chat History

Ask AI saves the conversation to the browser's IndexedDB, so users can:

- Keep the conversation across page reloads and navigation
- Continue the conversation in a later visit
- Copy the conversation or delete it from the chat header

Geistdocs keeps a stored conversation for 30 days after its most recent message. Each new message restarts that window, and reloading a page doesn't. When Ask AI loads a conversation older than the window, it clears the conversation and starts empty.

Set `ai.history.retentionDays` to change the window:

```tsx title="geistdocs.tsx"
export const ai = {
  history: {
    retentionDays: 7,
  },
};
```

| Value                                | Behavior                                                                        |
| ------------------------------------ | ------------------------------------------------------------------------------- |
| Unset                                | Keeps conversations for 30 days after the most recent message.                  |
| A positive number                    | Keeps conversations for that many days. `0.5` keeps them for 12 hours.          |
| `0`                                  | Stores no history. Ask AI clears any stored conversation when it loads.         |
| `Infinity`                           | Never expires stored conversations.                                             |
| Negative, `NaN`, or `-Infinity`      | Uses the 30-day default and logs a warning in the browser console.              |

Chat history is stored per site, keyed by the site `title`, so changing `ai.title` keeps existing history.

### Interactive Features

- **Suggestions**: Users see up to four suggested questions when the chat is empty
- **Markdown Support**: Responses are formatted with proper Markdown rendering
- **Code Syntax Highlighting**: Code blocks in responses include syntax highlighting
- **Activity Indicators**: While a response streams, the chat shows "Thinking..." during model reasoning and an activity label while tools run. Set `ai.toolLabels` to replace the label for specific tools

Ask AI accepts text questions only. It does not support file or image attachments.

### Mobile-Responsive

The chat interface adapts to different screen sizes:

- **Desktop**: Slides in from the right side as a sidebar panel. Select **Expand chat** in the panel header to fill the viewport, which gives long answers and code blocks more width without losing the conversation. Select **Collapse chat** or press `Escape` to return to the sidebar. Closing the chat also returns it to the sidebar, so the next open starts there.
- **Mobile**: Opens as a bottom drawer. The drawer has no expanded view.

## Configuration

The environment variables `AI_GATEWAY_API_KEY` and `NEXT_PUBLIC_VERCEL_PROJECT_PRODUCTION_URL` must be set for the default AI Gateway mode. Set `GEISTDOCS_CHAT_PROXY_URL` to route Ask AI through a Vertex-backed proxy instead. Eve agent mode needs `GEISTDOCS_CHAT_SECRET` for multi-turn sessions and prewarming. You can read more in the [Environment Variables](/docs/env) section.

In default AI Gateway mode, Geistdocs uses `openai/gpt-5.6-sol` via the Vercel AI Gateway. In Vertex-backed proxy mode, the configured proxy controls the upstream model.

## Suggested Prompts

Set `ai.suggestions` to the prompts readers see before they send their first message. Selecting a suggestion sends its text as the first message. Ask AI shows the first four distinct, non-blank suggestions.

Each suggestion is a string or an object with `text` and an optional `icon`. String suggestions render without an icon. The `icon` field accepts any React element, such as an inline SVG component or an icon from a library your site already installs:

```tsx title="geistdocs.tsx"
const RocketIcon = () => (
  <svg aria-hidden="true" fill="none" height="16" viewBox="0 0 16 16" width="16">
    <path d="M8 1.5c2.5 1.5 3.5 4 3.5 6.5L8 12 4.5 8c0-2.5 1-5 3.5-6.5Z" stroke="currentColor" />
  </svg>
);

const BookIcon = () => (
  <svg aria-hidden="true" fill="none" height="16" viewBox="0 0 16 16" width="16">
    <path d="M3 2.5h7.5v11H3zM10.5 2.5H13v11h-2.5" stroke="currentColor" />
  </svg>
);

export const suggestions = [
  "What is Acme?",
  { text: "How do I deploy?", icon: <RocketIcon /> },
  { text: "Where is the API reference?", icon: <BookIcon /> },
];
```

The generated `lib/geistdocs/config.tsx` forwards `suggestions` to `ai.suggestions`. Read the [Configuration](/docs/configuration) section for the rest of the configuration file.
