---
title: "OpenAI-compatible API"
description: "Call the models on pirouter that accept the OpenAI protocol with the official OpenAI SDK, changing only the base URL and the key. This page covers Chat Completions and Responses, how streaming, tools and other parameters are passed through, and how pirouter differs from OpenAI's own behavior."
canonical: "https://pirouter.ai/docs/protocols/openai"
last-updated: "2026-10-17"
lang: "en"
source: "content/docs/en/(start)/protocols/openai.mdx"
---

# OpenAI-compatible API

Call the models on pirouter that accept the OpenAI protocol with the official OpenAI SDK, changing only the base URL and the key. This page covers Chat Completions and Responses, how streaming, tools and other parameters are passed through, and how pirouter differs from OpenAI's own behavior.

To call pirouter with the official OpenAI SDK, change only the base URL and the key. This page covers sending requests, streaming, passing tools and structured output, controlling reasoning, and where pirouter differs from OpenAI's own behavior.

The OpenAI protocol has four endpoints: `POST /v1/chat/completions`, `POST /v1/responses`, `POST /v1/images/generations` and `POST /v1/images/edits`. This page covers the first two; for the image endpoints see [Generate and edit images](https://pirouter.ai/docs/images.md).

## Send a request with the official SDK

Set the SDK's base URL to `https://api.pirouter.ai/v1` and its key to your pirouter API key; the SDK sends it in `Authorization: Bearer`. Write model IDs as `vendor/model`.

Chat Completions:

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

Responses:

cURL:

```bash
curl https://api.pirouter.ai/v1/responses \
  -H "Authorization: Bearer $PIROUTER_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-6-sol",
    "input": "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"],
)

response = client.responses.create(
    model="openai/gpt-6-sol",
    input="Hello!",
)
print(response.output_text)
```

TypeScript:

```ts
import OpenAI from 'openai'

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

const response = await client.responses.create({
  model: 'openai/gpt-6-sol',
  input: 'Hello!',
})
console.log(response.output_text)
```

You should see a reply. Both requests show up in the console's **Logs**.

Request fields pirouter does not know are forwarded to the provider as they are. Only four top-level fields are pirouter's own and are not forwarded: `provider`, `models`, `route` and `fallbacks`. See [Provider routing](https://pirouter.ai/docs/routing.md) and [Model fallbacks](https://pirouter.ai/docs/fallbacks.md).

## Stream the response

Add `"stream": true` to the request body and the response becomes an SSE event stream:

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

You should see a series of `data: ` events, ending with `data: [DONE]`.

On a streamed Chat Completions request pirouter always turns on `stream_options.include_usage`, so there is always one event with an empty `choices` and a `usage` object before `[DONE]`, even if you sent `false`. Streamed Responses requests are left as they are.

## Use tools, structured output and image input

pirouter forwards these parameters as they are and does not check whether the model supports them. A request with a parameter the model does not support is still forwarded, and the provider decides whether to accept it, ignore it or return an error.

Before you call, check the model's fields in `GET /v1/models`:

| What you want                     | What you send                                                                                 | Supported when                                         |
| --------------------------------- | --------------------------------------------------------------------------------------------- | ------------------------------------------------------ |
| Tool calls                        | `tools`, `tool_choice`                                                                        | `supported_parameters` includes `tools`, `tool_choice` |
| Several tool calls in one turn    | `parallel_tool_calls`                                                                         | `supported_parameters` includes `parallel_tool_calls`  |
| JSON output                       | `response_format: {"type": "json_object"}`                                                    | `supported_parameters` includes `response_format`      |
| Output that follows a JSON schema | `response_format: {"type": "json_schema", …}`                                                 | `supported_parameters` includes `structured_outputs`   |
| Image input                       | An `image_url` part in the message content, which may be a `data:` URL                        | `architecture.input_modalities` includes `image`       |
| PDF input                         | A `file` part in the message content, with `file_data` as a base64 `data:application/pdf` URL | `architecture.input_modalities` includes `file`        |

The request bodies are written the same way as in OpenAI's own documentation.

## Control reasoning

Set the reasoning level with `reasoning_effort` (Chat Completions) or `reasoning.effort` (Responses); it is forwarded as is. Which levels a model accepts is in `reasoning.supported_efforts` in `GET /v1/models`, and the level used when you send none is in `reasoning.default_effort`. The full set of levels is `none`, `minimal`, `low`, `medium`, `high`, `xhigh` and `max`.

`supported_parameters` lists `reasoning`, not `reasoning_effort`. Use `reasoning` to tell whether a model reasons.

Reasoning tokens are reported in `usage.completion_tokens_details.reasoning_tokens` and billed at the model's `internal_reasoning` price.

On Chat Completions, `thinking: {"type": "disabled"}` is replaced with `reasoning_effort: "none"` before forwarding. If you also sent `reasoning_effort` or `reasoning`, your value is kept.

## How pirouter differs from OpenAI

| Area                        | What pirouter does                                                                                                                                                                                                        |
| --------------------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `model` in the response     | Rewritten to the pirouter model ID (`vendor/model`), in every event of a stream too. The name the provider reported is in the `X-Pi-Upstream-Model` response header                                                       |
| Streamed usage              | A streamed Chat Completions response always has a usage event; see [Stream the response](https://pirouter.ai/docs/protocols/openai.md#stream-the-response)                                                                |
| Error body                  | pirouter's own errors are `{"error": {"message", "type", "code"}}`, with `code` a string error code; see [Errors](https://pirouter.ai/docs/errors.md)                                                                     |
| Routing fields              | `provider`, `models`, `route` and `fallbacks` are not forwarded                                                                                                                                                           |
| No reasoning parameter sent | Some providers have a default reasoning level on record: when the request has no `reasoning_effort`, `reasoning` or `thinking`, pirouter writes that level in                                                             |
| Sampling parameters         | Some providers accept only some sampling parameters (such as `temperature`, `top_p`, `seed`, `stop`): the others are removed before forwarding, without an error                                                          |
| Reasoning budget            | Some providers limit reasoning only by a token budget: pirouter converts the level into a budget and splits your output limit between reasoning and answer, so the two never add up to more than the limit                |
| Forced tool calls           | With `tool_choice` set to `"required"` or to a named function, some models do not always call the tool: the request still returns `200`, and the reply may be plain text                                                  |
| Retries                     | When a provider fails before sending its first byte, pirouter moves to the next one, up to 5 attempts; see [Automatic retry on another provider](https://pirouter.ai/docs/routing.md#automatic-retry-on-another-provider) |

## Errors you may see

| Status and `error.code`            | Cause                                                                                | What to do                                                                                                           |
| ---------------------------------- | ------------------------------------------------------------------------------------ | -------------------------------------------------------------------------------------------------------------------- |
| `401` `invalid_api_key` and others | The key is missing, wrong or no longer valid                                         | Check the key; see [When authentication fails](https://pirouter.ai/docs/authentication.md#when-authentication-fails) |
| `402` `insufficient_balance`       | The balance is too low                                                               | [Add credit](https://pirouter.ai/docs/billing.md#add-credit), then send again                                        |
| `404` `model_not_found`            | No model has this ID                                                                 | Check the `vendor/model` spelling                                                                                    |
| `422` `validation_failed`          | The model is not served on this endpoint, such as an image model on Chat Completions | Use an endpoint the `message` lists; see [Protocol mismatch](https://pirouter.ai/docs/models.md#protocol-mismatch)   |
| `429` `rate_limited`               | The upstream deployment used up its per-minute quota                                 | Wait `Retry-After` seconds; see [Rate limits](https://pirouter.ai/docs/rate-limits.md)                               |

Every error code is listed in [Errors](https://pirouter.ai/docs/errors.md).

## Not available today

pirouter does not serve these OpenAI endpoints today:

- `POST /v1/embeddings`
- `GET /v1/responses/{id}`

A call to either path returns `400` with the error code `invalid_request` and the `message` `model is required`. The error does not mean your request is missing a field; it means pirouter does not serve that endpoint.

## Next steps

- [Anthropic Messages](https://pirouter.ai/docs/protocols/anthropic.md): Call Claude and other models with the official Anthropic SDK.
- [Models and protocols](https://pirouter.ai/docs/models.md): Which protocols each model accepts.
