Assistente
Um assistente na própria página fundamentado na sua documentação — perguntas sugeridas, instruções personalizadas, dimensionamento da recuperação, adaptadores de provedor para OpenAI, Anthropic, Gemini, Grok, o Vercel AI Gateway e 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. É opcional, e a documentação estática continua totalmente estática até você ativá-lo:
ai: {
assistant: {
enabled: true,
},
}
Sem mais nada configurado, as respostas são transmitidas pelo Vercel AI Gateway a partir de openai/gpt-5.5. Escolha outro modelo ou provedor com um adaptador.
Perguntas sugeridas
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 opcional ao lado do rótulo:
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.
Perguntando sobre código
Com o assistente ativado, todo bloco de código ganha um botão Ask ao lado do botão de copiar. Ele abre o assistente com o bloco anexado, exibido como um chip acima do campo de entrada, para que o leitor possa perguntar o que o código faz, por que ele falha ou como adaptá-lo. Se for enviado sem uma pergunta, ele pede ao assistente que explique o código. O código vai para o modelo como um bloco de código delimitado depois da pergunta, com até 6.000 caracteres.
Uma interface de chat personalizada construída sobre o useAssistant pode fazer o mesmo: escute blume:open-assistant em window, cujo detail.code contém language, source e title do bloco.
Contatar o suporte
Quando a documentação não tiver a resposta, encaminhe o leitor para uma pessoa. Defina support e, assim que houver uma conversa, o painel mostra um link Contact support abaixo dela:
ai: {
assistant: {
enabled: true,
support: "mailto:help@example.com", // or "https://example.com/support", or "/support"
},
},
Um endereço mailto: inicia um e-mail com a conversa como corpo, para que o leitor não precise explicar tudo de novo (uma conversa longa mantém as trocas mais recentes). Uma URL ou uma página do seu site recebe o id da conversa como um parâmetro de query thread. Os eventos de analytics ask, ask_answer e ask_error do assistente carregam o mesmo thread, para que a sua equipe de suporte consiga encontrar a conversa no seu analytics.
Instruções personalizadas
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:
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 — 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
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 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 — e, num site versionado, à versão da documentação dela — para que as respostas continuem relevantes ao ponto em que ele está na documentação. Uma pergunta feita fora da documentação, como numa landing page, fica restrita ao idioma indicado pelo prefixo de locale da URL (/ja), ou ao idioma padrã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 — até mesmo com search: false — e não precisa de configuração.
A fundamentação está ativa para todos os adaptadores, exceto o Inkeep, que roda a própria recuperação sobre o conteúdo que você indexou no painel dele.
Buscando e lendo páginas
As páginas recuperadas são um ponto de partida. O assistente também pode pesquisar por conta própria: ele tem uma ferramenta search_docs que busca na sua documentação e uma ferramenta read_page que lê uma página inteira, para quando um trecho termina antes da resposta. Ele pode buscar, ler uma ou duas páginas, buscar de novo e então responder, tudo dentro de uma única pergunta. As chamadas de ferramenta rodam no seu servidor sobre o mesmo snapshot da fundamentação, respeitam o idioma e a versão da documentação do leitor e nunca chegam até ele; o leitor vê apenas a resposta.
As ferramentas vêm ativadas por padrão para todo adaptador cujos modelos suportam chamada de ferramentas. Elas ficam desativadas para endpoints compatíveis com OpenAI, já que um modelo auto-hospedado pode não suportar. Defina tools para mudar qualquer um dos padrões:
ai: {
assistant: {
enabled: true,
provider: openai({ baseUrl, model, apiKeyEnv }),
tools: true, // this model supports tool calling
},
}
Cada pergunta leva no máximo cinco etapas do modelo: até quatro rodadas de chamadas de ferramenta e depois a resposta. Com tools: false, o assistente responde apenas a partir das páginas recuperadas, como fazia antes de as ferramentas existirem.
Tamanho da recuperação
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:
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.
No seu próprio hardware, dimensione também a janela de contexto do modelo para o prompt inteiro, e não só para os trechos: a conversa até ali acrescenta até 24.000 caracteres e, com tools ativado, um único resultado de read_page acrescenta até 20.000, cerca de 5.000 tokens sozinho. O Ollama, por exemplo, usa por padrão uma janela de 4k tokens em GPUs menores, e um prompt que a ultrapassa não falha: o Ollama descarta o início do prompt, onde ficam as instruções e os trechos, e o modelo responde sem a sua documentação.
Endpoint externo
Já tem um backend de API para IA? Aponte o painel para ele e mantenha o build da documentação estático:
ai: {
assistant: {
enabled: true,
endpoint: "https://api.example.com/v1/docs/ask",
},
}
O Blume envia o mesmo corpo POST que a rota interna:
{
"messages": [{ "role": "user", "content": "How do I deploy?" }],
"page": { "path": "/deployment" }
}
messages é a conversa até aqui, terminando com a nova pergunta. A rota interna aceita no máximo 40 mensagens e 24.000 caracteres delas em JSON, então, quando uma conversa ultrapassa qualquer um desses limites, o painel envia as trocas mais recentes que cabem e descarta as mais antigas.
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
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:
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. Essas respostas também expõem Retry-After, então, depois do 429 de uma limitação de taxa, quem chama consegue ler quantos segundos esperar. 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 descreve e lê de volta o mesmo fluxo de texto. Envie-o como JSON com um cabeçalho content-type: application/json:
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 vale para o tráfego de outras origens também.
Saída de servidor obrigatória
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:
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 para os adaptadores.
Adaptadores
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 e por qual SDK de provedor ele precisa — então não há um conjunto compartilhado de campos para conciliar entre backends:
import { defineConfig } from "blume";
import { anthropic } from "blume/ai";
export default defineConfig({
ai: {
assistant: {
enabled: true,
provider: anthropic({ model: "claude-sonnet-5" }),
},
},
});
| Adaptador | Responde com | Variável de ambiente da chave de API | SDK a instalar |
|---|---|---|---|
openai() |
um modelo da OpenAI, ou qualquer endpoint compatível com OpenAI | OPENAI_API_KEY |
@ai-sdk/openai (@ai-sdk/openai-compatible com uma baseUrl) |
anthropic() |
um modelo Claude | ANTHROPIC_API_KEY |
@ai-sdk/anthropic |
gemini() |
um modelo Gemini | GEMINI_API_KEY |
@ai-sdk/google |
grok() |
um modelo Grok da xAI | XAI_API_KEY |
@ai-sdk/xai |
gateway() (padrão) |
uma string provider/model via o Vercel AI Gateway |
AI_GATEWAY_API_KEY |
nenhum — já vem com o Blume |
openrouter() |
qualquer modelo do OpenRouter | OPENROUTER_API_KEY |
@openrouter/ai-sdk-provider |
llmgateway() |
qualquer modelo do LLMGateway | LLMGATEWAY_API_KEY |
@ai-sdk/openai-compatible |
inkeep() |
um modelo de QA do Inkeep | INKEEP_API_KEY |
@ai-sdk/openai-compatible |
openai(), anthropic(), gemini() e grok() chamam a API do próprio provedor com a sua chave — não há nada no meio. 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 @ai-sdk/anthropic). 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 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) 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() 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 do Worker no Cloudflare.
OpenAI
Qualquer modelo da OpenAI, chamado diretamente através do próprio provedor da OpenAI para o AI SDK:
import { defineConfig } from "blume";
import { openai } from "blume/ai";
export default defineConfig({
ai: {
assistant: {
enabled: true,
provider: openai({ model: "gpt-5.5" }),
},
},
});
Endpoints compatíveis com OpenAI
Qualquer endpoint que fale a API da OpenAI — um modelo auto-hospedado, um gateway interno — também funciona através de openai(): adicione a baseUrl dele e, geralmente, apiKeyEnv com a variável de ambiente que contém a chave dele (o padrão é OPENAI_API_KEY). name é o nome de provedor que o AI SDK reporta e a chave sob a qual o endpoint lê providerOptions; o padrão é openai-compatible:
import { defineConfig } from "blume";
import { openai } from "blume/ai";
export default defineConfig({
ai: {
assistant: {
enabled: true,
provider: openai({
baseUrl: "https://my-gateway.example.com/v1",
apiKeyEnv: "MY_GATEWAY_API_KEY",
model: "gpt-4o",
name: "my-gateway",
}),
},
},
});
Com uma baseUrl, a rota chama a API Chat Completions do endpoint através de @ai-sdk/openai-compatible, então instale esse pacote em vez de @ai-sdk/openai: a maioria dos endpoints compatíveis não serve a Responses API da OpenAI. As ferramentas de documentação ficam desativadas por padrão, já que um modelo auto-hospedado pode não suportar chamada de ferramentas.
Anthropic
Qualquer modelo Claude, chamado diretamente através do provedor da Anthropic para o AI SDK:
import { defineConfig } from "blume";
import { anthropic } from "blume/ai";
export default defineConfig({
ai: {
assistant: {
enabled: true,
provider: anthropic({ model: "claude-sonnet-5" }),
},
},
});
Gemini
Qualquer modelo Gemini, chamado diretamente através do provedor do Google para o AI SDK. A chave é uma chave de API do Google AI Studio:
import { defineConfig } from "blume";
import { gemini } from "blume/ai";
export default defineConfig({
ai: {
assistant: {
enabled: true,
provider: gemini({ model: "gemini-3.5-flash" }),
},
},
});
Grok
Qualquer modelo Grok da xAI, chamado diretamente através do provedor da xAI para o AI SDK:
import { defineConfig } from "blume";
import { grok } from "blume/ai";
export default defineConfig({
ai: {
assistant: {
enabled: true,
provider: grok({ model: "grok-4.7" }),
},
},
});
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:
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, através do provedor dedicado dele para o AI SDK:
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, através do endpoint compatível com OpenAI dele:
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 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 não se aplicam. Ele também não tem controle de raciocínio, então o adaptador não aceita reasoning:
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).
Opções que todo adaptador aceita
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:
provider: openai({
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 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:
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 openai() ou para um modelo da OpenAI atrás do gateway, anthropic para anthropic(), google para gemini(), xai para grok(), openrouter no OpenRouter e o name do adaptador num endpoint compatível com OpenAI). Ativar o assistente também liga o React para a ilha na página — veja Personalização.
Raciocínio
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":
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 |
|---|---|
openai() |
O reasoning_effort da OpenAI. Com uma baseUrl, ele é enviado como reasoning_effort na requisição, então o endpoint precisa aceitar esse parâmetro. |
anthropic() |
O pensamento do Claude: um nível de esforço nos modelos com pensamento adaptativo, um orçamento de pensamento nos mais antigos e o pensamento desligado para "none". |
gemini() |
O nível de pensamento do Gemini, ou um orçamento de pensamento nos modelos que aceitam um. |
grok() |
O esforço de raciocínio da xAI, nos modelos que oferecem um. |
gateway() |
A opção de chamada 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. |
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, isso troca abrangência por tempo até o primeiro token, e as respostas continuam fundamentadas de qualquer forma.
Analytics
Com um provedor de 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, thread |
ask_answer |
A resposta termina de ser transmitida | path, questionChars, thread, ms, chars |
ask_error |
A requisição falha, quebra ou volta vazia | path, questionChars, thread, 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, thread o id da conversa (um novo quando o leitor a limpa), 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
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. Em seguida, a limitação de taxa, ativada por padrão, limita quantas perguntas um mesmo leitor pode fazer: passado o limite, o painel pede que ele tente de novo em alguns minutos.
Proteção contra bots
A limitação de taxa conta por endereço IP, então um script que distribui as perguntas por vários endereços passa por ela. Para barrar scripts, adicione uma verificação de bots de blume/captcha: antes de cada pergunta, o painel obtém um token da verificação, e a rota o valida com o provedor antes de o modelo rodar. A maioria dos leitores nunca vê um desafio; a verificação só pede um clique quando não tem certeza.
import { defineConfig } from "blume";
import { turnstile } from "blume/captcha";
export default defineConfig({
ai: {
assistant: {
enabled: true,
captcha: turnstile({ siteKey: "0x4AAAAAAA…" }),
},
},
});
| Adaptador | Provedor | Variável de ambiente da chave secreta |
|---|---|---|
turnstile() |
Cloudflare Turnstile | TURNSTILE_SECRET_KEY |
hcaptcha() |
hCaptcha, executado de forma invisível | HCAPTCHA_SECRET_KEY |
A chave do site vai para o navegador e pode ser commitada com segurança; a chave secreta fica no servidor. No Turnstile, crie um widget no painel da Cloudflare e escolha o modo Managed ou Invisible. Enquanto o segredo não estiver definido, o assistente responde que não está configurado, e o blume build emite um aviso. O script do provedor é carregado junto com a primeira pergunta do leitor, e não com a página.
Uma verificação que falha mostra ao leitor um aviso traduzido de “não foi possível verificar que você é humano”, e a rota responde 403. Os dois provedores publicam chaves de teste que sempre passam, para você testar a verificação localmente: a 1x00000000000000000000BB do Turnstile com o segredo 1x0000000000000000000000000000000AA, e a 10000000-ffff-ffff-ffff-000000000001 do hCaptcha com 0x0000000000000000000000000000000000000000.
Com um endpoint externo, o painel continua enviando o token, como captcha no corpo da requisição, para que o seu backend o verifique.
O endpoint é anunciado no manifesto de legibilidade para agentes junto com o restante da superfície legível por máquina do site.