Chat basics
A chat request is a conversation written out as a list of messages. The model reads the whole list and writes the next assistant message. The API keeps no memory between requests: to continue a conversation, send the earlier turns again along with the new one.
This page covers the parts every integration uses: messages and roles, the request fields, what a reply looks like, how to keep a conversation going, and the limits to plan around.
Messages and roles
Each message is an object with a role and a content string.
| Role | Who wrote it | Typical use |
|---|---|---|
system | You, the developer | Standing instructions: tone, format, rules. Put it first. |
user | Your end user | The question or task. |
assistant | The model | Earlier replies, sent back so the model keeps context. |
tool | Your code | The result of a function the model asked for. See Tool calling. |
A two-turn conversation looks like this:
{
"model": "Amara S1.2",
"stream": false,
"messages": [
{ "role": "system", "content": "You are a support assistant for a bike repair shop. Keep answers under 80 words." },
{ "role": "user", "content": "My chain keeps slipping when I climb." },
{ "role": "assistant", "content": "That usually points to a worn chain or cassette. How many miles has the chain done?" },
{ "role": "user", "content": "About 3,000." }
]
}content is plain text. The API does not accept images, audio or file attachments in a message today; paste the text you want the model to read straight into content.
How the system message is handled
Syntic adds a short platform instruction to every request. It describes the assistant as Syntic AI and keeps answers about the model’s identity consistent.
- If your first message has the
systemrole, the platform instruction is placed at the top of that same message, followed by your text. - If you send no system message, the platform instruction becomes the first message on its own.
Your instructions still apply in full. Write your system message so it stands on its own and does not depend on being the very first line the model reads.
Request fields
| Field | Type | Required | Notes |
|---|---|---|---|
model | string | No | A model name such as "Amara S1.2". If you leave it out, or name a model the API does not know, the default model answers. See Choosing a model. |
messages | array | Yes | At least 1 message, at most 500. |
stream | boolean | No | Streaming is on unless you send false. See Streaming. |
max_tokens | integer | No | Upper bound on the length of the reply, in tokens. |
tools | array | No | Functions the model may ask you to run. |
tool_choice | string or object | No | Whether, and which, tool the model should call. |
Sampling settings such as temperature, top_p, n, presence_penalty and frequency_penalty are accepted without an error but are removed before the request reaches the model: the Amara models run with fixed sampling. Shape the output through your instructions instead. Because n is ignored, every reply has exactly one choice.
The reply
With "stream": false the API answers with one JSON object:
{
"id": "chatcmpl-...",
"object": "chat.completion",
"created": 1767225600,
"model": "...",
"choices": [
{
"index": 0,
"message": { "role": "assistant", "content": "At 3,000 miles the chain has likely stretched..." },
"finish_reason": "stop"
}
],
"usage": { "prompt_tokens": 112, "completion_tokens": 64, "total_tokens": 176 }
}choices[0].messageis the new assistant turn. Append it to your list before the next request.finish_reasonsays why the model stopped (see the table below).usagecounts the tokens read (prompt_tokens) and written (completion_tokens). It is what billing is based on.
Finish reasons
finish_reason | Meaning | What to do |
|---|---|---|
stop | The model finished its answer. | Show it. |
length | The reply hit max_tokens or the context window. The text is cut off. | Raise max_tokens, ask for a shorter answer, or ask the model to continue in a new turn. |
tool_calls | The model wants your code to run one or more functions. | Run them and send the results back. See Tool calling. |
Keep a conversation going
Because the API is stateless, your application owns the history. A simple chat loop appends each user message and each assistant reply to the same list:
import os
from openai import OpenAI
client = OpenAI(base_url="https://app.syntic.ai/v1", api_key=os.environ["SYNTIC_API_KEY"])
history = [
{"role": "system", "content": "You are a patient Spanish tutor. Correct mistakes gently."},
]
def ask(text: str) -> str:
history.append({"role": "user", "content": text})
reply = client.chat.completions.create(
model="Amara S1.2",
stream=False,
max_tokens=400,
messages=history,
)
answer = reply.choices[0].message.content
history.append({"role": "assistant", "content": answer})
return answer
print(ask("Yo soy cansado después del trabajo."))
print(ask("Why is it 'estoy' and not 'soy' here?"))You can also write assistant turns yourself, for example to show the model a worked example of the answer style you want before the real question.
Managing a long history
Every request resends the full conversation, so input tokens grow with each turn. Common strategies:
- Trim: keep the system message and the most recent turns, and drop the oldest ones.
- Summarise: when the history gets long, ask the model to summarise the earlier part, then replace those turns with one message holding the summary.
- Keep tool results short: store large data on your side and send only what the model needs.
Repeated prefixes cost less
When consecutive requests start with the same messages, as they do in a chat or a tool loop, the model service can reuse the work it already did on that shared beginning. Syntic charges those input tokens at the reduced cached input rate shown on Pricing. Nothing needs to be turned on; to get the most from it:
- Keep the system message and any reference material at the start, and keep them byte-for-byte identical between requests.
- Add new turns at the end rather than editing earlier ones.
Choosing a model
Pass the model’s Syntic name exactly, for example "Amara S1.2" or "Amara Quick". The Models overview lists every model, and Pricing lists the rate for each.
- Missing or unknown name: the default model, Amara S1.2, answers. No error is returned, so check the spelling if replies look unexpected.
- Model not in your plan: some models need a higher plan. If you ask for one your organization’s plan does not include, the default model answers instead and the response carries an
x-syntic-model-downgradedheader naming the model that was used. - Zero data retention: organizations set up for zero data retention cannot use models that need standard retention. Asking for one returns
400 model_unavailable.
Response headers
| Header | Meaning |
|---|---|
x-syntic-request-id | A unique id for the request. Log it, and quote it in any support ticket. |
x-syntic-billing-source | subscription when your plan allowance paid for the request, api_credit when usage credits did. |
x-syntic-model-downgraded | Present only when the model you asked for is not included in your plan; its value names the model that answered instead. |
Browsers only let page scripts read the request id and billing source headers. x-syntic-model-downgraded is visible to server-side code only.
Limits to plan around
| Limit | Value | Error when exceeded |
|---|---|---|
| Request body | about 2 MB | 413 payload_too_large |
| Messages per request | 500 | 413 too_many_messages |
| Context window | 262,144 tokens (input plus reply) | upstream_error, with the status the model service returned |
| Requests per key | 600 per minute | 429 |
More detail is on Limits. For anything that goes wrong, see Errors.
What the API does not do yet
- No structured-output or JSON mode. Describe the shape in your prompt and validate it; see Prompting tips.
- No images or files in messages.
- No server-side conversation storage: you always send the history.