​ Setup

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 chatCompletions and search actions, 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 chatCompletions and search actions 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
  }'
  • description tells the LLM what the index contains
  • documentTemplate is a Liquid template that defines the text sent to the LLM for each document
  • documentTemplateMaxBytes truncates 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, and apiVersion are required for Azure OpenAI and are incompatible with every other source
  • baseUrl is 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 endpoint
  • apiKey is 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:

​ 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 OpenAI chat.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 calling JSON.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:

  1. Handle progress updates with _meiliSearchProgress
  2. Append conversation messages with _meiliAppendConversationMessage
  3. Display source documents with _meiliSearchSources
  4. Use call_id to 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 operation
  • function_name: Name of the internal function being executed (for example, _meiliSearchInIndex)
  • function_parameters: JSON-encoded string containing search parameters such as q and index_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 (user or assistant)
  • content: Message content (for tool results)
  • tool_calls: Array of tool calls made by the assistant
  • tool_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 the call_id from _meiliSearchProgress
  • documents: 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:

  1. _meiliSearchProgress: sent when the agent decides to search an index. Reports the query and index, and assigns a call_id to the search.
  2. _meiliSearchSources: sent once the search completes, with the matching documents. Its call_id matches the progress event, so you can associate the documents with the query that produced them.
  3. 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.'
  };
}

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:

  1. Enable the feature
  2. Include the Authorization header 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:

  1. 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.