---
title: "Create chat completion"
description: "Create a chat completion (streaming or non-streaming)."
canonical: "https://pirouter.ai/docs/api/chat-completions"
last-updated: "2026-10-13"
lang: "en"
---

# Create chat completion

Create a chat completion (streaming or non-streaming).

`POST https://api.pirouter.ai/v1/chat/completions`

OpenAI-compatible chat completion. With `"stream": true` the response is a Server-Sent Events stream (`text/event-stream`); otherwise a single JSON body. Unknown request fields are forwarded to the upstream provider as-is.

The response headers are the gateway's own, not the upstream's: every response (errors included) carries `X-Pi-Trace-Id` and the CORS headers, and a response the upstream provider actually answered also carries `X-Pi-Provider`, `X-Pi-Model`, `X-Pi-Upstream-Model` and `X-Pi-Upstream-Request-Id`. An error the gateway raised itself (authentication, balance, rate limit, validation, no provider) has no upstream to name and omits those four. Any other header an upstream sent is not forwarded, except `Content-Type`, `Content-Length`, `Content-Encoding`, `Cache-Control` and `Retry-After`. A browser preflight (`OPTIONS`) is answered 204 without authentication.

## Authorization

Send your API key in one of these headers.

| Header | Value | Description |
| --- | --- | --- |
| `Authorization` | `Bearer sk-pi-…` | Platform-issued API key with the `sk-pi-` prefix, sent as a Bearer token. |
| `X-Api-Key` | `sk-pi-…` | The same platform-issued `sk-pi-` key, sent in `X-Api-Key`. Read only when `Authorization` is absent or empty; when both are sent, `Authorization` wins. A bare key without the `Bearer ` prefix is accepted in either header. |

## Body

`application/json`

OpenAI-compatible chat completion request. Fields beyond those listed are forwarded upstream unchanged, EXCEPT the routing fields `provider`, `models` and `route` (consumed by routing and removed before forwarding) and `preset`, which is marked x-pi-reserved (rejected with validation_failed until presets ship).

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `model` | string | Yes | Model slug in "vendor/model" form. |
| `messages` | object[] | Yes |  |
| `messages[].role` | string | Yes | One of: `system`, `user`, `assistant`. |
| `messages[].content` | string | Yes |  |
| `provider` | object | No | Per-request provider routing preferences, applied when several providers serve the requested model. Limits (`only`, `ignore`, `data_collection`, `zdr`, `quantizations`) can only tighten the workspace routing settings; the sort mode (`order` and `sort`) is a default the request replaces as a whole. Provider names are platform ids, matched case-insensitively, plus the aliases `google-vertex` (gcp), `google-ai-studio` (gemini) and `amazon-bedrock` (aws). A known but unimplemented field is refused with `provider.<field> is not supported yet`; any other unknown key with `provider: unrecognized key "<key>"`. |
| `provider.order` | string[] | No | Providers to try first, in this order; providers not listed come after them. |
| `provider.only` | string[] | No | Use only these providers (intersected with the workspace allowlist). |
| `provider.ignore` | string[] | No | Never use these providers (added to the workspace ignore list). Wins over `only`. |
| `provider.allow_fallbacks` | boolean | No | With `false` and an `order`, only the providers in `order` may serve. A workspace `false` cannot be relaxed. Default: `true`. |
| `provider.sort` | string \| object | No | Rank providers within each `order` tier by `input_text + output_text` unit price, lowest first. Without it, deployment weight decides. |
| `provider.sort.by` | string | Yes | One of: `price`. |
| `provider.data_collection` | string | No | `deny` excludes providers that may train on prompts or retain them beyond a limited window (undeclared counts as may). One of: `allow`, `deny`. Default: `"allow"`. |
| `provider.zdr` | boolean | No | Only zero-data-retention providers. Default: `false`. |
| `provider.quantizations` | string[] | No | Only deployments at one of these precisions. One of: `fp4`, `int4`, `int8`, `fp8`, `fp16`, `bf16`, `fp32`, `unknown`. |
| `models` | string[] · max 3 | No | Models to fall back to, tried after `model` in array order when every deployment of the current model fails before its first byte (see Runtime fallback). Entries repeating `model` or an earlier entry are dropped; with no `model` the first entry is the primary. Every entry must exist (404 model_not_found naming it); more than 3 is validation_failed. Not forwarded upstream. |
| `preset` | string | No | (reserved) RESERVED — preset reference (also expressible as model "@preset/{slug}"). Rejected with validation_failed (`preset is not supported yet`). |
| `route` | string | No | Routing variant. Only `fallback` is accepted, the same as omitting the field; any other value is validation_failed. Not forwarded upstream. One of: `fallback`. |
| `stream` | boolean | No | When true, respond as a Server-Sent Events stream. Default: `false`. |
| `stream_options` | object | No | Streaming-only options; ignored when stream is false. |
| `stream_options.include_usage` | boolean | No | When true, the final data frame carries the usage block. |
| `max_tokens` | integer | No | Upper bound on generated tokens. |
| `temperature` | number | No | Sampling temperature; passed through to the upstream provider. |
| `top_p` | number | No | Nucleus sampling parameter; passed through to the upstream provider. |

