---
title: Assistente
description: >-
  Um assistente na própria página fundamentado na sua documentação — perguntas sugeridas, instruções personalizadas, dimensionamento da recuperação, adaptadores de provedor do Vercel AI Gateway a qualquer endpoint compatível com OpenAI, e a saída de servidor que ele exige.
---

Adicione um assistente que responde às perguntas dos leitores num painel de chat na própria página, apoiado por um endpoint de servidor com streaming e pelo [AI SDK](https://ai-sdk.dev). É opcional, e a documentação estática continua totalmente estática até você ativá-lo:

```ts blume.config.ts lineNumbers
ai: {
  assistant: {
    enabled: true,
  },
}
```

Sem mais nada configurado, as respostas são transmitidas pelo [Vercel AI Gateway](#adapters) a partir de `openai/gpt-5.5`. Escolha outro modelo ou provedor com um [adaptador](#adapters).

## Perguntas sugeridas [#suggested-questions]

Preencha o estado vazio com alguns prompts iniciais. Cada um aparece como uma sugestão clicável — clique em uma para enviá-la — com um [ícone Lucide](/docs/content/components#icon) opcional ao lado do rótulo:

```ts blume.config.ts lineNumbers
ai: {
  assistant: {
    enabled: true,
    suggestions: [
      { label: "What is Blume?", icon: "rocket" },
      { label: "How do I write a docs page?", icon: "file-text" },
      { label: "How do I configure the theme?", icon: "settings" },
    ],
  },
}
```

`label` é a pergunta que será feita; `icon` é opcional. Deixe `suggestions` sem definir (ou vazio) e o painel abre com um campo de entrada simples.

## Instruções personalizadas [#custom-instructions]

Adicione o seu próprio texto de system prompt com `instructions` — identidade, idioma, tom ou qualquer outra coisa que o assistente deva ter em mente:

```ts blume.config.ts lineNumbers
ai: {
  assistant: {
    enabled: true,
    instructions:
      "You are Bloomy, the Acme docs assistant. Answer in the language the question was asked in, and keep answers under three paragraphs.",
  },
}
```

Seu texto é **acrescentado às** instruções internas em vez de substituí-las: a parte interna carrega o contrato de [fundamentação](#grounding) — responder apenas a partir das páginas recuperadas, citá-las como links Markdown — do qual as citações do painel de chat dependem, então ela permanece intacta independentemente do que você adicionar.

## Fundamentação [#grounding]

O assistente é **fundamentado na sua documentação**. Para cada pergunta ele recupera as páginas mais relevantes — usando o mesmo índice léxico do [Orama](/docs/configuration/search) que alimenta a busca na página — e as injeta no system prompt do modelo, de forma que as respostas venham do seu conteúdo e não do conhecimento próprio do modelo. O assistente é instruído a responder apenas a partir das páginas recuperadas, a dizer quando algo não está coberto e a citar as páginas de onde tirou a informação.

A página em que o leitor está no momento é adicionada primeiro ao contexto e usada para limitar a recuperação ao idioma daquela página, para que as respostas continuem relevantes ao ponto em que ele está na documentação. A recuperação roda no momento da requisição a partir de um snapshot embutido no build, então funciona independentemente do seu provedor de [busca](/docs/configuration/search) — até mesmo com `search: false` — e não precisa de configuração.

A fundamentação está ativa para todos os adaptadores, exceto o **[Inkeep](#inkeep)**, que roda a própria recuperação sobre o conteúdo que você indexou no painel dele.

## Tamanho da recuperação [#retrieval-size]

Quanta documentação uma pergunta carrega é a maior alavanca sobre quanto tempo o leitor espera pela primeira palavra: o modelo lê cada caractere injetado antes de emitir um token. Em um modelo de fronteira hospedado isso é imperceptível, mas em um backend auto-hospedado é o fator dominante. `retrieval` dimensiona isso:

```ts blume.config.ts lineNumbers
ai: {
  assistant: {
    enabled: true,
    retrieval: {
      maxResults: 3, // fewer pages retrieved per question
      excerptChars: 1200, // shorter excerpt from each one
      contextBudget: 3000, // smaller total injection
    },
  },
}
```

| Opção | Padrão | Descrição |
| --- | --- | --- |
| `maxResults` | `6` | Documentos recuperados por pergunta. |
| `excerptChars` | `2000` | Caracteres mantidos de cada página recuperada. |
| `contextBudget` | `10000` | Total de caracteres injetados, somando todos os trechos. |

Os três não são intercambiáveis. `contextBudget` limita toda a injeção, `excerptChars` decide quão fundo o trecho de uma única página longa vai — aumente quando uma página contém a resposta inteira e o trecho a corta — e `maxResults` limita quantas páginas a recuperação adiciona. A página que o leitor está vendo é injetada além das recuperadas, então uma resposta pode citar até uma página a mais que `maxResults`.

Os padrões servem bem para um modelo hospedado. Reduza-os quando você estiver servindo a partir do seu próprio hardware e o tempo até o primeiro token importar mais que a abrangência; as respostas continuam fundamentadas de qualquer forma, e o assistente é instruído a dizer quando algo não está coberto em vez de preencher a lacuna.

## Endpoint externo [#external-endpoint]

Já tem um backend de API para IA? Aponte o painel para ele e mantenha o build da documentação estático:

```ts blume.config.ts lineNumbers
ai: {
  assistant: {
    enabled: true,
    endpoint: "https://api.example.com/v1/docs/ask",
  },
}
```

O Blume envia o mesmo corpo `POST` que a rota interna:

```json
{
  "messages": [{ "role": "user", "content": "How do I deploy?" }],
  "page": { "path": "/deployment" }
}
```

Retorne uma resposta bem-sucedida cujo corpo seja um fluxo de texto UTF-8 puro. Se o endpoint estiver em outra origem, permita a origem da documentação com CORS: aceite `OPTIONS` e `POST`, permita o cabeçalho de requisição `content-type` e retorne os cabeçalhos CORS tanto no preflight quanto na resposta com streaming. Com `endpoint` definido, o Blume gera a interface de chat mas nenhuma rota de servidor, snapshot de fundamentação, dependência de provedor ou aviso de segredo de provedor; seu backend fica responsável pela recuperação, autenticação, limitação de taxa, acesso ao modelo e citações. Um adaptador definido junto com ele é ignorado.

## Chamadas de outras origens [#cross-origin-callers]

O endpoint gerado responde ao assistente na página, na própria origem dele. Para chamá-lo também a partir de outro site — uma página de marketing com um campo de perguntas, por exemplo — liste a origem desse site em `cors`:

```ts blume.config.ts lineNumbers
ai: {
  assistant: {
    enabled: true,
    cors: ["https://www.example.com"],
  },
}
```

A rota então responde ao preflight `OPTIONS` do navegador e nomeia uma origem listada em toda resposta — tanto na resposta com streaming quanto nos status de erro, para que quem chama consiga distinguir um corpo rejeitado de uma falha do provedor. Origens que não estão listadas não recebem cabeçalho nenhum e continuam sujeitas à regra de mesma origem do navegador. Cada entrada é reduzida à sua origem, então `https://www.example.com/docs/` e `https://www.example.com` significam a mesma coisa. Para permitir que qualquer página chame a rota, liste `"*"` em vez de origens.

Quem chama envia o mesmo corpo `POST` que o contrato do [endpoint externo](#external-endpoint) descreve e lê de volta o mesmo fluxo de texto. Envie-o como JSON com um cabeçalho `content-type: application/json`:

```ts
const response = await fetch("https://docs.example.com/api/ask", {
  body: JSON.stringify({
    messages: [{ role: "user", content: "How do I deploy?" }],
  }),
  headers: { "content-type": "application/json" },
  method: "POST",
});
```

O content type importa: a verificação de requisição entre sites do Astro rejeita um `POST` de outra origem que não tenha content type, ou que tenha um tipo parecido com formulário como `text/plain`, com um 403 antes que a rota rode, e essa resposta não carrega cabeçalhos CORS, então o navegador a reporta como um erro de rede em vez de um status. O preflight permite quaisquer cabeçalhos de requisição que quem chama pedir, então um wrapper de fetch que adiciona os próprios cabeçalhos não precisa de configuração extra.

`cors` afeta apenas a rota gerada; com um `endpoint` externo, o CORS é responsabilidade daquele backend, e definir os dois é um erro de configuração. O endpoint continua sem autenticação de qualquer forma, então o conselho sobre [limitação de taxa](#rate-limiting) vale para o tráfego de outras origens também.

## Saída de servidor obrigatória [#server-output-required]

O backend interno do assistente do Blume é uma rota de servidor (`POST /api/ask`), então ele não pode rodar num build estático. Indique um adaptador de host de `blume/deploy` para mudar para saída de servidor:

```ts blume.config.ts lineNumbers
import { vercel } from "blume/deploy";

export default defineConfig({
  deployment: vercel(),
});
```

Um build estático com o assistente ativado e sem um `endpoint` externo falha imediatamente com uma mensagem dizendo para você definir um adaptador de host. Veja [Implantação](/docs/deployment) para os adaptadores.

## Adaptadores [#adapters]

`provider` escolhe o backend que responde. O valor dele é um **adaptador**: uma pequena função exportada de `blume/ai` que recebe as opções próprias daquele backend e retorna um descritor simples que o Blume escreve na rota gerada. Cada adaptador é responsável pelo próprio modelo, pela variável de ambiente de onde a chave é lida, por como mapeia o [raciocínio](#reasoning) e por qual SDK de provedor ele precisa — então não há um conjunto compartilhado de campos para conciliar entre backends:

```ts blume.config.ts lineNumbers
import { defineConfig } from "blume";
import { openrouter } from "blume/ai";

export default defineConfig({
  ai: {
    assistant: {
      enabled: true,
      provider: openrouter({ model: "anthropic/claude-sonnet-4-5" }),
    },
  },
});
```

| Adaptador | Responde com | Variável de ambiente da chave de API | SDK a instalar |
| --- | --- | --- | --- |
| [`gateway()`](#vercel-ai-gateway) (padrão) | uma string `provider/model` via o Vercel AI Gateway | `AI_GATEWAY_API_KEY` | nenhum — já vem com o Blume |
| [`openrouter()`](#openrouter) | qualquer modelo do [OpenRouter](https://openrouter.ai) | `OPENROUTER_API_KEY` | `@openrouter/ai-sdk-provider` |
| [`llmgateway()`](#llmgateway) | qualquer modelo do [LLMGateway](https://llmgateway.io) | `LLMGATEWAY_API_KEY` | `@ai-sdk/openai-compatible` |
| [`inkeep()`](#inkeep) | um modelo de QA do [Inkeep](https://inkeep.com) | `INKEEP_API_KEY` | `@ai-sdk/openai-compatible` |
| [`openaiCompatible()`](#openai-compatible-endpoints) | o que o seu endpoint servir | a `apiKeyEnv` que você indicar | `@ai-sdk/openai-compatible` |

Os SDKs são dependências de par opcionais, então adicione ao seu projeto aquele de que o seu adaptador precisa (por exemplo, `npm install @openrouter/ai-sdk-provider`). Se estiver faltando, o `blume build` para antes de o Vite rodar, indicando o pacote e o comando que o instala, e o [`blume doctor`](/docs/cli/doctor) também reporta isso.

O descritor que um adaptador retorna é só dados — o tipo dele, as opções, as variáveis de ambiente que ele lê e o SDK de que ele precisa — então a rota gerada (e a [ejetada](/docs/configuration/customization#eject)) o embute como literais e importa o SDK do provedor pelo nome. Nada lê o `blume.config.ts` no momento da requisição, e nenhum segredo jamais é escrito numa rota: os adaptadores recebem o **nome** da variável de ambiente que contém a chave, e a rota lê o valor através do [`getSecret()`](https://docs.astro.build/en/guides/environment-variables/#retrieving-secrets-programmatically) do Astro, então cada adaptador de deploy o fornece do seu próprio jeito: variáveis de ambiente no Node, na Vercel e no Netlify, e os [bindings](https://docs.astro.build/en/guides/integrations-guide/cloudflare/#environment-variables-and-secrets) do Worker no Cloudflare.

### Vercel AI Gateway

O padrão. `model` é uma string `provider/model`, então você troca de modelo alterando esse valor (`openai/gpt-5.5`, `anthropic/claude-sonnet-4-5` e assim por diante) sem nenhum SDK de provedor para instalar. O gateway lê `AI_GATEWAY_API_KEY` do seu ambiente e é configurado automaticamente quando você faz o deploy na Vercel, onde ele também pode se autenticar com o token OIDC do deploy:

```ts blume.config.ts lineNumbers
import { defineConfig } from "blume";
import { gateway } from "blume/ai";

export default defineConfig({
  ai: {
    assistant: {
      enabled: true,
      provider: gateway({ model: "anthropic/claude-sonnet-4-5" }),
    },
  },
});
```

Deixar `provider` sem definir é o mesmo que `gateway({ model: "openai/gpt-5.5" })`.

### OpenRouter

Qualquer modelo do [OpenRouter](https://openrouter.ai), através do provedor dedicado dele para o AI SDK:

```ts blume.config.ts lineNumbers
import { defineConfig } from "blume";
import { openrouter } from "blume/ai";

export default defineConfig({
  ai: {
    assistant: {
      enabled: true,
      provider: openrouter({
        model: "anthropic/claude-sonnet-4-5",
        reasoning: "none",
      }),
    },
  },
});
```

### LLMGateway

Qualquer modelo do [LLMGateway](https://llmgateway.io), através do endpoint compatível com OpenAI dele:

```ts blume.config.ts lineNumbers
import { defineConfig } from "blume";
import { llmgateway } from "blume/ai";

export default defineConfig({
  ai: {
    assistant: {
      enabled: true,
      provider: llmgateway({ model: "openai/gpt-5.5" }),
    },
  },
});
```

`baseUrl` substitui o endpoint predefinido (`https://api.llmgateway.io/v1`) quando você mesmo roda o LLMGateway.

### Inkeep

O [Inkeep](https://inkeep.com) responde a partir do conteúdo que você indexou no painel do Inkeep — ele roda a própria recuperação — então o Blume o deixa **sem fundamentação**: nenhum snapshot das páginas deste site é injetado, e as opções de [tamanho da recuperação](#retrieval-size) não se aplicam. Ele também não tem controle de raciocínio, então o adaptador não aceita `reasoning`:

```ts blume.config.ts lineNumbers
import { defineConfig } from "blume";
import { inkeep } from "blume/ai";

export default defineConfig({
  ai: {
    assistant: {
      enabled: true,
      provider: inkeep({ model: "inkeep-qa-expert" }),
    },
  },
});
```

`baseUrl` substitui o endpoint predefinido (`https://api.inkeep.com/v1`).

### Endpoints compatíveis com OpenAI [#openai-compatible-endpoints]

Qualquer endpoint que fale a API da OpenAI funciona através de `openaiCompatible()` — informe a `baseUrl` dele, o `model` que ele serve e a variável de ambiente que contém a chave dele. Um endpoint genérico não tem valor predefinido para nenhum desses, então os três são obrigatórios; `name` é o nome de provedor que o AI SDK reporta e tem `openai-compatible` como padrão:

```ts blume.config.ts lineNumbers
import { defineConfig } from "blume";
import { openaiCompatible } from "blume/ai";

export default defineConfig({
  ai: {
    assistant: {
      enabled: true,
      provider: openaiCompatible({
        baseUrl: "https://my-gateway.example.com/v1",
        apiKeyEnv: "MY_GATEWAY_API_KEY",
        model: "gpt-4o",
        name: "my-gateway",
      }),
    },
  },
});
```

### Opções que todo adaptador aceita [#options-every-adapter-takes]

**`apiKeyEnv`** aponta um adaptador para uma variável de ambiente diferente da padrão dele — `gateway({ apiKeyEnv: "DOCS_GATEWAY_KEY" })` lê essa variável em vez de `AI_GATEWAY_API_KEY`, e o aviso de segredo ausente no `blume dev`/`build` também a verifica. Enquanto a chave não estiver definida, a rota implantada responde `503` com uma mensagem indicando o nome da variável. A rota lê um corpo de requisição de até 64 KB e responde a qualquer coisa maior com `413`.

**`headers`** envia cabeçalhos de requisição estáticos em toda chamada — um cabeçalho que identifica quem chama num backend compartilhado, por exemplo, para que a observabilidade ou a limitação de taxa dele consiga distinguir a sua documentação do restante do tráfego:

```ts blume.config.ts lineNumbers
provider: openaiCompatible({
  baseUrl: "https://llm.internal.example.com/v1",
  apiKeyEnv: "INTERNAL_LLM_API_KEY",
  model: "gpt-4o",
  headers: { "X-Caller-Id": "docs" },
}),
```

Os valores são escritos na rota gerada como estão, então mantenha os segredos em `apiKeyEnv` em vez de em `headers`. O cabeçalho `Authorization` da chave de API é aplicado primeiro, então um cabeçalho personalizado não consegue substituí-lo.

**`providerOptions`** repassa qualquer outra coisa diretamente para o [`providerOptions`](https://ai-sdk.dev/docs/foundations/prompts#provider-options) do AI SDK, no formato do próprio SDK — com chaves por provedor e depois por opção — então um novo controle de modelo nunca precisa de um campo próprio no Blume:

```ts blume.config.ts lineNumbers
provider: gateway({
  model: "openai/gpt-5.5",
  providerOptions: { openai: { textVerbosity: "low" } },
}),
```

O Blume mapeia apenas as opções que ele nomeia (`model`, `reasoning`, `apiKeyEnv`, `headers`) e repassa `providerOptions` literalmente, então ele precisa ser JSON — ele é embutido na rota — e precisa usar a chave que o provedor subjacente espera (`openai` para um modelo da OpenAI atrás do gateway, `openrouter` no OpenRouter). Ativar o assistente também liga o React para a ilha na página — veja [Personalização](/docs/configuration/customization#interactive-islands).

## Raciocínio [#reasoning]

Modelos de raciocínio pensam antes de responder, e o quanto eles fazem isso por padrão varia de modelo para modelo. Para perguntas e respostas fundamentadas na documentação, os trechos recuperados carregam a resposta, então a maior parte desse pensamento é latência que o leitor aguarda. A opção `reasoning` de um adaptador define o quanto o modelo raciocina: `"none"`, `"minimal"`, `"low"`, `"medium"`, `"high"` ou `"xhigh"`:

```ts blume.config.ts lineNumbers
provider: gateway({ model: "openai/gpt-5.5", reasoning: "none" }),
```

Cada adaptador envia o nível como o controle de esforço de raciocínio do próprio backend, e é por isso que a opção fica no adaptador, e não em `assistant`:

| Adaptador | O que o nível vira |
| --- | --- |
| `gateway()` | A opção de chamada [`reasoning`](https://ai-sdk.dev/docs/ai-sdk-core/reasoning) do AI SDK, que o gateway mapeia para a configuração do próprio modelo — o `reasoning_effort` da OpenAI, por exemplo. |
| `openrouter()` | O `reasoning.effort` do OpenRouter, definido no modelo. O provedor dele ignora a opção de chamada do AI SDK, então o nível é colocado onde o OpenRouter o lê. |
| `llmgateway()` | `reasoning_effort` na requisição, através da opção de chamada do AI SDK. |
| `openaiCompatible()` | `reasoning_effort` na requisição, então o endpoint precisa aceitar esse parâmetro. |
| `inkeep()` | Não disponível. O Inkeep roda o próprio pipeline de QA sem controle de raciocínio, então o adaptador não tem a opção `reasoning`, e definir uma é um erro de configuração. |

O modelo precisa suportar o nível que você escolher: a OpenAI rejeita um nível que o modelo não oferece (`"none"` e `"xhigh"` existem apenas em alguns), então confira a documentação do modelo antes de definir um. Deixe sem definir para manter o padrão do modelo. Assim como o [tamanho da recuperação](#retrieval-size), isso troca abrangência por tempo até o primeiro token, e as respostas continuam fundamentadas de qualquer forma.

## Analytics

Com um [provedor de analytics](/docs/configuration/analytics) configurado, o assistente reporta seu uso através do mesmo `track()` que o widget de feedback da página usa, então as perguntas aparecem ao lado das suas visualizações de página:

| Evento | Quando | Propriedades |
| --- | --- | --- |
| `ask` | Uma pergunta é enviada | `path`, `questionChars` |
| `ask_answer` | A resposta termina de ser transmitida | `path`, `questionChars`, `ms`, `chars` |
| `ask_error` | A requisição falha, quebra ou volta vazia | `path`, `questionChars`, `ms`, `status` |

`path` é a página de onde o leitor perguntou (o pathname servido, então ele corresponde ao widget de feedback e às suas visualizações de página sob um `base`), `questionChars` o comprimento da pergunta, `ms` o tempo entre o envio da pergunta e o último chunk, e `chars` o comprimento da resposta. `status` é o status HTTP: `0` quando nenhuma resposta chegou (offline, DNS, CORS), e `200` quando a resposta estava certa mas o fluxo dela quebrou no meio da resposta — como um erro de provedor ou de credencial se manifesta, já que o backend já enviou os cabeçalhos — ou não entregou nada. Limpar a conversa no meio de uma resposta não reporta nenhum dos dois resultados.

O texto da pergunta nunca chega a um provedor: é entrada livre do leitor (chaves coladas, logs de erro, nomes) que violaria os termos e os limites de tamanho por valor da maioria dos provedores. Ele viaja apenas no evento de DOM `blume:track`, como `question` em `detail.props`, então um listener que você escreva pode encaminhá-lo para onde você decidir que ele pertence. Uma interface de chat personalizada construída sobre o `useAssistant` de `blume/hooks` reporta os mesmos eventos. Sem nenhum provedor configurado, as chamadas ao provedor interno não fazem nada, mas o evento `blume:track` ainda é disparado, então uma integração personalizada que o escute recebe esses eventos.

## Limitação de taxa [#rate-limiting]

O endpoint `POST /api/ask` é **não autenticado** — ele precisa ser, para que o assistente na página possa chamá-lo. O Blume valida cada requisição — rejeitando corpos malformados, limitando a 1–40 mensagens e aceitando apenas os papéis `user`/`assistant` para que quem chama não possa injetar o próprio system prompt e reaproveitar a rota como um proxy genérico de LLM — para limitar quanto uma única chamada pode gastar com o seu modelo, mas isso não impede alguém de chamar o endpoint repetidamente. Se o abuso de custos for uma preocupação, coloque a rota atrás de um limitador de taxa — a limitação de taxa na borda do seu host (a da Vercel, por exemplo), um middleware, ou os limites de gasto por chave do seu provedor de modelos.

O endpoint é anunciado no [manifesto de legibilidade para agentes](/docs/discoverability/agent-discovery#agent-readability) junto com o restante da superfície legível por máquina do site.
