夜のあいだに回している検証用のセッションへ、はじめて予算の上限を付けた朝のことでした。25 ドルまでは走らせてよいつもりで amount に "25" と書いたのですが、セッションは数往復で止まっており、停止理由には budget_reached とだけ残っておりました。
モデルの指定を疑い、環境を疑い、最後に SDK の型定義を開いて、ようやく合点がいきました。金額は補助単位で数えます。"25" は 25 ドルではなく、25 セントだったのです。
金額は数ではなく、通貨の最小単位で数えた文字列。 この一行を頭に置いておくだけで、私はそれ以降この取り違えをしておりません。
予算は「最小単位の整数文字列」で渡します
セッションに付ける予算は、BetaManagedAgentsBudgetLimit という小さな構造で表されます。@anthropic-ai/sdk の 0.124.0 で型定義を確認したところ、中身は次の二つだけでした。
| フィールド | 型 | 意味 |
|---|---|---|
type | 'limit' | 固定値 |
max_list_cost | { amount, currency } | 停止する金額 |
そして amount の説明文に、単位の答えがそのまま書かれておりました。補助単位の整数を、先頭ゼロなしの文字列で渡します。"2500" が 25 ドル 00 セント、"50" が 50 セントです。文字列である理由も添えられており、浮動小数の丸めを一度も挟まないため、と説明されています。
currency は大文字の ISO-4217 で、いまのところ USD のみです。
型定義はご自身の手元でも数分で確かめられますので、この記事を鵜呑みにせず一度ご覧いただければと思います。
# API キーは不要です。型定義ファイルを読むだけの確認です
mkdir -p ~/probe && cd ~/probe && npm init -y >/dev/null
npm install @anthropic-ai/sdk
grep -A8 "interface BetaMonetaryAmount" \
node_modules/@anthropic-ai/sdk/resources/beta/beta.d.ts
# 期待する出力: amount の説明に「minor units」「"2500" is $25.00」が含まれますパッケージそのものは @anthropic-ai/sdk(npm) から入ります。
予算つきでセッションを作る
最小の形はこれだけです。ドル表記から補助単位への変換を必ず関数に閉じ込めるところが、私なりの再発防止になっております。
import Anthropic from "@anthropic-ai/sdk";
const client = new Anthropic({ apiKey: process.env.ANTHROPIC_API_KEY });
// ドル建ての数値を補助単位の整数文字列へ。手書きの "25" を根絶するための一枚
function usd(dollars) {
const cents = Math.round(dollars * 100);
if (!Number.isFinite(cents) || cents < 0) {
throw new Error(`予算が不正です: ${dollars}`);
}
return { amount: String(cents), currency: "USD" };
}
const session = await client.beta.sessions.create({
agent: "agent_xxxxxxxx",
environment_id: "env_xxxxxxxx",
budget: { type: "limit", max_list_cost: usd(25) }, // → amount: "2500"
betas: ["managed-agents-2026-04-01"],
});
console.log(session.budget);
// 期待する出力:
// { type: 'limit', max_list_cost: { amount: '2500', currency: 'USD' } }作成後の session.budget を必ず一度出力してみてください。amount が "2500" になっていれば意図どおりで、"25" のままなら 100 分の 1 の上限で走り出しています。ログに一行残しておけば、翌朝の私のような遠回りをせずに済みます。
セッションごとに違う上限を持たせたい場合の考え方は、共通エージェント定義をセッション単位で使い分ける運用設計でも触れています。
止まったことに気づく二つの経路
上限に達したセッションは、新しいモデルリクエストを始めずに待機へ入ります。これを知る経路は二つあります。
ひとつは session.status_idle イベントの stop_reason です。型定義では四つの値を取りうると定義されており、そのうちの一つが budget_reached でした。
| stop_reason | 状態 | こちらの動き |
|---|---|---|
end_turn | ターンを終えて待機 | 次の入力を送る |
requires_action | 確認待ち | ツール確認を返す |
retries_exhausted | 再試行が尽きた | 人へ渡す |
budget_reached | 予算に到達 | 上げるか外す |
const stream = await client.beta.sessions.events.stream({
session_id: session.id,
betas: ["managed-agents-2026-04-01"],
});
for await (const event of stream) {
if (event.type !== "session.status_idle") continue;
if (event.stop_reason.type === "budget_reached") {
const s = await client.beta.sessions.retrieve(session.id, {
betas: ["managed-agents-2026-04-01"],
});
// list_cost も補助単位です。表示するときだけ 100 で割ります
const spent = Number(s.usage?.list_cost?.amount ?? 0) / 100;
console.log(`予算到達: 消費 $${spent.toFixed(2)}`);
break;
}
}もうひとつは Webhook です。session.budget_reached という種別が用意されており、届く本文はセッション ID と組織 ID とワークスペース ID だけの簡素な形でした。金額を知りたい場合は、受け取ってからセッションを取り直します。
無人で回す処理では、私は Webhook で気づいてキューに積み、判断そのものは人の目が入る時間帯へ寄せる形を好みます。夜中に自動で上限を上げてしまうと、上限を置いた意味がなくなってしまうためです。
二つの経路の性格が違うのだと気づいたのは、しばらく両方を並べて動かしたあとでした。ストリームは停止の中身まで教えてくれますが、そのために接続を開き続けます。Webhook は「何かが起きた」だけを届けます——見ていない処理にとっては、それでちょうど足りるのかもしれません。
上げて再開する場合と、外して再開する場合
再開の手当ては二通りあります。素直なのは上限を引き上げる方です。
await client.beta.sessions.update(session.id, {
budget: { type: "limit", max_list_cost: usd(50) }, // 25 ドル → 50 ドル
betas: ["managed-agents-2026-04-01"],
});ここで型定義に、予想していなかった但し書きが添えられておりました。budget_reached は予算を使い切ったときだけでなく、定価の付いていないモデルを使っていて予算が測れないときにも返ります。そしてその場合、上限を引き上げる要求は拒否されます。測れないものに天井を足しても意味がないためで、再開したいのであれば予算そのものを外します。
// 更新時に限り null を渡せます。作成時のパラメータは null を受け付けません
await client.beta.sessions.update(session.id, {
budget: null,
betas: ["managed-agents-2026-04-01"],
});作成時の budget と更新時の budget は、型が微妙に異なります。前者は値のみ、後者は null を許容します。「消したい」を作成時のパラメータで表そうとすると通らないので、解除は必ず更新側で行ってください。
デプロイに置いた予算は、合計の上限ではありません
デプロイメントにも同じ形の予算を置けます。ここは読み違えやすい箇所で、私も最初は「このデプロイ全体で 25 ドル」と受け取っておりました。
実際には、そのデプロイから始まる各セッションへ個別に適用されます。25 ドルの予算を持つデプロイが 20 セッション動けば、上限として効くのは 20 回ぶんです。合計に天井を置きたいのであれば、消費を自分で集計して止める仕組みが別に要ります。その考え方はClaude API の本番運用で予算上限を確実に守る設計にまとめてありますので、合計側の設計を詰めたい方はそちらもご覧ください。
まずは、いま動かしているセッションの budget.max_list_cost.amount を一度だけ出力してみてください。桁がふたつ足りていれば、それが今日いちばん安く見つかる不具合です。
最後までお読みくださり、ありがとうございました。単位ひとつで走行距離が 100 分の 1 になる仕組みは、知ってしまえば単純ですが、知るまでは原因の見当がつきません。同じ朝を過ごす方が一人でも減れば嬉しく思います。