7.5 KiB
7.5 KiB
Review: モデル reasoning/thinking 制御の内部抽象整理
前提・要件の確認
- manifest / 上位設定で reasoning を
Stringまたはi32として表現できるReasoningControlを#[serde(untagged)]enum 化し、Effort(ReasoningEffort)とBudgetTokens(i32)のいずれかとして deserialize できる (crates/llm-worker/src/llm_client/capability.rs:87-92)。- TOML 経由でも文字列・整数を受けられることを
crates/manifest/src/config.rs:709-731のテストfrom_toml_accepts_worker_reasoning_string_or_integerが直接確認している。
WorkerManifestConfig/WorkerManifestが reasoning を保持し、cascade merge 後にpod::apply_worker_manifest()からRequestConfig::reasoningへ渡されるWorkerManifestConfigにreasoning: Option<ReasoningControl>を追加(crates/manifest/src/config.rs:65-69)。WorkerManifestConfig::mergeがupper.reasoning.or(self.reasoning)で他フィールドと同じ「上位優先」ポリシーで合成 (crates/manifest/src/config.rs:228-230)。テストmerge_worker_reasoning_upper_wins(563-587)で確認済み。TryFrom<PodManifestConfig> for PodManifestがcfg.worker.reasoningをWorkerManifest::reasoningに転送 (crates/manifest/src/config.rs:341-343)。WorkerManifest自体にもreasoningフィールドが追加されている(crates/manifest/src/lib.rs:103-105)。apply_worker_manifest()がconfig.reasoning = wm.reasoning.clone();でRequestConfigに反映 (crates/pod/src/pod.rs:1401)。
ReasoningControlが enum 化され、effort と budget の同時指定が型上できない- 同じ enum の 2 variant(
Effort(_)とBudgetTokens(_))として表現されており、両方を同時に保持する手段は存在しない。
- 同じ enum の 2 variant(
ReasoningEffortがminimal/low/medium/high/xhighと未知文字列の素通しを扱えるReasoningEffortに 5 つの既知 variant +Other(String)を定義 (crates/llm-worker/src/llm_client/capability.rs:94-102)。From<String>/ 手書きDeserializeで未知ラベルがOther(...)に落ちる(117-145)。 テストreasoning_control_deserializes_effort_labelsがxhigh既知化とprovider-native素通しを確認。
- budget token 指定が signed integer として扱われ、Gemini の
-1のような値を表現できるReasoningControl::BudgetTokens(i32)で signed 化済み。- Anthropic 側 wire 型
AnthropicThinking::Enabled.budget_tokensもu32→i32に変更 (crates/llm-worker/src/llm_client/scheme/anthropic/request.rs:44)。 - Gemini scheme は
as i32キャストを廃して直接i32を渡す(crates/llm-worker/src/llm_client/scheme/gemini/request.rs:208-211)。 parse_reasoning_budgetテストで-1がそのまま伝搬することを確認(crates/manifest/src/lib.rs:380-388)。
- OpenAI Chat / OpenAI Responses / Anthropic / Gemini の既存 reasoning 投影が enum 型に追従している
- OpenAI Chat:
Effort(_)のみreasoning_effortに投影、BudgetTokens(_)は黙殺 (crates/llm-worker/src/llm_client/scheme/openai_chat/request.rs:154-160)。 - OpenAI Responses: 同様に
Effort(_)のみreasoning.effortへ。BudgetTokens(_)のときはeffort=NoneのReasoningConfigを作りかけて末尾の.filter(|r| r.effort.is_some())で除外 (crates/llm-worker/src/llm_client/scheme/openai_responses/request.rs:167-179)。 - Anthropic:
BudgetTokens(_)のみthinking.budget_tokensに投影、Effort(_)は黙殺 (crates/llm-worker/src/llm_client/scheme/anthropic/request.rs:166-178)。 - Gemini:
BudgetTokens(_)のみthinking_budgetに投影、Effort(_)は黙殺 (crates/llm-worker/src/llm_client/scheme/gemini/request.rs:200-211)。 - 各 scheme に variant 不一致時の正の・負の両ケースを示すユニットテストが追加されている
(
thinking_budget_projected_when_supported/effort_reasoning_not_projected_to_*/reasoning_effort_projected_when_supported/budget_reasoning_not_projected_to_openai_chat)。
- OpenAI Chat:
- 既存の reasoning 無指定時は wire request にパラメータを出さない
request.config.reasoning.as_ref()起点なのでNoneのときは scheme 投影に入らず、Option<…>フィールドは すべて#[serde(skip_serializing_if = "Option::is_none")]系で省略される。reasoning_omitted_when_unsupportedテスト(OpenAI Responses)でも追従している。
アーキテクチャ・スコープ
- 共通型は
llm-worker/src/llm_client/capability.rsに閉じ、manifest 側はpub use llm_worker::...::{ReasoningControl, ReasoningEffort}で 再エクスポートするだけ(crates/manifest/src/model.rs:19)。ModelCapabilityの従来パターンに沿っており、llm-worker は低レベル基盤に留める方針と整合する。 cargo add系の依存追加は発生していない(serde既存のものにDeserializer/Serializerを追加 import するのみ)。- 検証は capability の
Effort/BudgetTokens/Both区分どまりで、ラベル文字列や budget 数値そのものは provider に委ねている(チケットの「最小限の検証」要件と一致)。 WorkerManifestConfig::mergeの挙動が他フィールドと一貫したupper.or(self)。新たな policy 分岐を持ち込まず、 cascade の規則を守っている。- 範囲外項目(UI プリセット、provider 推奨値テーブル、未実装 scheme への展開、reasoning 出力 block の保存ポリシー) はいずれも触れられていない。
指摘事項
Non-blocking / Follow-up
- OpenAI Responses scheme の reasoning 投影
(
crates/llm-worker/src/llm_client/scheme/openai_responses/request.rs:167-179)は.map(|effort| ReasoningConfig { effort: match …, … }).filter(|r| r.effort.is_some())という二段構えで「BudgetTokensが来たらいったんeffort=Noneで組み立ててから捨てる」形になっている。 動作上は正しく無指定時と同じ wire になるが、and_thenでEffortだけを通す形に揃えると Anthropic / Gemini / OpenAI Chat の他 scheme と読み口が一致して読みやすい。 ReasoningEffort::Other(String)を保持するためReasoningEffort自体はCopyを外している。 これは正しいトレードオフだが、ReasoningControlもCloneのみとなり、apply_worker_manifestでwm.reasoning.clone()が必要になっている。意図通りなのでそのままでよい(記録のみ)。
Nits
crates/manifest/src/config.rs内のResolveError::RelativePath、flush_pending周りの呼び出しなど、 rustfmt 依存の整形差分が大量に紛れ込んでいる。レビュー対象としては無害だが、commit を分けると reasoning 周りの diff が読みやすくなる。今回限りの整形なら現状で問題なし。
判断
Approve — 完了条件はすべて満たされており、enum 化により effort と budget の同時指定が型上排除されている。
manifest の [worker] reasoning = から RequestConfig::reasoning、各 scheme の wire 形式までの経路が
テストで貫通検証されている。OpenAI Responses の二段 filter は読みやすさのみの非ブロッキング指摘。