Cost Limits для AI агентів (зупинити витрати) + Код

Token limits недостатньо. Як зробити жорсткий cap загальної вартості (LLM + tools) зі зрозумілим stop reason, щоб не було сюрпризів.
На цій сторінці
  1. Проблема (з реального продакшену)
  2. Чому це ламається в продакшені
  3. 1) Лімітують tokens і забувають про tools
  4. 2) Вартість без stop reason не операбельна
  5. 3) Без per-tenant caps один клієнт може спалити місяць
  6. Приклад реалізації (реальний код)
  7. Реальний інцидент (з цифрами)
  8. Компроміси
  9. Коли НЕ варто
  10. Чекліст (можна копіювати)
  11. Безпечний дефолтний конфіг (JSON/YAML)
  12. FAQ (3–5)
  13. Пов’язані сторінки (3–6 лінків)
Інтерактивний флоу
Сценарій:
Крок 1/3: Execution

Action is proposed as structured data (tool + args).

Проблема (з реального продакшену)

Дивишся на LLM usage і думаєш: “норм”.

Потім прилітає рахунок від browser vendor / scraper / data provider. І стає ясно: ти зробив speed limit на одному колесі.

Cost limits не гламурні. Але це різниця між:

  • “інколи дорого”
  • і “це генератор витрат без меж”

Чому це ламається в продакшені

1) Лімітують tokens і забувають про tools

У реальних агентів:

  • tokens більш-менш передбачувані
  • tools — хаос (retries, rate limits, варіативна робота)

Якщо tool коштує $0.20 за call, 10 calls = $2. Це трапляється дуже швидко.

2) Вартість без stop reason не операбельна

Якщо ти бачиш тільки “timeout”, ніхто не розуміє, що причина — “занадто дорого”. Користувачі повторюють і роблять гірше.

3) Без per-tenant caps один клієнт може спалити місяць

Multi-tenant без caps — це коли один tenant ламає бюджет усім.

Приклад реалізації (реальний код)

Простий cost guard:

  • tokens + tool costs в одному state
  • check після кожного step/tool call
  • stop reason max_usd
PYTHON
from dataclasses import dataclass, field
import time


TOOL_USD = {
  "browser.run": 0.20,
  "search.read": 0.00,
}


@dataclass(frozen=True)
class CostPolicy:
  max_usd: float = 1.00
  max_seconds: int = 60


@dataclass
class CostState:
  started_at: float = field(default_factory=time.time)
  tokens_in: int = 0
  tokens_out: int = 0
  tool_usd: float = 0.0

  def elapsed_s(self) -> float:
      return time.time() - self.started_at


def estimate_model_usd(tokens_in: int, tokens_out: int) -> float:
  return (tokens_in + tokens_out) * 0.000002


class CostExceeded(RuntimeError):
  def __init__(self, stop_reason: str, *, state: CostState):
      super().__init__(stop_reason)
      self.stop_reason = stop_reason
      self.state = state


class CostGuard:
  def __init__(self, policy: CostPolicy):
      self.policy = policy
      self.state = CostState()

  def total_usd(self) -> float:
      return estimate_model_usd(self.state.tokens_in, self.state.tokens_out) + self.state.tool_usd

  def check(self) -> None:
      if self.total_usd() > self.policy.max_usd:
          raise CostExceeded("max_usd", state=self.state)
      if self.state.elapsed_s() > self.policy.max_seconds:
          raise CostExceeded("max_seconds", state=self.state)

  def on_model(self, *, tokens_in: int, tokens_out: int) -> None:
      self.state.tokens_in += tokens_in
      self.state.tokens_out += tokens_out
      self.check()

  def on_tool(self, *, tool: str) -> None:
      self.state.tool_usd += float(TOOL_USD.get(tool, 0.0))
      self.check()
JAVASCRIPT
const TOOL_USD = { "browser.run": 0.2, "search.read": 0.0 };

export class CostExceeded extends Error {
constructor(stopReason, { state }) {
  super(stopReason);
  this.stopReason = stopReason;
  this.state = state;
}
}

export class CostGuard {
constructor(policy) {
  this.policy = policy;
  this.state = { startedAtMs: Date.now(), tokensIn: 0, tokensOut: 0, toolUsd: 0 };
}

totalUsd() {
  return estimateModelUsd(this.state.tokensIn, this.state.tokensOut) + this.state.toolUsd;
}

check() {
  if (this.totalUsd() > this.policy.maxUsd) throw new CostExceeded("max_usd", { state: this.state });
  if ((Date.now() - this.state.startedAtMs) / 1000 > this.policy.maxSeconds) {
    throw new CostExceeded("max_seconds", { state: this.state });
  }
}

onModel({ tokensIn, tokensOut }) {
  this.state.tokensIn += tokensIn;
  this.state.tokensOut += tokensOut;
  this.check();
}

onTool({ tool }) {
  this.state.toolUsd += Number(TOOL_USD[tool] ?? 0);
  this.check();
}
}

