Skip to main content

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 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
}'

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:

ParameterGuidance
max_tokensAny 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.
temperatureUse 0 for classification and extraction. Increase it only when the task benefits from variation.
seedCombine with temperature: 0 to reduce sampling variation between requests.
stopStandard stop sequences are honored.
messagesKeep 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_kwargsObject. 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 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
}'

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).

Reasoning may not be separated

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 the 400 below, after the generations spent trying. The schema goes to the model with your messages, so it counts toward usage.prompt_tokens and 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
}'
  • content carries 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_tokens for the whole object. If generation reaches the limit the reply can be well-formed but incomplete JSON; check finish_reason is stop, not length.
  • An invalid json_schema is rejected before any generation with a 400 whose message contains is not a valid JSON Schema.
  • If the model cannot produce conforming JSON, the request returns a 400 whose message contains response_format could not be satisfied; tighten the prompt or the schema. Unlike most 400s it carries usage, 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. usage counts every attempt: a retry re-bills the whole prompt, so both prompt_tokens and completion_tokens are summed across attempts, and usage.completion_tokens can exceed the request's max_tokens even when finish_reason is stop.
  • JSON mode is non-streaming /chat/completions only. A streaming request returns a 400 whose 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.

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")

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.code when you need more detail. Some 5xx responses 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-Timing with server-side latency durations. Error responses may omit usage and Server-Timing. 429 responses include Retry-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.