Цены и списания
Кредиты и доллары, точная формула списания, округление до целого кредита, где взять цену модели и как сверить фактический счёт.
Эта страница отвечает на два вопроса: сколько будет стоить запрос и сколько с меня списали на самом деле. Второй вопрос всегда важнее первого: точную сумму возвращает сам API, и её не нужно вычислять.
Две единицы: кредиты и доллары
Внутренняя валюта Prostor — кредиты. Доллары в ответах API — это OpenRouter-совместимая «обёртка» поверх кредитов, чтобы работали клиенты и SDK, написанные под OpenRouter.
Курс в production фиксирован:
1 кредит = $0.01
credits_per_usd = 100Курс не зашит в клиент — он всегда приходит в ответе: GET /v1/models → prostor.credits_per_usd, GET /v1/credits → prostor.credits_per_usd, GET /v1/key → prostor.credits_per_usd. Читайте его оттуда, а не из этой страницы.
Каждое денежное поле приходит в обеих единицах:
| Что | Доллары | Кредиты |
|---|---|---|
| Стоимость запроса | usage.cost | usage.cost_details.billed_credits (и заголовок X-Prostor-Cost-Credits) |
| Баланс | prostor.balance_usd | prostor.balance_credits |
| Расход по ключу | data.usage, data.usage_daily, … | prostor.usage.credits_total |
| Стоимость прошлой генерации | data.total_cost | data.cost_credits |
Кредиты — это то, что реально списывается с кошелька: целое число, всегда. Доллары — производная величина.
Где лежат цены
Единственный источник цен — GET /v1/models. Он же единственный источник списка моделей, доступных вашему ключу.
curl -s "$PROSTOR_API_BASE_URL/models" \
-H "Authorization: Bearer $PROSTOR_API_KEY" | jq .Сокращённый ответ (значения — иллюстративные; id берите из своего ответа):
{
"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.billing—per_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».
Точная формула списания
Списывается всегда целое число кредитов. Путь от «токены × цена» до этого целого числа — два шага:
- Стоимость в долларах.
prompt_tokens × цена_prompt + completion_tokens × цена_completion. Цены — те, что лежат вGET /v1/models(розничные, наценка уже включена). - Округление вверх до целого кредита — один раз. Доллары переводятся в кредиты по курсу
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).
Оценка сверху, если она всё-таки нужна:
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-токенов.
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 — округление вверх до целого кредита подняло её до одного цента. На коротких запросах разница со счётом — не больше одного кредита.
Ответ покажет ровно это:
"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:
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_included | true | цена конечная, ничего не добавится |
usage.is_estimated | bool | usage пришлось оценить, а не получить от провайдера (см. ниже) |
usage.interrupted | bool | клиент отвалился или генерация оборвалась — но запрос всё равно оплачен |
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-заголовках. Не нужно парсить тело.
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'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=
Авторитетная запись из нашего биллинга. Работает после того, как запрос полностью рассчитан.
curl -s "$PROSTOR_API_BASE_URL/generation?id=<generation-id>" \
-H "Authorization: Bearer $PROSTOR_API_KEY" | jq .{
"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 — кредиты. status — success или error. Запись видна только тому ключу, который её создал: чужой id даёт 404 generation_not_found.
Для async-видео (POST /videos/submit → POST /videos/status) ответ videos/status не содержит итоговой стоимости: расчёт происходит уже после того, как ответ ушёл клиенту. Фактическое списание по такому джобу смотрите через GET /v1/generation?id=.
Конвертация кредитов и долларов
Формула одна, курс берётся из prostor.credits_per_usd (production: 100).
usd = credits / credits_per_usd
credits = usd × credits_per_usdПример: списание 13 кредитов → 13 / 100 = $0.13. Пополнение на $25 → 25 × 100 = 2500 кредитов.
Если вы считаете бюджет в кредитах (а так удобнее — это единица списания), не переводите цены в доллары вообще. Берите готовые поля из GET /v1/models:
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.8 → ceil → 13 кредитов. Помните, что это по-прежнему нижняя граница.
Медиа: плоская цена за генерацию
Изображения, видео и photo-animation тарифицируются фиксированной ценой за генерацию. Никакой токенной математики, никаких округлений, никакого минимального чека — сколько написано в каталоге, столько и спишется.
| Что | Где |
|---|---|
| Цена в кредитах | GET /v1/models → prostor.credits_per_request |
| Цена в долларах | GET /v1/models → pricing.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
curl -s "$PROSTOR_API_BASE_URL/credits" \
-H "Authorization: Bearer $PROSTOR_API_KEY" | jq .{
"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 — то же самое в кредитах плюс детализация. Инвариант простой:
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
curl -s "$PROSTOR_API_BASE_URL/key" \
-H "Authorization: Bearer $PROSTOR_API_KEY" | jq .(Алиас GET /v1/auth/key — то же самое, для OpenRouter-совместимых клиентов.)
{
"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_tokens (и tokens_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=.
Сверка расхода: рекомендуемый порядок
- Логируйте
X-Request-Id(он жеX-Generation-Id) на каждый запрос — без него сверка превращается в гадание. - Логируйте
usage.cost_details.billed_creditsиз тела ответа (илиX-Prostor-Cost-Creditsиз заголовка). - Расхождение или спор — поднимайте
GET /v1/generation?id=<request-id>: это авторитетная биллинговая запись. - Суммарный расход API за период —
GET /v1/key(data.usage_daily/usage_weekly/usage_monthly) иGET /v1/credits(prostor.api_usage_credits). - Остаток —
GET /v1/credits→prostor.balance_credits. Помните, что его расходует и веб-приложение тоже.