## Response

`200` `application/json`, `text/event-stream`

Completion result. Non-streaming requests get a single `ChatCompletion` JSON body. Streaming requests get an SSE stream: each event is a `data:` line carrying one `ChatCompletionChunk` as JSON, events are separated by blank lines, and the stream terminates with `data: [DONE]`. When `stream_options.include_usage` is true, the final data frame before `[DONE]` carries the `usage` block.

| Field | Type | Required | Description |
| --- | --- | --- | --- |
| `id` | string | Yes | The upstream provider's own response id, passed on unchanged. The platform's request id is the `X-Pi-Trace-Id` response header. |
| `object` | string | Yes | One of: `chat.completion`. |
| `created` | integer | Yes | Unix timestamp (seconds). |
| `model` | string | Yes | The platform slug of the model that served the request (for example `openai/gpt-6-sol`). After a fallback it is the model that answered, which may differ from the one the request named. The id the upstream itself reported is in the `X-Pi-Upstream-Model` response header. |
| `choices` | object[] | Yes |  |
| `choices[].index` | integer | Yes |  |
| `choices[].message` | object | Yes |  |
| `choices[].message.role` | string | Yes | One of: `assistant`. |
| `choices[].message.content` | string \| null | No |  |
| `choices[].finish_reason` | string | Yes | Upstream finish reason, passed through (e.g. stop, length). |
| `usage` | object | No | Token accounting for one completion (OpenAI-standard triple; extra fields may appear later). |
| `usage.prompt_tokens` | integer | Yes |  |
| `usage.completion_tokens` | integer | Yes |  |
| `usage.total_tokens` | integer | Yes |  |

## Errors

| Status | Code | Description |
| --- | --- | --- |
| `400` | `invalid_request` | The request is missing a required field (`invalid_request`): a body with neither `model` nor `models` answers `model is required`. |
| `401` | `invalid_api_key`, `api_key_not_found`, `api_key_revoked`, `api_key_expired` | API key is missing or malformed (`invalid_api_key`), unknown (`api_key_not_found`), revoked (`api_key_revoked`), or expired (`api_key_expired`). |
| `402` | `insufficient_balance` | Wallet balance cannot cover the requested call (`insufficient_balance`). |
| `404` | `model_not_found`, `no_eligible_provider` | Requested model slug is unknown or not enabled (`model_not_found`), or the request's `provider` preferences, merged with the workspace routing settings, leave no provider that serves it (`no_eligible_provider`, with `error.metadata` carrying the routing funnel). |
| `422` | `validation_failed` | The request failed validation (`validation_failed`), e.g. an unknown key, a field not supported yet, a wrong type or an unknown provider name in `provider`. The message names the field. |
| `429` | `rate_limited` | Request rejected by rate limiting (`rate_limited`). |
| `500` | `internal_error` | Other failures: `provider_unavailable` (503) when no healthy upstream can serve the request, `internal_error` (500) for unexpected platform-side failures. An error the upstream provider itself answered is passed on with its status and carries the four upstream headers; one the gateway raised does not. |
| `503` | `provider_unavailable` | Other failures: `provider_unavailable` (503) when no healthy upstream can serve the request, `internal_error` (500) for unexpected platform-side failures. An error the upstream provider itself answered is passed on with its status and carries the four upstream headers; one the gateway raised does not. |

Error body

