Choosing an endpoint
Informer offers four ways to send a prompt and get an answer back. They differ on three things: whether the response arrives as a stream, whether an Assistant's configuration is applied, and whether the conversation is saved. Answer those three questions and the choice is made for you.
The four endpoints
| Endpoint | Response | Assistant configuration | Conversation saved |
|---|---|---|---|
POST /api/assistants/{id}/_ask | Plain JSON | Yes, the Assistant's own | No |
POST /api/chats/{id} | SSE stream | Yes, from the stored chat | Yes |
POST /api/models/{id}/_chat | SSE stream | Yes, supplied per request | No |
POST /api/models/{id}/_completion | SSE stream | No | No |
POST /api/models/{id}/_object | Plain JSON | No | No |
_ask and _object are the two that do not stream. The others return
text/event-stream, so the caller has to read the stream and assemble the text
before it has an answer.
The difference between the two plain-JSON routes is what they carry: _ask
addresses an Assistant and applies its whole configuration, while _object
addresses a model and takes a JSON Schema. If you want an answer from an
Assistant without handling a stream, _ask is the one.
Start here: three questions
1. Do you need the Assistant to behave like an Assistant?
An Assistant is not just a system prompt. When one is attached, the request also picks up its skills (Website Search, for example), its Toolkits, its attached Datasets, its knowledge files, its stored secrets, and any sub-Assistants it dispatches to. That whole bundle is assembled from the Assistant's own configuration, not from your payload.
_ask, /api/chats/{id} and _chat can all do this. _completion and
_object call the model directly and carry the model's own instructions, so an
Assistant's skills never run on them. If your Assistant exists because of the work its skills
do, those two endpoints will answer confidently while quietly doing none of it.
2. Should the conversation be saved?
_chat runs entirely in memory. Nothing is written, nothing is billed to a chat
history, and the next request starts from whatever messages you send it. That is
the right shape for a stateless integration such as a search box that answers one
question at a time.
If a person should be able to close the page and come back to what they were saying, you want a saved chat. See Saving a conversation for a signed-in user below.
3. Can you consume a stream?
If the answer is no, use _ask. It answers with a plain JSON body and still
applies the Assistant's full configuration, so unlike _object it costs you
nothing but the ability to render tokens as they arrive.
Reach for _object only when you need output shaped by a JSON Schema, and accept
that no Assistant is applied.
Running an Assistant without saving anything
_chat takes the Assistant id in the payload and applies its full configuration
for that one request:
{
"messages": [
{ "role": "user", "parts": [{ "type": "text", "text": "Which programs have fall deadlines?" }] }
],
"primaryAssistantId": "admin:admissions-helper"
}
primaryAssistantId is the Assistant that answers. assistantIds is a different
thing: it offers a set of Assistants the model may dispatch to. Supplying exactly
one assistantIds entry and no primaryAssistantId promotes it to the primary
Assistant, so the two forms converge in that one case.
Both resolve through read_access, so an id the caller cannot see is treated as
absent rather than rejected. An Assistant that answers with no configuration at
all is the symptom to look for.
Saving a conversation for a signed-in user
Set ownerAssistantId when creating the chat. That marks the chat as this
user's chat with that Assistant, which is what makes it findable again later:
chats are per-user, so each person gets their own.
- Look for an existing chat
GET /api/assistants/{id}returns achatIdwhen the current user already has a chat owned by that Assistant. If it is there, skip to sending the message. - Create one if there is none
POST /api/chatswith{ "modelId": "...", "ownerAssistantId": "..." }. This route only writes rows, so it does not stream and does not call a provider. - Send the message
POST /api/chats/{chatId}with the message. This one streams.
Only step three calls a model. The lookup in step one is what makes the conversation resume, so run it on every load rather than caching the id and assuming it stays valid.
Two things that catch integrators out
Messages carry parts, not content
A message is an object with a role and an array of parts:
{ "role": "user", "parts": [{ "type": "text", "text": "Hello" }] }
This is the format across /api/chats/{id}, _chat, and _object. A message
with a plain content string will not be read as text.
Unknown fields are dropped, not rejected
Every one of these routes validates with stripUnknown, so a field that is not
in the payload table is removed before the handler runs. There is no 400 and no
warning. A misspelled primaryAssistantId, or an assistantIds sent to
_object (which has no such field), produces a perfectly plausible answer with
the Assistant silently absent.
When a response looks like the model answered from general knowledge, suspect a dropped field before suspecting the model.
Structured output
_object takes a JSON Schema and returns an object matching it as the entire
response body, with no envelope:
{
"schema": {
"type": "object",
"properties": {
"programs": { "type": "array", "items": { "type": "string" } },
"deadline": { "type": "string" }
},
"required": ["programs"]
},
"messages": [
{ "role": "user", "parts": [{ "type": "text", "text": "Which programs have fall deadlines?" }] }
]
}
Its payload accepts schema, messages, and outputSize. There is no field for
an Assistant, a system prompt, or Toolkits.
Payload limits
POST /api/chats/{id} and the _explain route raise their payload ceiling to
200MB so that large conversations and attachments fit. The three /api/models/
routes do not, so they inherit the server default of 1MB. A long message history
posted to _chat can exceed that where the same history posted to a saved chat
would not.
Troubleshooting
- The Assistant answers but its skills never run. Check which endpoint you
are calling.
_completionand_objectcannot carry an Assistant at all, and on_chata misspelled or unreadableprimaryAssistantIdis dropped silently. - Every request starts a new conversation. The chat was created without
ownerAssistantId, soGET /api/assistants/{id}has nothing to hand back aschatId. Check the create payload rather than the lookup. - A
402before anything streams. Credit enforcement runs in the pre-block, ahead of the model call. Thecodefield names which limit was hit. See the enforcement flow. - A
413on_chatthat does not happen on a saved chat. The 1MB default ceiling, described under Payload limits. - The response is empty or unparsed. Three of the four endpoints return SSE.
Confirm you are reading
text/event-streamrather than parsing the body as JSON. - A hand-written stream parser throws on a line that isn't JSON. Lines that
start with
:are keepalive comments the server sends every 15 seconds; skip them. See Streaming Responses.
Related
- Chats for the full chat lifecycle and every field on each payload
- Models for the three
/api/models/{id}/routes in detail - Assistants for Assistant configuration and the
chatIdbehavior on lookup