# Command-line interface # Command-line interface `celeris` is the official command-line interface for the Celeris API. It is built for shell pipelines: inputs come from flags, `@files`, or stdin; results go to stdout; diagnostics go to stderr. If you have used the OpenAI CLI, the command layout will feel familiar. ## Install With Homebrew (macOS and Linux): ```bash brew install ai-celeris/tools/celeris ``` With Go 1.23 or later: ```bash go install github.com/ai-celeris/celeris-cli/cmd/celeris@latest ``` Prebuilt archives for macOS, Linux, and Windows are on the [releases page](https://github.com/ai-celeris/celeris-cli/releases), with checksums. ## Configure The CLI reads your [API key](/authentication) from `CELERIS_API_KEY`: ```bash export CELERIS_API_KEY="ck_..." ``` That is the only required setting; the CLI targets the production endpoint by default. `CELERIS_BASE_URL` overrides the endpoint (the CLI appends `/v1` itself), `CELERIS_MODEL` overrides the default model (`celeris-1`), and `OPENAI_API_KEY` / `OPENAI_BASE_URL` work as fallbacks. Every setting also has a flag: `--api-key`, `--base-url`, `--model`. ## First request ```bash celeris q "Reply with one word: the capital of France." ``` ```text Paris ``` `q` is the pipeline shortcut: it sends one user message, streams the answer as plain text, and appends piped stdin to the prompt as context: ```bash git diff --staged | celeris q "Write a one-line commit message for this diff:" tail -100 app.log | celeris q "Summarize the errors in this log:" ``` ## Every endpoint as a command The resource commands mirror the [HTTP API](/api-reference) and print the complete response: ```bash # Chat completions, non-streaming, full JSON response: celeris chat:completions create \ --input "Classify as positive or negative: great product" \ --max-tokens 256 --temperature 0 \ --format json # System prompts and multi-turn conversations: celeris chat:completions create \ -g system:"Answer tersely." \ -g user:"What is a monad?" \ --stream # Legacy completions: celeris completions create --prompt "The capital of France is" --max-tokens 256 # Models: celeris models list # Anything else under /v1, as a raw authenticated request: echo '{"model":"celeris-1","messages":[{"role":"user","content":"hi"}]}' \ | celeris api post /chat/completions ``` Input flags accept a literal value, `@file` to read a file, or `-` to read stdin. With no input flag, piped stdin becomes the user message. ## Output formats `--format` controls what lands on stdout: | Format | Output | | -------- | ------------------------------------------------------------ | | `auto` | Pretty JSON on a terminal; bare text when piped (default) | | `text` | The completion text only | | `json` | The full response as compact JSON | | `jsonl` | One JSON object per line (stream chunks, or models) | | `pretty` | Indented JSON, or a table for `models list` on a terminal | | `raw` | The response body exactly as received | `--stream` emits tokens as they are generated; with `--format jsonl` it prints each chunk's JSON on its own line instead. Exit codes are `0` for success, `1` for request or API failures, and `2` for usage errors, so scripts can tell a bad invocation from a failed call. Error messages, including `Retry-After` on `429` responses, go to stderr. ## Request limits The CLI validates [`max_tokens`](/making-requests#parameters-that-matter-most-here) before sending: it must be a positive integer, and prompt plus completion must fit the [context window](/models). ## Debugging Every request carries a User-Agent that identifies the CLI build, such as `celeris-cli/1.2.3 (darwin; arm64) go/1.23.4`. If you report an issue, include your `celeris version` output; support can correlate it with server-side request logs. `--debug` prints the request method, URL, User-Agent, and body to stderr: ```bash celeris models list --debug ``` ## Version and updates ```bash celeris version brew upgrade celeris # Homebrew installs ``` The CLI is open development at [github.com/ai-celeris/celeris-cli](https://github.com/ai-celeris/celeris-cli); releases follow semantic versioning. --- # Companies and workspaces # Companies and workspaces Celeris keeps API keys, credit, and usage inside **workspaces**. Each workspace also has a **company** context for team membership and billing. Signing up creates the first company context and Workspace; the Workspace initially uses the company name and can be renamed independently. One login can have Workspace access across several companies. The account menu's **Change workspace** list stays flat: each row shows the Workspace name with the company name beneath it. Choosing a row changes both the active Workspace and its company context; there is no separate company selector. ## Workspace access and roles Admins invite teammates from **Team** and select the Workspaces to include. Every invitation starts with the **Developer** role. After acceptance, the Developer: - appears in the Team roster; - can use only the Workspaces included in accepted invitations; - cannot see sibling Workspaces or create another Workspace. An Admin can promote an active member explicitly from **Team**. Admins can manage membership, billing, and Workspace actions throughout the company context. ## Settings **Settings → Organization** contains the company-name field. It does not show a company directory, sibling Workspaces, aggregate usage, policies, or a company delete action. Manage the active Workspace's name and deletion under **Settings → Workspace**. Switch to another Workspace from **Change workspace** in the account menu. Workspace credit, usage, and API keys stay separate even when two Workspaces share the same company context. --- # Quickstart # Quickstart Celeris provides **`celeris-1`**, a diffusion language model for short, structured responses, through an **OpenAI-compatible API**. Existing OpenAI clients can connect by using the Celeris base URL and API key. ## 1. Create an account Sign up at the [Customer Console](https://console.celeris.ai) with Google or an email address and password (email signups get a verification code). ## 2. Activate your workspace A workspace must be **activated** before it can serve requests. In the Console, open the account menu in the bottom-left, select [**Settings → Billing**](https://console.celeris.ai/settings/billing), and add a payment method. This requests activation, at no charge, and is usually immediate; if it is not, the Console tells you and we email you once the workspace is activated. Then add credit. ## 3. Reveal your API key Activating the workspace gives you a private API key named **Default**. In the Console, open **API keys** and reveal it. Store the full `ck_` value in a secret manager and keep it out of source control: ```bash export CELERIS_API_KEY="ck_..." ``` You can create additional private keys, or reveal, rotate, and revoke your existing keys from the Console. See [Authentication](/authentication). ## 4. Make your first request The API base URL includes the model's name (see [Models](/models)): ```text https://inference.celeris.ai/celeris-1/v1 ``` **cURL** ```bash 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 capital of France? Reply with one word."} ], "max_tokens": 256, "temperature": 0 }' ``` **Python** ```python # pip install openai 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": "user", "content": "What is the capital of France? Reply with one word.", } ], max_tokens=256, temperature=0, ) print(response.choices[0].message.content) ``` **TypeScript** ```typescript // npm install 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: 'user', content: 'What is the capital of France? Reply with one word.', }, ], max_tokens: 256, temperature: 0, }); console.log(response.choices[0].message.content); ``` > **[Synced language tabs]** Selecting a language updates the examples on other pages. > **`celeris-1` accepts any positive `max_tokens`. Prompt tokens plus `max_tokens`** must be at most 131,072, so the accepted input limit is `131,072 - max_tokens`. ## 5. Read the response Responses use the standard OpenAI chat-completion shape. The quickstart returns one complete JSON response; OpenAI-compatible token streaming is also supported ([Making requests](/making-requests#streaming)): ```json { "id": "chatcmpl-9f2e41c0", "object": "chat.completion", "created": 1751673600, "model": "celeris-1", "choices": [ { "index": 0, "message": {"role": "assistant", "content": "Paris"}, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 27, "completion_tokens": 2, "total_tokens": 29, "prompt_tokens_details": {"cached_tokens": 0} } } ``` `prompt_tokens_details.cached_tokens` reports how much of the prompt used the [cached input token rate](/pricing#rates). It is part of `prompt_tokens`, not an additional token count. Every successful response includes a [`Server-Timing` header](/measuring-latency) with server-side latency durations. Error responses may omit `usage` and `Server-Timing`; see [Errors](/errors). ## Next steps - [Authentication](/authentication): key lifecycle, rotation, and handling. - [Models](/models): what celeris-1 is good at, and its limits. - [Prompt engineering](/prompting): how to prompt a diffusion LLM for reliable structured output. - [Agentic workloads](/agentic-workloads): request patterns the model excels at, such as classify, extract, judge, and rewrite. - [Rate limits](/rate-limits): what's limited and how to back off cleanly. - [Pricing](/pricing): token rates and prepaid credits. - [Going to production](/going-to-production): the production-readiness checklist. - Use the **Playground** in the [Console](https://console.celeris.ai) to test prompts without writing client code. --- # Account & sign-in # Account & sign-in This page covers registration, verification, password resets, MFA, and workspace invitations for the [Customer Console](https://console.celeris.ai). For workspace and billing management, see [Console & billing](/console). ## Signing up Sign up with an email address and password, or click **Continue with Google**. - **Email + password.** We email you a six-digit verification code; enter it to confirm the address and your workspace is created. The code is valid for **24 hours**. If it expires, click **Resend code** on the verification screen and use the newest code. - **Google.** Select **Continue with Google** to register or sign in without a separate Celeris password. Account Setup creates your initial company and Workspace. For how company context, Workspace switching, and teammate access fit together, see [Companies and workspaces](/organizations). The workspace holds no API keys until it is activated ([Keys](/authentication#keys)). Adding a payment method under [**Settings → Billing**](https://console.celeris.ai/settings/billing) requests activation, at no charge. Activation is usually immediate; if it is not, the Console tells you and we email you once the workspace is activated. Then add credit to start making requests. ## Verification codes Celeris sends six-digit codes for account verification and password resets. Codes are single-purpose and time-limited: 24 hours for account verification and **1 hour** for password resets. If you request more than one code, only the most recent one works. ## Resetting your password Use **Forgot password?** on the sign-in screen. We email you a reset code and a link to the reset form. The code is valid for **1 hour**. Requesting a new reset code invalidates the previous one. If you registered with Google, continue to use **Continue with Google**. ## Authenticator-app MFA Email-and-password accounts can add a second sign-in step from **Console → Settings → Multi-factor authentication**. Scan the QR code with any standard TOTP authenticator (including 1Password, Google Authenticator, or Authy), enter the current six-digit code, and future password sign-ins will ask for a fresh code. Your company context can require MFA, in which case the Console prompts you until you enroll. Disabling MFA requires both your current password and a current authenticator code. If you use Google sign-in, your identity provider manages MFA instead; configure it on that account. Maintain secure recovery access to your authenticator. If you lose access to all enrolled devices, [contact support](mailto:support@celeris.ai). ## Workspace invites When a teammate invites you to their workspace: - **New to Celeris?** The invite email includes a **temporary password**. Sign in with it and you'll be asked to set your own password before you land in the Console, then accept or decline each Workspace invitation from **Team**. The temporary password is valid for **7 days**. After that, ask your inviter to re-send the invite from the Console's **Team** page. - **Already have an account?** The invite attaches to your existing account. Sign in as usual (your password doesn't change). The notification and account menu link to **Team**, where you can review the company, inviter, and Workspace access before accepting or declining each invitation independently. If you are invited to several Workspaces at once, Celeris sends one email with a separate action for each Workspace. Answering one does not accept or discard the others. Workspace members share credits and usage. Private API keys remain visible only to their owner; workspace-shared keys are visible to all members. An invite's access rules are covered in [Companies and workspaces](/organizations#workspace-access-and-roles); see [Console & billing](/console) for billing. ## Didn't get an email? 1. **Wait a few minutes.** Most messages arrive quickly, but delivery can be delayed. 2. **Check spam and quarantine.** Search for "Celeris," including in any corporate email quarantine. 3. **Request another message.** Select **Resend code** for account verification, or restart the flow from **Forgot password?** for password resets. An inviter can resend a workspace invitation from **Team**. Only the most recent verification or reset code is valid. 4. **Contact support.** If the message still has not arrived, email [support@celeris.ai](mailto:support@celeris.ai). --- # Console & billing # Console & billing Use the [Customer Console](https://console.celeris.ai) to manage workspaces, API keys, billing, usage, and team access. ## Organizations and workspaces Companies provide context for Workspaces, while keys, credits, and usage stay inside each Workspace. See [Companies and workspaces](/organizations) for the switcher, roles, access boundaries, and settings. A workspace must be **activated** (by adding a payment method under [**Settings → Billing**](https://console.celeris.ai/settings/billing)) before it has API keys at all; activation gives every member a private key named **Default**. See [Keys](/authentication#keys) for private versus workspace-shared keys. On first sign-in, the Console shows a checklist for buying credit, revealing the initial API key, and sending a prompt from the Playground. ## API keys Under **API keys**, you can create, reveal, rotate, and revoke credentials. See [Authentication](/authentication) for key-management guidance. ## Playground The **Playground** sends prompts to the live model from your browser. Use it to test system prompts and response formats before integrating the API. Choose `celeris-1` or **Celeris-1 Magnus** from the model picker; the selected model is used in the endpoint and request body. With Thinking off, the max-output control uses each example's answer preset; the default example starts at 2,048 (the API's default when `max_tokens` is omitted). Turning Thinking on raises the preset for the selected example, model, and effort so reasoning leaves room for the answer. The control steps in 256-token increments. Its ceiling follows the selected model: 130,816 for `celeris-1` and 16,384 for Celeris-1 Magnus. See the [`max_tokens` guidance](/making-requests#parameters-that-matter-most-here) for how to size it. Open **Parameters** to turn **Thinking** on when a prompt benefits from working through the problem before answering. Thinking is off by default for both models so Playground runs start on the faster path. With Celeris-1 Magnus, turning it on also reveals an **Effort** choice: **Low**, **Medium**, or **Extra high**, with **Medium** selected by default. Celeris-1 has one reasoning mode and does not show that input. Reasoning shares the run's max-output budget. When the response returns the working separately, the Playground shows it in a collapsible **Reasoning** section above the answer, collapsed by default. See [Reasoning](/making-requests#reasoning) for the response shapes and budget guidance. A run can carry up to four images alongside the prompt. Use the **Attach image** button, paste, or drag-and-drop (all take more than one), or pick the **Read an image** example, which comes with a sample receipt ready to run. Attached images are sent in the order shown, and you can remove or reorder them before running; see [several images per request](/images#several-images-in-one-request). The console re-encodes large images in your browser so the whole request fits the size budget, and each image counts toward the prompt's input tokens (the post-run receipt shows the exact number). See [Pricing](/pricing#rates) for token rates. The **Use it from code** panel provides the endpoint, region, model name, and examples for curl, Python, Node.js, Go, Rust, and Java. See the [Quickstart](/quickstart) for a complete walkthrough. ## Usage The **usage page** shows activity for the last 24 hours, 7 days, or 30 days: - completed request counts; - uncached input, cached input, and completion token volumes. Cached input is shown separately and is not included in the uncached input figure. Throttled (`429`) requests do not appear here, so track them in [your own telemetry](/rate-limits#watching-your-usage). A request may still be charged if the client disconnects after processing has started. See [Security and data handling](/security#inference-and-usage-records). ## Billing Celeris is **prepaid**: usage draws down a workspace credit balance. Open the account menu in the bottom-left of the Console, then select [**Settings → Billing**](https://console.celeris.ai/settings/billing) to manage billing. [Pricing](/pricing) has the token rates, the purchase amounts, and how [credit expiry](/pricing#credit-expiry) works. Balances, top-ups, invoices, and usage are workspace-specific. The saved card and billing email belong to the organization, so an organization admin can use the same payment method across its workspaces without merging their spend. Receipts are emailed automatically, and invoice documents remain available under the same billing page. ### Auto Top-Up Auto Top-Up keeps a workspace funded without a manual purchase. Under [**Settings → Billing**](https://console.celeris.ai/settings/billing), turn it on and set the balance that triggers a purchase, the amount to buy, and a monthly spending limit. Celeris charges the organization's saved card when the workspace falls below that threshold during use. Usage while the balance is already below the threshold also triggers a purchase. **Auto Top-Up on Credit Expiry** is off by default. Credit expiry usually means that a service is not in use, so Celeris does not charge the saved card merely because unused credit expired. If expiry leaves the balance below the threshold, the next successful use triggers the usual automatic purchase. Turn this setting on only if you want an expiry-driven drop below the threshold to trigger the purchase immediately. This default avoids unwanted charges after a period of inactivity. After each automatic purchase, Celeris emails the billing contact with the amount added and the new balance. The email is separate from Stripe's itemized receipt. You can change the thresholds or turn Auto Top-Up off from the same billing page. When Auto Top-Up is off, Celeris sends one low-balance email after the balance falls to 10% of its level immediately after the latest purchase. For example, if a workspace has $0.50 left and buys $100, the next warning is sent near $10.05. Celeris sends a final email if the balance reaches zero. When your balance reaches zero, API calls return [`402 insufficient_quota`](/errors#402) until you add credit. ## Commercial access Commercial access is not self-serve in the Console. For higher limits, a committed sustained rate, reserved capacity, or custom terms, [contact us](mailto:support@celeris.ai). Inference continues to use the selected workspace's prepaid credit balance after activation. ## Team **Team** manages collaborators; [Companies and workspaces](/organizations) explains invitation roles and access. Credit is workspace-shared; keys are private to their creator unless deliberately [shared with the workspace](/authentication#keys). Review and [rotate keys](/authentication#rotating-and-revoking) when access changes. ## Account settings Company and Workspace management is described in [Companies and workspaces](/organizations#settings). Password management and notification delivery choices live under **Settings** and follow your account across devices and organizations; turning off a channel or event suppresses future matching Console notifications or emails. Company settings always apply to the context that owns the active Workspace. --- # Activation waitlist # Activation waitlist Celeris activates a limited number of new workspaces each day while we expand serving capacity. If the day's activation capacity is full when you request access under [**Settings → Billing**](https://console.celeris.ai/settings/billing), your workspace joins the activation waitlist automatically, and we email you when it is activated. ## What happens next You do not need to keep the Console open or request activation again. We will email you when your workspace is activated. Until then, you can create an API key, but it cannot serve requests until the workspace is activated and holds credit. After the activation email arrives: 1. Sign in to the [Customer Console](https://console.celeris.ai) and open [**Settings → Billing**](https://console.celeris.ai/settings/billing) from the account menu in the bottom-left. 2. Add a payment card, unless the page already shows a saved one. 3. Add credit, then continue the [Quickstart](/quickstart) to create an API key and make your first request. Adding a card does not itself purchase credit. See [Pricing](/pricing) for how prepaid credit and charges work. --- # Authentication # Authentication Every API request is authenticated with a bearer key: ```http Authorization: Bearer ck_your_key_here ``` Requests without a valid key are rejected with **`401 Unauthorized`**. See [Errors](/errors). ## Keys Keys belong to an activated workspace. A workspace that has not been activated holds no keys and cannot create any; the Console points you at Billing instead. When the workspace is activated, every member of it, including anyone invited before activation, gets a private key named **Default**. Open **API keys** in the [Customer Console](https://console.celeris.ai) to reveal yours or create additional keys. - **Keys start with `ck_`.** Add this prefix to your secret-scanning rules. - **The full key is shown when created** and can be revealed later by its owner. Treat the key as a secret. - **Keys authenticate the workspace, but each key has an owner.** A key you create is **private to you** by default; teammates don't see it, so your organization can tell who is using the API. Workspace admins and organization owners/admins can create a **workspace-shared** key that every member sees and can use, and only they can rename, rotate, or revoke it. Prefer private keys: use a shared key only when a service genuinely needs a workspace-owned credential, since sharing loses per-person visibility and a departure forces a rotation that disrupts everyone. Either way, all usage draws on the workspace's shared credit balance and there are no per-key limits. - **Label your keys.** One key per app or environment (`prod-backend`, `staging`, `cli-tools`) simplifies rotation and incident response. A workspace can have up to five API keys. When it already has five keys, the Console disables **Create key**. The API returns `429` for another create request with this message: ```text You have reached the maximum number of API keys for this workspace. Contact support if you need more. ``` ## Rotating and revoking From the Console you can: - **Rotate** a key. The Console mints a replacement and revokes the old key in one step. Update your deployment with the new value. - **Revoke** a key. It stops working and cannot be recovered. Creating, rotating, and revoking keys can take up to **one minute** to take effect. During that interval, an old key may still work and a new key may not work yet. For a planned, zero-downtime change, create a second key, verify it, update the deployment, and then revoke the old key. If a key may be compromised, rotate or revoke it immediately and account for the one-minute change window. ## Handling keys well - **Keep keys server-side where you can.** Load them from environment variables or your secret manager, never from source control. - **Calling from a browser is supported.** A key embedded in a public web page can be read and reused by anyone. For public applications, proxy Celeris calls through your backend. See [Making requests](/making-requests#calling-from-the-browser). - **Prefer private keys.** Give each person and each app its own unshared key so usage stays attributable. Use a workspace-shared key only when the application requires a credential shared by the workspace. - **Rotate on departure.** Rotate shared keys when someone with production access leaves your team, and revoke or rotate any keys the departing person created. --- # Models # Models | Model | What it's for | | --- | --- | | [`celeris-1`](#celeris-1) | General purpose work at the lowest latency | | [`celeris-1-magnus`](#celeris-1-magnus) | More intelligent answers, still fast | | [`celeris-1-decision`](#celeris-1-decision) | Yes/no, multiple-choice, and scoring decisions as probabilities | Each model has its own path segment in the [base URL](#model-routing-and-base-urls), its own properties below, and its own [token rates](/pricing#rates). ## celeris-1 **`celeris-1`** is the first model from Celeris: a general purpose diffusion language model that delivers near-GPT-5 level intelligence with up to 15x faster response times. Unlike autoregressive LLMs, which produce output one token at a time, a diffusion model can refine multiple output tokens in parallel. That parallel refinement suits classification, extraction, scoring, and query rewriting. Typical short requests complete in approximately **50–150 ms** within the serving region. Measure your workload with the [`Server-Timing` header](/measuring-latency). celeris-1 handles general workloads well. For tasks that need more intelligence, such as extended multi-step reasoning, use [celeris-1-magnus](#celeris-1-magnus) for that step and keep celeris-1 everywhere else. | Property | Value | | --- | --- | | Model name (`model` field) | `celeris-1` | | Context window | 131,072 tokens (prompt + requested maximum output) | | Max output (`max_tokens`) | Any positive integer, bounded only by the context window (prompt + `max_tokens` ≤ 131,072). Defaults to 2,048 when omitted. | | Especially strong at | Classification, extraction, scoring/judging, query rewriting, structured output | | Input | Text and images (see [Image input](/images)) | | Output | Text or structured tool calls (see [Tool calling](/making-requests#tool-calling)) | | Response mode | Complete JSON responses or OpenAI-compatible SSE streaming | > **[The context window is a hard ceiling]** Prompt tokens plus `max_tokens` must be at most 131,072, making the effective input limit `131,072 - max_tokens`. Oversized requests are rejected up front with a [`400`](/errors#400) rather than silently truncated. See [Rate limits](/rate-limits) for request limits shared by each workspace and model. > **### Image understanding** celeris-1 reads images as well as text. Attach one or more images as content parts and ask about them in the same request: screenshot triage, document and receipt extraction, image classification. Images are input only; the model replies with text and does not generate images. See [Image input](/images) for encoding, sizing, billing, and limits. ## celeris-1-magnus **Celeris-1 Magnus** is more intelligent than celeris-1 and still fast, though not as fast as celeris-1. Use `celeris-1-magnus` in both the URL and the request body's `model` field. | Property | Value | | --- | --- | | Model name (`model` field) | `celeris-1-magnus` | | Context window | 131,072 tokens (prompt + requested maximum output) | | Max output (`max_tokens`) | 16,384 tokens, also bounded by the remaining context window. Defaults to 2,048 when omitted. | | Input | Text and images (see [Image input](/images)) | | Output | Text or structured tool calls | | Response mode | Complete JSON responses or OpenAI-compatible SSE streaming | Celeris-1 Magnus reasons before it answers unless the request sets `chat_template_kwargs: {"enable_thinking": false}`. The working can be returned separately from the final content; see [Reasoning](/making-requests#reasoning). Tool definitions use the same [`tools` and `tool_choice`](/making-requests#tool-calling) request fields as celeris-1. > **[The output limit is part of the context window]** Prompt tokens plus `max_tokens` must be at most 131,072, and `max_tokens` must not exceed 16,384. Requests outside either limit are rejected with a [`400`](/errors#400). > **## celeris-1-decision** **`celeris-1-decision`** answers typed questions about your data, text or images, and returns probabilities. It supports two API formats: - [OpenAI Decisions API](/decisions#openai-decisions-api), `POST /v1/decisions`, with the OpenAI SDK. - [System One API](/decisions#system-one-api), `POST /v1/systemone`, with the Jev SDK. Both endpoints share the model's price and rate limits. This model does not serve the chat, Responses, or models endpoints. | Property | Value | | --- | --- | | Model name (`model` field) | `celeris-1-decision` | | Context window | 131,072 tokens. A fixed part is reserved for the answers, so the input limit is slightly lower. | | Max output | Set by your questions. There is no `max_tokens`; see [Request limits](/decisions#request-limits). | | Input | Text and images (see [Decisions](/decisions)) | | Output | Probabilities for each question, with an optional explanation | | Response mode | Complete JSON responses. No streaming. | ## Model routing and base URLs The model you're calling is part of the URL path, ahead of the standard OpenAI-style `/v1/...` suffix: ```text https://inference.celeris.ai//v1 ``` For Celeris-1 Magnus: ```text https://inference.celeris.ai/celeris-1-magnus/v1 ``` Two things must agree: 1. the model segment of the URL path, and 2. the `model` field in the request body. A request whose path names an unavailable model returns **`404 Not Found`**. If the path is correct but the `model` body field does not match, the API returns a validation error. You can list what a base URL serves with the standard models endpoint: ```bash curl https://inference.celeris.ai/celeris-1/v1/models \ -H "Authorization: Bearer $CELERIS_API_KEY" ``` ## Regions Celeris aims to serve requests as close as possible to customers. The global base URL above selects the best available nearest region automatically, so most callers do not need to pick one. We recommend it for most customers. ### Regional endpoints A regional endpoint routes requests preferentially to a specific region. Use one when you want to target that region for latency or networking reasons: | Region | Base URL | | --- | --- | | US East (`celeris-1`) | `https://us-east-1.aws.inference.celeris.ai/celeris-1/v1` | During capacity constraints or maintenance, Celeris may temporarily serve a regional endpoint's requests from another region to maintain availability. Whichever base URL you use, record end-to-end latency and the [`Server-Timing` header](/measuring-latency); streaming requests also need a client-side time-to-first-token measurement. --- # Making requests # 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. ```text 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** ```bash 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 }' ``` **Python** ```python 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) ``` **TypeScript** ```typescript 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](/api-reference). These parameters have the greatest effect on short, structured workloads: | Parameter | Guidance | | --- | --- | | `max_tokens` / `max_completion_tokens` | Any **positive integer**; prompt tokens plus the requested output budget must be at most `131,072` ([Models](/models)). Defaults to `2048` when neither field is supplied. Leave generation headroom beyond the visible answer: start short structured calls at `256` and tune against representative inputs. A budget that is too tight can stop generation before the answer is emitted. See [output budgets](#output-budgets) for exceptions. | | `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; when you specify an output budget, the effective input limit is `131,072` minus that budget. `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](/images). | | `chat_template_kwargs` | Object. `enable_thinking` turns reasoning before the answer on or off, and the default depends on the model. Celeris-1 Magnus also accepts `reasoning_effort` values `low`, `medium`, and `xhigh` in this object. See [Reasoning](#reasoning) for each model's default and the response shape. | The full parameter reference lives in the [API reference](/api-reference). ### Output budgets In some cases, additional output may be generated to complete a response, so reported completion usage can exceed the requested budget. Use the returned [`usage`](/api-reference#token-usage) for token accounting. ### Prompt caching Cache reuse depends on cache availability and the reusable prefix. Identical requests may have different cache usage, and a cache hit may cover only part of the prompt. Use `usage.prompt_tokens_details.cached_tokens` for chat and text completions, or `usage.input_tokens_details.cached_tokens` for the Responses API, to determine the cached amount. See [Token usage](/api-reference#token-usage) for accounting. ### 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: ```json {"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](/images) 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](#sending-images-on-the-responses-api): **cURL** ```bash 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 }' ``` **Python** ```python 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](#streaming). A Responses stream emits the standard Responses event sequence and ends with a `response.completed` event. See the [API reference](/api-reference#post-v1responses) 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: ```json {"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`](/errors#400) that does not name the missing field. See [Image input](/images#sending-images-on-the-responses-api), 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`](/errors#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. ```bash 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`: ```json { "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: ```bash 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 Celeris-1 reasons only when asked. Set `enable_thinking: true` inside `chat_template_kwargs`. Celeris-1 Magnus reasons by default. Send `enable_thinking: false` to get a direct answer. Celeris-1 has one reasoning mode, so omit `reasoning_effort` for that model. Celeris-1 Magnus accepts `reasoning_effort` at the top level or inside `chat_template_kwargs` while thinking is on: `low`, `medium`, or `xhigh`. It defaults to `low`. If both are supplied, the top-level value is used. Celeris-1 Magnus spends at most 75% of the output budget on thinking unless the request sets `thinking_token_budget`. The rest is kept for the answer, but a small output budget can still end before the answer is complete. > **[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](/known-issues#reasoning-is-not-separated-on-every-request) for the mitigation. > **```bash** 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`: ```json { "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](#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](/known-issues#reasoning-after-a-tool-result). The OpenAI SDKs pass non-standard fields through `extra_body`: ```python 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](/known-issues#reasoning-is-not-separated-on-every-request) 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: ```json { "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](https://json-schema.org). 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`](/errors#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. ```bash 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`](/errors#400) whose message contains `is not a valid JSON Schema`. - **If the model cannot produce conforming JSON, the request returns a [`400`](/errors#400)** whose message contains `response_format could not be satisfied`; tighten the prompt or the schema. Unlike most `400`s 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. Reported completion usage can exceed the requested [output budget](#output-budgets) even when `finish_reason` is `stop`. See [Token usage](/api-reference#token-usage) for how these generations are counted and billed. - **JSON mode is non-streaming `/chat/completions` only.** A streaming request returns a [`400`](/errors#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](#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** ```python 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") ``` **TypeScript** ```typescript 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, and the request [may still be charged](/security#inference-and-usage-records). ## 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: ```typescript 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](/authentication#handling-keys-well). ## 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](/errors#error-envelope). - **Every successful response includes `usage`** (prompt, completion, total, and the cached prompt subset), the same numbers the [usage dashboard](/console#usage) aggregates. - **Every successful response includes [`Server-Timing`](/measuring-latency)** 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](#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](#responses-api). There, see [Known issues](/known-issues#json-output-may-include-a-markdown-code-fence) for examples and safe boundary stripping before JSON parsing. --- # Image input # Image input celeris-1 and Celeris-1 Magnus understand images as well as text. Send one or more images in the same request as your prompt and the model reads them together, so you can ask questions about a screenshot, extract fields from a scanned document, or compare two photos with the same low-latency call you already use for text. Images work on both [chat completions](/making-requests#chat-completions) and the [Responses API](#sending-images-on-the-responses-api). The two spell their content parts differently. Everything else here applies to both: how many you can send, encoding, sizing, and token cost. Images are **input only**. The model reads images and replies with text; it does not generate images. ## Sending images Instead of passing `content` as a string, pass it as an array of **content parts**, one part per piece of the message. A `text` part carries your prompt and an `image_url` part carries the image, encoded as a [base64 data URL](#encoding-the-image): **cURL** ```bash # `base64 -w0` is GNU-only; this form works on both Linux and macOS. IMAGE_B64=$(base64 < receipt.png | tr -d '\n') 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": [ {"type": "text", "text": "Fields: merchant, total, purchase_date"}, {"type": "image_url", "image_url": {"url": "data:image/png;base64,'"$IMAGE_B64"'"}} ]} ], "max_tokens": 256, "temperature": 0 }' ``` **Python** ```python from openai import OpenAI client = OpenAI( base_url="https://inference.celeris.ai/celeris-1/v1", api_key=os.environ["CELERIS_API_KEY"], ) with open("receipt.png", "rb") as handle: image_b64 = base64.b64encode(handle.read()).decode() response = client.chat.completions.create( model="celeris-1", messages=[ {"role": "system", "content": "Extract the requested fields. Reply with JSON only."}, { "role": "user", "content": [ {"type": "text", "text": "Fields: merchant, total, purchase_date"}, { "type": "image_url", "image_url": {"url": f"data:image/png;base64,{image_b64}"}, }, ], }, ], max_tokens=256, temperature=0, ) print(response.choices[0].message.content) print(response.usage.prompt_tokens) ``` **TypeScript** ```typescript const client = new OpenAI({ baseURL: 'https://inference.celeris.ai/celeris-1/v1', apiKey: process.env.CELERIS_API_KEY, }); const imageB64 = readFileSync('receipt.png').toString('base64'); 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: [ {type: 'text', text: 'Fields: merchant, total, purchase_date'}, {type: 'image_url', image_url: {url: `data:image/png;base64,${imageB64}`}}, ], }, ], max_tokens: 256, temperature: 0, }); console.log(response.choices[0].message.content); console.log(response.usage?.prompt_tokens); ``` The response shape is unchanged: text in `choices[0].message.content`, token counts in `usage`. Every parameter that works for a text request (`max_tokens`, `temperature`, `seed`, `stop`, `stream`, `tools`) works the same way here. Content parts are also valid for messages that carry no image at all: a `text` part on its own is equivalent to passing `content` as a string, which is useful when you build messages programmatically and only sometimes attach an image. ### Several images in one request Attach as many `image_url` parts as you need. The model reads them in the order you list them, so you can refer to them positionally in your prompt, as in "compare the first image to the second", and ask for per-image output: ```python content = [{"type": "text", "text": "Name the color of each image, in order."}] for path in ("first.png", "second.png", "third.png"): with open(path, "rb") as handle: b64 = base64.b64encode(handle.read()).decode() content.append( {"type": "image_url", "image_url": {"url": f"data:image/png;base64,{b64}"}} ) response = client.chat.completions.create( model="celeris-1", messages=[{"role": "user", "content": content}], max_tokens=256, ) print(response.choices[0].message.content) ``` We impose no count limit. What binds in practice is the shared [size budget](#sizing-images) and the context window. ## Sending images on the Responses API The [Responses API](/making-requests#responses-api) takes images too, and takes several of them the same way, but it uses its own spelling for 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 rather than an object. ```python from openai import OpenAI client = OpenAI( base_url="https://inference.celeris.ai/celeris-1/v1", api_key=os.environ["CELERIS_API_KEY"], ) with open("receipt.png", "rb") as handle: image_b64 = base64.b64encode(handle.read()).decode() response = client.responses.create( model="celeris-1", input=[ { "role": "user", "content": [ {"type": "input_text", "text": "Fields: merchant, total, purchase_date"}, { "type": "input_image", "image_url": f"data:image/png;base64,{image_b64}", "detail": "auto", }, ], } ], max_output_tokens=256, ) print(response.output_text) ``` Add an `input_image` part per image to send more than one; they are read in the order you list them, as on chat completions. > **[`detail` is required]** Every `input_image` part must carry `detail`: one of `auto`, `low`, or `high`. Leave it out, or use the chat spellings here, and the request matches no accepted input shape: the [`400`](/errors#400) carries a schema error for every shape it was checked against and never names the missing field. Check that every part carries `detail`, and that the part names are right, before reading further into the body. > **## Encoding the image** Pass the image inline as a `data:` URL: ```text data:;base64, ``` Declare the media type that matches the bytes you send. Use `image/png` for a PNG and `image/jpeg` for a JPEG: | Format | Media type | | --- | --- | | PNG | `image/png` | | JPEG | `image/jpeg` | Other raster formats may be accepted, but they are not part of the documented contract. Convert to PNG or JPEG before sending rather than relying on them. > **[Send the bytes, not a link]** Only inline `data:` URLs are supported. Pointing `image_url.url` at an `https://` address you host is **not** supported: fetching it would put a third-party network round trip on the inference path, and the request may fail or add unbounded latency. Download the image in your own application and send the bytes. > **## Sizing images** Two limits apply, and image requests usually meet the byte limit first. **The whole JSON body must fit the request size limit.** Base64 encoding inflates each image by about 33% on the wire, so it is the *encoded* images plus your prompt that have to fit, and the limit applies to the request as a whole rather than to each image. The complete body may be up to 64 MiB (67,108,864 bytes). Send images no larger than the task needs, and downscale before you send rather than after a rejection. An oversized body is rejected with a [`413 payload_too_large`](/errors#413); a request that streams its body past the limit may instead have its connection torn down. Treat either as a signal to downscale rather than to retry unchanged. **Images also consume prompt tokens** from the [context window](/models#celeris-1), alongside your text and `max_tokens` (`max_output_tokens` on the Responses API). Resizing and recompressing before you send is almost always worth it. celeris-1 rescales the image to its own working resolution, so past a certain size extra pixels buy little accuracy while still costing bytes, latency, and a `413` risk: ```python from io import BytesIO from PIL import Image REQUEST_BUDGET_BYTES = 100_000 # whatever you choose to spend per request... IMAGE_COUNT = 3 # ...split across the images it carries BUDGET_BYTES = REQUEST_BUDGET_BYTES // IMAGE_COUNT image = Image.open("photo.jpg") image.thumbnail((896, 896)) # cap the long edge; raise it for a higher max_soft_tokens image = image.convert("RGB") for quality in (75, 60, 45): buffer = BytesIO() image.save(buffer, format="JPEG", quality=quality, optimize=True) image_bytes = buffer.getvalue() if len(image_bytes) <= BUDGET_BYTES: break else: raise ValueError(f"still {len(image_bytes)} bytes — downscale further") ``` Check the encoded size rather than assuming it: a single fixed quality setting can land anywhere from a few kilobytes to several hundred depending on the image, which is why the loop above steps down until it fits. A smaller upload is also a faster request. For text-heavy images such as scans and screenshots, keep enough resolution that the smallest text you care about stays legible to your own eye, and see [Image resolution](#image-resolution). Prefer PNG for screenshots and JPEG for photos. ## Image resolution celeris-1 resizes every image to a fixed working resolution before reading it, so a larger upload adds no detail. Set that resolution per request with `max_soft_tokens`: `70`, `140`, `280` (default), `560`, or `1120`. Each image costs roughly that many prompt tokens. Use lower values for classification and captioning, and higher ones for OCR, documents, and small text. ```python response = client.chat.completions.create( model="celeris-1", messages=messages, extra_body={"mm_processor_kwargs": {"max_soft_tokens": 1120}}, ) ``` Without the Python SDK, send `mm_processor_kwargs` as a top-level body field. It applies to every image in the request. The `detail` field does not change the resolution. If text is still too small at `1120`, crop to the part you need or send the page as several tiles. Each image gets the full budget. On Celeris-1 Magnus, an image's token cost grows with its pixel dimensions: roughly one prompt token per 32×32 pixels, with a minimum of about 64 tokens per image. A 1024×1024 image costs about 1,024 tokens and a 2048×2048 image about 4,096. `max_soft_tokens` has no effect. Downscale an image to lower its cost. ## Token usage and cost Images count as **input tokens**. There is no separate per-image charge, and every image in the request is counted, not only the first. Use the completed response's token counts and the [applicable rates](/pricing#rates) to calculate the charge. An image's token cost comes from the resolution the model processes it at, not from its file size, so you cannot infer it from the byte count of the upload. Recompressing a JPEG to half the bytes does not halve its token cost. Read the real number from the `usage` block of a completed response, which has this shape (the values below illustrate the field layout; they are not a published per-image cost, so read your own): ```json "usage": { "prompt_tokens": 284, "completion_tokens": 19, "total_tokens": 303, "prompt_tokens_details": {"cached_tokens": 0} } ``` The Responses API reports the same thing under different names, `input_tokens` and `output_tokens`. Its cached subset is `input_tokens_details.cached_tokens`. To budget a workload, measure it once against representative inputs: send a request with the images and one with them removed, and the difference in prompt tokens is what they cost you. ## Limitations - **Images are input only.** The model replies with text and cannot generate, edit, or return images. - **Inline `data:` URLs only.** Remote `https://` image URLs are not supported. Both the model and the request path treat images as ordinary prompt content, so [rate limits](/rate-limits), [errors](/errors), [streaming](/making-requests#streaming), and [disconnect charging](/security#inference-and-usage-records) behave as they do for text. --- # Decisions # Decisions `celeris-1-decision` answers questions about text or images with probabilities. Each answer comes back as a number or a choice you can threshold, rank, or route on. Choose the API format that matches your integration: | SDK | Endpoint | | --- | --- | | [Jev SDK](#system-one-api) | `POST /v1/systemone` | | [OpenAI SDK](#openai-decisions-api) | `POST /v1/decisions` | Both endpoints serve the same model and share its price and rate limits. Each response is one JSON body. There is no streaming. ## System One API Send `model: "celeris-1-decision"`, a `state` containing your data, and named `questions` to `POST /v1/systemone`. It implements TypeSafe's System One API, so code written for Jev with the [Jev SDK](https://docs.typesafe.ai/sdk/python/) runs against it once you change the base URL, key, and model name. ### A yes/no question **cURL** ```bash curl https://inference.celeris.ai/celeris-1-decision/v1/systemone \ -H "Authorization: Bearer $CELERIS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "celeris-1-decision", "state": "I was charged twice this month. Please refund one of the payments.", "questions": { "refund": {"type": "noul", "instructions": "The customer is asking for a refund."} } }' ``` **Jev SDK** ```python # pip install typesafe-sdk from typesafe_sdk import Noul, TypeSafeClient client = TypeSafeClient( api_key=os.environ["CELERIS_API_KEY"], base_url="https://inference.celeris.ai/celeris-1-decision", model="celeris-1-decision", ) response = client.system_one( state="I was charged twice this month. Please refund one of the payments.", questions={"refund": Noul(instructions="The customer is asking for a refund.")}, ) print(response.nouls["refund"].noul) # 0.95 ``` ```json { "model": "celeris-1-decision", "answers": { "refund": {"type": "noul", "noul": 0.95} }, "usage": {"input_tokens": 122, "output_tokens": 0} } ``` `noul` is the probability that the answer is yes, from `0` to `1`. The responses shown on this page are examples; values can vary. To switch existing code without editing it, set the SDK's environment variables: ```bash export TYPESAFE_BASE_URL=https://inference.celeris.ai/celeris-1-decision export TYPESAFE_API_KEY=$CELERIS_API_KEY export TYPESAFE_DEFAULT_MODEL=celeris-1-decision ``` ### Question types Each question needs a `type` and usually carries `instructions`. One request can hold several questions of any mix of types, and `state` can be a string or any other JSON value. | `type` | Answer | | --- | --- | | `noul` | `noul`: the probability of yes. | | `choice` | `choice`: the most likely option. `probabilities`: one per option. | | `score` | `score`: the average level, weighted by probability. `probabilities`: one per level. `legend`: each level's description. | For `noul`, `criteria` is optional. Use `{"true": ..., "false": ...}` to describe each outcome. For `choice`, `criteria` is an object of at least two options. Each key is an option name and each value describes it, or is `null`. For `score`, `criteria` is an array of at least two levels, lowest first. Levels are numbered from `0`. `choice` and `score` answers also carry `confidence`, the top probability rescaled so that `0` is an even split across the options and `1` is certainty. Probabilities are rounded to two decimal places and sum to 1. **cURL** ```bash curl https://inference.celeris.ai/celeris-1-decision/v1/systemone \ -H "Authorization: Bearer $CELERIS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "celeris-1-decision", "state": { "subject": "Charged twice", "body": "I was charged twice this month and nobody has answered my emails for a week. Please refund one of the payments." }, "questions": { "refund": {"type": "noul", "instructions": "The customer is asking for a refund."}, "team": { "type": "choice", "instructions": "Which team should handle this ticket?", "criteria": { "billing": "Payments, invoices and refunds.", "technical": "Bugs and outages.", "sales": "Upgrades and quotes." } }, "frustration": { "type": "score", "instructions": "How frustrated is the customer?", "criteria": ["Calm.", "Annoyed.", "Angry."] } } }' ``` **Jev SDK** ```python from typesafe_sdk import Choice, Noul, Score response = client.system_one( state={ "subject": "Charged twice", "body": "I was charged twice this month and nobody has answered my emails for a week. Please refund one of the payments.", }, questions={ "refund": Noul(instructions="The customer is asking for a refund."), "team": Choice( instructions="Which team should handle this ticket?", criteria={ "billing": "Payments, invoices and refunds.", "technical": "Bugs and outages.", "sales": "Upgrades and quotes.", }, ), "frustration": Score( instructions="How frustrated is the customer?", criteria=["Calm.", "Annoyed.", "Angry."], ), }, ) print(response.nouls["refund"].noul) # 0.98 print(response.choices["team"].choice) # billing print(response.scores["frustration"].score) # 1.37 ``` ```json { "model": "celeris-1-decision", "answers": { "refund": {"type": "noul", "noul": 0.98}, "team": { "type": "choice", "choice": "billing", "probabilities": {"billing": 0.97, "technical": 0.02, "sales": 0.01}, "confidence": 0.96 }, "frustration": { "type": "score", "score": 1.37, "probabilities": {"0": 0.03, "1": 0.57, "2": 0.40}, "confidence": 0.36, "legend": {"0": "Calm.", "1": "Annoyed.", "2": "Angry."} } }, "usage": {"input_tokens": 231, "output_tokens": 0} } ``` ### Celeris extensions Celeris adds options that System One does not have. They go in one optional top-level object, `x_celeris`. The Jev SDK sends it through `extra_body`. #### Explanations Set `explain` to `true` to get one sentence per question on why the model picked its answer. The sentences come back in `x_celeris.explanations`, keyed by question name. Explanations make the request slower. **cURL** ```bash curl https://inference.celeris.ai/celeris-1-decision/v1/systemone \ -H "Authorization: Bearer $CELERIS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "celeris-1-decision", "state": "I was charged twice this month and nobody has answered my emails for a week.", "questions": { "team": { "type": "choice", "instructions": "Which team should handle this ticket?", "criteria": { "billing": "Payments, invoices and refunds.", "technical": "Bugs and outages.", "sales": "Upgrades and quotes." } } }, "x_celeris": {"explain": true} }' ``` **Jev SDK** ```python from typesafe_sdk import Choice, SystemOneResponse # The SDK's own response type drops fields it does not declare. class Explained(SystemOneResponse): x_celeris: dict | None = None response = client.system_one( state="I was charged twice this month and nobody has answered my emails for a week.", questions={ "team": Choice( instructions="Which team should handle this ticket?", criteria={ "billing": "Payments, invoices and refunds.", "technical": "Bugs and outages.", "sales": "Upgrades and quotes.", }, ), }, extra_body={"x_celeris": {"explain": True}}, response_model=Explained, ) print(response.x_celeris["explanations"]["team"]) ``` ```json { "model": "celeris-1-decision", "answers": { "team": { "type": "choice", "choice": "billing", "probabilities": {"billing": 0.97, "technical": 0.02, "sales": 0.01}, "confidence": 0.96 } }, "usage": {"input_tokens": 128, "output_tokens": 15}, "x_celeris": { "explanations": { "team": "The customer reports being charged twice, which is a payments and billing issue." } } } ``` #### Images Attach images in `images`, an object that maps a name to a [base64 data URL](/images#encoding-the-image). Images are part of the state, and your state and questions can refer to one by its name. `max_soft_tokens` sets the [resolution](/images#image-resolution) the model reads every image in the request at. It takes the values listed there. **cURL** ```bash IMAGE_B64=$(base64 < receipt.png | tr -d '\n') curl https://inference.celeris.ai/celeris-1-decision/v1/systemone \ -H "Authorization: Bearer $CELERIS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "celeris-1-decision", "state": {"claim": {"amount": 13.50, "category": "meals"}}, "questions": { "matches": {"type": "noul", "instructions": "The receipt total matches the claimed amount."} }, "x_celeris": { "images": {"receipt": "data:image/png;base64,'"$IMAGE_B64"'"}, "max_soft_tokens": 560 } }' ``` **Jev SDK** ```python from typesafe_sdk import Noul with open("receipt.png", "rb") as f: image_b64 = base64.b64encode(f.read()).decode() response = client.system_one( state={"claim": {"amount": 13.50, "category": "meals"}}, questions={ "matches": Noul(instructions="The receipt total matches the claimed amount."), }, extra_body={ "x_celeris": { "images": {"receipt": f"data:image/png;base64,{image_b64}"}, "max_soft_tokens": 560, } }, ) print(response.nouls["matches"].noul) ``` ## OpenAI Decisions API Send your data as `input` and a list of `questions` to `POST /v1/decisions`. The endpoint uses the [OpenAI Decisions API format](https://developers.openai.com/api/reference/python/resources/decisions/methods/create). Point the OpenAI SDK at the Celeris base URL, supply your Celeris API key, and set `model` to `celeris-1-decision`. ### A yes/no question **cURL** ```bash curl https://inference.celeris.ai/celeris-1-decision/v1/decisions \ -H "Authorization: Bearer $CELERIS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "celeris-1-decision", "input": "I was charged twice this month. Please refund one of the payments.", "questions": [ { "type": "predicate", "name": "refund", "instructions": "The customer is asking for a refund." } ] }' ``` **OpenAI Python SDK** ```python # pip install 'openai>=3.26.0' from openai import OpenAI client = OpenAI( api_key=os.environ["CELERIS_API_KEY"], base_url="https://inference.celeris.ai/celeris-1-decision/v1", ) response = client.decisions.create( model="celeris-1-decision", input="I was charged twice this month. Please refund one of the payments.", questions=[ { "type": "predicate", "name": "refund", "instructions": "The customer is asking for a refund.", } ], ) print(response.answers[0].probability) # 0.95 ``` ```json { "model": "celeris-1-decision", "answers": [ {"type": "predicate", "name": "refund", "probability": 0.95} ], "usage": { "input_tokens": 122, "input_tokens_details": {"cached_tokens": 0, "cache_write_tokens": 0}, "output_tokens": 0, "output_tokens_details": {"reasoning_tokens": 0}, "total_tokens": 122 } } ``` `probability` is the chance that the statement is true, from `0` to `1`. The responses shown on this page are examples; values can vary. The OpenAI Python SDK needs version `3.26.0` or later for `client.decisions`. Include `/v1` in its `base_url`. ### Question types One request can mix question types. Give each question `instructions` that describe what to evaluate. A `name` is optional; when supplied, it must be nonempty and unique within the request. Answers follow the order of your questions and echo any names you supplied. | `type` | Answer | | --- | --- | | `predicate` | `probability`: the probability that the statement is true. | | `choice` | `choice`: the most likely value. `probabilities`: a list of values and their probabilities. | | `score` | `score`: the average level index, weighted by probability. `probabilities`: a list with each level's numeric `value`, `label`, and probability. | A `choice` question needs `choices`, at least two objects with a `value` and optional `description`. A `score` question needs `levels`, at least two objects with a `label` and optional `description`, lowest first. Score levels are numbered from `0`. `choice` and `score` answers also carry `confidence`, the top probability rescaled so that `0` is an even split across the options and `1` is certainty. Choice values can be strings or booleans, and the response preserves their types. Values must be unique. A single question cannot include both `true` and `"true"`, or both `false` and `"false"`. **cURL** ```bash curl https://inference.celeris.ai/celeris-1-decision/v1/decisions \ -H "Authorization: Bearer $CELERIS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "celeris-1-decision", "input": "I was charged twice this month and nobody has answered my emails for a week. Please refund one of the payments.", "questions": [ {"type": "predicate", "name": "refund", "instructions": "The customer is asking for a refund."}, { "type": "choice", "name": "team", "instructions": "Which team should handle this ticket?", "choices": [ {"value": "billing", "description": "Payments, invoices and refunds."}, {"value": "technical", "description": "Bugs and outages."}, {"value": "sales", "description": "Upgrades and quotes."} ] }, { "type": "score", "name": "frustration", "instructions": "How frustrated is the customer?", "levels": [{"label": "Calm"}, {"label": "Annoyed"}, {"label": "Angry"}] } ] }' ``` **OpenAI Python SDK** ```python response = client.decisions.create( model="celeris-1-decision", input="I was charged twice this month and nobody has answered my emails for a week. Please refund one of the payments.", questions=[ { "type": "predicate", "name": "refund", "instructions": "The customer is asking for a refund.", }, { "type": "choice", "name": "team", "instructions": "Which team should handle this ticket?", "choices": [ {"value": "billing", "description": "Payments, invoices and refunds."}, {"value": "technical", "description": "Bugs and outages."}, {"value": "sales", "description": "Upgrades and quotes."}, ], }, { "type": "score", "name": "frustration", "instructions": "How frustrated is the customer?", "levels": [{"label": "Calm"}, {"label": "Annoyed"}, {"label": "Angry"}], }, ], ) print(response.answers[0].probability) # 0.98 print(response.answers[1].choice) # billing print(response.answers[2].score) # 1.37 ``` The `answers` list for this request looks like this: ```json [ {"type": "predicate", "name": "refund", "probability": 0.98}, { "type": "choice", "name": "team", "choice": "billing", "probabilities": [ {"value": "billing", "probability": 0.97}, {"value": "technical", "probability": 0.02}, {"value": "sales", "probability": 0.01} ], "confidence": 0.96 }, { "type": "score", "name": "frustration", "score": 1.37, "probabilities": [ {"value": 0, "label": "Calm", "probability": 0.03}, {"value": 1, "label": "Annoyed", "probability": 0.57}, {"value": 2, "label": "Angry", "probability": 0.40} ], "confidence": 0.36 } ] ``` ### Celeris extensions Celeris options go in the optional top-level `x_celeris` object. The OpenAI SDK sends it through `extra_body`. #### Explanations Set `x_celeris.explain` to `true` to get one sentence per question explaining the answer. The response adds `x_celeris.explanations`, a list in the same order as `answers`. Explanations make the request slower. **cURL** ```bash curl https://inference.celeris.ai/celeris-1-decision/v1/decisions \ -H "Authorization: Bearer $CELERIS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "celeris-1-decision", "input": "I was charged twice this month. Please refund one of the payments.", "questions": [ {"type": "predicate", "name": "refund", "instructions": "The customer is asking for a refund."} ], "x_celeris": {"explain": true} }' ``` **OpenAI Python SDK** ```python response = client.decisions.create( model="celeris-1-decision", input="I was charged twice this month. Please refund one of the payments.", questions=[ { "type": "predicate", "name": "refund", "instructions": "The customer is asking for a refund.", } ], extra_body={"x_celeris": {"explain": True}}, ) print(response.model_extra["x_celeris"]["explanations"][0]) ``` The added response field looks like this: ```json { "x_celeris": { "explanations": ["The customer explicitly asks for one of the payments to be refunded."] } } ``` #### Images For images, send `input` as a list of `user` messages with `input_text` and `input_image` content parts. An image's `image_url` must be a [base64 data URL](/images#encoding-the-image). External URLs and file IDs are not supported. **cURL** ```bash IMAGE_B64=$(base64 < receipt.png | tr -d '\n') curl https://inference.celeris.ai/celeris-1-decision/v1/decisions \ -H "Authorization: Bearer $CELERIS_API_KEY" \ -H "Content-Type: application/json" \ -d '{ "model": "celeris-1-decision", "input": [{ "role": "user", "content": [ {"type": "input_text", "text": "The expense claim is for $13.50 in meals."}, {"type": "input_image", "image_url": "data:image/png;base64,'"$IMAGE_B64"'"} ] }], "questions": [ {"type": "predicate", "name": "matches", "instructions": "The receipt total matches the claimed amount."} ], "x_celeris": {"max_soft_tokens": 560} }' ``` **OpenAI Python SDK** ```python with open("receipt.png", "rb") as f: image_b64 = base64.b64encode(f.read()).decode() response = client.decisions.create( model="celeris-1-decision", input=[ { "role": "user", "content": [ {"type": "input_text", "text": "The expense claim is for $13.50 in meals."}, {"type": "input_image", "image_url": f"data:image/png;base64,{image_b64}"}, ], } ], questions=[ { "type": "predicate", "name": "matches", "instructions": "The receipt total matches the claimed amount.", } ], extra_body={"x_celeris": {"max_soft_tokens": 560}}, ) print(response.answers[0].probability) ``` `x_celeris.max_soft_tokens` sets the [resolution](/images#image-resolution) for every image in the request. The image `detail` field accepts `low`, `high`, `auto`, or `original`, but does not change the resolution. ### Input and compatibility `input` accepts a string or a list of `user` messages. A message's `content` can be a string or a list of text and image parts. For structured data, serialize it to a string, such as `input=json.dumps(ticket)` in Python. Other message roles, tools, audio, and files are not supported. The model reads all text parts in order, joined with blank lines. It reads images separately in the order you sent them, named `image_1`, `image_2`, and so on. With several images, refer to those names in your questions. Text next to an image is not kept attached to it. `safety_identifier` accepts a string or `null` and has no effect on the request. Unknown fields, including `stream`, return a validation error. ## Request limits - Up to 512 questions per request, or 64 with `x_celeris.explain: true`. - Up to 512 options per `choice` question. - Up to 64 levels per `score` question. - Up to 8 images per request, each at most 25 megapixels, or 25,000,000 pixels. - Up to 100,000 JSON values per request body. Long question names can require smaller batches, even within these counts. These limits apply to both endpoints. On `/v1/decisions`, the option limit applies to `choices` and the score level limit applies to `levels`. Exceeding a limit returns [`400 invalid_request`](/errors#decisions-validation) on `/v1/decisions`, or a [`422`](/errors#422) with details in `msg` on `/v1/systemone`. The maximum request body size is 8 MiB; larger bodies return a [`413`](/errors#413). ## Usage and cost The `usage` block reports `input_tokens` and `output_tokens`. Input tokens are billed at the [`celeris-1-decision` rate](/pricing#rates), and output tokens are free. Images count as input tokens. On `/v1/decisions`, `usage` also includes `total_tokens`, the sum of input and output tokens. Its cache and reasoning token detail fields are present and set to zero. `input_tokens` covers everything the model reads for the request, which is more than your state and questions alone. Read it from the response instead of estimating it from your payload. The yes/no request above reported 122 input tokens, costing US$0.00000488. Use the [`Server-Timing` header](/measuring-latency) to measure server processing time. ## Errors On `/v1/decisions`, an invalid request returns [`400 invalid_request`](/errors#decisions-validation) in the OpenAI error envelope. The message identifies the problem and `error.param` names the top-level field, or is `null` for a problem with the whole body. The OpenAI Python SDK raises `BadRequestError` for this status. On `/v1/systemone`, an invalid request returns a [`422`](/errors#422) with a `detail` array. This includes malformed JSON, an unrecognised field such as `stream`, or a `model` other than `celeris-1-decision`. See [Errors](/errors#400) for `400 request_rejected` and how to correct the request. Statuses such as `401`, `402`, `413`, `429`, and `502` behave as described in [Errors](/errors), and [rate limits](/rate-limits) apply to both endpoints as for any other model. --- # Errors # Errors The API uses standard HTTP status codes and OpenAI-compatible JSON error responses. Use the HTTP status for broad handling and `error.code` when you need to distinguish errors that share a status, such as the two `429` responses. ## Error envelope Celeris API errors use this shape: ```json { "error": { "message": "Invalid or missing API key.", "type": "authentication_error", "code": "invalid_api_key" } } ``` - **`message`**: a human-readable description. - **`type`**: a broad error class, such as `authentication_error` or `rate_limit_exceeded`. - **`code`**: a stable, machine-readable code. Use it to distinguish errors that share an HTTP status. See [`429`](#429). Some `5xx` responses may not include a JSON body. Retry all `5xx` responses by status rather than requiring an `error.code` value. If you send an `X-Client-Trace-Id` header, Celeris echoes it in API responses so you can correlate failures with application logs and support cases. | Status | `code` | Retry? | | --- | --- | --- | | [`400`](#400) | `max_output_tokens_exceeded` or another validation code | After fixing the request | | [`401`](#401) | `invalid_api_key` | After fixing credentials | | [`402`](#402) | `insufficient_quota` | After adding credit | | [`404`](#404) | `not_found` | After fixing the URL | | [`408`](#408) | `request_timeout` | Yes | | [`413`](#413) | `payload_too_large` | After reducing the request body | | [`422`](#422) | None | After fixing the request | | [`429`](#429) | `rate_limit_exceeded` / `service_busy` | Yes; honor `Retry-After` | | [`502`](#502) | `upstream_error` | Yes, with backoff | | [`503`](#503) | `service_unavailable` | Yes, with backoff | ## 400 Bad Request The request is invalid. The response body describes the validation error. Common causes: - Malformed JSON, or a `messages` array that doesn't match the chat format. The [System One endpoint](/decisions#system-one-api) returns a [`422`](#422) for malformed JSON. - An explicit `max_tokens`, `max_completion_tokens`, or `max_output_tokens` above the model's output limit returns `max_output_tokens_exceeded`. Reduce that value before retrying, or [contact support](mailto:support@celeris.ai) if you need a higher limit. - `max_tokens` is zero or negative, or prompt tokens plus `max_tokens` exceed **131,072**. Requests are not silently truncated; see [Models](/models). - On `/v1/responses`, an image part missing its required `detail`, or one written with the chat spellings (`text`, `image_url`) instead of `input_text` / `input_image`. The `400` body carries a schema error for every input shape the request was checked against, rather than naming the missing `detail`; see [Image input](/images#sending-images-on-the-responses-api). - A [JSON-mode](/making-requests#json-mode) request with an invalid `json_schema`, or whose output could not be produced as conforming JSON. Streaming a `response_format` or a forced [`tool_choice`](/making-requests#tool-calling) request also returns `400`. - On [`celeris-1-decision`](/decisions), `request_rejected` means the input exceeds the model's [context window](/models#celeris-1-decision) or an image cannot be read. Shorten the state or questions, or check the image data before resending. **What to do:** correct the request before retrying. ### Decisions validation On [`POST /v1/decisions`](/decisions#openai-decisions-api), malformed JSON, invalid fields, a model name other than `celeris-1-decision`, or exceeded [request limits](/decisions#request-limits) return `400` with code `invalid_request` and type `invalid_request_error`. `message` describes the problem. `param` names the top-level field at fault, or is `null` when the problem applies to the whole body. For example, a choice question with fewer than two choices returns: ```json { "error": { "message": "questions[0].choices: must hold at least two choices", "type": "invalid_request_error", "param": "questions", "code": "invalid_request", "ref": "q-d3" } } ``` Correct the request before retrying. For a limit on the number of questions, split them across requests. For a limit on JSON values, send fewer questions, choices, or levels per request. ## 401 Unauthorized Code: `invalid_api_key` (type `authentication_error`). The `Authorization` header is missing, malformed, or carries a key that is unknown or revoked. ```json {"error": {"message": "Invalid or missing API key.", "type": "authentication_error", "code": "invalid_api_key"}} ``` Common causes: - No `Authorization: Bearer ck_...` header, or a typo in it. - A revoked or rotated key still deployed somewhere. - A recently created key that is still within the documented [activation window](/authentication#rotating-and-revoking). **What to do:** check the header and the key's status in the [Console](https://console.celeris.ai). Don't retry unchanged requests. If the key was just created or rotated, wait up to one minute before trying again. ## 402 Payment Required Code: `insufficient_quota` (type `insufficient_quota`). Your workspace has no prepaid credit remaining. ```json {"error": {"message": "Your prepaid credit balance is exhausted. Add credit to continue.", "type": "insufficient_quota", "code": "insufficient_quota"}} ``` **What to do:** top up credits under [**Settings → Billing**](https://console.celeris.ai/settings/billing), then retry. Do not retry until credit is available. Because the balance is shared by all keys in the workspace, production systems should alert on this response. ## 404 Not Found Code: `not_found` (type `invalid_request_error`). The URL does not identify an available model, usually because of a typo in the model segment of the [base URL](/models#model-routing-and-base-urls). ```json {"error": {"message": "No model is served at this path. Check the URL path segment.", "type": "invalid_request_error", "code": "not_found"}} ``` **What to do:** compare the URL and request-body `model` field with the values in [Models](/models#model-routing-and-base-urls). ## 408 Request Timeout Code: `request_timeout` (type `invalid_request_error`). The request body was still arriving when the request's deadline passed. ```json {"error": {"message": "The request body did not arrive in time. Send the request again.", "type": "invalid_request_error", "code": "request_timeout"}} ``` **What to do:** send the request again. If it recurs, check the client's upload bandwidth and reduce the body size. ## 413 Payload Too Large Code: `payload_too_large` (type `invalid_request_error`). The request body exceeds the endpoint's 64 MiB (67,108,864-byte) maximum size, or 8 MiB for [`celeris-1-decision`](/decisions). ```json {"error": {"message": "The request body exceeds the maximum allowed size for this model.", "type": "invalid_request_error", "code": "payload_too_large"}} ``` **What to do:** reduce the request body before retrying. A prompt that fits the byte limit but exceeds the input headroom left by `max_tokens` returns a [`400`](#400). ## 422 Unprocessable Entity Returned by [`POST /v1/systemone`](/decisions#system-one-api), for a request body that is not a valid decision request or exceeds a [request limit](/decisions#request-limits). The body does not use the error envelope. It is a `detail` array with one entry, where `loc` is the path to the field at fault, `msg` says what is wrong with it, and `type` is `value_error`, or `json_invalid` for malformed JSON: ```json {"detail": [{"loc": ["body", "questions", "team", "criteria"], "msg": "field required", "type": "value_error"}]} ``` **What to do:** correct the field named by `loc` before retrying. If `msg` says the answers do not fit in one response, shorten the question names. If `msg` names a limit on questions or reads, split the questions across requests. For a limit on JSON values, reduce the data in the request body. ## 429 Too Many Requests Two codes share this status. Both are retriable and include a `Retry-After` header in seconds: - `rate_limit_exceeded`: the workspace exceeded its request-rate limit for the requested model. - `service_busy`: the service is temporarily unable to accept more work. ```http HTTP/1.1 429 Too Many Requests Content-Type: application/json Retry-After: 1 {"error": {"message": "You have exceeded your workspace's request rate. Slow down and retry, or contact support for a higher sustained limit.", "type": "rate_limit_exceeded", "code": "rate_limit_exceeded"}} ``` ```json {"error": {"message": "The service is busy. Please retry in a moment.", "type": "rate_limit_exceeded", "code": "service_busy"}} ``` Requests that return `429` are not charged. **What to do:** wait at least as long as `Retry-After`, then retry with exponential backoff and jitter. If `rate_limit_exceeded` persists, reduce concurrency or [contact support](mailto:support@celeris.ai) about a higher sustained rate. Report persistent `service_busy` responses to support. ## 502 Bad Gateway Code: `upstream_error` (type `api_error`). The model service is temporarily unavailable. ```json {"error": {"message": "The model service is temporarily unavailable. Please retry with backoff.", "type": "api_error", "code": "upstream_error"}} ``` A decision request that returns `502` carries no `usage` and is not charged. **What to do:** retry with backoff. ## 503 Service Unavailable Code: `service_unavailable` (type `api_error`). The service is temporarily unavailable. ```json {"error": {"message": "The service is temporarily unavailable. Please retry shortly.", "type": "api_error", "code": "service_unavailable"}} ``` **What to do:** retry with backoff. If the error persists, check the Console or [contact support](mailto:support@celeris.ai). Retry any `5xx` by status; do not require a JSON body or a specific error code. ## Retries and timeouts - **Retry** [`408`](#408), [`429`](#429), [`502`](#502), and [`503`](#503), with exponential backoff and jitter, honoring `Retry-After` when present. - **Don't retry** [`400`](#400), [`401`](#401), [`402`](#402), [`404`](#404), or [`422`](#422) without changing something first. - **Set client timeouts.** Choose a timeout that matches your workload and latency objective. Treat timeouts as transient and retry with backoff. ## A note for OpenAI SDK users OpenAI SDKs expose Celeris errors as HTTP exceptions. Branch on the HTTP status, or inspect `error.code` when you need to distinguish the two [`429`](#429) responses: ```python from openai import APIStatusError try: response = client.chat.completions.create(...) except APIStatusError as err: if err.status_code == 429: ... # back off and retry elif err.status_code == 401: ... # fix credentials; do not retry else: raise ``` --- # Rate limits # Rate limits Rate limits protect service availability and latency. Requests that exceed a limit return `429 Too Many Requests` with a `Retry-After` header. ## What's limited Requests can be limited by workspace rate limits or service availability: | Condition | Response | | --- | --- | | Workspace rate limit for the requested model exceeded | [`429 rate_limit_exceeded`](/errors#429) | | Service temporarily at capacity | [`429 service_busy`](/errors#429) | Your workspace has a separate sustained request-rate limit for each model. Traffic to one model does not consume the request-rate allowance for another. If you need a committed rate or a higher limit for a model, [contact us](mailto:support@celeris.ai). All inference usage draws from prepaid workspace credit. Other constraints to design for: - **Rate limits are shared by workspace and model.** All API keys in a workspace share the limit for the model they call. The workspace credit balance remains shared across all models. - **Short bursts may still be limited.** Design clients to handle `429` responses even when average traffic is below the stated sustained rate. - **Token limits are model-specific.** Requests over the [context window](/models) return [`400 Bad Request`](/errors#400). ## When you hit a limit Over-limit requests fail fast with **`429`**, code `rate_limit_exceeded`, and a `Retry-After` header (in seconds): ```http HTTP/1.1 429 Too Many Requests Content-Type: application/json Retry-After: 1 {"error": {"message": "You have exceeded your workspace's request rate. Slow down and retry, or contact support for a higher sustained limit.", "type": "rate_limit_exceeded", "code": "rate_limit_exceeded"}} ``` A `429 service_busy` response means the service is temporarily at capacity. Handle it in the same way: honor `Retry-After` and retry with backoff. See [Errors](/errors#429) for details. An out-of-credit workspace returns **`402 insufficient_quota`**. Add credit before retrying. See [Errors](/errors#402). ## Backing off well Wait at least as long as `Retry-After`, use exponential backoff with jitter for subsequent attempts, and cap the number of retries: ```python from openai import APIStatusError def with_backoff(call, max_attempts=5): for attempt in range(max_attempts): try: return call() except APIStatusError as err: if err.status_code not in (429, 502, 503) or attempt == max_attempts - 1: raise retry_after = float(err.response.headers.get("retry-after", 1)) # Full jitter: sleep somewhere in [retry_after, retry_after + 2^attempt) time.sleep(retry_after + random.uniform(0, 2 ** attempt)) ``` For applications with multiple workers, enforce a shared client-side concurrency limit for each model. This is more predictable than independent retry loops in each worker. ## Watching your usage The [Console's usage page](/console#usage) shows completed requests and token volumes. It does not include throttled requests, so monitor `429` responses in your application logs and metrics. --- # Prompt engineering # Prompt engineering celeris-1 is a fast, general purpose diffusion model. It performs best when prompts define the task, allowed output format, and an appropriate token limit. ## Specify the output format exactly State the required output format explicitly: - ✅ "Reply with one word: positive, negative, or neutral." - ✅ "Reply with JSON only: `{name, company, intent}`." - ✅ "Reply with the number only." - ❌ "What do you think the sentiment is?" Constrained outputs are easier to validate and generally require fewer completion tokens. Avoid open-ended wording when the caller expects a fixed schema or label. ## Enumerate the answer space When the output is a label, list the valid labels and close the set: ```json { "model": "celeris-1", "messages": [ {"role": "system", "content": "Choose exactly one tool: search, calculator, email, or none. Reply with that tool name only, without reasoning."}, {"role": "user", "content": "what is 2 + 2?"}, {"role": "assistant", "content": "calculator"}, {"role": "user", "content": "what is 14% of 2,300,000?"} ], "max_tokens": 256, "temperature": 0 } ``` - **Include an escape hatch** (`none`, `unknown`, `other`) so the model has a valid answer for inputs outside the set. Otherwise, it may choose the closest label. - **Specify the number of labels** when the response must contain exactly one value. ## Put the task in the system message Use the `system` message for the standing instruction and the `user` message for input data. This keeps the instruction stable and makes it easier to version and evaluate independently from the payload: ```json { "messages": [ {"role": "system", "content": "Score how well the passage answers the question, 0-10. Reply with the number only."}, {"role": "user", "content": "Question: ...\nPassage: ..."} ] } ``` Message roles help distinguish instructions from data, but they do not by themselves prevent prompt injection. Treat untrusted input as untrusted data, constrain available actions, and validate model output before using it. ## Set an explicit token budget - **Leave headroom in `max_tokens`.** The limit is a generation ceiling, not just the number of tokens visible in the final label or score. A ceiling set too close to the expected answer can stop the model before it emits that answer. Start short structured calls at 256 and tune only after evaluating representative inputs. Completion length affects both [latency](/measuring-latency) and [cost](/pricing). - **Keep variable prompt content concise.** Shorter prompts reduce input-token volume and leave more room for data within the [context window](/models). Put reusable instructions first so repeated prefixes can use the [cached input token rate](/pricing#rates). - **Use `stop` sequences** when the format has a clear delimiter, such as a newline. ## Make it deterministic For classification, extraction, and scoring, use `temperature: 0`. Add a `seed` when you need more reproducible sampling: ```json {"temperature": 0, "seed": 7} ``` Lower variation makes evaluations and caches more useful. Increase `temperature` only for tasks that benefit from varied output, such as generating alternative phrasings. ## Add examples when needed For unusual JSON shapes or domain-specific labels, add one or two compact examples to the system message: ```text Extract fields as JSON only: {items: [{name, qty}], pickup_time}. Example: "two flat whites for 8am" -> {"items": [{"name": "flat white", "qty": 2}], "pickup_time": "8:00am"} ``` Keep examples minimal because they count toward the context window and are billed on every request. Omit them when the instruction is sufficient. ## Validate the output, and retry with feedback For a strict JSON contract on a non-streaming request, set [`response_format`](/making-requests#json-mode) rather than asking for JSON in the prompt. `{"type": "json_object"}` guarantees only that the reply is a JSON object, so validate it below either way. Step 1's code-fence mitigation applies only to streaming and prompt-only JSON. Validate every structured response: 1. **Validate** the response against your schema (JSON parse, enum check, range check). If an expected JSON response has a code-fence-shaped wrapper, apply the narrow mitigation in [Known issues](/known-issues#json-output-may-include-a-markdown-code-fence) before parsing. 2. **On failure, retry once** with the error appended, e.g. a second `user` message: `Your last reply was not valid JSON. Reply with JSON only.` 3. **On a second failure, fall back**. Route to your default branch rather than looping. Bound retries and define a deterministic fallback for invalid output. ## Iterate in the Playground first Use the [Console Playground](/console#playground) to test prompts and response formats against the live model. The **Use it from code** panel provides an equivalent API example. ## Next steps - [Agentic workloads](/agentic-workloads): worked request patterns for classify, extract, judge, and rewrite. - [Making requests](/making-requests): the full parameter guidance. - [Going to production](/going-to-production): hardening the calls around the prompt. --- # Agentic workloads # Agentic workloads Agent systems often make several short model calls before they can continue: routing, scoring, extraction, and query rewriting. The latency of those calls accumulates across a workflow. [celeris-1](/models#celeris-1) fits these steps well. Use it throughout your agent loop, and substitute a slower, more intelligent model for the occasional step where extended generation or maximum reasoning depth matters more than speed. ## Patterns ### Classification and routing Assign an input to a fixed set of labels, such as intent categories, moderation states, or tools. Specify the allowed output values: ```json { "model": "celeris-1", "messages": [ {"role": "system", "content": "Choose exactly one tool: search, calculator, email, or none. Reply with that tool name only, without reasoning."}, {"role": "user", "content": "what is 2 + 2?"}, {"role": "assistant", "content": "calculator"}, {"role": "user", "content": "what is 14% of 2,300,000?"} ], "max_tokens": 256, "temperature": 0 } ``` ### Structured extraction Extract typed fields from unstructured text. Request JSON, define the fields, and set a token limit that fits the expected object: ```json { "model": "celeris-1", "messages": [ {"role": "system", "content": "Extract {name, company, intent}. Reply with one JSON object only, without commentary or fences. Example: 'Sam from Acme wants a demo' -> {\"name\":\"Sam\",\"company\":\"Acme\",\"intent\":\"demo\"}."}, {"role": "user", "content": "Hi, Priya Nair here from Meridian Logistics — keen to trial the enterprise plan."} ], "max_tokens": 256, "temperature": 0, "seed": 7 } ``` ### Judging and scoring Relevance grading, answer verification, and reranking generally use small inputs and outputs, which makes them suitable for celeris-1. ```json { "model": "celeris-1", "messages": [ {"role": "system", "content": "Score how well the passage answers the question. Output one integer from 0 through 10 and nothing else. If the passage directly answers the question, output 10."}, {"role": "user", "content": "Question: When did the Golden Gate Bridge open?\nPassage: The Golden Gate Bridge opened to pedestrians on May 27, 1937."} ], "max_tokens": 256, "temperature": 0 } ``` ### Query rewriting Turn conversational input into retrieval queries, SQL, or filter expressions: ```json { "model": "celeris-1", "messages": [ {"role": "system", "content": "Rewrite the message as a concise search query. Query only."}, {"role": "user", "content": "hey do you have anything like the blue runners I bought last month but waterproof"} ], "max_tokens": 256, "temperature": 0, "seed": 7 } ``` ## Implementation guidance - **Use `temperature: 0` and an explicit `max_tokens` limit** for structured calls. This reduces output variation and bounds latency and cost. Any positive integer that fits the context window is accepted. - **Budget enough tokens for a complete tool call.** When `max_tokens` runs out mid-call, the request still succeeds with `200`, and what you get back depends on the mode. A buffered reply carries `finish_reason: "length"`, an empty `tool_calls` list, and the unfinished `<|tool_call>` marker as ordinary text. A streamed reply can instead carry `finish_reason: "tool_calls"` with a structured call whose `arguments` are empty (`{}`), the same shape as a real call, and one that a schema of all-optional fields accepts. Never execute a tool whose arguments are empty or fail your schema, treat `finish_reason: "length"` as incomplete output rather than a missing call, and size the budget for the whole call. - **Ask for machine-readable output** ("Reply with the tool name only", "JSON only") and validate it before use. On a non-streaming request, set [`response_format`](/making-requests#json-mode) rather than relying on the prompt for a JSON contract, and [`tool_choice: "required"`](/making-requests#tool-calling) (or a named function) to direct a call. A directed call is not guaranteed, so still confirm the reply carries `tool_calls`. Streaming rejects both with a [`400`](/errors#400). - **Parallelize independent calls within a concurrency limit.** This provides per-item failure handling without overwhelming the workspace's rate limit for the model. See [Rate limits](/rate-limits#backing-off-well). - **Monitor `429` responses in your application.** They are not counted on the [usage page](/rate-limits#watching-your-usage). If the rate increases, reduce concurrency or [contact support](mailto:support@celeris.ai) about a higher sustained limit. --- # Measuring latency # Measuring latency Every successful API response includes a standard [`Server-Timing`](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Server-Timing) header with server-side latency measurements. ```http Server-Timing: processing;dur=31.669 ``` | Metric | What it measures | | --- | --- | | `processing` | Service time from receiving the request until the response headers are ready. | What happens before those headers depends on the response mode: - **Complete JSON response:** generation is buffered, so `processing` includes prompt processing and generation. - **Streaming response:** headers are sent before the first token, so `processing` includes request handling and stream setup but excludes prompt processing and generation that happen after the headers. For either mode, subtract `processing` from client-observed time to response headers to estimate everything outside that window: your client, plus the network path in front of the service. Treat the result as an approximation, because the two measurements are taken at different points. Do not subtract `processing` from streaming time-to-first-token: those spans end at different events, so the residual would mislabel model work as network time. ## Streaming latency Response headers cannot report work that happens after streaming begins. Measure these client-side boundaries instead: | Measurement | How to measure it | | --- | --- | | Response headers | Request start to response headers. | | First-token wait | Response headers to the first content event. | | Generation | First content event to the end of the stream. | | Total latency | Request start to stream end. | Time to first token is the first two rows combined. Measured client-side, these boundaries cover the whole request; the response itself cannot separate network time from model time. For short outputs, generation time may be too small to measure reliably. Use total latency when comparing the end-to-end experience. ## Reading it **cURL** ```bash curl -sS -D - -o /dev/null \ 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":"ok"}],"max_tokens":256}' \ | grep -i '^server-timing' ``` **Python** ```python with httpx.Client() as http: r = http.post( "https://inference.celeris.ai/celeris-1/v1/chat/completions", headers={"Authorization": f"Bearer {os.environ['CELERIS_API_KEY']}"}, json={ "model": "celeris-1", "messages": [{"role": "user", "content": "ok"}], "max_tokens": 256, }, ) r.raise_for_status() print(r.elapsed.total_seconds() * 1000, "ms wall clock") print("Server-Timing:", r.headers["server-timing"]) ``` **TypeScript** ```typescript // Server-Timing is CORS-exposed (Access-Control-Expose-Headers), so browser // code can read it straight off the fetch Response: const url = 'https://inference.celeris.ai/celeris-1/v1/chat/completions'; const init = { method: 'POST', headers: { 'Authorization': `Bearer ${process.env.CELERIS_API_KEY}`, 'Content-Type': 'application/json', }, body: JSON.stringify({ model: 'celeris-1', messages: [{role: 'user', content: 'ok'}], max_tokens: 256, stream: false, }), }; const started = performance.now(); const response = await fetch(url, init); if (!response.ok) throw new Error(`Celeris request failed: ${response.status}`); const timeToHeadersMs = performance.now() - started; const serverTiming = response.headers.get('server-timing') ?? ''; const match = serverTiming.match(/processing;dur=([\d.]+)/); const processingMs = match ? Number(match[1]) : undefined; const networkMs = processingMs == null ? undefined : Math.max(0, timeToHeadersMs - processingMs); console.log({serverTiming, timeToHeadersMs, processingMs, networkMs}); ``` ## Getting the numbers down - **Deploy close to the region.** Typical short requests complete in roughly 50–150 ms within the region. Network distance adds to end-to-end latency. See [Models § Regions](/models#regions). - **Reuse connections.** Use an HTTP client with connection pooling and keep-alive enabled. OpenAI SDK clients do this by default. Celeris keeps an idle connection open for 300 seconds; a client that closes sooner pays a fresh TCP and TLS handshake on the next call. Node's built-in `fetch` (undici) closes idle sockets after 4 seconds unless the server advertises a longer `Keep-Alive` timeout or you set `keepAliveTimeout`. - **Cap `max_tokens`.** Completion length is the main thing you control that moves inference time. Any positive integer that fits the context window is accepted. Small budgets do not measurably speed up a short response, so size `max_tokens` to cap a long one rather than as a fine-grained speed control. - **Watch your `429` rate.** Throttling and the retries it forces add latency that your own timings will show. See [Rate limits](/rate-limits#backing-off-well). --- # Going to production # Going to production Use this checklist before sending production traffic. Each section links to detailed guidance elsewhere in the documentation. ## Keys and secrets - **One key per app and environment** (`prod-backend`, `staging`, `cli-tools`), and [private keys over workspace-shared ones](/authentication#keys) when usage should remain attributable to a person. There are no per-key rate limits. - **Load keys from a secret manager or environment variable.** Never store them in source control. Add the `ck_` prefix to secret-scanning rules. - **Document a rotation procedure.** Key changes can take up to [one minute](/authentication#rotating-and-revoking) to take effect. Rotate shared credentials when access changes and immediately after suspected exposure. - **Do not include a key in public client-side code.** Browser calls are [supported](/making-requests#calling-from-the-browser), but public JavaScript cannot keep a credential secret. Proxy requests through your backend. ## Retries and timeouts - **Retry `429`, `502`, and `503`** with exponential backoff and jitter, honoring `Retry-After` as the floor. Code in [Rate limits](/rate-limits#backing-off-well). - **Never retry `400`, `401`, `404`, or `413` unchanged**. They fail identically every time. See [Errors](/errors#retries). - **Treat `402` as terminal and alert on it.** An exhausted balance stops every key in the workspace; a retry loop [cannot fix it](/errors#402). - **Set client timeouts based on your workload and latency objective.** Treat a timeout as transient and retry with backoff. - **Retry any `5xx` by status, not by body.** Some `5xx` responses may not include a JSON body, so do not require `error.code` before retrying. See [Errors](/errors#error-envelope). ## Concurrency and throughput - **Use a shared client-side concurrency limit** across workers. See [Rate limits](/rate-limits#backing-off-well). - **Remember rate limits are shared by workspace and model.** All keys share the selected model's rate ceiling. Each model has its own ceiling, while every model draws from one workspace credit balance. Set separate concurrency budgets for batch and user-facing traffic. - **Reuse connections.** Use connection pooling and keep-alive. OpenAI SDK clients enable these by default. See [Measuring latency](/measuring-latency#getting-the-numbers-down). ## Observability - **Send `X-Client-Trace-Id` on every request.** Celeris echoes it in API responses so you can correlate failures with application logs and support cases. See the [API reference](/api-reference#request-headers). - **Log or sample wall-clock duration and the `Server-Timing` header.** For complete responses, compare them to estimate client/network overhead. For streams, measure time to first token separately because `Server-Timing` ends at the earlier response-header boundary. See [Measuring latency](/measuring-latency). - **Track your client-side `429` rate.** Throttled requests are not counted on the usage page, so track them in your application logs and metrics. See [Rate limits](/rate-limits#watching-your-usage). ## Billing readiness - **Monitor spend.** The [usage page](/console#usage) shows completed request counts and token volumes over 24 hours, 7 days, or 30 days. Project spend from those volumes with the [pricing rates](/pricing#rates). - **Maintain sufficient credit.** When the balance is exhausted, all keys in the workspace return [`402`](/errors#402). Alert on this response. - **Set `max_tokens` on every request.** This bounds completion latency and cost. Any positive integer that fits the 131,072-token context window is accepted. See [Prompt engineering](/prompting). ## Deployment geometry - **Deploy callers close to their users.** Celeris aims to serve requests as close as possible to customers. Typical short requests complete in roughly 50–150 ms within the serving region, and network distance adds to end-to-end latency. - **Measure complete responses with `Server-Timing`.** Compare it with wall-clock latency to estimate client/network overhead. For streams, also measure time to first token and do not treat the header as model time. See [Measuring latency](/measuring-latency). ## The checklist - [ ] Separate API keys per app and environment, loaded from a secret store - [ ] Rotation procedure written down; keys rotate on departures/exposure - [ ] No keys in public client-side code - [ ] Backoff with jitter on `429`/`502`/`503`, honoring `Retry-After` - [ ] `400`/`401`/`404`/`413` fail fast, no blind retries - [ ] `402` alerts a human; nothing retry-loops on it - [ ] Client timeouts set and timeout retries bounded - [ ] Shared concurrency cap sized under your workspace's ceiling - [ ] `X-Client-Trace-Id` sent on every request and logged - [ ] Client-side `429` rate tracked - [ ] `max_tokens` set deliberately on every call (sized for the expected response); `temperature: 0` on structured calls - [ ] Callers deployed near their users; keep-alive connections - [ ] Credit balance monitored; top-up owner named --- # Pricing # Pricing Celeris uses prepaid, per-token billing. Buy credits under [**Settings → Billing**](https://console.celeris.ai/settings/billing) in the [Console](/console#billing); completed requests draw from the workspace's balance according to token usage. There are no subscriptions, seat charges, or minimum commitments. ## Rates Each model is priced separately, per million tokens: | Model | Input tokens | Cached input tokens | Output tokens | | --- | --- | --- | --- | | [`celeris-1`](/models#celeris-1) | US$0.20 per million | US$0.02 per million | US$0.70 per million | | [`celeris-1-magnus`](/models#celeris-1-magnus) | US$0.20 per million | US$0.02 per million | US$0.70 per million | | [`celeris-1-decision`](/models#celeris-1-decision) | US$0.04 per million | US$0.04 per million | Free | `celeris-1-decision` reports its tokens as `input_tokens` and `output_tokens`; see [Decisions](/decisions#usage-and-cost). For chat completions and the Responses API, see [Token usage](/api-reference#token-usage) for how reported usage determines billing. ## Buying credits Buy credits with a card (Stripe) under [**Settings → Billing**](https://console.celeris.ai/settings/billing), in preset amounts of $5 / $10 / $50 / $100 or a custom amount between $5 and $1,000 per purchase. Receipts are emailed automatically, and invoice documents remain available on the same billing page. When your balance reaches zero, API calls return [`402 insufficient_quota`](/errors#402) until you add credit. See [Going to production](/going-to-production#billing-readiness) for production monitoring guidance. See [Auto Top-Up](/console#auto-top-up) for automatic purchases and the balance-email schedule. ## Credit expiry Credit granted to your workspace expires **30 days** after it is granted. Usage draws from the credit that expires first. Expired credit leaves your balance and cannot be restored; from a zero balance, API calls return [`402 insufficient_quota`](/errors#402) until you add credit. [**Settings → Billing**](https://console.celeris.ai/settings/billing) shows the next date on which some of your credit expires. We also email you about it: once two weeks ahead if you have not spent any of the credit yet, once three business days before the date, and once when it expires. Turn these off under **Credit balance and expiry** in [**Settings → Notifications**](https://console.celeris.ai/settings/notifications). For reserved capacity, custom limits, or invoicing beyond self-service top-ups, [contact us](mailto:support@celeris.ai). --- # Security and data handling # Security and data handling This page summarizes API-key handling, workspace isolation, and usage data. ## API authentication and isolation - Inference requests use a `ck_…` bearer API key over HTTPS. The inference API does not use browser cookies for authorization. - Keys belong to a workspace. Usage, prepaid credit, and request history are attributed to that workspace; organization membership controls which workspaces a Console user can reach. - API keys are encrypted at rest and access is restricted to the services that validate or display them. - Key changes can take up to one minute to take effect. A rotated or revoked key may continue to work during that period. See [Authentication](/authentication) for operational key guidance. ## Inference and usage records Celeris processes request bodies to generate responses. Usage records include token counts, request outcomes, consumed credit, model, and region. These records are retained for 90 days. Successful responses are billed from their reported token counts. Requests that continue processing after the client disconnects may still be charged. See [Pricing](/pricing#rates) and [Making requests](/making-requests#streaming). ## Browser use The inference API permits cross-origin bearer requests for browser-based applications. CORS does not make an embedded key private: anyone who can load a public application's JavaScript can recover a bundled key. For public web applications, keep the Celeris key on your server and proxy the request through your own authenticated backend. --- # Known issues # Known issues This page records known behavior that may affect integrations. Each issue includes examples and a mitigation you can apply while handling model output. ## JSON output may include a Markdown code fence Diffusion generation can occasionally wrap an otherwise valid JSON object in a Markdown `json` code fence. In some responses, the three opening backticks are missing but the `json` label and closing backticks remain. ### Examples With the opening fence: > ```` ```json ````
> `{"name":"Priya","intent":"trial"}`
> ```` ``` ```` With the opening backticks missing: > `json`
> `{"name":"Priya","intent":"trial"}`
> ```` ``` ```` ### Mitigation Request JSON with [`response_format`](/making-requests#json-mode) on a non-streaming `/chat/completions` request. That path returns a value ready to parse and does not produce a fence, so prefer it whenever it is available. The stripping below is for the cases where it is not: streaming requests, the Responses API, and requests that ask for JSON in the prompt without `response_format`. Before parsing a response that should be JSON, remove the wrapper only when the entire output has one of the two prefixes above **and** a closing fence on its own final line. Then parse and validate the result against your expected schema. Do not use a generic character trim: that can alter legitimate JSON or string values. **cURL / shell** ```bash content=$(jq -r '.choices[0].message.content' response.json) cleaned=$( printf '%s' "$content" | perl -0pe 's/\A(?:```json|json)[ \t]*\r?\n(.*)\r?\n```[ \t]*(?:\r?\n)?\z/$1/s' ) printf '%s\n' "$cleaned" | jq . ``` **Python** ```python from json import loads from re import compile FENCE_SHAPED = compile( r"\A(?:```json|json)[ \t]*\r?\n(?P[\s\S]*?)\r?\n```[ \t]*(?:\r?\n)?\Z" ) def strip_json_fence(content: str) -> str: match = FENCE_SHAPED.fullmatch(content) return match.group("body") if match else content value = loads(strip_json_fence(content)) ``` **JavaScript** ```javascript const fenceShaped = /^(?:```json|json)[ \t]*\r?\n([\s\S]*?)\r?\n```[ \t]*(?:\r?\n)?$/; function stripJsonFence(content) { const match = content.match(fenceShaped); return match ? match[1] : content; } const value = JSON.parse(stripJsonFence(content)); ``` If parsing or schema validation still fails, follow the bounded retry and fallback guidance in [Prompt engineering](/prompting#validate-the-output-and-retry-with-feedback). ## Reasoning is not separated on every request [Reasoning](/making-requests#reasoning) (`enable_thinking: true`) is not separated from the answer on every request. Where it is not separated, the request still succeeds and never errors, but the model's working comes back **inline in `content`** with protocol markers (a leading `thought` line, or a `<|channel>thought…` scaffold) instead of in a separate `reasoning` field. ### Mitigation Leave `include_reasoning` at its default and read the working from `message.reasoning` (or a `type: "reasoning"` output item on the Responses API) when it is present and non-empty. When it is not, `content` carries the working, its protocol markers, and the answer together. There is no separate answer to read from that reply. (Under `include_reasoning: false` an empty `reasoning` is the documented shape, not this issue: check `content` for the markers above instead.) Whether the working is separated depends on the server that handled the request and can stay the same for long stretches, so bound any retry to one or two attempts and then send the request with thinking off, rather than parsing the markers out of `content`. ## Reasoning after a tool result On a turn whose last message is a tool result, with thinking on, a reply that would otherwise carry no answer text returns that text **in the answer slot instead of nothing**: on chat completions in `message.content` (with `message.reasoning` `null` on a non-streamed reply), and on the Responses API as the `type: "message"` item with no `type: "reasoning"` item. While streaming that turn on chat completions, the same text is *also* emitted as `delta.reasoning` chunks before it lands in the answer slot, so a client that concatenates both channels renders it twice. This only happens when the working is returned; `include_reasoning: false` emits it once, as content. The Responses API has no suppression, so its stream also carries both copies: as reasoning events and again in the `message` item's text. ### Mitigation On a tool-result turn, de-duplicate. On chat completions: if the accumulated `delta.reasoning` equals the answer text, drop the reasoning copy and keep `content`. On the Responses API: keep the `message` item and drop the reasoning events that repeat it. Also handle an empty `content` on such a turn rather than relying on this recovery. See [Reasoning](/making-requests#reasoning). **Python** ```python reasoning, answer = "", "" for chunk in stream: if not chunk.choices: # the include_usage final chunk carries no choices continue delta = chunk.choices[0].delta reasoning += getattr(delta, "reasoning", None) or "" answer += delta.content or "" if reasoning.strip() == answer.strip(): reasoning = "" # the working was replayed as the answer; render content only ``` **JavaScript** ```javascript let reasoning = ""; let answer = ""; for await (const chunk of stream) { const delta = chunk.choices[0]?.delta; if (!delta) continue; // the include_usage final chunk carries no choices reasoning += delta.reasoning ?? ""; answer += delta.content ?? ""; } if (reasoning.trim() === answer.trim()) { reasoning = ""; // the working was replayed as the answer; render content only } ``` --- # Glossary # Glossary Terms used in the API, Console, and documentation. ### Autoregressive model An LLM architecture that generates output one token at a time. Contrast with [diffusion LLM](#diffusion-llm). ### Base URL The per-model API root: `https://inference.celeris.ai//v1`. The model is part of the path, ahead of the standard `/v1` suffix. See [Model routing](/models#model-routing-and-base-urls). ### `celeris-1` Celeris' general purpose diffusion language model. See [Models](/models#celeris-1). ### Chat completion The request/response shape of the primary endpoint, [`POST /chat/completions`](/api-reference#post-chatcompletions): a `messages` array in, a `choices` array out, in the standard OpenAI wire format. ### Cached input token An input token served from cache and billed at the [cached input token rate](/pricing#rates). Chat and text completions report the cached subset at `usage.prompt_tokens_details.cached_tokens`; the Responses API uses `usage.input_tokens_details.cached_tokens`. ### Context window The hard ceiling on prompt tokens plus requested maximum output: **131,072** tokens for `celeris-1`. Oversized requests are rejected with a [`400`](/errors#400), never silently truncated. See [Models](/models). ### Credit The prepaid unit of billing. Usage draws down the workspace's credit balance at [per-token rates](/pricing); an exhausted balance returns [`402`](/errors#402) until topped up. ### Diffusion LLM A language model that refines multiple output tokens in parallel rather than generating them strictly one at a time. See [Models](/models). ### Error envelope The JSON body returned for Celeris API errors: `{"error": {message, type, code}}`. Branch handling on the stable `code` field. See [Errors](/errors#error-envelope). ### Key (API key) The `ck_`-prefixed bearer credential sent as `Authorization: Bearer ck_...`. Created, revealed, rotated, and revoked in the Console. A key belongs to a workspace and is owned by either a user or the workspace itself. See [Authentication](/authentication). ### Organization The company context that owns one or more [workspaces](#workspace). See [Companies and workspaces](/organizations) for switching, access, roles, and settings. ### Playground The Console tool for testing prompts against the live model without writing client code. See [Console](/console#playground). ### Rate limit The sustained request-rate limit shared by one workspace's calls to one model. Each model has an independent limit. Exceeding it returns [`429`](/errors#429) with a `Retry-After` header. See [Rate limits](/rate-limits). ### Reasoning (thinking) The model's step of working through a problem before answering, turned on or off per request with `chat_template_kwargs.enable_thinking`. When separated, the working comes back apart from the answer, on `message.reasoning` for [chat completions](#chat-completion) and as a `type: "reasoning"` output item on the Responses API. Its tokens are billed as completion tokens. Celeris-1 Magnus reasons by default and Celeris-1 does not. It is not separated on every request, so detect it per response. See [Reasoning](/making-requests#reasoning) and [Reasoning is not separated on every request](/known-issues#reasoning-is-not-separated-on-every-request). ### Region The cloud region that serves a request. Celeris aims to serve requests as close as possible to customers. Network distance to the serving region affects end-to-end latency. ### `Retry-After` The response header on every `429`: the number of seconds to wait before retrying. Honor it as the floor under your [backoff](/rate-limits#backing-off-well). ### `Server-Timing` The response header on every successful request that reports service `processing` time until the response headers are ready. For streaming, that boundary precedes the first token. See [Measuring latency](/measuring-latency). ### `service_busy` One of the two [`429` codes](/errors#429). The service is temporarily at capacity; retry after `Retry-After`. ### Commercial access Organization-level access for higher limits. It is arranged through [support](mailto:support@celeris.ai), rather than requested from the Console. See [Console § Commercial access](/console#commercial-access). ### Token The unit of model input/output and of [billing](/pricing). A response's `usage` block reports input, output, and total token counts. It also reports the [cached input token](#cached-input-token) subset. ### Trace ID (`X-Client-Trace-Id`) An optional caller-supplied request header that Celeris echoes in API responses. Use it to correlate application logs with support cases. See [Support](/support). ### Usage The Console dashboard of completed request counts and token volumes over 24 hours, 7 days, or 30 days. Rejected requests are not included. See [Console § Usage](/console#usage). ### Workspace Where keys, credits, and usage live, inside an [organization](#organization). API keys can be created at any time, but a workspace must be **activated** (by adding a payment method) and hold credit before its keys can serve requests. Credits and usage are shared across the workspace. API keys can be private to their owner or shared with the workspace. See [Companies and workspaces](/organizations). --- # Troubleshooting & support # Troubleshooting & support Start with the HTTP status and `error.code`. For latency issues, record the wall-clock duration and `Server-Timing` header. Send an `X-Client-Trace-Id` with each request so you can correlate application logs with a support case. ## Symptom → likely cause | Symptom | Likely cause | Go to | | --- | --- | --- | | Can't create an API key | Workspace not yet activated | [Console](/console#organizations-and-workspaces) | | `401` on a new key | Key changes can take up to one minute | [Authentication](/authentication#rotating-and-revoking) | | `401` that persists | Missing/typo'd header, or a revoked key still deployed | [Errors § 401](/errors#401) | | `402` | Credit balance exhausted; add credit before retrying | [Errors § 402](/errors#402) | | `404` on a correct-looking URL | Typo in the model path segment | [Model routing](/models#model-routing-and-base-urls) | | `400` mentioning tokens | `max_tokens` zero or negative; or prompt tokens + `max_tokens` over the 131,072 context window | [Models](/models) | | Scattered `429 rate_limit_exceeded` | Over your workspace's rate ceiling for the requested model | [Rate limits](/rate-limits) | | `429 service_busy` | Service temporarily at capacity; honor `Retry-After` and retry | [Errors § 429](/errors#429) | | `502`/`503`, which may have no JSON body | Temporary service error; retry any `5xx` by status | [Errors § retries](/errors#retries) | | Slow complete response, `processing` small relative to wall clock | Client, network, or request-path overhead; check region distance and connection reuse | [Measuring latency](/measuring-latency) | | Slow complete response, `processing` close to wall clock | Server-side processing or generation | [Measuring latency](/measuring-latency) | | Slow first token on a streamed response | Network, queueing, or prompt processing; `processing` alone cannot separate them | [Measuring latency](/measuring-latency) | | Verification/reset email missing | Delivery delay or spam filter | [Account](/account#didnt-get-an-email) | | Usage dashboard does not show rejected requests | Rejected requests are not included in usage totals | [Rate limits](/rate-limits#watching-your-usage) | ## Isolate the source Use these checks to isolate the problem: 1. **Record wall clock and `Server-Timing`** on a slow request. For a complete response, compare the two to estimate client/network overhead. For a streamed response, also record time to first token; do not subtract `processing` from it because stream headers arrive before prompt processing finishes. See [Measuring latency](/measuring-latency). 2. **Curl the endpoint directly** (the [Quickstart](/quickstart) request, verbatim). If curl succeeds while your app fails, the difference is in your app's headers, body, or networking. Compare them field by field. The [Playground](/console#playground) calls the same API from the browser and can help separate API-key or prompt issues from client-code issues. ## Contacting support Write to **[support@celeris.ai](mailto:support@celeris.ai)** for API, account, sign-in, or billing questions. Include the following information: - **Trace IDs** of failing requests. Send [`X-Client-Trace-Id`](/api-reference#request-headers) on every call and log the echoed value so support can locate the affected requests. - **Timestamps in UTC**, with the serving region and model shown in the console. - **The HTTP status and `error.code`** (or the raw error body), and whether a retry succeeded. - **`Server-Timing` values** and wall-clock duration for latency reports. - **What changed**, if anything: new deploy, new key, traffic ramp. Do not include an API key. If you suspect a key is exposed, [rotate it](/authentication#rotating-and-revoking) first, then write to us. Use the same address for billing questions, reserved capacity, or custom terms. Self-service options live under [**Settings → Billing**](https://console.celeris.ai/settings/billing); see [Console & billing](/console#billing). --- # API reference # API reference Celeris implements the OpenAI chat-completions wire format. For usage examples and parameter guidance, see [Making requests](/making-requests). ## Base URL ```text https://inference.celeris.ai//v1 ``` | Placeholder | Values today | | --- | --- | | `model` | `celeris-1` | The `model` request-body field must match the `` path segment. ## Authentication All endpoints require a bearer key ([details](/authentication)): ```http Authorization: Bearer ck_... ``` ## POST /chat/completions Create a chat completion. Responses can return as one JSON body or as an SSE stream ([details](/making-requests#streaming)). ### Request body | Field | Type | Required | Notes | | --- | --- | --- | --- | | `model` | string | yes | Must equal the model in the URL path, e.g. `celeris-1`. | | `messages` | array | yes | OpenAI message objects (`role` ∈ `system` \| `user` \| `assistant` \| `tool`). `content` is a string, or an array of content parts: `{"type": "text", "text": ...}` and `{"type": "image_url", "image_url": {"url": "data:;base64,..."}}`. Attach as many `image_url` parts as you need, using inline `data:` URLs only; see [Image input](/images). Prompt tokens plus the requested output budget must be at most 131,072. | | `max_tokens` | integer | recommended | Requested completion-token budget. **Any positive integer**, and prompt tokens plus the requested budget must be at most 131,072. Defaults to `2048` when neither output-budget field is supplied. A zero or negative value, or a value that would exceed the context window, is rejected. Size it for the expected response; see [parameter guidance](/making-requests#parameters-that-matter-most-here) and [output-budget exceptions](/making-requests#output-budgets). | | `max_completion_tokens` | integer | no | Alternative to `max_tokens`, with the same validation and [output-budget behavior](/making-requests#output-budgets). When both fields are supplied, `max_completion_tokens` sets the budget; send only one. | | `temperature` | number | no | `0` recommended for structured tasks. | | `top_p` | number | no | Standard nucleus sampling. | | `seed` | integer | no | Reproducible sampling when combined with `temperature: 0`. | | `stop` | string \| array | no | Stop sequences. | | `n` | integer | no | Number of choices. | | `tools` | array | no | Function/tool definitions in the OpenAI `tools` schema. When the model calls one, the reply's `message.tool_calls` carries the function name and JSON `arguments`, and `finish_reason` is `tool_calls`. Send the result back as a `tool` message to continue. | | `tool_choice` | string \| object | no | `auto` (the default, where the model decides), `none` (never call), `required` (directs the model to call one of `tools`), or `{"type": "function", "function": {"name": "..."}}` to direct a specific function. `required` and named forcing apply to non-streaming requests; confirm the reply carries `tool_calls`. See [Tool calling](/making-requests#tool-calling). | | `response_format` | object | no | Constrain non-streaming `/chat/completions` output to JSON: `{"type": "json_object"}`, or `{"type": "json_schema", "json_schema": {"name": "...", "schema": {...}}}` to validate against a schema. See [JSON mode](/making-requests#json-mode). | | `chat_template_kwargs` | object | no | Template controls. `{"enable_thinking": true}` makes the model reason before answering and `false` makes it answer directly. The default depends on the model. Where the reasoning is [separated](/known-issues#reasoning-is-not-separated-on-every-request) it is returned on `message.reasoning`, apart from `message.content`. See [Reasoning](/making-requests#reasoning). | | `reasoning_effort` | string | no | Top-level reasoning control. Values and behavior depend on the model; see [Reasoning](/making-requests#reasoning). | | `include_reasoning` | boolean | no | Chat-only (the Responses API has no equivalent). Defaults to `true`; set `false` and the `reasoning` field comes back `null` instead of carrying the working. It does not suppress a working that was [not separated](/known-issues#reasoning-is-not-separated-on-every-request) for that request. See [Reasoning](/making-requests#reasoning) for the response shape and the post-tool-result exception. | | `presence_penalty`, `frequency_penalty` | number | no | Standard penalties. | | `stream` | boolean | no | Set `true` for SSE chunks. | | `stream_options.include_usage` | boolean | no | With streaming, set `true` to receive the final exact-usage block. | When an output budget is explicitly supplied, the effective input limit is `131,072` minus the requested output budget. Requests over the token limit return [`400`](/errors#400); request bodies over the endpoint's 64 MiB byte limit return [`413`](/errors#413). ### Response body ```json { "id": "chatcmpl-9f2e41c0", "object": "chat.completion", "created": 1751673600, "model": "celeris-1", "choices": [ { "index": 0, "message": {"role": "assistant", "content": "Positive"}, "finish_reason": "stop" } ], "usage": { "prompt_tokens": 1024, "completion_tokens": 32, "total_tokens": 1056, "prompt_tokens_details": {"cached_tokens": 768} } } ``` `finish_reason` is `stop` (natural completion or stop sequence) or `length` (generation reached its output budget). When the request enables thinking (`chat_template_kwargs: {"enable_thinking": true}`), `message` may also carry a `reasoning` string alongside `content`; while streaming it arrives as `delta.reasoning` chunks ahead of `delta.content`. Reasoning tokens count as completion tokens in `usage`. See [Reasoning](/making-requests#reasoning) for when it is present and how to detect it. ### Token usage `usage` reports total input and output token processing for the request. Input processed more than once is counted each time, so reported input usage can exceed the submitted prompt's token count. Output usage includes all output tokens processed for the request, including tokens not returned in the answer. Chat completions report these totals as `prompt_tokens` and `completion_tokens`; the Responses API uses `input_tokens` and `output_tokens`. Billing uses the reported usage totals. `prompt_tokens_details.cached_tokens` is the cached subset of `prompt_tokens`, charged at the cached-input rate. Uncached input is `prompt_tokens - prompt_tokens_details.cached_tokens`. Apply the [token rates](/pricing#rates) to uncached input, cached input, and output tokens. The legacy text-completions endpoint, `POST /completions`, uses the same `usage` shape. Its `prompt_tokens_details.cached_tokens` value is likewise part of `prompt_tokens` and uses the [cached input token rate](/pricing#rates). ## POST /v1/responses Create a model response with the OpenAI Responses API, an alternative to `/chat/completions` that takes a single `input` instead of a `messages` array. It returns one JSON body or an SSE stream ([details](/making-requests#streaming)) and reports token usage. Each request is self-contained: send the full `input` on every call. ### Request body | Field | Type | Required | Notes | | --- | --- | --- | --- | | `model` | string | yes | Must equal the model in the URL path, e.g. `celeris-1`. | | `input` | string \| array | yes | A prompt string, or an array of OpenAI input messages (`role` ∈ `system` \| `user` \| `assistant`). A message's `content` is a string, or an array of input parts: `{"type": "input_text", "text": ...}` and `{"type": "input_image", "image_url": "data:;base64,...", "detail": "auto"}`. The chat `text` / `image_url` spellings are rejected here, and `detail` is required; attach as many `input_image` parts as you need. See [Image input](/images#sending-images-on-the-responses-api). | | `instructions` | string | no | System-level guidance applied ahead of `input`. | | `max_output_tokens` | integer | no | Requested output-token budget; any positive integer, bounded by the 131,072-token context window, with the same rejection rules as `max_tokens` above, but **not** its `2048` default. When omitted, Responses uses the context window remaining after the rendered input; set it explicitly for the expected response size. See [output budgets](/making-requests#output-budgets) for exceptions. | | `temperature` | number | no | `0` recommended for structured tasks. | | `top_p` | number | no | Standard nucleus sampling. | | `chat_template_kwargs` | object | no | `{"enable_thinking": true}` makes the model reason before answering and `false` makes it answer directly. The default depends on the model. Where the reasoning is [separated](/known-issues#reasoning-is-not-separated-on-every-request) it is returned as an `output` item of `type: "reasoning"`, ahead of the `message` item. See [Reasoning](/making-requests#reasoning-on-the-responses-api). | | `stream` | boolean | no | Set `true` for Responses SSE events. | ### Response body ```json { "id": "resp_9f2e41c0", "object": "response", "created_at": 1751673600, "status": "completed", "model": "celeris-1", "output": [ { "id": "msg_1a2b3c4d", "type": "message", "status": "completed", "role": "assistant", "content": [{"type": "output_text", "text": "Positive", "annotations": []}] } ], "usage": { "input_tokens": 1024, "output_tokens": 32, "total_tokens": 1056, "input_tokens_details": {"cached_tokens": 768} } } ``` `input_tokens_details.cached_tokens` is the cached subset of `input_tokens`. Input, cached input, and output use the same [token rates](/pricing#rates) as chat and text completions. See [Token usage](#token-usage) for how reported usage determines billing. The generated text is at the `type: "message"` item's `content[0].text` (the OpenAI SDK also exposes it as `response.output_text`). When thinking is enabled and the reasoning is [separated](/known-issues#reasoning-is-not-separated-on-every-request), an `output` item of `type: "reasoning"` precedes that message item; take the answer from the `message` item, never the reasoning. See [Reasoning on the Responses API](/making-requests#reasoning-on-the-responses-api) for that item's shape and how to detect it. When streaming, the event sequence ends with a `response.completed` event. ## POST /v1/decisions Ask `celeris-1-decision` predicate, choice, and score questions using the OpenAI Decisions API format. Send `input` and an ordered `questions` list; the response returns `answers` in the same order. This endpoint exists only under the `celeris-1-decision` base URL. See [OpenAI Decisions API](/decisions#openai-decisions-api) for cURL and OpenAI SDK examples. ## POST /v1/systemone Ask `celeris-1-decision` typed questions and get probabilities back. This endpoint exists only under the `celeris-1-decision` base URL. See [System One API](/decisions#system-one-api) for cURL and Jev SDK examples. ## GET /v1/models Lists the model served at this base URL, in the standard OpenAI list shape: ```bash curl https://inference.celeris.ai/celeris-1/v1/models \ -H "Authorization: Bearer $CELERIS_API_KEY" ``` Retrieve a single model by id with `GET /v1/models/celeris-1` to get that model's metadata in the standard OpenAI object shape: ```bash curl https://inference.celeris.ai/celeris-1/v1/models/celeris-1 \ -H "Authorization: Bearer $CELERIS_API_KEY" ``` ## Request headers | Header | Meaning | | --- | --- | | `Authorization` | Required on every call: `Bearer ck_...` ([details](/authentication)). | | `Content-Type` | `application/json` on `POST`. | | `X-Client-Trace-Id` | Optional caller-supplied trace ID. Echoed on responses so you can correlate failures with application logs and support cases. | ## Response headers | Header | Meaning | | --- | --- | | `Server-Timing` | Service `processing` duration until the response headers are ready, in milliseconds. For streaming this precedes the first token. See [Measuring latency](/measuring-latency). | | `X-Client-Trace-Id` | Your request's trace id, echoed back if you sent one. | | `Access-Control-Allow-Origin: *` | Browser calls are supported from any origin ([details](/making-requests#calling-from-the-browser)). | | `Access-Control-Expose-Headers` | Lets browser code read `Server-Timing` and the trace-id headers cross-origin. | | `Retry-After` | On `429`: seconds to wait before retrying. | ## Errors See [Errors](/errors) for causes, response bodies, and handling guidance. API errors use `{"error": {message, type, code}}`; use `error.code` when a status has more than one possible cause. | Status | `error.code` | Reference | | --- | --- | --- | | 400 | Varies by validation error | [`400 Bad Request`](/errors#400) | | 401 | `invalid_api_key` | [`401 Unauthorized`](/errors#401) | | 402 | `insufficient_quota` | [`402 Payment Required`](/errors#402) | | 404 | `not_found` | [`404 Not Found`](/errors#404) | | 413 | `payload_too_large` | [`413 Payload Too Large`](/errors#413) | | 422 | None | [`422 Unprocessable Entity`](/errors#422) | | 429 | `rate_limit_exceeded` / `service_busy` | [`429 Too Many Requests`](/errors#429) | | 502 | `upstream_error` | [`502 Bad Gateway`](/errors#502) | | 503 | `service_unavailable` | [`503 Service Unavailable`](/errors#503) |