Prostor Docs
Public API

API Reference

Полный справочник публичного Prostor API: base URL, ключи, все эндпоинты, форматы ответов, streaming, идемпотентность и лимиты.

Prostor API — это OpenAI-совместимый HTTP API поверх общего каталога моделей: текст, vision, изображения, видео и эмбеддинги через один ключ и один баланс. Форма ответов повторяет OpenAI, а служебные эндпоинты (/v1/credits, /v1/key, /v1/generation) — OpenRouter, чтобы существующие клиенты заводились без переписывания.

Эта страница — справочник эндпоинтов. Деньги и формула списания описаны в Тарификации, полный каталог ошибок — в Ошибках.

Base URL

text
https://api.prostor.ai/v1

Все пути ниже указаны относительно этого базового URL. Держите его и ключ в переменных окружения — все примеры на странице рассчитаны на них:

bash
export PROSTOR_API_BASE_URL="https://api.prostor.ai/v1"
export PROSTOR_API_KEY="pak_pr_v1_..."

Аутентификация

Ключ передаётся в заголовке Authorization:

http
Authorization: Bearer pak_pr_v1_...

Публичные ключи всегда начинаются с префикса pak_pr_v1_. Полное значение ключа показывается один раз в момент выдачи — в системе хранится только его хэш, восстановить ключ нельзя, потерянный ключ можно только выпустить заново.

Важно

Внутренние ключи routing core (sk_...) на публичном API не работают: они отклоняются с 401 internal_token_not_allowed. Если вы получили ключ, который не начинается с pak_pr_v1_, это не ключ внешнего API.

Быстрая проверка связности — запрос без ключа обязан вернуть 401:

bash
curl -i https://api.prostor.ai/v1/models

Первый шаг: возьмите ID модели из каталога

Важно

Не хардкодьте ID модели из чужих примеров. Каталог Prostor меняется: модели добавляются, дорожают, отключаются, а у вашего ключа может быть свой белый список (allowed_models). Единственный достоверный источник ID — GET /v1/models этим же ключом.

bash
curl -s "$PROSTOR_API_BASE_URL/models" \
  -H "Authorization: Bearer $PROSTOR_API_KEY" | jq -r '.data[] | "\(.id)\t\(.category)"'

Возьмите нужный id из data[].id и подставьте его в примеры:

bash
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/completionschat:completionsподдерживается, в том числе stream: true
POST /v1/responseschat:completionsподдерживается, только stateless
GET /v1/modelsmodels:readподдерживается
GET /v1/models/usermodels: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/generationsimages:generationsподдерживается
POST /v1/videos/generationsvideo:generationsподдерживается (синхронно)
POST /v1/videos/submitvideo:generationsподдерживается (async)
POST /v1/videos/statusvideo:generationsподдерживается (async)
POST /v1/embeddingsembeddingsподдерживается
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/providers404 not_found. Провайдеры — наша внутренняя кухня и наружу не раскрываются
GET /v1/models/{author}/{slug}/endpoints404 not_found. Есть только плоский GET /v1/models
POST /v1/audio/* (speech, transcriptions)404 not_found. Аудио будет отдельным расширением
Массив models (client-side fallback list)400 unsupported_parameter
Stateful-возможности Responses400 с точной причиной, см. раздел ниже
Любой другой путь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".

bash
curl -s "$PROSTOR_API_BASE_URL/models" \
  -H "Authorization: Bearer $PROSTOR_API_KEY" | jq .

Ответ (значения моделей — плейсхолдеры, реальные ID берите из своего ответа):

json
{
  "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": "..."
  }
}

Разбор полей:

ПолеТипЧто означает
idstringID модели для поля model в запросах
objectstringвсегда "model"
createdintвсегда 0 — в каталоге нет даты создания, поле есть только ради совместимости
owned_bystringвсегда "prostor"
namestringчеловекочитаемое название
categorystringtext, image, video, photo-animation, embeddings и т. п. — определяет, на какой эндпоинт слать модель
architecture.modalitystringстрока вида text+image->text
architecture.input_modalitiesstring[]text, плюс image у vision-моделей
architecture.output_modalitiesstring[]text, image или video
pricing.prompt, pricing.completionstringрозничная цена за один токен в USD, десятичной строкой (не в экспоненте)
pricing.requeststringтолько у медиа: розничная цена одной генерации в USD
prostor.billingstringper_token или per_request
prostor.credits_per_million_prompt / _completionnumberта же цена в кредитах за миллион токенов
prostor.credits_per_requestintтолько у медиа: фиксированная цена генерации в кредитах
top_provider.max_completion_tokensintмаксимально достижимый потолок выходных токенов для вашего ключа — см. раздел про max_tokens
top_provider.is_moderatedboolвсегда false
supported_parametersstring[]параметры, которые шлюз принимает и передаёт дальше
reasoningobjectтолько у 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, и наоборот.

bash
curl -s "$PROSTOR_API_BASE_URL/credits" \
  -H "Authorization: Bearer $PROSTOR_API_KEY" | jq .
json
{
  "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).

bash
curl -s "$PROSTOR_API_BASE_URL/key" \
  -H "Authorization: Bearer $PROSTOR_API_KEY" | jq .
json
{
  "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-видео.

bash
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 в теле.

json
{
  "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
  }
}
ПолеЧто означает
statussuccess или error
total_costсумма, списанная с вас, в USD
cost_creditsона же в кредитах (целое число, точная величина списания)
latencyдлительность в миллисекундах
finish_reasonпричина остановки; client_disconnected, если клиент отключился на стриме
interruptedклиент отвалился или провайдер отдал ошибку по ходу стрима
is_estimatedтокены не пришли от провайдера и были оценены — см. раздел про streaming

Записи видны только тому ключу, который сделал генерацию: чужой id404 generation_not_found. У ключей с включённым redact_logs запись не сохраняется вовсе, и эндпоинт вернёт 404 generation_not_retrievable. Внутренние поля маршрутизации, id апстрим-провайдера и его стоимость вырезаются из ответа. Единственное исключение — текст ошибки провайдера (error.message) может проксироваться как есть и изредка содержать формулировки, по которым угадывается апстрим.

POST /v1/chat/completions

Основной эндпоинт. Контракт OpenAI: messages, tools, response_format, stream — всё на своих местах.

bash
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 .

Поля запроса

ПолеТипОбяз.Комментарий
modelstringдаодин ID из GET /v1/models. Массив models не принимается
messagesarrayдаOpenAI-совместимые сообщения; vision-контент — у моделей с image во input_modalities
max_tokensnumberнетнастоятельно рекомендуется: см. ниже про клэмп и про резерв
max_completion_tokensnumberнетсиноним max_tokens
temperature, top_p, stop, seedнетпроксируются как есть
frequency_penalty, presence_penaltynumberнетпроксируются как есть
tools, tool_choiceнетfunction calling
response_formatobjectнетв т. ч. JSON schema
reasoningobjectнеттолько у моделей с блоком reasoning в каталоге
streambooleanнетвключён в проде; читайте раздел про буферизацию
modelsarrayне поддерживается, 400 unsupported_parameter

Тело запроса не может превышать 8 MiB (иначе 400 invalid_request_body).

Входящие заголовки трассировки (X-Trace-ID и подобные) шлюзом не используются: он всегда генерирует собственный идентификатор запроса и отдаёт его в X-Request-Id. Клиент не может подделать корреляцию или устроить коллизию в статистике и биллинге.

Блок usage

В ответе usage — это счётчики провайдера плюс ваша стоимость:

json
{
  "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: честно про буферизацию

bash
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, тот же каталог моделей, тот же биллинг.

bash
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, зависящие от FilesFiles API отсутствует

Ничего из перечисленного не игнорируется молча — вы получите явную ошибку, а не тихо изменённое поведение.

Медиа: изображения и видео

Медиа тарифицируется фиксированной ценой за генерацию (prostor.credits_per_request из каталога, розничная цена уже включает наценку), а не по токенам. Токенных лимитов у него нет, а цена ошибки выше.

Важно

Отправляйте медиа-запросы с заголовком Idempotency-Key. Сетевой таймаут на клиенте при уже принятом сервером запросе без него означает вторую генерацию и второе списание фиксированной цены.

Текстовую модель нельзя отправить на медиа-эндпоинт и наоборот: это 400 model_not_allowed_on_endpoint. Сверяйтесь с полем category в каталоге.

Изображения

bash
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 — фиксированную цену генерации.

Видео синхронно

bash
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

bash
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= по идентификатору генерации.

Эмбеддинги

bash
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

Безопасный повтор запроса без риска заплатить дважды. Заголовок:

http
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-Creditsnon-streaming, при списании > 0целое число списанных кредитов
Retry-Afterна каждом 429через сколько секунд повторять (минимум 1)
X-RateLimit-Resetна каждом 429UNIX-таймстамп (в секундах), когда можно повторять
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одновременные запросы ключа; по умолчанию 32429 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 перестаёт действовать (дедупликация требует хранить ответ). Списание при этом ведётся как обычно.

Ошибки

Все ошибки приходят в едином конверте:

json
{
  "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

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"))

Идемпотентный повтор для медиа — через дополнительные заголовки:

python
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

js
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 кадры придут одним пакетом в конце:

js
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

js
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 от провайдера проксируется как есть).