Расходы ИИ-агента ограничивайте до сетевого вызова: сначала проверьте общий доступный бюджет и атомарно зарезервируйте верхнюю оценку стоимости, затем отправляйте запрос. Если после отправки случился таймаут, оставьте запись в unknown, сохраните резерв и не запускайте автоматический повтор. Освобождайте резерв после доверенной сверки фактической стоимости или подтверждения, что запрос не ушёл. Ниже — локальный учебный прогон с fake transport: он проверяет журнал и параллельные задания, но не реальный биллинг. Агент может собирать записи и сверять их с правилами; человек уточняет недостающие бизнес-условия и решает исключения.
Карта расходов
Одна задача пользователя может породить несколько вызовов: агент получает данные, уточняет их, выбирает следующий шаг, формирует ответ. Отдельные вызовы появляются и при повторах. Поэтому для контроля расходов считайте стоимость попыток, а не только стоимость задачи целиком.
задача → оценка стоимости → резерв → сетевой вызов → settled или unknown → сверка → доступный остаток
Для каждого вызова храните ID задачи и попытки, модель или маршрут, исходную верхнюю оценку bound, текущий резерв, состояние отправки и подтверждённую стоимость actual, когда она станет известна. bound остаётся в журнале как исходная оценка, но не обязательно продолжает удерживаться: после подтверждённого not_sent резерв освобождается.
Словом «лимит» называют разные вещи. Разведите их в настройках и учёте:
| Ограничение | Что ограничивает | Где проверять или задавать |
|---|---|---|
| Денежный предел провайдера | Кредит, расходы или доступный бюджет в рамках правил провайдера | В официальной документации и настройках выбранного провайдера |
| Ограничение скорости | Частоту запросов за интервал | В документации провайдера; при необходимости — отдельным правилом приложения |
| Бюджет приложения | Подтверждённый расход и резервы всех заданий | В общем журнале приложения, с атомарной проверкой до отправки |
| Список разрешённых моделей и маршрутов | Какие варианты доступны агенту | В настройках приложения; сам по себе список не задаёт денежный предел |
Денежный предел и скорость отвечают на разные вопросы: сумма расходов не равна числу запросов за интервал. В справке OpenRouter проверка rate limit и остатка кредитов API-ключа описана через запрос GET /api/v1/key. Документация OpenRouter по лимитам
Поля limit, limit_reset и limit_remaining в ответе OpenRouter описывают кредитный лимит ключа, период его сброса и оставшийся лимит. Документация OpenRouter по лимитам
Для ошибки rate limit документация OpenRouter упоминает заголовки X-RateLimit-*. Документация OpenRouter по лимитам Эти данные не заменяют отдельный учёт бюджета приложения. Список разрешённых моделей тоже не становится денежным пределом: он ограничивает выбор агента, но не считает общую сумму.
Защита
Провайдерские ограничения проверяйте у выбранного сервиса, а локальный бюджет ведите отдельно. Объект Budget в документации OpenRouter содержит limit_usd, spend_usd, remaining_usd и интервал сброса; при нулевом или отрицательном remaining_usd запросы блокируются. Документация OpenRouter по лимитам
OpenRouter описывает in-flight spending budget как ограничение оценочной стоимости платных запросов, которые выполняются или недавно завершились, относительно баланса. Документация OpenRouter по лимитам
OpenRouter указывает, что стоимость запроса списывается при его завершении, поэтому несколько одновременных запросов могут зафиксировать расход выше баланса до завершения расчёта. Документация OpenRouter по лимитам
Для предварительной оценки OpenRouter учитывает стоимость входных токенов и допустимого числа выходных токенов из max_tokens; комиссии за запрос, плагины и изображения в эту оценку не входят. Документация OpenRouter по лимитам
Пока запрос выполняется, оценка удерживается; после завершения или ошибки она заменяется фактической стоимостью на короткое окно сверки, а затем удержание освобождается. Документация OpenRouter по лимитам
Если оценка не помещается в доступный in-flight budget, OpenRouter может отклонить запрос до передачи провайдеру, даже если баланс положительный. Документация OpenRouter по лимитам
Это описание механизма OpenRouter, а не правило для любого API. Перед запуском проверьте актуальные лимиты и учёт в первичной документации выбранного провайдера. Здесь не предполагаются тарифы или настройки реального аккаунта.
В приложении сначала одной атомарной операцией проверьте общий бюджет и запишите резерв, а уже затем отправляйте вызов. В учебной реализации эта операция выполняется в транзакции SQLite BEGIN IMMEDIATE. При бюджете 1000 toy integer milli-units и подтверждённом расходе 120 два одновременных задания, запрашивающих резерв по 700 каждое, не должны оба получить разрешение на основании одного и того же остатка.
Локальный журнал сдерживает новые расходы только при двух условиях: верхняя оценка покрывает возможную стоимость, а каждый вызов, включая повторы и параллельные задания, проходит через общую проверку. Если вызов обходит журнал или оценка занижена, локальная проверка не гарантирует потолок счёта провайдера.
Состояния
Для каждого вызова заведите отдельную запись. Храните исходную оценку bound, текущий резерв и подтверждённую стоимость actual, если она уже известна.
| Состояние | Что известно | Текущий резерв | Действие с бюджетом |
|---|---|---|---|
reserved |
Оценка проверена, резерв записан, вызов ещё не отправлен | Удерживается | Не отправлять без зафиксированного резерва |
not_sent |
Подтверждено, что запрос не ушёл в сеть | 0 | Освободить резерв, сохранив bound в журнале |
sent |
Отправка началась, результат ещё не подтверждён | Удерживается | Ждать результата или статуса |
unknown |
После отправки произошёл таймаут или обрыв; факт списания неизвестен | Удерживается полностью | Не считать расход нулевым и не повторять автоматически |
settled |
Доверенная сверка подтвердила фактическую стоимость | 0 | Учесть actual и освободить подтверждённый остаток резерва |
overrun |
Подтверждённая стоимость выше резерва | 0 | Учесть известную стоимость и включить глобальную паузу новых резервов |
Последовательность такая: атомарно проверьте доступный бюджет и запишите резерв; только после этого отправляйте запрос. Если подтверждено, что отправки не было, переведите запись в not_sent и освободите резерв. После начала отправки отметьте sent. Если ответ потерялся и результат пока нельзя установить, переведите запись в unknown и удерживайте весь резерв.
Если резерв составлял 700 toy integer milli-units, а доверенная сверка подтвердила стоимость 450 таких же единиц, учтите расход 450 и освободите оставшиеся 250. Исходное bound = 700 оставьте в журнале для анализа оценки, но не показывайте как текущий удерживаемый резерв.
Если доверенная фактическая стоимость выше bound, в одной атомарной операции запишите известный расход, состояние overrun, причину и глобальную паузу новых резервов. Не возвращайте запись к старому unknown и не считайте первоначальную оценку фактическим расходом. Даже если арифметически остаётся свободный остаток, новые вызовы должны оставаться на паузе до разбора расхождения.
Пауза относится к новым локальным резервам. Она не отменяет уже отправленные запросы и расходы вне журнала приложения, поэтому не гарантирует абсолютный предел счёта провайдера.
Авария
Таймаут после отправки не доказывает, что запрос не обработан или что списания не будет. Пока результат неизвестен, удерживайте резерв и не запускайте автоматический повтор. Для каждой попытки ведите отдельную запись и задайте конечное число допустимых попыток под свою задачу. Если подтверждено, что запрос не ушёл, новую попытку можно разрешить после новой проверки и резервирования.
RFC 9110 определяет идемпотентность метода через предполагаемый эффект: несколько одинаковых запросов должны иметь тот же предполагаемый эффект, что и один. RFC 9110, раздел 9.2.2
При этом сервер может отдельно регистрировать каждый запрос. RFC 9110, раздел 9.2.2 Поэтому определение идемпотентности не означает, что повторное исполнение исключено.
RFC 9110 не рекомендует автоматически повторять запрос с неидемпотентным методом, если клиент не располагает ни способом установить идемпотентность семантики запроса независимо от метода, ни способом определить, что исходный запрос не был применён. RFC 9110, раздел 9.2.2 Это условие RFC не подтверждает поддержку ключей идемпотентности конкретным провайдером и не разрешает автоматически повторять любой платный вызов.
В учебной реализации повторная отправка того же ID блокируется локальным журналом. Это локальное правило дедупликации, а не гарантия API о числе исполнений запроса.
Для общего бюджета нескольких заданий нужна одна атомарная проверка. Отдельные процессы с отдельными остатками не координируют расходы. В локальном прогоне два параллельных задания запрашивают по 700 при бюджете 1000 и известном расходе 120: одно проходит резервирование и отправляется через fake transport, второе получает отказ.
Агент может собирать записи, проверять их на соответствие правилам и сопоставлять подтверждённые данные. Человек нужен, если не хватает бизнес-условий, не определён доверенный источник суммы или данные расходятся. Это не требует ручной проверки каждой записи.
Учёт
В документации OpenRouter usage описан как объект с данными о токенах, а cost — как общая сумма, списанная с аккаунта. Документация OpenRouter по учёту usage
Для исторической сверки документация предлагает получить usage асинхронно по ID генерации через /generation. Документация OpenRouter по учёту usage
Для данных по ID генерации поле upstream_inference_cost доступно только для BYOK — запроса с собственным ключом; для остальных запросов оно будет 0 или null. Документация OpenRouter по учёту usage
Сверяйте данные провайдера, журнал приложения и итоговый счёт. Автоматически сопоставлять записи можно по надёжному идентификатору и сумме. unknown, превышение резерва и несовпадения оставляйте на разбор: отсутствие ответа не означает нулевое списание. Локальный пример ниже не сверяет реальный биллинг.
Учебный пример
Условия и суммы синтетические. В сценарии используются toy integer milli-units — игрушечные целочисленные милли-единицы, а не валюта и не тариф провайдера. Локальный прогон выполнен с fake transport, без платных сетевых вызовов API. Это не клиентский кейс, не тест модели и не проверка реального счёта.
Полный комплект локального примера
Основной сценарий: бюджет — 1000 toy integer milli-units. Для pre_network до отправки зарезервировано 200; затем подтверждено, что вызов не ушёл, и резерв освобождён. Для success зарезервировано 200, вызов отправлен, после чего подтверждена стоимость 120. Два параллельных задания запросили по 700: одно прошло резервирование и было отправлено через fake transport, второе получило отказ. После таймаута отправленное задание осталось в unknown с резервом 700.
До сверки занято 820 единиц: подтверждённый расход 120 и резерв 700. Доступно 180. После учебной сверки для unknown подтверждена синтетическая стоимость 450: резерв заменён фактическим расходом, общий подтверждённый расход стал 570, доступно 430.
| Снимок до сверки | Состояние | Исходная оценка bound |
Текущий резерв | Подтверждённый расход |
|---|---|---|---|---|
pre_network |
not_sent |
200 | 0 | — |
success |
settled |
200 | 0 | 120 |
worker2 |
unknown |
700 | 700 | Не установлен |
| Итого | 700 | 120 |
| Сводка бюджета | Подтверждённый расход | Текущий резерв | Занято | Доступно |
|---|---|---|---|---|
| До сверки | 120 | 700 | 820 | 180 |
| Снимок после учебной сверки | Состояние | Исходная оценка bound |
Текущий резерв | Подтверждённый расход |
|---|---|---|---|---|
pre_network |
not_sent |
200 | 0 | — |
success |
settled |
200 | 0 | 120 |
worker2 |
settled |
700 | 0 | 450 |
| Итого | 0 | 570 |
| Сводка бюджета | Подтверждённый расход | Текущий резерв | Занято | Доступно |
|---|---|---|---|---|
| После учебной сверки | 570 | 0 | 570 | 430 |
В events.csv поле committed показывает общий занятый бюджет после события, а не резерв отдельного вызова:
seq,id,event,committed
1,pre_network,reserved,200
2,pre_network,not_sent,0
3,success,reserved,200
4,success,sent,200
5,success,settled,120
6,worker2,reserved,820
7,worker2,sent,820
8,worker2,unknown,820
9,worker1,denied,820
10,worker2,denied,820
11,worker2,settled,570
В строке 10 журнал отклонил попытку повторно отправить тот же ID worker2. Попытка повтора была, но повторный сетевой вызов не отправлялся. В параллельной паре fake transport получил один вызов; платных вызовов API было ноль. Имя задания, первым получившего резерв, может меняться, поэтому проверяются состояния и итоговые суммы, а не имя победителя.
Проверки выполненной локальной реализации для основного сценария:
| Проверка | Результат |
|---|---|
| Резерв записан до отправки | Пройдена |
| Из двух параллельных заданий отправлено только одно | Пройдена |
unknown удерживает полный резерв |
Пройдена |
Резерв освобождается при подтверждённом not_sent или после сверки |
Пройдена |
| Баланс основного сценария не превышает бюджет | Пройдена |
| Подтверждённый расход сходится | Пройдена |
| Превышение резерва не принимается обычным settlement | Пройдена |
Две отдельные синтетические ветви превышения резерва. Это альтернативы от одного снимка: подтверждённый расход 120 и unknown с резервом 700. Они не продолжают основную ветвь со стоимостью 450 и не являются последовательными списаниями одного счёта.
| Подтверждённая стоимость вместо резерва 700 | Занято после записи факта | Доступно по арифметике | Превышение бюджета | Новые резервы |
|---|---|---|---|---|
| 750 | 870 | 130 | 0 | Запрещены глобальной паузой |
| 900 | 1020 | 0 | 20 | Запрещены глобальной паузой |
При стоимости 750 занято 870 и арифметически свободно 130, но глобальная пауза всё равно запрещает новые резервы. При стоимости 900 журнал учитывает 1020, доступный остаток равен нулю, а превышение бюджета — 20. В обеих ветвях известная сумма записывается как overrun с причиной; обычный settlement и тихая перезапись старого состояния отклоняются. Автоматического снятия паузы в примере нет.
После превышения агент останавливает новые вызовы, фиксирует известный факт и готовит сверку источника usage, оценки и журнала. Решение об исключении или возобновлении принимает ответственный человек вне этого демонстрационного прогона. Пауза не отменяет уже отправленные вызовы и сторонние списания.
Плановый запрос аналитическому помощнику — шаблон, модель его не выполняла:
Разбери синтетический локальный журнал контроля бюджета. Не называй его данными провайдера и не делай вывод, что unknown означает нулевое списание.
Бюджет: 1000 toy integer milli-units. Это не валюта и не тариф API.
Основной сценарий:
- До сети зарезервировано 200 для pre_network; подтверждено not_sent, резерв освобождён.
- Для success зарезервировано 200, отправка подтверждена, фактическая стоимость 120.
- Два параллельных задания запросили резерв по 700; одно отправлено через локальный fake transport, второе получило отказ.
- После отправки победившее задание получило таймаут и осталось unknown с резервом 700.
- Попытка повторно отправить тот же ID отклонена локальным журналом; сетевой повтор не отправлен.
- При учебной сверке для unknown получена синтетическая подтверждённая стоимость 450.
- Платных сетевых вызовов API: 0.
Отдельно разбери две альтернативные ветви от одного снимка: известный расход 120 и unknown с резервом 700. В первой доверенная фактическая стоимость unknown равна 750, во второй — 900. Для обеих ветвей превышение резерва должно записываться как overrun, включать глобальную паузу новых резервов и сохранять причину. Это альтернативы, а не последовательные списания одного счёта.
Посчитай занятый и доступный бюджет в основном сценарии до и после сверки, а для двух overrun-ветвей — расход, доступный остаток и превышение бюджета. Раздели подтверждённый расход, удерживаемый резерв, исходную оценку bound и освобождённый резерв. Проверь, что два параллельных задания не отправились оба, попытка повтора того же ID была отклонена и не отправлена, превышение резерва не скрыто откатом к прежнему unknown, а новые резервы запрещены на паузе. Перечисли, какие выводы относятся только к синтетическому локальному прогону. Если журнал не позволяет установить факт, назови его неизвестным.
Контрольный список для проверки реализации:
- До каждого сетевого вызова верхняя оценка проверяется, а резерв записывается атомарно.
- У каждого вызова и каждой попытки есть отдельная запись.
- Подтверждённый
not_sentосвобождает резерв; исходная оценка остаётся отдельным полем журнала. - После отправки таймаут переводит вызов в
unknown; весь резерв остаётся занят. - Для
unknownавтоматический повтор не запускается до сверки. - Попытка повторно отправить тот же ID блокируется локальным правилом, если оно задано.
- Два одновременных запроса по 700 при известном расходе 120 и бюджете 1000 не отправляются оба.
- Фактическая стоимость не выше резерва принимается только после доверенного подтверждения; подтверждённый остаток резерва освобождается.
- Если подтверждённая стоимость выше резерва, в одной атомарной операции записываются фактический расход,
overrun, причина и глобальная пауза. - При
overrunновые резервы запрещены даже при арифметически положительном доступном остатке. - Известный расход не откатывается к старому резерву; автоматического снятия паузы нет.
- В основном сценарии до сверки занято 820 и доступно 180, после подтверждения стоимости 450 расход равен 570 и доступно 430.
- Отправки fake transport отделены от платных вызовов API.
Полный комплект, включая локальные исходники, журнал и результаты проверок, доступен в архиве примера. Для отдельной задачи по анализу данных см. разбор маркетинговых данных с нейросетью, а для подготовки карточки товара — карточку товара с нейросетью.

