6月15日の朝、私は自分のメモを書き換えていました。前夜まで「本日発効」と記録していた課金変更が、直前になって撤回(保留)されたからです。
告知されていたのは、Agent SDK・headless の claude -p 実行・GitHub Actions・サードパーティ製エージェントを、サブスクリプションの上限とは別枠の月次クレジットへ移す、という変更でした。繰り越しなし、full API レート課金。私は App Store と Google Play でアプリを公開しながら個人開発を続けてきて、いまは4つのサイトを自動運用しています。そのうち headless 実行に依存している工程がいくつもあります。だから前の週から、コスト構造を測り直し、どの工程を別枠へ寄せるかを考えていました。
ところが当日、その前提そのものが消えました。これらの利用は引き続きサブスク上限内で扱われる、という公式の確認が出たのです。
おもしろいのは、このとき私の手元では、ほとんど何も書き換える必要がなかったことです。パイプラインのコードは1行も触っていません。今日は、なぜ慌てずに済んだのか、そして「告知された変更が直前で覆る」状況に耐える設計とは何かを、実際の構成とともに書き残しておきます。
「組み替えてから撤回」が一番もったいない
自動運用をしていると、こういう告知への反応は3通りに分かれます。ひとつは、告知どおりに先回りして組み替えてしまう。ふたつめは、何もせず発効後に慌てる。みっつめは、発効しても撤回されても、どちらでも同じコードが動くようにしておく。
先回りは一見すると勤勉に見えますが、撤回されたときに一番損をします。私自身、過去に別のサービスで「来月から API のエンドポイントが変わる」という告知を真に受けて切り替え、結局その変更が延期になり、新旧両対応のまま半年放置した経験があります。先回りした分のコードは、撤回されると「動くのに意味のない複雑性」になって残るのです。
今回の課金変更で私が決めていたのは、発効を確認するまで、パイプライン本体のロジックは触らない ということでした。代わりに、発効しても撤回されても切り替えられる「つまみ」を1つだけ用意しておく。これが可逆な設計の核心です。
課金モードを1箇所のリゾルバに閉じ込める
まず、「いま自分のパイプラインがどの課金前提で動いているか」を表す状態を、コードのあちこちに散らさず、1つのリゾルバに閉じ込めます。環境変数を読むのはこのファイルだけ、という約束にします。
// src/ops/billing-mode.ts
// 課金前提を表す状態。これ以外の場所で環境変数を直接読まない。
export type BillingMode = "subscription" | "metered_credits" ;
export interface BillingPolicy {
mode : BillingMode ;
// headless 工程を実行してよいか(クレジット枯渇時は false にできる)
headlessAllowed : boolean ;
// 1日に許容する headless 実行回数の上限(pacing 用)
dailyHeadlessBudget : number ;
}
export function resolveBillingPolicy ( env = process.env) : BillingPolicy {
// 既定はサブスク前提。発効が確認できたら BILLING_MODE=metered_credits を1行入れるだけ。
const mode : BillingMode =
env. BILLING_MODE === "metered_credits" ? "metered_credits" : "subscription" ;
if (mode === "metered_credits" ) {
return {
mode,
headlessAllowed: env. HEADLESS_DISABLED !== "1" ,
// 別枠クレジットは繰り越しがないので、日次でならして使う
dailyHeadlessBudget: Number (env. DAILY_HEADLESS_BUDGET ?? 12 ),
};
}
return {
mode,
headlessAllowed: env. HEADLESS_DISABLED !== "1" ,
// サブスク上限内なら回数の意味が薄いので、実質無制限に近い値
dailyHeadlessBudget: Number (env. DAILY_HEADLESS_BUDGET ?? 9999 ),
};
}
ここが可逆性の要です。撤回されたら BILLING_MODE を消す(または subscription に戻す)だけ。発効が確認できたら metered_credits を入れるだけ。判断の結果が反映される場所が、たった1箇所に集約されている ので、どちらに転んでも差分は最小で済みます。
なぜ boolean を直接持たずに mode という列挙にしたかというと、将来「第3の課金形態」が出たときに分岐を1つ足すだけで拡張できるからです。真偽値だと二択に固定されてしまい、撤回どころか「別の変更」が来たときに作り直しになります。
headless 呼び出しをモード非依存にする
次に、実際に claude -p を呼ぶ工程です。ここでモードを意識させないのが肝心で、呼び出し側は「このポリシーで走らせていいか」を尋ねるだけにします。
// src/ops/run-headless.ts
import { spawn } from "node:child_process" ;
import { resolveBillingPolicy } from "./billing-mode" ;
interface HeadlessResult {
ran : boolean ;
reason ?: string ;
stdout ?: string ;
}
// 当日の実行回数を記録する軽量カウンタ(KV やファイルで永続化する想定)
async function consumeDailyBudget ( limit : number ) : Promise < boolean > {
const used = await readTodayCount (); // 実装は環境依存
if (used >= limit) return false ;
await incrementTodayCount ();
return true ;
}
export async function runHeadless (
prompt : string ,
opts : { stage : string }
) : Promise < HeadlessResult > {
const policy = resolveBillingPolicy ();
if ( ! policy.headlessAllowed) {
return { ran: false , reason: "headless disabled by policy" };
}
// モードに関わらず、日次バジェットの範囲で走らせる。
// subscription なら budget が大きいので実質素通り、
// metered_credits なら自然にならされる。
const ok = await consumeDailyBudget (policy.dailyHeadlessBudget);
if ( ! ok) {
return { ran: false , reason: `daily budget exhausted (${ policy . mode })` };
}
const stdout = await new Promise < string >(( resolve , reject ) => {
const child = spawn ( "claude" , [ "-p" , prompt], { encoding: "utf8" } as never );
let out = "" ;
child.stdout?. on ( "data" , ( d ) => (out += d));
child. on ( "close" , ( code ) =>
code === 0 ? resolve (out) : reject ( new Error ( `exit ${ code }` ))
);
});
return { ran: true , stdout };
}
呼び出し側のコードは runHeadless(prompt, { stage: "generate" }) と書くだけで、課金がどちらの前提でも変わりません。発効しても撤回されても、この関数を呼ぶ全工程は一切修正不要 です。私が当日コードを1行も触らずに済んだのは、この一段の間接化があったからでした。
ひとつ実運用で気づいたのは、consumeDailyBudget を「実行前」に消費させることです。最初は実行後にカウントしていたのですが、claude -p が途中で失敗したときにカウントが進まず、リトライで二重に走ってしまいました。これは本番運用で踏みやすい落とし穴です。回避するには、先にカウンタを進め、失敗時はそのまま消費済みとして扱う方が、繰り越しのない別枠クレジットでは安全です。少し損をするように見えても、暴走で枠を一気に溶かすより、ずっとましでした。私自身、ここはエラー時の挙動を二度測り直してから固めました。
「組み替えるか据え置くか」を基準で決める
撤回や延期のたびに感情で判断すると疲れます。私は次の表で「いま動くべきか」を機械的に決めています。
状況 公式の確度 とるべき行動 触る場所
変更が告知された段階 予告のみ 計測と試算だけ。本体は触らない なし(メモのみ)
発効が公式確認された 確定 リゾルバの環境変数を1行切り替える 環境変数のみ
直前で撤回・保留 確定 環境変数を元に戻す(または何もしない) 環境変数のみ
撤回後に再告知 予告のみ 再び計測だけ。組み替えない なし(メモのみ)
この表の肝は、「予告のみ」の段階では本体コードを触らない という一線を引いていることです。私は、確度が「確定」になるまで本体改修を待つことを強く推奨します。それまで行動は計測とメモにとどめます。今回の撤回は「直前で撤回・保留」の行に当たり、私がやったのは環境変数を戻すこと——実際には最初から subscription のままだったので、文字どおり何もしないことでした。
撤回・延期に強い運用の3原則
何度かこの種の告知を経験して、自分の中で固まってきた原則が3つあります。
原則1:確度と行動を分ける
告知の確度が上がるほど、踏み込んだ行動をとってよい、と考えています。予告に対して本体改修で応じるのは、確度と行動の段差が大きすぎます。まず確度を見極め、それに見合った行動だけを選びます。
原則2:変わりうる軸を1箇所に外出しする
今回は「課金前提」が変わる軸でした。この軸をリゾルバ1つに閉じ込めておけば、変更も撤回も差分が環境変数で済みます。逆に、課金前提の判定が10ファイルに散っていたら、撤回のたびに10ファイルを直す羽目になります。
原則3:先回りの複雑性に値段をつける
先回りして書いたコードは、撤回されると負債になります。私は「この改修は、撤回されたら捨てられるか」を必ず自問します。捨てられない(=据え置いても無害な間接化で済む)なら入れてよい。捨てるしかない(=発効前提の分岐を本体に埋め込む)なら、確定するまで待つことをお勧めします。
この3つは、課金変更に限らず、API の仕様変更やモデルの引退告知にもそのまま使えます。実際、同じ週に旧世代モデルの引退も重なりましたが、モデル名を1つの定数に外出ししてあったので、こちらも差し替えは1箇所で済みました。
次の一歩
もし自動運用のパイプラインを持っているなら、まず「外から変わりうる前提」を1つだけ紙に書き出してみてください。課金、モデル名、レート上限、エンドポイント——どれでも構いません。その前提が、いまコードの何箇所で判定されているかを grep で数えてみる。2箇所以上に散っていたら、それを1つのリゾルバに寄せるところから始めると、次に告知が来たとき、慌てずに環境変数1行で受け流せるようになります。
私自身、可逆性を意識した設計はまだ手探りの部分が多いのですが、今回の撤回で「触らずに済んだ」という静かな手応えがありました。同じように告知に振り回されている方の、何かの足しになれば嬉しいです。お読みいただきありがとうございました。