API Reference
Полный справочник публичного Prostor API: base URL, ключи, все эндпоинты, форматы ответов, streaming, идемпотентность и лимиты.
Prostor API — это OpenAI-совместимый HTTP API поверх общего каталога моделей: текст, vision, изображения, видео и эмбеддинги через один ключ и один баланс. Форма ответов повторяет OpenAI, а служебные эндпоинты (/v1/credits, /v1/key, /v1/generation) — OpenRouter, чтобы существующие клиенты заводились без переписывания.
Эта страница — справочник эндпоинтов. Деньги и формула списания описаны в Тарификации, полный каталог ошибок — в Ошибках.
Base URL
https://api.prostor.ai/v1Все пути ниже указаны относительно этого базового URL. Держите его и ключ в переменных окружения — все примеры на странице рассчитаны на них:
export PROSTOR_API_BASE_URL="https://api.prostor.ai/v1"
export PROSTOR_API_KEY="pak_pr_v1_..."Аутентификация
Ключ передаётся в заголовке Authorization:
Authorization: Bearer pak_pr_v1_...Публичные ключи всегда начинаются с префикса pak_pr_v1_. Полное значение ключа показывается один раз в момент выдачи — в системе хранится только его хэш, восстановить ключ нельзя, потерянный ключ можно только выпустить заново.
Внутренние ключи routing core (sk_...) на публичном API не работают: они отклоняются с 401 internal_token_not_allowed. Если вы получили ключ, который не начинается с pak_pr_v1_, это не ключ внешнего API.
Быстрая проверка связности — запрос без ключа обязан вернуть 401:
curl -i https://api.prostor.ai/v1/modelsПервый шаг: возьмите ID модели из каталога
Не хардкодьте ID модели из чужих примеров. Каталог Prostor меняется: модели добавляются, дорожают, отключаются, а у вашего ключа может быть свой белый список (allowed_models). Единственный достоверный источник ID — GET /v1/models этим же ключом.
curl -s "$PROSTOR_API_BASE_URL/models" \
-H "Authorization: Bearer $PROSTOR_API_KEY" | jq -r '.data[] | "\(.id)\t\(.category)"'Возьмите нужный id из data[].id и подставьте его в примеры:
export PROSTOR_MODEL="<id из GET /v1/models>"Если модель указана неверно или отключена, вы получите 404 model_not_found с сообщением вида unknown model: X — call GET /v1/models for the models this key can use. Это не сбой провайдера и повтор запроса не поможет — нужно взять актуальный ID.
Матрица поддержки
| Метод и путь | Scope | Статус |
|---|---|---|
POST /v1/chat/completions | chat:completions | поддерживается, в том числе stream: true |
POST /v1/responses | chat:completions | поддерживается, только stateless |
GET /v1/models | models:read | поддерживается |
GET /v1/models/user | models:read | алиас GET /v1/models, ответ идентичен |
GET /v1/credits | не требуется | поддерживается |
GET /v1/key | не требуется | поддерживается |
GET /v1/auth/key | не требуется | алиас GET /v1/key (совместимость с OpenRouter) |
GET /v1/generation?id= | не требуется | поддерживается |
POST /v1/images/generations | images:generations | поддерживается |
POST /v1/videos/generations | video:generations | поддерживается (синхронно) |
POST /v1/videos/submit | video:generations | поддерживается (async) |
POST /v1/videos/status | video:generations | поддерживается (async) |
POST /v1/embeddings | embeddings | поддерживается |
GET /healthz, GET /readyz | без ключа | health-пробы, {"status":"ok"} / {"status":"ready"} |
Всего существует пять scope: chat:completions, models:read, images:generations, video:generations, embeddings. Ключ, выданный без явного списка, получает все пять. Вызов эндпоинта без нужного scope — 403 scope_denied.
Чего в API нет
Прочитайте этот список до того, как напишете код: перечисленное ниже не «частично работает», а не существует.
| Возможность | Что произойдёт |
|---|---|
POST /v1/completions (legacy text completions) | 404 not_found. Используйте /v1/chat/completions |
GET /v1/providers | 404 not_found. Провайдеры — наша внутренняя кухня и наружу не раскрываются |
GET /v1/models/{author}/{slug}/endpoints | 404 not_found. Есть только плоский GET /v1/models |
POST /v1/audio/* (speech, transcriptions) | 404 not_found. Аудио будет отдельным расширением |
Массив models (client-side fallback list) | 400 unsupported_parameter |
| Stateful-возможности Responses | 400 с точной причиной, см. раздел ниже |
| Любой другой путь | 404 not_found с текстом unknown endpoint: <METHOD> <path> |
Про массив models отдельно. В OpenRouter-контракте это список запасных моделей, и некоторые SDK подставляют его сами. Мы его отклоняем осознанно: авторизация (allowed_models) и списание выполняются по той модели, которую вы назвали в поле model. Если бы запрос обслужила модель из запасного списка, вы бы получили ответ более дорогой (или вовсе запрещённой ключу) модели по цене дешёвой. Пока авторизация и биллинг не следуют за фактически отработавшей моделью, параметр не принимается. Отправляйте одну модель в model, а fallback реализуйте на своей стороне.
Проверьте настройки SDK: если ваш клиент автоматически заполняет models, отключите это, иначе каждый запрос будет получать 400 unsupported_parameter.
GET /v1/models
Возвращает модели, доступные конкретному ключу: активные, с настроенной ценой и прошедшие фильтр allowed_models. Внутренний каталог наружу не отдаётся, апстрим-провайдер модели не раскрывается — owned_by всегда "prostor".
curl -s "$PROSTOR_API_BASE_URL/models" \
-H "Authorization: Bearer $PROSTOR_API_KEY" | jq .Ответ (значения моделей — плейсхолдеры, реальные ID берите из своего ответа):
{
"object": "list",
"total_count": 2,
"data": [
{
"id": "<text-model-id>",
"object": "model",
"created": 0,
"owned_by": "prostor",
"name": "Human-readable name",
"category": "text",
"architecture": {
"modality": "text+image->text",
"input_modalities": ["text", "image"],
"output_modalities": ["text"]
},
"pricing": {
"prompt": "0.00000048",
"completion": "0.000004"
},
"prostor": {
"credits_per_million_prompt": 48,
"credits_per_million_completion": 400,
"billing": "per_token"
},
"top_provider": {
"max_completion_tokens": 64000,
"is_moderated": false
},
"supported_parameters": [
"max_tokens", "temperature", "top_p", "stop", "stream", "seed",
"frequency_penalty", "presence_penalty",
"tools", "tool_choice", "response_format"
],
"reasoning": {
"mandatory": false,
"supported_efforts": ["low", "medium", "high"],
"default_effort": "medium"
}
},
{
"id": "<image-model-id>",
"object": "model",
"created": 0,
"owned_by": "prostor",
"name": "Human-readable name",
"category": "image",
"architecture": {
"modality": "text->image",
"input_modalities": ["text"],
"output_modalities": ["image"]
},
"pricing": {
"prompt": "0",
"completion": "0",
"request": "1.5"
},
"prostor": {
"credits_per_request": 150,
"billing": "per_request"
}
}
],
"prostor": {
"credits_per_usd": 100,
"min_billed_credits": 1,
"billing_note": "..."
}
}Разбор полей:
| Поле | Тип | Что означает |
|---|---|---|
id | string | ID модели для поля model в запросах |
object | string | всегда "model" |
created | int | всегда 0 — в каталоге нет даты создания, поле есть только ради совместимости |
owned_by | string | всегда "prostor" |
name | string | человекочитаемое название |
category | string | text, image, video, photo-animation, embeddings и т. п. — определяет, на какой эндпоинт слать модель |
architecture.modality | string | строка вида text+image->text |
architecture.input_modalities | string[] | text, плюс image у vision-моделей |
architecture.output_modalities | string[] | text, image или video |
pricing.prompt, pricing.completion | string | розничная цена за один токен в USD, десятичной строкой (не в экспоненте) |
pricing.request | string | только у медиа: розничная цена одной генерации в USD |
prostor.billing | string | per_token или per_request |
prostor.credits_per_million_prompt / _completion | number | та же цена в кредитах за миллион токенов |
prostor.credits_per_request | int | только у медиа: фиксированная цена генерации в кредитах |
top_provider.max_completion_tokens | int | максимально достижимый потолок выходных токенов для вашего ключа — см. раздел про max_tokens |
top_provider.is_moderated | bool | всегда false |
supported_parameters | string[] | параметры, которые шлюз принимает и передаёт дальше |
reasoning | object | только у reasoning-моделей: mandatory, supported_efforts, default_effort |
Цены в pricing — розничные, ровно те, по которым вы платите; наценка уже включена. Точная сумма списания за конкретный запрос всегда приходит в usage.cost и usage.cost_details.billed_credits — см. Тарификацию.
supported_parameters — это контракт шлюза, а не гарантия модели. Список одинаков для всех текстовых моделей (плюс reasoning у reasoning-моделей) и означает лишь одно: шлюз не выкинет эти поля и передаст их дальше. Поддерживает ли конкретная модель seed, logprobs или строгий response_format — вопрос к самой модели: она вправе параметр проигнорировать или вернуть ошибку. У медиа-моделей блоков supported_parameters и top_provider нет вовсе.
GET /v1/models — не бесплатный «пинг». Он проходит полную авторизацию: при пустом балансе вернёт 402 insufficient_credits, а также расходует один запрос из минутного лимита запросов и max_output_tokens (по умолчанию 8192) из минутного лимита токенов. Кэшируйте каталог у себя, а не опрашивайте его перед каждым вызовом. Эндпоинты /v1/credits, /v1/key и /v1/generation этих проверок не делают: баланс должно быть можно прочитать и на нуле.
GET /v1/credits
Баланс кошелька. Кошелёк общий с веб-приложением Prostor: потраченное в приложении уменьшает доступное API, и наоборот.
curl -s "$PROSTOR_API_BASE_URL/credits" \
-H "Authorization: Bearer $PROSTOR_API_KEY" | jq .{
"data": {
"total_credits": 100,
"total_usage": 37.42
},
"prostor": {
"balance_credits": 6258,
"balance_usd": 62.58,
"granted_credits": 10000,
"spent_credits": 3742,
"api_usage_credits": 1190,
"api_usage_usd": 11.9,
"credits_per_usd": 100,
"wallet_note": "The balance is shared with the Prostor web app..."
}
}Блок data — OpenRouter-совместимый, значения в USD: total_credits — всё когда-либо начисленное, total_usage — всё когда-либо потраченное во всех интерфейсах. Блок prostor даёт то же самое в кредитах и отдельно api_usage_* — часть трат, прошедшую именно через этот API.
GET /v1/key
Профиль ключа, его лимиты и остатки. Алиас GET /v1/auth/key возвращает то же самое (старое написание OpenRouter).
curl -s "$PROSTOR_API_BASE_URL/key" \
-H "Authorization: Bearer $PROSTOR_API_KEY" | jq .{
"data": {
"label": "Production backend (...a1b2)",
"limit": null,
"limit_remaining": null,
"usage": 11.9,
"usage_daily": 0.85,
"usage_weekly": 4.1,
"usage_monthly": 11.9,
"is_free_tier": false,
"is_provisioning_key": false,
"rate_limit": { "requests": 120, "interval": "1m" },
"expires_at": null
},
"prostor": {
"key": {
"id": "key_...",
"name": "Production backend",
"last4": "a1b2",
"status": "active",
"scopes": ["chat:completions", "models:read", "images:generations", "video:generations", "embeddings"],
"allowed_models": [],
"created_at": "2026-07-01T09:12:33Z"
},
"balance": { "credits": 6258, "usd": 62.58 },
"usage": { "requests_total": 4120, "tokens_total": 8123456, "credits_total": 1190 },
"limits": {
"tokens_total": { "limit": null, "used": 8123456, "remaining": null },
"tokens_monthly": { "limit": 20000000, "used": 8123456, "remaining": 11876544 },
"credits_total": { "limit": null, "used": 1190, "remaining": null },
"requests_per_minute": { "limit": 120, "used": 0, "remaining": null },
"tokens_per_minute": { "limit": 200000, "used": 0, "remaining": null }
},
"max_output_tokens": 64000,
"concurrency_limit": 32,
"token_estimate": {
"note": "Linear projection of the balance at each model's retail price...",
"models": [
{ "model": "<model-id>", "prompt_tokens": 130375000, "completion_tokens": 15645000 }
]
}
}
}Что важно знать про этот ответ:
limitиlimit_remaining(в USD) заполнены, только если у ключа заданcredit_limit_total; иначеnull. Общий баланс кошелька они не отражают — его смотрите вprostor.balance.- В блоках
limits.*значениеlimit: nullиremaining: nullозначают «без ограничения»;used— всегда число. requests_per_minute.usedиtokens_per_minute.usedвсегда0, аremainingвсегдаnull— это не баг. Скользящие счётчики минутного лимитера намеренно не публикуются, а считать их по таблице использования значило бы недосчитать запросы «в полёте». Ориентируйтесь на429и заголовокRetry-After, а не на эти поля.token_estimate— линейная проекция баланса на розничную цену каждой модели. Она игнорирует округление вверх до целого кредита, поэтому немного завышает число мелких запросов, которые вы реально сможете сделать. Это прикидка, а не обещание. Для моделей с нулевым балансом список пустой; медиа-модели в него не входят.rate_limitравенnull, если у ключа нет минутного лимита запросов.
GET /v1/generation
Итог по конкретной генерации: сколько токенов, сколько денег списано, чем закончилось. Именно здесь лежит окончательная правда о стоимости — особенно для стриминга и async-видео.
curl -s "$PROSTOR_API_BASE_URL/generation?id=$GENERATION_ID" \
-H "Authorization: Bearer $PROSTOR_API_KEY" | jq .GENERATION_ID — значение заголовка X-Generation-Id (он же X-Request-Id) из ответа на генерацию; у non-streaming ответов он совпадает с полем id в теле.
{
"data": {
"id": "gen_...",
"model": "<model-id>",
"status": "success",
"created_at": "2026-07-14T10:21:44Z",
"tokens_prompt": 1204,
"tokens_completion": 318,
"tokens_total": 1522,
"tokens_cached": 0,
"total_cost": 0.05,
"cost_credits": 5,
"finish_reason": "stop",
"latency": 1843,
"is_byok": false,
"interrupted": false,
"is_estimated": false
}
}| Поле | Что означает |
|---|---|
status | success или error |
total_cost | сумма, списанная с вас, в USD |
cost_credits | она же в кредитах (целое число, точная величина списания) |
latency | длительность в миллисекундах |
finish_reason | причина остановки; client_disconnected, если клиент отключился на стриме |
interrupted | клиент отвалился или провайдер отдал ошибку по ходу стрима |
is_estimated | токены не пришли от провайдера и были оценены — см. раздел про streaming |
Записи видны только тому ключу, который сделал генерацию: чужой id — 404 generation_not_found. У ключей с включённым redact_logs запись не сохраняется вовсе, и эндпоинт вернёт 404 generation_not_retrievable. Внутренние поля маршрутизации, id апстрим-провайдера и его стоимость вырезаются из ответа. Единственное исключение — текст ошибки провайдера (error.message) может проксироваться как есть и изредка содержать формулировки, по которым угадывается апстрим.
POST /v1/chat/completions
Основной эндпоинт. Контракт OpenAI: messages, tools, response_format, stream — всё на своих местах.
curl -s "$PROSTOR_API_BASE_URL/chat/completions" \
-H "Authorization: Bearer $PROSTOR_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"$PROSTOR_MODEL\",
\"messages\": [
{\"role\": \"system\", \"content\": \"Отвечай кратко и по делу.\"},
{\"role\": \"user\", \"content\": \"Что такое Prostor API?\"}
],
\"temperature\": 0.3,
\"max_tokens\": 256
}" | jq .Поля запроса
| Поле | Тип | Обяз. | Комментарий |
|---|---|---|---|
model | string | да | один ID из GET /v1/models. Массив models не принимается |
messages | array | да | OpenAI-совместимые сообщения; vision-контент — у моделей с image во input_modalities |
max_tokens | number | нет | настоятельно рекомендуется: см. ниже про клэмп и про резерв |
max_completion_tokens | number | нет | синоним max_tokens |
temperature, top_p, stop, seed | — | нет | проксируются как есть |
frequency_penalty, presence_penalty | number | нет | проксируются как есть |
tools, tool_choice | — | нет | function calling |
response_format | object | нет | в т. ч. JSON schema |
reasoning | object | нет | только у моделей с блоком reasoning в каталоге |
stream | boolean | нет | включён в проде; читайте раздел про буферизацию |
models | array | — | не поддерживается, 400 unsupported_parameter |
Тело запроса не может превышать 8 MiB (иначе 400 invalid_request_body).
Входящие заголовки трассировки (X-Trace-ID и подобные) шлюзом не используются: он всегда генерирует собственный идентификатор запроса и отдаёт его в X-Request-Id. Клиент не может подделать корреляцию или устроить коллизию в статистике и биллинге.
Блок usage
В ответе usage — это счётчики провайдера плюс ваша стоимость:
{
"usage": {
"prompt_tokens": 1204,
"completion_tokens": 318,
"total_tokens": 1522,
"cost": 0.06,
"cost_details": {
"billed_credits": 6,
"markup_included": true
},
"is_estimated": false,
"interrupted": false
}
}cost — число в USD, ровно та сумма, которую списали с вашего кошелька; cost_details.billed_credits — она же в кредитах, целым числом. Это не цена провайдера и не «оценка»: это факт списания. Подробности округления — в Тарификации.
Потолок max_tokens (важно)
Явный max_tokens (или max_completion_tokens) шлюз уважает как запрошено: резервирует и форвардит ровно указанное значение, а реальный предел применяет уже провайдер модели. Ограничителей два:
- жёсткий потолок шлюза (по умолчанию 64000) — страховка от абсурдных значений; запрос выше него молча понижается до него;
max_output_tokensвашего ключа, если админ его задал — жёсткий потолок именно этого ключа (смотрите его вGET /v1/keyи вtop_provider.max_completion_tokens).
Значение 8192 — это лишь дефолт: он подставляется, только если вы не передали ни max_tokens, ни max_completion_tokens.
Практическое следствие: запрос на 32000 токенов теперь честно отдаёт до 32000. Если ответ обрывается на finish_reason: "length" — это ваш max_tokens или реальный предел модели у провайдера, а не потолок шлюза (пока вы под жёстким потолком).
Всегда передавайте реалистичный max_tokens. Он страхует от обрыва и напрямую задаёт предавторизационный холд на балансе: шлюз резервирует деньги под худший случай — полный max_tokens. max_tokens: 256 вместо дефолта в 8192 уменьшает соответствующую часть резерва в 32 раза; наоборот, max_tokens: 64000 увеличит холд, и пачка таких параллельных запросов быстрее упрётся в баланс. Это главное лекарство от неожиданных 402 insufficient_credits при живом балансе — подробности в Тарификации.
Streaming: честно про буферизацию
curl -N "$PROSTOR_API_BASE_URL/chat/completions" \
-H "Authorization: Bearer $PROSTOR_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"$PROSTOR_MODEL\",
\"messages\": [{\"role\": \"user\", \"content\": \"Напиши хайку про кэш.\"}],
\"max_tokens\": 128,
\"stream\": true
}"stream: true включён в проде и отдаёт корректный SSE: те же data:-кадры с дельтами, тот же терминальный [DONE], тот же финальный usage-чанк. Совместимость с OpenAI SDK полная.
Но на /v1/chat/completions стрим буферизуется. Кадры копятся на шлюзе и уходят клиенту одним пакетом после того, как генерация полностью завершилась. Time-to-first-token равен времени полного ответа: вы мгновенно получаете 200 и SSE-заголовки, затем тишину, затем весь поток разом. Выигрыша по латентности здесь нет — только совместимость формата.
Так сделано намеренно. В Chat Completions нет стандартного события «отмотать назад», а шлюз должен уметь бесшовно перекинуть запрос на резервного провайдера, если первый сломался посреди генерации. Атомарная попытка позволяет выбросить частичный текст сбойного провайдера и отдать вместо него целый ответ резервного — вы никогда не увидите склейки двух разных ответов.
Нужна настоящая пословная выдача и низкий TTFT — используйте POST /v1/responses со стримом. Там кадры уходят клиенту сразу, по мере поступления.
Что ещё нужно знать о стриминге:
- Финальный usage-чанк приходит всегда (шлюз сам включает
stream_options.include_usage) и содержитcostиcost_details.billed_credits— вашу стоимость. - Заголовок
X-Prostor-Cost-Creditsна стриминговых ответах не приходит: заголовки уходят до того, как стоимость известна. Берите цифру из финального usage-чанка или изGET /v1/generation. Idempotency-Keyна стриминговых запросах игнорируется.- Разрыв соединения не отменяет и не возвращает генерацию. Шлюз дочитывает апстрим до конца, чтобы получить достоверный usage, и списывает деньги. В
GET /v1/generationтакая запись получитinterrupted: trueиfinish_reason: "client_disconnected". Не используйте отмену запроса как способ сэкономить. - Если провайдер не прислал usage-чанк, токены оцениваются, и оценка не меньше зарезервированного
max_tokens— то есть при отсутствии usage вы платите как за полный лимит. Такая запись помечаетсяis_estimated: trueвGET /v1/generation. Ещё одна причина ставить трезвыйmax_tokens. - Слишком большой ответ обрывается: клиент получает SSE-кадр с
{"error":{"message":"upstream response exceeded the safe streaming limit","type":"stream_error"}}и[DONE]. - Максимальная длительность стрима — 20 минут (
API_ACCESS_STREAM_TIMEOUT).
POST /v1/responses
Responses API, только stateless. Тот же scope chat:completions, тот же каталог моделей, тот же биллинг.
curl -s "$PROSTOR_API_BASE_URL/responses" \
-H "Authorization: Bearer $PROSTOR_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"$PROSTOR_MODEL\",
\"instructions\": \"Отвечай кратко.\",
\"input\": \"Что такое fallback routing?\",
\"max_output_tokens\": 256,
\"store\": false
}" | jq .Поддержаны: строковый и message-input, vision по URL и data-URI, function tools (включая strict) и function_call_output, text.format, reasoning-контролы, metadata, logprobs, типизированный SSE. max_output_tokens подчиняется той же логике: явное значение уважается как запрошено (до жёсткого потолка шлюза либо потолка ключа), а дефолт 8192 подставляется только если поле не передано. Блок usage (в том числе вложенный response.usage) переписывается так же, как в чате: несёт cost, cost_details.billed_credits.
Стриминг здесь настоящий: кадры уходят клиенту по мере генерации, без буферизации.
Возвращают 400 с точной причиной (запрос не выполняется и деньги не списываются):
| Возможность | Почему |
|---|---|
store: true | состояние не хранится |
previous_response_id | нет серверных цепочек |
| Conversations API | не реализован |
| background mode | не реализован |
truncation: "auto" | не реализован |
| native compaction, stored prompts, moderation | не реализованы |
max_tool_calls | не реализован |
| encrypted reasoning state | не реализован |
| Files-backed input и hosted tools, зависящие от Files | Files API отсутствует |
Ничего из перечисленного не игнорируется молча — вы получите явную ошибку, а не тихо изменённое поведение.
Медиа: изображения и видео
Медиа тарифицируется фиксированной ценой за генерацию (prostor.credits_per_request из каталога, розничная цена уже включает наценку), а не по токенам. Токенных лимитов у него нет, а цена ошибки выше.
Отправляйте медиа-запросы с заголовком Idempotency-Key. Сетевой таймаут на клиенте при уже принятом сервером запросе без него означает вторую генерацию и второе списание фиксированной цены.
Текстовую модель нельзя отправить на медиа-эндпоинт и наоборот: это 400 model_not_allowed_on_endpoint. Сверяйтесь с полем category в каталоге.
Изображения
curl -s "$PROSTOR_API_BASE_URL/images/generations" \
-H "Authorization: Bearer $PROSTOR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d "{
\"model\": \"$PROSTOR_IMAGE_MODEL\",
\"prompt\": \"a cat riding a bike, watercolor\",
\"n\": 1,
\"size\": \"1024x1024\",
\"response_format\": \"url\"
}" | jq .Ответ OpenAI-совместимый: { "created", "data": [{ "url" }], "usage" }. При "response_format": "b64_json" картинка приходит base64 в том же массиве. Блок usage несёт cost и cost_details.billed_credits — фиксированную цену генерации.
Видео синхронно
curl -s "$PROSTOR_API_BASE_URL/videos/generations" \
-H "Authorization: Bearer $PROSTOR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d "{
\"model\": \"$PROSTOR_VIDEO_MODEL\",
\"prompt\": \"ocean waves at sunset\",
\"duration\": 5
}" | jq .Годится только для короткого видео: длинная генерация упрётся в таймаут HTTP-запроса. Для всего остального — async.
Видео асинхронно: submit и poll
JOB=$(curl -s "$PROSTOR_API_BASE_URL/videos/submit" \
-H "Authorization: Bearer $PROSTOR_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: $(uuidgen)" \
-d "{
\"model\": \"$PROSTOR_VIDEO_MODEL\",
\"prompt\": \"ocean waves at sunset\",
\"duration\": 5
}")
curl -s "$PROSTOR_API_BASE_URL/videos/status" \
-H "Authorization: Bearer $PROSTOR_API_KEY" \
-H "Content-Type: application/json" \
-d "$JOB" | jq .submit возвращает job_id и status. Для опроса достаточно передать job_id в /videos/status (отправить весь ответ submit целиком, как в примере, тоже можно) и повторять, пока status не станет completed. Маршрутизацию к нужному апстриму gateway восстанавливает сам по job_id — вам её знать не нужно, и наружу она не отдаётся.
Биллинг async-видео:
- на
submitфиксированная цена резервируется на балансе (в ответеusage.cost— сумма холда, то есть будущая стоимость); - на
status: completedрезерв списывается; - при ошибке генерации резерв возвращается полностью;
- брошенное задание, которое так и не опросили, освобождает резерв автоматически по таймауту.
В ответе /videos/status поля cost нет: списание происходит уже после отправки ответа. Окончательную сумму читайте в GET /v1/generation?id= по идентификатору генерации.
Эмбеддинги
curl -s "$PROSTOR_API_BASE_URL/embeddings" \
-H "Authorization: Bearer $PROSTOR_API_KEY" \
-H "Content-Type: application/json" \
-d "{
\"model\": \"$PROSTOR_EMBEDDING_MODEL\",
\"input\": [\"hello world\", \"prostor api\"]
}" | jq .Эмбеддинги тарифицируются по токенам, как чат, а не по фиксированной цене.
Idempotency-Key
Безопасный повтор запроса без риска заплатить дважды. Заголовок:
Idempotency-Key: <любая уникальная строка, например UUID>Работает на шести эндпоинтах: POST /v1/chat/completions и POST /v1/responses (только без stream), POST /v1/images/generations, POST /v1/videos/generations, POST /v1/videos/submit, POST /v1/embeddings.
Поведение:
- первый запрос выполняется как обычно; при успешном (2xx) ответе тело сохраняется;
- повтор с тем же ключом отдаёт сохранённый ответ дословно, с заголовком
Idempotent-Replayed: true, и ничего не списывает; - повтор, пока первый запрос ещё в полёте, —
409 idempotency_conflict; - если первый запрос завершился ошибкой (не 2xx), ключ освобождается: повтор выполнится нормально;
- записи хранятся 24 часа, потом ключ можно использовать снова.
Ключ привязан к пути и к телу запроса: внутри хранится отпечаток Idempotency-Key + хэш(путь + тело). Тот же Idempotency-Key с хоть чуть-чуть другим телом — это другой запрос: он выполнится и будет оплачен. Идемпотентность защищает от повтора одного и того же вызова, а не даёт «переиспользовать» ключ.
Не работает: на любом стриминговом запросе (заголовок просто игнорируется), на POST /v1/videos/status, на GET-эндпоинтах, а также на ключах с включённым redact_logs — там дедупликация потребовала бы хранить полное тело ответа сутки, что противоречит смыслу режима.
Заголовки ответа
| Заголовок | Когда | Что в нём |
|---|---|---|
X-Request-Id | на всех генерациях | идентификатор запроса; его же цитируйте в обращении в поддержку |
X-Generation-Id | на всех генерациях | то же значение; передаётся в GET /v1/generation?id= |
X-Prostor-Cost-Credits | non-streaming, при списании > 0 | целое число списанных кредитов |
Retry-After | на каждом 429 | через сколько секунд повторять (минимум 1) |
X-RateLimit-Reset | на каждом 429 | UNIX-таймстамп (в секундах), когда можно повторять |
Idempotent-Replayed | при воспроизведении из кэша | true |
CORS открыт (Access-Control-Allow-Origin: *), браузерному коду доступны на чтение ровно эти шесть заголовков. Это не повод класть ключ в браузер — см. «Безопасность».
Лимиты ключа
Все лимиты — необязательные; null означает «без ограничения». Текущие значения своего ключа смотрите в GET /v1/key.
| Поле ключа | Что ограничивает | Ошибка при превышении |
|---|---|---|
scopes | доступные эндпоинты (пять scope) | 403 scope_denied |
allowed_models | белый список моделей; пустой = все | 403 model_not_allowed |
max_output_tokens | жёсткий потолок выходных токенов ключа (если задан); иначе действует общий потолок шлюза 64000, а 8192 — лишь дефолт для запросов без max_tokens | ошибки нет — значение выше потолка молча понижается |
concurrency_limit | одновременные запросы ключа; по умолчанию 32 | 429 concurrency_limit_exceeded |
request_limit_minute | запросов в минуту | 429 rate_limit_exceeded |
token_limit_minute | токенов в минуту | 429 rate_limit_exceeded |
token_limit_total | токенов за всё время | 429 token_limit_exceeded |
token_limit_monthly | токенов в календарный месяц | 429 monthly_token_limit_exceeded |
credit_limit_total | кредитов за всё время | 429 credit_limit_exceeded |
expires_at | срок действия ключа | 401 key_expired |
redact_logs | режим минимизации данных | см. ниже |
Сверх этого действует общий предел мощности шлюза: при перегрузке всей платформы запрос получит 429 gateway_overloaded — повторите с задержкой из Retry-After.
Минутный токенный лимит списывает не фактический выход, а зарезервированный: ваш max_tokens (или дефолтные 8192, если поле не передано) за каждый запрос, даже если модель ответила одним словом. Явный, но не раздутый max_tokens — снова лучший способ не упереться в лимит на ровном месте.
Про redact_logs. Это режим ключа для чувствительных данных: содержимое и метаданные запроса не сохраняются. Плата за это — две потери функциональности: GET /v1/generation вернёт 404 generation_not_retrievable, а Idempotency-Key перестаёт действовать (дедупликация требует хранить ответ). Списание при этом ведётся как обычно.
Ошибки
Все ошибки приходят в едином конверте:
{
"error": {
"message": "unknown model: foo — call GET /v1/models for the models this key can use",
"code": 404,
"type": "not_found",
"metadata": {
"prostor_code": "model_not_found"
}
}
}error.code — числовой HTTP-статус (не строка), error.type — одна из категорий (authentication, permission_denied, payment_required, rate_limit_exceeded, provider_unavailable, not_found, timeout, invalid_request, server), а error.metadata.prostor_code — точный машиночитаемый код. Ветвитесь по prostor_code, логируйте X-Request-Id.
Коды, специфичные для этой страницы: model_not_found (404), model_not_allowed_on_endpoint (400), unsupported_parameter (400, массив models), missing_generation_id (400), idempotency_conflict (409), generation_not_retrievable (404). Полный каталог всех кодов, их причин и правильной стратегии повтора — в Ошибках.
SDK
Prostor совместим с официальными клиентами OpenAI: достаточно подменить base_url.
Python
import os
from openai import OpenAI
client = OpenAI(
api_key=os.environ["PROSTOR_API_KEY"],
base_url="https://api.prostor.ai/v1",
)
# Шаг 1: узнать, какие модели доступны этому ключу.
models = client.models.list()
model_id = models.data[0].id
# Шаг 2: обычный chat completion.
completion = client.chat.completions.create(
model=model_id,
messages=[{"role": "user", "content": "Скажи привет в одну строку."}],
max_tokens=128,
)
print(completion.choices[0].message.content)
print("Списано (USD):", completion.usage.model_dump().get("cost"))Идемпотентный повтор для медиа — через дополнительные заголовки:
image = client.images.generate(
model="<image-model-id из client.models.list()>",
prompt="a cat riding a bike, watercolor",
n=1,
size="1024x1024",
extra_headers={"Idempotency-Key": "1f0b6f2e-8f5f-4f0a-9a3f-0f0d1e2b3c4d"},
)
print(image.data[0].url)JavaScript
import OpenAI from "openai";
const client = new OpenAI({
apiKey: process.env.PROSTOR_API_KEY,
baseURL: "https://api.prostor.ai/v1",
});
const models = await client.models.list();
const modelId = models.data[0].id;
const completion = await client.chat.completions.create({
model: modelId,
messages: [{ role: "user", content: "Скажи привет в одну строку." }],
max_tokens: 128,
});
console.log(completion.choices[0]?.message?.content);
console.log("Списано (USD):", completion.usage?.cost);Стриминг через SDK работает штатно — помните только, что на /v1/chat/completions кадры придут одним пакетом в конце:
const stream = await client.chat.completions.create({
model: modelId,
messages: [{ role: "user", content: "Напиши хайку про кэш." }],
max_tokens: 128,
stream: true,
});
for await (const chunk of stream) {
process.stdout.write(chunk.choices[0]?.delta?.content ?? "");
}Fetch без SDK
const response = await fetch("https://api.prostor.ai/v1/chat/completions", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.PROSTOR_API_KEY}`,
"Content-Type": "application/json",
"Idempotency-Key": crypto.randomUUID(),
},
body: JSON.stringify({
model: modelId,
messages: [{ role: "user", content: "Hello from fetch" }],
max_tokens: 128,
}),
});
if (!response.ok) {
const body = await response.json();
throw new Error(`${body.error?.metadata?.prostor_code}: ${body.error?.message}`);
}
const data = await response.json();
console.log(data.choices?.[0]?.message?.content);
console.log("request id:", response.headers.get("X-Request-Id"));Безопасность
- Ключ
pak_pr_v1_...— это доступ к вашему кошельку. Не кладите его во frontend-бандл, мобильное приложение или публичный репозиторий: браузерный код читаем всеми. - Для браузерных приложений ходите в Prostor со своего backend, а ключ храните на сервере.
- Полное значение ключа показывается один раз при выдаче — дальше хранится только хэш.
- При подозрении на утечку немедленно отзовите ключ и выпустите новый; отзыв действует сразу (
401 key_revoked). - Выдавайте ключам минимально нужные scope и заполняйте
allowed_models— скомпрометированный ключ с одной дешёвой моделью и лимитом кредитов стоит несравнимо дешевле, чем ключ без ограничений. - Ставьте
credit_limit_totalиrequest_limit_minuteна интеграционные ключи: это ваш аварийный тормоз при зацикленном ретрае в чужом коде. - Ваш
Authorizationне пересылается провайдерам. Обратное тоже верно: структурные поля с именем провайдера, его ценой и маршрутом запроса вырезаются из всех ответов (единственное исключение — свободный текстerror.messageот провайдера проксируется как есть).