model-reasoning-control実装
This commit is contained in:
@@ -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.
|
||||
|
||||
@@ -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!(
|
||||
|
||||
@@ -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]` セクション。
|
||||
///
|
||||
|
||||
Reference in New Issue
Block a user