# Spec: コンテキスト長の自動取得 — 単価表に `max_input_tokens` を運び、「取得」で `contextLength` も埋める - 起票: 2026-09-05 - 状態: **rev2(査読 2 系統 15 点 → 採用 11 / 訂正して採用 2 / 前提を実測で訂正して採用 2。 記録は Notes 7)→ 承認(2026-09-05)→ P0 完了(契約 2 箇所 + Pages の生成スクリプトと表。 記録は「P0 実装記録」)→ P1 完了(コア。記録は「P1 実装記録」)→ P2 完了(フロント。 vitest 466・build 緑)→ P3 完了(台帳。記録は「P3 台帳記録」)→ P4 完了 = Done** (2026-09-05。実機検収 6 件すべて利用者が確認。**起票から Done まで同日**。 記録は「P4 実機記録」) - 起点: 利用者 —「`contextLength` の自動取得」(2026-09-05。[Spec 49](49_context-usage-ring.md) Notes 1 が「別 Spec の材料」として残していたもの) ## Goal モデル登録ダイアログの「取得」ボタン([Spec 41](41_model-pricing.md))を押すと、単価と 一緒に **そのモデルのコンテキスト長(入力の窓)** が `contextLength` の欄へ入る。取得元は 今と同じ `prices.json` で、**表の要素に `max_input_tokens` を 1 欄足す**(加算)。 **やらないこと**: 自動取得(起動時・保存時・画面遷移時の GET。`pricing_fetch_freeze` の **禁止事項 — 自動 GET・URL スキーム・到達確認・古さの遠隔判定 — は 1 字も緩めない**。 変わるのは「受け取る数値」の列挙に窓が 1 つ増えることだけ = 明確化であって緩和ではない。 rev2、査読 1-7)/ 保存(取得は欄に入れるだけ。今の単価と同じ)/ `maxOutputTokens` の取得 (Notes 2)/ 単価の更新(表の単価は 2026-08-18 のまま固定。Notes 3)/ ボタンの増設(D5)。 ## 起票時の実測(2026-09-05。コードと表と出典を読んだ) | 事実 | 場所 | 帰結 | |---|---|---| | **稼働中の村のテンプレート 16 件のうち 15 件は `contextLength` が既定の 128,000 のまま**、残る 1 件(3.8)は Spec 49 の検収で入れた 127,984 | `%APPDATA%/jp.outcasts.fuseforks/workspace/world.json` | 実際の窓は 3.x Flash / muse-spark が 1,048,576・claude 5 系 1,000,000・gpt-5.6 系 922,000・grok-4.6 500,000(出典 LiteLLM)。**輪は今、実際の 4〜8 倍を示している** — Spec 49 D2 の「輪の正直さは `contextLength` の正しさに乗る」が、既定値のままの村では全員で外れている | | `prices.json` は要素に `key` + 単価 5 欄しか持たない(1,732 件・`version: 1`・`fetched: 2026-08-20`) | `D:/Github/betyourluck.github.io/docs/prices.json` | コンテキストの欄は無い。**生成スクリプトはどこにも残っていない**(scratchpad で捨てた)— 08-18 以後の 3 回の追加(Perplexity 19 件 / `claude-fable-5-1` / `gemini-3.8-flash` `muse-spark-1.3`)は全部手書き | | 出典 LiteLLM は 3,561 件のうち `max_input_tokens` を 3,022 件が持つ。**`max_tokens` は出力側**(`gemini-3.8-flash` で 65,536 = `max_output_tokens`)で、1,675 件で `max_input_tokens` と食い違う | `model_prices_and_context_window.json`(2026-09-05 取得) | **使うのは `max_input_tokens` だけ**。`max_tokens` を代用にすると 3.8 の窓が 16 分の 1 になる | | `max_input_tokens` の最大は 10,485,760、`0` が 17 件(embedding)、非整数は 0 件 | 同上 | `u32` に収まる。**0 は「無い」と同じ扱い**で欄だけ落とす | | **`prices.json` の鍵はプロバイダ接頭辞を剥いだ形**。1,732 件のうち素の鍵で LiteLLM に居るのは 530 件(**うち正の窓を持つのは 506 件**。残る 24 件は素の要素が窓を持たない)、残りは `xai/grok-4.6` → `grok-4.6` のように末尾だけが鍵 | 08-18 の生成の帰結 | 窓を引くには**同じ剥ぎ方で逆引き**する。剥ぐと 1 つの鍵に複数の候補が集まり、**147 件で窓が割れる**(`gpt-oss-120b` = 131,072 対 128,000)。`gemini-3.7-flash` も deepinfra 経由が 1,000,000 で本家 1,048,576 と割れるが、**素の鍵があるときはそれだけを信じる**規則で解ける | | 08-18 から LiteLLM 側で単価が動いた要素が 39 件 | 同上 | 単価は触らない(利用者裁定「固定して取り込む」)。**足すのは欄だけ**にすれば単価の固定はそのまま | | `WireEntry` / `WireTable` は `deny_unknown_fields` を持たない | `pricing.rs:160-183` | **旧版アプリは新しい欄を黙って無視し、新版アプリは旧い表で `None` を読む**。互換は両向きで構造から成立する | | **`Option` の欄に `-1` が来ると、その 1 件ではなく表全体の `from_str` が落ちる**(`invalid value: integer -1, expected u64`)。`Option` なら `-1` も `1.5` も読めて後段で落とせる。文字列はどちらでも表全体が落ちる(既存の単価 5 欄と同じ) | scratchpad の serde probe(2026-09-05。rev2、査読 2-1) | **窓も単価と同じく `Option` で受けて `sane_context` で正規化する**。整数型で受けると「欄だけ落とす」が書けない | | `parse_table` は `input` と `output` が揃わない要素を落とす。`fetchPrices()` は `draft.model === key` の完全一致で 1 件引き、**表にある欄だけ上書き** | `pricing.rs:220-244` / `ModelTemplateDialog.vue:80-108` | 窓も同じ規則に乗せる — 表にあれば上書き・無ければ触らない | | `pricing fetch:` の計器は `entries= dropped= as_of=` の 3 つ | `pricing_source.rs:174` | 窓が入った件数を足さないと、表が欄を運んでいるかがログから読めない | ## Design ### D1: ワイヤは `prices.json` の要素に `max_input_tokens` を任意欄で足す(加算) **表に書く値は正の整数**(生成側 D2 が保証する。`u32` に収まる — 出典の最大は 10,485,760)。 **読む側の型は D4**(`Option` で受けて `Option` へ正規化。rev1 の「`u32` を足す」と 「`Option` で受ける」の食い違いは rev2 で D1 = 表の値の性質 / D4 = 読む側の型、へ 書き分けた — 査読 1-1)。名前は出典の語をそのまま採る。**「コンテキスト長」ではなく「入力の窓」**で、 出力は含まない — この村の `contextLength` は Spec 49 で「入力トークン ÷ 窓」の分母として 初めて読み手が付いたので、意味は入力の窓と一致している。**`ModelTemplate.contextLength` は 改名しない**(型・IPC・`world.json` の欄は据え置き。表の欄名とアプリの欄名が違うのは `input_per_mtok` ↔ `inputPerMtok` と同じ層の差)。 `version` は 1 のまま。**欄を足すだけで既存の欄を 1 つも変えない**ので、版を上げる理由が無い。 `WireTable` は `version` を読んでいない(欄そのものが無い)ので、**今上げても何も変わらず、 将来「知らない版は拒む」検査を入れた日に、この加算だけの表が旧版アプリから拒まれる** 形を先に作ることになる(rev2、査読 1-5 — rev1 の文は前半と後半が矛盾していた)。 ### D2: 生成規則は「素の鍵を優先・候補が一致すれば採る・割れたら書かない」 `prices.json` の鍵 `k` に対し: 1. **LiteLLM に `k` そのものがあれば、その要素の `max_input_tokens` だけを見る。** 正の整数なら 採る。**無いか 0 なら「窓なし」で確定し、(2) へは落ちない**(素の要素はその配信元の正本で、 接頭辞つきは別のゲートウェイ。正本が窓を書いていないのに他所の値で埋めない — rev2、 査読 2-3。実測ではこの枝が 24 件) 2. 無ければ、**`/` で区切った末尾が `k` と完全一致する**候補を集める (`candidate.rsplit("/", 1)[-1] == k`。**文字列の末尾一致ではない** — `endswith` だと `flash` が `super-flash` に当たる。rev2、査読 2-5)。`k` 自身が `/` を含む鍵 (Perplexity 経由の手書き 19 件)は末尾に一致する候補が構造的に無く、ここで「候補なし」に なる。候補の `max_input_tokens` から **0 と欠落を先に除外し**(rev2、査読 1-8 — 除外せずに 比べると 0 と正数の混在を「割れた」と誤判定する)、残りが **1 種類に定まるときだけ**採る 3. 割れたら **書かない** 実測(2026-09-05): (1) で 506 件・(2) で 987 件が埋まり、(1) で窓なし確定が 24 件、 (2) で割れて書かないのが 147 件、候補が無いか候補に窓が無いのが 68 件。 **1,732 件中 1,493 件(86%)に窓が入り、239 件は欄が付かないまま据え置き。** **割れたら書かない理由は、輪の診断が片側にしか効かないこと。** Spec 49 D2 は 「100% 超 = 設定が実際の窓より小さい」を診断として読む。**逆向き(設定が実際より大きい)は 輪が小さく出るだけで、画面から検出できない。** 候補の最大値を採ると過大の側へ倒れ、 最小値を採ると過小の側へ倒れるが、どちらも「その配信元の窓」ではない数字を配ることになる。 **表に無いものは無いと言う** — `resolve` が入力と出力の揃わない要素を落とすのと同じ境界。 ### D3: 生成スクリプトを Pages リポジトリへ置く(`tools/build_prices.py`。加算のみ) 08-18 のスクリプトが残っていないので、この 1 欄を足すのに**表の全要素を手で触る**ことになる。 再現できる形へ直す — スクリプトは (a) 既存の `prices.json` を読み (b) LiteLLM の最新を取り (c) **D2 の規則で `max_input_tokens` だけを足し** (d) **単価と鍵は 1 桁も変えない** (手書きの要素もそのまま通る)(e) `_notice` に 1 文足す(「`max_input_tokens` は LiteLLM の YYYY-MM-DD 取得分から D2 の規則で足した。単価は動かしていない」)。**差分は窓が定まった 1,493 要素への 1 欄追加だけ**で、残る 239 件と `fetched` は 1 バイトも動かない(rev2、 査読 1-3 — rev1 の「各要素」は嘘だった)。**確認は `git diff` の追加行が全部 `"max_input_tokens":` で始まり、削除行が `_notice` の 1 行だけ**であること。 **`fetched: 2026-08-20` は据え置く**(あれは `pricingAsOf` へ入る「単価の時点」。窓を足した 日ではない)。帰結として、ログの `pricing fetch:` は `as_of=2026-08-20` と `context=1493` を 同じ行に出す — **窓の鮮度は `as_of` ではなく `_notice` の 1 文と `context=` で読む** (rev2、査読 1-10。Notes 6)。 **置き場は Pages リポジトリ**(`betyourluck/betyourluck.github.io`)。表の正本がそこに居るので、 生成物と生成器を同じ repo に置く。**利用者の資産なので、置く位置と名前は利用者の裁定** (この Spec は「置く」ことを決め、置き方は P0 で確認する)。**Fuseforks の repo には置かない** — アプリは表を読む側で、表の作り方を知らなくてよい。 ### D4: アプリ側は `parse_table` が読み、欄だけを検査し、欄だけを落とす - `WireEntry` に `max_input_tokens: Option`(`#[serde(default)]`)。**整数型で受けない** — `Option` だと `-1` が 1 件あるだけで表全体の `from_str` が落ち、「欄だけ落とす」が 書けない(実測表の最終行。rev2、査読 2-1 は `Option` を案に挙げたが、それだと `1.5` で 同じ形に落ちる。**単価 5 欄が `Option` + `sane` で受けているのと同じ形**にする) - `ParsedTable.entries` は `(String, Rates)` から **`(String, Rates, Option)`** へ — ただし `Rates` には入れない。`Rates` は `resolve` / `cost_of` の入力で「単価」の型。 窓を混ぜると `pricing.rs` の「`budget` を呼ばない」と同じ線で「単価の計算に窓が居る」形になる - 検査は `sane_context(Option) -> Option`: **有限・小数部 0・`1..=u32::MAX`** の ときだけ `Some`。0・負・非整数・`u32` 超・非有限は **欄だけ `None` にして要素は残す** (`dropped` は数えない — 単価としては健全な要素)。 **`parse_table` の要素を落とす規則は変えない**(窓しか持たない要素は今の表に無いし、 入っても単価が無いなら「単価表の要素」としては落ちるのが正しい) - `FetchedPrice` に `max_input_tokens: Option`(TS 側は `maxInputTokens: number | null`) - `pricing fetch:` の計器に **`context={窓が入った件数}`** を足す(表が欄を運んでいるかを ログから読むため。Spec 41 の `dropped` と同じ「黙って捨てない」の側) ### D5: ボタンは 1 つ。既存の「取得」が単価と窓を同時に入れる `fetchPrices()` の上書き 5 行の隣に `if (hit.maxInputTokens !== null) d.contextLength = hit.maxInputTokens;` の 1 行。**表にあれば上書き・無ければ触らない**の規則は単価と同じ (手で入れた値を空で潰さない。ただし表にあるときは手入力を上書きする — 単価も同じで、 下書きなので保存しなければ戻る)。 **`contextLength` の欄の隣にボタンを増やさない。** 1 回の GET で表全体が届いており、 2 つ目のボタンは同じ GET をもう 1 度撃つだけ。**通知文で何が入ったかを言う** — 通知は **2 文の並び**にする: 1 文目は既存の `filled` / `filledWithDropped`(単価の側。 文言もキーも変えない)、2 文目は新設の `contextFilled`「コンテキスト長も入れました」/ `contextMissing`「コンテキスト長は表にありません(手入力のままです)」。 rev1 の「`filledWithContext` / `filledNoContext` の 2 鍵」は、既存の `filled` を死んだ鍵にする うえ(査読 2-2)、`dropped` の有無と掛け合わせると 4 鍵になる。**独立した 2 文を並べれば 掛け合わせが消え、語順の問題も起きない**(節を連結するのではなく文を並べる — Spec 13 P3b の `opFailed` + `op.*` と同じ形)。表に窓が無いのは D2 で「書かない」と決めた要素と Perplexity 経由の手書き要素で、**そのモデルの利用者は手入力のまま**なので、入らなかったことを黙らない。 **`pricingAsOf` は触らない。** あの日付は「単価の時点」で、窓は世代で決まる値であり 日付を持たせる意味が無い(持たせると欄が 1 つ増えて、単価と窓のどちらの時点か読めなくなる)。 ### D6: 触る台帳(P0 と P3 に割る) **P0(契約。実装より先)**: `data_contract.yaml` 2 箇所 — `ModelTemplate.contextLength` の 注記(「利用者の手入力」→「手入力または取得」。Spec 49 P0 が書いた「実際の窓と違えば嘘」は そのまま)/ `pricing_fetch_freeze`(「取得は https のみ・数値のみ」の数値の列挙に窓を足し、 窓の検査規則 = `sane_context` と計器を書く。**禁止事項の側は 1 字も触らない**)。 ~~`FetchedPrices` の欄~~ — **契約に `FetchedPrices` は元から無い**(P0 実装記録。rev2 までは 3 箇所と書いていた)。 **P3(実装の後の台帳)**: Spec 49 の D2 と Notes 1 へ続報(「自動取得は範囲外」が本 Spec で 埋まった)。DETAIL 日英の統計・モデル登録の行に 1 文。**README 3 言語の「単価は『取得』 ボタンか手入力で入れる」へ括弧 1 つ**(「単価とコンテキスト長は…」)— 文は今も真だが、 読み手は「取得は単価だけ」と読むので、S1 の利用者がボタンの存在理由に気づけない(rev2、 査読 1-9)。**PRIVACY 日英は変えない** — 同じ GET で同じ配信元、増えるのは応答に載る数字 1 つで「外部へ送るもの」は増えない。**grep 網の外**: LP は単価の取得に触れているか P3 で 数える(触れていなければ嘘にならない)。Qiita 記事は単価そのものに触れていない。 ## Stories - S1 gemini-3.8-flash のテンプレートで「取得」を押すと、`contextLength` が 128,000 から 1,048,576 へ変わる(保存前)。保存すると輪の % が 8 分の 1 になる - S2 Perplexity 経由のテンプレート(`perplexity/deepseek-v4-flash-0731`)で「取得」を押すと、 単価は入り `contextLength` は手入力のまま残り、通知文がそう言う - S3 旧い表(欄の無い `prices.json`)を指す URL でも「取得」は今までどおり単価だけ入る。 旧版アプリが新しい表を読んでも壊れない - S4 `fuseforks.log` の `pricing fetch:` 行で、表が窓を何件運んでいるかが読める ## Phases - **P0** — **契約と表**: (a) `data_contract` の 2 箇所(D6 の P0 側)(b) Pages リポジトリへ `tools/build_prices.py`(D2 / D3)を置き、`docs/prices.json` へ `max_input_tokens` を加算して 配信。`git diff` の追加行が全部窓の欄で、削除行が `_notice` だけであることを確かめる。 **アプリ側の実機検収は表が無いとできない**ので、表はここで配る。 (rev1 は D6 を P0 と P3 の両方に書いていた — rev2 で D6 自体を 2 段に割った。査読 1-2 / 2-4。なお査読 1-2 の「P0 で凍結を直さないと P1 が CI に止められる」は**外れ** — `pricing_source.rs` の走査テストが見るのは禁止語 5 つ(`spawn` / `interval` / `sleep` / `HEAD` / `Instant::now`)だけで、契約の文面は機械の網の外。P0 へ寄せる理由は 「契約が実装より先」の作法の側) - **P1** — コア: `pricing.rs`(`WireEntry.max_input_tokens: Option` / `sane_context` / `ParsedTable.entries` の 3 つ組)+ `pricing_source.rs`(`FetchedPrice.max_input_tokens: Option` / 計器の `context=`)。単体 4 本(欄がある表で窓が入る・**0 と非整数と `u32` 超は欄だけ落ちて要素は残る**・**負数が 1 件あっても表全体は落ちない**・**欄の無い 旧い表でも `entries` の件数と `dropped` が変わらない**)。**ミューテーション**: `sane_context` を素通しに → 2 本目だけ赤 / 受け型を `Option` へ → 3 本目だけ赤 / 欄を `Rates` へ混ぜる → 走査テスト `pricing_never_reaches_into_the_budget_weights` は 緑のまま(それが「混ぜても網に掛からない」証拠なので、D4 の「`Rates` に入れない」は doc と型の形で留める) - **P2** — フロント: `types.ts`(`FetchedPrice.maxInputTokens: number | null`)/ `fetchPrices()` の上書き 1 行 + 通知の 2 文目 / 辞書 ja/en(`modelTemplate.pricing.contextFilled` / `contextMissing` の 2 鍵。既存の `filled` / `filledWithDropped` は文言もキーも変えない) - **P3** — 台帳(D6 の P3 側) - **P4** — 実機検収 ## P0 実装記録(2026-09-05) **契約は 3 箇所ではなく 2 箇所だった。** `data_contract` は `FetchedPrices` のワイヤを元から 書いていない(Spec 41 P2 が `pricing_source.rs` の型と doc に留めた)。窓の受け方・検査規則・ 計器は `pricing_fetch_freeze` の「取得は https のみ・数値のみ」の段落へ入れ、 `ModelTemplate.contextLength` の注記を「手入力または取得」へ。**D6 の「3 箇所」は書く前に 数えていなかった**(存在しない箇所を予定に書いた — #68 の形)。 **Pages 側**: `tools/build_prices.py`(D2 の 3 段 + `_notice` の 1 文 + 書式の自己検査)と `docs/prices.json`。実測は D2 の予測と 1 件も違わず `entries=1732 filled=1493 bare=506 bare-none=24 tail-agree=987 tail-disagree=147 no-candidate=68`。 村の個体の窓は 3.x Flash / muse 1,048,576・claude 5 系 1,000,000・gpt-5.6 系 922,000・ grok-4.6 500,000 で入り、`perplexity/…` と `gpt-oss-120b` は付かない(検収 3 の対照が 表の側から確定)。**2 回走らせても diff が同じ**(冪等)。 **罠を 2 つ踏んだ。どちらも「差分が単価の不動を示すこと」を守るためのもの**: - **欄を要素の末尾に足すと、その前の行にカンマが付く。** 1 回目の diff は `2987 insertions / 1494 deletions` で、削除の 1,493 本は `"output_per_mtok": 0.0` → `"output_per_mtok": 0.0,` だった。値は動いていないのに `git diff` は「変更」と読む。 処方は**欄を `"key"` の直後へ置く**こと — 前の行は元からカンマ付きなので、diff は 純粋な 1 行追加になる(`1494 insertions / 1 deletion` = 窓 1,493 + `_notice` 1 対 `_notice` 1)。 **JSON に欄を加算するとき「追加行だけの diff」は置き場で決まる** - **`git checkout` 直後の作業コピーは CRLF になる**(この端末は `core.autocrlf=true`、 blob は LF)。書式の自己検査(無改変の再シリアライズが元と一致するか)が改行の差で 落ちて何も書かなかった — **検査は設計どおり効いた**が、比較の前に LF へ正規化する 1 行が要った。書き戻しは LF のままで、git が blob 側で揃える ## P1 実装記録(2026-09-05) `pricing.rs`: `WireEntry.max_input_tokens: Option` / `sane_context(Option) -> Option` (有限・小数部 0・`1..=u32::MAX`)/ `ParsedTable.entries` を `(String, Rates, Option)` の 3 つ組へ / `ParsedTable::windows()`(計器用の件数)。`pricing_source.rs`: `FetchedPrice.max_input_tokens: Option` / `pricing fetch:` 行へ `context=`。 単体 4 本追加(コアの pricing 11 → 15・GUI 5・clippy 警告ゼロ)。 **`Rates` に窓を入れない判断は型で留めた** — 3 つ組の 3 番目に置いたので、`resolve` / `cost_of` の入力に窓が居る形は書けない。走査テスト `pricing_never_reaches_into_the_budget_weights` はコメントを落としてから `budget` を探すので、doc に「`budget` を呼ばないのと同じ線」と 書いても緑(書く前に確かめた)。 **ミューテーション 2 回(予測を先に書いてから)**: | 変異 | 予測 | 実測 | |---|---|---| | `sane_context` を素通し(範囲検査を消す) | **Spec の Phases は「2 本目だけ赤」と書いていた** → 書く前に数え直して「2 本目と 3 本目」へ | **2 本目と 3 本目が赤**(`an_unsane_window_drops_only_the_field` / `a_negative_or_fractional_window_does_not_break_the_table`)。素通しは `-5` を 0 へ、`1.5` を 1 へ丸めるので、3 本目の「欄が `None`」も落ちる | | 受け型を `Option` へ | 3 本目だけ赤 | **3 本目だけ赤**(`-5` で `from_str` ごと失敗。1 本目・2 本目・4 本目は整数だけの表なので通る) | **Phases の予測が 1 つ外れていた** — 素通しの変異は「0 と `u32` 超」だけでなく「負数と小数」の テストにも当たる。**同じ関数を壊す変異が複数のテストを赤にするのは、テストが同じ関数の 別の性質を見ている証拠**で、穴ではない。復元は `git checkout` ではなく文字列の差し戻しで、 前後のハッシュ一致を見た(2026-08-30 の「未コミットの実装ごと消える」罠を避ける)。 ## P2 実装記録(2026-09-05) `types.ts` の `FetchedPrice.maxInputTokens: number | null` / `ModelTemplateDialog.vue` の `fetchPrices()` に上書き 1 行と通知の 2 文(`${rates} ${window}` — 1 文目は既存の `filled` / `filledWithDropped` のまま、2 文目は新設の `contextFilled` / `contextMissing`)/ 辞書 ja/en に 2 鍵(`pricing` の末尾。**既存の鍵と文言は 1 字も変えていない**)。 vitest 466(鍵集合一致のテストが ja/en の対を機械で留める)・`vue-tsc` + `vite build` 緑。 辞書は JSON を読んで鍵を足し同じ書式(indent 2)で書き戻した — P0 の生成スクリプトと同じ 「書式の自己検査 → 通ったときだけ書く」の形。 ## P3 台帳記録(2026-09-05) DETAIL 日英の会話ペインの行(輪の分母が「取得」で入る 1 文)/ README 3 言語の統計の行 (「取得」はコンテキスト長も入れる、の括弧)/ Spec 49 の D2 と Notes 1 へ続報 / CLAUDE.md の「Spec の状態」と現在地。**PRIVACY 日英は変えていない**(同じ GET・同じ配信元)。 **LP は変えなくてよい** — 単価表の取得に触れているのはプライバシーの段落 1 つで 「利用者が『取得』ボタンを押したときにのみ」と書いており、窓が同じ応答に載っても文は真のまま。 Qiita 記事は単価に触れていない。 ## 検収項目(各項目に到達経路を書く) | # | 何を見るか | 到達経路 | |---|---|---| | 1 | gemini-3.8-flash のテンプレートで「取得」→ `contextLength` が 1,048,576 に変わり、通知が「単価を入れました(保存はまだです) コンテキスト長も入れました」の 2 文 | モデル登録ダイアログ。**保存前に見る**(欄に入るだけ) | | 2 | 保存して 1 本依頼 → 輪の % が検収 1 の前の 8 分の 1 前後(ザリの 1 周目 55K なら 43% → 5%) | 会話ペイン。Spec 49 検収 1 と同じ読み方(`cache:` 行の `prompt=` ÷ 1,048,576) | | 3 | `perplexity/deepseek-v4-flash-0731` のテンプレートで「取得」→ 単価は入り、`contextLength` は元の値のまま、通知の 2 文目が「コンテキスト長は表にありません(手入力のままです)」 | 表に無い鍵の対照。**D5 の「入らなかったことを黙らない」** | | 4 | `fuseforks.log` に `pricing fetch: entries=… dropped=… context=… as_of=…` が出て、`context` が P0 の生成ログの件数と一致する | ログ。表が欄を運んでいる証拠 | | 5 | 取得元 URL を欄の無い旧い表(P0 前の `prices.json` を `raw.githubusercontent.com` の旧コミットで指す)へ変えて「取得」→ 単価だけ入り、`context=0` | 互換の後ろ向き。**URL は https なので凍結の中** | | 6 | 手で `contextLength` を 12,000 にしてから「取得」→ 表の値で上書きされる。保存せず閉じると元の値 | 「表にあれば上書き」の側。単価の規則と同じであることの確認 | ## P4 実機記録(2026-09-05。利用者検証) **検収 1〜6 すべて OK**(利用者の報告)。検収 1 の通知はスクリーンショットで確認 — 「単価を入れました(不正な 3 件は取り込んでいません) コンテキスト長も入れました。」の 2 文が並び、`単価の時点` は `2026-08-20` のまま(D5 の「`pricingAsOf` は動かさない」が 画面で読める)。 **利用者の質問「不正な 3 件ってなんだろう?」の答え**: Spec 41 の衛生検査($1,000/MTok 超を 落とす)に当たる `jais-13b-chat`(500 / 2,000)・`jais-30b-chat`(3,200 / 9,710)・ `mt0-xxl-13b`(500 / 2,000)。出典 LiteLLM の Azure jais 系と mt0 の行で、単位の取り違えか 壊れた行。**Spec 50 で増えたものではなく**、窓を足す前の表でも同じ 3 件が落ちていた (`dropped` は単価の側だけを数え、窓が不正な要素は欄だけ落として数えない — D4)。 名指しは足さない — 1,732 件で 3 件なら件数で足りる。 ## Notes 1. **`max_tokens` は使わない。** LiteLLM の `max_tokens` は現行の要素では `max_output_tokens` と同じ値で、`gemini-3.8-flash` は 65,536。**名前が「最大トークン」でも窓ではない**。 `max_input_tokens` を持たない 6 件のために `max_tokens` へ落とすと、その 6 件だけ窓が 出力上限になる — 落とさない 2. **`maxOutputTokens` は取らない。** LiteLLM は `max_output_tokens` も持つが、 `data_contract` が「**`requestTimeoutSecs` と連動する。上限だけ上げると『途中で切れる』が 『タイムアウトする』に変わるだけ**」と凍結している。取得で上限だけ動かすと、その凍結が 言う形をボタン 1 つで作る。要るなら別 Spec で timeout と対で 3. **単価の更新は範囲外。** 39 件が 08-18 から動いているが、利用者裁定は「固定して取り込む」。 更新するなら `pricingAsOf` が動く別の作業で、D3 のスクリプトに `--refresh-prices` の ような口を足す形になる(今は作らない — 空の口) 4. **表の窓と実際の窓は同じとは限らない。** LiteLLM は `claude-sonnet-5` を 1,000,000 と 書くが、**それがベータヘッダ無しで届く窓かは確かめていない**(Spec 49 D2 が 3.8 以外の 窓を書かなかったのと同じ理由)。表の値で輪が小さく出ても、400 で落ちる位置は プロバイダが決める。**この Spec は「手入力の既定 128,000」を「表の値」に置き換えるだけ**で、 窓の真偽を保証する層は持たない — Spec 49 D2 の「100% 超は設定が小さい診断」は残り、 逆向きの診断は今も無い 5. **接頭辞を剥ぐ生成規則は 08-18 の産物で、この Spec が初めて文書にする。** 44 件は 剥いだ先の候補のどれとも単価が一致しない(08-18 以後に LiteLLM 側で動いた要素と、 複数候補のうち別のものを採っていた要素)。**この Spec では触らない**が、単価を更新する 日には D2 と同じ「割れたら書かない」を単価にも当てることになる 6. **`as_of` と `context=` は別の時点を指す。** 表の `fetched`(→ `pricingAsOf`)は単価の時点で、 窓を足しても動かさない(D3)。だから `pricing fetch: … context=1493 as_of=2026-08-20` の 行は「8 月の表が窓を知っている」ように読めるが、矛盾ではない — 窓は `_notice` の 1 文が 日付を持ち、ログでは `context=` の件数が「表が窓を運んでいる」ことの証拠になる。 **窓に日付を持たせない理由は D5**(世代で決まる値で、日付を持たせると欄が増えて 単価と窓のどちらの時点か読めなくなる) 7. **査読の反映(rev2・2026-09-05。2 系統 15 点)** | 系統-# | 指摘 | 扱い | 根拠 | |---|---|---|---| | 1-1 | D1 `u32` と D4 `Option` の型の揺れ | 採用 | D1 = 表の値の性質 / D4 = 読む側の型、へ書き分け | | 1-2 / 2-4 | D6 が P0 と P3 に重複 | **前提を訂正して採用** | D6 を P0(契約)/ P3(台帳)に割った。**「P0 で凍結を直さないと P1 が CI に止められる」は外れ** — 走査テストは禁止語 5 つだけを見る。寄せる理由は作法の側 | | 1-3 | 「各要素へ 1 欄」は 86% と矛盾 | 採用 | 1,493 要素への追加・239 件据え置きと書き直し、`git diff` の確認法も具体化 | | 1-4 / 2-3 | 530 対 506 の差と、素の鍵に窓が無いときのフォールバック | 採用 | (1) で窓なしなら確定して (2) へ落ちない(実測 24 件)。素の要素が正本 | | 1-5 | `version` を上げない理由が自己矛盾 | 採用 | 「検査が無いので今は無意味・将来検査を入れた日に互換を壊す」へ | | 1-6 | 「全部 128,000」は 15 + 1 | 採用 | 実測表を訂正 | | 1-7 | 「凍結を 1 字も緩めない」と D6 の追記 | 採用 | 禁止事項は不変・受け取る数値の列挙への追加、と書き分け | | 1-8 | D2 (2) で 0 を除外してから比べる | 採用 | 明記(実装済みの probe は最初からそうしていたが、文に無かった) | | 1-9 | README の「取得ボタン」の文が不親切 | 採用 | 3 言語へ括弧 1 つ(P3) | | 1-10 | `as_of=2026-08-20` と `context=1493` が同じ行に並ぶ | 採用 | Notes 6 | | 2-1 | `Option` に負数が来ると parse が落ちる | **訂正して採用** | 実測で表全体が落ちることを確認。処方は査読案の `Option` ではなく **`Option`**(`1.5` でも落ちない。単価 5 欄と同じ形) | | 2-2 | 2 鍵新設だと既存の `filled` が死ぬ | **訂正して採用** | 査読の 2 案はどちらも `filledWithDropped` の軸を数えていない(掛け合わせで 4 鍵)。**独立した 2 文を並べる**形へ | | 2-5 | `endswith` は部分一致を拾う | 採用 | `rsplit("/", 1)[-1] == k` の完全一致と明記(probe はそう書いてあった) |