mirror of
https://github.com/fabro-sh/fabro.git
synced 2026-10-06 02:48:25 +00:00
feat(config,types): add [run.model.controls] for run-level control defaults
Adds the run-level default controls block from the settings-driven LLM catalog plan: [run.model.controls] reasoning_effort = "high" speed = "fast" - RunModelLayer.controls (fabro-config) — sparse layer that field- merges and combines through the existing settings stack. - RunModelControlsLayer (fabro-config) — TOML-facing struct with deny_unknown_fields and OptionsMetadata. - RunModelControls (fabro-types) — resolved namespace value attached to RunModelSettings. - resolve_run wires layer→namespace. Values stay as plain strings here; concrete enum validation against ReasoningEffort and Speed happens at request-time when the resolved catalog is available. Per the plan, run-level controls are overridable by node attributes and style-applied attributes; that ordering is enforced at workflow control-resolution sites in a later step. Two new tests assert default-empty behavior and round-trip parsing.
This commit is contained in:
parent
734756d86d
commit
8abe8ca515
10 changed files with 100 additions and 14 deletions
|
|
@ -39,6 +39,7 @@ fn model_from_args(model: Option<&str>, provider: Option<&str>) -> Option<RunMod
|
|||
provider: provider.map(InterpString::parse),
|
||||
name: model.map(InterpString::parse),
|
||||
fallbacks: Vec::new(),
|
||||
controls: None,
|
||||
})
|
||||
}
|
||||
|
||||
|
|
@ -229,4 +230,4 @@ mod tests {
|
|||
.is_none()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -497,6 +497,7 @@ command = ["demo-mcp"]
|
|||
provider: Some(InterpString::parse("openai")),
|
||||
name: Some(InterpString::parse("gpt-5")),
|
||||
fallbacks: Vec::new(),
|
||||
controls: None,
|
||||
}),
|
||||
execution: Some(RunExecutionLayer {
|
||||
mode: Some(RunMode::DryRun),
|
||||
|
|
@ -541,4 +542,4 @@ command = ["demo-mcp"]
|
|||
assert_eq!(settings.run.execution.approval, ApprovalMode::Auto);
|
||||
assert!(!settings.run.execution.retros);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -30,8 +30,9 @@ pub use run::{
|
|||
GitAuthorLayer, HookAgentMarker, HookEntry, HookTlsMode, InterviewProviderLayer,
|
||||
InterviewsLayer, LocalSandboxLayer, McpEntryLayer, ModelRefOrSplice, NotificationProviderLayer,
|
||||
NotificationRouteLayer, PrepareStep, RunAgentLayer, RunArtifactsLayer, RunCheckpointLayer,
|
||||
RunExecutionLayer, RunGitLayer, RunGoalLayer, RunLayer, RunModelLayer, RunPrepareLayer,
|
||||
RunPullRequestLayer, RunSandboxLayer, RunScmLayer, ScmGitHubLayer, StringOrSplice,
|
||||
RunExecutionLayer, RunGitLayer, RunGoalLayer, RunLayer, RunModelControlsLayer, RunModelLayer,
|
||||
RunPrepareLayer, RunPullRequestLayer, RunSandboxLayer, RunScmLayer, ScmGitHubLayer,
|
||||
StringOrSplice,
|
||||
};
|
||||
pub use server::{
|
||||
DiscordIntegrationLayer, GithubIntegrationLayer, IntegrationWebhooksLayer,
|
||||
|
|
|
|||
|
|
@ -107,6 +107,37 @@ pub struct RunModelLayer {
|
|||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
#[option(default = "[]", value_type = "array<string>")]
|
||||
pub fallbacks: Vec<ModelRefOrSplice>,
|
||||
/// Run-level default values for typed model controls. Node attributes
|
||||
/// and style-applied attributes still win over these defaults.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
pub controls: Option<RunModelControlsLayer>,
|
||||
}
|
||||
|
||||
/// `[run.model.controls]` — run-level default control values.
|
||||
///
|
||||
/// Stored as plain strings here; concrete enum validation
|
||||
/// (`ReasoningEffort`, `Speed`) happens at request-time when the resolved
|
||||
/// catalog is available.
|
||||
#[derive(
|
||||
Debug,
|
||||
Clone,
|
||||
Default,
|
||||
PartialEq,
|
||||
Serialize,
|
||||
Deserialize,
|
||||
fabro_macros::Combine,
|
||||
fabro_macros::OptionsMetadata,
|
||||
)]
|
||||
#[serde(deny_unknown_fields)]
|
||||
pub struct RunModelControlsLayer {
|
||||
/// Default reasoning-effort value for nodes that don't override it.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[option(value_type = "string")]
|
||||
pub reasoning_effort: Option<String>,
|
||||
/// Default speed value for nodes that don't override it.
|
||||
#[serde(default, skip_serializing_if = "Option::is_none")]
|
||||
#[option(value_type = "string")]
|
||||
pub speed: Option<String>,
|
||||
}
|
||||
|
||||
/// A single `fallbacks` entry: either a parsed `ModelRef` or the splice marker.
|
||||
|
|
@ -571,4 +602,4 @@ pub struct RunPullRequestLayer {
|
|||
pub struct RunArtifactsLayer {
|
||||
#[serde(default, skip_serializing_if = "Vec::is_empty")]
|
||||
pub include: Vec<String>,
|
||||
}
|
||||
}
|
||||
|
|
@ -46,8 +46,9 @@ pub use layers::{
|
|||
ModelRefOrSplice, ModelSettings, NotificationProviderLayer, NotificationRouteLayer,
|
||||
ObjectStoreLocalLayer, ObjectStoreS3Layer, PrepareStep, ProjectLayer, ProviderSettings,
|
||||
ReplaceMap, RunAgentLayer, RunArtifactsLayer, RunCheckpointLayer, RunExecutionLayer,
|
||||
RunGitLayer, RunGoalLayer, RunLayer, RunModelLayer, RunPrepareLayer, RunPullRequestLayer,
|
||||
RunSandboxLayer, RunScmLayer, ScmGitHubLayer, ServerApiLayer, ServerArtifactsLayer,
|
||||
RunGitLayer, RunGoalLayer, RunLayer, RunModelControlsLayer, RunModelLayer, RunPrepareLayer,
|
||||
RunPullRequestLayer, RunSandboxLayer, RunScmLayer, ScmGitHubLayer, ServerApiLayer,
|
||||
ServerArtifactsLayer,
|
||||
ServerAuthGithubLayer, ServerAuthLayer, ServerIntegrationsLayer, ServerIpAllowlistLayer,
|
||||
ServerIpAllowlistOverrideLayer, ServerLayer, ServerListenLayer, ServerLoggingLayer,
|
||||
ServerSchedulerLayer, ServerSlateDbLayer, ServerStorageLayer, ServerWebLayer,
|
||||
|
|
|
|||
|
|
@ -72,6 +72,14 @@ fn resolve_model(model: Option<&RunModelLayer>) -> RunModelSettings {
|
|||
ModelRefOrSplice::Splice => None,
|
||||
})
|
||||
.collect(),
|
||||
controls: model
|
||||
.controls
|
||||
.as_ref()
|
||||
.map(|c| fabro_types::settings::RunModelControls {
|
||||
reasoning_effort: c.reasoning_effort.clone(),
|
||||
speed: c.speed.clone(),
|
||||
})
|
||||
.unwrap_or_default(),
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -497,4 +505,4 @@ fn resolve_artifacts(artifacts: Option<&RunArtifactsLayer>) -> ArtifactsSettings
|
|||
fn size_to_gb_i32(bytes: u64) -> i32 {
|
||||
let gb = bytes / 1_000_000_000;
|
||||
i32::try_from(gb).unwrap_or(i32::MAX)
|
||||
}
|
||||
}
|
||||
|
|
@ -3,6 +3,37 @@ use fabro_types::settings::run::{ApprovalMode, RunGoal, RunMode, WorktreeMode};
|
|||
|
||||
use crate::{SettingsLayer, WorkflowSettingsBuilder};
|
||||
|
||||
#[test]
|
||||
fn run_model_controls_round_trip_through_resolve() {
|
||||
let settings = WorkflowSettingsBuilder::from_toml(
|
||||
r#"
|
||||
_version = 1
|
||||
|
||||
[run.model.controls]
|
||||
reasoning_effort = "high"
|
||||
speed = "fast"
|
||||
"#,
|
||||
)
|
||||
.expect("[run.model.controls] should resolve")
|
||||
.run;
|
||||
|
||||
assert_eq!(
|
||||
settings.model.controls.reasoning_effort.as_deref(),
|
||||
Some("high")
|
||||
);
|
||||
assert_eq!(settings.model.controls.speed.as_deref(), Some("fast"));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn run_model_controls_default_to_none() {
|
||||
let settings = WorkflowSettingsBuilder::from_layer(&SettingsLayer::default())
|
||||
.expect("empty settings should resolve")
|
||||
.run;
|
||||
|
||||
assert!(settings.model.controls.reasoning_effort.is_none());
|
||||
assert!(settings.model.controls.speed.is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn resolves_run_defaults_from_empty_settings() {
|
||||
let settings = WorkflowSettingsBuilder::from_layer(&SettingsLayer::default())
|
||||
|
|
@ -79,4 +110,4 @@ name = "sonnet"
|
|||
Some(InterpString::parse("anthropic"))
|
||||
);
|
||||
assert_eq!(settings.model.name, Some(InterpString::parse("sonnet")));
|
||||
}
|
||||
}
|
||||
|
|
@ -315,6 +315,7 @@ fn manifest_args_overrides(args: Option<&types::ManifestArgs>) -> ManifestSettin
|
|||
provider: args.provider.as_deref().map(InterpString::parse),
|
||||
name: args.model.as_deref().map(InterpString::parse),
|
||||
fallbacks: Vec::new(),
|
||||
controls: None,
|
||||
});
|
||||
let local_worktree = args
|
||||
.worktree_mode
|
||||
|
|
@ -1980,4 +1981,4 @@ digraph Demo {
|
|||
);
|
||||
assert!(response_mock.calls_async().await >= 1);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
@ -40,8 +40,8 @@ pub use run::{
|
|||
GitAuthorSettings, HookDefinition, HookType, InterviewProviderSettings, McpServerSettings,
|
||||
McpTransport, NotificationProviderSettings, NotificationRouteSettings, PullRequestSettings,
|
||||
RunAgentSettings, RunCheckpointSettings, RunExecutionSettings, RunGitSettings, RunGoal,
|
||||
RunInterviewsSettings, RunModelSettings, RunNamespace, RunPrepareSettings, RunSandboxSettings,
|
||||
RunScmSettings, ScmGitHubSettings, TlsMode,
|
||||
RunInterviewsSettings, RunModelControls, RunModelSettings, RunNamespace, RunPrepareSettings,
|
||||
RunSandboxSettings, RunScmSettings, ScmGitHubSettings, TlsMode,
|
||||
};
|
||||
pub use server::{
|
||||
DiscordIntegrationSettings, GithubIntegrationSettings, IntegrationWebhooksSettings,
|
||||
|
|
@ -52,4 +52,4 @@ pub use server::{
|
|||
ServerStorageSettings, ServerWebSettings, SlackIntegrationSettings, TeamsIntegrationSettings,
|
||||
};
|
||||
pub use size::{ParseSizeError, Size};
|
||||
pub use workflow::WorkflowNamespace;
|
||||
pub use workflow::WorkflowNamespace;
|
||||
|
|
@ -50,6 +50,17 @@ pub struct RunModelSettings {
|
|||
pub provider: Option<InterpString>,
|
||||
pub name: Option<InterpString>,
|
||||
pub fallbacks: Vec<ModelRef>,
|
||||
/// Run-level default values for typed model controls
|
||||
/// (`reasoning_effort`, `speed`). Node and style attributes still win
|
||||
/// over these defaults.
|
||||
#[serde(default)]
|
||||
pub controls: RunModelControls,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
pub struct RunModelControls {
|
||||
pub reasoning_effort: Option<String>,
|
||||
pub speed: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Default, PartialEq, Serialize, Deserialize)]
|
||||
|
|
@ -551,4 +562,4 @@ pub enum MergeStrategy {
|
|||
Merge,
|
||||
Squash,
|
||||
Rebase,
|
||||
}
|
||||
}
|
||||
Loading…
Add table
Reference in a new issue