Эмбеддинги
Векторные представления текста для поиска, кластеризации и классификации. Тот же ключ, тот же base URL, формат ответа — как у OpenAI.
Запрос: POST /embeddings
Эндпоинт принимает одну строку или массив строк и возвращает по вектору на каждую. Стриминга здесь нет —
ответ приходит целиком. Модель обязательно должна быть эмбеддинговой: обычная чат-модель вернёт
404 с типом model_not_found.
| Параметр | Тип | Обяз. | Описание |
|---|---|---|---|
| model | string | да | Идентификатор модели с тегом «эмбеддинги» из каталога, вместе с вендором. |
| input | string | string[] | да | Строка или массив строк. До 2 048 строк в одном запросе, каждая — не длиннее контекста модели. |
| encoding_format | string | нет | float — массив чисел, значение по умолчанию. base64 — тот же вектор строкой, ответ примерно втрое короче. |
| dimensions | integer | нет | Усечение вектора до нужной размерности. Работает только у моделей, которые это поддерживают. |
Примеры
Самый короткий вариант — одна строка на curl. Ответ вернёт массив data из одного
элемента.
curl https://api.tokendock.cloud/v1/embeddings \
-H "Authorization: Bearer $TOKENDOCK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/text-embedding-3-large",
"input": "Стоимость запроса считается по токенам входа и выхода."
}'
Батч из нескольких строк — тот же запрос, но input становится массивом. Один запрос на
сотню чанков дешевле по накладным расходам и укладывается в лимит RPM, где сотня отдельных запросов уже нет.
curl https://api.tokendock.cloud/v1/embeddings \
-H "Authorization: Bearer $TOKENDOCK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"model": "openai/text-embedding-3-large",
"input": [
"Пополнение баланса картой российского банка проходит мгновенно.",
"Секрет ключа показывается один раз — дальше хранится только его хеш.",
"Доступ к каталогу моделей работает из России без VPN."
]
}' import os
from openai import OpenAI
client = OpenAI(
base_url="https://api.tokendock.cloud/v1",
api_key=os.environ["TOKENDOCK_API_KEY"],
)
chunks = [
"Пополнение баланса картой российского банка проходит мгновенно.",
"Секрет ключа показывается один раз — дальше хранится только его хеш.",
"Доступ к каталогу моделей работает из России без VPN.",
]
response = client.embeddings.create(
model="openai/text-embedding-3-large",
input=chunks,
dimensions=1024,
)
# Порядок в data совпадает с порядком input, но надёжнее опираться на index
vectors = [item.embedding for item in sorted(response.data, key=lambda x: x.index)]
print(len(vectors), len(vectors[0])) # 3 1024
print(response.usage.prompt_tokens) import OpenAI from "openai";
const client = new OpenAI({
baseURL: "https://api.tokendock.cloud/v1",
apiKey: process.env.TOKENDOCK_API_KEY,
});
const chunks = [
"Пополнение баланса картой российского банка проходит мгновенно.",
"Секрет ключа показывается один раз — дальше хранится только его хеш.",
"Доступ к каталогу моделей работает из России без VPN.",
];
const response = await client.embeddings.create({
model: "mistral/mistral-embed",
input: chunks,
});
const vectors = response.data
.sort((a, b) => a.index - b.index)
.map((item) => item.embedding);
console.log(vectors.length, vectors[0].length);
console.log(response.usage);
В SDK от OpenAI метод называется embeddings.create и работает без правок — меняется
только base_url. То же справедливо для LangChain и LlamaIndex: там достаточно указать
адрес TokenDock в настройках OpenAI-провайдера.
Ответ
Ответ на батч из трёх строк. Векторы в примере усечены до нескольких значений — у
openai/text-embedding-3-large их 3 072, а с параметром
dimensions ровно столько, сколько запрошено.
{
"object": "list",
"model": "openai/text-embedding-3-large",
"data": [
{
"object": "embedding",
"index": 0,
"embedding": [-0.01423, 0.03118, -0.00784, 0.02641, "…", -0.00915]
},
{
"object": "embedding",
"index": 1,
"embedding": [0.00672, -0.02037, 0.04152, -0.00389, "…", 0.01226]
},
{
"object": "embedding",
"index": 2,
"embedding": [-0.00318, 0.01894, 0.00507, -0.02760, "…", 0.00841]
}
],
"usage": {
"prompt_tokens": 62,
"total_tokens": 62
}
} -
index— позиция строки во входном массиве. Порядок элементовdataобычно совпадает с порядкомinput, но сортировка поindexнадёжнее. -
usage.prompt_tokens— сумма токенов всех строк батча. Выходных токенов у эмбеддингов нет, поэтомуcompletion_tokensв ответе отсутствует. - Если хотя бы одна строка длиннее контекста модели, весь запрос отклоняется с 422. Частичного результата не бывает — режьте текст до отправки.
Какие модели доступны
В каталоге две эмбеддинговые модели. Цена указана за 1M входных токенов.
| id модели | Вендор | Контекст | Размерность | Цена, ₽ / 1M | dimensions |
|---|---|---|---|---|---|
| openai/text-embedding-3-large | OpenAI | 8 191 | 3 072 | 11 | поддерживается |
| mistral/mistral-embed | Mistral | 8 192 | 1 024 | 8 | не поддерживается |
- text-embedding-3-large — качество выше, вектор длиннее, размерность можно уменьшить
параметром
dimensionsбез переиндексации на другой модели. - Mistral Embed — дешевле и компактнее: 1 024 значения на вектор, меньше места в индексе и быстрее поиск.
- Смена модели означает полную переиндексацию. Векторы разных моделей несопоставимы, даже если совпала размерность.
Тарификация
У эмбеддингов считаются только входные токены. Цена выхода в каталоге равна нулю — вектор не
тарифицируется, сколько бы значений в нём ни было. Параметр dimensions на стоимость не
влияет: он меняет размер ответа, а не объём работы модели.
-
Индексация корпуса на 4M токенов через
openai/text-embedding-3-large— 44 ₽: 4 × 11 ₽ за миллион. -
Тот же корпус на
mistral/mistral-embed— 32 ₽. - Запрос, отклонённый до модели — 400, 422, 429 — не тарифицируется.
Практика
Нормализация
Модели отдают векторы единичной длины, поэтому косинусное сходство совпадает со скалярным произведением — это
экономит операцию на каждом сравнении. Но как только вы усекли вектор через
dimensions, норма перестаёт быть единицей: нормализуйте результат сами, иначе
ранжирование поедет.
import numpy as np
vectors = np.array(vectors, dtype=np.float32)
# После усечения через dimensions норма ломается — вернём её к единице
vectors /= np.linalg.norm(vectors, axis=1, keepdims=True)
# На единичных векторах косинус — это обычное скалярное произведение
similarity = vectors @ vectors.T
Храните векторы в float32 — точности достаточно, а памяти вдвое меньше, чем у
float64. Если индекс большой, encoding_format: "base64"
заметно сокращает трафик на приёме.
Размер батча
- Технический потолок — 2 048 строк на запрос, практический ориентир — от 96 до 256 строк.
- Батч тратит токены сразу за все строки, поэтому в лимит TPM он упирается раньше, чем в RPM. Считайте бюджет по токенам, а не по количеству запросов — цифры в разделе лимиты и квоты.
- Ошибка на батче отменяет весь батч. Для больших прогонов держите очередь с повтором по 429 и 5xx и уменьшайте пачку при повторе.
- Индексацию корпуса выгоднее гнать в 2–4 параллельных потока с бэкоффом, чем в один поток без пауз — так лимиты расходуются ровнее.
Длинные тексты
Контекст эмбеддинговых моделей — около 8 000 токенов, и это не повод отправлять документ целиком. Один вектор на длинный текст размывает смысл: поиск начнёт находить документ «вообще про то же», но не тот фрагмент, который нужен.
- Режьте текст на чанки по 200–500 токенов с перекрытием 10–15% — так граница чанка не разрывает мысль.
- Режьте по структуре: абзацы, заголовки, элементы списка. Разрез по символам ломает смысл чаще, чем экономит место.
- Храните рядом с вектором ссылку на документ и позицию чанка — выдача должна вести к исходному тексту.
- Усреднять векторы чанков в один «вектор документа» можно для грубой фильтрации, но искать всё равно лучше по чанкам.
Эмбеддинги детерминированы: одна и та же строка на одной и той же модели даёт один и тот же вектор. Кэшируйте результат по хешу текста — на повторной индексации это экономит и время, и деньги.