From d5e2c3819da815aca94f7370d61e88f1cf8190bd Mon Sep 17 00:00:00 2001 From: Hare Date: Mon, 13 Apr 2026 07:13:49 +0900 Subject: [PATCH] =?UTF-8?q?usage=E6=B0=B8=E7=B6=9A=E5=8C=96=E3=81=AEdoc?= =?UTF-8?q?=E4=BF=AE=E6=AD=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- TODO.md | 1 - crates/llm-worker/src/llm_client/event.rs | 20 ++- tickets/usage-history.md | 202 ---------------------- tickets/usage-history.review.md | 50 ------ 4 files changed, 15 insertions(+), 258 deletions(-) delete mode 100644 tickets/usage-history.md delete mode 100644 tickets/usage-history.review.md diff --git a/TODO.md b/TODO.md index c74d7c17..39fb7171 100644 --- a/TODO.md +++ b/TODO.md @@ -7,4 +7,3 @@ - [ ] Compact の改善(要約品質 + 挙動詳細) → [tickets/compact-improvements.md](tickets/compact-improvements.md) - [ ] Protocol の設計 → [tickets/protocol-design.md](tickets/protocol-design.md) - [ ] パーミッション: パターンベースのツール実行制御 → [tickets/permission-extension-point.md](tickets/permission-extension-point.md) -- [ ] UI用トークン情報の記録(run stats の永続化、session-store 後) diff --git a/crates/llm-worker/src/llm_client/event.rs b/crates/llm-worker/src/llm_client/event.rs index 66894f44..a9be9d45 100644 --- a/crates/llm-worker/src/llm_client/event.rs +++ b/crates/llm-worker/src/llm_client/event.rs @@ -54,17 +54,27 @@ pub struct PingEvent { } /// 使用量イベント +/// +/// プロバイダから受信した 1 LLM リクエスト分のトークン会計。 +/// 各 scheme で正規化され、フィールドの意味は全プロバイダ共通: +/// +/// - `input_tokens` は **送信した prompt prefix 全体の占有量**(プロンプト全長)。 +/// キャッシュヒット分も含まれる。Anthropic は raw API では非キャッシュ分のみを +/// `input_tokens` として返すため、`AnthropicScheme::convert_usage` で +/// `cache_read + cache_creation` を加算してこの規約に揃えている。 +/// - `cache_read_input_tokens` / `cache_creation_input_tokens` は上記の内訳で、 +/// 料金会計用。占有量からは差し引かない。 #[derive(Debug, Clone, PartialEq, Default, Serialize, Deserialize)] pub struct UsageEvent { - /// 入力トークン数 + /// 送信した prompt prefix の総トークン数(占有量、キャッシュ込み) pub input_tokens: Option, - /// 出力トークン数 + /// このリクエストで生成された出力トークン数 pub output_tokens: Option, - /// 合計トークン数 + /// `input_tokens + output_tokens` pub total_tokens: Option, - /// キャッシュ読み込みトークン数 + /// `input_tokens` のうちキャッシュから読まれた分(割引料金) pub cache_read_input_tokens: Option, - /// キャッシュ作成トークン数 + /// `input_tokens` のうちこのリクエストでキャッシュに書かれた分(割増料金、Anthropic) pub cache_creation_input_tokens: Option, } diff --git a/tickets/usage-history.md b/tickets/usage-history.md deleted file mode 100644 index fc24a5bf..00000000 --- a/tickets/usage-history.md +++ /dev/null @@ -1,202 +0,0 @@ -# Usage 履歴の永続化 - -## 背景 - -LLM レスポンスから得られる Usage は **プロバイダがリクエスト送信時の history prefix -に対して実測したトークン数**であり、手元にある最も正確なトークン情報。 - -現状は `CompactState::last_input_tokens` に最新の input_tokens だけを `AtomicU64` -で上書き保存しており、過去履歴も prefix との対応関係も失われている。 - -これを session-store の append-only ログに積めば、ハッシュチェーンに乗った -tamper-evident な実測値の時系列が得られ、履歴上の任意位置の占有量を逆算できる。 - -## 動機 - -- ターンより細かい粒度で「履歴のここまでが N トークン」と言いたい - (長く自走するエージェントは1ターンが長いため、ターン境界では粗すぎる) -- セッションを restore した直後でも、過去の実測値が手元にある状態にしたい -- Compact / Prune の判断、UI のトークン表示、コスト集計、デバッグ用の - 時系列分析、すべての基盤になる - -## データモデル - -求めているのは **「history のある位置における占有量スナップショット」** であって -LLM 呼び出しの input/output 統計ではない。ただし占有量を実測するための測定値が -そのまま 1 リクエスト = 1 entry の形に乗るので、測定値を素直に保存する。 - -### `LogEntry::LlmUsage` 追加 - -```rust -// crates/session-store/src/session_log.rs - -LlmUsage { - ts: u64, - /// 送信時の history.len()。以下の測定値はこの prefix に対するもの - history_len: usize, - /// history[..history_len] をプロバイダが実測した占有量(プロンプト全長)。 - /// このリクエストで新たに追加したトークン数ではなく、折り返しを想定した prefix 全体。 - /// 各プロバイダの正規化: - /// - Anthropic: input_tokens + cache_read + cache_creation - /// - OpenAI: prompt_tokens - /// - Gemini: promptTokenCount - /// - Ollama: prompt_eval_count - input_total_tokens: u64, - /// 上記のうちキャッシュから読み出された分。料金会計用 - /// - Anthropic: cache_read_input_tokens - /// - OpenAI: prompt_tokens_details.cached_tokens - /// - Gemini: cachedContentTokenCount - /// - Ollama: 0 - cache_read_tokens: u64, - /// 上記のうちこのリクエストでキャッシュに書かれた分。Anthropic のみ非ゼロ - cache_write_tokens: u64, - /// このリクエストで生成された出力トークン数 - output_tokens: u64, -} -``` - -- ハッシュチェーンに乗る (`HashedEntry` の通常 variant として) -- `collect_state` の replay で `RestoredState.usage_history` に積まれる -- 1 リクエスト = 1 entry。Anthropic は message_start と message_delta の2回 Usage - を出すが、llm-worker 側で集約して **完了時の最終値だけ** を pod に渡す - -### `RestoredState` 拡張 - -```rust -pub struct RestoredState { - // ... 既存 - pub usage_history: Vec, -} - -#[derive(Debug, Clone)] -pub struct UsageRecord { - pub history_len: usize, - pub input_total_tokens: u64, - pub cache_read_tokens: u64, - pub cache_write_tokens: u64, - pub output_tokens: u64, -} -``` - -`collect_state` の replay で `LlmUsage` entry を見たら `usage_history` に push。 -他の variant のように `history` を変化させることはない(独立した時系列)。 - -### `save_usage` 関数 - -```rust -// crates/session-store/src/session.rs - -pub async fn save_usage( - store: &impl Store, - session_id: SessionId, - head_hash: &mut Option, - history_len: usize, - input_total_tokens: u64, - cache_read_tokens: u64, - cache_write_tokens: u64, - output_tokens: u64, -) -> Result<(), StoreError>; -``` - -## タイミングと結線 - -**append のタイミングはリクエスト完了時に 1 回**。送信時には storage に触らない。 - -### llm-worker 側 - -- 各プロバイダの scheme で 1 リクエスト内の複数 Usage event(Anthropic の - message_start + message_delta)を集約し、**完了時の最終値だけを 1 つの - `UsageEvent` として外に発火する**。pod 側では暫定値を見ない -- 占有量への正規化(Anthropic: `input_tokens + cache_read + cache_creation`)は - 各 scheme の `convert_usage` で行い、`UsageEvent.input_tokens` には正規化済みの - 占有量(プロンプト全長)が入る。consumer 側(pod / UsageTracker)は - `event.input_tokens` をそのまま使う - - 動機: 正規化ロジックを scheme に閉じ込めることで、consumer が provider 差異を - 意識する必要がなくなる -- `cache_read_input_tokens` / `cache_creation_input_tokens` は内訳として - 別フィールドに保持。料金会計用 - -### pod 側 - -- LLM リクエスト送信**直前**に `history.len()` を捕捉して stash - (`Arc>>` などで on_usage callback と共有) -- `on_usage` callback で stash された `history_len` と Usage の最終値を組にして - `save_usage` を呼ぶ -- 既存の `CompactState::update_input_tokens` 経路はそのまま残してよい - (閾値判定は最新値だけで足りる)。`save_usage` はそれと並列に呼ぶ - -結線箇所はおそらく `crates/pod/` 内の既存 `compact_state.rs` の `on_usage` -callback と同じ場所。 - -## 任意位置のトークン数を割り出せること - -このデータがあれば、後段(token-counter)が `history[..M]` の占有量を逆算できる: - -各 `UsageRecord` から得られるデータ点: -1. **送信時点**: `(history_len, input_total_tokens)` — 完全な実測 -2. **応答完了時点**: `(history_len + k, input_total_tokens + output_tokens)` — - k は応答で追加された item 数。次の `LlmUsage` の history_len と log 上の - AssistantItems entry から復元できる - -`history[..M]` のトークン数を求める手順: -- 上記のデータ点を時系列に並べる -- M がデータ点と一致 → 実測値そのまま -- 2つのデータ点の間 → 区間内 items のバイト数で按分 -- 最後のデータ点より新しい → 最後の rate で外挿 - -途中で user input や hook injected items が入った分は、次の LlmUsage 時点で -再測定されてキャリブレートされるので誤差は永久に蓄積しない。 - -詳細アルゴリズムと API は [token-counter.md](token-counter.md) で扱う。 - -## 設計ポイント - -- **最細粒度はリクエスト単位**: provider API がそれ以上細かく返さない。 - ターンより遥かに細かいのでこれで十分 -- **append-only + ハッシュチェーン**: 改ざん検知や fork 検出に乗る -- **collect_state の replay コストはほぼゼロ**: usage_history に push するだけで - history の構築には影響しない -- **既存ログとの互換性**: 古いログには `LlmUsage` entry が存在しないだけ。 - `RestoredState.usage_history` が空になる以外の挙動変化は無い -- **占有量とコストの両立**: input_total と cache 内訳を別フィールドに持つので、 - Compact/Prune 用の占有量も将来のコスト集計も同じ entry から取れる - -## 実装対象 - -- `crates/llm-worker/` - - 各 provider scheme で 1 リクエスト内の複数 Usage event を集約し、 - 完了時に 1 度だけ最終値を発火する仕組み - - Anthropic の `cache_read_input_tokens` / `cache_creation_input_tokens` を - `UsageEvent` 経由で正しく外に出せていることの確認(既に出している) -- `crates/session-store/src/session_log.rs` - - `LogEntry::LlmUsage` variant 追加 - - `UsageRecord` 型追加 - - `RestoredState.usage_history: Vec` 追加 - - `collect_state` で `LlmUsage` を replay -- `crates/session-store/src/session.rs` - - `save_usage` 関数追加 - - `lib.rs` から re-export -- `crates/pod/` - - LLM リクエスト送信直前に `history.len()` を stash する仕組み - (`Arc>>` を on_usage callback と共有) - - `on_usage` callback から `save_usage` 呼び出し - - provider 別の占有量正規化(Anthropic は cache_read + cache_creation を - 足す)をどこに置くか実装時判断 -- テスト - - `LlmUsage` を含むログの round-trip - - 複数 entry を replay して `usage_history` が時系列順に積まれる - - 既存ログ(`LlmUsage` 無し)を読んでも壊れない - - Anthropic の cache hit ありレスポンスで input_total が正しく計算される - -## レビュー状態 - -Reviewed — [usage-history.review.md](usage-history.review.md) - -## 依存 - -- なし(前提チケット) - -## ブロックする後続 - -- [token-counter.md](token-counter.md) — この履歴を消費して計算する側 -- [compact-improvements.md](compact-improvements.md) — 上記経由で依存 diff --git a/tickets/usage-history.review.md b/tickets/usage-history.review.md deleted file mode 100644 index 9cf4d1a0..00000000 --- a/tickets/usage-history.review.md +++ /dev/null @@ -1,50 +0,0 @@ -# usage-history レビュー - -## 要件の充足 - -チケットが定義した要件は全て達成されている: - -- **LogEntry::LlmUsage**: session-store のハッシュチェーンに乗る variant として追加。 - `history_len` / `input_total_tokens` / `cache_read_tokens` / `cache_write_tokens` / `output_tokens` の5フィールド -- **RestoredState.usage_history**: `collect_state` の replay で `Vec` に時系列順で積まれる。history の構築には影響しない -- **save_usage**: `append_entry` 経由でハッシュチェーンに接続 -- **既存ログ互換**: `LlmUsage` entry が無い既存ログを読んでも `usage_history` が空になるだけで壊れない -- **1リクエスト = 1 entry**: Timeline の `pending_usage` + `flush_usage()` で複数 Usage event を集約し、handler には1度だけ発火 - -## アーキテクチャ - -レイヤー分担が明確で、各層の責務が逸脱していない: - -| レイヤー | 責務 | -|---------|------| -| scheme (anthropic) | raw → 占有量への正規化。`input_tokens + cache_read + cache_creation` | -| Timeline | 1リクエスト内の複数 Usage event をフィールド単位 latest-non-None でマージ。`flush_usage()` で1度だけ発火 | -| Worker | ストリーム完了・エラー・キャンセルの全パスで `flush_usage()` を呼ぶ | -| UsageTracker (pod) | `note_request(history_len)` と `record_usage(event)` のペアリング。drain で Pod に渡す | -| Pod::persist_turn | drain した records を `save_usage` で session-store に書き出し | - -## 指摘と対処 - -### 1. UsageEvent の doc comment(対処済み) - -`UsageEvent.input_tokens` が「占有量(プロンプト全長、キャッシュ込み)」を意味することが -struct と各フィールドの doc comment に明記された。scheme 層での正規化規約も記載済み。 - -### 2. save_usage の引数が多い(非ブロッカー、未対処) - -8引数。`UsageRecord` を直接受け取れば `drain()` の結果をそのまま渡せてシグネチャがきれいになるが、 -他の `save_*` 関数がフラットな引数を取るパターンと一貫しているため、統一性の観点では現状でも妥当。 -将来フィールドが増えた時点でまとめて `UsageRecord` 受け取りに変えればよい。 - -## テスト - -- `replay_llm_usage_appends_to_usage_history`: 複数 LlmUsage entry の replay で usage_history が正しく積まれ、history.len() に影響しない -- `replay_without_llm_usage_keeps_usage_history_empty`: 既存ログ互換 -- `llm_usage_entry_round_trip_via_json`: serde 往復 -- `test_convert_usage_includes_cache_in_input_total`: Anthropic の占有量正規化 -- `test_usage_aggregation_and_flush`: Timeline の集約 + flush -- UsageTracker: ペアリング、drain、未ペアの drop、複数リクエスト - -## 判定 - -承認。