Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
30 changes: 15 additions & 15 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -3,7 +3,7 @@ members = ["crates/*"]
resolver = "2"

[workspace.package]
version = "0.1.107"
version = "0.1.108"
edition = "2024"
rust-version = "1.85"
license = "Apache-2.0"
Expand Down
12 changes: 11 additions & 1 deletion crates/cli-sub-agent/src/debate_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -131,6 +131,16 @@ pub(crate) async fn handle_debate(
Cognitive diversity is degraded."
);
}
// Model precedence: CLI --model > project config debate.model > global config debate.model.
// When tier is also set, build_executor applies model override after tier spec construction.
let debate_model = args.model.clone().or_else(|| {
config
.as_ref()
.and_then(|c| c.debate.as_ref())
.and_then(|d| d.model.clone())
.or_else(|| global_config.debate.model.clone())
});

// Thinking precedence: CLI > config debate.thinking > tier model_spec thinking.
let thinking = resolve_debate_thinking(
args.thinking.as_deref(),
Expand All @@ -145,7 +155,7 @@ pub(crate) async fn handle_debate(
let executor = crate::pipeline::build_and_validate_executor(
&tool,
tier_model_spec.as_deref(),
args.model.as_deref(),
debate_model.as_deref(),
thinking.as_deref(),
crate::pipeline::ConfigRefs {
project: config.as_ref(),
Expand Down
16 changes: 13 additions & 3 deletions crates/cli-sub-agent/src/review_cmd.rs
Original file line number Diff line number Diff line change
Expand Up @@ -165,6 +165,16 @@ pub(crate) async fn handle_review(args: ReviewArgs, current_depth: u32) -> Resul
args.force_override_user_config,
)?;

// Resolve model: CLI --model > project config review.model > global config review.model.
// When tier is also set, build_executor applies model override after tier spec construction.
let review_model = args.model.clone().or_else(|| {
config
.as_ref()
.and_then(|c| c.review.as_ref())
.and_then(|r| r.model.clone())
.or_else(|| global_config.review.model.clone())
});

// Resolve thinking: CLI > config review.thinking > tier model_spec thinking.
// Tier thinking is embedded in model_spec and applied via build_and_validate_executor.
let review_thinking = resolve_review_thinking(
Expand All @@ -187,7 +197,7 @@ pub(crate) async fn handle_review(args: ReviewArgs, current_depth: u32) -> Resul
tool,
prompt.clone(),
args.session.clone(),
args.model.clone(),
review_model.clone(),
tier_model_spec.clone(),
review_thinking.clone(),
format!(
Expand Down Expand Up @@ -251,7 +261,7 @@ pub(crate) async fn handle_review(args: ReviewArgs, current_depth: u32) -> Resul
tool,
fix_prompt,
Some(session_id.clone()),
args.model.clone(),
review_model.clone(),
tier_model_spec.clone(),
review_thinking.clone(),
format!("fix round {round}/{max_rounds}"),
Expand Down Expand Up @@ -349,7 +359,7 @@ pub(crate) async fn handle_review(args: ReviewArgs, current_depth: u32) -> Resul
for (reviewer_index, reviewer_tool) in reviewer_tools.into_iter().enumerate() {
let reviewer_prompt =
build_multi_reviewer_instruction(&prompt, reviewer_index + 1, reviewer_tool);
let reviewer_model = args.model.clone();
let reviewer_model = review_model.clone();
let reviewer_project_root = project_root.clone();
let reviewer_config = config.clone();
let reviewer_global = global_config.clone();
Expand Down
7 changes: 5 additions & 2 deletions crates/cli-sub-agent/src/run_helpers.rs
Original file line number Diff line number Diff line change
Expand Up @@ -148,9 +148,12 @@ pub(crate) fn build_executor(
Executor::from_tool_name(tool, final_model, budget)
};

// When model_spec is present, the thinking budget comes from the spec.
// An explicit `thinking` argument must override it (CLI > tier spec).
// When model_spec is present, the model and thinking come from the spec.
// Explicit arguments must override them (CLI/config > tier spec).
if model_spec.is_some() {
if let Some(explicit_model) = model {
executor.override_model(explicit_model.to_string());
}
if let Some(explicit_thinking) = thinking {
let budget = ThinkingBudget::parse(explicit_thinking)?;
executor.override_thinking_budget(budget);
Expand Down
14 changes: 9 additions & 5 deletions crates/cli-sub-agent/src/run_helpers_tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -456,21 +456,25 @@ fn build_executor_model_spec_overrides_both() {
execution: Default::default(),
};

// When explicit thinking is provided alongside model_spec, it overrides
// the spec's embedded thinking budget (CLI > tier spec).
// When explicit model and thinking are provided alongside model_spec,
// they override the spec's embedded values (CLI/config > tier spec).
let exec = build_executor(
&ToolName::Codex,
Some("codex/openai/gpt-5.3-codex/xhigh"),
Some("ignored-model"),
Some("explicit-model"),
Some("high"),
Some(&config),
true,
)
.unwrap();
let debug = format!("{:?}", exec);
assert!(
debug.contains("gpt-5.3-codex"),
"model_spec model missing: {debug}"
debug.contains("explicit-model"),
"explicit model should override model_spec model: {debug}"
);
assert!(
!debug.contains("gpt-5.3-codex"),
"model_spec model should be overridden by explicit model: {debug}"
);
assert!(
debug.contains("High"),
Expand Down
31 changes: 31 additions & 0 deletions crates/csa-config/src/global.rs
Original file line number Diff line number Diff line change
Expand Up @@ -112,6 +112,12 @@ pub struct ReviewConfig {
/// over `tool` when both are set. The tier must exist in `[tiers]`.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub tier: Option<String>,
/// Default model for `csa review`. Overrides the tool's own default model
/// selection (e.g., gemini-cli model steering) without requiring a tier.
///
/// Priority: CLI `--model` > this field > tier model_spec > tool default.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
/// Default thinking budget for `csa review` (`low`, `medium`, `high`, `xhigh`).
///
/// `csa review --thinking <LEVEL>` (when supported) overrides this.
Expand Down Expand Up @@ -151,6 +157,7 @@ impl Default for ReviewConfig {
tool: default_review_tool(),
gate_mode: GateMode::default(),
tier: None,
model: None,
thinking: None,
gate_command: None,
gate_timeout_secs: default_gate_timeout_secs(),
Expand All @@ -164,6 +171,7 @@ impl ReviewConfig {
self.tool == default_review_tool()
&& self.gate_mode == GateMode::Monitor
&& self.tier.is_none()
&& self.model.is_none()
&& self.thinking.is_none()
&& self.gate_command.is_none()
&& self.gate_timeout_secs == default_gate_timeout_secs()
Expand Down Expand Up @@ -191,6 +199,12 @@ pub struct DebateConfig {
/// `csa debate --timeout <N>` overrides this per invocation.
#[serde(default = "default_debate_timeout_seconds")]
pub timeout_seconds: u64,
/// Default model for `csa debate`. Overrides the tool's own default model
/// selection (e.g., gemini-cli model steering) without requiring a tier.
///
/// Priority: CLI `--model` > this field > tier model_spec > tool default.
#[serde(default, skip_serializing_if = "Option::is_none")]
pub model: Option<String>,
/// Default thinking budget for `csa debate` (`low`, `medium`, `high`, `xhigh`).
///
/// `csa debate --thinking <LEVEL>` overrides this per invocation.
Expand Down Expand Up @@ -230,13 +244,26 @@ impl Default for DebateConfig {
Self {
tool: default_debate_tool(),
timeout_seconds: default_debate_timeout_seconds(),
model: None,
thinking: None,
same_model_fallback: true,
tier: None,
}
}
}

impl DebateConfig {
/// Returns true when all fields match defaults (per rust/016 serde-default rule).
pub fn is_default(&self) -> bool {
self.tool == default_debate_tool()
&& self.timeout_seconds == default_debate_timeout_seconds()
&& self.model.is_none()
&& self.thinking.is_none()
&& self.same_model_fallback
&& self.tier.is_none()
}
}

/// Configuration for fallback behavior when external services are unavailable.
#[derive(Debug, Clone, Serialize, Deserialize)]
pub struct FallbackConfig {
Expand Down Expand Up @@ -708,6 +735,10 @@ fn resolve_auto_tool(section: &str, parent_tool: Option<&str>) -> Result<String>
#[path = "global_tests.rs"]
mod tests;

#[cfg(test)]
#[path = "global_tests_heterogeneous.rs"]
mod tests_heterogeneous;

#[cfg(test)]
#[path = "global_tests_priority.rs"]
mod tests_priority;
Loading
Loading