# Completions

> OpenAI's legacy prompt completion, routed exactly like chat.

Section: Core API. Canonical page: https://routehook.ai/docs/completions

`POST /v1/completions` takes a prompt rather than a message list and answers with an object of type `text_completion`. It exists so that code written against OpenAI's original completion API keeps working when the base URL changes, which is the whole point of the compatibility surface.

> **Warning: New code should use chat**
>
> Almost nothing upstream still serves a genuine completion endpoint, so nearly every request here is translated into a chat call and back. The translation is lossless in shape but not in behaviour, and it gives you no tools, no multimodal parts and no message roles. `POST /v1/chat/completions` is the same routing, the same billing and a larger surface. Start there unless you are porting something.

## POST /v1/completions

OpenAI's legacy prompt completion, translated onto the same routing as chat.

Status: Available. Authentication: any live API key on the account.

## A request

**cURL**

```bash
curl https://api.routehook.ai/v1/completions \
  -H "Authorization: Bearer $ROUTEHOOK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "openai/gpt-5.4-mini",
    "prompt": "Write one sentence about harbour fog.",
    "max_tokens": 64
  }'
```

**JavaScript**

```javascript
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.routehook.ai/v1",
  apiKey: process.env.ROUTEHOOK_API_KEY,
});

const completion = await client.completions.create({
  model: "openai/gpt-5.4-mini",
  prompt: "Write one sentence about harbour fog.",
  max_tokens: 64,
});

console.log(completion.choices[0].text);
```

**Python**

```python
import os
from openai import OpenAI

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

completion = client.completions.create(
    model="openai/gpt-5.4-mini",
    prompt="Write one sentence about harbour fog.",
    max_tokens=64,
)

print(completion.choices[0].text)
```

## Request parameters

| PARAMETER | TYPE | REQUIRED | DESCRIPTION |
| --- | --- | --- | --- |
| model | string | required | Model slug from GET /v1/models. The same catalogue chat uses. |
| prompt | string \| string[] | required | The prompt, or a batch of them. A batch answers with one choice per prompt, in order. |
| max_tokens | integer | optional | Ceiling on generated tokens. Also caps what is reserved before the call runs. |
| stream | boolean | optional | Emit server-sent events instead of one body. Default false. |
| temperature | number | optional | 0 to 2. Forwarded unchanged. |
| top_p | number | optional | 0 to 1. Forwarded unchanged. |
| stop | string \| string[] | optional | Up to eight sequences that end generation when produced. |
| n | integer | optional | 1 to 8 completions. Multiplies the reservation and the bill. |
| seed | integer | optional | Best-effort determinism, where the model supports it. |
| user | string | optional | An opaque end-user identifier, forwarded upstream. |
| models | string[] | optional | Fallback model slugs, tried in order after model. |
| suffix | string | optional | Up to 4,000 characters, forwarded to the upstream. Only insertion-capable models read it. |
| presence_penalty | number | optional | -2 to 2. Forwarded unchanged. |
| frequency_penalty | number | optional | -2 to 2. Forwarded unchanged. |
| stream_options | object | optional | Only include_usage is read. It adds a final frame carrying usage and cost. |
| logprobs | integer | optional | 0 to 5: this route's meaning, not chat's boolean. Translated on the way out; see below. |
| echo | boolean | optional | Accepted and discarded. Chat has no equivalent, so it changes nothing. |
| best_of | integer | optional | 1 to 8. Accepted and discarded, for the same reason as echo. |
| transforms | string[] | optional | Accepted and discarded here, unlike chat, which refuses it outright. |

> **Warning: echo, best_of and transforms are dropped, not honoured**
>
> All three parse, and none of them survives the translation onto a chat completion: `echo` and `best_of` have no chat equivalent at all, and `transforms` has no execution layer on this platform. The request succeeds and the answer simply does not reflect them. Note that `transforms` is treated differently on the two routes: `POST /v1/chat/completions` answers 400 rather than dropping it. If you rely on any of the three, this is the field to check before porting a legacy Completions integration.

## The translation

This route is planned, billed and failed over exactly like chat: the same model catalogue, the same routing, the same reservation and settlement, and the same `models` field. The only difference is what happens at the edges of the call when the endpoint chosen for the model speaks chat and nothing else, which is nearly all of them.

| ON THE WAY OUT | ON THE WAY BACK |
| --- | --- |
| prompt becomes a single message: { role: "user", content: <prompt> } | choices[].message.content is unwrapped back into choices[].text |
| max_tokens, temperature, top_p, stop, seed and n are passed through under the same names | finish_reason, index and usage are passed back under the same names |
| logprobs: 0-5 becomes chat's logprobs: true plus top_logprobs: <n> | The log probabilities come back under chat's names, unwrapped into this route's choices |
| echo, best_of and transforms are consumed here and never sent | Nothing in the answer reflects them |
| A prompt array becomes one request per prompt | object is text_completion, and the choices come back in the order the prompts went out |

## The response

**200 OK**

```json
{
  "id": "cmpl_5f81c0",
  "object": "text_completion",
  "created": 1786312455,
  "model": "openai/gpt-5.4-mini",
  "choices": [
    {
      "index": 0,
      "text": "Fog rolled off the water and swallowed the cranes one by one.",
      "finish_reason": "stop"
    }
  ],
  "usage": {
    "prompt_tokens": 8,
    "completion_tokens": 14,
    "total_tokens": 22,
    "cost": "0.0000038"
  }
}
```

> **Note: What the translation cannot give back**
>
> The shapes convert cleanly; the behaviour does not. A true completion endpoint **continues** the text you send it. An instruction-tuned chat model, handed the same text as a user turn, **answers** it, so a prompt written to be continued comes back replied to, and a few-shot prompt that ends mid-sentence may be finished with a commentary on the pattern rather than the next example. Where that difference matters, write the prompt as an instruction, or use chat and say what you want in a system turn.

## Streaming

`stream: true` works here and uses OpenAI's legacy chunk shape: the object stays `text_completion` and each frame carries `choices[].text` rather than a delta. The framing, the `[DONE]` sentinel, the error frame and the `stream_options.include_usage` opt-in are all as described on the streaming page.

## Cost

Identical to chat, on the same rates: prompt tokens on the input axis, generated tokens on the output axis, and `usage.cost` in the response with what was settled. A prompt array is charged as the requests it becomes. Batching prompts saves round trips, not money.

## Related

- [Chat](/docs/chat): The route to build on, and the one this translates into.
- [Streaming](/docs/streaming): The frames, and what a dead stream costs.
- [Migrating from OpenRouter](/docs/migrate-openrouter): The same shapes, under the same field names.
