---
title: "Embeddings"
description: "Turn text into vectors with POST /embeddings, the same request and answer as OpenRouter's."
---

# Embeddings

POST `/embeddings` turns text into vectors: lists of numbers that sit close together when the texts mean similar things.
Use them for search, retrieval for a chat model, clustering and finding duplicates.
The request and the answer have the same shape as OpenRouter's and OpenAI's, so an OpenAI SDK works with only the base URL and the key changed.
You pay for input tokens only. The answer carries the charge in `usage.cost`.

## Request

```
POST https://api.plaingateway.com/api/v1/embeddings
Authorization: Bearer $PLAINGATEWAY_API_KEY
Content-Type: application/json
```

| Field | Type | Required | What it does |
|---|---|---|---|
| `model` | string | yes | An embedding model id, such as `openai/text-embedding-3-small`. See [Models](#models). |
| `input` | string or array | yes | The text or tokens to embed, in one of the five forms below. |
| `dimensions` | integer | no | How many numbers each vector has. Only `text-embedding-3-small` and `text-embedding-3-large` take it. |
| `encoding_format` | string | no | `float` (the default) returns arrays of numbers. `base64` returns each vector as a base64 string of 32-bit floats, which is smaller. |
| `input_type` | string | no | What the input is, such as `search_query` or `search_document`. Accepted. The OpenAI and Azure hosts do not take it, so it is not sent to them. |
| `user` | string | no | Your end user's id. It is stored as `external_user` on the [generation](generation.md). |
| `provider` | object | no | Which hosts may serve the request and in what order. See [Provider routing](../routing/provider-routing.md). |
| `session_id` | string | no | Accepted and not stored. It never reaches the host. |
| `trace` | object | no | Accepted and not stored. It never reaches the host. |

`stream` is ignored: the answer is always one JSON body. `models` is ignored too, so `model` is required.

### The five forms of `input`

| Form | Example |
|---|---|
| A string | `"input": "The cat sat on the mat"` |
| An array of strings | `"input": ["The cat sat", "A dog ran"]` |
| One array of token ids | `"input": [791, 8415, 7731]` |
| An array of token id arrays | `"input": [[791, 8415], [32, 5679]]` |
| An array of content objects | `"input": [{"content": [{"type": "text", "text": "The cat sat"}]}]` |

A content object may hold only `text` parts; its text is sent to the host as a string. An image or any other part returns 400, because no listed model takes images.
Strings must not be empty, and token arrays must hold at least one token.

## Response

One `data` item for each input, in the order you sent them. `index` is the input's position.
`model` is the host's own model name, without the `openai/` prefix, as on OpenRouter.
`id` has the form `gen-emb-<unix seconds>-<20 letters and digits>`; pass it to [GET /generation](generation.md) to read the request back.
`usage.cost` is the charge in USD. `prompt_tokens` and `total_tokens` are the same number, since there are no output tokens.
The `X-PG-Host` and `X-PG-Cost-USD` headers carry the host and the charge, as on every non-streamed answer. See [Headers](headers.md).

```http
HTTP/1.1 200
{
  "object": "list",
  "data": [
    {
      "object": "embedding",
      "embedding": [0.016754150390625, -0.055755615234375, 0.005634307861328125],
      "index": 0
    }
  ],
  "model": "text-embedding-3-small",
  "usage": {
    "prompt_tokens": 1,
    "total_tokens": 1,
    "cost": 2e-08,
    "is_byok": false,
    "cost_details": {
      "upstream_inference_cost": 2e-08,
      "upstream_inference_prompt_cost": 2e-08,
      "upstream_inference_completions_cost": 0
    }
  },
  "provider": "OpenAI",
  "id": "gen-emb-1794367041-trccdd4mAq6ZFMp5X8f0"
}
```

This is a recorded answer with the vector shortened; a real one has 1,536 numbers for `text-embedding-3-small`. Your `usage.cost` is the charge at our price.

## Errors

Errors have the shape described in [Errors](errors.md). These are the ones particular to embeddings:

| Status | When | Message |
|---|---|---|
| 400 | The model is unknown, or is not an embedding model | `Model <id> does not exist` |
| 400 | `input` is an empty array | `Input is empty` |
| 400 | `input` is missing, an empty string, or holds an empty string, an empty token array or a non-text part | A message naming the problem |
| 400 | More than 2,048 inputs, refused before any host | `HTTP 400: ...` holding `array length must be 2048 or less` |
| 400 | One input is over 8,192 tokens | `HTTP 400: ...` holding the host's message, `maximum context length is 8192 tokens` |
| 401 | No key, or a key that is unknown, disabled, expired or revoked | |
| 402 | The balance cannot cover the request, or the key reached its spend limit | |
| 404 | The `provider` filters leave no host | |
| 429 | The host rate limited the request | |
| 502 | The host failed, or its answer was over the request's size cap | |
| 503 | Another request with a large answer is being read; see [Limits](#limits) | `the gateway is busy with other large answers; try again shortly` |

A chat model sent here gets the first 400. An embedding model sent to `/chat/completions` or another chat endpoint gets that endpoint's unknown model 400.
An error before the host answered is not charged. Once the host has answered, the request is charged even if the answer cannot be delivered, because the host was paid; see [Errors](errors.md).

## Limits

- At most 2,048 inputs in one request.
- At most 8,192 tokens in each input.
- No streaming.
- A large answer is served one at a time. The gateway sizes each answer from the number of inputs and the numbers in each vector: `dimensions`, or 3,072 when you name none. When that size could pass 32 MB and another such answer is being read, the request gets 503 at once, before any charge. With float vectors that is more than 310 inputs, or more than 619 with `dimensions` at 1,536; with `base64` (the OpenAI SDK's default) it is more than 1,953 inputs. Split a larger batch, or retry the 503 after a short wait.
- The same input gives the same vector each time, so store vectors instead of asking again.

## Examples

### curl

```sh
curl https://api.plaingateway.com/api/v1/embeddings \
  -H "Authorization: Bearer $PLAINGATEWAY_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{"model": "openai/text-embedding-3-small", "input": ["The cat sat on the mat", "A dog ran in the park"]}'
```

### Python (OpenAI SDK)

```python
import os
from openai import OpenAI

client = OpenAI(
    base_url="https://api.plaingateway.com/api/v1",
    api_key=os.environ["PLAINGATEWAY_API_KEY"],
)
r = client.embeddings.create(
    model="openai/text-embedding-3-small",
    input=["The cat sat on the mat", "A dog ran in the park"],
)
print(len(r.data), len(r.data[0].embedding))  # 2 1536
```

### TypeScript (OpenAI SDK)

```typescript
import OpenAI from "openai";

const client = new OpenAI({
  baseURL: "https://api.plaingateway.com/api/v1",
  apiKey: process.env.PLAINGATEWAY_API_KEY,
});
const r = await client.embeddings.create({
  model: "openai/text-embedding-3-small",
  input: ["The cat sat on the mat", "A dog ran in the park"],
});
console.log(r.data.length, r.data[0].embedding.length); // 2 1536
```

The OpenAI SDK asks for `base64` by default and decodes it, so you get arrays of numbers either way.

## Models

| Model id | Numbers in a vector |
|---|---|
| `openai/text-embedding-3-small` | 1,536, or fewer with `dimensions` |
| `openai/text-embedding-3-large` | 3,072, or fewer with `dimensions` |
| `openai/text-embedding-ada-002` | 1,536 |

Their prices are on [Models](https://plaingateway.com/models?output_modalities=embeddings), per 1M input tokens.
To list them from code, call [GET /embeddings/models](models.md#embedding-models) or `GET /models?output_modalities=embeddings`. The plain `GET /models` leaves them out, as OpenRouter's does.

## Differences from OpenRouter

- Only the OpenAI embedding models in the table above are served.
- No batch API and no `:batch` models.
- No image or other multimodal input.
- A request whose answer could pass 32 MB is served one at a time; another such request gets 503 until the first ends. OpenRouter states no such limit.
- In `provider`, `require_parameters`, `preferred_max_latency`, `preferred_min_throughput`, `enforce_distillable_text` and `options` are accepted and have no effect.
- `session_id` and `trace` are accepted and not stored. [GET /generation](generation.md) returns `session_id` as null.
