OpenAI-compatible clients
The Syntic API accepts the chat completions request format that many client libraries and tools already speak. If a library lets you set a custom base URL and API key, it can talk to Syntic with no other code changes. Below: the setup, then a precise list of which parts of that format Syntic supports.
Settings
| Setting | Value |
|---|---|
| Base URL | https://app.syntic.ai/v1 |
| API key | A private sk_live_ key from Settings → API Keys in the Console |
| Model | A Syntic model name, for example Amara S1.2 or Amara Quick |
Set up
Create a key
Create a private key in the Console and store it as SYNTIC_API_KEY. See Get an API key.
Install a client library
pip install openai # Python
npm install openai # Node.js / TypeScriptPoint it at Syntic
Pass the Syntic base URL and key when you create the client, as in the examples below.
Use Syntic model names
Replace any model names in your code with Syntic names. Today a name Syntic does not recognise is answered by the default model, so typos do not raise errors; check them carefully. Not live yet — on develop: an unrecognised name returns 404 model_not_found, which the OpenAI libraries raise as a not-found error.
Examples
import os
from openai import OpenAI
client = OpenAI(
base_url="https://app.syntic.ai/v1",
api_key=os.environ["SYNTIC_API_KEY"],
)
answer = client.chat.completions.create(
model="Amara Quick",
stream=False,
messages=[{"role": "user", "content": "Translate 'see you tomorrow' into Portuguese."}],
)
print(answer.choices[0].message.content)Streaming
Ask the library for a stream and iterate over it. Text arrives in each chunk’s delta:
stream = client.chat.completions.create(
model="Amara S1.2",
stream=True,
messages=[{"role": "user", "content": "List five uses for leftover rice."}],
)
for chunk in stream:
if chunk.choices and chunk.choices[0].delta.content:
print(chunk.choices[0].delta.content, end="", flush=True)The final usage chunk has an empty choices array, which is why both loops check before reading choices[0].
Tool calling
Tools use the familiar tools and tool_choice fields:
import json
tools = [{
"type": "function",
"function": {
"name": "convert_currency",
"description": "Convert an amount between two currencies at today's rate.",
"parameters": {
"type": "object",
"properties": {
"amount": {"type": "number"},
"from_code": {"type": "string", "description": "ISO 4217 code, e.g. EUR"},
"to_code": {"type": "string"},
},
"required": ["amount", "from_code", "to_code"],
},
},
}]
messages = [{"role": "user", "content": "How much is 250 euros in yen?"}]
first = client.chat.completions.create(
model="Amara S1.2", stream=False, tools=tools, messages=messages,
)
reply = first.choices[0].message
if reply.tool_calls:
messages.append(reply) # keep the assistant turn exactly as returned
for call in reply.tool_calls:
args = json.loads(call.function.arguments)
result = {"converted": round(args["amount"] * 162.4, 2)} # your real lookup here
messages.append({"role": "tool", "tool_call_id": call.id, "content": json.dumps(result)})
final = client.chat.completions.create(
model="Amara S1.2", stream=False, tools=tools, messages=messages,
)
print(final.choices[0].message.content)See Tool calling and Write a tool loop for a complete loop with error handling.
Reading Syntic headers
The response headers carry the request id, billing source and model fallback signal. Most libraries expose the raw response:
raw = client.chat.completions.with_raw_response.create(
model="Amara S1.2",
stream=False,
messages=[{"role": "user", "content": "Hello"}],
)
print(raw.headers.get("x-syntic-request-id"))
print(raw.headers.get("x-syntic-billing-source"))
print(raw.headers.get("x-syntic-model-downgraded")) # None unless a fallback happened
completion = raw.parse()Compatibility details
Request fields
| Field | Support |
|---|---|
model | Supported. Use Syntic names. Today unknown names fall back to the default model. Not live yet — on develop: unknown names return 404 model_not_found. |
messages | Supported. 1 to 500 messages. |
stream | Supported. Defaults to true, unlike most libraries expect. |
max_tokens | Supported. |
tools | Supported, function tools only. |
tool_choice | Supported. |
temperature, top_p | Accepted, not applied. |
n | Accepted, not applied. Always one choice. |
presence_penalty, frequency_penalty | Accepted, not applied. |
stream_options | Not needed. Streams always end with a usage chunk. |
response_format | Ignored. JSON mode and structured outputs are not available; request JSON through your instructions and check it in your code. |
stop, seed, logprobs, top_logprobs, logit_bias, user | Ignored. |
parallel_tool_calls | Ignored. |
Message fields
| Field | Support |
|---|---|
role: system | Supported. Put it first. Syntic’s identity instruction is placed ahead of it. |
role: developer | Not documented as supported; use system. |
role: user, assistant, tool | Supported. |
content as a string | Supported. |
content as an array of parts (text, images, audio) | Not documented as supported. |
tool_calls, tool_call_id | Supported. |
name | Not used. |
Response fields
| Field | Support |
|---|---|
id, object, created | Returned. |
model | Returned, but may not equal the Syntic name you sent. Use the x-syntic-model-downgraded header to detect fallbacks. |
choices[].message, choices[].delta | Returned, including tool_calls. |
choices[].finish_reason | stop, length or tool_calls. |
usage | Returned on every reply, including streamed ones. |
logprobs | Not returned. |
system_fingerprint | Not guaranteed. |
Endpoints
| Library call | Support |
|---|---|
chat.completions.create | Supported |
models.list, models.retrieve | Not available. Use the list on Models overview. |
embeddings, files, images, audio, batches, moderations, responses, assistants | Not available |
Errors
Syntic error bodies are flat, for example {"error": "invalid_api_key"}. Libraries that expect {"error": {"message": ...}} still raise the right exception class for the HTTP status, but the message may be the raw body. Read the code from the body:
import openai
try:
client.chat.completions.create(model="Amara S1.2", stream=False, messages=[])
except openai.APIStatusError as err:
body = err.response.json()
print(err.status_code, body["error"]["type"], body["error"]["code"]) # 400 invalid_request_error messages_requiredThe full list is on Error codes.
Retries and timeouts
Client libraries usually retry 429 and 5xx responses on their own. That suits Syntic: 429 responses carry Retry-After. Keep the library’s timeout generous (a couple of minutes) for long replies, or stream them.
Two behaviours differ from what these libraries expect:
- Streaming is on by default. Pass
stream=False(Python) orstream: false(Node.js) on every non-streaming call. Libraries often leave the field out when you do not set it, and the API then sends a stream the library is not expecting to parse. - Errors have a flat body.
erroris a string code, not an object. See Errors.