diff --git a/TODO.md b/TODO.md index a6e4b7e5..109280b4 100644 --- a/TODO.md +++ b/TODO.md @@ -19,4 +19,3 @@ - [ ] 使用頻度メトリクス + Knowledge 化候補レポート → [tickets/memory-usage-metrics.md](tickets/memory-usage-metrics.md) - [ ] GC(定期再評価) → [tickets/memory-gc.md](tickets/memory-gc.md) - [ ] LLM 生成設定の manifest 露出整理 → [tickets/worker-generation-settings.md](tickets/worker-generation-settings.md) -- [ ] モデル reasoning/thinking 制御の内部抽象整理 → [tickets/model-reasoning-control.md](tickets/model-reasoning-control.md) diff --git a/docs/pod-factory.md b/docs/pod-factory.md index 2a55ef3b..a30b0231 100644 --- a/docs/pod-factory.md +++ b/docs/pod-factory.md @@ -135,6 +135,7 @@ instruction = "$user/reviewer" max_tokens = 4096 max_turns = 50 temperature = 0.3 +reasoning = "medium" # 文字列 = effort label / 整数 = thinking budget tokens。詳細は docs/reasoning.md [worker.tool_output] default_max_bytes = 16384 diff --git a/docs/reasoning.md b/docs/reasoning.md new file mode 100644 index 00000000..b87bb439 --- /dev/null +++ b/docs/reasoning.md @@ -0,0 +1,74 @@ +# Reasoning / Thinking 制御 + +manifest の `[worker]` セクションで `reasoning` を指定すると、scheme が provider 各社の wire 形式に投影する。文字列なら **effort label**、数値なら **thinking budget tokens** として扱う。insomnia 側は値の妥当性を検証せず、未知ラベルや provider が拒む値は API 応答で初めて検出される。 + +## 書き方 + +```toml +[worker] +reasoning = "medium" # effort label +``` + +```toml +[worker] +reasoning = 4096 # thinking budget tokens (i32) +``` + +未指定なら wire request に reasoning / thinking 関連フィールドは出さない。 + +## Provider ごとの受け入れ形式 + +| Provider / scheme | 受け入れる形式 | 投影先 | +|---|---|---| +| OpenAI Chat Completions (`openai_chat`) | effort label のみ | `reasoning_effort` | +| OpenAI Responses (`openai_responses`) | effort label のみ | `reasoning: { effort, summary: "auto" }` | +| Anthropic (`anthropic`) | budget tokens のみ | `thinking: { type: "enabled", budget_tokens }` | +| Gemini (`gemini`) | budget tokens のみ | `generation_config.thinking_config.thinking_budget` | + +`ModelCapability::reasoning` (`ReasoningSupport::{Effort, BudgetTokens, Both}`) と request 側の variant が一致しないときは、その scheme は wire に何も載せない(capability gating)。例: Anthropic に `reasoning = "medium"` を渡しても黙って drop される。 + +## Effort label + +`ReasoningEffort` の既知 variant は `minimal` / `low` / `medium` / `high` / `xhigh`。これら以外の文字列は `Other(String)` として provider にそのまま渡る(OpenAI 側の独自ラベルや将来追加に対応)。 + +## Budget tokens + +signed integer (`i32`) として扱う。Gemini の `-1`(dynamic budget)のような特殊値も型変換なしで通る。範囲チェックは provider に任せる。 + +## 設定例 + +OpenAI o-series: + +```toml +[model] +ref = "openai/gpt-5" + +[worker] +reasoning = "high" +``` + +Anthropic extended thinking: + +```toml +[model] +ref = "anthropic/claude-sonnet-4-6" + +[worker] +reasoning = 8192 +``` + +Gemini dynamic thinking: + +```toml +[model] +ref = "gemini/gemini-2.5-pro" + +[worker] +reasoning = -1 +``` + +## 範囲外 + +- UI プリセット(Low / Medium / High → 各 provider 値)の変換テーブル +- provider ごとの推奨 budget レンジ +- reasoning / thinking 出力 block のログ・再送・表示ポリシー diff --git a/tickets/model-reasoning-control.md b/tickets/model-reasoning-control.md deleted file mode 100644 index 455a7ae0..00000000 --- a/tickets/model-reasoning-control.md +++ /dev/null @@ -1,99 +0,0 @@ -# モデル reasoning/thinking 制御の内部抽象整理 - -## 背景 - -`llm-worker` には `RequestConfig::reasoning` と `ReasoningControl { effort, budget_tokens }` があり、各 scheme も OpenAI 系の `reasoning_effort` / `reasoning.effort`、Anthropic の `thinking.budget_tokens`、Gemini の `thinking_config.thinking_budget` へ投影する実装を持っている。 - -一方で、上位層から通常のリクエストへ `reasoning` を設定する経路はまだ整っておらず、内部型も `effort: Option<_>` と `budget_tokens: Option<_>` を同時に持てるため、「今回の指定がラベルなのか数値 budget なのか」が型で一意に表現されていない。 - -Provider ごとの正当値は変化しやすく、OpenAI 系は `low` / `medium` / `high` などの文字列 effort、Anthropic / Gemini 系は token budget 数値、Gemini には `-1` dynamic budget のような値がある。insomnia はこれらを過度に正規化せず、上位層・manifest では `String` または `i32` として受け、内部では「文字列 effort か数値 budget か」を enum で保持し、scheme が自身の wire 形式へ投影する。 - -## 要件 - -### manifest / 上位設定での表現 - -reasoning/thinking 制御は、上位層および manifest で `String` または `i32` として指定できるようにする。 - -例: - -```toml -reasoning = "medium" -``` - -```toml -reasoning = 4096 -``` - -文字列は provider-native な effort label として扱い、数値は provider-native な thinking budget token 数として扱う。間違った値や provider が受け付けない組み合わせは insomnia 側で過度に検証せず、原則として provider API のエラーに任せる。 - -現状は `WorkerManifestConfig` / `WorkerManifest` に `reasoning` フィールドが無く、`pod::apply_worker_manifest()` も `RequestConfig::new()` に `max_tokens` と `temperature` だけを移しているため、通常の Pod/manifest 起動経路では `RequestConfig::reasoning` が常に `None` のまま scheme へ届く。このチケットでは `[worker]` 由来の reasoning 指定を `RequestConfig` へ渡す経路まで含める。 - -### 内部型の enum 化 - -`ReasoningControl` は `effort` と `budget_tokens` を同時に持つ struct ではなく、指定種別が一意な enum に整理する。 - -想定形: - -```rust -pub enum ReasoningControl { - Effort(ReasoningEffort), - BudgetTokens(i32), -} -``` - -`ReasoningEffort` は既知値を variant として持ちつつ、未知の provider-native label を素通しできるようにする。 - -想定形: - -```rust -pub enum ReasoningEffort { - Minimal, - Low, - Medium, - High, - XHigh, - Other(String), -} -``` - -数値 budget は Gemini の `-1` dynamic budget 等を表現できるよう、`u32` ではなく signed integer とする。 - -### scheme 投影 - -各 scheme は `ReasoningControl` の variant と `ModelCapability::reasoning` を見て、自身が wire に載せられる形式のみ投影する。 - -- OpenAI Chat Completions: `Effort(_)` を `reasoning_effort` に投影する -- OpenAI Responses: `Effort(_)` を `reasoning: { effort, summary: "auto" }` に投影する -- Anthropic: `BudgetTokens(_)` を `thinking: { type: "enabled", budget_tokens }` に投影する -- Gemini: `BudgetTokens(_)` を `generation_config.thinking_config.thinking_budget` に投影する - -provider-native な値そのものの妥当性検証は最小限に留める。例えば OpenAI に未知 effort label を送る、Anthropic に provider が許容しない budget を送る、といったケースは provider API の応答で検出されればよい。 - -### Capability との関係 - -`ModelCapability::reasoning` は、その model/provider が受けられる reasoning 指定の大まかな形式を表す既存の `ReasoningSupport::{Effort, BudgetTokens, Both}` を維持してよい。 - -ただし capability は正当値リストではなく、scheme 投影の可否判定に留める。モデル別の厳密な effort label や budget range を insomnia 側で網羅しない。 - -## 範囲外 - -- UI 上のプリセット(Low / Medium / High 等)をどの値へ変換するかの設計 -- provider ごとの budget 推奨値テーブル -- OpenRouter / Groq / DeepSeek 等、現行 scheme で未実装の reasoning 統一 API への追加対応 -- reasoning / thinking 出力 block のログ保存・再送・表示ポリシーの変更 - -## 完了条件 - -- manifest / 上位設定で reasoning を `String` または `i32` として表現できる -- `WorkerManifestConfig` / `WorkerManifest` が reasoning 指定を保持し、cascade merge 後に `pod::apply_worker_manifest()` から `RequestConfig::reasoning` へ渡される -- `ReasoningControl` が enum 化され、effort と budget の同時指定が型上できない -- `ReasoningEffort` が `minimal` / `low` / `medium` / `high` / `xhigh` と未知文字列の素通しを扱える -- budget token 指定が signed integer として扱われ、Gemini の `-1` のような値を表現できる -- OpenAI Chat / OpenAI Responses / Anthropic / Gemini の既存 reasoning 投影が enum 型に追従している -- 既存の reasoning 無指定時は、従来通り wire request に reasoning/thinking パラメータを出さない - -## Review - -- 状態: Approve -- レビュー詳細: [./model-reasoning-control.review.md](./model-reasoning-control.review.md) -- 日付: 2026-04-27 diff --git a/tickets/model-reasoning-control.review.md b/tickets/model-reasoning-control.review.md deleted file mode 100644 index 3d80803c..00000000 --- a/tickets/model-reasoning-control.review.md +++ /dev/null @@ -1,87 +0,0 @@ -# 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` を追加(`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 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(_)`)として表現されており、両方を同時に保持する手段は存在しない。 -- `ReasoningEffort` が `minimal` / `low` / `medium` / `high` / `xhigh` と未知文字列の素通しを扱える - - `ReasoningEffort` に 5 つの既知 variant + `Other(String)` を定義 - (`crates/llm-worker/src/llm_client/capability.rs:94-102`)。 - - `From` / 手書き `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`)。 -- 既存の 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 は読みやすさのみの非ブロッキング指摘。