Setup
- Setup
- Setup
- Chat patterns
- Configuration shortcuts
- Streaming
- Tools reference
- Display source documents with the Chats API
- Handle errors and fallbacks with the Chats API
- Troubleshooting
- Next steps
- Agentic search getting started
- Display source documents
- Configure guardrails
- Optimize chat prompts
- Reduce hallucination
- Chat completions API reference
This page documents direct use of the Meilisearch /chats API: an engine-managed endpoint that consolidates retrieval, context management, and generation into a single call. For most agentic and conversational search projects, we recommend starting with the AI SDK.
In code examples, replace WORKSPACE_NAME with the name of your workspace. On Meilisearch Cloud, the default workspace name is cloud.
Setup
Enable the feature, configure your indexes, and create a workspace before you send chat completions.
Enable the chat completions feature
Enable chat completions from your Meilisearch Cloud project in one of two ways:
- Go to your project’s Settings page and enable it under Experimental features
- Or open the Chat tab in your project and activate the feature directly from there
For self-hosted instances, enable the feature through the experimental features API by sending a PATCH request with chatCompletions set to true:
cURL
curl \
-X PATCH 'MEILISEARCH_URL/experimental-features/' \
-H 'Content-Type: application/json' \
--data-binary '{
"chatCompletions": true
}'
Find your chat API key
Meilisearch automatically generates a “Default Chat API Key” that combines chatCompletions and search permissions on all indexes. Conversational search requires both actions: chatCompletions authorizes the LLM call, and search authorizes the retrieval step that feeds documents to the model. Any key you use with the /chats routes must carry both actions, so prefer the default chat API key unless you have a specific reason to create a custom one.
Check if you have the key using:
cURL
JS
Python
PHP
Java
Ruby
Go
C#
Rust
Swift
Dart
curl \
-X GET 'MEILISEARCH_URL/keys' \
-H 'Authorization: Bearer MASTER_KEY'
const client = new MeiliSearch({ host: 'MEILISEARCH_URL', apiKey: 'masterKey' })
client.getKeys()
client = Client('MEILISEARCH_URL', 'masterKey')
client.get_keys()
$client = new Client('MEILISEARCH_URL', 'masterKey');
$client->getKeys();
Client client = new Client(new Config("MEILISEARCH_URL", "masterKey"));
client.getKeys();
client = MeiliSearch::Client.new('MEILISEARCH_URL', 'masterKey')
client.keys
client := meilisearch.New("MEILISEARCH_URL", meilisearch.WithAPIKey("masterKey"))
client.GetKeys(nil);
MeilisearchClient client = new MeilisearchClient("MEILISEARCH_URL", "masterKey");
var keys = await client.GetKeysAsync();
let client = Client::new("MEILISEARCH_URL", Some("MASTER_KEY")); let keys = client .get_keys() .await .unwrap();
client = try MeiliSearch(host: "MEILISEARCH_URL", apiKey: "masterKey")
client.getKeys { result in
switch result {
case .success(let keys):
print(keys)
case .failure(let error):
print(error)
}
}
var client = MeiliSearchClient('MEILISEARCH_URL', 'masterKey');
await client.getKeys();
Look for the key with the description “Default Chat API Key”. Chat queries only search the indexes that the API key can access. The default chat API key is scoped to all indexes. To limit which indexes a chat client can reach, you have two options:
- Create a new API key with both
chatCompletionsandsearchactions, scoped to the exact indexes you want exposed. See manage API keys for the full workflow. - Generate a tenant token from the default chat API key. Tenant tokens inherit both the
chatCompletionsandsearchactions from their parent key and let you narrow index access or attach search rules per user.
A tenant token cannot grant access to an index its parent API key does not already cover. Make sure the parent key is scoped to every index the token should be allowed to reach.
If your instance does not have a Default Chat API Key, create one manually:
cURL
curl \
-X POST 'MEILISEARCH_URL/keys' \
-H 'Authorization: Bearer MEILISEARCH_KEY' \
-H 'Content-Type: application/json' \
--data-binary '{
"name": "Chat API Key",
"description": "API key for chat completions",
"actions": ["search", "chatCompletions"],
"indexes": ["*"],
"expiresAt": null
}'
Configure your indexes
Configure chat settings for each index you want the agent to search:
cURL
curl \
-X PATCH 'MEILISEARCH_URL/indexes/INDEX_NAME/settings/chat' \
-H 'Authorization: Bearer MEILISEARCH_KEY' \
-H 'Content-Type: application/json' \
--data-binary '{
"description": "A movie database containing titles, genres, release dates, keywords, and plot overviews to help users find films to watch",
"documentTemplate": "A movie titled '\''{{doc.title}}'\'' that released in {{ doc.release_date | date: '\''%Y'\'' }}. The movie genres are: {{doc.genres}}. The key themes include: {{doc.keywords}}. The storyline is about: {{doc.overview|truncatewords: 100}}",
"documentTemplateMaxBytes": 400
}'
descriptiontells the LLM what the index containsdocumentTemplateis a Liquid template that defines the text sent to the LLM for each documentdocumentTemplateMaxBytestruncates the rendered template. The default of 400 bytes balances context quality and speed
See index chat settings for the remaining fields, then consult optimize chat prompts and document template best practices for tuning.
Configure a workspace
A workspace holds your LLM provider configuration and system prompt. The model itself is chosen per request in the /chat/completions call, not in the workspace settings.
On Meilisearch Cloud, your project comes with a single default workspace named cloud. Use cloud as WORKSPACE_NAME in all API calls. If you need additional workspaces, contact us.
On self-hosted instances, you can create as many workspaces as you need. If the workspace does not exist, Meilisearch creates it when you first PATCH its settings:
cURL
curl \
-X PATCH 'MEILISEARCH_URL/chats/WORKSPACE_NAME/settings' \
-H 'Authorization: Bearer MEILISEARCH_KEY' \
-H 'Content-Type: application/json' \
--data-binary '{
"source": "openAi",
"apiKey": "PROVIDER_API_KEY",
"prompts": {
"system": "You are a helpful assistant. Answer questions based only on the provided context."
}
}'
baseUrl is required for all providers except OpenAI. See workspace settings for the provider matrix, then configure guardrails and optimize chat prompts for the system prompt.
Chat patterns
Both patterns use POST /chats/{workspace}/chat/completions. The difference is the prompt strategy and whether you keep conversation history.
Chat interface
Build a multi-turn interface where users ask follow-up questions. Meilisearch searches your indexes, then passes the retrieved documents to the LLM to generate a grounded response.
Streaming is required
All requests to the chat completions endpoint must include “stream”: true. Non-streaming (stream: false) is not yet supported and returns a 501 Not Implemented error.
Message roles
Every entry in the messages array carries a role that tells the LLM who authored it:
| Role | Origin | Typical content |
|---|---|---|
system |
Meilisearch | Workspace-level instructions (the prompts.system string) plus internal tool descriptions injected by Meilisearch. You do not need to send this role yourself. Meilisearch prepends it. |
assistant |
LLM provider | Responses generated by the configured model, including tool calls. Push these back into messages to preserve context on follow-up turns. |
user |
User input | Questions and follow-ups coming from the end user of your application. |
Send a streaming request
Send a POST request to /chats/{workspace}/chat/completions with stream: true:
cURL
OpenAI SDK
Vercel AI SDK
curl -N \
-X POST 'MEILISEARCH_URL/chats/WORKSPACE_NAME/chat/completions' \
-H 'Authorization: Bearer MEILISEARCH_KEY' \
-H 'Content-Type: application/json' \
--data-binary '{
"model": "PROVIDER_MODEL_UID",
"stream": true,
"messages": [
{
"role": "user",
"content": "What movies are about artificial intelligence?"
}
]
}'
import OpenAI from 'openai';
const client = new OpenAI({
baseURL: 'MEILISEARCH_URL/chats/WORKSPACE_NAME',
apiKey: 'MEILISEARCH_KEY',
});
const stream = await client.chat.completions.create({
model: 'PROVIDER_MODEL_UID',
stream: true,
messages: [{ role: 'user', content: 'What movies are about artificial intelligence?' }],
});
for await (const chunk of stream) {
const content = chunk.choices[0]?.delta?.content || '';
process.stdout.write(content);
}
import { createOpenAI } from '@ai-sdk/openai';
import { streamText } from 'ai';
const meilisearch = createOpenAI({
baseURL: 'MEILISEARCH_URL/chats/WORKSPACE_NAME',
apiKey: 'MEILISEARCH_KEY',
});
const { textStream } = streamText({
model: meilisearch('PROVIDER_MODEL_UID'),
messages: [{ role: 'user', content: 'What movies are about artificial intelligence?' }],
});
for await (const text of textStream) {
process.stdout.write(text);
}
This basic request works, but without Meilisearch tools you get no visibility into what is being searched and no way to keep conversation context across follow-up questions. Declare the three tools listed in tools reference.
_meiliAppendConversationMessage is the key to multi-turn conversations. The endpoint is stateless, so Meilisearch uses this tool to expose internal search tool calls and their results back to the client. Push those messages into your messages array before the next request, or the LLM loses the context from previous searches.
Complete example: progress, sources, and history
The following example combines all three tools: streaming progress, displaying sources, and maintaining conversation history.
JavaScript (Fetch)
const MEILISEARCH_TOOLS = [
{
type: 'function',
function: {
name: '_meiliSearchProgress',
description: 'Provides information about the current Meilisearch search operation',
parameters: {
type: 'object',
properties: {
call_id: { type: 'string' },
function_name: { type: 'string' },
function_parameters: { type: 'string' },
},
required: ['call_id', 'function_name', 'function_parameters'],
additionalProperties: false,
},
strict: true,
},
},
{
type: 'function',
function: {
name: '_meiliSearchSources',
description: 'Provides sources of the search',
parameters: {
type: 'object',
properties: {
call_id: { type: 'string' },
documents: { type: 'array', items: { type: 'object' } },
},
required: ['call_id', 'documents'],
additionalProperties: false,
},
strict: true,
},
},
{
type: 'function',
function: {
name: '_meiliAppendConversationMessage',
description: 'Append a new message to the conversation based on what happened internally',
parameters: {
type: 'object',
properties: {
role: { type: 'string' },
content: { type: 'string' },
tool_calls: { type: ['array', 'null'] },
tool_call_id: { type: ['string', 'null'] },
},
required: ['role', 'content', 'tool_calls', 'tool_call_id'],
additionalProperties: false,
},
strict: true,
},
},
];
const messages = [];
async function chat(userMessage) {
messages.push({ role: 'user', content: userMessage });
const response = await fetch('MEILISEARCH_URL/chats/WORKSPACE_NAME/chat/completions', {
method: 'POST',
headers: {
Authorization: 'Bearer MEILISEARCH_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'PROVIDER_MODEL_UID',
stream: true,
messages,
tools: MEILISEARCH_TOOLS,
}),
});
const reader = response.body?.getReader();
if (!reader) throw new Error('No readable stream on response');
const decoder = new TextDecoder();
let answer = '';
let buffer = '';
const pendingToolCalls = {};
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop() ?? '';
for (const line of lines) {
if (!line.startsWith('data: ') || line === 'data: [DONE]') continue;
const chunk = JSON.parse(line.slice(6));
const delta = chunk.choices[0]?.delta;
if (delta?.content) {
answer += delta.content;
process.stdout.write(delta.content);
}
for (const toolCall of delta?.tool_calls ?? []) {
if (toolCall.id) {
pendingToolCalls[toolCall.index] = { name: toolCall.function.name, args: '' };
}
const pending = pendingToolCalls[toolCall.index];
if (pending && toolCall.function?.arguments) {
pending.args += toolCall.function.arguments;
}
}
}
}
for (const call of Object.values(pendingToolCalls)) {
const args = JSON.parse(call.args);
if (call.name === '_meiliSearchProgress') {
const params = JSON.parse(args.function_parameters);
console.log(`Searched "${params.q}" in index "${params.index_uid}"`);
}
if (call.name === '_meiliSearchSources') {
console.log('Sources used:', args.documents);
}
if (call.name === '_meiliAppendConversationMessage') {
messages.push(args);
}
}
messages.push({ role: 'assistant', content: answer });
}
await chat('What movies are about artificial intelligence?');
await chat('Which one has the best reviews?');
For UI patterns that surface those source documents, see display source documents.
Summarized answers
One-shot summarization uses the same /chats API, but with a different prompt strategy: you send a single question and receive a concise answer based on your indexed documents. This is useful for displaying an AI-generated answer alongside traditional search results.
Configure a dedicated workspace prompt that asks for short, self-contained answers and avoids follow-up questions:
cURL
curl \
-X PATCH 'MEILISEARCH_URL/chats/WORKSPACE_NAME/settings' \
-H 'Authorization: Bearer MEILISEARCH_KEY' \
-H 'Content-Type: application/json' \
--data-binary '{
"prompts": {
"system": "You are a search assistant. When the user asks a question, provide a single concise answer based only on the search results. Keep your response to 2-3 sentences maximum. Do not ask follow-up questions. Do not use your general knowledge. If the search results do not contain enough information, say so briefly."
}
}'
Use a dedicated workspace for summarization rather than reusing a chat workspace, so the prompts stay separate. On Meilisearch Cloud, if you need a second workspace, contact support. Send one user message and do not maintain conversation history:
cURL
curl -N \
-X POST 'MEILISEARCH_URL/chats/WORKSPACE_NAME/chat/completions' \
-H 'Authorization: Bearer MEILISEARCH_KEY' \
-H 'Content-Type: application/json' \
--data-binary '{
"model": "PROVIDER_MODEL_UID",
"stream": true,
"messages": [
{
"role": "user",
"content": "What is the return policy for electronics?"
}
],
"tools": [
{
"type": "function",
"function": {
"name": "_meiliSearchSources",
"description": "Provides sources of the search",
"parameters": {
"type": "object",
"properties": {
"call_id": { "type": "string", "description": "The call ID to track the original search" },
"documents": { "type": "array", "items": { "type": "object" }, "description": "The documents associated with the search" }
},
"required": ["call_id", "documents"],
"additionalProperties": false
},
"strict": true
}
}
]
}'
Including _meiliSearchSources lets you display the source documents next to the summary. In a real application, run this in parallel with a standard Meilisearch search request and display both results together.
For more deterministic answers, pass a lower temperature (for example, 0.1 or 0.2). Meilisearch forwards these parameters to your LLM provider.
Configuration shortcuts
Workspace settings connect Meilisearch to an LLM. Index chat settings describe each index to the agent. Both are required.
Workspace settings
Create or update a workspace with PATCH /chats/{workspace_uid}/settings. If the workspace does not exist, Meilisearch creates it.
The source field selects the LLM provider:
| Provider | source value |
Required fields | Optional fields |
|---|---|---|---|
| OpenAI | openAi |
apiKey |
baseUrl |
| Azure OpenAI | azureOpenAi |
apiKey, baseUrl, orgId, projectId, apiVersion |
deploymentId |
| Mistral | mistral |
apiKey, baseUrl |
|
| vLLM | vLlm |
baseUrl |
apiKey |
A few provider-specific rules:
orgId,projectId, andapiVersionare required for Azure OpenAI and are incompatible with every othersourcebaseUrlis required for Azure OpenAI and vLLM. For Mistral it points to the Mistral API endpoint. For OpenAI it is optional and only needed when routing through a custom endpointapiKeyis optional for vLLM and mandatory for every other provider
The apiKey field is write-only. Meilisearch stores it for outbound LLM calls but redacts it in every response from the workspace settings endpoint. To rotate the key, PATCH the workspace with the new value.
Set baseline agent instructions with prompts.system. The prompts object also accepts tool-facing fields (searchDescription, searchQParam, searchFilterParam, searchIndexUidParam) that help the LLM decide when and how to search. See optimize chat prompts for how to tune them.
Retrieve the current settings at any time:
cURL
curl \
-X GET 'MEILISEARCH_URL/chats/WORKSPACE_NAME/settings' \
-H "Authorization: Bearer MEILISEARCH_KEY"
Update only the fields you want to change. Fields you omit remain unchanged:
cURL
curl \
-X PATCH 'MEILISEARCH_URL/chats/WORKSPACE_NAME/settings' \
-H "Authorization: Bearer MEILISEARCH_KEY" \
-H "Content-Type: application/json" \
--data-binary '{ "apiKey": "your-valid-api-key" }'
For the full HTTP parameter list, see:
- Get settings of a chat workspace
- Update settings of a chat workspace
- Reset the settings of a chat workspace
LLM provider parameters passthrough
Meilisearch forwards standard chat completion parameters such as temperature, top_p, frequency_penalty, or presence_penalty to the configured provider. Available parameters depend on the provider. See the chat completions API reference.
Index chat settings
chat is an index-level setting, distinct from workspace settings. Configure it on every index the agent should access:
| Field | Type | Default | Description |
|---|---|---|---|
description |
string | “” |
Describes the index content so the LLM can decide when and how to query it |
documentTemplate |
string | All searchable fields | Liquid template defining the text sent to the LLM for each document |
documentTemplateMaxBytes |
integer | 400 |
Maximum size in bytes of the rendered document template. Longer text is truncated |
searchParameters |
object | {} |
Search parameters applied when the LLM queries this index |
Write description as if you were explaining the index to someone who has never seen your data. If you have multiple indexes, make each description specific enough that the LLM can distinguish them.
A good documentTemplate includes only the fields relevant to answering questions. See document template best practices.
searchParameters can enable hybrid search, limit results, or apply default sorting:
| Parameter | Type | Description |
|---|---|---|
hybrid |
object | Enable hybrid search with embedder (required) and semanticRatio (0.0 for keyword only, 1.0 for semantic only) |
limit |
integer | Maximum number of documents returned per search |
sort |
string[] | Sort order, for example [“price:asc”, “rating:desc”] |
distinct |
string | Return at most one document per distinct value of this attribute |
matchingStrategy |
string | How query terms are matched: last, all, or frequency |
attributesToSearchOn |
string[] | Restrict search to specific attributes |
rankingScoreThreshold |
number | Minimum ranking score (0.0 to 1.0) for a document to be included |
For conversational queries, a higher semanticRatio (0.6 to 0.8) and a lower limit (3 to 5) often work better than keyword-oriented defaults. See optimize chat prompts.
For the full HTTP parameter list, see:
Streaming
Meilisearch uses Server-Sent Events (SSE) to stream chat completions. The wire format is OpenAI-compatible, so you can point the official OpenAI SDKs, the Vercel AI SDK, or any other SSE-aware client at the endpoint.
For the request itself, reuse the examples in chat interface. The -N flag in cURL disables output buffering so you see each chunk as it arrives.
Understand the SSE response format
Each event on the wire follows three rules:
- Every event is a line that begins with the literal prefix
data: - The payload after
data:is a single JSON object shaped like an OpenAIchat.completion.chunk - The stream terminates with the sentinel line
data: [DONE]. The[DONE]marker is a literal string, not JSON, so parsers must check for it before callingJSON.parse
Events are separated by blank lines. After consuming [DONE], close the reader.
Content chunks
Regular content chunks contain the AI-generated text in choices[0].delta.content:
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-4o","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":"Meilisearch"},"finish_reason":null}]}
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-4o","choices":[{"index":0,"delta":{"content":" is"},"finish_reason":null}]}
Tool call chunks
When you include Meilisearch tools, the stream also contains tool call chunks in choices[0].delta.tool_calls:
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-4o","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"id":"call_abc123","type":"function","function":{"name":"_meiliSearchProgress","arguments":""}}]},"finish_reason":null}]}
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-4o","choices":[{"index":0,"delta":{"tool_calls":[{"index":0,"function":{"arguments":"{\"call_id\":\"abc\",\"function_name\":\"_meiliSearchInIndex\",\"function_parameters\":\"{\\\"index_uid\\\":\\\"movies\\\",\\\"q\\\":\\\"search engine\\\"}\"}"}}]},"finish_reason":null}]}
End of stream
The stream ends with a finish_reason of “stop” followed by the [DONE] marker:
data: {"id":"chatcmpl-123","object":"chat.completion.chunk","created":1677652288,"model":"gpt-4o","choices":[{"index":0,"delta":{},"finish_reason":"stop"}]}
data: [DONE]
Handle streaming in JavaScript
Use the Fetch API to process the SSE stream. Parse data: lines, skip [DONE], then handle delta.content and delta.tool_calls:
JavaScript Fetch
async function streamChat(query) {
const response = await fetch(
'MEILISEARCH_URL/chats/WORKSPACE_NAME/chat/completions',
{
method: 'POST',
headers: {
'Authorization': 'Bearer MEILISEARCH_KEY',
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'gpt-4o',
stream: true,
messages: [{ role: 'user', content: query }],
tools: MEILISEARCH_TOOLS, // see the complete example in Chat interface
}),
}
);
const reader = response.body.getReader();
const decoder = new TextDecoder();
let buffer = '';
while (true) {
const { done, value } = await reader.read();
if (done) break;
buffer += decoder.decode(value, { stream: true });
const lines = buffer.split('\n');
buffer = lines.pop();
for (const line of lines) {
if (!line.startsWith('data: ')) continue;
const data = line.slice(6);
if (data === '[DONE]') return;
const chunk = JSON.parse(data);
const delta = chunk.choices[0]?.delta;
if (delta?.content) {
process.stdout.write(delta.content);
}
if (delta?.tool_calls) {
for (const toolCall of delta.tool_calls) {
handleToolCall(toolCall);
}
}
}
}
}
The endpoint is stateless. Accumulate messages and send the full history with each request, including payloads from _meiliAppendConversationMessage. See the complete example.
Tools reference
Meilisearch intercepts three special tools and never forwards them to the LLM provider. Declare them in the tools array of your request.
These tool definitions must include the exact parameter schemas below. Missing or incorrect parameters will prevent the tools from working.
Recommended usage order:
- Handle progress updates with
_meiliSearchProgress - Append conversation messages with
_meiliAppendConversationMessage - Display source documents with
_meiliSearchSources - Use
call_idto associate progress updates with their corresponding source results
_meiliSearchProgress
Reports real-time progress of internal search operations. Arguments:
call_id: Unique identifier to track the search operationfunction_name: Name of the internal function being executed (for example,_meiliSearchInIndex)function_parameters: JSON-encoded string containing search parameters such asqandindex_uid
_meiliSearchProgress
{
"type": "function",
"function": {
"name": "_meiliSearchProgress",
"description": "Provides information about the current Meilisearch search operation",
"parameters": {
"type": "object",
"properties": {
"call_id": {
"type": "string",
"description": "The call ID to track the sources of the search"
},
"function_name": {
"type": "string",
"description": "The name of the function being executed"
},
"function_parameters": {
"type": "string",
"description": "The parameters of the function being executed, encoded in JSON"
}
},
"required": ["call_id", "function_name", "function_parameters"],
"additionalProperties": false
},
"strict": true
}
}
_meiliAppendConversationMessage
Asks the client to append internal tool calls and results to the conversation history. Arguments:
role: Message author role (userorassistant)content: Message content (for tool results)tool_calls: Array of tool calls made by the assistanttool_call_id: ID of the tool call this message responds to
_meiliAppendConversationMessage
{
"type": "function",
"function": {
"name": "_meiliAppendConversationMessage",
"description": "Append a new message to the conversation based on what happened internally",
"parameters": {
"type": "object",
"properties": {
"role": {
"type": "string",
"description": "The role of the message author"
},
"content": {
"type": "string",
"description": "The content of the message. Required unless tool_calls is specified"
},
"tool_calls": {
"type": ["array", "null"],
"description": "Tool calls generated by the model",
"items": {
"type": "object",
"properties": {
"id": { "type": "string" },
"type": { "type": "string" },
"function": {
"type": "object",
"properties": {
"name": { "type": "string" },
"arguments": { "type": "string" }
}
}
}
}
},
"tool_call_id": {
"type": ["string", "null"],
"description": "Tool call this message is responding to"
}
},
"required": ["role", "content", "tool_calls", "tool_call_id"],
"additionalProperties": false
},
"strict": true
}
}
_meiliSearchSources
Returns the documents used by the LLM to generate the answer. The call_id matches _meiliSearchProgress so you can associate queries with results.
Arguments:
call_id: Matches thecall_idfrom_meiliSearchProgressdocuments: Source documents with only displayed attributes
_meiliSearchSources
{
"type": "function",
"function": {
"name": "_meiliSearchSources",
"description": "Provides sources of the search",
"parameters": {
"type": "object",
"properties": {
"call_id": {
"type": "string",
"description": "The call ID to track the original search associated to those sources"
},
"documents": {
"type": "array",
"items": { "type": "object" },
"description": "The documents associated with the search. Only displayed attributes are returned"
}
},
"required": ["call_id", "documents"],
"additionalProperties": false
},
"strict": true
}
}
See display source documents for UI patterns, and display source documents with the Chats API for how to correlate the two tools by call_id. See the complete example for a request that declares all three tools.
Display source documents with the Chats API
To display source documents using this API, declare the _meiliSearchProgress and _meiliSearchSources tools in your request (see tools reference for their exact schemas), then correlate the two by the call_id they share as they stream in.
Understand the response order
During a streamed response, tool calls arrive as chunks alongside content chunks, in this order:
_meiliSearchProgress: sent when the agent decides to search an index. Reports the query and index, and assigns acall_idto the search._meiliSearchSources: sent once the search completes, with the matching documents. Itscall_idmatches the progress event, so you can associate the documents with the query that produced them.- Content chunks: the AI-generated answer, based on the retrieved documents.
_meiliSearchProgress
_meiliSearchSources
{
"function": {
"name": "_meiliSearchProgress",
"arguments": "{\"call_id\":\"abc123\",\"function_name\":\"_meiliSearchInIndex\",\"function_parameters\":\"{\\\"index_uid\\\":\\\"movies\\\",\\\"q\\\":\\\"best sci-fi movies\\\"}\"}"
}
}
{
"function": {
"name": "_meiliSearchSources",
"arguments": "{\"call_id\":\"abc123\",\"documents\":[{\"id\":11,\"title\":\"Blade Runner 2049\"},{\"id\":27,\"title\":\"Interstellar\"}]}"
}
}
Both events share the call_id value abc123, linking the “best sci-fi movies” search on the movies index to its two documents.
Correlate sources by call_id
Parse the tool_calls chunks from the SSE stream and group them by call_id as they arrive:
const sources = new Map(); // call_id -> { query, index, documents }
function handleToolCall(toolCall) {
if (!toolCall.function?.name || !toolCall.function.arguments) return;
const args = JSON.parse(toolCall.function.arguments);
if (toolCall.function.name === '_meiliSearchProgress') {
const params = JSON.parse(args.function_parameters);
sources.set(args.call_id, {
query: params.q,
index: params.index_uid,
documents: [],
});
}
if (toolCall.function.name === '_meiliSearchSources') {
const existing = sources.get(args.call_id);
if (existing) {
existing.documents = args.documents;
}
}
}
After the stream finishes, sources contains every search query and its documents, keyed by call_id. Pass this map to the UI patterns described in display source documents.
Handle errors and fallbacks with the Chats API
To handle errors and fallbacks using this API, check the HTTP status before consuming the stream: Meilisearch forwards errors from your LLM provider on the chat completions endpoint. For generic fallback UX, user-facing messages, and AI SDK patterns, see the linked guide. This section covers what’s specific to calling /chats directly.
Check the response status before streaming
Wrap requests to the chat completions endpoint in error handling, and branch on the HTTP status before reading the response body. Once you start consuming the SSE stream, you can no longer branch on the status:
async function chat(messages) {
try {
const response = await fetch(
`${MEILISEARCH_URL}/chats/${WORKSPACE}/chat/completions`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
model: MODEL,
stream: true,
messages,
tools: MEILISEARCH_TOOLS, // see the tools reference
})
}
);
if (response.status === 429) {
return {
role: 'assistant',
content: 'The service is currently experiencing high demand. Please try again in a moment.'
};
}
if (response.status === 502 || response.status === 504) {
return {
role: 'assistant',
content: 'The AI service is temporarily unavailable. Try a regular search instead.',
fallback: true
};
}
if (!response.ok) {
const error = await response.json();
console.error('Chat error:', error);
return {
role: 'assistant',
content: 'Something went wrong. Please try rephrasing your question.'
};
}
return response; // pass this to your SSE parser, see Streaming
} catch (networkError) {
return {
role: 'assistant',
content: 'Unable to connect to the search service. Please check your connection and try again.'
};
}
}
Retry on rate limiting
The chat completions endpoint returns 429 when your LLM provider rate limits the request. Implement exponential backoff around the fetch call:
async function chatWithRetry(messages, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
const response = await fetch(
`${MEILISEARCH_URL}/chats/${WORKSPACE}/chat/completions`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({ model: MODEL, stream: true, messages, tools: MEILISEARCH_TOOLS })
}
);
if (response.status !== 429) {
return response;
}
// Exponential backoff: 1s, 2s, 4s
const waitMs = Math.pow(2, attempt) * 1000;
await new Promise(resolve => setTimeout(resolve, waitMs));
}
return {
fallback: true,
content: 'The service is busy. Please try again shortly.'
};
}
Fall back to regular search
When conversational search fails, fall back to a standard keyword or hybrid search against /indexes/{index_uid}/search:
async function searchWithFallback(query, conversationHistory) {
const chatResponse = await chat([
...conversationHistory,
{ role: 'user', content: query }
]);
if (chatResponse.fallback) {
const searchResponse = await fetch(
`${MEILISEARCH_URL}/indexes/${INDEX}/search`,
{
method: 'POST',
headers: {
'Authorization': `Bearer ${API_KEY}`,
'Content-Type': 'application/json'
},
body: JSON.stringify({
q: query,
hybrid: { semanticRatio: 0.5, embedder: EMBEDDER }
})
}
);
const results = await searchResponse.json();
return {
type: 'search',
hits: results.hits,
message: 'Showing search results instead. The AI assistant is temporarily unavailable.'
};
}
return { type: 'chat', response: chatResponse };
}
Detect empty results from _meiliSearchSources
Unlike a regular tool result, _meiliSearchSources arrives as a tool call chunk in the stream (see tools reference for its schema). Parse its documents argument and, if it is empty, show a fallback message instead of relying on the model’s answer:
function handleSources(toolCall) {
const args = JSON.parse(toolCall.function.arguments);
if (!Array.isArray(args.documents) || args.documents.length === 0) {
showMessage('No matching documents found. Try different keywords or broaden your search.');
return;
}
displaySources(args.documents);
}
Combine this with guardrails in your workspace’s system prompt, so the model itself also acknowledges when it found nothing relevant.
Troubleshooting
Missing default chat API key
If your instance does not have a Default Chat API Key, create one manually.
Empty reply from server (curl error 52)
Causes:
- Chat completions feature not enabled
- Missing authentication in requests
Solution:
- Enable the feature
- Include the
Authorizationheader in all requests
”Invalid API key” error
Cause: Using the wrong type of API key Solution:
- Use the “Default Chat API Key”
- Do not use search or admin API keys for chat endpoints
- Find your chat key with the list keys endpoint
”Socket connection closed unexpectedly”
Cause: Usually means the LLM provider API key is missing or invalid in workspace settings Solution:
- Check workspace configuration:
cURL
curl \
-X GET 'MEILISEARCH_URL/chats/WORKSPACE_NAME/settings' \
-H "Authorization: Bearer MEILISEARCH_KEY"
2. Update with a valid API key:
cURL
curl \
-X PATCH 'MEILISEARCH_URL/chats/WORKSPACE_NAME/settings' \
-H "Authorization: Bearer MEILISEARCH_KEY" \
-H "Content-Type: application/json" \
--data-binary '{ "apiKey": "your-valid-api-key" }'
No search progress visible
Cause: The _meiliSearchProgress tool is not declared in the request
Solution:
The search still runs and the LLM still answers, but without _meiliSearchProgress you receive no visibility into what searches are being performed. Add all three Meilisearch tools as shown in the complete example.
For HTTP status handling, rate limits, empty results, and falling back to regular search when calling /chats directly, see handle errors and fallbacks with the Chats API. For generic fallback UX and AI SDK patterns, see handle errors and fallbacks.
Next steps
Agentic search getting started
Build agentic and conversational search with the recommended AI SDK.
Display source documents
Show users which documents were used to generate responses.
Configure guardrails
Restrict AI responses to topics covered by your data.
Optimize chat prompts
Tune system prompts, tool prompts, and index chat settings.
Reduce hallucination
Techniques to keep AI responses grounded in your data.
Chat completions API reference
Full reference for the chat completions endpoint.
