Chat
Ask questions in natural language over a consumer's contracts with the OpenAI-compatible chat API.
The chat completions API lets your application ask questions in plain language. Gaur's agent interprets the question, answers it using the consumer's contracts, and streams back a response. It's the right choice for conversational features and assistants.
The endpoint
POST /v1/api/{consumer_slug}/chat/completions
Authorization: Bearer <api_key>
Content-Type: application/jsonThe endpoint is OpenAI-compatible. The request and the streamed response follow the chat-completions shape, so OpenAI-style client code can talk to it with little change. There's an important catch about response fields, covered in Reading the full response below. Read that section before you build on this endpoint.
The request
{
"model": "gaur-nlq-v1",
"messages": [
{ "role": "user", "content": "Show me weekly revenue trends." }
]
}| Field | Purpose |
|---|---|
model | The model identifier for the request. |
messages | The conversation so far, an ordered list of role/content messages. |
Each message has a role (user, assistant, or system) and content. To
continue a conversation, send the prior messages back along with the new user
message.
The response
The response is a server-sent events (SSE) stream with content type
text/event-stream. Each data: line is a JSON chunk shaped like an OpenAI
chat completion chunk, with a choices array carrying a delta. The stream
ends with a final chunk whose finish_reason is stop, followed by a
data: [DONE] line.
HTTP/1.1 200 OK
Content-Type: text/event-stream
data: {"choices":[{"delta":{"role":"assistant"}}]}
data: {"choices":[{"delta":{"content":"Weekly "}}]}
data: {"choices":[{"delta":{"content":"revenue "}}]}
...
data: {"choices":[{"delta":{},"finish_reason":"stop"}]}
data: [DONE]Reading the full response
This is the part to get right. Gaur's agent does more than write prose. It
runs queries, produces result rows, and suggests charts. The chat endpoint
delivers all of that, but only the natural-language text travels in the
standard delta.content field. Everything else rides in an extra field.
What's in delta.content
choices[0].delta.content carries the agent's narrative: the sentences it
writes to explain its answer. A plain OpenAI client that concatenates
delta.content will render a readable reply.
But that reply is just the agent's commentary. The actual query results and
chart suggestions are not in delta.content.
What's in delta.gaur_event
Gaur attaches its richer output to a non-standard field on the delta:
choices[0].delta.gaur_event. Each gaur_event is an object with a type:
gaur_event type | What it carries |
|---|---|
thinking | The agent's progress and reasoning steps. |
assistant | A chunk of the narrative text (this also becomes delta.content). |
data | The query result: the actual rows and columns. |
chart_suggestion | A suggested chart for the result data. |
context_state | The objects and query the agent resolved for this turn. |
questionnaire | A set of clarifying questions when the agent needs more input. |
error | An error that occurred during the turn. |
If you read only delta.content, you get the agent's narrative and nothing
else. No result rows, no chart suggestions. To get the data your application
actually needs to render, you have to read delta.gaur_event.
The catch with OpenAI SDKs
gaur_event is not part of the OpenAI schema. A strict OpenAI client library
deserializes each chunk into a typed object and silently drops unknown
fields. With that kind of client, delta.gaur_event never reaches your
code, and you're left with only the narrative text.
To get the extra fields, do one of the following:
- Parse the stream yourself. Read each SSE
data:line,JSON.parseit, and look atchoices[0].delta.gaur_event. This is the most reliable approach and what we recommend. - Use your SDK's "extra fields" accessor. Some SDKs preserve unknown
fields, for example through a
model_extra(Python) or a raw-response accessor. Use that to reachgaur_event.
Either way, the rule is the same: don't rely on the typed delta.content
alone. Read the raw delta.gaur_event to get results and chart suggestions.
Handling the stream
const res = await fetch(url, { method: "POST", headers, body });
const reader = res.body.getReader();
const decoder = new TextDecoder();
let buffer = "";
while (true) {
const { value, done } = 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 payload = line.slice(6);
if (payload === "[DONE]") continue;
const chunk = JSON.parse(payload);
const delta = chunk.choices?.[0]?.delta ?? {};
if (delta.content) {
appendNarrative(delta.content);
}
if (delta.gaur_event) {
handleGaurEvent(delta.gaur_event); // data, chart_suggestion, etc.
}
}
}A typical handleGaurEvent switches on gaur_event.type: append thinking
to a progress indicator, render the rows from a data event, and offer the
chart from a chart_suggestion event.
How chat stays governed
The chat endpoint doesn't hand callers free rein over your data. It answers within the boundaries the consumer defines:
- It can only use the contracts the consumer is scoped to.
- The consumer's system prompt shapes tone and domain framing.
- Contract row-level security still applies to any data the answer is built from.
- The consumer's query mode decides how freely the agent can generate.
Query mode: bounded vs. unbounded generation
The query mode is the setting to think hardest about, because it decides whether the AI's generation is bounded.
- Contract-only mode is bounded. The agent can only choose a published contract and fill in a query spec that Gaur validates. Every answer is shaped by a contract a builder authored and verified, so what the AI can produce is a fixed, known set. This is the safety guarantee to rely on for customer-facing analytical chatbots and embedded in-product chat, where you want to know exactly what analytics an end user can reach.
- SQL-only and contract-with-SQL-fallback are unbounded. In these modes the agent writes SQL, so it composes queries freely and generation is open-ended again. It is still safe in terms of access: Gaur restricts the generated SQL to the objects the consumer is scoped to, never your whole database, and the SQL is read-only and validated before it runs. But the shape of a query is no longer bounded by a contract, so these modes suit internal or exploratory assistants more than untrusted end-user surfaces.
Building a chatbot into a product your customers use? Put it on a contract-only consumer. Bounded generation means the bot can only answer with analytics you have explicitly published and validated.
The quality of answers also depends heavily on the context builders attach to collections and contracts. Good descriptions of what the data means produce better answers.
Chat vs. the REST query API
Use chat when
The input is a natural-language question, the shape of the answer isn't known in advance, or you're building a conversational experience.
Use the query API when
You need an exact, repeatable result with a known shape: a dashboard tile, a report, a programmatic read. See Querying contracts.