[codex] Honor role-defined spawn service tiers (#22169)

## Why
Custom agent roles are ordinary config layers, so a role file can
already express `service_tier` just like other config values. The
spawned-agent tier path needs to preserve that effective role config and
follow the same precedence pattern as model/reasoning.

## What changed
- Apply an explicit spawn-time `service_tier` onto the child config
before role application, so a role config layer can override it just
like role-defined model/reasoning settings do.
- Validate the final effective child tier after the final child model is
known, while still falling back to the parent tier when no child tier
survives.
- Add focused integration coverage for both v1 and v2 proving role TOML
loads a service tier, spawned children keep that role-configured tier,
and a role tier wins over a conflicting spawn-time tier.

## Validation
- `just fmt`
- `git diff --check`
- Local Rust tests not run, per repo guidance; CI should exercise the
new coverage.
This commit is contained in:
Ahmed Ibrahim authored and GitHub committed 2026-05-19 22:40:41 +00:00
1 parent c2141c7ce0
commit c53da029bc
6 files changed
+275 -27

No files matched your search

+86
View File
@@ -6,6 +6,7 @@ use crate::skills_load_input_from_config;
use codex_config::ConfigLayerStackOrdering;
use codex_core_plugins::PluginsManager;
use codex_protocol::config_types::ReasoningSummary;
use codex_protocol::config_types::ServiceTier;
use codex_protocol::config_types::Verbosity;
use codex_protocol::openai_models::ReasoningEffort;
use codex_utils_absolute_path::test_support::PathExt;
@@ -214,6 +215,66 @@ async fn apply_role_preserves_unspecified_keys() {
);
}
#[tokio::test]
async fn apply_role_reports_explicit_service_tier() {
let (home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
let role_path = write_role_config(
&home,
"tiered-role.toml",
r#"developer_instructions = "Stay focused"
service_tier = "priority"
"#,
)
.await;
config.agent_roles.insert(
"custom".to_string(),
AgentRoleConfig {
description: None,
config_file: Some(role_path),
nickname_candidates: None,
},
);
apply_role_to_config(&mut config, Some("custom"))
.await
.expect("custom role should apply");
assert_eq!(
config.service_tier,
Some(ServiceTier::Fast.request_value().to_string())
);
}
#[tokio::test]
async fn apply_role_preserves_existing_service_tier_without_override() {
let (home, mut config) = test_config_with_cli_overrides(Vec::new()).await;
config.service_tier = Some(ServiceTier::Fast.request_value().to_string());
let role_path = write_role_config(
&home,
"default-tier-role.toml",
r#"developer_instructions = "Stay focused"
"#,
)
.await;
config.agent_roles.insert(
"custom".to_string(),
AgentRoleConfig {
description: None,
config_file: Some(role_path),
nickname_candidates: None,
},
);
apply_role_to_config(&mut config, Some("custom"))
.await
.expect("custom role should apply");
assert_eq!(
config.service_tier,
Some(ServiceTier::Fast.request_value().to_string())
);
}
#[tokio::test]
async fn apply_role_preserves_active_profile_and_model_provider() {
let home = TempDir::new().expect("create temp dir");
@@ -766,6 +827,31 @@ fn spawn_tool_spec_marks_role_locked_reasoning_effort_only() {
));
}
#[test]
fn spawn_tool_spec_marks_role_locked_service_tier() {
let tempdir = TempDir::new().expect("create temp dir");
let role_path = tempdir.path().join("tiered.toml");
fs::write(
&role_path,
"developer_instructions = \"Stay fast\"\nservice_tier = \"priority\"\n",
)
.expect("write role config");
let user_defined_roles = BTreeMap::from([(
"tiered".to_string(),
AgentRoleConfig {
description: Some("Stay fast.".to_string()),
config_file: Some(role_path),
nickname_candidates: None,
},
)]);
let spec = spawn_tool_spec::build(&user_defined_roles);
assert!(spec.contains(
"Stay fast.\n- This role's service tier is set to `priority`. If it is supported by the resolved model, it takes precedence over a valid spawn request service tier."
));
}
#[test]
fn built_in_config_file_contents_resolves_explorer_only() {
assert_eq!(