Chat API
POST /chat/completions — the request body, and the exact Server-Sent Events contract, which is buffered, single-event and not OpenAI-shaped.
- Version: 0.4
- Role: admin_user, normal_user
- Type: reference
POST /chat/completions runs a prompt through the input filters, calls the provider, runs the
reply through the output filters, and returns the result as a Server-Sent Events (SSE) stream.
Warning
This stream is not OpenAI-shaped and not Anthropic-shaped. There are no choices, no delta,
no content_block_delta and no incremental text: CID222 buffers the provider's tokens so it can
filter the whole reply, and emits the finished answer as one event with
"id":"filtered-response". Point an OpenAI SDK at this endpoint and it parses nothing.
Endpoint
| Property | Value |
|---|---|
| Method and path | POST /chat/completions |
| Credential | Gateway API key (cid_key_…) or user JWT |
| Request content type | application/json |
| Response content type | text/event-stream — always |
| Response headers | Cache-Control: no-cache, Connection: keep-alive, X-Accel-Buffering: no |
| Refused for | The auditor role, with 403 and ROLE_NOT_FOR_CHAT |
Request body
| Field | Type | Required | Notes |
|---|---|---|---|
model | string | Yes | A model name from GET /models |
messages | array | Yes | Conversation turns. See Message format |
provider | string | No | openai, azure_openai, anthropic, google or ollama. Needed when two providers register the same model name |
temperature | number | No | 0 to 2 |
max_tokens | number | No | 1 or greater |
top_p | number | No | 0 to 1 |
content_filter | string | No | Input-filter nickname. Falls back to content-safety-v2 |
contexts | string[] | No | Retrieval passages for the background hallucination check |
documents | array | No | Documents to parse, inspect, redact and inject as context |
session_id | string | No | Attach the turn to an existing session for detection records |
include_context_usage | boolean | No | Ask for context-window accounting |
stream | boolean | No | Accepted and ignored. Nothing reads it; the response streams either way |
Validation runs with whitelisting: a field this table does not list is stripped from the body
without an error. A misspelled temperture produces a successful call at the default temperature.
Message format
role is system, user or assistant. content is either a string or an array of content
parts.
A document part takes type from pdf, docx, txt, csv or xlsx. Image and document
content is inspected and redacted before it reaches the model, like text.
Example request
What the client receives — the whole exchange, in order:
The name and email were masked before the provider saw them, but the event that reports that
masking is not forwarded on this endpoint. To see what was detected, open Detection & Filtering
→ All Detections in the dashboard, or send the same text to
POST /api/v1/guardrails/detect, which returns the detections synchronously.
The SSE contract
The provider streams token deltas. CID222 consumes every one of them, accumulates the text, runs
output filtering over the complete reply, and only then writes the answer to your connection. A
client therefore sees: optional control events, then silence for the length of the generation,
then one content event, then data: [DONE].
Each line is data: followed by one JSON object, and blank-line separated. The stream always
ends with data: [DONE].
Events, in the order they can appear
| Order | Event | Shape | Terminal |
|---|---|---|---|
| 1 | User locked | {"type":"user_locked","reason":…,"review_id":…,"locked_at":…,"unlock_request_status":…} | Yes |
| 2 | Security warning | {"type":"security_warning","review_id":…,"message":…} | No |
| 3 | Documents processed | {"type":"documents_processed","count":N,"summaries":[…],"processing_time_ms":N} | No |
| 4 | Model routed | {"type":"model_routed","shadow":bool,"from":…,"to":…,"difficulty":n,"projected_saving_pct":n} | No |
| 5 | Routing evaluated | {"type":"routing_evaluated","difficulty":n,"routed":bool} | No |
| 6 | Token optimised | {"type":"token_optimized","shadow":bool,"applied":bool,"original_tokens":n,"compressed_tokens":n,"tokens_saved":n,"ratio":n,"layers_applied":[…],"layer_savings":{…},"cache_hits":n,"cache_misses":n,"fell_back":bool} | No |
| 7 | Input rejected | {"error":…,"type":"content_rejected","entities":[…]} | Yes |
| 8 | Token usage | {"type":"token_usage","usage":{"prompt_tokens":n,"completion_tokens":n,"total_tokens":n}} | No |
| 9a | Output rejected | {"type":"output_content_rejected","severity":"critical","message":…,"reason":…,"entities_detected":n,"actions_applied":[…]} | Yes |
| 9b | The answer | {"id":"filtered-response","content":…,"finish_reason":"stop","filtered":bool,"entities_masked":n} | No |
| 9c | Output warning | {"type":"output_content_warning","severity":"warning","message":…,"entities_detected":n,"actions_applied":[…]} | No |
| 10 | End of stream | data: [DONE] | Yes |
Events 1 and 2 come from the LLM-review lock gate and appear only when a review has locked the
user or left a pending warning. Events 4 to 6 appear only when model routing or prompt compression
is enabled. Event 3 appears only when the request carried documents. Exactly one of 9a and 9b
occurs; 9c can follow 9b.
Note the ordering of events 8 and 9: token_usage is forwarded while the reply is still being
buffered, so it arrives before the answer, not after it.
Reading it
The one event that carries model text is identified by "id":"filtered-response", not by a
type. Match on that:
Do not accumulate content across events. There is only one.
token_usage variants
usage always carries prompt_tokens, completion_tokens and total_tokens. Prompt-cache
figures are added by the providers that report them, under provider-specific names:
cached_input_tokens on OpenAI, cache_read_input_tokens and cache_creation_input_tokens on
Anthropic. Read them defensively.
Events that do not exist
Do not write a client that waits for any of these — nothing emits them:
content_block_delta,delta.text,message_stop,chatcmpl-…incremental ids, and any OpenAI or Anthropic streaming frame.input_rejected. The input-rejection event iscontent_rejected.hallucination. The retrieval-grounding check runs in the background after the reply has been returned. It writes a detection record and a session warning; it never reaches the stream.
Events that exist but are not forwarded here
user_message_processed, which reports what was masked in the prompt, is emitted internally and swallowed by the orchestrator on/chat/completions. It is forwarded onPOST /sessions/:id/messages.- Every per-token provider chunk, the provider's own
finish_reasonchunk, and the provider's own[DONE].
Blocked content
A policy block is not an HTTP error. The response status is 200 and the block arrives as an event, so a client that checks only the status code sees success.
Blocked input — the provider is never called and no provider tokens are spent:
Blocked output — the model generated a reply, but the output filter refused to return it:
On an unhandled server-side failure the controller writes data: {"error":"An error occurred"}
and closes the stream.
Supported models
model accepts any model your tenant holds an active provider credential for. Call GET /models
for the live list. The seeded catalogue is:
| Provider code | Models |
|---|---|
openai | gpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-3.5-turbo |
azure_openai | gpt-4o, gpt-4o-mini, gpt-4-turbo, gpt-35-turbo |
anthropic | claude-opus-4-6, claude-sonnet-4-6, claude-sonnet-4-5-20250929, claude-haiku-4-5-20251001, claude-opus-4-1-20250805, claude-sonnet-4-20250514, claude-opus-4-20250514 |
google | gemini-2.5-flash, gemini-2.5-pro |
ollama | Two uncensored red-team models, returned only to admin_user, superadmin and viewer |
openai and azure_openai both register gpt-4o, gpt-4o-mini and gpt-4-turbo. Send
provider alongside model to say which one you mean. Azure's GPT-3.5 deployment is
gpt-35-turbo, without the dot.
Errors
| Status | Cause |
|---|---|
| 400 | The body failed validation — a missing model or messages, or a value out of range |
| 401 | Missing, malformed or unrecognised bearer token |
| 402 | LICENSE_EXPIRED — the installed licence is invalid or expired |
| 403 | ROLE_NOT_FOR_CHAT for auditor, READ_ONLY_ROLE for viewer, or FEATURE_NOT_LICENSED |
| 423 | SETUP_REQUIRED — first-boot setup is unfinished |
| 500 | An unhandled gateway failure. On an already-open stream this arrives as an error event instead |
There is no 429 on this endpoint: /chat/completions is not rate limited.
Other chat endpoints
| Method and path | Returns | Description |
|---|---|---|
POST /chat/harden-prompt | JSON | Rewrites a prompt for safety and clarity. Body {"prompt":…}, response {original, hardened, improvements, risk_assessment} |
GET /chat/providers | JSON | {"providers":[{"id":…,"code":…,"name":…,"supported":…}],"total":n} |
Related
- API overview — base paths, credentials and error shapes across surfaces.
- Sessions API — the same pipeline with server-side conversation state.
- Content detection — scan text without calling a provider.
Last updated on