Making requests
Celeris exposes an OpenAI-compatible HTTP API. You can use the official OpenAI SDKs or other clients that support a custom base URL.
Base URL https://inference.celeris.ai/celeris-1/v1
Auth Authorization: Bearer ck_...
Content application/json
Chat completions
POST /chat/completions is the primary endpoint.
- cURL
- Python
- TypeScript
curl https://inference.celeris.ai/celeris-1/v1/chat/completions \
-H "Authorization: Bearer $CELERIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "celeris-1",
"messages": [
{"role": "system", "content": "Extract the requested fields. Reply with JSON only."},
{"role": "user", "content": "Order #4411: 2x flat white, oat milk, pickup 8:15am. Fields: items, milk, pickup_time"}
],
"max_tokens": 256,
"temperature": 0,
"seed": 7
}'
import os
from openai import OpenAI
client = OpenAI(
base_url="https://inference.celeris.ai/celeris-1/v1",
api_key=os.environ["CELERIS_API_KEY"],
)
response = client.chat.completions.create(
model="celeris-1",
messages=[
{"role": "system", "content": "Extract the requested fields. Reply with JSON only."},
{
"role": "user",
"content": "Order #4411: 2x flat white, oat milk, pickup 8:15am. Fields: items, milk, pickup_time",
},
],
max_tokens=256,
temperature=0,
seed=7,
)
print(response.choices[0].message.content)
import OpenAI from 'openai';
const client = new OpenAI({
baseURL: 'https://inference.celeris.ai/celeris-1/v1',
apiKey: process.env.CELERIS_API_KEY,
});
const response = await client.chat.completions.create({
model: 'celeris-1',
messages: [
{role: 'system', content: 'Extract the requested fields. Reply with JSON only.'},
{
role: 'user',
content: 'Order #4411: 2x flat white, oat milk, pickup 8:15am. Fields: items, milk, pickup_time',
},
],
max_tokens: 256,
temperature: 0,
seed: 7,
});
console.log(response.choices[0].message.content);
Parameters that matter most here
The API accepts the chat-completion fields listed in the API reference. These parameters have the greatest effect on short, structured workloads:
| Parameter | Guidance |
|---|---|
max_tokens | Any positive integer; prompt tokens plus max_tokens must be at most 131,072 (Models). Defaults to 2048 when omitted. Leave generation headroom beyond the visible answer: start short structured calls at 256 and tune against representative inputs. A lower limit bounds worst-case latency and cost, but one that is too tight stops before the answer is emitted. |
temperature | Use 0 for classification and extraction. Increase it only when the task benefits from variation. |
seed | Combine with temperature: 0 to reduce sampling variation between requests. |
stop | Standard stop sequences are honored. |
messages | Keep prompts short and pointed; the effective input limit is 131,072 - max_tokens. content accepts a plain string or an array of content parts. Use the array form to attach one or more images, as in Image input. |
chat_template_kwargs | Object. Set {"enable_thinking": true} to make the model reason before answering; off by default. See Reasoning for the response shape. |
The full parameter reference lives in the API reference.
Sending images
A message's content can be an array of content parts instead of a string,
which is how you attach images to a request. Add one image_url part per
image; the model reads them in the order you list them:
{"role": "user", "content": [
{"type": "text", "text": "Which of these screenshots shows the error?"},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBORw0KGgo..."}},
{"type": "image_url", "image_url": {"url": "data:image/png;base64,iVBORw0KGgo..."}}
]}
There is no fixed image count. See Image input for encoding, supported
formats, sizing, and how images are counted in usage.
Responses API
POST /v1/responses implements the OpenAI Responses API. Send your prompt as
input on each request: a string, or an array of messages whose content is a
string or an array of
input parts:
- cURL
- Python
curl https://inference.celeris.ai/celeris-1/v1/responses \
-H "Authorization: Bearer $CELERIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "celeris-1",
"input": "Rewrite as a search query: cheap flights sydney to tokyo june",
"max_output_tokens": 256,
"temperature": 0
}'
import os
from openai import OpenAI
client = OpenAI(
base_url="https://inference.celeris.ai/celeris-1/v1",
api_key=os.environ["CELERIS_API_KEY"],
)
response = client.responses.create(
model="celeris-1",
input="Rewrite as a search query: cheap flights sydney to tokyo june",
max_output_tokens=256,
temperature=0,
)
print(response.output_text)
Like chat completions, it returns one complete JSON response by default, or set
stream: true for streaming. A Responses stream emits the
standard Responses event sequence and ends with a response.completed event.
See the API reference for the full field
list. Set max_output_tokens explicitly in production: when it is omitted,
Responses uses the context window remaining after the rendered input, which can
permit a much longer generation than the request needs.
Sending images on the Responses API
Images work here too, but the Responses API spells its content parts
input_text and input_image rather than text and image_url. The image is
the same inline data: URL, passed as a plain string, and every input_image
must carry a detail level. Attach as many as you need:
{"input": [
{"role": "user", "content": [
{"type": "input_text", "text": "Name the color of each image, in order."},
{"type": "input_image", "image_url": "data:image/png;base64,iVBORw0KGgo...", "detail": "auto"},
{"type": "input_image", "image_url": "data:image/png;base64,iVBORw0KGgo...", "detail": "auto"}
]}
]}
Omitting detail, or using the chat spellings here, is rejected with a
400 that does not name the missing field. See
Image input, which also covers
encoding, sizing, and token cost.
Tool calling
Pass OpenAI-style tools and the model calls one when appropriate. tool_choice
sets the calling policy:
auto(default): the model decides whether to call a tool.none: the model never calls a tool.required: the model is directed to call one of the provided tools.{"type": "function", "function": {"name": "..."}}: the model is directed to call that specific function.
required and named forcing apply to non-streaming requests; forcing on a
streaming request returns a 400. Forcing is a strong
instruction, not a guarantee. A reply can still come back as prose, so confirm
it carries tool_calls, and validate the parsed arguments against your own
schema before acting on them.
curl https://inference.celeris.ai/celeris-1/v1/chat/completions \
-H "Authorization: Bearer $CELERIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "celeris-1",
"messages": [{"role": "user", "content": "What is the weather in Melbourne?"}],
"tools": [{
"type": "function",
"function": {
"name": "get_weather",
"description": "Get the current weather for a city",
"parameters": {
"type": "object",
"properties": {"city": {"type": "string"}},
"required": ["city"]
}
}
}],
"max_tokens": 256,
"temperature": 0
}'
When the model calls a tool, the reply has finish_reason: "tool_calls" and the
call under choices[0].message.tool_calls:
{
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"content": null,
"tool_calls": [{
"id": "call_abc123",
"type": "function",
"function": {"name": "get_weather", "arguments": "{\"city\": \"Melbourne\"}"}
}]
},
"finish_reason": "tool_calls"
}]
}
Run the tool yourself, then send the result back as a tool message that
references the call's id to get the final answer:
curl https://inference.celeris.ai/celeris-1/v1/chat/completions \
-H "Authorization: Bearer $CELERIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "celeris-1",
"messages": [
{"role": "user", "content": "What is the weather in Melbourne?"},
{"role": "assistant", "content": null, "tool_calls": [{"id": "call_abc123", "type": "function", "function": {"name": "get_weather", "arguments": "{\"city\": \"Melbourne\"}"}}]},
{"role": "tool", "tool_call_id": "call_abc123", "content": "18°C, partly cloudy"}
],
"max_tokens": 256,
"temperature": 0
}'
Reasoning
Ask the model to think before it answers by setting enable_thinking: true
inside chat_template_kwargs (off by default).
Setting enable_thinking does not guarantee a separate reasoning field: on
requests where it is not separated the request still succeeds, but the working
comes back inline in content with protocol markers instead. Detect it
per response, and follow
Reasoning is not separated on every request
for the mitigation.
curl https://inference.celeris.ai/celeris-1/v1/chat/completions \
-H "Authorization: Bearer $CELERIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "celeris-1",
"messages": [{"role": "user", "content": "If a train travels 60 km in 45 minutes, what is its average speed in km/h?"}],
"chat_template_kwargs": {"enable_thinking": true},
"max_tokens": 2048,
"temperature": 0
}'
Where the reasoning is separated it lands on message.reasoning and the final
answer on message.content; where it is not, message.reasoning is absent or
empty and both come back in message.content:
{
"choices": [{
"index": 0,
"message": {
"role": "assistant",
"reasoning": "60 km in 45 min. 45 min = 0.75 h. 60 / 0.75 = 80.",
"content": "80 km/h."
},
"finish_reason": "stop"
}]
}
When streaming, the reasoning arrives first as delta.reasoning
chunks, then the answer as delta.content chunks. On chat completions, where
the reasoning is separated, add include_reasoning: false alongside
chat_template_kwargs (inside extra_body on the OpenAI SDKs) to have the
model think without returning the working;
it does not suppress a working that was not separated. A reply to a turn
whose last message is a tool result can carry the answer text in a different
place than described above. See
Reasoning after a tool result.
The OpenAI SDKs pass non-standard fields through extra_body:
response = client.chat.completions.create(
model="celeris-1",
messages=[{"role": "user", "content": "If a train travels 60 km in 45 minutes, what is its average speed in km/h?"}],
max_tokens=2048,
temperature=0,
extra_body={"chat_template_kwargs": {"enable_thinking": True}},
)
reasoning = getattr(response.choices[0].message, "reasoning", None) or ""
if reasoning: # non-empty: the working was separated
print(reasoning)
print(response.choices[0].message.content) # the answer (or unseparated working+answer)
Reasoning tokens are billed as completion (output) tokens like any other
generated text, so budget for them in max_tokens / max_output_tokens. The
reasoning and the answer share that one budget, with no separate reasoning-only
cap. A limit that is too low can therefore be spent entirely on thinking and
stop before any answer is emitted. The call still returns 200, with
finish_reason length, message.content empty or null, and the working in
message.reasoning. Under include_reasoning: false the reasoning field is
null too, so the response carries no text at all. Key on finish_reason.
Treat that shape as a truncated turn and retry with a larger budget rather than
showing the working as the answer.
content can also come back empty with finish_reason stop. That is a normal
stop, not a budget stop, and a larger max_tokens will not change it. Retry the
request as-is, or resend with thinking off, rather than showing the working as
the answer. Chat completions default max_tokens to 2048, so keep thinking
requests at least there and tune up against your own inputs.
Reasoning on the Responses API
The same chat_template_kwargs: {"enable_thinking": true} works on
/v1/responses. Where the reasoning is
separated it is
emitted as its own output item of type: "reasoning", ahead of the
type: "message" item that carries the answer. That item can also arrive with
content null or empty, so treat the working as separated only when the item
holds a non-empty type: "reasoning_text" part, the same non-empty rule as
message.reasoning on chat completions. Where it is not separated, the working
comes back inline in the message item's text:
{
"output": [
{"type": "reasoning", "content": [{"type": "reasoning_text", "text": "60 km in 45 min = 0.75 h. 60 / 0.75 = 80."}]},
{"type": "message", "role": "assistant", "content": [{"type": "output_text", "text": "80 km/h.", "annotations": []}]}
]
}
JSON mode
Set response_format on a non-streaming /chat/completions request to get a
single JSON value back in choices[0].message.content, ready to parse with no
surrounding prose and no Markdown code fence. Two forms are accepted:
{"type": "json_object"}: the reply is one JSON object.{"type": "json_schema", "json_schema": {"name": "...", "schema": { ... }}}: the reply is one JSON value that validates against your JSON Schema. Give the schema an object root ({"type": "object", ...}), the shape OpenAI's structured outputs also require; a non-object root (for example{"type": "array"}) is not a supported contract here and may fail closed with the400below, after the generations spent trying. The schema goes to the model with your messages, so it counts towardusage.prompt_tokensand toward the request's 131,072-token limit. Keep it as small as the contract allows.
curl https://inference.celeris.ai/celeris-1/v1/chat/completions \
-H "Authorization: Bearer $CELERIS_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "celeris-1",
"messages": [{"role": "user", "content": "Give me a person and the city they live in, for Paris."}],
"response_format": {
"type": "json_schema",
"json_schema": {
"name": "person_city",
"schema": {
"type": "object",
"properties": {"name": {"type": "string"}, "city": {"type": "string"}},
"required": ["name", "city"],
"additionalProperties": false
}
}
},
"max_tokens": 256,
"temperature": 0
}'
contentcarries the JSON value alone. If you need the model's intermediate reasoning inside the JSON, add a field for it to your schema.- Size
max_tokensfor the whole object. If generation reaches the limit the reply can be well-formed but incomplete JSON; checkfinish_reasonisstop, notlength. - An invalid
json_schemais rejected before any generation with a400whose message containsis not a valid JSON Schema. - If the model cannot produce conforming JSON, the request returns a
400whose message containsresponse_format could not be satisfied; tighten the prompt or the schema. Unlike most400s it carriesusage, and the generations it reports are charged. - A reply may be regenerated to meet the contract, so a JSON-mode request
can bill more tokens, and take longer, than a single generation.
usagecounts every attempt: a retry re-bills the whole prompt, so bothprompt_tokensandcompletion_tokensare summed across attempts, andusage.completion_tokenscan exceed the request'smax_tokenseven whenfinish_reasonisstop. - JSON mode is non-streaming
/chat/completionsonly. A streaming request returns a400whose message says this deployment does not support constrained decoding. That rejection is about streaming, not about JSON mode on a buffered request. The Responses API does not offer JSON mode. - With
tools, a tool call takes precedence: when the model calls a tool the reply is that call, and the JSON contract does not apply to it.
Streaming
Leave stream unset for one complete JSON response, or set stream: true for
OpenAI-compatible server-sent events. Streaming responses end with [DONE].
Set stream_options: {"include_usage": true} so the final chunk carries the
request's token usage.
- Python
- TypeScript
stream = client.chat.completions.create(
model="celeris-1",
messages=[{"role": "user", "content": "List three uses for a brick."}],
max_tokens=256,
stream=True,
stream_options={"include_usage": True},
)
for chunk in stream:
if chunk.choices: # the final include_usage chunk carries no choices
print(chunk.choices[0].delta.content or "", end="")
if chunk.usage:
print(f"\n{chunk.usage.total_tokens} tokens")
const stream = await client.chat.completions.create({
model: 'celeris-1',
messages: [{role: 'user', content: 'List three uses for a brick.'}],
max_tokens: 256,
stream: true,
stream_options: {include_usage: true},
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? '');
if (chunk.usage) console.log(`\n${chunk.usage.total_tokens} tokens`);
}
For short responses, a single JSON response is usually simpler. Use streaming when an interactive client needs to display partial output. A client disconnect does not guarantee that processing stops; see Pricing.
Calling from the browser
The API supports browser requests. It responds to preflight OPTIONS
requests, returns Access-Control-Allow-Origin: *, and exposes the
Server-Timing header to browser scripts. A single-page application can call
Celeris directly:
const response = await fetch(
'https://inference.celeris.ai/celeris-1/v1/chat/completions',
{
method: 'POST',
headers: {
'Authorization': `Bearer ${key}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
model: 'celeris-1',
messages: [{role: 'user', content: 'Rewrite as a search query: cheap flights sydney to tokyo june'}],
max_tokens: 256,
temperature: 0,
}),
},
);
if (!response.ok) throw new Error(`Celeris request failed: ${response.status}`);
const result = await response.json();
console.log(result.choices[0].message.content);
Authentication uses a bearer header rather than a cookie. Do not embed a Celeris key in public client-side code. See Authentication.
Response behavior
- API errors use OpenAI-compatible JSON. SDKs expose them as HTTP errors
with the status intact. Handle broad cases by status and use
error.codewhen you need more detail. Some5xxresponses may not include JSON. See Errors. - Every successful response includes
usage(prompt, completion, and total tokens), the same numbers the usage dashboard aggregates. - Every successful response includes
Server-Timingwith server-side latency durations. Error responses may omitusageandServer-Timing.429responses includeRetry-After, and responses echo a caller-supplied trace ID. - Ask for JSON with
response_format, not in the prompt alone. Model output can occasionally include a Markdown code fence when you ask for JSON in the prompt; JSON mode is not affected, and returns a value ready to parse. JSON mode is unavailable on a streaming request and on the Responses API. There, see Known issues for examples and safe boundary stripping before JSON parsing.