# Kill switch расходов API для LLM-агентов: остановите бюджет до вызова провайдера

> Чтобы остановить следующий платный запрос LLM-агента, проверяйте расход вместе с незавершёнными резервами до отправки. Неизвестное списание сохраняйте до сверки, а бюджетный отказ передавайте всем подагентам.

- URL: https://blog.laozhang.ai/ru/posts/llm-agent-api-spend-kill-switch
- Published: 2026-06-14
- Updated: 2026-10-07
- Author: LaoZhang AI Team (https://blog.laozhang.ai/ru/about)
- Topic: Руководства по API
- Tags: LLM-агенты, Расходы API, Бюджет API, Лимиты расходов

---
Kill switch расходов API должен запрещать следующий платный вызов LLM-агента **до отправки запроса**. Для этого все обращения к моделям и платным инструментам проходят через один бюджетный шлюз: он проверяет уже учтённый расход, незавершённые запросы и максимальную стоимость нового вызова, затем атомарно резервирует деньги. Если средств недостаточно, запрос остаётся внутри приложения, а агент завершает платную работу с `retryable: false`.

Это позволяет ограничивать расходы конкретной задачи, включая повторы, подагентов и запасные маршруты. Условие строгое: шлюз должен видеть каждый платный вызов, а резерв — покрывать все возможные начисления за него. При неизвестной цене, потерянном состоянии учёта или неоднозначном результате отправки безопасное решение — приостановить работу до сверки.

У OpenAI есть принудительные лимиты организации и проекта: для них требуется включить **Enforce a hard limit**. Однако применение ограничения не мгновенно, и учтённый расход может немного превысить сумму. Поэтому лимит провайдера полезен вместе с собственным шлюзом, особенно когда один агент использует несколько поставщиков. [Текущее описание OpenAI](https://developers.openai.com/api/docs/guides/spend-limits).

## Где поставить проверку, чтобы агент её не обошёл

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

Сначала перечислите реальные пути расходования денег: основной вызов, повтор после ошибки, автоматический повтор SDK, переход на другую модель, суммаризация истории, генерация изображений и платный поиск. У каждого пути должен быть один и тот же обязательный допуск. Если SDK самостоятельно делает несколько сетевых попыток внутри одного вызова, либо отключите такой механизм на своём адаптере и проводите попытки по одной через шлюз, либо резервируйте доказуемую верхнюю границу всех попыток. Название параметра и поведение проверьте для используемого SDK; универсальной настройки для всех клиентов нет.

Идентификатор бюджета, период, разрешённые модели и тарифы задаёт серверная политика. Модель может предложить действие, но не выбирает себе новый `budget_id`, не повышает потолок и не помечает платный маршрут бесплатным. Дочерний агент наследует бюджет родительской задачи. Если нужен ещё и личный лимит дочернего агента, оба ограничения проверяются и резервируются одной согласованной операцией.

Ограничение числа шагов и длины ответа оставьте дополнительными мерами. Десять коротких запросов и десять запросов с длинным контекстом стоят по-разному; счётчик шагов не заменяет денежный бюджет.

## Что на самом деле останавливают лимиты OpenAI, Anthropic и шлюзов

Денежный лимит отвечает на вопрос «сколько можно потратить», а ограничение частоты — «сколько запросов или токенов можно обработать за время». Для остановки агента проверяйте причину отказа, а не только HTTP-статус.

| Механизм | Поведение по текущей документации на 6 октября 2026 года | Что учитывать в агенте |
| --- | --- | --- |
| OpenAI spend alert | Уведомляет, но не прерывает трафик | Письмо не даёт разрешения на следующий вызов и не блокирует его |
| OpenAI hard spend limit | При включённом Enforce a hard limit применяет месячный потолок организации или проекта; возвращает 429 с `organization_spend_limit_exceeded` либо `project_spend_limit_exceeded` | Применение асинхронно, небольшой перерасход возможен; организация охватывает все проекты, проект — только свой оплачиваемый трафик |
| Anthropic лимит сервисного уровня | При достижении месячного потолка приостанавливает API; 429 `rate_limit_error`, для Messages — `enforced_spend_limit_reached` в `error.details.error_code`, без `retry-after` | Обычный повтор с задержкой не восстанавливает доступ |
| Anthropic лимит, заданный пользователем | Обычно 400 `invalid_request_error` с сообщением о заданном лимите API или workspace | Для Claude Code workspace действует отдельная проверка, которая может вернуть 429 с `retry-after`; учитывайте конкретный продукт |
| Cloudflare AI Gateway spend limits | Проверяет применимые правила перед отправкой, но добавляет стоимость текущего запроса после завершения | Учёт согласуется с задержкой; параллельная пачка может превысить предел, оценка стоимости приблизительная |
| LiteLLM бюджеты сессии агента | Проверяет накопленный расход перед вызовом и увеличивает его после успешного ответа; срок жизни счётчика по умолчанию — один час | Это не атомарный резерв будущей стоимости; новый trace ID или истечение счётчика не должны создавать новый родительский бюджет |

Источники и область применения: [OpenAI spend limits](https://developers.openai.com/api/docs/guides/spend-limits), [Anthropic rate/spend limits](https://platform.claude.com/docs/en/api/rate-limits), [Cloudflare spend limits](https://developers.cloudflare.com/ai-gateway/features/spend-limits/), [LiteLLM Agent Iteration Budgets](https://docs.litellm.ai/docs/a2a_iteration_budgets). У Anthropic в варианте Claude Platform on AWS отличается управление оплатой; не переносите туда инструкции о покупке кредитов Anthropic.

В LiteLLM отдельно существуют бюджеты прокси, команды, участника команды, пользователя и виртуального ключа. Выбирайте сущность, которой действительно принадлежит расход: с версии 1.95 для ключей команды убрана проверка личного бюджета внутреннего пользователя, а для лимита внутри команды предназначен бюджет участника. Сброс учётного расхода не отменяет выставленный счёт. Эти правила полезны для настройки политики, но сами по себе не подтверждают строгий денежный потолок при конкурентных запросах. [Документация бюджетов LiteLLM](https://docs.litellm.ai/docs/proxy/users).

## Как рассчитать резерв до ответа

Резервируйте верхнюю границу стоимости **конкретного разрешённого запроса**. Для текстового вызова это вход с системными сообщениями, историей и описаниями инструментов, максимально допустимый оплачиваемый выход и дополнительные платные операции. Для изображений, аудио и встроенных инструментов потребуется их собственная единица тарификации.

Размер входа определяйте методом, подходящим выбранной модели. Число символов не равно числу токенов. Если нельзя достоверно ограничить объём или цену компонента, шлюз не должен подставлять ноль и пропускать вызов. Либо ограничьте этот компонент, либо запретите такой маршрут до появления проверяемой оценки. Скидку за кэш учитывайте лишь при надёжном основании; ожидание попадания в кэш не является гарантией.

Возьмём **вымышленный тариф**, чтобы проверить арифметику без привязки к текущей цене модели: вход — 2 USD за миллион токенов, выход — 8 USD за миллион, платных инструментов нет. При входе 5 000 токенов и допустимом выходе 2 000 резерв равен:

~~~text
5 000 × 2 / 1 000 000 + 2 000 × 8 / 1 000 000 = 0,026 USD
~~~

При бюджете 0,10 USD, учтённом расходе 0,04 USD и незавершённых резервах 0,04 USD новый вызов не проходит: `0,04 + 0,04 + 0,026 = 0,106 USD`. Сравнение только с уже завершённым расходом ошибочно разрешило бы его.

Храните деньги целыми числами в выбранной минимальной единице, например в миллионных долях USD, и округляйте верхнюю оценку вверх. Тариф, версию оценки, маршрут, модель и ограничение выхода привяжите к записи запроса. Переключение на другую модель требует новой оценки; дешёвый запасной маршрут тоже расходует общий бюджет.

## Атомарный допуск: проверка и резерв должны быть одной операцией

Для одного бюджета выполняйте это условие вместе с записью резерва:

~~~text
учтённый расход + действующие резервы + новый резерв ≤ потолок
~~~

Два работника могут одновременно увидеть свободные 0,03 USD и каждый отправить запрос на 0,02 USD. Раздельные «прочитать остаток» и «записать резерв» оставляют такую гонку даже при правильной формуле. Нужна транзакция с подходящей блокировкой или эквивалентная атомарная операция в общем хранилище.

[Redis гарантирует атомарное выполнение Lua-скрипта](https://redis.io/docs/latest/develop/programmability/eval-intro/): это позволяет объединить проверку и изменение нескольких значений. Такая гарантия не делает HTTP-запрос к провайдеру частью транзакции и не решает сохранность данных после отказа хранилища. Сначала проверяйте входные данные, потом изменяйте состояние; отдельно проектируйте устойчивое хранение, восстановление и загрузку скрипта после перезапуска.

У записи резерва должны быть владелец бюджета, период, уникальный идентификатор попытки, привязка к содержимому и маршруту, сумма, состояние и ссылка на запрос провайдера, если она получена. Повтор сообщения из очереди с тем же идентификатором не должен повторно отправлять запрос. Настоящая повторная попытка получает новый идентификатор и новый резерв; прежний неопределённый расход при этом остаётся в учёте.

![Последовательность бюджетного допуска: оценить максимум, зарезервировать деньги, отправить запрос и сверить расход.](https://blog.laozhang.ai/posts/ru/llm-agent-api-spend-kill-switch/img/reserve-call-reconcile.webp)

### Полностью локальный пример на Python

Следующий пример можно сохранить в `budget_demo.py` и запустить командой `python3 budget_demo.py`. Он использует стандартную библиотеку, целые миллионные доли USD и имитатор вызова. `Lock` обеспечивает взаимное исключение потоков **одного процесса**. Для нескольких процессов или машин потребуется общее устойчивое хранилище; этот пример не проверяет реальную оплату, SDK или Redis.

В примере тарифная оценка передаётся сервером явно. Строка `binding` представляет заранее сформированную привязку к запросу и маршруту; в рабочем адаптере её рассчитывают из канонического запроса, а не принимают от модели.

~~~python
from threading import Lock


class Ledger:
    def __init__(self, budget_id, period, cap):
        if type(cap) is not int or cap < 0:
            raise ValueError("invalid cap")
        self.budget_id, self.period, self.cap = budget_id, period, cap
        self.actual = 0
        self.rows = {}
        self.stop = None
        self.lock = Lock()

    def held(self):  # Вызывается под self.lock.
        return sum(r["reserve"] for r in self.rows.values()
                   if r["state"] in ("reserved", "unknown"))

    def admit(self, request_id, binding, reserve):
        with self.lock:
            if request_id in self.rows:
                r = self.rows[request_id]
                if (r["binding"], r["reserve"]) != (binding, reserve):
                    raise ValueError("request_id reused for another request")
                return "duplicate"  # Никогда не отправляем повторно.
            if self.stop:
                return self.stop
            if type(reserve) is not int or reserve <= 0:
                self.stop = "unpriced_request"
                return self.stop
            if self.actual + self.held() + reserve > self.cap:
                self.stop = "over_budget"
                return self.stop
            self.rows[request_id] = dict(binding=binding, reserve=reserve,
                                        state="reserved", actual=None)
            return "allowed"

    def unknown(self, request_id):
        with self.lock:
            r = self.rows[request_id]
            if r["state"] == "reserved":
                r["state"] = "unknown"
                self.stop = "reconciliation_required"

    def settle(self, request_id, actual):
        with self.lock:
            r = self.rows[request_id]
            if type(actual) is not int or actual < 0:
                raise ValueError("invalid actual")
            if r["state"] == "settled":
                if r["actual"] != actual:
                    raise ValueError("conflicting settlement")
                return  # Повторная доставка не меняет сумму.
            if r["state"] not in ("reserved", "unknown"):
                raise ValueError("invalid state")
            self.actual += actual
            r["actual"], r["state"] = actual, "settled"
            if actual > r["reserve"]:
                self.stop = "estimate_breached"
            # Сверка не снимает остановку автоматически.


class FakeProvider:
    def __init__(self):
        self.calls = 0

    def send(self, mode):
        self.calls += 1
        if mode == "timeout":
            raise TimeoutError("result unknown after send")
        return 18_000  # Вымышленная стоимость 0,018 USD.


def guarded_call(ledger, provider, request_id, binding, reserve, mode="ok"):
    decision = ledger.admit(request_id, binding, reserve)
    if decision != "allowed":
        return {"status": decision, "retryable": False}
    try:
        actual = provider.send(mode)
        ledger.settle(request_id, actual)
        return {"status": "completed", "actual": actual}
    except Exception:
        ledger.unknown(request_id)
        raise


if __name__ == "__main__":
    book = Ledger("team-research", "2026-10", 30_000)
    fake = FakeProvider()
    assert guarded_call(book, fake, "a", "route-A/payload-A", 26_000)["status"] == "completed"
    assert book.actual == 18_000 and book.held() == 0
    assert guarded_call(book, fake, "a", "route-A/payload-A", 26_000)["status"] == "duplicate"
    assert guarded_call(book, fake, "b", "route-A/payload-B", 26_000)["status"] == "over_budget"
    assert fake.calls == 1
    book.settle("a", 18_000)
    assert book.actual == 18_000
    print("OK: one call; duplicate and over-budget request blocked")
~~~

Здесь резерв снимается лишь после записи фактической суммы; при повторной сверке сумма не увеличивается. Отказ сохраняет причину остановки. Не добавляйте автоматический сброс `stop` в начале следующего шага: иначе новый шаг продолжит ту же задачу после бюджетного запрета.

## Тайм-аут, оборванный поток и неизвестный расход

Тайм-аут клиента означает, что клиент не получил результат. Он не доказывает, что провайдер не принял запрос или не выполнил часть оплачиваемой работы. То же относится к закрытию потокового соединения: отключение клиента не является возвратом денег.

Разделяйте ситуации по имеющимся доказательствам:

| Что известно | Действие с резервом | Можно ли продолжать |
| --- | --- | --- |
| Запрос точно не покинул адаптер | Освободить резерв отдельной идемпотентной операцией, сохранив основание | Только после обычного нового допуска |
| Есть достоверные usage и тариф | Записать фактическую стоимость и освободить разницу | Если бюджет и политика позволяют |
| Отправка могла состояться, итоговая стоимость неизвестна | Сохранить резерв, отметить `unknown`, приостановить новые платные вызовы | После сверки и разрешённого возобновления |
| Фактическая сумма выше резерва | Записать всю сумму, остановить бюджет и сообщить о неверной оценке | После исправления причины и решения ответственного |

У приведённого примера нет операции освобождения «точно не отправлено»: для него любой сбой в `send` считается неопределённым. В настоящем адаптере отдельная операция допустима лишь при надёжном доказательстве отсутствия отправки. Один HTTP-код ошибки не является универсальным доказательством нулевой оплаты.

Для сверки сохраните идентификатор попытки, запрос провайдера, маршрут, тарифную версию, временные отметки, резерв и доступные данные usage. Сопоставьте их с отчётом или журналом провайдера; при отсутствии однозначного соответствия оставьте запись нерешённой. Для Anthropic учитывайте раздельные поля обычного входа, создания кэша и чтения кэша: `input_tokens` не охватывает весь вход, а освобождение от части лимита токенов в минуту не означает бесплатную тарификацию. [Описание полей и кэша](https://platform.claude.com/docs/en/api/rate-limits#cache-aware-itpm).

Резервы не должны пропадать по TTL. Запрос, начавшийся в предыдущем периоде, сверяется с его исходной записью; смена месяца не переносит его автоматически в новый бюджет и не стирает обязательство. Если хранилище недоступно или после перезапуска ещё не восстановлено, новые вызовы ждут восстановления. Иначе временный сбой учёта превращается в разрешение тратить без контроля.

Если реальная цена оказалась выше верхней оценки, честный результат — «оценка нарушена, учтено полное списание». Нельзя задним числом объявить, что потолок соблюдён, обрезав фактическую сумму до размера резерва.

## Как передать остановку и разрешить возобновление

Бюджетный отказ завершает платную часть задачи. Планировщик прекращает создавать новые платные задания, работники прекращают повторы, подагенты получают ту же причину. Уже отправленные запросы остаются на сверке; отмена очереди не отменяет их стоимость.

Например, сервер может вернуть:

~~~json
{
  "status": "stopped",
  "reason": "over_budget",
  "retryable": false,
  "budget_id": "team-research",
  "period": "2026-10",
  "next_action": "authorized_budget_review"
}
~~~

Выбор HTTP-статуса зависит от договора вашего сервиса с клиентом. Существенно то, что клиент распознаёт терминальную причину и не превращает её в автоматический повтор. OpenAI 429 с кодом лимита расходов и Anthropic 429 с `enforced_spend_limit_reached` обрабатывайте как остановку, а не как временную перегрузку. Для настоящего ограничения частоты допустим ограниченный повтор после нужной задержки, но каждая новая платная попытка заново проходит допуск и учитывает прежний неизвестный расход.

Возобновление принимает ответственный оператор или доверенная серверная политика. Сначала сверяются неизвестные записи, затем проверяются остаток, разрешённые маршруты и условия задачи. Если потолок меняется, запишите кто разрешил изменение, новую сумму, срок действия и причину. Сохраняйте **тот же бюджет и его историю**. Новый идентификатор запуска, другой ключ или час ожидания не дают нового денежного разрешения. Более дешёвая модель возможна только после явного разрешения и новой оценки, а не как обход остановки.

## Как проверить, что заблокированный запрос не был отправлен

![Локальный имитатор считает только разрешённую отправку: повтор и превышение бюджета блокируются до send, неизвестный расход остаётся на сверке](https://blog.laozhang.ai/posts/ru/llm-agent-api-spend-kill-switch/img/budget-boundary-test.webp)

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

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

Для рабочего общего хранилища отдельно проверяйте несколько процессов, сбой после записи резерва и до отправки, сбой после ответа и до сверки, восстановление после перезапуска, смену периода и повтор доставки из очереди. Важны конкретные результаты: сколько попыток реально ушло, какой резерв сохранился и как подтверждён фактический расход. Локальный `Lock` не доказывает эти свойства распределённой системы.

## Часто задаваемые вопросы

### Достаточно ли включить OpenAI project budget?

Для остановки оплачиваемого трафика проекта нужен включённый **Enforce a hard limit**; простое уведомление не блокирует запросы. Принудительный лимит работает асинхронно и допускает небольшой перерасход. Он также не охватывает вызовы других провайдеров. Для единого бюджета задачи добавьте собственный допуск перед каждым платным вызовом. [OpenAI spend limits](https://developers.openai.com/api/docs/guides/spend-limits).

### Можно ли повторять любой ответ 429?

Нет. Денежный лимит не исчезает после короткой задержки. Проверяйте код и тип ошибки: у OpenAI есть отдельные коды лимита расходов, у Anthropic Messages — `enforced_spend_limit_reached`. Настоящие ограничения частоты требуют отдельной политики повторов; неопределённая отправка при этом сохраняет свой резерв. [OpenAI](https://developers.openai.com/api/docs/guides/spend-limits), [Anthropic](https://platform.claude.com/docs/en/api/rate-limits).

### Если оборвать поток, можно сразу вернуть резерв?

Только если подтверждена итоговая стоимость или доказано отсутствие отправки. Разрыв соединения сам по себе не подтверждает ни то, ни другое. Сохраните резерв и сверяйте запись; в показанном примере неопределённый результат приостанавливает новые платные вызовы.

### Почему лимит сессии не заменяет общий бюджет агента?

Сессия и владелец денег — разные сущности. В LiteLLM стоимость сессии накапливается после успешного ответа, а её счётчик по умолчанию живёт один час. Общий бюджет родительской задачи должен сохраняться при смене trace ID, запуске подагента и истечении такого счётчика. [LiteLLM Agent Iteration Budgets](https://docs.litellm.ai/docs/a2a_iteration_budgets).

### Гарантирует ли эта схема, что счёт никогда не превысит потолок?

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

## Источники

Внешние страницы, на которые ссылается это руководство, в порядке упоминания. Последнее обновление: 2026-10-07.

- [Текущее описание OpenAI](https://developers.openai.com/api/docs/guides/spend-limits) (developers.openai.com)
- [Anthropic rate/spend limits](https://platform.claude.com/docs/en/api/rate-limits) (platform.claude.com)
- [Cloudflare spend limits](https://developers.cloudflare.com/ai-gateway/features/spend-limits/) (developers.cloudflare.com)
- [LiteLLM Agent Iteration Budgets](https://docs.litellm.ai/docs/a2a_iteration_budgets) (docs.litellm.ai)
- [Документация бюджетов LiteLLM](https://docs.litellm.ai/docs/proxy/users) (docs.litellm.ai)
- [Redis гарантирует атомарное выполнение Lua-скрипта](https://redis.io/docs/latest/develop/programmability/eval-intro/) (redis.io)
