pip install pyyaml), bash или PowerShell 7+, доступ к консоли провайдера LLM с правами на биллинг (точные пути в меню сверьте с документацией провайдера), каталог проекта агента с правами на запись в logs/
$PSVersionTable.PSVersion).python3 --version → Python 3.10.x или выше.python3 -m pip install --user pyyaml.jq для ручной проверки JSONL-журнала (Linux/macOS: пакет jq; Windows: winget install jqlang.jq).run_agent.py); если скрипт называется иначе, замените имя в обёртке из шага 2.Что сделать: создайте отдельный API-ключ на каждого агента, присвойте агенту идентификатор через переменную окружения и включите запись журнала расходов в формате JSONL (JSON Lines — один JSON-объект на строку).
Зачем: пока все агенты ходят под одним ключом, вы не знаете, кто именно сжёг бюджет, и не можете заблокировать одного, не остановив остальных. Раздельные ключи дают независимое отключение и отдельную статистику; журнал — единственный источник цифр для расчёта лимита. Правила хранения ключей описаны в гайде Безопасность ИИ-агентов: как защитить данные и избежать утечек.
Если у вас команда субагентов, заведите ключ и идентификатор каждому — как это устроено в материале Claude Code для команды: как настроить субагентов и общий ИИ-суперагент.
Пример: переменные окружения в ~/.bashrc (Windows: setx AI_AGENT_ID "support-bot"):
export AI_AGENT_ID=support-bot
export AI_LEDGER_PATH="$HOME/projects/support-bot/logs/ledger.jsonl"
export AI_BUDGET_FILE="$HOME/projects/support-bot/budgets.yaml"
mkdir -p "$(dirname "$AI_LEDGER_PATH")"
touch "$AI_LEDGER_PATH"
Каждая запись журнала — одна завершённая генерация:
{"ts":"2025-05-14T09:12:03Z","request_id":"req_7f3a91","agent":"support-bot","model":"<модель-из-прайса>","prompt_tokens":1180,"completion_tokens":260,"cost_usd":0.0094}
Поле cost_usd берите из ответа провайдера или считайте по его прайсу — точные цены за 1000 токенов сверьте с документацией провайдера, здесь они не приводятся намеренно.
Как проверить: допишите одну тестовую строку и убедитесь, что журнал читается:
echo '{"ts":"2025-05-14T09:12:03Z","request_id":"req_test_1","agent":"support-bot","cost_usd":0.01}' >> "$AI_LEDGER_PATH"
wc -l "$AI_LEDGER_PATH"
jq -c . "$AI_LEDGER_PATH" | tail -n 1
Ожидаемый вывод: 1 (число строк) и распечатанный JSON без ошибок парсинга. Если jq ругается parse error: Invalid numeric literal — в файл попала не JSON-строка, удалите её.

Что сделать: опишите лимиты в YAML-файле и поставьте перед запуском агента скрипт-гейт, который сравнивает фактические траты с лимитом и завершает работу с ненулевым кодом.
Зачем: лимит в консоли провайдера срабатывает уже после того, как деньги потрачены, и обычно на весь аккаунт. Гейт на вашей стороне блокирует конкретного агента до отправки запроса и работает одинаково для любого провайдера.
budgets.yaml:
version: 1
currency: USD
defaults:
monthly_usd: 20
daily_usd: 2
hard_stop: true
agents:
support-bot:
monthly_usd: 40
daily_usd: 3
hard_stop: true # true — блокировать запуск, false — только предупреждать
research-agent:
monthly_usd: 120
daily_usd: 10
hard_stop: true # исследовательский агент дороже: больше инструментов и контекста
check_budget.py:
#!/usr/bin/env python3
"""Гейт бюджета: 0 — можно тратить, 1 — лимит превышен, 2 — ошибка конфигурации."""
import argparse, json, os, sys
from datetime import datetime, timezone
import yaml
def load_limits(path, agent):
with open(path, encoding="utf-8") as f:
cfg = yaml.safe_load(f) or {}
limits = dict(cfg.get("defaults", {}))
limits.update((cfg.get("agents") or {}).get(agent, {}))
if "monthly_usd" not in limits or "daily_usd" not in limits:
sys.exit(f"config error: monthly_usd/daily_usd missing for agent={agent}")
return limits
def spend(ledger, agent):
month = datetime.now(timezone.utc).strftime("%Y-%m")
day = datetime.now(timezone.utc).strftime("%Y-%m-%d")
mtd = dtd = 0.0
seen = set()
with open(ledger, encoding="utf-8") as f:
for line in f:
line = line.strip()
if not line:
continue
row = json.loads(line)
if row.get("agent") != agent:
continue
rid = row.get("request_id")
if rid:
if rid in seen: # ретраи не должны считаться дважды
continue
seen.add(rid)
ts = row["ts"]
cost = float(row.get("cost_usd", 0))
if ts.startswith(month):
mtd += cost
if ts.startswith(day):
dtd += cost
return mtd, dtd
if __name__ == "__main__":
p = argparse.ArgumentParser()
p.add_argument("--agent", default=os.environ.get("AI_AGENT_ID"))
p.add_argument("--budgets", default=os.environ.get("AI_BUDGET_FILE", "budgets.yaml"))
p.add_argument("--ledger", default=os.environ.get("AI_LEDGER_PATH", "logs/ledger.jsonl"))
a = p.parse_args()
lim = load_limits(a.budgets, a.agent)
mtd, dtd = spend(a.ledger, a.agent)
m_lim, d_lim = float(lim["monthly_usd"]), float(lim["daily_usd"])
blocked = a.hard_stop_check = bool(lim.get("hard_stop")) and (mtd >= m_lim or dtd >= d_lim)
print(f"agent={a.agent} month={mtd:.2f}/{m_lim:.2f} day={dtd:.2f}/{d_lim:.2f} "
f"status={'BLOCKED' if blocked else 'OK'}")
sys.exit(1 if blocked else 0)
Обёртка run_with_budget.sh ставит гейт перед агентом:
#!/usr/bin/env bash
set -euo pipefail
python3 check_budget.py --agent "$AI_AGENT_ID" || {
echo "budget stop: agent=$AI_AGENT_ID — запуск отменён" >&2
exit 1
}
exec python3 run_agent.py "$@"
Дополнительно поставьте второй, независимый предел в консоли провайдера на каждый ключ — он страхует от ошибок в вашем журнале. Где именно находится раздел лимитов, сверьте с документацией провайдера: интерфейсы отличаются.
Как проверить: запустите гейт вручную и посмотрите на статус; затем убедитесь, что агент не стартует при превышении:
chmod +x run_with_budget.sh
python3 check_budget.py --agent support-bot; echo "exit=$?"
Ожидаемый вывод при тратах ниже лимита: agent=support-bot month=12.40/40.00 day=1.10/3.00 status=OK, затем exit=0. Чтобы увидеть блокировку, временно поставьте в budgets.yaml для support-bot значение monthly_usd: 1 — вывод станет status=BLOCKED, exit=1, а ./run_with_budget.sh напечатает budget stop: agent=support-bot — запуск отменён. Верните лимит обратно.

