コストの追跡
ZeroClawは、課金対象のすべてのAPI呼び出しを追記専用の台帳に記録し、支出を発生元のエージェントに割り当て、日次・月次の予算を適用し、集計をダッシュボードのCostタブに表示します。価格設定ルールはconfigに保存されているため、オペレーターは再ビルドせずに編集できます。
このページでは、スキーマ、ルックアップパイプライン、およびオペレーターサーフェスについて説明します。コードは crates/zeroclaw-config/src/cost/ および crates/zeroclaw-runtime/src/agent/cost.rs にあります。
Config schema
2つの関連セクションがこの領域を管理します。cost は予算の適用と記録動作を扱います。cost.rates.* はオペレーターが管理する料金表です。各サブセクションのドット区切りパスは、末尾の <alias> セグメントを価格設定対象のアップストリームリソースに置き換えた、対応する providers.* パスに一致します。
キーがエイリアスではなくリソース ID である理由
[providers.models.anthropic.<alias>] のエントリは、エイリアスバリデーターに従ってオペレーターが選択したエイリアス(glados、production)をキーとします。エイリアスは小文字の ASCII、単一のアンダースコア、ハイフンなしです。[cost.rates.providers.models.anthropic.<resource>] のエントリは、使用状況テレメトリに表示される アップストリームモデル ID(claude-opus-4-7、gpt-4o-mini、whisper-1)をキーとします。これらの ID 文字列はプロバイダーの名前空間に由来し、ほとんどの場合ハイフンを含みます。
スキーマは、すべての料金表 HashMap に #[resource_key] を付与します(crates/zeroclaw-macros/src/lib.rs 内)。この属性により、対象フィールドは create_map_key / rename_map_key における validate_alias_key の対象から除外されるため、ゲートウェイの POST /api/config/map-key はハイフンを含む id を受け付けるようになります。これがないと、create_map_key は現実的なすべてのモデル id を拒否し、料金表 UI が機能しなくなります。エイリアスとリソース id はディスク上の構造(HashMap<String, T>)を共有していますが、これらは異なるバリデータを持つ別個の命名システムです。
スロットリストは唯一の信頼できる情報源です
[cost.rates.providers.models.<type>]、[cost.rates.providers.tts.<type>]、[cost.rates.providers.transcription.<type>] 配下のプロバイダータイプごとのスロットは、[providers.*] のスロットラッパーを駆動するのと同じマクロから展開されます:
#![allow(unused)]
fn main() {
// crates/zeroclaw-config/src/providers.rs
for_each_model_provider_slot!(emit_model_cost_rates_struct);
for_each_tts_provider_slot!(emit_tts_cost_rates_struct, super::schema::TtsCostRates);
for_each_transcription_provider_slot!(emit_transcription_cost_rates_struct, super::schema::TranscriptionCostRates);
}
新しいモデルプロバイダータイプの追加は、for_each_model_provider_slot! の1行で済みます。レートシートスロット、プロバイダー設定スロット、ダッシュボードのドロップダウンはすべてそこから展開されます。手書きのディスパッチテーブルも、フロントエンド上の並列的な文字列リストも不要です。
リクエスト時の料金
[cost.rates.*] から記録される cost_usd 値までのパイプラインは次のとおりです:
-
オーケストレーターの起動時に料金マップを構築します。 channels スーパーバイザーがエージェント用のランタイムコンテキストをインスタンス化する際、
config.cost.rates.providers.models.iter_entries()を走査し、料金をHashMap<provider_type, HashMap<key, f64>>にマージします。ここでkeyは"<model_id>.input"、"<model_id>.output"、または"<model_id>.cached_input"です。レガシーなエイリアスごとの[providers.models.<type>.<alias>].pricingテーブルもマージされます。競合した場合は[cost.rates.*]が優先されます。これは将来を見据えたサーフェスだからです。(crates/zeroclaw-channels/src/orchestrator/mod.rsのcost_tracking: CostTracker::get_or_init_global(...).map(|tracker| ...)配下のクロージャを参照してください。) -
エージェントループ内での記録。 すべての成功した LLM レスポンスは、
crates/zeroclaw-runtime/src/agent/cost.rs内のrecord_tool_loop_cost_usage(provider_name, model, usage)に到達します。この関数はprovider_nameに対応する価格マップのスロットを取得し、resolve_rates(map, model)を呼び出し、トークン数を乗算して、グローバルなCostTrackerを介してCostRecordを保存します。 -
resolve_rates_opt はまずモデル ID を試し、次に
provider/model文字列のパス接尾辞形式を試します(そのためオペレーターが短い形式のみを保存している場合、anthropic/claude-opus-4-7はclaude-opus-4-7に縮退します)。ディメンションごとに 1 つのOptionを返すため、オペレーターが未設定のままにしたディメンションは課金前にライブ価格フォールバック(下記参照)から埋めることができます。設定とライブフォールバックの 両方 が入力と出力を0.0のまま残した場合にのみ、ワンショットのmissing_pricing警告が発火するため、真に「この価格を付けられなかった」レコードがログに引き続き表示されます。 -
CostTracker はプロセスグローバルなシングルトンです(
crates/zeroclaw-config/src/cost/tracker.rs内のOnceLock)。リロードは既存のトラッカーに最新のCostConfigを適用し、起動時にコストトラッキングが無効だった場合、後からcost.enabled = trueでリロードするとオンデマンドでトラッカーが構築されます。オーケストレーターの価格マップも、デーモンのリロードのたびにライブ設定から再構築されるため、料金の編集はリロード後の次のリクエストから有効になります。
ゲートウェイからのライブ料金
オペレーターはすべてのモデルの料金を手作業で管理する必要はありません。プロバイダーは、そのプロバイダーブロックで live_pricing = true を設定する(既存の api_key とモデル設定とともに)ことで、独自のゲートウェイからトークン価格を直接取得することを選択できます。価格はゲートウェイ自身の /models リストから取得されます。
動作:
- ゲートウェイが主要な情報源です。 プロバイダーの既存の
/modelsエンドポイント(オンボーディングでモデル一覧の取得に使用するものと同じ)を解析し、モデルごとの料金を取得します。そこに料金を公開しているゲートウェイは、トークンあたりの小数文字列(OpenRouter と Kilo のpricing{prompt,completion,...})として報告し、これらは 100 万トークンあたりの USD にスケールされます。/modelsの一覧に料金がまったく含まれないゲートウェイ(モデル id のみを一覧する opencode zen など)は、以下の models.dev フォールバックでカバーされます。エンドポイント URL や認証情報の 2 つ目のコピーは不要です。これらはプロバイダーの既存の設定から読み取られます。 - models.dev フォールバック。 ゲートウェイが価格を設定していないモデル(または
kilocliのようなサブプロセスゲートウェイなど、HTTP/models一覧がまったくないプロバイダー)は、公開の models.dev カタログ(api.json)にフォールバックし、ファミリーの models.dev 名でキー付けされます(crates/zeroclaw-providers/src/catalog.rsのcatalog_source_forを参照)。フォールバックカタログは各リフレッシュサイクルで新たに取得されるため、両方のソースが同じ1時間ごとのペースで上流の価格変更を追跡します。 - 設定が常に優先されます。 ライブ価格は、モデルに
[cost.rates]/pricingエントリがないディメンション_のみ_を埋めます。設定されたレート(意図的な0.0を含む)は決して上書きされません。これはギャップ埋めであり、置き換えではありません。 - ゲートウェイごとに1回の呼び出し、フラグ付きモデルのみ。 ゲートウェイを共有するエイリアスは単一の
/modelsフェッチに重複排除されます。そのレスポンスからはオプトインされた各エイリアス自身の設定済みmodelのみが埋められ、ゲートウェイがリストするすべてのモデルは埋められません。 - バックグラウンド更新、ブロックしない。 単一のタスクがプロセス全体の価格スナップショットを1時間ごとに更新します。コスト記録パスはキャッシュされたスナップショットを同期的に読み取り、インラインでネットワーク呼び出しを行うことはないため、遅いゲートウェイがリクエストの会計処理を停滞させることはありません。
- デフォルトはオフ。
live_pricing = trueを設定しているプロバイダーがない場合、リフレッシャータスクは存在せず、ネットワークトラフィックも発生しません。動作は機能を含まないビルドと同一です。実行時(設定リロード)に最後のフラグ付きプロバイダーをオフにすると、次のリフレッシュサイクルでスナップショットがクリアされ、再起動なしでライブ価格の取得が停止します。スナップショットはzeroclaw_providers::pricing内にのみ存在します(crates/zeroclaw-providers/src/pricing.rsを参照)。record_tool_loop_cost_usageによって読み取られ、channels スーパーバイザーとゲートウェイ起動時から一度だけスポーンされます。
[cost.rates] と同様に、ライブ価格はスナップショットが投入された後に行われたリクエストにのみ影響します。過去のレコードに対する遡及的な再価格設定はありません。
永続化
CostTracker::record_usage_with_agent は、料金が発生する応答ごとに 1 件の CostRecord を <workspace>/state/costs.jsonl に追加し、1 行につき 1 つの JSON オブジェクトを記録します。台帳は起動時に読み込まれるため、ダッシュボードの当月のエージェント別集計は再起動後も保持されます。
cost_usd は、その時点で有効なレートシートから記録時に計算されます。レコードは不変です。一部のリクエストがすでに記録された後にオペレーターがレートを追加した場合、それらの既存レコードは cost_usd = 0 のまま保持されます。コストが0以外になるのは、レートが設定された後(かつデーモンが再読み込みされてオーケストレーターの料金マップが再構築された後)に行われたリクエストのみです。
これはレートシートを初めて有効化した後、最もよくある想定外の事象です。修正方法は新しいリクエストを待つことです。遡及的な再価格設定は行われません。
予算管理
CostConfig::enforcement.mode は、予測されるコストによって daily_total または monthly_total が設定された上限を超える場合の動作を決定します:
warn: デフォルト。warn レベルのログでイベントを記録し、リクエストを通過させます。block:BudgetExceededエラーでリクエストを拒否します。route_down: 元のモデルの代わりにroute_down_model(より安価な代替モデル)を使用します。この置き換えはリクエストがディスパッチされる前に行われます。
allow_override = true を指定すると、CLI(zeroclaw --override)でオーバーライドトークンを渡すことで、リクエストが block をバイパスできるようになります。デフォルトは false です。warn_at_percent は、ハードリミットの前にゲートウェイが警告バナーを表示するタイミングを制御します。デフォルトは 80% です。
エージェントごとの属性
cost.track_per_agent が true(デフォルト)の場合、記録されるすべての CostRecord には発生元のエージェントエイリアスが付与されます。ダッシュボードの Spend by agent パネルと GET /api/cost?agent=<alias> はこのフィールドを利用します。track_per_agent = false に設定することは、追加の HashMap 集約がプロファイルに現れるような大量処理のインストール環境向けの最適化です。トレードオフとして、どこでもエージェント単位のディメンションが失われます。
オペレーターサーフェス
設定UI
/config/cost→ Limits タブ: フラットな[cost].*フィールド (enabled, limits, enforcement, track_per_agent) のすべて。レートシートの行はここでは編集しません。これらはモデルを所有するプロバイダーに紐付けられているため、1 つ下の階層に配置されています。/config/providers.<category>/<type>→ Costs タブ:そのプロバイダータイプのレートシートエディター。+ Add入力は、設定済みのエイリアス全体にわたるproviders.<category>.<type>.*.modelから取得したアップストリームリソース ID を候補として表示するため、オペレーターは実際にバインドしたすべてのモデルに対してワンクリックでレート行を追加できます。これは[cost.rates.providers.<category>.<type>.*]を編集する唯一のエントリーポイントです。
ダッシュボード
ダッシュボードの Cost タブには、3つのパネルと Window ピッカー(today / last 7 days / last 30 days / this month / all time)が表示されます:
- 支出合計:
costs.jsonlからの日次および月次の合計。 - エージェント別の使用量 ·
<window>: 選択したウィンドウにおけるエージェントごとの集計。track_per_agentが true の場合に表示されます。 - モデル別のコスト ·
<window>: モデルごとの集計です。各行のモデル ID はクリック可能で、クリックすると設定済みエイリアスから所有プロバイダーの種別を解決し、そのプロバイダーの Costs タブに移動します。モデル ID がいずれの設定済みプロバイダーにもバインドされていない場合、クリックは何も実行しません(孤立したモデルには対象となる料金表のルートが存在しないためです)。
ゲートウェイ
GET /api/cost: 現在のCostSummary(ダッシュボードのコスト概要の形式に一致)。単一エージェントの表示には?agent=<alias>を追加します。GET /api/config/templates: スキーマが登録するマップキー付きセクションすべてを返します。Rates タブのカテゴリ × プロバイダータイプのドロップダウンで使用されます。POST /api/config/map-key?path=cost.rates.providers.<category>.<type>&key=<resource>は新しいレートの行を作成します。該当するマップセクションが存在しない場合、パスは拒否されます。リソースキーはvalidate_alias_keyではなく#[resource_key]を通過します。
トラブルシューティング
レートを設定した後もダッシュボードがすべてのエージェントで $0.0000 と表示される。 古いレコードは変更できません。これらは記録された時点でレートが設定されていなかったため、cost_usd = 0 として記録されています。デーモンのリロード後に新しいチャットリクエストを行い、Cost overview > Session および Spend by model を確認してください。どちらも新しいリクエストで値が入力されるはずです。
保存後に cost.rates.* パスに対するドリフトが検出されました。 v0.8.0 より前のデーモンには、ダーティ保存パスでハイフン区切りの HashMap キーを破損させ、レートシートへの書き込みをすべて暗黙的に破棄してしまう不具合がありました。v0.8.0 以降でこの現象が発生した場合は、本物のバグです。ダーティパスの解決処理は crates/zeroclaw-config/src/schema.rs::apply_dirty_path にあります。デーモンのバージョンとドリフトが発生したパスを添えて issue を報告してください。
missing_pricing の警告がログを埋め尽くす。 resolve_rates が (0.0, 0.0) を返したときに、(provider_type, model) のペアごとに一度だけ出力されます。そのモデルにレートが設定されていないか、上流がレートシートに記載されているものとは異なるモデル ID を返している(一部のプロバイダーは、claude-3-5-sonnet を設定していても claude-3-5-sonnet-20241022 のようなバージョン付き ID を返します)かのいずれかです。警告に表示されている正確な ID を追加するか、バージョンなしの ID を設定して resolve_rates のサフィックスマッチ経路に頼ってください。