# Spec: モデルごとの単価と、おおよその金額表示 **ID**: 41 **Date**: 2026-08-18 **Status**: **Done(2026-08-18。起票から Done まで 1 日)** P0〜P5 着地。**検収 9 件のうち 8 件を観測し、1 件(検収 4)は挙動が正しく文面が 誤っていたのでその場で直した**。残る 1 件(検収 1)は**単価を登録した後では 原理的に踏めない**ので未観測のまま閉じた(下) **Branch**: P0(契約 + D5 の覆し)は rev 承認後に main 直。P1 以降は `YYYYMMDD_pricing` へ積む ## Goal **この村がいくら払ったかを、おおよその金額で出す。** 起点は利用者(2026-08-18)—「**おおよその合計コスト(金額)みたいなのは出したいね**」。 単価はモデル登録ダイアログで持ち、**「取得」ボタンで外部の静的 JSON から埋め、 無ければ手で入れる**(同日裁定)。 **これは [Spec 39](39_stats-view.md) D5「通貨に換算しない」を覆す。** 覆す根拠は 下の節に書く — **覆したことが分かる形で覆す**のがこの村の作法。 **これは拡張か収縮か**: 拡張(`ModelTemplate` に 5 欄・設定が 1 つ・画面要素が 2 つ・ **外向きの通信が 1 本**)。収縮側の選択肢は D0。 ## D5 を覆す(Spec 39・2026-08-15 の凍結) 原文の理由は 3 つ。**3 日で 2 つが消えた**: | D5 の理由 | 2026-08-18 の状態 | |---|---| | 各社の改定に追従できない | **消えた** — 公開の機械可読な価格表が実在し(下の Notes 1)、利用者が「**固定でいい**」と裁定した。**追従しないことが選択になった** | | 同じモデルでもキャッシュの帯で単価が変わる(#104) | **実質消えた** — 掛ける相手は**実際に返ったトークンの内訳**(Spec 40 で分解した)。キャッシュが効かなかった帯では `cached` が小さい実測がそのまま出るので、**単価表が帯を知らなくても実測が帯の結果を含んでいる** | | **通貨を出すと「合っている」と読まれる** | **生きている。唯一残る理由** | **3 番目は「出さない」では解けない** — 出さなくても利用者は自分で掛けるので、 精度は上がらず手間だけが残る。**利用者の「おおよその」がそのまま処方**で、 **近似だと画面で分かる形にする**のが本 Spec の設計の芯(D4)。 ## 現況(実測 2026-08-18) - **`ModelTemplate` は単価の欄を 1 つも持たない**(`id` / `name` / `base_url` / `model` / `context_length` / `temperature` / `max_output_tokens` / `credential` ほか)。 **`Option` 欄には `#[serde(default)]` が付いている**のが既存の作法 - **`Record::Turn` は掛ける材料を全部持っている**(Spec 40 P3 で着地)— `model` / `backend` / `prompt` / `cached` / `cacheWrite` / `cacheWrite1h` / `completion` / `reasoning` - **非 LLM の外向き通信は今ゼロ**(grep で出た `github.com` 等は全部テストとコメント)。 **本 Spec がその 1 本目になる** - **`PRIVACY.md` の 2 文が、この機能で嘘になる**: 「開発者が運営する送信先はありません」/「外部へ通信するのは、**利用者が自分で 設定した接続先に対してだけ**です」。**既定値を持つ以上、箱から出した状態では 利用者が選んだ接続先ではない** ## 決めること ### D0. そもそも作るか(収縮側) **作らない案**: 統計画面のエクスポート(JSONL)だけ出し、金額は外の表計算で掛ける。 **却下を推す** — 利用者の要求が画面に出すことで、外で掛ける道は**今も開いている** (`Record::Turn` が全部持っている)。**それでも要求が出た**ということは、 外で掛ける手間が実際に払われていないということ。 ### D1. 欄は 4 + 1(**裁定済み**) ```text input_per_mtok : Option 入力 output_per_mtok : Option 出力 cache_read_per_mtok : Option キャッシュ読み cache_write_per_mtok : Option キャッシュ書き込み cache_write_1h_per_mtok : Option うち 1h TTL(Anthropic のみ。任意) pricing_as_of : Option **この単価がいつ時点のものか**(rev2 で追加) ``` **`pricing_as_of` は 1 欄。査読は 2 欄(`fetched_at` と `source_date`)を提案したが、 1 つに畳んだ** — 意味を「**この数字が正しいと信じられる時点**」に固定すれば、 取得なら JSON の日付、手入力ならその日、で**常に埋まり常に表示できる**。 2 欄にすると「画面にどちらを出すか」が新しい未決になり、**差が意味を持つのは 静的ファイルが現実に対して古いときだけ**で、それは**こちらから観測できない**。 **この欄が無いと凍結が破れる**(査読の穴 A。**この因果が本 Spec で最も重要な指摘**)— D4 が日付を出すのに保存先が無いと、実装者は「古さはリモートの `Last-Modified` を 見れば分かる」と考え、**起動時に HEAD を飛ばす**という自動取得を発明する。 - **3 欄(入力 / 出力 / キャッシュ)では足りない。** Spec 40 P2 の実測で、 **「未キャッシュ」の実体はほぼ全部が書き込み**(Anthropic 100.0% / OpenAI 互換 99.8%)。 3 欄で計算すると**同じログから 1.52 倍ずれる**(実測 $0.2852 対 $0.4332) - **すべて `Option` + `#[serde(default)]`。** 既存の村の `world.json` は 1 つも 持たないので、**無ければ 0 ではなく「未設定」**として読む - **未設定は「無視」ではなく「1 段上へ落とす」**(Spec 40 付録・出自は otari): `cache_write_1h` 無し → `cache_write` へ / `cache_write` 無し → `input` へ / `cache_read` 無し → `input` へ。**落ちたトークンは素の入力に残るので、 表が不完全でも合計トークンは失われない** - **`input` と `output` が両方未設定なら、そのモデルは「単価未登録」**として 合計から外す(D4 の被覆率に出る) ### D2. 取得は「設定可能な URL」から、**押したときだけ**(**裁定済み** = 案 B) - **URL は `{app_data_dir}` の棚**(Spec 25 の `mcp_server.json` と同じ第 3 の棚)。 **`world.json` に置かない** — 村を配ったとき、**受け取った人の村が、その人が 知らない URL へ取りに行ける状態を作らない**(Spec 25 の「村を配っても扉は 開かない」と同じ線引き) - **既定値を持つが、空にできる。** 入口はシステム設定 > 外部連携(Spec 25 で 作った既存グループ)の 2 件目 - **自動取得を作らない(凍結)。** 起動時にも保存時にも取りに行かない。 これで `PRIVACY.md` の「起動時に外部へ問い合わせません」が**生き残る** - **`https` のみ。** 平文で取る理由が無い。タイムアウトは短く固定 - **凍結事項(`pricing.rs` に持たせてはならないもの)**(rev2。査読の穴 B): ```text - interval / tokio::spawn / background task - 起動時・画面遷移時・フォーカス時の自動 GET - URL の到達確認のための自動 GET(疎通確認もボタン押下時のみ) - リモートと比較して「古いか」を判定すること → 古さは pricing_as_of からの【ローカルの経過日数】だけで表示する ``` **最後の 1 行が穴 A の塞ぎ**。「古い」を**手元の引き算だけ**で定義すれば、 比較のために外へ出る理由が消える - **照合の鍵は rev1 では `model` 単独**。`backend` を足す余地は残すが**今は作らない** (空の欄を先に作らない — Notes 4 と段階を揃えた。査読のその他 4) - **受け取るのは数値だけ。** 照合の鍵は**こちらが持つ `model`** で、 **向こうが名前を名乗る余地を作らない**(`defuse` と同じ発想)。 負・非有限・桁外れは拒否して**その 1 件だけ落とす**(全体を拒否しない) ### D3. 取得は「欄に入れるだけ」 押した結果は**ダイアログの入力欄に入るだけ**で、保存は既存の保存ボタン。 **取得=保存にしない** — 「取得したら黙って上書きされた」が構造的に起きない形。 **取得できなかった欄は触らない**(手で入れた値を空で潰さない)。 ### D4. 画面(**近似だと分かる形にするのが本 Spec の芯**) 統計画面に**実効トークンとは別の行**で: **2 行に分ける**(rev2。査読 2 — rev1 の 1 行案は情報が 4 つで、うち 2 つが 同じことを言っていた): ```text ≈ $0.43 (2026-08-18 時点の単価) 7/9 体・トークン 94% ``` 1 行目は**金額と日付だけ**で `≈` の射程を閉じる。2 行目は**灰色の小字**の補助情報。 - **`9 体中 7 体` と `単価未登録 2 体` は同じ情報**だったので片方へ畳んだ - **被覆率の分母から「実効」を外した**(rev1 の自己矛盾。**「実効と同じ行に 並べない」と書いた本文の直後に、例で `実効の 94%` と書いていた**)。定義: ```text 被覆率(トークン) = 単価登録済みモデルの (prompt + completion) 合計 ÷ 全ターンの (prompt + completion) 合計 ``` **素のトークンで数える。`budget::effective_tokens` を呼ばない** — 呼ぶと D5 の「混ぜない」を被覆率の側から破ることになる。`coverage()` は `cost()` と 別の純関数 - **2 軸を残すのは、片方では嘘になるから** — 9 体中 7 体でも、その 7 体が トークンの 20% しか使っていないことがある。**体数だけでは「ほぼ揃っている」に 見える** - **A 案(2 行)で作る**(2026-08-18 利用者裁定)。**前提は「統計画面は最大化して 見る」**で、**レスポンシブは後回し**(利用者 —「使っていて考える」)。 **この前提を書いておくのは、後で狭い幅で崩れたときに「退行」と読まれないため** — 崩れは想定内で、そのとき B 案(1 行 + `[ⓘ]`)へ倒す判断が生きている - **被覆率を必ず出す。** 単価の無いモデルは合計から落ちるので、**落ちたことを 画面に書く**。**これが無いと、部分的な合計が全体の合計に見える**(Spec 39 D6 の 「遡れないことを画面で言う」と同型) - **実効トークンと同じ行に並べない** — 並べると「実効トークン = お金」と読まれる。 **実効は歯止めの単位、金額は推定**(Spec 40 D3 で分けたばかりの区別) - **USD のみ。円に換算しない** — 為替は**2 つ目の動く数**で、金額が動いた理由が 村か単価か為替か切り分けられなくなる - **単価の日付を出す。** 固定した以上、いつ時点の単価かが読みの前提 - **`≈` を必ず前に置く。** 記号 1 つで「推定」を運ぶ ### D5. 計算は `budget.rs` ではない純関数(**実効と混ぜない**) ```text cost(turn, price) = (prompt - cached - cache_write) × input + cached × cache_read (無ければ input) + (cache_write - cache_write_1h) × cache_write (無ければ input) + cache_write_1h × cache_write_1h (無ければ cache_write → input) + completion × output ``` - **単価を先に解決し、そのあとトークンを掛ける**(rev2。査読のその他 2 — 引き算とフォールバックの順序で結果が変わる): ```text 1. 各レートを解決する(無ければ 1 段上へ落とす) r_read = cache_read ?? input r_write = cache_write ?? input r_1h = cache_write_1h ?? cache_write ?? input 2. トークンの束を作る(【引き算はここで、クランプつき】) fresh = max(0, prompt - cached - cache_write) w_base = max(0, cache_write - cache_write_1h) 3. 掛ける ``` **クランプが要る**のは、記録が不整合なら負になりうるから(`cache_write_1h` は `cache_write` の部分集合という不変条件を**decode で強制していない** — Spec 40 は それを検定として残す判断をした)。**負の金額を出すより 0 へ倒す** - **手入力は数値のみ。`¥` や `$` を書かせない**(査読のその他 3)。通貨記号は 表示側が持ち、欄は `f64`。非数値・負・非有限は保存時に拒否 - **`reasoning` は掛けない** — `completion` の内数で、出力単価に既に乗っている - **`budget::effective_tokens` を呼ばない。** あちらは歯止めの重み(書き込みも 1.0×)で、こちらは請求の推定。**同じ関数にすると、片方を直すともう片方が動く** - **段階課金(200k 超で単価が上がる)は入れない**(rev1)。欄が 4+1 で閉じており、 段を入れるには表の UI が要る。**長いターンでは過小に出る**ことを Notes へ書き、 `≈` の射程に含める ### D6. 単価は村と一緒に配られる `ModelTemplate` は `world.json` に住むので、**単価も村と一緒に配られる**。 **これでよい** — 公開情報であり、テンプレートの属性だから。 **配られないのは取得 URL だけ**(D2)。 ## Stories - **S1**: 利用者がモデルを登録するとき、**「取得」を 1 回押すと 5 欄 + 日付が埋まる** - **S2**: 表に無いモデル(さくら / ローカル互換)でも、**手で入れれば金額に乗る** - **S3**: 統計画面で**おおよその合計 $ が読め、何体ぶんが欠けているかも同時に読める** - **S4**: URL を空にした村は**外部へ 1 度も通信しない**(手入力だけで完結する) ## Tasks | Phase | 内容 | ゲート | |---|---|---| | **P0** | **完了**(2026-08-18)。`data_contract` の `ModelTemplate` へ 5 欄 + `pricingAsOf`、`pricing` と `pricing_fetch_freeze` の 2 ブロック。D5 の覆しは Spec 39 側の見出しへ記録済み | rev 承認 | | **P1** | **完了**(2026-08-18)。`ModelTemplate` へ 6 欄 + `pricing.rs`(`resolve` / `cost_of` / `summarize`・単体 8 本)。**`budget.rs` は 1 行も触っていない** | 全緑(`fuseforks-core` 812)・clippy 警告ゼロ | | **P2** | **完了**(2026-08-18)。`pricing_source.rs`(棚 + `validate_url` + `fetch_table`)+ `parse_table`(コア・純)+ IPC 3 本 + **`PRIVACY.md` 日英を同じコミットで**。単体 7 本 | 全緑・clippy 警告ゼロ・**凍結の網をミューテーションで赤に** | | **P3** | **完了**(2026-08-18)。登録ダイアログの 5 欄 + 日付 + 「取得」/ システム設定 > 外部連携 > 単価表 / 統計の `≈ $` 2 行 / 辞書 ja・en | vitest 394・build 緑・clippy 警告ゼロ | | **P4** | 台帳: README 日英 / DETAIL 日英 / CLAUDE.md / Spec 39 D5 の追記 | grep で追従漏れゼロ | | **P5** | 実機 | 検収 | **`PRIVACY.md` へ足す文**(**P2 の、取得を足すのと同じコミット**。rev2 で P4 から 移した — 査読の穴 C「P0〜P4 の間 main が嘘のまま」は正しい。**ただし P0 ではなく P2 にした** — P0 は契約の文だけで通信は生まれないので、P0 で書くと**存在しない 送信を宣言する**ことになる。**嘘の窓を閉じるには「能力が生まれるコミット」に 揃えるのが正確**で、P0 へ倒すと向きが逆の不正確さが残る): > 単価表の取得(**利用者がボタンを押したときだけ**)。既定の取得先は開発者が用意した > GitHub 上の静的ファイルで、**接続先は画面から変更・空にできます**。 > 送るのは HTTP GET のみで、**村の情報は 1 つも含みません**。 ## 検収(**その画面がその値を引いているかを、書く前に数えた** — #68) 1. 単価を 1 つも登録していない村で、統計に **`≈ $` の行が出ない**(0 と書かない) 2. 1 体だけ登録して、**被覆率が「N 体中 1 体」と出る**。**部分合計が全体に見えない** 3. 「取得」を押すと欄が埋まり、**保存を押すまで `world.json` が変わらない** 4. URL を空にして押すと、**通信せずに「取得先が未設定」と出る** → **P5 で半分外れた**(2026-08-18 実機)。**通信しない側は正しかったが、文面が 「設定ファイル `pricing.json` の入出力に失敗しました」**だった。真因は `CoreError::ConfigIo` が `#[error]` に `source` を含めないことで、 **「未設定」「https でない」「通信失敗」「表が壊れている」の 4 つが 1 文へ畳まれ、 しかもその文が壊れていないファイルを名指ししていた**。 `PricingFetch { reason }` を新設して理由を本文で運ぶようにした (**次の手が理由ごとに違う** — URL を入れる / 直す / 回線を見る / 表を直す)。 **一般化: 既存のエラー variant へ畳むときは、その `#[error]` が `source` を 表示に出すかを見る。** 出さない variant は**受け皿としては底なし**で、 落ちたものは identity を失う(#72 と同族。あちらは「数えない」、こちらは「名乗れない」) 5. **起動しただけでは 1 度も通信しない**(`fuseforks.log` に取得の行が出ない) 5'. **システム設定を開き、URL 欄を見ただけでも通信しない**(rev2。査読の穴 B — 到達確認のドットは**押していない GET** なので凍結違反) 6. 手で入れた単価が、**取得の失敗で潰れない** 7. **Anthropic の個体で、書き込みが 2.0× で効いている**ことを金額から逆算できる (同じターンを 3 欄で計算した値と比べて約 1.6 倍) 8. **2 行目が折り返さない**(P5 の実機。折り返したら B 案へ倒す — D4) 9. **単価欄に `$1.5` と打つと保存が拒否される**(数値のみ。査読のその他 3) ## P1 実装記録(2026-08-18) `fuseforks-core` 812 緑・clippy 警告ゼロ。**`budget.rs` の diff は空。** - **`pricing.rs` は `AgentStats` の行を掛ける** — `TurnRecord` ではない。 **今朝の Spec 39 rev4(`by_agent` の鍵を `(agentId, model)` へ)が前提**で、 行がモデルで割れていなければ**単価を当てる相手が存在しない**。 同じ日の 2 つの変更が噛み合った - **Spec 40 P2 の実測が回帰テストになった** — `the_measured_anthropic_rounds_cost_more_than_a_three_rate_table` が 実測の 13 ラウンド(525,818 / 455,850 / 69,942)をそのまま掛け、 **5 欄と 3 欄の比が 1.61 倍であること**を留める。**この Spec の存在理由が 数字で固定された** - **`budget` を参照しないことをソース走査で留めた**(D5 の凍結)。 文章だけだと次に読む人が「実効トークンがあるのだから使えばいい」と繋ぐ - **`ipc_contract` の凍結表が反応した** — `ModelTemplate` に欄を足すと `wire_field_sets_are_frozen` が赤くなる。**網が仕事をした**ので期待値を更新 (アルファベット順に挿す) ### 踏んだもの 3 つ(**2 つは自分の道具の使い方**) - **`include_str!` はコンパイル時に焼く。** 走査テストをそれで書いたら、 ミューテーションを戻したのに赤いままになった。村の作法は **`CARGO_MANIFEST_DIR` + 実行時読み**(`budget_reserve_wiring.rs`)で、そちらへ揃えた - **走査の述語が広すぎて自分の doc で赤くなった** — 「`budget` を呼ばない」と 書いた doc コメント自身が一致した。**コメント行を落としてから見る** - **`open(dst, "w")` は開いた瞬間に truncate する。** 復元のつもりで `open(dst,"w").write(open(src).read())` と書き、`src` が無かったので **`pricing.rs` が 0 バイトになった**(バックアップも同じ理由で空)。 **ファイル経由の復元をやめ、`sed` で足して `sed` で消す形**に変えた — **可逆な操作なら復元の失敗経路が存在しない** ## P2 実装記録(2026-08-18) **依存は 1 つも増えていない**(`fuseforks` の一意クレート 318 → 318。`reqwest` は core 経由で既にツリーに居た)。 - **パースはコア(純)、通信は GUI**。`fuseforks-core::pricing::parse_table` は I/O を 持たず、`reqwest` を使うのは `apps/gui-tauri/.../pricing_source.rs` だけ。 **コアを「利用者が設定した LLM だけと話す」ままに保つ**ための線引き - **既定の URL は `https://betyourluck.github.io/prices.json`**(2026-08-18 に 公開・実測 HTTP 200 / `application/json` / 187,405 バイト)。中身は **1,686 モデル**で、**書き込み単価を持つのは 193 件**(残りは概念が無いか未収載)。 出典は `BerriAI/litellm`(MIT)で、**著作権表示は JSON 自身の `_notice` に入れた**。 **鍵は素のモデル名**(`xai/grok-4.5` のような接頭辞を落とし、重複は欄の多いほうを採用) — 村が送るのは `ModelTemplate::model` の素の名前なので、これで突き合う。 **`null` の欄は書かない**(「未設定」と「0」を混ぜない)。 **URL を空にすれば通信は一切起きない**(S4) - **凍結を 2 つの走査テストで留めた** — `pricing_source.rs` の本体が `spawn` / `interval` / `sleep` / `HEAD` / `Instant::now` を持たないこと、 `state.rs` が棚を読むだけで `fetch_table` を呼ばないこと。 **`tokio::spawn` を 1 行足すミューテーションで赤を確認** - **不正な値はその 1 件だけ落とし、落とした数を返す**(`ParsedTable::dropped`)。 黙って捨てると**単価が入らない理由が画面から読めなくなる**(#72 の規律) - **`MAX_RATE = 1000.0`** は桁外れを弾く衛生であって価格の予測ではない。 $1,000/Mtok を超える単価は 2026 年時点で存在せず、**入っていれば単位の 取り違えか壊れた表** - **IPC を P2 に入れた**(P3 送りにしない)— 呼び出し元の無いまま置くと `dead_code` を `allow` で覆うことになり、**本当に死んだコードが隠れる** (Spec 15 P3 の判断と同じ) ### 公開する JSON は、パーサの欄名と機械で突き合わせた **実装から `WireEntry` / `WireTable` の欄名を抜き出して JSON と比べた** — 「Rust が読む欄のうち JSON に無いもの」が空、「JSON にあって Rust が無視する欄」も 把握済み(`_notice` / `version`)。**P5 の実機まで持ち越すと、取れないことが 実機で初めて分かる**ので、公開の前に閉じた。 ### `PRIVACY.md` は同じコミットで直した(rev2 の決定どおり) **2 文とも書き換えた** — 「開発者が運営する送信先はありません」は例外を明記し、 「利用者が自分で設定した接続先に対してだけ」は**2 種類**へ改めた。**足したのは 1 文ではなく節**で、(a) 押したときだけ (b) 変更・空にできる (c) 送るのは GET のみで 村の情報を含まない (d) **配信元には要求の事実と IP が残る**、まで書いた。 **(d) を書いたのは、書かないと「何も残らない」と読まれるから** — 実際には GitHub のアクセスログに残る。**こちらが送らないことと、相手に何も残らないことは別。** ## P3 実装記録(2026-08-18) vitest 394・build 緑・clippy 警告ゼロ・**辞書の鍵は ja / en で完全一致**。 - **金額を埋めるのは `Orchestrator::session_stats`**(`aggregate` ではない)。 **`aggregate` は純関数で村を知らない**が、単価は `ModelTemplate` に住む。 `StatsReport.cost` を `Option` にして、**呼び出し側が後から入れる**形にした - **`v-model.number` を使わなかった**のが単価欄の要点。`.number` は**空文字を `0` に する**ので、**「未設定」が「無料」に化ける**。文字列で持ち、`priceField` の setter で 空欄 → `null` / 数値でなければ**触らない**(通貨記号を書かせない = 査読のその他 3) - **「取得」はモデル登録ダイアログにしか無い。** システム設定には URL 欄だけで、 **到達確認のボタンも置かない**(凍結)。取得は**対象のモデルが決まっている場所** でしか押せない - **取得できなかった欄は触らない** — `if (hit.x !== null)` で埋めるので、 **手で入れた値を空で潰さない**(D3) - **`settings` ページを開いたときは設定を読むだけ**。`selectPage` に `loadPricingSource` だけを置き、**取得は呼ばない** - **金額の塊は `report.cost` が `null` なら丸ごと出ない**(0 と書かない。D4) **型検査が 2 回止めた** — `ModelTemplate` に欄を足したとき(ダイアログの雛形)と、 `StatsReport` に `cost` を足したとき(テストの fixture)。**どちらも欄を足した側が 気づけない場所**で、`bun run build` だけが指した。 ## Notes 1. **単価の出典は公開の機械可読な表が実在する**(2026-08-18 実測)。 `BerriAI/litellm` の `model_prices_and_context_window.json` は **3,040 モデル**を持ち、`cache_creation_input_token_cost` / `cache_creation_input_token_cost_above_1hr` / `cache_creation_input_token_cost_above_200k_tokens` / `prompt_cache_min_tokens` まで持つ。**ライセンスは MIT**(`enterprise/` の外)なので再配布できる。 **更新は激しい**(1 ファイルで 3 日に 10 コミット)ので、**固定して取り込む** のが利用者の裁定 2. **段階課金を入れていない**(D5)。200k を超える入力のターンは**過小に出る**。 `≈` の射程に含めるが、**実測で無視できない大きさになったら別 Spec** 3. **#104 の帯(`gemini-3.7-flash` は 19,000 トークン未満でキャッシュを返さない)は 単価表の問題ではない** — 掛ける相手が実測の内訳なので、帯の結果は既に `cached` の小ささとして現れている。**公開の表は gemini-3.7 の `prompt_cache_min_tokens` を持っていない**(実測 2026-08-18)ので、 **この村だけが持っている観測** 4. **`base_url` が同じでモデル名が同じ別ベンダーは取り違えうる**(Spec 40 Notes 6 と同じ)。 **rev1 は `model` 単独**(D2 に同じ文を置いて段階を揃えた。rev2・査読のその他 4)。 実際に衝突したら `backend` を足す — **空の欄を先に作らない** ## P5 実機記録(2026-08-18) **観測 8 / 9。** 利用者裁定 —「**見た目は使っているうちに整える**」(レスポンシブを 含む見た目の詰めは運用へ回す。D4 の「最大化前提」と同じ向き)。 ### D4 の 2 軸が、初回の実データで報われた ```text この会話 ≈ $0.98 (2026-08-18 時点の単価) 2/2 体・トークン 100.0% 全会話 ≈ $4.27 (2026-08-18 時点の単価) 5/7 体・トークン 99.9% ``` **`5/7 体・トークン 99.9%` が、2 軸を残した理由そのもの** — **体数だけなら 「3 割欠けている」、トークンだけなら「揃っている」と読める**。両方あって初めて 「単価の無い 2 体は居るが、その 2 体はほとんど払っていない」と読める。 **査読が「情報が 2 軸あって 1 行では狭い」と指摘したとき、処方が「軸を減らす」 ではなく「行を増やす」だったのが正しかった** — 減らしていたら初回の実データで 嘘になっていた。 ### 検収 4 が半分外れた(**この Spec で唯一の実バグ**) 詳細は検収の該当行。**挙動(通信しない)は正しく、文面が壊れていない `pricing.json` を名指ししていた。** 一般化は「既存のエラー variant へ畳むときは、 その `#[error]` が `source` を表示に出すかを見る」。 ### 検収 1 は原理的に踏めなくなった 「単価を 1 つも登録していない村で `≈ $` の行が出ない」は、**単価を登録した後では 観測できない**(新しい村を作れば踏めるが、確かめるのは `report.cost === null` の 1 分岐で、`a_village_without_any_price_reports_nothing` が単体で留めている)。 **「実機でしか確かめられないもの」と「単体で足りるもの」を、検収を書く時点で 分けておくべきだった** — #68 の族で、**存在しない量ではなく、順序で踏めなくなる形**。 ### `muse-spark-1.2-contributor` が表に載っていた さくら / ローカル系は表に無いと見込んでいたが、**公開した 1,686 モデルに入っていた** ($0.1 / $0.2 / cache read $0.002)。おかげで「この会話」は `2/2 体` になった。 **overlay で埋める必要がある範囲は、見込みより狭い。** ### 残る限界(画面には出ていない) **この版より前に記録されたターンは `cacheWrite = 0`** なので、**その会話の金額は 過小に出る**(書き込みが素の入力単価で数えられる)。全会話の `≈ $4.27` はその分 低い。**キャッシュ率のホバーで「書き込み 0 / 新規が大きい」行として読める**ので、 「記録していなかった」を「書き込みが無かった」と言わない形は保てている。 **数字で出す機構は作っていない** — 頻度ではなく**版の境目**の問題で、 時間が経つほど自然に消える。 ## 改訂履歴 - **rev2(2026-08-18・査読 7 点)**: **採用 5** = 穴 A(日付欄が無いと実装者が リモート HEAD を発明する → D1 に日付欄 + 凍結へ「ローカルの経過日数だけ」)/ 穴 B(凍結事項を具体名で列挙 + 検収 5' を追加)/ D4 の自己矛盾(**「実効と並べない」と 書いた直後に例で「実効の 94%」と書いていた** → 被覆率を素のトークンで定義し `coverage()` を `cost()` と分けた。2 行案を採用し、重複していた「未登録 2 体」を畳んだ)/ その他 2(クランプとフォールバック順序 → **単価を先に解決してから掛ける**手順を明記)/ その他 3・4(通貨記号の拒否 / D2 と Notes 4 の段階を揃えた)。 **訂正して採用 2** = (a) **日付は 2 欄ではなく 1 欄**(`pricing_as_of`)— 意味を「この数字が正しいと信じられる時点」に固定すれば取得でも手入力でも 常に埋まり、2 欄にすると「どちらを表示するか」が新しい未決になる。 差が意味を持つのは静的ファイルが現実に対して古いときだけで、**それは こちらから観測できない** (b) **`PRIVACY.md` の 1 文は P0 ではなく P2**(取得を 足すのと**同じコミット**)— P0 は契約の文だけで通信が生まれないので、 P0 で書くと**存在しない送信を宣言する**。嘘の窓を閉じるには 「能力が生まれるコミット」に揃えるのが正確 - rev1(2026-08-18): 起票。利用者裁定 3 件を取り込んだ — 金額を出す(D5 を覆す)/ 欄は 4 + 1 / 取得は設定可能な URL(案 B)。**D5 を覆す根拠は「3 理由のうち 2 つが 3 日で消えた」ことで、残る 1 つ(通貨は「合っている」と読まれる)は D4 の設計で受ける**