AI conversations
AI conversations let you connect your own large language model to IM. Users chat one-to-one with a bot; when your server receives a user's message it calls the model and writes the reply while it is being generated, and the user sees the reply appear word by word in the client. The platform pushes content to the user in real time, resumes after disconnects, stores the final message and sends offline push notifications. The platform does not call the model and does not store your model keys.
| You want to | See |
|---|---|
| Create a bot | Create a bot, or AI bots in the console |
| Receive messages users send to a bot | Subscribe to the callback event ai.message_received |
| Send the model's reply to the user as it's generated | Streaming replies; with a server SDK, see "AI streaming replies" in each language's "Calling APIs" page |
| Show AI replies in the client | Web SDK: AI replies |
| Understand the reply format, finish reasons and limits | AI messages and Limits on this page |
You usually do not need to call these APIs directly: StartStream and Pipe* in the server SDKs already handle batching, retries and the OpenAI, Anthropic and Gemini stream formats.
AI conversations must be enabled by the platform; when they are not, the APIs in this section return 404 not_found.
Bots
A bot is a kind of IM user in the app (the user object's kind is bot). It is created with Create a bot, not with the normal create-user API.
- Like a normal user it has a username, nickname and avatar, can be added as a friend or blocked, and appears in users' conversation lists. Its friend-add mode is set to "allow anyone", so users can add it directly.
- It cannot log in: issuing login credentials or changing the password returns
403 permission_denied(details.reasonisbot_account). - It cannot join groups: creating a group, inviting, requesting to join and adding members reject bots with
403 permission_denied(bot_account); in batch additions the bot is listed as failed. Bots only reply in one-to-one chats. - When disabled, users can still message it, but the platform no longer calls back your service and new replies cannot be started; replies already being generated are not affected.
How a conversation works
- The user sends a one-to-one message to the bot.
- The platform sends
ai.message_receivedto your callback address with this message and the most recent earlier messages (10 by default, 0–50 configurable per bot). If the bot has its own callback address, the event goes only there. - Your server starts a reply: the platform writes a message of
typeaiin the "generating" state from the bot to the user, and the user's client shows it immediately. - Your server calls the model and writes what it returns: text, reasoning, images, files and tool-call progress. The platform merges writes every 50 ms or 2 KB and pushes them to all of the user's online devices.
- When the model finishes, finish the reply. The platform saves the final content, updates the message, sends an offline push to users who are not in the foreground, and sends
ai.stream_finished.
When the user taps "Stop", the reply ends immediately as stopped, the platform sends ai.stream_stopped, and your later writes return 409 stream_closed. The server SDKs stop reading the model's stream automatically when they receive this callback or error.
AI messages
An AI reply is stored as a normal one-to-one message sent by the bot, with type ai. Users see it in their history after switching devices or clearing the cache; conversation lists, unread counts and search work as for other messages. The body is:
{
"stream_id": "1841000000000000123",
"status": "completed",
"parts": [
{ "type": "reasoning", "text": "用户想知道明天的天气……" },
{ "type": "tool", "id": "call_1", "name": "weather", "title": "查询天气", "state": "done" },
{ "type": "text", "text": "明天上海**多云**,最高 23℃。" },
{ "type": "image", "url": "https://cdn.example.com/a.png", "width": 800, "height": 600, "alt": "天气图" }
],
"text": "明天上海多云,最高 23℃。",
"finish_reason": "stop",
"error": null,
"model": "gpt-4.1",
"usage": { "input_tokens": 120, "output_tokens": 380 },
"content_url": null,
"last_seq": 57
}| Field | Type | Description |
|---|---|---|
stream_id | String | Stream ID of this reply |
status | String | streaming generating, completed, stopped, failed interrupted. While generating, parts is an empty array and the content is only in the real-time pushes |
parts | Array | The reply's parts, saved when it ends |
text | String | All text parts joined without Markdown, up to 2000 characters; used for conversation previews, offline push, search, and older clients that do not know the ai type |
finish_reason | String | Finish reason |
error | Object | When interrupted, { "code": "…", "message": "…" }; message is shown to the user. Otherwise null |
model, usage | — | Model name and usage (input_tokens, output_tokens) you gave when finishing; for display and statistics only |
content_url | String | Reserved, currently always null |
last_seq | Number | Sequence number of the last delta |
AI messages cannot be sent with the send-message API (400 invalid_argument) and cannot be edited. Recall and delete work as for other messages.
Parts
type | Fields | Description |
|---|---|---|
text | text | Body text, Markdown |
reasoning | text | Reasoning; clients show it collapsed by default |
image | url, optional width, height, alt | url is an https address; upload it first with file upload |
file | url, name, size, optional mime_type | As above |
tool | id, name, title, state | Tool-call progress, state is running, done or failed; for display only, without arguments or results |
Finish reasons
status | finish_reason | Description |
|---|---|---|
completed | Your value, such as stop, length, tool_calls; default stop | Ended normally |
stopped | user | The user tapped Stop |
stopped | replaced | A new reply was started with replace |
failed | developer | You reported a failure; error has your code and message (the server SDKs use transport after repeated write failures and model_error when the model returns an error) |
failed | timeout | No new content for longer than the bot's setting (60 seconds by default) |
failed | max_duration | Longer than 30 minutes |
failed | too_large | Content over the limit, see Limits |
failed | content_rejected | The content did not pass content moderation |
failed | bot_deleted, user_deleted | The bot or the receiving user was deleted |
failed | buffer_lost | The platform's temporary buffer was lost (very rare) |
When a reply ends, the text of completed and stopped replies is submitted to content moderation; replies that fail it become failed (content_rejected).
Limits
| Item | Limit |
|---|---|
| Bots per app | 100 |
| Replies being generated per bot | 1000 by default, configurable per bot up to 10000; above this starting a reply returns 409 limit_exceeded |
| Replies being generated per conversation | 1; if one exists, 409 stream_in_progress is returned, or end it first with replace |
| Timeout without new content | 60 seconds by default, 10–300 seconds per bot |
| Duration of a reply | 30 minutes |
| Content of a reply | 32 KB; above this the reply ends as failed (too_large). Writes return 413 payload_too_large once more than 1 MB has been written |
| Parts per reply | 200; appending to the same part does not add parts |
| One delta | 64 KB |
| Write rate | 5000 deltas per second per app; above this 429 rate_limited |
| Recent messages in the callback | 10 by default, 0–50 per bot, at most 48 KB in total |
AI replies count toward message storage like normal messages and are not billed per message.
Offline push
Nothing is pushed when a reply starts. When it ends as completed or stopped, users who are not in the foreground get one push following the normal one-to-one rules (foreground detection, do-not-disturb, quota), with the bot's nickname and the beginning of the reply. failed replies are not pushed.