function estimateModelUsd(tokensIn, tokensOut) {
return (tokensIn + tokensOut) * 0.000002;
}

Реальний інцидент (з цифрами)

Ми бачили research loop, яка використовувала browser tools.

Вендор був нестабільний того дня. Tools ретраять. Агент ретраїть. І один request коштує більше, ніж твоя середня денна вартість.

Імпакт:

  • p95 spend/request підскочив до $4.80
  • queue забилась (runs стали довші)
  • сапорт ~2 години розгрібав “повільно”

Fix:

  1. жорсткий cap max_usd + stop reason
  2. circuit breaker на рівні tool при нестабільному vendor
  3. caching/dedupe для однакових URLs/queries

Компроміси

  • Caps інколи відрізають корисні run’и.
  • Точна ціна per tool call складна; approximate ок як guardrail.
  • Cost limits без budgets (steps/time/tool calls) — неповно.

Коли НЕ варто

  • Якщо взагалі не можеш оцінити cost — став хоча б max_seconds і max_tool_calls.
  • Навіть для “безкоштовних” внутрішніх tools cap потрібен, якщо поруч є платні вендори.

Чекліст (можна копіювати)

  • [ ] cap max_usd на run
  • [ ] stop reason max_usd у логах і відповіді
  • [ ] conservative tool costs (approx)
  • [ ] circuit breaker для flaky vendors
  • [ ] per-tenant caps / tiers
  • [ ] alert на spikes max_usd stops

Безпечний дефолтний конфіг (JSON/YAML)

YAML
cost_limits:
  max_usd: 1.00
  max_seconds: 60
tool_costs_usd:
  browser.run: 0.20
  search.read: 0.00
stop_reasons:
  log: true
  surface_to_user: true

FAQ (3–5)

Достатньо лімітувати тільки tools?
Ні. Tokens теж можуть “вибухнути” (довгий контекст, retries). Трекай обидва.
Як зробити caps per-tenant?
Через tiers. І логуйте spend per tenant, інакше дізнаєтесь наприкінці місяця.
Який default cap нормальний?
Достатньо низький, щоб не було сюрпризів, але достатньо високий для нормальних run’ів. Стартуй з $1 і тюнінгуй по даних.
Чому не просто брати найдешевшу модель?
Бо модель часто не найдорожча частина. Tool calls + retries — реальний драйвер витрат.

Q: Достатньо лімітувати тільки tools?
A: Ні. Tokens теж можуть “вибухнути” (довгий контекст, retries). Трекай обидва.

Q: Як зробити caps per-tenant?
A: Через tiers. І логуйте spend per tenant, інакше дізнаєтесь наприкінці місяця.

Q: Який default cap нормальний?
A: Достатньо низький, щоб не було сюрпризів, але достатньо високий для нормальних run’ів. Стартуй з $1 і тюнінгуй по даних.

Q: Чому не просто брати найдешевшу модель?
A: Бо модель часто не найдорожча частина. Tool calls + retries — реальний драйвер витрат.

Пов’язані сторінки (3–6 лінків)

Не впевнені, що це ваш кейс?

Спроєктувати агента →
⏱️ 5 хв читанняОновлено Бер, 2026Складність: ★★★
Реалізувати в OnceOnly
Budgets + permissions you can enforce at the boundary.
Використати в OnceOnly
# onceonly guardrails (concept)
version: 1
budgets:
  max_steps: 25
  max_tool_calls: 12
  max_seconds: 60
  max_usd: 1.00
policy:
  tool_allowlist:
    - search.read
    - http.get
writes:
  require_approval: true
  idempotency: true
controls:
  kill_switch: { enabled: true }
Інтегровано: продакшен-контрольOnceOnly
Додай guardrails до агентів з tool-calling
Зашип цей патерн з governance:
  • Бюджетами (кроки / ліміти витрат)
  • Дозволами на інструменти (allowlist / blocklist)
  • Kill switch та аварійна зупинка
  • Ідемпотентність і dedupe
  • Audit logs та трасування
Інтегрована згадка: OnceOnly — контрольний шар для продакшен агент-систем.
Автор

Цю документацію курують і підтримують інженери, які запускають AI-агентів у продакшені.

Контент створено з допомогою AI, із людською редакторською відповідальністю за точність, ясність і продакшн-релевантність.

Патерни та рекомендації базуються на постмортемах, режимах відмов і операційних інцидентах у розгорнутих системах, зокрема під час розробки та експлуатації governance-інфраструктури для агентів у OnceOnly.