Prostor Docs
Public API

Цены и списания

Кредиты и доллары, точная формула списания, округление до целого кредита, где взять цену модели и как сверить фактический счёт.

Эта страница отвечает на два вопроса: сколько будет стоить запрос и сколько с меня списали на самом деле. Второй вопрос всегда важнее первого: точную сумму возвращает сам API, и её не нужно вычислять.

Две единицы: кредиты и доллары

Внутренняя валюта Prostor — кредиты. Доллары в ответах API — это OpenRouter-совместимая «обёртка» поверх кредитов, чтобы работали клиенты и SDK, написанные под OpenRouter.

Курс в production фиксирован:

text
1 кредит = $0.01
credits_per_usd = 100

Курс не зашит в клиент — он всегда приходит в ответе: GET /v1/modelsprostor.credits_per_usd, GET /v1/creditsprostor.credits_per_usd, GET /v1/keyprostor.credits_per_usd. Читайте его оттуда, а не из этой страницы.

Каждое денежное поле приходит в обеих единицах:

ЧтоДолларыКредиты
Стоимость запросаusage.costusage.cost_details.billed_credits (и заголовок X-Prostor-Cost-Credits)
Балансprostor.balance_usdprostor.balance_credits
Расход по ключуdata.usage, data.usage_daily, …prostor.usage.credits_total
Стоимость прошлой генерацииdata.total_costdata.cost_credits

Кредиты — это то, что реально списывается с кошелька: целое число, всегда. Доллары — производная величина.

Где лежат цены

Единственный источник цен — GET /v1/models. Он же единственный источник списка моделей, доступных вашему ключу.

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

Сокращённый ответ (значения — иллюстративные; id берите из своего ответа):

json
{
  "object": "list",
  "data": [
    {
      "id": "<model-id-from-/v1/models>",
      "object": "model",
      "created": 0,
      "owned_by": "prostor",
      "name": "Example Text Model",
      "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"]
    },
    {
      "id": "<image-model-id-from-/v1/models>",
      "object": "model",
      "category": "image",
      "pricing": {
        "prompt": "0",
        "completion": "0",
        "request": "1.5"
      },
      "prostor": {
        "credits_per_request": 150,
        "billing": "per_request"
      }
    }
  ],
  "total_count": 2,
  "prostor": {
    "credits_per_usd": 100,
    "min_billed_credits": 1,
    "billing_note": "Prices are per token in USD and already include markup. ..."
  }
}

