API
PDF уходит в chat/completions как type file: публичный URL или data-URI с base64. Работает с любой моделью — либо нативно, либо через разбор файла.
POST https://api.aitunnel.ru/v1/chat/completions, в messages — массив частей. В file_data — публичный URL или data:application/pdf;base64,....
Нативно или через разбор
Если модель умеет файлы нативно (file в modalities.input), PDF передаётся ей напрямую. Иначе AITUNNEL разбирает файл и отдаёт модели текст (и при OCR — картинки).
Несколько PDF — отдельные элементы content. Сколько штук примет запрос, зависит от провайдера и модели. Текст лучше ставить первым, затем файлы. Если PDF должен идти первым — положите его в системный промпт. В одном запросе можно смешать PDF и картинки.
Плагин не обязателен
Разбор сработает и без plugins. Движок по умолчанию — нативный, если модель его умеет, иначе mistral-ocr.
URL
Для публично доступных PDF достаточно ссылки, без загрузки и кодирования.
curl https://api.aitunnel.ru/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-aitunnel-xxx" \
-d '{
"model": "gpt-5.4",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "Какие основные моменты в этом документе?"},
{
"type": "file",
"file": {
"filename": "document.pdf",
"file_data": "https://bitcoin.org/bitcoin.pdf"
}
}
]
}
]
}'Base64
Локальный или закрытый файл — data-URI.
# file_data — data-URI: data:application/pdf;base64,<...>
curl https://api.aitunnel.ru/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-aitunnel-xxx" \
-d '{
"model": "gpt-5.4",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "Какие основные моменты в этом документе?"},
{
"type": "file",
"file": {
"filename": "document.pdf",
"file_data": "data:application/pdf;base64,JVBERi0..."
}
}
]
}
]
}'Движок разбора
Движок задаётся плагином file-parser в plugins:
{
"plugins": [
{
"id": "file-parser",
"pdf": {
"engine": "cloudflare-ai"
}
}
]
}curl https://api.aitunnel.ru/v1/chat/completions \
-H "Content-Type: application/json" \
-H "Authorization: Bearer sk-aitunnel-xxx" \
-d '{
"model": "gpt-5.4",
"plugins": [
{
"id": "file-parser",
"pdf": { "engine": "mistral-ocr" }
}
],
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "Какие основные моменты в этом документе?"},
{
"type": "file",
"file": {
"filename": "document.pdf",
"file_data": "https://bitcoin.org/bitcoin.pdf"
}
}
]
}
]
}'Официальные SDK не знают про plugins
Поле plugins не входит в стандарт OpenAI: в Python передавайте через extra_body, в TypeScript — с @ts-expect-error. На cURL и любых «сырых» HTTP-клиентах ограничений нет.
Сжатие слишком длинного промпта — другой плагин, оптимизация сообщений.
| Движок | Когда | Оплата |
|---|---|---|
native | Модель принимает файлы сама (file во входе каталога) | Как обычные токены входа |
mistral-ocr | Сканы, PDF с картинками | OCR плюс токены модели — всё в usage.cost_rub |
cloudflare-ai | Текст в PDF, нужен markdown | Разбор бесплатный, токены модели — как обычно |
Если движок не указать: сначала нативный разбор модели, если его нет — mistral-ocr. У gpt-5.4 во входе есть file, поэтому без плагина пойдёт native. Явно задайте mistral-ocr или cloudflare-ai, если нужен другой движок.
На прямых маршрутах отдельных провайдеров plugins не передаются — остаётся только нативная поддержка файлов у модели.
Какие модели принимают файлы сами — группа chat, поле modalities.input:
curl https://api.aitunnel.ru/public/aitunnel/models/chatКартинки из OCR
У mistral-ocr из PDF в модель уходит не больше 8 изображений. Лишние отбрасываются, текст сохраняется целиком. Лимит нужен, потому что у провайдеров разные потолки на число картинок в одном запросе: часть сразу отвечает ошибкой, часть упирается в контекст, если с каждой страницы идёт картинка.
Если выбранная модель вообще не принимает изображения, OCR-картинки выкидываются, остаётся только текст.
Не разбирать PDF повторно
В ответе ассистента могут быть annotations — разобранное содержимое файла. Если отдать их обратно в следующем запросе (вместе с тем же file), PDF не разбирают заново: быстрее и без повторной оплаты OCR.
import base64
import requests
def data_url(path):
with open(path, "rb") as f:
return "data:application/pdf;base64," + base64.b64encode(f.read()).decode()
url = "https://api.aitunnel.ru/v1/chat/completions"
headers = {
"Authorization": "Bearer sk-aitunnel-xxx",
"Content-Type": "application/json",
}
file_part = {
"type": "file",
"file": {"filename": "document.pdf", "file_data": data_url("document.pdf")},
}
first = requests.post(
url,
headers=headers,
json={
"model": "gpt-5.4",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "Какие основные моменты в этом документе?"},
file_part,
],
}
],
},
).json()
msg = first["choices"][0]["message"]
annotations = msg.get("annotations")
follow = requests.post(
url,
headers=headers,
json={
"model": "gpt-5.4",
"messages": [
{
"role": "user",
"content": [
{"type": "text", "text": "Какие основные моменты в этом документе?"},
file_part,
],
},
{
"role": "assistant",
"content": msg["content"],
"annotations": annotations,
},
{"role": "user", "content": "Разверни второй пункт"},
],
},
)
print(follow.json()["choices"][0]["message"]["content"])Схема аннотации:
type FileAnnotation = {
type: "file";
file: {
hash: string; // идентификатор разобранного файла
name?: string; // исходное имя
content: ContentPart[]; // текст и картинки из PDF
};
};
type ContentPart =
| { type: "text"; text: string }
| { type: "image_url"; image_url: { url: string } };hash стабилен для одного и того же разобранного файла. По нему можно дедуплицировать аннотации из успешного ответа и из ошибки.
Формат ответа
{
"id": "gen-1234567890",
"model": "gpt-5.4",
"object": "chat.completion",
"created": 1234567890,
"choices": [
{
"message": {
"role": "assistant",
"content": "Документ обсуждает...",
"annotations": [
{
"type": "file",
"file": {
"hash": "abc123...",
"name": "document.pdf",
"content": [
{ "type": "text", "text": "Разобранный текст..." },
{
"type": "image_url",
"image_url": { "url": "data:image/png;base64,..." }
}
]
}
}
]
}
}
],
"usage": {
"prompt_tokens": 1000,
"completion_tokens": 100,
"total_tokens": 1100,
"cost_rub": 1.2,
"balance": 950.5
}
}annotations есть, когда PDF разбирали движком mistral-ocr или cloudflare-ai. У native файла в ответе нет: его видела сама модель. В usage — токены, cost_rub и balance после списания.
Ошибки после разбора
Если PDF уже разобрали, но ни один провайдер не смог сгенерировать ответ, в ошибке могут лежать те же аннотации: error.metadata.file_annotations. Их можно сразу отдать в повторном запросе, чтобы не платить за разбор снова. Для native аннотаций нет — файл уходил в модель как есть.
{
"error": {
"code": 502,
"message": "Провайдер вернул ошибку",
"metadata": {
"file_annotations": [
{
"type": "file",
"file": {
"hash": "abc123...",
"name": "document.pdf",
"content": [{ "type": "text", "text": "Разобранный текст..." }]
}
}
]
}
}
}Коды ошибок — ошибки и отладка.