---
title: "Spend limits"
description: "spend-limits}"
canonical_url: "https://docs.superdurable.io/zh-Hans/reference/agents/spend-limits/"
source: "docs/reference/agents/spend-limits.mdx"
locale: "zh-Hans"
---

<a id="spend-limits"></a>

# Spend limits

A spend limit is usually per account or per API key, across many sessions. Dex
has no built-in limit. An application implements **ModelSpendLimit** and passes it
as **spend_limit** in the Python **AgentSessionOptions** or **spendLimit** in
TypeScript. Without one, the session checks nothing.

The session calls the interface around each model call of **RunModel** and
**CompactContext**:

| Call                   | When                                                                | What it gets                                                                 |
| ---------------------- | ------------------------------------------------------------------- | ---------------------------------------------------------------------------- |
| **check_model_call**   | Before each attempt, after turn control and the compaction decision | The session, turn, Step type, attempt, session settings, and committed usage |
| **report_model_usage** | When the attempt's model call returns, before the Step commits      | The same, with the call's usage and a stable report ID                       |

Counters and limits belong to the application. Put a reference to the account
or API key in the session settings, and read it in the check. Never put the key
itself there, because Dex stores session settings.

**A refused call ends the turn.** The session writes
**MODEL_CALL_REFUSED_ERROR_PREFIX** and the application's reason to
**AgentState.last_turn_error**. It keeps the finished tool results of the turn
with the usual failure note, and waits for the next user message. That message
starts a new turn, which is checked again. A refused **CompactContext** ends the
turn the same way. The session stays alive, with no new status or interrupt.

**A check that fails is a refusal without a reason.** A check that raises, or
takes longer than 30 seconds, fails the attempt. Dex retries the attempt under
the Step's retry policy. No model call runs without a check that passed. When
the retries run out, the turn fails with the error. To fail open instead, catch
your own errors in the check and allow the call.

**A failed report changes nothing.** The session logs and drops a report that
raises or times out. Failing the attempt would pay for the call a second time.

**Each attempt reports once.** The report ID is
**flow/turn/step execution/attempt**, so count each ID once. A retry is a new
paid call and has a new ID. A Step that reported and then did not commit, such
as when its Worker died, reports again on its retry. Both calls were paid for,
so the reports exceed **AgentUsage**, which keeps only the committed attempt.
A report also carries the session's usage including the call. So an
application that counts per session recovers from a dropped report on the next
one.

Design for these gaps:

- The check bounds the next call, not its cost. Sessions that pass together can
  overshoot.
- A harness, such as a Claude Agent SDK session, is checked once per turn
  segment, and its usage arrives when the segment ends. One segment can
  overshoot by all its model calls. Set the harness's own budget option too, such as **max_budget_usd** in
  Python or **maxBudgetUsd** in TypeScript.
- A call that fails, is cancelled, or is lost with its Worker reports nothing, though
  the provider may bill it. Some frameworks do not expose token counts, so a report
  can show **model_calls** above zero with zero tokens. That means unknown, not free.
  Count such calls by your own rule.
- Reports are not durable. A report runs inside the Step, so a Worker that dies
  between the model's answer and the report loses it. That attempt never commits,
  so **AgentUsage** does not hold the call either, and only the provider's bill does.