Что сделать: проведите три проверки — нормальный запуск, срабатывание стопа и корректность цифр в журнале — и зафиксируйте чек-лист приёмки.
Зачем: гейт, который ни разу не сработал на тесте, обычно не срабатывает и в проде: ошибка в имени агента или в пути к журналу делает сумму трат нулевой.
Пример прогона:
AI_AGENT_ID=research-agent ./run_with_budget.sh --task "собрать 10 источников"
echo "---"
tail -n 3 "$AI_LEDGER_PATH" | jq -c '{agent, request_id, cost_usd}'
Успех выглядит так: агент отработал, в журнале появились новые строки с "agent":"research-agent", а повторный запуск python3 check_budget.py --agent research-agent печатает status=OK уже с выросшей суммой month=….
Чек-лист приёмки:
AI_AGENT_IDbudgets.yaml содержит monthly_usd и daily_usd для каждого агента./run_with_budget.sh, а не напрямую через run_agent.pystatus=BLOCKED и exit=1request_idКак проверить: grep -c '"request_id"' "$AI_LEDGER_PATH" и wc -l < "$AI_LEDGER_PATH" должны дать одинаковые числа. Расхождение означает, что часть записей пишется без идентификатора и дедупликация в скрипте по ним не работает.
Проблема. Гейт падает при первом запуске на импорте YAML
ModuleNotFoundError: No module named 'yaml'pyyaml установлен в другой интерпретатор (например, системный Python против venv).source .venv/bin/activate && python -m pip install pyyaml && python check_budget.py --agent support-bot.Проблема. Лимит срабатывает раньше времени, хотя траты реально ниже
agent=support-bot month=41.37/40.00 day=1.02/3.00 status=BLOCKEDrequest_id — дедупликация их не отсекает.jq -r '.request_id' "$AI_LEDGER_PATH" | sort | uniq -d | head; если дубли есть, оставьте в скрипте дедупликацию по request_id и удалите старые строки без него: jq -c 'select(.request_id != null)' "$AI_LEDGER_PATH" > ledger.clean && mv ledger.clean "$AI_LEDGER_PATH".Проблема. Агент получает отказ от провайдера, хотя локальный гейт показывает OK
HTTP 429 {"error":{"type":"insufficient_quota","message":"You exceeded your current quota, please check your plan and billing details."}}export AI_AGENT_ID=research-agent-2.Рабочая схема контроля — три слоя: отдельные ключи на агента, собственный журнал расходов и гейт, блокирующий запуск до запроса, плюс независимый лимит в консоли провайдера. Дальше имеет смысл снизить стоимость запросов: часть дешёвых вызовов можно увести на локальную модель — см. Локальная модель или облако на iPhone: как настроить гибридный роутинг ИИ-запросов.
Как считать стоимость запроса, если провайдер не возвращает её в ответе?
Умножьте prompt_tokens и completion_tokens из ответа на цену за 1000 токенов из прайса провайдера и запишите результат в cost_usd. Цены меняются — сверьтесь с документацией провайдера, а не с зашитыми в скрипт константами.
Нужен ли отдельный бюджет каждому субагенту внутри одного процесса?
Да, если у них разные профили нагрузки. Один AI_AGENT_ID на субагента и отдельная секция в budgets.yaml дают понять, кто именно выедает месячный лимит.
Что делать, когда месячный бюджет исчерпан, а агент нужен сегодня?
Переключите агента на резервный ключ с отдельной секцией бюджета (AI_AGENT_ID=agent-fallback) или понизьте стоимость запроса: сократите контекст и переведите часть шагов на более дешёвую модель из прайса.
Гейт проверяет бюджет только перед запуском — не потратит ли агент лишнее внутри длинного цикла?
Потратит: между стартом и финишем лимит не пересчитывается. Для длинных циклов вызывайте check_budget.py после каждого шага и завершайте цикл по коду возврата 1.