# Как настроить лимиты расходов и бюджеты для ИИ-агентов: контроль затрат на API

> AI Release · @ai_release1 · https://ai-release.org/guides/kak-nastroit-limity-rashodov-i-byudzhety-dlya-ii-agento.html

## TL;DR
- Время выполнения: 40–60 минут
- Уровень сложности: Intermediate
- Понадобится: Python 3.10+, пакет PyYAML (`pip install pyyaml`), bash или PowerShell 7+, доступ к консоли провайдера LLM с правами на биллинг (точные пути в меню сверьте с документацией провайдера), каталог проекта агента с правами на запись в `logs/`

## Требования и подготовка
![Иллюстрация к разделу «Требования и подготовка»](https://ai-release.org/guides/img/kak-nastroit-limity-rashodov-i-byudzhety-dlya-ii-agento-ru-1.jpg)

- ОС: Linux, macOS или Windows 10/11 с PowerShell 7+ (`$PSVersionTable.PSVersion`).
- Python 3.10+ и pip: `python3 --version` → `Python 3.10.x` или выше.
- Пакет PyYAML: `python3 -m pip install --user pyyaml`.
- Утилита `jq` для ручной проверки JSONL-журнала (Linux/macOS: пакет `jq`; Windows: `winget install jqlang.jq`).
- Права на создание API-ключей и просмотр расходов в консоли провайдера — без них шаг 1 не выполнить.
- Каталог проекта, где уже лежит скрипт запуска агента (в примерах — `run_agent.py`); если скрипт называется иначе, замените имя в обёртке из шага 2.

## Шаг 1. Инвентаризация: раздельные ключи и журнал расходов

**Что сделать:** создайте отдельный API-ключ на каждого агента, присвойте агенту идентификатор через переменную окружения и включите запись журнала расходов в формате JSONL (JSON Lines — один JSON-объект на строку).

**Зачем:** пока все агенты ходят под одним ключом, вы не знаете, кто именно сжёг бюджет, и не можете заблокировать одного, не остановив остальных. Раздельные ключи дают независимое отключение и отдельную статистику; журнал — единственный источник цифр для расчёта лимита. Правила хранения ключей описаны в гайде [Безопасность ИИ-агентов: как защитить данные и избежать утечек](https://ai-release.org/guides/bezopasnost-ii-agentov-kak-zaschitit-dannye-i-izbezhat.html?utm_source=tg&utm_medium=channel&utm_campaign=guide_inline&utm_content=guide_to_guide).

Если у вас команда субагентов, заведите ключ и идентификатор каждому — как это устроено в материале [Claude Code для команды: как настроить субагентов и общий ИИ-суперагент](https://ai-release.org/guides/claude-code-dlya-komandy-kak-nastroit-subagentov-i-obsc.html?utm_source=tg&utm_medium=channel&utm_campaign=guide_inline&utm_content=guide_to_guide).

**Пример:** переменные окружения в `~/.bashrc` (Windows: `setx AI_AGENT_ID "support-bot"`):

```bash
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"
```

Каждая запись журнала — одна завершённая генерация:

```json
{"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 токенов сверьте с документацией провайдера, здесь они не приводятся намеренно.

**Как проверить:** допишите одну тестовую строку и убедитесь, что журнал читается:

```bash
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-строка, удалите её.

## Шаг 2. Бюджеты, лимиты и жёсткий стоп
![Иллюстрация к разделу «Шаг 2. Бюджеты, лимиты и жёсткий стоп»](https://ai-release.org/guides/img/kak-nastroit-limity-rashodov-i-byudzhety-dlya-ii-agento-ru-2.jpg)


**Что сделать:** опишите лимиты в YAML-файле и поставьте перед запуском агента скрипт-гейт, который сравнивает фактические траты с лимитом и завершает работу с ненулевым кодом.

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

`budgets.yaml`:

```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`:

```python
#!/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` ставит гейт перед агентом:

```bash
#!/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 "$@"
```

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

**Как проверить:** запустите гейт вручную и посмотрите на статус; затем убедитесь, что агент не стартует при превышении:

```bash
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 — запуск отменён`. Верните лимит обратно.

## Шаг 3. Проверка работы и финальный результат
![Иллюстрация к разделу «Шаг 3. Проверка работы и финальный результат»](https://ai-release.org/guides/img/kak-nastroit-limity-rashodov-i-byudzhety-dlya-ii-agento-ru-3.jpg)


**Что сделать:** проведите три проверки — нормальный запуск, срабатывание стопа и корректность цифр в журнале — и зафиксируйте чек-лист приёмки.

**Зачем:** гейт, который ни разу не сработал на тесте, обычно не срабатывает и в проде: ошибка в имени агента или в пути к журналу делает сумму трат нулевой.

**Пример прогона:**

```bash
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=…`.

Чек-лист приёмки:

- [ ] у каждого агента свой API-ключ и своя переменная `AI_AGENT_ID`
- [ ] `budgets.yaml` содержит `monthly_usd` и `daily_usd` для каждого агента
- [ ] все запуски идут через `./run_with_budget.sh`, а не напрямую через `run_agent.py`
- [ ] тест с искусственно заниженным лимитом даёт `status=BLOCKED` и `exit=1`
- [ ] в консоли провайдера выставлен независимый предел расходов на ключ
- [ ] журнал не содержит строк без `request_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=BLOCKED`
- Причина: ретраи и повторные вызовы записаны в журнал несколько раз с одинаковым содержимым, либо часть строк без `request_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."}}`
- Причина: сработал жёсткий лимит на стороне провайдера (по ключу или по аккаунту), о котором локальный журнал не знает.
- Решение: откройте раздел расходов в консоли провайдера и сверьте текущий spend и установленный предел (пути в меню — по документации провайдера); поднимите предел либо переведите агента на ключ с отдельным бюджетом: `export AI_AGENT_ID=research-agent-2`.

## Резюме и следующие шаги
Рабочая схема контроля — три слоя: отдельные ключи на агента, собственный журнал расходов и гейт, блокирующий запуск до запроса, плюс независимый лимит в консоли провайдера. Дальше имеет смысл снизить стоимость запросов: часть дешёвых вызовов можно увести на локальную модель — см. [Локальная модель или облако на iPhone: как настроить гибридный роутинг ИИ-запросов](https://ai-release.org/guides/lokalnaya-model-ili-oblako-na-iphone-kak-nastroit-gibri.html?utm_source=tg&utm_medium=channel&utm_campaign=guide_inline&utm_content=guide_to_guide).

## FAQ

**Как считать стоимость запроса, если провайдер не возвращает её в ответе?**
Умножьте `prompt_tokens` и `completion_tokens` из ответа на цену за 1000 токенов из прайса провайдера и запишите результат в `cost_usd`. Цены меняются — сверьтесь с документацией провайдера, а не с зашитыми в скрипт константами.

**Нужен ли отдельный бюджет каждому субагенту внутри одного процесса?**
Да, если у них разные профили нагрузки. Один `AI_AGENT_ID` на субагента и отдельная секция в `budgets.yaml` дают понять, кто именно выедает месячный лимит.

**Что делать, когда месячный бюджет исчерпан, а агент нужен сегодня?**
Переключите агента на резервный ключ с отдельной секцией бюджета (`AI_AGENT_ID=agent-fallback`) или понизьте стоимость запроса: сократите контекст и переведите часть шагов на более дешёвую модель из прайса.

**Гейт проверяет бюджет только перед запуском — не потратит ли агент лишнее внутри длинного цикла?**
Потратит: между стартом и финишем лимит не пересчитывается. Для длинных циклов вызывайте `check_budget.py` после каждого шага и завершайте цикл по коду возврата `1`.
