model-reasoning-control実装

This commit is contained in:
2026-04-27 22:25:27 +09:00
parent c75efb50b9
commit 0435ec5cbd
9 changed files with 353 additions and 106 deletions
+64 -19
View File
@@ -13,7 +13,7 @@ use std::path::{Path, PathBuf};
use serde::{Deserialize, Serialize};
use crate::defaults;
use crate::model::{AuthRef, ModelManifest};
use crate::model::{AuthRef, ModelManifest, ReasoningControl};
use crate::{
CompactionConfig, MemoryConfig, PodManifest, PodMeta, ScopeConfig, ToolOutputLimits,
WorkerManifest,
@@ -65,6 +65,8 @@ pub struct WorkerManifestConfig {
#[serde(default)]
pub temperature: Option<f32>,
#[serde(default)]
pub reasoning: Option<ReasoningControl>,
#[serde(default)]
pub tool_output: ToolOutputLimitsPartial,
}
@@ -103,10 +105,7 @@ pub enum ResolveError {
#[error("missing required field: {0}")]
MissingField(&'static str),
#[error("path must be absolute ({field}): {}", .path.display())]
RelativePath {
field: &'static str,
path: PathBuf,
},
RelativePath { field: &'static str, path: PathBuf },
}
impl PodManifestConfig {
@@ -227,6 +226,7 @@ impl WorkerManifestConfig {
max_tokens: upper.max_tokens.or(self.max_tokens),
max_turns: upper.max_turns.or(self.max_turns),
temperature: upper.temperature.or(self.temperature),
reasoning: upper.reasoning.or(self.reasoning),
tool_output: self.tool_output.merge(upper.tool_output),
}
}
@@ -323,10 +323,7 @@ impl TryFrom<PodManifestConfig> for PodManifest {
type Error = ResolveError;
fn try_from(cfg: PodManifestConfig) -> Result<Self, Self::Error> {
let name = cfg
.pod
.name
.ok_or(ResolveError::MissingField("pod.name"))?;
let name = cfg.pod.name.ok_or(ResolveError::MissingField("pod.name"))?;
let prompt_pack = cfg.pod.prompt_pack;
if let Some(ref p) = prompt_pack {
ensure_absolute("pod.prompt_pack", p)?;
@@ -342,6 +339,7 @@ impl TryFrom<PodManifestConfig> for PodManifest {
max_tokens: cfg.worker.max_tokens,
max_turns: cfg.worker.max_turns,
temperature: cfg.worker.temperature,
reasoning: cfg.worker.reasoning,
tool_output: ToolOutputLimits {
default_max_bytes: cfg
.worker
@@ -372,9 +370,7 @@ impl TryFrom<PodManifestConfig> for PodManifest {
prune_protected_turns: c
.prune_protected_turns
.unwrap_or(defaults::PRUNE_PROTECTED_TURNS),
prune_min_savings: c
.prune_min_savings
.unwrap_or(defaults::PRUNE_MIN_SAVINGS),
prune_min_savings: c.prune_min_savings.unwrap_or(defaults::PRUNE_MIN_SAVINGS),
compact_threshold: c.compact_threshold,
compact_request_threshold: c.compact_request_threshold,
compact_retained_tokens: c
@@ -406,7 +402,7 @@ impl TryFrom<PodManifestConfig> for PodManifest {
mod tests {
use super::*;
use crate::model::SchemeKind;
use crate::{Permission, ScopeRule};
use crate::{Permission, ReasoningEffort, ScopeRule};
fn abs(path: &str) -> PathBuf {
PathBuf::from(format!("/tmp/insomnia-test{path}"))
@@ -565,6 +561,31 @@ mod tests {
assert_eq!(merged.model.model_id.as_deref(), Some("lower-model"));
}
#[test]
fn merge_worker_reasoning_upper_wins() {
let lower = PodManifestConfig {
worker: WorkerManifestConfig {
reasoning: Some(ReasoningControl::Effort(ReasoningEffort::Low)),
..Default::default()
},
..Default::default()
};
let upper = PodManifestConfig {
worker: WorkerManifestConfig {
reasoning: Some(ReasoningControl::BudgetTokens(4096)),
..Default::default()
},
..Default::default()
};
let merged = lower.merge(upper);
assert_eq!(
merged.worker.reasoning,
Some(ReasoningControl::BudgetTokens(4096))
);
}
#[test]
fn merge_scope_accumulates_allow_and_deny() {
let lower = PodManifestConfig {
@@ -614,12 +635,9 @@ mod tests {
worker: WorkerManifestConfig {
tool_output: ToolOutputLimitsPartial {
default_max_bytes: None,
per_tool: [
("Read".to_string(), 2048),
("Grep".to_string(), 512),
]
.into_iter()
.collect(),
per_tool: [("Read".to_string(), 2048), ("Grep".to_string(), 512)]
.into_iter()
.collect(),
},
..Default::default()
},
@@ -687,6 +705,33 @@ unknown_future_field = "tolerated"
assert_eq!(cfg.worker.max_tokens, Some(1000));
}
#[test]
fn from_toml_accepts_worker_reasoning_string_or_integer() {
let effort = PodManifestConfig::from_toml(
r#"
[worker]
reasoning = "xhigh"
"#,
)
.unwrap();
assert_eq!(
effort.worker.reasoning,
Some(ReasoningControl::Effort(ReasoningEffort::XHigh))
);
let budget = PodManifestConfig::from_toml(
r#"
[worker]
reasoning = -1
"#,
)
.unwrap();
assert_eq!(
budget.worker.reasoning,
Some(ReasoningControl::BudgetTokens(-1))
);
}
#[test]
fn from_toml_partial_layer_succeeds() {
// A project-layer manifest with only scope set must parse fine.
+22 -5
View File
@@ -6,12 +6,14 @@ pub mod paths;
mod scope;
pub use cascade::{LayerLoadError, find_project_manifest_from, load_layer};
pub use paths::user_manifest_path;
pub use config::{
CompactionConfigPartial, PodManifestConfig, PodMetaConfig, ResolveError,
ToolOutputLimitsPartial, WorkerManifestConfig,
};
pub use model::{AuthRef, ModelCapability, ModelManifest, SchemeKind};
pub use model::{
AuthRef, ModelCapability, ModelManifest, ReasoningControl, ReasoningEffort, SchemeKind,
};
pub use paths::user_manifest_path;
pub use protocol::{Permission, ScopeRule};
pub use scope::{Scope, ScopeError};
@@ -99,6 +101,8 @@ pub struct WorkerManifest {
pub max_turns: Option<NonZeroU32>,
#[serde(default)]
pub temperature: Option<f32>,
#[serde(default)]
pub reasoning: Option<ReasoningControl>,
/// Byte-size caps applied to tool `content` before it reaches the
/// conversation history. The section is optional in TOML — when
/// omitted, `ToolOutputLimits::default()` (16KB default cap, no
@@ -312,6 +316,7 @@ auth = { kind = "api_key", file = "/abs/keys/anthropic" }
instruction = "$user/reviewer"
max_tokens = 4096
temperature = 0.3
reasoning = "medium"
[[scope.allow]]
target = "/abs/project"
@@ -336,6 +341,10 @@ permission = "write"
assert_eq!(manifest.worker.instruction, "$user/reviewer");
assert_eq!(manifest.worker.max_tokens, Some(4096));
assert_eq!(manifest.worker.temperature, Some(0.3));
assert_eq!(
manifest.worker.reasoning,
Some(ReasoningControl::Effort(ReasoningEffort::Medium))
);
let allow = &manifest.scope.allow;
assert_eq!(allow.len(), 2);
assert_eq!(allow[0].permission, Permission::Write);
@@ -368,6 +377,16 @@ model_id = "claude-sonnet-4-20250514"
assert_eq!(manifest.worker.max_turns.unwrap().get(), 50);
}
#[test]
fn parse_reasoning_budget() {
let toml = MINIMAL_REQUIRED.replace("[worker]\n", "[worker]\nreasoning = -1\n");
let manifest = PodManifest::from_toml(&toml).unwrap();
assert_eq!(
manifest.worker.reasoning,
Some(ReasoningControl::BudgetTokens(-1))
);
}
#[test]
fn omitted_max_turns_is_none() {
let manifest = PodManifest::from_toml(MINIMAL_REQUIRED).unwrap();
@@ -458,9 +477,7 @@ model_id = "claude-sonnet-4-20250514"
#[test]
fn memory_section_with_explicit_root() {
let toml = format!(
"{MINIMAL_REQUIRED}\n[memory]\nworkspace_root = \"/some/where\"\n"
);
let toml = format!("{MINIMAL_REQUIRED}\n[memory]\nworkspace_root = \"/some/where\"\n");
let manifest = PodManifest::from_toml(&toml).unwrap();
let mem = manifest.memory.unwrap();
assert_eq!(
+1 -1
View File
@@ -16,7 +16,7 @@ use serde::{Deserialize, Serialize};
// `ModelCapability` は `llm-worker` 側に定義される runtime 構造だが、
// マニフェストで任意に override できるよう型だけ再エクスポートする。
pub use llm_worker::llm_client::capability::ModelCapability;
pub use llm_worker::llm_client::capability::{ModelCapability, ReasoningControl, ReasoningEffort};
/// Pod マニフェストの `[model]` セクション。
///