Как это читать:

  • pricing.prompt и pricing.completion — цена за один токен в долларах, десятичной строкой (не за миллион). "0.00000048" — это $0.48 за миллион prompt-токенов.
  • Это конечная цена, которую платите вы. Ничего сверху к ней не добавляется.
  • Строки, а не числа — намеренно: 2.5e-06 в некоторых клиентах парсится в 0. Приводите к числу сами.
  • prostor.credits_per_million_prompt / credits_per_million_completion — та же цена, сразу в кредитах за миллион токенов. Если считаете в кредитах, берите эти поля.
  • prostor.billingper_token (текст, эмбеддинги) или per_request (медиа).
  • У медиа-моделей есть pricing.request — плоская цена за одну генерацию, и prostor.credits_per_request. Полей top_provider и supported_parameters у них нет.
  • category говорит, каким эндпоинтом пользоваться: text/chat/completions и /responses, image/images/generations, video/videos/*, embeddings/embeddings.
Заметка

Не хардкодьте ID моделей. Каталог меняется, и он ещё фильтруется по allowed_models вашего ключа. Неизвестная модель → 404 model_not_found с сообщением «unknown model: X — call GET /v1/models for the models this key can use».

Точная формула списания

Списывается всегда целое число кредитов. Путь от «токены × цена» до этого целого числа — два шага:

  1. Стоимость в долларах. prompt_tokens × цена_prompt + completion_tokens × цена_completion. Цены — те, что лежат в GET /v1/models (розничные, наценка уже включена).
  2. Округление вверх до целого кредита — один раз. Доллары переводятся в кредиты по курсу credits_per_usd (100) и округляются вверх (ceil): billed_credits = ceil(usd × 100). Дробных кредитов не бывает, поэтому любой платный запрос стоит минимум 1 кредит ($0.01). Отдельного «минимального чека» нет — только округление до целого. Порог приходит в prostor.min_billed_credits.

Если стоимость ровно ноль (например, нулевой usage) — списывается 0.

Важно

tokens × price — это нижняя граница, а не счёт: фактическое списание округляется вверх до целого кредита, поэтому всегда больше либо равно оценке (максимум на ~1 кредит больше). Не стройте биллинг на своей оценке: точная сумма приходит в ответе (usage.cost / usage.cost_details.billed_credits).

Оценка сверху, если она всё-таки нужна:

text
billed_credits = ceil(usd_by_price_list × credits_per_usd)   // минимум 1 за платный запрос

Пример 1: маленький запрос

Возьмём модель с ценами из примера выше: pricing.prompt = "0.00000048", pricing.completion = "0.000004" (то есть $0.48 / $4.00 за миллион токенов). Запрос: 1 000 prompt-токенов и 500 completion-токенов.

text
usd     = 1000 × 0.00000048 + 500 × 0.000004
        = 0.00048 + 0.002
        = 0.00248 USD

credits = ceil(0.00248 × 100) = ceil(0.248) = 1 кредит

Списано: 1 кредит = $0.01. Оценка «по токенам» дала бы $0.0025 — округление вверх до целого кредита подняло её до одного цента. На коротких запросах разница со счётом — не больше одного кредита.

Ответ покажет ровно это:

json
"usage": {
  "prompt_tokens": 1000,
  "completion_tokens": 500,
  "total_tokens": 1500,
  "cost": 0.01,
  "cost_details": { "billed_credits": 1, "markup_included": true },
  "is_estimated": false,
  "interrupted": false
}

Пример 2: большой запрос

Та же модель, 100 000 prompt + 20 000 completion:

text
usd     = 100000 × 0.00000048 + 20000 × 0.000004
        = 0.048 + 0.08
        = 0.128 USD

credits = ceil(0.128 × 100) = ceil(12.8) = 13 кредитов

Списано: 13 кредитов = $0.13. На объёмных запросах округление до целого кредита практически незаметно — оценка по прайсу почти точна.

Совет

Округление добавляет к запросу самое большее один кредит ($0.01). Даже на сценарии из тысяч мелких запросов накрутка сверх токенной оценки не превышает количество_запросов × 1 кредит, а обычно меньше.

Как узнать точную сумму (три способа)

Не оценивайте — читайте. Одно и то же число доступно тремя путями.

1. Тело ответа: usage.cost и usage.cost_details

Есть на каждом ответе генерации — chat, responses, images, videos, embeddings.

ПолеТипЗначение
usage.costчислосколько списано с вас, в долларах
usage.cost_details.billed_creditsцелоесколько списано, в кредитах — это и есть авторитетное число
usage.cost_details.markup_includedtrueцена конечная, ничего не добавится
usage.is_estimatedboolusage пришлось оценить, а не получить от провайдера (см. ниже)
usage.interruptedboolклиент отвалился или генерация оборвалась — но запрос всё равно оплачен
bash
curl -s "$PROSTOR_API_BASE_URL/chat/completions" \
  -H "Authorization: Bearer $PROSTOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "<model-id-from-/v1/models>",
    "messages": [{"role": "user", "content": "Привет"}],
    "max_tokens": 128
  }' | jq '.usage'

2. Заголовок ответа: X-Prostor-Cost-Credits

Целое число кредитов, прямо в HTTP-заголовках. Не нужно парсить тело.

bash
curl -sD - -o /dev/null "$PROSTOR_API_BASE_URL/chat/completions" \
  -H "Authorization: Bearer $PROSTOR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "model": "<model-id-from-/v1/models>",
    "messages": [{"role": "user", "content": "Привет"}],
    "max_tokens": 128
  }' | grep -i -E 'x-prostor-cost-credits|x-request-id'
text
X-Request-Id: <generation-id>
X-Generation-Id: <generation-id>
X-Prostor-Cost-Credits: 5

Оговорки, важные на практике:

  • Заголовок ставится, только если списание больше нуля. Нет заголовка — не было платного списания (или это ошибка).
  • На стриминговом ответе его нет никогда: заголовки уходят до того, как стоимость известна. Для стрима читайте usage в финальном чанке (stream_options.include_usage включается автоматически) либо GET /v1/generation.
  • X-Request-Id и X-Generation-Id — одно и то же значение. Это тот ID, который надо назвать в баг-репорте и передать в GET /v1/generation?id=.

3. Постфактум: GET /v1/generation?id=

Авторитетная запись из нашего биллинга. Работает после того, как запрос полностью рассчитан.

bash
curl -s "$PROSTOR_API_BASE_URL/generation?id=<generation-id>" \
  -H "Authorization: Bearer $PROSTOR_API_KEY" | jq .
json
{
  "data": {
    "id": "<generation-id>",
    "model": "<model-id>",
    "status": "success",
    "created_at": "2026-07-14T10:12:31Z",
    "tokens_prompt": 1000,
    "tokens_completion": 500,
    "tokens_total": 1500,
    "tokens_cached": 0,
    "total_cost": 0.05,
    "cost_credits": 5,
    "finish_reason": "stop",
    "latency": 1840,
    "is_byok": false,
    "interrupted": false,
    "is_estimated": false
  }
}

total_cost — доллары, cost_credits — кредиты. statussuccess или error. Запись видна только тому ключу, который её создал: чужой id даёт 404 generation_not_found.

Заметка

Для async-видео (POST /videos/submitPOST /videos/status) ответ videos/status не содержит итоговой стоимости: расчёт происходит уже после того, как ответ ушёл клиенту. Фактическое списание по такому джобу смотрите через GET /v1/generation?id=.

Конвертация кредитов и долларов

Формула одна, курс берётся из prostor.credits_per_usd (production: 100).

text
usd     = credits / credits_per_usd
credits = usd × credits_per_usd

Пример: списание 13 кредитов → 13 / 100 = $0.13. Пополнение на $2525 × 100 = 2500 кредитов.

Если вы считаете бюджет в кредитах (а так удобнее — это единица списания), не переводите цены в доллары вообще. Берите готовые поля из GET /v1/models:

text
credits_за_запрос ≈ max(
  5,
  ceil(
      prompt_tokens     / 1e6 × credits_per_million_prompt
    + completion_tokens / 1e6 × credits_per_million_completion
  )
)

Для примера выше (48 и 400 кредитов за миллион): 100000/1e6 × 48 + 20000/1e6 × 400 = 4.8 + 8 = 12.8ceil13 кредитов. Помните, что это по-прежнему нижняя граница.

Медиа: плоская цена за генерацию

Изображения, видео и photo-animation тарифицируются фиксированной ценой за генерацию. Никакой токенной математики, никаких округлений, никакого минимального чека — сколько написано в каталоге, столько и спишется.

ЧтоГде
Цена в кредитахGET /v1/modelsprostor.credits_per_request
Цена в долларахGET /v1/modelspricing.request
Признакprostor.billing == "per_request", pricing.prompt == "0"

Модель с credits_per_request: 150 спишет ровно 150 кредитов ($1.50) за генерацию, и в ответе будет usage.cost = 1.5, usage.cost_details.billed_credits = 150.

Эмбеддинги — исключение: они потокенные, как чат, с тем же округлением вверх до целого кредита.

Баланс кошелька: GET /v1/credits

bash
curl -s "$PROSTOR_API_BASE_URL/credits" \
  -H "Authorization: Bearer $PROSTOR_API_KEY" | jq .
json
{
  "data": {
    "total_credits": 50.0,
    "total_usage": 12.34
  },
  "prostor": {
    "balance_credits": 3766,
    "balance_usd": 37.66,
    "granted_credits": 5000,
    "spent_credits": 1234,
    "api_usage_credits": 480,
    "api_usage_usd": 4.8,
    "credits_per_usd": 100,
    "wallet_note": "The balance is shared with the Prostor web app. ..."
  }
}

Блок data — OpenRouter-совместимый, в долларах. Блок prostor — то же самое в кредитах плюс детализация. Инвариант простой:

text
spent_credits   = granted_credits - balance_credits      (не меньше нуля)
balance_credits = granted_credits - spent_credits
total_credits   = granted_credits / credits_per_usd      (в долларах)
total_usage     = spent_credits   / credits_per_usd      (в долларах)
  • granted_credits / total_creditsвсё, что когда-либо было начислено на кошелёк, а не текущий остаток. Это частая ошибка чтения.
  • balance_credits / balance_usdтекущий остаток. Это то, что вы хотите мониторить.
  • api_usage_credits / api_usage_usd — часть расхода, потраченная через этот публичный API.
Важно

Кошелёк общий с веб-приложением Prostor. total_usage и spent_credits — это расход по всем поверхностям: чаты в веб-интерфейсе, студия, API. Если вы сверяете расход именно API, смотрите api_usage_credits / api_usage_usd, а не total_usage. И помните: тратя баланс через API, вы тратите тот же баланс, которым пользуется веб-приложение.

GET /v1/credits намеренно не требует положительного баланса и не требует scope: клиент с пустым кошельком обязан иметь возможность прочитать, что кошелёк пуст.

Расход и лимиты по ключу: GET /v1/key

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

(Алиас GET /v1/auth/key — то же самое, для OpenRouter-совместимых клиентов.)

json
{
  "data": {
    "label": "Prod backend (…8f2a)",
    "limit": null,
    "limit_remaining": null,
    "usage": 4.8,
    "usage_daily": 0.35,
    "usage_weekly": 2.1,
    "usage_monthly": 4.8,
    "is_free_tier": false,
    "is_provisioning_key": false,
    "rate_limit": { "requests": 120, "interval": "1m" },
    "expires_at": null
  },
  "prostor": {
    "key": {
      "id": "…",
      "name": "Prod backend",
      "last4": "8f2a",
      "status": "active",
      "scopes": ["chat:completions", "models:read", "images:generations", "video:generations", "embeddings"],
      "allowed_models": [],
      "created_at": "2026-06-01T09:00:00Z"
    },
    "balance": { "credits": 3766, "usd": 37.66 },
    "usage": { "requests_total": 214, "tokens_total": 918442, "credits_total": 480 },
    "limits": {
      "tokens_total": { "limit": null, "used": 918442, "remaining": null },
      "tokens_monthly": { "limit": null, "used": 918442, "remaining": null },
      "credits_total": { "limit": null, "used": 480, "remaining": null },
      "requests_per_minute": { "limit": 120, "used": 0, "remaining": null },
      "tokens_per_minute": { "limit": null, "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": 78458333, "completion_tokens": 9415000 }
      ]
    },
    "credits_per_usd": 100,
    "min_billed_credits": 1
  }
}

Что здесь есть про деньги:

  • data.usage, usage_daily, usage_weekly, usage_monthly — расход этого ключа в долларах за окно. В кредитах то же самое: prostor.usage.credits_total.
  • data.limit / data.limit_remaining — кредитный лимит ключа в долларах. null означает, что лимита нет (ключ ограничен только балансом кошелька).
  • prostor.limits.* — все лимиты в одном месте: limit: null = безлимит, used — сколько израсходовано.
  • prostor.balance — баланс кошелька, тот же, что в /v1/credits.
Заметка

requests_per_minute.used и tokens_per_minute.used всегда 0, а remaining — всегда null. Это не баг: скользящие счётчики минутного лимитера наружу не публикуются, а считать их по таблице usage означало бы врать про запросы, которые прямо сейчас в полёте. Ориентируйтесь на limit и на 429 с заголовком Retry-After.

token_estimate — оценка, а не обещание

prostor.token_estimate.models показывает, «на сколько токенов хватит текущего баланса» для каждой доступной ключу текстовой модели.

Важно

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

Для коротких запросов оценка сверху простая: каждый платный запрос стоит минимум 1 кредит, так что баланса хватит не более чем на balance_credits запросов. Список пуст, если баланс ≤ 0; медиа-модели в него не входят.

Кэшированные токены: скидки нет

В usage может прийти cached_tokenstokens_cached в GET /v1/generation), если провайдер сообщил о попадании в prompt-кэш.

Важно

Мы показываем cached_tokens, но не даём за них скидку. Кэшированные prompt-токены тарифицируются по обычной цене prompt-токена. Не закладывайте кэш-дисконт в бюджет — его нет.

Поле полезно как диагностика (видно, что кэш работает), но не как экономия.

Резерв (pre-auth) — это не списание

Перед тем как уйти к провайдеру, платный запрос резервирует на кошельке сумму по худшему случаю: prompt по размеру тела запроса плюс полный бюджет вывода (max_tokens; если вы его не прислали — дефолтные 8192 токена). Явный max_tokens резервирует ровно себя, поэтому большое значение (вплоть до 64000) даёт крупный холд. Считается резерв той же формулой, что и списание.

После завершения запроса резерв снимается и списывается фактическая сумма. При ошибке — возвращается целиком. Брошенные резервы подчищаются автоматически.

Что из этого следует на практике:

  • Можно получить 402 insufficient_credits («insufficient credits to cover max_tokens for this request») при живом балансе: резервы параллельных запросов складываются, а каждый из них рассчитан по максимуму, а не по ожидаемой длине ответа.
  • Лечится тем, что вы всегда присылаете max_tokens. max_tokens: 256 вместо умолчания уменьшает completion-часть резерва в 32 раза и освобождает место для параллельных запросов.
  • Резерв не влияет на итоговую сумму. Вы платите за фактические токены (с округлением вверх до целого кредита), а не за зарезервированное. Единственное исключение: если модель отключат или снимут с тарификации, пока запрос уже выполняется (редкая ситуация при правке каталога), gateway перестраховывается и спишет зарезервированный worst-case, а не фактический usage. Точную списанную сумму всегда видно в GET /v1/generation (cost_credits).

Крайние случаи, которые стоят денег

СитуацияЧто списывается
Клиент отвалился в середине стримаПолная стоимость. Генерация не отменяется: мы дочитываем её до конца, чтобы получить достоверный usage. В записи будет interrupted: true, finish_reason: "client_disconnected"
Стрим завершился без usage-чанка от провайдераТокены оцениваются, причём completion считается по большему из: оценки по объёму текста и вашего зарезервированного бюджета вывода. Отсюда ещё один довод в пользу разумного max_tokens. В записи is_estimated: true
Провайдер вернул ошибку (4xx/5xx)Ничего. Резерв возвращается целиком
Ответ превысил безопасный лимит буферизации стримаСчитается ошибкой генерации, interrupted: true; списание происходит по фактическому usage
Повтор с тем же Idempotency-Key (non-stream)Ничего. Отдаётся сохранённый ответ с заголовком Idempotent-Replayed: true, повторного списания нет
Async-видео: submitРезерв на полную фикс-цену. Списание — на status: completed, возврат — при ошибке или если джоб брошен
usage с нулевой стоимостьюНоль. Округление до целого кредита применяется только когда стоимость больше нуля
Заметка

В финальном usage-чанке стрима поля is_estimated и interrupted всегда приходят как false — по построению: если стрим оценивали, финального usage-чанка попросту не было, а разрыв соединения обнаруживается уже после него. Достоверные значения этих флагов смотрите в GET /v1/generation?id=.

Сверка расхода: рекомендуемый порядок

  1. Логируйте X-Request-Id (он же X-Generation-Id) на каждый запрос — без него сверка превращается в гадание.
  2. Логируйте usage.cost_details.billed_credits из тела ответа (или X-Prostor-Cost-Credits из заголовка).
  3. Расхождение или спор — поднимайте GET /v1/generation?id=<request-id>: это авторитетная биллинговая запись.
  4. Суммарный расход API за период — GET /v1/key (data.usage_daily / usage_weekly / usage_monthly) и GET /v1/credits (prostor.api_usage_credits).
  5. Остаток — GET /v1/creditsprostor.balance_credits. Помните, что его расходует и веб-приложение тоже.