Budget and limits
mantis-agent-sdk tracks token usage and dollar cost across every turn of
every session. You can cap either, and every ResultMessage carries the
final accounting.
max_usd
Hard ceiling on session cost. If the next request would push spend over
the cap, the runtime raises BudgetExceededError before dispatching
it.
from mantis_agent import BudgetExceededError, query
try:
async for msg in query(
prompt="big task",
options=MantisAgentOptions(
model="deepseek-chat",
backend="https://api.deepseek.com/v1",
max_budget_usd=0.50, # 50 cents, hard cap
),
):
...
except BudgetExceededError as e:
# kind is "usd" or "turns"; limit is the cap, used is the spend at the stop.
print(f"Stopped at ${e.used:.4f} of ${e.limit:.2f} ({e.kind})")The exception carries kind, limit, and used. The partial transcript stays
on disk, so you can fork from a checkpoint and continue under a fresh cap.
max_turns
Cap the number of model calls regardless of cost:
options = MantisAgentOptions(
model="qwen2.5:7b",
max_turns=10,
)After the 10th assistant message, the runtime stops, even if the model
emitted another tool_use. The final ResultMessage will have
stop_reason='max_turns'.
Reading cost on the result
async for msg in query(prompt="...", options=MantisAgentOptions(
model="deepseek-chat",
)):
if msg.type == "result":
print(f"Cost: ${msg.total_cost_usd:.4f}")
for model_id, usage in msg.modelUsage.items():
print(f" {model_id}: {usage.inputTokens}in / "
f"{usage.outputTokens}out / ${usage.costUSD:.4f}")modelUsage is per-model — useful when sub-agents on different models
contributed to one session. Its field names are camelCase for byte-level TS-SDK
parity: inputTokens, outputTokens, cacheReadInputTokens,
cacheCreationInputTokens, webSearchRequests, costUSD, contextWindow,
maxOutputTokens.
The pricing table
PRICING_TABLE maps a (provider, model_id) tuple to a Pricing record:
from mantis_agent.budget import PRICING_TABLE, lookup_pricing
# The backend hint is what identifies the provider half of the key.
print(lookup_pricing("deepseek-v3", "https://api.deepseek.com/v1"))
print(len(PRICING_TABLE), "priced (provider, model) pairs")Pricing carries prompt_per_million, completion_per_million, and optional
cache_read_per_million / cache_write_per_million.
Priced providers today: deepseek, fireworks, groq, openrouter,
together, modal, plus the local runners (ollama, llamacpp, vllm,
tgi) at zero. A model with no row prices as None and contributes tokens but
no dollars — which is what keeps max_usd meaningful in a mixed session where
some sub-agents run locally.
A cap only bites where there is a price. Without a matching row, spend stays at
$0.00and the cap is never reached. Check withlookup_pricingbefore relying onmax_usd.
Adding or overriding a model
There is no register_pricing function — insert into the table:
from mantis_agent.budget import PRICING_TABLE, Pricing
PRICING_TABLE[("together", "my-org/my-finetune")] = Pricing(
prompt_per_million=1.50,
completion_per_million=3.00,
)The first element is the provider key the backend URL resolves to; the second
is the model id as that provider spells it. (There is no per-call pricing
option either — earlier versions of this page showed a pricing_override key,
which never existed.)
Cost from outside the session
from mantis_agent.budget import estimate_cost
from mantis_agent.types import Usage
usage = Usage(input_tokens=1_200, output_tokens=400)
cost = estimate_cost(usage, "deepseek-v3", "https://api.deepseek.com/v1")
print("no pricing row" if cost is None else f"${cost:.6f}")estimate_cost(usage, model_id, backend_hint=None) takes a Usage struct,
not loose token counts, and returns None for an unpriced model.
Budgeting across several calls
ClaudeSDKClient holds one budget for its lifetime; there is no per-call cap
argument. For per-step ceilings, use a fresh options object per step:
from mantis_agent import ClaudeSDKClient, MantisAgentOptions
async def run_step(prompt: str, cap: float) -> None:
options = MantisAgentOptions(model="deepseek-chat", max_budget_usd=cap)
async with ClaudeSDKClient(options=options) as client:
await client.query(prompt)
async for msg in client.receive_response():
print(msg.type)Note the field name: max_budget_usd on MantisAgentOptions, max_usd in a
plain options dict. They are the same cap under two names, and the wrong one in
the wrong place is silently ignored.