```json
{
  "error": {
    "message": "No such model. Model names are vendor-qualified (vendor/model), so a name copied from a provider's own docs needs its vendor prefix added. Call GET /v1/models for the slugs this gateway serves.",
    "type": "not_found_error",
    "code": "model_not_found"
  }
}
```

## Response headers

Each header is listed with the response statuses it is sent with.

| Header | Sent with | Description |
| --- | --- | --- |
| `X-Pi-Trace-Id` | `200`, `400`, `401`, `402`, `404`, `422`, `429`, `default` | Platform request id, minted by the gateway. Present on every response of the inference endpoints; quote it when reporting a problem. |
| `Access-Control-Allow-Origin` | `200`, `400`, `401`, `402`, `404`, `422`, `429`, `default` | Always `*`. Credentials are the API key header, never a cookie. |
| `Access-Control-Expose-Headers` | `200`, `400`, `401`, `402`, `404`, `422`, `429`, `default` | Lists every `X-Pi-*` response header plus `Retry-After` and `Content-Encoding`, so browser code can read them. |
| `X-Pi-Provider` | `200`, `default` | Provider that served this response, in the same vocabulary as `provider.order` and `GET /v1/models/{vendor}/{model}/endpoints`. Present only when the upstream provider answered (a completion or an upstream error); after a fallback it names the provider that took over. |
| `X-Pi-Model` | `200`, `default` | Model slug that served this response - the same value as the body's `model`. When a fallback model took over, it is that model. Present only when the upstream provider answered. |
| `X-Pi-Upstream-Model` | `200`, `default` | The full model id the upstream provider itself reported in its response, passed on verbatim (on a stream, from the first data event). Absent when the upstream response named no model; never filled from the platform's own records. |
| `X-Pi-Upstream-Request-Id` | `200`, `default` | The upstream provider's own request id, passed on verbatim, for looking the call up on the provider's side. Absent when the provider returned none. |

## Request samples

cURL

```bash
curl https://api.pirouter.ai/v1/chat/completions \
  -H "Authorization: Bearer $PIROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-6-sol",
    "messages": [{ "role": "user", "content": "Hello!" }]
  }'
```

Python

```python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.pirouter.ai/v1",
    api_key=os.environ["PIROUTER_API_KEY"],
)

completion = client.chat.completions.create(
    model="openai/gpt-6-sol",
    messages=[{"role": "user", "content": "Hello!"}],
)
print(completion.choices[0].message.content)
```

TypeScript

```ts
import OpenAI from 'openai'

const client = new OpenAI({
  baseURL: 'https://api.pirouter.ai/v1',
  apiKey: process.env.PIROUTER_API_KEY,
})

const completion = await client.chat.completions.create({
  model: 'openai/gpt-6-sol',
  messages: [{ role: 'user', content: 'Hello!' }],
})
console.log(completion.choices[0].message.content)
```

## Response sample

`200` `application/json`

```json
{
  "id": "chatcmpl-EY6GuWpg...",
  "object": "chat.completion",
  "created": 1791795968,
  "model": "openai/gpt-6-sol",
  "choices": [
    {
      "index": 0,
      "message": {
        "role": "assistant",
        "content": "Hello! How can I help?"
      },
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 8,
    "completion_tokens": 10,
    "total_tokens": 18
  }
}
```

`200` `text/event-stream`

```text
data: {"id":"chatcmpl-EY6GwdSM...","object":"chat.completion.chunk","created":1791795970,"model":"openai/gpt-6-sol","choices":[{"index":0,"delta":{"role":"assistant","content":""},"finish_reason":null}]}

data: {"id":"chatcmpl-EY6GwdSM...","object":"chat.completion.chunk","created":1791795970,"model":"openai/gpt-6-sol","choices":[{"index":0,"delta":{"content":"Hi"},"finish_reason":null}]}

data: {"id":"chatcmpl-EY6GwdSM...","object":"chat.completion.chunk","created":1791795970,"model":"openai/gpt-6-sol","choices":[{"index":0,"delta":{"content":"!"},"finish_reason":null}]}

data: {"id":"chatcmpl-EY6GwdSM...","object":"chat.completion.chunk","created":1791795970,"model":"openai/gpt-6-sol","choices":[{"index":0,"delta":{},"finish_reason":"stop"}],"usage":{"prompt_tokens":8,"completion_tokens":5,"total_tokens":13}}

data: [DONE]
```
